pi-prompt-template-model 0.6.5 → 0.6.6
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 +16 -1
- package/README.md +80 -3
- package/args.ts +23 -1
- package/chain-parser.ts +19 -12
- package/index.ts +76 -28
- package/loop-utils.ts +8 -0
- package/package.json +1 -1
- package/prompt-loader.ts +94 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.6.6] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added `--model=provider/model-id` runtime flag to override a template's model for a single invocation. Works with single execution, loops, and delegation.
|
|
7
|
+
- Added `--fork` runtime flag to enable `inheritContext` (forked context) at invocation time. Implies `--subagent` if not already set.
|
|
8
|
+
- Inline loop iterations now include a `[Loop 2/5]` prefix so the agent knows it's in a loop and which iteration it's on. Delegated (subagent) loops are unaffected.
|
|
9
|
+
- Added `loop: unlimited` (and `loop: true`) frontmatter for open-ended loops that run until convergence, user interrupt, or the 999-iteration safety cap.
|
|
10
|
+
- Added model rotation for loop iterations via `rotate: true` frontmatter. Cycles through comma-separated models and thinking levels each iteration instead of using fallback semantics.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Pressing Escape during a loop or chain iteration now stops the loop. Previously, aborted inline turns were treated as "no changes" and the loop continued.
|
|
14
|
+
- Delegation errors during loop iterations no longer abort the entire loop. The error is reported and the loop continues to the next iteration (useful with model rotation where one model may fail but others succeed).
|
|
15
|
+
- Per-step bare `--loop` in chain declarations (e.g., `double-check --loop -> deslop`) now correctly runs unlimited iterations instead of running once.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Unlimited loops (`--loop` bare or `loop: unlimited`) no longer force convergence on. Convergence follows the `converge` field like bounded loops. Safety cap raised from 50 to 999.
|
|
4
19
|
|
|
5
20
|
## [0.6.5] - 2026-03-24
|
|
6
21
|
|
package/README.md
CHANGED
|
@@ -69,7 +69,8 @@ All fields are optional. Templates that don't use any extension features (no `mo
|
|
|
69
69
|
| Field | Default | What it does |
|
|
70
70
|
|-------|---------|--------------|
|
|
71
71
|
| `restore` | `true` | After the command finishes, switch back to whatever model and thinking level were active before. Set `false` to stay on the new model. |
|
|
72
|
-
| `loop` | — | Run this template multiple times by default (1–999). CLI `--loop` overrides this. See [Loop Execution](#loop-execution). |
|
|
72
|
+
| `loop` | — | Run this template multiple times by default (1–999, `true`, or `unlimited`). CLI `--loop` overrides this. See [Loop Execution](#loop-execution). |
|
|
73
|
+
| `rotate` | `false` | When `true` and looping, cycle through models in the `model` list instead of using fallback semantics. Thinking levels can also be comma-separated to pair with each model. |
|
|
73
74
|
| `fresh` | `false` | When looping, collapse the conversation between iterations to a brief summary instead of carrying the full context forward. Saves tokens on long loops. |
|
|
74
75
|
| `converge` | `true` | When looping, stop early if an iteration makes no file changes. Set `false` to always run every iteration. |
|
|
75
76
|
|
|
@@ -241,6 +242,17 @@ During execution, a live progress widget appears above the editor showing elapse
|
|
|
241
242
|
|
|
242
243
|
You can override delegation at runtime per invocation with `--subagent`, `--subagent=<name>`, `--subagent:<name>`, or `--cwd=<path>`. `--cwd=<path>` must be absolute after optional `~/...` expansion. Runtime flags take precedence for that invocation only.
|
|
243
244
|
|
|
245
|
+
Two additional runtime flags work for any prompt (not just delegated ones):
|
|
246
|
+
|
|
247
|
+
- `--model=provider/model-id` — override the template's `model` for this invocation. Works with single execution, loops, and delegation.
|
|
248
|
+
- `--fork` — run with `inheritContext` (forked context). Implies `--subagent` if not already set.
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
/double-check --model=anthropic/claude-opus-4-6
|
|
252
|
+
/double-check --fork --subagent:worker
|
|
253
|
+
/deslop --model=openai/gpt-5.4 --loop 3
|
|
254
|
+
```
|
|
255
|
+
|
|
244
256
|
## Loop Execution
|
|
245
257
|
|
|
246
258
|
Run a template multiple times with `--loop`:
|
|
@@ -248,7 +260,7 @@ Run a template multiple times with `--loop`:
|
|
|
248
260
|
```
|
|
249
261
|
/deslop --loop 5
|
|
250
262
|
/deslop --loop=5
|
|
251
|
-
/deslop --loop # unlimited — runs until convergence
|
|
263
|
+
/deslop --loop # unlimited — runs until convergence or cap (999)
|
|
252
264
|
```
|
|
253
265
|
|
|
254
266
|
You can also set a default in frontmatter. CLI `--loop` always overrides:
|
|
@@ -259,11 +271,22 @@ loop: 5
|
|
|
259
271
|
---
|
|
260
272
|
```
|
|
261
273
|
|
|
274
|
+
Use `loop: unlimited` (or `loop: true`) for open-ended loops that run until convergence, user interrupt, or the safety cap of 999 iterations:
|
|
275
|
+
|
|
276
|
+
```markdown
|
|
277
|
+
---
|
|
278
|
+
loop: unlimited
|
|
279
|
+
converge: false
|
|
280
|
+
fresh: true
|
|
281
|
+
subagent: true
|
|
282
|
+
---
|
|
283
|
+
```
|
|
284
|
+
|
|
262
285
|
### How looping works
|
|
263
286
|
|
|
264
287
|
Each iteration runs the same prompt. By default, context accumulates — iteration 3 sees the full conversation from iterations 1 and 2 and builds on that work.
|
|
265
288
|
|
|
266
|
-
**Convergence**: If an iteration makes no file changes (no `write` or `edit` tool calls), the loop stops early. This is on by default. Use `--no-converge` or `converge: false` to always run every iteration.
|
|
289
|
+
**Convergence**: If an iteration makes no file changes (no `write` or `edit` tool calls), the loop stops early. This is on by default. Use `--no-converge` or `converge: false` to always run every iteration.
|
|
267
290
|
|
|
268
291
|
**Fresh context**: Add `--fresh` (or `fresh: true` in frontmatter) to collapse the conversation between iterations. Each iteration gets a clean slate with only brief summaries of what previous iterations did. Good for long loops where accumulated context would blow up the token count.
|
|
269
292
|
|
|
@@ -271,6 +294,60 @@ Each iteration runs the same prompt. By default, context accumulates — iterati
|
|
|
271
294
|
|
|
272
295
|
Model, thinking level, and skill are maintained throughout. If `restore: true` (the default), everything is restored after the final iteration.
|
|
273
296
|
|
|
297
|
+
## Model Rotation
|
|
298
|
+
|
|
299
|
+
`rotate: true` turns a comma-separated `model` list from a fallback chain into a cycling list. Each loop iteration uses the next model in the list, wrapping around:
|
|
300
|
+
|
|
301
|
+
```markdown
|
|
302
|
+
---
|
|
303
|
+
model: claude-opus-4-6, gpt-5.4, gpt-5.3-codex
|
|
304
|
+
thinking: high, xhigh, off
|
|
305
|
+
loop: 9
|
|
306
|
+
rotate: true
|
|
307
|
+
fresh: true
|
|
308
|
+
---
|
|
309
|
+
Review and fix issues in this codebase.
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Iteration 1 runs Opus + `high`, iteration 2 runs GPT-5.4 + `xhigh`, iteration 3 runs Codex + `off`, then wraps back to Opus. The status bar shows which model is active: `loop 2/9 · gpt-5.4 xhigh`.
|
|
313
|
+
|
|
314
|
+
This is especially useful for [ralph-style loops](https://ghuntley.com/ralph/) where different models catch different things. The `subagent` examples below require [pi-subagents](https://github.com/nicobailon/pi-subagents/). A single-model ralph loop that delegates with fresh context each iteration:
|
|
315
|
+
|
|
316
|
+
```markdown
|
|
317
|
+
---
|
|
318
|
+
model: claude-sonnet-4-20250514
|
|
319
|
+
subagent: true
|
|
320
|
+
inheritContext: true
|
|
321
|
+
loop: 5
|
|
322
|
+
fresh: true
|
|
323
|
+
---
|
|
324
|
+
Simplify this code: $@
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Add `rotate` and multiple models to cycle different perspectives on each pass:
|
|
328
|
+
|
|
329
|
+
```markdown
|
|
330
|
+
---
|
|
331
|
+
model: claude-opus-4-6, gpt-5.4, gpt-5.3-codex
|
|
332
|
+
thinking: xhigh, high, high
|
|
333
|
+
loop: 9
|
|
334
|
+
rotate: true
|
|
335
|
+
fresh: true
|
|
336
|
+
subagent: true
|
|
337
|
+
---
|
|
338
|
+
Review and fix issues in this codebase.
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Each iteration gets fresh context, a different model, and its own thinking level. Convergence stops the loop when an iteration makes no file changes — use `converge: false` to guarantee every model gets at least one shot.
|
|
342
|
+
|
|
343
|
+
`thinking` pairing with `rotate: true`:
|
|
344
|
+
|
|
345
|
+
- Single value (`thinking: high`) — applied to every model.
|
|
346
|
+
- Comma-separated (`thinking: high, xhigh, off`) — positional, must match the number of models.
|
|
347
|
+
- Omitted — each iteration inherits the session default.
|
|
348
|
+
|
|
349
|
+
Without `loop`, `rotate` has no effect and comma-separated `model` keeps normal fallback behavior.
|
|
350
|
+
|
|
274
351
|
## Chaining Templates
|
|
275
352
|
|
|
276
353
|
`/chain-prompts` runs multiple templates in sequence. Each step uses its own model, skill, and thinking level, while conversation context flows between them:
|
package/args.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface SubagentOverrideExtraction {
|
|
|
20
20
|
args: string;
|
|
21
21
|
override?: SubagentOverride;
|
|
22
22
|
cwd?: string;
|
|
23
|
+
model?: string;
|
|
24
|
+
fork?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function extractLoopCount(argsString: string): LoopExtraction | null {
|
|
@@ -113,7 +115,7 @@ export function extractLoopCount(argsString: string): LoopExtraction | null {
|
|
|
113
115
|
cleaned = cleaned.slice(0, start) + cleaned.slice(end);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
const converge =
|
|
118
|
+
const converge = !noConverge;
|
|
117
119
|
return { args: cleaned.trim(), loopCount, fresh, converge };
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -210,6 +212,8 @@ export function extractChainContextFlag(argsString: string): { args: string; cha
|
|
|
210
212
|
export function extractSubagentOverride(argsString: string): SubagentOverrideExtraction {
|
|
211
213
|
let override: SubagentOverride | undefined;
|
|
212
214
|
let cwdRaw: string | undefined;
|
|
215
|
+
let modelRaw: string | undefined;
|
|
216
|
+
let fork = false;
|
|
213
217
|
const tokensToRemove: Array<{ start: number; end: number }> = [];
|
|
214
218
|
|
|
215
219
|
let i = 0;
|
|
@@ -250,6 +254,20 @@ export function extractSubagentOverride(argsString: string): SubagentOverrideExt
|
|
|
250
254
|
tokensToRemove.push({ start: tokenStart, end: i });
|
|
251
255
|
const value = token.slice("--cwd=".length);
|
|
252
256
|
cwdRaw = value || undefined;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token.startsWith("--model=")) {
|
|
261
|
+
tokensToRemove.push({ start: tokenStart, end: i });
|
|
262
|
+
const value = token.slice("--model=".length);
|
|
263
|
+
modelRaw = value || undefined;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (token === "--fork") {
|
|
268
|
+
tokensToRemove.push({ start: tokenStart, end: i });
|
|
269
|
+
fork = true;
|
|
270
|
+
continue;
|
|
253
271
|
}
|
|
254
272
|
}
|
|
255
273
|
|
|
@@ -261,10 +279,14 @@ export function extractSubagentOverride(argsString: string): SubagentOverrideExt
|
|
|
261
279
|
cleaned = cleaned.slice(0, start) + cleaned.slice(end);
|
|
262
280
|
}
|
|
263
281
|
|
|
282
|
+
if (fork && !override) override = { enabled: true };
|
|
283
|
+
|
|
264
284
|
return {
|
|
265
285
|
args: cleaned.trim(),
|
|
266
286
|
...(override ? { override } : {}),
|
|
267
287
|
...(cwdRaw !== undefined ? { cwd: cwdRaw } : {}),
|
|
288
|
+
...(modelRaw !== undefined ? { model: modelRaw } : {}),
|
|
289
|
+
...(fork ? { fork: true } : {}),
|
|
268
290
|
};
|
|
269
291
|
}
|
|
270
292
|
|
package/chain-parser.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { parseCommandArgs } from "./args.js";
|
|
|
3
3
|
export interface ChainStep {
|
|
4
4
|
name: string;
|
|
5
5
|
args: string[];
|
|
6
|
-
loopCount?: number;
|
|
6
|
+
loopCount?: number | null;
|
|
7
7
|
withContext?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -81,11 +81,11 @@ function scanSegmentTokens(segment: string): SegmentToken[] {
|
|
|
81
81
|
return tokens;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number; withContext: boolean } {
|
|
84
|
+
function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number | null; withContext: boolean } {
|
|
85
85
|
const tokens = scanSegmentTokens(segment);
|
|
86
86
|
const loopTokenRanges: Array<{ start: number; end: number }> = [];
|
|
87
87
|
const withContextTokenRanges: Array<{ start: number; end: number }> = [];
|
|
88
|
-
let loopCount: number | undefined;
|
|
88
|
+
let loopCount: number | null | undefined;
|
|
89
89
|
let withContext = false;
|
|
90
90
|
|
|
91
91
|
for (let i = 1; i < tokens.length; i++) {
|
|
@@ -109,17 +109,24 @@ function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?
|
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (token.value === "--loop"
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
if (token.value === "--loop") {
|
|
113
|
+
loopTokenRanges.push({ start: token.start, end: token.end });
|
|
114
|
+
if (i + 1 < tokens.length) {
|
|
115
|
+
const next = tokens[i + 1];
|
|
116
|
+
if (!next.quoted && /^\d+$/.test(next.value)) {
|
|
117
|
+
loopTokenRanges.push({ start: next.start, end: next.end });
|
|
118
|
+
const parsed = parseInt(next.value, 10);
|
|
119
|
+
if (parsed >= 1 && parsed <= 999 && loopCount === undefined) {
|
|
120
|
+
loopCount = parsed;
|
|
121
|
+
}
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
119
124
|
}
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
125
|
}
|
|
126
|
+
if (loopCount === undefined) {
|
|
127
|
+
loopCount = null;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
132
|
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@m
|
|
|
3
3
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
4
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 { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
|
|
6
|
+
import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } 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";
|
|
@@ -16,6 +16,7 @@ import { renderDelegatedSubagentResult } from "./subagent-renderer.js";
|
|
|
16
16
|
interface LoopState {
|
|
17
17
|
currentIteration: number;
|
|
18
18
|
totalIterations: number | null;
|
|
19
|
+
rotationLabel?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface FreshCollapse {
|
|
@@ -58,7 +59,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
58
59
|
let accumulatedSummaries: string[] = [];
|
|
59
60
|
let lastDiagnostics = "";
|
|
60
61
|
let storedCommandCtx: ExtensionCommandContext | null = null;
|
|
61
|
-
const UNLIMITED_LOOP_CAP =
|
|
62
|
+
const UNLIMITED_LOOP_CAP = 999;
|
|
62
63
|
|
|
63
64
|
const toolManager = createToolManager(pi, {
|
|
64
65
|
isActive: () => !!(loopState || chainActive),
|
|
@@ -196,6 +197,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
196
197
|
override?: SubagentOverride,
|
|
197
198
|
inheritedModel?: Model<any>,
|
|
198
199
|
taskPreamble?: string,
|
|
200
|
+
loopContext?: string,
|
|
199
201
|
): Promise<PromptStepResult | "aborted"> {
|
|
200
202
|
if (shouldDelegatePrompt(prompt, override)) {
|
|
201
203
|
try {
|
|
@@ -216,7 +218,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
216
218
|
return { changed: delegated.changed };
|
|
217
219
|
} catch (error) {
|
|
218
220
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
219
|
-
return
|
|
221
|
+
return { changed: false };
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -258,10 +260,14 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
258
260
|
pendingSkillMessage = skillResolution.kind === "ready" ? skillResolution.message : undefined;
|
|
259
261
|
|
|
260
262
|
const startId = ctx.sessionManager.getLeafId();
|
|
261
|
-
|
|
263
|
+
const content = loopContext ? `[${loopContext}]\n\n${prepared.content}` : prepared.content;
|
|
264
|
+
pi.sendUserMessage(content);
|
|
262
265
|
await waitForTurnStart(ctx);
|
|
263
266
|
await ctx.waitForIdle();
|
|
264
|
-
|
|
267
|
+
|
|
268
|
+
const entries = getIterationEntries(ctx, startId);
|
|
269
|
+
if (wasIterationAborted(entries)) return "aborted";
|
|
270
|
+
return { changed: didIterationMakeChanges(entries) };
|
|
265
271
|
}
|
|
266
272
|
|
|
267
273
|
async function restoreSessionState(
|
|
@@ -353,10 +359,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
353
359
|
function updateLoopStatus(ctx: ExtensionContext) {
|
|
354
360
|
if (!ctx.hasUI) return;
|
|
355
361
|
if (loopState) {
|
|
362
|
+
const suffix = loopState.rotationLabel ? ` · ${loopState.rotationLabel}` : "";
|
|
356
363
|
const label =
|
|
357
364
|
loopState.totalIterations !== null
|
|
358
|
-
? `loop ${loopState.currentIteration}/${loopState.totalIterations}`
|
|
359
|
-
: `loop ${loopState.currentIteration}`;
|
|
365
|
+
? `loop ${loopState.currentIteration}/${loopState.totalIterations}${suffix}`
|
|
366
|
+
: `loop ${loopState.currentIteration}${suffix}`;
|
|
360
367
|
ctx.ui.setStatus("prompt-loop", ctx.ui.theme.fg("warning", label));
|
|
361
368
|
} else {
|
|
362
369
|
ctx.ui.setStatus("prompt-loop", undefined);
|
|
@@ -385,6 +392,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
385
392
|
ctx: ExtensionCommandContext,
|
|
386
393
|
subagentOverride?: SubagentOverride,
|
|
387
394
|
cwdOverride?: string,
|
|
395
|
+
promptOverrides?: Partial<Pick<PromptWithModel, "models" | "inheritContext">>,
|
|
388
396
|
) {
|
|
389
397
|
refreshPrompts(ctx.cwd, ctx);
|
|
390
398
|
const initialPrompt = prompts.get(name);
|
|
@@ -401,7 +409,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
401
409
|
const useFresh = freshFlag || initialPrompt.fresh === true;
|
|
402
410
|
const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
|
|
403
411
|
const isUnlimited = totalIterations === null;
|
|
404
|
-
const useConverge =
|
|
412
|
+
const useConverge = converge && initialPrompt.converge !== false;
|
|
405
413
|
const anchorId = useFresh ? ctx.sessionManager.getLeafId() : null;
|
|
406
414
|
|
|
407
415
|
loopState = { currentIteration: 1, totalIterations };
|
|
@@ -414,9 +422,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
414
422
|
try {
|
|
415
423
|
for (let i = 0; i < effectiveMax; i++) {
|
|
416
424
|
loopState.currentIteration = i + 1;
|
|
417
|
-
updateLoopStatus(ctx);
|
|
418
425
|
const iterationLabel = totalIterations !== null ? `${i + 1}/${totalIterations}` : `${i + 1}`;
|
|
419
|
-
notify(ctx, `Loop ${iterationLabel}: ${name}`, "info");
|
|
420
426
|
|
|
421
427
|
refreshPrompts(ctx.cwd, ctx);
|
|
422
428
|
const prompt = prompts.get(name);
|
|
@@ -424,15 +430,40 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
424
430
|
notify(ctx, `Prompt "${name}" no longer exists`, "error");
|
|
425
431
|
break;
|
|
426
432
|
}
|
|
427
|
-
const effectivePrompt = cwdOverride ? {
|
|
433
|
+
const effectivePrompt = { ...prompt, ...(cwdOverride ? { cwd: cwdOverride } : {}), ...promptOverrides };
|
|
434
|
+
let iterationPrompt = effectivePrompt;
|
|
435
|
+
loopState!.rotationLabel = undefined;
|
|
436
|
+
if (effectivePrompt.rotate && effectivePrompt.models.length > 1) {
|
|
437
|
+
const rotationIndex = i % effectivePrompt.models.length;
|
|
438
|
+
const rotatedThinking = effectivePrompt.thinkingLevels
|
|
439
|
+
? effectivePrompt.thinkingLevels[rotationIndex]
|
|
440
|
+
: effectivePrompt.thinking;
|
|
441
|
+
iterationPrompt = {
|
|
442
|
+
...effectivePrompt,
|
|
443
|
+
models: [effectivePrompt.models[rotationIndex]],
|
|
444
|
+
thinking: rotatedThinking,
|
|
445
|
+
};
|
|
446
|
+
const shortModel = effectivePrompt.models[rotationIndex].split("/").pop() || effectivePrompt.models[rotationIndex];
|
|
447
|
+
const thinkingLabel = rotatedThinking ? ` ${rotatedThinking}` : "";
|
|
448
|
+
loopState!.rotationLabel = `${shortModel}${thinkingLabel}`;
|
|
449
|
+
}
|
|
450
|
+
updateLoopStatus(ctx);
|
|
451
|
+
const rotationSuffix = loopState!.rotationLabel ? ` [${loopState!.rotationLabel}]` : "";
|
|
452
|
+
notify(ctx, `Loop ${iterationLabel}: ${name}${rotationSuffix}`, "info");
|
|
428
453
|
|
|
454
|
+
const loopContext = loopState!.rotationLabel
|
|
455
|
+
? `Loop ${iterationLabel} · ${loopState!.rotationLabel}`
|
|
456
|
+
: `Loop ${iterationLabel}`;
|
|
429
457
|
const iterationStartId = ctx.sessionManager.getLeafId();
|
|
430
458
|
const stepResult = await executePromptStep(
|
|
431
|
-
|
|
459
|
+
iterationPrompt,
|
|
432
460
|
parseCommandArgs(cleanedArgs),
|
|
433
461
|
ctx,
|
|
434
462
|
currentModel,
|
|
435
463
|
subagentOverride,
|
|
464
|
+
undefined,
|
|
465
|
+
undefined,
|
|
466
|
+
loopContext,
|
|
436
467
|
);
|
|
437
468
|
if (stepResult === "aborted") break;
|
|
438
469
|
|
|
@@ -440,7 +471,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
440
471
|
currentThinking = pi.getThinkingLevel();
|
|
441
472
|
completedIterations++;
|
|
442
473
|
|
|
443
|
-
const iterationChanged = shouldDelegatePrompt(
|
|
474
|
+
const iterationChanged = shouldDelegatePrompt(iterationPrompt, subagentOverride)
|
|
444
475
|
? stepResult.changed
|
|
445
476
|
: didIterationMakeChanges(getIterationEntries(ctx, iterationStartId));
|
|
446
477
|
if (useConverge && (isUnlimited || effectiveMax > 1) && !iterationChanged) {
|
|
@@ -567,7 +598,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
567
598
|
pendingSkillMessage = undefined;
|
|
568
599
|
const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
|
|
569
600
|
const isUnlimited = totalIterations === null;
|
|
570
|
-
const useConverge =
|
|
601
|
+
const useConverge = converge;
|
|
571
602
|
|
|
572
603
|
const anchorId = fresh ? ctx.sessionManager.getLeafId() : null;
|
|
573
604
|
const chainStepNames = steps
|
|
@@ -612,7 +643,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
612
643
|
...(cwdOverride ? { cwd: cwdOverride } : {}),
|
|
613
644
|
},
|
|
614
645
|
stepArgs: step.args,
|
|
615
|
-
stepLoop: step.loopCount
|
|
646
|
+
stepLoop: step.loopCount !== undefined ? step.loopCount : 1,
|
|
616
647
|
stepWithContext: step.withContext === true,
|
|
617
648
|
},
|
|
618
649
|
},
|
|
@@ -676,7 +707,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
676
707
|
}
|
|
677
708
|
|
|
678
709
|
const singleStep = stepTemplate.singleStep;
|
|
679
|
-
const
|
|
710
|
+
const stepLoopTotal = singleStep.stepLoop;
|
|
711
|
+
const stepLoopMax = stepLoopTotal ?? UNLIMITED_LOOP_CAP;
|
|
712
|
+
const isStepLooping = stepLoopMax > 1;
|
|
680
713
|
const effectiveArgs = singleStep.stepArgs.length > 0 ? singleStep.stepArgs : sharedArgs;
|
|
681
714
|
const shouldInjectSummary =
|
|
682
715
|
shouldDelegatePrompt(singleStep.prompt, subagentOverride) &&
|
|
@@ -684,19 +717,23 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
684
717
|
(chainContextEnabled || singleStep.stepWithContext === true);
|
|
685
718
|
const outerLoopState = loopState ? { ...loopState } : null;
|
|
686
719
|
const stepStartId = ctx.sessionManager.getLeafId();
|
|
687
|
-
if (
|
|
688
|
-
loopState = { currentIteration: 1, totalIterations:
|
|
720
|
+
if (isStepLooping) {
|
|
721
|
+
loopState = { currentIteration: 1, totalIterations: stepLoopTotal };
|
|
689
722
|
updateLoopStatus(ctx);
|
|
690
723
|
}
|
|
691
724
|
|
|
692
725
|
try {
|
|
693
|
-
for (let stepIteration = 0; stepIteration <
|
|
694
|
-
if (
|
|
695
|
-
loopState = { currentIteration: stepIteration + 1, totalIterations:
|
|
726
|
+
for (let stepIteration = 0; stepIteration < stepLoopMax; stepIteration++) {
|
|
727
|
+
if (isStepLooping) {
|
|
728
|
+
loopState = { currentIteration: stepIteration + 1, totalIterations: stepLoopTotal };
|
|
696
729
|
updateLoopStatus(ctx);
|
|
697
730
|
}
|
|
698
731
|
|
|
699
|
-
const iterSuffix =
|
|
732
|
+
const iterSuffix = isStepLooping
|
|
733
|
+
? stepLoopTotal !== null
|
|
734
|
+
? ` (iter ${stepIteration + 1}/${stepLoopTotal})`
|
|
735
|
+
: ` (iter ${stepIteration + 1})`
|
|
736
|
+
: "";
|
|
700
737
|
notify(
|
|
701
738
|
ctx,
|
|
702
739
|
`${loopPrefix}Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix} ${buildPromptCommandDescription(singleStep.prompt)}`,
|
|
@@ -709,6 +746,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
709
746
|
? `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`
|
|
710
747
|
: undefined;
|
|
711
748
|
|
|
749
|
+
const stepLoopContext = isStepLooping
|
|
750
|
+
? `Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix}`
|
|
751
|
+
: undefined;
|
|
712
752
|
const stepIterationStartId = ctx.sessionManager.getLeafId();
|
|
713
753
|
const stepResult = await executePromptStep(
|
|
714
754
|
singleStep.prompt,
|
|
@@ -718,6 +758,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
718
758
|
subagentOverride,
|
|
719
759
|
chainInheritedModel,
|
|
720
760
|
taskPreamble,
|
|
761
|
+
stepLoopContext,
|
|
721
762
|
);
|
|
722
763
|
if (stepResult === "aborted") {
|
|
723
764
|
aborted = true;
|
|
@@ -729,12 +770,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
729
770
|
|
|
730
771
|
const stepIterationEntries = getIterationEntries(ctx, stepIterationStartId);
|
|
731
772
|
const stepIterationChanged = didIterationMakeChanges(stepIterationEntries);
|
|
732
|
-
if (
|
|
773
|
+
if (isStepLooping && singleStep.prompt.converge !== false && !stepIterationChanged) {
|
|
733
774
|
break;
|
|
734
775
|
}
|
|
735
776
|
}
|
|
736
777
|
} finally {
|
|
737
|
-
if (
|
|
778
|
+
if (isStepLooping) {
|
|
738
779
|
loopState = outerLoopState ? { ...outerLoopState } : null;
|
|
739
780
|
updateLoopStatus(ctx);
|
|
740
781
|
}
|
|
@@ -817,10 +858,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
817
858
|
const argsWithoutSubagent = subagent.args;
|
|
818
859
|
|
|
819
860
|
if (prompt.chain) {
|
|
861
|
+
if (subagent.model) notify(ctx, `--model is not supported on chain prompts (ignored)`, "warning");
|
|
862
|
+
if (subagent.fork) notify(ctx, `--fork is not supported on chain prompts (ignored)`, "warning");
|
|
820
863
|
const extracted = extractChainContextFlag(argsWithoutSubagent);
|
|
821
864
|
const chainContextEnabled = extracted.chainContext || prompt.chainContext === "summary";
|
|
822
865
|
const loop = extractLoopCount(extracted.args);
|
|
823
|
-
let totalIterations: number | null = prompt.loop
|
|
866
|
+
let totalIterations: number | null = prompt.loop !== undefined ? prompt.loop : 1;
|
|
824
867
|
let fresh = false;
|
|
825
868
|
let converge = true;
|
|
826
869
|
let cleanedArgs = extracted.args;
|
|
@@ -863,19 +906,24 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
863
906
|
return;
|
|
864
907
|
}
|
|
865
908
|
|
|
909
|
+
const promptOverrides: Partial<Pick<PromptWithModel, "models" | "inheritContext">> = {
|
|
910
|
+
...(subagent.model ? { models: [subagent.model] } : {}),
|
|
911
|
+
...(subagent.fork ? { inheritContext: true } : {}),
|
|
912
|
+
};
|
|
913
|
+
|
|
866
914
|
const loop = extractLoopCount(argsWithoutSubagent);
|
|
867
915
|
if (loop) {
|
|
868
|
-
await runPromptLoop(name, loop.args, loop.loopCount, loop.fresh, loop.converge, ctx, subagent.override, runtimeCwd);
|
|
916
|
+
await runPromptLoop(name, loop.args, loop.loopCount, loop.fresh, loop.converge, ctx, subagent.override, runtimeCwd, promptOverrides);
|
|
869
917
|
return;
|
|
870
918
|
}
|
|
871
919
|
|
|
872
920
|
if (prompt.loop !== undefined) {
|
|
873
921
|
const flags = extractLoopFlags(argsWithoutSubagent);
|
|
874
|
-
await runPromptLoop(name, flags.args, prompt.loop, flags.fresh, flags.converge, ctx, subagent.override, runtimeCwd);
|
|
922
|
+
await runPromptLoop(name, flags.args, prompt.loop, flags.fresh, flags.converge, ctx, subagent.override, runtimeCwd, promptOverrides);
|
|
875
923
|
return;
|
|
876
924
|
}
|
|
877
925
|
|
|
878
|
-
const effectivePrompt = runtimeCwd ? {
|
|
926
|
+
const effectivePrompt = { ...prompt, ...(runtimeCwd ? { cwd: runtimeCwd } : {}), ...promptOverrides };
|
|
879
927
|
const savedModel = getCurrentModel(ctx);
|
|
880
928
|
const savedThinking = pi.getThinkingLevel();
|
|
881
929
|
const stepResult = await executePromptStep(
|
package/loop-utils.ts
CHANGED
|
@@ -153,3 +153,11 @@ export function getIterationEntries(ctx: Pick<ExtensionContext, "sessionManager"
|
|
|
153
153
|
if (startIdx < 0) return branch;
|
|
154
154
|
return branch.slice(startIdx + 1);
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
export function wasIterationAborted(entries: SessionEntry[]): boolean {
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
160
|
+
if ((entry.message as AssistantMessage).stopReason === "aborted") return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
package/package.json
CHANGED
package/prompt-loader.ts
CHANGED
|
@@ -42,8 +42,10 @@ export interface PromptWithModel {
|
|
|
42
42
|
restore: boolean;
|
|
43
43
|
skill?: string;
|
|
44
44
|
thinking?: ThinkingLevel;
|
|
45
|
+
thinkingLevels?: ThinkingLevel[];
|
|
46
|
+
rotate?: boolean;
|
|
45
47
|
fresh?: boolean;
|
|
46
|
-
loop?: number;
|
|
48
|
+
loop?: number | null;
|
|
47
49
|
converge?: boolean;
|
|
48
50
|
subagent?: true | string;
|
|
49
51
|
inheritContext?: boolean;
|
|
@@ -243,14 +245,43 @@ function normalizeFresh(
|
|
|
243
245
|
return false;
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
function normalizeRotate(
|
|
249
|
+
value: unknown,
|
|
250
|
+
filePath: string,
|
|
251
|
+
source: PromptSource,
|
|
252
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
253
|
+
): boolean {
|
|
254
|
+
if (value === undefined) return false;
|
|
255
|
+
if (typeof value === "boolean") return value;
|
|
256
|
+
if (typeof value === "string") {
|
|
257
|
+
const normalized = value.trim().toLowerCase();
|
|
258
|
+
if (normalized === "true") return true;
|
|
259
|
+
if (normalized === "false") return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
diagnostics.push(
|
|
263
|
+
createDiagnostic(
|
|
264
|
+
"invalid-rotate",
|
|
265
|
+
filePath,
|
|
266
|
+
source,
|
|
267
|
+
`Using default rotate=false for ${filePath}: frontmatter field "rotate" must be true or false.`,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
function normalizeLoop(
|
|
247
274
|
value: unknown,
|
|
248
275
|
filePath: string,
|
|
249
276
|
source: PromptSource,
|
|
250
277
|
diagnostics: PromptLoaderDiagnostic[],
|
|
251
|
-
): number | undefined {
|
|
278
|
+
): number | null | undefined {
|
|
252
279
|
if (value === undefined) return undefined;
|
|
253
280
|
|
|
281
|
+
if (value === true || (typeof value === "string" && value.trim().toLowerCase() === "unlimited")) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
let normalizedValue: number | undefined;
|
|
255
286
|
if (typeof value === "number") {
|
|
256
287
|
normalizedValue = value;
|
|
@@ -267,7 +298,7 @@ function normalizeLoop(
|
|
|
267
298
|
"invalid-loop",
|
|
268
299
|
filePath,
|
|
269
300
|
source,
|
|
270
|
-
`Ignoring invalid loop value in ${filePath}: frontmatter field "loop" must be an integer between 1 and 999.`,
|
|
301
|
+
`Ignoring invalid loop value in ${filePath}: frontmatter field "loop" must be an integer between 1 and 999, true, or "unlimited".`,
|
|
271
302
|
),
|
|
272
303
|
);
|
|
273
304
|
return undefined;
|
|
@@ -481,6 +512,48 @@ function normalizeThinking(
|
|
|
481
512
|
return undefined;
|
|
482
513
|
}
|
|
483
514
|
|
|
515
|
+
function normalizeThinkingLevels(
|
|
516
|
+
value: unknown,
|
|
517
|
+
modelCount: number,
|
|
518
|
+
filePath: string,
|
|
519
|
+
source: PromptSource,
|
|
520
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
521
|
+
): ThinkingLevel[] | undefined {
|
|
522
|
+
if (typeof value !== "string") return undefined;
|
|
523
|
+
|
|
524
|
+
const levels = value
|
|
525
|
+
.split(",")
|
|
526
|
+
.map((item) => item.trim())
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
|
|
529
|
+
const invalidLevel = levels.find((level) => !(VALID_THINKING_LEVELS as readonly string[]).includes(level.toLowerCase()));
|
|
530
|
+
if (invalidLevel) {
|
|
531
|
+
diagnostics.push(
|
|
532
|
+
createDiagnostic(
|
|
533
|
+
"invalid-thinking-levels",
|
|
534
|
+
filePath,
|
|
535
|
+
source,
|
|
536
|
+
`Ignoring invalid thinking level in ${filePath}: ${JSON.stringify(invalidLevel)}.`,
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (levels.length !== modelCount) {
|
|
543
|
+
diagnostics.push(
|
|
544
|
+
createDiagnostic(
|
|
545
|
+
"invalid-thinking-level-count",
|
|
546
|
+
filePath,
|
|
547
|
+
source,
|
|
548
|
+
`Ignoring comma-separated thinking levels in ${filePath}: expected ${modelCount} entries to match frontmatter field "model".`,
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return levels.map((level) => level.toLowerCase() as ThinkingLevel);
|
|
555
|
+
}
|
|
556
|
+
|
|
484
557
|
function loadPromptsWithModelFromDir(
|
|
485
558
|
dir: string,
|
|
486
559
|
source: PromptSource,
|
|
@@ -619,6 +692,7 @@ function loadPromptsWithModelFromDir(
|
|
|
619
692
|
const parsedModels = chain ? [] : normalizeModelSpecs(frontmatter.model, fullPath, source, diagnostics);
|
|
620
693
|
if (!chain && hasModelField && !parsedModels) continue;
|
|
621
694
|
const models = chain ? [] : (parsedModels ?? []);
|
|
695
|
+
const rotate = chain ? false : normalizeRotate(frontmatter.rotate, fullPath, source, diagnostics);
|
|
622
696
|
|
|
623
697
|
const name = entry.name.slice(0, -3);
|
|
624
698
|
if (RESERVED_COMMAND_NAMES.has(name)) {
|
|
@@ -637,7 +711,15 @@ function loadPromptsWithModelFromDir(
|
|
|
637
711
|
const safeCwd = (chain || subagent !== undefined) ? cwd : undefined;
|
|
638
712
|
const description = normalizeStringField("description", frontmatter.description, fullPath, source, diagnostics) ?? "";
|
|
639
713
|
const skill = chain ? undefined : normalizeStringField("skill", frontmatter.skill, fullPath, source, diagnostics);
|
|
640
|
-
|
|
714
|
+
let thinking: ThinkingLevel | undefined;
|
|
715
|
+
let thinkingLevels: ThinkingLevel[] | undefined;
|
|
716
|
+
if (!chain) {
|
|
717
|
+
if (rotate && typeof frontmatter.thinking === "string" && frontmatter.thinking.includes(",")) {
|
|
718
|
+
thinkingLevels = normalizeThinkingLevels(frontmatter.thinking, models.length, fullPath, source, diagnostics);
|
|
719
|
+
} else {
|
|
720
|
+
thinking = normalizeThinking(frontmatter.thinking, fullPath, source, diagnostics);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
641
723
|
const restore = normalizeRestore(frontmatter.restore, fullPath, source, diagnostics);
|
|
642
724
|
const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics);
|
|
643
725
|
const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics);
|
|
@@ -666,8 +748,10 @@ function loadPromptsWithModelFromDir(
|
|
|
666
748
|
restore,
|
|
667
749
|
skill,
|
|
668
750
|
thinking,
|
|
751
|
+
thinkingLevels,
|
|
752
|
+
rotate: rotate || undefined,
|
|
669
753
|
fresh: fresh || undefined,
|
|
670
|
-
loop: loop
|
|
754
|
+
loop: loop !== undefined ? loop : undefined,
|
|
671
755
|
converge: converge === false ? false : undefined,
|
|
672
756
|
subagent,
|
|
673
757
|
inheritContext: safeInheritContext || undefined,
|
|
@@ -753,13 +837,15 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
753
837
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
754
838
|
}
|
|
755
839
|
const modelLabel = prompt.models.length > 0 ? prompt.models.map((model) => model.split("/").pop() || model).join("|") : "current";
|
|
840
|
+
const rotateLabel = prompt.rotate ? " rotate" : "";
|
|
756
841
|
const skillLabel = prompt.skill ? ` +${prompt.skill}` : "";
|
|
757
|
-
const
|
|
758
|
-
const
|
|
842
|
+
const thinkingValue = prompt.thinkingLevels ? prompt.thinkingLevels.join(",") : prompt.thinking;
|
|
843
|
+
const thinkingLabel = thinkingValue ? ` ${thinkingValue}` : "";
|
|
844
|
+
const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : "";
|
|
759
845
|
const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : "";
|
|
760
846
|
const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : "";
|
|
761
847
|
const inheritContextLabel = prompt.inheritContext ? " fork" : "";
|
|
762
|
-
const details = `[${modelLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${cwdLabel}${inheritContextLabel}] ${sourceLabel}`;
|
|
848
|
+
const details = `[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${cwdLabel}${inheritContextLabel}] ${sourceLabel}`;
|
|
763
849
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
764
850
|
}
|
|
765
851
|
|