@zihanw/pi-forge 0.5.0 → 0.5.2

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +6 -2
  3. package/README.zh-CN.md +5 -2
  4. package/dist/codecs/prompt-stack.d.ts.map +1 -1
  5. package/dist/codecs/prompt-stack.js +17 -0
  6. package/dist/codecs/prompt-stack.js.map +1 -1
  7. package/dist/compiler.d.ts.map +1 -1
  8. package/dist/compiler.js +81 -0
  9. package/dist/compiler.js.map +1 -1
  10. package/dist/context-diff-history.d.ts +61 -0
  11. package/dist/context-diff-history.d.ts.map +1 -0
  12. package/dist/context-diff-history.js +84 -0
  13. package/dist/context-diff-history.js.map +1 -0
  14. package/dist/context-diff-snapshot.d.ts +19 -0
  15. package/dist/context-diff-snapshot.d.ts.map +1 -0
  16. package/dist/context-diff-snapshot.js +146 -0
  17. package/dist/context-diff-snapshot.js.map +1 -0
  18. package/dist/context-diff.d.ts +70 -0
  19. package/dist/context-diff.d.ts.map +1 -0
  20. package/dist/context-diff.js +259 -0
  21. package/dist/context-diff.js.map +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/lifecycle.d.ts +2 -0
  26. package/dist/lifecycle.d.ts.map +1 -1
  27. package/dist/lifecycle.js +19 -2
  28. package/dist/lifecycle.js.map +1 -1
  29. package/dist/payload-capture.d.ts +10 -0
  30. package/dist/payload-capture.d.ts.map +1 -1
  31. package/dist/payload-capture.js +39 -9
  32. package/dist/payload-capture.js.map +1 -1
  33. package/dist/payload-command.d.ts +2 -0
  34. package/dist/payload-command.d.ts.map +1 -1
  35. package/dist/payload-command.js +36 -6
  36. package/dist/payload-command.js.map +1 -1
  37. package/dist/payload-state.d.ts +3 -0
  38. package/dist/payload-state.d.ts.map +1 -1
  39. package/dist/payload-state.js +5 -0
  40. package/dist/payload-state.js.map +1 -1
  41. package/dist/preview.d.ts.map +1 -1
  42. package/dist/preview.js +43 -9
  43. package/dist/preview.js.map +1 -1
  44. package/dist/regex.d.ts +12 -0
  45. package/dist/regex.d.ts.map +1 -1
  46. package/dist/regex.js +81 -5
  47. package/dist/regex.js.map +1 -1
  48. package/dist/runtime/tool-policy-runtime.d.ts.map +1 -1
  49. package/dist/runtime/tool-policy-runtime.js +13 -3
  50. package/dist/runtime/tool-policy-runtime.js.map +1 -1
  51. package/dist/runtime/web-editor-runtime.d.ts +2 -1
  52. package/dist/runtime/web-editor-runtime.d.ts.map +1 -1
  53. package/dist/runtime/web-editor-runtime.js +8 -3
  54. package/dist/runtime/web-editor-runtime.js.map +1 -1
  55. package/dist/types.d.ts +17 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/ui-contribution/contrib-port.d.ts +170 -0
  59. package/dist/ui-contribution/contrib-port.d.ts.map +1 -0
  60. package/dist/ui-contribution/contrib-port.js +640 -0
  61. package/dist/ui-contribution/contrib-port.js.map +1 -0
  62. package/dist/ui-contribution/index.d.ts +3 -0
  63. package/dist/ui-contribution/index.d.ts.map +1 -0
  64. package/dist/ui-contribution/index.js +2 -0
  65. package/dist/ui-contribution/index.js.map +1 -0
  66. package/dist/web-editor/client-script.generated.d.ts.map +1 -1
  67. package/dist/web-editor/client-script.generated.js +1 -1
  68. package/dist/web-editor/client-script.generated.js.map +1 -1
  69. package/dist/web-editor/client-styles.generated.d.ts.map +1 -1
  70. package/dist/web-editor/client-styles.generated.js +1 -1
  71. package/dist/web-editor/client-styles.generated.js.map +1 -1
  72. package/dist/web-editor/contrib-service.d.ts +41 -0
  73. package/dist/web-editor/contrib-service.d.ts.map +1 -0
  74. package/dist/web-editor/contrib-service.js +174 -0
  75. package/dist/web-editor/contrib-service.js.map +1 -0
  76. package/dist/web-editor/line-diff.d.ts +28 -0
  77. package/dist/web-editor/line-diff.d.ts.map +1 -0
  78. package/dist/web-editor/line-diff.js +216 -0
  79. package/dist/web-editor/line-diff.js.map +1 -0
  80. package/dist/web-editor/page.d.ts +1 -1
  81. package/dist/web-editor/page.d.ts.map +1 -1
  82. package/dist/web-editor/page.js +2 -2
  83. package/dist/web-editor/page.js.map +1 -1
  84. package/dist/web-editor/schema-form.d.ts +63 -0
  85. package/dist/web-editor/schema-form.d.ts.map +1 -0
  86. package/dist/web-editor/schema-form.js +213 -0
  87. package/dist/web-editor/schema-form.js.map +1 -0
  88. package/dist/web-editor/server.d.ts.map +1 -1
  89. package/dist/web-editor/server.js +125 -7
  90. package/dist/web-editor/server.js.map +1 -1
  91. package/dist/web-editor/styles.d.ts.map +1 -1
  92. package/dist/web-editor/styles.js +188 -21
  93. package/dist/web-editor/styles.js.map +1 -1
  94. package/dist/web-editor/types.d.ts +19 -0
  95. package/dist/web-editor/types.d.ts.map +1 -1
  96. package/dist/web-host.d.ts +7 -3
  97. package/dist/web-host.d.ts.map +1 -1
  98. package/dist/web-host.js +85 -12
  99. package/dist/web-host.js.map +1 -1
  100. package/docs/README.md +1 -0
  101. package/docs/concepts/agent-profiles.md +1 -1
  102. package/docs/concepts/prompt-stacks.md +4 -0
  103. package/docs/design/0.5.1-plan.md +237 -0
  104. package/docs/design/architecture-0.5.md +22 -1
  105. package/docs/design/context-diff-plan.md +8 -2
  106. package/docs/development/release.md +7 -1
  107. package/docs/development/roadmap.md +14 -0
  108. package/docs/getting-started.md +1 -1
  109. package/docs/guides/delegation.md +3 -3
  110. package/docs/guides/use-cases.md +4 -2
  111. package/docs/guides/web-editor.md +5 -1
  112. package/docs/reference/commands.md +1 -1
  113. package/docs/reference/configuration.md +6 -3
  114. package/docs/reference/features.md +5 -0
  115. package/docs/reference/public-api.md +14 -2
  116. package/docs/reference/stack-schema.md +59 -2
  117. package/docs/reference/ui-contribution-port.md +51 -0
  118. package/docs/zh-CN/concepts/agent-profiles.md +1 -1
  119. package/docs/zh-CN/concepts/prompt-stacks.md +10 -4
  120. package/docs/zh-CN/guides/delegation.md +2 -2
  121. package/docs/zh-CN/guides/web-editor.md +4 -0
  122. package/docs/zh-CN/reference/commands.md +1 -1
  123. package/examples/custom-system-status-extension/README.md +4 -3
  124. package/examples/custom-system-status-extension/index.ts +8 -6
  125. package/examples/hack-prompt-stack.json +118 -0
  126. package/examples/minimal-prompt-stack.json +36 -0
  127. package/package.json +6 -1
  128. package/examples/image-reader-prompt-stack.json +0 -114
  129. package/examples/reviewer-prompt-stack.json +0 -104
