pi-ptc-subagents 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ 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.3] - 2026-10-12
9
+
10
+ ### Fixed
11
+
12
+ - **`ptc_subagent` was registered, described to the model, and still uncallable — with nothing
13
+ saying so.** On a `subagents` surface the tool needs at least one agent definition under
14
+ `~/.pi/agent/agents/<name>.md` or `<project>/.pi/agents/<name>.md`, and there is no default agent
15
+ (ADR-0016). With an empty registry the tool was live and inert: a real 920-message session made
16
+ 520 tool calls and used it zero times, the model decided subagents were right, spent twenty-plus
17
+ calls hunting the install for definitions that were not there, and gave up. `session_start` now
18
+ reports it — naming both directories and the file shape — and the same fact is painted on the
19
+ `ptc-mode` status, so it is still readable after the notice is gone.
20
+ - **Agent files saved with CRLF, a UTF-8 BOM, or a blank line before the delimiter parsed as
21
+ "no such agent".** The frontmatter pattern matched exactly one shape, and a `null` return reads,
22
+ from every layer above, as an unregistered name. The body is normalised after the match rather
23
+ than inside it, so the system prompt handed to a child carries no stray carriage returns. A file
24
+ that exists but cannot be parsed is now also distinguishable from one that is absent.
25
+ - **`ptc_subagent`'s description states where definitions load from**, and that there is no default
26
+ agent — it points at the refusal rather than restating it, so the actionable detail keeps one
27
+ home (the renderer, wired to the same resolver a dispatch uses).
28
+
29
+ ### Added
30
+
31
+ - **A real-process smoke test**, with a fake `pi` on `PATH` rather than a stubbed lifecycle: the
32
+ spawn, the `--append-system-prompt` materialisation, the stdout parsing and the exit code are
33
+ all exercised. Both directions are asserted — an empty registry reports, and a registered agent
34
+ dispatches to completion — because either alone passes against a build that is broken in the
35
+ other direction.
36
+ - **A `能力的可用前提` review dimension**, asking when a user learns a tool's precondition is
37
+ unmet, with "never" as the answer that blocks.
38
+
39
+ ## [2.0.2] - 2026-10-10
40
+
41
+ ### Changed
42
+
43
+ - **The README leads with what the package is instead of how it works.** The opening now
44
+ names the two capabilities up front — subagent fan-out and programmable tool calling —
45
+ and states the relationship with pi's own `codemode` in the first screen. The surface
46
+ detection deep-dive (three-question table, startup notices, `--print` / `--no-extensions`
47
+ edge cases) moves out of the README to
48
+ [docs/usage/surface.md](./docs/usage/surface.md), which now owns it; the README keeps
49
+ the short version and a pointer. Everything else is tightened, not deleted.
50
+ - **The built-in tool enumeration no longer claims a count pi moved past.** The README used
51
+ to say "all seven built-ins"; pi 1.1.0 ships eight (`powershell` joined), so the copy now
52
+ enumerates the tools this package binds (`read`, `bash`, `edit`, `write`, `grep`, `find`,
53
+ `ls`) without claiming that is pi's full set. Docs only; no code change — note the
54
+ binding list itself is unchanged, and `powershell` is currently not reachable from PTC
55
+ programs even when enabled (tracked separately).
56
+
57
+ [2.0.3]: https://github.com/a1121611810/pi-ptc-subagents/compare/v2.0.2...v2.0.3
58
+ [2.0.2]: https://github.com/a1121611810/pi-ptc-subagents/compare/v2.0.1...v2.0.2
59
+
8
60
  ## [2.0.1] - 2026-10-10
9
61
 
10
62
  ### 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,48 @@ 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
