@zhuxixi/pi-agent-board 0.3.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.
Files changed (65) hide show
  1. package/IMPLEMENTATION_PLAN.md +920 -0
  2. package/LICENSE +21 -0
  3. package/PRD.md +484 -0
  4. package/PROGRESS.md +127 -0
  5. package/README.md +131 -0
  6. package/VERIFY.md +113 -0
  7. package/docs/BATCH_SELECTION_READ_FLOW.md +277 -0
  8. package/docs/EXPLORATION.md +187 -0
  9. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +579 -0
  10. package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +704 -0
  11. package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +499 -0
  12. package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +366 -0
  13. package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +105 -0
  14. package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +142 -0
  15. package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +59 -0
  16. package/index.ts +6 -0
  17. package/package.json +81 -0
  18. package/runner/job-runner.mjs +420 -0
  19. package/runner/pty-runner.mjs +310 -0
  20. package/runner/state-runner.mjs +120 -0
  21. package/runner/title-runner.mjs +80 -0
  22. package/scripts/patch-vulns.mjs +59 -0
  23. package/src/commands/agent-board.ts +318 -0
  24. package/src/commands/attach-flow.ts +231 -0
  25. package/src/commands/bg.ts +70 -0
  26. package/src/core/atomic.mjs +145 -0
  27. package/src/core/auto-state.mjs +320 -0
  28. package/src/core/dashboard-render.mjs +10 -0
  29. package/src/core/derive.mjs +114 -0
  30. package/src/core/diagnostics.mjs +109 -0
  31. package/src/core/events.mjs +268 -0
  32. package/src/core/evidence.mjs +242 -0
  33. package/src/core/follow-up-queue.mjs +193 -0
  34. package/src/core/heuristics.mjs +240 -0
  35. package/src/core/ids.mjs +35 -0
  36. package/src/core/invocation.mjs +43 -0
  37. package/src/core/launch-options.mjs +317 -0
  38. package/src/core/launch.mjs +116 -0
  39. package/src/core/locks.mjs +80 -0
  40. package/src/core/paths.mjs +86 -0
  41. package/src/core/pid.mjs +42 -0
  42. package/src/core/prewarm-schedule.mjs +41 -0
  43. package/src/core/prompt-transport.mjs +13 -0
  44. package/src/core/pty-attach-jiggle-retry.mjs +90 -0
  45. package/src/core/pty-attach-render.mjs +51 -0
  46. package/src/core/pty-input.mjs +15 -0
  47. package/src/core/pty-links.mjs +71 -0
  48. package/src/core/pty-scroll.mjs +155 -0
  49. package/src/core/pty-support.mjs +327 -0
  50. package/src/core/repo.mjs +47 -0
  51. package/src/core/rows.mjs +290 -0
  52. package/src/core/screen-log-gc.mjs +198 -0
  53. package/src/core/screen-log.mjs +160 -0
  54. package/src/core/session-view.mjs +174 -0
  55. package/src/core/steering-prompts.mjs +34 -0
  56. package/src/core/steering.mjs +133 -0
  57. package/src/core/store.mjs +308 -0
  58. package/src/core/title.mjs +43 -0
  59. package/src/core/types.mjs +380 -0
  60. package/src/core/worktree.mjs +64 -0
  61. package/src/index.ts +109 -0
  62. package/src/runtime/service.mjs +1194 -0
  63. package/src/ui/dashboard-evidence.mjs +85 -0
  64. package/src/ui/dashboard.ts +1952 -0
  65. package/src/ui/pty-attach.ts +1378 -0