@@ -9,13 +9,16 @@ Project configuration lives in `.pi/forge/config.json` and is loaded only for a
9
9
  ```json
10
10
  {
11
11
  "webEditor": {
12
- "port": 41738
12
+ "port": 41738,
13
+ "locale": "auto"
13
14
  }
14
15
  }
15
16
  ```
16
17
 
17
18
  The port is preferred, not guaranteed. The editor binds only to `127.0.0.1` and chooses another available port when necessary.
18
19
 
20
+ `webEditor.locale` selects the editor's interface language: `"en"`, `"zh-CN"`, or `"auto"` (default). `"auto"` follows the browser language. The language selector in the editor's top bar writes this setting; `"auto"` removes the key.
21
+
19
22
  ## Experimental subagents
20
23
 
21
24
  Subagent configuration is owned by the optional `@zihanw/pi-forge-subagents` package. Dedicated files are `.pi/forge/subagents.json` for a trusted project and `~/.pi/forge/subagents.json` for user defaults. Legacy `.pi/forge/config.json` / `~/.pi/forge/config.json` `subagents` sections are read-only fallback material and emit a warning.
@@ -38,7 +41,7 @@ Trusted project `subagents.json` may override defaults, authorize individual pro
38
41
  "allowAgentInvocationWithoutApproval": false,
39
42
  "summaryInToolDescription": false,
