pi-crew 0.11.6 → 0.11.8

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.
Files changed (58) hide show
  1. package/.githooks/pre-commit +22 -0
  2. package/.githooks/pre-push +56 -0
  3. package/CHANGELOG.md +216 -1
  4. package/dist/index.mjs +531 -508
  5. package/docs/publishing.md +29 -0
  6. package/index.ts +3 -3
  7. package/package.json +9 -6
  8. package/scripts/install-hooks.mjs +32 -0
  9. package/skills/real-test-pi-crew/SKILL.md +31 -18
  10. package/src/config/config-validation.ts +35 -0
  11. package/src/config/env-vars.ts +38 -0
  12. package/src/config/types.ts +71 -0
  13. package/src/extension/registration/foreground-run-controller.ts +19 -10
  14. package/src/extension/registration/lifecycle-handlers.ts +6 -491
  15. package/src/extension/registration/lifecycle.ts +7 -90
  16. package/src/extension/registration/render-loop.ts +502 -0
  17. package/src/extension/team-tool/doctor.ts +6 -0
  18. package/src/prompt/prompt-runtime.ts +33 -4
  19. package/src/runtime/child-pi/child-pi-spawn.ts +14 -0
  20. package/src/runtime/child-pi/child-pi.ts +130 -6
  21. package/src/runtime/child-pi/pi-version.ts +187 -0
  22. package/src/runtime/child-pi/session-recovery.ts +334 -0
  23. package/src/runtime/classifier/classifier-service.ts +298 -0
  24. package/src/runtime/dispatch-batch.ts +4 -0
  25. package/src/runtime/live-session/live-session-runtime.ts +31 -0
  26. package/src/runtime/model/model-scope.ts +141 -21
  27. package/src/runtime/model/pi-args.ts +166 -6
  28. package/src/runtime/peer-dep.ts +24 -0
  29. package/src/runtime/process/zombie-scanner.ts +168 -7
  30. package/src/runtime/rpc/README.md +139 -0
  31. package/src/runtime/rpc/frame-client.ts +328 -0
  32. package/src/runtime/rpc/rpc-worker.ts +271 -0
  33. package/src/runtime/rpc/ui-request-policy.ts +112 -0
  34. package/src/runtime/run-worker.ts +91 -3
  35. package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
  36. package/src/runtime/skill-instructions.ts +66 -1
  37. package/src/runtime/surface/resolve-surface.ts +34 -19
  38. package/src/runtime/surface/surface-spawn.ts +8 -1
  39. package/src/runtime/task-output-context.ts +191 -32
  40. package/src/runtime/task-packet.ts +3 -0
  41. package/src/runtime/task-runner/child-executor.ts +172 -2
  42. package/src/runtime/task-runner/post-execution.ts +5 -3
  43. package/src/runtime/task-runner/pre-execution.ts +32 -1
  44. package/src/runtime/task-runner/prompt-builder.ts +10 -2
  45. package/src/runtime/task-runner.ts +6 -0
  46. package/src/schema/config-schema.ts +37 -0
  47. package/src/schema/team-tool-schema.ts +14 -2
  48. package/src/state/contracts.ts +5 -0
  49. package/src/ui/format-helpers.ts +11 -0
  50. package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
  51. package/src/ui/pi-ui-compat.ts +18 -9
  52. package/src/ui/rail.ts +14 -1
  53. package/src/ui/settings-overlay.ts +11 -2
  54. package/src/ui/widget/index.ts +0 -1
  55. package/src/ui/widget/task-list.ts +10 -3
  56. package/src/ui/widget/widget-renderer.ts +53 -4
  57. package/src/utils/relative-time.ts +11 -3
  58. package/src/utils/visual.ts +32 -2
@@ -18,6 +18,22 @@
18
18
  * is no longer alive (parent crashed/exited without reaping the child). A sub-agent
19
19
  * whose parent is still running is NOT a zombie — it's a legitimate in-flight task.
20
20
  *
