pi-prompt-template-model 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.0] - 2026-04-25
6
+
7
+ ### Added
8
+ - Added `boomerang: true` prompt frontmatter so non-chain templates, including looped templates, can run through prompt-template-model and then collapse their execution context back to the pre-run branch.
9
+
10
+ ### Fixed
11
+ - Migrated extension tool schemas from `@sinclair/typebox` to `typebox` 1.x so packaged installs follow Pi's current extension runtime contract.
12
+
13
+ ### Changed
14
+ - Added `typebox` as a runtime dependency for packaged installs.
15
+
16
+ ## [0.8.2] - 2026-04-21
17
+
18
+ ### Added
19
+ - Added the packaged `prompt-template-authoring` skill for creating and maintaining prompt templates with this extension.
20
+
21
+ ### Changed
22
+ - Tightened the shipped skill guide so it stays short, repo-specific, and aligned with the actual prompt-template runtime.
23
+
24
+ ### Fixed
25
+ - Corrected the shipped skill examples for model fallback vs rotation, argument substitution, deterministic handoff behavior, chain context wording, runtime flag syntax, and prompt discovery rules.
26
+ - Removed the emoji from the skill-loaded renderer so the UI copy matches the extension's plain-text style.
27
+
28
+ ## [0.8.1] - 2026-04-21
29
+
30
+ ### Added
31
+ - Added agent skill `prompt-template-authoring` for writing, managing, and running custom prompt templates. Registered in `package.json` under `pi.skills`.
32
+
5
33
  ## [0.8.0] - 2026-04-21
6
34
 
7
35
  ### Added
package/README.md CHANGED
@@ -73,6 +73,7 @@ All fields are optional. Templates that don't use any extension features (no `mo
73
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. |
74
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. |
75
75
  | `converge` | `true` | When looping, stop early if an iteration makes no file changes. Set `false` to always run every iteration. |
76
+ | `boomerang` | `false` | After a non-chain prompt finishes, collapse its execution context back to the branch point with a brief summary. Works with loops, including `fresh` loop summaries. Useful for review prompts like `/double-check`. |
76
77
  | `worktree` | `false` | When `true`, parallel delegated work runs in separate git worktrees. Valid on chain templates with `parallel()` steps, on delegated prompts with `parallel: N`, and on compare templates via `bestOfN.worktree`. |
77
78
 
78
79
  ### Delegation
@@ -291,7 +292,7 @@ This repo ships one example compare prompt under `examples/`:
291
292
  - `examples/best-of-n.md` installs as `/best-of-n`, runs in the current repo, and shows mixed workers, mixed reviewers, and an optional final apply phase.
292
293
  - Smoke test: `/best-of-n smoke test`.
293
294
 
294
- Install them manually from this repo checkout (or from the installed package directory):
295
+ Install it manually from this repo checkout (or from the installed package directory):
295
296
 
