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 +37 -2
- package/README.md +62 -9
- package/index.ts +73 -5
- package/package.json +1 -1
- package/prompt-dry-run-tui.ts +71 -4
- package/prompt-dry-run.ts +5 -1
- package/prompt-includes.ts +663 -70
- package/prompt-loader.ts +608 -51
- package/prompt-validation.ts +255 -3
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.
|
|
387
|
-
2.
|
|
388
|
-
3.
|
|
389
|
-
4.
|
|
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
|
-
|
|
395
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
package/prompt-dry-run-tui.ts
CHANGED
|
@@ -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:
|
|
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}
|
|
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 = (
|
|
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
|
-
|
|
385
|
+
includeGraph: effectivePrompt.includeGraph,
|
|
386
|
+
details: { skills: skillPreviews, includeGraph: effectivePrompt.includeGraph },
|
|
383
387
|
runtime,
|
|
384
388
|
};
|
|
385
389
|
}
|