# 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` - [x] **Step 1: Write the failing transcript-render tests** Add tests to `src/wrapper/render.test.ts` for three behaviors: ```ts 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"); }); ``` - [x] **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)`. - [x] **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: ```js 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: ```js 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`. - [x] **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` - [x] **Step 1: Write the failing parent-progress tests** Add tests to `src/tool.test.ts` for assistant-text priority and humanized fallback: ```ts 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"]); }); ``` - [x] **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: `. - [x] **Step 3: Write the minimal parent integration** Update `src/tool.ts` to create one formatter per child run and emit only formatted lines: ```ts 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. - [x] **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 - [x] **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. - [x] **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** ```bash 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" ```