296
297
  ```bash
297
298
  PTM_DIR=/path/to/pi-prompt-template-model
package/index.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  type SubagentOverride,
17
17
  } from "./args.js";
18
18
  import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
19
- import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
19
+ import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
20
20
  import { selectModelCandidate } from "./model-selection.js";
21
21
  import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
22
22
  import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.js";
@@ -56,6 +56,12 @@ interface FreshCollapse {
56
56
  totalIterations: number | null;
57
57
  }
58
58
 
59
+ interface BoomerangCollapse {
60
+ targetId: string;
61
+ task: string;
62
+ previousSummaries: string[];
63
+ }
64
+
59
65
  interface PendingSkillMessage {
60
66
  customType: "skill-loaded";
61
67
  content: string;
@@ -108,6 +114,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
108
114
  let chainActive = false;
109
115
  let loopState: LoopState | null = null;
110
116
  let freshCollapse: FreshCollapse | null = null;
117
+ let boomerangCollapse: BoomerangCollapse | null = null;
111
118
  let accumulatedSummaries: string[] = [];
112
119
  let lastDiagnostics = "";
113
120
  let storedCommandCtx: ExtensionCommandContext | null = null;
@@ -918,6 +925,26 @@ export default function promptModelExtension(pi: ExtensionAPI) {
918
925
  }
919
926
  }
920
927
 
928
+ async function collapseBoomerangPrompt(
929
+ ctx: ExtensionContext,
930
+ name: string,
931
+ targetId: string | null,
932
+ previousSummaries: string[] = [],
933
+ ) {
934
+ if (!targetId) {
935
+ notify(ctx, `Cannot boomerang prompt \`${name}\`: no session entry to return to.`, "warning");
936
+ return;
937
+ }
938
+
939
+ boomerangCollapse = { targetId, task: name, previousSummaries };
940
+ try {
941
+ const result = await ctx.navigateTree(targetId, { summarize: true });
942
+ if (result.cancelled) notify(ctx, `Boomerang cancelled for prompt \`${name}\``, "warning");
943
+ } finally {
944
+ boomerangCollapse = null;
945
+ }
946
+ }
947
+
921
948
  async function runPromptLoop(
922
949
  name: string,
923
950
  cleanedArgs: string,
@@ -942,10 +969,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
942
969
  let currentThinking = savedThinking;
943
970
  const shouldRestore = initialPrompt.restore;
944
971
  const useFresh = freshFlag || initialPrompt.fresh === true;
972
+ const shouldBoomerang = initialPrompt.boomerang === true;
945
973
  const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
946
974
  const isUnlimited = totalIterations === null;
947
975
  const useConverge = converge && initialPrompt.converge !== false;
948
- const anchorId = useFresh ? ctx.sessionManager.getLeafId() : null;
976
+ const anchorId = useFresh || shouldBoomerang ? ctx.sessionManager.getLeafId() : null;
949
977
 
950
978
  loopState = { currentIteration: 1, totalIterations };
951
979
  accumulatedSummaries = [];
@@ -955,6 +983,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
955
983
  let loopErrorState: ExecutionErrorState = { hasError: false, error: undefined };
956
984
  let lastDelegatedText: string | undefined;
957
985
  let loopAborted = false;
986
+ let boomerangPreviousSummaries: string[] = [];
958
987
 
959
988
  try {
960
989
  for (let i = 0; i < effectiveMax; i++) {
@@ -1024,7 +1053,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1024
1053
  break;
1025
1054
  }
1026
1055
 
1027
- if (anchorId && i < effectiveMax - 1) {
1056
+ if (useFresh && anchorId && i < effectiveMax - 1) {
1028
1057
  freshCollapse = { targetId: anchorId, task: name, iteration: i + 1, totalIterations };
1029
1058
  const result = await ctx.navigateTree(anchorId, { summarize: true });
1030
1059
  freshCollapse = null;
@@ -1049,9 +1078,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1049
1078
  "loop",
1050
1079
  );
1051
1080
 
1081
+ boomerangPreviousSummaries = accumulatedSummaries;
1052
1082
  loopState = null;
1053
1083
  pendingSkillMessage = undefined;
1054
1084
  freshCollapse = null;
1085
+ boomerangCollapse = null;
1055
1086
  accumulatedSummaries = [];
1056
1087
  updateLoopStatus(ctx);
1057
1088
 
@@ -1069,6 +1100,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1069
1100
  await ctx.waitForIdle();
1070
1101
  }
1071
1102
 
1103
+ if (!loopErrorState.hasError && !loopAborted && shouldBoomerang) {
1104
+ await collapseBoomerangPrompt(ctx, name, anchorId, boomerangPreviousSummaries);
1105
+ }
1106
+
1072
1107
  if (loopErrorState.hasError) {
1073
1108
  throw loopErrorState.error;
1074
1109
  }
@@ -1402,6 +1437,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1402
1437
  chainActive = false;
1403
1438
  loopState = null;
1404
1439
  freshCollapse = null;
1440
+ boomerangCollapse = null;
1405
1441
  accumulatedSummaries = [];
1406
1442
  updateLoopStatus(ctx);
1407
1443
  if (ctx.hasUI) {
@@ -1549,6 +1585,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1549
1585
  };
1550
1586
  const savedModel = getCurrentModel(ctx);
1551
1587
  const savedThinking = pi.getThinkingLevel();
1588
+ const boomerangTargetId = effectivePrompt.boomerang ? ctx.sessionManager.getLeafId() : null;
1552
1589
  const stepResult = await executePromptStep(
1553
1590
  effectivePrompt,
1554
1591
  parseCommandArgs(argsWithoutSubagent),
@@ -1573,6 +1610,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1573
1610
  previousThinking = savedThinking;
1574
1611
  }
1575
1612
  }
1613
+
1614
+ if (effectivePrompt.boomerang) {
1615
+ await collapseBoomerangPrompt(ctx, name, boomerangTargetId);
1616
+ }
1576
1617
  }
