@maxiaochao/pi-toolkit 0.9.6 → 0.10.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -2
  3. package/package.json +5 -5
  4. package/packages/herdr-subagents/CHANGELOG.md +50 -0
  5. package/packages/herdr-subagents/README.md +283 -0
  6. package/packages/herdr-subagents/TESTING.md +25 -0
  7. package/packages/herdr-subagents/agents/explorer.md +8 -0
  8. package/packages/herdr-subagents/agents/reviewer.md +8 -0
  9. package/packages/{tiny-subagent → herdr-subagents}/agents/worker.md +1 -0
  10. package/packages/herdr-subagents/docs/demo.webm +0 -0
  11. package/packages/herdr-subagents/docs/screenshot.png +0 -0
  12. package/packages/herdr-subagents/extensions/herdr-subagents.ts +258 -0
  13. package/packages/herdr-subagents/extensions/subagent-report.ts +65 -0
  14. package/packages/{tiny-subagent → herdr-subagents}/package.json +7 -6
  15. package/packages/herdr-subagents/src/batch-runner.ts +219 -0
  16. package/packages/herdr-subagents/src/blocking.ts +175 -0
  17. package/packages/herdr-subagents/src/config.ts +129 -0
  18. package/packages/herdr-subagents/src/continue-run.ts +87 -0
  19. package/packages/herdr-subagents/src/dispatcher.ts +73 -0
  20. package/packages/herdr-subagents/src/errors.ts +3 -0
  21. package/packages/herdr-subagents/src/fleet-controller.ts +82 -0
  22. package/packages/herdr-subagents/src/fleet.ts +56 -0
  23. package/packages/herdr-subagents/src/herdr.ts +224 -0
  24. package/packages/herdr-subagents/src/history.ts +210 -0
  25. package/packages/herdr-subagents/src/live-agent.ts +87 -0
  26. package/packages/herdr-subagents/src/monitor.ts +18 -0
  27. package/packages/herdr-subagents/src/ownership.ts +17 -0
  28. package/packages/herdr-subagents/src/parameters.ts +21 -0
  29. package/packages/{tiny-subagent → herdr-subagents}/src/personas.ts +13 -2
  30. package/packages/herdr-subagents/src/protocol.ts +23 -0
  31. package/packages/herdr-subagents/src/queued-runs.ts +70 -0
  32. package/packages/herdr-subagents/src/reconcile.ts +167 -0
  33. package/packages/herdr-subagents/src/records.ts +213 -0
  34. package/packages/herdr-subagents/src/result-path.ts +7 -0
  35. package/packages/herdr-subagents/src/run-locator.ts +19 -0
  36. package/packages/herdr-subagents/src/run-pane-allocator.ts +78 -0
  37. package/packages/herdr-subagents/src/run-status.ts +23 -0
  38. package/packages/herdr-subagents/src/runner.ts +250 -0
  39. package/packages/herdr-subagents/src/state.ts +11 -0
  40. package/packages/herdr-subagents/src/tab-cleanup.ts +68 -0
  41. package/packages/herdr-subagents/src/tool.ts +217 -0
  42. package/packages/herdr-subagents/src/validation.ts +38 -0
  43. package/packages/herdr-subagents/tests/extension-contract.ts +225 -0
  44. package/packages/herdr-subagents/tests/run-tests.mjs +895 -0
  45. package/packages/todo/extensions/todo.ts +1 -1
  46. package/packages/tiny-subagent/CHANGELOG.md +0 -12
  47. package/packages/tiny-subagent/README.md +0 -128
  48. package/packages/tiny-subagent/TESTING.md +0 -20
  49. package/packages/tiny-subagent/docs/demo.gif +0 -0
  50. package/packages/tiny-subagent/docs/screenshot.png +0 -0
  51. package/packages/tiny-subagent/extensions/tiny-subagent.ts +0 -228
  52. package/packages/tiny-subagent/src/runner.ts +0 -319
  53. package/packages/tiny-subagent/tests/run-tests.mjs +0 -111
  54. package/skills/calldiff/LICENSE +0 -21
  55. package/skills/calldiff/NOTICE +0 -19
  56. package/skills/calldiff/SKILL.md +0 -121