40
43
  "profiles": {
41
- "reviewer": {
44
+ "project:reviewer": {
42
45
  "enabled": true,
43
46
  "backend": "pi-rpc-readonly",
44
47
  "timeoutMs": 180000
@@ -51,7 +54,7 @@ Valid timeouts are 1,000–3,600,000 ms. Invalid fields warn and fall back to th
51
54
 
52
55
  `summaryInToolDescription` (default `false`) embeds a compact, bounded summary of enabled subagent profiles directly in the `forge_subagent` tool description so the parent model can pick a profile without a discovery call. Ready profiles appear first, and unavailable enabled profiles include their first resolution error. It may be set in user or trusted-project `subagents.json` and applies wherever it is enabled.
53
56
 
54
- `profiles` in global `subagents.json` authorizes `global:<id>` profiles; the trusted project's `profiles` authorizes `project:<id>` profiles. Same-ID profiles never inherit enablement, backend, or timeout policy from each other. `allowAgentInvocationWithoutApproval` is project-only, requires trust, and fails closed when malformed. Deleting a profile does not modify `subagents.json`; remove any enabled entry for the deleted profile manually.
57
+ Profile authorization keys should use canonical selectors: `project:<id>` or `global:<id>`. A bare key is a compatibility spelling for `project:<id>` regardless of which config file contains it; it never authorizes a global profile. Therefore a global profile must be written explicitly as `"global:reviewer": { "enabled": true }` in `~/.pi/forge/subagents.json`. Same-ID profiles never inherit enablement, backend, or timeout policy from each other. `allowAgentInvocationWithoutApproval` is project-only, requires trust, and fails closed when malformed. Deleting a profile does not modify `subagents.json`; remove any enabled entry for the deleted profile manually.
55
58
 
56
59
  Treat project configuration as an authorization boundary. In particular, do not commit unattended delegation unless every permitted parent agent may transmit compiled prompt and readable project content without another human approval.
57
60
 
@@ -72,12 +72,16 @@ This file tracks the currently implemented feature surface for agent profiles, t
72
72
  - Chat history can filter summaries/roles, drop prior tool history, and cap recent history by message count or approximate characters with dangling tool calls/results repaired after filtering.
73
73
  - Duplicate chat-history warning unless explicitly allowed.
74
74
  - Synthetic `user`, `assistant`, and hidden `custom` messages.
75
+ - Optional consecutive-role merge (`context.mergeConsecutiveRoles`/`mergeSeparator`): runs of consecutive stack-authored `user`/`assistant` items compile into a single message; chat-history output and custom-role items are hard boundaries, and merging runs after compiled-stage regex.
76
+ - Validation warning when an enabled `system` item appears after non-system items (cross-channel item position has no effect on compilation).
75
77
  - Context rewrite limited to the first provider request of each user-submitted turn.
76
78
  - Tool policy filters Pi's active tool list while the stack is active and restores the previous active tools when the stack no longer applies.
77
79
  - Tool policy restores its pre-policy baseline during extension shutdown so Pi reload/session replacement cannot carry a restricted built-in tool set into the replacement runtime.
78
80
  - Startup tool enforcement waits until extension `session_start` configuration is complete, reasserts before user input and turns, and blocks disallowed model tool calls at execution time.
79
81
  - Skill policy filters skills rendered by pi-forge `skills` slots.
80
82
  - Outgoing regex transforms can run after `chat-history` insertion and after final prompt compilation.
83
+ - Per-rule `frequency: "request"` re-runs an outgoing message rule on every tool-result follow-up request over Pi's full natural context; the default `"turn"` keeps first-request-only behavior.
84
+ - `finalize` rules whose `roles` explicitly include `"toolResult"` also rewrite stored tool-result messages at completion time (destructive, documented); rules without `roles` stay assistant-only.
81
85
  - Finalize regex transforms can rewrite completed assistant messages at `message_end`.
82
86
 
83
87
  ## Regex Transforms
@@ -197,6 +201,7 @@ This file tracks the currently implemented feature surface for agent profiles, t
197
201
  - Collapsible prompt-stack sidebar.
198
202
  - Collapsible stack metadata panel and main-area tabs for Items, Regex, Policy, and Stack JSON/context/variables work.
199
203
  - Light/dark theme toggle, button icons, and tooltips for common actions.
204
+ - English/中文 interface switch (`webEditor.locale`: `"en"`, `"zh-CN"`, or `"auto"` following the browser language); contributed settings tabs remain provider-authored and are not translated.
200
205
  - Unsaved-change badge in the top bar.
201
206
  - Create a new prompt stack from the browser, including when no stack files exist yet; new stacks start from the default Pi prompt mirror layout.
202
207
  - View immutable stack ID and edit name, mode, `autoActivate`, description, and existing stack file content; use Fork to create a new ID.
@@ -4,7 +4,7 @@
4
4
 
5
5
  pi-forge is pre-1.0. This document defines the intentional integration surfaces of the 0.5.0 line.
6
6
 
7
- ## The three intentional entry points
7
+ ## The four intentional entry points
8
8
 
9
9
  `check-package` enforces this allowlist; nothing else is importable from the package.
10
10
 
@@ -60,10 +60,22 @@ The experimental host port over the Pi event bus: discovery, profile listing/sna
60
60
 
61
61
  The optional `@zihanw/pi-forge-subagents` package consumes this port and owns subagent execution and configuration.
62
62
 
63
+ ### 4. `@zihanw/pi-forge/ui-contribution`: versioned settings port
64
+
65
+ ```ts
66
+ import {
67
+ UiContributionProvider,
68
+ UiContributionClient,
69
+ UI_CONTRIBUTION_PORT_VERSION,
70
+ } from "@zihanw/pi-forge/ui-contribution";
71
+ ```
72
+
73
+ The experimental generic Settings integration surface. Optional packages contribute recursively validated, JSON-compatible schemas and values over the Pi event bus; pi-forge owns only the renderer and web proxy. Providers own validation and persistence, may resolve operations asynchronously, and receive an abort signal tied to provider generation so stale requests can stop before side effects. The full contract is documented in the [UI contribution port reference](ui-contribution-port.md).
74
+
63
75
  ## Compatibility policy
64
76
 
65
77
  - **Stable** surfaces (root factory, macro/slot registration) preserve source compatibility within the documented release range unless a changelog entry announces a breaking release.
66
- - **Experimental** surfaces (the `/subagent` host port) are typed, tested, and documented, but may change deliberately as integration experience exposes missing semantics.
78
+ - **Experimental** surfaces (the `/subagent` and `/ui-contribution` ports) are typed, tested, and documented, but may change deliberately as integration experience exposes missing semantics.
67
79
  - Everything not listed above is internal and may change without notice. In particular: no `src/*` subpath aliases exist, `./examples/*` is not an import surface (examples ship as browsable files), and removed 0.4 surfaces (the execution contract re-exports, loader/profile/catalog helpers) now live either nowhere or in `@zihanw/pi-forge-subagents`.
68
80
 
69
81
  ## Removed in 0.5.0
@@ -25,7 +25,7 @@ Block:
25
25
  }
26
26
  ```
27
27
 
28
- Valid roles are `system`, `user`, `assistant`, and `custom`. Custom-role content participates in compilation but does not produce a provider message directly.
28
+ Valid roles are `system`, `user`, `assistant`, and `custom`. Custom-role content participates in compilation and reaches the provider as a `user` message: Pi converts `custom` messages to `user` when it builds the wire request, so the wire carries no distinct custom role. Custom-role items never merge with other items (see [Context options](#context-options)).
29
29
 
30
30
  Slot:
31
31
 
@@ -45,12 +45,30 @@ Slot:
45
45
 
46
46
  Item IDs must be unique. Unsupported slots and missing required custom registrations produce diagnostics. Multiple chat-history slots warn unless explicitly permitted.
47
47
 
48
+ Item position only matters within each channel: all `system` items join the system prompt in their relative order, and all non-system items become messages in their relative order. A `system` item placed after non-system items therefore has no effect on placement and produces a validation warning; roles are never silently converted. Use a `user` item for in-conversation injection.
49
+
48
50
  ## Modes
49
51
 
50
52
  - `replace` replaces Pi's base system prompt; empty output falls back to the base.
51
53
  - `append` places stack system text after Pi's base.
52
54
  - `prepend` places stack system text before Pi's base.
53
55
 
56
+ ## Context options
57
+
58
+ ```json
59
+ {
60
+ "context": {
61
+ "allowDuplicateChatHistory": false,
62
+ "mergeConsecutiveRoles": true,
63
+ "mergeSeparator": "\n\n"
64
+ }
65
+ }
66
+ ```
67
+
68
+ - `allowDuplicateChatHistory` permits multiple enabled chat-history slots; otherwise only the first expands.
69
+ - `mergeConsecutiveRoles` (default `false`) merges runs of consecutive stack items that share the same declared role into a single message, joined by `mergeSeparator` (default a blank line). Only stack-authored `user`/`assistant` items merge: chat-history output (including the implicit history tail) and `custom` items are hard boundaries, and merging runs after compiled-stage regex so regex semantics are unchanged. The web editor preview reflects the merged layout.
70
+ - `mergeSeparator` is inserted verbatim between merged texts and may be an empty string.
71
+
54
72
  ## Chat-history options
55
73
 
56
74
  ```json
@@ -95,7 +113,7 @@ Patterns are exact by default and support `*` wildcards:
95
113
  }
96
114
  ```
97
115
 
98
- Each resource may have a non-empty `allow` list or `deny` list, never both. Tool allow keeps matching active tools; deny removes matching active tools. Unmatched allow patterns are surfaced during validation/preflight.
116
+ Each resource may have a non-empty `allow` list or `deny` list, never both. A selective tool `allow` list chooses matching tools from Pi's complete registered tool catalog, so it can activate a registered tool that was inactive when the stack was selected. A tool `deny` list removes matching tools from the active baseline. `allow: ["*"]` remains unrestricted and does not activate every registered tool. Unmatched allow patterns are surfaced during validation/preflight.
99
117
 
100
118
  Tool policy changes Pi's active tool list, is reasserted before input/turns, and has a tool-call guard. It preserves external additions in the restorable baseline and restores that baseline when policy no longer applies or the extension shuts down.
101
119
 
@@ -151,6 +169,43 @@ Regex rules are ordered, deterministic JavaScript `RegExp` replacements. There i
151
169
 
152
170
  Message rules may filter by `roles`, `maxMessages`, `maxChars`, `minDepth`, and `maxDepth` (depth 0 is latest). `trimStrings` removes literal strings from expanded matches/captures. Supported flags are `g`, `i`, `m`, `s`, and `u`. Replacements use JavaScript `$&`/`$1`; `$0` is accepted as a full-match alias and `$$` yields a literal dollar sign.
153
171
 
172
+ ### Frequency
173
+
174
+ Outgoing rules default to `"frequency": "turn"`: they run during the full compilation on the first provider request of each user turn. Tool-result follow-up requests receive Pi's natural context, so a turn-scoped rule never sees tool output until the next user turn.
175
+
176
+ Set `"frequency": "request"` to also run an outgoing message rule on every tool-result follow-up request, applied to Pi's full natural context:
177
+
178
+ ```json
179
+ {
180
+ "id": "redact-api-keys",
181
+ "stage": "history",
182
+ "effect": "outgoing",
183
+ "frequency": "request",
184
+ "pattern": "\\b(sk-[A-Za-z0-9_-]{12,})\\b",
185
+ "flags": "g",
186
+ "replace": "[REDACTED]"
187
+ }
188
+ ```
189
+
190
+ Each provider request is rebuilt from the stored transcript, so re-applying a rule to older messages is wire-consistent and never doubles. History-stage rules and compiled-stage rules with a `messages` target both participate; on follow-ups there is no stack layout rewrite, so both stages collapse onto the natural context (history-stage rules run first). `frequency` has no effect on `finalize` rules or system-only targets, and validation says so. It is wire-only scrubbing: the stored transcript keeps the original text.
191
+
192
+ Regex rules only cover the text targets and patterns you declare. They cannot recognize every credential format and do not scan system text, tool definitions, or arbitrary request metadata unless those targets are explicitly supported and selected. Treat them as deterministic text transforms, not as a security boundary.
193
+
194
+ To scrub the same recognized shapes from stored assistant/tool-result text, pair a request-frequency outgoing rule with a `finalize` rule:
195
+
196
+ ```json
197
+ {
198
+ "id": "redact-api-keys-finalize",
199
+ "stage": "compiled",
200
+ "effect": "finalize",
201
+ "targets": ["messages"],
202
+ "roles": ["assistant", "toolResult"],
203
+ "pattern": "\\b(sk-[A-Za-z0-9_-]{12,})\\b",
204
+ "flags": "g",
205
+ "replace": "[REDACTED]"
206
+ }
207
+ ```
208
+
154
209
  Outgoing rules change future model input. To destructively change a completed assistant transcript message:
155
210
 
156
211
  ```json
@@ -170,3 +225,5 @@ Outgoing rules change future model input. To destructively change a completed as
170
225
  > `finalize` runs at `message_end`, after raw output may have streamed. It replaces the stored assistant message, so the original output is not preserved.
171
226
 
172
227
  `effect: "outgoing"` and `"finalize"` are the only valid effects; `"display"` and `"both"` are rejected during validation. Runtime diagnostics report match and changed-segment counts.
228
+
229
+ `finalize` applies to assistant messages by default, and additionally to stored tool-result messages when a rule's `roles` explicitly includes `"toolResult"` — useful for scrubbing secrets out of stored tool output before the follow-up request replays it. Rules without `roles` keep assistant-only behavior, user messages are never finalized, and unsupported roles warn.
@@ -0,0 +1,51 @@
1
+ # UI contribution port contract
2
+
3
+ [Documentation](../README.md)
4
+
5
+ Status: experimental, versioned (`UI_CONTRIBUTION_PORT_VERSION = 1`). The `@zihanw/pi-forge/ui-contribution` entry point is the generic cross-extension port that lets optional packages contribute schema-driven pages to the pi-forge web editor's top-level **Settings** surface. The forge side knows nothing about specific providers; the first consumer is [`@zihanw/pi-forge-subagents`](https://github.com/MacroSony/pi-forge-subagents), which contributes its Subagent Settings page when installed.
6
+
7
+ ## Ownership boundary
8
+
9
+ - **The contributing package owns** its tab's form schema, current values, server-side validation on write, and persistence (for example, the subagent package owns `subagents.json`). It implements the provider side of the port.
10
+ - **pi-forge owns** provider discovery over the bus, rendering contributed pages through the generic schema-form renderer, and proxying browser writes back over the bus through the web server routes. It never interprets or stores contributed configuration itself. Contributed pages are not stack tabs and never mount in the stack Preview dock.
11
+ - **Never crosses the port:** functions, components, live contexts, internal registries, or any non-JSON-compatible value. All payloads are plain recursively validated JSON. The port is not a trust boundary — providers must re-validate everything they receive and never trust the web client.
12
+
13
+ ## Transport and lifecycle
14
+
15
+ `UiContributionTransport` is a minimal `{ emit(channel, data), on(channel, handler) }` interface; the production wiring is `pi.events`. Messages travel on the dedicated `@zihanw/pi-forge/ui-contribution/v1` channel namespace with its own version counter, separate from the `/subagent` host port. Wire messages are plain JSON-compatible data validated recursively at both boundaries (exact field sets, typed enums, plain objects only, unknown fields rejected).
16
+
17
+ Channels: `discover`, `available`, `request`, `reply`, `unavailable`.
18
+
19
+ Lifecycle rules:
20
+
21
+ 1. Version negotiation: `discover` carries `protocolVersion` plus a supported `minVersion`/`maxVersion` range; a compatible provider answers `available` with its own `protocolVersion`, range, `capabilities`, `hostId`, and `generation`.
22
+ 2. A second compatible provider fails discovery explicitly (`duplicate`).
23
+ 3. `request`/`reply` messages carry `requestId` + `hostId` + `generation`; stale-generation and wrong-host requests are rejected server-side, mismatched replies ignored client-side.
24
+ 4. Provider disposal sends `unavailable`. The web editor clears that provider's contributed Settings pages and re-discovers when a provider reappears across sessions; late-surfacing providers are picked up without a page reload path change. The local HTTP listing also carries a forge-owned monotonic, opaque provider-session key so a fast restart refreshes a still-visible form even when browser polling never observes the empty interval. Browser PUT handling binds each response to the session that received it; a delayed success from an older session cannot mark or overwrite the newer session and the preserved draft is retried instead.
25
+ 5. Operation handlers may return a result or a promise of one and must never throw across the bus; rejected promises and thrown failures are converted to `{ ok: false, error }` results. Each invocation receives `{ signal, generation }`; stopping the provider aborts the signal. Handlers that await before persistence must check it before side effects. Replies from a stopped generation are discarded as a final transport guard.
26
+
27
+ The Settings host keeps the first descriptor for each `tabId`; later duplicates are ignored. Browser button IDs use a Settings-specific prefix and cannot collide with built-in stack tabs. Providers should still emit unique stable `tabId` values because `writeValues` routes by that identifier. In-progress drafts and save status are tracked per tab, so switching between contributed pages does not discard a pending edit or leak its status into another page.
28
+
29
+ ## Operations
30
+
31
+ ### Discovery
32
+
33
+ The web host acts as the client: `UiContributionClient.discover()` announces on the channel namespace and waits (bounded timeout) for an `available` announcement. Discovered tab descriptors are fetched at page load through `GET /api/contrib`.
34
+
35
+ ### `listContributions`
36
+
37
+ Request: `{}`. Response: `{ tabs: UiContributionTabDescriptor[] }` — each descriptor carries `tabId`, `title`, `icon`, a `FormSchema`, and the current `values`. Read-only; listing a tab contributes it to the editor but performs no other side effect. Every HTTP `GET /api/contrib` refreshes this operation, so providers may update plain-data option catalogs without restarting their session.
38
+
39
+ ### `writeValues`
40
+
41
+ Request: `{ tabId, patch }` — a partial values patch for one contributed tab. Omitted top-level fields preserve their stored values. A supplied `record` field is the complete keyed table produced by the form, so omitted rows represent deletion. The provider merges those semantics over its current values, re-validates server-side, and persists the result to its own storage. Response: `{ ok: true, values? }` with the canonical stored values, or `{ ok: false, errors }` with per-field error strings keyed by field key (dotted paths for record rows). The web server exposes this as `PUT /api/contrib/<tabId>` and rejects malformed or oversized request bodies with 400/413 before any bus call.
42
+
43
+ The browser serializes autosave requests. If the form changes while a PUT is in flight, the latest complete normalized snapshot is queued and written only after the current request settles. This ordering prevents older provider writes from landing after newer ones.
44
+
45
+ ## Form schema
46
+
47
+ v1 field types are deliberately restricted: `boolean`, `number`, `enum`, `string`, and `record` (a keyed table of entries — for example per-profile settings). Fields carry `key`, `label`, optional `description`/`required`/`default`, enum `options` (plain strings or `{ value, label }`), numeric `min`/`max`, string `maxLength`/`pattern`/`placeholder`, and record sub-fields (`recordFields`, `keyLabel`, `keyPlaceholder`). A record may also provide `keyOptions` (the same string-or-labelled-option shape as enum options); the generic renderer then uses a selector for row identity and prevents choosing a key already used by another row. Providers remain responsible for refreshing those plain-data options and validating them again on write.
48
+
49
+ ## Errors
50
+
51
+ `UiContributionPortError` carries `code: "timeout" | "duplicate" | "unavailable" | "protocol" | "invalid"`. Operation-level failures return `{ ok: false, error }` results rather than throwing across the bus.
@@ -43,4 +43,4 @@ Agent profile 是项目级或用户全局、带 schema version 的预设,只
43
43
 
44
44
  `/profile status` 会把 profile 源定义变化和当前模型/思考等级/stack drift 分开显示。Provenance 只用于 branch 状态报告;reload、resume、tree navigation 和 compaction 不会重新应用 profile。
45
45
 
46
- 普通 profile 默认不能委派。Delegation 授权由可选包 `@zihanw/pi-forge-subagents` 通过专用文件持有:可信项目 `.pi/forge/subagents.json` 只授权 `project:<id>`,用户全局 `~/.pi/forge/subagents.json` 只授权 `global:<id>`,并可按 profile 覆盖 backend/timeout;同 ID 的全局和项目 profile 永不互相继承授权。主包不读取任何 subagent 配置,删除 profile 也不会改动 `subagents.json`。启用前见[前台 delegation](../guides/delegation.md)。
46
+ 普通 profile 默认不能委派。Delegation 授权由可选包 `@zihanw/pi-forge-subagents` 通过专用文件持有,使用 `project:<id>` 或 `global:<id>` 完整 key,并可按 profile 覆盖 backend/timeout。裸授权 key 无论位于哪个文件都只是项目 profile 的兼容别名;同 ID 的全局和项目 profile 永不互相继承授权。主包不读取任何 subagent 配置,删除 profile 也不会改动 `subagents.json`。启用前见[前台 delegation](../guides/delegation.md)。
@@ -2,7 +2,11 @@
2
2
 
3
3
  [中文文档](../README.md) · [English](../../concepts/prompt-stacks.md)
4
4
 
5
- Prompt stack 是一份有序、声明式的 prompt 与策略描述,由固定 **block** 和动态 **slot** 组成。Stack 可以放在项目 `.pi/forge/prompt-stacks/`,也可以放在用户全局 `~/.pi/forge/prompt-stacks/`。命令接受 `reviewer`、`project:reviewer` 和 `global:reviewer`;未限定 ID 优先解析项目 stack,项目 stack 会遮蔽同 ID 全局 stack。重复 ID 只在同一 scope 内算错误。
5
+ Prompt stack 是一份有序、声明式的 prompt 与策略描述,由固定 **block** 和动态 **slot** 组成。
6
+
7
+ > **命名说明。** Prompt stack 由 `/preset` 命令族管理:stack 文件把编排与策略打包在一起,实际作用相当于一份完整预设。命名将在未来版本统一;本文档沿用"prompt stack/提示词栈"指代该资源。
8
+
9
+ Stack 可以放在项目 `.pi/forge/prompt-stacks/`,也可以放在用户全局 `~/.pi/forge/prompt-stacks/`。命令接受 `reviewer`、`project:reviewer` 和 `global:reviewer`;未限定 ID 优先解析项目 stack,项目 stack 会遮蔽同 ID 全局 stack。重复 ID 只在同一 scope 内算错误。
6
10
 
7
11
  ## 编译模型
8
12
 
@@ -13,8 +17,8 @@ Prompt stack 是一份有序、声明式的 prompt 与策略描述,由固定 *
13
17
  3. 在可移动 `chat-history` 周围插入 user/assistant 消息。
14
18
  4. 用 forge-v1 编译 `runtime.*` / `parameters.*` / `extensions.*` 模板。
15
19
  5. 对 Pi 执行工具策略,并过滤 pi-forge 渲染的 skills。
16
- 6. 应用 history/compiled outgoing regex。
17
- 7. 可选地在消息完成后应用破坏性的 finalize regex。
20
+ 6. 应用 history/compiled outgoing regex。`frequency: "request"` 的 outgoing 消息规则还会在 tool 结果后续请求上对 Pi 的完整自然上下文再次运行;默认 `"turn"` 保持仅在每轮首次请求运行。
21
+ 7. 可选地在消息完成后应用破坏性的 finalize regex;`roles` 显式包含 `"toolResult"` 的 finalize 规则还会改写存储的 tool 结果消息。
18
22
 
19
23
  ## 常用历史布局
20
24
 
@@ -25,9 +29,11 @@ Prompt stack 是一份有序、声明式的 prompt 与策略描述,由固定 *
25
29
 
26
30
  这样既保留旧上下文,又只在最后明确出现一次当前请求。History 还可以过滤 summary/role、去掉旧工具消息、移除 assistant thinking,并限制消息数或字符数。
27
31
 
32
+ Stack 还可以通过 `context.mergeConsecutiveRoles`(及可选的 `context.mergeSeparator`)把连续的同角色条目合并成一条消息;chat-history 输出和 custom 角色条目永远不会被合并。详见英文 [stack schema](../../reference/stack-schema.md#context-options)。
33
+
28
34
  ## 策略边界
29
35
 
30
- 工具 `allow`/`deny` 会修改 Pi active tools,并在 tool call 时再次检查。Skill policy 只过滤 pi-forge 渲染给模型的列表;它不能阻止明确调用,也不是安全边界。若必须控制模型可见 skill 列表,请使用 `replace`,因为 Pi 的基础 prompt 可能已经在 `append`/`prepend` 内容之前列出 skills。
36
+ 工具 `allow`/`deny` 会修改 Pi active tools,并在 tool call 时再次检查。具体的 `allow` 列表会从 Pi 的完整已注册工具目录中选择,因此可以启用 stack 激活前处于 inactive 状态的工具;`deny` 只从原 active baseline 中移除工具,`allow: ["*"]` 仍表示不限制且不会启用全部工具。Skill policy 只过滤 pi-forge 渲染给模型的列表;它不能阻止明确调用,也不是安全边界。若必须控制模型可见 skill 列表,请使用 `replace`,因为 Pi 的基础 prompt 可能已经在 `append`/`prepend` 内容之前列出 skills。
31
37
 
32
38
  ## Scope 与自动启用
33
39
 
@@ -15,7 +15,7 @@ Profile 默认不能委派。请在可信项目的 `.pi/forge/subagents.json`
15
15
  "backend": "pi-subprocess-readonly",
16
16
  "timeoutMs": 60000,
17
17
  "profiles": {
18
- "reviewer": {
18
+ "project:reviewer": {
19
19
  "enabled": true,
20
20
  "timeoutMs": 300000
21
21
  }
@@ -23,7 +23,7 @@ Profile 默认不能委派。请在可信项目的 `.pi/forge/subagents.json`
23
23
  }
24
24
  ```
25
25
 
26
- 授权跟随 profile scope:项目 `subagents.json` 的 `profiles.<id>` 只授权 `project:<id>`,全局 `subagents.json` 的 `profiles.<id>` 只授权 `global:<id>`。同 ID 的全局和项目 profile 不会互相继承 enable/backend/timeout。未启用或未列出的 ID 不会被 discovery 返回,即使猜中 ID 也会被拒绝。
26
+ 授权 key 应使用完整 selector:`project:<id>` 或 `global:<id>`。裸 key 仅为项目 profile 的兼容写法,即使写在 `~/.pi/forge/subagents.json` 中也只授权 `project:<id>`;授权全局 profile 必须显式写成 `"global:reviewer": { "enabled": true }`。同 ID 的全局和项目 profile 不会互相继承 enable/backend/timeout。未启用或未列出的 ID 不会被 discovery 返回,即使猜中 ID 也会被拒绝。
27
27
 
28
28
  ## Plan 与运行
29
29
 
@@ -20,6 +20,10 @@
20
20
 
21
21
  端口被占用时会自动选择其他端口。请不要把带 token 的编辑器 URL 暴露或代理到不可信网络。写入操作要求项目已被 Pi 信任。
22
22
 
23
+ ## 界面语言
24
+
25
+ 编辑器界面提供英文和中文。使用顶栏的语言选择器(Auto / English / 中文);选择会写入项目配置中的 `webEditor.locale`。默认为 `Auto`,跟随浏览器语言,首次页面渲染也会参考浏览器的 `Accept-Language` 请求头。界面框架、内置 stack/profile 界面以及预览/差异停靠栏均已本地化;编译器诊断信息、插件提供的设置页面以及堆栈内容(条目名称、块文本)保持其原始语言。
26
+
23
27
  ## Prompt stack 工作区
24
28
 
25
29
  支持:
@@ -49,7 +49,7 @@
49
49
  | `/forge-agent plan <profile> [--backend <id>] <task>` | 准备、显示并丢弃计划,不联系 provider |
50
50
  | `/forge-agent run <profile> [--backend <id>] <task>` | 审批并执行前台只读任务 |
51
51
 
52
- 只接受匹配 scope 明确授权的 profile:项目 `subagents.json` 授权 `project:<id>`,全局 `subagents.json` 授权 `global:<id>`;也可使用 `.pi/forge/config.json.subagents` 作为只读兼容来源。模型工具为 `forge_subagent_profiles` 和 `forge_subagent`。见[安全说明](../guides/delegation.md)。
52
+ 只接受明确 scope 的授权:请在 `subagents.json` 中使用 `project:<id>` 或 `global:<id>` key。裸授权 key 始终表示 `project:<id>`,即使它位于全局配置中;`.pi/forge/config.json.subagents` 仅作为只读兼容来源。模型工具为 `forge_subagent_profiles` 和 `forge_subagent`。见[安全说明](../guides/delegation.md)。
53
53
 
54
54
  ## Payload
55
55
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  This example shows how trusted pi-forge extension modules can register a custom macro and custom slot without importing `@zihanw/pi-forge` from a loose Pi extension file.
4
4
 
5
- It registers:
5
+ At registration time it captures one machine snapshot, then registers:
6
6
 
7
7
  - `{{ extensions.cpuLoad }}` macro: one-line CPU load summary.
8
8
  - `machine-status` slot: CPU load, OS load average, memory, and uptime snapshot.
9
9
 
10
- The renderers are synchronous, so this example uses Node's OS load average and memory APIs. It is a rough machine-load signal, not an async sampled CPU-utilization profiler.
10
+ The renderers only format that captured value, so repeated prompt compilation over the same registered extension is deterministic. The snapshot stays fixed until the extension is reloaded; this is intentionally a registration/pure-render example, not live telemetry or an async sampled CPU-utilization profiler.
11
11
 
12
12
  ## Try It
13
13
 
@@ -27,6 +27,7 @@ Start Pi, trust the project if prompted, then run:
27
27
  ```
28
28
 
29
29
  Use `/preset diagnostics` to confirm the extension file is listed under loaded pi-forge extensions.
30
+ Run `/preset reload` whenever you want to capture a fresh machine snapshot.
30
31
 
31
32
  ## Where To Put The Extension
32
33
 
@@ -54,7 +55,7 @@ Each module exports a default function or named `register` function:
54
55
  ```ts
55
56
  export default function register(api) {
56
57
  api.registerMacro({ name: "cpuLoad", dependencies: [], render: ({ env, helpers }) => "..." });
57
- api.registerSlot({ name: "machine-status", render: () => "..." });
58
+ api.registerSlot({ name: "machine-status", dependencies: [], render: ({ options, helpers }) => "..." });
58
59
  }
59
60
  ```
60
61
 
@@ -12,10 +12,10 @@ interface ForgeRegistrationApi {
12
12
  name: string;
13
13
  source?: string;
14
14
  description?: string;
15
+ dependencies?: string[];
15
16
  options?: Record<string, unknown>;
16
17
  render: (ctx: {
17
18
  options: Record<string, unknown>;
18
- format: () => "xml" | "plain" | "json";
19
19
  helpers: {
20
20
  escapeXml(value: string): string;
21
21
  plainBullet(label: string, value: string): string;
@@ -38,17 +38,20 @@ interface SystemStatusSnapshot {
38
38
  }
39
39
 
40
40
  export default function registerSystemStatus(api: ForgeRegistrationApi): void {
41
+ const snapshot = Object.freeze(readSystemStatus());
41
42
  api.registerMacro({
42
43
  name: "cpuLoad",
43
44
  source: "pi-forge-example-system-status",
44
- description: "Current machine CPU load as normalized 1-minute OS load average.",
45
- render: () => formatCpuLoad(readSystemStatus()),
45
+ description: "Machine CPU load captured when the trusted extension was registered.",
46
+ dependencies: [],
47
+ render: () => formatCpuLoad(snapshot),
46
48
  });
47
49
 
48
50
  api.registerSlot({
49
51
  name: "machine-status",
50
52
  source: "pi-forge-example-system-status",
51
- description: "Current machine CPU, memory, and uptime snapshot.",
53
+ description: "Machine CPU, memory, and uptime captured when the trusted extension was registered.",
54
+ dependencies: [],
52
55
  options: {
53
56
  format: { type: "enum", values: ["plain", "xml"], default: "plain" },
54
57
  heading: { type: "string", default: "Machine status" },
@@ -56,14 +59,13 @@ export default function registerSystemStatus(api: ForgeRegistrationApi): void {
56
59
  includeUptime: { type: "boolean", default: true },
57
60
  },
58
61
  render: (ctx) => {
59
- const snapshot = readSystemStatus();
60
62
  const includeMemory = ctx.options.includeMemory !== false;
61
63
  const includeUptime = ctx.options.includeUptime !== false;
62
64
  const heading = typeof ctx.options.heading === "string" && ctx.options.heading.trim()
63
65
  ? ctx.options.heading.trim()
64
66
  : "Machine status";
65
67
 
66
- if (ctx.format() === "xml") {
68
+ if (ctx.options.format === "xml") {
67
69
  const lines = [
68
70
  "<machine_status>",
69
71
  ` <cpu logical_cores=\"${snapshot.logicalCores}\" normalized_load_1m=\"${snapshot.normalizedLoad1.toFixed(3)}\" model=\"${ctx.helpers.escapeXml(snapshot.cpuModel)}\">${ctx.helpers.escapeXml(formatCpuLoad(snapshot))}</cpu>`,
@@ -0,0 +1,118 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "type": "pi-forge.prompt-stack",
4
+ "id": "regex-hack",
5
+ "name": "Regex Hack Pack",
6
+ "description": "Default layout plus outgoing-history regex rules: ANSI escapes are removed and illustrative sk-/ghp_ token shapes are redacted. This is a non-exhaustive demo, not a general secret scanner.",
7
+ "autoActivate": false,
8
+ "mode": "replace",
9
+ "defaults": {
10
+ "syntheticMessagesVisible": false,
11
+ "unresolvedMacroPolicy": "warn"
12
+ },
13
+ "context": {
14
+ "allowDuplicateChatHistory": false
15
+ },
16
+ "tools": {
17
+ "allow": [
18
+ "*"
19
+ ]
20
+ },
21
+ "items": [
22
+ {
23
+ "kind": "block",
24
+ "id": "main-role",
25
+ "name": "Pi Default Role",
26
+ "enabled": true,
27
+ "role": "system",
28
+ "content": "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files."
29
+ },
30
+ {
31
+ "kind": "slot",
32
+ "id": "tools",
33
+ "name": "Available Tools",
34
+ "enabled": true,
35
+ "role": "system",
36
+ "slot": "tools",
37
+ "options": {
38
+ "format": "plain",
39
+ "onlyWithSnippets": true
40
+ }
41
+ },
42
+ {
43
+ "kind": "slot",
44
+ "id": "project-context",
45
+ "name": "Project Context",
46
+ "enabled": true,
47
+ "role": "system",
48
+ "slot": "project-context"
49
+ },
50
+ {
51
+ "kind": "slot",
52
+ "id": "date-cwd",
53
+ "name": "Date and Working Directory",
54
+ "enabled": true,
55
+ "role": "system",
56
+ "slot": "date-cwd"
57
+ },
58
+ {
59
+ "kind": "slot",
60
+ "id": "chat-history",
61
+ "name": "Chat History",
62
+ "enabled": true,
63
+ "slot": "chat-history",
64
+ "options": {
65
+ "stripAssistantThinking": false
66
+ }
67
+ }
68
+ ],
69
+ "regex": {
70
+ "schemaVersion": 1,
71
+ "rules": [
72
+ {
73
+ "id": "strip-ansi-escapes",
74
+ "name": "ANSI terminal escape codes removed from outgoing history",
75
+ "enabled": true,
76
+ "stage": "history",
77
+ "targets": [
78
+ "messages"
79
+ ],
80
+ "pattern": "\\x1B\\[[0-?]*[ -/]*[@-~]",
81
+ "flags": "g",
82
+ "replace": ""
83
+ },
84
+ {
85
+ "id": "redact-api-keys-outgoing",
86
+ "name": "Illustrative sk-/ghp_ token shapes redacted on every provider request",
87
+ "enabled": true,
88
+ "stage": "history",
89
+ "effect": "outgoing",
90
+ "frequency": "request",
91
+ "targets": [
92
+ "messages"
93
+ ],
94
+ "pattern": "\\b(sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,})\\b",
95
+ "flags": "g",
96
+ "replace": "[REDACTED]"
97
+ },
98
+ {
99
+ "id": "redact-api-keys-finalize",
100
+ "name": "Illustrative sk-/ghp_ token shapes scrubbed from the stored transcript",
101
+ "enabled": true,
102
+ "stage": "compiled",
103
+ "effect": "finalize",
104
+ "targets": [
105
+ "messages"
106
+ ],
107
+ "roles": [
108
+ "assistant",
109
+ "toolResult"
110
+ ],
111
+ "pattern": "\\b(sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,})\\b",
112
+ "flags": "g",
113
+ "replace": "[REDACTED]"
114
+ }
115
+ ]
116
+ },
117
+ "parameters": {}
118
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "type": "pi-forge.prompt-stack",
4
+ "id": "minimal",
5
+ "name": "Minimal Worker",
6
+ "description": "Bare-minimum stack: one role block, one tools slot, chat history. Policy allows bash only - what you see is literally everything that gets sent.",
7
+ "autoActivate": false,
8
+ "mode": "replace",
9
+ "defaults": {
10
+ "syntheticMessagesVisible": false,
11
+ "unresolvedMacroPolicy": "warn"
12
+ },
13
+ "tools": {
14
+ "allow": [
15
+ "bash"
16
+ ]
17
+ },
18
+ "items": [
19
+ {
20
+ "kind": "block",
21
+ "id": "worker-role",
22
+ "name": "Worker Role",
23
+ "enabled": true,
24
+ "role": "system",
25
+ "content": "You are a helpful software engineer assistant."
26
+ },
27
+ {
28
+ "kind": "slot",
29
+ "id": "chat-history",
30
+ "name": "Chat History",
31
+ "enabled": true,
32
+ "slot": "chat-history"
33
+ }
34
+ ],
35
+ "parameters": {}
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihanw/pi-forge",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Pi extension for prompt stacks, one-shot agent profiles, policy, import, and debugging.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,6 +17,11 @@
17
17
  "types": "./dist/subagent/index.d.ts",
18
18
  "import": "./dist/subagent/index.js",
19
19
  "default": "./dist/subagent/index.js"
20
+ },
21
+ "./ui-contribution": {
22
+ "types": "./dist/ui-contribution/index.d.ts",
23
+ "import": "./dist/ui-contribution/index.js",
24
+ "default": "./dist/ui-contribution/index.js"
20
25
  }
21
26
  },
22
27
  "keywords": [