pi-fabric 0.22.2 → 0.22.3

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
@@ -88,18 +88,20 @@ pi -e /absolute/path/to/pi-fabric
88
88
 
89
89
  ## What you can ask for
90
90
 
91
- Every pattern below is a skill Pi loads on demand. Invoke it with `/skill:<name>`, or just describe the task and let Pi pick it up.
92
-
93
- | You want | Ask for | Skill |
94
- | -------- | ------- | ----- |
95
- | Parallel audits, migrations, or research with phased progress and a final synthesis | “Audit every auth file in parallel and synthesize findings.” | `/skill:fabric-workflow` |
96
- | Work too big for one context window, decomposed recursively | “Produce a compact architecture map of this repo.” | `/skill:fabric-rlm` |
97
- | A persistent watcher that steers only when you drift | “Watch this migration until it's complete and tested.” | `/skill:fabric-supervisor` |
98
- | A quiet decision-point reviewer | “Review my decisions at idle and tool-error points.” | `/skill:fabric-advisor` |
99
- | Several reviewers reconciled into one verdict | “Run correctness, security, and test reviewers, then merge.” | `/skill:fabric-council` |
100
- | Multi-model deliberation with a compare-not-merge judge | “Deliberate this design across models.” | `/skill:fabric-fusion` |
101
- | A durable team coordinating through shared tasks | “Stand up a team that claims tasks atomically and reports progress.” | `/skill:fabric-swarm` |
102
- | Edits gated behind typed evidence and postconditions | “Make this parser change only if focused tests stay green.” | `/skill:fabric-schema` |
91
+ Advanced patterns are user-invoked and are not advertised for automatic selection. Run `/skill:fabric-guide` when you want one recommendation, or invoke the exact `/skill:<name>` yourself. Describing an ordinary coding task keeps Pi on the core `fabric-exec` path.
92
+
93
+ | You want | Run |
94
+ | -------- | --- |
95
+ | Help choosing the smallest advanced mechanism | `/skill:fabric-guide Choose a mechanism to audit every auth file and verify the findings.` |
96
+ | Parallel audits, migrations, or research with verification | `/skill:fabric-workflow Audit every auth file and synthesize verified findings.` |
97
+ | Work too big for one context window | `/skill:fabric-rlm Produce a compact architecture map of this repo.` |
98
+ | A persistent watcher for one measurable goal | `/skill:fabric-supervisor Watch this migration until it is complete and tested.` |
99
+ | A quiet decision-point reviewer | `/skill:fabric-advisor Focus on migration correctness.` |
100
+ | Same-model independent reviewers and one decision | `/skill:fabric-council Review this design for correctness, security, and operability.` |
101
+ | Multi-model compare-not-merge deliberation | `/skill:fabric-fusion Deliberate this design across models.` |
102
+ | One command that infers advisor versus supervisor | `/skill:fabric-ambient advisor Focus on migration correctness.` |
103
+ | A durable team coordinating through versioned tasks | `/skill:fabric-swarm Coordinate this migration across owned task partitions.` |
104
+ | Evidence-gated edits with postconditions | `/skill:fabric-schema Make this parser change only if focused tests stay green.` |
103
105
 
104
106
  The foundation is the `fabric-exec` reference skill: the model loads it before its first `fabric_exec` call and again when a call errors on argument shape.
105
107
 
@@ -120,7 +122,7 @@ See the [interface & commands reference](docs/interface.md) for every view, keyb
120
122
  - [Agents, actors & mesh](docs/agents.md) — subagents, the Claude runner, transports, steering, persistent actors, global templates, councils, recursive queries, and durable coordination.
121
123
  - [External providers](docs/providers.md) — the versioned provider protocol for extensions.
122
124
  - [Architecture & security](docs/architecture.md) — the host bridge, sandboxing, tool-call robustness, and limitations.
123
- - [Skills](skills/) — the model-invoked patterns and the full `fabric_exec` API reference.
125
+ - [Skills](docs/skills.md) — the core-first invocation policy and user-invoked advanced patterns.
124
126
 
125
127
  ## Development
126
128
 
