pi-superteam 0.1.0
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/CHANGELOG.md +37 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/agents/architect.md +45 -0
- package/agents/implementer.md +40 -0
- package/agents/performance-reviewer.md +51 -0
- package/agents/quality-reviewer.md +53 -0
- package/agents/scout.md +24 -0
- package/agents/security-reviewer.md +52 -0
- package/agents/spec-reviewer.md +46 -0
- package/docs/guides/agents.md +200 -0
- package/docs/guides/configuration.md +164 -0
- package/docs/guides/rules.md +91 -0
- package/docs/guides/sdd-workflow.md +173 -0
- package/docs/guides/tdd-guard.md +144 -0
- package/package.json +53 -0
- package/prompts/implement.md +9 -0
- package/prompts/review-parallel.md +11 -0
- package/prompts/scout.md +8 -0
- package/prompts/sdd.md +9 -0
- package/rules/no-impl-before-spec.md +17 -0
- package/rules/test-first.md +11 -0
- package/rules/yagni.md +11 -0
- package/skills/acceptance-test-driven-development/SKILL.md +60 -0
- package/skills/brainstorming/SKILL.md +49 -0
- package/skills/subagent-driven-development/SKILL.md +86 -0
- package/skills/test-driven-development/SKILL.md +97 -0
- package/skills/writing-plans/SKILL.md +65 -0
- package/src/config.ts +181 -0
- package/src/dispatch.ts +567 -0
- package/src/index.ts +721 -0
- package/src/review-parser.ts +212 -0
- package/src/rules/engine.ts +215 -0
- package/src/workflow/sdd.ts +379 -0
- package/src/workflow/state.ts +422 -0
- package/src/workflow/tdd-guard.ts +516 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superteam — pi extension entry point.
|
|
3
|
+
*
|
|
4
|
+
* Thin composition root: registers tools, commands, shortcuts, events.
|
|
5
|
+
* All business logic lives in dispatch, config, guard, rules, state modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
10
|
+
import { Type } from "@sinclair/typebox";
|
|
11
|
+
import { getConfig, getPackageDir } from "./config.js";
|
|
12
|
+
import {
|
|
13
|
+
type AgentProfile,
|
|
14
|
+
type DispatchDetails,
|
|
15
|
+
type DispatchResult,
|
|
16
|
+
aggregateUsage,
|
|
17
|
+
checkCostBudget,
|
|
18
|
+
discoverAgents,
|
|
19
|
+
dispatchAgent,
|
|
20
|
+
dispatchChain,
|
|
21
|
+
dispatchParallel,
|
|
22
|
+
formatTokens,
|
|
23
|
+
formatUsage,
|
|
24
|
+
getFinalOutput,
|
|
25
|
+
getSessionCost,
|
|
26
|
+
resetSessionCost,
|
|
27
|
+
} from "./dispatch.js";
|
|
28
|
+
import {
|
|
29
|
+
type TddMode,
|
|
30
|
+
buildStatusLines,
|
|
31
|
+
getState,
|
|
32
|
+
initState,
|
|
33
|
+
loadPlanIntoState,
|
|
34
|
+
restoreFromBranch,
|
|
35
|
+
setTddMode,
|
|
36
|
+
updateWidget,
|
|
37
|
+
} from "./workflow/state.js";
|
|
38
|
+
import {
|
|
39
|
+
handleContext as handleRuleContext,
|
|
40
|
+
loadRules,
|
|
41
|
+
resetRuleStates,
|
|
42
|
+
} from "./rules/engine.js";
|
|
43
|
+
import {
|
|
44
|
+
consumeAtddWarning,
|
|
45
|
+
grantBashWriteAllowance,
|
|
46
|
+
handleToolCall,
|
|
47
|
+
handleToolResult,
|
|
48
|
+
handleUserBash,
|
|
49
|
+
resetTddState,
|
|
50
|
+
restoreTddState,
|
|
51
|
+
serializeTddState,
|
|
52
|
+
} from "./workflow/tdd-guard.js";
|
|
53
|
+
|
|
54
|
+
export default function superteam(pi: ExtensionAPI) {
|
|
55
|
+
// --- team tool ---
|
|
56
|
+
|
|
57
|
+
const TaskItem = Type.Object({
|
|
58
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
59
|
+
task: Type.String({ description: "Task to delegate to the agent" }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const ChainItem = Type.Object({
|
|
63
|
+
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
64
|
+
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const TeamParams = Type.Object({
|
|
68
|
+
// Single mode
|
|
69
|
+
agent: Type.Optional(Type.String({ description: "Name of the agent to dispatch (single mode)" })),
|
|
70
|
+
task: Type.Optional(Type.String({ description: "Task description (single mode)" })),
|
|
71
|
+
// Parallel mode
|
|
72
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
73
|
+
// Chain mode
|
|
74
|
+
chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution. Use {previous} in task to reference prior output." })),
|
|
75
|
+
// Options
|
|
76
|
+
includeProjectAgents: Type.Optional(
|
|
77
|
+
Type.Boolean({
|
|
78
|
+
description: "Include project-local agents from .pi/agents/. Default: false.",
|
|
79
|
+
default: false,
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
overrideCostLimit: Type.Optional(
|
|
83
|
+
Type.Boolean({
|
|
84
|
+
description: "Override hard cost limit for this dispatch. Default: false.",
|
|
85
|
+
default: false,
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
pi.registerTool({
|
|
91
|
+
name: "team",
|
|
92
|
+
label: "Team",
|
|
93
|
+
description: [
|
|
94
|
+
"Dispatch specialized agents with isolated context windows.",
|
|
95
|
+
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
96
|
+
"Each agent runs in its own pi subprocess with specific model, tools, and system prompt.",
|
|
97
|
+
"Available agents include: scout (fast recon), implementer (TDD implementation),",
|
|
98
|
+
"and any user-defined agents in ~/.pi/agent/agents/.",
|
|
99
|
+
].join(" "),
|
|
100
|
+
parameters: TeamParams,
|
|
101
|
+
|
|
102
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
103
|
+
const includeProject = params.includeProjectAgents ?? false;
|
|
104
|
+
const { agents, projectAgentsDir } = discoverAgents(ctx.cwd, includeProject);
|
|
105
|
+
|
|
106
|
+
// Determine mode
|
|
107
|
+
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
108
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
109
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
110
|
+
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
|
111
|
+
|
|
112
|
+
if (modeCount !== 1) {
|
|
113
|
+
const available =
|
|
114
|
+
agents.map((a) => ` ${a.name} (${a.source}): ${a.description}`).join("\n") || " (none found)";
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Provide exactly one mode: single (agent+task), parallel (tasks), or chain.\n\nAvailable agents:\n${available}`,
|
|
119
|
+
}],
|
|
120
|
+
details: { mode: "single", results: [] } as DispatchDetails,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cost check (unless overridden)
|
|
125
|
+
if (!params.overrideCostLimit) {
|
|
126
|
+
const costCheck = checkCostBudget(ctx.cwd);
|
|
127
|
+
if (!costCheck.allowed) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: costCheck.warning! }],
|
|
130
|
+
details: { mode: "single", results: [] } as DispatchDetails,
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (costCheck.warning && ctx.hasUI) {
|
|
135
|
+
ctx.ui.notify(costCheck.warning, "warning");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Resolve agent by name, with error handling
|
|
140
|
+
const resolveAgent = (name: string): AgentProfile | string => {
|
|
141
|
+
const agent = agents.find((a) => a.name === name);
|
|
142
|
+
if (!agent) {
|
|
143
|
+
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
144
|
+
return `Unknown agent: "${name}". Available: ${available}`;
|
|
145
|
+
}
|
|
146
|
+
if (agent.source === "project" && !ctx.hasUI) {
|
|
147
|
+
return `Project agent "${name}" not available in non-interactive mode.`;
|
|
148
|
+
}
|
|
149
|
+
return agent;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Trust confirmation for project agents
|
|
153
|
+
const confirmProjectAgents = async (agentNames: string[]): Promise<string | null> => {
|
|
154
|
+
if (!ctx.hasUI) return null;
|
|
155
|
+
const projectNames = agentNames
|
|
156
|
+
.map((n) => agents.find((a) => a.name === n))
|
|
157
|
+
.filter((a): a is AgentProfile => a?.source === "project")
|
|
158
|
+
.map((a) => a.name);
|
|
159
|
+
if (projectNames.length === 0) return null;
|
|
160
|
+
|
|
161
|
+
const ok = await ctx.ui.confirm(
|
|
162
|
+
"Run project-local agents?",
|
|
163
|
+
`Agents: ${projectNames.join(", ")}\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
164
|
+
);
|
|
165
|
+
return ok ? null : "Cancelled: project agents not approved.";
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// --- CHAIN MODE ---
|
|
169
|
+
if (params.chain && params.chain.length > 0) {
|
|
170
|
+
const chainAgents: AgentProfile[] = [];
|
|
171
|
+
const chainTasks: string[] = [];
|
|
172
|
+
|
|
173
|
+
for (const step of params.chain) {
|
|
174
|
+
const resolved = resolveAgent(step.agent);
|
|
175
|
+
if (typeof resolved === "string") {
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: resolved }],
|
|
178
|
+
details: { mode: "chain", results: [] } as DispatchDetails,
|
|
179
|
+
isError: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
chainAgents.push(resolved);
|
|
183
|
+
chainTasks.push(step.task);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const cancelMsg = await confirmProjectAgents(params.chain.map((s) => s.agent));
|
|
187
|
+
if (cancelMsg) {
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: cancelMsg }],
|
|
190
|
+
details: { mode: "chain", results: [] } as DispatchDetails,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const results = await dispatchChain(chainAgents, chainTasks, ctx.cwd, signal, onUpdate);
|
|
195
|
+
const lastResult = results[results.length - 1];
|
|
196
|
+
const output = lastResult ? getFinalOutput(lastResult.messages) : "(no output)";
|
|
197
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
198
|
+
const totalUsage = aggregateUsage(results);
|
|
199
|
+
|
|
200
|
+
const isError = successCount < results.length;
|
|
201
|
+
const summary = isError
|
|
202
|
+
? `Chain stopped at step ${results.length}/${params.chain.length}: ${lastResult?.errorMessage || "error"}`
|
|
203
|
+
: output;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: summary || "(no output)" }],
|
|
207
|
+
details: { mode: "chain", results } as DispatchDetails,
|
|
208
|
+
isError,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- PARALLEL MODE ---
|
|
213
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
214
|
+
const parallelAgents: AgentProfile[] = [];
|
|
215
|
+
const parallelTasks: string[] = [];
|
|
216
|
+
|
|
217
|
+
for (const t of params.tasks) {
|
|
218
|
+
const resolved = resolveAgent(t.agent);
|
|
219
|
+
if (typeof resolved === "string") {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: resolved }],
|
|
222
|
+
details: { mode: "parallel", results: [] } as DispatchDetails,
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
parallelAgents.push(resolved);
|
|
227
|
+
parallelTasks.push(t.task);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cancelMsg = await confirmProjectAgents(params.tasks.map((t) => t.agent));
|
|
231
|
+
if (cancelMsg) {
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: cancelMsg }],
|
|
234
|
+
details: { mode: "parallel", results: [] } as DispatchDetails,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const results = await dispatchParallel(parallelAgents, parallelTasks, ctx.cwd, signal, onUpdate);
|
|
240
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
241
|
+
const totalUsage = aggregateUsage(results);
|
|
242
|
+
|
|
243
|
+
const summaries = results.map((r) => {
|
|
244
|
+
const output = getFinalOutput(r.messages);
|
|
245
|
+
const preview = output.slice(0, 200) + (output.length > 200 ? "..." : "");
|
|
246
|
+
return `[${r.agent}] ${r.exitCode === 0 ? "✓" : "✗"}: ${preview || "(no output)"}`;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
253
|
+
}],
|
|
254
|
+
details: { mode: "parallel", results } as DispatchDetails,
|
|
255
|
+
};
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text", text: `Parallel dispatch error: ${e.message}` }],
|
|
259
|
+
details: { mode: "parallel", results: [] } as DispatchDetails,
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- SINGLE MODE ---
|
|
266
|
+
if (params.agent && params.task) {
|
|
267
|
+
const resolved = resolveAgent(params.agent);
|
|
268
|
+
if (typeof resolved === "string") {
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: resolved }],
|
|
271
|
+
details: { mode: "single", results: [] } as DispatchDetails,
|
|
272
|
+
isError: true,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (resolved.source === "project" && ctx.hasUI) {
|
|
277
|
+
const ok = await ctx.ui.confirm(
|
|
278
|
+
"Run project-local agent?",
|
|
279
|
+
`Agent: ${resolved.name}\nSource: ${resolved.filePath}\nProject agents are repo-controlled.`,
|
|
280
|
+
);
|
|
281
|
+
if (!ok) {
|
|
282
|
+
return {
|
|
283
|
+
content: [{ type: "text", text: "Cancelled: project agent not approved." }],
|
|
284
|
+
details: { mode: "single", results: [] } as DispatchDetails,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = await dispatchAgent(resolved, params.task, ctx.cwd, signal,
|
|
290
|
+
onUpdate ? (partial) => onUpdate(partial) : undefined,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
294
|
+
const output = getFinalOutput(result.messages);
|
|
295
|
+
|
|
296
|
+
if (isError) {
|
|
297
|
+
const errorMsg = result.errorMessage || result.stderr || output || "(no output)";
|
|
298
|
+
return {
|
|
299
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
300
|
+
details: { mode: "single", results: [result] } as DispatchDetails,
|
|
301
|
+
isError: true,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: output || "(no output)" }],
|
|
307
|
+
details: { mode: "single", results: [result] } as DispatchDetails,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Should not reach here
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: "Invalid parameters." }],
|
|
314
|
+
details: { mode: "single", results: [] } as DispatchDetails,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
renderCall(args, theme) {
|
|
319
|
+
if (args.chain && args.chain.length > 0) {
|
|
320
|
+
let text =
|
|
321
|
+
theme.fg("toolTitle", theme.bold("team ")) +
|
|
322
|
+
theme.fg("accent", `chain (${args.chain.length} steps)`);
|
|
323
|
+
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
324
|
+
const step = args.chain[i];
|
|
325
|
+
const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
|
|
326
|
+
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
|
|
327
|
+
text += `\n ${theme.fg("muted", `${i + 1}.`)} ${theme.fg("accent", step.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
|
328
|
+
}
|
|
329
|
+
if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
|
|
330
|
+
return new Text(text, 0, 0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
334
|
+
let text =
|
|
335
|
+
theme.fg("toolTitle", theme.bold("team ")) +
|
|
336
|
+
theme.fg("accent", `parallel (${args.tasks.length} tasks)`);
|
|
337
|
+
for (const t of args.tasks.slice(0, 3)) {
|
|
338
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
|
339
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
|
340
|
+
}
|
|
341
|
+
if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
342
|
+
return new Text(text, 0, 0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const agentName = args.agent || "...";
|
|
346
|
+
const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
|
|
347
|
+
let text = theme.fg("toolTitle", theme.bold("team ")) + theme.fg("accent", agentName);
|
|
348
|
+
text += `\n ${theme.fg("dim", preview)}`;
|
|
349
|
+
return new Text(text, 0, 0);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
renderResult(result, { expanded }, theme) {
|
|
353
|
+
const details = result.details as DispatchDetails | undefined;
|
|
354
|
+
if (!details || details.results.length === 0) {
|
|
355
|
+
const text = result.content[0];
|
|
356
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- Single mode ---
|
|
360
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
361
|
+
const r = details.results[0];
|
|
362
|
+
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
363
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
364
|
+
const output = getFinalOutput(r.messages);
|
|
365
|
+
const usageStr = formatUsage(r.usage, r.model);
|
|
366
|
+
|
|
367
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
368
|
+
if (isError && r.errorMessage) {
|
|
369
|
+
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
370
|
+
} else if (output) {
|
|
371
|
+
const preview = expanded ? output : output.split("\n").slice(0, 10).join("\n");
|
|
372
|
+
text += `\n${theme.fg("toolOutput", preview)}`;
|
|
373
|
+
if (!expanded && output.split("\n").length > 10) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
374
|
+
} else {
|
|
375
|
+
text += `\n${theme.fg("muted", "(no output)")}`;
|
|
376
|
+
}
|
|
377
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
378
|
+
return new Text(text, 0, 0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// --- Chain mode ---
|
|
382
|
+
if (details.mode === "chain") {
|
|
383
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
384
|
+
const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
385
|
+
|
|
386
|
+
if (expanded) {
|
|
387
|
+
const container = new Container();
|
|
388
|
+
container.addChild(new Text(
|
|
389
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${details.results.length} steps`)}`,
|
|
390
|
+
0, 0,
|
|
391
|
+
));
|
|
392
|
+
|
|
393
|
+
for (const r of details.results) {
|
|
394
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
395
|
+
const output = getFinalOutput(r.messages);
|
|
396
|
+
container.addChild(new Spacer(1));
|
|
397
|
+
container.addChild(new Text(
|
|
398
|
+
`${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`,
|
|
399
|
+
0, 0,
|
|
400
|
+
));
|
|
401
|
+
if (output) {
|
|
402
|
+
container.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
403
|
+
}
|
|
404
|
+
const stepUsage = formatUsage(r.usage, r.model);
|
|
405
|
+
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
409
|
+
if (totalUsage) {
|
|
410
|
+
container.addChild(new Spacer(1));
|
|
411
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${totalUsage}`), 0, 0));
|
|
412
|
+
}
|
|
413
|
+
return container;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Collapsed chain
|
|
417
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${details.results.length} steps`)}`;
|
|
418
|
+
for (const r of details.results) {
|
|
419
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
420
|
+
const output = getFinalOutput(r.messages);
|
|
421
|
+
const preview = output ? output.split("\n").slice(0, 3).join("\n") : "(no output)";
|
|
422
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
423
|
+
text += `\n${theme.fg("toolOutput", preview)}`;
|
|
424
|
+
}
|
|
425
|
+
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
426
|
+
if (totalUsage) text += `\n\n${theme.fg("dim", `Total: ${totalUsage}`)}`;
|
|
427
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
428
|
+
return new Text(text, 0, 0);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// --- Parallel mode ---
|
|
432
|
+
if (details.mode === "parallel") {
|
|
433
|
+
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
434
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
435
|
+
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
436
|
+
const isRunning = running > 0;
|
|
437
|
+
const icon = isRunning
|
|
438
|
+
? theme.fg("warning", "⏳")
|
|
439
|
+
: failCount > 0 ? theme.fg("warning", "◐") : theme.fg("success", "✓");
|
|
440
|
+
const status = isRunning
|
|
441
|
+
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
442
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
443
|
+
|
|
444
|
+
if (expanded && !isRunning) {
|
|
445
|
+
const container = new Container();
|
|
446
|
+
container.addChild(new Text(
|
|
447
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
448
|
+
0, 0,
|
|
449
|
+
));
|
|
450
|
+
|
|
451
|
+
for (const r of details.results) {
|
|
452
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
453
|
+
const output = getFinalOutput(r.messages);
|
|
454
|
+
container.addChild(new Spacer(1));
|
|
455
|
+
container.addChild(new Text(
|
|
456
|
+
`${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`,
|
|
457
|
+
0, 0,
|
|
458
|
+
));
|
|
459
|
+
if (output) container.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
|
460
|
+
const taskUsage = formatUsage(r.usage, r.model);
|
|
461
|
+
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
465
|
+
if (totalUsage) {
|
|
466
|
+
container.addChild(new Spacer(1));
|
|
467
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${totalUsage}`), 0, 0));
|
|
468
|
+
}
|
|
469
|
+
return container;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Collapsed parallel
|
|
473
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
474
|
+
for (const r of details.results) {
|
|
475
|
+
const rIcon = r.exitCode === -1
|
|
476
|
+
? theme.fg("warning", "⏳")
|
|
477
|
+
: r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
478
|
+
const output = getFinalOutput(r.messages);
|
|
479
|
+
const preview = r.exitCode === -1
|
|
480
|
+
? "(running...)"
|
|
481
|
+
: output ? output.split("\n").slice(0, 3).join("\n") : "(no output)";
|
|
482
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
483
|
+
text += `\n${theme.fg("toolOutput", preview)}`;
|
|
484
|
+
}
|
|
485
|
+
if (!isRunning) {
|
|
486
|
+
const totalUsage = formatUsage(aggregateUsage(details.results));
|
|
487
|
+
if (totalUsage) text += `\n\n${theme.fg("dim", `Total: ${totalUsage}`)}`;
|
|
488
|
+
}
|
|
489
|
+
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
490
|
+
return new Text(text, 0, 0);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const text = result.content[0];
|
|
494
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// --- /team command ---
|
|
499
|
+
|
|
500
|
+
pi.registerCommand("team", {
|
|
501
|
+
description: "List available agents, show session cost",
|
|
502
|
+
async handler(args, ctx) {
|
|
503
|
+
const includeProject = args.trim() === "--project" || args.trim() === "-p";
|
|
504
|
+
const { agents, projectAgentsDir } = discoverAgents(ctx.cwd, includeProject);
|
|
505
|
+
|
|
506
|
+
if (agents.length === 0) {
|
|
507
|
+
ctx.ui.notify("No agents found. Place .md files in ~/.pi/agent/agents/ or install a package with agents.", "info");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const lines = agents.map((a) => {
|
|
512
|
+
const model = a.model || "(default)";
|
|
513
|
+
const tools = a.tools?.join(", ") || "(all)";
|
|
514
|
+
return `${a.name} [${a.source}] — ${a.description}\n model: ${model}, tools: ${tools}`;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const cost = getSessionCost();
|
|
518
|
+
const config = getConfig(ctx.cwd);
|
|
519
|
+
const costLine = `\nSession cost: $${cost.toFixed(4)} / $${config.costs.hardLimitUsd.toFixed(2)} limit`;
|
|
520
|
+
|
|
521
|
+
ctx.ui.notify(`Available agents (${agents.length}):\n\n${lines.join("\n\n")}${costLine}`, "info");
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// --- /sdd command (plan management) ---
|
|
526
|
+
|
|
527
|
+
pi.registerCommand("sdd", {
|
|
528
|
+
description: "SDD workflow. Usage: /sdd load <file> | /sdd run | /sdd status | /sdd next | /sdd reset",
|
|
529
|
+
async handler(args, ctx) {
|
|
530
|
+
const parts = args.trim().split(/\s+/);
|
|
531
|
+
const sub = parts[0]?.toLowerCase() || "status";
|
|
532
|
+
|
|
533
|
+
switch (sub) {
|
|
534
|
+
case "load": {
|
|
535
|
+
const filePath = parts.slice(1).join(" ").trim();
|
|
536
|
+
if (!filePath) {
|
|
537
|
+
ctx.ui.notify("Usage: /sdd load <plan-file.md>", "warning");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const { count, source } = loadPlanIntoState(filePath);
|
|
541
|
+
if (count === 0) {
|
|
542
|
+
ctx.ui.notify(`No tasks found in ${filePath}. Use \`\`\`superteam-tasks block or ### Task N: headings.`, "warning");
|
|
543
|
+
} else {
|
|
544
|
+
ctx.ui.notify(`Loaded ${count} tasks from ${filePath} (${source} parser)`, "info");
|
|
545
|
+
}
|
|
546
|
+
updateWidget(ctx);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case "status": {
|
|
551
|
+
const state = getState();
|
|
552
|
+
const lines = buildStatusLines();
|
|
553
|
+
if (lines.length === 0) {
|
|
554
|
+
ctx.ui.notify("No active workflow. Use /sdd load <file> to load a plan.", "info");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const taskLines = state.tasks.map((t, i) => {
|
|
558
|
+
const marker = i === state.currentTaskIndex ? "→" : t.status === "complete" ? "✓" : " ";
|
|
559
|
+
return `${marker} ${t.id}. ${t.title} [${t.status}]`;
|
|
560
|
+
});
|
|
561
|
+
const cost = state.cumulativeCostUsd > 0 ? `\nCost: $${state.cumulativeCostUsd.toFixed(2)}` : "";
|
|
562
|
+
ctx.ui.notify(`SDD Status:\n${taskLines.join("\n")}${cost}`, "info");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "next": {
|
|
567
|
+
const { advanceTask, getCurrentTask } = await import("./workflow/state.js");
|
|
568
|
+
const current = getCurrentTask();
|
|
569
|
+
if (!current) {
|
|
570
|
+
ctx.ui.notify("No tasks loaded or all tasks complete.", "info");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const next = advanceTask();
|
|
574
|
+
if (next) {
|
|
575
|
+
ctx.ui.notify(`Advanced to Task ${next.id}: ${next.title}`, "info");
|
|
576
|
+
} else {
|
|
577
|
+
ctx.ui.notify("All tasks complete!", "info");
|
|
578
|
+
}
|
|
579
|
+
updateWidget(ctx);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case "run": {
|
|
584
|
+
const { runSddTask } = await import("./workflow/sdd.js");
|
|
585
|
+
const task = getCurrentTask();
|
|
586
|
+
if (!task) {
|
|
587
|
+
ctx.ui.notify("No current task. Use /sdd load <file> first.", "warning");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
ctx.ui.notify(`Starting SDD for Task ${task.id}: ${task.title}`, "info");
|
|
592
|
+
const result = await runSddTask(ctx, undefined, (msg) => {
|
|
593
|
+
// Status updates during SDD run
|
|
594
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "info");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (result.status === "complete") {
|
|
598
|
+
const { formatUsage, aggregateUsage } = await import("./dispatch.js");
|
|
599
|
+
ctx.ui.notify(
|
|
600
|
+
`✓ Task ${result.taskId}: "${result.taskTitle}" completed!\n` +
|
|
601
|
+
`Reviews: ${result.reviewResults.length} total\n` +
|
|
602
|
+
`Usage: ${formatUsage(result.totalUsage)}`,
|
|
603
|
+
"info",
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Auto-advance to next task
|
|
607
|
+
const { advanceTask } = await import("./workflow/state.js");
|
|
608
|
+
const next = advanceTask();
|
|
609
|
+
if (next) {
|
|
610
|
+
ctx.ui.notify(`Next: Task ${next.id}: ${next.title}. Run /sdd run to continue.`, "info");
|
|
611
|
+
} else {
|
|
612
|
+
ctx.ui.notify("All tasks complete! 🎉", "info");
|
|
613
|
+
}
|
|
614
|
+
} else if (result.status === "escalated") {
|
|
615
|
+
ctx.ui.notify(
|
|
616
|
+
`⚠ Task ${result.taskId}: "${result.taskTitle}" — escalated\n\n` +
|
|
617
|
+
`${result.escalationReason}\n\n` +
|
|
618
|
+
`Options: fix manually, then /sdd run to retry, or /sdd next to skip.`,
|
|
619
|
+
"warning",
|
|
620
|
+
);
|
|
621
|
+
} else {
|
|
622
|
+
ctx.ui.notify(`Task ${result.taskId}: aborted — ${result.escalationReason}`, "warning");
|
|
623
|
+
}
|
|
624
|
+
updateWidget(ctx);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case "reset": {
|
|
629
|
+
const { updateState } = await import("./workflow/state.js");
|
|
630
|
+
updateState((s) => {
|
|
631
|
+
s.tasks = [];
|
|
632
|
+
s.currentTaskIndex = -1;
|
|
633
|
+
s.reviewCycles = [];
|
|
634
|
+
s.cumulativeCostUsd = 0;
|
|
635
|
+
s.planFile = undefined;
|
|
636
|
+
});
|
|
637
|
+
ctx.ui.notify("SDD state reset.", "info");
|
|
638
|
+
updateWidget(ctx);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
default:
|
|
643
|
+
ctx.ui.notify("Unknown subcommand. Usage: /sdd load|run|status|next|reset", "warning");
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// --- /tdd command (toggle TDD mode + escape hatch) ---
|
|
649
|
+
|
|
650
|
+
pi.registerCommand("tdd", {
|
|
651
|
+
description: "TDD mode control. Usage: /tdd [off|tdd|atdd] | /tdd allow-bash-write once <reason>",
|
|
652
|
+
async handler(args, ctx) {
|
|
653
|
+
const parts = args.trim().split(/\s+/);
|
|
654
|
+
const sub = parts[0]?.toLowerCase() || "";
|
|
655
|
+
|
|
656
|
+
// Toggle or set mode
|
|
657
|
+
if (!sub || ["off", "tdd", "atdd"].includes(sub)) {
|
|
658
|
+
if (sub && ["off", "tdd", "atdd"].includes(sub)) {
|
|
659
|
+
const mode = sub as TddMode;
|
|
660
|
+
setTddMode(mode);
|
|
661
|
+
ctx.ui.notify(`TDD mode: ${mode.toUpperCase()}`, "info");
|
|
662
|
+
} else {
|
|
663
|
+
const current = getState().tddMode;
|
|
664
|
+
const next: TddMode = current === "off" ? "tdd" : current === "tdd" ? "atdd" : "off";
|
|
665
|
+
setTddMode(next);
|
|
666
|
+
ctx.ui.notify(`TDD mode: ${next.toUpperCase()}`, "info");
|
|
667
|
+
}
|
|
668
|
+
updateWidget(ctx);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Bash write allowance escape hatch
|
|
673
|
+
if (sub === "allow-bash-write" && parts[1]?.toLowerCase() === "once") {
|
|
674
|
+
const reason = parts.slice(2).join(" ").trim();
|
|
675
|
+
if (!reason) {
|
|
676
|
+
ctx.ui.notify("Usage: /tdd allow-bash-write once <reason>", "warning");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
grantBashWriteAllowance(reason);
|
|
680
|
+
ctx.ui.notify(`Bash write allowed once: ${reason}`, "info");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
ctx.ui.notify("Usage: /tdd [off|tdd|atdd] | /tdd allow-bash-write once <reason>", "warning");
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// --- TDD Guard event handlers ---
|
|
689
|
+
|
|
690
|
+
pi.on("tool_call", (event, ctx) => {
|
|
691
|
+
return handleToolCall(event, ctx);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
pi.on("tool_result", (event, ctx) => {
|
|
695
|
+
return handleToolResult(event, ctx);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
pi.on("user_bash", (event, ctx) => {
|
|
699
|
+
return handleUserBash(event, ctx);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// --- Rule engine (TTSR-like context injection) ---
|
|
703
|
+
|
|
704
|
+
pi.on("context", (event, _ctx) => {
|
|
705
|
+
return handleRuleContext(event);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// --- Session lifecycle ---
|
|
709
|
+
|
|
710
|
+
initState(pi);
|
|
711
|
+
loadRules(); // Initial load (session_start reloads)
|
|
712
|
+
|
|
713
|
+
pi.on("session_start", (_event, ctx) => {
|
|
714
|
+
resetSessionCost();
|
|
715
|
+
resetTddState();
|
|
716
|
+
resetRuleStates();
|
|
717
|
+
loadRules(); // Load rules from package rules/ dir
|
|
718
|
+
restoreFromBranch(ctx);
|
|
719
|
+
updateWidget(ctx);
|
|
720
|
+
});
|
|
721
|
+
}
|