- ## Tools
38
+ **What that command actually writes is in [docs/how-to-install.md](./docs/how-to-install.md)** — worth reading before you install, if you would rather know where the settings entry goes.
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
+ Documentation site: **[pi-ptc-subagents on GitHub Pages](https://a1121611810.github.io/pi-ptc-subagents/)**. Every page under its Documentation section is projected at build time from the files in `docs/`, so it never says anything this README does not.
52
41
 
53
- Long output follows pi's own truncation contract ([ADR-0015](./docs/adr/0015-pi-truncation-contract.md)):
42
+ ## Tools
54
43
 
55
- the text block keeps the tail (50 KB / 2000 lines) and the untruncated text is written to a temp file
44
+ - `ptc_subagent` — the top-level subagent face: dispatch a fresh `pi` subprocess for a task
45
+ without writing a program. Registered only when the detected surface is `subagents`
46
+ ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); on `full` the same
47
+ capability is the `pi.dispatch` binding inside a program.
48
+ - `ptc_run_code` — run a JS/TS program that composes tool calls; the program reaches the
49
+ session's enabled built-in tools as `tools.read(...)`, `tools.write(...)`, etc.
50
+ (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), plus `tools["pi.dispatch"]`;
51
+ its return value and `console.log` output are reported back.
52
+ - `ptc_workflow` — structured variant with `meta` + plain-JSON `args`, plus the workflow
53
+ helpers (`log`, `phase`, `parallel`, `pipeline`). There is no `agent()` helper on either
54
+ surface.
55
+ - `ptc_task_list` / `ptc_task_output` / `ptc_task_stop` — manage background dispatches
56
+ (see [Background dispatch](#background-dispatch)). They stay available when PTC mode is off.
56
57
 
57
- the next program can `tools.read`; the collapsed row then shows `truncated` in its meta.
58
+ Long output follows pi's own truncation contract ([ADR-0015](./docs/adr/0015-pi-truncation-contract.md)):
59
+ the text block keeps the tail (50 KB / 2000 lines), the untruncated text is written to a temp
60
+ file the next program can `tools.read`, and the collapsed row shows `truncated` in its meta.
58
61
 
59
62
  ## Dispatch (fan-out to per-call pi subprocesses)
60
63
 
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.
64
+ PTC programs can spawn a fresh `pi` subprocess per call via the **`pi.dispatch(...)`** binding
65
+ ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md)) — the child loads the named agent's
66
+ markdown, runs that agent's tool set and system prompt in isolation, and returns a structured
67
+ result. Bindings live in the one `tools` table — there is no `pi` global in the worker — so
68
+ the binding is called as `tools["pi.dispatch"]({ … })` with a single object argument.
64
69
 
65
70
  ```ts
66
71
  // inside a ptc_run_code program
@@ -76,7 +81,9 @@ const result = await tools["pi.dispatch"]({
76
81
  // result.exitCode, result.durationMs, result.stderr?, result.errorMessage?
77
82
  ```
78
83
 
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:
84
+ **Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a
85
+ model-visible lifecycle tool, so the dispatcher handles concurrency the same way it does for
86
+ any other tool call:
80
87
 
81
88
  ```ts
82
89
  const [read, scoutA, scoutB] = await Promise.all([
@@ -86,62 +93,34 @@ const [read, scoutA, scoutB] = await Promise.all([
86
93
  ]);
87
94
  ```
88
95
 
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.
96
+ **The child report.** A dispatched child returns more than prose: under the report contract
97
+ ([ADR-0032](./docs/adr/0032-child-report.md)) it hands back a `summary`, `findings` each
98
+ carrying independent evidence, `files_touched`, and token usage the host measured. The
99
+ result names the channel that delivered it — `"tool"`, `"prompt-json"`, or `"none"` — and
100
+ `"none"` means the child ran but did not comply, which must not read as "returned nothing".
101
+ An agent opts out with one frontmatter line, `childReport: false`, and its channel reads
102
+ `"opted-out"`. `ptc_subagent` renders the same report into the text the model reads, bounded
103
+ at 20 findings with the withheld count stated in-band.
105
104
 
106
105
  **Bounded.** Three knobs keep fan-out from running away:
107
106
 
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.
107
+ - `PtcConfig.dispatchConcurrency` (default **8**) — one counter per pi session, spent by
108
+ foreground dispatches, the `ptc_subagent` front, and live background children. A call over
109
+ the cap resolves `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }`
110
+ instead of queueing.
111
+ - `PtcConfig.maxDispatchDepth` (default **3**) — recursion bound; the child sees a
112
+ `<pi-ptc-context depth="N" max-depth="M">` hint so it can budget its own recursion.
113
+ - `signal` — when the parent run is cancelled, every in-flight child gets `SIGTERM` then
114
+ `SIGKILL` after a 5-second grace window.
138
115
 
139
116
  ### Background dispatch
140
117
 
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:
118
+ Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to
119
+ spawn the child and return immediately with a `DispatchHandle`
120
+ ([ADR-0022](./docs/adr/0022-background-dispatch.md)); the child outlives both the program and
121
+ the turn:
142
122
 