package/docs/agents.md ADDED
@@ -0,0 +1,291 @@
1
+ # Agents, actors & mesh
2
+
3
+ This is the human-facing reference for Fabric's multi-agent runtime. The model-facing API lives in [`skills/fabric-exec/references/agents.md`](../skills/fabric-exec/references/agents.md) and [`mesh.md`](../skills/fabric-exec/references/mesh.md); the reusable patterns live in the [skills](../skills/) (`fabric-workflow`, `fabric-swarm`, `fabric-council`, `fabric-rlm`, `fabric-supervisor`, `fabric-advisor`, `fabric-fusion`). See [configuration](configuration.md) for the `subagents` and `mesh` settings.
4
+
5
+ ## Workflows
6
+
7
+ Fabric programs already keep orchestration and intermediate values in code. The workflow globals add Claude Code-style names and progress phases without introducing a second JavaScript runtime.
8
+
9
+ Available helpers:
10
+
11
+ - `workflow.agent(prompt, options)` or `agent(...)` — one worker. Set `label` on every call.
12
+ - `workflow.parallel(thunks, { concurrency })` or `parallel(...)` — fan-out. Pass functions, not promises.
13
+ - `workflow.pipeline(items, ...stages)` or `pipeline(...)` — per-item sequential stages with cross-item concurrency.
14
+ - `workflow.configure({ name, description })` — names the activity surface.
15
+ - `workflow.phase(name, { id?, description?, total? })` or `phase(...)` — progress groups.
16
+ - `workflow.item(...)` — non-agent work items whose status changes over time.
17
+ - `workflow.event(...)` — notable milestones in the dashboard feed.
18
+ - `workflow.log(...)` — compact progress notes.
19
+ - `workflow.budget` — token-budget observations.
20
+
21
+ `fabric_exec` accepts optional `agentBudget` and `tokenBudget` limits; configuration supplies a hard per-execution agent cap. A JSON Schema on an agent request makes the worker return validated structured data through `result.value`; workflow helpers return that value directly and otherwise return the agent's final text. See [`/skill:fabric-workflow`](../skills/fabric-workflow/SKILL.md) for the full pattern.
22
+
23
+ ## Subagents
24
+
25
+ ```ts
26
+ const result = await agents.run({
27
+ name: "security-review",
28
+ task: "Review the current diff for concrete security defects. Do not edit files.",
29
+ transport: "localterm",
30
+ tools: ["read", "grep", "find", "ls"],
31
+ });
32
+ return result;
33
+ ```
34
+
35
+ Background handles are explicit:
36
+
37
+ ```ts
38
+ const handle = await agents.spawn({
39
+ task: "Map the persistence layer and identify its public entry points.",
40
+ transport: "tmux",
41
+ });
42
+
43
+ // Do independent work here.
44
+
45
+ return await agents.wait({ id: handle.id });
46
+ ```
47
+
48
+ `runner` is `"pi"` or `"claude"` and defaults to `subagents.runner` (`"pi"`). Pi children use `subagents.model` or inherit the parent model unless `model` is specified. Claude children use `subagents.claude.model` or Claude Code's own runtime default. Their tool allowlist defaults to `subagents.defaultTools`. Reasoning effort defaults to `subagents.thinking` (`medium`); Pi clamps it to model support, while Claude forwards it through `--effort` (`off`/`minimal` map to `low`).
49
+
50
+ ### Claude Code runner
51
+
52
+ Install and authenticate the official Claude Code CLI (`claude`) normally; Fabric invokes that binary rather than Anthropic's Agent SDK or a third-party API client. Select it per call or globally:
53
+
54
+ ```ts
55
+ const models = await agents.models({ runner: "claude" });
56
+ const haiku = models.find((model) => model.key === "claude/haiku");
57
+ return agents.run({
58
+ runner: "claude",
59
+ model: haiku?.key,
60
+ task: "Review the current diff. Do not edit files.",
61
+ tools: ["read", "grep", "find", "ls"],
62
+ });
63
+ ```
64
+
65
+ `agents.models({ runner: "claude" })` asks the installed CLI for its initialization model catalog, including aliases, resolved IDs, descriptions, and supported effort levels. The list is not hard-coded and the handshake sends no user prompt or model inference request, so model discovery itself is not billable. Because it launches the configured local binary, model-authored `agents.models` calls carry Fabric's `execute` risk. Fabric caches it for 60 seconds. Claude model keys use `claude/<runtime-value>` (for example `claude/default`, `claude/sonnet`, or `claude/haiku`); Fabric strips that namespace before `--model`.
66
+
67
+ Claude runs use `claude -p` with stream-JSON input/output, partial messages, `--permission-mode dontAsk`, and both `--tools` and `--allowedTools`. Fabric maps its portable core allowlist as follows:
68
+
69
+ | Fabric tool | Claude Code tool |
70
+ | ------------ | ---------------- |
71
+ | `read` | `Read` |
72
+ | `grep` | `Grep` |
73
+ | `find`, `ls` | `Glob` |
74
+ | `bash` | `Bash` |
75
+ | `edit` | `Edit` |
76
+ | `write` | `Write` |
77
+
78
+ Unknown tools fail before launch. `extensions: false` starts Claude in safe mode; the default `true` preserves the user's normal Claude Code customizations while the explicit tool list still controls model-facing tools. JSON schemas use Claude's native `--json-schema`; usage, cost, turns, tool activity, errors, and Claude's session ID are normalized into the ordinary Fabric result and dashboard transcript. One-shot runs add `--no-session-persistence`.
79
+
80
+ Claude-backed children are intentionally **not recursively Fabric-equipped**: `recursive: true`, `fabric_exec`, and direct `mesh.*` access are rejected. Use `runner: "pi"` for RLM/recursive Fabric, or use a Claude-backed persistent actor for host-managed mailbox/event coordination.
81
+
82
+ ### Transports
83
+
84
+ | Transport | Behavior | Attach command |
85
+ | ----------- | ---------------------------------------------------------- | ---------------------------- |
86
+ | `process` | Detached local worker process; default and lowest overhead | none |
87
+ | `tmux` | One detached tmux session per child | `tmux attach-session -t …` |
88
+ | `screen` | One detached GNU Screen session per child | `screen -r …` |
89
+ | `localterm` | One pinned LocalTerm PTY per child | `localterm session attach …` |
90
+ | `herdr` | One background Herdr tab per child | `herdr terminal attach …` |
91
+ | `auto` | Tries Herdr, LocalTerm, tmux, screen, then process | transport-specific |
92
+
93
+ Herdr uses its local socket API to create an argv-backed background tab atomically, without shell quoting or focus changes. Automatic selection is enabled only when the parent Pi process is already inside Herdr (`HERDR_ENV=1` with an injected workspace and socket); select `transport: "herdr"` under the same conditions. Each child can be opened directly with the attach command in its handle.
94
+
95
+ LocalTerm already exposes the needed tmux-parity primitives: detached creation, pinning, listing, capture, exec, attach, and kill. Pi Fabric therefore requires no LocalTerm patch. Start its daemon before selecting it:
96
+
97
+ ```bash
98
+ localterm start
99
+ ```
100
+
101
+ Use `/fabric agents` to list children and `/fabric attach <id>` to display the appropriate attach command. Abort signals propagate to the transport and selected child process. When a program uses orchestration entry points (`agent`/`workflow.agent`, `agents.run`/`agents.wait`/`agents.ask`, `council.run`, `rlm.query`)—including `agents.*` refs invoked through `tools.call()` and refs computed at runtime—Fabric raises the whole-program `executor.timeoutMs` to at least `subagents.timeoutMs`, so the parent deadline cannot stop children that are still within their own per-agent budget.
102
+
103
+ Set `worktree: true` to create a dedicated Git worktree and `pi-fabric/<name>-<id>` branch. Worktrees are retained for inspection until `agents.cleanup()` is called.
104
+
105
+ ## Steering running agents
106
+
107
+ The dashboard-owning root Pi session is **Main**. Other live root Pi sessions sharing the project mesh are **Peers**, named `Peer <session-prefix>`. `agents.peers()` returns their live heartbeat records; stopped or crashed sessions disappear after the presence lease expires. Peers are steerable by exact id from the dashboard or through `agents.steer`/`agents.followUp`.
108
+
109
+ Fabric messaging is target-oriented rather than tied to fixed planner/worker roles. The user-facing Pi session is a first-class target named **Main**: `agents.main()` returns its exact identity, and the stable alias `"main"` works with `agents.steer` and `agents.followUp`. Main, recursive Pi children, and persistent Pi actors can initiate Fabric calls; ordinary non-recursive Pi children and Claude children/actors can receive host-routed messages but cannot initiate `agents.*` themselves.
110
+
111
+ ```ts
112
+ const main = await agents.main();
113
+ const peers = await agents.peers();
114
+ if (peers[0]) await agents.steer({ id: peers[0].id, message: "Coordinate on the shared migration." });
115
+ await agents.followUp({ id: main.id, message: "After the audit, reconcile the findings." });
116
+
117
+ const handle = await agents.spawn({ task: "Audit auth flows.", tools: ["read", "grep", "find", "ls"] });
118
+ const s = await agents.status({ id: handle.id });
119
+ if (s.text.includes("rotating refresh tokens")) {
120
+ await agents.steer({ id: handle.id, message: "Skip refresh-token rotation; focus on session expiry only." });
121
+ await agents.setSteeringMode({ id: handle.id, mode: "all" });
122
+ }
123
+ return await agents.wait({ id: handle.id });
124
+ ```
125
+
126
+ For Main and one-shot agents, `agents.steer({ id, message })` is delivered after the current turn's tool calls and before the next model call; `agents.followUp({ id, message })` waits for the current run to settle. For a persistent actor, both operations enqueue its serial mailbox. Pi children use the Pi RPC queue; Claude children receive additional user records on the same `claude -p` stream. `agents.status({ id }).pendingMessages` shows a local one-shot queue; Main status exposes only a boolean because Pi does not expose host queue contents to extensions. `agents.setSteeringMode`/`setFollowUpMode` configure `"all"` vs `"one-at-a-time"` for local one-shot agents only.
127
+
128
+ Routing returns `"main"`, `"local"`, or `"mesh"`. Cross-process delivery publishes an exact target id to `fabric.steer`; the owning process can relay it to Main, a recursive descendant, or an actor. Mesh routing is best-effort and requires `mesh.enabled`. The dashboard exposes the same path: `s` messages/steers Main, active one-shot agents, actors, and observed remote mesh agents; `u` queues a follow-up where that target has a distinct follow-up queue. See [`references/agents.md`](../skills/fabric-exec/references/agents.md).
129
+
130
+ ## Persistent actors
131
+
132
+ `agents.create()` creates a named actor with a fixed runner, a persistent runner session, a serial mailbox, and optional subscriptions to parent-session events or durable mesh topics:
133
+
134
+ ```ts
135
+ return agents.create({
136
+ name: "auth-supervisor",
137
+ instructions: `Watch the main session until the auth migration is complete and tested.
138
+ Prefer silence. Reply with a directive only for material drift, a blocker, or verified completion.`,
139
+ events: ["agent_settled", "tool_error"],
140
+ responseMode: "directive",
141
+ delivery: "steer",
142
+ triggerTurn: true,
143
+ thinking: "high",
144
+ tools: ["read", "grep", "find", "ls"],
145
+ });
146
+ ```
147
+
148
+ A host-managed Claude actor uses the same mailbox and event surface while retaining Claude Code context across activations:
149
+
150
+ ```ts
151
+ return agents.create({
152
+ name: "claude-reviewer",
153
+ runner: "claude",
154
+ model: "claude/haiku",
155
+ instructions: "Review each delivered event and report only concrete regressions.",
156
+ events: ["agent_settled", "tool_error"],
157
+ responseMode: "directive",
158
+ delivery: "steer",
159
+ triggerTurn: false,
160
+ tools: ["read", "grep", "find", "ls"],
161
+ });
162
+ ```
163
+
164
+ Claude actors can retain context, inspect/edit with mapped Claude Code tools, consume host events and mesh messages delivered by Fabric, and return text or directives. They cannot themselves call `fabric_exec`, `agents.*`, or `mesh.*`; use a Pi actor when the actor must recursively coordinate through Fabric. If Claude's private session has been removed, the next activation fails clearly rather than silently discarding actor context. Recreate the actor to start a fresh Claude session.
165
+
166
+ This is the primitive behind emergent supervisors and advisors; neither requires another extension. Host events include a bounded recent-session snapshot. Actors process messages one at a time, coalesce repeated host events by default, and restore with the trusted project actor registry. Pi actors keep model context in their Fabric-owned Pi session file. Claude actors persist the session ID emitted by the official CLI, reapply tools/permissions/schema/system-prompt flags on every activation, and use `--resume <id>` after the first message; Fabric also keeps a runner-neutral stream transcript instead of reading Claude's private JSONL format. Each actor's reasoning effort is its `thinking` level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`), defaulting to `subagents.thinking` (`medium`); set it at creation or change it later with `e` from the dashboard. Its `tools` array is a persisted allowlist: set it at creation, replace it with `agents.setTools({ id, tools })`, or press `o` in the dashboard. An empty list disables optional tools; Pi actors retain the host-required `fabric_exec` capability for mailbox and mesh coordination unless created with `extensions: false`. Set `extensions: false` at creation to opt a Pi actor out of Fabric entirely — the activation runs without `fabric_exec`, `agents.*`, or `mesh.*`, while the host still manages its mailbox and delivery. This does not make the actor read-only by itself: `tools` still defaults to `subagents.defaultTools`. For a read-only persistent actor, also set `tools: ["read", "grep", "find", "ls"]`; use `tools: []` for an actor with no tools.
167
+
168
+ ### Response modes and delivery
169
+
170
+ Two response modes are available:
171
+
172
+ - `text`: every non-empty response becomes an actor outbox message.
173
+ - `directive`: validated `{ action: "silent" | "message" | "stop", message?, data? }` output lets the actor decide whether intervention is useful.
174
+
175
+ Delivery can remain in `mailbox` or enter the main session as `steer`, `followUp`, or `nextTurn`. `steer` and `followUp` require an explicit `triggerTurn: true | false`: `true` starts Main when it is idle, while `false` is passive and is visibly labeled as not starting Main. `mailbox` and `nextTurn` never start Main and reject `triggerTurn: true`. This explicit policy prevents a delivered actor message from looking like a stalled continuation.
176
+
177
+ The actor cannot escalate delivery in its own response, but the owner can update a live actor or global template without losing history:
178
+
179
+ ```ts
180
+ await agents.setDeliveryPolicy({
181
+ id: actor.id,
182
+ delivery: "steer",
183
+ triggerTurn: true,
184
+ });
185
+ ```
186
+
187
+ Pass `scope: "global"` to update a reusable template. In the dashboard, press `y` on an actor or template to choose among mailbox, passive/active steer, passive/active follow-up, and next-turn delivery. Use `agents.ask()` for a blocking exchange, `agents.tell()` for fire-and-forget mail, `agents.messages()` for history, and `agents.remove()` for cleanup.
188
+
189
+ ## Paged agent logs
190
+
191
+ `agents.log()` reads JSONL logs in bounded pages instead of loading the complete file. The first call returns the newest entries. When `hasMore` (or `sessionHasMore` for an actor session) is true, pass the returned `before` (or `sessionBefore`) cursor to load the next older page:
192
+
193
+ ```ts
194
+ const newest = await agents.log({ id, type: "run", lines: 100 });
195
+ if ("before" in newest && newest.hasMore) {
196
+ const older = await agents.log({ id, type: "run", lines: 100, before: newest.before });
197
+ return older;
198
+ }
199
+ return newest;
200
+ ```
201
+
202
+ Log-line `offset` values and page cursors are byte offsets into the JSONL file.
203
+
204
+ ## Global actor templates
205
+
206
+ Persistent actors live in a project mesh, but a persona worth reusing across projects belongs in a project-independent **template library** stored in your agent dir (`~/.pi/agent/fabric/actors/`). Templates carry only an actor definition — name, instructions, subscriptions, and run settings — never any history (mailbox, session transcript, or run logs). They are not live; you stamp one into a project to make it run.
207
+
208
+ ```ts
209
+ // Save a reusable persona to the global registry (not a live actor).
210
+ return agents.create({
211
+ name: "security-reviewer",
212
+ instructions: "Review changes for security defects. Reply with a directive only for material drift.",
213
+ events: ["agent_settled"],
214
+ responseMode: "directive",
215
+ scope: "global",
216
+ });
217
+
218
+ // List templates, then stamp one into the current project as a fresh actor.
219
+ const [template] = agents.actors({ scope: "global" });
220
+ return agents.import({ name: template.name }); // fresh: no inherited history
221
+ return agents.import({ name: "security-reviewer", as: "security-reviewer-2" }); // rename on collision
222
+
223
+ // Promote a tuned project actor back to the global library (no history).
224
+ return agents.export({ id: actorId, overwrite: true });
225
+
226
+ // Refine a template's default instruction and continuation policy.
227
+ await agents.setInstructions({ id: template.id, instructions: "Be brief.", scope: "global" });
228
+ return agents.setDeliveryPolicy({
229
+ id: template.id,
230
+ delivery: "steer",
231
+ triggerTurn: false,
232
+ scope: "global",
233
+ });
234
+ ```
235
+
236
+ `agents.setInstructions` also edits a live project actor (`scope: "project"`, the default); the new instruction takes effect on the actor's next queued message. History never crosses the project⇄global boundary — import and export move only the definition. Slash commands mirror the API: `/fabric global` lists templates, `/fabric import <name> [as <new>]` stamps one into the project, and `/fabric export <id> [--overwrite]` promotes a project actor. The dashboard lists global templates alongside live actors and lets you import, export, delete, edit instructions, and change delivery policy without writing code. Legacy persisted actors/templates still load as passive, but new active delivery definitions must state `triggerTurn` explicitly.
237
+
238
+ ## Councils
239
+
240
+ ```ts
241
+ return council.run({
242
+ task: "Review the current implementation and recommend whether it is ready to merge.",
243
+ roles: ["correctness reviewer", "security reviewer", "test reviewer"],
244
+ transport: "localterm",
245
+ synthesize: true,
246
+ });
247
+ ```
248
+
249
+ Council members run concurrently under the global subagent semaphore. With `synthesize: true`, a final child agent reconciles their reports. See [`/skill:fabric-council`](../skills/fabric-council/SKILL.md).
250
+
251
+ ## Recursive queries
252
+
253
+ ```ts
254
+ return rlm.query({
255
+ runner: "pi",
256
+ task: "Recursively decompose this repository and produce a compact architecture map.",
257
+ transport: "process",
258
+ });
259
+ ```
260
+
261
+ `rlm.query()` is `agents.run({ runner: "pi", recursive: true })` with Fabric enabled in the child. Claude runners are intentionally rejected for recursive Fabric. Recursion is rejected at `subagents.maxDepth`. Approval of the initial recursive call delegates only the `agent` risk capability to recursive children; network, execution, and write approvals are not inherited. Each Fabric process enforces its own configured concurrency and timeout limits. When `subagents.budgetUsd` is set, a shared append-only cost ledger bounds total spend across the whole recursion tree: every node records the cost of the children it spawns into one ledger file inherited via environment, and each node rejects a new child when the accumulated spend reaches the budget. The check is best-effort (concurrent children can each pass before any cost lands, so a tree may slightly overshoot); the race-free ceiling remains `subagents.maxPerExecution`. The result and live status of every recursive child carry a `budget` summary (`limit`, `spent`, `remaining`, `tokens`). Fabric also keeps the latest bounded nested-agent status tree in memory, so completed recursive leaves remain visible in **Topology · Run** after the child process removes its temporary nested run directories. The snapshot is released when the parent run is cleaned up or the Fabric session shuts down.
262
+
263
+ `subagents.maxTokensPerChild` (0 = disabled) bounds each child's cumulative token usage. The wall-clock `timeoutMs` and the cost `budgetUsd` bound time and money; this bounds a single runaway child's context before the host session compacts, terminating it with the same `timed_out` status and a `token limit` error. See [`/skill:fabric-rlm`](../skills/fabric-rlm/SKILL.md).
264
+
265
+ ## Durable mesh coordination
266
+
267
+ The `mesh` API is a project-scoped, event-sourced coordination substrate:
268
+
269
+ ```ts
270
+ const event = await mesh.publish({
271
+ topic: "team.auth",
272
+ kind: "finding",
273
+ text: "Refresh-token rotation is not atomic",
274
+ data: { path: "src/auth/refresh.ts" },
275
+ });
276
+
277
+ const task = await mesh.put({
278
+ key: "tasks/auth-review",
279
+ value: { status: "ready", owner: null },
280
+ ifVersion: 0,
281
+ });
282
+
283
+ const claimed = await mesh.put({
284
+ key: task.key,
285
+ value: { status: "claimed", owner: "security-reviewer" },
286
+ ifVersion: task.version,
287
+ });
288
+ return { event, claimed };
289
+ ```
290
+
291
+ Topics provide durable channel and direct-message semantics with sequence cursors. `mesh.members()` discovers actor presence across live Fabric sessions. Versioned `get`/`put`/`delete` operations provide compare-and-swap state for task claims, leases, reservations, and decisions. Together with persistent actors, these are sufficient to express messenger-style swarms in Fabric code without a daemon or fixed planner/worker roles. See [`/skill:fabric-swarm`](../skills/fabric-swarm/SKILL.md) for the pattern and [`references/mesh.md`](../skills/fabric-exec/references/mesh.md) for the full API.
@@ -0,0 +1,73 @@
1
+ # Architecture & security
2
+
3
+ ## Architecture
4
+
5
+ ```text
6
+ fabric_exec
7
+
8
+
9
+ TypeScript checker → QuickJS sandbox (default)
10
+ └→ disposable Node process (unsafe opt-in)
11
+ │ JSON-only host bridge
12
+
13
+ ActionRegistry
14
+ ├── pi.* built-in Pi tool definitions
15
+ ├── extensions.* captured pi.registerTool definitions
16
+ ├── mcp.* pooled mcporter runtime
17
+ ├── agents.* one-shot workers + persistent mailbox actors
18
+ ├── mesh.* durable topics + compare-and-swap state
19
+ └── external explicit pi.events providers
20
+
21
+ ActivityStore → compact widget + footer status + interactive dashboard
22
+ ```
23
+
24
+ In the default QuickJS runtime, guest code has no `process`, `require`, filesystem, network, or subprocess globals. All effects cross the host bridge, where schemas, approvals, audit records, timeouts, and cancellation apply. Each execution receives a fresh QuickJS context. Named strings passed in the `strings` tool parameter are available as `π.key`; accessing a key that was not provided throws a clear, actionable error listing the provided keys rather than silently returning `undefined`.
25
+
26
+ The optional `node-process` executor runs the same type-checked guest API and host-call protocol in a fresh child process with a configurable V8 heap. It exists for workloads that exceed WASM32's memory ceiling. It is not a security boundary: Node's `vm` module cannot safely contain hostile code, so this mode is restricted to trusted configuration, described as unsafe in `/fabric settings`, and disabled by Schema enforce mode. Parent-side deadlines and cancellation terminate the entire child process.
27
+
28
+ ## Tool discovery and generic calls
29
+
30
+ Inside `fabric_exec`, the `tools` surface discovers and calls any provider generically — useful when you don't know the exact ref ahead of time:
31
+
32
+ ```ts
33
+ const providers = await tools.providers();
34
+ const candidates = await tools.search({ query: "GitHub issues" });
35
+ const schema = await tools.describe({ ref: candidates[0].ref });
36
+ const result = await tools.call({
37
+ ref: schema.ref,
38
+ args: { query: "is:open label:bug" },
39
+ });
40
+ return result;
41
+ ```
42
+
43
+ Known actions have first-class proxies and still cross the same registry path: `mcp.<sanitized_server>.<sanitized_tool>(args)`, `memory.*`, `state.*`, `schema.*`, and `compact.*`. For example, `mcp.fal_ai.get_model_schema(args)` resolves the mcporter names `fal-ai` and `get-model-schema`. Captured extension tools use `extensions.<tool>(args)` in full code mode. Keep `tools.call()` for refs discovered or computed at runtime.
44
+
45
+ Refs are namespaced: `pi.grep`, `extensions.<tool>`, `mcp.<server>.<tool>`, `schema.<action>`; bare names are rejected. `tools.providers()` → `[{name,description}]`; `tools.search({query,limit?})` → `FabricAction[]`; `tools.describe({ref})` → the full `FabricAction` (read its `inputSchema` first); `tools.call({ref,args?})`; `tools.list({provider?,namespace?,query?,limit?})`; `tools.models()` → Pi `[{provider,id,name,key}]`; `agents.models({runner:"claude"})` → Claude Code runtime models. The model-facing `fabric-exec` skill holds the exact signatures and the read → describe → retry error loop.
46
+
47
+ ## Tool-call robustness
48
+
49
+ The model-facing `fabric_exec` schema is intentionally flat — one large `code` string plus scalar/optional parameters — with no nested arrays-of-objects containing escaped content. Newer SOTA models are post-trained on one dominant harness's flat tool shapes and can invent trailing keys at the highest-entropy point of a nested escaped-JSON field (e.g. right after closing a long multiline string), which a strict schema rejects. The only nested field, `display`, ignores unknown keys: extras are accepted by the schema and filtered to `{ name, description }` before execution, mirroring the silent-filter behavior the dominant harness's client is trained against.
50
+
51
+ Fabric's architecture is itself a mitigation for this class of bug. The model authors TypeScript that calls tools, so it never has to faithfully emit an alternative tool schema under sampling pressure; nested object construction happens in deterministic, type-checked code. The residual failure mode is incorrect TypeScript, caught by Fabric's TypeScript checker with an actionable, line-numbered error — the validate/report/retry loop at the code level rather than the JSON-schema level.
52
+
53
+ For sessions that also call pi tools directly (`read`/`write`/`edit`/`grep`/`find`/`ls`/`bash`), install [pi-tool-repair](https://github.com/monotykamary/pi-tool-repair) as a companion. It validates-then-repairs the finite set of tool-call mistakes those direct calls make — invented keys, wrong field names, stringified arrays, anchor bleed, and leaked tool-call grammars — before tools execute. It hooks `before_provider_request`/`message_end`/`tool_call`; fabric registers a tool, so the two do not conflict.
54
+
55
+ An external lever outside fabric's control is enabling Anthropic strict tool use at the provider, which prevents the server from sampling keys not in the schema. It is the strongest mitigation for schema drift but trades against Anthropic's complexity limits on strict tool definitions.
56
+
57
+ ## Security and limitations
58
+
59
+ - Pi Fabric invokes separately constructed Pi built-in definitions when no captured override exists. Those unoverridden built-in calls do not pass through Pi's top-level `tool_call` and `tool_result` hooks. Captured overrides and other extension calls do run those hooks; Fabric's approval and audit layer remains authoritative around every nested call.
60
+ - Captured tools execute with the full privileges of their owning extension. Hiding a tool schema is context optimization, not sandboxing. Captured tools retain their definitions and native renderers, but nested calls render as part of the enclosing Fabric execution rather than as separate native tool rows.
61
+ - Registry interception composes through the public `ExtensionRunner.getAllRegisteredTools()` method. An extension that replaces that method without delegating to the previous implementation can prevent capture.
62
+ - MCP servers and external providers execute with their own host privileges. Review their configuration and code.
63
+ - Type checking improves reliability but is not a security boundary. In the default runtime, QuickJS isolation and the host capability bridge are the boundaries. The optional Node process deliberately gives up the QuickJS boundary and must be treated as trusted native execution.
64
+ - Child Pi processes load normal extensions by default so provider-backed models continue to work. Claude children use the official installed CLI and its existing authentication. Both runners restrict the active model-facing tools to `defaultTools`; Pi adds `fabric_exec` only for explicit recursion, while Claude rejects recursion and unmapped tools.
65
+ - Claude `extensions: true` preserves the user's normal Claude Code customizations, including applicable settings and hooks; those hooks execute with their usual host privileges. Use `extensions: false` for Claude safe mode. `Bash` remains unrestricted inside the child when allowed, just as Fabric's `bash` capability is.
66
+ - Claude model discovery uses a local initialization control request and does not invoke a model. Actual one-shot and actor activations use the account/API billing already configured in Claude Code; Fabric records the CLI's reported `total_cost_usd` in normal usage and budget ledgers.
67
+ - A Git worktree isolates files, not credentials, network access, processes, or external services.
68
+ - Agent transcripts are projected from local `events.jsonl` run logs. The dashboard redacts common credentials from compact tool previews, but the permission-restricted raw event log can contain assistant text, tool arguments/results, diagnostics, and extension protocol payloads. Persisted `fabric_exec` traces also retain projected bash command text for command previews; treat retained session and run data as sensitive.
69
+ - Background one-shot children are stopped when the parent Pi session shuts down. A detached `agents.spawn()` sends a follow-up completion message unless the caller later waits for it or `notifyOnComplete` is disabled. Completed worktrees are intentionally retained.
70
+ - Persistent actors are suspended on shutdown and restored when project trust is active. Claude actor session IDs refer to Claude Code's own persisted session store; removing that private session makes resume fail, and removing a Fabric actor does not currently delete Claude Code's private transcript. By default (`mesh.actorScope: "project"`), their definitions, mailbox history, and child session files live under `.pi/fabric/mesh/actors/` and are shared across all Pi sessions in the project, so actors survive `/new`. Set `mesh.actorScope: "session"` to isolate actors per Pi session instead. Mesh topics and shared state are always project-scoped. Do not place secrets in actor prompts, messages, or mesh state.
71
+ - Approving `agents.create()` delegates future subscribed events to that actor until it is stopped. Each activation uses the actor's fixed runner and persisted tool allowlist/model setting; review them before approving a persistent actor. Tool changes apply only to later activations.
72
+ - Actor responses can enter the main context only through the delivery policy fixed at creation. Directive output is schema-validated, but it is still untrusted model output that the main agent should weigh.
73
+ - One Pi process should own the actor registry at a time. This is especially important with project scope, where concurrent Pi sessions in the same project share one registry and may race on writes. Mesh topics are append-only and are not compacted automatically; archive or remove an old mesh root when its history is no longer useful.
@@ -0,0 +1,112 @@
1
+ # Fabric execution trace V1
2
+
3
+ Final `fabric_exec` result details are a bounded durable envelope containing only `success` and `trace`. Rich `audits`, logs, values, type errors, elapsed time, media, and raw runtime/provider errors remain in memory only and are not copied into final session JSONL details. Live partial updates may still carry richer audits for the active UI.
4
+
5
+ The complete serialized final details object is at most 512 KiB. Consumers use current traces structurally. The chat renderer has one compatibility exception: it may match a pre-change bash digest against string literals or named strings already visible in the outer `fabric_exec` arguments so old previews can show the original command.
6
+
7
+ ## Envelope
8
+
9
+ ```ts
10
+ interface FabricPersistedExecutionDetailsV1 {
11
+ success: boolean;
12
+ trace: FabricExecutionTraceV1;
13
+ }
14
+
15
+ interface FabricExecutionTraceV1 {
16
+ kind: "pi-fabric.execution";
17
+ version: 1;
18
+ outcome: "succeeded" | "failed" | "aborted" | "timed_out";
19
+ phases: string[];
20
+ operations: FabricExecutionTraceOperationV1[];
21
+ counts: {
22
+ droppedValues: number;
23
+ truncatedValues: number;
24
+ redactedValues: number;
25
+ droppedOperations: number;
26
+ };
27
+ error?: string;
28
+ }
29
+ ```
30
+
31
+ The trace contains no run or call timestamps, elapsed durations, random call IDs, source code, media payloads, or arbitrary argument/result content. Runtime and call errors are fixed stage/outcome messages rather than provider, validator, approval, or guest exception prose.
32
+
33
+ `phases` is occurrence-ordered. Repeated transitions are retained, so `A → B → A` is represented as `["A", "B", "A"]`.
34
+
35
+ ## Call operation
36
+
37
+ ```ts
38
+ interface FabricExecutionTraceOperationV1 {
39
+ type: "call";
40
+ sequence: number;
41
+ ref: string;
42
+ provider?: string;
43
+ action?: string;
44
+ args: Record<string, JsonValue>;
45
+ outcome: "succeeded" | "failed" | "aborted" | "timed_out";
46
+ failureStage?: "resolve" | "prepare" | "validate" | "approve" | "invoke" | "guard";
47
+ error?: string;
48
+ result?: JsonValue;
49
+ }
50
+ ```
51
+
52
+ `sequence` is assigned when the host bridge receives any durable operation. Parallel completion updates that record without changing operation order. Action attempts are issued before reference resolution, preparation, schema validation, approval, and execution guards. Discovery and workflow attempts are likewise issued before their guards, lookups, validation, or activity mutation. Failures in those stages therefore remain visible. The configured executor returns a typed termination reason; trace sealing uses that reason for deadline and cancellation outcomes and never classifies exception text.
53
+
54
+ V1 retains `type: "call"` for wire compatibility. Exact internal refs distinguish discovery, lifecycle, and combinator operations from provider action calls. V1 also keeps `result` optional, but all discovery, workflow lifecycle, and combinator results are omitted. The generic recorder omits provider results except for the exact `{ created: true }` creation outcome from `pi.write`; no output or provider details accompany it. It projects arguments by exact reference:
55
+
56
+ - `pi.read`: local `path`, numeric `offset`, numeric `limit`
57
+ - `pi.grep`: local `path`, numeric `context`, numeric `limit`; pattern/query omitted
58
+ - `pi.find`, `pi.ls`: local `path`, numeric `limit`; pattern/query omitted
59
+ - `pi.edit`, `pi.write`: local `path` only; edit replacements and write content omitted; `pi.write` may retain `{ created: true }`
60
+ - `pi.bash`: bounded command text
61
+ - selected `agents.*` lifecycle calls: `id` only; task, message, instructions, names, model options, and outputs omitted
62
+ - `mesh.publish`/`read`: topic/address and numeric cursor/limit; payload text/data omitted
63
+ - `mesh.get`/`put`/`delete`/`list`: key or prefix and limit; values omitted
64
+ - memory, state, schema, compact, MCP, extension, unknown, and external calls: no arguments or results
65
+
66
+ ### Discovery operations
67
+
68
+ Read-only discovery continues to bypass mutation authorization and approval budgets, but every attempt is durable in the same `sequence` space as actions and workflow activity:
69
+
70
+ - `fabric.discovery.providers`: no arguments or results
71
+ - `fabric.discovery.models`: no arguments or results
72
+ - `fabric.discovery.list`: identifier-shaped `provider` and `namespace`, plus numeric `limit`; free-form `query` and results omitted
73
+ - `fabric.discovery.search`: numeric `limit`; free-form `query` and results omitted
74
+ - `fabric.discovery.describe`: identifier-shaped action `ref`; results omitted
75
+
76
+ Discovery operations record `succeeded`, `failed`, `aborted`, or `timed_out` with the applicable `guard`, `resolve`, or `invoke` stage. Model-registry enumeration keeps its existing best-effort empty-list behavior when enumeration throws, while the corresponding operation is marked failed.
77
+
78
+ ### Workflow lifecycle operations
79
+
80
+ Declarative workflow calls remain transient activity updates for the live UI and are also durable occurrence records:
81
+
82
+ - `fabric.workflow.configure`: `name`; description omitted
83
+ - `fabric.workflow.phase`: `name`, identifier-shaped `id`, numeric `total`; description omitted
84
+ - `fabric.workflow.item`: identifier-shaped `id`, `status`, `phase`, and `kind`, plus numeric `total` and `completed`; label, detail, current value, and data omitted
85
+ - `fabric.workflow.event`: identifier-shaped `level`; message and data omitted
86
+ - `fabric.workflow.progress`: no arguments; message omitted
87
+
88
+ These operations preserve bridge issue order alongside actions and discovery. The separate `phases` compatibility field remains occurrence-ordered and still retains repeated transitions.
89
+
90
+ ### Workflow combinator spans
91
+
92
+ Calls to `workflow.parallel` and `workflow.pipeline` are instrumented in the shared guest implementation and recorded as `fabric.workflow.parallel` and `fabric.workflow.pipeline`. Start creates one operation; end updates that same operation. Persisted metadata is limited to `kind`, numeric `itemCount`, numeric `stageCount` for pipelines, and effective bounded `concurrency` for parallel calls. Empty combinators are represented. Pipeline execution naturally nests its parallel fan-out, so the pipeline operation is issued before the nested parallel operation and both precede stage actions.
93
+
94
+ Guest span IDs are deterministic execution-local bridge correlation values. They are never persisted, and the internal start/end bridge is closure-private rather than part of the guest API. Internal span calls do not enter provider resolution, authorization, approval, or agent-budget accounting. A thrown stage closes active spans as failed; runtime failure, deadline, or cancellation seals any still-open operation with the typed final execution outcome.
95
+
96
+ Only plain local paths are retained. URL paths are omitted, including credentials and query/fragment data. Plain path query/fragment suffixes are removed. Sensitive-key normalization, media/base64 rejection, JSON safety, depth/node limits, and UTF-8 truncation remain defense in depth after projection; they are not the primary secrecy mechanism.
97
+
98
+ Identifiers (`ref`, `provider`, `action`), outcomes, failure stage, operation sequence, and occurrence-ordered phase labels remain durable. These fields, retained local paths/mesh addresses, and bash command text are not secret containers; callers must not intentionally place credentials in identifiers, local filenames, topics, keys, phase names, or commands.
99
+
100
+ ## Reading and rendering traces
101
+
102
+ The package exports `isFabricExecutionTraceV1`, `isFabricExecutionTraceOperationV1`, `readFabricExecutionTraceV1`, `createFabricPersistedExecutionDetails`, and `readFabricExecutionRenderDetails`. Guards reject malformed envelopes, extra fields, oversized data, and unknown versions.
103
+
104
+ Current trace-only sessions reconstruct compact nested-call rows from operation metadata, including bash command text. Old sessions containing `details.audits` and `details.phases` continue to render through the legacy adapter. For old digest-only bash traces, the renderer matches the digest against literal and named strings in the already-visible outer `fabric_exec` arguments; if no exact command can be recovered, it omits the digest instead of displaying a hash. New final details never write `audits`.
105
+
106
+ Compaction and memory read only `toolResult.details.trace` through the trace guard. Compaction emits phases and operations in sequence order with stable `entryId/subordinal` addresses, and memory emits one normalized child per operation with address `<outer-entry-id>/<sequence>`. Neither consumer parses `fabric_exec` source, outer output, operation results, or rendered audit prose to recover calls, files, or failures.
107
+
108
+ A present but invalid or unknown trace blocks semantic legacy reinterpretation. Only when the trace field is absent may compaction use its separate strict old-session `details.audits` adapter. Memory indexes trace operations only.
109
+
110
+ ## Limitations
111
+
112
+ Safe projections intentionally reduce durable reconstruction. Final rendering cannot show read bodies, edit diffs, write bodies, agent tasks, discovery queries/results, workflow descriptions/labels/messages/data, external/MCP arguments, or provider results. Bash command text is retained. Combinator traces show structure and typed outcome, not item values, stage functions, stage results, parent IDs, or timing. Generic failure resolution has ref identity only when arguments are omitted. Rich action audits and workflow activity content remain available only while the live execution result or activity store is in memory.