pi-ptc-subagents 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +102 -249
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.2] - 2026-10-10
9
+
10
+ ### Changed
11
+
12
+ - **The README leads with what the package is instead of how it works.** The opening now
13
+ names the two capabilities up front — subagent fan-out and programmable tool calling —
14
+ and states the relationship with pi's own `codemode` in the first screen. The surface
15
+ detection deep-dive (three-question table, startup notices, `--print` / `--no-extensions`
16
+ edge cases) moves out of the README to
17
+ [docs/usage/surface.md](./docs/usage/surface.md), which now owns it; the README keeps
18
+ the short version and a pointer. Everything else is tightened, not deleted.
19
+ - **The built-in tool enumeration no longer claims a count pi moved past.** The README used
20
+ to say "all seven built-ins"; pi 1.1.0 ships eight (`powershell` joined), so the copy now
21
+ enumerates the tools this package binds (`read`, `bash`, `edit`, `write`, `grep`, `find`,
22
+ `ls`) without claiming that is pi's full set. Docs only; no code change — note the
23
+ binding list itself is unchanged, and `powershell` is currently not reachable from PTC
24
+ programs even when enabled (tracked separately).
25
+
26
+ [2.0.2]: https://github.com/a1121611810/pi-ptc-subagents/compare/v2.0.1...v2.0.2
27
+
8
28
  ## [2.0.1] - 2026-10-10
9
29
 
10
30
  ### Changed
package/README.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # pi-ptc-subagents
2
2
 
