@zihanw/pi-forge 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,286 +1,455 @@
1
1
  # pi-forge
2
2
 
3
- Pi extension package for prompt stack and agent profile management. This first milestone implements file-backed prompt stacks.
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
4
 
5
- ## Package setup
5
+ ![pi-forge header](https://raw.githubusercontent.com/MacroSony/pi-forge/main/assets/pi-forge-header-concept-1.png)
6
6
 
7
- This repository is a Pi package. Its package manifest exposes `./src/index.ts` as the extension entrypoint.
7
+ **pi-forge** lets you customize how Pi thinks and behaves. It gives you prompt stacks — JSON files that can replace, append to, or prepend Pi's default system prompt while controlling the AI's personality, visible tools, conversation history layout, template variables, and prompt transforms.
8
8
 
9
- For local development in this checkout, `.pi/settings.json` points at the package root:
9
+ Think of it as a character sheet for your AI agent.
10
10
 
11
- ```json
12
- {
13
- "packages": [".."]
14
- }
15
- ```
11
+ ## What you can do with it
16
12
 
17
- After starting Pi in this directory, trust the project and use `/reload` if needed.
13
+ - **Give Pi a personality** — turn it into a creative writer, a roleplay partner, a strict code reviewer, or anything in between.
14
+ - **Switch contexts instantly** — one command to swap between "coding mode", "writing mode", and "translation mode".
15
+ - **Control what the AI sees** — choose which tools, skills, and project context appear in each prompt.
16
+ - **Limit tools and skills per stack** — enforce active tool policy and filter skill visibility for focused modes.
17
+ - **Use template variables** — define static values such as `{{char}}` / `{{user}}`, and use ST-style turn/session variable macros inside prompt text.
18
+ - **Transform outgoing and finalized text** — run deterministic regex replacements on selected history, compiled prompt text, or finalized assistant messages.
19
+ - **Import SillyTavern presets** — bring your existing ST character presets into Pi with one command.
20
+ - **Debug your prompts** — intercept and inspect exactly what gets sent to the model.
18
21
 
19
- After publishing under your chosen npm package name, install it with:
22
+ ## Quick start
23
+
24
+ ### Install
20
25
 
21
26
  ```bash
22
27
  pi install npm:@zihanw/pi-forge
23
28
  ```
24
29
 
25
- ## Prompt stacks
30
+ ### Your first prompt stack
31
+
32
+ Create `.pi/forge/prompt-stacks/default.json` from [examples/default-prompt-stack.json](examples/default-prompt-stack.json).
26
33
 
27
- Prompt stacks live in:
34
+ The default example mirrors Pi's own prompt builder from `@earendil-works/pi-coding-agent/dist/core/system-prompt.js`, but splits it into movable pi-forge slots: role, tools, guidelines, Pi docs guidance, appended system prompt text, project context, skills, date/cwd, and chat history.
28
35
 
29
- ```txt
30
- .pi/prompt-stacks/*.json
36
+ ```bash
37
+ mkdir -p .pi/forge/prompt-stacks
38
+ $EDITOR .pi/forge/prompt-stacks/default.json
31
39
  ```
32
40
 
33
- Activation rules:
41
+ Paste the example JSON into that file. If you are working inside this repository, you can copy it directly with `cp examples/default-prompt-stack.json .pi/forge/prompt-stacks/default.json`.
34
42
 
35
- 1. A restored `/preset use <id>` selection is used first when that stack still exists and has no validation errors.
36
- 2. A restored `/preset use none` selection disables prompt stack replacement.
37
- 3. `.pi/prompt-stacks/default.json` auto-activates when present unless the stack sets `"autoActivate": false`.
38
- 4. Otherwise, the first stack with `"autoActivate": true` is used.
39
- 5. `/preset use <id>` switches stacks for the session and persists that choice.
40
- 6. `/preset use none` disables prompt stack replacement and persists that choice.
43
+ That's it. Restart Pi or run `/preset reload`. If no stack is already selected, `default.json` auto-activates. If you previously chose another stack or `/preset use none`, run `/preset use default`.
41
44
 
42
- When a stack is active, pi-forge replaces Pi's default system prompt by default and rebuilds the first provider request for each user message around a movable `chat-history` slot. Tool-result follow-up turns use Pi's natural context so post-history instructions are not repeatedly re-appended after every tool call.
45
+ ### Visual editor
43
46
 
44
- ## Commands
47
+ Prefer clicking over typing JSON? pi-forge has a built-in web editor:
45
48
 
46
- ```txt
47
- /preset list
48
- /preset use <id|none>
49
- /preset preview [id]
50
- /preset validate [id]
51
- /preset reload
52
- /preset vars [set <name> <value>|get <name>|clear [name]]
53
- /preset import-silly <path> [character_id]
54
- /intercept
49
+ ```
50
+ /preset ui
55
51
  ```
56
52
 
57
- ## SillyTavern preset import
53
+ Drag, drop, edit, validate, inspect full previews and captured payloads, manage variables/context/regex rules in tabs, switch dark mode, recover through raw stack JSON, import, export, fork, and delete stacks — all in your browser. Stack metadata is collapsible so the active editor stays in view.
58
54
 
59
- Import a SillyTavern preset JSON into `.pi/prompt-stacks/<id>.json` and write a migration report to `.pi/forge/import-reports/<id>.md`:
55
+ Import accepts native pi-forge stack JSON and SillyTavern preset JSON. SillyTavern presets are converted to prompt stacks automatically; if a preset contains multiple `character_id` configs, the editor asks which one to use.
60
56
 
61
- ```txt
62
- /preset import-silly <path> [character_id]
57
+ The editor runs on an available `127.0.0.1` port with a session token, so multiple Pi instances can run editors at the same time. If Pi reinitializes the extension after session navigation or a new session, `/preset ui` reuses the existing editor URL for the same project instead of orphaning the old server. Writes require a trusted project and stay inside prompt-stack storage. New stacks are written to `.pi/forge/prompt-stacks`; existing legacy stacks under `.pi/prompt-stacks` remain readable and editable. Successful save, import, fork, and delete actions reload into the current Pi session. Use `/preset ui restart` or `/preset ui stop` when needed.
58
+
59
+ To copy old stacks into the new location, run `/preset migrate-stacks`. Add `--dry-run` to preview, `--overwrite` to replace existing target files, and `--delete-legacy` to remove old files after successful copy.
60
+
61
+ To prefer a specific port, create `.pi/forge/config.json`. If that port is busy, pi-forge falls back to another available port and shows the actual URL:
62
+
63
+ ```json
64
+ {
65
+ "webEditor": {
66
+ "port": 41738
67
+ }
68
+ }
63
69
  ```
64
70
 
65
- If a preset contains multiple `prompt_order` entries, pass the desired `character_id`. Imported stacks are created with `autoActivate: false`; activate one with `/preset use <id>` after reviewing the report.
71
+ ## Use cases
72
+
73
+ ### 🎭 Roleplay & creative writing
66
74
 
67
- ## Agent tools
75
+ Turn Pi into a character. Define their personality in the system prompt, inject writing style rules as user messages, and use `{{lastUserMessage}}` to re-insert the user's input after the conversation history.
68
76
 
69
- ### forge_set_var
77
+ Useful pattern:
78
+ - Put long-term character rules in a `system` block.
79
+ - Keep Pi runtime context (tools, skills, project) in `user` slots.
80
+ - Set the `chat-history` slot to skip the latest user message.
81
+ - Add a final `user` block with `{{lastUserMessage}}`.
70
82
 
71
- Sets a persistent session variable. Only variables starting with `agent.` can be written by the agent; other variables are read-only. Useful for cross-turn state tracking in roleplay (character mood, story progress) or coding (task checkpoints, discovered facts).
83
+ This keeps the latest request clear and avoids duplicating it.
72
84
 
73
- When the `variables` slot is active in a prompt stack, the agent sees its variable state and can update it. Without the slot, variables still work for macro substitution but the agent won't have a structured view of the state.
85
+ For a baseline stack to fork before turning Pi into a character, start from [examples/default-prompt-stack.json](examples/default-prompt-stack.json).
74
86
 
75
- ## Stack format
87
+ ### 🧑‍💻 Focused code review
76
88
 
77
- Minimal example:
89
+ Create a `reviewer.json` stack with a strict review block: "prioritize correctness, regressions, security, and missing tests." Keep the `tools`, `project-context`, `variables`, and `chat-history` slots enabled so Pi can still inspect the repo and see any template variables you expose.
90
+
91
+ Use `mode: "append"` if you want to keep Pi's normal coding behavior and only add the sharper review lens.
92
+
93
+ ### 🌐 Translation mode
94
+
95
+ Create a small `translator.json` stack with one system block for tone and target language, then keep `chat-history` and `{{lastUserMessage}}` in the layout. This works well for switching between bilingual editing, literal translation, and localization review without changing your default assistant.
96
+
97
+ ### 🔀 Multi-mode switching
98
+
99
+ Create separate stacks for different tasks:
100
+
101
+ ```
102
+ .pi/forge/prompt-stacks/
103
+ coder.json # strict coding assistant
104
+ writer.json # creative writing partner
105
+ translator.json # bilingual translator
106
+ ```
107
+
108
+ Switch with `/preset use coder`, `/preset use writer`, etc.
109
+
110
+ ### 🧪 Presets that show off pi-forge
111
+
112
+ - **Pi mirror** — start from [examples/default-prompt-stack.json](examples/default-prompt-stack.json). It preserves normal Pi behavior while making every runtime section movable and inspectable.
113
+ - **Focused reviewer** — see [examples/reviewer-prompt-stack.json](examples/reviewer-prompt-stack.json). It denies file-writing tools, wraps prior chat history as background, removes the latest user message from history, then reinserts `{{lastUserMessage}}` as the explicit review target.
114
+ - **Read-only scout** — use `tools.allow` for `read`, `grep`, `find`, and `ls`; omit editing tools; cap `chat-history` with `maxChars`. Good for exploration turns where the model should report findings without changing files.
115
+ - **Surgical patcher** — keep the Pi mirror, require `read`, `edit`, and `bash`, strip assistant thinking from inserted history, and move `project-context` near the final user turn. Good for focused implementation passes.
116
+ - **SillyTavern DM writer** — see [examples/sillytavern-dm-writer-prompt-stack.json](examples/sillytavern-dm-writer-prompt-stack.json). It defines a Dungeon Master character with `{{char}}` / `{{user}}`, wraps prior adventure history, reinserts `{{lastUserMessage}}` as the current player action, and uses regex cleanup for OOC notes, secret-roll markers, dice notation, and `Player:` prefixes.
117
+ - **Payload lab** — include `active-model`, `date-cwd`, and variables slots, then add `compiled` regex rules for deterministic redaction or formatting. Pair it with `/payload next` or the web editor's capture view to audit exactly what changed.
118
+ - **Docs-only Pi expert** — allow only read/search tools, enable the `pi-docs` slot, and keep project context. Useful when you want answers grounded in the installed Pi docs instead of general memory.
119
+
120
+ ### 🔧 Template variables
78
121
 
79
122
  ```json
80
- {
81
- "schemaVersion": 1,
82
- "type": "pi-forge.prompt-stack",
83
- "id": "default",
84
- "autoActivate": true,
85
- "mode": "replace",
86
- "items": [
87
- {
88
- "kind": "block",
89
- "id": "main-role",
90
- "enabled": true,
91
- "role": "system",
92
- "content": "You are a precise coding assistant."
93
- },
94
- {
95
- "kind": "slot",
96
- "id": "tools",
97
- "enabled": true,
98
- "role": "system",
99
- "slot": "tools"
100
- },
101
- {
102
- "kind": "block",
103
- "id": "history-open",
104
- "enabled": true,
105
- "role": "user",
106
- "content": "<conversation_context>"
107
- },
108
- {
109
- "kind": "slot",
110
- "id": "chat-history",
111
- "enabled": true,
112
- "slot": "chat-history"
113
- },
114
- {
115
- "kind": "block",
116
- "id": "history-close",
117
- "enabled": true,
118
- "role": "user",
119
- "content": "</conversation_context>"
120
- }
121
- ]
123
+ "variables": {
124
+ "char": "Konata",
125
+ "user": "User"
122
126
  }
123
127
  ```
124
128
 
125
- ## Items
129
+ Use static variables for stable prompt constants, and ST-style macros for local prompt-time mutation:
130
+
131
+ ```
132
+ {{setvar::mood::focused}}
133
+ {{getvar::mood}}
134
+ {{setsessionvar::topic::compiler cleanup}}
135
+ ```
136
+
137
+ For durable project memory, use normal files in the repo rather than pi-forge prompt variables.
138
+
139
+ ### 📦 SillyTavern migration
126
140
 
127
- ### Blocks
141
+ Bring your ST presets into Pi:
142
+
143
+ ```
144
+ /preset import-silly ~/SillyTavern/presets/my-preset.json
145
+ ```
146
+
147
+ pi-forge converts the preset to a prompt stack and generates a migration report showing what was handled and what needs manual tweaking.
148
+
149
+ Deterministic SillyTavern `promptOnly` regex scripts are converted to pi-forge `regex.rules` as history-stage rules when they can be represented safely, including full-match token conversion, trim strings, depth fields, and clear user/assistant placements. Display-only, mixed prompt/display, DOM/browser, CSS/HTML decoration, JavaScript, unsupported placements, and invalid regex scripts stay report-only for manual review.
150
+
151
+ ### 🔍 Prompt debugging
152
+
153
+ See exactly what gets sent to the model:
154
+
155
+ ```
156
+ /payload next save=.pi/forge/payloads/last.json
157
+ ```
158
+
159
+ Or open `/preset ui`, click **Arm payload**, send the next Pi prompt, and inspect the redacted provider payload in the browser.
160
+
161
+ Or preview your compiled prompt without sending anything:
162
+
163
+ ```
164
+ /preset preview
165
+ ```
128
166
 
129
- Static text:
167
+ ## How it works
168
+
169
+ A prompt stack is a JSON file with two kinds of items:
170
+
171
+ | Kind | What it does |
172
+ |------|-------------|
173
+ | **Block** | Static text inserted at a specific position (system prompt, user message, assistant message) |
174
+ | **Slot** | Dynamic content from Pi's runtime — tools, skills, chat history, date, project context, etc. |
175
+
176
+ Items are arranged in order. When the stack is active, pi-forge:
177
+
178
+ 1. Builds a system prompt from your `system`-role blocks and slots, then applies it with the stack's `mode`.
179
+ 2. Inserts `user`/`assistant` blocks and slots around the conversation history.
180
+ 3. Expands `{{macros}}` like `{{lastUserMessage}}`, `{{date}}`, and custom variables.
181
+ 4. Applies stack tool policy to Pi's active tool set and filters pi-forge-rendered tool/skill slots.
182
+ 5. Applies enabled outgoing regex rules for the `history` and `compiled` stages.
183
+ 6. Optionally applies destructive `finalize` regex rules when an assistant message finishes.
184
+
185
+ ### Slots at a glance
186
+
187
+ | Slot | What it inserts |
188
+ |------|----------------|
189
+ | `chat-history` | The current conversation |
190
+ | `tools` | Available tools and their descriptions |
191
+ | `tool-guidelines` | Tool usage instructions |
192
+ | `skills` | Loaded Pi skills |
193
+ | `project-context` | Project instructions and context files |
194
+ | `variables` | Static/session/turn template variables |
195
+ | `date` / `cwd` / `date-cwd` | Current date and working directory |
196
+ | `active-model` | Which model is being used |
197
+ | `append-system-prompt` | User's appended system prompt text |
198
+ | `pi-docs` | Pi documentation guidance |
199
+
200
+ ### Modes
201
+
202
+ - **replace** (default) — your stack replaces Pi's system prompt entirely.
203
+ - **append** — your stack is added after Pi's default system prompt.
204
+ - **prepend** — your stack is added before Pi's default system prompt.
205
+
206
+ ## Common commands
207
+
208
+ ### Managing stacks
209
+
210
+ | Command | What it does |
211
+ |---------|-------------|
212
+ | `/preset list` | Show all available stacks |
213
+ | `/preset use <id>` | Activate a stack |
214
+ | `/preset use none` | Disable prompt stacks for the session |
215
+ | `/preset preview [id]` | See the compiled prompt |
216
+ | `/preset validate [id]` | Check a stack for issues |
217
+ | `/preset status` | Show the active stack and diagnostics summary |
218
+ | `/preset diagnostics` | Show runtime diagnostics |
219
+ | `/preset reload` | Reload stacks from disk |
220
+ | `/preset migrate-stacks [--dry-run] [--overwrite] [--delete-legacy]` | Copy legacy `.pi/prompt-stacks` files into `.pi/forge/prompt-stacks` |
221
+ | `/preset ui [stop\|restart]` | Open, stop, or restart the web editor |
222
+
223
+ ### Import & debug
224
+
225
+ | Command | What it does |
226
+ |---------|-------------|
227
+ | `/preset import-silly <path>` | Import a SillyTavern preset |
228
+ | `/intercept` | Show the next provider payload |
229
+ | `/payload next [save=<path>]` | Show, save, and expose the next payload to the web editor |
230
+
231
+ ## Common macros
232
+
233
+ Use these in block content to insert dynamic values:
234
+
235
+ | Macro | Expands to |
236
+ |-------|-----------|
237
+ | `{{lastUserMessage}}` | The user's latest message |
238
+ | `{{date}}` | Current date (YYYY-MM-DD) |
239
+ | `{{time}}` | Current time (HH:MM:SS) |
240
+ | `{{cwd}}` | Current working directory |
241
+ | `{{tools}}` | Comma-separated tool names |
242
+ | `{{selectedTools}}` | Alias for selected tool names |
243
+ | `{{activeModel}}` | Current model (provider/id) |
244
+ | `{{char}}` / `{{user}}` | Custom variables from your stack |
245
+
246
+ ### Variable macros
247
+
248
+ ```
249
+ {{setvar::name::value}} set a turn variable (cleared each message)
250
+ {{setsessionvar::name::value}} set a session variable (persists)
251
+ {{setvar::session::name::value}} also set a session variable
252
+ {{getvar::name}} read a variable (turn → session → static)
253
+ {{getturnvar::name}} read only a turn variable
254
+ {{getsessionvar::name}} read only a session variable
255
+ {{clearvar::name}} clear a variable
256
+ {{clearturnvar::name}} clear a turn variable
257
+ {{clearsessionvar::name}} clear a session variable
258
+ ```
259
+
260
+ ## Stack reference
261
+
262
+ ### Full item types
263
+
264
+ **Block:**
130
265
 
131
266
  ```json
132
267
  {
133
268
  "kind": "block",
134
- "id": "post-history-nudge",
269
+ "id": "unique-id",
270
+ "name": "Readable label",
135
271
  "enabled": true,
136
- "role": "user",
137
- "content": "Reason carefully about the latest request before answering."
272
+ "role": "system",
273
+ "content": "Your text here. Use {{macros}} for dynamic content."
138
274
  }
139
275
  ```
140
276
 
141
- ### Slots
277
+ Valid roles: `system`, `user`, `assistant`, `custom`.
142
278
 
143
- Live Pi runtime content:
279
+ **Slot:**
144
280
 
145
281
  ```json
146
282
  {
147
283
  "kind": "slot",
148
- "id": "skills",
284
+ "id": "unique-id",
285
+ "name": "Chat History",
149
286
  "enabled": true,
150
- "role": "system",
151
- "slot": "skills"
287
+ "role": "user",
288
+ "slot": "chat-history",
289
+ "options": {
290
+ "includeLastUserMessage": false
291
+ }
152
292
  }
153
293
  ```
154
294
 
155
- Supported slots:
156
-
157
- - `chat-history` — current Pi conversation context from the session
158
- - `tools` — active tool names and prompt snippets
159
- - `tool-guidelines` — active tool guidance
160
- - `skills` — loaded Pi skills
161
- - `project-context` — project instructions/context files
162
- - `append-system-prompt` — user-provided append system prompt text
163
- - `variables` — static/session/turn variables rendered as structured XML
164
- - `date`
165
- - `cwd`
166
- - `date-cwd`
167
- - `active-model`
168
- - `pi-docs`
169
-
170
- ### Variables slot
171
-
172
- Renders variable state as structured XML:
173
-
174
- ```xml
175
- <prompt_variables>
176
- <static>
177
- <char>Agent</char>
178
- </static>
179
- <session>
180
- <agent.mood>happy</agent.mood>
181
- <agent.progress>step 3</agent.progress>
182
- </session>
183
- <turn>
184
- <recent>just happened</recent>
185
- </turn>
186
- </prompt_variables>
187
- ```
188
-
189
- Options:
295
+ ### Chat history options
190
296
 
191
297
  ```json
192
- {
193
- "includeStatic": true,
194
- "includeSession": true,
195
- "includeTurn": true
298
+ "options": {
299
+ "includeLastUserMessage": false,
300
+ "stripAssistantThinking": true,
301
+ "includeSummaries": true,
302
+ "toolMode": "keep",
303
+ "roles": ["user", "assistant"],
304
+ "maxMessages": 40,
305
+ "maxChars": 20000
196
306
  }
197
307
  ```
198
308
 
199
- Use this slot to give the agent visibility into mutable state that it can also update via `forge_set_var`.
309
+ Set to `false` when you use `{{lastUserMessage}}` after the history — prevents the user's message from appearing twice.
200
310
 
201
- ## Roles
311
+ Set `stripAssistantThinking` to `true` to remove prior assistant thinking blocks from inserted chat history. Visible assistant text, tool calls, and tool result messages are preserved. This only affects history inserted by that slot and does not alter the current agent loop or stored transcript.
202
312
 
203
- - `system` items are compiled into the replacement system prompt.
204
- - `user` items are inserted as ephemeral user messages.
205
- - `assistant` items are inserted as ephemeral assistant messages.
206
- - `custom` items are inserted as hidden Pi custom messages and converted to user context by Pi.
313
+ Use `includeSummaries: false` to omit Pi branch/compaction summary messages, `roles` to keep only specific message roles, `toolMode: "drop"` to remove prior tool-call/tool-result history, and `maxMessages` / `maxChars` to keep only recent history. When filters or limits can break tool-call pairs, pi-forge removes dangling tool calls/results instead of sending inconsistent tool history.
207
314
 
208
- `chat-history` expands to the live conversation at its position. By default, only the first enabled `chat-history` slot is expanded.
315
+ ### Structured slot format options
209
316
 
210
- To omit the latest user message from a chat history slot, use:
317
+ Structured runtime slots default to XML-style wrappers. Add `"format": "plain"` to `tools`, `tool-guidelines`, `skills`, `project-context`, or `variables` slots for compact newline-separated output.
211
318
 
212
319
  ```json
213
320
  {
214
321
  "kind": "slot",
215
- "id": "chat-history",
322
+ "id": "tools",
216
323
  "enabled": true,
217
- "slot": "chat-history",
324
+ "role": "system",
325
+ "slot": "tools",
218
326
  "options": {
219
- "includeLastUserMessage": false
327
+ "format": "plain"
220
328
  }
221
329
  }
222
330
  ```
223
331
 
224
- This is useful for SillyTavern-style stacks that re-insert `{{lastUserMessage}}` later as a post-history instruction.
332
+ The default Pi mirror uses a few extra slot options:
225
333
 
226
- To intentionally duplicate history:
334
+ ```json
335
+ {
336
+ "slot": "tools",
337
+ "options": {
338
+ "format": "plain",
339
+ "onlyWithSnippets": true
340
+ }
341
+ }
342
+ ```
343
+
344
+ `tools.onlyWithSnippets` matches Pi's default "Available tools" section by hiding tools that do not provide prompt snippets. `tool-guidelines.heading`, `tool-guidelines.includePiDefaultGuidelines`, and `tool-guidelines.piStyle` make the guidelines slot match Pi's default heading and bullets. `skills.requireReadTool` hides skills unless the read tool is active, matching Pi's default behavior.
345
+
346
+ ### Tool and skill policy
347
+
348
+ Prompt stacks can constrain tools and skills with stack-level `allow` or `deny` lists. Patterns are exact by default and support `*` wildcards.
227
349
 
228
350
  ```json
229
351
  {
230
- "context": {
231
- "allowDuplicateChatHistory": true
352
+ "tools": {
353
+ "allow": ["read", "bash"]
354
+ },
355
+ "skills": {
356
+ "deny": ["browser-danger"]
232
357
  }
233
358
  }
234
359
  ```
235
360
 
236
- ## Macros
361
+ Use `allow` when only matching tools or skills should remain active. Use `deny` when everything except matching tools or skills should remain active. A single resource policy cannot contain both non-empty lists; mixed `allow` and `deny` entries are validation errors.
237
362
 
238
- Supported macros in block content:
363
+ Tool policy is enforced through Pi's active tool list while the stack is active. pi-forge remembers the previous active tools and restores them when prompt stacks are disabled or switched to an unrestricted stack.
239
364
 
240
- - `{{cwd}}`
241
- - `{{date}}`
242
- - `{{lastUserMessage}}`
243
- - `{{selectedTools}}` / `{{tools}}`
244
- - `{{activeModel}}`
245
- - custom variables from the stack `variables` object, e.g. `{{char}}`
365
+ Skill policy filters skills rendered by pi-forge's `skills` slot. If a stack uses `mode: "append"` or `"prepend"`, Pi's base prompt may already contain unfiltered skills; use `mode: "replace"` when skill visibility must be controlled.
246
366
 
247
- ### Variables
367
+ ### Regex transforms
248
368
 
249
- Static variables come from the stack file:
369
+ Prompt stacks can run deterministic regex replacements on model-bound prompt text and, optionally, finalized assistant messages. Outgoing rules support `history` and `compiled` stages. Destructive final-message cleanup uses `effect: "finalize"` at `stage: "compiled"` with the `messages` target. True display-only streaming transforms and provider-payload rewrites are not active yet.
250
370
 
251
371
  ```json
252
- "variables": {
253
- "char": "Assistant",
254
- "user": "USER"
372
+ "regex": {
373
+ "schemaVersion": 1,
374
+ "rules": [
375
+ {
376
+ "id": "trim-ooc",
377
+ "enabled": true,
378
+ "stage": "history",
379
+ "effect": "outgoing",
380
+ "pattern": "\\(OOC:[^)]+\\)",
381
+ "flags": "gi",
382
+ "replace": "",
383
+ "roles": ["assistant"],
384
+ "maxMessages": 20
385
+ }
386
+ ]
255
387
  }
256
388
  ```
257
389
 
258
- Mutable turn variables are cleared for each user message:
390
+ Use `stage: "history"` to transform messages inserted by the `chat-history` slot. Use `stage: "compiled"` with optional `targets: ["system"]`, `["messages"]`, or both to transform the final compiled prompt. Message rules can filter by `roles`, `maxMessages`, `maxChars`, `minDepth`, and `maxDepth`, where depth `0` is the latest message. Replacements use JavaScript syntax (`$&` for the full match, `$1` for captures; `$0` is also accepted as a full-match alias, and `$$` escapes a literal `$`). `trimStrings` removes literal strings from expanded replacement matches/captures, matching SillyTavern's Trim Out behavior. Supported regex flags are `g`, `i`, `m`, `s`, and `u`.
259
391
 
260
- ```txt
261
- {{setvar::name::value}}
262
- {{setturnvar::name::value}}
263
- {{getvar::name}}
264
- {{getturnvar::name}}
265
- {{var::name}}
266
- {{clearvar::name}}
392
+ To clean a completed assistant message after streaming, use `effect: "finalize"`:
393
+
394
+ ```json
395
+ {
396
+ "id": "finalize-ooc",
397
+ "enabled": true,
398
+ "stage": "compiled",
399
+ "effect": "finalize",
400
+ "targets": ["messages"],
401
+ "roles": ["assistant"],
402
+ "pattern": "\\s*\\(OOC:[^)]+\\)",
403
+ "flags": "gi",
404
+ "replace": ""
405
+ }
267
406
  ```
268
407
 
269
- Mutable session variables persist in the Pi session as extension state:
408
+ Warning: `finalize` runs at `message_end`, after raw output may already have streamed in the TUI. It returns a cleaned replacement message to Pi, so the original model output is not preserved in the stored transcript.
409
+
410
+ `effect: "outgoing"` changes model input. `effect: "finalize"` changes finalized assistant transcript content. `effect: "display"` and `"both"` validate with warnings but are ignored at runtime until true display transforms are implemented.
411
+
412
+ SillyTavern imports convert deterministic prompt-only `{{match}}` / `$0` full-match replacements to JavaScript `$&` (both `$0` and `$&` work in pi-forge), preserve original regex metadata in `source.sillytavern`, and run as history-stage rules so depth stays chat-relative. Display-only/browser/unsupported-placement scripts stay report-only. The web editor has a structured Regex dialog for these rule fields and preserves advanced unknown fields for raw JSON editing.
413
+
414
+ ### Variables slot options
270
415
 
271
- ```txt
272
- {{setsessionvar::name::value}}
273
- {{setvar::session::name::value}}
274
- {{getsessionvar::name}}
275
- {{getvar::name}}
276
- {{clearsessionvar::name}}
277
- {{clearvar::session::name}}
416
+ ```json
417
+ {
418
+ "kind": "slot",
419
+ "id": "variables",
420
+ "enabled": true,
421
+ "role": "user",
422
+ "slot": "variables",
423
+ "options": {
424
+ "includeStatic": true,
425
+ "includeSession": true,
426
+ "includeTurn": false,
427
+ "format": "xml"
428
+ }
429
+ }
430
+ ```
431
+
432
+ ## Package setup for development
433
+
434
+ ```bash
435
+ git clone <repo>
436
+ cd pi-forge
437
+ # .pi/settings.json already points at the package root
438
+ pi # start Pi, trust the project, /reload if needed
278
439
  ```
279
440
 
280
- Lookup order for `{{getvar::name}}`, `{{var::name}}`, and bare `{{name}}` is:
441
+ Run tests:
442
+
443
+ ```bash
444
+ npm test
445
+ ```
446
+
447
+ Typecheck:
448
+
449
+ ```bash
450
+ npm run typecheck
451
+ ```
281
452
 
282
- 1. turn variables
283
- 2. session variables
284
- 3. static stack variables
453
+ ## License
285
454
 
286
- `setvar` macros output empty text. Unknown macros warn by default and are kept literally.
455
+ MIT