@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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level store operations over the agent-board layout (see paths.mjs).
|
|
3
|
+
* Roster/meta/state/status read+write, row listing, and view creation/recovery.
|
|
4
|
+
* Used by the extension, the runner, and tests. Pure node, no Pi imports.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { atomicWriteJson, ensureDir, readJson } from "./atomic.mjs";
|
|
8
|
+
import * as P from "./paths.mjs";
|
|
9
|
+
import { isAlive } from "./pid.mjs";
|
|
10
|
+
import { readDiagnosticSummary } from "./diagnostics.mjs";
|
|
11
|
+
import { readEvidence, summarizeEvidence } from "./evidence.mjs";
|
|
12
|
+
import { readFollowUpQueue, summarizeFollowUpQueue } from "./follow-up-queue.mjs";
|
|
13
|
+
import { readSteering, summarizeSteering } from "./steering.mjs";
|
|
14
|
+
|
|
15
|
+
/** @typedef {import("./types.mjs").Roster} Roster */
|
|
16
|
+
/** @typedef {import("./types.mjs").ViewMeta} ViewMeta */
|
|
17
|
+
/** @typedef {import("./types.mjs").ViewState} ViewState */
|
|
18
|
+
/** @typedef {import("./types.mjs").RunStatus} RunStatus */
|
|
19
|
+
/** @typedef {import("./types.mjs").HostStatus} HostStatus */
|
|
20
|
+
/** @typedef {import("./types.mjs").LaunchPrefs} LaunchPrefs */
|
|
21
|
+
|
|
22
|
+
const META_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
// ---- roster ---------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** @param {string} root @returns {Roster} */
|
|
27
|
+
export function readRoster(root) {
|
|
28
|
+
return readJson(P.rosterPath(root), { version: 1, views: [] });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {string} root @param {Roster} roster */
|
|
32
|
+
export function writeRoster(root, roster) {
|
|
33
|
+
atomicWriteJson(P.rosterPath(root), roster);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} root @param {string} viewId */
|
|
37
|
+
export function addToRoster(root, viewId) {
|
|
38
|
+
const roster = readRoster(root);
|
|
39
|
+
if (!roster.views.includes(viewId)) {
|
|
40
|
+
roster.views.push(viewId);
|
|
41
|
+
writeRoster(root, roster);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string} root @param {string} viewId */
|
|
46
|
+
export function removeFromRoster(root, viewId) {
|
|
47
|
+
const roster = readRoster(root);
|
|
48
|
+
const next = roster.views.filter((v) => v !== viewId);
|
|
49
|
+
if (next.length !== roster.views.length) {
|
|
50
|
+
roster.version = roster.version ?? 1;
|
|
51
|
+
writeRoster(root, { version: roster.version, views: next });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} root @returns {LaunchPrefs} */
|
|
56
|
+
export function readLaunchPrefs(root) {
|
|
57
|
+
return readJson(P.launchPrefsPath(root), {
|
|
58
|
+
version: 1,
|
|
59
|
+
cwd: null,
|
|
60
|
+
model: null,
|
|
61
|
+
thinkingLevel: null,
|
|
62
|
+
screenLogRetentionDays: null,
|
|
63
|
+
screenLogMaxSize: null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} root @param {Partial<LaunchPrefs>} prefs */
|
|
68
|
+
export function writeLaunchPrefs(root, prefs) {
|
|
69
|
+
atomicWriteJson(P.launchPrefsPath(root), {
|
|
70
|
+
version: 1,
|
|
71
|
+
cwd: prefs.cwd ?? null,
|
|
72
|
+
model: prefs.model ?? null,
|
|
73
|
+
thinkingLevel: prefs.thinkingLevel ?? null,
|
|
74
|
+
screenLogRetentionDays: prefs.screenLogRetentionDays ?? null,
|
|
75
|
+
screenLogMaxSize: prefs.screenLogMaxSize ?? null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- meta / state / status ------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** @param {string} root @param {string} viewId @returns {ViewMeta|null} */
|
|
82
|
+
export function readMeta(root, viewId) {
|
|
83
|
+
return readJson(P.metaPath(root, viewId), /** @type {ViewMeta|null} */ (null));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {string} root @param {ViewMeta} meta */
|
|
87
|
+
export function writeMeta(root, meta) {
|
|
88
|
+
meta.updatedAt = Date.now();
|
|
89
|
+
atomicWriteJson(P.metaPath(root, meta.id), meta);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {string} root @param {string} viewId @returns {ViewState|null} */
|
|
93
|
+
export function readState(root, viewId) {
|
|
94
|
+
return readJson(P.statePath(root, viewId), /** @type {ViewState|null} */ (null));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {string} root @param {ViewState} state */
|
|
98
|
+
export function writeState(root, state) {
|
|
99
|
+
atomicWriteJson(P.statePath(root, state.viewId), state);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {string} root @param {string} viewId @param {string} runId @returns {RunStatus|null} */
|
|
103
|
+
export function readStatus(root, viewId, runId) {
|
|
104
|
+
return readJson(P.statusPath(root, viewId, runId), /** @type {RunStatus|null} */ (null));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {string} root @param {RunStatus} status */
|
|
108
|
+
export function writeStatus(root, status) {
|
|
109
|
+
atomicWriteJson(P.statusPath(root, status.viewId, status.runId), status);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {string} root @param {string} viewId @returns {HostStatus|null} */
|
|
113
|
+
export function readHost(root, viewId) {
|
|
114
|
+
return readJson(P.hostPath(root, viewId), /** @type {HostStatus|null} */ (null));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** @param {string} root @param {HostStatus} host */
|
|
118
|
+
export function writeHost(root, host) {
|
|
119
|
+
atomicWriteJson(P.hostPath(root, host.viewId), host);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {string} root @param {string} viewId @param {number|null} pid */
|
|
123
|
+
export function writeHostPid(root, viewId, pid) {
|
|
124
|
+
atomicWriteJson(P.hostPidPath(root, viewId), { pid, at: Date.now() });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @param {string} root @param {string} viewId @returns {number|null} */
|
|
128
|
+
export function readHostPid(root, viewId) {
|
|
129
|
+
return readJson(P.hostPidPath(root, viewId), { pid: null }).pid ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {string} root @param {string} viewId @param {string} runId @param {number|null} pid */
|
|
133
|
+
export function writePid(root, viewId, runId, pid) {
|
|
134
|
+
atomicWriteJson(P.pidPath(root, viewId, runId), { pid, at: Date.now() });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {string} root @param {string} viewId @param {string} runId @returns {number|null} */
|
|
138
|
+
export function readPid(root, viewId, runId) {
|
|
139
|
+
return readJson(P.pidPath(root, viewId, runId), { pid: null }).pid ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- listing / loading ----------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** @param {string} root @param {string} viewId @returns {string[]} run ids, newest first by mtime. */
|
|
145
|
+
export function listRunIds(root, viewId) {
|
|
146
|
+
const dir = P.runsDir(root, viewId);
|
|
147
|
+
if (!existsSync(dir)) return [];
|
|
148
|
+
try {
|
|
149
|
+
return readdirSync(dir)
|
|
150
|
+
.filter((name) => {
|
|
151
|
+
try {
|
|
152
|
+
return statSync(P.runDir(root, viewId, name)).isDirectory();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.sort((a, b) => mtime(P.runDir(root, viewId, b)) - mtime(P.runDir(root, viewId, a)));
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {string} dir */
|
|
164
|
+
function mtime(dir) {
|
|
165
|
+
try {
|
|
166
|
+
return statSync(dir).mtimeMs;
|
|
167
|
+
} catch {
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A merged dashboard row: meta + derived state. `state` may be null for a never-run row.
|
|
174
|
+
* @typedef {Object} Row
|
|
175
|
+
* @property {ViewMeta} meta
|
|
176
|
+
* @property {ViewState|null} state
|
|
177
|
+
* @property {boolean} alive Whether the row's current run pid/foreground activity is alive.
|
|
178
|
+
* @property {boolean} hostAlive Whether a PTY host/socket is alive and attachable.
|
|
179
|
+
* @property {HostStatus|null} host
|
|
180
|
+
* @property {import("./types.mjs").ReviewSummary} [review]
|
|
181
|
+
* @property {import("./types.mjs").DiagnosticSummary} [diagnostics]
|
|
182
|
+
* @property {import("./types.mjs").FollowUpSummary} [followUps]
|
|
183
|
+
* @property {import("./types.mjs").SteeringSummary} [steering]
|
|
184
|
+
*/
|
|
185
|
+
|
|
186
|
+
/** @param {string} root @param {string} viewId @returns {Row|null} */
|
|
187
|
+
export function loadRow(root, viewId) {
|
|
188
|
+
const meta = readMeta(root, viewId);
|
|
189
|
+
if (!meta) return null;
|
|
190
|
+
const state = readState(root, viewId);
|
|
191
|
+
const summaries = readViewArtifactSummaries(root, viewId);
|
|
192
|
+
const enrichedState = state ? { ...state, ...summaries } : state;
|
|
193
|
+
let alive = false;
|
|
194
|
+
if (enrichedState?.currentRunId) {
|
|
195
|
+
const pid = readPid(root, viewId, enrichedState.currentRunId);
|
|
196
|
+
alive = isAlive(pid);
|
|
197
|
+
}
|
|
198
|
+
// A managed session can also be active in the foreground after the user attaches
|
|
199
|
+
// and types a follow-up. In that path there is no detached runner pid for us to
|
|
200
|
+
// poll, but foreground extension events mirror processState into state.json.
|
|
201
|
+
if (!alive && enrichedState?.processState === "alive") alive = true;
|
|
202
|
+
const host = readHost(root, viewId);
|
|
203
|
+
const hostPid = host?.runnerPid ?? readHostPid(root, viewId);
|
|
204
|
+
const hostAlive = Boolean(host && (host.state === "alive" || host.state === "starting") && isAlive(hostPid));
|
|
205
|
+
return { meta, state: enrichedState, alive, hostAlive, host, ...summaries };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** @param {string} root @param {string} viewId */
|
|
209
|
+
export function readViewArtifactSummaries(root, viewId) {
|
|
210
|
+
const review = summarizeEvidence(readEvidence(root, viewId));
|
|
211
|
+
const diagnostics = readDiagnosticSummary(root, viewId);
|
|
212
|
+
const followUps = summarizeFollowUpQueue(readFollowUpQueue(root, viewId));
|
|
213
|
+
const steering = summarizeSteering(readSteering(root, viewId));
|
|
214
|
+
return { review, diagnostics, followUps, steering };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load every non-archived row referenced by the roster.
|
|
219
|
+
* @param {string} root
|
|
220
|
+
* @param {{ includeArchived?: boolean }} [opts]
|
|
221
|
+
* @returns {Row[]}
|
|
222
|
+
*/
|
|
223
|
+
export function listRows(root, opts = {}) {
|
|
224
|
+
const roster = readRoster(root);
|
|
225
|
+
/** @type {Row[]} */
|
|
226
|
+
const rows = [];
|
|
227
|
+
for (const viewId of roster.views) {
|
|
228
|
+
// Archived short-circuit: archived rows are invisible on the dashboard, so
|
|
229
|
+
// never pay for their artifact files (state/evidence/host/diagnostics...).
|
|
230
|
+
// meta.json is the single authoritative source of the archived flag.
|
|
231
|
+
const meta = readMeta(root, viewId);
|
|
232
|
+
if (!meta) continue;
|
|
233
|
+
if (meta.archived && !opts.includeArchived) continue;
|
|
234
|
+
const row = loadRow(root, viewId);
|
|
235
|
+
if (!row) continue;
|
|
236
|
+
if (row.meta.archived && !opts.includeArchived) continue;
|
|
237
|
+
rows.push(row);
|
|
238
|
+
}
|
|
239
|
+
return rows;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- creation -------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a new managed view (row) and persist meta + roster + an initial queued state.
|
|
246
|
+
* Does not launch anything — the caller launches a run afterward.
|
|
247
|
+
* @param {string} root
|
|
248
|
+
* @param {{
|
|
249
|
+
* id: string, name: string, cwd: string, repoCwd?: string, repoRoot?: string|null,
|
|
250
|
+
* worktreeMode?: import("./types.mjs").WorktreeMode, worktreePath?: string|null,
|
|
251
|
+
* defaultModel?: string|null,
|
|
252
|
+
* defaultThinking?: "off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null,
|
|
253
|
+
* writeCapable?: boolean,
|
|
254
|
+
* sessionFile?: string,
|
|
255
|
+
* }} opts
|
|
256
|
+
* @returns {ViewMeta}
|
|
257
|
+
*/
|
|
258
|
+
export function createView(root, opts) {
|
|
259
|
+
ensureDir(P.sessionsDir(root));
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
/** @type {ViewMeta} */
|
|
262
|
+
const meta = {
|
|
263
|
+
version: META_VERSION,
|
|
264
|
+
id: opts.id,
|
|
265
|
+
name: opts.name,
|
|
266
|
+
cwd: opts.cwd,
|
|
267
|
+
repoCwd: opts.repoCwd ?? opts.cwd,
|
|
268
|
+
repoRoot: opts.repoRoot ?? null,
|
|
269
|
+
sessionFile: opts.sessionFile ?? P.sessionFilePath(root, opts.id),
|
|
270
|
+
createdAt: now,
|
|
271
|
+
updatedAt: now,
|
|
272
|
+
pinned: false,
|
|
273
|
+
kind: "pi-session",
|
|
274
|
+
defaultModel: opts.defaultModel ?? null,
|
|
275
|
+
defaultThinking: opts.defaultThinking ?? null,
|
|
276
|
+
worktreeMode: opts.worktreeMode ?? "off",
|
|
277
|
+
worktreePath: opts.worktreePath ?? null,
|
|
278
|
+
writeCapable: opts.writeCapable ?? true,
|
|
279
|
+
archived: false,
|
|
280
|
+
source: "agent-board",
|
|
281
|
+
};
|
|
282
|
+
ensureDir(P.viewDir(root, meta.id));
|
|
283
|
+
writeMeta(root, meta);
|
|
284
|
+
/** @type {ViewState} */
|
|
285
|
+
const state = {
|
|
286
|
+
version: 1,
|
|
287
|
+
viewId: meta.id,
|
|
288
|
+
currentRunId: null,
|
|
289
|
+
semanticState: "queued",
|
|
290
|
+
processState: "exited",
|
|
291
|
+
summary: "Queued",
|
|
292
|
+
lastActivityAt: now,
|
|
293
|
+
updatedAt: now,
|
|
294
|
+
needsInput: false,
|
|
295
|
+
hasError: false,
|
|
296
|
+
latestAssistantPreview: "",
|
|
297
|
+
latestTool: null,
|
|
298
|
+
question: null,
|
|
299
|
+
pendingQuestions: [],
|
|
300
|
+
error: null,
|
|
301
|
+
lastVisitedAt: null,
|
|
302
|
+
lastAgentActivityAt: null,
|
|
303
|
+
autoState: null,
|
|
304
|
+
};
|
|
305
|
+
writeState(root, state);
|
|
306
|
+
addToRoster(root, meta.id);
|
|
307
|
+
return meta;
|
|
308
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Title-generation helpers for session names. */
|
|
2
|
+
|
|
3
|
+
/** Default cheap model for session titles. Override via $AGENT_BOARD_TITLE_MODEL. */
|
|
4
|
+
export const DEFAULT_TITLE_MODEL = "openai-codex/gpt-5.5";
|
|
5
|
+
|
|
6
|
+
/** Default thinking level for session titles. Override via $AGENT_BOARD_TITLE_THINKING_LEVEL. */
|
|
7
|
+
export const DEFAULT_TITLE_THINKING_LEVEL = "low";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prompt for a short session title derived from the user's initial task.
|
|
11
|
+
* @param {string} taskPrompt
|
|
12
|
+
*/
|
|
13
|
+
export function titlePrompt(taskPrompt) {
|
|
14
|
+
return [
|
|
15
|
+
"Write a concise title for this coding task.",
|
|
16
|
+
"Rules:",
|
|
17
|
+
"- 3 or 4 words max",
|
|
18
|
+
"- plain text only",
|
|
19
|
+
"- no quotes",
|
|
20
|
+
"- no markdown",
|
|
21
|
+
"- describe the task, not the format",
|
|
22
|
+
"",
|
|
23
|
+
`Task: ${String(taskPrompt || "").trim()}`,
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a model-generated title into a compact dashboard label.
|
|
29
|
+
* @param {string|null|undefined} text
|
|
30
|
+
* @param {string} fallback
|
|
31
|
+
* @param {number} [maxWords]
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeGeneratedTitle(text, fallback, maxWords = 4) {
|
|
34
|
+
const cleaned = String(text || "")
|
|
35
|
+
.replace(/^\s*(title\s*:\s*)?/i, "")
|
|
36
|
+
.replace(/^['"`\-–—•*\s]+|['"`\-–—•*\s]+$/g, "")
|
|
37
|
+
.replace(/\s+/g, " ")
|
|
38
|
+
.trim();
|
|
39
|
+
if (!cleaned) return fallback;
|
|
40
|
+
const words = cleaned.split(/\s+/).filter(Boolean).slice(0, maxWords);
|
|
41
|
+
const compact = words.join(" ").replace(/[.,;:!?]+$/g, "").trim();
|
|
42
|
+
return compact || fallback;
|
|
43
|
+
}
|