pi-ptc-subagents 1.3.0 → 1.5.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
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ (nothing yet)
11
+
12
+ ## [1.5.0] - 2026-10-08
13
+
14
+ ### Added
15
+
16
+ - **`verify:dist` is now part of the release gate.** `scripts/verify-dist-render.mjs` is the only
17
+ check that exercises the _built_ artifact, and it ran nowhere: not in CI, not in the publish
18
+ workflow, not in `prepublishOnly`. A feature in this project's own history passed three review
19
+ rounds and 696 tests and then failed this script on the release artifact. It now runs on every
20
+ pull request, in `publish.yml` before the publish step, and in `prepublishOnly`
21
+ ([ADR-0031](./docs/adr/0031-open-source-and-publish-authority.md) §D).
22
+ - **`THIRD_PARTY_NOTICES.md`**, stating what is derived from DeepSeek Harness (MIT, Copyright (c)
23
+ 2026 DeepSeek) and from `pi` (MIT), and shipped inside the npm tarball rather than only on
24
+ GitHub. The MIT notice is an obligation for the source excerpts in `docs/research/`, not a
25
+ courtesy.
26
+ - **A dispatched child returns a _child report_ instead of prose alone.** `summary` in the child's
27
+ own words, `findings` each carrying the independent thing that supports the claim,
28
+ `files_touched`, and the token usage **the host measured**. The child's prose is kept alongside
29
+ the report, never replaced by it ([ADR-0032](./docs/adr/0032-child-report.md)).
30
+ - **Two delivery channels, and the result names which one delivered it.** A declared
31
+ `ptc_child_report` tool (the reliable one) or a fenced JSON block in the child's final text (the
32
+ fallback, for installs where this package does not load in the child). `reportChannel` is
33
+ **always** present — `tool`, `prompt-json`, `none` or `opted-out` — because a degradation a
34
+ caller cannot see is a silent failure, and "ran but did not comply" must not read as "returned
35
+ nothing".
36
+ - **The report contract is on by default** and an agent opts out with one frontmatter line,
37
+ `childReport: false`. An opted-out agent reads as `opted-out`, not `none`: nobody was asked is a
38
+ different claim from having been asked and ignored.
39
+ - **`ptc_subagent` renders the report** where the model reads it, bounded at 20 findings /
40
+ 20 files / 150 characters of evidence per finding, each bound stated in-band when it withholds.
41
+ This is the first real reader of that tool's declared `structuredContent` — on the `subagents`
42
+ surface there is no `codemode` to read it.
43
+
44
+ ### Changed
45
+
46
+ - `src/tools/subagent.ts` gained the OCR rule anchor it never had. It was resolving to the `**`
47
+ catch-all and being reviewed against the generic floor only.
48
+ - **The repository is public, and `main` is writable only by the maintainer.** Everything else is
49
+ a pull request that needs CI green and one approving review. Release authority is enforced by a
50
+ `refs/tags/v*` ruleset plus the npm package's "Require two-factor authentication and disallow
51
+ tokens" setting, so it no longer depends on where a credential file is kept
52
+ ([ADR-0031](./docs/adr/0031-open-source-and-publish-authority.md) §A–§B).
53
+ - **The next version published from here carries an npm provenance attestation.** Under trusted
54
+ publishing npm generates it automatically once the source repository is public, with no workflow
55
+ change — so the `homepage` and `repository` fields that pointed at a private GitHub now resolve,
56
+ and the missing provenance badge that ADR-0018 §7 recorded as expected is no longer expected.
57
+ - **`node scripts/preview-ptc-render.mjs` requires `PI_ROOT`.** It imported pi's theme from a hard-coded
58
+ path on one developer's machine, so following the README from anywhere else failed inside a
59
+ module loader. It now reads the install directory from the environment and, when it is missing or
60
+ wrong, says so with the commands to find it.
61
+ - **The DSH citations in `docs/research/` point at the public upstream repository** instead of a
62
+ temporary local extraction, so a reader can follow them. The baseline is tag `dsh-v0.2.0-rc.2`
63
+ — the release the research actually read. `src/runtime/limits.ts:4` named `0.1.6-alpha.2`; the
64
+ constants are byte-identical across both tags, so only the version label changed and no behaviour
65
+ did ([ADR-0031](./docs/adr/0031-open-source-and-publish-authority.md) §C).
66
+
67
+ ## [1.4.0] - 2026-10-03
68
+
69
+ ### Added
70
+
71
+ - **`/ptc surface off|subagents|full` switches the extension surface without a new session.** The
72
+ surface was configurable only by hand-editing `~/.pi/agent/ptc.json` and restarting, because it is
73
+ read once in the extension factory and pi has no way to unregister a tool. The new subcommand
74
+ writes the key and then performs pi's own `/reload`, which clears pi's extension cache and
75
+ re-runs every factory — the same path the built-in takes, from inside the extension. A malformed
76
+ `ptc.json` is reported and left byte-for-byte alone, `defaultMode` in the same file survives, and
77
+ setting the value that is already there writes nothing and does not reload
78
+ ([ADR-0030](./docs/adr/0030-surface-switch-reloads.md)).
79
+
80
+ ### Fixed
81
+
82
+ - **A default pi session no longer starts with no orchestration tool at all.** The detected surface
83
+ followed two questions — does this pi ship a `codemode` directory, and will pi load it — and on a
84
+ stock pi 1.0.0 install the answer to both is yes, so the surface resolved to `subagents`. But pi
85
+ registers `codemode` with `defaultActive: false`: it joins the model's tool list only when a
86
+ loadout names it, and a session that configured nothing has no such loadout. Since the
87
+ `subagents` surface deliberately does not register `ptc_run_code`, the result was a session
88
+ holding `ptc_subagent` and three `ptc_task_*` tools with no way to compose any of them, plus a
89
+ startup warning on **every** session. The detection now asks a third question — whether `codemode`
90
+ will actually be in the tool list, read from `--tools` and from `defaultTools` in the project and
91
+ user settings the way pi resolves them — and `subagents` is chosen only on positive evidence that
92
+ the model can call the tool. The default on an unconfigured install is now `full`, with no
93
+ warning. To opt into `subagents`, add `codemode` to `defaultTools` or `--tools`
94
+ ([ADR-0029](./docs/adr/0029-surface-follows-codemode-activation.md)).
95
+ - **The "pi does not register codemode" startup notice no longer hard-codes the wrong surface.** With
96
+ a fifth cell in the table, the detected surface is not always `subagents` in that branch, and the
97
+ message named `SurfaceModeConfig.detected` — a field populated only when an explicit `surfaceMode`
98
+ overrode detection, so on the very path the notice fires from it would have printed `undefined`.
99
+ It reports the surface the session actually built.
100
+
10
101
  ## [1.3.0] - 2026-10-02