3
- DSH-style **PTC mode** (Programmable Tool Calling) for [pi](https://pi.dev):
4
- the model writes a JS/TS program that calls pi's tools from inside a worker,
5
- and only the program's return value plus its logs come back to the model.
3
+ Subagents and programmable tool calling for [pi](https://pi.dev).
4
+
5
+ One package, two capabilities:
6
+
7
+ - **Subagent fan-out** — dispatch a task to a fresh `pi` subprocess running a named agent
8
+ (from `~/.pi/agent/agents/<name>.md` or `.pi/agents/<name>.md`), in isolation. Foreground
9
+ await, or background with a lifecycle you can inspect and stop.
10
+ - **Programmable tool calling** — `ptc_run_code` / `ptc_workflow` run a JS/TS program in a
11
+ worker; the program composes the session's tools as `tools.<name>(args)`, and only the
12
+ program's return value plus its logs come back to the model.
13
+
14
+ pi ships its own programmable tool calling (`codemode` — a QuickJS sandbox that cannot spawn
15
+ processes). This package is the part that can spawn processes — subagents, background tasks,
16
+ a real Node runtime inside programs — and it composes with `codemode` instead of replacing
17
+ it. On a session where pi's `codemode` orchestrates, this package registers underneath it as
18
+ the execution layer ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); the deep
19
+ dive lives in [docs/usage/surface.md](./docs/usage/surface.md).
6
20
 
7
21
  **Source is open.** This repository is public and the source is here — `dist/` on npm is the
8
22
  compiled form of what you read below. Contributions go through pull requests: see
@@ -10,57 +24,44 @@ compiled form of what you read below. Contributions go through pull requests: se
10
24
  [SECURITY.md](./SECURITY.md) before reporting anything. Releases are cut from `main` by the
11
25
  maintainer only; if you find something you think needs a release, open an issue and say so.
12
26
 
13
- ## Status
14
-
15
- Functional and actively used: `ptc_run_code` and `ptc_workflow` are registered and run
16
- programs through the same tested worker machinery (dispatcher, wire protocol,
17
- budgets, built-in bindings). The implementation is written clean-room from
18
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) PTC
19
- behaviour — see [ADR-0002](./docs/adr/0002-source-strategy.md) for how that boundary is
20
- kept, and [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for the attribution that
21
- follows from it.
22
-
23
27
  ## Install
24
28
 
25
29
  ```bash
26
30
  pi install npm:pi-ptc-subagents
27
31
  ```
28
32
 
29
- From a local checkout: `pnpm install && pnpm run build && pi install /abs/path/to/this/repo` (or `npm install && npm run build && …` — the lockfile is `pnpm-lock.yaml`; with npm you'll need `npm i` to regenerate `package-lock.json`).
33
+ From a local checkout: `pnpm install && pnpm run build && pi install /abs/path/to/this/repo`.
30
34
 
31
- pi reads the `pi.extensions` manifest field, so no extra setup steps are
32
- required — install it and the extension is on for the next pi startup.
35
+ pi reads the `pi.extensions` manifest field — install it and the extension is on for the next
36
+ pi startup, no extra setup.
33
37
 
34
38
  ## Tools
35
39
 
36
- - `ptc_run_code` — run a JS/TS program that composes tool calls; the program
37
- reaches tools as `tools.read(...)`, `tools.write(...)`, etc. (all seven
38
- built-ins, `bash` included); its return value and `console.log` output are
39
- reported back.
40
- - `ptc_workflow` — structured variant with `meta` + plain-JSON `args`, plus the
41
- workflow helpers (`log`, `phase`, `parallel`, `pipeline`). There is no
42
- `agent()` helper on either surface.
43
- - `ptc_subagent` — the top-level subagent face: dispatch a fresh `pi`
44
- subprocess for a task without writing a program. Registered only when the
45
- detected surface is `subagents`
46
- ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); on `full`
47
- the same capability is the `pi.dispatch` binding inside a program (see
48
- [Dispatch](#dispatch-fan-out-to-per-call-pi-subprocesses)).
49
- - `ptc_task_list` / `ptc_task_output` / `ptc_task_stop` — manage background
50
- dispatches (see [Background dispatch](#background-dispatch)). They stay
51
- available when PTC mode is off.
40
+ - `ptc_subagent` — the top-level subagent face: dispatch a fresh `pi` subprocess for a task
41
+ without writing a program. Registered only when the detected surface is `subagents`
42
+ ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); on `full` the same
43
+ capability is the `pi.dispatch` binding inside a program.
44
+ - `ptc_run_code` — run a JS/TS program that composes tool calls; the program reaches the
45
+ session's enabled built-in tools as `tools.read(...)`, `tools.write(...)`, etc.
46
+ (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), plus `tools["pi.dispatch"]`;
47
+ its return value and `console.log` output are reported back.
48
+ - `ptc_workflow` — structured variant with `meta` + plain-JSON `args`, plus the workflow
49
+ helpers (`log`, `phase`, `parallel`, `pipeline`). There is no `agent()` helper on either
50
+ surface.
51
+ - `ptc_task_list` / `ptc_task_output` / `ptc_task_stop` — manage background dispatches
52
+ (see [Background dispatch](#background-dispatch)). They stay available when PTC mode is off.
52
53
 
53
54
  Long output follows pi's own truncation contract ([ADR-0015](./docs/adr/0015-pi-truncation-contract.md)):
54
-
55
- the text block keeps the tail (50 KB / 2000 lines) and the untruncated text is written to a temp file
56
-
57
- the next program can `tools.read`; the collapsed row then shows `truncated` in its meta.
55
+ the text block keeps the tail (50 KB / 2000 lines), the untruncated text is written to a temp
56
+ file the next program can `tools.read`, and the collapsed row shows `truncated` in its meta.
58
57
 
59
58
  ## Dispatch (fan-out to per-call pi subprocesses)
60
59
 
61
- PTC programs can spawn a fresh `pi` subprocess per call via the **`pi.dispatch(...)`** binding ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md)). Use it to fan out to a specialist agent — the child subprocess loads the named agent's markdown from `~/.pi/agent/agents/<name>.md` (or `.pi/agents/<name>.md` for project-scope agents), runs that agent's tool set and system prompt in isolation, and returns a structured result.
62
-
63
- Bindings are reached through the one `tools` table — there is no `pi` global in the worker — so the binding named `pi.dispatch` is called as `tools["pi.dispatch"]({ … })` with a single object argument.
60
+ PTC programs can spawn a fresh `pi` subprocess per call via the **`pi.dispatch(...)`** binding
61
+ ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md)) — the child loads the named agent's
62
+ markdown, runs that agent's tool set and system prompt in isolation, and returns a structured
63
+ result. Bindings live in the one `tools` table — there is no `pi` global in the worker — so
64
+ the binding is called as `tools["pi.dispatch"]({ … })` with a single object argument.
64
65
 
65
66
  ```ts
66
67
  // inside a ptc_run_code program
@@ -76,7 +77,9 @@ const result = await tools["pi.dispatch"]({
76
77
  // result.exitCode, result.durationMs, result.stderr?, result.errorMessage?
77
78
  ```
78
79
 
79
- **Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a model-visible lifecycle tool, so the dispatcher handles concurrency the same way it does for any other tool call:
80
+ **Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a
81
+ model-visible lifecycle tool, so the dispatcher handles concurrency the same way it does for
82
+ any other tool call:
80
83
 
81
84
  ```ts
82
85
  const [read, scoutA, scoutB] = await Promise.all([
@@ -86,62 +89,34 @@ const [read, scoutA, scoutB] = await Promise.all([
86
89
  ]);
87
90
  ```
88
91
 
89
- **The child report.** A dispatched child returns more than prose. Under the report contract ([ADR-0032](./docs/adr/0032-child-report.md)) a child hands back a **child report** — a `summary` in its own words, `findings` each carrying the independent thing that supports the claim, the `files_touched` it is sure about, and the token usage **the host measured** (never a number the child made up). The child's prose is kept alongside the report, never replaced by it.
90
-
91
- The report travels one of two channels. It prefers a declared `ptc_child_report` tool, whose payload the host reads back as JSON. If that tool is not available to the child, the host still reads a fenced JSON block from its final message. Either way the result **names the channel that delivered it**:
92
-
93
- ```ts
94
- const r = await tools["pi.dispatch"]({ agent: "scout", task: "survey the auth code" });
95
- if (r.reportChannel === "none") {
96
- // The child ran and did not comply. r.text is its prose; treat it as unbacked.
97
- } else {
98
- for (const f of r.report?.findings ?? []) console.log(f.what, "←", f.evidence);
99
- }
100
- ```
101
-
102
- `reportChannel` is **always present** — `"tool"`, `"prompt-json"` or `"none"` — because a degradation a caller cannot see is a silent failure, and "ran but did not comply" must not read as "returned nothing". `ptc_subagent` renders the same report into the text the model reads, bounded at 20 findings with the withheld count stated in-band.
103
-
104
- The contract is **on by default**. An agent opts out with one line of frontmatter, `childReport: false`, and then its channel reads `"opted-out"` — nobody was asked, which is a different claim from having been asked and ignored.
92
+ **The child report.** A dispatched child returns more than prose: under the report contract
93
+ ([ADR-0032](./docs/adr/0032-child-report.md)) it hands back a `summary`, `findings` each
94
+ carrying independent evidence, `files_touched`, and token usage the host measured. The
95
+ result names the channel that delivered it — `"tool"`, `"prompt-json"`, or `"none"` — and
96
+ `"none"` means the child ran but did not comply, which must not read as "returned nothing".
97
+ An agent opts out with one frontmatter line, `childReport: false`, and its channel reads
98
+ `"opted-out"`. `ptc_subagent` renders the same report into the text the model reads, bounded
99
+ at 20 findings with the withheld count stated in-band.
105
100
 
106
101
  **Bounded.** Three knobs keep fan-out from running away:
107
102
 
108
- - `PtcConfig.dispatchConcurrency` (default **8**) — hard cap on concurrently in-flight dispatch **in one pi session**. It is one counter, not one per run: foreground `pi.dispatch`, the top-level `ptc_subagent` front, and live background children all spend it, and a background child holds its slot for its whole lifetime. The N+1th concurrent call resolves immediately with `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }` instead of queuing or spawning — so a call over the cap is not made to wait for a slot to come back.
109
- - `PtcConfig.maxDispatchDepth` (default **3**) — recursion bound. The child subprocess loads pi-ptc too, so it can write its own PTC programs and call `pi.dispatch` itself; the `childDepth = parentDepth + 1` is rejected when it would exceed `maxDispatchDepth`. The child sees a `<pi-ptc-context depth="N" max-depth="M">…</pi-ptc-context>` hint appended to its system prompt so it can budget its recursion.
110
- - `signal` — when the parent run is cancelled (deadline, abort, user Esc), every in-flight child receives `SIGTERM` followed by `SIGKILL` after a 5-second grace window, the same shape as pi's `examples/extensions/subagent/index.ts` reference.
111
-
112
- **What the concurrency cap now governs, and which knob is live.** The cap is **one counter per pi
113
- session** ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md) §2 as amended,
114
- [ADR-0022](./docs/adr/0022-background-dispatch.md) §9), acquired inside `dispatch()` so a single
115
- owner gates every front. Two consequences are worth stating plainly, because both were measured and
116
- neither is a rounding difference: two programs running concurrently in one session now share 8
117
- rather than 8 each, and a program sharing a session with eight live background children can be
118
- refused **every** foreground slot. The live control is `createBackgroundTaskRuntime({ concurrency })`,
119
- the call that builds that session counter, and the value it is given is `PtcConfig.dispatchConcurrency`.
120
- It is not a background-only knob: changing it changes how many foreground children a whole session
121
- can have in flight.
122
-
123
- The `dispatchConcurrency` a caller passes to `runPtcProgram({ config })` sizes the
124
- dispatcher's own per-run counter, and that counter is only reached when no session counter is
125
- supplied (`dispatcher.ts` hands the binding `options.dispatchDeps?.slots ?? dispatchSlots`). In a
126
- pi session a session counter always is, so the per-run one is not what enforces the cap you are
127
- looking at.
128
-
129
- **Opt out.** Pass an explicit binding subset to `createBuiltinBindings` to opt out — the parallel binding is mixed in only when the caller accepts the default set:
130
-
131
- ```ts
132
- // in a hypothetical runner that wants to keep reads-only:
133
- createBuiltinBindings({ cwd: "/abs/path", names: ["read", "grep"] });
134
- // `pi.dispatch` is NOT in the resulting `tools` table.
135
- ```
136
-
137
- **Not a subagent.** The term _subagent_ is overloaded in this field (DSH's `subagent` is a different thing; pi's `examples/extensions/subagent/` extension is also a different thing). pi-ptc uses _parallel binding_ and _concurrent tool call_ throughout; see `CONTEXT.md` for the canonical terms.
103
+ - `PtcConfig.dispatchConcurrency` (default **8**) — one counter per pi session, spent by
104
+ foreground dispatches, the `ptc_subagent` front, and live background children. A call over
105
+ the cap resolves `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }`
106
+ instead of queueing.
107
+ - `PtcConfig.maxDispatchDepth` (default **3**) — recursion bound; the child sees a
108
+ `<pi-ptc-context depth="N" max-depth="M">` hint so it can budget its own recursion.
109
+ - `signal` — when the parent run is cancelled, every in-flight child gets `SIGTERM` then
110
+ `SIGKILL` after a 5-second grace window.
138
111
 
