pi-prompt-template-model-enhanced 0.11.2 → 0.13.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
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/Nabsku/pi-prompt-template-model-enhanced/compare/v0.12.0...v0.13.0) (2026-06-21)
4
+
5
+ ### Prompt library
6
+
7
+ * Added `.pi/prompt-library` and `~/.pi/agent/prompt-library` roots for reusable prompt commands, chain steps, and include fragments ([b5e235b](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/b5e235b099df3a4089b196a3e482c64e545f5bbb), [4eba6b3](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/4eba6b3c98b83d50ab167e66eb26c782ade00d99)).
8
+ * Prompt-library commands now support validation, source summaries, dry-run/TUI labels, and includes from library-local fragments ([7dec4c8](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/7dec4c8bc2f774fa6451c842f9ee12cc875de648), [5468b16](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/5468b16e19ac1e556980f9a5416fc574be055aeb), [ceda208](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/ceda208e9f027d7a45c600bf2bcfb36d4552f55e)).
9
+ * Added `hidden: true` for internal prompt-library commands: hidden commands stay out of slash-command discovery and pickers, but remain addressable by exact dry-run/print lookup and chain references ([413ef55](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/413ef55ac8c3bafe61c8b1a3d560eb644e7e7d15)).
10
+
11
+ ### Hardening and compatibility
12
+
13
+ * Tightened project prompt-library trust boundaries, including chain preflight approval and safer handling of project library includes ([c1198cf](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/c1198cf918045db38d2d7c8f767e1be82843f634), [40c4458](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/40c44587ff8a94e2f6d6d94f13a9c2894503d35b)).
14
+ * Hardened prompt-library loading around symlinks, dot-prefixed entries, invalid metadata, include-only fragments, and stale hidden command handlers ([94c8bdd](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/94c8bddcba1d16502676d34f47fccbeb3235fd05), [b62cbec](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/b62cbecbeaf25ecef66abf14c6e71bca03c40c3c), [ace7651](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/ace76519fcea80a20b217ce82e62a201ad0afa06)).
15
+ * Plain prompt-library fragments now stay quiet unless included, while valid thinking-only and model-conditional library commands still load normally ([c353e1e](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/c353e1e0d10377e94bae763084efd1f9ac398260), [f2e8850](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/f2e8850ea2c5be080d7c73764303f5fabdcd1c44), [8808b05](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/8808b05eeafd226adfb5a57a07c195d0550fe4e8), [dea05fc](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/dea05fc9b27755f9407976e4028f77704e27e044)).
16
+ * Validation source summaries now count broken, skipped, duplicate, empty-model, invalid-marker, and shadowed library commands consistently instead of misreporting them as fragments or dropping them ([354bea9](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/354bea956a13e0845167246f4ba68778db5ce6b8), [126ab9b](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/126ab9bbe2149f1c6c78967ded308f56f933a6e7), [fd028ca](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/fd028ca1ad78f329ef1d9325158d66a2541701e4), [0fc6877](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/0fc6877dca2b16385d7a95e6d01a7a3f94d2ce07), [e8a91d1](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/e8a91d14be0430894cdc3716f28754ad561edd68), [73c91c1](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/73c91c1b2d41792528dc32c5e9798b7bb213c09e)).
17
+
18
+ ## [0.12.0](https://github.com/Nabsku/pi-prompt-template-model-enhanced/compare/v0.11.2...v0.12.0) (2026-06-20)
19
+
20
+
21
+ ### Features
22
+
23
+ * add includes pane to dry-run TUI ([50624a4](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/50624a46a5722d2ca74ebcb0423e5e99f75dbad1))
24
+ * attach include graphs to dry runs ([a1d7eba](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/a1d7eba676158faed0c4e9cf889e5525cf5e5923))
25
+ * attach include graphs to validation ([d694724](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/d69472452a6af1b7f94f75df3002b65d5bdc62c8))
26
+ * collect prompt include graphs ([be7c726](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/be7c726fb17a43524068ef8da819306304e79091))
27
+ * collect prompt source records ([0d0df7a](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/0d0df7a38a460fb1d890596e8f334a9be5772892))
28
+ * expose prompt include parsing helpers ([3e784a6](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/3e784a672304100e70ae3a6603a05be6c2744e5e))
29
+ * report prompt include graphs ([5332c8e](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/5332c8e8c49925aec20ebe2ba913e6b6a13f814f))
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * avoid duplicate include graph diagnostics ([adb6c97](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/adb6c971fa3e1faea902aead4493fd47cf4121a6))
35
+ * preserve skipped include graphs for overrides ([2b0e69f](https://github.com/Nabsku/pi-prompt-template-model-enhanced/commit/2b0e69f3f158c8e5e8ef3bae45053cdd267a19e7))
36
+
3
37
  ## [0.11.2](https://github.com/Nabsku/pi-prompt-template-model-enhanced/compare/v0.11.1...v0.11.2) (2026-06-19)
4
38
 
5
39
 
@@ -39,10 +73,11 @@
39
73
 
40
74
  ### Added
41
75
  - Added `/print-prompt` and `/dry-run-prompt` to preview rendered prompt templates without execution.
42
- - Added a Pi-native dry-run TUI: no-name dry-run commands open a searchable template picker in Pi TUI mode, while named templates open a read-only inspector for prompt body, metadata, skills, warnings, and the raw report.
76
+ - Added a Pi-native dry-run TUI: no-name dry-run commands open a searchable template picker in Pi TUI mode, while named templates open a read-only inspector for prompt body, metadata, skills, includes, warnings, and the raw report.
77
+ - Added a static, permanent `Includes` pane to the dry-run TUI inspector. It shows the include graph captured during dry-run rendering, or `No includes.` for prompts without include metadata or inline include directives.
43
78
 
44
79
  ### Changed
45
- - Added `/validate-prompts` to validate prompt templates, includes, chain declarations, and skill references before runtime.
80
+ - Added `/validate-prompts` to validate prompt templates, includes, chain declarations, and skill references before runtime, including include graph reporting for prompt dependencies and include failures.
46
81
  - Updated development-only Pi packages from the deprecated `@mariozechner/*` namespace to `@earendil-works/*` `0.79.7`, and bumped `tsx` to `^4.22.4`.
47
82
  - Added Dependabot version updates for npm development dependencies and GitHub Actions.
48
83
  - Added Release Please release automation and npm trusted publishing workflow.
package/README.md CHANGED
@@ -83,10 +83,15 @@ Start a Python REPL session and help me debug: $@
83
83
 
84
84
  Run `/validate-prompts` to check prompt templates before using them. It reloads the project and user prompt directories, validates frontmatter, include paths, include cycles, chain declarations, reserved command names, and skill references that can be resolved from registered or filesystem skills.
85
85
 
86
+ Validation also reports an include graph for prompts that declare frontmatter includes (`include` / `includes`), use inline include directives, or fail include processing. Each relevant prompt is listed with its include dependencies, including nested partial-to-partial includes. The report also includes a source summary that separates project prompts, user prompts, prompt-library commands, and include-only prompt-library fragments so command-capable library entries are easy to distinguish from reusable parts.
87
+
86
88
  A clean library reports success:
87
89
 
88
90
  ```text
89
91
  [pi-prompt-template-model-enhanced] Prompt validation passed: 4 prompt template(s) loaded.
92
+ Include graph:
93
+ - review [ok] /repo/.pi/prompts/review.md
94
+ - review -> /repo/.pi/prompts/shared/rules.md (frontmatter shared/rules.md) [ok]
90
95
  ```
91
96
 
92
97
  Invalid libraries fail with explicit diagnostics:
@@ -95,8 +100,14 @@ Invalid libraries fail with explicit diagnostics:
95
100
  [pi-prompt-template-model-enhanced] Prompt validation failed: 2 issue(s) found across 3 loaded prompt template(s).
96
101
  - include-not-found (project) /repo/.pi/prompts/review.md: Prompt include "shared/rules.md" was not found ...
97
102
  - skill-not-found (project) /repo/.pi/prompts/debug.md: Prompt template ... references skill "tmux", but it was not found ...
103
+ Include graph:
104
+ - review [skipped] /repo/.pi/prompts/review.md
105
+ - review -> unresolved:shared/rules.md (frontmatter shared/rules.md) [failed]
106
+ ! include-not-found: Prompt include "shared/rules.md" was not found ...
98
107
  ```
99
108
 
109
+ Include graph statuses are concise: `[ok]` means the prompt or edge resolved successfully, `[skipped]` means the loader skipped that prompt because include processing failed or was invalid, and `[failed]` marks a failed edge or root diagnostic. Prompts skipped for missing, cyclic, or invalid includes still appear in the include graph with `[skipped]`, failed include edges, and diagnostic codes such as `include-not-found` or `include-cycle`.
110
+
100
111
  ## Dry-run and TUI preview
101
112
 
102
113
  Use `/print-prompt` or `/dry-run-prompt` to preview what a prompt template would send before it executes. The preview uses the same include rendering, model conditionals, argument substitution, skill resolution, loop metadata, delegation metadata, and runtime flags as normal execution, but it does not switch models, send user messages, run deterministic commands, or start subagents.
@@ -123,10 +134,13 @@ In Pi TUI mode, the commands open an interactive picker/inspector by default:
123
134
  TUI behavior:
124
135
 
125
136
  - no template name opens a searchable template picker;
137
+ - picker rows label prompt-library commands as `project library` or `user library`, so reusable library commands are distinct from core `.pi/prompts` templates;
138
+ - `hidden: true` command-capable prompts are omitted from the picker, but an exact `/print-prompt <name>` or `/dry-run-prompt <name>` still opens them;
126
139
  - a template name opens the inspector directly;
127
140
  - `--plain` forces the stdout/plain report even in TUI mode;
128
141
  - unsupported templates, such as deterministic or chain templates, show the same diagnostic as the plain path;
129
- - full skill content remains hidden unless the dry-run result was created with `--show-skills`.
142
+ - full skill content remains hidden unless the dry-run result was created with `--show-skills`;
143
+ - the inspector always includes a static `Includes` pane. Prompts with include metadata or inline include directives show the include graph captured during dry-run rendering; prompts without includes show `No includes.`.
130
144
 
131
145
  The inspector is read-only. It has no execute button and does not mutate the session.
132
146
 
@@ -145,6 +159,7 @@ All fields are optional. Templates that don't use any extension features (no `mo
145
159
  | `includes` | — | List of shared `.md` partials to insert into the prompt. See [Prompt includes](#prompt-includes). |
146
160
  | `include` | — | Shortcut for a single partial, equivalent to `includes: [file.md]`. See [Prompt includes](#prompt-includes). |
147
161
  | `description` | — | Short text shown next to the command in autocomplete. |
162
+ | `hidden` | `false` | Hide a command-capable prompt from slash-command registration and the TUI picker while keeping it addressable by exact `/print-prompt` / `/dry-run-prompt` name and usable as an internal chain step. Visibility metadata alone does not make a plain file command-capable. |
148
163
  | `chain` | — | Declares a reusable pipeline of templates (`step -> step`). When set, the body is ignored. See [Chain Templates](#chain-templates). |
149
164
  | `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). |
150
165
 
@@ -308,6 +323,38 @@ Compare prompts (`bestOfN`) cannot combine with `skill` or `skills` in v1 becaus
308
323
 
309
324
  Prompt includes let you write the common parts of your prompts once and reuse them. Put shared Markdown in partials, then pull those partials into any prompt that needs them.
310
325
 
326
+ ### Prompt library
327
+
328
+ `.pi/prompt-library/` is an extension-managed prompt library. Pi core does not load files from this directory. If you want a file managed only by this extension rather than Pi core, place it in `.pi/prompt-library/` instead of `.pi/prompts/`.
329
+
330
+ User prompt-library files live at `~/.pi/agent/prompt-library/` (that is the current OS user's home directory as reported by the runtime, not the repository root).
331
+
332
+ Prompt-library files can be executable extension prompt templates, chain steps, or include targets. A prompt-library file becomes an extension command only when it is command-capable under the same rules as `.pi/prompts` templates: for example, it has extension frontmatter such as `model`, `chain`, `skill`, `skills`, `include`, `includes`, or other supported extension fields. Plain Markdown fragments under `partials/` are intended to be included and should not appear as slash commands, chain steps, or dry-run targets. The `partials/` directory name is a convention, not an enforced policy: any plain, non-command-capable prompt-library Markdown file can be included, and command-capable files can live under any non-hidden directory.
333
+
334
+ Set `hidden: true` on command-capable prompt-library files that should stay internal. Hidden commands are not registered as top-level slash commands and do not appear in the dry-run picker, but they can still be opened by exact `/print-prompt <name>` / `/dry-run-prompt <name>` and referenced by chains. Project prompt-library trust approval still applies when a hidden project-library step executes through a chain.
335
+
336
+ Dot-prefixed files and directories under prompt-library roots are ignored. Symlinks are followed only when their resolved target remains inside the canonical prompt root; symlinks that escape the root are skipped.
337
+
338
+ Project prompt-library commands require extension-side per-session UI approval before execution. This approval is separate from Pi's project trust state: even trusted projects still need the extension approval for commands loaded from `.pi/prompt-library/`. Non-UI/headless contexts cannot show that prompt, so move commands that must run headlessly in trusted projects to a core prompt root such as `.pi/prompts/`; keep `.pi/prompt-library/` for UI-approved project commands and include-only fragments.
339
+
340
+ For example, a project prompt can include a standards fragment from the project prompt library:
341
+
342
+ ```text
343
+ .pi/prompts/review.md
344
+ .pi/prompt-library/partials/repo-standards.md
345
+ ```
346
+
347
+ ```markdown
348
+ ---
349
+ description: Review with shared repo standards
350
+ model: claude-sonnet-4-20250514
351
+ include: partials/repo-standards.md
352
+ ---
353
+ Review this change: $@
354
+ ```
355
+
356
+ With that layout, `.pi/prompts/review.md` resolves `partials/repo-standards.md` to `.pi/prompt-library/partials/repo-standards.md` when no closer match exists. Included prompt-library files insert only their Markdown body; frontmatter such as `description`, `model`, `skill`, or `skills` is not inherited by the including prompt.
357
+
311
358
  ### Syntax
312
359
 
313
360
  Use `includes:` for the shared block you want at the top of a prompt:
@@ -383,21 +430,25 @@ Include paths are local `.md` files. Resolution starts next to the file that ask
383
430
 
384
431
  If Pi does not find the file there, it checks these roots in order:
385
432
 
386
- 1. The directory of the file containing the directive
387
- 2. The owning prompt root: `~/.pi/agent/prompts` for user prompts, or `<cwd>/.pi/prompts` for project prompts
388
- 3. `~/.pi/agent/prompt-partials`
389
- 4. `<cwd>/.pi/prompt-partials`
433
+ 1. Current file directory
434
+ 2. Current owner root
435
+ 3. Original prompt root
436
+ 4. Project prompt-library (project prompts only; user/global prompts do not fall back into the current project's prompt library)
437
+ 5. User prompt-library
438
+ 6. Global prompt-partials
439
+ 7. Project prompt-partials
390
440
 
391
- Example layout:
441
+ Example project layout:
392
442
 
393
443
  ```text
394
- ~/.pi/agent/prompts/review.md
395
- ~/.pi/agent/prompts/shared/repo-rules.md
444
+ <cwd>/.pi/prompts/review.md
445
+ <cwd>/.pi/prompts/shared/repo-rules.md
446
+ <cwd>/.pi/prompt-library/partials/repo-standards.md
396
447
  ~/.pi/agent/prompt-partials/shared/review-checklist.md
397
448
  <cwd>/.pi/prompt-partials/languages/typescript.md
398
449
  ```
399
450
 
400
- With that layout, `review.md` can include `shared/repo-rules.md`, `shared/review-checklist.md`, and `languages/typescript.md` without absolute paths.
451
+ With that layout, the project prompt `.pi/prompts/review.md` can include `shared/repo-rules.md`, `partials/repo-standards.md`, `shared/review-checklist.md`, and `languages/typescript.md` without absolute paths. The project-library fallback (`partials/repo-standards.md`) applies because `review.md` is a project prompt; a user prompt from `~/.pi/agent/prompts/` would not search `<cwd>/.pi/prompt-library/` for that include.
401
452
 
402
453
  ### Rules and guardrails
403
454
 
@@ -410,6 +461,8 @@ With that layout, `review.md` can include `shared/repo-rules.md`, `shared/review
410
461
  - `chain:` wrapper templates cannot use frontmatter `include` or `includes` in v1. Put includes on the step templates instead.
411
462
  - `~/...` paths are allowed only when they resolve under a Pi prompt root or prompt-partials root. Other absolute paths are rejected.
412
463
  - Include boundary comments appear only in debug/diagnostic mode. Normal prompt content does not contain `<!-- BEGIN include ... -->` / `<!-- END include ... -->` comments.
464
+ - Prompt and prompt-library command names are still plain basenames. There is no source or directory namespace for slash commands; if two effective templates use the same basename, normal source precedence/duplicate handling decides which one registers.
465
+ - `validate-prompts` validates extension-visible prompts, command-capable prompt-library files, and include graphs. It is not a general filesystem audit; plain unreferenced fragments are ignored except when they are pulled into an include graph.
413
466
 
414
467
  ## Inline Model Conditionals
415
468
 
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve as resolvePath } from "node:path";
3
3
  import type { Model } from "@earendil-works/pi-ai";
4
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
6
6
  import {
7
7
  extractChainContextFlag,
@@ -23,6 +23,7 @@ import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-e
23
23
  import {
24
24
  buildPromptCommandDescription,
25
25
  expandCwdPath,
26
+ formatPromptSourceLabel,
26
27
  loadPromptsWithModel,
27
28
  type DelegationLineupSlot,
28
29
  type PromptWithModel,
@@ -128,6 +129,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
128
129
  let accumulatedSummaries: string[] = [];
129
130
  let lastDiagnostics = "";
130
131
  let storedCommandCtx: ExtensionCommandContext | null = null;
132
+ const approvedProjectPromptLibraryCwds = new Set<string>();
131
133
  const UNLIMITED_LOOP_CAP = 999;
132
134
 
133
135
  const toolManager = createToolManager(pi, {
@@ -148,6 +150,50 @@ export default function promptModelExtension(pi: ExtensionAPI) {
148
150
  return runtimeModel ?? ctx.model;
149
151
  }
150
152
 
153
+ function isProjectPromptLibraryPrompt(prompt: PromptWithModel): boolean {
154
+ return prompt.source === "project" && prompt.rootKind === "prompt-library";
155
+ }
156
+
157
+ async function ensureProjectPromptLibraryApproved(prompt: PromptWithModel, ctx: ExtensionCommandContext): Promise<boolean> {
158
+ if (!isProjectPromptLibraryPrompt(prompt)) return true;
159
+ // Pi core trust has historically covered only core-known project resources
160
+ // (for example .pi/prompts), not extension-defined .pi/prompt-library
161
+ // commands. Keep a prompt-library-specific session approval instead of
162
+ // relying on ctx.isProjectTrusted(), which can be true for unrelated roots.
163
+
164
+ const cwdKey = resolvePath(ctx.cwd);
165
+ if (approvedProjectPromptLibraryCwds.has(cwdKey)) return true;
166
+
167
+ const message =
168
+ `Project prompt-library command \`${prompt.name}\` is loaded from ${prompt.filePath}. ` +
169
+ "Pi core project trust may not cover .pi/prompt-library in this version. Approve running project prompt-library commands for this session?";
170
+
171
+ if (!ctx.hasUI || typeof (ctx.ui as { confirm?: unknown }).confirm !== "function") {
172
+ notify(ctx, `${message} Run in an interactive UI session and approve it, or move trusted commands to .pi/prompts.`, "error");
173
+ return false;
174
+ }
175
+
176
+ const approved = await ctx.ui.confirm("Approve project prompt-library command", message, { timeout: 30_000 });
177
+ if (!approved) {
178
+ notify(ctx, `Project prompt-library command \`${prompt.name}\` was not approved.`, "warning");
179
+ return false;
180
+ }
181
+
182
+ approvedProjectPromptLibraryCwds.add(cwdKey);
183
+ return true;
184
+ }
185
+
186
+ async function ensureProjectPromptLibraryStepsApproved(promptsToCheck: PromptWithModel[], ctx: ExtensionCommandContext): Promise<boolean> {
187
+ const seen = new Set<string>();
188
+ for (const prompt of promptsToCheck) {
189
+ const key = prompt.filePath;
190
+ if (seen.has(key)) continue;
191
+ seen.add(key);
192
+ if (!(await ensureProjectPromptLibraryApproved(prompt, ctx))) return false;
193
+ }
194
+ return true;
195
+ }
196
+
151
197
  pi.registerMessageRenderer<SkillLoadedDetails>("skill-loaded", renderSkillLoaded);
152
198
  pi.registerMessageRenderer(PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE, renderDelegatedSubagentResult);
153
199
  pi.registerMessageRenderer(PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE, renderDeterministicResult);
@@ -168,7 +214,8 @@ export default function promptModelExtension(pi: ExtensionAPI) {
168
214
  prompts = result.prompts;
169
215
  chainPrompts = chainResult.prompts;
170
216
 
171
- for (const name of prompts.keys()) {
217
+ for (const [name, prompt] of prompts) {
218
+ if (prompt.hidden) continue;
172
219
  registerPromptCommand(name);
173
220
  }
174
221
 
@@ -227,6 +274,8 @@ export default function promptModelExtension(pi: ExtensionAPI) {
227
274
  taskPreamble?: string,
228
275
  loopContext?: string,
229
276
  ): Promise<PromptStepResult | "aborted"> {
277
+ if (!(await ensureProjectPromptLibraryApproved(prompt, ctx))) return "aborted";
278
+
230
279
  const requestedSkills = getRequestedSkills(prompt);
231
280
  const skillResolution = resolvePromptSkills(requestedSkills, ctx.cwd, pi.getCommands() as RuntimeSkillCommand[]);
232
281
  if (skillResolution.kind === "error") {
@@ -682,6 +731,8 @@ export default function promptModelExtension(pi: ExtensionAPI) {
682
731
  currentModel: Model<any> | undefined,
683
732
  runtime: { cwd?: string; model?: string; subagentOverride?: SubagentOverride; fork?: boolean },
684
733
  ) {
734
+ if (!(await ensureProjectPromptLibraryApproved(prompt, ctx))) return;
735
+
685
736
  if (runtime.subagentOverride) {
686
737
  notify(ctx, `--subagent is not supported for compare prompts (ignored)`, "warning");
687
738
  }
@@ -1148,7 +1199,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1148
1199
  return true;
1149
1200
  };
1150
1201
 
1202
+ const resolveChainStepPrompts = (): PromptWithModel[] => flattenChainSteps()
1203
+ .map((step) => chainPrompts.get(step.name))
1204
+ .filter((prompt): prompt is PromptWithModel => prompt !== undefined);
1205
+
1151
1206
  if (!validateChainSteps()) return;
1207
+ if (!(await ensureProjectPromptLibraryStepsApproved(resolveChainStepPrompts(), ctx))) return;
1152
1208
 
1153
1209
  const originalModel = getCurrentModel(ctx);
1154
1210
  const chainInheritedModel = originalModel;
@@ -1186,6 +1242,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1186
1242
  chainAborted = true;
1187
1243
  break;
1188
1244
  }
1245
+ if (!(await ensureProjectPromptLibraryStepsApproved(resolveChainStepPrompts(), ctx))) {
1246
+ chainAborted = true;
1247
+ break;
1248
+ }
1189
1249
  }
1190
1250
 
1191
1251
  const templates = steps.map((step) =>
@@ -1239,6 +1299,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1239
1299
  taskPreamble = `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`;
1240
1300
  }
1241
1301
 
1302
+ if (!(await ensureProjectPromptLibraryStepsApproved(stepTemplate.tasks.map((task) => task.prompt), ctx))) {
1303
+ aborted = true;
1304
+ break;
1305
+ }
1306
+
1242
1307
  let delegated;
1243
1308
  try {
1244
1309
  delegated = await executeSubagentPromptStep({
@@ -1444,11 +1509,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1444
1509
  for (const [name, prompt] of chainPrompts) merged.set(name, prompt);
1445
1510
  for (const [name, prompt] of prompts) merged.set(name, prompt);
1446
1511
  return Array.from(merged.values())
1512
+ .filter((prompt) => !prompt.hidden)
1447
1513
  .sort((a, b) => a.name.localeCompare(b.name))
1448
1514
  .map((prompt) => ({
1449
1515
  name: prompt.name,
1450
1516
  source: prompt.source,
1451
- displaySource: prompt.source,
1517
+ displaySource: formatPromptSourceLabel(prompt),
1452
1518
  file: prompt.filePath,
1453
1519
  description: prompt.description,
1454
1520
  model: prompt.models[0],
@@ -1569,10 +1635,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1569
1635
  storedCommandCtx = ctx;
1570
1636
  refreshPrompts(ctx.cwd, ctx);
1571
1637
  const prompt = prompts.get(name);
1572
- if (!prompt) {
1573
- notify(ctx, `Prompt "${name}" no longer exists`, "error");
1638
+ if (!prompt || prompt.hidden) {
1639
+ notify(ctx, `Prompt "${name}" is no longer available as a slash command`, "error");
1574
1640
  return;
1575
1641
  }
1642
+ if (!(await ensureProjectPromptLibraryApproved(prompt, ctx))) return;
1576
1643
 
1577
1644
  const subagent = extractSubagentOverride(args);
1578
1645
  const runtimeCwd = subagent.cwd ? expandCwdPath(subagent.cwd) : undefined;
@@ -1723,6 +1790,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
1723
1790
 
1724
1791
  function resetSessionScopedState(ctx: ExtensionContext) {
1725
1792
  storedCommandCtx = null;
1793
+ approvedProjectPromptLibraryCwds.clear();
1726
1794
  pendingSkillMessage = undefined;
1727
1795
  previousModel = undefined;
1728
1796
  previousThinking = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-prompt-template-model-enhanced",
3
- "version": "0.11.2",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Prompt template model selector extension for pi coding agent",
6
6
  "author": "Nico Bailon",
@@ -1,10 +1,12 @@
1
1
  import { decodeKittyPrintable, Key, matchesKey, truncateToWidth, type Component } from "@earendil-works/pi-tui";
2
2
  import type { PromptDryRunResult } from "./prompt-dry-run.js";
3
+ import type { PromptIncludeGraph, PromptIncludeGraphEdge, PromptIncludeGraphNode } from "./prompt-includes.js";
4
+ import type { PromptLoaderDiagnostic } from "./prompt-loader.js";
3
5
 
4
6
  export interface PromptTemplateCatalogItem {
5
7
  name: string;
6
8
  source: "project" | "user" | "reserved";
7
- displaySource: "project" | "user" | "reserved";
9
+ displaySource: string;
8
10
  file?: string;
9
11
  description?: string;
10
12
  model?: string;
@@ -20,6 +22,7 @@ export interface PromptDryRunTuiViewModel {
20
22
  prompt: string;
21
23
  metadata: string;
22
24
  skills: string;
25
+ includes: string;
23
26
  warnings: string;
24
27
  raw: string;
25
28
  };
@@ -83,6 +86,69 @@ function formatSkills(result: PromptDryRunResult): string {
83
86
  return lines.join("\n");
84
87
  }
85
88
 
89
+ function lexicalCompare(a: string, b: string): number {
90
+ if (a < b) return -1;
91
+ if (a > b) return 1;
92
+ return 0;
93
+ }
94
+
95
+ function diagnosticKey(diagnostic: PromptLoaderDiagnostic): string {
96
+ return diagnostic.key || `${diagnostic.code}:${diagnostic.source}:${diagnostic.filePath}:${diagnostic.message}`;
97
+ }
98
+
99
+ function sortDiagnostics(diagnostics: PromptLoaderDiagnostic[]): PromptLoaderDiagnostic[] {
100
+ return [...diagnostics].sort((a, b) => lexicalCompare(a.filePath, b.filePath) || lexicalCompare(a.code, b.code) || lexicalCompare(a.message, b.message));
101
+ }
102
+
103
+ function sortIncludeGraphEdges(edges: PromptIncludeGraphEdge[]): PromptIncludeGraphEdge[] {
104
+ return [...edges].sort((a, b) => a.order - b.order || lexicalCompare(a.fromNodeId, b.fromNodeId) || lexicalCompare(a.toNodeId, b.toNodeId) || lexicalCompare(a.includePath, b.includePath));
105
+ }
106
+
107
+ function includeGraphNodeLabel(nodes: Map<string, PromptIncludeGraphNode>, nodeId: string): string {
108
+ const node = nodes.get(nodeId);
109
+ if (!node) return nodeId;
110
+ if (node.filePath) return node.filePath;
111
+ if (node.includePath) return `unresolved:${node.includePath}`;
112
+ return node.id;
113
+ }
114
+
115
+ function formatIncludeDiagnostic(prefix: string, diagnostic: PromptLoaderDiagnostic): string {
116
+ return `${prefix}${diagnostic.code}: ${diagnostic.message}`;
117
+ }
118
+
119
+ function rootOnlyIncludeDiagnostics(graph: PromptIncludeGraph): PromptLoaderDiagnostic[] {
120
+ const edgeDiagnosticKeys = new Set(graph.edges.flatMap((edge) => edge.diagnostics.map(diagnosticKey)));
121
+ return graph.diagnostics.filter((diagnostic) => !edgeDiagnosticKeys.has(diagnosticKey(diagnostic)));
122
+ }
123
+
124
+ function formatIncludes(result: PromptDryRunResult): string {
125
+ const graph = result.status === "ok" ? result.includeGraph : undefined;
126
+ if (!graph) return "No includes.";
127
+
128
+ const rootDiagnostics = sortDiagnostics(rootOnlyIncludeDiagnostics(graph));
129
+ if (graph.edges.length === 0 && rootDiagnostics.length === 0) return "No includes.";
130
+
131
+ const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
132
+ const rootNode = graph.nodes.find((node) => node.kind === "prompt" && node.filePath === graph.root.filePath);
133
+ const rootStatus = rootNode?.status ?? (rootDiagnostics.length ? "failed" : "ok");
134
+ const lines = [`- ${graph.root.promptName} [${rootStatus}] ${graph.root.filePath}`];
135
+
136
+ for (const diagnostic of rootDiagnostics) {
137
+ lines.push(formatIncludeDiagnostic(" ! ", diagnostic));
138
+ }
139
+
140
+ for (const edge of sortIncludeGraphEdges(graph.edges)) {
141
+ const from = includeGraphNodeLabel(nodes, edge.fromNodeId);
142
+ const to = includeGraphNodeLabel(nodes, edge.toNodeId);
143
+ lines.push(` - ${from} -> ${to} (${edge.kind} ${edge.includePath}) [${edge.status}]`);
144
+ for (const diagnostic of sortDiagnostics(edge.diagnostics)) {
145
+ lines.push(formatIncludeDiagnostic(" ! ", diagnostic));
146
+ }
147
+ }
148
+
149
+ return lines.join("\n");
150
+ }
151
+
86
152
  export function createPromptDryRunTuiViewModel(result: PromptDryRunResult, plainReport: string): PromptDryRunTuiViewModel {
87
153
  const warnings = result.warnings.length ? result.warnings.map((warning) => `- ${warning}`).join("\n") : "No warnings.";
88
154
  const prompt = result.status === "ok" ? `# Prompt body\n${result.content}` : `# Error\n${result.error}`;
@@ -100,6 +166,7 @@ export function createPromptDryRunTuiViewModel(result: PromptDryRunResult, plain
100
166
  prompt,
101
167
  metadata,
102
168
  skills: formatSkills(result),
169
+ includes: formatIncludes(result),
103
170
  warnings,
104
171
  raw: plainReport,
105
172
  },
@@ -191,7 +258,7 @@ export class PromptDryRunPicker implements Component {
191
258
  invalidate(): void {}
192
259
  }
193
260
 
194
- const PANE_NAMES = ["Prompt", "Metadata", "Skills", "Warnings", "Raw"] as const;
261
+ const PANE_NAMES = ["Prompt", "Metadata", "Skills", "Includes", "Warnings", "Raw"] as const;
195
262
  type PaneName = typeof PANE_NAMES[number];
196
263
 
197
264
  export class PromptDryRunInspector implements Component {
@@ -229,7 +296,7 @@ export class PromptDryRunInspector implements Component {
229
296
  "",
230
297
  ...body,
231
298
  "",
232
- `pane ${this.paneIndex + 1}/5 · line ${Math.min(this.scroll + 1, paneLines.length)}/${paneLines.length} · j/down scroll · tab next · b back · q quit`,
299
+ `pane ${this.paneIndex + 1}/${PANE_NAMES.length} · line ${Math.min(this.scroll + 1, paneLines.length)}/${paneLines.length} · j/down scroll · tab next · b back · q quit`,
233
300
  ];
234
301
  return linesSafe(lines, width);
235
302
  }
@@ -248,7 +315,7 @@ export class PromptDryRunInspector implements Component {
248
315
  this.scroll = 0;
249
316
  return;
250
317
  }
251
- const paneKey = (["1", "2", "3", "4", "5"] as const).find((key) => matchesKey(data, key));
318
+ const paneKey = Array.from({ length: PANE_NAMES.length }, (_, index) => String(index + 1)).find((key) => matchesKey(data, key));
252
319
  if (paneKey) {
253
320
  this.paneIndex = Number(paneKey) - 1;
254
321
  this.scroll = 0;
package/prompt-dry-run.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import type { RegistryLike } from "./model-selection.js";
12
12
  import { preparePromptExecution } from "./prompt-execution.js";
13
13
  import { expandCwdPath, type PromptWithModel } from "./prompt-loader.js";
14
+ import type { PromptIncludeGraph } from "./prompt-includes.js";
14
15
  import { getRequestedSkills, resolvePromptSkills, type RuntimeSkillCommand } from "./prompt-skills.js";
15
16
  import { DEFAULT_SUBAGENT_NAME } from "./subagent-runtime.js";
16
17
 
@@ -53,6 +54,7 @@ export interface PromptDryRunRuntimeMetadata {
53
54
 
54
55
  export interface PromptDryRunDetails {
55
56
  skills: PromptDryRunSkillPreview[];
57
+ includeGraph?: PromptIncludeGraph;
56
58
  }
57
59
 
58
60
  export interface PromptDryRunSuccess {
@@ -65,6 +67,7 @@ export interface PromptDryRunSuccess {
65
67
  warnings: string[];
66
68
  skills: PromptDryRunSkillPreview[];
67
69
  details: PromptDryRunDetails;
70
+ includeGraph?: PromptIncludeGraph;
68
71
  runtime: PromptDryRunRuntimeMetadata;
69
72
  }
70
73
 
@@ -379,7 +382,8 @@ export async function createPromptDryRun(
379
382
  modelAlreadyActive: prepared.selectedModel.alreadyActive,
380
383
  warnings,
381
384
  skills: skillPreviews,
382
- details: { skills: skillPreviews },
385
+ includeGraph: effectivePrompt.includeGraph,
386
+ details: { skills: skillPreviews, includeGraph: effectivePrompt.includeGraph },
383
387
  runtime,
384
388
  };
385
389
  }