@leing2021/super-pi 0.23.8 → 0.23.10

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,453 @@
1
+ /**
2
+ * TUI renderers for ce_subagent and ce_parallel_subagent.
3
+ *
4
+ * Displays real-time status in pi tool frames:
5
+ * - Collapsed: status icon + agent name + recent tool calls + usage
6
+ * - Expanded (Ctrl+O): full tool calls + final output + per-task usage
7
+ *
8
+ * Tool call formatting (formatToolCall) adapted directly from pi official
9
+ * subagent example with super-pi skill semantic adaptations.
10
+ */
11
+
12
+ import * as os from "node:os"
13
+ import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"
14
+ import {
15
+ type SingleResult,
16
+ getFinalOutput,
17
+ getDisplayItems,
18
+ isFailedResult,
19
+ formatUsageStats,
20
+ type DisplayItem,
21
+ } from "./subagent-events"
22
+ import type { SubagentLiveDetails } from "./subagent"
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const COLLAPSED_ITEM_COUNT = 10
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Tool call formatting (from pi official subagent example)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export function formatToolCall(
35
+ toolName: string,
36
+ args: Record<string, unknown>,
37
+ themeFg: (color: any, text: string) => string,
38
+ ): string {
39
+ const shortenPath = (p: string) => {
40
+ const home = os.homedir()
41
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p
42
+ }
43
+
44
+ switch (toolName) {
45
+ case "bash": {
46
+ const command = (args.command as string) || "..."
47
+ const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command
48
+ return themeFg("muted", "$ ") + themeFg("toolOutput", preview)
49
+ }
50
+ case "read": {
51
+ const rawPath = (args.file_path || args.path || "...") as string
52
+ const filePath = shortenPath(rawPath)
53
+ const offset = args.offset as number | undefined
54
+ const limit = args.limit as number | undefined
55
+ let text = themeFg("accent", filePath)
56
+ if (offset !== undefined || limit !== undefined) {
57
+ const startLine = offset ?? 1
58
+ const endLine = limit !== undefined ? startLine + limit - 1 : ""
59
+ text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`)
60
+ }
61
+ return themeFg("muted", "read ") + text
62
+ }
63
+ case "write": {
64
+ const rawPath = (args.file_path || args.path || "...") as string
65
+ const filePath = shortenPath(rawPath)
66
+ const content = (args.content || "") as string
67
+ const lines = content.split("\n").length
68
+ let text = themeFg("muted", "write ") + themeFg("accent", filePath)
69
+ if (lines > 1) text += themeFg("dim", ` (${lines} lines)`)
70
+ return text
71
+ }
72
+ case "edit": {
73
+ const rawPath = (args.file_path || args.path || "...") as string
74
+ return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath))
75
+ }
76
+ case "ls": {
77
+ const rawPath = (args.path || ".") as string
78
+ return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath))
79
+ }
80
+ case "find": {
81
+ const pattern = (args.pattern || "*") as string
82
+ const rawPath = (args.path || ".") as string
83
+ return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`)
84
+ }
85
+ case "grep": {
86
+ const pattern = (args.pattern || "") as string
87
+ const rawPath = (args.path || ".") as string
88
+ return (
89
+ themeFg("muted", "grep ") +
90
+ themeFg("accent", `/${pattern}/`) +
91
+ themeFg("dim", ` in ${shortenPath(rawPath)}`)
92
+ )
93
+ }
94
+ default: {
95
+ const argsStr = JSON.stringify(args)
96
+ const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr
97
+ return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`)
98
+ }
99
+ }
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Theme type
104
+ // ---------------------------------------------------------------------------
105
+
106
+ interface Theme {
107
+ fg: (color: any, text: string) => string
108
+ bold: (text: string) => string
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // renderCall
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export function renderSubagentCall(
116
+ args: {
117
+ agent?: string
118
+ task?: string
119
+ chain?: Array<{ agent: string; task: string }>
120
+ tasks?: Array<{ agent: string; task: string }>
121
+ },
122
+ theme: Theme,
123
+ ): any {
124
+ if (args.chain && args.chain.length > 0) {
125
+ let text =
126
+ theme.fg("toolTitle", theme.bold("ce_subagent ")) +
127
+ theme.fg("accent", `chain (${args.chain.length} steps)`)
128
+ for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
129
+ const step = args.chain[i]
130
+ const cleanTask = step.task.replace(/\{previous\}/g, "").trim()
131
+ const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask
132
+ text +=
133
+ "\n " +
134
+ theme.fg("muted", `${i + 1}.`) +
135
+ " " +
136
+ theme.fg("accent", step.agent) +
137
+ theme.fg("dim", ` ${preview}`)
138
+ }
139
+ if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`
140
+ return new Text(text, 0, 0)
141
+ }
142
+
143
+ if (args.tasks && args.tasks.length > 0) {
144
+ let text =
145
+ theme.fg("toolTitle", theme.bold("ce_parallel_subagent ")) +
146
+ theme.fg("accent", `parallel (${args.tasks.length} tasks)`)
147
+ for (const t of args.tasks.slice(0, 3)) {
148
+ const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task
149
+ text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`
150
+ }
151
+ if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`
152
+ return new Text(text, 0, 0)
153
+ }
154
+
155
+ // Single mode
156
+ const agentName = args.agent || "..."
157
+ const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "..."
158
+ let text =
159
+ theme.fg("toolTitle", theme.bold("ce_subagent ")) +
160
+ theme.fg("accent", agentName)
161
+ text += `\n ${theme.fg("dim", preview)}`
162
+ return new Text(text, 0, 0)
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // renderResult
167
+ // ---------------------------------------------------------------------------
168
+
169
+ interface RenderContext {
170
+ expanded: boolean
171
+ }
172
+
173
+ export function renderSubagentResult(
174
+ details: SubagentLiveDetails,
175
+ context: RenderContext,
176
+ theme: Theme,
177
+ ): any {
178
+ if (!details || !details.results || details.results.length === 0) {
179
+ return new Text("(no output)", 0, 0)
180
+ }
181
+
182
+ const mdTheme = getMarkdownTheme()
183
+
184
+ const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
185
+ const toShow = limit ? items.slice(-limit) : items
186
+ const skipped = limit && items.length > limit ? items.length - limit : 0
187
+ let text = ""
188
+ if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`)
189
+ for (const item of toShow) {
190
+ if (item.type === "text") {
191
+ const preview = context.expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n")
192
+ text += `${theme.fg("toolOutput", preview)}\n`
193
+ } else {
194
+ text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`
195
+ }
196
+ }
197
+ return text.trimEnd()
198
+ }
199
+
200
+ // --- Single mode ---
201
+ if (details.mode === "single" && details.results.length === 1) {
202
+ return renderSingleResult(details.results[0], context, theme, mdTheme, renderDisplayItems)
203
+ }
204
+
205
+ // --- Chain mode ---
206
+ if (details.mode === "chain") {
207
+ return renderChainResult(details.results, context, theme, mdTheme, renderDisplayItems)
208
+ }
209
+
210
+ // --- Parallel mode ---
211
+ if (details.mode === "parallel") {
212
+ return renderParallelResult(details.results, context, theme, mdTheme, renderDisplayItems)
213
+ }
214
+
215
+ return new Text("(no output)", 0, 0)
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Single result renderer
220
+ // ---------------------------------------------------------------------------
221
+
222
+ function renderSingleResult(
223
+ r: SingleResult,
224
+ context: RenderContext,
225
+ theme: Theme,
226
+ mdTheme: any,
227
+ renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
228
+ ): any {
229
+ const isError = isFailedResult(r)
230
+ const icon = isError ? theme.fg("error", "✗") : r.exitCode === -1 ? theme.fg("warning", "⏳") : theme.fg("success", "✓")
231
+ const displayItems = getDisplayItems(r.messages)
232
+ const finalOutput = getFinalOutput(r.messages)
233
+
234
+ if (context.expanded) {
235
+ const container = new Container()
236
+ let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
237
+ if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`
238
+ container.addChild(new Text(header, 0, 0))
239
+ if (isError && r.errorMessage)
240
+ container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0))
241
+ container.addChild(new Spacer(1))
242
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0))
243
+ container.addChild(new Text(theme.fg("dim", r.task), 0, 0))
244
+ container.addChild(new Spacer(1))
245
+ container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0))
246
+ if (displayItems.length === 0 && !finalOutput) {
247
+ container.addChild(new Text(theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)"), 0, 0))
248
+ } else {
249
+ for (const item of displayItems) {
250
+ if (item.type === "toolCall")
251
+ container.addChild(
252
+ new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0),
253
+ )
254
+ }
255
+ if (finalOutput) {
256
+ container.addChild(new Spacer(1))
257
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
258
+ }
259
+ }
260
+ const usageStr = formatUsageStats(r.usage, r.model)
261
+ if (usageStr) {
262
+ container.addChild(new Spacer(1))
263
+ container.addChild(new Text(theme.fg("dim", usageStr), 0, 0))
264
+ }
265
+ return container
266
+ }
267
+
268
+ // Collapsed
269
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
270
+ if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`
271
+ if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`
272
+ else if (r.exitCode === -1) text += `\n${theme.fg("muted", "(running...)")}`
273
+ else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
274
+ else {
275
+ text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`
276
+ if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
277
+ }
278
+ const usageStr = formatUsageStats(r.usage, r.model)
279
+ if (usageStr) text += `\n${theme.fg("dim", usageStr)}`
280
+ return new Text(text, 0, 0)
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Chain result renderer
285
+ // ---------------------------------------------------------------------------
286
+
287
+ function aggregateUsage(results: SingleResult[]) {
288
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 }
289
+ for (const r of results) {
290
+ total.input += r.usage.input
291
+ total.output += r.usage.output
292
+ total.cacheRead += r.usage.cacheRead
293
+ total.cacheWrite += r.usage.cacheWrite
294
+ total.cost += r.usage.cost
295
+ total.contextTokens += r.usage.contextTokens
296
+ total.turns += r.usage.turns
297
+ }
298
+ return total
299
+ }
300
+
301
+ function renderChainResult(
302
+ results: SingleResult[],
303
+ context: RenderContext,
304
+ theme: Theme,
305
+ mdTheme: any,
306
+ renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
307
+ ): any {
308
+ const successCount = results.filter(r => r.exitCode === 0).length
309
+ const icon = successCount === results.length ? theme.fg("success", "✓") : theme.fg("error", "✗")
310
+
311
+ if (context.expanded) {
312
+ const container = new Container()
313
+ container.addChild(
314
+ new Text(
315
+ `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`,
316
+ 0, 0,
317
+ ),
318
+ )
319
+ for (const r of results) {
320
+ const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
321
+ const displayItems = getDisplayItems(r.messages)
322
+ const finalOutput = getFinalOutput(r.messages)
323
+
324
+ container.addChild(new Spacer(1))
325
+ container.addChild(new Text(`${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0))
326
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0))
327
+ for (const item of displayItems) {
328
+ if (item.type === "toolCall")
329
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
330
+ }
331
+ if (finalOutput) {
332
+ container.addChild(new Spacer(1))
333
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
334
+ }
335
+ const stepUsage = formatUsageStats(r.usage, r.model)
336
+ if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0))
337
+ }
338
+ const usageStr = formatUsageStats(aggregateUsage(results))
339
+ if (usageStr) {
340
+ container.addChild(new Spacer(1))
341
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
342
+ }
343
+ return container
344
+ }
345
+
346
+ // Collapsed
347
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`
348
+ for (const r of results) {
349
+ const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
350
+ const displayItems = getDisplayItems(r.messages)
351
+ text += `\n\n${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`
352
+ if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
353
+ else text += `\n${renderDisplayItems(displayItems, 5)}`
354
+ }
355
+ const usageStr = formatUsageStats(aggregateUsage(results))
356
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`
357
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
358
+ return new Text(text, 0, 0)
359
+ }
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Parallel result renderer
363
+ // ---------------------------------------------------------------------------
364
+
365
+ function renderParallelResult(
366
+ results: SingleResult[],
367
+ context: RenderContext,
368
+ theme: Theme,
369
+ mdTheme: any,
370
+ renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
371
+ ): any {
372
+ const running = results.filter(r => r.exitCode === -1).length
373
+ const successCount = results.filter(r => r.exitCode !== -1 && !isFailedResult(r)).length
374
+ const failCount = results.filter(r => r.exitCode !== -1 && isFailedResult(r)).length
375
+ const isRunning = running > 0
376
+ const icon = isRunning
377
+ ? theme.fg("warning", "⏳")
378
+ : failCount > 0
379
+ ? theme.fg("warning", "◐")
380
+ : theme.fg("success", "✓")
381
+ const status = isRunning
382
+ ? `${successCount + failCount}/${results.length} done, ${running} running`
383
+ : `${successCount}/${results.length} tasks`
384
+
385
+ if (context.expanded && !isRunning) {
386
+ const container = new Container()
387
+ container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`, 0, 0))
388
+
389
+ for (const r of results) {
390
+ const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓")
391
+ const displayItems = getDisplayItems(r.messages)
392
+ const finalOutput = getFinalOutput(r.messages)
393
+
394
+ container.addChild(new Spacer(1))
395
+ container.addChild(new Text(`${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0))
396
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0))
397
+ for (const item of displayItems) {
398
+ if (item.type === "toolCall")
399
+ container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
400
+ }
401
+ if (finalOutput) {
402
+ container.addChild(new Spacer(1))
403
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
404
+ }
405
+ const taskUsage = formatUsageStats(r.usage, r.model)
406
+ if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0))
407
+ }
408
+
409
+ const usageStr = formatUsageStats(aggregateUsage(results))
410
+ if (usageStr) {
411
+ container.addChild(new Spacer(1))
412
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
413
+ }
414
+ return container
415
+ }
416
+
417
+ // Collapsed (or still running)
418
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`
419
+ for (const r of results) {
420
+ const rIcon =
421
+ r.exitCode === -1
422
+ ? theme.fg("warning", "⏳")
423
+ : isFailedResult(r)
424
+ ? theme.fg("error", "✗")
425
+ : theme.fg("success", "✓")
426
+ const displayItems = getDisplayItems(r.messages)
427
+ text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`
428
+ if (displayItems.length === 0)
429
+ text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`
430
+ else text += `\n${renderDisplayItems(displayItems, 5)}`
431
+ }
432
+ if (!isRunning) {
433
+ const usageStr = formatUsageStats(aggregateUsage(results))
434
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`
435
+ }
436
+ if (!context.expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
437
+ return new Text(text, 0, 0)
438
+ }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // Markdown theme helper
442
+ // ---------------------------------------------------------------------------
443
+
444
+ function getMarkdownTheme(): any {
445
+ // Return a simple theme for testing / default usage
446
+ // In production, pi provides getMarkdownTheme()
447
+ try {
448
+ const { getMarkdownTheme: gmt } = require("@mariozechner/pi-coding-agent")
449
+ return gmt()
450
+ } catch {
451
+ return undefined
452
+ }
453
+ }
@@ -4,12 +4,22 @@
4
4
  * Features:
5
5
  * - Recursion depth guard (PI_SUBAGENT_DEPTH / PI_SUBAGENT_MAX_DEPTH)
6
6
  * - Optional context slimming via --no-skills flag
7
+ * - Live TUI updates via onUpdate callback + SingleResult details
7
8
  */
8
9
 
9
10
  import {
10
11
  checkSubagentDepth,
11
12
  getChildDepthEnv,
12
13
  } from "./subagent-depth-guard"
14
+ import {
15
+ type SingleResult,
16
+ getFinalOutput,
17
+ invokeRunner,
18
+ } from "./subagent-events"
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public types
22
+ // ---------------------------------------------------------------------------
13
23
 
14
24
  export interface SubagentTask {
15
25
  agent: string
@@ -24,22 +34,47 @@ export interface SubagentInput {
24
34
  inheritSkills?: boolean
25
35
  }
26
36
 
27
- export interface SubagentResult {
37
+ export interface SubagentLiveDetails {
28
38
  mode: "single" | "chain"
29
- outputs: string[]
39
+ results: SingleResult[]
30
40
  }
31
41
 
42
+ /** @deprecated Use SubagentLiveRunner instead */
43
+ export type SubagentRunner = (
44
+ prompt: string,
45
+ options?: SubagentExecOptions,
46
+ ) => Promise<string>
47
+
32
48
  export interface SubagentExecOptions {
33
49
  /** Extra CLI flags to pass to the child pi process */
34
50
  extraFlags?: string[]
35
51
  /** Environment variables to inject into the child process */
36
52
  extraEnv?: Record<string, string>
53
+ /** Callback for live TUI partial updates */
54
+ onUpdate?: (partial: SingleResult) => void
37
55
  }
38
56
 
39
- export type SubagentRunner = (
57
+ /**
58
+ * Live runner that returns a structured SingleResult and supports
59
+ * onUpdate for partial progress.
60
+ */
61
+ export type SubagentLiveRunner = (
40
62
  prompt: string,
41
- options?: SubagentExecOptions,
42
- ) => Promise<string>
63
+ options?: SubagentLiveExecOptions,
64
+ ) => Promise<SingleResult>
65
+
66
+ export type SubagentLiveExecOptions = SubagentExecOptions
67
+
68
+ export interface SubagentLiveResult {
69
+ mode: "single" | "chain"
70
+ results: SingleResult[]
71
+ /** @deprecated Backward compat: final text output per step */
72
+ outputs?: string[]
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Pipeline stage guard
77
+ // ---------------------------------------------------------------------------
43
78
 
44
79
  const PIPELINE_STAGE_SKILLS = new Set([
45
80
  "01-brainstorm",
@@ -58,10 +93,26 @@ export function assertNonPipelineStageSkill(agent: string, toolName: string): vo
58
93
  }
59
94
  }
60
95
 
96
+ // ---------------------------------------------------------------------------
97
+ // Tool factory
98
+ // ---------------------------------------------------------------------------
99
+
61
100
  export function createSubagentTool() {
62
101
  return {
63
- name: "ce_subagent",
64
- async execute(input: SubagentInput, runner: SubagentRunner): Promise<SubagentResult> {
102
+ name: "ce_subagent" as const,
103
+
104
+ /**
105
+ * Execute the subagent tool.
106
+ *
107
+ * Supports two runner signatures:
108
+ * - SubagentLiveRunner (preferred): returns SingleResult, supports onUpdate
109
+ * - SubagentRunner (legacy): returns string, no onUpdate
110
+ */
111
+ async execute(
112
+ input: SubagentInput,
113
+ runner: SubagentLiveRunner | SubagentRunner,
114
+ toolCtx?: { onUpdate?: (details: SubagentLiveDetails) => void },
115
+ ): Promise<SubagentLiveResult> {
65
116
  // Recursion depth guard
66
117
  const depthCheck = checkSubagentDepth()
67
118
  if (!depthCheck.allowed) {
@@ -80,46 +131,71 @@ export function createSubagentTool() {
80
131
  if (hasSingle) {
81
132
  assertNonPipelineStageSkill(input.agent!, "ce_subagent")
82
133
  const prompt = buildPrompt(input.agent!, input.task!)
83
- const output = await runner(prompt, execOptions)
134
+
135
+ const liveOptions: SubagentLiveExecOptions = {
136
+ ...execOptions,
137
+ onUpdate: (partial: SingleResult) => {
138
+ if (toolCtx?.onUpdate) {
139
+ toolCtx.onUpdate({ mode: "single", results: [partial] })
140
+ }
141
+ },
142
+ }
143
+
144
+ const result = await invokeRunner(runner, prompt, liveOptions)
145
+ result.agent = input.agent!
146
+ result.task = input.task!
84
147
  return {
85
148
  mode: "single",
86
- outputs: [output],
149
+ results: [result],
150
+ outputs: [getFinalOutput(result.messages)],
87
151
  }
88
152
  }
89
153
 
90
- const outputs: string[] = []
154
+ // Chain mode
155
+ const results: SingleResult[] = []
91
156
  let previous = ""
92
157
 
93
158
  for (const task of input.chain ?? []) {
94
159
  assertNonPipelineStageSkill(task.agent, "ce_subagent")
95
160
  const prompt = buildPrompt(task.agent, task.task.replace(/\{previous\}/g, previous))
96
- const output = await runner(prompt, execOptions)
97
- outputs.push(output)
98
- previous = output
161
+
162
+ const liveOptions: SubagentLiveExecOptions = {
163
+ ...execOptions,
164
+ onUpdate: (partial: SingleResult) => {
165
+ if (toolCtx?.onUpdate) {
166
+ toolCtx.onUpdate({ mode: "chain", results: [...results, partial] })
167
+ }
168
+ },
169
+ }
170
+
171
+ const result = await invokeRunner(runner, prompt, liveOptions)
172
+ result.agent = task.agent
173
+ result.task = task.task
174
+ results.push(result)
175
+ previous = getFinalOutput(result.messages)
99
176
  }
100
177
 
101
178
  return {
102
179
  mode: "chain",
103
- outputs,
180
+ results,
181
+ outputs: results.map(r => getFinalOutput(r.messages)),
104
182
  }
105
183
  },
106
184
  }
107
185
  }
108
186
 
109
- /**
110
- * Build exec options based on subagent configuration.
111
- * Controls context inheritance and recursion depth.
112
- */
113
- function buildExecOptions(inheritSkills?: boolean): SubagentExecOptions {
187
+ // ---------------------------------------------------------------------------
188
+ // Helpers
189
+ // ---------------------------------------------------------------------------
190
+
191
+ function buildExecOptions(inheritSkills?: boolean): { extraFlags?: string[]; extraEnv: Record<string, string> } {
114
192
  const flags: string[] = []
115
193
  const env: Record<string, string> = {}
116
194
 
117
- // Context slimming: skip skills inheritance when explicitly disabled
118
195
  if (inheritSkills === false) {
119
196
  flags.push("--no-skills")
120
197
  }
121
198
 
122
- // Recursion depth: pass incremented depth to child process
123
199
  Object.assign(env, getChildDepthEnv())
124
200
 
125
201
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leing2021/super-pi",
3
- "version": "0.23.8",
3
+ "version": "0.23.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Pi-native Compound Engineering package for iterative development workflows",
@@ -58,6 +58,17 @@ Anti-rationalization — when a gate fails or evidence is missing:
58
58
 
59
59
  This is a hard gate — do not push past a failing test or broken build to continue implementation. Errors compound.
60
60
 
61
+ ## Error compaction after recovery
62
+
63
+ After a stop-the-line failure is diagnosed, fixed, and verified:
64
+
65
+ 1. Replace full traces in handoff/context with `ERROR(resolved): <root cause>`
66
+ 2. Keep only the final repro, root cause, fix summary, and verification result
67
+ 3. Remove intermediate debug output and failed exploratory runs that are no longer relevant
68
+ 4. Update `session_checkpoint` with the compacted state only
69
+
70
+ If the same tool, command, or implementation unit fails 3 consecutive times, stop retrying and ask the user for direction with a concise evidence summary.
71
+
61
72
  ## Workflow
62
73
 
63
74
  1. **Load context**: consume latest handoff before any broad file reads — `context_handoff load` or read `.context/compound-engineering/handoffs/latest.md`. If found, use `activeFiles`, `blocker`, `verification`, `activeRules` as starting point. If not found, proceed normally.
@@ -29,6 +29,15 @@ Before reading any project files or running repository-wide scans, load the most
29
29
 
30
30
  Core principle: **consume handoff before broad project file reads** — a single handoff read (~500 tokens) avoids 5-10 project file scans (~5K-10K tokens).
31
31
 
32
+ ## Context hygiene rules
33
+
34
+ Applies to all Phase 1 skills when preparing context or saving handoff.
35
+
36
+ 1. **Compact resolved errors** — Once an error is diagnosed, fixed, and verified, do not carry the full trace forward. Replace it with `ERROR(resolved): <root cause>` and keep repro/verification only if still relevant.
37
+ 2. **Fetch obvious prerequisites** — If the next step has an obvious deterministic prerequisite, fetch it before reasoning further instead of spending an LLM round trip asking for it.
38
+ 3. **Cap repeated failures** — After 3 consecutive failures on the same tool, command, or implementation unit, stop retrying. Summarize evidence and ask the user for direction.
39
+ 4. **Prune before handoff** — Before saving handoff, keep only what the next stage needs. Move broad history to artifact paths; remove intermediate debug output that is no longer relevant.
40
+
32
41
  ## End of skill: save handoff + status + context
33
42
 
34
43
  Every Phase 1 skill (02-plan through 05-learn) must save context handoff at completion: