@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,187 @@
|
|
|
1
|
+
# Exploration & Decisions — Pi Agent Board Extension
|
|
2
|
+
|
|
3
|
+
**For future agents.** This is the distilled gist of the codebase recon done before
|
|
4
|
+
implementing `IMPLEMENTATION_PLAN.md`. Read this first; it saves re-deriving the Pi API.
|
|
5
|
+
|
|
6
|
+
Runtime target: `/opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent` (v0.75.5).
|
|
7
|
+
`pi` binary is on PATH at `/opt/homebrew/bin/pi` → `dist/cli.js`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. How a Pi extension is loaded & shaped
|
|
12
|
+
|
|
13
|
+
- An extension is a TS module: `export default function (pi: ExtensionAPI) { ... }`
|
|
14
|
+
(may be `async`; Pi awaits it before `session_start`).
|
|
15
|
+
- Auto-discovered from `~/.pi/agent/extensions/*.ts` or `<name>/index.ts` (global) and
|
|
16
|
+
`.pi/extensions/...` (project). `/reload` hot-reloads them. `pi -e ./path.ts` for quick tests.
|
|
17
|
+
- Pi's loader (jiti) resolves `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai`,
|
|
18
|
+
`@earendil-works/pi-tui`, `typebox` **for you at runtime** — you do not bundle them.
|
|
19
|
+
For local typechecking we path-map those to the global install (see `tsconfig.json`).
|
|
20
|
+
- Loaded TS runs via jiti. **Detached external processes (our runner) must NOT rely on
|
|
21
|
+
jiti** → the runner is a plain `.mjs` that we spawn with `node`.
|
|
22
|
+
|
|
23
|
+
## 2. ExtensionAPI surface we actually use
|
|
24
|
+
|
|
25
|
+
- `pi.registerCommand(name, { description, handler(args, ctx), getArgumentCompletions? })`
|
|
26
|
+
— handler `ctx` is `ExtensionCommandContext` (adds `newSession/fork/switchSession/
|
|
27
|
+
navigateTree/reload/waitForIdle`). Session-switch methods deadlock from event handlers,
|
|
28
|
+
so they exist only on command ctx.
|
|
29
|
+
- `pi.registerShortcut(KeyId, { description, handler(ctx) })`.
|
|
30
|
+
- `pi.registerFlag(name, { description, type:"boolean"|"string", default })` + `pi.getFlag`.
|
|
31
|
+
- `pi.exec(cmd, args, { signal, timeout })` → `{ stdout, stderr, code, killed }`.
|
|
32
|
+
- `pi.appendEntry(customType, data)` persists extension state into the session JSONL
|
|
33
|
+
(custom entry, not sent to LLM). Restore by scanning `ctx.sessionManager.getEntries()`.
|
|
34
|
+
- `ctx.ui.*`: `select/confirm/input/editor/notify`, `setStatus/setWidget/setFooter`,
|
|
35
|
+
and **`custom<T>(factory, opts?) => Promise<T>`** (the dashboard surface).
|
|
36
|
+
- `ctx.switchSession(path, { withSession })` — attach = switch the interactive session to
|
|
37
|
+
a managed session file. `withSession(ctx)` runs **after** the old runtime is torn down;
|
|
38
|
+
only use the fresh `ctx` passed in, never captured old `pi`/`ctx`/`sessionManager`.
|
|
39
|
+
|
|
40
|
+
### `ctx.ui.custom` (the important one)
|
|
41
|
+
```ts
|
|
42
|
+
const result = await ctx.ui.custom<T>((tui, theme, keybindings, done) => {
|
|
43
|
+
// return a Component: { render(width): string[]; invalidate(); handleInput?(data) }
|
|
44
|
+
// call tui.requestRender() after state changes to repaint
|
|
45
|
+
// call done(value) to close the surface and resolve the promise
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
- It **replaces the editor** with your component until `done()` (full-screen-ish surface).
|
|
49
|
+
- It returns a `Promise`, NOT a handle. Live updates: hold a `tui` ref, call
|
|
50
|
+
`tui.requestRender()` from a `setInterval`/file-watch/poll loop. Terminal height =
|
|
51
|
+
`tui.terminal.rows`, width comes in as the `render(width)` arg.
|
|
52
|
+
- `{ overlay: true, overlayOptions }` renders a floating modal instead (used for peek/dialogs
|
|
53
|
+
if we want; MVP keeps peek as an internal mode of the dashboard component).
|
|
54
|
+
|
|
55
|
+
## 3. TUI building blocks (`@earendil-works/pi-tui`)
|
|
56
|
+
|
|
57
|
+
Components implement `{ render(width:number):string[]; invalidate():void; handleInput?(data):void }`
|
|
58
|
+
— each rendered line **must not exceed `width`** (use `truncateToWidth`).
|
|
59
|
+
Exports we use: `Text, Box, Container, Spacer, Markdown, SelectList, SettingsList,
|
|
60
|
+
matchesKey, Key, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component`.
|
|
61
|
+
From coding-agent: `DynamicBorder, BorderedLoader, getMarkdownTheme, getSettingsListTheme,
|
|
62
|
+
keyHint`. Theme: `theme.fg(color, text)`, `theme.bold/italic/strikethrough`.
|
|
63
|
+
Key input: `matchesKey(data, Key.up)` etc. After state change in `handleInput`, call
|
|
64
|
+
`tui.requestRender()`.
|
|
65
|
+
|
|
66
|
+
## 4. Sessions & the headless worker (the core mechanism)
|
|
67
|
+
|
|
68
|
+
- Session files are JSONL trees (`id`/`parentId`); first line is a header
|
|
69
|
+
`{"type":"session","version":3,"id":"<uuid>","timestamp":"<iso>","cwd":"<cwd>"}`.
|
|
70
|
+
- **Launching a managed run:**
|
|
71
|
+
`pi --mode json -p --session <file> "<prompt>"`, spawned with `cwd: <repo|worktree>`.
|
|
72
|
+
- `--session <path>` with a path that contains `/` or ends `.jsonl` → `SessionManager.open(path)`.
|
|
73
|
+
If the file **doesn't exist**, Pi creates a fresh session and persists to that **exact path**
|
|
74
|
+
(verified in `dist/core/session-manager.ts` `open()` → `setSessionFile` else-branch).
|
|
75
|
+
The header `cwd` becomes the spawn `cwd`, so always set `cwd` correctly.
|
|
76
|
+
- `--mode json` = one-shot: it `session.subscribe((e)=>stdout(JSON.stringify(e)+"\n"))`,
|
|
77
|
+
runs the single prompt to completion (a full agent loop, many turns/tools), then exits
|
|
78
|
+
**with `agent_end` as the last event** — `worker.on("close")` is the run-finished signal.
|
|
79
|
+
- **⚠️ AUTH/NETWORK CAVEAT (verified 2026-05-30):** the one-shot exits cleanly *only when the
|
|
80
|
+
worker can actually reach the model provider*. With no/blocked pi provider auth (or a
|
|
81
|
+
network/proxy stall), `pi --mode json -p` records `session → agent_start → turn_start →
|
|
82
|
+
message_start(user) → message_end(user)` and then **blocks indefinitely at the provider
|
|
83
|
+
request** (idle, ~3% CPU) — **no assistant reply, no `agent_end`, no process exit**. This is
|
|
84
|
+
a pi/runtime condition, NOT an extension bug. The runner handles it correctly: the row stays
|
|
85
|
+
`working/alive` until the user stops it (`s` in the dashboard → SIGTERM → finalized `stopped`).
|
|
86
|
+
Note: pi's own provider auth is separate from Claude Code's `/login`. Validate with
|
|
87
|
+
`pi --mode json -p --no-session "say hi"` — it must print events ending in `agent_end` and exit.
|
|
88
|
+
- `-p` forces non-interactive. `--model <m>`, `--tools a,b`, `--append-system-prompt <file>`
|
|
89
|
+
are available. `--no-session` = ephemeral (we do NOT use it for managed rows; we want persistence).
|
|
90
|
+
- Reply / resume = launch another run against the **same** `--session <file>`; Pi appends to it.
|
|
91
|
+
|
|
92
|
+
### JSON event vocabulary actually emitted (parse these in the runner)
|
|
93
|
+
First line: the session header `{type:"session",...}`. Then per `AgentSessionEvent`:
|
|
94
|
+
`agent_start`, `agent_end`, `turn_start`, `turn_end`,
|
|
95
|
+
`message_start` / `message_update` / `message_end` (each has `.message`: an AgentMessage),
|
|
96
|
+
`tool_execution_start` `{toolCallId,toolName,args}`,
|
|
97
|
+
`tool_execution_update` `{...,partialResult}`,
|
|
98
|
+
`tool_execution_end` `{toolCallId,toolName,result,isError}`,
|
|
99
|
+
`tool_call`, `tool_result`, plus `queue_update`, `compaction_*`, `auto_retry_*`,
|
|
100
|
+
`model_select`, `thinking_level_*`, `session_*`.
|
|
101
|
+
> NOTE: the subagent example reads `tool_result_end` — that string is **never emitted** (dead
|
|
102
|
+
> defensive code). Use `message_end` (role assistant → text/toolCalls/usage/stopReason/errorMessage)
|
|
103
|
+
> and `tool_execution_start/end` for live state. `agent_end` ⇒ run finished.
|
|
104
|
+
|
|
105
|
+
AssistantMessage shape: `{ role:"assistant", content:(TextContent|ThinkingContent|ToolCall)[],
|
|
106
|
+
model, usage, stopReason:"stop"|"length"|"toolUse"|"error"|"aborted", errorMessage? }`.
|
|
107
|
+
ToolCall content block: `{ type:"toolCall", id, name, arguments }`.
|
|
108
|
+
|
|
109
|
+
## 5. Architecture chosen (matches plan §3.3) — file-backed detached runner
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Pi extension (parent, interactive) runner/job-runner.mjs (detached, 1 per run)
|
|
113
|
+
/agent-board dashboard (ctx.ui.custom) ─► spawns: pi --mode json -p --session <file> <prompt>
|
|
114
|
+
store reader/writer parses JSON event lines from worker stdout
|
|
115
|
+
launches runner via child_process writes runs/<runId>/{status.json,events.jsonl,
|
|
116
|
+
polls store files to repaint stdout.log,stderr.log,pid.json}
|
|
117
|
+
updates views/<id>/state.json on progress/exit
|
|
118
|
+
```
|
|
119
|
+
Why detached `.mjs` runner (not spawn pi directly from the extension, not a daemon):
|
|
120
|
+
survives parent `/reload`/exit, durable status, no socket/daemon complexity. Each run is the
|
|
121
|
+
durable monitor for one worker.
|
|
122
|
+
|
|
123
|
+
### Store layout (user-scoped) — `~/.pi/agent/agent-board/`
|
|
124
|
+
```
|
|
125
|
+
roster.json { version, views: [viewId...] }
|
|
126
|
+
views/<viewId>/meta.json stable: id,name,cwd,sessionFile,createdAt,pinned,kind,
|
|
127
|
+
worktreeMode,worktreePath,defaultModel,source,archived
|
|
128
|
+
views/<viewId>/state.json derived: currentRunId,semanticState,processState,summary,
|
|
129
|
+
needsInput,hasError,latestAssistantPreview,latestTool,lastActivityAt
|
|
130
|
+
views/<viewId>/runs/<runId>/status.json per-run durable snapshot (pid,startedAt,endedAt,
|
|
131
|
+
exitCode,semanticState,currentTool,latestAssistantPreview,question,error)
|
|
132
|
+
views/<viewId>/runs/<runId>/{events.jsonl,stdout.log,stderr.log,pid.json}
|
|
133
|
+
sessions/<viewId>.jsonl the managed Pi session file (Pi creates/persists it)
|
|
134
|
+
```
|
|
135
|
+
- One row = one real Pi session file. One run = one detached worker against it.
|
|
136
|
+
- Both `state.json` (row, long-lived) and `status.json` (run, single attempt) exist so
|
|
137
|
+
reply/resume/attach stay clean.
|
|
138
|
+
- Atomic writes: write tmp + `rename`. Liveness: `pid.json` + `process.kill(pid,0)`.
|
|
139
|
+
|
|
140
|
+
## 6. State derivation rules (plan §9)
|
|
141
|
+
|
|
142
|
+
Semantic: `queued`→`working` (alive, once assistant/tool activity); on clean exit
|
|
143
|
+
`needs_input` (assistant ends asking) | `completed` | `idle`; on bad exit `failed`;
|
|
144
|
+
user-stopped `stopped`. Process: `alive` if pid live else `exited`.
|
|
145
|
+
Summary priority: model-summary(opt-in) → active tool (`Editing x`,`Running tests`) →
|
|
146
|
+
blocker/question → first sentence of latest assistant → stderr/error → fallback status.
|
|
147
|
+
Needs-input heuristic: assistant text ends in `?` or contains `need your input`/
|
|
148
|
+
`which option`/`should I`/`please confirm`.
|
|
149
|
+
|
|
150
|
+
## 7. Safety / worktree (plan §11)
|
|
151
|
+
|
|
152
|
+
Same-repo parallel **writers** require git worktree isolation (locked decision). MVP:
|
|
153
|
+
explicit `worktreeMode:"worktree"` per dispatch → `git worktree add` under
|
|
154
|
+
`~/.pi/agent/agent-board/worktrees/<viewId>`; persist `worktreePath`; block a 2nd active
|
|
155
|
+
writer in the same repo if not isolated. Delete row ≠ delete data: archive row, keep
|
|
156
|
+
session file; worktree removal needs explicit confirm.
|
|
157
|
+
|
|
158
|
+
## 8. Decisions made for this implementation
|
|
159
|
+
|
|
160
|
+
1. **Runner is `runner/job-runner.mjs` (plain ESM)**, spawned with `node`. It imports only
|
|
161
|
+
`node:*`. The pi binary it spawns is passed in via run-config (`piBin`, default `"pi"`).
|
|
162
|
+
2. **Extension code is TS**, loaded by Pi via jiti. We add `package.json` + `tsconfig.json`
|
|
163
|
+
only for local typecheck/tests (path-mapped to the global pi install); not needed at runtime.
|
|
164
|
+
3. **Summaries: cheap model is the DEFAULT** (`DEFAULT_SUMMARY_MODEL = "gpt-4o"` in the runner;
|
|
165
|
+
user choice 2026-05-30), with the **heuristic kept as a graceful fallback** (no API key /
|
|
166
|
+
offline / timeout) and still used for live state detection (needs-input, semantic state).
|
|
167
|
+
Override with `AGENT_BOARD_SUMMARY_MODEL=<model>`, disable with `=off`. The summary call has a
|
|
168
|
+
15s safety timeout and runs *after* the heuristic terminal state is already persisted, so a
|
|
169
|
+
slow/unreachable summary model never stalls the dashboard (row flips to final state at once,
|
|
170
|
+
summary upgrades a few seconds later). gpt-4o needs OpenAI auth; without it → heuristic.
|
|
171
|
+
4. **Dashboard = single `ctx.ui.custom` component** owning list + peek + dispatch input as
|
|
172
|
+
internal modes (plan §10.4 recommendation). Live updates via a poll loop calling
|
|
173
|
+
`tui.requestRender()` (store files are the source of truth; cheap to re-read).
|
|
174
|
+
5. **Attach = `ctx.switchSession(sessionFile, { withSession })`** from the `/agent-board` command
|
|
175
|
+
handler. If the row's run is alive → confirm interrupt (stop run) then attach (locked decision).
|
|
176
|
+
6. **Verification without a TTY/API key:** unit tests for all pure logic + an integration test
|
|
177
|
+
that runs the real `.mjs` runner against a **fake `pi`** stub script emitting canned JSON
|
|
178
|
+
event lines, asserting the durable store artifacts. (`test-support/fake-pi.mjs`.)
|
|
179
|
+
|
|
180
|
+
## 9. Gotchas
|
|
181
|
+
|
|
182
|
+
- `ctx.ui.custom` callback's component must return lines ≤ width; always `truncateToWidth`.
|
|
183
|
+
- Rebuild themed content on `invalidate()` (theme change) if you pre-bake `theme.fg(...)`.
|
|
184
|
+
- Don't import `pi-subagents/src/...` internals (unstable) — reference only.
|
|
185
|
+
- Session-switch `withSession` runs post-teardown; capture only plain strings/ids.
|
|
186
|
+
- The runner must update `status.json` atomically and frequently but throttled (avoid fs thrash).
|
|
187
|
+
- `process.kill(pid, 0)` throws ESRCH if dead, EPERM if alive-but-not-ours → treat EPERM as alive.
|