pi-ptc-subagents 7.5.0 → 7.6.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 ADDED
@@ -0,0 +1,513 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [7.6.0] - 2026-11-02
9
+
10
+ ### Added
11
+
12
+ - **Persistent subagent status panel** (above the pi TUI editor): the state of dispatched
13
+ subagents moves out of the model context into the human's line of sight. The panel lists every
14
+ in-flight subagent (glyph + status color + start time) under the title
15
+ `Tasks · N running · M succeeded · K failed`, and disappears when nothing is in flight.
16
+ **Panel text is English by default** — the same language as the pi host UI.
17
+ Refresh rides the registry's existing transition callback, so the panel changes exactly when a
18
+ record changes. The guard is `ctx.mode === "tui"` (`hasUI` is also true in RPC mode, so it
19
+ cannot serve as the guard); a plain string array is enough — the host caps widgets at 10 rows,
20
+ and pi-tui's `Text` keeps ANSI, so the colors come straight from the theme.
21
+ Two frozen limits (ADR-0022 §8): the row count is derived from the host cap, and the label
22
+ truncation stays at the 48 chars of the old ADR-0022 §11.
23
+ Panel text (written by the model, often copied verbatim from the user) is stripped of ANSI and
24
+ control characters, then whitespace is collapsed, and only then is it truncated — in the other
25
+ order escape sequences would be cut in half and a newline would split one task into two lines.
26
+ The wiring lives in `packages/tui/pi`'s `createTaskPanel`, which subscribes to the registry
27
+ itself; `entry` only creates and disposes it.
28
+ The panel survives `/reload` with no leftover and no duplicate: the same widget key overwrites,
29
+ and the reloaded instance re-subscribes and follows the state.
30
+
31
+ ### Changed
32
+
33
+ - **`packages/tui` is now the display layer** (ADR-0022): the root entry is still the
34
+ zero-dependency shared text primitives (`output` / `search` / `task` are unchanged), while the
35
+ pi picture — the collapsible result rendering plus the panel above — moves behind the `./pi`
36
+ subpath. `entry` goes back to assembly only, and the repository's single pi-tui **import site**
37
+ is gone from `entry`'s sources. `entry` keeps declaring `@earendil-works/pi-tui`, though: its
38
+ built `dist/index.js` still imports it, so the declaration follows the artifact, not just the
39
+ source (a clean install that cannot resolve it breaks the extension — caught by the
40
+ pre-release clean-worktree check).
41
+ The root entry **must not** re-export the pi face: `kimi-mcp`'s build alias points at
42
+ `src/index.ts`, so a re-export would bundle pi host code into the Kimi artifact. `build:kimi`
43
+ therefore gained a gate — the artifact throws if `@earendil-works` shows up in it. The
44
+ published shape is unchanged (still only `pi-coding-agent` and `pi-tui` as external imports)
45
+ and the Kimi artifact is byte-identical.
46
+
47
+ ### Verification
48
+
49
+ - Real pi TUI over a real pty (120×40): the panel draws right above the editor, updates as
50
+ tasks finish (`Tasks · 2 running · 0 succeeded · 0 failed` → `Tasks · 1 running · 1 succeeded
51
+ · 0 failed`), and vanishes when they are all done. Floor 0.86.0 renders it too (`setWidget`
52
+ has the same signature on both — no version gate).
53
+ - 16-version real-machine smoke (`scripts/version-smoke.sh`): **16/16, fails=0** — 0.86.0 /
54
+ 0.86.1 / 0.87.0 / 0.87.1 / 0.99.0 / 0.99.1 / 0.99.2 / 1.0.0–1.0.4 / 1.1.0 / 1.2.0 / 1.3.0 plus
55
+ 1.4.0 on PATH.
56
+ - Repository tests: **172 pass, 0 fail** (139 pi / 6 output / 17 Kimi / 10 standalone); the panel
57
+ probe is 12/12 (9 for the pure renderer, 3 for the wiring).
58
+
59
+ ## [7.5.0] - 2026-11-01
60
+
61
+ ### Added
62
+
63
+ - **Program-internal task output** (`tools.task_output({ taskId })`): a PTC program can now fetch
64
+ the prose answer a dispatched child wrote. Measured gap (real pi 1.4.0): a `wait:false`
65
+ dispatch returns only a handle, and the in-program task records carry metadata only
66
+ (`delivery,finishedAt,id,outputPath,outputTruncated,ownerBootMs,ownerPid,reportChannel,
67
+ startedAt,status,task,whySuppressed` — no `text`, no `output`), so the child's answer was
68
+ unreachable from a program. The new binding uses the same reduction layer as the model-facing
69
+ `ptc_task_output` (the child's prose, not the raw event stream), never throws (unknown id →
70
+ `{error}`), and returns `status` so a program can tell "not finished yet" from "finished
71
+ without an answer". It also finds records that were pushed out of memory into the compressed
72
+ archive (decision 29).
73
+
74
+ ### Changed
75
+
76
+ - **The route-index token gate now measures the real install path.** It used a short relative
77
+ path, which was green at 555 tokens while the real installed index was 625–632 against a 560
78
+ ceiling — the model saw a `> WARNING: index over token budget` line on every turn. The gate now
79
+ uses a 100-character path (the longest realistic install), route prose was trimmed of text
80
+ duplicated at runtime, and an inaccurate tool list in the index was corrected
81
+ (`tools.read/grep/find/ls/edit/write` → "the same-named tools"; pi 1.4.0's active table has no
82
+ grep/find/ls). The index now measures 558 at the gate path, 552 for an nvm install, 541 in the
83
+ repo.
84
+ - **`RouteEntry.fact` removed.** The field was never rendered (dead weight) and it contradicted
85
+ ADR-0015's own decision that the index carries no fact section; facts live in the detail files
86
+ now. ADR-0015's gate table was amended accordingly (gate 5 now guards the `how` section).
87
+ - The web-search detail file now says plainly that the `webDsSearch` switch is the user's own
88
+ configuration — don't flip it, say that search is unavailable. Measured (real pi 1.4.0, four
89
+ arms): sessions that read "default off" and went hunting for the switch to change it were 4×
90
+ slower than sessions that simply said it could not be done.
91
+
92
+ ### Fixed
93
+
94
+ - The program-internal binding accepted only `taskId`, while the model-facing tool spells it
95
+ `task_id`; a model copying the model-facing spelling got an error naming a parameter it never
96
+ used. Both spellings are accepted now.
97
+
98
+ ## [7.4.0] - 2026-11-01
99
+
100
+ ### Added
101
+
102
+ - **PTC as a standalone, host-neutral package** (`packages/ptc`): the engine — run cell, wire
103
+ protocol, worker pool, compiler, file bindings — now depends on nothing and imports no host.
104
+ The model-facing surface moved out to `packages/entry`, so `packages/ptc` is the engine alone
105
+ (zero dependencies, no `peerDependencies`). It ships as a subpath of the existing root package
106
+ (`pi-ptc-subagents/ptc`) and as a `ptc` binary, so installing it needs no second package and no
107
+ extra dependency — verified by a real `npm pack` into a clean directory: one package, zero
108
+ transitive dependencies, both the subpath import and the binary work.
109
+ - **`ptc <program.ts>` CLI**: runs a program file without a model and without pi. The program's
110
+ tool surface is the file four (`read` / `write` / `grep` / `glob` / `edit`), confined to the
111
+ work directory. The published `dist/index.js` stays one self-contained 244 KB file — the
112
+ published shape did not change.
113
+ - **Program tools on pi 0.86 / 0.87** (entry): the nested call channel (`ctx.executeTool`) only
114
+ exists from 0.99. On the floor versions the extension now builds pi's own tool definitions
115
+ (its public `create*ToolDefinition` factories) and calls them directly, so the program tool
116
+ set and parameter shapes are byte-identical to 0.99 — including `edit`'s
117
+ `{path, edits:[{oldText,newText}]}`. That path is gated on the pi version: on 0.99+ a missing
118
+ channel is an error, never a silent bypass of pi's tool pipeline (which would hide the call
119
+ from other extensions' `tool_call` hooks and from usage accounting).
120
+
121
+ ### Changed
122
+
123
+ - **Package layout**: `packages/ptc/src/{tools,render,bindings,task-binding}.ts` moved to
124
+ `packages/entry/src/{ptc-tools,ptc-render,ptc-bindings,task-binding}.ts`; the file bindings
125
+ moved the other way (`packages/kimi-mcp/src/fs-tools.ts` → `packages/ptc/src/fs-bindings.ts`),
126
+ because they are host-neutral. The engine's zero-host-dependency rule is now a test
127
+ (`tests/standalone/standalone-engine.test.mjs`).
128
+ - **Kimi bundle**: 785 KB → 592 KB after pi-tui left the engine.
129
+
130
+ ### Fixed
131
+
132
+ - The floor versions' file-binding fallback was unreachable — all four of them ship the tool
133
+ factories — and its `edit` shape (`{path, old_string, new_string}`) differed from pi's, so a
134
+ program written for 0.99+ would break on the floor. It is gone.
135
+ - Two dead exports (`HostToWorkerFrame`, `WorkerToHostFrame`) and a stale
136
+ `external: [/^@earendil-works\//]` in the engine's rolldown config (the engine has no
137
+ dependencies any more).
138
+
139
+ ### Verification
140
+
141
+ - 16-version real-machine smoke (`scripts/version-smoke.sh`): **16/16, fails=0** — 0.86.0 /
142
+ 0.86.1 / 0.87.0 / 0.87.1 / 0.99.0 / 0.99.1 / 0.99.2 / 1.0.0–1.0.4 / 1.1.0 / 1.2.0 / 1.3.0 plus
143
+ 1.4.0 on PATH. The floor versions and 0.99+ return the same `READ=2216`.
144
+ - Repository tests: **144 pass, 0 fail** (111 pi / 6 output / 17 Kimi / 10 standalone).
145
+
146
+ ## [7.3.0] - 2026-11-01
147
+
148
+ ### Added
149
+
150
+ - **Task-surface recall** (task + entry + dispatch + routes): the parent can find its old
151
+ tasks again and read what the child actually said. `ptc_task_list` takes `scope`
152
+ (recent / all — "all" lists the compaction archive too), `limit` (default 50, max 200),
153
+ `status` and `session`, and answers `{scope, total, tasks[]}` — `total` is the candidate
154
+ count, so a `total` larger than `tasks.length` means older records exist that were not
155
+ listed. Every field is capped (task 80 chars, summary 100, label 120) and the response is
156
+ trimmed entry by entry against a byte budget, so it is always valid JSON (text-level
157
+ truncation would cut JSON in half and `JSON.parse` would throw). Measured: 520 records
158
+ were 104,781 tokens in the old shape; the capped shape stays under 48 KB. Reading the
159
+ whole archive is not the bottleneck either — 3,000 archived records scan in 57 ms (their
160
+ bulk, 1,137 KB ≈ 291k tokens, is what costs).
161
+ - **The child's prose is the deliverable** (`ptc_task_output`): the response now carries
162
+ `text` — the child's own answer, computed from the event stream on the fly — while the
163
+ report is only a summary. With no prose, the raw tail and the layered guidance are
164
+ returned as before, and the full stream stays reachable through `outputPath`.
165
+ - **Task blood-line** (dispatch + entry): every task record carries the host session id
166
+ (`TaskRecord.sessionId`, taken from `ctx.sessionManager.getSessionId()` — present since
167
+ floor 0.86.0, so no version gate), so the parent can filter its own tasks by session.
168
+ - **Re-injection after compaction** (entry + routes): a compaction drops the messages that
169
+ started the tasks, but the tasks keep running. On `session_compact` the host re-injects the
170
+ in-flight tasks and the five most recently completed ones as a `<compacted-tasks>` block on
171
+ the next LLM call. Verified on real pi at both ends — floor 0.86.0 and 1.4.0 both compact
172
+ successfully and inject the list (28→7 and 13→6 messages). No version gate: the `context`
173
+ event payload is byte-identical on 0.86.0.
174
+ - **Stall guard** (dispatch): a child that has produced nothing for the threshold and has
175
+ no tool call in flight is reaped by the host (default 5 minutes — an engine-side constant,
176
+ not a model-facing knob; `timeoutMs` keeps its wall-clock meaning). `stopReason` records
177
+ `stalled: no activity for Nms`, so the parent can see that the child was killed rather
178
+ than finished.
179
+ - **`tools.task_records` in-program binding** (ptc + entry): a program can pull the task
180
+ records itself (`{scope: "all"}` includes the compaction archive) — the mechanical scan
181
+ stays on the host, filtering and reduction happen in the program, and only the few
182
+ records the program returns enter the context. Regular expressions run inside the worker
183
+ cell, which terminates a runaway one: a pathological `(a+)+$` really hangs Node v24 for
184
+ over 120 s, but inside a program it is reaped (`canceled: timeout after 1000ms`) while the
185
+ host keeps running and can start another program afterwards.
186
+ - **`scripts/compact-inject-smoke.mjs`**: real-machine probe for the compaction
187
+ re-injection. Assertions need no model cooperation — the compaction is triggered by the
188
+ rpc `compact` command while the agent is idle, and the injection is checked by a second
189
+ extension reading the request context. Passes on floor 0.86.0 and 1.4.0.
190
+
191
+ ### Changed
192
+
193
+ - **Task-surface responses are objects, not arrays**: `ptc_task_list` answers
194
+ `{scope, total, tasks[]}` and `ptc_task_output` answers the record plus the reduced
195
+ `text`. Nothing in this repository parsed the old array shape, and the version smoke's
196
+ task-face leg stays green on 1.4.0, 0.86.0 and the current PATH (`task_face=OK`, no
197
+ `isError`).
198
+
199
+ ### Fixed
200
+
201
+ - **The child now inherits the parent session's model** (entry): `buildPiArgv` already
202
+ resolved `input.model ?? input.agentModel ?? context.sessionModel`, but the entry never
203
+ passed `sessionModel` — children ran pi's default model instead of the parent's. The
204
+ model is now read at dispatch time through a live getter, so switching models mid
205
+ session switches the child's model too. Verified on eight pi versions 0.86.0–1.4.0, with
206
+ a negative control that goes red when the wiring is removed.
207
+
208
+ ## [7.2.0] - 2026-10-31
209
+
210
+ ### Added
211
+
212
+ - **`timeoutMs` and `projectContext` knobs on task dispatch** (dispatch). `timeoutMs` arms a
213
+ timer only when given: timeout goes through the normal stop path (SIGTERM → SIGKILL),
214
+ `stopReason` records the timeout, terminal state `canceled` — no host-side default; whether
215
+ to use it is the model's call. `projectContext: "none"` loads no AGENTS.md/CLAUDE.md into
216
+ the child (`--no-context-files`, present since pi 0.86.0, so no version gate; default
217
+ `auto` unchanged). Engine-level real-machine probe `scripts/knobs-smoke.mjs` (assertions
218
+ need no model cooperation — timeout reaping, auto/none A/B, report channel, usage fallback)
219
+ passes on eight pi versions 0.86.0 / 0.87.1 / 0.99.0 / 1.0.0 / 1.1.0 / 1.2.0 / 1.3.0 / 1.4.0.
220
+ - **Delivery check** (dispatch + entry + routes): the engine extracts the child's real tool
221
+ trajectory from `tool_execution_start/end` into `ChildDelivery` {tool counts, paths
222
+ touched, tool errors, text chars} — measured from the stream, not model self-report —
223
+ attaches it to the task record alongside the report, and the task surface renders a
224
+ one-line check; when the child delivered nothing, the parent is told to re-dispatch or
225
+ name the gap rather than fill in numbers. Verified against real print-mode streams on pi
226
+ 0.86.0 and 1.4.0 (parser on real events yields e.g. `tools={read:1}`,
227
+ `paths=["/etc/hosts"]`).
228
+ - **`write` — a fifth fs tool in the Kimi face** (`ptc_run_code`): programs can now create
229
+ files, closing the "can't create a file in-program" capability gap found in A/B. Shape
230
+ `{path, content}`, overwrite allowed, parent directory must already exist (clean error, no
231
+ implicit mkdir), 2MB cap counted in bytes, atomic tmp+rename, same realpath jail.
232
+ - **Kimi-face adoption guidance from A/B benching**: SKILL.md gains a two-column "when to
233
+ write a program" checklist (≥5 same-shape calls / ≥20-file read-then-reduce / known-anchor
234
+ batch edits, plus four negative examples); when a fulfilled program made calls, the result
235
+ footer shows "N in-program tool calls reduced to 1"; tool mentions use the full-name
236
+ pattern (`mcp__` prefix + `__ptc_run_code` suffix) after the "skill says write a program
237
+ but the tool isn't in my list" perception break; `ptc_run_code`'s description gains a
238
+ quantified trigger line; `web_ds_search` loses its "in-program" demotion box.
239
+
240
+ ### Changed
241
+
242
+ - **Env pass-through fix, with measured child cost**: the default spawn dropped `env`, so
243
+ `PI_PTC_DEPTH` / `PI_PTC_TASK_ID` never reached the child — children fully assembled as
244
+ depth 0 (whole route book injected, recursion gate ineffective). After the fix children
245
+ take the depth>0 early exit: per-child input −43% (ptc 2830→1607, both 3121→1766).
246
+ Model-side cost multiple corrected to "about 10×" (measured 9–12×; the old 15–22× was
247
+ from before this fix).
248
+ - **Report contract**: the old contract block said `"summary" is your deliverable`, so
249
+ children stuffed facts into self-invented JSON blocks that were then dropped (3/12
250
+ affected; parent recomputed → grader flagged hallucinations). Reworded: the report is the
251
+ summary, the deliverable goes in the task's own format, and the parent-side briefing now
252
+ names the child-report JSON block shape. Measured: named shape → 12/12 reports delivered;
253
+ self-invented formats → 0/4.
254
+ - **Usage fallback**: a child self-reporting all-empty/zero usage no longer masks the
255
+ host-observed value (the task surface was permanently showing 0).
256
+ - **`projectContext: none` docs corrected by measurement** (2026-10-31): none only removes
257
+ the injection — children still read AGENTS.md on their own and apply its conventions
258
+ (none-vs-auto input diff ≈15%, not the "about half" previously claimed; in 6/6 rounds the
259
+ model chose none unprompted).
260
+ - **Parent-side route book**: new first guidance section — dispatch criteria and cost
261
+ ("lots of material / disjoint topics / each piece needs reading ⇒ dispatch; one computable
262
+ job ⇒ don't"); briefing discipline adds "parent writes it wrong → child executes it
263
+ wrong, consistently".
264
+
265
+ ### Fixed
266
+
267
+ - `scripts/version-smoke.sh` installed the wrong package: npm's `pi` is
268
+ IonicaBizau/pi-number (no `pi` bin exists), so every shim install failed — the real
269
+ package is `@earendil-works/pi-coding-agent`.
270
+ - Kimi-face review round: partial run-ab runs no longer wipe `results.jsonl`; arm-restore
271
+ validates its backup (missing or identical-to-arm → refuse to run); the batch-read grader
272
+ verifies per-file line counts; the `write` cap counts `Buffer.byteLength` (1M CJK chars ≈
273
+ 3MB slipped through the character count).
274
+
275
+ ## [7.1.0] - 2026-10-29
276
+
277
+ ### Added
278
+
279
+ - **Kimi Code plugin face (`ptc-tools`, MCP)** (ADR-0020). One repo now ships two install
280
+ faces: the pi extension (this npm package, unchanged name) and a Kimi Code plugin that
281
+ exposes `ptc_run_code` over MCP — the model writes one TypeScript program, batch-calls a
282
+ fixed fs toolset (read/grep/glob/edit) inside a vm cell, and gets one reduced result back
283
+ (measured context economy 1:138 on a 100-file fan-out). `workdir` is a real trust boundary
284
+ (realpath-jailed; symlink escapes rejected). Verified end-to-end in real Kimi Code: skill
285
+ guidance auto-triggers program writing, both Kimi (K2.8) and MiniMax (M3.1-Flash) models
286
+ answered 12/12 deterministic cases with zero unnecessary tool calls and full jail
287
+ compliance. Ships as `release/kimi/` committed at the tag plus `kimi.plugin.json`; update
288
+ detection verified against the official `MoonshotAI/kimi-code` source (GitHub-source-only,
289
+ needs a GitHub Release object per version).
290
+ - **`web_ds_search` in the Kimi face** (P2): the DeepSeek-backed search core (search/open/find)
291
+ is now fully host-neutral and exposed as a second MCP tool; per-process quota with
292
+ fail-soft missing-key behavior. DeepSeek API key resolves env-first, then reuses the key
293
+ already configured in Kimi Code's own `config.toml` (commented-out keys are honored as
294
+ disabled).
295
+ - **Kimi-face quality gates** (`tests/kimi`): mechanical registry gates (tools/list ↔ route
296
+ registry bidirectional with verbatim description match, description token budget, argument
297
+ matrix), plus `scripts/kimi-bench.mjs` (21-case diversity/stress probe; floor Node 22.21.0
298
+ verified 21/21) and `scripts/kimi-model-ab.sh` (model A/B harness).
299
+
300
+ ### Changed
301
+
302
+ - **Host-neutral output core** (`@pi-ptc/output`): run-result shaping (model-visible text,
303
+ ADR-0012 layout + ADR-0015 truncation) split out of the ptc package so both faces share it;
304
+ pi-tui rendering stays pi-side. Test suite split three ways: `tests/pi` / `tests/output` /
305
+ `tests/kimi` (87 cases total, all green).
306
+ - **Worker `execArgv` is now empty** — worker threads no longer inherit the host's exec
307
+ flags. Found by real-machine testing: the Kimi Code host passes `--max-semi-space-size=16`,
308
+ which Node's worker flag validation rejects, killing every program run. Blacklist
309
+ filtering (pi's eval flags) could never keep up with host-specific flags.
310
+ - **Truncation temp files now self-clean**: first truncation in a process sweeps own-prefix
311
+ temp files older than 24h (previously only grew forever).
312
+ - SKILL.md documents exact argument/return shapes for the four fs tools (removes the #1
313
+ model-side friction found in A/B testing); README explains the npm-name vs plugin-name
314
+ split.
315
+
316
+ ### Fixed
317
+
318
+ - `grep` with a file path (`ENOTDIR`), unreadable files killing the whole grep, stale build
319
+ chunks leaking into shipped plugin artifacts, raw `ENOENT` for missing workdir, and
320
+ commented-out DeepSeek keys being picked up as active.
321
+
322
+ ## [7.0.0] - 2026-10-28
323
+
324
+ ### Changed
325
+
326
+ - **BREAKING: PTC programs run in a dedicated vm cell** (spec 决策 30, ADR-0019). Each run
327
+ now executes inside a fresh `vm.createContext`. The host injects only three thin bridges;
328
+ an **IIFE preamble** builds `tools` / `console` / `args` (frozen) / the four `workflow.*`
329
+ helpers **inside the cell (context realm)** and then drops the bridges — the IIFE matters:
330
+ script-level `var` in a vm context lands on the cell's global, and a leaked bridge name
331
+ (`__bridge`) was empirically reachable and escapable during review (fixed, regression-tested).
332
+ Prototype-chain escapes of the `x.constructor.constructor("return process")()` class find
333
+ no host object, and tool results cross the boundary as JSON strings so no host
334
+ Promise/object is ever exposed. `process`, `fetch`, `require`, dynamic `import()`, and all
335
+ `node:*` modules are unreachable — previously a program could `await import("node:fs")`
336
+ and bypass pi's tool pipeline (permissions/approvals) entirely. Programs that used those
337
+ escape hatches will break; programs that only use `tools.*` and `Date`/`Math` are
338
+ unaffected. Worker reset is now "drop the cell" — the globalThis baseline sweep, its known
339
+ prototype-pollution shortcut, and the console patch/restore dance are all gone.
340
+ Tool-call arguments and results are now pure JSON at the cell boundary: `undefined` values
341
+ in arguments are dropped, a `BigInt` argument rejects the call (old structured-clone
342
+ semantics differed; pinned by tests).
343
+ - **Changed: `console.log` of objects prints JSON** (`{"a":1}`) instead of `util.inspect`
344
+ (`{ a: 1 }`) — deterministic, and a side effect of the cell's no-host-object rule.
345
+ Measured cost: none measurable (n=50, fresh-spawn medians ~35–37ms, same as before the
346
+ cell). Accident-surface reduction, **not** a security boundary (trust posture unchanged;
347
+ Node's `vm` is not a sandbox against determined adversaries — see ADR-0019 for the
348
+ residual cosmetic leaks). Deliberately not adopted from the reference designs: determinism
349
+ bans (no replay/journal to serve), process-per-run (warm pool is an asset), QuickJS (WASM
350
+ dependency with no matching gain). 35 new test cases cover visibility, the escape battery
351
+ (13 cases, incl. preamble-internal names), cross-realm semantics, isolation, and protocol
352
+ edges — each proven live by mutation probes (3 mutations, each turning exactly its own
353
+ cases red).
354
+
355
+ ## [6.2.0] - 2026-10-28
356
+
357
+ ### Added
358
+
359
+ - **Lean child process argv**: children are now spawned with `--no-extensions --no-skills`
360
+ (extension discovery noise — user skills, host-side prose, discovered extensions — no longer
361
+ rides into every child), and `--no-mcp` when the host pi is ≥ 1.1.0 (the flag first appears
362
+ there; `EngineContext.piVersion` is resolved once at session start). Measured on real
363
+ children: worst-case per-child input drops 86% (32524 → 4356 tokens).
364
+ - **Cache-aware usage observation**: the JSONL stream parser now carries pi ≥ 1.4.0's
365
+ `usage.cacheRead` / `usage.cacheWrite` into the observed usage (absent on older pi, floor-safe).
366
+
367
+ ### Changed
368
+
369
+ - **Tighter child report contract prose**: summary is the deliverable, findings carry
370
+ load-bearing evidence only, no recapping read content, no report .md files. JSON shape
371
+ unchanged. Measured output reduction: -57% on recap-prone tasks, -9~23% on mixed tasks.
372
+ - `create-a-task.md` gains briefing discipline (exact paths, state what the final message
373
+ must carry, merge same-domain work into one serial child).
374
+ - `docs/pi-version-matrix.md`: `--no-mcp` first-appearance (1.1.0) and 1.4.0 usage cache
375
+ fields recorded.
376
+
377
+ ## [6.1.0] - 2026-10-27
378
+
379
+ ### Added
380
+
381
+ - **Task traces have a capacity boundary and a compressed archive** (spec decision 29).
382
+ Terminal records are capped at the newest 500 (`taskTraceKeep`, evicted oldest-first
383
+ by `(finishedAt, taskId)`; non-terminal records are never evicted). Runtime evictions
384
+ are archived — record and output gzip-compressed (3.4x measured on real child
385
+ streams) on an async event-loop chain the main path never waits on; boot-time
386
+ history is deleted directly. Archived traces stay retrievable by exact id through
387
+ `ptc_task_output` for 30 days from archival (`archiveTtlDays`; archive files are
388
+ purged at boot, 500 per pass). All three knobs live in `~/.pi/agent/ptc.json`;
389
+ `archiveEnabled: false` restores immediate delete.
390
+ - **`outputTruncated` on task records and results.** Child stdout now has a 16MB
391
+ tail cap (the stderr cap's long-missing twin), so a runaway child can no longer
392
+ exhaust the host's memory; the record says when the trace is tail-only.
393
+
394
+ ### Fixed
395
+
396
+ - **The task door no longer advertises a `cwd` parameter nothing consumed** (audit F1).
397
+ It was pre-planted in the shared parameter table, but neither entry point forwarded
398
+ it and no engine layer accepted it; the schema now tells the truth.
399
+ - **Search memory keeps pages opened directly** (audit F5). `open` on a URL that
400
+ never appeared in search results previously had its text silently dropped, so a
401
+ later `find` on the same page re-fetched and double-spent quota; a fallback bucket
402
+ (same 400K budget) now holds it.
403
+ - **Value-tree rendering enforces the frozen contract** (audit F7, ADR-0016): at most
404
+ 6 children per level with an explicit `…(余 N 项)` marker, and line width back to
405
+ the contracted 120 — previously a 100k-element array rendered in full before the
406
+ downstream truncator threw the work away.
407
+ - **Pooled workers drop their per-run listeners when returned** (audit F8); dozens of
408
+ programs in one turn no longer pile dead closures on the reused workers.
409
+ - **The task face no longer removes user-pinned tools** (audit F9): visibility sweeps
410
+ with zero records now retract only tools the session added itself.
411
+ - **Dead code removed** (audit F6): `listAgentNames`, `drainGraceMs`, a duplicated
412
+ `pi-tui` devDependency; three rich-row renderer bounds formally de-registered via
413
+ [ADR-0016](docs/adr/0016-render-bounds-cleanup.md) instead of being deleted as junk.
414
+
415
+ ### Changed
416
+
417
+ - **Value-tree lines narrow from 200 to the contracted 120 characters** — a
418
+ model-visible shortening of oversized render lines.
419
+
420
+ ## [6.0.3] - 2026-10-26
421
+
422
+ ### Fixed
423
+
424
+ - **The route index fits its token budget again.** Every route line carried the
425
+ detail file's absolute path; in an installed package that pushed the index to 695
426
+ tokens against the 560 ceiling, so every turn's system prompt ended with a
427
+ `> WARNING: index over token budget` line — and the warning told the model to
428
+ move the details into the directory they were already in. The directory is now
429
+ stated once and each line keeps only the file name: 481 tokens.
430
+ - **`/ptc mode` no longer claims it is narrowing.** PTC mode stopped narrowing the
431
+ tool loadout in 6.0.1, but the command's reply still read `mode=on(收窄中)`.
432
+ It now reads `on(引导层)`.
433
+ - **A tool result that is an image no longer carries its base64 into the program.**
434
+ pi's `read` returns `{type:"image", data:<base64>}` for image files, so the whole
435
+ image ended up in the program's return value as text — while the image itself was
436
+ already handed to the model as an image block. The bridge now returns the
437
+ readable note instead.
438
+ - **The binding scope no longer lists tools the session cannot call.** The fallback
439
+ to "every registered tool" now applies only to the pi versions without a
440
+ nested-call channel (0.86/0.87). On 0.99+ the session's own active set is used,
441
+ so a session that narrowed its tools no longer sees bindings that would be
442
+ rejected on the first call.
443
+
444
+ ### Changed
445
+
446
+ - The seven route detail files live in one place now
447
+ (`packages/routes/docs/routes/`); the identical copy at the repository root is
448
+ gone, and the copy `prepack` drops there is now git-ignored.
449
+ - ADR-0015 records the reversal of its "no absolute path in the index" decision,
450
+ the token ruler it actually uses, and the cost of the PTC-mode switch.
451
+
452
+ ## [6.0.2] - 2026-10-26
453
+
454
+ ### Fixed
455
+
456
+ - **Program-internal tool calls work across the whole supported range.** The tool
457
+ execution context sits in a different argument slot on older pi releases, so the
458
+ bridge now identifies it by "whoever carries `executeTool`" instead of a fixed position.
459
+ - **Tools return text to programs on every version.** pi 0.99–1.0.3 hand back a
460
+ content-block array where 1.0.4+ hands back a string; the bridge normalises both into
461
+ text, so `tools.read` reads the same way everywhere.
462
+ - **The binding scope is read from the session's own tool set.** On pi 0.86 and 0.87
463
+ `getActiveTools()` lists only an extension's own tools, so the built-in tools are taken
464
+ from `getAllTools()` there; on 0.99+ the active set is the declared set and is used as is.
465
+ - **A clear message where the platform has no channel at all.** pi 0.86 and 0.87 expose no
466
+ nested tool execution API; a program's `tools.*` call now says so — "this pi version has
467
+ no in-program tool channel (needs pi ≥ 0.99)" — instead of failing vaguely.
468
+
469
+ ## [6.0.1] - 2026-10-26
470
+
471
+ ### Fixed
472
+
473
+ - **Programs can call the session's tools again.** The binding bridge looked for
474
+ `ctx.executeTool` on the session context. pi 1.4.0 exposes it on the **tool execution
475
+ context** — `execute(toolCallId, params, signal, onUpdate, ctx)` — with the signature
476
+ `executeTool(name, args, options)`. Every `tools.*` call inside a program failed with
477
+ "executeTool 不可用". Measured on a real pi: a program that reads `/etc/hosts` now gets
478
+ its length back.
479
+ - **Route details ship with the package and resolve.** The seven `docs/routes/*.md` files
480
+ are in the npm tarball now, and the route index points at their absolute path inside the
481
+ installed package, so the model's `read` finds them instead of a path relative to the
482
+ user's project.
483
+
484
+ ### Changed
485
+
486
+ - **PTC mode no longer narrows the tool loadout.** On pi 1.4.0 a tool is callable from a
487
+ program only while it is active, so narrowing took `tools.read` away from programs too
488
+ ("Tool read not found"). A built-in tool's exposure is not an extension's to set, so the
489
+ two cannot both hold. PTC mode is now a routing lead in the system prompt: the built-in
490
+ tools stay visible and callable, from the model and from programs.
491
+
492
+ ## [6.0.0] - 2026-10-26
493
+
494
+ ### Changed
495
+
496
+ - **Clean-room rewrite.** The package is rebuilt as seven packages — `entry`, `ptc`,
497
+ `dispatch`, `task`, `tui`, `routes`, `search` — and ships as one self-contained
498
+ artifact. The previous implementation is preserved on `backup/main-legacy-20261026`.
499
+ - **Two surfaces, detected.** `full_ptc` keeps the PTC pair model-facing; `codemode_ptc`
500
+ registers underneath pi's codemode and exposes the task door to scripts. There is no
501
+ setting.
502
+ - **Subagent dispatch is a task engine.** Every dispatch gets a record, a slot, and its
503
+ own `pi` process. Busy means queue, not reject. Reports arrive through
504
+ `ptc_child_report` first, with the trailing-JSON channel as fallback.
505
+ - **The task face** (`ptc_task_list` / `ptc_task_output` / `ptc_task_stop`) appears only
506
+ while there is a task to see.
507
+ - **Web search lives in children.** `web_ds_search` registers only inside dispatched
508
+ children and is off by default.
509
+
510
+ ### Compatibility
511
+
512
+ - pi `>= 0.86.0`. Real-machine smoke tests cover 0.86.0, 0.87, 0.99, 1.0, 1.1, 1.2,
513
+ 1.3, and 1.4 — including a program that dispatches a real child process.
package/README.md CHANGED
@@ -81,6 +81,26 @@ URL tracks the latest GitHub Release (pinned URLs get no update checks); run `/r
81
81
  activate. A bundled `ptc` skill teaches the model when to write a program instead of
82
82
  fanning out tool calls one by one.
83
83
 
84
+ **Update badges need a marketplace entry.** Kimi Code only offers "update 7.1.0 → 7.5.0" in
85
+ `/plugins` for plugins the marketplace catalog can match by id — a bare GitHub install is
86
+ installed but never listed, so it never gets a badge. This repo ships the catalog; point
87
+ Kimi Code at it:
88
+
89
+ ```bash
90
+ /plugins marketplace https://raw.githubusercontent.com/a1121611810/pi-ptc-subagents/main/plugin-marketplace.json
91
+ ```
92
+
93
+ or keep it for every session (desktop app included):
94
+
95
+ ```bash
96
+ export KIMI_CODE_PLUGIN_MARKETPLACE_URL=https://raw.githubusercontent.com/a1121611810/pi-ptc-subagents/main/plugin-marketplace.json
97
+ ```
98
+
99
+ The entry deliberately omits `version`: Kimi Code resolves it from the latest GitHub
100
+ Release, so the catalog never goes stale. A custom catalog **replaces** the official one
101
+ rather than adding to it, and updates stay one keystroke (`Enter` in `/plugins`) — Kimi
102
+ Code has no auto-download path.
103
+
84
104
  ## The engine on its own (no model, no pi)
85
105
 
86
106
  The PTC runtime is a zero-dependency engine inside this package. Install it and run programs
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as piHost from "@earendil-works/pi-coding-agent";
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { execSync, spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { mkdir, open, readFile, readdir, rename, writeFile } from "node:fs/promises";
@@ -9,8 +9,8 @@ import { gunzipSync, gzip } from "node:zlib";
9
9
  import { promisify } from "node:util";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { tmpdir } from "node:os";
12
- import { Worker } from "node:worker_threads";
13
12
  import { Box, Text } from "@earendil-works/pi-tui";
13
+ import { Worker } from "node:worker_threads";
14
14
  // ../../node_modules/.pnpm/typebox@1.3.27/node_modules/typebox/build/system/memory/metrics.mjs
15
15
  /** TypeBox instantiation metrics */
16
16
  const Metrics = {
@@ -4294,6 +4294,118 @@ async function readTaskOutput(source, record) {
4294
4294
  })
4295
4295
  };
4296
4296
  }
4297
+ // ../tui/src/pi/render.ts
4298
+ function renderPtcResult(surface) {
4299
+ return (result, options, _theme, context) => {
4300
+ const args = context.args;
4301
+ const box = new Box();
4302
+ const title = surface === "workflow" ? `workflow · ${args.meta?.name ?? ""}` : args.description ?? "程序";
4303
+ box.addChild(new Text(options.isPartial ? `${title} …` : title));
4304
+ const details = result.details;
4305
+ if (!options.isPartial && details) {
4306
+ const flags = [
4307
+ details.status,
4308
+ details.truncated ? "已截断" : "",
4309
+ details.fullOutputPath ? `全文在 ${details.fullOutputPath}` : ""
4310
+ ].filter(Boolean).join(" · ");
4311
+ box.addChild(new Text(flags));
4312
+ }
4313
+ if (options.expanded && !options.isPartial) for (const block of result.content) {
4314
+ if (block.type !== "text") continue;
4315
+ const lines = block.text.split("\n");
4316
+ const kept = lines.slice(0, RENDER_BOUNDS.MAX_LOG_LINES_EXPANDED);
4317
+ for (const line of kept) box.addChild(new Text(line));
4318
+ if (lines.length > kept.length) box.addChild(new Text(`…(共 ${lines.length} 行,省略 ${lines.length - kept.length} 行)`));
4319
+ }
4320
+ return box;
4321
+ };
4322
+ }
4323
+ // ../tui/src/pi/task-panel.ts
4324
+ /** 面板的宿主 widget key——同 key 覆盖 ⇒ /reload 与重复注册都幂等(pi 的 setExtensionWidget 先删旧再设新) */
4325
+ const TASK_PANEL_KEY = "ptc-tasks";
4326
+ const STATUS_STYLE = {
4327
+ queued: {
4328
+ glyph: "○",
4329
+ color: "muted"
4330
+ },
4331
+ running: {
4332
+ glyph: "●",
4333
+ color: "accent"
4334
+ },
4335
+ stopping: {
4336
+ glyph: "◐",
4337
+ color: "warning"
4338
+ },
4339
+ succeeded: {
4340
+ glyph: "✓",
4341
+ color: "success"
4342
+ },
4343
+ failed: {
4344
+ glyph: "✗",
4345
+ color: "error"
4346
+ },
4347
+ canceled: {
4348
+ glyph: "⊘",
4349
+ color: "muted"
4350
+ },
4351
+ lost: {
4352
+ glyph: "?",
4353
+ color: "dim"
4354
+ }
4355
+ };
4356
+ const IN_FLIGHT = [
4357
+ "queued",
4358
+ "running",
4359
+ "stopping"
4360
+ ];
4361
+ function clockOf(startedAt) {
4362
+ const d = new Date(startedAt);
4363
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
4364
+ }
4365
+ /**
4366
+ * 面板行(纯函数):本会话进行中的子代理逐条 + 计数标题;没有进行中的任务 ⇒ 空数组(宿主据此卸载面板)。
4367
+ * 只出字符串数组——宿主自带 10 行上限,pi-tui 的 Text 保留 ANSI ⇒ 颜色直接由 theme.fg 上。
4368
+ *
4369
+ * 只看本会话的记录:registry 启动时会把磁盘上的历史会话记录一并恢复(真机实测:标题一度写「succeeded 180」——
4370
+ * 那是上一轮会话攒下的)。sessionId 缺失时不筛(老记录没有这个字段)。
4371
+ */
4372
+ function renderTaskPanel(records, theme, sessionId) {
4373
+ const mine = sessionId ? records.filter((record) => record.sessionId === sessionId) : records;
4374
+ const inFlight = mine.filter((record) => IN_FLIGHT.includes(record.status));
4375
+ if (inFlight.length === 0) return [];
4376
+ const count = (status) => mine.filter((record) => record.status === status).length;
4377
+ const lines = [theme.fg("accent", `Tasks · ${inFlight.length} running · ${count("succeeded")} succeeded · ${count("failed")} failed`)];
4378
+ const shown = inFlight.slice(0, 6);
4379
+ for (const [index, record] of shown.entries()) {
4380
+ const style = STATUS_STYLE[record.status];
4381
+ const label = truncateLine(sanitizeText(record.label || record.task).replace(/\s+/g, " ").trim(), 48);
4382
+ const last = index === shown.length - 1 && inFlight.length <= 6;
4383
+ lines.push(theme.fg("dim", last ? "└─ " : "├─ ") + theme.fg(style.color, `${style.glyph} ${label}`) + (record.startedAt ? theme.fg("dim", ` · ${clockOf(record.startedAt)}`) : ""));
4384
+ }
4385
+ if (inFlight.length > 6) lines.push(theme.fg("dim", `└─ …${inFlight.length - 6} more`));
4386
+ return lines;
4387
+ }
4388
+ // ../tui/src/pi/panel-widget.ts
4389
+ /**
4390
+ * 常驻面板的接线(ADR-0022):登记 widget、记录一变就重画、退出时卸载。
4391
+ * 没有 ui(print/json 模式)⇒ 什么都不做——那两种模式下 pi 的 ctx.ui 是空实现。
4392
+ */
4393
+ function createTaskPanel(options) {
4394
+ const { ui, source, sessionId } = options;
4395
+ if (!ui) return { dispose() {} };
4396
+ let disposed = false;
4397
+ const refresh = () => {
4398
+ if (disposed) return;
4399
+ const lines = renderTaskPanel(source.list(), ui.theme, sessionId);
4400
+ ui.setWidget(TASK_PANEL_KEY, lines.length > 0 ? lines : void 0);
4401
+ };
4402
+ source.subscribe(refresh);
4403
+ refresh();
4404
+ return { dispose() {
4405
+ disposed = true;
4406
+ ui.setWidget(TASK_PANEL_KEY, void 0);
4407
+ } };
4408
+ }
4297
4409
  // ../search/src/provider.ts
4298
4410
  const SEARCH_COUNT_RANGE = {
4299
4411
  min: 1,
@@ -4685,6 +4797,184 @@ function createWebDsSearchTool(context) {
4685
4797
  }
4686
4798
  };
4687
4799
  }
4800
+ // src/upgrade-notice.ts
4801
+ const PACKAGE_NAME = "pi-ptc-subagents";
4802
+ const BULLET_LIMIT = 5;
4803
+ const SUMMARY_MAX = 200;
4804
+ /** Keep a Changelog 里只有这三段是"变了什么";Verification/Compatibility 是测试与兼容矩阵,不是要点 */
4805
+ const CHANGE_SECTIONS = [
4806
+ "Added",
4807
+ "Changed",
4808
+ "Fixed"
4809
+ ];
4810
+ /** x.y.z → 可比整数;非 semver → undefined */
4811
+ function versionNumber(version) {
4812
+ if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) return void 0;
4813
+ const parts = version.split(".").map(Number);
4814
+ return (parts[0] ?? 0) * 0xe8d4a51000 + (parts[1] ?? 0) * 1e6 + (parts[2] ?? 0);
4815
+ }
4816
+ /** CHANGELOG.md → 版本段(文件里新版本在前) */
4817
+ function parseChangelog(text) {
4818
+ const sections = [];
4819
+ for (const raw of text.split(/^## /m).slice(1)) {
4820
+ const version = /^\[(\d+\.\d+\.\d+)\]/.exec(raw)?.[1];
4821
+ if (!version) continue;
4822
+ const bullets = [];
4823
+ let inChangeSection = false;
4824
+ let current = [];
4825
+ const flush = () => {
4826
+ if (current.length > 0) bullets.push(current.join(" "));
4827
+ current = [];
4828
+ };
4829
+ for (const line of raw.split("\n")) {
4830
+ const heading = /^###\s+(.+)$/.exec(line);
4831
+ if (heading) {
4832
+ flush();
4833
+ inChangeSection = CHANGE_SECTIONS.includes(heading[1]?.trim() ?? "");
4834
+ continue;
4835
+ }
4836
+ if (!inChangeSection) continue;
4837
+ if (line.startsWith("- ")) {
4838
+ flush();
4839
+ current = [line.slice(2).trim()];
4840
+ continue;
4841
+ }
4842
+ if (current.length > 0 && line.trim().length > 0) {
4843
+ current.push(line.trim());
4844
+ continue;
4845
+ }
4846
+ flush();
4847
+ }
4848
+ flush();
4849
+ sections.push({
4850
+ version,
4851
+ bullets
4852
+ });
4853
+ }
4854
+ return sections;
4855
+ }
4856
+ /** bullet → 一行要点:粗体标题是作者自己的摘要,优先用它;否则取首句。列表项统一不带句点 */
4857
+ function summarize(bullet) {
4858
+ const title = /^\*\*(.+?)\*\*/.exec(bullet);
4859
+ if (title) return truncate(stripMarkdown(title[1] ?? "").replace(/\.$/, ""));
4860
+ const sentence = /(?<!\d)\.(?=\s)/.exec(bullet);
4861
+ return truncate(stripMarkdown(sentence ? bullet.slice(0, sentence.index + 1) : bullet).replace(/\.$/, ""));
4862
+ }
4863
+ /** (previous, current] 区间的要点,新版本在前 */
4864
+ function pickBullets(sections, previous, current) {
4865
+ const low = versionNumber(previous);
4866
+ const high = versionNumber(current);
4867
+ if (low === void 0 || high === void 0) return [];
4868
+ const bullets = [];
4869
+ for (const section of sections) {
4870
+ const value = versionNumber(section.version);
4871
+ if (value === void 0 || value <= low || value > high) continue;
4872
+ for (const bullet of section.bullets) bullets.push(summarize(bullet));
4873
+ }
4874
+ return bullets;
4875
+ }
4876
+ function showUpgradeNotice(ctx, environment) {
4877
+ if (!ctx.hasUI) return;
4878
+ if (typeof ctx.ui?.notify !== "function") return;
4879
+ const env = environment ?? resolveEnvironment();
4880
+ if (!env) return;
4881
+ const current = versionNumber(env.version);
4882
+ if (current === void 0) return;
4883
+ const previous = readSeenVersion(env.statePath);
4884
+ const previousNumber = versionNumber(previous);
4885
+ if (previousNumber === current) return;
4886
+ if (previousNumber !== void 0 && previousNumber > current) return;
4887
+ if (!writeSeenVersion(env.statePath, env.version)) return;
4888
+ const sections = parseChangelog(readText(env.changelogPath));
4889
+ const baseline = previousNumber === void 0 ? newestOlderVersion(sections, env.version) : previous;
4890
+ if (baseline === void 0) return;
4891
+ const bullets = pickBullets(sections, baseline, env.version);
4892
+ ctx.ui.notify(compose(previousNumber === void 0 ? void 0 : previous, env.version, bullets, changelogUrl(env.repository)), "info");
4893
+ }
4894
+ /** 已安装包:bundle 往上找 package.json,name 对上就是它——发布形态 <pkg>/dist/index.js,开发形态仓库根 */
4895
+ function findInstalledPackage(bundleUrl = import.meta.url) {
4896
+ for (const relative of ["..", "../../.."]) try {
4897
+ const dir = fileURLToPath(new URL(`${relative}/`, bundleUrl)).replace(/[\\/]+$/, "");
4898
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
4899
+ if (pkg.name !== PACKAGE_NAME || !pkg.version) continue;
4900
+ return {
4901
+ dir,
4902
+ version: pkg.version,
4903
+ repository: typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url
4904
+ };
4905
+ } catch {}
4906
+ }
4907
+ function resolveEnvironment() {
4908
+ const pkg = findInstalledPackage();
4909
+ if (!pkg) return void 0;
4910
+ return {
4911
+ version: pkg.version,
4912
+ repository: pkg.repository,
4913
+ changelogPath: join(pkg.dir, "CHANGELOG.md"),
4914
+ statePath: join(getAgentDir(), PACKAGE_NAME, "last-seen-version.json")
4915
+ };
4916
+ }
4917
+ /** CHANGELOG 里比当前版本小的最新一节——没有历史记录时用它当基线 */
4918
+ function newestOlderVersion(sections, current) {
4919
+ const high = versionNumber(current);
4920
+ if (high === void 0) return void 0;
4921
+ let best;
4922
+ for (const section of sections) {
4923
+ const value = versionNumber(section.version);
4924
+ if (value === void 0 || value >= high) continue;
4925
+ if (best === void 0 || value > (versionNumber(best) ?? 0)) best = section.version;
4926
+ }
4927
+ return best;
4928
+ }
4929
+ function compose(previous, version, bullets, url) {
4930
+ const lines = [`${PACKAGE_NAME} ${version}`];
4931
+ if (previous !== void 0) lines.push(`Updated from ${previous}.`);
4932
+ if (bullets.length > 0) {
4933
+ lines.push("", "What's new:");
4934
+ for (const bullet of bullets.slice(0, BULLET_LIMIT)) lines.push(`- ${bullet}`);
4935
+ if (bullets.length > BULLET_LIMIT) lines.push(`...and ${bullets.length - BULLET_LIMIT} more`);
4936
+ }
4937
+ if (url) lines.push("", `Changelog: ${url}`);
4938
+ return lines.join("\n");
4939
+ }
4940
+ function changelogUrl(repository) {
4941
+ if (!repository) return void 0;
4942
+ const url = repository.replace(/^git\+/, "").replace(/\.git$/, "");
4943
+ return url.startsWith("https://github.com/") ? `${url}/blob/main/CHANGELOG.md` : void 0;
4944
+ }
4945
+ function readSeenVersion(statePath) {
4946
+ try {
4947
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
4948
+ return typeof parsed.version === "string" ? parsed.version : void 0;
4949
+ } catch {
4950
+ return;
4951
+ }
4952
+ }
4953
+ function writeSeenVersion(statePath, version) {
4954
+ try {
4955
+ mkdirSync(dirname(statePath), { recursive: true });
4956
+ writeFileSync(statePath, JSON.stringify({ version }));
4957
+ return true;
4958
+ } catch {
4959
+ return false;
4960
+ }
4961
+ }
4962
+ function readText(path) {
4963
+ try {
4964
+ return readFileSync(path, "utf8");
4965
+ } catch {
4966
+ return "";
4967
+ }
4968
+ }
4969
+ function stripMarkdown(text) {
4970
+ return text.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*/g, "").trim();
4971
+ }
4972
+ function truncate(text) {
4973
+ if (text.length <= SUMMARY_MAX) return text;
4974
+ const cut = text.slice(0, SUMMARY_MAX);
4975
+ const space = cut.lastIndexOf(" ");
4976
+ return `${cut.slice(0, space > 0 ? space : SUMMARY_MAX).trimEnd()}…`;
4977
+ }
4688
4978
  // ../ptc/src/limits.ts
4689
4979
  const DEFAULT_LIMITS = {
4690
4980
  timeoutMs: 12e4,
@@ -5026,32 +5316,6 @@ function renderToolResult(outcome) {
5026
5316
  fullOutputPath
5027
5317
  };
5028
5318
  }
5029
- // src/ptc-render.ts
5030
- function renderPtcResult(surface) {
5031
- return (result, options, _theme, context) => {
5032
- const args = context.args;
5033
- const box = new Box();
5034
- const title = surface === "workflow" ? `workflow · ${args.meta?.name ?? ""}` : args.description ?? "程序";
5035
- box.addChild(new Text(options.isPartial ? `${title} …` : title));
5036
- const details = result.details;
5037
- if (!options.isPartial && details) {
5038
- const flags = [
5039
- details.status,
5040
- details.truncated ? "已截断" : "",
5041
- details.fullOutputPath ? `全文在 ${details.fullOutputPath}` : ""
5042
- ].filter(Boolean).join(" · ");
5043
- box.addChild(new Text(flags));
5044
- }
5045
- if (options.expanded && !options.isPartial) for (const block of result.content) {
5046
- if (block.type !== "text") continue;
5047
- const lines = block.text.split("\n");
5048
- const kept = lines.slice(0, RENDER_BOUNDS.MAX_LOG_LINES_EXPANDED);
5049
- for (const line of kept) box.addChild(new Text(line));
5050
- if (lines.length > kept.length) box.addChild(new Text(`…(共 ${lines.length} 行,省略 ${lines.length - kept.length} 行)`));
5051
- }
5052
- return box;
5053
- };
5054
- }
5055
5319
  // src/ptc-tools.ts
5056
5320
  const RUN_CODE_PARAMETERS = _Object_({
5057
5321
  code: String$1({ description: "要执行的 TypeScript 程序体,可用顶层 await 和 return" }),
@@ -5345,6 +5609,9 @@ function piPtcEntry(pi) {
5345
5609
  registerChildToolsOnly(pi);
5346
5610
  return;
5347
5611
  }
5612
+ try {
5613
+ showUpgradeNotice(ctx);
5614
+ } catch {}
5348
5615
  const surface = pi.getActiveTools().includes("codemode") ? "codemode_ptc" : FALLBACK_SURFACE_MODE;
5349
5616
  let modeEnabled = readDefaultModeConfig(getAgentDir());
5350
5617
  let modeActive = false;
@@ -5447,8 +5714,14 @@ function piPtcEntry(pi) {
5447
5714
  cmdCtx.ui.notify(`/ptc:dispatchEnabled=${dispatchEnabled},mode=${modeActive ? "on(引导层)" : "off"},whyEnabled=${whyEnabledOverride ?? whyEnabledPersisted}(在飞任务不受影响)`, "info");
5448
5715
  }
5449
5716
  });
5717
+ const taskPanel = createTaskPanel({
5718
+ ui: ctx.mode === "tui" ? ctx.ui : void 0,
5719
+ source: session.registry,
5720
+ sessionId: session.sessionId
5721
+ });
5450
5722
  pi.on("session_shutdown", () => {
5451
5723
  sessionEnding = true;
5724
+ taskPanel.dispose();
5452
5725
  pool.drain();
5453
5726
  session.shutdown();
5454
5727
  });
@@ -5784,4 +6057,4 @@ function registerTaskDoor(registerTool, session, surface) {
5784
6057
  }
5785
6058
  });
5786
6059
  }
5787
- export { assemblePtcSession, buildEngineContext, createTaskOutputBinding, piPtcEntry as default, withDeliveryNote };
6060
+ export { assemblePtcSession, buildEngineContext, createTaskOutputBinding, piPtcEntry as default, findInstalledPackage, parseChangelog, pickBullets, showUpgradeNotice, summarize, versionNumber, withDeliveryNote };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ptc-subagents",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "description": "Programmable tool calling (PTC) and subagent fan-out for pi",
5
5
  "keywords": [
6
6
  "pi",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist/**/*.js",
26
26
  "docs/routes/**/*.md",
27
+ "CHANGELOG.md",
27
28
  "README.md",
28
29
  "LICENSE"
29
30
  ],