143
123
  ```ts
144
- // inside a ptc_run_code program — bindings are reached as tools["<name>"]
145
124
  const handle = await tools["pi.dispatch"]({
146
125
  agent: "scout",
147
126
  task: "audit the auth code",
@@ -151,15 +130,19 @@ const handle = await tools["pi.dispatch"]({
151
130
  // handle: { taskId: "01J…", label: "auth audit", status: "running" }
152
131
  ```
153
132
 
154
- (The binding's name is `pi.dispatch`; a program reaches it as `tools["pi.dispatch"]`.)
155
-
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:
133
+ A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed` / `canceled`
134
+ / `lost`). The model observes it with three always-on tools — they survive `/ptc off` and are
135
+ registered on every surface:
157
136
 
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).
137
+ - `ptc_task_list({ status?, limit? })` — this session's tasks, newest first (default limit 100).
138
+ - `ptc_task_output({ taskId, sinceBytes? })` — a task's captured output, tail-truncated to the
139
+ 50 KB / 2000-line contract (ADR-0015).
160
140
  - `ptc_task_stop({ taskId, reason? })` — ask a running task to stop.
161
141
 
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).
142
+ Background tasks count against the same `dispatchConcurrency` for their whole lifetime and
143
+ share the `maxDispatchDepth` bound. A background task is owned by the dispatching pi process
144
+ (ADR-0023): it survives programs, turns, and `/ptc off`, and ends when that session ends. Full
145
+ guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
163
146
 
164
147
  ## TUI rendering
165
148
 
@@ -176,167 +159,41 @@ PTC Find AssistantMessageComponent instantiations
176
159
  └─ totalLines: 47
177
160
  ```
178
161
 
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.
162
+ The completion value renders as a **value tree** — one row per property, nested containers
163
+ behind `├─` / `└─` / `│`, capped at 4 levels deep / 6 children per container / 120 characters
164
+ per row, with the withheld amount stated in-band. A scalar is one line (`→ 47`, `done`, or
165
+ `failed: <reason>` in red). The text block the model reads is a separate contract with its own
166
+ bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a compact value under 100
167
+ characters stays on one line, every line caps at 200. Nothing is ever printed as escaped JSON.
200
168
 
201
169
  ## Images
202
170
 
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.
171
+ An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to
172
+ the PTC tool result as a real image block, so the model sees the picture instead of a marker
173
+ string or a wall of base64 ([ADR-0014](./docs/adr/0014-image-hoisting.md)). Nothing is capped
174
+ or deduped: every image the program's tool calls produced is attached, in call order; the
175
+ collapsed row's meta shows the count (`· 1 image`).
210
176
 
211
177
  ## PTC default mode
212
178
 
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.
179
+ On a TUI start the session narrows its tool loadout so the built-in tools are reachable only
180
+ _from inside a program_: the model calls `ptc_run_code` / `ptc_workflow` (or `ptc_subagent`)
181
+ and reaches `read` / `bash` / `edit` / `write` / `grep` / `find` / `ls` through
182
+ `tools.<name>(args)` inside it. Tools contributed by _other_ extensions (`web_search`, `todo`,
183
+ …) stay directly callable. Turn it off for one session with `/ptc off` (also `/ptc on`, `/ptc`
184
+ for status), permanently with `{ "defaultMode": false }` in `~/.pi/agent/ptc.json`.
185
+
186
+ Which model-facing tools this package registers is **detected, not configured** — there is no
187
+ setting for it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)). The short version:
188
+ a pi whose `codemode` is loaded _and_ callable by the model gets the `subagents` surface
189
+ (`ptc_subagent` on top; the program pair underneath at `codemode` reach); every other pi gets
190
+ `full` (`ptc_run_code` / `ptc_workflow` model-visible). To use the `subagents` surface, put
191
+ `codemode` in your tool list (`pi --tools read,bash,edit,write,codemode`, or `"defaultTools":
192
+ ["read", "bash", "edit", "write", "+codemode"]`). To keep this package out of your sessions
193
+ entirely, run `pi config` and disable its extensions there.
194
+
195
+ The full detection table, startup notices, and edge cases (`--print`, `--no-extensions`,
196
+ untrusted projects) live in [docs/usage/surface.md](./docs/usage/surface.md).
340
197
 
341
198
  ## Trust posture (read me)
342
199