pi-ptc-subagents 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,221 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [1.3.0] - 2026-10-02
11
+
12
+ ### Fixed
13
+
14
+ - **A failing `bash` binding no longer resolves as a success on pi 1.0.0.** pi 1.0.0 added a
15
+ non-throwing failure channel — `AgentToolResult.isError`: "Report a failure without throwing. The
16
+ model sees `content` as an error result, like a thrown error" — and `bash` moved its non-zero exit
17
+ onto it, so `tools.bash({ command: "exit 3" })` inside a PTC program **resolved** where on 0.86.1
18
+ it threw. The binding wrapper forwarded only `{ content, details }`, so `isError` was dropped at
19
+ the boundary and the failure crossed into the program as a successful resolution whose only tell
20
+ was a sentence at the end of its stdout. The binding layer now translates `isError` back into the
21
+ rejection ADR-0024's contract already promises, which is what keeps a failing call from being
22
+ silent.
23
+ - **Disabling pi's `codemode` now brings the PTC surfaces back.** The detected surface asked one
24
+ question — does this pi ship a `codemode` extension directory — and since pi 0.99.0 that is no
25
+ longer the same as the one that matters. A user who put `"extensions": ["-builtin:codemode"]` in
26
+ their settings, or launched with `--no-extensions`, turned the orchestrator off and this package
27
+ kept handing the orchestration to it: `ptc_run_code` and `ptc_workflow` stayed unregistered and
28
+ the session was left with `ptc_subagent` and nothing to compose with. Nothing errored; two tools
29
+ were simply missing.
30
+
31
+ A second probe now reads the **switch** — whether pi will actually load the extension — from the
32
+ same three places pi reads it and in the same order (command line, `<cwd>/.pi/settings.json`,
33
+ `<agentDir>/settings.json`), and the detected default follows both questions:
34
+
35
+ | ships `codemode`? | will load it? | surface |
36
+ | ----------------- | ------------------------------------------- | ----------- |
37
+ | yes | yes (default, or `+builtin:codemode`) | `subagents` |
38
+ | yes | no (`-builtin:codemode`, `--no-extensions`) | `full` |
39
+ | no | — | `full` |
40
+
41
+ An explicit `surfaceMode` still wins; a settings file that cannot be read as a JSON object still
42
+ falls back to the next source rather than half-applying. Two notices are added at session start:
43
+ one naming an unreadable settings file, one reporting that a pinned `surfaceMode` disagrees with
44
+ the table. [ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md)
45
+
46
+ Verified on a real 1.0.0 install for the three reachable cells, asserting on the registered tool
47
+ set rather than on the resolver: `-builtin:codemode` → `ptc_run_code` + `ptc_workflow`; no switch
48
+ entry → `ptc_subagent`; `+builtin:codemode` → `ptc_subagent`. The fourth cell (no `codemode` on
49
+ disk at all) is not reachable on a machine with 1.0 installed and is covered by unit tests only.
50
+
51
+ ### Added
52
+
53
+ - **The four model-facing tools declare a structured result, so pi's `codemode` can consume them.**
54
+ A script calling a tool used to get back prose: `ptc_subagent` returned
55
+ `"Started background task 01J…"` with the id embedded, and the `ptc_task_*` tools returned
56
+ newline-joined lines to re-parse. `ptc_subagent` is the acute case — it is the only way a
57
+ `subagents`-surface session can start a subagent at all, because pi's QuickJS sandbox has no file
58
+ system, no network and no `child_process`.
59
+
60
+ All four now declare an `outputSchema` and return a matching `structuredContent`, which codemode
61
+ scripts receive instead of the text. The shape is a lean projection rather than a mirror of
62
+ `details`: `ptc_task_list` mirroring `TaskRecord[]` would push ~200 KB of per-record
63
+ `outputPreview` into a sandbox whose purpose is to keep intermediate data away from the model.
64
+ `content` and `details` are byte-identical to before on every path, and the model still sees
65
+ exactly the same text — `structuredContent` is documented as "not sent to the model".
66
+ [ADR-0028](./docs/adr/0028-structured-results-for-codemode.md); usage in
67
+ [Structured results for codemode](./docs/usage/structured-results.md).
68
+
69
+ ### Changed
70
+
71
+ - **The codemode switch now mirrors pi's own matchers instead of approximating them.** Reading the
72
+ `!` bucket by comparing a pattern's literal prefix was the wrong SHAPE of fix: the bucket is a
73
+ continuum (globs, character classes, extglobs, nested negations, backslash escapes, multi-segment
74
+ paths), so every review round found one more member — four costful defects in four rounds, and a
75
+ 412-case sweep of the prefix version still had 24 divergences. A case list cannot be finished.
76
+ `resolveCodemodeSwitch` now calls the same `minimatch` pi does, through the same
77
+ `matchesAnyPattern` / `normalizeExactPattern` helpers and with pi's own `baseDir` per scope, which
78
+ adds one small runtime dependency (`minimatch ^10.2.6`, the version pi itself pins) and **shortens**
79
+ the code. A 192-case sweep of the mirror has zero divergences in either direction, and all eight
80
+ reachable cells agree with a real pi 1.0.0 end to end.
81
+ - **The dev toolchain now compiles against pi 1.0.0** rather than 0.86.1 / 0.87.0, so the type check
82
+ and the whole suite run against the pi this package is actually used with. `peerDependencies`
83
+ already declared `>=0.86.0`; that claim had no evidence behind it until now. Two consequences of
84
+ the bump are fixed in the same change: pi narrowed a tool's fifth `execute` parameter from
85
+ `ExtensionContext` to `ExtensionToolContext` (which adds `tools` and `executeTool`), and the
86
+ `isError` channel above.
87
+
88
+ ## [1.2.1] - 2026-09-30
89
+
90
+ ### Fixed
91
+
92
+ - **The `ptc_subagent` tests no longer depend on a pi agent being registered.**
93
+ Five tests reached for pi's `__smoke_echo` agent, which does not resolve on every pi build,
94
+ so the suite was green on one platform and red on the release runner. The fixture is now
95
+ written by the test into `<cwd>/.pi/agents`, the path the agent resolver actually reads at
96
+ project scope, and every dispatch asks for project scope explicitly rather than inheriting
97
+ the `user` default, which points at the real `~/.pi`. Verified with an empty `HOME`: the whole
98
+ suite is green with no ambient agent state at all.
99
+
100
+ No product code changed in this release.
101
+
102
+ ## [1.2.0] - 2026-09-30
103
+
104
+ ### Added
105
+
106
+ - **The surface default is detected from the pi that loaded us (ADR-0026).** With no
107
+ `surfaceMode` set, a pi that ships its own `codemode` resolves to `subagents` and a pi that
108
+ does not resolves to `full`. Setting the key always wins, and a probe that cannot answer
109
+ falls back to `full` rather than guessing.
110
+
111
+ This is the one behaviour change that is not invisible on upgrade: on pi 0.99.1 or newer, a
112
+ user who has never set `surfaceMode` moves from `full` to `subagents`. `codemode` ships
113
+ inactive (`defaultActive: false`), so such a session has a subagent front and no active
114
+ orchestrator until codemode is added to the tool list -- the startup warning says so. Set
115
+ `{ "surfaceMode": "full" }` to keep today's tools.
116
+
117
+ Detection is a filesystem probe over `process.argv[1]`, because pi's own tool listing is
118
+ unavailable at factory time: `getAllTools()` is a `notInitialized` stub until `bindCore`
119
+ runs, and a throwing factory makes the extension fail to load rather than return an empty
120
+ list.
121
+
122
+ A detection the user cannot see is the one failure this design has, so the outcome is
123
+ reported: at session start, through the TUI notification channel, a session with no
124
+ `surfaceMode` set is told how the probe came out whenever it could not answer, and which
125
+ surface the default therefore is. The expected case — a pi that ships `codemode`, detected as
126
+ `subagents` — stays silent. A second notice covers the case the probe structurally cannot
127
+ see: it walks the filesystem, so under `--no-extensions` or `--exclude-tools codemode` it
128
+ answers `present` for a tool the session does not have. `session_start` cross-checks the
129
+ probe against pi's own `getAllTools()` and reports a disagreement in either direction.
130
+
131
+ Both notices are `ui.notify`, which is TUI-only — and that gap is **new here**, not inherited:
132
+ measured, three `--print` runs that each emit one produced 0 bytes on stdout and 0 on
133
+ stderr. The README is the only channel on which a `--print` user learns why they got the
134
+ surface they got. What is established is that the outcome is issued through the documented
135
+ TUI channel; no test observes either notice end to end through a real pi TUI.
136
+
137
+ - **The model-facing surface is now a setting, and a subagent can be started
138
+ without writing a program (ADR-0025).** A new `surfaceMode` key in
139
+ `~/.pi/agent/ptc.json` decides what this extension registers: `full`
140
+ (today's behaviour, and the detected default on a pi without `codemode`)
141
+ registers `ptc_run_code`, `ptc_workflow` and the `ptc_task_*` trio;
142
+ `subagents` registers a new top-level `ptc_subagent`
143
+ plus the `ptc_task_*` trio and leaves orchestration to pi's own `codemode`;
144
+ `off` registers nothing at all, so the session is stock pi. The new tool takes
145
+ the same arguments as the `pi.dispatch` binding and calls the same dispatcher,
146
+ so depth, concurrency and the background task lifecycle behave identically and
147
+ a task spawned through it is listable with `ptc_task_list`.
148
+
149
+ Motivation, measured rather than assumed: pi 0.99.1 ships its own programmable
150
+ tool calling as the built-in `codemode`, which is stronger than `ptc_run_code` at
151
+ isolation and tool discovery but cannot spawn a process at all. With both
152
+ installed the model is taught two programming models per request
153
+ (`docs/research/codemode-vs-ptc-capability-20260930.md`).
154
+
155
+ - **The PTC tool descriptions now declare what a binding call resolves to
156
+ (ADR-0024).** `ptc_run_code` and `ptc_workflow` carry one shared binding
157
+ contract: a binding result is `{ content, details }`, `content` is an array of
158
+ content blocks (the text of a text-file result is `result.content[0].text`),
159
+ `details` is an object or `null`, and there is no `files` / `output` /
160
+ `matches` / `entries` field to read — `bash`, `grep`, `find` and `ls` return one text block
161
+ of newline-separated rows that the program splits itself. A `builtin binding`
162
+ that fails rejects with `ToolCallError`; `pi.dispatch` resolves to a
163
+ `DispatchResult` with `text` and `status` and no `content`.
164
+
165
+ Model-facing only: the wire, the worker and every existing program are
166
+ unchanged. Field report before: a pty-driven run of the real TUI with PTC mode
167
+ on produced 31 program crashes across 16 runs, the largest error class being the
168
+ model treating a binding result as a string or as an object with a `files`
169
+ field — neither is ever true, and nothing said so
170
+ (`docs/research/ptc-binding-contract-measurement-20260930.md`).
171
+
172
+ Re-measured after the change, same harness and tasks: **0 of those three crash
173
+ classes in 16 runs**, the correct `result.content[0].text` access in 16 of 16,
174
+ and the context median for a PTC run down from 36,669 to 8,360 with median
175
+ turns from 8 to 2. Exact-match rate is not claimed to have improved — it moved
176
+ 10/16 to 11/16 while the control arm moved 15/16 to 13/16, which is this
177
+ harness's noise floor
178
+ (`docs/research/ptc-binding-contract-re-measurement-20260930.md`).
179
+
180
+ ### Changed
181
+
182
+ - **The dispatch concurrency cap is one counter per session, not one per run.** The acquire
183
+ moved out of the dispatcher and into `dispatch()`, so a single `DispatchSlotCounter` now
184
+ serves every front: concurrent programs in the same session, the `ptc_subagent` tool, and
185
+ background children. The value is still `PtcConfig.dispatchConcurrency` (default 8) and the
186
+ refusal shape is unchanged; what changed is whose calls the cap counts.
187
+
188
+ This reduces effective concurrency in two measurable ways, and neither is a rounding
189
+ difference — 24 concurrent foreground calls against a real fake-`pi` spawn:
190
+
191
+ - two concurrent programs that could each have 8 in flight now share 8 (16 spawned -> 8);
192
+ - a program sharing a session with 8 live background children can now be refused **every**
193
+ foreground slot (8 foreground spawned -> 0). The refusal is a hard reject with no queue, so
194
+ an over-cap call is not parked behind a long-running child.
195
+
196
+ A single program on its own is unchanged (8 of 24, before and after), which is why the
197
+ change passed three review rounds. ADR-0016 §2 and ADR-0022 §9 are amended; the depth cap is
198
+ not.
199
+
200
+ The knob is now live rather than dead: the live control is
201
+ `createBackgroundTaskRuntime({ concurrency })`, which sizes the session counter from
202
+ `PtcConfig.dispatchConcurrency`. Passing `dispatchConcurrency` through
203
+ `runPtcProgram({ config })` no longer sizes the cap a pi session uses (measured: 2
204
+ configured, 8 dispatched).
205
+
206
+ ### Fixed
207
+
208
+ - **A background task that produced no answer is no longer reported as a
209
+ success (#70, field report).** A child that hits a rate limit or a model
210
+ error still exits 0: pi writes the reason onto the assistant `message_end`
211
+ as `stopReason: "error"`, retries, and closes clean. The background pump
212
+ read the exit code alone, so such a task was recorded `succeeded` and the
213
+ model was told it had worked — while `ptc_task_output` answered "(no output
214
+ yet; task X is succeeded)". `succeeded` now requires the child to exit 0
215
+ **and** to have produced assistant text, which is the rule
216
+ `decideCloseOutcome` already applied to the foreground path; one failure now
217
+ has one verdict and one sentence in both paths. A failing `resolve-exit`
218
+ writes `errorMessage`, and the child's own `stopReason: "error"` text is
219
+ carried onto the record so the model is told _why_, not only _that_
220
+ (ADR-0022 §2, amended). A non-zero exit is still left unlabelled and a model
221
+ stop still resolves `canceled` — neither is relabelled by this change.
222
+
8
223
  ## [1.1.1] - 2026-09-30
9
224
 
10
225
  ### Fixed
package/README.md CHANGED
@@ -74,10 +74,27 @@ const [read, scoutA, scoutB] = await Promise.all([
74
74
 
75
75
  **Bounded.** Three knobs keep fan-out from running away:
76
76
 
77
- - `PtcConfig.dispatchConcurrency` (default **8**) — hard cap on concurrently in-flight `pi.dispatch` calls per run. The N+1th concurrent call resolves immediately with `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }` instead of queuing or spawning.
77
+ - `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.
78
78
  - `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.
79
79
  - `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.
80
80
 
81
+ **What the concurrency cap now governs, and which knob is live.** The cap is **one counter per pi
82
+ session** ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md) §2 as amended,
83
+ [ADR-0022](./docs/adr/0022-background-dispatch.md) §9), acquired inside `dispatch()` so a single
84
+ owner gates every front. Two consequences are worth stating plainly, because both were measured and
85
+ neither is a rounding difference: two programs running concurrently in one session now share 8
86
+ rather than 8 each, and a program sharing a session with eight live background children can be
87
+ refused **every** foreground slot. The live control is `createBackgroundTaskRuntime({ concurrency })`,
88
+ the call that builds that session counter, and the value it is given is `PtcConfig.dispatchConcurrency`.
89
+ It is not a background-only knob: changing it changes how many foreground children a whole session
90
+ can have in flight.
91
+
92
+ The `dispatchConcurrency` a caller passes to `runPtcProgram({ config })` sizes the
93
+ dispatcher's own per-run counter, and that counter is only reached when no session counter is
94
+ supplied (`dispatcher.ts` hands the binding `options.dispatchDeps?.slots ?? dispatchSlots`). In a
95
+ pi session a session counter always is, so the per-run one is not what enforces the cap you are
96
+ looking at.
97
+
81
98
  **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:
82
99
 
83
100
  ```ts
@@ -111,7 +128,7 @@ A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed`
111
128
  - `ptc_task_output({ taskId, sinceBytes? })` — read a task's captured output, tail-truncated to pi's 50 KB / 2000-line contract (ADR-0015).
112
129
  - `ptc_task_stop({ taskId, reason? })` — ask a running task to stop.
113
130
 
114
- Background tasks count against the same `dispatchConcurrency` (default 8) for their whole lifetime and share the `maxDispatchDepth` (default 3) recursion bound. 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).
131
+ 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).
115
132
 
116
133
  ## TUI rendering
117
134
 
@@ -183,10 +200,80 @@ the session. The mode's rationale and rejected alternatives are in [ADR-0010](./
183
200
  { "defaultMode": false }
184
201
  ```
185
202
 
203
+ **Choosing the surface.** `defaultMode` decides whether the session _enters_ PTC mode; `surfaceMode`
204
+ decides which model-facing tools this package registers at all. It is read once, at startup, so a
205
+ surface change needs a new session ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)):
206
+
207
+ ```jsonc
208
+ // ~/.pi/agent/ptc.json
209
+ { "surfaceMode": "subagents" }
210
+ ```
211
+
212
+ - `off` — a stock pi session: no tool, no `/ptc` command, no briefing.
213
+ - `subagents` — `ptc_subagent` plus the three `ptc_task_*` tools, with pi's own `codemode`
214
+ doing the orchestration; warns at startup when `codemode` is not in the active tool set.
215
+ - `full` — today's set: `ptc_run_code` / `ptc_workflow` plus the three `ptc_task_*` tools.
216
+
217
+ **The default is detected, and it is not `full` everywhere.** With no `surfaceMode` key, the surface
218
+ follows two questions, not one ([ADR-0026](./docs/adr/0026-surface-default-is-detected.md),
219
+ [ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md)):
220
+
221
+ | does this pi ship `codemode`? | will pi load it? | surface |
222
+ | ----------------------------- | ---------------------------------------------- | ----------- |
223
+ | yes | yes (default, or `+builtin:codemode`) | `subagents` |
224
+ | yes | no (`-builtin:codemode`, or `--no-extensions`) | `full` |
225
+ | no | — | `full` |
226
+
227
+ Setting the key always wins. A probe that cannot answer falls back to `full` — the safe direction,
228
+ since `subagents` as a failure mode would take away the orchestration tool the session was relying
229
+ on.
230
+
231
+ > Upgrading onto pi 0.99.1 or newer without setting the key moves you to `subagents`. `codemode`
232
+ > ships **inactive** (`defaultActive: false`), so until you add it to your tool list you get a
233
+ > subagent front with no orchestrator, and the startup warning says so. If you would rather keep
234
+ > today's tools, set `{ "surfaceMode": "full" }`.
235
+
236
+ > Turning pi's `codemode` **off** — `"extensions": ["-builtin:codemode"]`, or launching with
237
+ > `--no-extensions` — brings the PTC surfaces back on its own. Before ADR-0027 it did not: the
238
+ > detection asked only whether the extension directory exists, so a pi told not to load it still
239
+ > counted as an orchestrator and you got `ptc_subagent` with nothing to compose with. The switch is
240
+ > read from the same three places pi reads it — the command line, `<cwd>/.pi/settings.json`, and
241
+ > `<agentDir>/settings.json` — in the same order.
242
+
243
+ No file, or a value outside that set, falls back to the detected default and says so at startup
244
+ rather than half-applying: which tools exist is not something to change on a guess.
245
+
246
+ **A detection you cannot see is the failure this design has**, so the result is reported. With no
247
+ `surfaceMode` key, the outcome is issued through the TUI notification channel at session start —
248
+ how the probe came out and which surface the default therefore is — but only when the probe could
249
+ not answer. A pi that ships `codemode` and is detected as `subagents` is the expected case and says
250
+ nothing. A second notice is issued when the probe and pi's own tool registry disagree, which is the
251
+ case the probe structurally cannot see: it walks the filesystem, so under `--exclude-tools codemode`
252
+ it answers `present` for a tool this session does not have (and the mirror: a restructured `dist`
253
+ answers `not-found` for one pi plainly registers). A third notice covers ADR-0027: a settings file
254
+ that could not be read, and an explicit `surfaceMode` that disagrees with the table — the pinned
255
+ value still wins, and the notice only says so. What is **not**
256
+ established is that either line actually paints in a real pi TUI: a pty capture at review time
257
+ showed neither the notice nor a control marker, and a TUI quits on stdin EOF before a toast
258
+ renders, so that is an unmeasured end to end rather than a broken one. No test in this repository
259
+ observes a notice through a real TUI. If you are relying on the notice rather than on your own
260
+ `surfaceMode` key, verify it once.
261
+
262
+ **On a `--print` session, none of it prints.** `ui.notify` is the TUI channel; measured across
263
+ three `--print` runs that each emit one of these notices, stdout and stderr received **0 bytes**
264
+ each. That makes this page the only channel on which a `--print` user learns why they got the
265
+ surface they got — ADR-0025's decision-4 warning shares the gap, and there the answer is the same
266
+ one line of JSON: set `surfaceMode` yourself and the detection no longer matters.
267
+
186
268
  **Where it does not run.** Print / JSON / RPC sessions are left exactly as launched, and so is a
187
269
  session started with an explicit tool restriction (`--tools`, `--exclude-tools`,
188
- `--no-builtin-tools`) — the extension does not override what you asked for. If another extension
189
- changes the tool set while the mode is on, the mode yields and tells you.
270
+ `--no-builtin-tools`, `--no-extensions`) — the extension does not override what you asked for.
271
+ `--no-extensions` does, however, change the **detected surface**: pi's own `codemode` is a built-in
272
+ extension, so turning extensions off means it will not load, and ADR-0027's table resolves the
273
+ default to `full` — you keep `ptc_run_code` / `ptc_workflow` rather than a `ptc_subagent` with
274
+ nothing to compose with. If you would rather pin the surface regardless, set `"surfaceMode"` in
275
+ `ptc.json`. If another extension changes the tool set while the mode is on, the mode yields and
276
+ tells you.
190
277
 
