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 +513 -0
- package/README.md +20 -0
- package/dist/index.js +302 -29
- package/package.json +2 -1
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.
|
|
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
|
],
|