@gotgenes/pi-permission-system 32.0.5 → 32.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [32.0.6](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v32.0.5...pi-permission-system-v32.0.6) (2026-09-16)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** stop a removed prompt section from swallowing the prose after it ([af79199](https://github.com/gotgenes/pi-packages/commit/af79199ac42e6b09587bf0cbb256435a176660ab))
|
|
14
|
+
* **pi-permission-system:** keep a custom system prompt's own tool and guideline sections ([7cce96a](https://github.com/gotgenes/pi-packages/commit/7cce96af106d4822799b4728ecf1432526a79ba1))
|
|
15
|
+
* **pi-permission-system:** leave an unedited prompt region byte-identical ([1f1ce66](https://github.com/gotgenes/pi-packages/commit/1f1ce669d6d19d591da1262e89b3c919300b0f75))
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* **pi-permission-system:** record the tool-surface removal boundary ([ebbb8f0](https://github.com/gotgenes/pi-packages/commit/ebbb8f07c77eaa22c45b83e2d43397770dde1054))
|
|
20
|
+
* **pi-permission-system:** record the footerless-prompt edge in the tool-surface pass ([2aa7435](https://github.com/gotgenes/pi-packages/commit/2aa74353864cf648f802a8a63cbe5b1b141c27af))
|
|
21
|
+
|
|
8
22
|
## [32.0.5](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v32.0.4...pi-permission-system-v32.0.5) (2026-09-16)
|
|
9
23
|
|
|
10
24
|
|
package/docs/configuration.md
CHANGED
|
@@ -1178,7 +1178,8 @@ Additional behaviors:
|
|
|
1178
1178
|
- On the turn a tool is restored, it is callable immediately but its `Available tools:` line reappears one turn later: pi builds the prompt parts an extension receives before the extension runs, so the restored tool has no one-line description to render until it is already active
|
|
1179
1179
|
- A tool is removed only when every value under its surface resolves to `deny`; a surface with any reachable `allow` or `ask` pattern stays available (see [Tool Surfaces](#tool-surfaces))
|
|
1180
1180
|
- The `Available tools:` and `Guidelines:` sections are **relocated** rather than edited in place: the copies pi wrote are removed, and this session's own are rendered at the end of the system prompt, after pi's `Current working directory:` footer.
|
|
1181
|
-
Each session states its own tool surface, which is what keeps a subagent child's inherited prompt byte-identical to its parent's (see [ADR 0014](decisions/0014-tool-surface-is-node-local-prose.md)); the tool list moves to the end of the prompt for every session, whether or not anything is denied
|
|
1181
|
+
Each session states its own tool surface, which is what keeps a subagent child's inherited prompt byte-identical to its parent's (see [ADR 0014](decisions/0014-tool-surface-is-node-local-prose.md)); the tool list moves to the end of the prompt for every session, whether or not anything is denied.
|
|
1182
|
+
Only the copies pi wrote are removed: a custom system prompt (`.pi/SYSTEM.md`, `~/.pi/agent/SYSTEM.md`, `--system-prompt`) keeps its own text untouched, sections and all, because pi writes no tool surface of its own under one — so a prompt that lists tools itself is shown alongside this session's block rather than replaced by it.
|
|
1182
1183
|
- The rendered sections follow pi's own rules: a tool is listed only when pi supplied a one-line description for it, and the guideline bullets are the allowed tools' own contributions around pi's built-in ones
|
|
1183
1184
|
- The prompt is recomputed and returned on every turn but is stable across turns for a stable policy/agent, so the provider's prompt cache (tools + system prefix) is preserved rather than rewritten each turn.
|
|
1184
1185
|
A policy change is an intentional cache transition, as a mid-session agent switch already is.
|
package/package.json
CHANGED
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
* differs from its parent's (#890).
|
|
11
11
|
*
|
|
12
12
|
* So the region is *relocated* rather than narrowed: the sections Pi wrote are
|
|
13
|
-
* removed
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* removed, and this node's own are rendered at the end of the prompt, past
|
|
14
|
+
* everything a child inherits. Each session then states its own tool surface
|
|
15
|
+
* and no session edits another's bytes.
|
|
16
|
+
*
|
|
17
|
+
* Removal is bounded to the text this package or Pi wrote. The prompt is split
|
|
18
|
+
* at Pi's `Current working directory:` footer, which it writes last and
|
|
19
|
+
* unconditionally: everything after it was appended by an extension, and
|
|
20
|
+
* everything before it is Pi's own preamble only when Pi did not build the
|
|
21
|
+
* prompt from a `customPrompt`. Under a custom prompt Pi writes no tool
|
|
22
|
+
* surface at all, so a section matched above the footer is a user's or another
|
|
23
|
+
* extension's — removing it destroyed their text (#919, #932).
|
|
16
24
|
*
|
|
17
25
|
* Rendering follows `buildSystemPrompt`'s own rules — a tool is listed only
|
|
18
26
|
* when it has a snippet, and the guideline bullets are the allowed tools' own
|
|
@@ -28,6 +36,15 @@ export interface ToolSurfaceInputs {
|
|
|
28
36
|
readonly toolSnippets: Readonly<Record<string, string>>;
|
|
29
37
|
/** Guideline bullets each tool contributes, keyed by tool name. */
|
|
30
38
|
readonly guidelinesByTool: ReadonlyMap<string, readonly string[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Whether Pi wrote the prompt's preamble itself.
|
|
41
|
+
*
|
|
42
|
+
* False when Pi assembled the prompt from `customPrompt` — a user's
|
|
43
|
+
* SYSTEM.md, or a subagent child's assembled prompt — in which case Pi wrote
|
|
44
|
+
* no tool-surface sections and every line above its footer belongs to
|
|
45
|
+
* somebody else.
|
|
46
|
+
*/
|
|
47
|
+
readonly piAuthoredPreamble: boolean;
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
type LineSection = {
|
|
@@ -46,6 +63,18 @@ const GUIDELINES_SECTION_HEADER = "Guidelines:";
|
|
|
46
63
|
*/
|
|
47
64
|
const CUSTOM_TOOLS_FILLER_PREFIX = "In addition to the tools above";
|
|
48
65
|
|
|
66
|
+
/** What Pi writes under `Available tools:` when no selected tool has a snippet. */
|
|
67
|
+
const EMPTY_LIST_PLACEHOLDER = "(none)";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The first line of the footer Pi writes last, in both of its branches.
|
|
71
|
+
*
|
|
72
|
+
* It is the boundary between what Pi assembled and what extensions appended
|
|
73
|
+
* after it — the same anchor `@gotgenes/pi-subagents` uses to find Pi's
|
|
74
|
+
* session-resolved tail.
|
|
75
|
+
*/
|
|
76
|
+
const PROMPT_FOOTER_PREFIX = "Current working directory: ";
|
|
77
|
+
|
|
49
78
|
/** Pi's two unconditional guideline bullets, in the order it writes them. */
|
|
50
79
|
const UNIVERSAL_GUIDELINES: readonly string[] = [
|
|
51
80
|
"Be concise in your responses",
|
|
@@ -63,23 +92,69 @@ export function renderToolSurface(
|
|
|
63
92
|
systemPrompt: string,
|
|
64
93
|
inputs: ToolSurfaceInputs,
|
|
65
94
|
): string {
|
|
66
|
-
const lines =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
const lines = normalizePrompt(systemPrompt).split("\n");
|
|
96
|
+
const tailStart = extensionTailStart(lines);
|
|
97
|
+
const body = [
|
|
98
|
+
settleRegion(lines.slice(0, tailStart), inputs.piAuthoredPreamble),
|
|
99
|
+
settleRegion(lines.slice(tailStart), true),
|
|
100
|
+
]
|
|
101
|
+
.filter((region) => region.length > 0)
|
|
102
|
+
.join("\n")
|
|
103
|
+
.trimEnd();
|
|
70
104
|
const block = renderToolSurfaceBlock(inputs);
|
|
71
105
|
|
|
72
106
|
return body.length > 0 ? `${body}\n\n${block}` : block;
|
|
73
107
|
}
|
|
74
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Where the text extensions appended begins: the line after Pi's footer, or
|
|
111
|
+
* the end of the prompt when nothing downstream left one.
|
|
112
|
+
*
|
|
113
|
+
* The last footer is Pi's own — it appends one after everything it assembled,
|
|
114
|
+
* so a line of the same shape in a custom prompt is always above it.
|
|
115
|
+
*
|
|
116
|
+
* Accepted edge: a prompt carrying no footer at all is treated as all head, so
|
|
117
|
+
* a block appended to *that* prompt cannot be found and replaced, and a custom
|
|
118
|
+
* preamble would collect a second one. Pi writes the footer last and in both
|
|
119
|
+
* branches, so reaching this needs a downstream rewrite of Pi's whole output —
|
|
120
|
+
* which has already broken `@gotgenes/pi-subagents`' identity anchor, since it
|
|
121
|
+
* reads the same line.
|
|
122
|
+
*/
|
|
123
|
+
function extensionTailStart(lines: readonly string[]): number {
|
|
124
|
+
const footerAt = lines.findLastIndex((line) =>
|
|
125
|
+
line.startsWith(PROMPT_FOOTER_PREFIX),
|
|
126
|
+
);
|
|
127
|
+
return footerAt === -1 ? lines.length : footerAt + 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One region's surviving text: its sections removed, when they are ours to
|
|
132
|
+
* remove.
|
|
133
|
+
*
|
|
134
|
+
* Blank runs are collapsed only where a removal opened one, so a region this
|
|
135
|
+
* pass took nothing out of is returned exactly as it arrived rather than
|
|
136
|
+
* reflowed by a pass that had nothing to do with it.
|
|
137
|
+
*/
|
|
138
|
+
function settleRegion(
|
|
139
|
+
lines: readonly string[],
|
|
140
|
+
removalAllowed: boolean,
|
|
141
|
+
): string {
|
|
142
|
+
if (!removalAllowed) {
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
const kept = removeToolSurfaceSections(lines);
|
|
146
|
+
const text = kept.join("\n");
|
|
147
|
+
return kept.length === lines.length ? text : collapseExtraBlankLines(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
75
150
|
/**
|
|
76
151
|
* Remove the `Available tools:` and `Guidelines:` sections, and the filler
|
|
77
|
-
* sentence between them.
|
|
152
|
+
* sentence between them, from one region.
|
|
78
153
|
*
|
|
79
154
|
* Each section is located by its own header, so the two are removed whether
|
|
80
|
-
* they sit adjacent in Pi's preamble or alone in
|
|
81
|
-
*
|
|
82
|
-
*
|
|
155
|
+
* they sit adjacent in Pi's preamble or alone in the tail — including a block
|
|
156
|
+
* this function already produced, which is what makes it safe to apply to its
|
|
157
|
+
* own output, and what keeps it order-independent with a second writer.
|
|
83
158
|
*/
|
|
84
159
|
function removeToolSurfaceSections(lines: readonly string[]): string[] {
|
|
85
160
|
let remaining = [...lines];
|
|
@@ -209,21 +284,26 @@ function collapseExtraBlankLines(text: string): string {
|
|
|
209
284
|
return text.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
210
285
|
}
|
|
211
286
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Whether the line belongs to the body of the section above it.
|
|
289
|
+
*
|
|
290
|
+
* Pi writes a section as its header, then bullets — or the `(none)` placeholder
|
|
291
|
+
* when the list is empty — and separates it from what follows with a blank
|
|
292
|
+
* line. Anything else is already outside the section, however it is punctuated:
|
|
293
|
+
* a section that ran on to "the next line ending in a colon" swallowed the
|
|
294
|
+
* prose in between, which is somebody else's text whenever the match was not
|
|
295
|
+
* Pi's own (#919, #932).
|
|
296
|
+
*/
|
|
219
297
|
function isSectionBodyLine(line: string): boolean {
|
|
220
298
|
const trimmed = line.trim();
|
|
221
299
|
if (trimmed.length === 0) return true; // blank line
|
|
222
300
|
if (trimmed.startsWith("- ")) return true; // bullet
|
|
301
|
+
if (trimmed === EMPTY_LIST_PLACEHOLDER) return true; // Pi's empty list
|
|
223
302
|
if (line !== line.trimStart()) return true; // indented
|
|
224
303
|
return false;
|
|
225
304
|
}
|
|
226
305
|
|
|
306
|
+
/** The header line plus its own body, or `null` when the header is absent. */
|
|
227
307
|
function findSection(
|
|
228
308
|
lines: readonly string[],
|
|
229
309
|
header: string,
|
|
@@ -233,25 +313,9 @@ function findSection(
|
|
|
233
313
|
return null;
|
|
234
314
|
}
|
|
235
315
|
|
|
236
|
-
// If a subsequent recognised section header exists, use it as the boundary.
|
|
237
|
-
// This preserves the original behaviour for the common case where sections
|
|
238
|
-
// are adjacent (e.g. "Available tools:" followed by "Guidelines:") and
|
|
239
|
-
// ensures any prose continuation between the two headers is also removed.
|
|
240
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
241
|
-
if (isTopLevelSectionHeader(lines[index])) {
|
|
242
|
-
return { start, end: index };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// No subsequent section header — stop at the first non-body line so that
|
|
247
|
-
// content after the section (e.g. custom user notes) is not silently deleted.
|
|
248
316
|
let end = start + 1;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
end = index;
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
end = index + 1;
|
|
317
|
+
while (end < lines.length && isSectionBodyLine(lines[end])) {
|
|
318
|
+
end += 1;
|
|
255
319
|
}
|
|
256
320
|
|
|
257
321
|
return { start, end };
|
|
@@ -22,8 +22,11 @@ interface BeforeAgentStartPayload {
|
|
|
22
22
|
* The parts Pi assembled the prompt from. `toolSnippets` is what lets this
|
|
23
23
|
* handler render the session's own tool list instead of editing the one Pi
|
|
24
24
|
* wrote — including in a child, whose inherited identity carries none.
|
|
25
|
+
* `customPrompt` says whether Pi wrote a preamble at all: under one, it
|
|
26
|
+
* writes no tool surface, so there is nothing of Pi's to remove.
|
|
25
27
|
*/
|
|
26
28
|
systemPromptOptions?: {
|
|
29
|
+
customPrompt?: string;
|
|
27
30
|
toolSnippets?: Record<string, string>;
|
|
28
31
|
};
|
|
29
32
|
}
|
|
@@ -106,6 +109,9 @@ export class AgentPrepHandler {
|
|
|
106
109
|
allowedTools,
|
|
107
110
|
toolSnippets: event.systemPromptOptions?.toolSnippets ?? {},
|
|
108
111
|
guidelinesByTool: registered.guidelinesByTool,
|
|
112
|
+
// Pi's own `if (customPrompt)` test, so an empty string reads here the
|
|
113
|
+
// way it reads there: as no custom prompt at all.
|
|
114
|
+
piAuthoredPreamble: !event.systemPromptOptions?.customPrompt,
|
|
109
115
|
});
|
|
110
116
|
const skillPromptResult = resolveSkillPromptEntries(
|
|
111
117
|
toolSurfacePrompt,
|