pi-prompt-template-model 0.6.3 → 0.6.5

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.5] - 2026-03-24
6
+
7
+ ### Added
8
+ - Added delegated chain-step context summaries via `chainContext: summary` (chain frontmatter), `/chain-prompts ... --chain-context` (command-level), and per-step `--with-context` (single delegated chain steps).
9
+
10
+ ## [0.6.4] - 2026-03-23
11
+
12
+ ### Fixed
13
+ - Updated skill command resolution to use `sourceInfo.path` instead of the removed `path` field on `SlashCommandInfo`, fixing compatibility with pi-coding-agent 0.62.0 source provenance changes.
14
+
5
15
  ## [0.6.3] - 2026-03-21
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -62,6 +62,7 @@ All fields are optional. Templates that don't use any extension features (no `mo
62
62
  | `thinking` | — | Thinking level for the model: `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`. |
63
63
  | `description` | — | Short text shown next to the command in autocomplete. |
64
64
  | `chain` | — | Declares a reusable pipeline of templates (`step -> step`). When set, the body is ignored. See [Chain Templates](#chain-templates). |
65
+ | `chainContext` | — | Chain templates only. Set to `summary` so delegated steps receive a compact summary of what previous steps did. Steps with `inheritContext: true` are excluded. See [Chain context for delegated steps](#chain-context-for-delegated-steps). |
65
66
 
66
67
  ### Execution Control
67
68
 
@@ -328,6 +329,33 @@ This runs the full analyze → fix chain 3 times, with fresh context between ite
328
329
 
329
330
  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.
330
331
 
332
+ ### Chain context for delegated steps
333
+
334
+ Delegated chain steps start fresh — they don't see what earlier steps did. Chain context prepends a compact summary of previous steps to each delegated task so later steps can build on earlier work.
335
+
336
+ Enable it chain-wide with `chainContext: summary` in frontmatter or `--chain-context` on the CLI:
337
+
338
+ ```markdown
339
+ ---
340
+ chain: analyze -> fix
341
+ chainContext: summary
342
+ ---
343
+ ```
344
+
345
+ ```
346
+ /chain-prompts analyze -> fix --chain-context
347
+ ```
348
+
349
+ To enable it for a single step, attach `--with-context` to that step name:
350
+
351
+ ```
352
+ /chain-prompts analyze -> reviewer --with-context -> summarize
353
+ ```
354
+
355
+ Here only `reviewer` receives the summary of `analyze`. The `summarize` step does not.
356
+
357
+ Steps using `inheritContext: true` already fork the full parent conversation and skip the summary preamble. `--with-context` is not supported inside `parallel(...)` groups. When a chain uses `loop`, summaries reset each iteration.
358
+
331
359
  ### Parallel and looping from the CLI
332
360
 
333
361
  Parallel groups work in `/chain-prompts` too:
@@ -359,6 +387,7 @@ Once enabled, the agent sees `run-prompt` in its tool list:
359
387
 
360
388
  ```
361
389
  run-prompt({ command: "deslop --loop 5 --fresh" })
390
+ run-prompt({ command: "chain-prompts analyze -> fix --chain-context" })
362
391
  run-prompt({ command: "chain-prompts analyze -> fix --loop 3" })
363
392
  run-prompt({ command: "deslop --subagent" })
364
393
  ```
package/args.ts CHANGED
@@ -163,6 +163,50 @@ export function extractLoopFlags(argsString: string): LoopFlags {
163
163
  return { args: cleaned.trim(), fresh, converge: !noConverge };
164
164
  }
165
165
 
166
+ export function extractChainContextFlag(argsString: string): { args: string; chainContext: boolean } {
167
+ let chainContext = false;
168
+ const tokensToRemove: Array<{ start: number; end: number }> = [];
169
+
170
+ let i = 0;
171
+ while (i < argsString.length) {
172
+ const char = argsString[i];
173
+
174
+ if (char === '"' || char === "'") {
175
+ const quote = char;
176
+ i++;
177
+ while (i < argsString.length && argsString[i] !== quote) i++;
178
+ if (i < argsString.length) i++;
179
+ continue;
180
+ }
181
+
182
+ if (/\s/.test(char)) {
183
+ i++;
184
+ continue;
185
+ }
186
+
187
+ const tokenStart = i;
188
+ while (i < argsString.length && !/\s/.test(argsString[i])) i++;
189
+ const token = argsString.slice(tokenStart, i);
190
+
191
+ if (token === "--chain-context") {
192
+ chainContext = true;
193
+ tokensToRemove.push({ start: tokenStart, end: i });
194
+ }
195
+ }
196
+
197
+ if (tokensToRemove.length === 0) {
198
+ return { args: argsString.trim(), chainContext: false };
199
+ }
200
+
201
+ tokensToRemove.sort((a, b) => b.start - a.start);
202
+ let cleaned = argsString;
203
+ for (const { start, end } of tokensToRemove) {
204
+ cleaned = cleaned.slice(0, start) + cleaned.slice(end);
205
+ }
206
+
207
+ return { args: cleaned.trim(), chainContext };
208
+ }
209
+
166
210
  export function extractSubagentOverride(argsString: string): SubagentOverrideExtraction {
167
211
  let override: SubagentOverride | undefined;
168
212
  let cwdRaw: string | undefined;
package/chain-parser.ts CHANGED
@@ -4,6 +4,7 @@ export interface ChainStep {
4
4
  name: string;
5
5
  args: string[];
6
6
  loopCount?: number;
7
+ withContext?: boolean;
7
8
  }
8
9
 
9
10
  export interface ParallelChainStep {
@@ -80,15 +81,23 @@ function scanSegmentTokens(segment: string): SegmentToken[] {
80
81
  return tokens;
81
82
  }
82
83
 
83
- function extractStepLoopCount(segment: string): { cleanedSegment: string; loopCount?: number } {
84
+ function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number; withContext: boolean } {
84
85
  const tokens = scanSegmentTokens(segment);
85
86
  const loopTokenRanges: Array<{ start: number; end: number }> = [];
87
+ const withContextTokenRanges: Array<{ start: number; end: number }> = [];
86
88
  let loopCount: number | undefined;
89
+ let withContext = false;
87
90
 
88
91
  for (let i = 1; i < tokens.length; i++) {
89
92
  const token = tokens[i];
90
93
  if (token.quoted) continue;
91
94
 
95
+ if (token.value === "--with-context") {
96
+ withContext = true;
97
+ withContextTokenRanges.push({ start: token.start, end: token.end });
98
+ continue;
99
+ }
100
+
92
101
  if (token.value.startsWith("--loop=")) {
93
102
  loopTokenRanges.push({ start: token.start, end: token.end });
94
103
  const value = token.value.slice("--loop=".length);
@@ -114,17 +123,18 @@ function extractStepLoopCount(segment: string): { cleanedSegment: string; loopCo
114
123
  }
115
124
  }
116
125
 
117
- if (loopCount === undefined || loopTokenRanges.length === 0) {
118
- return { cleanedSegment: segment };
126
+ const loopRangesToRemove = loopCount !== undefined ? loopTokenRanges : [];
127
+ if (loopRangesToRemove.length === 0 && withContextTokenRanges.length === 0) {
128
+ return { cleanedSegment: segment, withContext: false };
119
129
  }
120
130
 
121
- loopTokenRanges.sort((a, b) => b.start - a.start);
131
+ const rangesToRemove = [...loopRangesToRemove, ...withContextTokenRanges].sort((a, b) => b.start - a.start);
122
132
  let cleanedSegment = segment;
123
- for (const { start, end } of loopTokenRanges) {
133
+ for (const { start, end } of rangesToRemove) {
124
134
  cleanedSegment = `${cleanedSegment.slice(0, start)}${cleanedSegment.slice(end)}`;
125
135
  }
126
136
 
127
- return { cleanedSegment: cleanedSegment.trim(), loopCount };
137
+ return { cleanedSegment: cleanedSegment.trim(), loopCount, withContext };
128
138
  }
129
139
 
130
140
  function splitByTopLevelSeparator(input: string, separator: string): string[] {
@@ -192,10 +202,10 @@ function findMatchingParen(segment: string, openIndex: number): number {
192
202
  }
193
203
 
194
204
  function parseSingleStepSegment(segment: string): ChainStep | undefined {
195
- const { cleanedSegment, loopCount } = extractStepLoopCount(segment);
205
+ const { cleanedSegment, loopCount, withContext } = extractStepFlags(segment);
196
206
  const tokens = parseCommandArgs(cleanedSegment);
197
207
  if (tokens.length === 0) return undefined;
198
- return { name: tokens[0], args: tokens.slice(1), loopCount };
208
+ return { name: tokens[0], args: tokens.slice(1), loopCount, ...(withContext ? { withContext: true } : {}) };
199
209
  }
200
210
 
201
211
  function parseParallelStepSegment(segment: string): ParallelChainStep | undefined {
package/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  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
- import { extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
4
+ import { extractChainContextFlag, extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
5
5
  import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
6
- import { generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
6
+ import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
7
7
  import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
8
8
  import { preparePromptExecution } from "./prompt-execution.js";
9
9
  import { buildPromptCommandDescription, expandCwdPath, loadPromptsWithModel, readSkillContent, resolveSkillPath, type PromptWithModel } from "./prompt-loader.js";
@@ -131,9 +131,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
131
131
 
132
132
  for (const command of pi.getCommands()) {
133
133
  if (command.source !== "skill") continue;
134
- if (!command.path) continue;
134
+ if (!command.sourceInfo.path) continue;
135
135
  if (!candidates.has(command.name)) continue;
136
- return command.path;
136
+ return command.sourceInfo.path;
137
137
  }
138
138
 
139
139
  return undefined;
@@ -195,6 +195,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
195
195
  currentModel: Model<any> | undefined,
196
196
  override?: SubagentOverride,
197
197
  inheritedModel?: Model<any>,
198
+ taskPreamble?: string,
198
199
  ): Promise<PromptStepResult | "aborted"> {
199
200
  if (shouldDelegatePrompt(prompt, override)) {
200
201
  try {
@@ -206,6 +207,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
206
207
  currentModel,
207
208
  override,
208
209
  inheritedModel,
210
+ taskPreamble,
209
211
  });
210
212
  if (!delegated) {
211
213
  notify(ctx, `Prompt \`${prompt.name}\` is not configured for delegated execution.`, "error");
@@ -496,6 +498,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
496
498
  ctx: ExtensionCommandContext,
497
499
  subagentOverride?: SubagentOverride,
498
500
  cwdOverride?: string,
501
+ chainContextEnabled = false,
499
502
  ) {
500
503
  const flattenChainSteps = (): ChainStep[] => {
501
504
  const flattened: ChainStep[] = [];
@@ -524,6 +527,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
524
527
  notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --loop.`, "error");
525
528
  return false;
526
529
  }
530
+ if (parallelStep.withContext === true) {
531
+ notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --with-context.`, "error");
532
+ return false;
533
+ }
527
534
  const stepPrompt = prompts.get(parallelStep.name);
528
535
  if (!stepPrompt) continue;
529
536
  if (stepPrompt.chain) {
@@ -599,14 +606,18 @@ export default function promptModelExtension(pi: ExtensionAPI) {
599
606
  }
600
607
  : {
601
608
  kind: "single" as const,
602
- template: {
603
- ...prompts.get(step.name)!,
604
- ...(cwdOverride ? { cwd: cwdOverride } : {}),
609
+ singleStep: {
610
+ prompt: {
611
+ ...prompts.get(step.name)!,
612
+ ...(cwdOverride ? { cwd: cwdOverride } : {}),
613
+ },
605
614
  stepArgs: step.args,
606
615
  stepLoop: step.loopCount ?? 1,
616
+ stepWithContext: step.withContext === true,
607
617
  },
608
618
  },
609
619
  );
620
+ const chainStepSummaries: string[] = [];
610
621
  let aborted = false;
611
622
  let iterationChanged = false;
612
623
  let loopPrefix = "";
@@ -619,10 +630,17 @@ export default function promptModelExtension(pi: ExtensionAPI) {
619
630
  const stepNumber = index + 1;
620
631
  if (stepTemplate.kind === "parallel") {
621
632
  const stepNames = stepTemplate.tasks.map((task) => task.name).join(", ");
633
+ const stepLabel = `parallel(${stepNames})`;
622
634
  notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: parallel(${stepNames})`, "info");
623
635
  if (ctx.hasUI) {
624
636
  ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: parallel(${stepNames})`));
625
637
  }
638
+ const stepStartId = ctx.sessionManager.getLeafId();
639
+ let taskPreamble: string | undefined;
640
+ const isForkedParallelContext = stepTemplate.tasks.some((task) => task.prompt.inheritContext === true);
641
+ if (chainContextEnabled && !isForkedParallelContext && chainStepSummaries.length > 0) {
642
+ taskPreamble = `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`;
643
+ }
626
644
 
627
645
  let delegated;
628
646
  try {
@@ -636,6 +654,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
636
654
  prompt: task.prompt,
637
655
  args: task.args.length > 0 ? task.args : sharedArgs,
638
656
  })),
657
+ taskPreamble,
639
658
  });
640
659
  } catch (error) {
641
660
  notify(ctx, error instanceof Error ? error.message : String(error), "error");
@@ -650,14 +669,21 @@ export default function promptModelExtension(pi: ExtensionAPI) {
650
669
 
651
670
  currentModel = getCurrentModel(ctx);
652
671
  currentThinking = pi.getThinkingLevel();
653
- if (delegated.changed) iterationChanged = true;
672
+ const stepEntries = getIterationEntries(ctx, stepStartId);
673
+ if (didIterationMakeChanges(stepEntries)) iterationChanged = true;
674
+ chainStepSummaries.push(generateChainStepSummary(stepEntries, stepLabel, stepNumber));
654
675
  continue;
655
676
  }
656
677
 
657
- const template = stepTemplate.template;
658
- const stepLoop = template.stepLoop;
659
- const effectiveArgs = template.stepArgs.length > 0 ? template.stepArgs : sharedArgs;
678
+ const singleStep = stepTemplate.singleStep;
679
+ const stepLoop = singleStep.stepLoop;
680
+ const effectiveArgs = singleStep.stepArgs.length > 0 ? singleStep.stepArgs : sharedArgs;
681
+ const shouldInjectSummary =
682
+ shouldDelegatePrompt(singleStep.prompt, subagentOverride) &&
683
+ singleStep.prompt.inheritContext !== true &&
684
+ (chainContextEnabled || singleStep.stepWithContext === true);
660
685
  const outerLoopState = loopState ? { ...loopState } : null;
686
+ const stepStartId = ctx.sessionManager.getLeafId();
661
687
  if (stepLoop > 1) {
662
688
  loopState = { currentIteration: 1, totalIterations: stepLoop };
663
689
  updateLoopStatus(ctx);
@@ -671,19 +697,27 @@ export default function promptModelExtension(pi: ExtensionAPI) {
671
697
  }
672
698
 
673
699
  const iterSuffix = stepLoop > 1 ? ` (iter ${stepIteration + 1}/${stepLoop})` : "";
674
- notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: ${template.name}${iterSuffix} ${buildPromptCommandDescription(template)}`, "info");
700
+ notify(
701
+ ctx,
702
+ `${loopPrefix}Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix} ${buildPromptCommandDescription(singleStep.prompt)}`,
703
+ "info",
704
+ );
675
705
  if (ctx.hasUI) {
676
- ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${template.name}`));
706
+ ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}`));
677
707
  }
708
+ const taskPreamble = shouldInjectSummary && chainStepSummaries.length > 0
709
+ ? `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`
710
+ : undefined;
678
711
 
679
712
  const stepIterationStartId = ctx.sessionManager.getLeafId();
680
713
  const stepResult = await executePromptStep(
681
- template,
714
+ singleStep.prompt,
682
715
  effectiveArgs,
683
716
  ctx,
684
717
  currentModel,
685
718
  subagentOverride,
686
719
  chainInheritedModel,
720
+ taskPreamble,
687
721
  );
688
722
  if (stepResult === "aborted") {
689
723
  aborted = true;
@@ -693,11 +727,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
693
727
  currentModel = getCurrentModel(ctx);
694
728
  currentThinking = pi.getThinkingLevel();
695
729
 
696
- const stepChanged = shouldDelegatePrompt(template, subagentOverride)
697
- ? stepResult.changed
698
- : didIterationMakeChanges(getIterationEntries(ctx, stepIterationStartId));
699
- if (stepChanged) iterationChanged = true;
700
- if (stepLoop > 1 && template.converge !== false && !stepChanged) {
730
+ const stepIterationEntries = getIterationEntries(ctx, stepIterationStartId);
731
+ const stepIterationChanged = didIterationMakeChanges(stepIterationEntries);
732
+ if (stepLoop > 1 && singleStep.prompt.converge !== false && !stepIterationChanged) {
701
733
  break;
702
734
  }
703
735
  }
@@ -709,6 +741,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
709
741
  }
710
742
 
711
743
  if (aborted) break;
744
+ const stepEntries = getIterationEntries(ctx, stepStartId);
745
+ if (didIterationMakeChanges(stepEntries)) iterationChanged = true;
746
+ chainStepSummaries.push(generateChainStepSummary(stepEntries, singleStep.prompt.name, stepNumber));
712
747
  }
713
748
 
714
749
  if (aborted) break;
@@ -782,11 +817,13 @@ export default function promptModelExtension(pi: ExtensionAPI) {
782
817
  const argsWithoutSubagent = subagent.args;
783
818
 
784
819
  if (prompt.chain) {
785
- const loop = extractLoopCount(argsWithoutSubagent);
820
+ const extracted = extractChainContextFlag(argsWithoutSubagent);
821
+ const chainContextEnabled = extracted.chainContext || prompt.chainContext === "summary";
822
+ const loop = extractLoopCount(extracted.args);
786
823
  let totalIterations: number | null = prompt.loop ?? 1;
787
824
  let fresh = false;
788
825
  let converge = true;
789
- let cleanedArgs = argsWithoutSubagent;
826
+ let cleanedArgs = extracted.args;
790
827
 
791
828
  if (loop) {
792
829
  totalIterations = loop.loopCount;
@@ -794,7 +831,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
794
831
  converge = loop.converge;
795
832
  cleanedArgs = loop.args;
796
833
  } else if (prompt.loop !== undefined) {
797
- const flags = extractLoopFlags(argsWithoutSubagent);
834
+ const flags = extractLoopFlags(extracted.args);
798
835
  fresh = flags.fresh;
799
836
  converge = flags.converge;
800
837
  cleanedArgs = flags.args;
@@ -821,6 +858,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
821
858
  ctx,
822
859
  subagent.override,
823
860
  cwdOverride,
861
+ chainContextEnabled,
824
862
  );
825
863
  return;
826
864
  }
@@ -962,8 +1000,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
962
1000
  notify(ctx, `Invalid --cwd path: must be absolute`, "error");
963
1001
  return;
964
1002
  }
965
- const loop = extractLoopCount(subagent.args);
966
- const cleanedArgs = loop ? loop.args : subagent.args;
1003
+ const extracted = extractChainContextFlag(subagent.args);
1004
+ const loop = extractLoopCount(extracted.args);
1005
+ const cleanedArgs = loop ? loop.args : extracted.args;
967
1006
 
968
1007
  const { steps, sharedArgs, invalidSegments } = parseChainSteps(cleanedArgs);
969
1008
  if (invalidSegments.length > 0) {
@@ -985,6 +1024,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
985
1024
  ctx,
986
1025
  subagent.override,
987
1026
  runtimeCwd,
1027
+ extracted.chainContext,
988
1028
  );
989
1029
  }
990
1030
 
package/loop-utils.ts CHANGED
@@ -7,6 +7,13 @@ interface DelegatedMessageDetails {
7
7
  parallelResults?: Array<{ messages?: Message[] }>;
8
8
  }
9
9
 
10
+ interface CollectedSummaryData {
11
+ filesRead: Set<string>;
12
+ filesWritten: Set<string>;
13
+ commandCount: number;
14
+ lastAssistantText: string;
15
+ }
16
+
10
17
  function collectAssistantActions(messages: Message[], filesRead: Set<string>, filesWritten: Set<string>): { commandCount: number; lastText: string } {
11
18
  let commandCount = 0;
12
19
  let lastText = "";
@@ -39,7 +46,7 @@ function delegatedDetails(entry: SessionEntry): DelegatedMessageDetails | undefi
39
46
  return entry.details as DelegatedMessageDetails;
40
47
  }
41
48
 
42
- export function generateIterationSummary(entries: SessionEntry[], task: string, iteration: number, totalIterations: number | null): string {
49
+ function collectSummaryData(entries: SessionEntry[]): CollectedSummaryData {
43
50
  const filesRead = new Set<string>();
44
51
  const filesWritten = new Set<string>();
45
52
  let commandCount = 0;
@@ -68,7 +75,18 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
68
75
  }
69
76
  }
70
77
 
71
- let summary = totalIterations !== null ? `[Loop iteration ${iteration}/${totalIterations}]\nTask: "${task}"` : `[Loop iteration ${iteration}]\nTask: "${task}"`;
78
+ return {
79
+ filesRead,
80
+ filesWritten,
81
+ commandCount,
82
+ lastAssistantText,
83
+ };
84
+ }
85
+
86
+ function formatSummary(header: string, entries: SessionEntry[]): string {
87
+ const { filesRead, filesWritten, commandCount, lastAssistantText } = collectSummaryData(entries);
88
+
89
+ let summary = header;
72
90
 
73
91
  const actionParts: string[] = [];
74
92
  if (filesRead.size > 0) actionParts.push(`read ${filesRead.size} file(s)`);
@@ -87,6 +105,17 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
87
105
  return summary;
88
106
  }
89
107
 
108
+ export function generateIterationSummary(entries: SessionEntry[], task: string, iteration: number, totalIterations: number | null): string {
109
+ const header = totalIterations !== null
110
+ ? `[Loop iteration ${iteration}/${totalIterations}]\nTask: "${task}"`
111
+ : `[Loop iteration ${iteration}]\nTask: "${task}"`;
112
+ return formatSummary(header, entries);
113
+ }
114
+
115
+ export function generateChainStepSummary(entries: SessionEntry[], stepLabel: string, stepNumber: number): string {
116
+ return formatSummary(`Step ${stepNumber} — ${stepLabel}:`, entries);
117
+ }
118
+
90
119
  export function didIterationMakeChanges(entries: SessionEntry[]): boolean {
91
120
  for (const entry of entries) {
92
121
  if (entry.type === "message") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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
@@ -38,6 +38,7 @@ export interface PromptWithModel {
38
38
  content: string;
39
39
  models: string[];
40
40
  chain?: string;
41
+ chainContext?: "summary";
41
42
  restore: boolean;
42
43
  skill?: string;
43
44
  thinking?: ThinkingLevel;
@@ -432,6 +433,29 @@ function normalizeChain(
432
433
  return undefined;
433
434
  }
434
435
 
436
+ function normalizeChainContext(
437
+ value: unknown,
438
+ filePath: string,
439
+ source: PromptSource,
440
+ diagnostics: PromptLoaderDiagnostic[],
441
+ ): "summary" | undefined {
442
+ if (value === undefined) return undefined;
443
+ if (typeof value === "string") {
444
+ const normalized = value.trim().toLowerCase();
445
+ if (normalized === "summary") return "summary";
446
+ }
447
+
448
+ diagnostics.push(
449
+ createDiagnostic(
450
+ "invalid-chain-context",
451
+ filePath,
452
+ source,
453
+ `Ignoring invalid chainContext value in ${filePath}: frontmatter field "chainContext" must be "summary".`,
454
+ ),
455
+ );
456
+ return undefined;
457
+ }
458
+
435
459
  function normalizeThinking(
436
460
  value: unknown,
437
461
  filePath: string,
@@ -542,6 +566,7 @@ function loadPromptsWithModelFromDir(
542
566
  if (!frontmatter) continue;
543
567
  const { body } = parsed;
544
568
  const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics);
569
+ const chainContext = chain ? normalizeChainContext(frontmatter.chainContext, fullPath, source, diagnostics) : undefined;
545
570
  if (chain && /\bparallel\s*\(/.test(chain)) {
546
571
  const parsedChain = parseChainDeclaration(chain);
547
572
  if (parsedChain.invalidSegments.length > 0 || parsedChain.steps.length === 0) {
@@ -637,6 +662,7 @@ function loadPromptsWithModelFromDir(
637
662
  content: body,
638
663
  models,
639
664
  chain: chain || undefined,
665
+ chainContext,
640
666
  restore,
641
667
  skill,
642
668
  thinking,
@@ -721,8 +747,9 @@ export function loadPromptsWithModel(cwd: string): LoadPromptsWithModelResult {
721
747
  export function buildPromptCommandDescription(prompt: PromptWithModel): string {
722
748
  const sourceLabel = prompt.subdir ? `(${prompt.source}:${prompt.subdir})` : `(${prompt.source})`;
723
749
  if (prompt.chain) {
750
+ const chainContextLabel = prompt.chainContext ? ` ${prompt.chainContext}` : "";
724
751
  const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : "";
725
- const details = `[chain: ${prompt.chain}${cwdLabel}] ${sourceLabel}`;
752
+ const details = `[chain: ${prompt.chain}${chainContextLabel}${cwdLabel}] ${sourceLabel}`;
726
753
  return prompt.description ? `${prompt.description} ${details}` : details;
727
754
  }
728
755
  const modelLabel = prompt.models.length > 0 ? prompt.models.map((model) => model.split("/").pop() || model).join("|") : "current";
package/subagent-step.ts CHANGED
@@ -38,6 +38,7 @@ interface DelegatedPromptBaseOptions {
38
38
  override?: SubagentOverride;
39
39
  signal?: AbortSignal;
40
40
  inheritedModel?: Model<any>;
41
+ taskPreamble?: string;
41
42
  }
42
43
 
43
44
  interface DelegatedSinglePromptOptions extends DelegatedPromptBaseOptions {
@@ -156,6 +157,7 @@ async function prepareDelegatedTask(
156
157
  currentModel: Model<any> | undefined,
157
158
  override: SubagentOverride | undefined,
158
159
  inheritedModel: Model<any> | undefined,
160
+ taskPreamble: string | undefined,
159
161
  runtime: Awaited<ReturnType<typeof ensureSubagentRuntime>>,
160
162
  ): Promise<PreparedDelegatedTask> {
161
163
  const requestedAgent = resolveDelegationName(task.prompt, override);
@@ -183,11 +185,15 @@ async function prepareDelegatedTask(
183
185
  if (effectiveCwd !== ctx.cwd && !existsSync(effectiveCwd)) {
184
186
  throw new Error(`cwd directory does not exist: ${effectiveCwd}`);
185
187
  }
188
+ let taskText = prepared.content;
189
+ if (!task.prompt.inheritContext && taskPreamble) {
190
+ taskText = `${taskPreamble}\n\n---\n\n${prepared.content}`;
191
+ }
186
192
 
187
193
  return {
188
194
  promptName: task.prompt.name,
189
195
  agent,
190
- task: prepared.content,
196
+ task: taskText,
191
197
  context: task.prompt.inheritContext ? "fork" : "fresh",
192
198
  model: `${prepared.selectedModel.model.provider}/${prepared.selectedModel.model.id}`,
193
199
  cwd: effectiveCwd,
@@ -429,7 +435,7 @@ async function requestDelegatedRun(
429
435
  }
430
436
 
431
437
  export async function executeSubagentPromptStep(options: DelegatedPromptOptions): Promise<DelegatedPromptOutcome | undefined> {
432
- const { pi, ctx, currentModel, override, signal, inheritedModel } = options;
438
+ const { pi, ctx, currentModel, override, signal, inheritedModel, taskPreamble } = options;
433
439
  const runtime = await ensureSubagentRuntime(ctx.cwd);
434
440
  const isParallelRequest = "parallel" in options;
435
441
 
@@ -440,7 +446,7 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
440
446
 
441
447
  const preparedTasks: PreparedDelegatedTask[] = [];
442
448
  for (const task of tasks) {
443
- const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, runtime);
449
+ const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble, runtime);
444
450
  preparedTasks.push(preparedTask);
445
451
  }
446
452
 
package/tool-manager.ts CHANGED
@@ -61,10 +61,10 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
61
61
  "Supports --loop for loops (e.g. 'deslop --loop 5', 'deslop --loop=5', 'deslop --loop' for unlimited until convergence with a 50-iteration cap), " +
62
62
  "--fresh for context collapse between iterations, and --no-converge to disable early stopping for bounded loops. " +
63
63
  "Supports runtime delegation override via --subagent, --subagent=<name>, or --subagent:<name>. " +
64
- "Use 'chain-prompts template1 -> template2' for chaining.",
64
+ "Use 'chain-prompts template1 -> template2' for chaining and add --chain-context to pass previous step summaries into delegated steps.",
65
65
  parameters: Type.Object({
66
66
  command: Type.String({
67
- description: "Template name and arguments (e.g. 'deslop --loop 5 --fresh', 'deslop --subagent:worker', 'deslop --subagent', 'chain-prompts analyze -> fix --loop=3')",
67
+ description: "Template name and arguments (e.g. 'deslop --loop 5 --fresh', 'deslop --subagent:worker', 'deslop --subagent', 'chain-prompts analyze -> fix --chain-context', 'chain-prompts analyze -> fix --loop=3')",
68
68
  }),
69
69
  }),
70
70
  execute: async (_id, params) => {