pi-fabric 0.22.0 → 0.22.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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -20
- package/dist/index.js.map +1 -1
- package/dist/ui/fabric-render.d.ts +1 -0
- package/dist/ui/fabric-render.d.ts.map +1 -1
- package/dist/ui/fabric-render.js +1 -0
- package/dist/ui/fabric-render.js.map +1 -1
- package/dist/ui/transcript.d.ts.map +1 -1
- package/dist/ui/transcript.js +96 -5
- package/dist/ui/transcript.js.map +1 -1
- package/package.json +1 -1
- package/skills/fabric-advisor/SKILL.md +11 -103
- package/skills/fabric-ambient/SKILL.md +13 -94
- package/skills/fabric-ambient/references/setup.md +71 -0
- package/skills/fabric-council/SKILL.md +5 -18
- package/skills/fabric-exec/SKILL.md +8 -7
- package/skills/fabric-fusion/SKILL.md +6 -12
- package/skills/fabric-rlm/SKILL.md +16 -43
- package/skills/fabric-schema/SKILL.md +33 -50
- package/skills/fabric-supervisor/SKILL.md +12 -75
- package/skills/fabric-swarm/SKILL.md +26 -36
- package/skills/fabric-workflow/SKILL.md +15 -41
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Ambient actor setup
|
|
2
|
+
|
|
3
|
+
Pass `strings.name`, `strings.instructions`, JSON `strings.events`, `strings.triggerTurn` (`"true"`/`"false"`), and `strings.model` (key/substring; empty when unset).
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const events = JSON.parse(π.events) as FabricActorHostEvent[];
|
|
7
|
+
const triggerTurn = π.triggerTurn === "true";
|
|
8
|
+
let model: string | undefined;
|
|
9
|
+
let runner: FabricAgentRunner | undefined;
|
|
10
|
+
if (π.model) {
|
|
11
|
+
const models: Array<FabricModelInfo & { runner: FabricAgentRunner }> = (
|
|
12
|
+
await tools.models()
|
|
13
|
+
).map((entry) => ({ ...entry, runner: "pi" as const }));
|
|
14
|
+
try {
|
|
15
|
+
models.push(...(await agents.models({ runner: "claude" })).map((entry) => ({
|
|
16
|
+
...entry,
|
|
17
|
+
runner: "claude" as const,
|
|
18
|
+
})));
|
|
19
|
+
} catch {}
|
|
20
|
+
const needle = π.model.toLowerCase();
|
|
21
|
+
const selected = models.find((entry) =>
|
|
22
|
+
entry.key.toLowerCase() === needle ||
|
|
23
|
+
entry.id.toLowerCase().includes(needle) ||
|
|
24
|
+
entry.name.toLowerCase().includes(needle)
|
|
25
|
+
);
|
|
26
|
+
if (!selected) throw new Error(`Model "${π.model}" not found: ${models.map((entry) => entry.key).join(", ")}`);
|
|
27
|
+
model = selected.key;
|
|
28
|
+
runner = selected.runner;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const existing = (await agents.actors()).find(
|
|
32
|
+
(actor) => actor.name === π.name && actor.status !== "stopped",
|
|
33
|
+
);
|
|
34
|
+
if (existing) {
|
|
35
|
+
await agents.setInstructions({ id: existing.id, instructions: π.instructions });
|
|
36
|
+
if (existing.events.length !== events.length || events.some((event) => !existing.events.includes(event))) {
|
|
37
|
+
await agents.setEvents({ id: existing.id, events });
|
|
38
|
+
}
|
|
39
|
+
if (existing.delivery !== "steer" || existing.triggerTurn !== triggerTurn) {
|
|
40
|
+
await agents.setDeliveryPolicy({ id: existing.id, delivery: "steer", triggerTurn });
|
|
41
|
+
}
|
|
42
|
+
const runnerMatches = !runner || existing.runner === runner;
|
|
43
|
+
const modelMatches = !model || existing.model === model;
|
|
44
|
+
return {
|
|
45
|
+
reused: true,
|
|
46
|
+
actor: await agents.actorStatus({ id: existing.id }),
|
|
47
|
+
warnings: [
|
|
48
|
+
...(existing.responseMode !== "directive" ? ["recreate for responseMode=directive"] : []),
|
|
49
|
+
...(existing.coalesce !== true ? ["recreate for coalesce=true"] : []),
|
|
50
|
+
...(!runnerMatches ? [`runner "${runner}" requires recreation`] : []),
|
|
51
|
+
...(runnerMatches && !modelMatches ? [`model "${model}" requires a dashboard change or recreation`] : []),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const actor = await agents.create({
|
|
57
|
+
name: π.name,
|
|
58
|
+
instructions: π.instructions,
|
|
59
|
+
events,
|
|
60
|
+
responseMode: "directive",
|
|
61
|
+
delivery: "steer",
|
|
62
|
+
triggerTurn,
|
|
63
|
+
coalesce: true,
|
|
64
|
+
tools: ["read", "grep", "find", "ls"],
|
|
65
|
+
...(runner ? { runner } : {}),
|
|
66
|
+
...(model ? { model } : {}),
|
|
67
|
+
});
|
|
68
|
+
return { started: true, actor };
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Reuse updates instructions/events/delivery. Recreate for runner, `responseMode`, or `coalesce`; use dashboard/recreation for model. Report ID/warnings and messages/stop; do not wait.
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-council
|
|
3
|
-
description: Runs a bounded multi-perspective Pi Fabric council with independent reviewers and
|
|
3
|
+
description: Runs a bounded multi-perspective Pi Fabric council with independent reviewers and optional synthesis. Use for architecture choices, plans, reviews, and adversarial cross-checking.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Fabric Council
|
|
8
8
|
|
|
9
|
-
Use one `fabric_exec` call and `council.run()`. Choose roles that disagree usefully rather than duplicating one another.
|
|
10
|
-
|
|
11
|
-
A council runs N role agents concurrently plus a sequential synthesizer, all inside the single `fabric_exec` sandbox. The sandbox has a hard wall-clock ceiling (`executor.timeoutMs`, default 120s; raise it in `fabric.json` or `/fabric` settings for read-heavy councils). Keep roles and per-role tool work bounded so `max(role durations) + synthesizer` fits the ceiling, or the sandbox times out and aborts every in-flight agent. Council and `rlm.query` usage counts toward `budget.spent()` and the `tokenBudget` guard.
|
|
9
|
+
Use one `fabric_exec` call and `council.run()`. Choose three to five roles that disagree usefully rather than duplicating one another, such as correctness, security, operability, maintainability, and requirements skepticism.
|
|
12
10
|
|
|
13
11
|
```ts
|
|
14
12
|
const roles = JSON.parse(π.roles) as string[];
|
|
@@ -17,25 +15,14 @@ await workflow.configure({
|
|
|
17
15
|
description: `${roles.length} independent perspectives with synthesis`,
|
|
18
16
|
});
|
|
19
17
|
await phase("Deliberate", { total: roles.length });
|
|
20
|
-
|
|
18
|
+
return council.run({
|
|
21
19
|
task: π.task,
|
|
22
20
|
roles,
|
|
23
21
|
tools: ["read", "grep", "find", "ls"],
|
|
24
22
|
synthesize: true,
|
|
25
23
|
});
|
|
26
|
-
return decision;
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
Pass the
|
|
30
|
-
|
|
31
|
-
- implementation correctness reviewer
|
|
32
|
-
- security and abuse-case reviewer
|
|
33
|
-
- test and operability reviewer
|
|
34
|
-
- simplicity and maintenance reviewer
|
|
35
|
-
- user-requirements skeptic
|
|
36
|
-
|
|
37
|
-
Council members must inspect evidence independently. The synthesizer must preserve material disagreement, reject unsupported claims, and make a concrete recommendation. Use `synthesize: false` only when the user asked for raw independent opinions.
|
|
38
|
-
|
|
39
|
-
Do not use a council for a simple lookup or a decision with no meaningful competing considerations.
|
|
26
|
+
Pass the task as `strings.task` and the JSON role array as `strings.roles`. Members inspect evidence independently; synthesis must preserve material disagreement, reject unsupported claims, and make a concrete recommendation. Use `synthesize: false` only for raw opinions.
|
|
40
27
|
|
|
41
|
-
|
|
28
|
+
A council costs N concurrent role agents plus one sequential synthesizer and must fit `executor.timeoutMs`; its usage counts toward the execution budget. Keep roles bounded and set top-level `agentBudget`/`tokenBudget` for large runs. Do not use a council for a simple lookup or a decision with no meaningful competing considerations.
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-exec
|
|
3
3
|
description: >-
|
|
4
|
-
Reference for
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
proxies, and the validate, describe, retry error loop. Load before your first
|
|
9
|
-
`fabric_exec` call and whenever a call errors on argument shape. See
|
|
10
|
-
`references/mcp.md` for MCP naming and management.
|
|
4
|
+
Reference for `fabric_exec` TypeScript programs: Pi core tool signatures,
|
|
5
|
+
discovery, provider and MCP proxies, named strings, return shapes, and
|
|
6
|
+
schema-driven error recovery. Load before the first Fabric call or after an
|
|
7
|
+
argument-shape error.
|
|
11
8
|
---
|
|
12
9
|
|
|
13
10
|
# fabric_exec — core reference
|
|
@@ -29,6 +26,8 @@ One type-checked TS program in a fresh executor (isolated QuickJS by default). O
|
|
|
29
26
|
|
|
30
27
|
Aliases (normalized to canonical before the host validates args): `cmd`/`shell`/`cmdline`→`command`, `timeoutMs`→`timeout`; `query`/`regex`/`search`→`pattern`; `ic`/`caseInsensitive`→`ignoreCase`; `globPattern`→`glob`; `ctx`→`context`; `max`→`limit`; `file`/`dir`→`path`; `start`→`offset`; `old`→`oldText`; `new`/`replacement`→`newText`; `contents`/`body`/`text`→`content`. Misspelled keys still fail the excess-property type check.
|
|
31
28
|
|
|
29
|
+
When a program needs a string containing literal `${...}` (shell snippets, tool arguments, or grep patterns), do not use a TypeScript template literal: TypeScript will interpolate it. Use a plain quoted string or pass the content through `strings` and read it from `π.key`.
|
|
30
|
+
|
|
32
31
|
## First-class provider calls
|
|
33
32
|
Use direct proxies when the action is known. No-argument actions such as `schema.status()`, `state.get()`, and `compact.status()` take no options object. Provider calls still cross the same registry validation, approval, audit, timeout, and cancellation path as generic calls.
|
|
34
33
|
|
|
@@ -78,3 +77,5 @@ Multi-agent orchestration is opt-in: load `/skill:fabric-workflow`, `/skill:fabr
|
|
|
78
77
|
`agents.main()` returns the dashboard-owning root Pi session; `agents.peers()` lists other live root sessions in the shared project mesh as `Peer <session-prefix>` targets. Peers support `agents.steer()` and `agents.followUp()` by exact id.
|
|
79
78
|
|
|
80
79
|
Agent requests and persistent actors accept `runner: "pi" | "claude"`. Pi is the default and is required for `recursive: true`, `rlm.query()`, and actors that must call Fabric or mesh APIs themselves. Claude invokes the official `claude -p` harness; it supports mapped Claude Code tools and host-managed persistent actors, but not recursive/direct Fabric APIs. Use `agents.models({ runner: "claude" })` for runtime-enumerated `claude/<value>` model keys.
|
|
80
|
+
|
|
81
|
+
Omit `timeoutMs` for subagents and actors unless requesting longer than the configured `subagents.timeoutMs` (60 minutes by default). Per-call values below the configured default are ignored.
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-fusion
|
|
3
|
-
description: Multi-model deliberation.
|
|
3
|
+
description: Multi-model deliberation. Up to 8 distinct models answer in parallel with web-capable tools, then a judge compares consensus, contradictions, coverage gaps, unique insights, and blind spots. Use when the cost of being wrong justifies multiple completions.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Fabric Fusion
|
|
8
8
|
|
|
9
|
-
Use one `fabric_exec` call
|
|
9
|
+
Use one `fabric_exec` call for a 1–8 model panel followed by one judge. The judge compares rather than merges responses; return its structured analysis so the caller writes the final answer. Use fusion for model-diverse research or critique, not tactical work or a lookup.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Pass the task and panel through `strings` and reference them as `π.*`. `π.task` is the prompt. `π.panel` is OpenRouter's `analysis_models`, a JSON `Array<{ model, label? }>` of 1–8 models; `label` defaults to the model id and is used only for attribution in the judge's input and the dashboard, never injected into a member's prompt. `π.judge` is OpenRouter's judge `model` (defaults to the first panel model). `π.tools` is the panel+judge tool allowlist (defaults to read, grep, find, ls, and bash; `bash` is the `web_search`/`web_fetch` analog via `gsearch`/`curl`). `π.thinking` is the reasoning effort for panel+judge (maps to OpenRouter's `reasoning`; defaults to `subagents.thinking`, medium). Pass every referenced key, using empty string for the optionals (`judge`, `tools`, `thinking`) when not setting them.
|
|
11
|
+
Pass every key: `strings.task`; JSON `strings.panel` as `Array<{ model, label? }>`; and optional `strings.judge`, `strings.tools`, and `strings.thinking` as empty strings when unset. Labels are attribution only. Tools default to `read`, `grep`, `find`, `ls`, and `bash`; thinking defaults to configured subagent thinking.
|
|
14
12
|
|
|
15
13
|
```ts
|
|
16
14
|
type FusionAnalysis = {
|
|
@@ -130,12 +128,8 @@ await workflow.event({ message: `Fusion complete · ${members.length}-model pane
|
|
|
130
128
|
return analysis;
|
|
131
129
|
```
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Cost is N panel + 1 judge: a 3-model panel is roughly 4× a single answer. The run counts toward `budget.spent()` and the `tokenBudget` guard; `subagents.budgetUsd` bounds total spend. Set `agentBudget` and `tokenBudget` on `fabric_exec` when the panel or per-member work is large.
|
|
136
|
-
|
|
137
|
-
Deliberation is bounded to one level. Members and the judge run as plain `agent()` calls without `recursive: true`, so they do not receive `fabric_exec` and cannot launch their own fusion panel, the same invariant OpenRouter enforces with its `x-openrouter-fusion-depth` header. Unlike OpenRouter, where the outer model decides per request whether to call `openrouter:fusion`, here the caller decides by running this skill; once invoked, the panel + judge always run.
|
|
131
|
+
Choose distinct models by intent: strongest available, budget-balanced with a frontier judge, or similar-latency models for faster fan-out. The default panel size is three. Cost is N panel calls plus one judge; set top-level `agentBudget` and `tokenBudget`, while `subagents.budgetUsd` bounds spend.
|
|
138
132
|
|
|
139
|
-
|
|
133
|
+
Panel members and the judge are plain, non-recursive agents, so deliberation is one level. `bash` enables web access through local search/fetch commands and requires execute approval. Concurrency is capped by `subagents.maxConcurrent`; inner calls otherwise inherit provider limits and use `thinking` for reasoning effort.
|
|
140
134
|
|
|
141
|
-
|
|
135
|
+
Use `/skill:fabric-council` instead for same-model, role-diverse review. Use a plain agent when competing model perspectives do not justify the cost.
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-rlm
|
|
3
|
-
description: Recursively decomposes
|
|
3
|
+
description: Recursively decomposes oversized tasks into bounded child Pi agents with fresh context windows. Use for whole-repo audits, massive-context analysis, and multi-file refactors that do not fit one context.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Fabric Recursive Decomposition
|
|
7
|
+
# Fabric Recursive Decomposition
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use recursion for context size, not mere difficulty. If the relevant material fits one context, work directly. Otherwise pass the root task as `strings.task` and use one `fabric_exec` program to size up → orient → delegate bounded chunks → combine.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Core pattern: size up → search → delegate → combine
|
|
14
|
-
|
|
15
|
-
1. **Size up** — `wc -l`, `wc -c`, file counts, grep scope. If it fits one context window, do it directly; do not delegate.
|
|
16
|
-
2. **Search** — `grep`, `find`, `ls`, `read` to orient before delegating.
|
|
17
|
-
3. **Delegate** — hand clear, bounded subtasks to children. Use `rlm.query()` when the child should be able to recurse again; use `agent()` for a one-shot leaf worker that does not need to recurse.
|
|
18
|
-
4. **Combine** — aggregate, deduplicate, resolve conflicts, produce the final output.
|
|
19
|
-
5. **Do it directly when it's small** — never delegate what you can do in one step.
|
|
20
|
-
|
|
21
|
-
## One fabric_exec program
|
|
22
|
-
|
|
23
|
-
Turn the request into a single type-checked `fabric_exec` program. The program holds the decomposition loop and branches. Pass the root task through `strings.task`.
|
|
11
|
+
`rlm.query()` is `agents.run({ runner: "pi", recursive: true })`: each child gets a fresh context and may recurse up to `subagents.maxDepth`. Use plain `agent()` for leaves that do not need recursion.
|
|
24
12
|
|
|
25
13
|
```ts
|
|
26
14
|
await workflow.configure({
|
|
@@ -28,10 +16,9 @@ await workflow.configure({
|
|
|
28
16
|
description: "Size up, delegate bounded subtasks, combine",
|
|
29
17
|
});
|
|
30
18
|
|
|
31
|
-
// Size up and orient with read-only tools before delegating.
|
|
32
19
|
await phase("Orient", { total: 1 });
|
|
33
20
|
const scope = await agent<{ paths: string[] }>(
|
|
34
|
-
`Identify the
|
|
21
|
+
`Identify only the files relevant to this task.\n\nTask:\n${π.task}`,
|
|
35
22
|
{
|
|
36
23
|
label: "scope",
|
|
37
24
|
tools: ["read", "grep", "find", "ls"],
|
|
@@ -44,15 +31,12 @@ const scope = await agent<{ paths: string[] }>(
|
|
|
44
31
|
},
|
|
45
32
|
);
|
|
46
33
|
|
|
47
|
-
// Delegate each bounded subtask to a recursive child. Each child gets a fresh
|
|
48
|
-
// context window and may call rlm.query() again up to subagents.maxDepth.
|
|
49
34
|
await phase("Decompose", { total: scope.paths.length });
|
|
50
35
|
const findings = await parallel(
|
|
51
|
-
scope.paths.map((
|
|
36
|
+
scope.paths.map((path) => () =>
|
|
52
37
|
rlm.query({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
name: `analyze ${p}`.slice(0, 50),
|
|
38
|
+
task: `Analyze ${path} for this objective; return concrete evidence.\n\nObjective:\n${π.task}`,
|
|
39
|
+
name: `analyze ${path}`.slice(0, 50),
|
|
56
40
|
tools: ["read", "grep", "find", "ls"],
|
|
57
41
|
}),
|
|
58
42
|
),
|
|
@@ -60,27 +44,16 @@ const findings = await parallel(
|
|
|
60
44
|
);
|
|
61
45
|
|
|
62
46
|
await phase("Combine", { total: 1 });
|
|
63
|
-
|
|
64
|
-
`
|
|
47
|
+
return agent(
|
|
48
|
+
`Deduplicate, reconcile, and synthesize these findings; drop unsupported claims:\n${JSON.stringify(findings)}`,
|
|
65
49
|
{ label: "combine", tools: ["read", "grep", "find", "ls"] },
|
|
66
50
|
);
|
|
67
|
-
return summary;
|
|
68
51
|
```
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
## When to recurse vs do it directly
|
|
73
|
-
|
|
74
|
-
- A 30-line file or a single function: read it and act directly. No delegation.
|
|
75
|
-
- A multi-file refactor: delegate per file or per module and combine.
|
|
76
|
-
- A whole-repo audit or massive-context analysis: size up first, fan out bounded subtasks, then synthesize.
|
|
77
|
-
|
|
78
|
-
## Guardrails
|
|
53
|
+
Guardrails:
|
|
79
54
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
- The pre-spawn budget check is best-effort (concurrent children can overshoot slightly); the race-free ceiling is `subagents.maxPerExecution`.
|
|
86
|
-
- A recursive child that drifts can be redirected without losing its context: `agents.steer({ id, message })` is delivered between the child's turns (after its current tool calls, before the next LLM call), and `agents.status({ id }).pendingMessages` shows the queued steers. Prefer steering over abandoning a child that has accumulated useful context. See the `agents` reference.
|
|
55
|
+
- Size scope before spawning; deeper children should make fewer calls and act more directly.
|
|
56
|
+
- Partition edit ownership by path or use `worktree: true`; concurrent children must not edit the same files.
|
|
57
|
+
- `subagents.maxDepth` bounds each branch. The shared `subagents.budgetUsd` ledger is a best-effort whole-tree spend guard, not a hard cap; concurrent branches can overshoot its pre-spawn check. `maxPerExecution` and top-level `agentBudget` cap calls only in the current process.
|
|
58
|
+
- `budget.remaining()` reports the current execution's token budget from completed usage; it does not expose agent-call or USD budget. Concurrent fan-out can overshoot that observation, so dispatch sequentially or in checked batches when the token ceiling matters.
|
|
59
|
+
- Initial approval delegates only agent risk; network, execution, and write approvals are not inherited. Redirect a valuable drifting child with `agents.steer` rather than discarding its context.
|
|
@@ -1,54 +1,42 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-schema
|
|
3
|
-
description: Uses Fabric's typed Schema evidence loop and, when enabled,
|
|
3
|
+
description: Uses Fabric's typed Schema evidence loop and, when enabled, its bounded local-file transaction channel. Use when surprise must void a plan and mutation claims need explicit postconditions.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Fabric Schema
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Start with `const status = await schema.status();`.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- `off` is the compatibility default. `state.*` can be used as workflow discipline, but it does not gate direct `pi.edit`, `pi.write`, or `pi.bash`.
|
|
16
|
-
- `audit` preserves current behavior and durably reports actions that enforce mode would block.
|
|
17
|
-
- `enforce` is a host authorization boundary for model-originated protected-workspace file changes. Direct mutations, agents, state/mesh writes, compaction requests, MCP, extensions, and external providers are blocked. Use one same-`fabric_exec` `schema.hypothesize → schema.verify → schema.commit` sequence.
|
|
11
|
+
- `off`: compatibility mode; state discipline is optional and does not gate direct `pi.edit`, `pi.write`, or `pi.bash`.
|
|
12
|
+
- `audit`: reports actions enforce mode would block without changing behavior.
|
|
13
|
+
- `enforce`: host authorization for protected-workspace file changes. Reads remain available; mutations require one same-`fabric_exec` `schema.hypothesize → schema.verify → schema.commit` sequence. Direct mutations, agents, state/mesh writes, compaction, MCP, extensions, and external providers are blocked.
|
|
18
14
|
|
|
19
|
-
Evidence is not proof.
|
|
15
|
+
Evidence is not proof. Verification confirms falsifiable observations at one fingerprinted workspace state; postconditions confirm scoped observations after declared operations. Either can pass without establishing general semantic correctness.
|
|
20
16
|
|
|
21
17
|
## Enforce loop
|
|
22
18
|
|
|
23
|
-
Observe with
|
|
19
|
+
Observe, hypothesize with literal/SHA evidence or a host-configured trusted command, verify, then commit only with the returned short-lived certificate and observed SHA:
|
|
24
20
|
|
|
25
21
|
```ts
|
|
26
22
|
await pi.read({ path: "src/parser.ts" });
|
|
27
|
-
|
|
28
23
|
const hypothesis = await schema.hypothesize({
|
|
29
24
|
label: "parser-local-form",
|
|
30
|
-
summary: "
|
|
25
|
+
summary: "The declared parser edit accepts the local form while focused checks remain green",
|
|
31
26
|
evidence: [
|
|
32
27
|
{ kind: "file_contains", path: "src/parser.ts", literal: "old literal" },
|
|
33
|
-
{ kind: "trusted_command", name: "parser-focused-tests" }
|
|
34
|
-
]
|
|
28
|
+
{ kind: "trusted_command", name: "parser-focused-tests" },
|
|
29
|
+
],
|
|
35
30
|
});
|
|
31
|
+
const verification = await schema.verify({ hypothesisId: hypothesis.hypothesisId });
|
|
32
|
+
if (!verification.verified || !verification.certificate) return verification;
|
|
36
33
|
|
|
37
|
-
const
|
|
38
|
-
hypothesisId: hypothesis.hypothesisId
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (!verification.verified || !verification.certificate) {
|
|
42
|
-
// Missing, empty, stale, nonconfirmed, errored, timed-out, cancelled, or
|
|
43
|
-
// workspace-changing evidence voids the plan. Do not act.
|
|
44
|
-
return verification;
|
|
45
|
-
}
|
|
46
|
-
const parserEvidence = verification.results.find(
|
|
34
|
+
const observed = verification.results.find(
|
|
47
35
|
(result) => result.evidence.path === "src/parser.ts",
|
|
48
|
-
);
|
|
49
|
-
if (!
|
|
36
|
+
)?.observedSha256;
|
|
37
|
+
if (!observed) return { void: true, reason: "missing observed SHA-256" };
|
|
50
38
|
|
|
51
|
-
|
|
39
|
+
return schema.commit({
|
|
52
40
|
hypothesisId: hypothesis.hypothesisId,
|
|
53
41
|
certificate: verification.certificate,
|
|
54
42
|
operations: [{
|
|
@@ -56,56 +44,51 @@ const outcome = await schema.commit({
|
|
|
56
44
|
path: "src/parser.ts",
|
|
57
45
|
oldText: "old literal",
|
|
58
46
|
newText: "new literal",
|
|
59
|
-
expectedSha256:
|
|
47
|
+
expectedSha256: observed,
|
|
60
48
|
}],
|
|
61
49
|
postconditions: [
|
|
62
50
|
{ kind: "file_contains", path: "src/parser.ts", literal: "new literal" },
|
|
63
|
-
{ kind: "trusted_command", name: "parser-focused-tests" }
|
|
64
|
-
]
|
|
51
|
+
{ kind: "trusted_command", name: "parser-focused-tests" },
|
|
52
|
+
],
|
|
65
53
|
});
|
|
66
|
-
return outcome;
|
|
67
54
|
```
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
Any missing, stale, failed, timed-out, cancelled, or workspace-changing evidence voids the plan. Never invent trusted-command shell text or arguments: names resolve to static host configuration.
|
|
70
57
|
|
|
71
|
-
The certificate is random,
|
|
58
|
+
The certificate is random, single-use, invocation-bound, and tied to the hypothesis, state head/version, workspace fingerprint, and generation. Do not return it for later use. If stopping after verification, call this with the values from that verification:
|
|
72
59
|
|
|
73
|
-
```
|
|
60
|
+
```text
|
|
74
61
|
await schema.abort({
|
|
75
62
|
hypothesisId: hypothesis.hypothesisId,
|
|
76
|
-
certificate: verification.certificate
|
|
63
|
+
certificate: verification.certificate,
|
|
77
64
|
});
|
|
78
65
|
```
|
|
79
66
|
|
|
80
|
-
|
|
67
|
+
Operations are project-relative regular files:
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
- `edit`: `oldText`, `newText`, `expectedSha256`
|
|
70
|
+
- `write`: `expected: { absent: true }` for creation or `{ sha256 }` for replacement
|
|
71
|
+
- `delete`: `expectedSha256`
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
- `{ kind: "file_absent", path }`
|
|
86
|
-
- `{ kind: "file_contains", path, literal }` — literal containment, not a regex
|
|
87
|
-
- `{ kind: "file_sha256", path, sha256 }`
|
|
88
|
-
- `{ kind: "trusted_command", name }` — static host-configured executable/argv
|
|
73
|
+
Path/symlink escape is rejected. Keep operations declared, local, and small. Treat only `committed` as success; `rolled_back` restored declared paths after failure, while `quarantined` means rollback needs operator inspection.
|
|
89
74
|
|
|
90
|
-
|
|
75
|
+
Evidence/postcondition forms are `file_exists`, `file_absent`, `file_contains` (literal, not regex), `file_sha256`, and `trusted_command` (static host executable/argv). Trusted commands should be deterministic, local, and read-only. Remote/network/database effects are not transactional and remain blocked in enforce mode.
|
|
91
76
|
|
|
92
|
-
For a
|
|
77
|
+
For a complexity-reduction claim, set `complexityReduction: true` and include behavior-preservation postconditions. Certification means those scoped checks passed, not that complexity was objectively measured.
|
|
93
78
|
|
|
94
79
|
## Off-mode state discipline
|
|
95
80
|
|
|
96
|
-
When mode is `off`, the
|
|
81
|
+
When mode is `off`, the independent labeled state layer can still record and verify a falsifiable claim:
|
|
97
82
|
|
|
98
83
|
```ts
|
|
99
84
|
const transition = await state.transition({
|
|
100
85
|
label: "claim",
|
|
101
86
|
to: "claim-stated",
|
|
102
87
|
summary: "A falsifiable delta",
|
|
103
|
-
evidence: ["pnpm exec vitest run tests/focused.test.ts"]
|
|
88
|
+
evidence: ["pnpm exec vitest run tests/focused.test.ts"],
|
|
104
89
|
});
|
|
105
90
|
const verification = await state.verify();
|
|
106
91
|
if (!verification.certified) return { void: true, transition, verification };
|
|
107
92
|
```
|
|
108
93
|
|
|
109
|
-
This is discipline
|
|
110
|
-
|
|
111
|
-
See `docs/schema-enforcement.md` for the exact guarantee, trusted-command argv/shell behavior, fingerprint coverage, recovery model, and limitations. See `docs/state-layer.md` for the independent labeled state timeline.
|
|
94
|
+
This is workflow discipline, not enforcement. See `../../docs/schema-enforcement.md` for the guarantee and recovery model, and `../../docs/state-layer.md` for the labeled timeline.
|
|
@@ -1,75 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-supervisor
|
|
3
|
-
description: Starts a persistent
|
|
3
|
+
description: Starts a persistent Pi Fabric supervisor that watches the main session toward a concrete goal and steers only when needed. Use for long-running goal supervision without another extension.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Fabric Supervisor
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Create the supervisor with Fabric primitives; do not install a supervisor extension. Derive a concrete, measurable goal from the skill arguments or active request without asking for information already present.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Read `../fabric-ambient/references/setup.md` completely, then use its program with:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
description: "Persistent ambient goal supervision",
|
|
19
|
-
});
|
|
20
|
-
await phase("Start actor", { total: 1 });
|
|
21
|
-
const current = await agents.actors();
|
|
22
|
-
const existing = current.find((actor) => actor.name === π.name && actor.status !== "stopped");
|
|
23
|
-
if (existing) {
|
|
24
|
-
const migrated: string[] = [];
|
|
25
|
-
await agents.setInstructions({ id: existing.id, instructions: π.instructions });
|
|
26
|
-
if (
|
|
27
|
-
existing.events.length !== 2 ||
|
|
28
|
-
!existing.events.includes("agent_settled") ||
|
|
29
|
-
!existing.events.includes("tool_error")
|
|
30
|
-
) {
|
|
31
|
-
await agents.setEvents({ id: existing.id, events: ["agent_settled", "tool_error"] });
|
|
32
|
-
migrated.push("events");
|
|
33
|
-
}
|
|
34
|
-
if (existing.delivery !== "steer" || existing.triggerTurn !== true) {
|
|
35
|
-
await agents.setDeliveryPolicy({
|
|
36
|
-
id: existing.id,
|
|
37
|
-
delivery: "steer",
|
|
38
|
-
triggerTurn: true,
|
|
39
|
-
});
|
|
40
|
-
migrated.push("deliveryPolicy");
|
|
41
|
-
}
|
|
42
|
-
const warnings = [
|
|
43
|
-
...(existing.responseMode !== "directive" ? ["responseMode should be directive; recreate the actor"] : []),
|
|
44
|
-
...(existing.coalesce !== true ? ["coalesce should be true; recreate the actor"] : []),
|
|
45
|
-
];
|
|
46
|
-
return {
|
|
47
|
-
reused: true,
|
|
48
|
-
actor: await agents.actorStatus({ id: existing.id }),
|
|
49
|
-
migrated,
|
|
50
|
-
warnings,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const actor = await agents.create({
|
|
55
|
-
name: π.name,
|
|
56
|
-
instructions: π.instructions,
|
|
57
|
-
events: ["agent_settled", "tool_error"],
|
|
58
|
-
responseMode: "directive",
|
|
59
|
-
delivery: "steer",
|
|
60
|
-
triggerTurn: true,
|
|
61
|
-
coalesce: true,
|
|
62
|
-
tools: ["read", "grep", "find", "ls"],
|
|
63
|
-
});
|
|
64
|
-
return {
|
|
65
|
-
started: true,
|
|
66
|
-
actor,
|
|
67
|
-
inspect: `/fabric messages ${actor.id.slice(0, 8)}`,
|
|
68
|
-
stop: `/fabric stop ${actor.id.slice(0, 8)}`,
|
|
69
|
-
};
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Use this prompt, replacing `GOAL`:
|
|
13
|
+
- `strings.name`: `supervisor`
|
|
14
|
+
- `strings.events`: `["agent_settled","tool_error"]`
|
|
15
|
+
- `strings.triggerTurn`: `true`
|
|
16
|
+
- `strings.model`: model key or substring, or an empty string when unset
|
|
17
|
+
- `strings.instructions`: the prompt below with `GOAL` replaced
|
|
73
18
|
|
|
74
19
|
```text
|
|
75
20
|
You are an ambient supervisor for this goal:
|
|
@@ -78,19 +23,11 @@ You are an ambient supervisor for this goal:
|
|
|
78
23
|
GOAL
|
|
79
24
|
</goal>
|
|
80
25
|
|
|
81
|
-
Review
|
|
26
|
+
Review the supplied parent-session event and recent transcript as an outside observer, not a second implementer.
|
|
82
27
|
|
|
83
|
-
Return {"action":"silent"} while work is productively advancing. Return {"action":"message","message":"..."} only when
|
|
28
|
+
Return {"action":"silent"} while work is productively advancing. Return {"action":"message","message":"..."} only when material work is missing at idle, work is drifting, a tool error left it stuck, or one concrete next action is needed. Keep guidance direct and at most three sentences. Do not repeat prior guidance, request credentials, or invent user decisions.
|
|
84
29
|
|
|
85
|
-
The goal is complete only when the requested result and
|
|
30
|
+
The goal is complete only when the requested result and relevant validation are evident. Then return {"action":"stop","message":"Goal verified complete."}.
|
|
86
31
|
```
|
|
87
32
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
The supervisor subscribes to `agent_settled` and `tool_error`, not `turn_end` or `input`. It runs at decision points (idle and on failures) and stays silent while work is productively advancing, so it does not invoke a model review on every turn. Intervene only on a concrete, high-confidence signal: material work missing at idle, drift from the goal, a stuck state after a tool error, or verified completion.
|
|
91
|
-
|
|
92
|
-
After creation, report the goal, actor short ID, and inspect/stop commands. Do not wait for it; host events drive it across later turns.
|
|
93
|
-
|
|
94
|
-
## Steering running subagents
|
|
95
|
-
|
|
96
|
-
A supervisor (or any orchestrator) can redirect a running worker without discarding its context: `agents.steer({ id, message })` delivers between the child's turns, and `agents.status({ id }).pendingMessages` shows the queued steers. See the `agents` reference. Prefer this over stopping and respawning a worker that has accumulated useful context but is drifting from the goal. Because Pi actors run with Fabric, the supervisor can call `agents.main()` plus `agents.steer`/`agents.followUp` to message the user-facing Main session, redirect a running child directly, or route to an exact agent id in another process.
|
|
33
|
+
Idle/error events avoid a model run on every turn. `triggerTurn: true` lets a material steer resume an idle Main session. Report the goal and setup result; do not wait for the actor.
|