pi-prompt-template-model 0.6.2 → 0.6.3

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.3] - 2026-03-21
6
+
7
+ ### Added
8
+ - Chain step progress now shows in the status bar (e.g., `step 2/3: simplify`) and persists until the chain completes, instead of only appearing as a notification that scrolls away.
9
+ - Added `parallel(...)` chain step support for delegated prompt templates, including parser/frontmatter validation, delegated task fan-out, aggregated result rendering, and per-task progress display.
10
+
11
+ ### Fixed
12
+ - Delegated subagent errors now show as clean notifications instead of extension crash messages with stack traces, matching how other validation errors (missing skill, missing template, etc.) are presented.
13
+ - When no subagent bridge is listening (extension not loaded, name collision with another extension), delegation now fails immediately with a diagnostic message instead of silently waiting 15 seconds before timing out.
14
+ - Timeout error message no longer dumps the full rendered prompt content; uses the agent name instead.
15
+ - Parallel delegated status now prefers aggregate `parallel X/Y running` updates over first-task tool labels, avoiding misleading `running <tool>` status lines during multi-task execution.
16
+
5
17
  ## [0.6.2] - 2026-03-20
6
18
 
7
19
  ### Added
package/README.md CHANGED
@@ -301,6 +301,16 @@ chain: double-check --loop 2 -> deslop --loop 2
301
301
 
302
302
  This registers the file's name as a command that runs `double-check` twice, then `deslop` twice. Per-step `--loop N` repeats that step before moving to the next, with per-step convergence (stops early if no changes, unless the step's template has `converge: false`).
303
303
 
304
+ Chain declarations also support parallel groups with `parallel(...)`:
305
+
306
+ ```markdown
307
+ ---
308
+ chain: parallel(scan-frontend, scan-backend) -> consolidate
309
+ ---
310
+ ```
311
+
312
+ Each entry inside `parallel(...)` runs as a delegated subagent task concurrently. Parallel entries can include per-step args (for example `parallel(scan-frontend, scan-backend "auth")`), but per-step `--loop` is not supported inside parallel groups. Nested `parallel(...)` is rejected. Parallel entries must be delegated templates (`subagent: ...` or runtime `--subagent` override), and all entries in the same parallel group must resolve to the same `inheritContext` mode and `cwd`.
313
+
304
314
  Steps with a `model` field use their own model. Steps without one inherit a snapshot of whatever model was active when the chain started — not the previous step's model. This keeps behavior deterministic regardless of what earlier steps do.
305
315
 
306
316
  Chain templates support `loop`, `fresh`, `converge`, `restore`, and `cwd` in their frontmatter for controlling the overall execution:
@@ -318,7 +328,15 @@ This runs the full analyze → fix chain 3 times, with fresh context between ite
318
328
 
319
329
  When a chain template sets `cwd`, it becomes the default delegated subprocess working directory for all delegated steps in that chain. Runtime `--cwd=<path>` overrides the chain template value.
320
330
 
321
- ### Looping chains from the CLI
331
+ ### Parallel and looping from the CLI
332
+
333
+ Parallel groups work in `/chain-prompts` too:
334
+
335
+ ```
336
+ /chain-prompts parallel(scan-fe, scan-be) -> review
337
+ ```
338
+
339
+ Looping applies to the entire chain:
322
340
 
