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

217 lines
8.3 KiB
Markdown

# 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: <event.type>`.
- [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"
```