11
102
 
12
103
  ### Fixed
package/README.md CHANGED
@@ -4,13 +4,21 @@ DSH-style **PTC mode** (Programmable Tool Calling) for [pi](https://pi.dev):
4
4
  the model writes a JS/TS program that calls pi's tools from inside a worker,
5
5
  and only the program's return value plus its logs come back to the model.
6
6
 
7
+ **Source is open.** This repository is public and the source is here — `dist/` on npm is the
8
+ compiled form of what you read below. Contributions go through pull requests: see
9
+ [CONTRIBUTING.md](./CONTRIBUTING.md) for the gate your PR has to pass, and
10
+ [SECURITY.md](./SECURITY.md) before reporting anything. Releases are cut from `main` by the
11
+ maintainer only; if you find something you think needs a release, open an issue and say so.
12
+
7
13
  ## Status
8
14
 
9
- Pre-1.0, but functional: `ptc_run_code` and `ptc_workflow` are registered and run
15
+ Functional and actively used: `ptc_run_code` and `ptc_workflow` are registered and run
10
16
  programs through the same tested worker machinery (dispatcher, wire protocol,
11
17
  budgets, built-in bindings). The implementation is written clean-room from
12
18
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) PTC
13
- behaviour (tracked as a wayfinder map in this repo's issues).
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.
14
22
 
15
23
  ## Install
16
24
 
@@ -72,6 +80,23 @@ const [read, scoutA, scoutB] = await Promise.all([
72
80
  ]);
73
81
  ```
74
82
 
83
+ **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.
84
+
85
+ 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**:
86
+
87
+ ```ts
88
+ const r = await tools["pi.dispatch"]({ agent: "scout", task: "survey the auth code" });
89
+ if (r.reportChannel === "none") {
90
+ // The child ran and did not comply. r.text is its prose; treat it as unbacked.
91
+ } else {
92
+ for (const f of r.report?.findings ?? []) console.log(f.what, "←", f.evidence);
93
+ }
94
+ ```
95
+
96
+ `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.
97
+
98
+ 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.
99
+
75
100
  **Bounded.** Three knobs keep fan-out from running away:
76
101
 
77
102
  - `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.
@@ -201,8 +226,12 @@ the session. The mode's rationale and rejected alternatives are in [ADR-0010](./
201
226
  ```
202
227
 
203
228
  **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)):