191
278
  **The important consequence:** in a TUI session, bindings come from the loadout recorded _before_
192
279
  the mode narrowed it. That is what keeps `tools.read(…)` working — and it is why a `--tools`
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ExtensionAPI, Skill, truncateTail } from "@earendil-works/pi-coding-agent";
2
+ import "typebox";
2
3
  import "@earendil-works/pi-ai";
3
4
  import { MessagePort, Worker } from "node:worker_threads";
4
5
  //#region src/runtime/ulid.d.ts
@@ -271,6 +272,7 @@ type TaskCommand = {
271
272
  outputRef?: string;
272
273
  outputBytes?: number;
273
274
  outputPreview?: string;
275
+ childError?: string;
274
276
  };
275
277
  /** Outcome of one successful command: the persisted record, emitted events, and cursor. */
276
278
  interface TransitionResult {
@@ -362,10 +364,11 @@ declare class DispatchSlotCounter {
362
364
  release(holder?: string): void;
363
365
  }
364
366
  /**
365
- * Injected dependencies for the background branch. The foreground path ignores this object
366
- * entirely (it uses the module-level {@link DISPATCH_LIFECYCLE}); every field is optional so
367
- * existing zero-argument call sites keep working. Tests pass an in-memory registry and a
368
- * mock lifecycle so no real `pi` process is ever spawned.
367
+ * Injected dependencies. The background branch uses this object for everything; the foreground
368
+ * branch uses `slots` (the one cap both fronts share since ADR-0026 round 3) and
369
+ * `lifecycle`. Every field is optional so existing zero-argument call sites keep working, and
370
+ * both branches fall back to the module-level {@link DISPATCH_LIFECYCLE}. Tests pass an
371
+ * in-memory registry and a mock lifecycle so no real `pi` process is ever spawned.
369
372
  */
370
373
  interface DispatchDeps {
371
374
  /** Session-level TaskRegistry (ADR-0022 §3). Defaults to a lazy in-memory registry. */
@@ -544,6 +547,139 @@ interface DefaultModeConfig {
544
547
  * on** — the caller should surface the problem rather than silently changing behavior.
545
548
  */
546
549
  export declare function readDefaultModeConfig(agentDir: string): DefaultModeConfig;
550
+ /**
551
+ * Which model-facing tools this package registers, independent of PTC mode (which decides
552
+ * which registered tools are *active*). Read from the same agent-dir file as
553
+ * {@link readDefaultModeConfig}, beside the `defaultMode` key. ADR-0025.
554
+ *
555
+ * The order below is the order of increasing responsibility: `off` hands the whole
556
+ * orchestration question back to pi, `subagents` keeps only the subagent face and lets pi's
557
+ * `codemode` orchestrate, `full` keeps today's set.
558
+ */
559
+ declare const SURFACE_MODES: readonly ["off", "subagents", "full"];
560
+ type SurfaceMode = (typeof SURFACE_MODES)[number];
561
+ /**
562
+ * Whether the pi that launched us will actually LOAD its `codemode` extension.
563
+ *
564
+ * - `"absent"` — nothing in any settings file names it, so pi's own default applies, which is to
565
+ * load it (`settings.md`: "They load by default").
566
+ * - `"enabled"` — an explicit `+builtin:codemode`, or `-e builtin:codemode` on the command line.
567
+ * - `"disabled"` — `-builtin:codemode` or `!builtin:codemode` in a settings file, or `--no-extensions`.
568
+ *
569
+ * This is a different question from {@link CodemodePresence}, which asks whether the directory is
570
+ * on disk. Both are needed, and conflating them is what ADR-0027 fixes: pi 0.99.0 added
571
+ * `-builtin:<name>`, so "the directory exists" and "codemode will run" stopped being the same
572
+ * answer, and a probe that only asks the first one hands orchestration to a tool that is not there.
573
+ */
574
+ type CodemodeSwitch = "absent" | "enabled" | "disabled";
575
+ /** How the switch was decided, so a test can tell a measured answer from a failed read. */
576
+ type CodemodeSwitchSource = "cli" | "project" | "user" | "default" | "invalid";
577
+ /** The switch plus enough provenance to explain it in a notice. */
578
+ interface CodemodeSwitchResolution {
579
+ switch: CodemodeSwitch;
580
+ source: CodemodeSwitchSource;
581
+ /** Set when a settings file could not be read as a JSON object; the switch still resolves. */
582
+ error?: string;
583
+ }
584
+ /**
585
+ * The two directories pi resolves the `!` bucket's globs against.
586
+ *
587
+ * `matchesAnyPattern` also tests `relative(baseDir, path)` and the basename, so a glob whose
588
+ * answer depends on where pi's directories sit is only correct for the directories pi actually
589
+ * used. These are the two it uses: `join(cwd, CONFIG_DIR_NAME)` for project scope -- pi 1.0.0's
590
+ * `CONFIG_DIR_NAME` is `".pi"`, `package-manager.js:720` -- and `agentDir` for user scope (`:719`).
591
+ */
592
+ interface CodemodeBaseDirs {
593
+ project: string;
594
+ user: string;
595
+ }
596
+ export declare function resolveCodemodeSwitch(argv: readonly string[], projectSettings: unknown, userSettings: unknown, baseDirs?: CodemodeBaseDirs): CodemodeSwitchResolution;
597
+ /**
598
+ * Read pi's own settings files and resolve the switch.
599
+ *
600
+ * Project settings live at `<cwd>/.pi/settings.json` and user settings at
601
+ * `<agentDir>/settings.json` — the two `DefaultPackageManager` reads, via
602
+ * `join(this.cwd, CONFIG_DIR_NAME)` and `this.agentDir`.
603
+ */
604
+ export declare function readCodemodeSwitch(agentDir: string, cwd: string, argv?: readonly string[]): CodemodeSwitchResolution;
605
+ /**
606
+ * Whether an explicit `surfaceMode` disagrees with what the table above decided.
607
+ *
608
+ * The explicit key always WINS — that is what an override is for — so this exists only to be
609
+ * reported. `off` is exempt: it means "I do not want this package's surface at all", which is a
610
+ * statement about the package rather than a claim about who orchestrates, and warning about it on
611
+ * every session would be crying wolf.
612
+ */
613
+ export declare function surfaceModeConflict(explicit: SurfaceMode | undefined, detected: SurfaceMode): boolean;
614
+ /**
615
+ * Whether the pi that launched us ships its own `codemode` orchestration tool.
616
+ *
617
+ * pi's own tool listing is NOT usable here, and that is why this probe exists at all.
618
+ * `getAllTools()` and `getActiveTools()` are `notInitialized` stubs until `bindCore` runs
619
+ * (`loader.js:106-108`), which happens after every factory body has returned. Calling one from
620
+ * a factory throws, and `initializeExtension` catches that throw while `loadExtension` answers
621
+ * `{ extension: null, error }` (`loader.js:493-520`) -- a throwing factory makes the extension
622
+ * fail to load entirely, not degrade to an empty tool list. Registration has to happen in the
623
+ * factory, so the default has to be knowable there.
624
+ *
625
+ * Those line numbers are pi **0.99.1**'s `dist/core/extensions/loader.js`, read from a real
626
+ * install, and that is the version this reasoning is about rather than the one this repo compiles
627
+ * against: the 0.86.1 in `devDependencies` has the same two regions at 106-108 and 445-473.
628
+ *
629
+ * So this walks the filesystem from the entry script instead. `process.argv[1]` is whatever the
630
+ * user typed, which for a package-manager install is a shim, so it is resolved first. The answer
631
+ * is "does this pi ship codemode", NOT "can this session call it": codemode registers with
632
+ * `defaultActive: false`, so it is absent from `getActiveTools()` even when fully present.
633
+ * `session_start` is where the second question gets asked, and where a session handed to an
634
+ * orchestrator that is not there gets told.
635
+ */
636
+ export declare function probeCodemodePresence(argv?: readonly string[]): CodemodePresence;
637
+ interface CodemodePresence {
638
+ /** Whether the directory was found. */
639
+ present: boolean;
640
+ /** How the answer was reached, so a test can tell a measured yes from a failed probe. */
641
+ how: "found" | "not-found" | "no-entry" | "unresolvable-entry";
642
+ }
643
+ /** Result of reading the surface mode, with enough detail to warn about a broken file. */
644
+ interface SurfaceModeConfig {
645
+ surfaceMode: SurfaceMode;
646
+ source: "file" | "default" | "invalid";
647
+ error?: string;
648
+ /**
649
+ * What the probe found, on the paths where the surface was NOT decided by the file. Absent
650
+ * when the user set the key: that call never consults the probe (see
651
+ * {@link readSurfaceModeConfig}), so there is nothing to report.
652
+ */
653
+ codemode?: CodemodePresence;
654
+ /**
655
+ * Whether pi will actually load its own codemode (ADR-0027). Absent for the same reason as
656
+ * {@link codemode}: an explicit key short-circuits the probe, so there is nothing to report.
657
+ */
658
+ codemodeSwitch?: CodemodeSwitchResolution;
659
+ /**
660
+ * What the four-case table decided, carried even when an explicit key overrode it — that
661
+ * difference is exactly what {@link surfaceModeConflict} reports on.
662
+ */
663
+ detected?: SurfaceMode;
664
+ }
665
+ /**
666
+ * Read `surfaceMode` from the agent-dir config file.
667
+ *
668
+ * A pure function over the filesystem, shaped like {@link readDefaultModeConfig} on purpose:
669
+ * an absent file, an absent key, unparseable JSON, a non-object, a wrong-typed value and an
670
+ * out-of-set value all resolve to the detected default rather than a hardcoded one, and every
671
+ * malformed shape additionally reports `invalid` with a reason. A malformed setting must never
672
+ * half-apply - which tools exist is not something to change on a guess.
673
+ *
674
+ * The `presence` and `codemodeSwitch` arguments are parameters rather than hidden calls, so a test
675
+ * can state the pi it is reasoning about instead of depending on the machine it runs on. Omit
676
+ * them and the real probes answer, but only on a path that actually needs the answer: they are
677
+ * resolved inside the fallback branches rather than in a default parameter, because a default
678
+ * parameter is evaluated on EVERY call -- including the ones an explicit `surfaceMode` key
679
+ * short-circuits, where the user paid a `realpathSync` plus up to three `statSync` and two
680
+ * settings reads to set one line of JSON and get a constant.
681
+ */
682
+ export declare function readSurfaceModeConfig(agentDir: string, presence?: CodemodePresence, codemodeSwitch?: CodemodeSwitchResolution, cwd?: string): SurfaceModeConfig;
547
683
  /** Why the mode declined to turn on. Surfaced in the entry notification / debug logs. */
548
684
  type ModeBlockReason = "not-tui" | "config-off" | "tools-unavailable" | "restricted-session";
549
685
  /** Either the loadout to apply, or the reason the mode stayed off. */
@@ -689,13 +825,26 @@ interface PtcConfig {
689
825
  */
690
826
  maxParallelSubCalls: number;
691
827
  /**
692
- * Per-run hard cap on concurrently in-flight `pi.dispatch(...)` calls,
693
- * enforced by the dispatcher: the next concurrent call resolves
694
- * immediately with `{ status: "rejected", errorMessage: "dispatch
695
- * concurrency limit reached" }` — never queued, never spawned.
828
+ * Hard cap on concurrently in-flight dispatch in ONE pi session, not
829
+ * per run. Enforced inside `dispatch()` by a single
830
+ * `DispatchSlotCounter`, so every front spends it: concurrent programs,
831
+ * the `ptc_subagent` tool, and background children (which hold their
832
+ * slot for the task's whole lifetime). The next call over the cap
833
+ * resolves immediately with `{ status: "rejected", errorMessage:
834
+ * "dispatch concurrency limit reached" }` — never queued, never
835
+ * spawned, and never parked behind a long-running child.
696
836
  * Default 8, matches pi's `subagent` extension `MAX_PARALLEL_TASKS`.
697
- * ADR-0016 section 2. Independent of `maxParallelSubCalls`: builtin
698
- * calls never consume a dispatch slot and vice versa.
837
+ * ADR-0016 section 2 (as amended 2026-09-30), ADR-0022 section 9.
838
+ *
839
+ * Where the number is READ from is the counter's construction, not
840
+ * this field alone: a pi session's counter is built by
841
+ * `createBackgroundTaskRuntime({ concurrency })` with this value, and
842
+ * the dispatcher hands the binding `options.dispatchDeps?.slots ??
843
+ * dispatchSlots` — so a `runPtcProgram({ config })` override sizes only
844
+ * the per-run counter, which a pi session never reaches.
845
+ *
846
+ * Independent of `maxParallelSubCalls`: builtin calls never consume a
847
+ * dispatch slot and vice versa.
699
848
  */
700
849
  dispatchConcurrency: number;
701
850
  /** Maximum recursion depth for `pi.dispatch`. The child PTC run spawned by
@@ -1170,8 +1319,29 @@ export declare class TurnPools {
1170
1319
  export interface PtcSubagentsOptions {
1171
1320
  /** Use this session-scoped background runtime instead of constructing one. */
1172
1321
  backgroundRuntime?: BackgroundTaskRuntime;
1322
+ /**
1323
+ * Test seam for ADR-0025's surface mode. When set it wins over the agent-dir `ptc.json`,
1324
+ * so a test never reads the developer's real settings -- and the four test files that all
1325
+ * build this factory through one stub would otherwise inherit whatever the machine happens
1326
+ * to have. Undefined in production, where the file is the only source.
1327
+ */
1328
+ surfaceMode?: SurfaceMode;
1329
+ /**
1330
+ * ADR-0026 test seam: what the codemode probe found. Undefined in production, where the
1331
+ * probe really runs. A test that exercises the DETECTED default states the pi it
1332
+ * assumes rather than inheriting whatever process.argv the test runner happens to have,
1333
+ * which is how the previous version of that test passed for the wrong reason.
1334
+ */
1335
+ codemode?: CodemodePresence;
1336
+ /**
1337
+ * ADR-0027 test seam: whether pi will actually LOAD its own codemode. Undefined in
1338
+ * production, where `readCodemodeSwitch` reads pi's real settings files. Separate from
1339
+ * `codemode` on purpose — a pi can ship the directory and still be told not to load it,
1340
+ * and that is the case this seam exists to state.
1341
+ */
1342
+ codemodeSwitch?: CodemodeSwitchResolution;
1173
1343
  }
1174
1344
  export default function ptcSubagents(pi: ExtensionAPI, options?: PtcSubagentsOptions): void;
1175
1345
  //#endregion
1176
- export type { Binding, BindingContext, BindingTable, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
1346
+ export type { Binding, BindingContext, BindingTable, CodemodePresence, CodemodeSwitch, CodemodeSwitchResolution, CodemodeSwitchSource, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, SurfaceModeConfig, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
1177
1347
  //# sourceMappingURL=index.d.ts.map