pi-ptc-subagents 7.5.0 → 7.7.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,554 @@
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.7.0] - 2026-11-02
9
+
10
+ ### Added
11
+
12
+ - **The plugin now says so when a newer release exists** (Kimi face, ADR-0023): a
13
+ `UserPromptSubmit` hook compares the plugin's own manifest version against the latest
14
+ GitHub Release and, when they differ, attaches the notice to your next message — the
15
+ agent then says so in its reply, with the update command. Nothing to configure, no
16
+ marketplace to point at. Kimi Code has no plugin auto-update path: the update badge
17
+ only exists for plugins a marketplace catalog can match by id, and pointing Kimi Code
18
+ at a custom catalog _replaces_ the default one (measured: the default catalog has 7
19
+ entries — 3 official, 4 curated — a custom one has 1; the official plugins drop out of
20
+ the panel and lose their "update detected" notification, which requires the default
21
+ CDN catalog). The badge is also only visible when you open `/plugins`: the desktop
22
+ "installed" row shows version, toggle and delete, and no update button.
23
+ The check reads the latest release through `/releases/latest` (a redirect, so
24
+ `api.github.com` is never called), accepts plain `x.y.z` only, caches for 6 hours,
25
+ speaks once per version (again after 7 ignored days), and stays silent offline —
26
+ fail-open, it never blocks a turn. Probes: `tests/kimi/version-check.test.mjs`, 7/7 —
27
+ one of them caught a real bug while it was being written (the cache stored a parsed
28
+ array and re-parsed it as a string, so the check exited silently — exactly the failure
29
+ this feature exists to prevent).
30
+
31
+ - **Artifact import gate** (`scripts/check-artifact-imports.mjs`, wired into `pnpm test`): a
32
+ built artifact's bare imports must be declared by the package that produced them. The rule is
33
+ "the declaration follows the artifact, not just the source" — moving code between packages used
34
+ to carry the dependency declaration along with the source, while the built `dist/index.js` kept
35
+ importing the package: on a clean install the extension could not resolve it (7 probes red, CI
36
+ red too; the local tree only stayed green because a months-old symlink in the root
37
+ `node_modules` happened to cover it). `dependencies` / `peerDependencies` /
38
+ `optionalDependencies` count, `devDependencies` does not (they do not ship), and imports shown
39
+ inside comments or strings are not imports.
40
+
41
+ ### Fixed
42
+
43
+ - **Marketplace catalog entry** (`plugin-marketplace.json`, ADR-0021): the entry's `tier`
44
+ must be `official` or `curated` — that is the parser's whitelist, which is stricter than
45
+ the response schema's enum. Writing `third-party` (read off the response schema) made the
46
+ whole catalog fail to load, taking the official entries with it. The entry now omits
47
+ `tier`, and `build:kimi` fails the build if a `tier` ever shows up.
48
+
49
+ ## [7.6.0] - 2026-11-02
50
+
51
+ ### Added
52
+
53
+ - **Persistent subagent status panel** (above the pi TUI editor): the state of dispatched
54
+ subagents moves out of the model context into the human's line of sight. The panel lists every
55
+ in-flight subagent (glyph + status color + start time) under the title
56
+ `Tasks · N running · M succeeded · K failed`, and disappears when nothing is in flight.
57
+ **Panel text is English by default** — the same language as the pi host UI.
58
+ Refresh rides the registry's existing transition callback, so the panel changes exactly when a
59
+ record changes. The guard is `ctx.mode === "tui"` (`hasUI` is also true in RPC mode, so it
60
+ cannot serve as the guard); a plain string array is enough — the host caps widgets at 10 rows,
61
+ and pi-tui's `Text` keeps ANSI, so the colors come straight from the theme.
62
+ Two frozen limits (ADR-0022 §8): the row count is derived from the host cap, and the label
63
+ truncation stays at the 48 chars of the old ADR-0022 §11.
64
+ Panel text (written by the model, often copied verbatim from the user) is stripped of ANSI and
65
+ control characters, then whitespace is collapsed, and only then is it truncated — in the other
66
+ order escape sequences would be cut in half and a newline would split one task into two lines.
67
+ The wiring lives in `packages/tui/pi`'s `createTaskPanel`, which subscribes to the registry
68
+ itself; `entry` only creates and disposes it.
69
+ The panel survives `/reload` with no leftover and no duplicate: the same widget key overwrites,
70
+ and the reloaded instance re-subscribes and follows the state.
71
+
72
+ ### Changed
73
+
74
+ - **`packages/tui` is now the display layer** (ADR-0022): the root entry is still the
75
+ zero-dependency shared text primitives (`output` / `search` / `task` are unchanged), while the
76
+ pi picture — the collapsible result rendering plus the panel above — moves behind the `./pi`
77
+ subpath. `entry` goes back to assembly only, and the repository's single pi-tui **import site**
78
+ is gone from `entry`'s sources. `entry` keeps declaring `@earendil-works/pi-tui`, though: its
79
+ built `dist/index.js` still imports it, so the declaration follows the artifact, not just the
80
+ source (a clean install that cannot resolve it breaks the extension — caught by the
81
+ pre-release clean-worktree check).
82
+ The root entry **must not** re-export the pi face: `kimi-mcp`'s build alias points at
83
+ `src/index.ts`, so a re-export would bundle pi host code into the Kimi artifact. `build:kimi`
84
+ therefore gained a gate — the artifact throws if `@earendil-works` shows up in it. The
85
+ published shape is unchanged (still only `pi-coding-agent` and `pi-tui` as external imports)
86
+ and the Kimi artifact is byte-identical.
87
+
88
+ ### Verification
89
+
90
+ - Real pi TUI over a real pty (120×40): the panel draws right above the editor, updates as
91
+ tasks finish (`Tasks · 2 running · 0 succeeded · 0 failed` → `Tasks · 1 running · 1 succeeded
92
+ · 0 failed`), and vanishes when they are all done. Floor 0.86.0 renders it too (`setWidget`
93
+ has the same signature on both — no version gate).
94
+ - 16-version real-machine smoke (`scripts/version-smoke.sh`): **16/16, fails=0** — 0.86.0 /
95
+ 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
96
+ 1.4.0 on PATH.
97
+ - Repository tests: **172 pass, 0 fail** (139 pi / 6 output / 17 Kimi / 10 standalone); the panel
98
+ probe is 12/12 (9 for the pure renderer, 3 for the wiring).
99
+
100
+ ## [7.5.0] - 2026-11-01
101
+
102
+ ### Added
103
+
104
+ - **Program-internal task output** (`tools.task_output({ taskId })`): a PTC program can now fetch
105
+ the prose answer a dispatched child wrote. Measured gap (real pi 1.4.0): a `wait:false`
106
+ dispatch returns only a handle, and the in-program task records carry metadata only
107
+ (`delivery,finishedAt,id,outputPath,outputTruncated,ownerBootMs,ownerPid,reportChannel,
108
+ startedAt,status,task,whySuppressed` — no `text`, no `output`), so the child's answer was
109
+ unreachable from a program. The new binding uses the same reduction layer as the model-facing
110
+ `ptc_task_output` (the child's prose, not the raw event stream), never throws (unknown id →
111
+ `{error}`), and returns `status` so a program can tell "not finished yet" from "finished
112
+ without an answer". It also finds records that were pushed out of memory into the compressed
113
+ archive (decision 29).
114
+
115
+ ### Changed
116
+
117
+ - **The route-index token gate now measures the real install path.** It used a short relative
118
+ path, which was green at 555 tokens while the real installed index was 625–632 against a 560
119
+ ceiling — the model saw a `> WARNING: index over token budget` line on every turn. The gate now
120
+ uses a 100-character path (the longest realistic install), route prose was trimmed of text
121
+ duplicated at runtime, and an inaccurate tool list in the index was corrected
122
+ (`tools.read/grep/find/ls/edit/write` → "the same-named tools"; pi 1.4.0's active table has no
123
+ grep/find/ls). The index now measures 558 at the gate path, 552 for an nvm install, 541 in the
124
+ repo.
125
+ - **`RouteEntry.fact` removed.** The field was never rendered (dead weight) and it contradicted
126
+ ADR-0015's own decision that the index carries no fact section; facts live in the detail files
127
+ now. ADR-0015's gate table was amended accordingly (gate 5 now guards the `how` section).
128
+ - The web-search detail file now says plainly that the `webDsSearch` switch is the user's own
129
+ configuration — don't flip it, say that search is unavailable. Measured (real pi 1.4.0, four
130
+ arms): sessions that read "default off" and went hunting for the switch to change it were 4×
131
+ slower than sessions that simply said it could not be done.
132
+
133
+ ### Fixed
134
+
135
+ - The program-internal binding accepted only `taskId`, while the model-facing tool spells it
136
+ `task_id`; a model copying the model-facing spelling got an error naming a parameter it never
137
+ used. Both spellings are accepted now.
138
+
139
+ ## [7.4.0] - 2026-11-01
140
+
141
+ ### Added
142
+
143
+ - **PTC as a standalone, host-neutral package** (`packages/ptc`): the engine — run cell, wire
144
+ protocol, worker pool, compiler, file bindings — now depends on nothing and imports no host.
145
+ The model-facing surface moved out to `packages/entry`, so `packages/ptc` is the engine alone
146
+ (zero dependencies, no `peerDependencies`). It ships as a subpath of the existing root package
147
+ (`pi-ptc-subagents/ptc`) and as a `ptc` binary, so installing it needs no second package and no
148
+ extra dependency — verified by a real `npm pack` into a clean directory: one package, zero
149
+ transitive dependencies, both the subpath import and the binary work.
150
+ - **`ptc <program.ts>` CLI**: runs a program file without a model and without pi. The program's
151
+ tool surface is the file four (`read` / `write` / `grep` / `glob` / `edit`), confined to the
152
+ work directory. The published `dist/index.js` stays one self-contained 244 KB file — the
153
+ published shape did not change.
154
+ - **Program tools on pi 0.86 / 0.87** (entry): the nested call channel (`ctx.executeTool`) only
155
+ exists from 0.99. On the floor versions the extension now builds pi's own tool definitions
156
+ (its public `create*ToolDefinition` factories) and calls them directly, so the program tool
157
+ set and parameter shapes are byte-identical to 0.99 — including `edit`'s
158
+ `{path, edits:[{oldText,newText}]}`. That path is gated on the pi version: on 0.99+ a missing
159
+ channel is an error, never a silent bypass of pi's tool pipeline (which would hide the call
160
+ from other extensions' `tool_call` hooks and from usage accounting).
161
+
162
+ ### Changed
163
+
164
+ - **Package layout**: `packages/ptc/src/{tools,render,bindings,task-binding}.ts` moved to
165
+ `packages/entry/src/{ptc-tools,ptc-render,ptc-bindings,task-binding}.ts`; the file bindings
166
+ moved the other way (`packages/kimi-mcp/src/fs-tools.ts` → `packages/ptc/src/fs-bindings.ts`),
167
+ because they are host-neutral. The engine's zero-host-dependency rule is now a test
168
+ (`tests/standalone/standalone-engine.test.mjs`).
169
+ - **Kimi bundle**: 785 KB → 592 KB after pi-tui left the engine.
170
+
171
+ ### Fixed
172
+
173
+ - The floor versions' file-binding fallback was unreachable — all four of them ship the tool
174
+ factories — and its `edit` shape (`{path, old_string, new_string}`) differed from pi's, so a
175
+ program written for 0.99+ would break on the floor. It is gone.
176
+ - Two dead exports (`HostToWorkerFrame`, `WorkerToHostFrame`) and a stale
177
+ `external: [/^@earendil-works\//]` in the engine's rolldown config (the engine has no
178
+ dependencies any more).
179
+
180
+ ### Verification
181
+
182
+ - 16-version real-machine smoke (`scripts/version-smoke.sh`): **16/16, fails=0** — 0.86.0 /
183
+ 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
184
+ 1.4.0 on PATH. The floor versions and 0.99+ return the same `READ=2216`.
185
+ - Repository tests: **144 pass, 0 fail** (111 pi / 6 output / 17 Kimi / 10 standalone).
186
+
187
+ ## [7.3.0] - 2026-11-01
188
+
189
+ ### Added
190
+
191
+ - **Task-surface recall** (task + entry + dispatch + routes): the parent can find its old
192
+ tasks again and read what the child actually said. `ptc_task_list` takes `scope`
193
+ (recent / all — "all" lists the compaction archive too), `limit` (default 50, max 200),
194
+ `status` and `session`, and answers `{scope, total, tasks[]}` — `total` is the candidate
195
+ count, so a `total` larger than `tasks.length` means older records exist that were not
196
+ listed. Every field is capped (task 80 chars, summary 100, label 120) and the response is
197
+ trimmed entry by entry against a byte budget, so it is always valid JSON (text-level
198
+ truncation would cut JSON in half and `JSON.parse` would throw). Measured: 520 records
199
+ were 104,781 tokens in the old shape; the capped shape stays under 48 KB. Reading the
200
+ whole archive is not the bottleneck either — 3,000 archived records scan in 57 ms (their
201
+ bulk, 1,137 KB ≈ 291k tokens, is what costs).
202
+ - **The child's prose is the deliverable** (`ptc_task_output`): the response now carries
203
+ `text` — the child's own answer, computed from the event stream on the fly — while the
204
+ report is only a summary. With no prose, the raw tail and the layered guidance are
205
+ returned as before, and the full stream stays reachable through `outputPath`.
206
+ - **Task blood-line** (dispatch + entry): every task record carries the host session id
207
+ (`TaskRecord.sessionId`, taken from `ctx.sessionManager.getSessionId()` — present since
208
+ floor 0.86.0, so no version gate), so the parent can filter its own tasks by session.
209
+ - **Re-injection after compaction** (entry + routes): a compaction drops the messages that
210
+ started the tasks, but the tasks keep running. On `session_compact` the host re-injects the
211
+ in-flight tasks and the five most recently completed ones as a `<compacted-tasks>` block on
212
+ the next LLM call. Verified on real pi at both ends — floor 0.86.0 and 1.4.0 both compact
213
+ successfully and inject the list (28→7 and 13→6 messages). No version gate: the `context`
214
+ event payload is byte-identical on 0.86.0.
215
+ - **Stall guard** (dispatch): a child that has produced nothing for the threshold and has
216
+ no tool call in flight is reaped by the host (default 5 minutes — an engine-side constant,
217
+ not a model-facing knob; `timeoutMs` keeps its wall-clock meaning). `stopReason` records
218
+ `stalled: no activity for Nms`, so the parent can see that the child was killed rather
219
+ than finished.
220
+ - **`tools.task_records` in-program binding** (ptc + entry): a program can pull the task
221
+ records itself (`{scope: "all"}` includes the compaction archive) — the mechanical scan
222
+ stays on the host, filtering and reduction happen in the program, and only the few
223
+ records the program returns enter the context. Regular expressions run inside the worker
224
+ cell, which terminates a runaway one: a pathological `(a+)+$` really hangs Node v24 for
225
+ over 120 s, but inside a program it is reaped (`canceled: timeout after 1000ms`) while the
226
+ host keeps running and can start another program afterwards.
227
+ - **`scripts/compact-inject-smoke.mjs`**: real-machine probe for the compaction
228
+ re-injection. Assertions need no model cooperation — the compaction is triggered by the
229
+ rpc `compact` command while the agent is idle, and the injection is checked by a second
230
+ extension reading the request context. Passes on floor 0.86.0 and 1.4.0.
231
+
232
+ ### Changed
233
+
234
+ - **Task-surface responses are objects, not arrays**: `ptc_task_list` answers
235
+ `{scope, total, tasks[]}` and `ptc_task_output` answers the record plus the reduced
236
+ `text`. Nothing in this repository parsed the old array shape, and the version smoke's
237
+ task-face leg stays green on 1.4.0, 0.86.0 and the current PATH (`task_face=OK`, no
238
+ `isError`).
239
+
240
+ ### Fixed
241
+
242
+ - **The child now inherits the parent session's model** (entry): `buildPiArgv` already
243
+ resolved `input.model ?? input.agentModel ?? context.sessionModel`, but the entry never
244
+ passed `sessionModel` — children ran pi's default model instead of the parent's. The
245
+ model is now read at dispatch time through a live getter, so switching models mid
246
+ session switches the child's model too. Verified on eight pi versions 0.86.0–1.4.0, with
247
+ a negative control that goes red when the wiring is removed.
248
+
249
+ ## [7.2.0] - 2026-10-31
250
+
251
+ ### Added
252
+
253
+ - **`timeoutMs` and `projectContext` knobs on task dispatch** (dispatch). `timeoutMs` arms a
254
+ timer only when given: timeout goes through the normal stop path (SIGTERM → SIGKILL),
255
+ `stopReason` records the timeout, terminal state `canceled` — no host-side default; whether
256
+ to use it is the model's call. `projectContext: "none"` loads no AGENTS.md/CLAUDE.md into
257
+ the child (`--no-context-files`, present since pi 0.86.0, so no version gate; default
258
+ `auto` unchanged). Engine-level real-machine probe `scripts/knobs-smoke.mjs` (assertions
259
+ need no model cooperation — timeout reaping, auto/none A/B, report channel, usage fallback)
260
+ 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.
261
+ - **Delivery check** (dispatch + entry + routes): the engine extracts the child's real tool
262
+ trajectory from `tool_execution_start/end` into `ChildDelivery` {tool counts, paths
263
+ touched, tool errors, text chars} — measured from the stream, not model self-report —
264
+ attaches it to the task record alongside the report, and the task surface renders a
265
+ one-line check; when the child delivered nothing, the parent is told to re-dispatch or
266
+ name the gap rather than fill in numbers. Verified against real print-mode streams on pi
267
+ 0.86.0 and 1.4.0 (parser on real events yields e.g. `tools={read:1}`,
268
+ `paths=["/etc/hosts"]`).
269
+ - **`write` — a fifth fs tool in the Kimi face** (`ptc_run_code`): programs can now create
270
+ files, closing the "can't create a file in-program" capability gap found in A/B. Shape
271
+ `{path, content}`, overwrite allowed, parent directory must already exist (clean error, no
272
+ implicit mkdir), 2MB cap counted in bytes, atomic tmp+rename, same realpath jail.
273
+ - **Kimi-face adoption guidance from A/B benching**: SKILL.md gains a two-column "when to
274
+ write a program" checklist (≥5 same-shape calls / ≥20-file read-then-reduce / known-anchor
275
+ batch edits, plus four negative examples); when a fulfilled program made calls, the result
276
+ footer shows "N in-program tool calls reduced to 1"; tool mentions use the full-name
277
+ pattern (`mcp__` prefix + `__ptc_run_code` suffix) after the "skill says write a program
278
+ but the tool isn't in my list" perception break; `ptc_run_code`'s description gains a
279
+ quantified trigger line; `web_ds_search` loses its "in-program" demotion box.
280
+
281
+ ### Changed
282
+
283
+ - **Env pass-through fix, with measured child cost**: the default spawn dropped `env`, so
284
+ `PI_PTC_DEPTH` / `PI_PTC_TASK_ID` never reached the child — children fully assembled as
285
+ depth 0 (whole route book injected, recursion gate ineffective). After the fix children
286
+ take the depth>0 early exit: per-child input −43% (ptc 2830→1607, both 3121→1766).
287
+ Model-side cost multiple corrected to "about 10×" (measured 9–12×; the old 15–22× was
288
+ from before this fix).
289
+ - **Report contract**: the old contract block said `"summary" is your deliverable`, so
290
+ children stuffed facts into self-invented JSON blocks that were then dropped (3/12
291
+ affected; parent recomputed → grader flagged hallucinations). Reworded: the report is the
292
+ summary, the deliverable goes in the task's own format, and the parent-side briefing now
293
+ names the child-report JSON block shape. Measured: named shape → 12/12 reports delivered;
294
+ self-invented formats → 0/4.
295
+ - **Usage fallback**: a child self-reporting all-empty/zero usage no longer masks the
296
+ host-observed value (the task surface was permanently showing 0).
297
+ - **`projectContext: none` docs corrected by measurement** (2026-10-31): none only removes
298
+ the injection — children still read AGENTS.md on their own and apply its conventions
299
+ (none-vs-auto input diff ≈15%, not the "about half" previously claimed; in 6/6 rounds the
300
+ model chose none unprompted).
301
+ - **Parent-side route book**: new first guidance section — dispatch criteria and cost
302
+ ("lots of material / disjoint topics / each piece needs reading ⇒ dispatch; one computable
303
+ job ⇒ don't"); briefing discipline adds "parent writes it wrong → child executes it
304
+ wrong, consistently".
305
+
306
+ ### Fixed
307
+
308
+ - `scripts/version-smoke.sh` installed the wrong package: npm's `pi` is
309
+ IonicaBizau/pi-number (no `pi` bin exists), so every shim install failed — the real
310
+ package is `@earendil-works/pi-coding-agent`.
311
+ - Kimi-face review round: partial run-ab runs no longer wipe `results.jsonl`; arm-restore
312
+ validates its backup (missing or identical-to-arm → refuse to run); the batch-read grader
313
+ verifies per-file line counts; the `write` cap counts `Buffer.byteLength` (1M CJK chars ≈
314
+ 3MB slipped through the character count).
315
+
316
+ ## [7.1.0] - 2026-10-29
317
+
318
+ ### Added
319
+
320
+ - **Kimi Code plugin face (`ptc-tools`, MCP)** (ADR-0020). One repo now ships two install
321
+ faces: the pi extension (this npm package, unchanged name) and a Kimi Code plugin that
322
+ exposes `ptc_run_code` over MCP — the model writes one TypeScript program, batch-calls a
323
+ fixed fs toolset (read/grep/glob/edit) inside a vm cell, and gets one reduced result back
324
+ (measured context economy 1:138 on a 100-file fan-out). `workdir` is a real trust boundary
325
+ (realpath-jailed; symlink escapes rejected). Verified end-to-end in real Kimi Code: skill
326
+ guidance auto-triggers program writing, both Kimi (K2.8) and MiniMax (M3.1-Flash) models
327
+ answered 12/12 deterministic cases with zero unnecessary tool calls and full jail
328
+ compliance. Ships as `release/kimi/` committed at the tag plus `kimi.plugin.json`; update
329
+ detection verified against the official `MoonshotAI/kimi-code` source (GitHub-source-only,
330
+ needs a GitHub Release object per version).
331
+ - **`web_ds_search` in the Kimi face** (P2): the DeepSeek-backed search core (search/open/find)
332
+ is now fully host-neutral and exposed as a second MCP tool; per-process quota with
333
+ fail-soft missing-key behavior. DeepSeek API key resolves env-first, then reuses the key
334
+ already configured in Kimi Code's own `config.toml` (commented-out keys are honored as
335
+ disabled).
336
+ - **Kimi-face quality gates** (`tests/kimi`): mechanical registry gates (tools/list ↔ route
337
+ registry bidirectional with verbatim description match, description token budget, argument
338
+ matrix), plus `scripts/kimi-bench.mjs` (21-case diversity/stress probe; floor Node 22.21.0
339
+ verified 21/21) and `scripts/kimi-model-ab.sh` (model A/B harness).
340
+
341
+ ### Changed
342
+
343
+ - **Host-neutral output core** (`@pi-ptc/output`): run-result shaping (model-visible text,
344
+ ADR-0012 layout + ADR-0015 truncation) split out of the ptc package so both faces share it;
345
+ pi-tui rendering stays pi-side. Test suite split three ways: `tests/pi` / `tests/output` /
346
+ `tests/kimi` (87 cases total, all green).
347
+ - **Worker `execArgv` is now empty** — worker threads no longer inherit the host's exec
348
+ flags. Found by real-machine testing: the Kimi Code host passes `--max-semi-space-size=16`,
349
+ which Node's worker flag validation rejects, killing every program run. Blacklist
350
+ filtering (pi's eval flags) could never keep up with host-specific flags.
351
+ - **Truncation temp files now self-clean**: first truncation in a process sweeps own-prefix
352
+ temp files older than 24h (previously only grew forever).
353
+ - SKILL.md documents exact argument/return shapes for the four fs tools (removes the #1
354
+ model-side friction found in A/B testing); README explains the npm-name vs plugin-name
355
+ split.
356
+
357
+ ### Fixed
358
+
359
+ - `grep` with a file path (`ENOTDIR`), unreadable files killing the whole grep, stale build
360
+ chunks leaking into shipped plugin artifacts, raw `ENOENT` for missing workdir, and
361
+ commented-out DeepSeek keys being picked up as active.
362
+
363
+ ## [7.0.0] - 2026-10-28
364
+
365
+ ### Changed
366
+
367
+ - **BREAKING: PTC programs run in a dedicated vm cell** (spec 决策 30, ADR-0019). Each run
368
+ now executes inside a fresh `vm.createContext`. The host injects only three thin bridges;
369
+ an **IIFE preamble** builds `tools` / `console` / `args` (frozen) / the four `workflow.*`
370
+ helpers **inside the cell (context realm)** and then drops the bridges — the IIFE matters:
371
+ script-level `var` in a vm context lands on the cell's global, and a leaked bridge name
372
+ (`__bridge`) was empirically reachable and escapable during review (fixed, regression-tested).
373
+ Prototype-chain escapes of the `x.constructor.constructor("return process")()` class find
374
+ no host object, and tool results cross the boundary as JSON strings so no host
375
+ Promise/object is ever exposed. `process`, `fetch`, `require`, dynamic `import()`, and all
376
+ `node:*` modules are unreachable — previously a program could `await import("node:fs")`
377
+ and bypass pi's tool pipeline (permissions/approvals) entirely. Programs that used those
378
+ escape hatches will break; programs that only use `tools.*` and `Date`/`Math` are
379
+ unaffected. Worker reset is now "drop the cell" — the globalThis baseline sweep, its known
380
+ prototype-pollution shortcut, and the console patch/restore dance are all gone.
381
+ Tool-call arguments and results are now pure JSON at the cell boundary: `undefined` values
382
+ in arguments are dropped, a `BigInt` argument rejects the call (old structured-clone
383
+ semantics differed; pinned by tests).
384
+ - **Changed: `console.log` of objects prints JSON** (`{"a":1}`) instead of `util.inspect`
385
+ (`{ a: 1 }`) — deterministic, and a side effect of the cell's no-host-object rule.
386
+ Measured cost: none measurable (n=50, fresh-spawn medians ~35–37ms, same as before the
387
+ cell). Accident-surface reduction, **not** a security boundary (trust posture unchanged;
388
+ Node's `vm` is not a sandbox against determined adversaries — see ADR-0019 for the
389
+ residual cosmetic leaks). Deliberately not adopted from the reference designs: determinism
390
+ bans (no replay/journal to serve), process-per-run (warm pool is an asset), QuickJS (WASM
391
+ dependency with no matching gain). 35 new test cases cover visibility, the escape battery
392
+ (13 cases, incl. preamble-internal names), cross-realm semantics, isolation, and protocol
393
+ edges — each proven live by mutation probes (3 mutations, each turning exactly its own
394
+ cases red).
395
+
396
+ ## [6.2.0] - 2026-10-28
397
+
398
+ ### Added
399
+
400
+ - **Lean child process argv**: children are now spawned with `--no-extensions --no-skills`
401
+ (extension discovery noise — user skills, host-side prose, discovered extensions — no longer
402
+ rides into every child), and `--no-mcp` when the host pi is ≥ 1.1.0 (the flag first appears
403
+ there; `EngineContext.piVersion` is resolved once at session start). Measured on real
404
+ children: worst-case per-child input drops 86% (32524 → 4356 tokens).
405
+ - **Cache-aware usage observation**: the JSONL stream parser now carries pi ≥ 1.4.0's
406
+ `usage.cacheRead` / `usage.cacheWrite` into the observed usage (absent on older pi, floor-safe).
407
+
408
+ ### Changed
409
+
410
+ - **Tighter child report contract prose**: summary is the deliverable, findings carry
411
+ load-bearing evidence only, no recapping read content, no report .md files. JSON shape
412
+ unchanged. Measured output reduction: -57% on recap-prone tasks, -9~23% on mixed tasks.
413
+ - `create-a-task.md` gains briefing discipline (exact paths, state what the final message
414
+ must carry, merge same-domain work into one serial child).
415
+ - `docs/pi-version-matrix.md`: `--no-mcp` first-appearance (1.1.0) and 1.4.0 usage cache
416
+ fields recorded.
417
+
418
+ ## [6.1.0] - 2026-10-27
419
+
420
+ ### Added
421
+
422
+ - **Task traces have a capacity boundary and a compressed archive** (spec decision 29).
423
+ Terminal records are capped at the newest 500 (`taskTraceKeep`, evicted oldest-first
424
+ by `(finishedAt, taskId)`; non-terminal records are never evicted). Runtime evictions
425
+ are archived — record and output gzip-compressed (3.4x measured on real child
426
+ streams) on an async event-loop chain the main path never waits on; boot-time
427
+ history is deleted directly. Archived traces stay retrievable by exact id through
428
+ `ptc_task_output` for 30 days from archival (`archiveTtlDays`; archive files are
429
+ purged at boot, 500 per pass). All three knobs live in `~/.pi/agent/ptc.json`;
430
+ `archiveEnabled: false` restores immediate delete.
431
+ - **`outputTruncated` on task records and results.** Child stdout now has a 16MB
432
+ tail cap (the stderr cap's long-missing twin), so a runaway child can no longer
433
+ exhaust the host's memory; the record says when the trace is tail-only.
434
+
435
+ ### Fixed
436
+
437
+ - **The task door no longer advertises a `cwd` parameter nothing consumed** (audit F1).
438
+ It was pre-planted in the shared parameter table, but neither entry point forwarded
439
+ it and no engine layer accepted it; the schema now tells the truth.
440
+ - **Search memory keeps pages opened directly** (audit F5). `open` on a URL that
441
+ never appeared in search results previously had its text silently dropped, so a
442
+ later `find` on the same page re-fetched and double-spent quota; a fallback bucket
443
+ (same 400K budget) now holds it.
444
+ - **Value-tree rendering enforces the frozen contract** (audit F7, ADR-0016): at most
445
+ 6 children per level with an explicit `…(余 N 项)` marker, and line width back to
446
+ the contracted 120 — previously a 100k-element array rendered in full before the
447
+ downstream truncator threw the work away.
448
+ - **Pooled workers drop their per-run listeners when returned** (audit F8); dozens of
449
+ programs in one turn no longer pile dead closures on the reused workers.
450
+ - **The task face no longer removes user-pinned tools** (audit F9): visibility sweeps
451
+ with zero records now retract only tools the session added itself.
452
+ - **Dead code removed** (audit F6): `listAgentNames`, `drainGraceMs`, a duplicated
453
+ `pi-tui` devDependency; three rich-row renderer bounds formally de-registered via
454
+ [ADR-0016](docs/adr/0016-render-bounds-cleanup.md) instead of being deleted as junk.
455
+
456
+ ### Changed
457
+
458
+ - **Value-tree lines narrow from 200 to the contracted 120 characters** — a
459
+ model-visible shortening of oversized render lines.
460
+
461
+ ## [6.0.3] - 2026-10-26
462
+
463
+ ### Fixed
464
+
465
+ - **The route index fits its token budget again.** Every route line carried the
466
+ detail file's absolute path; in an installed package that pushed the index to 695
467
+ tokens against the 560 ceiling, so every turn's system prompt ended with a
468
+ `> WARNING: index over token budget` line — and the warning told the model to
469
+ move the details into the directory they were already in. The directory is now
470
+ stated once and each line keeps only the file name: 481 tokens.
471
+ - **`/ptc mode` no longer claims it is narrowing.** PTC mode stopped narrowing the
472
+ tool loadout in 6.0.1, but the command's reply still read `mode=on(收窄中)`.
473
+ It now reads `on(引导层)`.
474
+ - **A tool result that is an image no longer carries its base64 into the program.**
475
+ pi's `read` returns `{type:"image", data:<base64>}` for image files, so the whole
476
+ image ended up in the program's return value as text — while the image itself was
477
+ already handed to the model as an image block. The bridge now returns the
478
+ readable note instead.
479
+ - **The binding scope no longer lists tools the session cannot call.** The fallback
480
+ to "every registered tool" now applies only to the pi versions without a
481
+ nested-call channel (0.86/0.87). On 0.99+ the session's own active set is used,
482
+ so a session that narrowed its tools no longer sees bindings that would be
483
+ rejected on the first call.
484
+
485
+ ### Changed
486
+
487
+ - The seven route detail files live in one place now
488
+ (`packages/routes/docs/routes/`); the identical copy at the repository root is
489
+ gone, and the copy `prepack` drops there is now git-ignored.
490
+ - ADR-0015 records the reversal of its "no absolute path in the index" decision,
491
+ the token ruler it actually uses, and the cost of the PTC-mode switch.
492
+
493
+ ## [6.0.2] - 2026-10-26
494
+
495
+ ### Fixed
496
+
497
+ - **Program-internal tool calls work across the whole supported range.** The tool
498
+ execution context sits in a different argument slot on older pi releases, so the
499
+ bridge now identifies it by "whoever carries `executeTool`" instead of a fixed position.
500
+ - **Tools return text to programs on every version.** pi 0.99–1.0.3 hand back a
501
+ content-block array where 1.0.4+ hands back a string; the bridge normalises both into
502
+ text, so `tools.read` reads the same way everywhere.
503
+ - **The binding scope is read from the session's own tool set.** On pi 0.86 and 0.87
504
+ `getActiveTools()` lists only an extension's own tools, so the built-in tools are taken
505
+ from `getAllTools()` there; on 0.99+ the active set is the declared set and is used as is.
506
+ - **A clear message where the platform has no channel at all.** pi 0.86 and 0.87 expose no
507
+ nested tool execution API; a program's `tools.*` call now says so — "this pi version has
508
+ no in-program tool channel (needs pi ≥ 0.99)" — instead of failing vaguely.
509
+
510
+ ## [6.0.1] - 2026-10-26
511
+
512
+ ### Fixed
513
+
514
+ - **Programs can call the session's tools again.** The binding bridge looked for
515
+ `ctx.executeTool` on the session context. pi 1.4.0 exposes it on the **tool execution
516
+ context** — `execute(toolCallId, params, signal, onUpdate, ctx)` — with the signature
517
+ `executeTool(name, args, options)`. Every `tools.*` call inside a program failed with
518
+ "executeTool 不可用". Measured on a real pi: a program that reads `/etc/hosts` now gets
519
+ its length back.
520
+ - **Route details ship with the package and resolve.** The seven `docs/routes/*.md` files
521
+ are in the npm tarball now, and the route index points at their absolute path inside the
522
+ installed package, so the model's `read` finds them instead of a path relative to the
523
+ user's project.
524
+
525
+ ### Changed
526
+
527
+ - **PTC mode no longer narrows the tool loadout.** On pi 1.4.0 a tool is callable from a
528
+ program only while it is active, so narrowing took `tools.read` away from programs too
529
+ ("Tool read not found"). A built-in tool's exposure is not an extension's to set, so the
530
+ two cannot both hold. PTC mode is now a routing lead in the system prompt: the built-in
531
+ tools stay visible and callable, from the model and from programs.
532
+
533
+ ## [6.0.0] - 2026-10-26
534
+
535
+ ### Changed
536
+
537
+ - **Clean-room rewrite.** The package is rebuilt as seven packages — `entry`, `ptc`,
538
+ `dispatch`, `task`, `tui`, `routes`, `search` — and ships as one self-contained
539
+ artifact. The previous implementation is preserved on `backup/main-legacy-20261026`.
540
+ - **Two surfaces, detected.** `full_ptc` keeps the PTC pair model-facing; `codemode_ptc`
541
+ registers underneath pi's codemode and exposes the task door to scripts. There is no
542
+ setting.
543
+ - **Subagent dispatch is a task engine.** Every dispatch gets a record, a slot, and its
544
+ own `pi` process. Busy means queue, not reject. Reports arrive through
545
+ `ptc_child_report` first, with the trailing-JSON channel as fallback.
546
+ - **The task face** (`ptc_task_list` / `ptc_task_output` / `ptc_task_stop`) appears only
547
+ while there is a task to see.
548
+ - **Web search lives in children.** `web_ds_search` registers only inside dispatched
549
+ children and is off by default.
550
+
551
+ ### Compatibility
552
+
553
+ - pi `>= 0.86.0`. Real-machine smoke tests cover 0.86.0, 0.87, 0.99, 1.0, 1.1, 1.2,
554
+ 1.3, and 1.4 — including a program that dispatches a real child process.
package/README.md CHANGED
@@ -81,6 +81,19 @@ 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
+ **You get told when a new version lands.** The plugin ships a `UserPromptSubmit` hook that
85
+ compares its own version against the latest GitHub Release; when they differ, the notice is
86
+ attached to your next message and the agent says so in its reply. No marketplace setup, no
87
+ config, nothing to click. Update with
88
+ `/plugins install https://github.com/a1121611810/pi-ptc-subagents` — the bare repo URL
89
+ installs the latest release.
90
+
91
+ **Optional: the marketplace badge.** Kimi Code also shows "update 7.1.0 → 7.5.0" in `/plugins`
92
+ for plugins a marketplace catalog can match by id, and this repo ships such a catalog
93
+ (`plugin-marketplace.json`). But pointing Kimi Code at it _replaces_ the default catalog, so
94
+ the official plugins drop out of the panel and lose their update notifications. Treat it as
95
+ opt-in, not as a setup step.
96
+
84
97
  ## The engine on its own (no model, no pi)
85
98
 
86
99
  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.7.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
  ],
@@ -38,8 +39,9 @@
38
39
  "build:kimi": "pnpm --filter @pi-ptc/kimi-mcp run build && node scripts/assemble-kimi.mjs",
39
40
  "prepack": "pnpm run build && rm -rf dist docs/routes && cp -R packages/entry/dist dist && cp -R packages/ptc/dist dist/ptc && cp -R packages/routes/docs/routes docs/routes",
40
41
  "check": "pnpm -r --filter=!@pi-ptc/website run check",
42
+ "check:imports": "node scripts/check-artifact-imports.mjs",
41
43
  "lint": "oxlint",
42
- "test": "pnpm run test:pi && pnpm run test:output && pnpm run test:kimi && pnpm run test:standalone",
44
+ "test": "pnpm run check:imports && pnpm run test:pi && pnpm run test:output && pnpm run test:kimi && pnpm run test:standalone",
43
45
  "test:pi": "node --test tests/pi/*.test.mjs",
44
46
  "test:output": "node --test tests/output/*.test.mjs",
45
47
  "test:kimi": "node --test tests/kimi/*.test.mjs",