1577
1618
 
1578
1619
  function resetSessionScopedState(ctx: ExtensionContext) {
@@ -1581,6 +1622,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1581
1622
  previousModel = undefined;
1582
1623
  previousThinking = undefined;
1583
1624
  runtimeModel = ctx.model;
1625
+ boomerangCollapse = null;
1584
1626
  toolManager.clearQueue();
1585
1627
  refreshPrompts(ctx.cwd, ctx);
1586
1628
  }
@@ -1644,6 +1686,15 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1644
1686
  });
1645
1687
 
1646
1688
  pi.on("session_before_tree", async (event) => {
1689
+ if (boomerangCollapse && event.preparation.targetId === boomerangCollapse.targetId) {
1690
+ const summary = generateBoomerangSummary(event.preparation.entriesToSummarize, boomerangCollapse.task);
1691
+ return {
1692
+ summary: {
1693
+ summary: [...boomerangCollapse.previousSummaries, summary].join("\n\n---\n\n"),
1694
+ },
1695
+ };
1696
+ }
1697
+
1647
1698
  if (!freshCollapse) return;
1648
1699
  if (event.preparation.targetId !== freshCollapse.targetId) return;
1649
1700
 
package/loop-utils.ts CHANGED
@@ -88,7 +88,7 @@ function collectSummaryData(entries: SessionEntry[]): CollectedSummaryData {
88
88
  };
89
89
  }
90
90
 
91
- function formatSummary(header: string, entries: SessionEntry[]): string {
91
+ function formatSummary(header: string, entries: SessionEntry[], preserveOutcome = false): string {
92
92
  const { filesRead, filesWritten, commandCount, lastAssistantText } = collectSummaryData(entries);
93
93
 
94
94
  let summary = header;
@@ -102,9 +102,11 @@ function formatSummary(header: string, entries: SessionEntry[]): string {
102
102
  }
103
103
 
104
104
  if (lastAssistantText) {
105
- const cleaned = lastAssistantText.replace(/\n+/g, " ").trim();
106
- const truncated = cleaned.slice(0, 500);
107
- summary += `\nOutcome: ${truncated}${cleaned.length > 500 ? "..." : ""}`;
105
+ const cleaned = preserveOutcome
106
+ ? lastAssistantText.replace(/\r\n?/g, "\n").trim()
107
+ : lastAssistantText.replace(/\n+/g, " ").trim();
108
+ const outcome = preserveOutcome || cleaned.length <= 500 ? cleaned : `${cleaned.slice(0, 500)}...`;
109
+ summary += `\nOutcome: ${outcome}`;
108
110
  }
109
111
 
110
112
  return summary;
@@ -117,6 +119,10 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
117
119
  return formatSummary(header, entries);
118
120
  }
119
121
 
122
+ export function generateBoomerangSummary(entries: SessionEntry[], task: string): string {
123
+ return formatSummary(`[Boomerang]\nTask: "${task}"`, entries, true);
124
+ }
125
+
120
126
  export function generateChainStepSummary(entries: SessionEntry[], stepLabel: string, stepNumber: number): string {
121
127
  return formatSummary(`Step ${stepNumber} — ${stepLabel}:`, entries);
122
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Prompt template model selector extension for pi coding agent",
6
6
  "author": "Nico Bailon",
@@ -40,6 +40,7 @@
40
40
  "tool-manager.ts",
41
41
  "template-conditionals.ts",
42
42
  "examples",
43
+ "skills",
43
44
  "banner.png",
44
45
  "README.md",
45
46
  "CHANGELOG.md",
@@ -48,6 +49,9 @@
48
49
  "scripts": {
49
50
  "test": "tsx --test test/**/*.test.ts"
50
51
  },
52
+ "dependencies": {
53
+ "typebox": "^1.1.24"
54
+ },
51
55
  "devDependencies": {
52
56
  "@mariozechner/pi-agent-core": "^0.65.0",
53
57
  "@mariozechner/pi-ai": "^0.65.0",
@@ -58,6 +62,9 @@
58
62
  "pi": {
59
63
  "extensions": [
60
64
  "./index.ts"
65
+ ],
66
+ "skills": [
67
+ "./skills"
61
68
  ]
62
69
  }
63
70
  }
package/prompt-loader.ts CHANGED
@@ -74,6 +74,7 @@ export interface PromptWithModel {
74
74
  fresh?: boolean;
75
75
  loop?: number | null;
76
76
  converge?: boolean;
77
+ boomerang?: boolean;
77
78
  parallel?: number;
78
79
  worktree?: boolean;
79
80
  deterministic?: DeterministicStep;
@@ -303,6 +304,31 @@ function normalizeRotate(
303
304
  return false;
304
305
  }
305
306
 
307
+ function normalizeBoomerang(
308
+ value: unknown,
309
+ filePath: string,
310
+ source: PromptSource,
311
+ diagnostics: PromptLoaderDiagnostic[],
312
+ ): boolean {
313
+ if (value === undefined) return false;
314
+ if (typeof value === "boolean") return value;
315
+ if (typeof value === "string") {
316
+ const normalized = value.trim().toLowerCase();
317
+ if (normalized === "true") return true;
318
+ if (normalized === "false") return false;
319
+ }
320
+
321
+ diagnostics.push(
322
+ createDiagnostic(
323
+ "invalid-boomerang",
324
+ filePath,
325
+ source,
326
+ `Using default boomerang=false for ${filePath}: frontmatter field "boomerang" must be true or false.`,
327
+ ),
328
+ );
329
+ return false;
330
+ }
331
+
306
332
  function normalizeLoop(
307
333
  value: unknown,
308
334
  filePath: string,
@@ -1643,6 +1669,18 @@ function loadPromptsWithModelFromDir(
1643
1669
  const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics);
1644
1670
  const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics);
1645
1671
  const converge = normalizeConverge(frontmatter.converge, fullPath, source, diagnostics);
1672
+ let boomerang = normalizeBoomerang(frontmatter.boomerang, fullPath, source, diagnostics);
1673
+ if (chain && boomerang) {
1674
+ diagnostics.push(
1675
+ createDiagnostic(
1676
+ "invalid-boomerang-chain",
1677
+ fullPath,
1678
+ source,
1679
+ `Ignoring boomerang in ${fullPath}: frontmatter fields "chain" and "boomerang" cannot be combined.`,
1680
+ ),
1681
+ );
1682
+ boomerang = false;
1683
+ }
1646
1684
  if (loop !== undefined && deterministic !== undefined) {
1647
1685
  diagnostics.push(
1648
1686
  createDiagnostic(
@@ -1695,6 +1733,7 @@ function loadPromptsWithModelFromDir(
1695
1733
  fresh === true ||
1696
1734
  loop !== undefined ||
1697
1735
  converge === false ||
1736
+ boomerang === true ||
1698
1737
  safeParallel !== undefined ||
1699
1738
  deterministic !== undefined ||
1700
1739
  hasLineup ||
@@ -1721,6 +1760,7 @@ function loadPromptsWithModelFromDir(
1721
1760
  fresh: fresh || undefined,
1722
1761
  loop: loop !== undefined ? loop : undefined,
1723
1762
  converge: converge === false ? false : undefined,
1763
+ boomerang: boomerang || undefined,
1724
1764
  parallel: safeParallel,
1725
1765
  worktree: safeWorktree,
1726
1766
  deterministic,
@@ -1821,6 +1861,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
1821
1861
  const thinkingValue = prompt.thinkingLevels ? prompt.thinkingLevels.join(",") : prompt.thinking;
1822
1862
  const thinkingLabel = thinkingValue ? ` ${thinkingValue}` : "";
1823
1863
  const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : "";
1864
+ const boomerangLabel = prompt.boomerang ? " boomerang" : "";
1824
1865
  const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : "";
1825
1866
  const parallelLabel = prompt.parallel !== undefined ? ` parallel:${prompt.parallel}` : "";
1826
1867
  const deterministicLabel = prompt.deterministic ? ` deterministic-step:${prompt.deterministic.handoff}` : "";
@@ -1831,7 +1872,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
1831
1872
  const inheritContextLabel = prompt.inheritContext ? " fork" : "";
1832
1873
  const worktreeLabel = prompt.worktree ? " worktree" : "";
1833
1874
  const details =
1834
- `[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
1875
+ `[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${boomerangLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
1835
1876
  return prompt.description ? `${prompt.description} ${details}` : details;
1836
1877
  }
1837
1878
 
@@ -25,7 +25,7 @@ export function renderSkillLoaded(
25
25
  container.addChild(new Spacer(1));
26
26
 
27
27
  const box = new Box(1, 1, (text: string) => theme.bg("toolSuccessBg", text));
28
- box.addChild(new Text(theme.fg("toolTitle", theme.bold(`⚡ Skill loaded: ${skillName}`)), 0, 0));
28
+ box.addChild(new Text(theme.fg("toolTitle", theme.bold(`Skill loaded: ${skillName}`)), 0, 0));
29
29
  box.addChild(new Text(theme.fg("toolOutput", ` ${skillPath}`), 0, 0));
30
30
  box.addChild(new Spacer(1));
31
31
 
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: prompt-template-authoring
3
+ description: |
4
+ Write and run custom Pi prompt templates (slash commands) for this extension.
5
+ Use when creating templates with model selection, deterministic pre-steps,
6
+ loops, chains, subagents, or best-of-N compare flows.
7
+ ---
8
+
9
+ # Prompt Template Authoring
10
+
11
+ Use this skill when working on prompt templates for `pi-prompt-template-model`.
12
+ Templates are markdown files that register as slash commands.
13
+
14
+ ## Where Templates Live
15
+
16
+ - `~/.pi/agent/prompts/` — user prompts (highest priority)
17
+ - `.pi/prompts/` inside a project — project-specific prompts
18
+
19
+ Extension `examples/` are reference files only. Copy them to a prompt directory to register them.
20
+
21
+ ## Minimal Template
22
+
23
+ ```markdown
24
+ ---
25
+ model: claude-sonnet-4-20250514
26
+ ---
27
+ Your prompt body here.
28
+ ```
29
+
30
+ Save as `my-command.md`, restart Pi, run `/my-command`. Use `description:` for autocomplete text.
31
+
32
+ ## Model Selection
33
+
34
+ Omit `model:` to inherit the current session model. Otherwise:
35
+
36
+ - `model: claude-sonnet-4-20250514` — specific model
37
+ - `model: claude-opus-4, gpt-5.4` — fallback order (tries first, falls back to second if unavailable)
38
+ - `model: claude-opus-4, gpt-5.4` + `rotate: true` — cycle through list on each loop iteration
39
+
40
+ ## Argument Substitution
41
+
42
+ The prompt body can use placeholders:
43
+
44
+ - `$@` — all arguments passed to the command
45
+ - `$1`, `$2` — specific positional arguments
46
+ - `${@:1}` — argument 1 and everything after
47
+
48
+ ## Deterministic Steps (Pre-LLM Execution)
49
+
50
+ Run a command or script before the LLM turn. The model only sees the output if you want it to.
51
+
52
+ Two equivalent forms. Don't mix them in the same prompt.
53
+
54
+ **Shorthand form** — top-level keys:
55
+
56
+ ```yaml
57
+ ---
58
+ run: git status --short
59
+ handoff: always
60
+ ---
61
+ Summarize the repo state.
62
+ ```
63
+
64
+ **Nested form** — under `deterministic:`:
65
+
66
+ ```yaml
67
+ ---
68
+ deterministic:
69
+ run: ./scripts/ship.sh
70
+ handoff: on-failure
71
+ timeout: 60000
72
+ ---
73
+ Diagnose the failure and suggest a fix.
74
+ ```
75
+
76
+ **Handoff controls when the LLM sees the result:**
77
+
78
+ - `never` — run, show result, done (no LLM turn)
79
+ - `always` — always hand result to model
80
+ - `on-failure` — only hand off if command exits non-zero
81
+ - `on-success` — only hand off if command exits zero
82
+
83
+ **Execution forms:**
84
+
85
+ - `run: command string` — runs via `/bin/bash -lc`
86
+ - `run: {command: git, args: [status], shell: false}` — explicit args, optional shell
87
+ - `script: ./script.sh` or `script: {path: ./script.sh, args: [--fast]}` — run a file
88
+
89
+ **Constraints:**
90
+ - Only single prompt templates (no `chain`, `loop`, `subagent`, or `parallel`)
91
+ - Runtime flags `--loop`, `--subagent`, `--fork` are rejected for deterministic prompts
92
+
93
+ ## Subagent Delegation
94
+
95
+ Delegate to another Pi agent instead of running inline:
96
+
97
+ ```yaml
98
+ ---
99
+ model: claude-sonnet-4-20250514
100
+ subagent: delegate # or true, or a specific agent name
101
+ inheritContext: true # fork conversation context (optional)
102
+ cwd: /absolute/path # working directory for the subagent (optional)
103
+ parallel: 3 # run 3 copies in parallel (optional)
104
+ ---
105
+ $@
106
+ ```
107
+
108
+ Requires [pi-subagents](https://github.com/nicobailon/pi-subagents/) to be installed.
109
+
110
+ ## Loops
111
+
112
+ Run the prompt multiple times:
113
+
114
+ ```yaml
115
+ ---
116
+ model: claude-sonnet-4-20250514
117
+ loop: 5 # run exactly 5 times
118
+ converge: true # stop early if no changes (default)
119
+ fresh: true # collapse context between iterations
120
+ ---
121
+ $@
122
+ ```
123
+
124
+ Or at runtime: `/command --loop 5`, `/command --loop` (unlimited), or `/command --loop=5 --fresh`.
125
+
126
+ ## Chains
127
+
128
+ Chain templates declare a reusable pipeline:
129
+
130
+ ```yaml
131
+ ---
132
+ chain: analyze -> fix -> test
133
+ chainContext: summary # pass step summaries to later delegated steps
134
+ ---
135
+ $@
136
+ ```
137
+
138
+ Or use `/chain-prompts analyze -> fix -> test` at runtime. Chain templates ignore the body and `model:` field.
139
+
140
+ ## Model Conditionals
141
+
142
+ Show different content based on which model runs:
143
+
144
+ ```markdown
145
+ <if-model is="anthropic/*">
146
+ Use Claude-specific instructions.
147
+ <else>
148
+ Use default instructions.
149
+ </if-model>
150
+ ```
151
+
152
+ Supports exact IDs, `provider/model-id` pairs, wildcards (`anthropic/*`), and comma-separated combinations.
153
+
154
+ ## Best-of-N Compare
155
+
156
+ Run multiple workers, aggregate with reviewers, optionally apply final changes:
157
+
158
+ ```yaml
159
+ ---
160
+ description: Best-of-N code review
161
+ bestOfN:
162
+ worktree: true # required if using finalApplier
163
+ workers:
164
+ - model: openai-codex/gpt-5.4-mini:low
165
+ count: 2
166
+ reviewers:
167
+ - model: anthropic/claude-sonnet-4-20250514:medium
168
+ finalApplier:
169
+ agent: delegate
170
+ model: anthropic/claude-sonnet-4-20250514:high
171
+ ---
172
+ $@
173
+ ```
174
+
175
+ ## Runtime Flags
176
+
177
+ Override frontmatter at invocation:
178
+
179
+ - `--model=provider/model-id` — use this model instead
180
+ - `--subagent` / `--subagent=<name>` / `--subagent:<name>` — force delegation
181
+ - `--fork` — force delegation with context fork
182
+ - `--loop N` / `--loop=N` / `--loop` — override loop count (unlimited if bare)
183
+ - `--fresh` — collapse context between iterations
184
+ - `--no-converge` — run all iterations even if no changes
185
+ - `--cwd=/absolute/path` — working directory override when the prompt supports `cwd`
186
+ - `--chain-context` — pass summaries to later delegated chain steps
187
+ - `--worktree` — use git worktrees for parallel delegated work
188
+
189
+ When stuck, check `README.md` and `examples/best-of-n.md` in this extension.
package/tool-manager.ts CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
5
- import { Type } from "@sinclair/typebox";
5
+ import { Type } from "typebox";
6
6
  import { notify } from "./notifications.js";
7
7
 
8
8
  export interface ToolManagerDeps {