21
+ * R3-16 (2026-10-04): a SECOND class — "foreign pi processes" — is detected via
22
+ * the CLI-entry markers `AI_AGENT=pi` / `PI_CODING_AGENT=true` (SDK docs
23
+ * environment-variables.md:15-16: both are set at runtime by the CLI/RPC entry
24
+ * points and inherited by children). Because they are set POST-exec, they show
25
+ * up in a process's own frozen /proc/<pid>/environ ONLY when that process was
26
+ * spawned BY another pi process — a top-level interactive main session's
27
+ * at-exec environ never carries them. Discriminator hierarchy:
28
+ * 1. `PI_CREW_KIND=subagent` → crew worker (zombies/live arrays, authoritative);
29
+ * 2. markers + pi-looking argv + not self/not an ancestor → foreign, REPORT ONLY.
30
+ * The argv gate matters: every shell/tool/MCP-server process pi spawns INHERITS
31
+ * the markers (uv/python/bash/xclip …), so marker presence alone over-matches.
32
+ * Residual ambiguity: a pi the USER deliberately starts from inside a pi shell
33
+ * tool (nested pi) inherits the markers too and is indistinguishable from a
34
+ * leak. No 100%-reliable discriminator exists for that residue → foreign
35
+ * entries are a WARN listing with caveat, NEVER offered as kill candidates.
36
+ *
21
37
  * This module is READ-ONLY. It never kills anything. The caller (doctor --zombies)
22
38
  * prints the list and asks for explicit confirmation before any kill.
23
39
  */
@@ -51,10 +67,29 @@ export interface ZombieScanResult {
51
67
  zombies: ZombieSubagent[];
52
68
  /** Sub-agents whose parent is still alive — shown for transparency, never killed. */
53
69
  live: ZombieSubagent[];
70
+ /** R3-16: non-crew pi processes carrying the CLI markers
71
+ * (AI_AGENT=pi / PI_CODING_AGENT=true) with a pi-looking argv. REPORT ONLY —
72
+ * never a kill candidate (a user-started nested pi is indistinguishable
73
+ * from a leaked one; see module header). */
74
+ foreign: ForeignPiProcess[];
54
75
  /** Errors encountered while scanning (per-pid). Never aborts the whole scan. */
55
76
  errors: string[];
56
77
  }
57
78
 
79
+ export interface ForeignPiProcess {
80
+ pid: number;
81
+ ppid: number;
82
+ /** Parent dead or re-parented to init (ppid=1) — strongest available orphan signal. */
83
+ orphaned: boolean;
84
+ /** argv carries headless markers (`--mode json` / `-p` / `--print`) — an
85
+ * interactive TUI session never does; combined with orphaned this is the
86
+ * highest-confidence "likely leak" pair, still WARN-only. */
87
+ headless: boolean;
88
+ rssKb: number;
89
+ elapsedSec: number | undefined;
90
+ cmd: string;
91
+ }
92
+
58
93
  /** Read /proc/<pid>/environ as a key=value record. Returns {} if unreadable. */
