@ssheleg/agent-stack 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
1
+ # Building on Pi — SDK, RPC, and the extension seams
2
+
3
+ **Load this when:** embedding an agent in your own process, driving one from another
4
+ language, or extending a harness — and you want a real API surface rather than a sketch.
5
+
6
+ **Spec pinned:** `@earendil-works/pi-coding-agent`, `pi.dev/docs/latest` (sdk, rpc, json, extensions, custom-provider) · read 2026-08-15
7
+
8
+ Read `pi.md` first for the harness itself. This file is the programmable half, and its
9
+ value is the **seams**: the eight or so places a real harness lets you intervene, each
10
+ matched to the rule it lets you implement.
11
+
12
+ ## Contents
13
+
14
+ - Choosing a way in
15
+ - The SDK
16
+ - Custom tools
17
+ - RPC: driving it from any language
18
+ - JSON mode, and why it differs
19
+ - The extension API
20
+ - The seams that matter
21
+ - Custom providers
22
+ - Traps
23
+
24
+ ## Choosing a way in
25
+
26
+ | You want | Use | Because |
27
+ |---|---|---|
28
+ | your process owns the loop, in Node | **SDK** | direct objects, no serialization |
29
+ | to drive an agent from Python, Go, a UI | **RPC** | JSONL over stdin/stdout, bidirectional |
30
+ | to consume a run's events, one shot | **JSON mode** | delta-only stream, linear in size |
31
+ | a scripted answer | **print** | `pi -p` |
32
+ | to change behaviour rather than call it | **an extension** | it runs inside the loop |
33
+
34
+ **The distinction people get wrong:** RPC and JSON both emit events, but only RPC accepts
35
+ commands. If you need to steer mid-run, it is RPC.
36
+
37
+ ## The SDK
38
+
39
+ ```bash
40
+ npm install @earendil-works/pi-coding-agent
41
+ ```
42
+
43
+ ```javascript
44
+ import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
45
+
46
+ const modelRuntime = await ModelRuntime.create();
47
+ const { session } = await createAgentSession({
48
+ sessionManager: SessionManager.inMemory(),
49
+ modelRuntime,
50
+ });
51
+ ```
52
+
53
+ **`AgentSession`** — `prompt(text, options?)`, `steer(text)` and `followUp(text)` to queue
54
+ during streaming, `subscribe(listener)`, `setModel()`, `setThinkingLevel()`, `compact()`,
55
+ `abort()`, `dispose()`.
56
+
57
+ **Events** arrive structured:
58
+
59
+ ```javascript
60
+ session.subscribe((event) => {
61
+ if (event.type === "message_update" &&
62
+ event.assistantMessageEvent.type === "text_delta") {
63
+ process.stdout.write(event.assistantMessageEvent.delta);
64
+ }
65
+ });
66
+ ```
67
+
68
+ **`SessionManager`** factories decide persistence: `inMemory()`, `create(cwd)`,
69
+ `continueRecent(cwd)`, `open(filePath)`. **`AgentSessionRuntime`** handles replacement —
70
+ `newSession()`, `switchSession(path)`, `fork(entryId)`.
71
+
72
+ **`createAgentSession()` options** worth knowing: `model` (from `@earendil-works/pi-ai`),
73
+ `thinkingLevel` (`"off"` … `"max"`), `tools` (names to enable), `cwd`, `agentDir`
74
+ (defaults `~/.pi/agent`), `resourceLoader`, `settingsManager`.
75
+
76
+ **`ModelRuntime`** carries credentials and availability:
77
+
78
+ ```javascript
79
+ const modelRuntime = await ModelRuntime.create({
80
+ allowModelNetwork: true,
81
+ modelRefreshTimeoutMs: 15_000,
82
+ });
83
+ await modelRuntime.setRuntimeApiKey("anthropic", "sk-key");
84
+ const available = await modelRuntime.getAvailable();
85
+ ```
86
+
87
+ Resolution: runtime overrides → `auth.json` → environment.
88
+
89
+ **`DefaultResourceLoader`** discovers extensions, skills and prompts, and is where you
90
+ override the system prompt for an embedded agent:
91
+
92
+ ```javascript
93
+ const loader = new DefaultResourceLoader({
94
+ cwd: process.cwd(),
95
+ additionalExtensionPaths: ["/path/to/extension.ts"],
96
+ systemPromptOverride: () => "Custom system prompt",
97
+ });
98
+ await loader.reload();
99
+ ```
100
+
101
+ **`allowModelNetwork` and `systemPromptOverride` are the two options an embedded agent
102
+ almost always needs** — the first because a server should not discover models at runtime
103
+ unless you meant it, the second because the default prompt is a coding agent's and yours
104
+ probably is not.
105
+
106
+ ## Custom tools
107
+
108
+ ```javascript
109
+ const myTool = defineTool({
110
+ name: "my_tool",
111
+ description: "Does something useful",
112
+ parameters: Type.Object({ input: Type.String() }),
113
+ execute: async (_id, params) => ({
114
+ content: [{ type: "text", text: `Result: ${params.input}` }],
115
+ details: {},
116
+ }),
117
+ });
118
+ ```
119
+
120
+ Passed as `customTools: [myTool]`. Built-ins: `read`, `bash`, `edit`, `write`, `grep`,
121
+ `find`, `ls`.
122
+
123
+ **`details` is not decoration.** It is how a tool result carries structured state into the
124
+ session, and Pi's own guidance is to rebuild in-memory state after a restart by walking
125
+ `ctx.sessionManager.getBranch()` and reading it. That is **structured note-taking**
126
+ (`techniques.md`) with a durable home.
127
+
128
+ Write the `description` to `tools.md`'s standard — this is the same field, and the same
129
+ leverage.
130
+
131
+ ## RPC: driving it from any language
132
+
133
+ ```bash
134
+ pi --mode rpc [--provider … --model … --name … --no-session --session-dir …]
135
+ ```
136
+
137
+ JSON Lines over stdin/stdout: **commands** in, **responses** (`type: "response"`)
138
+ acknowledging them, **events** streaming asynchronously.
139
+
140
+ > **Framing warning, quoted because it bites in exactly one language at a time:** *"Split
141
+ > records on `\n` only; accept optional `\r\n` input by stripping a trailing `\r`."* Some
142
+ > standard line readers split on Unicode separators too, and a model that emits one inside
143
+ > a string will then desynchronize your parser.
144
+
145
+ **Commands**, by group:
146
+
147
+ | Group | Commands |
148
+ |---|---|
149
+ | Prompting | `prompt`, `steer` (delivered after the current tool), `follow_up`, `abort` |
150
+ | State | `get_state`, `get_messages`, `set_model`, `cycle_model`, `set_thinking_level`, `set_steering_mode`, `set_follow_up_mode` |
151
+ | Sessions | `new_session`, `switch_session`, `fork`, `clone`, `get_session_stats`, `export_html`, `set_session_name` |
152
+ | Execution | `bash`, `compact`, `set_auto_compaction`, `set_auto_retry` |
153
+ | Introspection | `get_available_models`, `get_commands`, `get_fork_messages`, `get_entries`, `get_tree` |
154
+
155
+ **Event lifecycle**, in order:
156
+
157
+ ```
158
+ agent_start → turn_start → message_start → message_update* → message_end
159
+ → tool_execution_start → tool_execution_update* → tool_execution_end
160
+ → turn_end → agent_end → agent_settled
161
+ ```
162
+
163
+ Plus `queue_update`, `compaction_start/end`, `auto_retry_start/end`,
164
+ `bash_execution_update` (correlated by the command's `id`), and `extension_error`.
165
+
166
+ **`agent_settled` is the one to wait on, not `agent_end`** — it means no further auto-retry
167
+ is queued. A client that treats `agent_end` as final will occasionally act on a run that is
168
+ about to continue.
169
+
170
+ Message shapes are stable and worth matching: `UserMessage`, `AssistantMessage` (with
171
+ `model`, `usage`, `stopReason`), `ToolResultMessage` (`toolCallId`, `toolName`, `isError`),
172
+ `BashExecutionMessage` (`command`, `output`, `exitCode`, `cancelled`).
173
+
174
+ **Extensions can ask the user something over RPC**, which is the part most integrations
175
+ forget. `extension_ui_request` events carry `select`, `confirm`, `input`, `editor` and
176
+ expect an `extension_ui_response` with the matching `id`; `notify`, `setStatus`, `setWidget`
177
+ are fire-and-forget.
178
+
179
+ ```json
180
+ {"type": "extension_ui_request", "id": "uuid-1", "method": "select", "title": "Choose", "options": ["A", "B"]}
181
+ {"type": "extension_ui_response", "id": "uuid-1", "value": "A"}
182
+ ```
183
+
184
+ **A client that ignores these hangs the agent** whenever an extension asks a question. Not
185
+ implementing them is a decision; not knowing about them is an outage.
186
+
187
+ ## JSON mode, and why it differs
188
+
189
+ `pi --mode json "…"` streams the same lifecycle, opening with a header:
190
+
191
+ ```json
192
+ {"type":"session","version":3,"id":"uuid","timestamp":"...","cwd":"/path"}
193
+ ```
194
+
195
+ **`message_update` records are delta-only** — they omit the cumulative `message` field and
196
+ `assistantMessageEvent.partial` *"to keep stream size linear."* Consumers assemble text from
197
+ `contentIndex` and `delta`.
198
+
199
+ That is a deliberate trade: RPC gives you snapshots you can resync from, JSON gives you a
200
+ stream that does not grow quadratically. **Pick JSON for pipelines, RPC for UIs.**
201
+
202
+ ## The extension API
203
+
204
+ Auto-discovered from `~/.pi/agent/extensions/*.ts` (global), `.pi/extensions/*.ts`
205
+ (project, after trust) or an `extensions` array in settings. A file, a directory with
206
+ `index.ts`, or a package with its own `node_modules`.
207
+
208
+ ```typescript
209
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
210
+
211
+ export default function (pi: ExtensionAPI) {
212
+ // may be async — do initialization here, not resource startup
213
+ }
214
+ ```
215
+
216
+ **Registration surface:**
217
+
218
+ ```typescript
219
+ pi.registerTool({ name, label, description, promptSnippet, promptGuidelines,
220
+ parameters, prepareArguments?, execute, renderCall?, renderResult? })
221
+ pi.registerCommand(name, { description, getArgumentCompletions?, handler })
222
+ pi.registerProvider(name, config) / pi.unregisterProvider(name)
223
+ pi.registerMessageRenderer / registerEntryRenderer / registerMarkdownTransformer
224
+ pi.registerShortcut(shortcut, options) / pi.registerFlag(name, options)
225
+ pi.on(eventName, handler) / pi.events.on|emit
226
+ pi.getActiveTools() / getAllTools() / setActiveTools(names)
227
+ pi.setModel(model) / getThinkingLevel() / setThinkingLevel(level)
228
+ pi.sendMessage / sendUserMessage / appendEntry / setSessionName / setLabel
229
+ pi.exec(command, args, options?)
230
+ ```
231
+
232
+ **`promptSnippet` and `promptGuidelines` deserve attention**: a tool contributes not only a
233
+ schema but a line to the system prompt and a set of guidelines. That is `system-prompt.md`'s
234
+ *"tool policy belongs in the prompt"* built into the registration call, which is the right
235
+ place for it — the policy cannot drift from the tool because they are declared together.
236
+
237
+ **Context (`ctx`)** in every handler: `ui`, `mode` (`"tui" | "rpc" | "json" | "print"`),
238
+ `hasUI`, `cwd`, `isProjectTrusted()`, `sessionManager`, `modelRegistry`, `model`,
239
+ `thinkingLevel`, `signal`, `isIdle()`, `abort()`, `getContextUsage()`, `compact(options)`,
240
+ `getSystemPrompt()`. Commands additionally get `newSession()`, `fork()`, `navigateTree()`,
241
+ `switchSession()`, `waitForIdle()`, `reload()`.
242
+
243
+ **`ctx.mode` and `ctx.hasUI` are how an extension stays honest** across surfaces: an
244
+ extension that calls `ctx.ui.confirm()` unconditionally works in the TUI and hangs in a
245
+ pipeline unless the client implements the UI sub-protocol.
246
+
247
+ ## The seams that matter
248
+
249
+ Pi exposes ~30 events. These are the ones that let you implement doctrine this pack
250
+ otherwise only describes:
251
+
252
+ | Event | What it lets you do | Implements |
253
+ |---|---|---|
254
+ | **`tool_call`** — *can block* | refuse a call before it runs, per caller, per argument | the per-hop permission gate of `agent-orchestrator/references/governance.md`; track 6 of `audit.md` |
255
+ | **`tool_result`** — *middleware chain* | rewrite, truncate or annotate a result; handlers see the previous handler's output | tool-output offload and token efficiency (`tools.md`) |
256
+ | **`context`** | modify messages **before** the provider call | the compaction ladder's upper rungs, and typed carryover |
257
+ | **`before_agent_start`** | inject a message, modify the system prompt | capability-aware prompt assembly (`system-prompt.md`) |
258
+ | **`before_provider_headers` / `before_provider_request` / `after_provider_response`** | mutate headers, inspect or replace the payload, handle the response | provider routing, proxying and cost attribution |
259
+ | **`session_before_compact` / `session_compact`** | decide what survives | *preserve decisions and open questions, not the discussion* |
260
+ | **`input`** — *can intercept* | rewrite or absorb a user message | routing before the loop |
261
+ | **`resources_discover`** | add skill/prompt/theme paths at runtime | dynamic capability |
262
+ | **`project_trust`** | act on the trust decision | the delegation boundary of `layers.md` |
263
+
264
+ **`tool_call` blocking is the single most important one for an audit.** It is where a
265
+ per-tool, per-caller policy can actually live in a Pi-based system — so its absence is a
266
+ finding, and its presence is where you read the policy.
267
+
268
+ **Lifecycle discipline**, from Pi's own guidance and worth generalizing: start background
269
+ resources in `session_start` and clean up in `session_shutdown`, **never from the factory**;
270
+ after `/new`, `/resume` or `/fork` a fresh context arrives and **stale `ctx` objects must not
271
+ be reused**; and tools that mutate files should use `withFileMutationQueue()` to avoid
272
+ racing the built-ins.
273
+
274
+ ## Custom providers
275
+
276
+ ```typescript
277
+ // route an existing provider through a proxy — baseUrl and/or headers only,
278
+ // and the existing model list is preserved
279
+ pi.registerProvider("anthropic", { baseUrl: "https://proxy.example.com" });
280
+
281
+ // or a whole new one
282
+ pi.registerProvider("my-llm", {
283
+ baseUrl: "https://api.my-llm.com/v1",
284
+ apiKey: "$MY_LLM_API_KEY",
285
+ api: "openai-completions",
286
+ models: [{
287
+ id: "my-llm-large", name: "My LLM Large", reasoning: true,
288
+ input: ["text", "image"],
289
+ cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
290
+ contextWindow: 200000, maxTokens: 16384,
291
+ }],
292
+ });
293
+ ```
294
+
295
+ For a non-standard API, implement `streamSimple`, pushing an `AssistantMessageEventStream`:
296
+ start → content (text, thinking blocks, tool calls) → done or error, updating usage and cost.
297
+
298
+ **The `cost` block is the hook for everything in
299
+ `agent-orchestrator/references/llm-proxy-billing.md`.** A provider that declares its per-token
300
+ cost makes attribution arithmetic rather than estimation — and a custom provider that omits
301
+ it silently makes every downstream number a guess.
302
+
303
+ Auth supports API keys with env interpolation, and OAuth with refresh, browser and
304
+ device-code flows.
305
+
306
+ ## Traps
307
+
308
+ - **Waiting on `agent_end` instead of `agent_settled`**, and acting on a run that continues.
309
+ - **Ignoring `extension_ui_request`** in a non-TUI client, and hanging the first time an
310
+ extension asks a question.
311
+ - **Splitting JSONL on anything but `\n`.** The docs warn about it; the failure is rare,
312
+ data-dependent and looks like corruption.
313
+ - **Reusing a `ctx` after a session replacement.** It points at the old session.
314
+ - **Starting background work in the extension factory** rather than `session_start`, so it
315
+ outlives the session and doubles on reload.
316
+ - **Registering a custom provider with no `cost`**, then trusting the spend numbers.
317
+ - **Assuming an extension is a boundary.** It runs in the Pi process, with the Pi process's
318
+ permissions — see `pi.md` → *trust*.
@@ -0,0 +1,241 @@
1
+ # Pi — a harness you can read, and what each of its parts implements
2
+
3
+ **Load this when:** you want a **worked example** of the harness doctrine, are choosing a
4
+ kernel to build on, or are auditing a system built on Pi.
5
+
6
+ **Spec pinned:** Pi (`@earendil-works/pi-coding-agent`, MIT, Earendil Inc.), `pi.dev/docs/latest` · read 2026-08-15
7
+
8
+ **Why this file exists.** Everything else in this skill states a rule; Pi is small enough
9
+ to read and complete enough to have made every one of those decisions in public. So each
10
+ section below says **what Pi does** and then **which rule it is an instance of** — the
11
+ value is in the second half. Where Pi disagrees with the doctrine, that is said too.
12
+
13
+ This is not a substitute for `pi.dev`. It moves faster than this file; the stamp above is
14
+ the honest boundary.
15
+
16
+ ## Contents
17
+
18
+ - What Pi is, and the stance underneath it
19
+ - Four ways to run it
20
+ - Sessions are a tree, not a log
21
+ - Compaction, with the actual numbers
22
+ - Configuration and precedence
23
+ - Skills, prompts and packages
24
+ - Trust, and the deliberate absence of a sandbox
25
+ - Containerization — three patterns, three threat models
26
+ - Providers and credentials
27
+ - Where Pi and this pack's doctrine differ
28
+ - Traps
29
+
30
+ ## What Pi is, and the stance underneath it
31
+
32
+ *"A minimal agent harness."* Its stated position is **primitives, not features**: it ships
33
+ `read`, `write`, `edit`, `bash` and a loop, and deliberately omits sub-agents and plan mode,
34
+ expecting you to build them as extensions.
35
+
36
+ **This is the kernel layer of `layers.md`, made concrete.** The omissions are the argument:
37
+ a kernel that shipped a plan mode would have chosen your planning shape for you. When
38
+ comparing Pi against a workbench, remember the comparison is across layers and will not
39
+ converge.
40
+
41
+ ## Four ways to run it
42
+
43
+ | Mode | Invocation | For |
44
+ |---|---|---|
45
+ | **Interactive TUI** | `pi` | a human at a terminal |
46
+ | **Print** | `pi -p "…"` | one shot, text out |
47
+ | **JSON** | `pi --mode json "…"` | events as JSON lines, for another tool's UI |
48
+ | **RPC** | `pi --mode rpc` | a long-lived subprocess you drive both ways |
49
+ | **Embedded** | the SDK | your process owns the loop |
50
+
51
+ **One agent, five front doors.** That separation — a core that does not know which surface
52
+ is attached — is the same shape `agent-orchestrator` describes when it insists the loop
53
+ must not know which provider answered. Details of the last three: `pi-sdk.md`.
54
+
55
+ ## Sessions are a tree, not a log
56
+
57
+ Sessions persist to `~/.pi/agent/sessions/` as **JSONL, one entry per line**, each carrying
58
+ an 8-character hex `id` and a `parentId`. The current position is a leaf; context is built
59
+ by walking leaf→root.
60
+
61
+ | Entry type | Holds |
62
+ |---|---|
63
+ | `session` | the header: `version` (currently **3**), `id`, `timestamp`, `cwd` |
64
+ | `SessionMessageEntry` | a message with its role and content |
65
+ | `ModelChangeEntry` / `ThinkingLevelChangeEntry` | mid-conversation switches, recorded rather than implied |
66
+ | `CompactionEntry` | a summary, with an optional `retainedTail` |
67
+ | `BranchSummaryEntry` | what an abandoned branch was about |
68
+ | `CustomEntry` / `CustomMessageEntry` | extension data — the second participates in context, the first does not |
69
+
70
+ Commands: `/tree` navigates within one file, `/fork` starts a new session from an earlier
71
+ prompt, `/clone` duplicates the active branch, `/export` writes HTML, `/share` uploads a
72
+ private gist. Flags: `pi -c` continues, `pi -r` browses, `--no-session` keeps nothing.
73
+
74
+ **This implements `agent-orchestrator/references/runtime.md` → *time travel and forking*.**
75
+ That file argues you must be able to fork a past checkpoint and debug **through the real
76
+ loop** rather than a reconstruction. A parent-pointer tree is what makes that cheap: no
77
+ copy, no replay, and the abandoned branch leaves a `BranchSummaryEntry` behind so the
78
+ context is not simply lost.
79
+
80
+ **Two design details worth stealing.** Model and thinking-level changes are *entries*, so a
81
+ session explains its own cost curve. And the version field is honest about migration —
82
+ v1 was linear, v2 introduced the tree, v3 unified role naming.
83
+
84
+ ## Compaction, with the actual numbers
85
+
86
+ Auto-compaction fires when `contextTokens > contextWindow - reserveTokens`.
87
+
88
+ | Setting | Default | Meaning |
89
+ |---|---|---|
90
+ | `reserveTokens` | 16,384 | held back for the response |
91
+ | `keepRecentTokens` | 20,000 | recent tail never summarized |
92
+
93
+ Preserved messages run from `firstKeptEntryId` onward and are sent alongside the summary.
94
+ `/compact [instructions]` runs it manually and the instructions steer the summary. Setting
95
+ `"enabled": false` disables the automatic path while leaving the manual one.
96
+
97
+ **Two caveats stated in the docs and worth carrying:** tool results are **truncated to 2,000
98
+ characters** while summarizing, and a turn larger than `keepRecentTokens` produces two
99
+ summaries that are then merged.
100
+
101
+ **This is the ladder from `agent-orchestrator/references/context-engineering.md` with one
102
+ rung.** Pi reserves, keeps a tail, and summarizes the rest. What that file adds and Pi
103
+ leaves to you: clearing old tool results *before* paying a summarizer, offloading a large
104
+ tool result to a file and keeping the path, and **typed carryover** — the observation that a
105
+ summarizer keeps the discussion and drops the state. Pi's `BranchSummaryEntry` and
106
+ `retainedTail` are the seams to hang that on.
107
+
108
+ ## Configuration and precedence
109
+
110
+ | File | Scope |
111
+ |---|---|
112
+ | `~/.pi/agent/settings.json` | global |
113
+ | `.pi/settings.json` | project — **overrides global, merging nested objects** |
114
+
115
+ Keys cluster into model and thinking (`defaultProvider`, `defaultModel`,
116
+ `defaultThinkingLevel`, `thinkingBudgets`), UI, network and retry (`retry` with `enabled`,
117
+ `maxRetries`, `baseDelayMs`; `httpProxy`, `transport`, timeouts), content handling
118
+ (`shellPath`, `npmCommand`, `defaultTools`), and resources (`packages`, `extensions`,
119
+ `skills`, `prompts`, `themes`).
120
+
121
+ **Merge, not replace, is the part that matters.** A project that wants one different model
122
+ should not have to restate the whole file — and a harness that replaced wholesale would
123
+ make every project config a copy that drifts.
124
+
125
+ ## Skills, prompts and packages
126
+
127
+ **Pi implements the Agent Skills standard**, with progressive disclosure: at startup it
128
+ scans skill locations and takes only `name` and `description` into the system prompt as XML;
129
+ the full `SKILL.md` loads when a task matches.
130
+
131
+ It discovers skills from `~/.pi/agent/skills/`, **`~/.agents/skills/`**, `.pi/skills/` and
132
+ `.agents/skills/` (project paths only after the project is trusted), from packages, from a
133
+ `skills` array in settings, and from `--skill <path>`.
134
+
135
+ > **Concretely relevant here: `~/.agents/skills/` is the ssheleg hub.** On the machine this
136
+ > file was written on, that directory holds 72 entries including every family skill, each
137
+ > with the `name` and `description` front matter Pi requires — so the family is already in a
138
+ > directory Pi reads. **Not verified by running Pi**, which is not installed here; this is a
139
+ > statement about the path and the front matter, not an observation of a load.
140
+
141
+ **Pi documents one deliberate divergence from the standard:** it allows a skill's `name` to
142
+ differ from its directory, calling that rule *"suboptimal for shared skill directories used
143
+ across multiple agent harnesses."* Which is exactly what `~/.agents/skills/` is. Note the
144
+ asymmetry before relying on it — `make-skill`'s validator enforces the strict rule, so a
145
+ skill built to Pi's leniency fails the family gate.
146
+
147
+ **Prompt templates** are Markdown in `~/.pi/agent/prompts/*.md`; the filename becomes the
148
+ command (`review.md` → `/review`). Front matter takes `description` and `argument-hint`
149
+ (`<required>`, `[optional]`). Arguments substitute as `$1`, `$@` / `$ARGUMENTS`,
150
+ `${1:-default}`, `${@:N}` and `${@:N:L}`. Discovery is **not recursive**.
151
+
152
+ **Packages** bundle extensions, skills, prompts and themes over npm or git, declared under a
153
+ `pi` key in `package.json` or by convention (`extensions/`, `skills/`, `prompts/`,
154
+ `themes/`). Installed with `pi install npm:@foo/bar@1.0.0`, `git:…`, an https URL, or a
155
+ path; `-l` writes to project settings for a team. Resource lists take globs with `!`
156
+ exclusions, `[]` for none, `+path` / `-path` to force.
157
+
158
+ ## Trust, and the deliberate absence of a sandbox
159
+
160
+ Pi *"runs with the permissions of the user account that starts it"* and treats files that
161
+ user can write as inside the same trust boundary.
162
+
163
+ **Project trust** is asked for when a repository carries `.pi/settings.json`, local
164
+ extensions, skills, prompts or themes, a `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`, or
165
+ project agent skills in ancestor directories. Decisions persist in `~/.pi/agent/trust.json`.
166
+
167
+ What it buys, in the docs' own words: it *"prevents a repository from silently changing pi's
168
+ settings or extensions before you approve it"* — and explicitly **does not** protect against
169
+ untrusted code, prompts, or model output.
170
+
171
+ **There is no built-in sandbox, on purpose.** The stated reasons: a partial in-process
172
+ sandbox creates false assumptions, real isolation needs an OS or container boundary, and Pi
173
+ is meant to invoke project toolchains with full local access. And the sentence worth
174
+ quoting to anyone who claims otherwise about any harness:
175
+
176
+ > *"prompt injection from repository files, comments, documentation, context files, or build
177
+ > output is expected local-agent risk and cannot be reliably prevented by pi."*
178
+
179
+ **This is `layers.md` → *what a harness should delegate*, stated by the project itself.** A
180
+ harness that also claimed to be a sandbox would be claiming a guarantee it cannot keep from
181
+ inside the same process. **For an audit this changes the finding**: "no permission model" is
182
+ not a defect here, it is a delegation — so audit what surrounds the process (`audit.md`,
183
+ track 6).
184
+
185
+ ## Containerization — three patterns, three threat models
186
+
187
+ | Pattern | Isolates | Credentials live |
188
+ |---|---|---|
189
+ | **Gondolin extension** | built-in tools and `!` commands, in a micro-VM | on the host — auth never enters the boundary |
190
+ | **Plain Docker** | the whole Pi process | **inside the container** |
191
+ | **OpenShell** | filesystem, process, network, credentials by policy | per policy; local or remote gateway |
192
+
193
+ ```bash
194
+ docker run --rm -it -e ANTHROPIC_API_KEY -v "$PWD:/workspace" pi-sandbox
195
+ ```
196
+
197
+ **The distinction that decides which you want: extensions execute wherever the Pi process
198
+ runs.** Host-side Pi routing tools into a micro-VM keeps auth local and isolates execution;
199
+ containerized Pi needs the key inside the boundary. Mounting `/root/.pi/agent` as a named
200
+ volume keeps settings isolated — mounting your host directory *"exposes host auth and
201
+ session files to the container"*, which is the opposite of the intent.
202
+
203
+ ## Providers and credentials
204
+
205
+ Two paths: **subscription OAuth** via `/login` (ChatGPT Plus/Pro, Claude Pro/Max, GitHub
206
+ Copilot, xAI, OpenRouter, Radius) with refresh handled, and **API keys**. 30+ providers.
207
+
208
+ Resolution order — **CLI `--api-key` → `auth.json` → environment variable → custom provider
209
+ keys in `models.json`**. `auth.json` is written `0600` and takes priority over the
210
+ environment, which is the ordering you want: an explicit file beats an inherited variable.
211
+
212
+ Keys support literals, `$ENV_VAR` interpolation, and shell commands
213
+ (`!security find-generic-password …`) — so a key can live in a system keychain rather than a
214
+ file. Registering a custom provider is `pi-sdk.md`.
215
+
216
+ ## Where Pi and this pack's doctrine differ
217
+
218
+ Named rather than smoothed over:
219
+
220
+ - **No iteration guard is documented as a first-class setting.** `agent-orchestrator` treats
221
+ a bounded loop as non-negotiable. Pi has `auto_retry` and abort; a max-iteration ceiling
222
+ is yours to add. Check this first when auditing a Pi-based system.
223
+ - **No sub-agents.** Deliberate. `agent-orchestrator`'s sub-agent protocol and
224
+ `techniques.md`'s "distilled summary, never a transcript" are things you build here.
225
+ - **Skill naming leniency** contradicts the standard `make-skill` enforces (above).
226
+ - **Compaction is one rung**, not the ladder.
227
+
228
+ None of these is a defect in a kernel. They are the difference between a harness and a
229
+ platform, and they are the work you are signing up for.
230
+
231
+ ## Traps
232
+
233
+ - **Reading the omissions as gaps.** They are the layer boundary. If you need all of them
234
+ filled, you wanted a workbench.
235
+ - **Assuming trust means safety.** It means the repository did not silently change your
236
+ configuration. Nothing more, and the docs say so.
237
+ - **Mounting your host `~/.pi/agent` into a container** and calling the result isolated.
238
+ - **Building on skill-name leniency** and then failing a stricter harness's validator.
239
+ - **Forgetting `keepRecentTokens` against a large turn.** One turn bigger than the budget
240
+ becomes two summaries merged — surprising if you are diffing summaries.
241
+ - **Treating the docs here as current.** Check the stamp; Pi moves.