@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.
- package/IMPLEMENTATION_PLAN.md +920 -0
- package/LICENSE +21 -0
- package/PRD.md +484 -0
- package/PROGRESS.md +127 -0
- package/README.md +131 -0
- package/VERIFY.md +113 -0
- package/docs/BATCH_SELECTION_READ_FLOW.md +277 -0
- package/docs/EXPLORATION.md +187 -0
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +579 -0
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +704 -0
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +499 -0
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +366 -0
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +105 -0
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +142 -0
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +59 -0
- package/index.ts +6 -0
- package/package.json +81 -0
- package/runner/job-runner.mjs +420 -0
- package/runner/pty-runner.mjs +310 -0
- package/runner/state-runner.mjs +120 -0
- package/runner/title-runner.mjs +80 -0
- package/scripts/patch-vulns.mjs +59 -0
- package/src/commands/agent-board.ts +318 -0
- package/src/commands/attach-flow.ts +231 -0
- package/src/commands/bg.ts +70 -0
- package/src/core/atomic.mjs +145 -0
- package/src/core/auto-state.mjs +320 -0
- package/src/core/dashboard-render.mjs +10 -0
- package/src/core/derive.mjs +114 -0
- package/src/core/diagnostics.mjs +109 -0
- package/src/core/events.mjs +268 -0
- package/src/core/evidence.mjs +242 -0
- package/src/core/follow-up-queue.mjs +193 -0
- package/src/core/heuristics.mjs +240 -0
- package/src/core/ids.mjs +35 -0
- package/src/core/invocation.mjs +43 -0
- package/src/core/launch-options.mjs +317 -0
- package/src/core/launch.mjs +116 -0
- package/src/core/locks.mjs +80 -0
- package/src/core/paths.mjs +86 -0
- package/src/core/pid.mjs +42 -0
- package/src/core/prewarm-schedule.mjs +41 -0
- package/src/core/prompt-transport.mjs +13 -0
- package/src/core/pty-attach-jiggle-retry.mjs +90 -0
- package/src/core/pty-attach-render.mjs +51 -0
- package/src/core/pty-input.mjs +15 -0
- package/src/core/pty-links.mjs +71 -0
- package/src/core/pty-scroll.mjs +155 -0
- package/src/core/pty-support.mjs +327 -0
- package/src/core/repo.mjs +47 -0
- package/src/core/rows.mjs +290 -0
- package/src/core/screen-log-gc.mjs +198 -0
- package/src/core/screen-log.mjs +160 -0
- package/src/core/session-view.mjs +174 -0
- package/src/core/steering-prompts.mjs +34 -0
- package/src/core/steering.mjs +133 -0
- package/src/core/store.mjs +308 -0
- package/src/core/title.mjs +43 -0
- package/src/core/types.mjs +380 -0
- package/src/core/worktree.mjs +64 -0
- package/src/index.ts +109 -0
- package/src/runtime/service.mjs +1194 -0
- package/src/ui/dashboard-evidence.mjs +85 -0
- package/src/ui/dashboard.ts +1952 -0
- 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.
|