pi-session-mcp 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.pi-session-mcp-cli/config.js +47 -7
  2. package/.pi-session-mcp-cli/doctor.js +75 -2
  3. package/.pi-session-mcp-cli/execution-profile.js +1 -0
  4. package/.pi-session-mcp-cli/override-lock.js +125 -0
  5. package/.pi-session-mcp-cli/setup-platform.js +195 -0
  6. package/.pi-session-mcp-cli/setup-result.js +8 -0
  7. package/.pi-session-mcp-cli/skill-catalog.js +35 -0
  8. package/.pi-session-mcp-cli/skill-customize.js +268 -0
  9. package/.pi-session-mcp-cli/skill-library.js +188 -0
  10. package/CHANGELOG.md +176 -0
  11. package/README.md +15 -3
  12. package/dist/capabilities.d.ts +1 -0
  13. package/dist/capabilities.js +2 -1
  14. package/dist/client-adapters/mcp-verifier.js +6 -2
  15. package/dist/config.d.ts +4 -0
  16. package/dist/config.js +47 -7
  17. package/dist/doctor.d.ts +7 -1
  18. package/dist/doctor.js +75 -2
  19. package/dist/execution-profile.d.ts +4 -0
  20. package/dist/execution-profile.js +1 -0
  21. package/dist/override-lock.d.ts +34 -0
  22. package/dist/override-lock.js +125 -0
  23. package/dist/pi-adapter.d.ts +7 -5
  24. package/dist/pi-adapter.js +1 -0
  25. package/dist/pi-skills.d.ts +17 -0
  26. package/dist/pi-skills.js +64 -0
  27. package/dist/sdk-pi-adapter.d.ts +7 -3
  28. package/dist/sdk-pi-adapter.js +82 -16
  29. package/dist/server.js +1 -0
  30. package/dist/session-registry.d.ts +8 -6
  31. package/dist/session-registry.js +3 -2
  32. package/dist/setup-cli.d.ts +2 -0
  33. package/dist/setup-cli.js +16 -0
  34. package/dist/setup-main.d.ts +1 -1
  35. package/dist/setup-main.js +18 -3
  36. package/dist/setup-ownership.d.ts +9 -0
  37. package/dist/setup-ownership.js +32 -5
  38. package/dist/setup-skills.d.ts +76 -0
  39. package/dist/setup-skills.js +586 -0
  40. package/dist/setup.d.ts +7 -0
  41. package/dist/setup.js +49 -6
  42. package/dist/skill-catalog.d.ts +26 -0
  43. package/dist/skill-catalog.js +35 -0
  44. package/dist/skill-customize.d.ts +41 -0
  45. package/dist/skill-customize.js +268 -0
  46. package/dist/skill-library.d.ts +106 -0
  47. package/dist/skill-library.js +188 -0
  48. package/docs/acceptance.md +593 -1
  49. package/docs/adr/0006-pi-extension-allowlist.md +3 -0
  50. package/docs/adr/0007-profile-skill-selection.md +228 -0
  51. package/docs/architecture.md +10 -6
  52. package/docs/client-setup-design.md +219 -1
  53. package/docs/client-setup.md +86 -3
  54. package/docs/doctor-design.md +9 -1
  55. package/docs/installation.md +128 -5
  56. package/docs/pi-extensions-design.md +28 -4
  57. package/docs/releasing.md +18 -9
  58. package/docs/session-mcp.md +7 -2
  59. package/docs/skills.md +616 -0
  60. package/docs/threat-model.md +19 -2
  61. package/docs/tool-contracts.md +9 -5
  62. package/package.json +2 -1
  63. package/pi-session-mcp.example.json +9 -3
  64. package/skills/pi-session-implementation/SKILL.md +42 -0
  65. package/skills/pi-session-orchestration/SKILL.md +94 -0
  66. package/skills/pi-session-review/SKILL.md +58 -0
