@heyhuynhgiabuu/pi-task 0.1.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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Task Extension — Pure helper functions.
3
+ *
4
+ * No side effects, no ExtensionAPI dependency. All functions here are
5
+ * unit-testable with node:assert/strict.
6
+ */
7
+ export declare function parseMarkdownFrontmatter(content: string): {
8
+ frontmatter: Record<string, string>;
9
+ body: string;
10
+ };
11
+ export interface AgentConfig {
12
+ name: string;
13
+ description: string;
14
+ model?: string;
15
+ thinking?: string;
16
+ /** Explicit allowlist from frontmatter `tools:` */
17
+ tools?: string | string[];
18
+ disallowedTools?: string[];
19
+ body: string;
20
+ source: "project" | "user" | "bundled";
21
+ path: string;
22
+ }
23
+ export interface ParsedResult {
24
+ status: string;
25
+ summary: string;
26
+ findings: string;
27
+ evidence: string;
28
+ confidence: string;
29
+ raw: string;
30
+ }
31
+ /** A single tool call extracted from a subagent session JSONL. */
32
+ export interface ToolCallRecord {
33
+ /** Tool name (e.g. "websearch", "read", "bash") */
34
+ name: string;
35
+ /** Short, human-readable summary of the call's primary argument */
36
+ detail: string;
37
+ /** "done" if a matching toolResult was seen, "error" if isError, "in_progress" otherwise */
38
+ status: "done" | "error" | "in_progress";
39
+ /** Entry id of the toolCall block (used for stable sorting/debug) */
40
+ id: string;
41
+ }
42
+ export declare const TASK_BACKGROUND_DEFAULT = true;
43
+ export declare const TASK_RESULT_XML_INSTRUCTIONS = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
44
+ export declare const OUTPUT_FORMAT_GUIDE = "<status>success|failure|blocked|partial</status>\n<summary>One sentence: what was accomplished</summary>\n<findings>Key findings with file:line references</findings>\n<evidence>Verification evidence, commands run, output snippets</evidence>\n<confidence>high|medium|low (optional \u2014 how certain the findings are)</confidence>\n<files>Comma-separated absolute paths of files read/created (optional)</files>\n\nPrefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.";
45
+ export declare const TASK_TOOL_DESCRIPTION = "Launch a new agent to handle complex, multistep tasks autonomously.\n\nInclude relevant context from your current work in the prompt parameter \u2014\nthis becomes the subagent's instructions. The subagent knows nothing about what you've been doing except what you put in the prompt.\n\nWhen NOT to use:\n- To read a specific file path, use Read or Grep instead\n- To search for a class definition like 'class Foo', use Grep instead\n- To search code within 2-3 files, use Read instead\n- If no available agent fits the task, use other tools directly\n\nUsage notes:\n1. Provide complete context in the prompt \u2014 the subagent starts with a fresh context\n2. Launch multiple agents concurrently when possible (use a single message with multiple tool calls)\n3. Once you delegate work, do NOT duplicate it. Continue with non-overlapping tasks, or wait for the result\n4. Background is the default. Use background:false only when you need the caller to wait inline for the tmux task result\n5. Do not trust delegated output blindly. Read changed files, review the diff, verify scope, and run the relevant checks before claiming completion\n6. Clearly tell the agent whether to write code or just research, since it doesn't know the user's intent\n7. The result returned by the agent is not visible to the user. Send a concise summary back to the user\n8. Pass task_id to resume a previous subagent session (continues with its prior context)\n\nBackground mode (background: true):\n- Launches the subagent asynchronously and returns immediately\n- You will be notified automatically when it finishes\n- DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background\n- Avoid working with the same files or topics the background task is using\n- Work on non-overlapping tasks, or briefly tell the user what you launched and end your response";
46
+ /** @deprecated Import from ./agent-tools.js */
47
+ export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
48
+ export declare function extractTag(raw: string, re: RegExp): string;
49
+ export declare function parseResultXml(raw: string): ParsedResult;
50
+ export declare function formatMs(ms: number): string;
51
+ export declare function parseIdTimestamp(id: string): number;
52
+ export declare function shellQuote(value: string): string;
53
+ export declare function buildTmuxSendKeysArgs(paneId: string, command: string): string[];
54
+ export interface BackgroundReceiptInput {
55
+ taskId: string;
56
+ agentType: string;
57
+ tmuxSession: string;
58
+ artifactDir: string;
59
+ }
60
+ export declare function formatBackgroundReceipt(input: BackgroundReceiptInput): string;
61
+ export declare function findPiDir(cwd: string): string | null;
62
+ export declare function getGlobalAgentDir(): string;
63
+ export declare function loadAgentsFromDir(dir: string, source: "project" | "user" | "bundled"): AgentConfig[];
64
+ export declare function discoverAgents(cwd: string, bundledAgentDir?: string): {
65
+ agents: AgentConfig[];
66
+ piDir: string;
67
+ };
68
+ export declare function formatAgentList(agents: AgentConfig[]): string;
69
+ /**
70
+ * Build pi CLI arguments for spawning or resuming a sub-agent session.
71
+ *
72
+ * - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
73
+ * - Resume: pass `resume=true` — `--session <name>` is included so pi
74
+ * continues the existing session file in --session-dir.
75
+ */
76
+ export declare function buildPiArgs(agent: AgentConfig, sessionName: string, sessionDir: string, promptContent: string, resume?: boolean, parentToolNames?: string[]): string[];
77
+ /** Count tool uses and turns from pi JSONL session files. */
78
+ export declare function countToolUses(sessionDir: string): {
79
+ toolUses: number;
80
+ turns: number;
81
+ };
82
+ /**
83
+ * Extract a short, human-readable summary of a tool call's primary argument.
84
+ * Falls back to the first string-valued property for unknown tools.
85
+ */
86
+ export declare function summarizeArgs(toolName: string, args: unknown): string;
87
+ /**
88
+ * Read the most recent tool calls from a pi JSONL session directory,
89
+ * with each call's status (done / error / in_progress) determined by
90
+ * whether a matching toolResult has been written.
91
+ *
92
+ * Returns total counts plus the last `limit` records in chronological order.
93
+ * Safe against malformed lines and missing fields.
94
+ */
95
+ export declare function readRecentToolCalls(sessionDir: string, limit?: number): {
96
+ toolUses: number;
97
+ turns: number;
98
+ recent: ToolCallRecord[];
99
+ };
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Task Extension — Pure helper functions.
3
+ *
4
+ * No side effects, no ExtensionAPI dependency. All functions here are
5
+ * unit-testable with node:assert/strict.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { join, dirname, basename, resolve } from "node:path";
9
+ import { parseToolList } from "./agent-tools.js";
10
+ import { parseMergedDisallowedTools } from "./policy.js";
11
+ import { buildPiArgv } from "./subagent/buildArgv.js";
12
+ export function parseMarkdownFrontmatter(content) {
13
+ if (!content.startsWith("---\n")) {
14
+ return { frontmatter: {}, body: content };
15
+ }
16
+ const end = content.indexOf("\n---", 4);
17
+ if (end === -1)
18
+ return { frontmatter: {}, body: content };
19
+ const raw = content.slice(4, end).trim();
20
+ const body = content.slice(end + "\n---".length).replace(/^\n/, "");
21
+ const frontmatter = {};
22
+ for (const line of raw.split("\n")) {
23
+ const idx = line.indexOf(":");
24
+ if (idx === -1)
25
+ continue;
26
+ const key = line.slice(0, idx).trim();
27
+ const value = line.slice(idx + 1).trim();
28
+ if (key)
29
+ frontmatter[key] = value;
30
+ }
31
+ return { frontmatter, body };
32
+ }
33
+ // ─── Constants ───────────────────────────────────────────────────────────────
34
+ export const TASK_BACKGROUND_DEFAULT = true;
35
+ export const TASK_RESULT_XML_INSTRUCTIONS = `<status>success|failure|blocked|partial</status>
36
+ <summary>One sentence: what was accomplished</summary>
37
+ <findings>Key findings with file:line references</findings>
38
+ <evidence>Verification evidence, commands run, output snippets</evidence>
39
+ <confidence>high|medium|low (optional — how certain the findings are)</confidence>
40
+ <files>Comma-separated absolute paths of files read/created (optional)</files>
41
+
42
+ Prefer writing this block to RESULT.md when done. If you cannot write the file, your final assistant message MUST include the same XML block.`;
43
+ export const OUTPUT_FORMAT_GUIDE = TASK_RESULT_XML_INSTRUCTIONS;
44
+ export const TASK_TOOL_DESCRIPTION = `Launch a new agent to handle complex, multistep tasks autonomously.
45
+
46
+ Include relevant context from your current work in the prompt parameter —
47
+ this becomes the subagent's instructions. The subagent knows nothing about what you've been doing except what you put in the prompt.
48
+
49
+ When NOT to use:
50
+ - To read a specific file path, use Read or Grep instead
51
+ - To search for a class definition like 'class Foo', use Grep instead
52
+ - To search code within 2-3 files, use Read instead
53
+ - If no available agent fits the task, use other tools directly
54
+
55
+ Usage notes:
56
+ 1. Provide complete context in the prompt — the subagent starts with a fresh context
57
+ 2. Launch multiple agents concurrently when possible (use a single message with multiple tool calls)
58
+ 3. Once you delegate work, do NOT duplicate it. Continue with non-overlapping tasks, or wait for the result
59
+ 4. Background is the default. Use background:false only when you need the caller to wait inline for the tmux task result
60
+ 5. Do not trust delegated output blindly. Read changed files, review the diff, verify scope, and run the relevant checks before claiming completion
61
+ 6. Clearly tell the agent whether to write code or just research, since it doesn't know the user's intent
62
+ 7. The result returned by the agent is not visible to the user. Send a concise summary back to the user
63
+ 8. Pass task_id to resume a previous subagent session (continues with its prior context)
64
+
65
+ Background mode (background: true):
66
+ - Launches the subagent asynchronously and returns immediately
67
+ - You will be notified automatically when it finishes
68
+ - DO NOT sleep, poll, ask the task for status, or duplicate its work while it runs in background
69
+ - Avoid working with the same files or topics the background task is using
70
+ - Work on non-overlapping tasks, or briefly tell the user what you launched and end your response`;
71
+ /** @deprecated Import from ./agent-tools.js */
72
+ export { ALL_TOOL_NAMES, BUILTIN_TOOL_NAMES } from "./agent-tools.js";
73
+ // Cached regex patterns for XML result parsing
74
+ const STATUS_RE = /<status>([\s\S]*?)<\/status>/i;
75
+ const SUMMARY_RE = /<summary>([\s\S]*?)<\/summary>/i;
76
+ const FINDINGS_RE = /<findings>([\s\S]*?)<\/findings>/i;
77
+ const EVIDENCE_RE = /<evidence>([\s\S]*?)<\/evidence>/i;
78
+ const CONFIDENCE_RE = /<confidence>([\s\S]*?)<\/confidence>/i;
79
+ // ─── Result Parsing ──────────────────────────────────────────────────────────
80
+ export function extractTag(raw, re) {
81
+ const m = raw.match(re);
82
+ return m ? m[1].trim() : "";
83
+ }
84
+ export function parseResultXml(raw) {
85
+ const status = extractTag(raw, STATUS_RE);
86
+ if (!status &&
87
+ !extractTag(raw, SUMMARY_RE) &&
88
+ !extractTag(raw, FINDINGS_RE) &&
89
+ !extractTag(raw, EVIDENCE_RE)) {
90
+ return {
91
+ status: "unknown",
92
+ summary: raw.slice(0, 500),
93
+ findings: "",
94
+ evidence: "",
95
+ confidence: "",
96
+ raw,
97
+ };
98
+ }
99
+ const confidence = extractTag(raw, CONFIDENCE_RE);
100
+ return {
101
+ status: status || "unknown",
102
+ summary: extractTag(raw, SUMMARY_RE) || "",
103
+ findings: extractTag(raw, FINDINGS_RE) || "",
104
+ evidence: extractTag(raw, EVIDENCE_RE) || "",
105
+ confidence: confidence || "",
106
+ raw,
107
+ };
108
+ }
109
+ // ─── Formatting ──────────────────────────────────────────────────────────────
110
+ export function formatMs(ms) {
111
+ if (ms >= 60_000)
112
+ return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1_000)}s`;
113
+ if (ms >= 1_000)
114
+ return `${(ms / 1_000).toFixed(1)}s`;
115
+ return `${ms}ms`;
116
+ }
117
+ export function parseIdTimestamp(id) {
118
+ try {
119
+ const ts36 = id.split("-")[0];
120
+ if (ts36)
121
+ return parseInt(ts36, 36);
122
+ }
123
+ catch {
124
+ /* fall through */
125
+ }
126
+ return Date.now();
127
+ }
128
+ export function shellQuote(value) {
129
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
130
+ }
131
+ export function buildTmuxSendKeysArgs(paneId, command) {
132
+ return ["send-keys", "-t", paneId, command, "Enter"];
133
+ }
134
+ export function formatBackgroundReceipt(input) {
135
+ return [
136
+ `Started task ${input.taskId} with ${input.agentType}.`,
137
+ `Tmux session: ${input.tmuxSession}.`,
138
+ `Artifact directory: ${input.artifactDir}.`,
139
+ "A completion notification will arrive automatically; do not poll or duplicate this work.",
140
+ ].join("\n");
141
+ }
142
+ // ─── Agent Discovery ─────────────────────────────────────────────────────────
143
+ export function findPiDir(cwd) {
144
+ let current = resolve(cwd);
145
+ while (true) {
146
+ if (basename(current) === ".pi") {
147
+ const parent = dirname(current);
148
+ if (parent === current)
149
+ return current;
150
+ current = parent;
151
+ continue;
152
+ }
153
+ if (existsSync(join(current, ".pi")))
154
+ return join(current, ".pi");
155
+ const parent = dirname(current);
156
+ if (parent === current)
157
+ return null;
158
+ current = parent;
159
+ }
160
+ }
161
+ export function getGlobalAgentDir() {
162
+ const home = process.env.HOME || process.env.USERPROFILE || "";
163
+ return join(home, ".pi", "agent", "agents");
164
+ }
165
+ export function loadAgentsFromDir(dir, source) {
166
+ const agents = [];
167
+ if (!existsSync(dir))
168
+ return agents;
169
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
170
+ if (!entry.name.endsWith(".md"))
171
+ continue;
172
+ if (!entry.isFile() && !entry.isSymbolicLink())
173
+ continue;
174
+ const filePath = join(dir, entry.name);
175
+ let content;
176
+ try {
177
+ content = readFileSync(filePath, "utf-8");
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ const { frontmatter, body } = parseMarkdownFrontmatter(content);
183
+ if (!frontmatter.description)
184
+ continue;
185
+ const name = basename(entry.name, ".md");
186
+ const disallowedRaw = frontmatter.disallowed_tools;
187
+ const merged = parseMergedDisallowedTools(parseToolList(disallowedRaw).join(","));
188
+ const disallowedTools = merged.length > 0 ? merged : undefined;
189
+ const tools = parseToolList(frontmatter.tools);
190
+ agents.push({
191
+ name,
192
+ description: frontmatter.description,
193
+ model: frontmatter.model,
194
+ thinking: frontmatter.thinking,
195
+ tools: tools.length > 0 ? tools : undefined,
196
+ disallowedTools,
197
+ body,
198
+ source,
199
+ path: filePath,
200
+ });
201
+ }
202
+ return agents;
203
+ }
204
+ export function discoverAgents(cwd, bundledAgentDir) {
205
+ const piDir = findPiDir(cwd) || join(cwd, ".pi");
206
+ const projectDir = join(piDir, "agents");
207
+ const userDir = getGlobalAgentDir();
208
+ const bundledAgents = bundledAgentDir
209
+ ? loadAgentsFromDir(bundledAgentDir, "bundled")
210
+ : [];
211
+ const userAgents = loadAgentsFromDir(userDir, "user");
212
+ const projectAgents = loadAgentsFromDir(projectDir, "project");
213
+ // Override order: bundled < user < project.
214
+ const agentMap = new Map();
215
+ for (const a of bundledAgents)
216
+ agentMap.set(a.name, a);
217
+ for (const a of userAgents)
218
+ agentMap.set(a.name, a);
219
+ for (const a of projectAgents)
220
+ agentMap.set(a.name, a);
221
+ return {
222
+ agents: Array.from(agentMap.values()).sort((a, b) => a.name.localeCompare(b.name)),
223
+ piDir,
224
+ };
225
+ }
226
+ export function formatAgentList(agents) {
227
+ if (agents.length === 0)
228
+ return "none available";
229
+ return agents
230
+ .map((a) => `${a.name} (${a.source}): ${a.description}`)
231
+ .join("\n");
232
+ }
233
+ // ─── Sub-agent CLI args ─────────────────────────────────────────────────────
234
+ /**
235
+ * Build pi CLI arguments for spawning or resuming a sub-agent session.
236
+ *
237
+ * - Fresh spawn: omit `resume` or pass falsy — `--session` is not included.
238
+ * - Resume: pass `resume=true` — `--session <name>` is included so pi
239
+ * continues the existing session file in --session-dir.
240
+ */
241
+ export function buildPiArgs(agent, sessionName, sessionDir, promptContent, resume, parentToolNames) {
242
+ return buildPiArgv({
243
+ agent,
244
+ sessionName,
245
+ sessionDir,
246
+ promptContent,
247
+ resume,
248
+ parentToolNames,
249
+ });
250
+ }
251
+ // ─── JSONL Session Helpers ───────────────────────────────────────────────────
252
+ /** Count tool uses and turns from pi JSONL session files. */
253
+ export function countToolUses(sessionDir) {
254
+ let toolUses = 0;
255
+ let turns = 0;
256
+ try {
257
+ if (!existsSync(sessionDir))
258
+ return { toolUses, turns };
259
+ const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
260
+ for (const file of files) {
261
+ const content = readFileSync(join(sessionDir, file), "utf-8");
262
+ for (const rawLine of content.split("\n")) {
263
+ const line = rawLine.trim();
264
+ if (!line)
265
+ continue;
266
+ try {
267
+ const entry = JSON.parse(line);
268
+ if (entry.type === "message" &&
269
+ entry.message?.role === "assistant" &&
270
+ Array.isArray(entry.message.content)) {
271
+ turns++;
272
+ for (const block of entry.message.content) {
273
+ if (block.type === "toolCall")
274
+ toolUses++;
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ // Skip malformed lines
280
+ }
281
+ }
282
+ }
283
+ }
284
+ catch {
285
+ // Session dir might not exist or be inaccessible
286
+ }
287
+ return { toolUses, turns };
288
+ }
289
+ // ─── JSONL Session Helpers — streaming ───────────────────────────────────────
290
+ /**
291
+ * Extract a short, human-readable summary of a tool call's primary argument.
292
+ * Falls back to the first string-valued property for unknown tools.
293
+ */
294
+ export function summarizeArgs(toolName, args) {
295
+ if (!args || typeof args !== "object")
296
+ return "";
297
+ const a = args;
298
+ const pick = (...keys) => {
299
+ for (const k of keys) {
300
+ const v = a[k];
301
+ if (typeof v === "string" && v.length > 0)
302
+ return v;
303
+ }
304
+ return "";
305
+ };
306
+ switch (toolName) {
307
+ case "read":
308
+ case "write":
309
+ case "edit":
310
+ case "ls":
311
+ return pick("path", "file_path");
312
+ case "bash":
313
+ return pick("command", "cmd");
314
+ case "grep":
315
+ case "codesearch":
316
+ case "websearch":
317
+ return pick("query", "pattern", "search_term", "glob");
318
+ case "web_fetch":
319
+ case "webclaw_scrape":
320
+ case "lightpanda_markdown":
321
+ case "lightpanda_links":
322
+ case "lightpanda_structuredData":
323
+ return pick("url");
324
+ case "webclaw_batch":
325
+ return Array.isArray(a.urls) ? `${a.urls.length} urls` : pick("urls");
326
+ case "context7":
327
+ return pick("libraryId", "topic", "libraryName");
328
+ case "deepwiki":
329
+ return pick("question", "repo");
330
+ case "find":
331
+ return pick("pattern", "glob");
332
+ default: {
333
+ // Fallback: first non-empty string property
334
+ for (const v of Object.values(a)) {
335
+ if (typeof v === "string" && v.length > 0)
336
+ return v;
337
+ }
338
+ return "";
339
+ }
340
+ }
341
+ }
342
+ /**
343
+ * Read the most recent tool calls from a pi JSONL session directory,
344
+ * with each call's status (done / error / in_progress) determined by
345
+ * whether a matching toolResult has been written.
346
+ *
347
+ * Returns total counts plus the last `limit` records in chronological order.
348
+ * Safe against malformed lines and missing fields.
349
+ */
350
+ export function readRecentToolCalls(sessionDir, limit = 12) {
351
+ let toolUses = 0;
352
+ let turns = 0;
353
+ const calls = [];
354
+ const resultsById = new Map();
355
+ try {
356
+ if (!existsSync(sessionDir))
357
+ return { toolUses, turns, recent: [] };
358
+ const files = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl"));
359
+ for (const file of files) {
360
+ const content = readFileSync(join(sessionDir, file), "utf-8");
361
+ for (const rawLine of content.split("\n")) {
362
+ const line = rawLine.trim();
363
+ if (!line)
364
+ continue;
365
+ let entry;
366
+ try {
367
+ entry = JSON.parse(line);
368
+ }
369
+ catch {
370
+ continue;
371
+ }
372
+ const msg = entry?.message;
373
+ if (!msg || typeof msg !== "object")
374
+ continue;
375
+ // Collect tool results first so we can match them to tool calls
376
+ if (msg.role === "toolResult") {
377
+ const ts = typeof msg.timestamp === "number"
378
+ ? msg.timestamp
379
+ : Date.parse(entry?.timestamp ?? "") || 0;
380
+ if (typeof msg.toolCallId === "string") {
381
+ resultsById.set(msg.toolCallId, {
382
+ isError: Boolean(msg.isError),
383
+ ts,
384
+ });
385
+ }
386
+ continue;
387
+ }
388
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
389
+ continue;
390
+ turns++;
391
+ for (const block of msg.content) {
392
+ if (!block || block.type !== "toolCall")
393
+ continue;
394
+ toolUses++;
395
+ const id = typeof block.id === "string" ? block.id : "";
396
+ if (!id)
397
+ continue; // can't match results without an id
398
+ calls.push({
399
+ name: typeof block.name === "string" ? block.name : "tool",
400
+ detail: summarizeArgs(typeof block.name === "string" ? block.name : "", block.arguments),
401
+ id,
402
+ ts: typeof msg.timestamp === "number"
403
+ ? msg.timestamp
404
+ : Date.parse(entry?.timestamp ?? "") || 0,
405
+ });
406
+ }
407
+ }
408
+ }
409
+ }
410
+ catch {
411
+ return { toolUses, turns, recent: [] };
412
+ }
413
+ // Determine status for each call, then take the last `limit` in order
414
+ const ordered = calls.slice().sort((a, b) => a.ts - b.ts);
415
+ const all = ordered.map((c) => {
416
+ const r = resultsById.get(c.id);
417
+ if (!r)
418
+ return {
419
+ name: c.name,
420
+ detail: c.detail,
421
+ id: c.id,
422
+ status: "in_progress",
423
+ };
424
+ return {
425
+ name: c.name,
426
+ detail: c.detail,
427
+ id: c.id,
428
+ status: r.isError ? "error" : "done",
429
+ };
430
+ });
431
+ const recent = all.slice(Math.max(0, all.length - limit));
432
+ return { toolUses, turns, recent };
433
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Task Tool — Delegate complex work to specialist agents.
3
+ *
4
+ * Spawns pi CLI in a tmux split pane (so you can watch it live) and
5
+ * detects completion via RESULT.md polling. On completion, tool call
6
+ * count and duration are reported as a notification.
7
+ *
8
+ * Three agent sources:
9
+ * - .pi/agents/*.md project-local agents
10
+ * - ~/.pi/agent/agents/*.md user-global agents (fallback)
11
+ *
12
+ * P0: Persistent task registry (appendEntry + JSON), --session resume,
13
+ * sendMessage completion notification.
14
+ * P1: Foreground mode (background:false, inline subprocess), pane death
15
+ * detection, 30-minute timeout.
16
+ */
17
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+ export default function (pi: ExtensionAPI): void;