59
94
  function readProcEnviron(pid: number): Record<string, string> {
60
95
  try {
@@ -121,12 +156,17 @@ function isPidAlive(pid: number): boolean {
121
156
  }
122
157
 
123
158
  function readProcCmdline(pid: number): string {
159
+ return readProcCmdlineTokens(pid).join(" ").trim() || `pid ${pid}`;
160
+ }
161
+
162
+ /** /proc/<pid>/cmdline as argv tokens (NUL-split). Empty array if unreadable. */
163
+ function readProcCmdlineTokens(pid: number): string[] {
124
164
  try {
125
165
  // /proc/<pid>/cmdline is NUL-separated argv.
126
166
  const raw = fs.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
127
- return raw.split("\0").filter(Boolean).join(" ").trim() || `pid ${pid}`;
167
+ return raw.split("\0").filter(Boolean);
128
168
  } catch {
129
- return `pid ${pid}`;
169
+ return [];
130
170
  }
131
171
  }
132
172
 
@@ -169,13 +209,16 @@ function listCandidatePids(): number[] {
169
209
  * has no side effects.
170
210
  */
171
211
  export function scanZombieSubagents(): ZombieScanResult {
172
- const result: ZombieScanResult = { zombies: [], live: [], errors: [] };
212
+ const result: ZombieScanResult = { zombies: [], live: [], foreign: [], errors: [] };
173
213
  if (process.platform !== "linux") {
174
214
  result.errors.push("zombie scan is Linux-only (/proc required); skipping on " + process.platform);
175
215
  return result;
176
216
  }
177
217
 
178
218
  const myUid = tryGetUid();
219
+ // R3-16: self + ancestors are live hosting sessions (main session / crew
220
+ // host chain) — never reportable as foreign, even when they carry markers.
221
+ const ancestors = collectAncestorPids();
179
222
  for (const pid of listCandidatePids()) {
180
223
  try {
181
224
  // Cheap rejection first: only inspect processes we own (avoid scanning system procs).
@@ -185,7 +228,33 @@ export function scanZombieSubagents(): ZombieScanResult {
185
228
  // AUTHORITATIVE GATE: a process is a pi-crew sub-agent ONLY if it carries
186
229
  // PI_CREW_KIND=subagent. The user's main session never sets this, so it can
187
230
  // never be matched — this is the fix for accidentally killing main sessions.
188
- if (environ.PI_CREW_KIND !== "subagent") continue;
231
+ // R3-16: non-crew processes get ONE extra look — the foreign pi branch
232
+ // below (markers + pi-looking argv, REPORT ONLY); all else is invisible.
233
+ if (environ.PI_CREW_KIND !== "subagent") {
234
+ // Exact value match matters: other agents reuse the AI_AGENT name with
235
+ // their own value (claude-code sets AI_AGENT=claude-code_…_agent), and
236
+ // marker INHERITORS (shells, MCP servers, xclip) are filtered by the
237
+ // argv gate below. Crew workers never reach here — the gate above owns
238
+ // them (pi-crew also scrubs these markers from worker env).
239
+ const hasPiMarker = environ.AI_AGENT === "pi" || environ.PI_CODING_AGENT === "true";
240
+ if (hasPiMarker && pid !== process.pid && !ancestors.has(pid)) {
241
+ const tokens = readProcCmdlineTokens(pid);
242
+ if (looksLikePiBinary(tokens)) {
243
+ const foreignStat = readProcStat(pid);
244
+ const ppid = foreignStat?.ppid ?? 0;
245
+ result.foreign.push({
246
+ pid,
247
+ ppid,
248
+ orphaned: ppid <= 1 || !isPidAlive(ppid),
249
+ headless: isHeadlessArgv(tokens),
250
+ rssKb: readProcRssKb(pid),
251
+ elapsedSec: foreignStat?.elapsedSec,
252
+ cmd: readProcCmdline(pid),
253
+ });
254
+ }
255
+ }
256
+ continue;
257
+ }
189
258
 
190
259
  const crewParentPid = Number.parseInt(environ.PI_CREW_PARENT_PID ?? "", 10);
191
260
  const stat = readProcStat(pid);
@@ -216,9 +285,11 @@ export function scanZombieSubagents(): ZombieScanResult {
216
285
  }
217
286
  }
218
287
 
219
- // Sort: zombies first by descending RSS (biggest leaks first), live by pid.
288
+ // Sort: zombies first by descending RSS (biggest leaks first), live by pid;
289
+ // foreign: orphans first (the likely leaks), then by descending RSS.
220
290
  result.zombies.sort((a, b) => b.rssKb - a.rssKb);
221
291
  result.live.sort((a, b) => a.pid - b.pid);
292
+ result.foreign.sort((a, b) => Number(b.orphaned) - Number(a.orphaned) || b.rssKb - a.rssKb);
222
293
  return result;
223
294
  }
224
295
 
@@ -241,6 +312,63 @@ function getProcUid(pid: number): number | undefined {
241
312
  }
242
313
  }
243
314
 
315
+ function pathBasename(p: string): string {
316
+ const idx = p.lastIndexOf("/");
317
+ return idx === -1 ? p : p.slice(idx + 1);
318
+ }
319
+
320
+ /**
321
+ * R3-16: does the argv look like the pi CLI binary itself?
322
+ *
323
+ * Every process pi spawns (bash, uv, python MCP servers, xclip, …) INHERITS the
324
+ * AI_AGENT/PI_CODING_AGENT markers, so marker presence alone over-matches the
325
+ * user's own tool processes. Only pi-looking argv[0..1] counts (argv[0] itself,
326
+ * or a node-style script path at argv[1]): basename `pi` (covers
327
+ * /usr/local/bin/pi, .nvm/…/bin/pi, ./pi), `pi-coding-agent`, or a path
328
+ * containing pi-coding-agent (node …/pi-coding-agent/dist/…). Tokens deeper
329
+ * than argv[1] are ARGUMENTS (e.g. `bash -c "pi …"`) — never the binary.
330
+ */
331
+ function looksLikePiBinary(tokens: string[]): boolean {
332
+ for (const token of tokens.slice(0, 2)) {
333
+ const base = pathBasename(token);
334
+ if (base === "pi" || base === "pi-coding-agent" || base === "pi.js") return true;
335
+ if (token.includes("pi-coding-agent")) return true;
336
+ }
337
+ return false;
338
+ }
339
+
340
+ /** R3-16: headless argv detection — an interactive TUI session never carries these. */
341
+ function isHeadlessArgv(tokens: string[]): boolean {
342
+ for (let i = 0; i < tokens.length; i++) {
343
+ const token = tokens[i] ?? "";
344
+ if (token === "-p" || token === "--print" || token === "--mode=json") return true;
345
+ if (token === "--mode" && tokens[i + 1] === "json") return true;
346
+ }
347
+ return false;
348
+ }
349
+
350
+ /**
351
+ * R3-16: the scanner's own ancestor chain (self's parent up to init).
352
+ *
353
+ * Any pi process on this chain is a LIVE hosting session (the user's main
354
+ * session / a crew host) and must never be reported — even in the nested case
355
+ * (worker runs doctor → its parent crew host and the user's main session above
356
+ * it sit on this chain and carry markers when pi-in-pi). Bounded at 64 hops to
357
+ * survive a hypothetical pid cycle.
358
+ */
359
+ function collectAncestorPids(): Set<number> {
360
+ const chain = new Set<number>();
361
+ let current = process.ppid;
362
+ let guard = 0;
363
+ while (current > 1 && guard++ < 64) {
364
+ chain.add(current);
365
+ const stat = readProcStat(current);
366
+ if (!stat || stat.ppid <= 0) break;
367
+ current = stat.ppid;
368
+ }
369
+ return chain;
370
+ }
371
+
244
372
  /**
245
373
  * Render a ZombieScanResult as human-readable text for the doctor report.
246
374
  * Explicitly labels main-session safety and never suggests killing live parents.
@@ -252,7 +380,7 @@ export function formatZombieReport(scan: ZombieScanResult): string {
252
380
  lines.push(`Sub-agents identified by PI_CREW_KIND=subagent marker. Main sessions (no marker) are never listed.`);
253
381
  lines.push("");
254
382
 
255
- if (scan.zombies.length === 0 && scan.live.length === 0) {
383
+ if (scan.zombies.length === 0 && scan.live.length === 0 && scan.foreign.length === 0) {
256
384
  lines.push("No pi-crew sub-agent processes found.");
257
385
  if (scan.errors.length > 0) {
258
386
  lines.push("");
@@ -284,6 +412,23 @@ export function formatZombieReport(scan: ZombieScanResult): string {
284
412
  lines.push("");
285
413
  }
286
414
 
415
+ if (scan.foreign.length > 0) {
416
+ lines.push(`### WARN — Foreign pi processes, REPORT ONLY (${scan.foreign.length})`);
417
+ lines.push("CLI-spawned pi processes (AI_AGENT=pi / PI_CODING_AGENT=true markers) that are");
418
+ lines.push("NOT pi-crew children. A top-level interactive session never carries these");
419
+ lines.push("markers in its own environment (the CLI sets them post-exec) — but a pi YOU");
420
+ lines.push("deliberately started from inside a pi shell looks identical to a leak.");
421
+ lines.push("NO reliable discriminator exists for that residue: verify cmd / session-dir");
422
+ lines.push("/ cwd by hand before ever killing one. These are never kill candidates and");
423
+ lines.push("never feed pane cleanup. This tool never kills.");
424
+ lines.push("");
425
+ lines.push(" PID PPID STATE RSS CMD");
426
+ for (const f of scan.foreign) {
427
+ lines.push(formatForeignRow(f));
428
+ }
429
+ lines.push("");
430
+ }
431
+
287
432
  if (scan.errors.length > 0) {
288
433
  lines.push(`Scan errors (${scan.errors.length}, first 5 shown):`);
289
434
  for (const err of scan.errors.slice(0, 5)) lines.push(` - ${err}`);
@@ -301,6 +446,14 @@ function formatZombieRow(z: ZombieSubagent): string {
301
446
  return ` ${String(z.pid).padEnd(9)}${String(z.crewParentPid).padEnd(8)}${formatRss(z.rssKb).padEnd(10)}${(z.role ?? "?").padEnd(14)}${surface.padEnd(16)}${z.cmd.slice(0, 44)}`;
302
447
  }
303
448
 
449
+ /** One table row for the R3-16 foreign section — state carries the triage flags. */
450
+ function formatForeignRow(f: ForeignPiProcess): string {
451
+ // "orphaned" = parent dead/re-parented to init (strongest leak signal);
452
+ // "headless" = --mode json / -p argv (never an interactive TUI session).
453
+ const state = `${f.orphaned ? "orphaned" : "live-parent"}${f.headless ? " headless" : ""}`;
454
+ return ` ${String(f.pid).padEnd(9)}${String(f.ppid).padEnd(8)}${state.padEnd(17)}${formatRss(f.rssKb).padEnd(10)}${f.cmd.slice(0, 40)}`;
455
+ }
456
+
304
457
  function formatRss(kb: number): string {
305
458
  if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)}G`;
306
459
  if (kb >= 1024) return `${(kb / 1024).toFixed(0)}M`;
@@ -308,4 +461,12 @@ function formatRss(kb: number): string {
308
461
  }
309
462
 
310
463
  // Re-export for tests + callers that want to inspect proc helpers in isolation.
311
- export const __test = { readProcEnviron, isPidAlive, computeElapsedSec };
464
+ export const __test = {
465
+ readProcEnviron,
466
+ isPidAlive,
467
+ computeElapsedSec,
468
+ readProcCmdlineTokens,
469
+ looksLikePiBinary,
470
+ isHeadlessArgv,
471
+ collectAncestorPids,
472
+ };
@@ -0,0 +1,139 @@
1
+ # RPC worker transport — W7 (P2-3) EXPERIMENTAL
2
+
3
+ Opt-in behind `PI_CREW_WORKER_TRANSPORT=rpc`. **WIRED at the run-worker seam as
4
+ of 2026-10-04** (integration phase, lane D6) after the live-fire probe below —
5
+ previously the seam returned a structured not-implemented result. The
6
+ production stdio child-pi path (`child-pi.ts`) is untouched for the default
7
+ transport; the rpc path carries a REDUCED input (see Limitations).
8
+
9
+ ## What this is
10
+
11
+ A JSONL frame client that speaks the pi SDK's RPC mode protocol
12
+ (`pi --mode rpc`) on stdin/stdout, implementing the two Round-2 design gates
13
+ the adoption review (§R2.5) proved are mandatory for any RPC transport:
14
+
15
+ 1. **`extension_ui_request` drain policy** (`ui-request-policy.ts`) —
16
+ fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`,
17
+ `set_editor_text`) are counted per method and dropped. Round-2 live
18
+ evidence: 15 of the first 16 records a worker emits belong to this family.
19
+ 2. **Dialog auto-answer policy** (`cancel` | `block`) — dialog methods
20
+ (`select`/`confirm`/`input`/`editor`) park a server-side promise that NEVER
21
+ resolves if unanswered (ask-over-RPC deadlock, R2.5). `cancel` (default)
22
+ answers the universal safe answer `{type:"extension_ui_response", id,
23
+ cancelled:true}` (select/input → undefined, confirm → false). `block`
24
+ leaves it pending on purpose (debug). **There is no auto-confirm mode —
25
+ deliberate security decision.** An unattended transport must never say
26
+ "yes" for the user.
27
+
28
+ Counters (fake-stream asserted): `uiRequestsDrained`, `drainedByMethod`,
29
+ `dialogsCancelled`, `dialogsBlocked`, `malformedFrames`, `responsesMatched`,
30
+ `responsesUnmatched`.
31
+
32
+ ## Deviation from "use SDK RpcClient" — recorded, evidence-based
33
+
34
+ The task's original phrasing said "transport dùng SDK RpcClient/runRpcMode".
35
+ The verified recon packet (adaptive-07) proved `RpcClient` unusable for this
36
+ prototype and the leader routed the packet through; deviation accepted per
37
+ packet §C:
38
+
39
+ 1. **No public API to answer `extension_ui_request`** — `send`/`process` are
40
+ private; non-response records go to event listeners only. Gate 2 would
41
+ require bracket-accessing private members.
42
+ 2. **Version skew** — `RpcClient.start()` spawns `node <cliPath ?? "dist/cli.js">`
43
+ cwd-relative, NOT the host pi binary the stdio transport resolves
44
+ (`getPiSpawnCommand` / `PI_TEAMS_PI_BIN`).
45
+ 3. **Not injectable** — the W7 test requirement ("fake RPC stream, no real
46
+ spawn") is impossible against `RpcClient`.
47
+
48
+ `runRpcMode` is a server-side embed API — inapplicable (pi-crew is a spawner,
49
+ not an embedder).
50
+
51
+ So: **own strict-LF frame loop over the SDK's exported protocol TYPES**
52
+ (`RpcCommand` / `RpcResponse` / `RpcExtensionUIRequest` /
53
+ `RpcExtensionUIResponse` — all imported from `@earendil-works/pi-coding-agent`).
54
+ The framing rule follows the SDK's own `modes/rpc/jsonl.d.ts` spec: split on
55
+ `\n` ONLY. **Node readline is unsafe here** — it also splits on U+2028/U+2029,
56
+ which are legal inside JSON strings. `serializeJsonLine` /
57
+ `attachJsonlLineReader` are not exported top-level, hence the ~15-line
58
+ hand-rolled splitter.
59
+
60
+ ## Config & env
61
+
62
+ - `runtime.workerTransport: "stdio" (default) | "rpc"` — declared in
63
+ `src/schema/config-schema.ts` + `src/config/types.ts`. **Config→seam plumbing
64
+ is NOT wired yet** (needs a `parseRuntimeConfig` line in
65
+ `config-validation.ts`, which is outside this lane's file ownership);
66
+ the live gate is env `PI_CREW_WORKER_TRANSPORT` (`runWorker` has no config
67
+ access — packet §A).
68
+ - `PI_CREW_WORKER_TRANSPORT=rpc` — selects the transport at the
69
+ `run-worker.ts` seam. WIRED 2026-10-04: calls `runRpcWorker` for real
70
+ (worker-cap slot semantics identical to stdio; early transport failure →
71
+ one structured-warn fallback to the stdio path inside the same slot).
72
+ - `PI_CREW_RPC_DIALOG_ANSWER=cancel|block` — GATE 2 policy (default `cancel`,
73
+ invalid fails safe to `cancel`).
74
+
75
+ ## Why the seam was not wired before 2026-10-04
76
+
77
+ The original lane was not allowed to live-spawn pi, so a live-wired seam would
78
+ have shipped an end-to-end path that NO test could validate — one env var away
79
+ from silently degrading real runs. The integration phase (this lane) ran the
80
+ live-fire probe FIRST and only then flipped the seam.
81
+
82
+ ## Live-fire probe 2026-10-04 — GREEN → seam wired
83
+
84
+ Probe setup: real `pi --mode rpc --no-session` spawned by the unmodified
85
+ frame-client default spawner (`getPiSpawnCommand`), scripts importing directly
86
+ from `src/runtime/rpc/` via `node --experimental-strip-types`, PI_CREW_* /
87
+ PI_TEAMS_* env scrubbed, isolated cwd `/tmp/rpc-lf/`, `timeout -k 5 120` per
88
+ run. Raw artifacts (kept on disk): `/tmp/rpc-lf/probe1{,b}.{log,stdout.txt,stderr.txt}`
89
+ (probe1b = default settings model), `probe2.*` (dialog), `probe3.*` (steer).
90
+
91
+ | # | Item | Result |
92
+ |---|------|--------|
93
+ | a | `extension_ui_request` drain (GATE 1 live) | **24 drained** on probe1b (`setStatus`×20, `setWidget`×3, `notify`×1 — pi-crew widget keys visible in raw frames); 0 malformed, 0 unmatched responses |
94
+ | b | prompt round-trip | ack **24 ms**, `agent_settled` **4.2 s**, `rawFinalText="OK"`, child exit **0**, orderly stdin-end shutdown |
95
+ | c | dialog (confirm) | **executed** (not skipped): a `-e` mini extension opened a real `confirm` → policy `cancel` answered `extension_ui_response {cancelled:true}` → `ui.confirm` resolved `false` → the model literally echoed `"false"`; settled 1.7 s, **no deadlock**, `dialogsCancelled=1` |
96
+ | d | steer mid-turn | `steer` at t+1.6 s → response RTT **53 ms**, disposition **`"queued"`**, final assistant text honored (`"STOP"`), settled 6.5 s, exit 0 |
97
+
98
+ First run (probe1) used `--thinking off` against a reasoning-only model
99
+ (`chiase/deepseek-v4.1-flash`, thinkingLevelMap has only `max`) → the assistant
100
+ message came back `content: []` with usage 0. Not a frame-client defect — the
101
+ turn still completed and settled — but a live argv caveat: **do not pass
102
+ `--thinking off` to reasoning-only models**; the wired seam passes the model
103
+ raw and never emits `--thinking`.
104
+
105
+ ### Limitations of the wired rpc path (honest list)
106
+
107
+ - Input mapping is the minimal prototype surface: `cwd`/`task`/`model`/`signal`.
108
+ Agent config, system-prompt files, skills, transcript path, steering file,
109
+ lifecycle/stdout callbacks, `maxTurns`/`graceTurns` remain **stdio-only**.
110
+ - The model string is passed RAW (`--model <model>`), no `applyThinkingSuffix`
111
+ composition — thinking level comes from the settings default.
112
+ - Workers are ephemeral (`--no-session`): no deterministic session identity
113
+ (`--session-id`/`--session-dir`, W2 crash recovery) for rpc yet. Attaching a
114
+ worker to the cwd project's default session would pollute the user's session.
115
+ - `runtime.workerTransport` config key is still NOT plumbed to the seam
116
+ (needs a `parseRuntimeConfig` line in `config-validation.ts`, outside the
117
+ lane's ownership); the env var is the only live gate.
118
+ - Early-failure fallback: only a result with `error` set, NO `rawFinalText`,
119
+ and NOT aborted is retried on stdio (double-execution guard,
120
+ `isEarlyRpcTransportFailure` in run-worker.ts).
121
+
122
+ ## Files
123
+
124
+ - `frame-client.ts` — strict-LF JSONL frame client, pending-command map with
125
+ per-command timeout, malformed-frame resilience (count + drop, never throw),
126
+ orderly stop (stdin end → exit → SIGKILL fallback, kill-once guard).
127
+ - `ui-request-policy.ts` — the two design gates + counters.
128
+ - `rpc-worker.ts` — env resolvers (`resolveWorkerTransport`,
129
+ `resolveDialogAnswerPolicy`), the live-fire argv builder
130
+ (`buildRpcWorkerArgv`) + the `runRpcWorker` runner the seam calls.
131
+
132
+ ## Production-ready? Honestly: NOT for general agent configs.
133
+
134
+ Live-fire validated for the probe scenarios above (small prompt, one confirm
135
+ dialog, one steer, plain model, default extensions). The stdio transport
136
+ remains the default and the only complete path (agent config, system prompts,
137
+ skills, session identity, retries semantics). Do not enable
138
+ `PI_CREW_WORKER_TRANSPORT=rpc` outside experiments until the Limitations
139
+ list above shrinks.