229
+ decides which model-facing tools this package registers at all ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)):
230
+
231
+ ```
232
+ /ptc surface # report the current surface and where it came from
233
+ /ptc surface subagents # write the key and reload so it applies now
234
+ ```
206
235
 
207
236
  ```jsonc
208
237
  // ~/.pi/agent/ptc.json
@@ -214,31 +243,60 @@ surface change needs a new session ([ADR-0025](./docs/adr/0025-extension-surface
214
243
  doing the orchestration; warns at startup when `codemode` is not in the active tool set.
215
244
  - `full` — today's set: `ptc_run_code` / `ptc_workflow` plus the three `ptc_task_*` tools.
216
245
 
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` |
246
+ The command exists because the alternative is editing JSON by hand and starting a new session: the
247
+ surface is read once in the extension factory and pi has no way to unregister a tool, so a change
248
+ cannot apply in place. `/ptc surface` writes the key and then performs pi's own `/reload`, which
249
+ re-runs every extension factory ([ADR-0030](./docs/adr/0030-surface-switch-reloads.md)). Two things
250
+ it will not do silently: a malformed `ptc.json` is reported and left exactly as it was, and an
251
+ unchanged value does not reload — a reload replaces every extension instance in the session, so
252
+ retyping the value you already have costs you in-flight state for nothing. Switching to
253
+ `subagents` or `off` also ends a running PTC mode, which the command says before it does it.
254
+
255
+ **The default is detected, and it follows three questions, not one**
256
+ ([ADR-0026](./docs/adr/0026-surface-default-is-detected.md),
257
+ [ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md),
258
+ [ADR-0029](./docs/adr/0029-surface-follows-codemode-activation.md)). With no `surfaceMode` key:
259
+
260
+ | does this pi ship `codemode`? | will pi load it? | can the model call it? | surface |
261
+ | ----------------------------- | ---------------------------------------------- | --------------------------------------------- | ----------- |
262
+ | yes | yes (default, or `+builtin:codemode`) | yes (`--tools …,codemode`, or `defaultTools`) | `subagents` |
263
+ | yes | yes (default, or `+builtin:codemode`) | no — **the default on a stock install** | `full` |
264
+ | yes | no (`-builtin:codemode`, or `--no-extensions`) | — | `full` |
265
+ | no | — | — | `full` |
266
+
267
+ The third column is the one that decides most sessions, and it is why the default is `full` on a pi
268
+ that has never been configured. pi ships `codemode` and loads it by default, but registers it
269
+ **inactive** (`defaultActive: false`) — it joins the model's tool list only when a loadout names it.
270
+ Handing orchestration to a tool the model cannot call is the failure this avoids, so `subagents` is
271
+ chosen only on positive evidence.
226
272
 
227
273
  Setting the key always wins. A probe that cannot answer falls back to `full` — the safe direction,
228
274
  since `subagents` as a failure mode would take away the orchestration tool the session was relying
229
275
  on.
230
276
 
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" }`.
277
+ > **To use the `subagents` surface, put `codemode` in your tool list.** Without that you get
278
+ > `ptc_run_code` / `ptc_workflow` and no startup warning, which is the correct answer for a session
279
+ > that never asked for delegation:
280
+ >
281
+ > ```jsonc
282
+ > // ~/.pi/agent/settings.json
283
+ > { "defaultTools": ["read", "bash", "edit", "write", "+codemode"] }
284
+ > ```
285
+ >
286
+ > or per launch, `pi --tools read,bash,edit,write,codemode`. A list made only of modifiers starts
287
+ > from pi's four defaults, so `{ "defaultTools": ["+codemode"] }` means the same thing. If you would
288
+ > rather pin the surface regardless of what pi is doing, set `{ "surfaceMode": "subagents" }` —
289
+ > and note that pinning it does not activate `codemode`, so a pinned `subagents` on a session that
290
+ > never configured it still warns, by design ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)
291
+ > decision 4).
235
292
 
236
293
  > Turning pi's `codemode` **off** — `"extensions": ["-builtin:codemode"]`, or launching with
237
294
  > `--no-extensions` — brings the PTC surfaces back on its own. Before ADR-0027 it did not: the
238
295
  > detection asked only whether the extension directory exists, so a pi told not to load it still
239
296
  > counted as an orchestrator and you got `ptc_subagent` with nothing to compose with. The switch is
240
297
  > 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.
298
+ > `<agentDir>/settings.json` — in the same order, and the activation probe reads the first two plus
299
+ > `--tools`.
242
300
 
243
301
  No file, or a value outside that set, falls back to the detected default and says so at startup
244
302
  rather than half-applying: which tools exist is not something to change on a guess.
