@rohaquinlop/pi-subagents 0.4.0 → 0.5.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/agents/worker.md CHANGED
@@ -16,6 +16,7 @@ Guidelines:
16
16
  - Read files before editing to understand existing code
17
17
  - Make targeted edits, not wholesale rewrites
18
18
  - Use safe_bash for running commands (tests, builds, installs, etc.)
19
+ - `safe_bash` has a 5-minute default timeout (300s). For long-running commands (builds, tests, installs), pass a larger `timeout`, e.g. `timeout: 600` for 10 minutes.
19
20
  - If something fails, diagnose and fix it
20
21
  - Report what you did and what changed when done
21
22
 
package/index.ts CHANGED
@@ -17,6 +17,8 @@ import "./tools/safe-bash";
17
17
  import type { AgentConfig, AgentUsage, PipelineStepResult, PipelineResult, LoopIterationResult, LoopResult } from "./lib/types";
18
18
  import { discoverAgents, mergeAgents, substitutePlaceholders, formatConnectorContext } from "./lib/helpers";
19
19
  import { zeroUsage, accumulateUsage, validateAgents, MAX_LOOP_CONTEXT, parseJudgeVerdict } from "./lib/pipeline-helpers";
20
+ import { buildSubagentErrorContent, buildPipelineErrorContent, buildLoopErrorContent } from "./lib/error-helpers";
21
+ import { detectCycle } from "./lib/loop-detector";
20
22
 