@@ -0,0 +1,579 @@
1
+ # Implementation Plan: PTY-backed Live Attach
2
+
3
+ **Status:** Proposed / spike-ready
4
+ **Goal:** make attaching to a live agent-board row feel like attaching to the same normal Pi session, without interrupting the running work.
5
+ **Created:** 2026-05-31
6
+
7
+ ## 1. Decision
8
+
9
+ The previous `SessionClient`/RPC idea is clean, but it will not be 1:1 with normal Pi unless we reimplement or refactor a lot of interactive Pi UI behavior.
10
+
11
+ For a 1:1 user experience, invert the design:
12
+
13
+ > A background row should be hosted by a real interactive Pi process running inside a PTY. Attach should connect the user to that live PTY, not recreate Pi UI over RPC.
14
+
15
+ Current MVP keeps a detached one-shot JSON worker:
16
+
17
+ ```text
18
+ Dashboard
19
+ └─ job-runner.mjs
20
+ └─ pi --mode json -p --session <file> <prompt>
21
+ ├─ emits JSON events
22
+ ├─ writes session file
23
+ └─ exits after the prompt finishes
24
+ ```
25
+
26
+ Target live-attach model:
27
+
28
+ ```text
29
+ Dashboard
30
+ └─ pty-runner.mjs
31
+ ├─ control.sock
32
+ ├─ host.json
33
+ └─ PTY
34
+ └─ pi --session <file> <initial prompt>
35
+ ├─ real interactive Pi TUI
36
+ ├─ real slash commands/keybindings/extensions
37
+ ├─ writes same session file
38
+ └─ stays alive until explicitly closed
39
+ ```
40
+
41
+ ## 2. Critical exploration findings
42
+
43
+ ### Existing code that helps
44
+
45
+ - `src/runtime/service.mjs` already owns dispatch/reply/stop/archive and same-repo worktree safety.
46
+ - `runner/job-runner.mjs` already demonstrates detached durable process ownership.
47
+ - Store layout under `~/.pi/agent/agent-board/` is sound: `views/<id>/meta.json`, `state.json`, per-run artifacts, session file per row.
48
+ - `src/index.ts` already mirrors foreground session events into managed row state via `service.syncForegroundEvent(...)`.
49
+ - `src/commands/agent-board.ts` already owns attach and back-to-dashboard behavior.
50
+ - `src/ui/dashboard.ts` already has list/peek/reply/session modes and can add a new attach mode/result.
51
+
52
+ ### Important mismatch in current state model
53
+
54
+ `Row.alive` currently means “the current managed run pid is alive” and is also overloaded for foreground mirrored activity.
55
+
56
+ For PTY hosting we need two separate concepts:
57
+
58
+ 1. **host liveness** — is the interactive Pi PTY process alive and attachable?
59
+ 2. **agent activity** — is the agent currently processing/streaming/tooling?
60
+
61
+ A PTY-hosted row can be:
62
+
63
+ ```text
64
+ host: alive
65
+ agent activity: idle/completed/needs_input
66
+ ```
67
+
68
+ That should still be directly attachable without fallback resume.
69
+
70
+ ### Biggest technical caveat
71
+
72
+ Pi extension `ctx.ui.custom()` renders line-based components. It does **not** expose a documented raw terminal takeover API.
73
+
74
+ Therefore there are three implementation options:
75
+
76
+ | Option | Description | 1:1 fidelity | Fits extension-only? | Recommendation |
77
+ |---|---|---:|---:|---|
78
+ | A | PTY + terminal emulator component (`xterm-headless` style) | high, not perfect | yes | try first |
79
+ | B | Pi core raw terminal takeover API | highest | requires Pi core change | later if needed |
80
+ | C | standalone/tmux-like external agent-board CLI | highest | separate CLI, not pure `/agent-board` | fallback |
81
+
82
+ This plan attempts **Option A** first because it preserves the extension flow and avoids rebuilding the agent protocol/UI. It runs real Pi, captures its PTY output, and renders a virtual terminal buffer inside `ctx.ui.custom()`.
83
+
84
+ ## 3. Target architecture
85
+
86
+ ```text
87
+ ┌──────────────────────────────┐
88
+ │ Parent Pi / /agent-board dashboard │
89
+ └──────────────┬───────────────┘
90
+ │ dispatch
91
+
92
+ ┌────────────────────────────────────────────────────────────┐
93
+ │ pty-runner.mjs │
94
+ │ │
95
+ │ host.json durable host status │
96
+ │ control.sock IPC for attach/input/resize/stop │
97
+ │ screen.log optional raw PTY output log │
98
+ │ │
99
+ │ ┌──────────────────────────────────────────────────────┐ │
100
+ │ │ PTY │ │
101
+ │ │ └─ pi --session <managed.jsonl> <initial prompt> │ │
102
+ │ │ └─ real Pi interactive TUI │ │
103
+ │ └──────────────────────────────────────────────────────┘ │
104
+ └────────────────────────────────────────────────────────────┘
105
+
106
+ │ child extension events write row state
107
+ │ AGENT_BOARD_ROOT / AGENT_BOARD_VIEW_ID
108
+
109
+ ┌──────────────┴───────────────┐
110
+ │ agent-board extension in child │
111
+ │ mirrors agent/tool/message │
112
+ │ events to state.json │
113
+ └──────────────────────────────┘
114
+ ```
115
+
116
+ Attach path:
117
+
118
+ ```text
119
+ User presses Enter on live row
120
+ ├─ if host socket alive:
121
+ │ open PtyAttachComponent
122
+ │ connect to control.sock
123
+ │ replay PTY snapshot/log into virtual terminal
124
+ │ forward keyboard input to PTY
125
+ │ intercept detach chord only
126
+
127
+ └─ else:
128
+ fallback to existing ctx.switchSession(sessionFile)
129
+ ```
130
+
131
+ ## 4. Store/data model changes
132
+
133
+ ### 4.1 Add host status file
134
+
135
+ New path helpers in `src/core/paths.mjs`:
136
+
137
+ ```js
138
+ hostPath(root, viewId) // views/<viewId>/host.json
139
+ controlSocketPath(root,id) // views/<viewId>/control.sock
140
+ screenLogPath(root,id) // views/<viewId>/screen.log
141
+ ```
142
+
143
+ New shape in `src/core/types.mjs`:
144
+
145
+ ```ts
146
+ type HostMode = "json-runner" | "pty";
147
+ type HostState = "starting" | "alive" | "exited" | "failed";
148
+
149
+ interface HostStatus {
150
+ version: 1;
151
+ viewId: string;
152
+ mode: HostMode;
153
+ runnerPid: number | null;
154
+ childPid: number | null;
155
+ socketPath: string | null;
156
+ state: HostState;
157
+ startedAt: number;
158
+ lastSeenAt: number;
159
+ endedAt: number | null;
160
+ exitCode: number | null;
161
+ error: string | null;
162
+ cols: number;
163
+ rows: number;
164
+ attachedClients: number;
165
+ }
166
+ ```
167
+
168
+ ### 4.2 Extend `Row`
169
+
170
+ In `src/core/store.mjs`, `loadRow()` should return:
171
+
172
+ ```ts
173
+ interface Row {
174
+ meta: ViewMeta;
175
+ state: ViewState | null;
176
+ alive: boolean; // current agent activity/run alive, legacy-compatible
177
+ hostAlive: boolean; // PTY host/socket alive and attachable
178
+ host: HostStatus | null;
179
+ }
180
+ ```
181
+
182
+ Dashboard row labels should distinguish:
183
+
184
+ ```text
185
+ ● working agent active
186
+ ◌ hosted Pi process alive but idle
187
+ ✓ completed completed, host may still be alive
188
+ ```
189
+
190
+ ## 5. Runner changes
191
+
192
+ ### 5.1 Keep current JSON runner as fallback
193
+
194
+ Do not delete `runner/job-runner.mjs`. Keep it for:
195
+
196
+ - tests,
197
+ - non-PTY fallback,
198
+ - environments where `node-pty` cannot install,
199
+ - comparison/debugging.
200
+
201
+ ### 5.2 Add `runner/pty-runner.mjs`
202
+
203
+ Responsibilities:
204
+
205
+ 1. read `HostConfig` from JSON file,
206
+ 2. create/remove stale socket,
207
+ 3. spawn real interactive Pi in a PTY,
208
+ 4. write `host.json`,
209
+ 5. expose control socket,
210
+ 6. append raw PTY output to `screen.log`,
211
+ 7. broadcast output to attached clients,
212
+ 8. accept input/resize/stop commands,
213
+ 9. finalize host status on exit.
214
+
215
+ Proposed host config:
216
+
217
+ ```ts
218
+ interface HostConfig {
219
+ root: string;
220
+ viewId: string;
221
+ sessionFile: string;
222
+ cwd: string;
223
+ initialPrompt: string | null;
224
+ piCommand: string;
225
+ piArgsPrefix: string[];
226
+ model: string | null;
227
+ tools: string | null;
228
+ env: Record<string, string>;
229
+ cols: number;
230
+ rows: number;
231
+ }
232
+ ```
233
+
234
+ Spawn args:
235
+
236
+ ```js
237
+ const args = [
238
+ ...piArgsPrefix,
239
+ "--session", sessionFile,
240
+ ];
241
+ if (model) args.push("--model", model);
242
+ if (tools) args.push("--tools", tools);
243
+ if (initialPrompt) args.push(initialPrompt);
244
+ ```
245
+
246
+ Environment injected into child Pi:
247
+
248
+ ```text
249
+ AGENT_BOARD_ROOT=<root>
250
+ AGENT_BOARD_VIEW_ID=<viewId>
251
+ AGENT_BOARD_CHILD=1
252
+ AGENT_BOARD_HOSTED=pty
253
+ ```
254
+
255
+ `AGENT_BOARD_CHILD=1` lets the extension avoid dashboard-first startup behavior and any recursive agent-board UI side effects inside hosted children.
256
+
257
+ ### 5.3 Control socket protocol
258
+
259
+ Use JSONL messages over a Unix socket.
260
+
261
+ Client → runner:
262
+
263
+ ```json
264
+ {"type":"hello","clientId":"...","wantOutput":true}
265
+ {"type":"input","data":"raw terminal bytes"}
266
+ {"type":"resize","cols":120,"rows":36}
267
+ {"type":"detach","clientId":"..."}
268
+ {"type":"interrupt"}
269
+ {"type":"terminate"}
270
+ {"type":"get_status"}
271
+ ```
272
+
273
+ Runner → client:
274
+
275
+ ```json
276
+ {"type":"hello","status":{...}}
277
+ {"type":"output","data":"raw terminal bytes"}
278
+ {"type":"status","status":{...}}
279
+ {"type":"exit","exitCode":0}
280
+ {"type":"error","message":"..."}
281
+ ```
282
+
283
+ For attach, the parent sends raw input bytes through `input`. The only intercepted key should be the detach chord.
284
+
285
+ Recommended detach chord for spike: `ctrl+]`, because it is already an editor jump key but less commonly needed than arrows/escape/enter. Make it configurable later.
286
+
287
+ ## 6. Attach UI design
288
+
289
+ ### 6.1 Option A: virtual terminal component
290
+
291
+ Add `src/ui/pty-attach.ts`:
292
+
293
+ ```ts
294
+ export class PtyAttachComponent implements Component {
295
+ // connects to control.sock
296
+ // feeds output into terminal emulator buffer
297
+ // renders buffer lines via ctx.ui.custom
298
+ // forwards input to socket
299
+ // sends resize from tui.terminal rows/cols
300
+ // done({ action: "detached" }) on detach chord
301
+ }
302
+ ```
303
+
304
+ Potential dependency choices:
305
+
306
+ - `node-pty` for PTY creation in runner,
307
+ - `xterm-headless` or equivalent for terminal emulation in attach component.
308
+
309
+ If adding dependencies, update `package.json` `dependencies`, not `devDependencies`, because Pi package installs use production deps.
310
+
311
+ Expected fidelity:
312
+
313
+ - normal Pi input behavior: high, because real Pi receives real terminal bytes,
314
+ - normal Pi visual behavior: high for text UI,
315
+ - possible gaps: inline images, OSC hyperlinks, exact hardware cursor/IME, mouse support.
316
+
317
+ ### 6.2 Option B: raw takeover API if Option A is insufficient
318
+
319
+ If virtual rendering cannot meet expectations, propose a Pi core API:
320
+
321
+ ```ts
322
+ ctx.ui.rawTerminalSession(async ({ input, output, resize, restore }) => {
323
+ // parent TUI suspends rendering
324
+ // extension proxies bytes to/from child PTY
325
+ });
326
+ ```
327
+
328
+ This would give true terminal proxy behavior, but requires Pi core work outside this extension.
329
+
330
+ ## 7. Extension changes
331
+
332
+ ### 7.1 `src/index.ts`
333
+
334
+ Current extension mirrors events for any managed foreground session. Adjust it for hosted child processes:
335
+
336
+ - if `process.env.AGENT_BOARD_CHILD === "1"`, skip dashboard auto-open handling,
337
+ - still register event listeners,
338
+ - when `AGENT_BOARD_VIEW_ID` is present, mirror events directly to that row,
339
+ - keep footer status disabled/no-op in child to avoid confusing nested hosted Pi.
340
+
341
+ Potential helper:
342
+
343
+ ```ts
344
+ const hostedViewId = process.env.AGENT_BOARD_VIEW_ID;
345
+ const isHostedChild = process.env.AGENT_BOARD_CHILD === "1";
346
+ ```
347
+
348
+ ### 7.2 `src/runtime/service.mjs`
349
+
350
+ Add methods:
351
+
352
+ ```js
353
+ launchHost(meta, initialPrompt)
354
+ ensureHost(viewId)
355
+ attachTarget(viewId) // { kind: "pty", socketPath } | { kind: "session", sessionFile }
356
+ stopActivity(viewId) // sends interrupt to socket if hosted, else current stop
357
+ terminateHost(viewId)
358
+ injectReply(viewId, text) // if host alive, paste/submit into PTY; else fallback launch one-shot/host
359
+ ```
360
+
361
+ Dispatch flow becomes:
362
+
363
+ ```text
364
+ createView
365
+ launchHost(meta, prompt)
366
+ state = queued/working once child extension events arrive
367
+ ```
368
+
369
+ Reply flow:
370
+
371
+ ```text
372
+ if hostAlive:
373
+ send text + Enter to PTY
374
+ else:
375
+ launchHost(existingMeta, text) or existing JSON reply fallback
376
+ ```
377
+
378
+ Stop flow needs two actions:
379
+
380
+ - **interrupt active agent**: send Escape or RPC-like interrupt if available,
381
+ - **kill hosted Pi**: terminate PTY runner and child process.
382
+
383
+ Existing `ctrl+s stop` should initially mean “interrupt/stop active work”; add a separate confirm for killing host if needed.
384
+
385
+ ### 7.3 `src/commands/agent-board.ts`
386
+
387
+ Change attach decision:
388
+
389
+ ```ts
390
+ if (row.hostAlive) {
391
+ openPtyAttach(ctx, row)
392
+ } else if (row.alive) {
393
+ existing confirm interrupt + switchSession fallback
394
+ } else {
395
+ ctx.switchSession(row.meta.sessionFile, ...)
396
+ }
397
+ ```
398
+
399
+ `openPtyAttach` should return to the dashboard without switching sessions.
400
+
401
+ ### 7.4 `src/ui/dashboard.ts`
402
+
403
+ Update row rendering:
404
+
405
+ - show host-alive indicator,
406
+ - attach hint says `enter attach live` when `hostAlive`,
407
+ - running row no longer always asks “Interrupt and attach?” if host socket exists,
408
+ - session read-only view remains useful as non-interrupting transcript.
409
+
410
+ ## 8. Worktree/safety implications
411
+
412
+ Existing same-repo writer worktree isolation remains valid.
413
+
414
+ PTY-hosted sessions make the isolation rule more important because hosts can stay alive after task completion. Active writer detection should consider:
415
+
416
+ ```text
417
+ hostAlive && writeCapable && worktreeMode !== "worktree"
418
+ ```
419
+
420
+ However, an idle hosted process may not be writing. For MVP, be conservative:
421
+
422
+ - if another non-worktree host is alive in the same repo, force worktree for new write-capable dispatch,
423
+ - later refine by checking `state.semanticState === "working"`.
424
+
425
+ ## 9. Testing plan
426
+
427
+ ### 9.1 Unit tests
428
+
429
+ Add tests for:
430
+
431
+ - host path helpers,
432
+ - host status read/write,
433
+ - `loadRow().hostAlive`,
434
+ - service attach target selection,
435
+ - hosted vs fallback reply behavior,
436
+ - child event mirroring does not mark idle host as dead.
437
+
438
+ ### 9.2 Runner integration tests
439
+
440
+ Add fake interactive child script:
441
+
442
+ ```text
443
+ test-support/fake-pty-pi.mjs
444
+ ```
445
+
446
+ It should:
447
+
448
+ - write recognizable ANSI output,
449
+ - accept stdin,
450
+ - echo prompts,
451
+ - simulate busy/idle states through environment or marker files,
452
+ - exit on a command.
453
+
454
+ Test `pty-runner.mjs`:
455
+
456
+ - creates socket and host.json,
457
+ - broadcasts PTY output,
458
+ - forwards input,
459
+ - handles resize,
460
+ - finalizes on child exit,
461
+ - kills child on terminate.
462
+
463
+ ### 9.3 Attach component tests
464
+
465
+ Keep most logic pure:
466
+
467
+ - socket client parser,
468
+ - terminal buffer projection,
469
+ - detach chord interception,
470
+ - resize event generation.
471
+
472
+ Manual verification is required for final TUI fidelity.
473
+
474
+ ## 10. Phased implementation
475
+
476
+ ### Phase 0 — feasibility spike
477
+
478
+ Goal: prove or disprove extension-only PTY attach.
479
+
480
+ Tasks:
481
+
482
+ 1. Add temporary `node-pty` experiment script outside app flow.
483
+ 2. Spawn `pi --session <tmp.jsonl> "say hi"` inside PTY.
484
+ 3. Verify Pi runs initial prompt and remains interactive afterward.
485
+ 4. Build tiny `ctx.ui.custom` component that renders a fake ANSI stream through a terminal emulator.
486
+ 5. Confirm parent Pi TUI can render the virtual terminal without corrupting its own screen.
487
+ 6. Confirm child extension can mirror events via `AGENT_BOARD_ROOT/VIEW_ID`.
488
+
489
+ Exit criteria:
490
+
491
+ - Can attach, see live Pi UI, type into it, detach, and keep child alive.
492
+
493
+ If this fails due to public TUI limitations, switch to Option B/C instead of forcing a bad clone.
494
+
495
+ ### Phase 1 — host store + service plumbing
496
+
497
+ - Add `HostStatus` types/path helpers/store functions.
498
+ - Extend `Row` with `host`/`hostAlive`.
499
+ - Add service methods for host launch/status/terminate.
500
+ - Keep legacy JSON runner as fallback.
501
+
502
+ ### Phase 2 — PTY runner
503
+
504
+ - Add `runner/pty-runner.mjs`.
505
+ - Add socket protocol.
506
+ - Add fake PTY integration tests.
507
+ - Persist `host.json` and `screen.log`.
508
+
509
+ ### Phase 3 — child event mirroring
510
+
511
+ - Modify `src/index.ts` for hosted child env vars.
512
+ - Make `syncForegroundEvent` or new `syncHostedEvent` update row activity without confusing host liveness.
513
+ - Verify state transitions: queued → working → completed/needs_input while host remains alive.
514
+
515
+ ### Phase 4 — live attach component
516
+
517
+ - Add socket client.
518
+ - Add virtual terminal renderer.
519
+ - Intercept detach chord.
520
+ - Forward all other input bytes.
521
+ - Handle resize.
522
+ - Integrate into `/agent-board` attach flow.
523
+
524
+ ### Phase 5 — reply/stop behavior
525
+
526
+ - Reply to host by injecting text + Enter into PTY.
527
+ - Stop active work by sending Escape first.
528
+ - Add terminate-host confirm if needed.
529
+ - Preserve existing fallback for dead hosts.
530
+
531
+ ### Phase 6 — hardening/polish
532
+
533
+ - stale socket cleanup,
534
+ - host TTL or user-visible “close host” command,
535
+ - dependency install docs,
536
+ - dashboard indicators,
537
+ - worktree cleanup with live hosts,
538
+ - manual verification matrix.
539
+
540
+ ## 11. Acceptance criteria
541
+
542
+ MVP live attach is accepted when:
543
+
544
+ 1. Dispatch creates a real managed session and launches an interactive hosted Pi.
545
+ 2. Dashboard shows the row working via child extension event mirroring.
546
+ 3. Pressing attach while work is live opens the hosted session without interrupting it.
547
+ 4. User can type normally into the attached Pi session.
548
+ 5. Slash commands like `/session`, `/model`, `/tree` are handled by the child Pi, not reimplemented by agent-board.
549
+ 6. Detach returns to dashboard and child Pi keeps running.
550
+ 7. Reattach returns to the same live child Pi process.
551
+ 8. If the host dies, attach falls back to `ctx.switchSession(sessionFile)`.
552
+ 9. Existing tests pass; new host tests cover runner/socket/store behavior.
553
+
554
+ ## 12. Risk register
555
+
556
+ | Risk | Impact | Mitigation |
557
+ |---|---|---|
558
+ | `ctx.ui.custom` cannot faithfully render child terminal output | high | Phase 0 spike; fallback to Pi core raw takeover API or standalone CLI |
559
+ | `node-pty` native install friction | medium | keep JSON runner fallback; document dependency; consider tmux fallback |
560
+ | child Pi extension recursion | medium | `AGENT_BOARD_CHILD=1`; skip dashboard auto-open/footer in child |
561
+ | host liveness conflated with agent activity | high | add `host.json`; separate `hostAlive` from `row.alive` |
562
+ | worktree safety too conservative with idle hosts | low/medium | conservative MVP, later refine with activity state |
563
+ | detach chord conflicts with Pi keybindings | low | configurable later; start with `ctrl+]` |
564
+ | terminal images/OSC links not perfect in virtual renderer | medium | document limitation; raw takeover/core API if needed |
565
+
566
+ ## 13. Confidence
567
+
568
+ - **PTY-hosted process + socket + status mirroring:** high, ~80–85%.
569
+ - **Extension-only virtual-terminal attach:** medium, ~60–70% until Phase 0 proves TUI fidelity.
570
+ - **True raw 1:1 terminal attach:** high conceptually, but likely needs Pi core or standalone CLI support.
571
+
572
+ ## 14. Recommended next step
573
+
574
+ Do **Phase 0 only** first. Do not refactor the whole service until we prove that a parent Pi extension can render and drive a child Pi PTY well enough inside `ctx.ui.custom()`.
575
+
576
+ If Phase 0 passes, proceed with phases 1–6. If it fails, stop and design either:
577
+
578
+ 1. a small Pi core raw-terminal takeover API, or
579
+ 2. a standalone `pi-agent-board` CLI that owns the terminal like tmux.