@@ -246,13 +304,13 @@ rather than half-applying: which tools exist is not something to change on a gue
246
304
  **A detection you cannot see is the failure this design has**, so the result is reported. With no
247
305
  `surfaceMode` key, the outcome is issued through the TUI notification channel at session start —
248
306
  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**
307
+ not answer. A pi that ships `codemode`, loads it, and has it in the tool list is the expected case
308
+ and says nothing. A second notice is issued when the probe and pi's own tool registry disagree, which
309
+ is the case the probe structurally cannot see: it walks the filesystem, so under
310
+ `--exclude-tools codemode` it answers `present` for a tool this session does not have (and the
311
+ mirror: a restructured `dist` answers `not-found` for one pi plainly registers). A third notice
312
+ covers ADR-0027: a settings file that could not be read, and an explicit `surfaceMode` that
313
+ disagrees with the table — the pinned value still wins, and the notice only says so. What is **not**
256
314
  established is that either line actually paints in a real pi TUI: a pty capture at review time
257
315
  showed neither the notice nor a control marker, and a TUI quits on stdin EOF before a toast
258
316
  renders, so that is an unmeasured end to end rather than a broken one. No test in this repository
@@ -305,7 +363,8 @@ pnpm run test:ui # vp test --ui (local browser UI; not for CI)
305
363
  pnpm exec vp test --run tests/render-ptc.test.ts # renderer unit tests only
306
364
  pnpm run build # vp pack + declaration emit
307
365
  pnpm run verify:dist # exercise renderCall/renderResult through the built dist (no LLM needed)