@@ -1,319 +0,0 @@
1
- import { createWriteStream, existsSync, realpathSync } from "node:fs";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { basename, join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { spawn } from "node:child_process";
6
-
7
- export const MAX_INLINE_BYTES = 16 * 1024;
8
- const CHILD_ENV = "PI_SUBAGENT_CHILD";
9
- const BINARY_ENV = "PI_TINY_SUBAGENT_PI_BINARY";
10
-
11
- export interface TinySubagentOptions {
12
- model?: string;
13
- thinking?: string;
14
- tools?: string[];
15
- systemPrompt?: string;
16
- cwd?: string;
17
- signal?: AbortSignal;
18
- onUpdate?: (result: TinySubagentResult) => void;
19
- }
20
-
21
- export interface TinySubagentArtifacts {
22
- directory: string;
23
- stdout: string;
24
- stderr: string;
25
- finalOutput?: string;
26
- cleanup: "remove directory when no longer needed";
27
- }
28
-
29
- export interface TinySubagentContent {
30
- type?: string;
31
- text?: string;
32
- name?: string;
33
- arguments?: Record<string, unknown>;
34
- }
35
-
36
- export interface TinySubagentMessage {
37
- role?: string;
38
- content?: string | TinySubagentContent[];
39
- toolCallId?: string;
40
- toolName?: string;
41
- isError?: boolean;
42
- stopReason?: string;
43
- errorMessage?: string;
44
- model?: string;
45
- }
46
-
47
- export interface TinySubagentResult {
48
- ok: boolean;
49
- output: string;
50
- outputBytes: number;
51
- stderr: string;
52
- stderrBytes: number;
53
- stdout: string;
54
- stdoutBytes: number;
55
- protocolErrors: string[];
56
- errorMessage?: string;
57
- stopReason?: string;
58
- exitCode: number | null;
59
- signal: NodeJS.Signals | null;
60
- model?: string;
61
- cwd: string;
62
- durationMs: number;
63
- messages: TinySubagentMessage[];
64
- artifacts?: TinySubagentArtifacts;
65
- }
66
-
67
- interface JsonEvent {
68
- type?: string;
69
- message?: TinySubagentMessage;
70
- }
71
-
72
- class Capture {
73
- private readonly stream;
74
- private inline = "";
75
- private truncated = false;
76
- bytes = 0;
77
-
78
- readonly path: string;
79
- constructor(path: string) {
80
- this.path = path;
81
- this.stream = createWriteStream(path, { encoding: "utf8", mode: 0o600 });
82
- }
83
-
84
-
85
- write(text: string): void {
86
- this.bytes += Buffer.byteLength(text, "utf8");
87
- this.stream.write(text);
88
- if (this.truncated) return;
89
- const next = this.inline + text;
90
- if (Buffer.byteLength(next, "utf8") <= MAX_INLINE_BYTES) {
91
- this.inline = next;
92
- return;
93
- }
94
- this.inline = truncateUtf8(next, MAX_INLINE_BYTES);
95
- this.truncated = true;
96
- }
97
-
98
- async close(): Promise<void> {
99
- await new Promise<void>((resolve, reject) => {
100
- this.stream.once("error", reject);
101
- this.stream.once("finish", resolve);
102
- this.stream.end();
103
- });
104
- }
105
-
106
- preview(): string {
107
- return this.truncated ? `${this.inline}\n[truncated after ${MAX_INLINE_BYTES} bytes; see artifact file]` : this.inline;
108
- }
109
- }
110
-
111
- function truncateUtf8(text: string, maxBytes: number): string {
112
- let result = text.slice(0, maxBytes);
113
- while (Buffer.byteLength(result, "utf8") > maxBytes) result = result.slice(0, -1);
114
- return result;
115
- }
116
-
117
- function getFinalText(messages: TinySubagentMessage[]): string {
118
- for (let i = messages.length - 1; i >= 0; i--) {
119
- const message = messages[i];
120
- if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
121
- return message.content
122
- .filter((part) => part.type === "text" && typeof part.text === "string")
123
- .map((part) => part.text as string)
124
- .join("");
125
- }
126
- return "";
127
- }
128
-
129
- function parseLine(line: string, messages: TinySubagentMessage[], protocolErrors: string[]): boolean {
130
- if (!line.trim()) return false;
131
- try {
132
- const event = JSON.parse(line) as JsonEvent;
133
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
134
- messages.push(event.message);
135
- return true;
136
- }
137
- } catch {
138
- if (protocolErrors.length < 20) protocolErrors.push(truncateUtf8(line, 4096));
139
- }
140
- return false;
141
- }
142
-
143
- function getPiInvocation(args: string[]): { command: string; args: string[] } {
144
- const configuredBinary = process.env[BINARY_ENV]?.trim();
145
- if (configuredBinary) return { command: configuredBinary, args };
146
-
147
- const entry = process.argv[1];
148
- if (entry && existsSync(entry) && /\.(?:mjs|cjs|js)$/i.test(entry)) {
149
- try {
150
- const resolved = realpathSync(entry);
151
- if (resolved === entry || existsSync(resolved)) return { command: process.execPath, args: [entry, ...args] };
152
- } catch {
153
- // Fall through to the Pi binary on PATH.
154
- }
155
- }
156
-
157
- if (/^pi(?:\.exe)?$/i.test(basename(process.execPath))) return { command: process.execPath, args };
158
- return { command: "pi", args };
159
- }
160
-
161
- function stopReasonIsFailure(stopReason: string | undefined): boolean {
162
- return stopReason === "error" || stopReason === "aborted";
163
- }
164
-
165
- export async function runTinySubagent(prompt: string, options: TinySubagentOptions = {}): Promise<TinySubagentResult> {
166
- const cwd = options.cwd ?? process.cwd();
167
- const artifactDirectory = await mkdtemp(join(tmpdir(), "pi-tiny-subagent-"));
168
- const stdoutCapture = new Capture(join(artifactDirectory, "stdout.jsonl"));
169
- const stderrCapture = new Capture(join(artifactDirectory, "stderr.log"));
170
- const args = ["--mode", "json", "--print", "--no-session", "--no-extensions", "--no-skills"];
171
- if (options.model?.trim()) args.push("--model", options.model.trim());
172
- if (options.thinking?.trim()) args.push("--thinking", options.thinking.trim());
173
- if (options.tools?.length) args.push("--tools", options.tools.join(","));
174
- if (options.systemPrompt?.trim()) {
175
- const systemPromptPath = join(artifactDirectory, "system-prompt.md");
176
- await writeFile(systemPromptPath, options.systemPrompt, { encoding: "utf8", mode: 0o600 });
177
- args.push("--append-system-prompt", systemPromptPath);
178
- }
179
- args.push("--", prompt);
180
-
181
- const startedAt = Date.now();
182
- const messages: TinySubagentMessage[] = [];
183
- const protocolErrors: string[] = [];
184
- let stdoutBuffer = "";
185
- let errorMessage: string | undefined;
186
- let aborted = false;
187
- let exitCode: number | null = null;
188
- let childSignal: NodeJS.Signals | null = null;
189
- let spawnError: Error | undefined;
190
- let child: ReturnType<typeof spawn> | undefined;
191
- const invocation = getPiInvocation(args);
192
-
193
-
194
- const emitUpdate = () => {
195
- if (!options.onUpdate) return;
196
- const output = getFinalText(messages);
197
- const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
198
- options.onUpdate({
199
- ok: false,
200
- output,
201
- outputBytes: Buffer.byteLength(output, "utf8"),
202
- stderr: stderrCapture.preview(),
203
- stderrBytes: stderrCapture.bytes,
204
- stdout: stdoutCapture.preview(),
205
- stdoutBytes: stdoutCapture.bytes,
206
- protocolErrors: [...protocolErrors],
207
- ...(lastAssistant?.stopReason ? { stopReason: lastAssistant.stopReason } : {}),
208
- exitCode,
209
- signal: childSignal,
210
- ...(options.model ?? lastAssistant?.model ? { model: options.model ?? lastAssistant?.model } : {}),
211
- cwd,
212
- durationMs: Date.now() - startedAt,
213
- messages: messages.slice(),
214
- });
215
- };
216
-
217
- emitUpdate();
218
- await new Promise<void>((resolve) => {
219
- let settled = false;
220
- const finish = () => {
221
- if (settled) return;
222
- settled = true;
223
- resolve();
224
- };
225
- try {
226
- child = spawn(invocation.command, invocation.args, {
227
- cwd,
228
- shell: false,
229
- stdio: ["ignore", "pipe", "pipe"],
230
- env: { ...process.env, [CHILD_ENV]: "1" },
231
- });
232
- } catch (error) {
233
- spawnError = error instanceof Error ? error : new Error(String(error));
234
- finish();
235
- return;
236
- }
237
- child.stdout?.on("data", (chunk: Buffer | string) => {
238
- const text = chunk.toString();
239
- stdoutCapture.write(text);
240
- stdoutBuffer += text;
241
- const lines = stdoutBuffer.split("\n");
242
- stdoutBuffer = lines.pop() ?? "";
243
- for (const line of lines) {
244
- if (parseLine(line, messages, protocolErrors)) emitUpdate();
245
- }
246
- });
247
- child.stderr?.on("data", (chunk: Buffer | string) => stderrCapture.write(chunk.toString()));
248
- child.once("error", (error) => {
249
- spawnError = error;
250
- });
251
- child.once("close", (code, signal) => {
252
- if (stdoutBuffer && parseLine(stdoutBuffer, messages, protocolErrors)) emitUpdate();
253
- // Node may report a negative pseudo-exit code for ENOENT; expose a stable process failure code and keep the real error in errorMessage.
254
- exitCode = spawnError ? 1 : code;
255
- childSignal = signal;
256
- finish();
257
- });
258
-
259
- const terminate = () => {
260
- if (aborted) return;
261
- aborted = true;
262
- child?.kill("SIGTERM");
263
- setTimeout(() => {
264
- if (child && !child.killed) child.kill("SIGKILL");
265
- }, 5000).unref();
266
- };
267
- if (options.signal) {
268
- if (options.signal.aborted) terminate();
269
- else options.signal.addEventListener("abort", terminate, { once: true });
270
- }
271
- });
272
-
273
- await Promise.all([stdoutCapture.close(), stderrCapture.close()]);
274
- const finalOutput = getFinalText(messages);
275
- const lastAssistant = [...messages].reverse().find((message) => message.role === "assistant");
276
- const stopReason = lastAssistant?.stopReason;
277
- errorMessage = spawnError?.message ?? lastAssistant?.errorMessage;
278
- if (aborted) errorMessage ||= "Tiny subagent was aborted";
279
- if (!finalOutput && exitCode === 0 && !stopReason) errorMessage ||= "No assistant output received";
280
- if (protocolErrors.length > 0 && !finalOutput && !errorMessage) errorMessage = "Pi emitted invalid JSON output";
281
- if (stopReasonIsFailure(stopReason)) errorMessage ||= `Subagent stopped with reason: ${stopReason}`;
282
-
283
- const outputBytes = Buffer.byteLength(finalOutput, "utf8");
284
- const keepArtifacts = stdoutCapture.bytes > MAX_INLINE_BYTES || stderrCapture.bytes > MAX_INLINE_BYTES || outputBytes > MAX_INLINE_BYTES;
285
- let artifacts: TinySubagentArtifacts | undefined;
286
- if (keepArtifacts) {
287
- const finalOutputPath = join(artifactDirectory, "final-output.txt");
288
- if (outputBytes > 0) await writeFile(finalOutputPath, finalOutput, { encoding: "utf8", mode: 0o600 });
289
- artifacts = {
290
- directory: artifactDirectory,
291
- stdout: stdoutCapture.path,
292
- stderr: stderrCapture.path,
293
- cleanup: "remove directory when no longer needed",
294
- ...(outputBytes > MAX_INLINE_BYTES ? { finalOutput: finalOutputPath } : {}),
295
- };
296
- } else {
297
- await rm(artifactDirectory, { recursive: true, force: true });
298
- }
299
-
300
- return {
301
- ok: !errorMessage && exitCode === 0 && childSignal === null && !stopReasonIsFailure(stopReason),
302
- output: outputBytes > MAX_INLINE_BYTES ? `${truncateUtf8(finalOutput, MAX_INLINE_BYTES)}\n[truncated after ${MAX_INLINE_BYTES} bytes; see artifact file]` : finalOutput,
303
- outputBytes,
304
- stderr: stderrCapture.preview(),
305
- stderrBytes: stderrCapture.bytes,
306
- stdout: stdoutCapture.preview(),
307
- stdoutBytes: stdoutCapture.bytes,
308
- protocolErrors,
309
- ...(errorMessage ? { errorMessage } : {}),
310
- ...(stopReason ? { stopReason } : {}),
311
- exitCode,
312
- signal: childSignal,
313
- ...(options.model ?? lastAssistant?.model ? { model: options.model ?? lastAssistant?.model } : {}),
314
- cwd,
315
- durationMs: Date.now() - startedAt,
316
- messages: messages.slice(),
317
- ...(artifacts ? { artifacts } : {}),
318
- };
319
- }
@@ -1,111 +0,0 @@
1
- import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { tmpdir } from "node:os";
4
- import { discoverPackageAgents, parseAgentMarkdown } from "../src/personas.ts";
5
- import { MAX_INLINE_BYTES, runTinySubagent } from "../src/runner.ts";
6
-
7
- let passed = 0;
8
- let failed = 0;
9
- function check(name, condition, detail = "") {
10
- if (condition) {
11
- passed += 1;
12
- console.log(`PASS ${name}`);
13
- } else {
14
- failed += 1;
15
- console.log(`FAIL ${name} ${detail}`);
16
- }
17
- }
18
-
19
- const root = mkdtempSync(join(tmpdir(), "pi-tiny-subagent-test-"));
20
- const fakePi = join(root, "fake-pi.mjs");
21
- writeFileSync(fakePi, `#!/usr/bin/env node
22
- import { appendFileSync, readFileSync } from "node:fs";
23
- const args = process.argv.slice(2);
24
- const capture = process.env.TINY_CAPTURE;
25
- if (capture) appendFileSync(capture, JSON.stringify({ args, cwd: process.cwd(), child: process.env.PI_SUBAGENT_CHILD, promptFile: args[args.indexOf("--append-system-prompt") + 1] ? readFileSync(args[args.indexOf("--append-system-prompt") + 1], "utf8") : null }) + "\\n");
26
- const mode = process.env.TINY_MODE || "success";
27
- if (mode === "abort") { setTimeout(() => {}, 30000); }
28
- if (mode === "success") {
29
- process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", model: "fake/model", stopReason: "stop", content: [{ type: "text", text: "worker result" }] } }) + "\\n");
30
- } else if (mode === "events") {
31
- process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", content: [{ type: "toolCall", name: "bash", arguments: { command: "printf live" } }] } }) + "\\n");
32
- process.stdout.write(JSON.stringify({ type: "tool_result_end", message: { role: "toolResult", toolName: "bash", toolCallId: "call-1", isError: false, content: [{ type: "text", text: "live result" }] } }) + "\\n");
33
- process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", model: "fake/model", stopReason: "stop", content: [{ type: "text", text: "event result" }] } }) + "\\n");
34
- } else if (mode === "error") {
35
- process.stderr.write("provider failed\\n");
36
- process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", stopReason: "error", errorMessage: "rate limited", content: [] } }) + "\\n");
37
- process.exitCode = 7;
38
- } else if (mode === "protocol") {
39
- process.stdout.write("not-json\\n");
40
- process.exitCode = 2;
41
- } else if (mode === "large") {
42
- const text = "x".repeat(${MAX_INLINE_BYTES + 1000});
43
- process.stderr.write("e".repeat(${MAX_INLINE_BYTES + 1000}));
44
- process.stdout.write(JSON.stringify({ type: "message_end", message: { role: "assistant", stopReason: "stop", content: [{ type: "text", text }] } }) + "\\n");
45
- }
46
- `, { encoding: "utf8", mode: 0o700 });
47
- chmodSync(fakePi, 0o700);
48
-
49
- const capture = join(root, "capture.jsonl");
50
- const oldEnv = { ...process.env };
51
- function setEnv(values) {
52
- for (const key of Object.keys(process.env)) delete process.env[key];
53
- Object.assign(process.env, oldEnv, values);
54
- }
55
- function restoreEnv() {
56
- for (const key of Object.keys(process.env)) delete process.env[key];
57
- Object.assign(process.env, oldEnv);
58
- }
59
-
60
- try {
61
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_CAPTURE: capture, TINY_MODE: "success" });
62
- const success = await runTinySubagent("hello", { cwd: root, model: "fake/model", tools: ["read"], systemPrompt: "You are a worker." });
63
- const launch = JSON.parse(readFileSync(capture, "utf8").trim());
64
- check("success output", success.ok && success.output === "worker result");
65
- check("model, tools, and fresh-process flags", launch.args.includes("--model") && launch.args.includes("fake/model") && launch.args.includes("--tools") && launch.args.includes("read") && launch.args.includes("--no-session") && launch.args.includes("--no-extensions") && launch.args.includes("--no-skills"));
66
- check("cwd and child marker", launch.cwd === root && launch.child === "1");
67
- check("persona system prompt file", launch.promptFile === "You are a worker.");
68
-
69
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "events" });
70
- const updates = [];
71
- const events = await runTinySubagent("events", { cwd: root, onUpdate: (partial) => updates.push(partial) });
72
- check("event transcript and live updates", events.ok && events.output === "event result" && events.messages.length === 3 && events.messages[0].content?.[0]?.type === "toolCall" && events.messages[1].role === "toolResult" && updates.length >= 4 && updates.at(-1).messages.length === 3);
73
-
74
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "error" });
75
- const error = await runTinySubagent("fail", { cwd: root });
76
- check("Pi error details", !error.ok && error.exitCode === 7 && error.stopReason === "error" && error.errorMessage === "rate limited" && error.stderr.includes("provider failed"));
77
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "protocol" });
78
- const protocol = await runTinySubagent("bad", { cwd: root });
79
- check("protocol error details", !protocol.ok && protocol.exitCode === 2 && protocol.protocolErrors.includes("not-json"));
80
-
81
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: "/definitely/missing/pi", TINY_MODE: "success" });
82
- const missing = await runTinySubagent("missing", { cwd: root });
83
- check("spawn error details", !missing.ok && missing.exitCode === 1 && missing.errorMessage?.includes("ENOENT"));
84
-
85
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "large" });
86
- const large = await runTinySubagent("large", { cwd: root });
87
- check("large output spills to artifacts", large.ok && large.artifacts && large.artifacts.cleanup.includes("remove directory") && large.output.includes("see artifact file") && large.outputBytes > MAX_INLINE_BYTES && large.artifacts.finalOutput && readFileSync(large.artifacts.finalOutput, "utf8").length > MAX_INLINE_BYTES);
88
- if (large.artifacts) rmSync(large.artifacts.directory, { recursive: true, force: true });
89
-
90
- setEnv({ PI_TINY_SUBAGENT_PI_BINARY: fakePi, TINY_MODE: "abort" });
91
- const controller = new AbortController();
92
- const pending = runTinySubagent("abort", { cwd: root, signal: controller.signal });
93
- setTimeout(() => controller.abort(), 50);
94
- const aborted = await pending;
95
- check("abort details", !aborted.ok && aborted.errorMessage === "Tiny subagent was aborted");
96
-
97
- const definition = parseAgentMarkdown(`---\nname: reviewer\ndescription: Review code\nmodel: fake/model\ntools: read, grep\n---\nReview carefully.`, "reviewer.md");
98
- check("persona frontmatter parsing", definition.name === "reviewer" && definition.model === "fake/model" && definition.tools?.join(",") === "read,grep" && definition.systemPrompt === "Review carefully.");
99
- const agentsDir = join(root, "agents");
100
- await import("node:fs/promises").then(({ mkdir }) => mkdir(agentsDir));
101
- writeFileSync(join(agentsDir, "worker.md"), `---\nname: worker\ndescription: Worker\n---\nDo work.`);
102
- writeFileSync(join(agentsDir, "broken.md"), "not frontmatter");
103
- const discovery = discoverPackageAgents(agentsDir);
104
- check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
105
- } finally {
106
- restoreEnv();
107
- rmSync(root, { recursive: true, force: true });
108
- }
109
-
110
- console.log(`${passed} passed, ${failed} failed`);
111
- if (failed > 0) process.exitCode = 1;
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Tanishq Kancharla
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,19 +0,0 @@
1
- calldiff Skill
2
-
3
- This directory is copied verbatim from the calldiff repository:
4
- https://github.com/tanishqkancharla/calldiff
5
- skills/calldiff/SKILL.md at commit 2086f6e4e9ed3c410bba7c94ca3dde77791b1956 (main)
6
-
7
- The upstream repository is licensed under the MIT License. A copy of that
8
- license is included in LICENSE.
9
-
10
- Upstream publishes no release tags, so this file is pinned to a main commit
11
- instead of a tag. The CLI can also write its own copy with `calldiff skills
12
- add` (to ~/.agents/skills/calldiff/SKILL.md), but that generated file is terser
13
- than the committed one — it leaves the --entry/--file descriptions empty and
14
- says 22 languages — so pi-toolkit ships the committed file.
15
-
16
- To refresh, copy skills/calldiff/SKILL.md and LICENSE from a newer upstream
17
- commit and update the commit above. The bundled commands were smoke-tested
18
- against calldiff 0.5.0 (`diff`, `tree`, `reach`, `--entry`, `--file`,
19
- `--maxDepth`/`--max-depth`, `--locs`, `--format json`).
@@ -1,121 +0,0 @@
1
- ---
2
- name: calldiff
3
- description: Diff call stacks across git commits — like git diff, but for who-calls-whom. Shows which callees appeared, disappeared, or moved under an entrypoint across 23 languages (diff, tree, and reach). Use for agentic code review when call flow changed and line diffs bury the shape of the change.
4
- requires_bin: calldiff
5
- command: calldiff
6
- ---
7
-
8
- # calldiff diff
9
-
10
- Diff call stacks between two git trees
11
-
12
- ## Arguments
13
-
14
- | Name | Type | Required | Description |
15
- |------|------|----------|-------------|
16
- | `from` | `string` | no | Before ref (default: HEAD) |
17
- | `to` | `string` | no | After ref (default: working tree) |
18
- | `paths` | `array` | no | Limit to these path prefixes |
19
-
20
- ## Options
21
-
22
- | Flag | Type | Default | Description |
23
- |------|------|---------|-------------|
24
- | `--entry` | `unknown` | | Entrypoint symbol(s): functionName or ClassName.method |
25
- | `--file` | `unknown` | | Entrypoint file(s): indexed source path; expands to that file's exports |
26
- | `--maxDepth` | `number` | `12` | Max call-tree depth |
27
- | `--locs` | `boolean` | `false` | Show call-site source locations (file:line) |
28
- | `--from` | `string` | | Left / "before" tree |
29
- | `--to` | `string` | | Right / "after" tree |
30
-
31
- ## Examples
32
-
33
- ```sh
34
- # HEAD vs working tree
35
- calldiff diff
36
-
37
- # One ref vs working tree
38
- calldiff diff main
39
-
40
- # Two commits / branches
41
- calldiff diff abc123 def456
42
-
43
- # Force entrypoints
44
- calldiff diff main feature --entry createAgentSession
45
-
46
- # File expands to that file's exports
47
- calldiff diff main feature --file src/routes.ts
48
- ```
49
-
50
- > Semantics match git diff: no refs → HEAD vs worktree; one ref → that vs worktree; two refs → compare those trees.
51
-
52
- ---
53
-
54
- # calldiff reach
55
-
56
- Find all call paths from an entrypoint to a target symbol
57
-
58
- ## Arguments
59
-
60
- | Name | Type | Required | Description |
61
- |------|------|----------|-------------|
62
- | `ref` | `string` | no | Git ref (default: working tree) |
63
- | `paths` | `array` | no | Limit to these path prefixes |
64
-
65
- ## Options
66
-
67
- | Flag | Type | Default | Description |
68
- |------|------|---------|-------------|
69
- | `--entry` | `unknown` | | Entrypoint symbol(s): functionName or ClassName.method |
70
- | `--file` | `unknown` | | Entrypoint file(s): indexed source path; expands to that file's exports |
71
- | `--to` | `string` | | Target symbol to reach (functionName or ClassName.method) |
72
- | `--maxDepth` | `number` | `12` | Max call-tree depth |
73
- | `--locs` | `boolean` | `false` | Show call-site source locations (file:line) |
74
-
75
- ## Examples
76
-
77
- ```sh
78
- # Paths in the working tree
79
- calldiff reach --entry runCheckout --to sendEmail
80
-
81
- # Paths at a commit, limited to a directory
82
- calldiff reach HEAD examples/checkout --entry runCheckout --to sendEmail
83
-
84
- # Paths from every export in a file
85
- calldiff reach --file packages/api/src/flow.ts --to notify
86
- ```
87
-
88
- ---
89
-
90
- # calldiff tree
91
-
92
- View a call tree (no diff) for one or more entrypoints
93
-
94
- ## Arguments
95
-
96
- | Name | Type | Required | Description |
97
- |------|------|----------|-------------|
98
- | `ref` | `string` | no | Git ref (default: working tree) |
99
- | `paths` | `array` | no | Limit to these path prefixes |
100
-
101
- ## Options
102
-
103
- | Flag | Type | Default | Description |
104
- |------|------|---------|-------------|
105
- | `--entry` | `unknown` | | Entrypoint symbol(s): functionName or ClassName.method |
106
- | `--file` | `unknown` | | Entrypoint file(s): indexed source path; expands to that file's exports |
107
- | `--maxDepth` | `number` | `12` | Max call-tree depth |
108
- | `--locs` | `boolean` | `false` | Show call-site source locations (file:line) |
109
-
110
- ## Examples
111
-
112
- ```sh
113
- # Tree from working tree
114
- calldiff tree --entry createAgentSession
115
-
116
- # Tree from a commit
117
- calldiff tree HEAD --entry PiService.createAgentSession
118
-
119
- # Every export in a file
120
- calldiff tree --file packages/api/src/routes.ts
121
- ```