21
23
  interface ToolEvent {
22
24
  tool: string;
@@ -55,6 +57,7 @@ interface AgentProgress {
55
57
  durationMs: number;
56
58
  lastMessage: string;
57
59
  error?: string;
60
+ warning?: string;
58
61
  }
59
62
 
60
63
  interface AgentResult {
@@ -78,6 +81,8 @@ interface Details {
78
81
 
79
82
  interface ExtensionConfig {
80
83
  maxConcurrency?: number;
84
+ subagentTimeoutMs?: number; // wall-clock, default 600000 (10 min). 0 = disabled.
85
+ subagentIdleTimeoutMs?: number; // no-stdout watchdog, default 300000 (5 min). 0 = disabled.
81
86
  }
82
87
 
83
88
  const EXT_DIR = path.dirname(new URL(import.meta.url).pathname);
@@ -85,6 +90,10 @@ const AGENTS_DIR = path.join(EXT_DIR, "agents");
85
90
  const TOOLS_DIR = path.join(EXT_DIR, "tools");
86
91
  const CONFIG_PATH = path.join(EXT_DIR, "config.json");
87
92
  const DEFAULT_MAX_CONCURRENCY = 4;
93
+ const DEFAULT_SUBAGENT_TIMEOUT_MS = 600_000; // 10 minutes
94
+ const DEFAULT_SUBAGENT_IDLE_TIMEOUT_MS = 300_000; // 5 minutes
95
+
96
+ let extensionConfig: ExtensionConfig = {};
88
97
 
89
98
  function loadConfig(): ExtensionConfig {
90
99
  try {
@@ -479,6 +488,11 @@ function flatten(s: string): string {
479
488
  // doesn't need to read inline anyway.
480
489
  const MAX_ARG_PREVIEW = 4000;
481
490
 
491
+ // Hard cap on recentTools entries to prevent unbounded memory growth in
492
+ // long-running subagents. Generous for expanded-view history; matches the
493
+ // callHistory trim pattern.
494
+ const MAX_RECENT_TOOLS = 50;
495
+
482
496
  function extractToolArgsPreview(args: Record<string, unknown>): string {
483
497
  const cap = (s: string) => (s.length > MAX_ARG_PREVIEW ? s.slice(0, MAX_ARG_PREVIEW) + "…" : s);
484
498
  if (args.command) return cap(flatten(String(args.command)));
@@ -502,7 +516,7 @@ async function runSubagent(
502
516
  task: string,
503
517
  cwd: string,
504
518
  signal: AbortSignal | undefined,
505
- onUpdate?: (progress: AgentProgress, usage: AgentResult["usage"]) => void,
519
+ onUpdate?: (progress: AgentProgress, usage: AgentResult["usage"], finalExitCode?: number) => void,
506
520
  ): Promise<AgentResult> {
507
521
  const { args, tempDir, childEnv } = await buildPiArgs(agent, task, cwd);
508
522
  const command = args[0];
@@ -544,14 +558,73 @@ async function runSubagent(
544
558
 
545
559
  let buf = "";
546
560
  let stderrBuf = "";
561
+ let resolved = false;
562
+ let wallTimer: ReturnType<typeof setTimeout> | undefined;
563
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
564
+ let inFlightToolCount = 0;
565
+ let childClosed = false;
566
+ let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
567
+ let callHistory: string[] = []; // sliding window of tool-call signatures for cycle detection
568
+
569
+ const safeResolve = (code: number) => {
570
+ if (resolved) return;
571
+ resolved = true;
572
+ clearTimeout(wallTimer);
573
+ clearTimeout(idleTimer);
574
+ clearTimeout(sigkillTimer);
575
+ if (signal) signal.removeEventListener("abort", abortKill);
576
+ resolve(code);
577
+ };
578
+
579
+ // Wall-clock timeout
580
+ const timeoutMs = extensionConfig.subagentTimeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS;
581
+ if (timeoutMs > 0) {
582
+ wallTimer = setTimeout(() => {
583
+ if (!progress.error) progress.error = `Subagent timed out after ${Math.round(timeoutMs / 1000)}s`;
584
+ proc.kill("SIGTERM");
585
+ clearTimeout(sigkillTimer);
586
+ sigkillTimer = setTimeout(() => {
587
+ if (!childClosed) proc.kill("SIGKILL");
588
+ }, 5000);
589
+ }, timeoutMs);
590
+ }
591
+
592
+ // Idle (no-stdout) watchdog
593
+ const idleMs = extensionConfig.subagentIdleTimeoutMs ?? DEFAULT_SUBAGENT_IDLE_TIMEOUT_MS;
594
+ const resetIdle = () => {
595
+ if (idleMs <= 0) return;
596
+ clearTimeout(idleTimer);
597
+ idleTimer = setTimeout(() => {
598
+ if (!progress.error) progress.error = `Subagent idle for ${Math.round(idleMs / 1000)}s — likely stuck`;
599
+ proc.kill("SIGTERM");
600
+ clearTimeout(sigkillTimer);
601
+ sigkillTimer = setTimeout(() => {
602
+ if (!childClosed) proc.kill("SIGKILL");
603
+ }, 5000);
604
+ }, idleMs);
605
+ };
606
+ resetIdle();
607
+
608
+ const pauseIdle = () => {
609
+ clearTimeout(idleTimer);
610
+ idleTimer = undefined;
611
+ };
612
+ const resumeIdle = () => {
613
+ if (inFlightToolCount === 0) resetIdle();
614
+ };
615
+
616
+ const MAX_STDERR_BYTES = 100_000;
547
617
 
548
618
  const processLine = (line: string) => {
619
+ if (inFlightToolCount === 0) resetIdle();
549
620
  if (!line.trim()) return;
550
621
  try {
551
622
  const evt = JSON.parse(line) as any;
552
623
  progress.durationMs = Date.now() - startTime;
553
624
 
554
625
  if (evt.type === "tool_execution_start") {
626
+ inFlightToolCount++;
627
+ pauseIdle();
555
628
  progress.toolCount++;
556
629
  progress.recentTools.push({
557
630
  tool: evt.toolName,
@@ -559,6 +632,37 @@ async function runSubagent(
559
632
  toolCallId: evt.toolCallId,
560
633
  status: "running",
561
634
  });
635
+ // Trim oldest completed entries, but never evict an in-flight tool —
636
+ // otherwise tool_execution_end's .find(toolCallId) would no-op and leave a
637
+ // permanently-"running" ghost.
638
+ while (progress.recentTools.length > MAX_RECENT_TOOLS) {
639
+ const idx = progress.recentTools.findIndex((t) => t.status !== "running");
640
+ if (idx === -1) break; // only running entries left — don't evict in-flight
641
+ progress.recentTools.splice(idx, 1);
642
+ }
643
+ // ── Cycle detection (parent-side, context-free) ──
644
+ // Signature = toolName + args preview. Two calls with different args
645
+ // (different file, or same file different content) → different sig.
646
+ const sig = `${evt.toolName}:${extractToolArgsPreview((evt.args || {}) as Record<string, unknown>)}`;
647
+ const cycleResult = detectCycle(callHistory, sig);
648
+ callHistory.push(sig);
649
+ if (callHistory.length > 24) callHistory = callHistory.slice(-24);
650
+
651
+ if (cycleResult.cycle) {
652
+ const toolNames = (cycleResult.pattern || []).map((s) => {
653
+ const colonIdx = s.indexOf(":");
654
+ return colonIdx >= 0 ? s.slice(0, colonIdx) : s;
655
+ });
656
+ const patternStr = toolNames.join("→");
657
+ if (!progress.error) {
658
+ progress.error = `Subagent stuck in a tool-call loop: repeating ${patternStr}`;
659
+ }
660
+ proc.kill("SIGTERM");
661
+ clearTimeout(sigkillTimer);
662
+ sigkillTimer = setTimeout(() => {
663
+ if (!childClosed) proc.kill("SIGKILL");
664
+ }, 5000);
665
+ }
562
666
  fireUpdate();
563
667
  }
564
668
 
@@ -593,6 +697,8 @@ async function runSubagent(
593
697
  hit.children = finalChildren as AgentResult[];
594
698
  }
595
699
  }
700
+ inFlightToolCount = Math.max(0, inFlightToolCount - 1);
701
+ resumeIdle();
596
702
  fireUpdate();
597
703
  }
598
704
 
@@ -659,26 +765,37 @@ async function runSubagent(
659
765
  });
660
766
 
661
767
  proc.stderr.on("data", (d: Buffer) => {
768
+ if (stderrBuf.length >= MAX_STDERR_BYTES) return;
662
769
  stderrBuf += d.toString();
770
+ if (stderrBuf.length >= MAX_STDERR_BYTES) {
771
+ stderrBuf = stderrBuf.slice(0, MAX_STDERR_BYTES) + "\n[stderr truncated]";
772
+ }
663
773
  });
664
774
 
665
775
  proc.on("close", (code) => {
776
+ childClosed = true;
666
777
  if (buf.trim()) processLine(buf);
667
778
  if (code !== 0 && stderrBuf.trim() && !progress.error) {
668
779
  progress.error = stderrBuf.trim();
780
+ } else if (code === 0 && stderrBuf.trim()) {
781
+ // Non-fatal: surface stderr (e.g. deprecation warnings) on a successful exit.
782
+ progress.warning = stderrBuf.trim().slice(0, 2000);
669
783
  }
670
- resolve(code ?? 1);
784
+ safeResolve(code ?? 1);
671
785
  });
672
786
 
673
- proc.on("error", () => resolve(1));
787
+ proc.on("error", () => safeResolve(1));
674
788
 
789
+ const abortKill = () => {
790
+ proc.kill("SIGTERM");
791
+ clearTimeout(sigkillTimer);
792
+ sigkillTimer = setTimeout(() => {
793
+ if (!childClosed) proc.kill("SIGKILL");
794
+ }, 3000);
795
+ };
675
796
  if (signal) {
676
- const kill = () => {
677
- proc.kill("SIGTERM");
678
- setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
679
- };
680
- if (signal.aborted) kill();
681
- else signal.addEventListener("abort", kill, { once: true });
797
+ if (signal.aborted) abortKill();
798
+ else signal.addEventListener("abort", abortKill, { once: true });
682
799
  }
683
800
  });
684
801
 
@@ -690,6 +807,13 @@ async function runSubagent(
690
807
  result.exitCode = exitCode;
691
808
  progress.status = exitCode === 0 && !progress.error ? "completed" : "failed";
692
809
  progress.durationMs = Date.now() - startTime;
810
+
811
+ // Push the terminal status to the live renderer so the TUI doesn't keep
812
+ // showing "running" after the child has exited. Pass exitCode so callers
813
+ // that hold a live result object (the subagent tool) can sync its exitCode
814
+ // and render the correct ✓/✗ icon instead of the -1 placeholder.
815
+ onUpdate?.(progress, result.usage, exitCode);
816
+
693
817
  if (progress.error) result.output = result.output || `Error: ${progress.error}`;
694
818
 
695
819
  // Truncate output if very large
@@ -900,6 +1024,10 @@ function renderAgentProgress(
900
1024
  addLine(theme.fg("error", `Error: ${prog.error}`));
901
1025
  }
902
1026
 
1027
+ if (prog.warning) {
1028
+ addLine(theme.fg("warning", `Warning: ${prog.warning}`));
1029
+ }
1030
+
903
1031
  return c;
904
1032
  }
905
1033
 
@@ -959,10 +1087,13 @@ async function runPipeline(
959
1087
  totalUsage = accumulateUsage(totalUsage, result.usage);
960
1088
  previousOutput = result.output;
961
1089
 
962
- // Stop on error
1090
+ // Stop on error — surface the failing step's error as finalOutput (the pipeline
1091
+ // tool returns finalOutput as content, so the main LLM sees the actual failure,
1092
+ // not the previous step's success text).
963
1093
  if (result.exitCode !== 0 || result.progress.error) {
1094
+ const errorDetail = buildPipelineErrorContent(i, step.agent, result);
964
1095
  return {
965
- steps: results, finalOutput: previousOutput,
1096
+ steps: results, finalOutput: errorDetail,
966
1097
  stoppedAt: i, error: result.progress.error || `Agent ${step.agent} exited with code ${result.exitCode}`,
967
1098
  totalUsage, totalDurationMs: Date.now() - startTime,
968
1099
  };
@@ -1062,8 +1193,9 @@ async function runLoop(
1062
1193
 
1063
1194
  if (result.exitCode !== 0 || result.progress.error) {
1064
1195
  stoppedBecause = "error";
1196
+ const errorDetail = buildLoopErrorContent(i, agentName, result);
1065
1197
  return {
1066
- iterations, finalOutput: result.output || "(error)",
1198
+ iterations, finalOutput: errorDetail,
1067
1199
  stoppedBecause, totalUsage, totalDurationMs: Date.now() - startTime,
1068
1200
  };
1069
1201
  }
@@ -1239,8 +1371,8 @@ function renderLoopResult(
1239
1371
  // ── Extension ─────────────────────────────────────────────────────────
1240
1372
 
1241
1373
  export default function (pi: ExtensionAPI) {
1242
- const config = loadConfig();
1243
- semaphore = new Semaphore(config.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
1374
+ extensionConfig = loadConfig();
1375
+ semaphore = new Semaphore(extensionConfig.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
1244
1376
  agents = loadAgents();
1245
1377
 
1246
1378
  // If spawned as a child by a parent subagent process, PI_SUBAGENT_ALLOWED
@@ -1262,6 +1394,7 @@ export default function (pi: ExtensionAPI) {
1262
1394
  "Use subagent to delegate *reasoning and decisions*: codebase exploration (scout), web research (researcher), or isolated code changes (worker)",
1263
1395
  "For multiple independent subagent tasks, emit multiple `subagent` tool calls in the same turn — they run in parallel automatically.",
1264
1396
  "Subagents have NO context from the current conversation — include ALL necessary context in the task description",
1397
+ "When a subagent returns an error, read it carefully. For transient failures (timeout, API/network), retry once with the same task plus 'Previous attempt failed with: {error}'. For structural failures (wrong approach, missing context), simplify the task or switch agents. If it persists after retry, report to the user with the specific error.",
1265
1398
  ],
1266
1399
  parameters: Type.Object({
1267
1400
  agent: Type.String({ description: "Name of the agent to invoke" }),
@@ -1296,9 +1429,10 @@ export default function (pi: ExtensionAPI) {
1296
1429
  };
1297
1430
 
1298
1431
  const result = await semaphore.run(() =>
1299
- runSubagent(agent, params.task!, params.cwd ?? cwd, signal, (progress, usage) => {
1432
+ runSubagent(agent, params.task!, params.cwd ?? cwd, signal, (progress, usage, finalExitCode) => {
1300
1433
  liveResult.progress = progress;
1301
1434
  liveResult.usage = { ...usage };
1435
+ if (finalExitCode !== undefined) liveResult.exitCode = finalExitCode;
1302
1436
  onUpdate?.({
1303
1437
  content: [{ type: "text", text: "(running...)" }],
1304
1438
  details: { results: [liveResult] },
@@ -1308,8 +1442,11 @@ export default function (pi: ExtensionAPI) {
1308
1442
 
1309
1443
  result.contextWindow = contextWindow;
1310
1444
  const isError = result.exitCode !== 0 || !!result.progress.error;
1445
+ const contentText = isError
1446
+ ? buildSubagentErrorContent(result)
1447
+ : (result.output || "(no output)");
1311
1448
  return {
1312
- content: [{ type: "text", text: result.output || "(no output)" }],
1449
+ content: [{ type: "text", text: contentText }],
1313
1450
  details: { results: [result] },
1314
1451
  ...(isError ? { isError: true } : {}),
1315
1452
  };
@@ -1403,7 +1540,8 @@ export default function (pi: ExtensionAPI) {
1403
1540
  promptGuidelines: [
1404
1541
  "Use pipeline when a task naturally decomposes into sequential agent roles (e.g. explore → plan → implement → review).",
1405
1542
  "Each step receives the previous step's output automatically via {previous} placeholder substitution.",
1406
- "Pipelines stop on first error. The finalOutput is the last successful step's output.",
1543
+ "Pipelines stop on first error. The finalOutput is the failing step's error detail.",
1544
+ "When a pipeline fails at a step, the error identifies which step and why. Retry the failing step with a simpler task, or re-scope the pipeline. Early-step (exploration) failures → retry the whole pipeline with a more focused scope.",
1407
1545
  ],
1408
1546
  parameters: Type.Object({
1409
1547
  steps: Type.Array(
@@ -1535,6 +1673,7 @@ export default function (pi: ExtensionAPI) {
1535
1673
  "Use loop for tasks that benefit from iterative refinement (e.g. drafting → reviewing → polishing).",
1536
1674
  "Configure a judge agent to stop early when quality is sufficient, avoiding wasted iterations.",
1537
1675
  "Each iteration receives all prior outputs as context, enabling progressive improvement.",
1676
+ "When a loop iteration fails, the error shows which iteration. Reduce max_iterations or simplify the task; if the judge consistently rejects, refine the criteria or switch judge agent.",
1538
1677
  ],
1539
1678
  parameters: Type.Object({
1540
1679
  agent: Type.String({ description: "Agent name to run in the loop" }),
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Error-message helpers for subagent tool returns.
3
+ *
4
+ * pi-agent-core drops the `isError` flag on normal tool returns, so `content`
5
+ * is the only reliable channel to communicate failure context back to the main
6
+ * LLM. These helpers build clear, actionable messages so the LLM can retry,
7
+ * redirect, or report.
8
+ */
9
+
10
+ /** Minimal shape the helper needs from a subagent result. */
11
+ interface SubagentErrorResult {
12
+ agent: string;
13
+ exitCode: number;
14
+ output: string;
15
+ progress: {
16
+ error?: string;
17
+ lastMessage?: string;
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Build a clear, actionable error message for the main LLM when a single
23
+ * subagent fails. The `content` channel is the only reliable one (pi-agent-core
24
+ * drops a returned `isError` flag on normal tool returns), so this must carry
25
+ * enough context for the LLM to retry, redirect, or report.
26
+ */
27
+ export function buildSubagentErrorContent(result: SubagentErrorResult): string {
28
+ const parts: string[] = [];
29
+ parts.push(`[Subagent error] Agent "${result.agent}" failed (exit code ${result.exitCode}).`);
30
+ if (result.progress.error) {
31
+ parts.push(`Error: ${result.progress.error}`);
32
+ }
33
+ if (result.progress.lastMessage) {
34
+ parts.push(`Last message: ${result.progress.lastMessage}`);
35
+ }
36
+ if (result.output && result.output !== "(no output)") {
37
+ parts.push(`Output:\n${result.output}`);
38
+ }
39
+ return parts.join("\n");
40
+ }
41
+
42
+ export function buildPipelineErrorContent(
43
+ stepIndex: number,
44
+ stepAgent: string,
45
+ result: { exitCode: number; output: string; progress: { error?: string } },
46
+ ): string {
47
+ return [
48
+ `Pipeline failed at step ${stepIndex + 1} (agent: ${stepAgent}).`,
49
+ `Exit code: ${result.exitCode}`,
50
+ result.progress.error ? `Error: ${result.progress.error}` : null,
51
+ result.output ? `Output:\n${result.output}` : "(no output)",
52
+ ].filter(Boolean).join("\n");
53
+ }
54
+
55
+ export function buildLoopErrorContent(
56
+ iteration: number,
57
+ agentName: string,
58
+ result: { exitCode: number; output: string; progress: { error?: string } },
59
+ ): string {
60
+ return [
61
+ `Loop failed at iteration ${iteration + 1} (agent: ${agentName}).`,
62
+ `Exit code: ${result.exitCode}`,
63
+ result.progress.error ? `Error: ${result.progress.error}` : null,
64
+ result.output ? `Output:\n${result.output}` : "(no output)",
65
+ ].filter(Boolean).join("\n");
66
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Cycle detector for tool-call streams. Watches a sliding window of
3
+ * tool-call signatures for a sub-sequence of length P repeated 3 times
4
+ * (P in [2,8]). Catches an agent stuck in a tool-call loop without
5
+ * imposing any count cap on legitimate work. Purely parent-side.
6
+ */
7
+ const WINDOW_SIZE = 24;
8
+ const MIN_PATTERN_LEN = 2;
9
+ const MAX_PATTERN_LEN = 8;
10
+ const REPETITIONS = 3;
11
+
12
+ export interface CycleResult {
13
+ cycle: boolean;
14
+ pattern?: string[]; // the repeating sub-sequence of signatures (length P)
15
+ }
16
+
17
+ /**
18
+ * Detect whether `newSig` completes a 3× cycle. Pure: does NOT mutate `history`.
19
+ * The caller pushes `newSig` onto its own history AFTER calling this.
20
+ */
21
+ export function detectCycle(history: string[], newSig: string): CycleResult {
22
+ const window = [...history, newSig];
23
+ const start = Math.max(0, window.length - WINDOW_SIZE);
24
+ const w = window.slice(start);
25
+ for (let p = MIN_PATTERN_LEN; p <= MAX_PATTERN_LEN; p++) {
26
+ const needed = p * REPETITIONS;
27
+ if (w.length < needed) continue;
28
+ const tail = w.slice(-needed);
29
+ const pattern = tail.slice(0, p);
30
+ let isCycle = true;
31
+ for (let i = 0; i < p; i++) {
32
+ if (pattern[i] !== tail[p + i] || pattern[i] !== tail[2 * p + i]) {
33
+ isCycle = false;
34
+ break;
35
+ }
36
+ }
37
+ if (isCycle) return { cycle: true, pattern };
38
+ }
39
+ return { cycle: false };
40
+ }
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
1
  {
2
- "name": "@rohaquinlop/pi-subagents",
3
- "version": "0.4.0",
4
- "description": "Pi extension for delegating tasks to subagents — parallel execution, agent discovery, and TUI rendering",
5
- "keywords": [
6
- "pi-package",
7
- "subagent",
8
- "parallel",
9
- "coding-agent",
10
- "extension"
11
- ],
12
- "license": "MIT",
13
- "author": "rohaquinlop",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/rohaquinlop/pi-subagents.git"
17
- },
18
- "files": [
19
- "index.ts",
20
- "agents/",
21
- "tools/",
22
- "lib/",
23
- "README.md"
24
- ],
25
- "pi": {
26
- "extensions": [
27
- "./index.ts"
28
- ]
29
- },
30
- "peerDependencies": {
31
- "@earendil-works/pi-coding-agent": "*",
32
- "@earendil-works/pi-tui": "*",
33
- "@sinclair/typebox": "*"
34
- },
35
- "scripts": {
36
- "test": "vitest run",
37
- "test:watch": "vitest"
38
- },
39
- "devDependencies": {
40
- "vitest": "^3.2.0"
41
- }
2
+ "name": "@rohaquinlop/pi-subagents",
3
+ "version": "0.5.0",
4
+ "description": "Pi extension for delegating tasks to subagents — parallel execution, agent discovery, and TUI rendering",
5
+ "keywords": [
6
+ "pi-package",
7
+ "subagent",
8
+ "parallel",
9
+ "coding-agent",
10
+ "extension"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "rohaquinlop",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/rohaquinlop/pi-subagents.git"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "agents/",
21
+ "tools/",
22
+ "lib/",
23
+ "README.md"
24
+ ],
25
+ "pi": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ },
30
+ "peerDependencies": {
31
+ "@earendil-works/pi-coding-agent": "*",
32
+ "@earendil-works/pi-tui": "*",
33
+ "@sinclair/typebox": "*"
34
+ },
35
+ "scripts": {
36
+ "test": "vitest run",
37
+ "test:watch": "vitest"
38
+ },
39
+ "devDependencies": {
40
+ "vitest": "^3.2.0"
41
+ }
42
42
  }
@@ -6,6 +6,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { createBashTool } from "@earendil-works/pi-coding-agent";
7
7
  import { Type } from "@sinclair/typebox";
8
8
 
9
+ const DEFAULT_SAFE_BASH_TIMEOUT_S = 300; // 5 minutes — kills stuck commands instead of relying on the 10-min wall-clock
10
+
9
11
  const DANGEROUS_PATTERNS = [
10
12
  /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
11
13
  /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
@@ -46,7 +48,7 @@ export default function (pi: ExtensionAPI) {
46
48
  parameters: Type.Object({
47
49
  command: Type.String({ description: "Bash command to execute" }),
48
50
  timeout: Type.Optional(
49
- Type.Number({ description: "Timeout in seconds (optional)" }),
51
+ Type.Number({ description: "Timeout in seconds (default: 300s/5min; pass a larger value for builds/tests/installs)" }),
50
52
  ),
51
53
  }),
52
54
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -54,7 +56,8 @@ export default function (pi: ExtensionAPI) {
54
56
  if (danger) {
55
57
  throw new Error(danger);
56
58
  }
57
- return bashTool.execute(toolCallId, params, signal, onUpdate);
59
+ const timeout = params.timeout ?? DEFAULT_SAFE_BASH_TIMEOUT_S;
60
+ return bashTool.execute(toolCallId, { ...params, timeout }, signal, onUpdate);
58
61
  },
59
62
  });
60
63
  }