@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.
- package/README.md +1 -0
- package/extensions/ce-core/index.ts +77 -38
- package/extensions/ce-core/tools/parallel-subagent.ts +126 -36
- package/extensions/ce-core/tools/subagent-events.ts +287 -0
- package/extensions/ce-core/tools/subagent-json-runner.ts +175 -0
- package/extensions/ce-core/tools/subagent-renderer.ts +453 -0
- package/extensions/ce-core/tools/subagent.ts +97 -21
- package/package.json +1 -1
- package/skills/03-work/SKILL.md +11 -0
- package/skills/references/pipeline-config.md +9 -0
|
@@ -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
|
|
37
|
+
export interface SubagentLiveDetails {
|
|
28
38
|
mode: "single" | "chain"
|
|
29
|
-
|
|
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
|
-
|
|
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?:
|
|
42
|
-
) => Promise<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
results: [result],
|
|
150
|
+
outputs: [getFinalOutput(result.messages)],
|
|
87
151
|
}
|
|
88
152
|
}
|
|
89
153
|
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
180
|
+
results,
|
|
181
|
+
outputs: results.map(r => getFinalOutput(r.messages)),
|
|
104
182
|
}
|
|
105
183
|
},
|
|
106
184
|
}
|
|
107
185
|
}
|
|
108
186
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
function buildExecOptions(inheritSkills?: boolean):
|
|
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
package/skills/03-work/SKILL.md
CHANGED
|
@@ -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:
|