139
112
  ### Background dispatch
140
113
 
141
- Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to spawn the child and return immediately with a `DispatchHandle` ([ADR-0022](./docs/adr/0022-background-dispatch.md)); the child outlives both the program and the turn:
114
+ Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to
115
+ spawn the child and return immediately with a `DispatchHandle`
116
+ ([ADR-0022](./docs/adr/0022-background-dispatch.md)); the child outlives both the program and
117
+ the turn:
142
118
 
143
119
  ```ts
144
- // inside a ptc_run_code program — bindings are reached as tools["<name>"]
145
120
  const handle = await tools["pi.dispatch"]({
146
121
  agent: "scout",
147
122
  task: "audit the auth code",
@@ -151,15 +126,19 @@ const handle = await tools["pi.dispatch"]({
151
126
  // handle: { taskId: "01J…", label: "auth audit", status: "running" }
152
127
  ```
153
128
 
154
- (The binding's name is `pi.dispatch`; a program reaches it as `tools["pi.dispatch"]`.)
129
+ A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed` / `canceled`
130
+ / `lost`). The model observes it with three always-on tools — they survive `/ptc off` and are
131
+ registered on every surface:
155
132
 
156
- A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed` / `canceled` / `lost`), and the model observes it with three always-on tools — they are not part of the PTC-mode loadout, so `/ptc off` (which only blocks new spawns) does not remove them. A background task is owned by the dispatching pi process (ADR-0023): it survives programs, turns, and `/ptc off`, ends when that session ends or the process dies, and no other pi process in the same directory can reap it (background dispatch children share the session's task storage, so pre-ADR-0023 any same-directory pi process — including a dispatch child itself — could reap every task on startup). Known edges: pre-upgrade ownerless records are still reaped by whichever process binds the directory first; a recycled pid can leave a record `running` after its owner died; and `ptc_task_stop` from another process can write a `stopping` state into your record even though the stop signal itself never crosses the process boundary:
157
-
158
- - `ptc_task_list({ status?, limit? })` — list this session's tasks, newest first (default limit 100).
159
- - `ptc_task_output({ taskId, sinceBytes? })` — read a task's captured output, tail-truncated to pi's 50 KB / 2000-line contract (ADR-0015).
133
+ - `ptc_task_list({ status?, limit? })` — this session's tasks, newest first (default limit 100).
134
+ - `ptc_task_output({ taskId, sinceBytes? })` — a task's captured output, tail-truncated to the
135
+ 50 KB / 2000-line contract (ADR-0015).
160
136
  - `ptc_task_stop({ taskId, reason? })` — ask a running task to stop.
161
137
 
162
- Background tasks count against the same `dispatchConcurrency` (default 8) for their whole lifetime and share the `maxDispatchDepth` (default 3) recursion bound — and since the gate moved into `dispatch()` that cap is the **one session counter** the foreground path uses too, not a second one held beside it. A session running eight long background children therefore has no foreground dispatch headroom left, and a foreground call over the cap is refused outright rather than queued behind them. A pre-spawn refusal (depth or concurrency cap, unknown agent) still comes back as the familiar `DispatchResult` with `status: "rejected"`. Full guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
138
+ Background tasks count against the same `dispatchConcurrency` for their whole lifetime and
139
+ share the `maxDispatchDepth` bound. A background task is owned by the dispatching pi process
140
+ (ADR-0023): it survives programs, turns, and `/ptc off`, and ends when that session ends. Full
141
+ guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
163
142
 
164
143
  ## TUI rendering
165
144
 
@@ -176,167 +155,41 @@ PTC Find AssistantMessageComponent instantiations
176
155
  └─ totalLines: 47
177
156
  ```
178
157
 
179
- The call row is the tool label plus the model's `description`. Under it, the completion value is
180
- shown as a **tree**: an object or array with content gives one row per property (or index), nested
181
- containers recurse behind `├─` / `└─` / `│` connectors, and a small all-scalar container collapses
182
- onto one row (`{file: "a", line: 12}`). Depth caps at 4 levels, 6 children per container and 120
183
- characters per row; whatever is withheld is reported (`…+N more keys`, a trailing `…`). A scalar
184
- value is one line instead — `→ 47`, `→ {}`, `done` when the program returned nothing, or
185
- `failed: <reason>` in red. The run's countable facts — output lines, workflow phases, attached
186
- images, warnings, duration — stay pinned to the right edge of the area's first row. Nothing is ever
187
- printed as escaped JSON. Expanding a row (ctrl+e) adds the code head, phase roll-up, `console.log`
188
- output and plan-drift warnings, each block labelled and capped. `renderShell` stays at pi's default,
189
- so these rows keep the same box and colors as the built-in tools.
190
-
191
- The copy above is the human's. The text block the **model** reads is a separate contract with
192
- separate bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a completion value whose
193
- compact form fits in 100 characters stays on one line, and every line of the assembled block is
194
- capped at 200 characters with a trailing `…`. Those are not the numbers above, and they are not
195
- variants of them. **4 / 6 / 120** bound the on-screen tree — depth, children per container,
196
- characters per row, aligned by visible width — because they serve the eye; **100 / 200** bound the
197
- model's copy because they serve what the model has to read. Neither set derives from the other, so
198
- moving 200 to 120 so they "match" is a behaviour change that needs its own ADR, not an edit to a
199
- number on this page.
158
+ The completion value renders as a **value tree** — one row per property, nested containers
159
+ behind `├─` / `└─` / `│`, capped at 4 levels deep / 6 children per container / 120 characters
160
+ per row, with the withheld amount stated in-band. A scalar is one line (`→ 47`, `done`, or
161
+ `failed: <reason>` in red). The text block the model reads is a separate contract with its own
162
+ bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a compact value under 100
163
+ characters stays on one line, every line caps at 200. Nothing is ever printed as escaped JSON.
200
164
 
201
165
  ## Images
202
166
 
203
- An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to the PTC
204
- tool result as a real image block, so the model sees the picture instead of a marker string or a wall
205
- of base64. This is what DSH does by deferring a context message after the run
206
- ([ADR-0014](./docs/adr/0014-image-hoisting.md)). Nothing is capped or deduped: every image the program's
207
- tool calls produced is attached, in call order, because how much context a run spends is the program's
208
- call. The collapsed row's meta shows the count (`· 1 image`), so the volume is visible without being
209
- policed. The program receives the image either way.
167
+ An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to
168
+ the PTC tool result as a real image block, so the model sees the picture instead of a marker
169
+ string or a wall of base64 ([ADR-0014](./docs/adr/0014-image-hoisting.md)). Nothing is capped
170
+ or deduped: every image the program's tool calls produced is attached, in call order; the
171
+ collapsed row's meta shows the count (`· 1 image`).
210
172
 
211
173
  ## PTC default mode
212
174
 
213
- On a TUI start — install, restart, done — the session narrows its tool loadout so the built-in
214
- tools are reachable only _from inside a program_:
215
-
216
- ```
217
- PTC Verify the inserted image file
218
- → {file, clipNow} • 6 output lines · 1 image · 536ms
219
- ```
220
-
221
- The model calls `ptc_run_code` / `ptc_workflow`, and reaches `read` / `bash` / `edit` / `write` /
222
- `grep` / `find` / `ls` through `tools.<name>(args)` inside the program. Tools contributed by _other_
223
- extensions (`web_search`, `todo`, …) stay directly callable — they cannot become bindings
224
- (`pi.getAllTools()` returns metadata, not `execute`), so hiding one would make it unreachable for
225
- the session. The mode's rationale and rejected alternatives are in [ADR-0010](./docs/adr/0010-ptc-default-mode.md).
226
-
227
- **Turning it off.** For one session: `/ptc off` (and `/ptc on`, `/ptc` for status). Permanently:
228
-
229
- ```jsonc
230
- // ~/.pi/agent/ptc.json
231
- { "defaultMode": false }
232
- ```
233
-
234
- **Choosing the surface.** `defaultMode` decides whether the session _enters_ PTC mode. Which
235
- model-facing tools this package registers is **detected, not configured** — there is no setting for
236
- it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)):
237
-
238
- - `subagents` — `ptc_subagent` plus the three `ptc_task_*` tools, with pi's own `codemode`
239
- doing the orchestration; warns at startup when `codemode` is not in the active tool set and
240
- `ptc_run_code` is not declared to the model either (which on a pi below 0.99.0 it is, since
241
- `exposure` does not exist there and the pair falls back to being model-visible).
242
- `ptc_run_code` / `ptc_workflow` are registered here too, but at `codemode` reach: a `codemode`
243
- script can call them, and the model is not shown them. That is what puts `pi.dispatch` and
244
- background tasks underneath `codemode`, whose sandbox cannot spawn a process itself.
245
- - `full` — the rest: `ptc_run_code` / `ptc_workflow` plus the three `ptc_task_*` tools. Also the
246
- answer to every "the probe could not tell" case.
247
-
248
- **To keep this package out of your sessions, use pi, not this package.** Run `pi config` and
249
- disable this package's extensions there. Measured on pi 1.1.0 against this package's own
250
- `dist/index.js`: a `packages` entry whose `extensions` is `[]` or `["!dist/index.js"]` is not
251
- loaded at all, while `["+dist/index.js"]` and an omitted key are. pi does this **without loading
252
- the extension**, which no value of a key this package reads could achieve — reading the key
253
- requires having run the code that reads it.
254
-
255
- > In the **project-level** `.pi/settings.json`, write `"extensions": ["!dist/index.js"]` rather than
256
- > `"extensions": []`. The two settings files are resolved by two different functions in pi
257
- > (`package-manager.js:1850`): the personal-level path reads `[]` as "load nothing", the
258
- > project-level path reads it as an empty delta, which means "no change".
259
-
260
- **If you had `surfaceMode` in `ptc.json`, it is no longer read** — including `"off"`, so upgrading
261
- brings this package back. At session start you get one `warning` naming the file and pointing here.
262
- Delete the key to silence it.
263
-
264
- **The surface is detected, and it follows three questions, not one**
265
- ([ADR-0026](./docs/adr/0026-surface-default-is-detected.md),
266
- [ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md),
267
- [ADR-0029](./docs/adr/0029-surface-follows-codemode-activation.md)):
268
-
269
- | does this pi ship `codemode`? | will pi load it? | can the model call it? | surface |
270
- | ----------------------------- | ---------------------------------------------- | --------------------------------------------- | ----------- |
271
- | yes | yes (default, or `+builtin:codemode`) | yes (`--tools …,codemode`, or `defaultTools`) | `subagents` |
272
- | yes | yes (default, or `+builtin:codemode`) | no — **the default on a stock install** | `full` |
273
- | yes | no (`-builtin:codemode`, or `--no-extensions`) | — | `full` |
274
- | no | — | — | `full` |
275
-
276
- The third column is the one that decides most sessions, and it is why the answer is `full` on a pi
277
- that has never been configured. pi ships `codemode` and loads it by default, but registers it
278
- **inactive** (`defaultActive: false`) — it joins the model's tool list only when a loadout names it.
279
- Handing orchestration to a tool the model cannot call is the failure this avoids, so `subagents` is
280
- chosen only on positive evidence.
281
-
282
- A probe that cannot answer falls back to `full` — the safe direction, since `subagents` as a
283
- failure mode would take away the orchestration tool the session was relying on. There is no key
284
- that overrides this, by design ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)).
285
-
286
- > **To use the `subagents` surface, put `codemode` in your tool list.** Without that you get
287
- > `ptc_run_code` / `ptc_workflow` and no startup warning, which is the correct answer for a session
288
- > that never asked for delegation:
289
- >
290
- > ```jsonc
291
- > // ~/.pi/agent/settings.json
292
- > { "defaultTools": ["read", "bash", "edit", "write", "+codemode"] }
293
- > ```
294
- >
295
- > or per launch, `pi --tools read,bash,edit,write,codemode`. A list made only of modifiers starts
296
- > from pi's four defaults, so `{ "defaultTools": ["+codemode"] }` means the same thing.
297
-
298
- > Turning pi's `codemode` **off** — `"extensions": ["-builtin:codemode"]`, or launching with
299
- > `--no-extensions` — brings the PTC surfaces back on its own. Before ADR-0027 it did not: the
300
- > detection asked only whether the extension directory exists, so a pi told not to load it still
301
- > counted as an orchestrator and you got `ptc_subagent` with nothing to compose with. The switch is
302
- > read from the same three places pi reads it — the command line, `<cwd>/.pi/settings.json`, and
303
- > `<agentDir>/settings.json` — in the same order. The activation probe does **not** read the
304
- > settings at all: it asks pi for its own tool loadout at session start, by which time pi has
305
- > already applied the command line _and_ its project-trust decision. That matters for a project
306
- > you have not approved — its `defaultTools` is not evidence of anything, because pi is not
307
- > reading it either ([ADR-0035](./docs/adr/0035-ask-pi-for-the-loadout.md)).
308
-
309
- **A detection you cannot see is the failure this design has**, so the result is reported. The
310
- outcome is issued through the TUI notification channel at session start — how the probe came out
311
- and which surface therefore is — but only when the probe could not answer. A pi that ships
312
- `codemode`, loads it, and has it in the tool list is the expected case and says nothing. A second
313
- notice is issued when the probe and pi's own tool registry disagree, which is the case the probe
314
- structurally cannot see: it walks the filesystem, so under `--exclude-tools codemode` it answers
315
- `present` for a tool this session does not have (and the mirror: a restructured `dist` answers
316
- `not-found` for one pi plainly registers). A third notice covers ADR-0027: a settings file that
317
- could not be read. What is **not** established is that either line actually paints in a real pi
318
- TUI: a pty capture at review time showed neither the notice nor a control marker, and a TUI quits
319
- on stdin EOF before a toast renders, so that is an unmeasured end to end rather than a broken one.
320
- No test in this repository observes a notice through a real TUI.
321
-
322
- **On a `--print` session, none of it prints.** `ui.notify` is the TUI channel; measured across
323
- three `--print` runs that each emit one of these notices, stdout and stderr received **0 bytes**
324
- each. That makes this page the only channel on which a `--print` user learns why they got the
325
- surface they got.
326
-
327
- **Where it does not run.** Print / JSON / RPC sessions are left exactly as launched, and so is a
328
- session started with an explicit tool restriction (`--tools`, `--exclude-tools`,
329
- `--no-builtin-tools`, `--no-extensions`) — the extension does not override what you asked for.
330
- `--no-extensions` does, however, change the **detected surface**: pi's own `codemode` is a built-in
331
- extension, so turning extensions off means it will not load, and ADR-0027's table resolves to
332
- `full` — you keep `ptc_run_code` / `ptc_workflow` rather than a `ptc_subagent` with
333
- nothing to compose with. If another extension changes the tool set while the mode is on, the mode
334
- yields and tells you.
335
-
336
- **The important consequence:** in a TUI session, bindings come from the loadout recorded _before_
337
- the mode narrowed it. That is what keeps `tools.read(…)` working — and it is why a `--tools`
338
- restriction still holds: the snapshot is read from `pi.getActiveTools()`, so it can never contain
339
- tools your session was not launched with.
175
+ On a TUI start the session narrows its tool loadout so the built-in tools are reachable only
176
+ _from inside a program_: the model calls `ptc_run_code` / `ptc_workflow` (or `ptc_subagent`)
177
+ and reaches `read` / `bash` / `edit` / `write` / `grep` / `find` / `ls` through
178
+ `tools.<name>(args)` inside it. Tools contributed by _other_ extensions (`web_search`, `todo`,
179
+ …) stay directly callable. Turn it off for one session with `/ptc off` (also `/ptc on`, `/ptc`
180
+ for status), permanently with `{ "defaultMode": false }` in `~/.pi/agent/ptc.json`.
181
+
182
+ Which model-facing tools this package registers is **detected, not configured** — there is no
183
+ setting for it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)). The short version:
184
+ a pi whose `codemode` is loaded _and_ callable by the model gets the `subagents` surface
185
+ (`ptc_subagent` on top; the program pair underneath at `codemode` reach); every other pi gets
186
+ `full` (`ptc_run_code` / `ptc_workflow` model-visible). To use the `subagents` surface, put
187
+ `codemode` in your tool list (`pi --tools read,bash,edit,write,codemode`, or `"defaultTools":
188
+ ["read", "bash", "edit", "write", "+codemode"]`). To keep this package out of your sessions
189
+ entirely, run `pi config` and disable its extensions there.
190
+
191
+ The full detection table, startup notices, and edge cases (`--print`, `--no-extensions`,
192
+ untrusted projects) live in [docs/usage/surface.md](./docs/usage/surface.md).
340
193
 
341
194
  ## Trust posture (read me)
342
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ptc-subagents",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Programmable tool calling (PTC) and subagent fan-out for pi — the model writes JS/TS programs that call pi's tools, or fans out to a fresh pi subprocess per subagent task",
5
5
  "keywords": [
6
6
  "pi",