323
341
  ```
324
342
  /chain-prompts analyze -> fix --loop 3
package/chain-parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { parseCommandArgs, splitByUnquotedSeparator } from "./args.js";
1
+ import { parseCommandArgs } from "./args.js";
2
2
 
3
3
  export interface ChainStep {
4
4
  name: string;
@@ -6,14 +6,20 @@ export interface ChainStep {
6
6
  loopCount?: number;
7
7
  }
8
8
 
9
+ export interface ParallelChainStep {
10
+ parallel: ChainStep[];
11
+ }
12
+
13
+ export type ChainStepOrParallel = ChainStep | ParallelChainStep;
14
+
9
15
  export interface ParsedChainSteps {
10
- steps: ChainStep[];
16
+ steps: ChainStepOrParallel[];
11
17
  sharedArgs: string[];
12
18
  invalidSegments: string[];
13
19
  }
14
20
 
15
21
  export interface ParsedChainDeclaration {
16
- steps: ChainStep[];
22
+ steps: ChainStepOrParallel[];
17
23
  invalidSegments: string[];
18
24
  }
19
25
 
@@ -121,27 +127,130 @@ function extractStepLoopCount(segment: string): { cleanedSegment: string; loopCo
121
127
  return { cleanedSegment: cleanedSegment.trim(), loopCount };
122
128
  }
123
129
 
130
+ function splitByTopLevelSeparator(input: string, separator: string): string[] {
131
+ const parts: string[] = [];
132
+ let start = 0;
133
+ let inQuote: string | null = null;
134
+ let parenDepth = 0;
135
+
136
+ for (let i = 0; i < input.length; i++) {
137
+ const char = input[i];
138
+ if (inQuote) {
139
+ if (char === inQuote) inQuote = null;
140
+ continue;
141
+ }
142
+
143
+ if (char === '"' || char === "'") {
144
+ inQuote = char;
145
+ continue;
146
+ }
147
+ if (char === "(") {
148
+ parenDepth++;
149
+ continue;
150
+ }
151
+ if (char === ")" && parenDepth > 0) {
152
+ parenDepth--;
153
+ continue;
154
+ }
155
+
156
+ if (parenDepth === 0 && i <= input.length - separator.length && input.startsWith(separator, i)) {
157
+ parts.push(input.slice(start, i));
158
+ start = i + separator.length;
159
+ i += separator.length - 1;
160
+ }
161
+ }
162
+
163
+ parts.push(input.slice(start));
164
+ return parts;
165
+ }
166
+
167
+ function findMatchingParen(segment: string, openIndex: number): number {
168
+ let inQuote: string | null = null;
169
+ let depth = 0;
170
+
171
+ for (let i = openIndex; i < segment.length; i++) {
172
+ const char = segment[i];
173
+ if (inQuote) {
174
+ if (char === inQuote) inQuote = null;
175
+ continue;
176
+ }
177
+
178
+ if (char === '"' || char === "'") {
179
+ inQuote = char;
180
+ continue;
181
+ }
182
+ if (char === "(") {
183
+ depth++;
184
+ continue;
185
+ }
186
+ if (char !== ")") continue;
187
+ depth--;
188
+ if (depth === 0) return i;
189
+ }
190
+
191
+ return -1;
192
+ }
193
+
194
+ function parseSingleStepSegment(segment: string): ChainStep | undefined {
195
+ const { cleanedSegment, loopCount } = extractStepLoopCount(segment);
196
+ const tokens = parseCommandArgs(cleanedSegment);
197
+ if (tokens.length === 0) return undefined;
198
+ return { name: tokens[0], args: tokens.slice(1), loopCount };
199
+ }
200
+
201
+ function parseParallelStepSegment(segment: string): ParallelChainStep | undefined {
202
+ if (!/^parallel\s*\(/.test(segment)) return undefined;
203
+ const openIndex = segment.indexOf("(");
204
+ if (openIndex < 0) return undefined;
205
+
206
+ const closeIndex = findMatchingParen(segment, openIndex);
207
+ if (closeIndex < 0) return undefined;
208
+ if (segment.slice(closeIndex + 1).trim().length > 0) return undefined;
209
+
210
+ const inner = segment.slice(openIndex + 1, closeIndex).trim();
211
+ if (!inner) return undefined;
212
+
213
+ const parsedSteps: ChainStep[] = [];
214
+ for (const rawEntry of splitByTopLevelSeparator(inner, ",")) {
215
+ const entry = rawEntry.trim();
216
+ if (!entry) return undefined;
217
+ if (/^parallel\s*\(/.test(entry)) return undefined;
218
+ const parsed = parseSingleStepSegment(entry);
219
+ if (!parsed) return undefined;
220
+ parsedSteps.push(parsed);
221
+ }
222
+
223
+ if (parsedSteps.length === 0) return undefined;
224
+ return { parallel: parsedSteps };
225
+ }
226
+
227
+ function parseChainSegment(segment: string): ChainStepOrParallel | undefined {
228
+ const parallelStep = parseParallelStepSegment(segment);
229
+ if (parallelStep) return parallelStep;
230
+ if (/^parallel\s*\(/.test(segment)) return undefined;
231
+ return parseSingleStepSegment(segment);
232
+ }
233
+
124
234
  export function parseChainSteps(args: string): ParsedChainSteps {
125
- const sharedArgsSplit = splitByUnquotedSeparator(args, " -- ");
235
+ const sharedArgsSplit = splitByTopLevelSeparator(args, " -- ");
126
236
  const templatesPart = sharedArgsSplit[0];
127
237
  const argsPart = sharedArgsSplit.length > 1 ? sharedArgsSplit.slice(1).join(" -- ") : "";
128
238
 
129
239
  const invalidSegments: string[] = [];
130
- const steps: ChainStep[] = [];
240
+ const steps: ChainStepOrParallel[] = [];
131
241
 
132
- for (const rawSegment of splitByUnquotedSeparator(templatesPart, "->")) {
242
+ for (const rawSegment of splitByTopLevelSeparator(templatesPart, "->")) {
133
243
  const segment = rawSegment.trim();
134
244
  if (!segment) {
135
245
  invalidSegments.push(rawSegment);
136
246
  continue;
137
247
  }
138
- const { cleanedSegment, loopCount } = extractStepLoopCount(segment);
139
- const tokens = parseCommandArgs(cleanedSegment);
140
- if (tokens.length === 0) {
248
+ const parsedSegment = parseChainSegment(segment);
249
+ if (!parsedSegment) {
141
250
  invalidSegments.push(segment);
142
251
  continue;
143
252
  }
144
- steps.push({ name: tokens[0], args: tokens.slice(1), loopCount });
253
+ steps.push(parsedSegment);
145
254
  }
146
255
 
147
256
  return { steps, sharedArgs: parseCommandArgs(argsPart), invalidSegments };
@@ -149,27 +258,20 @@ export function parseChainSteps(args: string): ParsedChainSteps {
149
258
 
150
259
  export function parseChainDeclaration(chain: string): ParsedChainDeclaration {
151
260
  const invalidSegments: string[] = [];
152
- const steps: ChainStep[] = [];
261
+ const steps: ChainStepOrParallel[] = [];
153
262
 
154
- for (const rawSegment of splitByUnquotedSeparator(chain, "->")) {
263
+ for (const rawSegment of splitByTopLevelSeparator(chain, "->")) {
155
264
  const segment = rawSegment.trim();
156
265
  if (!segment) {
157
266
  invalidSegments.push(rawSegment);
158
267
  continue;
159
268
  }
160
-
161
- const { cleanedSegment, loopCount } = extractStepLoopCount(segment);
162
- const tokens = parseCommandArgs(cleanedSegment);
163
- if (tokens.length === 0) {
269
+ const parsedSegment = parseChainSegment(segment);
270
+ if (!parsedSegment) {
164
271
  invalidSegments.push(segment);
165
272
  continue;
166
273
  }
167
-
168
- steps.push({
169
- name: tokens[0],
170
- args: tokens.slice(1),
171
- loopCount,
172
- });
274
+ steps.push(parsedSegment);
173
275
  }
174
276
 
175
277
  return { steps, invalidSegments };
package/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { Model } from "@mariozechner/pi-ai";
2
2
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
3
  import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
4
4
  import { extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
5
- import { parseChainSteps, parseChainDeclaration, type ChainStep } from "./chain-parser.js";
5
+ import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
6
6
  import { generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
7
7
  import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
8
8
  import { preparePromptExecution } from "./prompt-execution.js";
@@ -184,6 +184,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
184
184
  return prompt.subagent !== undefined || override?.enabled === true;
185
185
  }
186
186
 
187
+ function isParallelChainStep(step: ChainStepOrParallel): step is ParallelChainStep {
188
+ return "parallel" in step;
189
+ }
190
+
187
191
  async function executePromptStep(
188
192
  prompt: PromptWithModel,
189
193
  args: string[],
@@ -193,19 +197,25 @@ export default function promptModelExtension(pi: ExtensionAPI) {
193
197
  inheritedModel?: Model<any>,
194
198
  ): Promise<PromptStepResult | "aborted"> {
195
199
  if (shouldDelegatePrompt(prompt, override)) {
196
- const delegated = await executeSubagentPromptStep({
197
- pi,
198
- prompt,
199
- args,
200
- ctx,
201
- currentModel,
202
- override,
203
- inheritedModel,
204
- });
205
- if (!delegated) {
206
- throw new Error(`Prompt \`${prompt.name}\` is not configured for delegated execution.`);
200
+ try {
201
+ const delegated = await executeSubagentPromptStep({
202
+ pi,
203
+ prompt,
204
+ args,
205
+ ctx,
206
+ currentModel,
207
+ override,
208
+ inheritedModel,
209
+ });
210
+ if (!delegated) {
211
+ notify(ctx, `Prompt \`${prompt.name}\` is not configured for delegated execution.`, "error");
212
+ return "aborted";
213
+ }
214
+ return { changed: delegated.changed };
215
+ } catch (error) {
216
+ notify(ctx, error instanceof Error ? error.message : String(error), "error");
217
+ return "aborted";
207
218
  }