308
- node scripts/preview-ptc-render.mjs # print the rendered rows with real theme colors
366
+ PI_ROOT=<global-node-modules>/@earendil-works/pi-coding-agent \
367
+ node scripts/preview-ptc-render.mjs # print the rendered rows with real theme colors
309
368
  ```
310
369
 
311
370
  Tooling: [oxc](https://oxc.rs) — `oxlint` + `oxfmt` (official defaults) — alongside
@@ -313,6 +372,14 @@ Tooling: [oxc](https://oxc.rs) — `oxlint` + `oxfmt` (official defaults) — al
313
372
 
314
373
  See ADR-0009 for the Vitest adoption decision (reopens ADR-0008's earlier deferment).
315
374
 
375
+ ## Credits
376
+
377
+ Built clean-room from the PTC behaviour of
378
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (MIT,
379
+ Copyright (c) 2026 DeepSeek), read at tag `dsh-v0.2.0-rc.2`, and hosted by
380
+ [pi](https://pi.dev) (`earendil-works/pi`, MIT). Full attribution, and what is
381
+ derived from what, is in [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
382
+
316
383
  ## License
317
384
 
318
- Apache-2.0
385
+ Apache-2.0. See [LICENSE](./LICENSE) and [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
@@ -0,0 +1,59 @@
1
+ # Third-Party Notices
2
+
3
+ `pi-ptc-subagents` is licensed under [Apache-2.0](./LICENSE). It depends on the third-party
4
+ software listed below. Each project remains under its own license; nothing in this file changes
5
+ those terms.
6
+
7
+ ## What is actually derived from what
8
+
9
+ This project implements DSH's **PTC** (Programmable Tool Calling) mode as a `pi` extension. It is
10
+ a clean-room implementation: see [ADR-0002](docs/adr/0002-source-strategy.md). What the code
11
+ _derives_ from DeepSeek Harness is the **behavioural contract** — the tool surface, the
12
+ `run_code` semantics, the numeric limits — and the **research notes under `docs/research/` cite
13
+ it, line by line, from the public source.** Those notes reproduce substantial portions of DSH's
14
+ source, which is why the attribution below is a licence obligation rather than a courtesy.
15
+
16
+ | Project | Licence | Role |
17
+ | ------------------------------------------------------------------------------------------------------------------ | ------- | -------------------------------------------------------------------------------------------------- |
18
+ | [`deepseek-ai/deepseek-harness`](https://github.com/deepseek-ai/deepseek-harness) | MIT | Behavioural reference and citation target for the PTC contract. Read at tag **`dsh-v0.2.0-rc.2`**. |
19
+ | [`earendil-works/pi`](https://github.com/earendil-works/pi) (`@earendil-works/pi-coding-agent`, `pi-ai`, `pi-tui`) | MIT | The host this extension loads into. Peer dependency, not vendored. |
20
+
21
+ ## DeepSeek Harness
22
+
23
+ ```
24
+ MIT License
25
+
26
+ Copyright (c) 2026 DeepSeek
27
+
28
+ Permission is hereby granted, free of charge, to any person obtaining a copy
29
+ of this software and associated documentation files (the "Software"), to deal
30
+ in the Software without restriction, including without limitation the rights
31
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
+ copies of the Software, and to permit persons to whom the Software is
33
+ furnished to do so, subject to the following conditions:
34
+
35
+ The above copyright notice and this permission notice shall be included in all
36
+ copies or substantial portions of the Software.
37
+
38
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44
+ SOFTWARE.
45
+ ```
46
+
47
+ Citations in `docs/research/upstream-20260930/` name paths inside that repository, pinned to
48
+ `dsh-v0.2.0-rc.2`. Upstream also carries its own `THIRD_PARTY_NOTICES.md` for its dependency
49
+ closure; that closure is upstream's concern, and this package depends on the harness only through
50
+ reading it, not through importing it.
51
+
52
+ ## Runtime dependency
53
+
54
+ | Package | Licence |
55
+ | -------------------------------------------------- | ------------- |
56
+ | [`minimatch`](https://github.com/isaacs/minimatch) | BlueOak-1.0.0 |
57
+
58
+ `minimatch` is the only runtime (`dependencies`) entry; everything else is a `devDependency` or a
59
+ `peerDependency` on the host.
package/dist/index.d.ts CHANGED
@@ -41,6 +41,27 @@ interface ParsedAgentEvent {
41
41
  errorMessage?: string;
42
42
  };
43
43
  message_text?: string;
44
+ /**
45
+ * `tool_execution_end` carries the executed tool's name (`pi-agent-core`'s
46
+ * `ToolExecutionEndEvent`, alongside `toolCallId` and `isError`).
47
+ */
48
+ toolName?: string;
49
+ /** The tool call this event closes; carried through because the wire carries it. */
50
+ toolCallId?: string;
51
+ /** Whether that call was treated as an error; a failed call's payload is not a report. */
52
+ isError?: boolean;
53
+ /**
54
+ * The executed tool's full `AgentToolResult`, verbatim. `result.structuredContent` is where a
55
+ * tool that DECLARES an `outputSchema` puts its machine-readable value (ADR-0032's tool
56
+ * channel), and this repo already depends on that reaching it for three other tools.
57
+ *
58
+ * Typed `unknown` rather than a hand-written mirror of `AgentToolResult`: what arrives is
59
+ * whatever the child actually wrote, and the one reader of it validates through
60
+ * `validateChildReport` rather than trusting the shape to have survived the wire.
61
+ */
62
+ result?: {
63
+ structuredContent?: unknown;
64
+ };
44
65
  }
45
66
  /**
46
67
  * Opaque per-handle state owned by the adapter that produced it. The dispatch code
@@ -111,6 +132,76 @@ interface ChildProcessLifecycle {
111
132
  stderr(handle: ChildHandle): Promise<string>;
112
133
  }
113
134
  //#endregion
135
+ //#region src/runtime/child-report.d.ts
136
+ /**
137
+ * The child report's types (ADR-0032, `CONTEXT.md` §child report).
138
+ *
139
+ * These four declarations used to live in `dispatch.ts` next to the extraction that produces
140
+ * them. They moved here so the *persisted* `TaskRecord` field (`task-storage.ts`, Layer 1) can
141
+ * name the shape without a type-only import back up into the dispatch layer — a record that
142
+ * stores a report has to be able to say what a report is, and the storage layer is below the
143
+ * dispatcher, not above it. `dispatch.ts` re-exports all four, so every existing import site
144
+ * (and the report tool's) keeps working unchanged.
145
+ *
146
+ * The EXTRACTION stays in `dispatch.ts`. This module owns vocabulary, not parsing.
147
+ *
148
+ * #101 added the last two constants here, and the reason is the same one: ADR-0032's "the
149
+ * contract has exactly one home". The report tool's NAME and the SHAPE it demands are both
150
+ * needed by two modules that must not know about each other — `dispatch.ts`, which has to put
151
+ * the tool in the child's argv and read its `structuredContent` back, and the tool declaration
152
+ * in `src/tools/`, which must not import `dispatch.ts` (that edge has broken this repo's tests
153
+ * once; see `CHILD_REPORT_MAX_FINDINGS` below for why). Neither can reach the other's module, so
154
+ * both read the text from here, which imports nothing at all.
155
+ */
156
+ /** One claim the child makes, with the evidence it rests on. */
157
+ interface ChildReportFinding {
158
+ what: string;
159
+ evidence: string;
160
+ }
161
+ /**
162
+ * Which channel a {@link ChildReport} arrived over (ADR-0032 "The channel is always stated").
163
+ *
164
+ * `tool` is the report tool's `structuredContent`, read off `tool_execution_end`; `prompt-json`
165
+ * is the fenced block in the child's final assistant message, which is the channel that still
166
+ * works when this package does not load in the child at all; `none` means the contract was on and
167
+ * the child did not comply.
168
+ *
169
+ * `opted-out` is the fourth value, added by ticket #102, and it is the one that keeps `none`
170
+ * honest. Without it, an agent that opted out and a child that ignored the contract would produce
171
+ * the same string, and ADR-0032 has already said which of those two is a defect — so a reader
172
+ * could not tell "nobody asked" from "it did not comply". A field whose value cannot distinguish
173
+ * those is the silent failure `docs/testing-constraints.md` #3 forbids.
174
+ */
175
+ type ChildReportChannel = "tool" | "prompt-json" | "none" | "opted-out";
176
+ /**
177
+ * What the child DECLARES. `files_touched` is snake_case on purpose: this object is produced by a
178
+ * model emitting JSON, and renaming it on the way in would mean the wire text and the type
179
+ * disagree. The child's prose is returned alongside it, never replaced by it.
180
+ */
181
+ interface ChildReportPayload {
182
+ summary: string;
183
+ findings: ChildReportFinding[];
184
+ files_touched: string[];
185
+ }
186
+ /**
187
+ * The child report as the host stamps it: the payload the child declared, plus `usage`, which is
188
+ * what the host OBSERVED and read off the child's own `message_end` usage blocks.
189
+ *
190
+ * `usage` is deliberately not on {@link ChildReportPayload}. A model cannot know its token
191
+ * count, so a child-declared `usage` would be a fabricated number that happened to look like a
192
+ * measurement — `docs/testing-constraints.md` #4 requires the expected value to point at an
193
+ * independent source, and the host's counter is that source. Anything the child puts under
194
+ * `usage` is read and discarded.
195
+ */
196
+ interface ChildReport extends ChildReportPayload {
197
+ usage: {
198
+ input: number;
199
+ output: number;
200
+ cost: number;
201
+ turns: number;
202
+ };
203
+ }
204
+ //#endregion
114
205
  //#region src/runtime/task-storage.d.ts
115
206
  /**
116
207
  * The 6-state TaskRecord status (ADR-0022 §2). `queued` is deliberately absent in v1
@@ -167,6 +258,26 @@ interface TaskRecord {
167
258
  ownerPid?: number;
168
259
  /** ADR-0023: wall-clock ms when the owning runtime instance started; pairs with `ownerPid`. */
169
260
  ownerBootMs?: number;
261
+ /**
262
+ * ADR-0032: the child report a background child produced, read off its final message by the
263
+ * same extraction the foreground loop runs and stamped onto the record at the terminal
264
+ * transition — so `ptc_task_output` can hand a background child back with the same shape a
265
+ * foreground `DispatchResult` carries.
266
+ *
267
+ * **Both report fields are ABSENT unless the record reached `succeeded`** (the registry writes
268
+ * them; see `resolve-exit` in `task-registry.ts`). Absent is a claim in its own right: a child
269
+ * that is still running has not reported *yet*, and a child that failed did not report at all.
270
+ * Neither is the same claim as "ran and complied with nothing to say", which is what a
271
+ * `succeeded` record carrying `reportChannel: "none"` states.
272
+ */
273
+ report?: ChildReport;
274
+ /**
275
+ * ADR-0032 "The channel is always stated": which channel delivered `report`, and — when there
276
+ * is no `report` — the explicit marker that the child ignored the contract. Written with
277
+ * `report` and only on a `succeeded` record, so `reportChannel === undefined` never has to be
278
+ * read as "none".
279
+ */
280
+ reportChannel?: ChildReportChannel;
170
281
  }
171
282
  /**
172
283
  * Per-subscriber cursor for one TaskRecord (ADR-0022 §5). Cursor is per-subscriber (not per-task)
@@ -273,6 +384,8 @@ type TaskCommand = {
273
384
  outputBytes?: number;
274
385
  outputPreview?: string;
275
386
  childError?: string;
387
+ report?: ChildReport;
388
+ reportChannel: ChildReportChannel;
276
389
  };
277
390
  /** Outcome of one successful command: the persisted record, emitted events, and cursor. */
278
391
  interface TransitionResult {
@@ -556,7 +669,7 @@ export declare function readDefaultModeConfig(agentDir: string): DefaultModeConf
556
669
  * orchestration question back to pi, `subagents` keeps only the subagent face and lets pi's
557
670
  * `codemode` orchestrate, `full` keeps today's set.
558
671
  */
559
- declare const SURFACE_MODES: readonly ["off", "subagents", "full"];
672
+ export declare const SURFACE_MODES: readonly ["off", "subagents", "full"];
560
673
  type SurfaceMode = (typeof SURFACE_MODES)[number];
561
674
  /**
562
675
  * Whether the pi that launched us will actually LOAD its `codemode` extension.
@@ -602,6 +715,38 @@ export declare function resolveCodemodeSwitch(argv: readonly string[], projectSe
602
715
  * `join(this.cwd, CONFIG_DIR_NAME)` and `this.agentDir`.
603
716
  */
604
717
  export declare function readCodemodeSwitch(agentDir: string, cwd: string, argv?: readonly string[]): CodemodeSwitchResolution;
718
+ /**
719
+ * Whether `codemode` will be CALLABLE, as a third question distinct from whether pi ships the
720
+ * directory (ADR-0026) and whether it will load the extension (ADR-0027).
721
+ *
722
+ * - `"active"` — a loadout names it, so the model can call it.
723
+ * - `"inactive"` — nothing names it, which on a real install is the DEFAULT: pi registers
724
+ * `codemode` with `defaultActive: false` and activates it only when a loadout says so.
725
+ *
726
+ * `"inactive"` is what every failure of this probe on the positive side resolves to, and that is
727
+ * the design rather than a fallback: `subagents` is chosen only on positive evidence that the tool
728
+ * is callable. See ADR-0029, "The bound that makes the weaker guarantee sufficient".
729
+ */
730
+ type CodemodeActivation = "active" | "inactive";
731
+ /** How the answer was decided, so a test can tell a configured answer from pi's default. */
732
+ type CodemodeActivationSource = "cli" | "project" | "user" | "default" | "invalid";
733
+ /** The answer plus enough provenance to explain it in a notice. */
734
+ interface CodemodeActivationResolution {
735
+ activation: CodemodeActivation;
736
+ source: CodemodeActivationSource;
737
+ /** Set when a settings file could not be read as a JSON object; the answer still resolves. */
738
+ error?: string;
739
+ }
740
+ /**
741
+ * Resolve whether `codemode` will be in the model's tool list, in the order pi resolves the
742
+ * loadout: the command-line allowlist, then the merged `defaultTools`, then pi's own default.
743
+ *
744
+ * The last of those is the load-bearing one and is what makes absence of evidence mean
745
+ * `inactive`: pi registers `codemode` inactive, so a session that configured nothing does not get
746
+ * it, and delegating orchestration to a tool the model cannot call is the failure this exists to
747
+ * prevent.
748
+ */
749
+ export declare function resolveCodemodeActivation(argv: readonly string[], projectSettings: unknown, userSettings: unknown): CodemodeActivationResolution;
605
750
  /**
606
751
  * Whether an explicit `surfaceMode` disagrees with what the table above decided.
607
752
  *
@@ -656,6 +801,11 @@ interface SurfaceModeConfig {
656
801
  * {@link codemode}: an explicit key short-circuits the probe, so there is nothing to report.
657
802
  */
658
803
  codemodeSwitch?: CodemodeSwitchResolution;
804
+ /**
805
+ * Whether `codemode` will be in the model's tool list (ADR-0029), the third axis of the
806
+ * detected table. Absent for the same reason as {@link codemodeSwitch}.
807
+ */
808
+ codemodeActivation?: CodemodeActivationResolution;
659
809
  /**
660
810
  * What the four-case table decided, carried even when an explicit key overrode it — that
661
811
  * difference is exactly what {@link surfaceModeConflict} reports on.
@@ -679,7 +829,36 @@ interface SurfaceModeConfig {
679
829
  * short-circuits, where the user paid a `realpathSync` plus up to three `statSync` and two
680
830
  * settings reads to set one line of JSON and get a constant.
681
831
  */
682
- export declare function readSurfaceModeConfig(agentDir: string, presence?: CodemodePresence, codemodeSwitch?: CodemodeSwitchResolution, cwd?: string): SurfaceModeConfig;
832
+ export declare function readSurfaceModeConfig(agentDir: string, presence?: CodemodePresence, codemodeSwitch?: CodemodeSwitchResolution, codemodeActivation?: CodemodeActivationResolution, cwd?: string): SurfaceModeConfig;
833
+ /** Outcome of writing `surfaceMode` for `/ptc surface`. */
834
+ type SurfaceModeWrite = {
835
+ ok: true;
836
+ path: string;
837
+ /** The value already in the file, or `undefined` when there was none. */
838
+ previous: SurfaceMode | undefined;
839
+ /** False when the file already said this, in which case NOTHING was written. */
840
+ changed: boolean;
841
+ } | {
842
+ ok: false;
843
+ path: string;
844
+ error: string;
845
+ };
846
+ /**
847
+ * Write one `surfaceMode` key into the agent-dir config, preserving every other key.
848
+ *
849
+ * Three rules, each of which is a way a naive rewrite goes wrong:
850
+ *
851
+ * - **A malformed file is never overwritten.** An unparseable `ptc.json` is a file the user may
852
+ * be mid-edit on, and this command is not a licence to replace it with something valid that
853
+ * drops whatever was in it. The read side already reports that shape rather than acting on it
854
+ * ({@link readSurfaceModeConfig}), and the write side has to agree.
855
+ * - **Other keys survive.** `defaultMode` lives in the same file (ADR-0010), and a command that
856
+ * wrote `{"surfaceMode": …}` wholesale would silently reset the user's mode preference.
857
+ * - **An unchanged value writes nothing.** `/ptc surface full` on a session already at `full`
858
+ * should not touch the file's mtime, and — more to the point — should not trigger the reload
859
+ * that would follow, since a reload replaces every extension instance for no reason.
860
+ */
861
+ export declare function setSurfaceMode(agentDir: string, value: unknown): SurfaceModeWrite;
683
862
  /** Why the mode declined to turn on. Surfaced in the entry notification / debug logs. */
684
863
  type ModeBlockReason = "not-tui" | "config-off" | "tools-unavailable" | "restricted-session";
685
864
  /** Either the loadout to apply, or the reason the mode stayed off. */
@@ -791,10 +970,32 @@ export declare function buildPtcSkillsSection(skills: readonly Skill[], format?:
791
970
  /**
792
971
  * PTC run limits and spawn-time hardening, in one frozen `DEFAULT_CONFIG`.
793
972
  *
794
- * The numbers are DSH's (`dsh-v0.1.6-alpha.2`, `@deepseek-ai/dsh-ptc-runtime-node`)
795
- * carried over verbatim — see ADR-0003 (output budget), ADR-0004 (pending calls) and
796
- * ADR-0005 (execution boundary, F1–F4). Tests assert against these constants rather
797
- * than repeating the literals, so a future re-sync only has to change this file.
973
+ * The numbers are DSH's (`dsh-v0.2.0-rc.2`, `@deepseek-ai/dsh-ptc-runtime-node`,
974
+ * `NodePtcRuntime.Config` defaults) carried over verbatim — see ADR-0003 (output budget),
975
+ * ADR-0004 (pending calls) and ADR-0005 (execution boundary, F1–F4). Tests assert against
976
+ * these constants rather than repeating the literals, so a future re-sync only has to change
977
+ * this file.
978
+ *
979
+ * The baseline was `dsh-v0.1.6-alpha.2` until 2026-10-03. That tag was never the source of
980
+ * these numbers — the research the values came from read a `0.2.0-rc.2` checkout, and the
981
+ * values are byte-identical in both tags (verified field by field: 120000 / 600000 /
982
+ * 67108864 / 134217728 / 128 / 3000, in `packages/ptc-runtime/ptc-runtime-node/src/index.ts`).
983
+ * So the correction is to the version label only; no constant changed. The prior label was
984
+ * wrong for a different reason worth keeping in mind: it was read off this comment rather
985
+ * than off the research, and `docs/research/ptc-upstream-parity-audit-20260930.md` had
986
+ * already recorded the mismatch (and that this file's self-description was the stale side).
987
+ *
988
+ * **These limits match a generation of the upstream that upstream has since deprecated.**
989
+ * `dsh-v0.0.x` through `v0.1.6-alpha.2` ran a PTC program on `worker_threads` inside the host
990
+ * process; DSH superseded that on 2026-09-11 and moved Node PTC into a separate process in
991
+ * `v0.1.7-rc.1`, which also renamed the packages into the `ptc-runtime` family with no legacy
992
+ * aliases. The numeric defaults did not change across that move — which is why they still match
993
+ * — but the *shape* around them did, and this file configures the superseded shape (ADR-0005's
994
+ * worker boundary, not a process boundary). So "the numbers are DSH's" is true of two versions
995
+ * and describes an architecture upstream no longer recommends. Upstream's own README warns that
996
+ * there will be compatibility-breaking changes; the parity audit's recommendations 1-3 (upgrade
997
+ * pi, compare against its built-in `codemode`, and re-base this project's position) are the open
998
+ * work, and none of them is a comment fix.
798
999
  *
799
1000
  * Deliberately absent:
800
1001
  * - `syncTimeoutMs` / `maxConcurrentAgents` / `maxTotalAgents` — workflow-engine caps