pi-subagents 0.3.0 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "cli"
22
22
  ],
23
23
  "bin": {
24
- "pi-subagents": "./install.mjs"
24
+ "pi-subagents": "install.mjs"
25
25
  },
26
26
  "files": [
27
27
  "*.ts",
package/render.ts ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Rendering functions for subagent results
3
+ */
4
+
5
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
6
+ import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import { Container, Markdown, Spacer, Text, type Widget } from "@mariozechner/pi-tui";
8
+ import {
9
+ type AsyncJobState,
10
+ type Details,
11
+ MAX_WIDGET_JOBS,
12
+ WIDGET_KEY,
13
+ } from "./types.js";
14
+ import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.js";
15
+ import { getFinalOutput, getDisplayItems, getOutputTail, getLastActivity } from "./utils.js";
16
+
17
+ type Theme = ExtensionContext["ui"]["theme"];
18
+
19
+ // Track last rendered widget state to avoid no-op re-renders
20
+ let lastWidgetHash = "";
21
+
22
+ /**
23
+ * Compute a simple hash of job states for change detection
24
+ */
25
+ function computeWidgetHash(jobs: AsyncJobState[]): string {
26
+ return jobs.slice(0, MAX_WIDGET_JOBS).map(job =>
27
+ `${job.asyncId}:${job.status}:${job.currentStep}:${job.updatedAt}:${job.totalTokens?.total ?? 0}`
28
+ ).join("|");
29
+ }
30
+
31
+ /**
32
+ * Render the async jobs widget
33
+ */
34
+ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
35
+ if (!ctx.hasUI) return;
36
+ if (jobs.length === 0) {
37
+ if (lastWidgetHash !== "") {
38
+ lastWidgetHash = "";
39
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
40
+ }
41
+ return;
42
+ }
43
+
44
+ // Check if anything changed since last render
45
+ // Always re-render if any displayed job is running (output tail updates constantly)
46
+ const displayedJobs = jobs.slice(0, MAX_WIDGET_JOBS);
47
+ const hasRunningJobs = displayedJobs.some(job => job.status === "running");
48
+ const newHash = computeWidgetHash(jobs);
49
+ if (!hasRunningJobs && newHash === lastWidgetHash) {
50
+ return; // Skip re-render, nothing changed
51
+ }
52
+ lastWidgetHash = newHash;
53
+
54
+ const theme = ctx.ui.theme;
55
+ const lines: string[] = [];
56
+ lines.push(theme.fg("accent", "Async subagents"));
57
+
58
+ for (const job of displayedJobs) {
59
+ const id = job.asyncId.slice(0, 6);
60
+ const status =
61
+ job.status === "complete"
62
+ ? theme.fg("success", "complete")
63
+ : job.status === "failed"
64
+ ? theme.fg("error", "failed")
65
+ : theme.fg("warning", "running");
66
+
67
+ const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
68
+ const stepIndex = job.currentStep !== undefined ? job.currentStep + 1 : undefined;
69
+ const stepText = stepIndex !== undefined ? `step ${stepIndex}/${stepsTotal}` : `steps ${stepsTotal}`;
70
+ const endTime = (job.status === "complete" || job.status === "failed") ? (job.updatedAt ?? Date.now()) : Date.now();
71
+ const elapsed = job.startedAt ? formatDuration(endTime - job.startedAt) : "";
72
+ const agentLabel = job.agents ? job.agents.join(" -> ") : (job.mode ?? "single");
73
+
74
+ const tokenText = job.totalTokens ? ` | ${formatTokens(job.totalTokens.total)} tok` : "";
75
+ const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
76
+ const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
77
+
78
+ lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
79
+
80
+ if (job.status === "running" && job.outputFile) {
81
+ const tail = getOutputTail(job.outputFile, 3);
82
+ for (const line of tail) {
83
+ lines.push(theme.fg("dim", ` > ${line}`));
84
+ }
85
+ }
86
+ }
87
+
88
+ ctx.ui.setWidget(WIDGET_KEY, lines);
89
+ }
90
+
91
+ /**
92
+ * Render a subagent result
93
+ */
94
+ export function renderSubagentResult(
95
+ result: AgentToolResult<Details>,
96
+ _options: { expanded: boolean },
97
+ theme: Theme,
98
+ ): Widget {
99
+ const d = result.details;
100
+ if (!d || !d.results.length) {
101
+ const t = result.content[0];
102
+ return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
103
+ }
104
+
105
+ const mdTheme = getMarkdownTheme();
106
+
107
+ if (d.mode === "single" && d.results.length === 1) {
108
+ const r = d.results[0];
109
+ const isRunning = r.progress?.status === "running";
110
+ const icon = isRunning
111
+ ? theme.fg("warning", "...")
112
+ : r.exitCode === 0
113
+ ? theme.fg("success", "ok")
114
+ : theme.fg("error", "X");
115
+ const output = r.truncation?.text || getFinalOutput(r.messages);
116
+
117
+ const progressInfo = isRunning && r.progress
118
+ ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
119
+ : r.progressSummary
120
+ ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
121
+ : "";
122
+
123
+ const c = new Container();
124
+ c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
125
+ c.addChild(new Spacer(1));
126
+ c.addChild(
127
+ new Text(theme.fg("dim", `Task: ${r.task.slice(0, 150)}${r.task.length > 150 ? "..." : ""}`), 0, 0),
128
+ );
129
+ c.addChild(new Spacer(1));
130
+
131
+ const items = getDisplayItems(r.messages);
132
+ for (const item of items) {
133
+ if (item.type === "tool")
134
+ c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
135
+ }
136
+ if (items.length) c.addChild(new Spacer(1));
137
+
138
+ if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
139
+ c.addChild(new Spacer(1));
140
+ c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
141
+ if (r.sessionFile) {
142
+ c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
143
+ }
144
+
145
+ if (r.artifactPaths) {
146
+ c.addChild(new Spacer(1));
147
+ c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
148
+ }
149
+ return c;
150
+ }
151
+
152
+ const hasRunning = d.progress?.some((p) => p.status === "running")
153
+ || d.results.some((r) => r.progress?.status === "running");
154
+ const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
155
+ const icon = hasRunning
156
+ ? theme.fg("warning", "...")
157
+ : ok === d.results.length
158
+ ? theme.fg("success", "ok")
159
+ : theme.fg("error", "X");
160
+
161
+ const totalSummary =
162
+ d.progressSummary ||
163
+ d.results.reduce(
164
+ (acc, r) => {
165
+ const prog = r.progress || r.progressSummary;
166
+ if (prog) {
167
+ acc.toolCount += prog.toolCount;
168
+ acc.tokens += prog.tokens;
169
+ acc.durationMs =
170
+ d.mode === "chain"
171
+ ? acc.durationMs + prog.durationMs
172
+ : Math.max(acc.durationMs, prog.durationMs);
173
+ }
174
+ return acc;
175
+ },
176
+ { toolCount: 0, tokens: 0, durationMs: 0 },
177
+ );
178
+
179
+ const summaryStr =
180
+ totalSummary.toolCount || totalSummary.tokens
181
+ ? ` | ${totalSummary.toolCount} tools, ${formatTokens(totalSummary.tokens)} tok, ${formatDuration(totalSummary.durationMs)}`
182
+ : "";
183
+
184
+ const modeLabel = d.mode === "parallel" ? "parallel (no live progress)" : d.mode;
185
+ // For parallel-in-chain, show task count (results) for consistency with step display
186
+ // For sequential chains, show logical step count
187
+ const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
188
+ const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
189
+ const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
190
+ const stepInfo = hasRunning ? ` ${currentStep}/${totalCount}` : ` ${ok}/${totalCount}`;
191
+
192
+ // Build chain visualization: "scout → planner" with status icons
193
+ // Note: Only works correctly for sequential chains. Chains with parallel steps
194
+ // (indicated by "[agent1+agent2]" format) have multiple results per step,
195
+ // breaking the 1:1 mapping between chainAgents and results.
196
+ const chainVis = d.chainAgents?.length && !hasParallelInChain
197
+ ? d.chainAgents
198
+ .map((agent, i) => {
199
+ const result = d.results[i];
200
+ const isFailed = result && result.exitCode !== 0 && result.progress?.status !== "running";
201
+ const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
202
+ const isCurrent = i === (d.currentStepIndex ?? d.results.length);
203
+ const icon = isFailed
204
+ ? theme.fg("error", "✗")
205
+ : isComplete
206
+ ? theme.fg("success", "✓")
207
+ : isCurrent && hasRunning
208
+ ? theme.fg("warning", "●")
209
+ : theme.fg("dim", "○");
210
+ return `${icon} ${agent}`;
211
+ })
212
+ .join(theme.fg("dim", " → "))
213
+ : null;
214
+
215
+ const c = new Container();
216
+ c.addChild(
217
+ new Text(
218
+ `${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
219
+ 0,
220
+ 0,
221
+ ),
222
+ );
223
+ // Show chain visualization
224
+ if (chainVis) {
225
+ c.addChild(new Text(` ${chainVis}`, 0, 0));
226
+ }
227
+
228
+ // === STATIC STEP LAYOUT (like clarification UI) ===
229
+ // Each step gets a fixed section with task/output/status
230
+ // Note: For chains with parallel steps, chainAgents indices don't map 1:1 to results
231
+ // (parallel steps produce multiple results). Fall back to result-based iteration.
232
+ const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
233
+ const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
234
+
235
+ c.addChild(new Spacer(1));
236
+
237
+ for (let i = 0; i < stepsToShow; i++) {
238
+ const r = d.results[i];
239
+ const agentName = useResultsDirectly
240
+ ? (r?.agent || `step-${i + 1}`)
241
+ : (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
242
+
243
+ if (!r) {
244
+ // Pending step
245
+ c.addChild(new Text(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), 0, 0));
246
+ c.addChild(new Text(theme.fg("dim", ` status: ○ pending`), 0, 0));
247
+ c.addChild(new Spacer(1));
248
+ continue;
249
+ }
250
+
251
+ const progressFromArray = d.progress?.find((p) => p.index === i)
252
+ || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
253
+ const rProg = r.progress || progressFromArray || r.progressSummary;
254
+ const rRunning = rProg?.status === "running";
255
+
256
+ // Step header with status
257
+ const statusIcon = rRunning
258
+ ? theme.fg("warning", "●")
259
+ : r.exitCode === 0
260
+ ? theme.fg("success", "✓")
261
+ : theme.fg("error", "✗");
262
+ const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
263
+ const stepHeader = rRunning
264
+ ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${stats}`
265
+ : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${stats}`;
266
+ c.addChild(new Text(stepHeader, 0, 0));
267
+
268
+ // Task (truncated)
269
+ const taskPreview = r.task.slice(0, 120) + (r.task.length > 120 ? "..." : "");
270
+ c.addChild(new Text(theme.fg("dim", ` task: ${taskPreview}`), 0, 0));
271
+
272
+ // Output target (extract from task)
273
+ const outputMatch = r.task.match(/[Oo]utput(?:\s+to)?\s+([^\s]+\.(?:md|txt|json))/);
274
+ if (outputMatch) {
275
+ c.addChild(new Text(theme.fg("dim", ` output: ${outputMatch[1]}`), 0, 0));
276
+ }
277
+
278
+ if (rRunning && rProg) {
279
+ // Current tool for running step
280
+ if (rProg.currentTool) {
281
+ const toolLine = rProg.currentToolArgs
282
+ ? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 100)}${rProg.currentToolArgs.length > 100 ? "..." : ""}`
283
+ : rProg.currentTool;
284
+ c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
285
+ }
286
+ // Recent tools
287
+ if (rProg.recentTools?.length) {
288
+ for (const t of rProg.recentTools.slice(0, 3)) {
289
+ const args = t.args.slice(0, 90) + (t.args.length > 90 ? "..." : "");
290
+ c.addChild(new Text(theme.fg("dim", ` ${t.tool}: ${args}`), 0, 0));
291
+ }
292
+ }
293
+ // Recent output (limited)
294
+ const recentLines = (rProg.recentOutput ?? []).slice(-5);
295
+ for (const line of recentLines) {
296
+ c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), 0, 0));
297
+ }
298
+ }
299
+
300
+ c.addChild(new Spacer(1));
301
+ }
302
+
303
+ if (d.artifacts) {
304
+ c.addChild(new Spacer(1));
305
+ c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
306
+ }
307
+ return c;
308
+ }
package/schemas.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * TypeBox schemas for subagent tool parameters
3
+ */
4
+
5
+ import { Type } from "@sinclair/typebox";
6
+
7
+ export const TaskItem = Type.Object({
8
+ agent: Type.String(),
9
+ task: Type.String(),
10
+ cwd: Type.Optional(Type.String())
11
+ });
12
+
13
+ // Sequential chain step (single agent)
14
+ export const SequentialStepSchema = Type.Object({
15
+ agent: Type.String(),
16
+ task: Type.Optional(Type.String({ description: "Task template. Use {task}, {previous}, {chain_dir}. Required for first step." })),
17
+ cwd: Type.Optional(Type.String()),
18
+ // Chain behavior overrides
19
+ output: Type.Optional(Type.Union([
20
+ Type.String(),
21
+ Type.Boolean(),
22
+ ], { description: "Override output filename (string), or false for text-only" })),
23
+ reads: Type.Optional(Type.Union([
24
+ Type.Array(Type.String()),
25
+ Type.Boolean(),
26
+ ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
27
+ progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
28
+ });
29
+
30
+ // Parallel task item (within a parallel step)
31
+ export const ParallelTaskSchema = Type.Object({
32
+ agent: Type.String(),
33
+ task: Type.Optional(Type.String({ description: "Task template. Defaults to {previous}." })),
34
+ cwd: Type.Optional(Type.String()),
35
+ output: Type.Optional(Type.Union([
36
+ Type.String(),
37
+ Type.Boolean(),
38
+ ], { description: "Override output filename (string), or false for text-only" })),
39
+ reads: Type.Optional(Type.Union([
40
+ Type.Array(Type.String()),
41
+ Type.Boolean(),
42
+ ], { description: "Override files to read from {chain_dir} (array), or false to disable" })),
43
+ progress: Type.Optional(Type.Boolean({ description: "Override progress tracking" })),
44
+ });
45
+
46
+ // Parallel chain step (multiple agents running concurrently)
47
+ export const ParallelStepSchema = Type.Object({
48
+ parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
49
+ concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
50
+ failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
51
+ });
52
+
53
+ // Chain item can be either sequential or parallel
54
+ export const ChainItem = Type.Union([SequentialStepSchema, ParallelStepSchema]);
55
+
56
+ export const MaxOutputSchema = Type.Optional(
57
+ Type.Object({
58
+ bytes: Type.Optional(Type.Number({ description: "Max bytes (default: 204800)" })),
59
+ lines: Type.Optional(Type.Number({ description: "Max lines (default: 5000)" })),
60
+ }),
61
+ );
62
+
63
+ export const SubagentParams = Type.Object({
64
+ agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
65
+ task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
66
+ tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
67
+ chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: [{agent}, {agent, task:'{previous}'}] - sequential pipeline" })),
68
+ async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
69
+ agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'user')" })),
70
+ cwd: Type.Optional(Type.String()),
71
+ maxOutput: MaxOutputSchema,
72
+ artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
73
+ includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
74
+ share: Type.Optional(Type.Boolean({ description: "Create shareable session log (default: true)", default: true })),
75
+ sessionDir: Type.Optional(
76
+ Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
77
+ ),
78
+ // Chain clarification TUI
79
+ clarify: Type.Optional(Type.Boolean({ description: "Show TUI to clarify chain templates (default: true for chains). Implies sync mode." })),
80
+ // Solo agent output override
81
+ output: Type.Optional(Type.Union([
82
+ Type.String(),
83
+ Type.Boolean(),
84
+ ], { description: "Override output file for single agent (string), or false to disable (uses agent default if omitted)" })),
85
+ });
86
+
87
+ export const StatusParams = Type.Object({
88
+ id: Type.Optional(Type.String({ description: "Async run id or prefix" })),
89
+ dir: Type.Optional(Type.String({ description: "Async run directory (overrides id search)" })),
90
+ });
package/settings.ts CHANGED
@@ -1,30 +1,14 @@
1
1
  /**
2
- * Subagent settings, chain behavior, and template management
2
+ * Chain behavior, template resolution, and directory management
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
- import * as os from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { AgentConfig } from "./agents.js";
9
8
 
10
- const SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
11
9
  const CHAIN_RUNS_DIR = "/tmp/pi-chain-runs";
12
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
13
11
 
14
- // =============================================================================
15
- // Settings Types
16
- // =============================================================================
17
-
18
- export interface ChainTemplates {
19
- [chainKey: string]: {
20
- [agentName: string]: string;
21
- };
22
- }
23
-
24
- export interface SubagentSettings {
25
- chains?: ChainTemplates;
26
- }
27
-
28
12
  // =============================================================================
29
13
  // Behavior Resolution Types
30
14
  // =============================================================================
@@ -83,10 +67,6 @@ export function isParallelStep(step: ChainStep): step is ParallelStep {
83
67
  return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
84
68
  }
85
69
 
86
- export function isSequentialStep(step: ChainStep): step is SequentialStep {
87
- return "agent" in step && !("parallel" in step);
88
- }
89
-
90
70
  /** Get all agent names in a step (single for sequential, multiple for parallel) */
91
71
  export function getStepAgents(step: ChainStep): string[] {
92
72
  if (isParallelStep(step)) {
@@ -95,51 +75,6 @@ export function getStepAgents(step: ChainStep): string[] {
95
75
  return [step.agent];
96
76
  }
97
77
 
98
- /** Get total task count in a step */
99
- export function getStepTaskCount(step: ChainStep): number {
100
- if (isParallelStep(step)) {
101
- return step.parallel.length;
102
- }
103
- return 1;
104
- }
105
-
106
- // =============================================================================
107
- // Settings Management
108
- // =============================================================================
109
-
110
- export function loadSubagentSettings(): SubagentSettings {
111
- try {
112
- const data = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
113
- return (data.subagent as SubagentSettings) ?? {};
114
- } catch {
115
- return {};
116
- }
117
- }
118
-
119
- export function saveChainTemplate(chainKey: string, templates: Record<string, string>): void {
120
- let settings: Record<string, unknown> = {};
121
- try {
122
- settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
123
- } catch {}
124
-
125
- if (!settings.subagent) settings.subagent = {};
126
- const subagent = settings.subagent as Record<string, unknown>;
127
- if (!subagent.chains) subagent.chains = {};
128
- const chains = subagent.chains as Record<string, unknown>;
129
-
130
- chains[chainKey] = templates;
131
-
132
- const dir = path.dirname(SETTINGS_PATH);
133
- if (!fs.existsSync(dir)) {
134
- fs.mkdirSync(dir, { recursive: true });
135
- }
136
- fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
137
- }
138
-
139
- export function getChainKey(agents: string[]): string {
140
- return agents.join("->");
141
- }
142
-
143
78
  // =============================================================================
144
79
  // Chain Directory Management
145
80
  // =============================================================================
@@ -183,36 +118,6 @@ export function cleanupOldChainDirs(): void {
183
118
  // Template Resolution
184
119
  // =============================================================================
185
120
 
186
- /**
187
- * Resolve templates for each step in a chain.
188
- * Priority: inline task > saved template > default
189
- * Default for step 0: "{task}", for others: "{previous}"
190
- */
191
- export function resolveChainTemplates(
192
- agentNames: string[],
193
- inlineTasks: (string | undefined)[],
194
- settings: SubagentSettings,
195
- ): string[] {
196
- const chainKey = getChainKey(agentNames);
197
- const savedTemplates = settings.chains?.[chainKey] ?? {};
198
-
199
- return agentNames.map((agent, i) => {
200
- // Priority: inline > saved > default
201
- const inline = inlineTasks[i];
202
- if (inline) return inline;
203
-
204
- const saved = savedTemplates[agent];
205
- if (saved) return saved;
206
-
207
- // Default: first step uses {task}, others use {previous}
208
- return i === 0 ? "{task}" : "{previous}";
209
- });
210
- }
211
-
212
- // =============================================================================
213
- // Parallel-Aware Template Resolution
214
- // =============================================================================
215
-
216
121
  /** Resolved templates for a chain - string for sequential, string[] for parallel */
217
122
  export type ResolvedTemplates = (string | string[])[];
218
123
 
@@ -220,9 +125,8 @@ export type ResolvedTemplates = (string | string[])[];
220
125
  * Resolve templates for a chain with parallel step support.
221
126
  * Returns string for sequential steps, string[] for parallel steps.
222
127
  */
223
- export function resolveChainTemplatesV2(
128
+ export function resolveChainTemplates(
224
129
  steps: ChainStep[],
225
- settings: SubagentSettings,
226
130
  ): ResolvedTemplates {
227
131
  return steps.map((step, i) => {
228
132
  if (isParallelStep(step)) {
@@ -241,43 +145,6 @@ export function resolveChainTemplatesV2(
241
145
  });
242
146
  }
243
147
 
244
- /**
245
- * Flatten templates for display (TUI navigation needs flat list)
246
- */
247
- export function flattenTemplates(templates: ResolvedTemplates): string[] {
248
- const result: string[] = [];
249
- for (const t of templates) {
250
- if (Array.isArray(t)) {
251
- result.push(...t);
252
- } else {
253
- result.push(t);
254
- }
255
- }
256
- return result;
257
- }
258
-
259
- /**
260
- * Unflatten templates back to structured form
261
- */
262
- export function unflattenTemplates(
263
- flat: string[],
264
- steps: ChainStep[],
265
- ): ResolvedTemplates {
266
- const result: ResolvedTemplates = [];
267
- let idx = 0;
268
- for (const step of steps) {
269
- if (isParallelStep(step)) {
270
- const count = step.parallel.length;
271
- result.push(flat.slice(idx, idx + count));
272
- idx += count;
273
- } else {
274
- result.push(flat[idx]!);
275
- idx++;
276
- }
277
- }
278
- return result;
279
- }
280
-
281
148
  // =============================================================================
282
149
  // Behavior Resolution
283
150
  // =============================================================================
@@ -311,20 +178,6 @@ export function resolveStepBehavior(
311
178
  return { output, reads, progress };
312
179
  }
313
180
 
314
- /**
315
- * Find index of first agent in chain that has progress enabled
316
- */
317
- export function findFirstProgressAgentIndex(
318
- agentConfigs: AgentConfig[],
319
- stepOverrides: StepOverrides[],
320
- ): number {
321
- return agentConfigs.findIndex((config, i) => {
322
- const override = stepOverrides[i];
323
- if (override?.progress !== undefined) return override.progress;
324
- return config.defaultProgress ?? false;
325
- });
326
- }
327
-
328
181
  // =============================================================================
329
182
  // Chain Instruction Injection
330
183
  // =============================================================================
@@ -472,21 +325,4 @@ export function aggregateParallelOutputs(results: ParallelTaskResult[]): string
472
325
  .join("\n\n");
473
326
  }
474
327
 
475
- /**
476
- * Check if any parallel task failed
477
- */
478
- export function hasParallelFailures(results: ParallelTaskResult[]): boolean {
479
- return results.some((r) => r.exitCode !== 0);
480
- }
481
328
 
482
- /**
483
- * Get failure summary for parallel step
484
- */
485
- export function getParallelFailureSummary(results: ParallelTaskResult[]): string {
486
- const failures = results.filter((r) => r.exitCode !== 0);
487
- if (failures.length === 0) return "";
488
-
489
- return failures
490
- .map((f) => `- Task ${f.taskIndex + 1} (${f.agent}): ${f.error || "failed"}`)
491
- .join("\n");
492
- }