Files
pi-subagents/docs/superpowers/plans/2026-04-12-subagent-live-progress.md

8.3 KiB

Subagent Live Progress Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [x]) syntax for tracking.

Goal: Make running subagents show assistant intent text or humanized tool activity instead of raw tool_call / tool_result labels.

Architecture: Add one shared event-to-status formatter in src/ so parent live updates and wrapper transcript lines use the same wording. Keep wrapper protocol, runner behavior, artifact layout, and result.json unchanged; only change formatting and the tiny call sites that consume it.

Tech Stack: TypeScript, Node ESM .mjs, Node test runner via tsx --test


File structure

  • Create: src/progress.mjs — shared, stateful formatter for normalized child events
  • Modify: src/tool.ts — use formatter for parent onUpdate progress messages
  • Modify: src/wrapper/render.mjs — use same formatter for transcript lines
  • Modify: src/wrapper/cli.mjs — skip transcript/log output when formatter returns no line for blank assistant text
  • Modify: src/tool.test.ts — regressions for parent live progress
  • Modify: src/wrapper/render.test.ts — regressions for transcript rendering

Task 1: Shared formatter + transcript rendering

Files:

  • Create: src/progress.mjs

  • Modify: src/wrapper/render.mjs

  • Modify: src/wrapper/cli.mjs

  • Test: src/wrapper/render.test.ts

  • Step 1: Write the failing transcript-render tests

Add tests to src/wrapper/render.test.ts for three behaviors:

import { createEventLineRenderer, renderHeader, renderEventLine } from "./render.mjs";

test("renderEventLine humanizes read and result lines", () => {
  const render = createEventLineRenderer();
  assert.equal(render({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } }), "Reading src/auth.ts");
  assert.equal(render({ type: "tool_result", toolName: "read", isError: false }), "Finished reading src/auth.ts");
});

test("renderEventLine prefers assistant text and drops blank assistant text", () => {
  const render = createEventLineRenderer();
  assert.equal(render({ type: "assistant_text", text: "Inspecting auth flow" }), "Inspecting auth flow");
  assert.equal(render({ type: "assistant_text", text: "   " }), null);
});

test("renderEventLine keeps bash commands concise and humanizes grep failures", () => {
  const render = createEventLineRenderer();
  assert.equal(render({ type: "tool_call", toolName: "bash", args: { command: "rg -n authentication src" } }), "$ rg -n authentication src");
  assert.equal(render({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } }), "Searching code for auth");
  assert.equal(render({ type: "tool_result", toolName: "grep", isError: true }), "Search failed");
});
  • Step 2: Run transcript-render tests to verify they fail

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/wrapper/render.test.ts Expected: FAIL because createEventLineRenderer does not exist and current renderer emits JSON-like tool lines / (no assistant text).

  • Step 3: Write the minimal shared formatter and transcript integration

Implement src/progress.mjs with a stateful formatter API and wire src/wrapper/render.mjs to use it:

export function createProgressFormatter() {
  let lastTool = null;

  return {
    format(event) {
      if (event.type === "assistant_text") {
        const text = typeof event.text === "string" ? event.text.trim() : "";
        return text || null;
      }
      if (event.type === "tool_call") {
        lastTool = { toolName: event.toolName, args: event.args ?? {} };
        return formatToolCall(lastTool.toolName, lastTool.args);
      }
      if (event.type === "tool_result") {
        return formatToolResult(event, lastTool);
      }
      return JSON.stringify(event);
    },
  };
}

In src/wrapper/render.mjs, export both:

export function createEventLineRenderer() {
  const formatter = createProgressFormatter();
  return (event) => formatter.format(event);
}

const defaultRenderEventLine = createEventLineRenderer();
export function renderEventLine(event) {
  return defaultRenderEventLine(event);
}

In src/wrapper/cli.mjs, only append/log transcript lines when the rendered value is not null.

  • Step 4: Run transcript-render tests to verify they pass

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/wrapper/render.test.ts Expected: PASS.

Task 2: Parent live progress updates

Files:

  • Modify: src/tool.ts

  • Test: src/tool.test.ts

  • Step 1: Write the failing parent-progress tests

Add tests to src/tool.test.ts for assistant-text priority and humanized fallback:

test("single-mode subagent prefers assistant text for live progress", async () => {
  const updates: string[] = [];
  const tool = createSubagentTool({
    runSingleTask: async ({ onEvent, meta }: any) => {
      onEvent?.({ type: "assistant_text", text: "Inspecting auth flow" });
      onEvent?.({ type: "tool_call", toolName: "read", args: { path: "src/auth.ts" } });
      onEvent?.({ type: "tool_result", toolName: "read", isError: false });
      return { runId: "run-1", task: meta.task, exitCode: 0, finalText: "done" };
    },
  } as any);

  await tool.execute(/* existing ctx setup */, (partial: any) => {
    const first = partial.content?.[0];
    if (first?.type === "text") updates.push(first.text);
  }, /* existing ctx */);

  assert.deepEqual(updates.slice(0, 3), [
    "Inspecting auth flow",
    "Reading src/auth.ts",
    "Finished reading src/auth.ts",
  ]);
  assert.doesNotMatch(updates.join("\n"), /tool_call|tool_result/);
});

test("single-mode subagent ignores blank assistant text and falls back to tool text", async () => {
  const updates: string[] = [];
  const tool = createSubagentTool({
    runSingleTask: async ({ onEvent, meta }: any) => {
      onEvent?.({ type: "assistant_text", text: "   " });
      onEvent?.({ type: "tool_call", toolName: "grep", args: { pattern: "auth" } });
      return { runId: "run-1", task: meta.task, exitCode: 0, finalText: "done" };
    },
  } as any);

  await tool.execute(/* existing ctx setup */, (partial: any) => {
    const first = partial.content?.[0];
    if (first?.type === "text") updates.push(first.text);
  }, /* existing ctx */);

  assert.deepEqual(updates, ["Searching code for auth"]);
});
  • Step 2: Run parent-progress tests to verify they fail

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts Expected: FAIL because src/tool.ts still emits Running subagent: <event.type>.

  • Step 3: Write the minimal parent integration

Update src/tool.ts to create one formatter per child run and emit only formatted lines:

import { createProgressFormatter } from "./progress.mjs";

const formatter = createProgressFormatter();

onEvent(event) {
  const text = formatter.format(event);
  if (!text) return;
  onUpdate?.({
    content: [{ type: "text", text }],
    details: makeDetails(input.mode, []),
  });
}

Keep parallel/chain completion summaries and all result handling unchanged.

  • Step 4: Run parent-progress tests to verify they pass

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts Expected: PASS.

Task 3: Verification

Files:

  • Verify only

  • Step 1: Run touched regression tests together

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npx tsx --test src/tool.test.ts src/wrapper/render.test.ts src/wrapper/cli.test.ts Expected: PASS.

  • Step 2: Run full package test suite

Run: cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress && npm test Expected: PASS. If it fails, stop and investigate before claiming completion.

  • Step 3: Commit implementation
cd /home/dev/pi-packages/pi-subagents/.worktree/subagent-live-progress
git add src/progress.mjs src/tool.ts src/tool.test.ts src/wrapper/render.mjs src/wrapper/render.test.ts src/wrapper/cli.mjs docs/superpowers/plans/2026-04-12-subagent-live-progress.md
git commit -m "feat(progress): humanize subagent live updates"