paseo-bm-plugin 0.0.0-placeholder.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/LICENSE +21 -0
- package/README.md +53 -0
- package/client/agent-tree.ts +308 -0
- package/client/answer-state.ts +62 -0
- package/client/bead-chips.tsx +147 -0
- package/client/beads-header-button.ts +108 -0
- package/client/beads-model.ts +581 -0
- package/client/beads-screen.tsx +516 -0
- package/client/beads-tab.tsx +58 -0
- package/client/chat-card.tsx +636 -0
- package/client/chat-cards.ts +1038 -0
- package/client/dashboard-actions.tsx +255 -0
- package/client/dashboard-model.ts +947 -0
- package/client/dashboard-view.ts +215 -0
- package/client/dashboard.tsx +318 -0
- package/client/launch-manager.ts +323 -0
- package/client/launcher.tsx +516 -0
- package/client/markdown-view.tsx +112 -0
- package/client/markdown.ts +145 -0
- package/client/settings.tsx +104 -0
- package/client/setup-model.ts +552 -0
- package/client/setup-screen.tsx +913 -0
- package/client/slot.ts +47 -0
- package/client/tree.tsx +204 -0
- package/client/ui.tsx +262 -0
- package/client/waiting-pills-model.ts +156 -0
- package/client/waiting-pills.tsx +201 -0
- package/index.client.tsx +232 -0
- package/index.server.ts +168 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/roles/manager.md +181 -0
- package/roles/reviewer.md +160 -0
- package/roles/worker.md +407 -0
- package/server/agent-labels.ts +194 -0
- package/server/agent-role.ts +102 -0
- package/server/answer-marks.ts +120 -0
- package/server/bead-actions.ts +88 -0
- package/server/bead-work.ts +80 -0
- package/server/beads-store.ts +342 -0
- package/server/bm-report.ts +433 -0
- package/server/chat-peers.ts +65 -0
- package/server/chat-rpc.ts +122 -0
- package/server/chat-waiting.ts +182 -0
- package/server/collector.ts +629 -0
- package/server/config-writer.ts +222 -0
- package/server/cost.ts +88 -0
- package/server/dashboard-rpc.ts +662 -0
- package/server/fallback-detect.ts +183 -0
- package/server/fallback-handover.ts +365 -0
- package/server/fallback-manager.ts +170 -0
- package/server/fallback-reviewer.ts +198 -0
- package/server/fallback-rpc.ts +306 -0
- package/server/fallback-settings.ts +322 -0
- package/server/fallback-state.ts +518 -0
- package/server/fallback-switch.ts +191 -0
- package/server/fallback-wait.ts +188 -0
- package/server/format-check.ts +352 -0
- package/server/install-home.ts +187 -0
- package/server/live-timeline.ts +129 -0
- package/server/manager-instructions.ts +9 -0
- package/server/manager.ts +647 -0
- package/server/model-costs.ts +238 -0
- package/server/notice-queue.ts +315 -0
- package/server/notices.ts +81 -0
- package/server/paseo-cli.ts +115 -0
- package/server/provider-id.ts +12 -0
- package/server/review-budget.ts +208 -0
- package/server/reviewer-instructions.ts +9 -0
- package/server/role-choices.ts +161 -0
- package/server/role-extras.ts +270 -0
- package/server/role-hook.ts +347 -0
- package/server/role-mode.ts +397 -0
- package/server/role-settings-rpc.ts +325 -0
- package/server/roles.ts +96 -0
- package/server/settings-notices.ts +112 -0
- package/server/setup-rpc.ts +70 -0
- package/server/setup-skills.ts +121 -0
- package/server/setup-tools.ts +162 -0
- package/server/shell.ts +68 -0
- package/server/stop-propagation.ts +365 -0
- package/server/tools-check.ts +118 -0
- package/server/trace-store.ts +1137 -0
- package/server/traces.ts +1356 -0
- package/server/worker-instructions.ts +9 -0
- package/server/workflow-steps.ts +422 -0
- package/shared/bead-ids.ts +25 -0
- package/shared/bm-fallback.ts +91 -0
- package/shared/bm-format.ts +424 -0
- package/shared/bm-questions.ts +213 -0
- package/shared/bm-report.ts +433 -0
- package/shared/contracts.ts +1371 -0
- package/shared/fallback-patterns.ts +201 -0
- package/shared/fallback.ts +46 -0
- package/shared/new-request.ts +20 -0
- package/shared/order.ts +22 -0
- package/shared/prices.ts +65 -0
- package/shared/settings.ts +57 -0
- package/shared/sole-worker.ts +20 -0
- package/shared/version.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only reader for a workspace's bead store (WP-208, Dashboard Design §10).
|
|
3
|
+
*
|
|
4
|
+
* `.beads/issues.jsonl` at the repository root is the committed source of truth
|
|
5
|
+
* for `br`, so the Dashboard reads exactly that file and nothing else:
|
|
6
|
+
*
|
|
7
|
+
* - no `br` subprocess — the daemon's `PATH` is not ours to assume, and
|
|
8
|
+
* spawning processes from a plugin is a trust boundary we do not need;
|
|
9
|
+
* - no `beads.db` — that is a local cache and `br` holds locks on it;
|
|
10
|
+
* - no writes, ever. `br` owns this data (AGENTS.md).
|
|
11
|
+
*
|
|
12
|
+
* "Ready" is computed the way `br ready` defines it: open, and every `blocks`
|
|
13
|
+
* dependency closed. `parent-child` edges do not block — getting that wrong
|
|
14
|
+
* produces a number that looks plausible and is wrong, which is worse than an
|
|
15
|
+
* obvious failure.
|
|
16
|
+
*/
|
|
17
|
+
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
18
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
19
|
+
import { DashboardError, type BeadStats } from "../shared/contracts";
|
|
20
|
+
|
|
21
|
+
/** Largest bead store this reads in one go (REQ-049a, Q-034). */
|
|
22
|
+
export const MAX_BEADS_FILE_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/** Path of the bead store inside a workspace. */
|
|
25
|
+
export const BEADS_RELATIVE_PATH = join(".beads", "issues.jsonl");
|
|
26
|
+
|
|
27
|
+
/** One bead, reduced to what the Dashboard needs. */
|
|
28
|
+
export interface BeadRecord {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
/** `task`, `bug`, `epic`, …; epics are containers and are never "ready". */
|
|
33
|
+
issueType: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
labels: string[];
|
|
36
|
+
/** Ids this bead is blocked by (`blocks` dependencies only). */
|
|
37
|
+
blockedBy: string[];
|
|
38
|
+
priority: number | null;
|
|
39
|
+
createdAt: string | null;
|
|
40
|
+
closedAt: string | null;
|
|
41
|
+
closeReason: string | null;
|
|
42
|
+
description: string | null;
|
|
43
|
+
/** The `parent-child` parent, when the bead has one. */
|
|
44
|
+
parentId: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ReadBeadsResult {
|
|
48
|
+
beads: Map<string, BeadRecord>;
|
|
49
|
+
skippedLines: number;
|
|
50
|
+
present: boolean;
|
|
51
|
+
source: string;
|
|
52
|
+
readAt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CacheEntry {
|
|
56
|
+
mtimeMs: number;
|
|
57
|
+
size: number;
|
|
58
|
+
result: ReadBeadsResult;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cache = new Map<string, CacheEntry>();
|
|
62
|
+
|
|
63
|
+
/** Drops the bead-store cache. Test helper and post-mutation escape hatch. */
|
|
64
|
+
export function clearBeadsCache(): void {
|
|
65
|
+
cache.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unreadable(detail: string, cause?: unknown): DashboardError {
|
|
69
|
+
return new DashboardError("E_BEADS_STORE_UNREADABLE", detail, cause ? { cause } : undefined);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves `<workspace>/.beads/issues.jsonl`, refusing to leave the workspace.
|
|
74
|
+
*
|
|
75
|
+
* Same no-follow principle as the trace store (design §3.8) and the installer's
|
|
76
|
+
* `src/paths-guard.ts`: a symlinked `.beads` or `issues.jsonl` could point at
|
|
77
|
+
* any file on the machine, and reading it would leak whatever it holds into the
|
|
78
|
+
* Dashboard.
|
|
79
|
+
*/
|
|
80
|
+
export function beadsStorePath(workspaceDirectory: string): string {
|
|
81
|
+
if (!isAbsolute(workspaceDirectory)) {
|
|
82
|
+
throw unreadable(`workspace directory must be absolute: ${workspaceDirectory}`);
|
|
83
|
+
}
|
|
84
|
+
const root = resolve(workspaceDirectory);
|
|
85
|
+
const target = resolve(join(root, BEADS_RELATIVE_PATH));
|
|
86
|
+
const rel = relative(root, target);
|
|
87
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
88
|
+
throw unreadable(`bead store path escapes the workspace: ${target}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let current = root;
|
|
92
|
+
for (const part of rel.split(sep)) {
|
|
93
|
+
if (part === "" || part === ".") continue;
|
|
94
|
+
current = join(current, part);
|
|
95
|
+
try {
|
|
96
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
97
|
+
throw unreadable(`refusing to follow a symlinked bead store path: ${current}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof DashboardError) throw error;
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return target;
|
|
102
|
+
throw unreadable(`cannot inspect ${current}`, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return target;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function asStringArray(value: unknown): string[] {
|
|
109
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parentOf(value: unknown): string | null {
|
|
113
|
+
if (!Array.isArray(value)) return null;
|
|
114
|
+
for (const entry of value) {
|
|
115
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
116
|
+
const dep = entry as { type?: unknown; depends_on_id?: unknown };
|
|
117
|
+
if (dep.type === "parent-child" && typeof dep.depends_on_id === "string") return dep.depends_on_id;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const text = (value: unknown): string | null => (typeof value === "string" && value !== "" ? value : null);
|
|
123
|
+
|
|
124
|
+
function blockedByOf(value: unknown): string[] {
|
|
125
|
+
if (!Array.isArray(value)) return [];
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
for (const entry of value) {
|
|
128
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
129
|
+
const dep = entry as { type?: unknown; depends_on_id?: unknown };
|
|
130
|
+
if (dep.type !== "blocks") continue;
|
|
131
|
+
if (typeof dep.depends_on_id === "string") out.push(dep.depends_on_id);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Reads and indexes the bead store.
|
|
138
|
+
*
|
|
139
|
+
* A missing file is an empty state (`present: false`), not an error: a
|
|
140
|
+
* workspace without beads is a perfectly normal workspace (REQ-046d). A single
|
|
141
|
+
* unparseable line is skipped and counted, so one bad line cannot hide the
|
|
142
|
+
* other ninety-nine (REQ-046e).
|
|
143
|
+
*/
|
|
144
|
+
export function readBeads(workspaceDirectory: string): ReadBeadsResult {
|
|
145
|
+
const path = beadsStorePath(workspaceDirectory);
|
|
146
|
+
const readAt = new Date().toISOString();
|
|
147
|
+
|
|
148
|
+
let stats: { mtimeMs: number; size: number };
|
|
149
|
+
try {
|
|
150
|
+
const stat = statSync(path);
|
|
151
|
+
stats = { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
154
|
+
return { beads: new Map(), skippedLines: 0, present: false, source: path, readAt };
|
|
155
|
+
}
|
|
156
|
+
throw unreadable(`cannot read ${path}`, error);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (stats.size > MAX_BEADS_FILE_BYTES) {
|
|
160
|
+
throw unreadable(
|
|
161
|
+
`${path} is ${stats.size} bytes, over the ${MAX_BEADS_FILE_BYTES}-byte limit this reader accepts`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cached = cache.get(path);
|
|
166
|
+
if (cached !== undefined && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
167
|
+
return { ...cached.result, readAt };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let body: string;
|
|
171
|
+
try {
|
|
172
|
+
body = readFileSync(path, "utf8");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw unreadable(`cannot read ${path}`, error);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const beads = new Map<string, BeadRecord>();
|
|
178
|
+
let skippedLines = 0;
|
|
179
|
+
for (const line of body.split("\n")) {
|
|
180
|
+
if (line.trim() === "") continue;
|
|
181
|
+
let parsed: unknown;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(line) as unknown;
|
|
184
|
+
} catch {
|
|
185
|
+
skippedLines += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
189
|
+
skippedLines += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const row = parsed as Record<string, unknown>;
|
|
193
|
+
const id = row["id"];
|
|
194
|
+
const status = row["status"];
|
|
195
|
+
if (typeof id !== "string" || id === "" || typeof status !== "string") {
|
|
196
|
+
skippedLines += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const record: BeadRecord = {
|
|
200
|
+
id,
|
|
201
|
+
title: typeof row["title"] === "string" ? (row["title"] as string) : null,
|
|
202
|
+
status,
|
|
203
|
+
issueType: typeof row["issue_type"] === "string" ? (row["issue_type"] as string) : "task",
|
|
204
|
+
updatedAt: typeof row["updated_at"] === "string" ? (row["updated_at"] as string) : "",
|
|
205
|
+
labels: asStringArray(row["labels"]),
|
|
206
|
+
blockedBy: blockedByOf(row["dependencies"]),
|
|
207
|
+
// `br` writes priority as a number; some exports write it as a string.
|
|
208
|
+
priority: Number.isFinite(Number(row["priority"])) && row["priority"] !== null && row["priority"] !== "" ? Number(row["priority"]) : null,
|
|
209
|
+
createdAt: text(row["created_at"]),
|
|
210
|
+
closedAt: text(row["closed_at"]),
|
|
211
|
+
closeReason: text(row["close_reason"]),
|
|
212
|
+
description: text(row["description"]),
|
|
213
|
+
parentId: parentOf(row["dependencies"]),
|
|
214
|
+
};
|
|
215
|
+
// Several lines can carry the same id; the latest `updated_at` wins.
|
|
216
|
+
const existing = beads.get(id);
|
|
217
|
+
if (existing === undefined || record.updatedAt >= existing.updatedAt) beads.set(id, record);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result: ReadBeadsResult = { beads, skippedLines, present: true, source: path, readAt };
|
|
221
|
+
cache.set(path, { ...stats, result });
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Startable right now, matching what `br ready` lists: open, every `blocks`
|
|
227
|
+
* dependency closed, and **not an epic**.
|
|
228
|
+
*
|
|
229
|
+
* Two rules here were derived by comparing against two real stores rather than
|
|
230
|
+
* from the field names:
|
|
231
|
+
*
|
|
232
|
+
* - **Epics are excluded.** They are containers; `br ready` never offers one as
|
|
233
|
+
* work. Counting them produced 12 ready where `br` reported 3.
|
|
234
|
+
* - **A dependency pointing at an id that is not in the store counts as NOT
|
|
235
|
+
* closed.** Treating an unknown edge as satisfied would advertise work as
|
|
236
|
+
* startable when nobody can tell whether it is.
|
|
237
|
+
*/
|
|
238
|
+
export function isReady(bead: BeadRecord, beads: Map<string, BeadRecord>): boolean {
|
|
239
|
+
if (bead.status === "closed" || bead.status === "in_progress") return false;
|
|
240
|
+
if (bead.issueType === "epic") return false;
|
|
241
|
+
return bead.blockedBy.every((id) => beads.get(id)?.status === "closed");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Counts for one workspace, shaped for the `beads.stats` RPC (REQ-046a).
|
|
246
|
+
*
|
|
247
|
+
* The buckets follow `br stats`, which was checked against two real stores
|
|
248
|
+
* while this was written, and they are not all statuses:
|
|
249
|
+
*
|
|
250
|
+
* - `total = open + inProgress + closed`;
|
|
251
|
+
* - **`ready` is derived**: an open non-epic bead whose every `blocks`
|
|
252
|
+
* dependency is closed;
|
|
253
|
+
* - **`blocked` is derived** as the rest of the open work (`open - ready`),
|
|
254
|
+
* which is why an open epic counts as blocked;
|
|
255
|
+
* - so `open = ready + blocked`.
|
|
256
|
+
*
|
|
257
|
+
* Reading `blocked` off a `status` field instead would report 0 on a store
|
|
258
|
+
* where `br` reports 22 — a number that looks fine and is wrong.
|
|
259
|
+
*/
|
|
260
|
+
export function beadStats(workspaceDirectory: string): BeadStats {
|
|
261
|
+
const { beads, skippedLines, present, source, readAt } = readBeads(workspaceDirectory);
|
|
262
|
+
let open = 0;
|
|
263
|
+
let inProgress = 0;
|
|
264
|
+
let blocked = 0;
|
|
265
|
+
let closed = 0;
|
|
266
|
+
let ready = 0;
|
|
267
|
+
for (const bead of beads.values()) {
|
|
268
|
+
switch (bead.status) {
|
|
269
|
+
case "closed":
|
|
270
|
+
closed += 1;
|
|
271
|
+
break;
|
|
272
|
+
case "in_progress":
|
|
273
|
+
inProgress += 1;
|
|
274
|
+
break;
|
|
275
|
+
case "blocked":
|
|
276
|
+
// `br` derives this bucket, but honour an explicit status too.
|
|
277
|
+
blocked += 1;
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
// Everything else is open work: `open` is the bucket `br` reports, and
|
|
281
|
+
// it splits into ready and blocked.
|
|
282
|
+
open += 1;
|
|
283
|
+
if (isReady(bead, beads)) ready += 1;
|
|
284
|
+
else blocked += 1;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
total: beads.size,
|
|
290
|
+
open,
|
|
291
|
+
inProgress,
|
|
292
|
+
blocked,
|
|
293
|
+
closed,
|
|
294
|
+
ready,
|
|
295
|
+
readAt,
|
|
296
|
+
source,
|
|
297
|
+
skippedLines,
|
|
298
|
+
present,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** One bead as the trace detail shows it (REQ-044a). */
|
|
303
|
+
export interface BeadLookup {
|
|
304
|
+
id: string;
|
|
305
|
+
title: string | null;
|
|
306
|
+
status: string;
|
|
307
|
+
/** `updated_at` from the store; "" when the store has none. */
|
|
308
|
+
updatedAt: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Looks up the beads a trace reported, by id.
|
|
313
|
+
*
|
|
314
|
+
* Ids that are no longer in the store come back in `missing` rather than
|
|
315
|
+
* throwing: a bead can legitimately be gone (another repo, renamed prefix,
|
|
316
|
+
* store not committed yet), and the trace must still render — the UI marks it
|
|
317
|
+
* "not in store" (REQ-044d).
|
|
318
|
+
*/
|
|
319
|
+
export function lookupBeads(
|
|
320
|
+
workspaceDirectory: string,
|
|
321
|
+
ids: readonly string[],
|
|
322
|
+
): { found: BeadLookup[]; missing: string[] } {
|
|
323
|
+
if (ids.length === 0) return { found: [], missing: [] };
|
|
324
|
+
let beads: Map<string, BeadRecord>;
|
|
325
|
+
try {
|
|
326
|
+
beads = readBeads(workspaceDirectory).beads;
|
|
327
|
+
} catch {
|
|
328
|
+
// An unreadable store must not break a trace; every id is simply unknown.
|
|
329
|
+
return { found: [], missing: [...ids] };
|
|
330
|
+
}
|
|
331
|
+
const found: BeadLookup[] = [];
|
|
332
|
+
const missing: string[] = [];
|
|
333
|
+
for (const id of ids) {
|
|
334
|
+
const bead = beads.get(id);
|
|
335
|
+
if (bead === undefined) {
|
|
336
|
+
missing.push(id);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
found.push({ id: bead.id, title: bead.title, status: bead.status, updatedAt: bead.updatedAt });
|
|
340
|
+
}
|
|
341
|
+
return { found, missing };
|
|
342
|
+
}
|