208
- return { changed: delegated.changed };
209
219
  }
210
220
 
211
221
  const prepared =
@@ -477,7 +487,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
477
487
  }
478
488
 
479
489
  async function runSharedChainExecution(
480
- steps: ChainStep[],
490
+ steps: ChainStepOrParallel[],
481
491
  sharedArgs: string[],
482
492
  totalIterations: number | null,
483
493
  fresh: boolean,
@@ -487,14 +497,47 @@ export default function promptModelExtension(pi: ExtensionAPI) {
487
497
  subagentOverride?: SubagentOverride,
488
498
  cwdOverride?: string,
489
499
  ) {
500
+ const flattenChainSteps = (): ChainStep[] => {
501
+ const flattened: ChainStep[] = [];
502
+ for (const step of steps) {
503
+ if (isParallelChainStep(step)) {
504
+ flattened.push(...step.parallel);
505
+ } else {
506
+ flattened.push(step);
507
+ }
508
+ }
509
+ return flattened;
510
+ };
511
+
490
512
  const validateChainSteps = (): boolean => {
491
- const missingTemplates = steps.filter((step) => !prompts.has(step.name));
513
+ const flattened = flattenChainSteps();
514
+ const missingTemplates = flattened.filter((step) => !prompts.has(step.name));
492
515
  if (missingTemplates.length > 0) {
493
516
  notify(ctx, `Templates not found: ${missingTemplates.map((step) => step.name).join(", ")}`, "error");
494
517
  return false;
495
518
  }
496
519
 
497
520
  for (const step of steps) {
521
+ if (isParallelChainStep(step)) {
522
+ for (const parallelStep of step.parallel) {
523
+ if (parallelStep.loopCount !== undefined) {
524
+ notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --loop.`, "error");
525
+ return false;
526
+ }
527
+ const stepPrompt = prompts.get(parallelStep.name);
528
+ if (!stepPrompt) continue;
529
+ if (stepPrompt.chain) {
530
+ notify(ctx, `Step "${parallelStep.name}" is a chain template. Chain nesting is not supported.`, "error");
531
+ return false;
532
+ }
533
+ if (!shouldDelegatePrompt(stepPrompt, subagentOverride)) {
534
+ notify(ctx, `Step "${parallelStep.name}" in parallel() must use delegated execution (subagent).`, "error");
535
+ return false;
536
+ }
537
+ }
538
+ continue;
539
+ }
540
+
498
541
  const stepPrompt = prompts.get(step.name);
499
542
  if (!stepPrompt) continue;
500
543
  if (stepPrompt.chain) {
@@ -509,7 +552,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
509
552
  if (!validateChainSteps()) return;
510
553
 
511
554
  const originalModel = getCurrentModel(ctx);
512
- const chainInheritedModel = getCurrentModel(ctx);
555
+ const chainInheritedModel = originalModel;
513
556
  const originalThinking = pi.getThinkingLevel();
514
557
  let currentModel = originalModel;
515
558
  let currentThinking = originalThinking;
@@ -520,7 +563,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
520
563
  const useConverge = isUnlimited ? true : converge;
521
564
 
522
565
  const anchorId = fresh ? ctx.sessionManager.getLeafId() : null;
523
- const chainStepNames = steps.map((step) => step.name).join(" -> ");
566
+ const chainStepNames = steps
567
+ .map((step) => (isParallelChainStep(step) ? `parallel(${step.parallel.map((item) => item.name).join(", ")})` : step.name))
568
+ .join(" -> ");
524
569
  let completedIterations = 0;
525
570
  let converged = false;
526
571
  let chainErrorState: ExecutionErrorState = { hasError: false, error: undefined };
@@ -539,17 +584,77 @@ export default function promptModelExtension(pi: ExtensionAPI) {
539
584
  if (!validateChainSteps()) break;
540
585
  }
541
586
 
542
- const templates = steps.map((step) => ({
543
- ...prompts.get(step.name)!,
544
- ...(cwdOverride ? { cwd: cwdOverride } : {}),
545
- stepArgs: step.args,
546
- stepLoop: step.loopCount ?? 1,
547
- }));
587
+ const templates = steps.map((step) =>
588
+ isParallelChainStep(step)
589
+ ? {
590
+ kind: "parallel" as const,
591
+ tasks: step.parallel.map((item) => ({
592
+ name: item.name,
593
+ args: item.args,
594
+ prompt: {
595
+ ...prompts.get(item.name)!,
596
+ ...(cwdOverride ? { cwd: cwdOverride } : {}),
597
+ },
598
+ })),
599
+ }
600
+ : {
601
+ kind: "single" as const,
602
+ template: {
603
+ ...prompts.get(step.name)!,
604
+ ...(cwdOverride ? { cwd: cwdOverride } : {}),
605
+ stepArgs: step.args,
606
+ stepLoop: step.loopCount ?? 1,
607
+ },
608
+ },
609
+ );
548
610
  let aborted = false;
549
611
  let iterationChanged = false;
612
+ let loopPrefix = "";
613
+ if (effectiveMax > 1) {
614
+ const label = totalIterations !== null ? `${iteration + 1}/${totalIterations}` : `${iteration + 1}`;
615
+ loopPrefix = `Loop ${label}, `;
616
+ }
550
617
 
551
- for (const [index, template] of templates.entries()) {
618
+ for (const [index, stepTemplate] of templates.entries()) {
552
619
  const stepNumber = index + 1;
620
+ if (stepTemplate.kind === "parallel") {
621
+ const stepNames = stepTemplate.tasks.map((task) => task.name).join(", ");
622
+ notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: parallel(${stepNames})`, "info");
623
+ if (ctx.hasUI) {
624
+ ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: parallel(${stepNames})`));
625
+ }
626
+
627
+ let delegated;
628
+ try {
629
+ delegated = await executeSubagentPromptStep({
630
+ pi,
631
+ ctx,
632
+ currentModel,
633
+ override: subagentOverride,
634
+ inheritedModel: chainInheritedModel,
635
+ parallel: stepTemplate.tasks.map((task) => ({
636
+ prompt: task.prompt,
637
+ args: task.args.length > 0 ? task.args : sharedArgs,
638
+ })),
639
+ });
640
+ } catch (error) {
641
+ notify(ctx, error instanceof Error ? error.message : String(error), "error");
642
+ aborted = true;
643
+ break;
644
+ }
645
+ if (!delegated) {
646
+ notify(ctx, "Parallel chain step was not delegated.", "error");
647
+ aborted = true;
648
+ break;
649
+ }
650
+
651
+ currentModel = getCurrentModel(ctx);
652
+ currentThinking = pi.getThinkingLevel();
653
+ if (delegated.changed) iterationChanged = true;
654
+ continue;
655
+ }
656
+
657
+ const template = stepTemplate.template;
553
658
  const stepLoop = template.stepLoop;
554
659
  const effectiveArgs = template.stepArgs.length > 0 ? template.stepArgs : sharedArgs;
555
660
  const outerLoopState = loopState ? { ...loopState } : null;
@@ -565,14 +670,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
565
670
  updateLoopStatus(ctx);
566
671
  }
567
672
 
568
- const loopPrefix =
569
- effectiveMax > 1
570
- ? totalIterations !== null
571
- ? `Loop ${iteration + 1}/${totalIterations}, `
572
- : `Loop ${iteration + 1}, `
573
- : "";
574
673
  const iterSuffix = stepLoop > 1 ? ` (iter ${stepIteration + 1}/${stepLoop})` : "";
575
674
  notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: ${template.name}${iterSuffix} ${buildPromptCommandDescription(template)}`, "info");
675
+ if (ctx.hasUI) {
676
+ ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${template.name}`));
677
+ }
576
678
 
577
679
  const stepIterationStartId = ctx.sessionManager.getLeafId();
578
680
  const stepResult = await executePromptStep(
@@ -648,6 +750,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
648
750
  freshCollapse = null;
649
751
  accumulatedSummaries = [];
650
752
  updateLoopStatus(ctx);
753
+ if (ctx.hasUI) {
754
+ ctx.ui.setStatus("prompt-chain", undefined);
755
+ }
651
756
 
652
757
  if (!chainErrorState.hasError) {
653
758
  notifyLoopCompletion(ctx, completedIterations, totalIterations, effectiveMax, converged, true);
package/loop-utils.ts CHANGED
@@ -4,6 +4,7 @@ import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.js";
4
4
 
5
5
  interface DelegatedMessageDetails {
6
6
  messages?: Message[];
7
+ parallelResults?: Array<{ messages?: Message[] }>;
7
8
  }
8
9
 
9
10
  function collectAssistantActions(messages: Message[], filesRead: Set<string>, filesWritten: Set<string>): { commandCount: number; lastText: string } {
@@ -55,10 +56,16 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
55
56
  }
56
57
 
57
58
  const delegated = delegatedDetails(entry);
58
- if (!delegated?.messages) continue;
59
- const collected = collectAssistantActions(delegated.messages, filesRead, filesWritten);
60
- commandCount += collected.commandCount;
61
- if (collected.lastText) lastAssistantText = collected.lastText;
59
+ if (!delegated) continue;
60
+ const messageGroups =
61
+ delegated.parallelResults && delegated.parallelResults.length > 0
62
+ ? delegated.parallelResults.map((result) => result.messages ?? [])
63
+ : delegated.messages ? [delegated.messages] : [];
64
+ for (const messages of messageGroups) {
65
+ const collected = collectAssistantActions(messages, filesRead, filesWritten);
66
+ commandCount += collected.commandCount;
67
+ if (collected.lastText) lastAssistantText = collected.lastText;
68
+ }
62
69
  }
63
70
 
64
71
  let summary = totalIterations !== null ? `[Loop iteration ${iteration}/${totalIterations}]\nTask: "${task}"` : `[Loop iteration ${iteration}]\nTask: "${task}"`;
@@ -92,12 +99,18 @@ export function didIterationMakeChanges(entries: SessionEntry[]): boolean {
92
99
  }
93
100
 
94
101
  const delegated = delegatedDetails(entry);
95
- if (!delegated?.messages) continue;
96
- for (const message of delegated.messages) {
97
- if (message.role !== "assistant") continue;
98
- for (const block of (message as AssistantMessage).content) {
99
- if (block.type !== "toolCall") continue;
100
- if (block.name === "write" || block.name === "edit") return true;
102
+ if (!delegated) continue;
103
+ const delegatedGroups =
104
+ delegated.parallelResults && delegated.parallelResults.length > 0
105
+ ? delegated.parallelResults.map((result) => result.messages ?? [])
106
+ : [delegated.messages ?? []];
107
+ for (const messages of delegatedGroups) {
108
+ for (const message of messages) {
109
+ if (message.role !== "assistant") continue;
110
+ for (const block of (message as AssistantMessage).content) {
111
+ if (block.type !== "toolCall") continue;
112
+ if (block.name === "write" || block.name === "edit") return true;
113
+ }
101
114
  }
102
115
  }
103
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "type": "module",
5
5
  "description": "Prompt template model selector extension for pi coding agent",
6
6
  "author": "Nico Bailon",
package/prompt-loader.ts CHANGED
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
5
5
  import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
6
+ import { parseChainDeclaration } from "./chain-parser.js";
6
7
 
7
8
  const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
8
9
  export const RESERVED_COMMAND_NAMES = new Set([
@@ -541,6 +542,20 @@ function loadPromptsWithModelFromDir(
541
542
  if (!frontmatter) continue;
542
543
  const { body } = parsed;
543
544
  const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics);
545
+ if (chain && /\bparallel\s*\(/.test(chain)) {
546
+ const parsedChain = parseChainDeclaration(chain);
547
+ if (parsedChain.invalidSegments.length > 0 || parsedChain.steps.length === 0) {
548
+ diagnostics.push(
549
+ createDiagnostic(
550
+ "invalid-chain-declaration",
551
+ fullPath,
552
+ source,
553
+ `Skipping prompt template at ${fullPath}: invalid chain declaration segment ${JSON.stringify(parsedChain.invalidSegments[0] ?? chain)}.`,
554
+ ),
555
+ );
556
+ continue;
557
+ }
558
+ }
544
559
  let subagent = normalizeSubagent(frontmatter.subagent, fullPath, source, diagnostics);
545
560
  const cwd = normalizeCwd(frontmatter.cwd, fullPath, source, diagnostics);
546
561
  const inheritContext = normalizeInheritContext(frontmatter.inheritContext, fullPath, source, diagnostics);