@@ -0,0 +1,228 @@
1
+ # ADR 0007: Execution profiles select Pi skills from an operator catalog
2
+
3
+ **Status:** Accepted (2026-10-12). Issue #46. Guide: [Pi skills in execution
4
+ profiles](../skills.md).
5
+
6
+ ## Context
7
+
8
+ Embedded sessions are meant to run without skills. A controlling agent that
9
+ delegates work to Pi workers needs a way to give a worker role-specific
10
+ instructions, such as an implementation or review procedure, under the same
11
+ rule as everything else: the operator decides, the MCP caller picks aliases.
12
+
13
+ Verified behavior of Pi SDK 1.1.0:
14
+
15
+ - `noSkills` does not stop skills that an allowlisted extension package
16
+ ships. Those skills reached the session although `noSkills` was set. The
17
+ same held for prompt templates and themes that packages ship. ADR 0006 did
18
+ not intend this and did not decide it.
19
+ - `loadSkills` removes duplicate skill names before the SDK's `skillsOverride`
20
+ runs. An override alone therefore cannot report a collision between two
21
+ selected files, so the server has to check names itself.
22
+ - Resources that extension hooks add later (`resources_discover`) pass through
23
+ the same override.
24
+
25
+ ADR 0003 chose maintenance without a new feature phase and reserves public
26
+ contract changes for a minor release with their own decision. ADR 0006 is the
27
+ precedent for an owner-approved scope expansion. This ADR is such a decision
28
+ for issue #46. It does not rewrite ADR 0006; that ADR records only a forward
29
+ link.
30
+
31
+ On 2026-10-12 the owner rejected installing the shipped skills by manual
32
+ symlinking and asked for skills that can be installed at install time and
33
+ adjusted and extended one by one. The decisions under "Skill library,
34
+ installation and customization" below record that, in the same change.
35
+
36
+ ## Decision
37
+
38
+ The operator may register skills in a catalog and select them per execution
39
+ profile.
40
+
41
+ - **Catalog.** An optional top-level `skillCatalog` maps a skill alias
42
+ (`^[a-z][a-z0-9_-]{0,63}$`, at most 64 entries) to the path of exactly one
43
+ `SKILL.md` (at most 4096 characters). Directories, globs, URLs,
44
+ scheme-prefixed sources, `~` and backslashes are invalid. The one accepted
45
+ scheme is `builtin:<name>` (see below). Relative paths resolve against the
46
+ configuration file's directory. A missing catalog is empty. A skill folder
47
+ needs no `package.json` and no extension.
48
+ - **Selection.** An optional `executionProfiles.<alias>.skills` lists catalog
49
+ aliases (at most 16, no duplicates). Missing or empty means no skills. There
50
+ is no inheritance and no default skill. An unknown alias, a duplicate alias,
51
+ or two selected aliases that resolve to the same path is
52
+ `invalid_execution_profile_config`. All other profile fields are unchanged
53
+ and unknown fields are still rejected.
54
+ - **MCP surface.** Callers still choose only workspace and profile aliases.
55
+ `pi_session_start` and `pi_session_prompt` get no skill input. No tool is
56
+ added.
57
+ - **Loading.** At session start the server loads exactly the selected files
58
+ through the SDK's public `loadSkills`, with no default directories, and
59
+ validates them completely. `skillsOverride` returns exactly that set, also
60
+ after later resource additions by extension hooks.
61
+ - **Failure.** A missing, unreadable or oversized (over 256 KiB) file, an
62
+ invalid skill (SDK diagnostics such as a missing description or invalid
63
+ name), two selected files with the same Pi skill name, two aliases on the
64
+ same canonical file (for example through a symlink), or
65
+ `disable-model-invocation: true` fail the start with the new error
66
+ `skill_load_failed` and the fixed message `Configured Pi skill could not be
67
+ loaded`. The message never contains a path, a skill name or SDK text. The
68
+ same Pi skill name in different sessions is fine.
69
+ - **Explicit-only skills are incompatible in v1.** MCP prompts never run
70
+ `/skill:` commands, and Pi hides such skills from the model. The server does
71
+ not rewrite the flag.
72
+ - **Names.** The catalog alias may differ from the skill's declared `name`.
73
+ Nothing is renamed.
74
+ - **Capabilities.** Every execution profile in `pi_capabilities_get` gains
75
+ `skills: string[]`, the selected catalog aliases in configured order, `[]`
76
+ when none. No paths, file names, contents or SDK diagnostics. The setup
77
+ verifier accepts the field and still rejects unexpected fields.
78
+ - **Doctor.** Offline, it never parses or executes a skill. New check IDs after
79
+ the extension checks, with the subject `<profile>:<skill-alias>`:
80
+ `skill_missing`, `skill_unreadable` (not a regular file, over 256 KiB, or not
81
+ readable) and `skill_duplicate`. Frontmatter, name rules, collisions and
82
+ explicit-only flags are checked only at session start. A further warning
83
+ check, `skill_override_outdated`, is described below.
84
+ - **Diagnostics.** Stderr gets the new stage `skill_load` for a selected file
85
+ that fails validation, with an optional `skill` field holding the catalog
86
+ alias. A catalog that differs from the selection after construction reports
87
+ the existing stages `postcheck`, `extension_load` or `extension_bind`. No
88
+ diagnostic carries a path.
89
+ - **Snapshot semantics.** Profile and skill selection are fixed per session.
90
+ The content is not snapshotted: the SDK keeps file paths and later reads may
91
+ see changed files. Operators manage skill files and companion files, do not
92
+ change them during active sessions, and close affected sessions before an
93
+ update. The configuration fingerprint covers configuration bytes, not skill
94
+ contents. There is no hot reload, watcher, copy store for sessions or
95
+ versioning.
96
+ - **Shipped skills.** The package ships `pi-session-orchestration`,
97
+ `pi-session-implementation` and `pi-session-review` under `skills/`, one
98
+ canonical copy each. They are ordinary skill files. The two worker skills are
99
+ registered through the catalog like any operator skill, by `builtin:`. The
100
+ orchestration skill is for the controlling client agent and is not a Pi
101
+ worker skill.
102
+
103
+ ## Skill library, installation and customization
104
+
105
+ Owner decision of 2026-10-12, same issue. The first design of this change had
106
+ the operator register shipped skills by absolute path and link the
107
+ orchestration skill into the clients by hand; the owner rejected that. Three
108
+ decisions were taken:
109
+ installation through `pi-session-mcp-setup --apply`, an override directory for
110
+ customization, and `builtin:` references in the Pi skill catalog.
111
+
112
+ - **Skill library.** Shipped skills live in the package's `skills/` directory.
113
+ The operator's override directory is `skills/` next to the configuration file
114
+ (for the default configuration `~/.config/pi-session-mcp/skills/<name>/SKILL.md`).
115
+ An override with the same name replaces the shipped skill; a new name adds a
116
+ skill. Names follow the Agent Skills rule: lowercase letters and digits in
117
+ hyphen-separated groups, 1 to 64 characters. Nothing else is searched.
118
+ - **`builtin:<name>`.** A `skillCatalog` value resolves to the override if
119
+ `<override dir>/<name>/SKILL.md` exists, else to the shipped skill. A missing
120
+ skill keeps the shipped location, so Doctor reports `skill_missing` and the
121
+ session start fails with `skill_load_failed`. Resolution happens when the
122
+ server loads the configuration: adding or removing an override needs a
123
+ restart, like any configuration change. Edits to an existing skill file reach
124
+ new sessions under the snapshot semantics above. The example configuration
125
+ uses `builtin:pi-session-implementation` and `builtin:pi-session-review`, so a
126
+ copied example needs no path edits.
127
+ - **Customization.** `pi-session-mcp-setup --customize-skill <name>` (a
128
+ dry-run) and `--apply --customize-skill <name>` copy a shipped skill into the
129
+ override directory as a starting point and record the shipped fingerprint in
130
+ `<override dir>/.pi-session-mcp-bases.json`. An existing override is never
131
+ overwritten. When the shipped skill changed after the copy, or no base is
132
+ recorded, the command reports `skill_override_outdated`; `--apply` then moves
133
+ only the recorded base (`skill_base_updated`) and never touches the
134
+ operator's files, which is how an operator acknowledges a reviewed change.
135
+ The finding target is `{ client: "skills", scope: "override", alias: <name> }`.
136
+ It works without `--target`, requires `PI_SESSION_MCP_CONFIG`, and follows
137
+ the platform rule of setup. A completely new skill needs no command: create
138
+ `<override dir>/<new-name>/SKILL.md` and reference it as `builtin:<new-name>`.
139
+ - **Installation of the controller skill.** `setup --apply` installs
140
+ `pi-session-orchestration` for each target, after the registration, as a real
141
+ copy of the effective skill (override, else shipped). Codex `codex:user` goes
142
+ to `~/.agents/skills/`, Claude Code `user` and `local` share
143
+ `~/.claude/skills/`, and Claude Code `project` goes to
144
+ `<project>/.claude/skills/`, the directory setup runs in. Ownership records
145
+ hold only a content fingerprint and, while an update is pending, the
146
+ fingerprint it replaces; a project record is bound to an opaque project
147
+ identity, never a path. Setup never replaces or deletes a modified installed
148
+ copy, hidden operator entries inside it, a foreign skill of the same name or a
149
+ linked or non-directory entry; it reports `skill_divergent`, which does not
150
+ block the registration. A link to exactly the effective skill is the
151
+ operator's own equivalent setup (`skill_linked`) and is never owned. In the home directory the client's skills
152
+ directory may be the operator's own link and is followed. Inside a project,
153
+ which may be untrusted, any link below the project root makes the slot
154
+ divergent, so setup never writes outside the project. Setup installs only
155
+ `pi-session-orchestration` into clients; the worker skills are for Pi
156
+ profiles through `builtin:`, and controller skills of the operator's own
157
+ belong in the clients' native skill directories. The operator customizes the
158
+ controller skill through the override and runs `--apply` again, and never
159
+ edits the installed copy.
160
+ - **Doctor.** The warning check `skill_override_outdated` has the subject
161
+ `skill:<name>` when a shipped skill's current fingerprint differs from the
162
+ recorded base, with a remediation that names the customize command. A
163
+ corrupt bases file reports the subject `skill:bases`. Doctor stays offline
164
+ and executes nothing.
165
+ - **Fallback.** Where setup does not run, the operator copies the controller
166
+ skill directory into the client's documented skill directory. There is no
167
+ symlink instruction.
168
+
169
+ ## Compatibility change
170
+
171
+ Previously, with Pi SDK 1.1.0, an allowlisted extension package's bundled
172
+ skills, prompt templates and themes reached the session although `noSkills`
173
+ was set. Now an extension allowlist alone makes none of its skills available.
174
+ A skill is available only through the catalog plus the profile's explicit
175
+ selection.
176
+
177
+ Migration: add a catalog entry with the absolute path to the package's
178
+ `SKILL.md` inside Pi's install (normally under
179
+ `<Pi agent dir>/npm/node_modules/<package>/…`; `~` is not expanded) and select
180
+ it in the profile. Prompt templates and themes that packages ship are now also
181
+ closed through the SDK's public overrides. They were already never expanded
182
+ for MCP prompts. This does not mean every previous profile behaves unchanged:
183
+ a profile that used a package's skills needs the migration above.
184
+
185
+ ## Consequences
186
+
187
+ - Skills are operator-managed prompt content. In `coding` profiles a skill's
188
+ instructions are executed with the profile's tools. A skill grants no rights.
189
+ - Skill selection limits the regular skill catalog only. An allowlisted
190
+ extension stays trusted executable code and can still read files or inject
191
+ prompt text. This is no sandbox.
192
+ - Three states stay distinct: configured (capabilities list the alias; intent
193
+ only), available in the SDK catalog (validated at session start and listed
194
+ to the model in its system prompt), and actually read by the model (the model
195
+ decides; not guaranteed). Task success is separate again.
196
+ - Skill files can change under a running session. Operators carry that
197
+ discipline; the server does not enforce it.
198
+ - Setup now writes into client skill directories, in the home directory and in
199
+ a project. It copies only from the package or the operator's override
200
+ directory, never follows a link inside a project, and never replaces what it
201
+ did not install or what the operator modified. The override directory and its
202
+ bases file are operator-trusted configuration, not a workspace. Installed
203
+ skill content becomes instructions for the controlling agent.
204
+ - The server needs a name check of its own for selected files, because
205
+ `loadSkills` dedupes by name before `skillsOverride`.
206
+ - Sessions of profiles with extensions each have their own model runtime
207
+ (ADR 0006). Skill loading does not change that.
208
+
209
+ ## Versioning
210
+
211
+ ADR 0003 requires a minor release for a public contract change. The new
212
+ `skills` capability field and the new `skill_load_failed` error code are such a
213
+ change, and so are the new setup findings, the `--customize-skill` option and
214
+ the controller skill that `setup --apply` installs, so the next release after
215
+ 0.7.1 is 0.8.0. The change is recorded under
216
+ `## Unreleased` in the changelog. Implementing it does not itself release or
217
+ bump the version.
218
+
219
+ ## Note of 2026-10-12: fingerprints, locking and recovery after 0.8.0
220
+
221
+ The decisions above stand. Three details changed after 0.8.0 (issues #50, #51
222
+ and #52): skill tree fingerprints are version 2 (`v2:` values; a 0.8.0 value
223
+ counts only when a trusted reference proves it), `--apply --customize-skill`
224
+ runs under a kernel lock on the override directory and writes the bases file as
225
+ schema version 2, and an interrupted update of an installed copy is settled by
226
+ the next `--apply`, `--remove` or `--rollback`. See [Pi
227
+ skills](../skills.md#upgrade-from-080) and the [setup lifecycle
228
+ design](../client-setup-design.md#skill-installation).
@@ -47,13 +47,13 @@ reconciled only when the registration still matches that binding. A changed
47
47
  release fails closed and cannot silently inherit or replace another release's
48
48
  registration.
49
49
 
50
- `pi_capabilities_get` projects the validated workspace and execution-profile maps plus the immutable configuration metadata captured while loading the file. It returns aliases plus the configured permission/provider/model/thinking intent, sorted by alias, and `configuration: { fingerprint, reloadPolicy: "restart-required" }`. The fingerprint is SHA-256 over the exact loaded file bytes, including formatting; it is not a current-file freshness check. The projection makes no provider, authentication, filesystem, network, server-reachability, or catalog-state probe, so it cannot assert availability.
50
+ `pi_capabilities_get` projects the validated workspace and execution-profile maps plus the immutable configuration metadata captured while loading the file. It returns aliases plus the configured permission/provider/model/thinking intent, the configured extension specs and the selected skill aliases, sorted by alias, and `configuration: { fingerprint, reloadPolicy: "restart-required" }`. The fingerprint is SHA-256 over the exact loaded file bytes, including formatting; it is not a current-file freshness check. The projection makes no provider, authentication, filesystem, network, server-reachability, or catalog-state probe, so it cannot assert availability.
51
51
 
52
52
  ## Deterministic Pi selection
53
53
 
54
- The SDK adapter owns one durable `ModelRuntime`. For the configured profile, it uses that same runtime to call `getProvider(providerId)`, `getModel(providerId, modelId)`, `checkAuth()`, `getAuth(model)`, and `getAvailable(providerId)`. It rejects an absent provider or model, missing or unavailable local authentication, and a model not present in the local available-model set before constructing a session. Authentication uses only Pi's existing local mechanism; auth objects are presence-checked and never retained, serialized, or logged.
54
+ The SDK adapter owns one durable `ModelRuntime` for profiles without extensions. (A profile with `extensions` creates a fresh runtime for each session; see [Pi extensions design](pi-extensions-design.md).) For the configured profile, it uses that runtime to call `getProvider(providerId)`, `getModel(providerId, modelId)`, `checkAuth()`, `getAuth(model)`, and `getAvailable(providerId)`. It rejects an absent provider or model, missing or unavailable local authentication, and a model not present in the local available-model set before constructing a session. Authentication uses only Pi's existing local mechanism; auth objects are presence-checked and never retained, serialized, or logged.
55
55
 
56
- Only then does the adapter call `createAgentSession` with the same runtime and explicit `model`, `thinkingLevel`, and permission-derived `tools`. It never calls `resolveCliModel` or a fuzzy resolver, and does not depend on SDK settings or first-available fallback behavior. After construction it verifies `session.model.provider`, `session.model.id`, and effective `session.thinkingLevel`. A clamped thinking level or any selection mismatch fails closed. Any post-creation failure disposes the SDK session before a handle can enter the registry.
56
+ Only then does the adapter call `createAgentSession` with that runtime and explicit `model`, `thinkingLevel`, and permission-derived `tools`. It never calls `resolveCliModel` or a fuzzy resolver, and does not depend on SDK settings or first-available fallback behavior. After construction it verifies `session.model.provider`, `session.model.id`, and effective `session.thinkingLevel`. A clamped thinking level or any selection mismatch fails closed. Any post-creation failure disposes the SDK session before a handle can enter the registry.
57
57
 
58
58
  These postchecks establish the selected provider, model, and effective Pi session
59
59
  thinking level. They do not inspect provider-side reasoning or enforce a
@@ -65,11 +65,15 @@ provider/engine behavior rather than inferred from the effective level.
65
65
 
66
66
  The registry owns public UUIDs, lifecycle state, the turn read model, background turn promises, abort, and disposal. A per-session operation queue serializes prompt admission and close; concurrent abort callers share one bounded abort attempt for the active turn. It registers a session only after adapter creation and postconditions complete. Once shutdown begins, starts fail with `server_stopping`; a handle created by an already-running start is disposed instead of admitted. The SDK session ID is never exposed.
67
67
 
68
+ ## Skills
69
+
70
+ `skillCatalog` and each profile's `skills` are parsed by `loadConfig`. A path value resolves against the configuration file's directory, like a workspace; a `builtin:<name>` value resolves through the skill library to the operator's override in `skills/<name>` next to the configuration file, else to the skill shipped in the package. Setup, not the server, installs the controller skill into the clients' skill directories. A profile keeps its selected skills as a frozen list of `{alias, path}` pairs; the path never leaves the server. At session start the adapter loads exactly the selected files through the Pi SDK's public `loadSkills` with no default directories and validates them completely (SDK diagnostics, duplicate Pi skill names, aliases that share one canonical file, `disable-model-invocation: true`, files over 256 KiB). The SDK's skill override returns exactly that set, also after extension hooks add resources, so a package's bundled skills, prompt templates and themes no longer reach a session. Any failure is `skill_load_failed` with a fixed message. The model sees the loaded skills in its system prompt and decides itself whether to read one; the server neither guarantees that nor snapshots skill contents. Capability discovery projects only the selected aliases. See [Pi skills](skills.md) and [ADR 0007](adr/0007-profile-skill-selection.md).
71
+
68
72
  ## Lifecycle and public data
69
73
 
70
- `pi_session_prompt` creates a provisional turn identity, invokes Pi preflight, and only after accepted preflight publishes `{session, turn}`; rejected preflight leaves no orphan turn. Completion returns to `idle`; failure is generic. Each active session retains at most 20 turns. A turn is `running`, `completed`, `failed`, or `aborted`; its public view contains IDs, timestamps, and terminal `completedAt`. Completed text is normalized and capped at 64 KiB UTF-8 with Unicode-safe truncation. It can contain model-visible workspace data and remains potentially sensitive.
74
+ `pi_session_prompt` creates a provisional turn identity, invokes Pi preflight, and only after accepted preflight publishes `{session, turn}`; rejected preflight leaves no orphan turn. Completion returns to `idle`; failure is generic. Each active session retains at most 20 turns. A turn is `running`, `completed`, `failed`, or `aborted`; its public view contains IDs, timestamps, and terminal `completedAt`. While a turn runs, its `updatedAt` is the time of the last observed public SDK activity (assistant message and tool execution events, content-free) and is a liveness lower bound only; see [Tool contracts](tool-contracts.md). Completed text is normalized and capped at 64 KiB UTF-8 with Unicode-safe truncation. It can contain model-visible workspace data and remains potentially sensitive.
71
75
 
72
- Session metadata contains only whitelisted values: public session ID, workspace alias, execution-profile alias, derived permission `profile`, post-verified provider/model/effective thinking level, lifecycle state, and timestamps. Capability metadata uses a separate whitelist: server name/version, loaded configuration fingerprint/reload policy, workspace aliases, and configured profile fields. Neither view contains workspace paths, full model objects, SDK session IDs, auth sources, credential paths, headers, base URLs, prompts, transcripts, or raw provider errors.
76
+ Session metadata contains only whitelisted values: public session ID, workspace alias, execution-profile alias, derived permission `profile`, post-verified provider/model/effective thinking level, lifecycle state, and timestamps. Capability metadata uses a separate whitelist: server name/version, loaded configuration fingerprint/reload policy, workspace aliases, and configured profile fields, including the selected skill aliases. Neither view contains workspace paths, full model objects, SDK session IDs, auth sources, credential paths, headers, base URLs, prompts, transcripts, or raw provider errors.
73
77
 
74
78
  Profile-specific authentication/catalog operations and session creation have fixed server-side deadlines of 5,000 ms and 10,000 ms. Prompt preflight, abort, close cleanup, registry shutdown, and process shutdown have fixed server-side deadlines of 5,000 ms, 5,000 ms, 5,000 ms, 12,000 ms, and 15,000 ms respectively. A preflight timeout returns `prompt_timeout` without publishing a turn. The unpublished operation remains quarantined until its prompt and abort cannot race with later work; late settlement can release that quarantine but never publish the turn. An abort timeout returns `abort_timeout` without falsely marking the turn aborted; a later normal outcome or successful SDK abort may still terminalize it exactly once. Close marks the session terminal, removes its public registry entry deterministically, and performs best-effort abort/dispose. Repeated close calls return a cached closed view while its tombstone is retained. Each SDK handle is disposed at most once. A late promise cannot revive a removed session or mutate a removed turn.
75
79
 
@@ -79,4 +83,4 @@ Public errors are stable and sanitized. They omit caller values, complete prompt
79
83
 
80
84
  ## Scope
81
85
 
82
- The product boundary is local stdio and in-memory-only operation for one trusted local MCP peer. Allowlisted Pi extensions may persist state of their own outside this boundary, and their input hooks see every client prompt as `rpc` input, never `interactive`; Pi Session MCP itself keeps no state beyond process memory. A controller keeps one immutable configuration/profile snapshot for its lifetime; file edits require a genuinely new controller process and do not hot-reload, mark stale, or mutate running sessions. Each new session performs external catalog discovery within the loaded grants, so catalog changes do not require a controller restart, but a running session's active tools remain fixed. Restarting the controller loses all sessions; reconnecting a client reloads configuration only when it actually starts a new controller process. Capability discovery reports captured metadata only and does not prove current-file freshness, server reachability, provider availability, or catalog state. It excludes HTTP, a web UI, persistence/resume, remote access, multi-user behavior, transcript export, streaming, live provider/model discovery, availability probes during capability discovery, and provider credential management. A coding execution profile is an application-level tool selection, not an OS sandbox.
86
+ The product boundary is local stdio and in-memory-only operation for one trusted local MCP peer. Allowlisted Pi extensions may persist state of their own outside this boundary, and their input hooks see every client prompt as `rpc` input, never `interactive`; Pi Session MCP itself keeps no state beyond process memory. A controller keeps one immutable configuration/profile snapshot for its lifetime; file edits require a genuinely new controller process and do not hot-reload, mark stale, or mutate running sessions. Each new session performs external catalog discovery within the loaded grants, so catalog changes do not require a controller restart, but a running session's active tools remain fixed. Skill selection is part of that snapshot, but skill file contents are not: the SDK keeps file paths, so operators must not change a skill file while sessions that use it are active (see [Pi skills](skills.md)). Restarting the controller loses all sessions; reconnecting a client reloads configuration only when it actually starts a new controller process. Capability discovery reports captured metadata only and does not prove current-file freshness, server reachability, provider availability, or catalog state. It excludes HTTP, a web UI, persistence/resume, remote access, multi-user behavior, transcript export, streaming, live provider/model discovery, availability probes during capability discovery, and provider credential management. A coding execution profile is an application-level tool selection, not an OS sandbox.
@@ -10,6 +10,7 @@ The repository-local command shape is:
10
10
 
11
11
  ```text
12
12
  npm run --silent setup -- [--dry-run|--apply|--verify|--rollback|--remove] [--json] --target CLIENT:SCOPE:pi-session-mcp
13
+ npm run --silent setup -- [--apply] [--json] --customize-skill NAME
13
14
  ```
14
15
 
15
16
  The script executes the already-built production entry point. Building is a
@@ -61,6 +62,204 @@ of descendant termination must assert zombie-or-absence without catching their
61
62
  own failed assertion; fence retention is checked separately with a live member
62
63
  and an indeterminate scan.
63
64
 
65
+ ## Skill installation
66
+
67
+ Besides the registration, setup installs the controller skill
68
+ `pi-session-orchestration` (`src/setup-skills.ts`). The skill step runs inside
69
+ the same ownership transaction as the registration, per target and after it.
70
+ `--dry-run` and `--verify` run it for every target. `--apply` runs it only for a
71
+ registration finding of `ok` or `unchanged`, and `--rollback` and `--remove`
72
+ only after the registration finding is `ok`. A skill finding therefore never
73
+ blocks a registration, but `divergent`, `failed` and `unsupported` skill
74
+ findings make the overall result `failed`, like any other finding.
75
+
76
+ **Source.** The effective skill is the operator's override if
77
+ `<override dir>/pi-session-orchestration/SKILL.md` exists, else the shipped skill
78
+ (see [Pi skills](skills.md#the-skill-library)). Setup reads the tree with a
79
+ bounded reader: regular files only, at most 32 files, 1 MiB and 8 levels, no
80
+ links or special files inside the tree, entries whose name starts with `.`
81
+ skipped. A missing or unusable source is `skill_source_invalid`. The fingerprint
82
+ is version 2 (`src/skill-library.ts`): `v2:` and a SHA-256 over a domain and
83
+ version tag, the file count, and per file in path byte order the length-prefixed
84
+ UTF-8 path and the length-prefixed content. Version 1 (0.8.0) hashed
85
+ `path NUL content NUL` per file and was ambiguous, because content may contain
86
+ NUL bytes. A version 1 value never proves identity alone (see *Legacy records*
87
+ below).
88
+
89
+ **Slots.** A slot is the skill directory a client reads:
90
+
91
+ | Target | Slot |
92
+ | --- | --- |
93
+ | `codex:user` | `~/.agents/skills/pi-session-orchestration` |
94
+ | `claude-code:user`, `claude-code:local` | `~/.claude/skills/pi-session-orchestration` (one shared slot) |
95
+ | `claude-code:project` | `<project>/.claude/skills/pi-session-orchestration` (the directory in which setup runs) |
96
+
97
+ **Ownership records.** One record per client slot holds the target client,
98
+ the scope kind (`project` or `user`, so Claude Code `user` and `local` share a
99
+ record), the phase and a content fingerprint, plus `replaces` during an update (see below). A home slot uses the
100
+ alias `skill:pi-session-orchestration`. A project slot uses
101
+ `skill:pi-session-orchestration@<id>`, where `<id>` is the first 16 hex digits
102
+ of a SHA-256 over the project's real path: an opaque identity, not a path, so a
103
+ `--remove` in another project never touches this project's copy. The `pending`
104
+ phase is written before the first filesystem change and promoted to `owned`
105
+ after a post-install inspection. Skill records hold `v2:` values in
106
+ `fingerprint` and `replaces`; registration records keep their 64-hex snapshot
107
+ hash. During an update, the pending record also keeps the fingerprint of the
108
+ copy it replaces (`replaces`), so an interrupted update leaves a copy that
109
+ setup still recognizes as its own: the next `--apply` finishes the update and
110
+ `--remove` deletes it. A later run that finds a `pending` record whose slot
111
+ already equals the source promotes it.
112
+
113
+ **Atomic copy.** Setup creates missing parent directories (mode `0755`), stages
114
+ the tree in a staging directory in the slot's parent (files `0644`, each file
115
+ and directory fsynced), and re-checks that the slot is unchanged
116
+ (fingerprint, device and inode). It then renames the exact old tree to a backup
117
+ name, renames the staged directory into place, inspects the result, verifies
118
+ that the backup is still the exact old tree it moved, renames it to a trash
119
+ name and deletes the trash entry by entry. The slot is never edited in place.
120
+
121
+ **Intermediates and recovery.** The three intermediate directories are named
122
+ after the pending record's transaction id, in the slot's parent:
123
+ `.pi-session-mcp-<skill>-<transactionId>.staging`, `.backup` and `.trash`.
124
+ Recovery derives them from the persisted record and never searches. The order
125
+ is: pending record, staging, backup rename, install, verify backup, rename to
126
+ trash, delete the trash entry by entry, owned record. `--apply`, `--remove` and
127
+ `--rollback` settle a pending transaction first, inside the ownership
128
+ transaction, and only when the slot is empty, a link, or one of setup's two
129
+ copies:
130
+
131
+ - A trash directory is deleted after a structural recheck: a real directory,
132
+ regular files and directories only, no hidden entries, within the tree
133
+ limits. It may be partial, because it was verified before the rename.
134
+ - The backup must equal the replaced copy; the staging directory must equal
135
+ the target or be an interrupted write of it. A partially written staging
136
+ directory counts only when every file is a prefix-consistent part of the
137
+ current library tree and that tree is the record's target.
138
+ - `--apply` with an empty slot finishes forward with a complete staging
139
+ directory, else moves the verified backup back. `--remove` and `--rollback`
140
+ delete the verified intermediates and leave the slot to the removal.
141
+ - Anything modified or foreign stays and is reported as
142
+ `skill_recovery_retained` (divergent). A failed update leaves its staging
143
+ directory until the next run settles it, and an unhealthy ownership fence
144
+ retains intermediates the same way.
145
+ - Random-name directories of 0.8.0 updates are never searched for or deleted;
146
+ an operator may remove them by hand.
147
+ - `--dry-run` and `--verify` do not report leftover intermediates.
148
+
149
+ **Legacy records.** A skill record whose `fingerprint` or `replaces` is a bare
150
+ 64-hex value was written by 0.8.0. It names the slot only if a trusted
151
+ reference has that version 1 value and is byte-identical, by its version 2
152
+ fingerprint, to the installed copy. The references are the shipped skills of
153
+ 0.8.0 (a fixed table in `src/skill-library.ts`) and the current library tree.
154
+ When proven, `--apply` migrates the record to version 2 and then updates the
155
+ copy as usual; `--remove` removes the proven copy. Otherwise `--dry-run`,
156
+ `--apply`, `--remove` and `--rollback` report `skill_legacy_unverified`
157
+ (divergent) and change neither the copy nor the record. The operator inspects
158
+ the copy and, if it is unchanged setup content that is no longer needed,
159
+ deletes it; an absent slot clears the record on the next operation. The
160
+ intermediates of an interrupted 0.8.0 update cannot be derived and are not
161
+ recovered.
162
+
163
+ **Divergence rules.** Setup never replaces or deletes, and reports
164
+ `skill_divergent` for:
165
+
166
+ - an installed copy whose fingerprint differs from setup's own copy (the
167
+ owned fingerprint, or the one a pending update replaces), even if it happens
168
+ to equal the new source;
169
+ - an installed copy that a 0.8.0 record names but no trusted reference proves
170
+ (`skill_legacy_unverified`);
171
+ - a copy that differs from the source without a record (a foreign skill of the
172
+ same name);
173
+ - any entry whose name starts with `.` inside the installed copy: setup never
174
+ writes such entries, so they are operator content that the fingerprint over
175
+ the visible files would not protect;
176
+ - a link to anything other than the effective skill, a non-directory at the
177
+ skill entry, or a tree that cannot be read.
178
+
179
+ A copy that equals the source stays unowned when no record exists
180
+ (`skill_equivalent` or `skill_already_equivalent`). A link at the skill entry
181
+ whose real path is exactly the effective library skill directory (your
182
+ override, else the shipped skill) is the operator's own equivalent setup:
183
+ `--dry-run` and `--apply` report `skill_linked` (unchanged), `--verify` reports
184
+ `skill_verified`, and removal leaves it alone (`skill_not_owned`). An unmodified owned copy
185
+ that differs from the source is replaced after a package update or a changed
186
+ override (`skill_update_planned`, `skill_updated`).
187
+
188
+ **Link rules.** The skill entry itself is never followed. In the home
189
+ directory, the client's skills directory (`~/.claude/skills`, `~/.agents/skills`)
190
+ may be the operator's own link, for example into a dotfiles checkout, and is
191
+ resolved to its real path. A project may be untrusted: only the project root is
192
+ canonicalized, and a link anywhere below it makes the slot divergent, so setup
193
+ never writes outside the project. If a slot cannot be resolved at all, the
194
+ finding is `skill_source_invalid` for `--dry-run`, `skill_install_failed` for
195
+ `--apply`, `skill_missing` for `--verify` and `skill_divergent` for removal.
196
+
197
+ **Shared slot.** For `--rollback` and `--remove`, the Claude Code `user` and
198
+ `local` registrations share one slot. While the other registration is still
199
+ `owned`, the skill stays and the finding is `skill_kept_shared`. The last
200
+ removal deletes it (`skill_removed`; `skill_absent` if it is already gone;
201
+ `skill_not_owned` if setup never owned it). A modified copy is left in place
202
+ (`skill_divergent`) and its record stays, so a later run can finish after you
203
+ removed the copy.
204
+
205
+ **Remove retry.** Registration removal can finish before skill cleanup, for
206
+ example after an interruption or a `skill_divergent`. When the registration
207
+ record is gone but setup still owns the skill of that target, `--remove` and
208
+ `--rollback` run once more for that target. They first prove that the
209
+ registration is absent and then report the registration as removed and clean up
210
+ the skill. They never run for a target whose skill setup does not own, nor for
211
+ a Claude Code `user` or `local` target while the other one of the two is still
212
+ registered, and an unowned live registration stays untouched
213
+ (`ownership_unavailable`).
214
+
215
+ Skill reports carry only client, scope, the alias `pi-session-orchestration`,
216
+ status and a stable code. No skill content, fingerprint or path leaves setup.
217
+
218
+ ## Skill customization
219
+
220
+ `setup --customize-skill <name>` (and `--apply --customize-skill <name>`,
221
+ `src/skill-customize.ts`) does not touch clients and takes no ownership lock. It
222
+ copies a shipped skill into the override directory `skills/` next to the
223
+ configuration file and records the shipped fingerprint under the skill name in
224
+ `<override dir>/.pi-session-mcp-bases.json`
225
+ (`{"schemaVersion":2,"bases":{"<name>":"<fingerprint>"}}`, strictly validated,
226
+ at most 64 KiB). A value is a `v2:` fingerprint or a legacy 64-hex value that
227
+ could not be migrated. A version 1 file is still read. The copy is staged in
228
+ a temporary directory and renamed into place; an existing override is never
229
+ overwritten. A changed shipped skill is only reported (`skill_override_outdated`);
230
+ `--apply` then moves the recorded base and nothing else.
231
+
232
+ `--apply` is a cross-process transaction per override directory
233
+ (`src/override-lock.ts`). It takes an exclusive kernel lock on the stable lock
234
+ file `<override dir>/.pi-session-mcp-bases.lock` (mode `0600`, never deleted;
235
+ on Linux the trusted util-linux `flock` at `/usr/bin/flock` or `/bin/flock`, on
236
+ macOS `O_EXLOCK`; the same supported local filesystems as the ownership
237
+ state). Under the lock it re-reads and validates the bases file, copies the
238
+ skill if no override exists, merges the base and writes the file atomically: an
239
+ fsynced temporary file, a rename and a directory fsync. The lock is released on
240
+ every path. The wait is bounded at 30 seconds (`skill_customize_lock_busy`);
241
+ a lock that cannot be taken is `skill_customize_lock_unavailable`. A killed
242
+ holder releases the lock through the kernel and may leave a stale
243
+ `.pi-session-mcp-bases.json.<hex>.tmp` or a temporary copy directory
244
+ `.<skill>.<hex>.tmp`, which are harmless. The dry-run takes no
245
+ lock and writes nothing: no directory, lock file or temporary file. Every write
246
+ produces version 2 and migrates each legacy value that names a released 0.8.0
247
+ shipped skill; other legacy values are kept as they are.
248
+
249
+ Decisions for an existing legacy base: if it names the current shipped skill,
250
+ the dry-run reports `skill_base_migration_planned` and `--apply` reports
251
+ `skill_base_migrated`; if it names no released shipped skill, the dry-run
252
+ reports `skill_base_unverified` and `--apply` acknowledges it as
253
+ `skill_base_updated`; if it names another released skill, the result is
254
+ `skill_override_outdated`. An existing override that equals the shipped skill
255
+ but has no base completes a customization whose bases commit failed
256
+ (`skill_customize_planned`, `skill_customized`). Of two concurrent applies for
257
+ the same skill, one reports `skill_customized` and the other
258
+ `skill_override_current`. Doctor stays offline, read-only and lock-free. It cannot be combined
259
+ with `--target`, `--verify`, `--rollback` or `--remove`. The finding target is
260
+ `{ client: "skills", scope: "override", alias: <name> }`; the alias is the
261
+ skill name only if it is valid, else `invalid`.
262
+
64
263
  ## Verification boundary
65
264
 
66
265
  Production verification requires the offline Doctor, exact public client-CLI registration inspection, and a directly launched bounded MCP handshake. The verifier calls `pi_capabilities_get` with `{}` only; session start/prompt/turn and provider/model operations are forbidden. Its child process is terminated and awaited before success is reported.
@@ -84,7 +283,26 @@ Stable finding codes are grouped by responsibility:
84
283
  - verification: `registration_not_equivalent`, `mcp_verified`,
85
284
  `mcp_unsupported`, `mcp_timeout`, `mcp_failed`;
86
285
  - rollback/removal: `rolled_back`, `removed`, `ownership_diverged`,
87
- `cleanup_failed`.
286
+ `cleanup_failed`;
287
+ - controller skill, dry-run: `skill_absent`, `skill_equivalent`, `skill_linked`,
288
+ `skill_update_planned`, `skill_recovery_planned`, `skill_divergent`,
289
+ `skill_legacy_unverified`, `skill_source_invalid`;
290
+ - controller skill, apply: `skill_installed`, `skill_already_equivalent`, `skill_recovered`, `skill_linked`,
291
+ `skill_updated`, `skill_divergent`, `skill_legacy_unverified`,
292
+ `skill_recovery_retained`, `skill_source_invalid`,
293
+ `skill_install_failed`;
294
+ - controller skill, verify: `skill_verified`, `skill_missing`,
295
+ `skill_not_equivalent`;
296
+ - controller skill, rollback/removal: `skill_removed`, `skill_kept_shared`,
297
+ `skill_not_owned`, `skill_absent`, `skill_divergent`,
298
+ `skill_legacy_unverified`, `skill_recovery_retained`;
299
+ - skill customization: `skill_customize_planned`, `skill_customized`,
300
+ `skill_override_current`, `skill_override_outdated`,
301
+ `skill_base_migration_planned`, `skill_base_migrated`,
302
+ `skill_base_unverified`, `skill_base_updated`, `skill_not_shipped`,
303
+ `skill_bases_corrupt`, `skill_override_invalid`,
304
+ `skill_customize_lock_busy`, `skill_customize_lock_unavailable`,
305
+ `skill_customize_failed`.
88
306
 
89
307
  The public statuses are `planned`, `ok`, `unchanged`, `absent`, `divergent`,
90
308
  `unsupported`, and `failed`. Reports never contain command arguments, command
@@ -90,6 +90,89 @@ bounded direct MCP `initialize`, `tools/list`, and
90
90
  `pi_capabilities_get({})` check. It never starts a session or invokes
91
91
  a provider.
92
92
 
93
+ ### Controller skill
94
+
95
+ Besides the registration, `--apply` installs the controller skill
96
+ `pi-session-orchestration` for each target, as a real copy of the effective
97
+ skill: your override in the skill library if you have one, else the skill
98
+ shipped in the package ([Pi skills](skills.md)). Clients discover it without
99
+ further steps.
100
+
101
+ | Target | Where the skill goes |
102
+ | --- | --- |
103
+ | `codex:user:pi-session-mcp` | `~/.agents/skills/pi-session-orchestration` |
104
+ | `claude-code:user:pi-session-mcp`, `claude-code:local:pi-session-mcp` | `~/.claude/skills/pi-session-orchestration` (one shared directory) |
105
+ | `claude-code:project:pi-session-mcp` | `<project>/.claude/skills/pi-session-orchestration`, where `<project>` is the directory in which setup runs |
106
+
107
+ Every result therefore has a second finding per target, with the same client
108
+ and scope and the alias `pi-session-orchestration`:
109
+
110
+ ```text
111
+ apply: ok
112
+ claude-code/user/pi-session-mcp: ok (applied)
113
+ claude-code/user/pi-session-orchestration: ok (skill_installed)
114
+ ```
115
+
116
+ Skill findings per operation:
117
+
118
+ - `--dry-run`: `skill_absent` (absent), `skill_equivalent` (unchanged),
119
+ `skill_linked` (unchanged; a link to exactly the effective skill),
120
+ `skill_update_planned` (planned), `skill_recovery_planned` (planned; the
121
+ ownership record is pending for an update that `--apply` would settle),
122
+ `skill_divergent` (divergent), `skill_legacy_unverified` (divergent),
123
+ `skill_source_invalid` (failed).
124
+ - `--apply`: `skill_installed` (ok), `skill_already_equivalent` (unchanged),
125
+ `skill_recovered` (ok; `--apply` settled an interrupted update and needed no
126
+ further update), `skill_linked` (unchanged),
127
+ `skill_updated` (ok; setup replaced its own unmodified copy after a package
128
+ update or a changed override), `skill_divergent`, `skill_legacy_unverified`,
129
+ `skill_recovery_retained` (divergent), `skill_source_invalid`,
130
+ `skill_install_failed` (failed).
131
+ - `--verify`: `skill_verified` (ok), `skill_missing` and
132
+ `skill_not_equivalent` (failed).
133
+ - `--rollback` and `--remove`: `skill_removed` (ok), `skill_kept_shared`
134
+ (unchanged; the other Claude Code `user` or `local` registration still uses
135
+ the shared directory), `skill_not_owned` (unchanged), `skill_absent` (ok) and
136
+ `skill_divergent` (a modified copy stays in place), `skill_legacy_unverified`
137
+ (a record of 0.8.0 that no trusted reference proves; copy and record stay) and
138
+ `skill_recovery_retained` (a modified or foreign intermediate directory of an
139
+ interrupted update stays).
140
+
141
+ Setup never replaces or deletes a modified installed copy, a skill of the same
142
+ name that it did not install, or a link or non-directory at the skill entry; it
143
+ reports `skill_divergent`. That does not block the registration, but it makes
144
+ the overall status `failed` (exit 1). Never edit the installed copy. To
145
+ customize the skill, run `--apply --customize-skill pi-session-orchestration`,
146
+ edit the override, and run `--apply` for the target again. Design and trust
147
+ rules: [setup lifecycle design](client-setup-design.md#skill-installation).
148
+
149
+ Setup installs only `pi-session-orchestration` into clients. Worker skills are
150
+ for Pi profiles and are selected through `builtin:` entries in `skillCatalog`.
151
+
152
+ ### Customize a shipped skill
153
+
154
+ ```bash
155
+ npm run --silent setup -- --customize-skill pi-session-review
156
+ npm run --silent setup -- --apply --customize-skill pi-session-review
157
+ ```
158
+
159
+ This works without `--target` and needs `PI_SESSION_MCP_CONFIG`. It copies the
160
+ shipped skill into the `skills/` directory next to the configuration file and
161
+ records the fingerprint of the shipped skill in
162
+ `.pi-session-mcp-bases.json` there. The finding target is
163
+ `skills/override/<name>`; the codes are `skill_customize_planned` (planned),
164
+ `skill_customized` (ok), `skill_override_current` (unchanged),
165
+ `skill_override_outdated` (planned), `skill_base_migration_planned` (planned),
166
+ `skill_base_unverified` (planned), `skill_base_migrated` (ok),
167
+ `skill_base_updated` (ok), `skill_not_shipped`, `skill_bases_corrupt`,
168
+ `skill_override_invalid`, `skill_customize_lock_busy`,
169
+ `skill_customize_lock_unavailable` and `skill_customize_failed` (failed). An
170
+ existing override is never overwritten. `--apply` runs under an exclusive lock
171
+ on the override directory (`.pi-session-mcp-bases.lock`, 30 s wait); the dry-run
172
+ takes no lock and writes nothing.
173
+ After `skill_override_outdated`, `--apply` moves only the recorded base, which
174
+ acknowledges a change you reviewed. Details: [Pi skills](skills.md#customize-and-add-skills).
175
+
93
176
  ## Local configuration
94
177
 
95
178
  Create an unversioned JSON configuration file such as `pi-session-mcp.json`:
@@ -118,7 +201,7 @@ Create an unversioned JSON configuration file such as `pi-session-mcp.json`:
118
201
  }
119
202
  ```
120
203
 
121
- Configuration is strict. A profile has exactly `default`, `permissionProfile`, `provider`, `model`, and `thinkingLevel`, plus the optional `mcpServers` and `extensions` described in [Installation](installation.md); aliases follow the configured alias naming rules. At least one profile and exactly one default are required, and that default must be `read-only`. `thinkingLevel` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Workspace paths resolve relative to the configuration file. Clients can select only configured aliases, never an arbitrary working directory.
204
+ Configuration is strict. A profile has exactly `default`, `permissionProfile`, `provider`, `model`, and `thinkingLevel`, plus the optional `mcpServers`, `extensions` and `skills` described in [Installation](installation.md) and [Pi skills](skills.md); aliases follow the configured alias naming rules. At least one profile and exactly one default are required, and that default must be `read-only`. `thinkingLevel` is one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Workspace paths resolve relative to the configuration file. Clients can select only configured aliases, never an arbitrary working directory.
122
205
 
123
206
  Use placeholders until a locally available provider/model pair is known. Do not put keys, tokens, credential objects, headers, base URLs, or authentication paths in this file.
124
207
 
@@ -190,7 +273,7 @@ Claude Code enforces each tool's declared JSON Schema (`additionalProperties: fa
190
273
  ## Expected flow and lifecycle deadlines
191
274
 
192
275
  1. Call `pi_capabilities_get` with `{}` and select explicit workspace and read-only execution-profile aliases from the returned config-derived lists.
193
- 2. Call `pi_session_start` with those aliases. The server resolves the configured provider/model through its one `ModelRuntime`, checks existing local Pi authentication and availability, creates with explicit model/thinking/tools, and verifies the effective SDK result.
276
+ 2. Call `pi_session_start` with those aliases. The server resolves the configured provider/model through its shared `ModelRuntime` (a profile with extensions gets a fresh runtime for each session), loads and validates the profile's selected skills, checks existing local Pi authentication and availability, creates with explicit model/thinking/tools, and verifies the effective SDK result.
194
277
  3. Call `pi_session_prompt` with the returned `sessionId` and a read-only prompt. It returns after accepted preflight with a `turnId` in `running` state.
195
278
  4. Poll `pi_turn_get` with that `sessionId` and `turnId` until `completed`, `failed`, or `aborted`, with a deadline no longer than three minutes. There is no `pi_turn_wait` tool.
196
279
  5. Call `pi_session_close` when the session is no longer needed.
@@ -246,7 +329,7 @@ Poll `pi_turn_get` for at most three minutes. If it does not become terminal, re
246
329
 
247
330
  ### A prompt fails immediately after a provider config or credential change
248
331
 
249
- `pi_session_prompt` reads the configured provider/model through one `ModelRuntime` that is created once and cached for the lifetime of the Pi Session MCP server process; the underlying SDK reads `~/.pi/agent/models.json` only at that creation. Editing that file (for example adding or fixing a provider API key) does not take effect in an already-running server process, and a resulting turn typically fails (`turn_failed`) within milliseconds rather than after a real provider round trip. Restart or reconnect the MCP server (which restarts the process) after any provider config or credential change, then retry.
332
+ For profiles without extensions, `pi_session_prompt` reads the configured provider/model through one `ModelRuntime` that is created once and cached for the lifetime of the Pi Session MCP server process (a profile with extensions creates a fresh runtime when each session starts); the underlying SDK reads `~/.pi/agent/models.json` only at that creation. Editing that file (for example adding or fixing a provider API key) does not take effect in an already-running server process, and a resulting turn typically fails (`turn_failed`) within milliseconds rather than after a real provider round trip. Restart or reconnect the MCP server (which restarts the process) after any provider config or credential change, then retry.
250
333
 
251
334
  ### Session cleanup
252
335