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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Questions waiting" pills above a Manager's chat (delta
|
|
3
|
+
* 20260918d-card-replies §4.8, REQ-059 j; owner decision Q15 b).
|
|
4
|
+
*
|
|
5
|
+
* One composer pill per Worker whose latest report asks the user something,
|
|
6
|
+
* next to Paseo's own pills. Pressing it opens a popover with that report's
|
|
7
|
+
* card, so the questions are answered there, through the card's own Reply
|
|
8
|
+
* path. Paseo 0.8 gives a plugin no way to scroll the chat to a card, which is
|
|
9
|
+
* why a popover stands in for scrolling.
|
|
10
|
+
*
|
|
11
|
+
* A Manager whose chat has fallback incidents waiting for the user's decision
|
|
12
|
+
* gets one more pill that counts them; its popover shows their cards, with
|
|
13
|
+
* the buttons (delta 20260921 §4.4.6).
|
|
14
|
+
*
|
|
15
|
+
* The list comes from `chat.waiting`, read at registration and every
|
|
16
|
+
* `WAITING_POLL_MS`. A failed read keeps the pills as they are.
|
|
17
|
+
*
|
|
18
|
+
* Each pill keeps ONE popover component for its whole life, and that component
|
|
19
|
+
* reads the pill's newest report (or incidents) from a small store: redrawing a
|
|
20
|
+
* pill after a refresh must not remount an open popover and lose a Reply being
|
|
21
|
+
* typed (delta 20260918f F13).
|
|
22
|
+
*/
|
|
23
|
+
import type {
|
|
24
|
+
PluginButton,
|
|
25
|
+
PluginButtonContentProps,
|
|
26
|
+
PluginButtonRegistration,
|
|
27
|
+
PluginClientContext,
|
|
28
|
+
} from "@getpaseo/plugin/client";
|
|
29
|
+
import { useSyncExternalStore, type ComponentType } from "react";
|
|
30
|
+
import { View } from "react-native";
|
|
31
|
+
import { chatWaitingRpc, type FallbackIncident, type WaitingWorker } from "../shared/contracts";
|
|
32
|
+
import { ChatCardView } from "./chat-card";
|
|
33
|
+
import { CHAT_CARD_KIND, CHAT_CARD_VERSION, fallbackCardOfIncident, toChatCard } from "./chat-cards";
|
|
34
|
+
import {
|
|
35
|
+
FALLBACK_PILL_ICON,
|
|
36
|
+
WAITING_PILL_ICON,
|
|
37
|
+
WAITING_POLL_MS,
|
|
38
|
+
pillChatOf,
|
|
39
|
+
planPills,
|
|
40
|
+
type ComposerPill,
|
|
41
|
+
} from "./waiting-pills-model";
|
|
42
|
+
|
|
43
|
+
/** The newest state of each shown pill, by pill id; one per loaded client bundle. */
|
|
44
|
+
const entries = new Map<string, ComposerPill>();
|
|
45
|
+
const listeners = new Set<() => void>();
|
|
46
|
+
|
|
47
|
+
function setEntry(pillId: string, pill: ComposerPill | undefined): void {
|
|
48
|
+
if (pill === undefined) entries.delete(pillId);
|
|
49
|
+
else entries.set(pillId, pill);
|
|
50
|
+
for (const listener of listeners) listener();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function subscribeEntries(listener: () => void): () => void {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
return () => {
|
|
56
|
+
listeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The report a pill's popover shows now, if the pill is shown. */
|
|
61
|
+
export function waitingEntryOf(pillId: string): WaitingWorker | undefined {
|
|
62
|
+
const pill = entries.get(pillId);
|
|
63
|
+
return pill?.kind === "questions" ? pill.entry : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The incidents a fallback pill's popover shows now, if the pill is shown. */
|
|
67
|
+
export function fallbackEntriesOf(pillId: string): FallbackIncident[] | undefined {
|
|
68
|
+
const pill = entries.get(pillId);
|
|
69
|
+
return pill?.kind === "fallback" ? pill.incidents : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The popover of one pill: the card of its newest report, or the card of each
|
|
74
|
+
* pending incident, whose buttons read and act on `fallback.incidents`. Made
|
|
75
|
+
* once per pill.
|
|
76
|
+
*/
|
|
77
|
+
function contentFor(pillId: string): ComponentType<PluginButtonContentProps> {
|
|
78
|
+
return function WaitingCard({ theme, layout, host }: PluginButtonContentProps) {
|
|
79
|
+
const read = () => entries.get(pillId);
|
|
80
|
+
const pill = useSyncExternalStore(subscribeEntries, read, read);
|
|
81
|
+
if (pill === undefined) return null;
|
|
82
|
+
if (pill.kind === "fallback") {
|
|
83
|
+
return (
|
|
84
|
+
<View style={{ gap: 8 }}>
|
|
85
|
+
{pill.incidents.map((incident) => (
|
|
86
|
+
<ChatCardView
|
|
87
|
+
key={incident.id}
|
|
88
|
+
theme={theme}
|
|
89
|
+
layout={layout}
|
|
90
|
+
host={host}
|
|
91
|
+
agentId={pill.managerId}
|
|
92
|
+
item={{ type: "plugin", kind: CHAT_CARD_KIND, version: CHAT_CARD_VERSION, data: fallbackCardOfIncident(incident) }}
|
|
93
|
+
timestamp={new Date(incident.detectedAt)}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const entry = pill.entry;
|
|
100
|
+
const card = toChatCard({ type: "user_message", text: entry.text }, "complete");
|
|
101
|
+
if (card === undefined) return null;
|
|
102
|
+
return (
|
|
103
|
+
<ChatCardView
|
|
104
|
+
theme={theme}
|
|
105
|
+
layout={layout}
|
|
106
|
+
host={host}
|
|
107
|
+
agentId={entry.managerId}
|
|
108
|
+
item={{ type: "plugin", kind: CHAT_CARD_KIND, version: CHAT_CARD_VERSION, data: card }}
|
|
109
|
+
timestamp={entry.at === null ? new Date() : new Date(entry.at)}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buttonOf(pill: ComposerPill, Content: ComponentType<PluginButtonContentProps>): PluginButton {
|
|
116
|
+
return {
|
|
117
|
+
title: pill.title,
|
|
118
|
+
label: pill.label,
|
|
119
|
+
icon: pill.kind === "fallback" ? FALLBACK_PILL_ICON : WAITING_PILL_ICON,
|
|
120
|
+
behavior: { kind: "popover", Content },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Starts the pills; returns the cleanup that stops reading and removes them all. */
|
|
125
|
+
export function registerWaitingPills(client: PluginClientContext): () => void {
|
|
126
|
+
const shown = new Map<
|
|
127
|
+
string,
|
|
128
|
+
{
|
|
129
|
+
key: string;
|
|
130
|
+
managerId: string;
|
|
131
|
+
workspaceId: string;
|
|
132
|
+
Content: ComponentType<PluginButtonContentProps>;
|
|
133
|
+
registration: PluginButtonRegistration;
|
|
134
|
+
}
|
|
135
|
+
>();
|
|
136
|
+
let pending = false;
|
|
137
|
+
let stopped = false;
|
|
138
|
+
|
|
139
|
+
const refresh = async () => {
|
|
140
|
+
if (pending || stopped) return;
|
|
141
|
+
pending = true;
|
|
142
|
+
try {
|
|
143
|
+
// `fallback` is absent from a server built before it.
|
|
144
|
+
const { waiting, fallback = [] } = await client.rpc(chatWaitingRpc, {});
|
|
145
|
+
if (stopped) return;
|
|
146
|
+
const plan = planPills(new Map([...shown].map(([id, pill]) => [id, pill.key])), waiting, fallback);
|
|
147
|
+
for (const id of plan.remove) {
|
|
148
|
+
shown.get(id)?.registration.remove();
|
|
149
|
+
shown.delete(id);
|
|
150
|
+
setEntry(id, undefined);
|
|
151
|
+
}
|
|
152
|
+
const added = [...plan.add];
|
|
153
|
+
for (const pill of plan.update) {
|
|
154
|
+
const current = shown.get(pill.id);
|
|
155
|
+
if (current === undefined) continue;
|
|
156
|
+
const chat = pillChatOf(pill);
|
|
157
|
+
// A registration cannot move to another chat: re-add it there instead.
|
|
158
|
+
if (current.managerId !== chat.managerId || current.workspaceId !== chat.workspaceId) {
|
|
159
|
+
current.registration.remove();
|
|
160
|
+
shown.delete(pill.id);
|
|
161
|
+
added.push(pill);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
setEntry(pill.id, pill);
|
|
165
|
+
current.registration.update(buttonOf(pill, current.Content));
|
|
166
|
+
current.key = pill.key;
|
|
167
|
+
}
|
|
168
|
+
for (const pill of added) {
|
|
169
|
+
const Content = contentFor(pill.id);
|
|
170
|
+
const chat = pillChatOf(pill);
|
|
171
|
+
setEntry(pill.id, pill);
|
|
172
|
+
const registration = client.addComposerPill({
|
|
173
|
+
id: pill.id,
|
|
174
|
+
workspaceId: chat.workspaceId,
|
|
175
|
+
agentId: chat.managerId,
|
|
176
|
+
button: buttonOf(pill, Content),
|
|
177
|
+
});
|
|
178
|
+
shown.set(pill.id, { key: pill.key, managerId: chat.managerId, workspaceId: chat.workspaceId, Content, registration });
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// A failed read keeps the pills as they are: better a pill one period
|
|
182
|
+
// late than pills that flicker whenever the daemon is slow.
|
|
183
|
+
} finally {
|
|
184
|
+
pending = false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
void refresh();
|
|
189
|
+
const timer = setInterval(() => void refresh(), WAITING_POLL_MS);
|
|
190
|
+
// Node (tests) would otherwise stay alive for the timer; hosts without unref ignore this.
|
|
191
|
+
(timer as { unref?: () => void }).unref?.();
|
|
192
|
+
return () => {
|
|
193
|
+
stopped = true;
|
|
194
|
+
clearInterval(timer);
|
|
195
|
+
for (const [id, pill] of shown) {
|
|
196
|
+
pill.registration.remove();
|
|
197
|
+
setEntry(id, undefined);
|
|
198
|
+
}
|
|
199
|
+
shown.clear();
|
|
200
|
+
};
|
|
201
|
+
}
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { PluginClientContext, PluginWorkspaceCommandContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import {
|
|
3
|
+
LAUNCHER_ICON,
|
|
4
|
+
LAUNCHER_SURFACE_ID,
|
|
5
|
+
selectFromCommandCenter,
|
|
6
|
+
} from "./client/launch-manager";
|
|
7
|
+
import { agentsStopAllRpc, managerEnsureRpc } from "./shared/contracts";
|
|
8
|
+
import { NEW_REQUEST_MARKER } from "./shared/new-request";
|
|
9
|
+
import { ManagerLauncherSurface } from "./client/launcher";
|
|
10
|
+
import { AGENT_TREE_ICON, AGENT_TREE_PANEL_ID } from "./client/agent-tree";
|
|
11
|
+
import { AgentTreePanel } from "./client/tree";
|
|
12
|
+
import { DASHBOARD_ICON } from "./client/dashboard-model";
|
|
13
|
+
import { BEADS_TAB_PANEL_ID, launcherNotices, selectDashboardFromCommandCenter } from "./client/dashboard-view";
|
|
14
|
+
import { DashboardSettingsScreen, SETTINGS_ICON, SETTINGS_SCREEN_ID } from "./client/settings";
|
|
15
|
+
import { CHAT_CARD_KIND, CHAT_CARD_VERSION, chatCardSchema, toChatCard } from "./client/chat-cards";
|
|
16
|
+
import { ChatCardView } from "./client/chat-card";
|
|
17
|
+
import { ChatBeadsPanel } from "./client/bead-chips";
|
|
18
|
+
import { BeadsTabPanel } from "./client/beads-tab";
|
|
19
|
+
import { registerBeadsHeaderButtons } from "./client/beads-header-button";
|
|
20
|
+
import { registerWaitingPills } from "./client/waiting-pills";
|
|
21
|
+
|
|
22
|
+
/** A chat item as a paseo-bm card, or nothing (the item stays Paseo's). */
|
|
23
|
+
function chatCardItems(item: { type: string }, phase: "streaming" | "complete") {
|
|
24
|
+
const card = toChatCard(item, phase);
|
|
25
|
+
return card === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: { items: [{ type: "plugin" as const, kind: CHAT_CARD_KIND, version: CHAT_CARD_VERSION, data: card }] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Slash commands (delta 20260917e §4.4).
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** What Paseo hands a slash command: the workspace context plus the typed text. */
|
|
35
|
+
type WorkspaceCommand = PluginWorkspaceCommandContext & { args: string };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `/bm-worker-new <request>`.
|
|
39
|
+
*
|
|
40
|
+
* A typing shortcut, not a second way to create a Worker (owner decision Q24):
|
|
41
|
+
* the request goes to the Manager, and the Manager keeps doing what it always
|
|
42
|
+
* does — mint the `requestId`, size the work, create the Worker, report back.
|
|
43
|
+
* Creating a Worker here would leave those steps to nobody.
|
|
44
|
+
*
|
|
45
|
+
* `paseo.agents.ref().send()` is the path the app's own composer uses, so the
|
|
46
|
+
* request is recorded as a real user message (verified fact P2) and none of the
|
|
47
|
+
* three role prompts has to learn about this command.
|
|
48
|
+
*/
|
|
49
|
+
async function runWorkerNew(context: WorkspaceCommand): Promise<void> {
|
|
50
|
+
const request = context.args.trim();
|
|
51
|
+
// `manager.ensure` is the same call the launcher button makes: it finds this
|
|
52
|
+
// workspace's live Manager or starts one, and de-duplicates server-side.
|
|
53
|
+
const { agentId } = await context.rpc(managerEnsureRpc, { workspaceId: context.workspace.id });
|
|
54
|
+
// Verbatim: anything we prefixed would be quoted back at the user as their
|
|
55
|
+
// own words, and would land in the trace store as part of the request. No
|
|
56
|
+
// args sends nothing at all, because an empty message would force the Manager
|
|
57
|
+
// to invent a request, which its own rules forbid.
|
|
58
|
+
// The flag line tells the Manager this is NEW work, not a follow-up. Without
|
|
59
|
+
// it the Manager applies its own rule -- a message while a request is live is
|
|
60
|
+
// a follow-up -- and hands the words to the Worker already busy, which is the
|
|
61
|
+
// opposite of what this command is for (delta 20260917f §2 lỗi A).
|
|
62
|
+
if (request !== "") {
|
|
63
|
+
await context.paseo.agents.ref(agentId).send(`${NEW_REQUEST_MARKER}\n${request}`);
|
|
64
|
+
}
|
|
65
|
+
// Either way the chat opens, so the user can keep typing into it. That goes
|
|
66
|
+
// through the Command Center hand-off, because a command context has no
|
|
67
|
+
// `navigation.openAgent` — only a surface does. The surface calls
|
|
68
|
+
// `manager.ensure` a second time on the way; that call is idempotent and
|
|
69
|
+
// returns the Manager this one just ensured.
|
|
70
|
+
selectFromCommandCenter(context);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** English plural for a count: `1 Worker`, `0 Workers`, `2 Workers`. */
|
|
74
|
+
function counted(value: number, noun: string): string {
|
|
75
|
+
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What the user is told after `/bm-worker-stop-all`.
|
|
80
|
+
*
|
|
81
|
+
* Every word is weighed against verified fact P4: Paseo gives a plugin no way
|
|
82
|
+
* to cancel an agent, so the command delivers a notice and nothing more.
|
|
83
|
+
* "Asked … to stop" is what happened; "stopped" would be a promise the product
|
|
84
|
+
* cannot keep, and a user who believes it will not go back and check.
|
|
85
|
+
*/
|
|
86
|
+
function stopAllSummary(result: { workers: number; reviewers: number; skipped: number }): string {
|
|
87
|
+
return (
|
|
88
|
+
`Asked ${counted(result.workers, "Worker")} and ${counted(result.reviewers, "Reviewer")} ` +
|
|
89
|
+
`in this workspace to stop; skipped ${counted(result.skipped, "agent")} (archived, or not running). ` +
|
|
90
|
+
`Paseo cannot cancel an agent from a plugin, so each one stops itself once it reads the notice.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `/bm-worker-stop-all`. The server picks the targets and sends the notices;
|
|
96
|
+
* this only reports the counts it answers with.
|
|
97
|
+
*/
|
|
98
|
+
async function runWorkerStopAll(context: WorkspaceCommand): Promise<void> {
|
|
99
|
+
const result = await context.rpc(agentsStopAllRpc, { workspaceId: context.workspace.id });
|
|
100
|
+
// Paseo 0.8 gives a slash command no report channel: the only thing the
|
|
101
|
+
// composer ever shows is a REJECTION, turned into an error toast. Reporting a
|
|
102
|
+
// success that way would paint "Asked 2 Workers to stop" in the colour of a
|
|
103
|
+
// failure. So the summary is posted to the Beads Manager surface, which draws
|
|
104
|
+
// it as an ordinary notice. Staying silent is not an option either: the user
|
|
105
|
+
// must learn the agents were ASKED, not killed (P4).
|
|
106
|
+
launcherNotices.put(stopAllSummary(result));
|
|
107
|
+
context.openSurface(LAUNCHER_SURFACE_ID);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Client entry of the paseo-bm plugin (design §2.4).
|
|
112
|
+
*
|
|
113
|
+
* WP-211 adds the Dashboard to that same surface (Q-036): a "Dashboard" button
|
|
114
|
+
* per workspace, a workspace Command Center item that queues a workspace and
|
|
115
|
+
* opens the surface, and a plugin settings screen for the trace-store warning
|
|
116
|
+
* threshold.
|
|
117
|
+
*
|
|
118
|
+
* WP-113 registers the "Beads Manager" surface, its sidebar item and the
|
|
119
|
+
* workspace Command Center item; both open the Manager through
|
|
120
|
+
* `manager.ensure`. It also registers the read-only "Beads agents" workspace
|
|
121
|
+
* panel (agent tree + role configuration).
|
|
122
|
+
*
|
|
123
|
+
* It also turns messages between the Manager, Workers and Reviewers into chat
|
|
124
|
+
* cards (delta 20260916-chat-cards), registers the two slash commands of
|
|
125
|
+
* delta 20260917e §4.4, and adds the "Beads" tab that Paseo lists in the "+"
|
|
126
|
+
* menu of every workspace (delta 20260918e §4.1).
|
|
127
|
+
*
|
|
128
|
+
* This entry must never import from `server/`: that is a compile error.
|
|
129
|
+
*
|
|
130
|
+
* Declared as a hoisted `export default function`, not `const` + `export
|
|
131
|
+
* default`: Paseo 0.8 rewrites esbuild's export getters into eager copies
|
|
132
|
+
* (makeHermesInteropEager), so a late-bound default export is copied while
|
|
133
|
+
* still undefined and the daemon refuses the plugin ("must default export a
|
|
134
|
+
* function"). Guarded by test/plugin-bundle-cjs.test.ts.
|
|
135
|
+
*/
|
|
136
|
+
export default function contribute(client: PluginClientContext): () => void {
|
|
137
|
+
// Register the surface before the sidebar item that points at it.
|
|
138
|
+
const removers = [
|
|
139
|
+
client.addSurface(LAUNCHER_SURFACE_ID, ManagerLauncherSurface),
|
|
140
|
+
client.addSidebarItem({
|
|
141
|
+
id: LAUNCHER_SURFACE_ID,
|
|
142
|
+
title: "Beads Manager",
|
|
143
|
+
icon: LAUNCHER_ICON,
|
|
144
|
+
surface: LAUNCHER_SURFACE_ID,
|
|
145
|
+
}),
|
|
146
|
+
client.addCommandCenterItem({
|
|
147
|
+
id: "open-beads-manager",
|
|
148
|
+
title: "Open Beads Manager",
|
|
149
|
+
icon: LAUNCHER_ICON,
|
|
150
|
+
keywords: ["beads", "manager", "paseo-bm"],
|
|
151
|
+
context: "workspace",
|
|
152
|
+
onSelect: (context) => selectFromCommandCenter(context),
|
|
153
|
+
}),
|
|
154
|
+
client.addCommandCenterItem({
|
|
155
|
+
id: "open-beads-dashboard",
|
|
156
|
+
title: "Open Beads Metric",
|
|
157
|
+
icon: DASHBOARD_ICON,
|
|
158
|
+
keywords: ["beads", "metric", "dashboard", "traces", "paseo-bm"],
|
|
159
|
+
context: "workspace",
|
|
160
|
+
onSelect: (context) => selectDashboardFromCommandCenter(context, LAUNCHER_SURFACE_ID),
|
|
161
|
+
}),
|
|
162
|
+
client.addSlashCommand({
|
|
163
|
+
name: "bm-worker-new",
|
|
164
|
+
description: "Give Beads Manager a new request; it creates the Worker",
|
|
165
|
+
argumentHint: "<what you want done>",
|
|
166
|
+
context: "workspace",
|
|
167
|
+
onSubmit: (context) => runWorkerNew(context),
|
|
168
|
+
}),
|
|
169
|
+
client.addSlashCommand({
|
|
170
|
+
name: "bm-worker-stop-all",
|
|
171
|
+
description: "Ask every running Beads Worker and Reviewer in this workspace to stop",
|
|
172
|
+
argumentHint: "",
|
|
173
|
+
context: "workspace",
|
|
174
|
+
onSubmit: (context) => runWorkerStopAll(context),
|
|
175
|
+
}),
|
|
176
|
+
client.addSettingsScreen({
|
|
177
|
+
id: SETTINGS_SCREEN_ID,
|
|
178
|
+
title: "Beads Dashboard",
|
|
179
|
+
icon: SETTINGS_ICON,
|
|
180
|
+
Component: DashboardSettingsScreen,
|
|
181
|
+
}),
|
|
182
|
+
// Messages between the Manager, Workers and Reviewers as cards
|
|
183
|
+
// (delta 20260916-chat-cards). Only paseo-bm's own messages are changed.
|
|
184
|
+
client.addTimelineTransformer({
|
|
185
|
+
id: "bm-chat-received",
|
|
186
|
+
query: { itemType: "user_message" },
|
|
187
|
+
transform: ({ item, phase }) => chatCardItems(item, phase),
|
|
188
|
+
}),
|
|
189
|
+
client.addTimelineTransformer({
|
|
190
|
+
id: "bm-chat-sent",
|
|
191
|
+
query: { itemType: "assistant_message" },
|
|
192
|
+
transform: ({ item, phase }) => chatCardItems(item, phase),
|
|
193
|
+
}),
|
|
194
|
+
client.addTimelineRenderer({
|
|
195
|
+
kind: CHAT_CARD_KIND,
|
|
196
|
+
version: CHAT_CARD_VERSION,
|
|
197
|
+
schema: chatCardSchema,
|
|
198
|
+
Component: ChatCardView,
|
|
199
|
+
}),
|
|
200
|
+
client.addWorkspacePanel({
|
|
201
|
+
id: "bm-chat-beads",
|
|
202
|
+
title: "Beads in this chat",
|
|
203
|
+
icon: "ListChecks",
|
|
204
|
+
context: "agent",
|
|
205
|
+
Component: ChatBeadsPanel,
|
|
206
|
+
}),
|
|
207
|
+
// Before "Beads agents", so it leads paseo-bm's entries in the "+" menu.
|
|
208
|
+
// No `locations`: Paseo's default, ["workspace"], is the tab bar.
|
|
209
|
+
client.addWorkspacePanel({
|
|
210
|
+
id: BEADS_TAB_PANEL_ID,
|
|
211
|
+
title: "Beads",
|
|
212
|
+
icon: "ListChecks",
|
|
213
|
+
context: "workspace",
|
|
214
|
+
Component: BeadsTabPanel,
|
|
215
|
+
}),
|
|
216
|
+
// The same tab from a button on every workspace header: Paseo 0.8's mobile
|
|
217
|
+
// app has no "+" menu (delta 20260918e §4.6).
|
|
218
|
+
registerBeadsHeaderButtons(client),
|
|
219
|
+
client.addWorkspacePanel({
|
|
220
|
+
id: AGENT_TREE_PANEL_ID,
|
|
221
|
+
title: "Beads agents",
|
|
222
|
+
icon: AGENT_TREE_ICON,
|
|
223
|
+
context: "workspace",
|
|
224
|
+
Component: AgentTreePanel,
|
|
225
|
+
}),
|
|
226
|
+
// A pill per Worker waiting for the user's answer (delta 20260918d §4.8).
|
|
227
|
+
registerWaitingPills(client),
|
|
228
|
+
];
|
|
229
|
+
return () => {
|
|
230
|
+
for (const remove of removers) void remove();
|
|
231
|
+
};
|
|
232
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { MANAGER_INSTRUCTIONS } from "./server/manager-instructions";
|
|
3
|
+
import { ensureManager, listWorkspaceAgents } from "./server/manager";
|
|
4
|
+
import { registerCollector } from "./server/collector";
|
|
5
|
+
import { checkReviewBudget, type BudgetOverrun, type BudgetPaseo } from "./server/review-budget";
|
|
6
|
+
import { registerDashboardRpcs } from "./server/dashboard-rpc";
|
|
7
|
+
import { registerRoleHook } from "./server/role-hook";
|
|
8
|
+
import { describeRoles } from "./server/roles";
|
|
9
|
+
import { registerStopPropagation } from "./server/stop-propagation";
|
|
10
|
+
import { registerAgentLabels } from "./server/agent-labels";
|
|
11
|
+
import { registerFallbackRpcs } from "./server/fallback-rpc";
|
|
12
|
+
import { registerFallbackDetection, replacementsFor } from "./server/fallback-state";
|
|
13
|
+
import { createManagerSwitch } from "./server/fallback-manager";
|
|
14
|
+
import { createReviewerResend, createReviewerSwitch } from "./server/fallback-reviewer";
|
|
15
|
+
import type { FallbackAction } from "./server/fallback-rpc";
|
|
16
|
+
import { createWorkerSwitch } from "./server/fallback-switch";
|
|
17
|
+
import { createFallbackWaiter } from "./server/fallback-wait";
|
|
18
|
+
import { registerFormatCheck } from "./server/format-check";
|
|
19
|
+
import { registerNoticeQueue } from "./server/notice-queue";
|
|
20
|
+
import { currentInstructions } from "./server/role-extras";
|
|
21
|
+
import { registerSetupRpcs } from "./server/setup-rpc";
|
|
22
|
+
import { registerRoleSettingsRpcs } from "./server/role-settings-rpc";
|
|
23
|
+
import { registerChatRpcs } from "./server/chat-rpc";
|
|
24
|
+
import { agentsListRpc, managerEnsureRpc, rolesDescribeRpc, type FallbackIncident } from "./shared/contracts";
|
|
25
|
+
import { dashboardSettings } from "./shared/settings";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Text of `roles/manager.md`.
|
|
29
|
+
*
|
|
30
|
+
* Embedded at build time (scripts/generate-role-instructions.mjs), never read
|
|
31
|
+
* from disk: Paseo 0.8 compiles this entry into a CommonJS bundle and runs it
|
|
32
|
+
* in a forked worker without a cwd, so this code has no way to know where its
|
|
33
|
+
* payload lives (bm-dnc). Nothing in the server entry may depend on file
|
|
34
|
+
* locations at run time.
|
|
35
|
+
*/
|
|
36
|
+
export function readManagerInstructions(): Promise<string> {
|
|
37
|
+
return Promise.resolve(MANAGER_INSTRUCTIONS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Server entry of the paseo-bm plugin.
|
|
42
|
+
*
|
|
43
|
+
* WP-112 registers `manager.ensure`, `agents.list` and `roles.describe`. All
|
|
44
|
+
* three only read or create; there is deliberately no RPC that deletes or archives agents:
|
|
45
|
+
* their lifecycle belongs to the user (ADR-005).
|
|
46
|
+
*
|
|
47
|
+
* WP-208 and WP-210 add the Dashboard RPCs (`beads.stats`, `traces.delete`);
|
|
48
|
+
* `traces.delete` is the only handler in the plugin that removes anything, and
|
|
49
|
+
* it only ever removes paseo-bm's own trace records.
|
|
50
|
+
*
|
|
51
|
+
* WP-205 adds the trace collector: `agent.turn_started` and `agent.turn_ended`
|
|
52
|
+
* hooks that write one record per `bm-*` agent turn into the trace store, so the
|
|
53
|
+
* Dashboard has history that outlives the agents themselves (ADR-007). It
|
|
54
|
+
* resolves the store itself from the hook context and can never throw into an
|
|
55
|
+
* agent turn.
|
|
56
|
+
*
|
|
57
|
+
* It also registers a `before("agent.create")` hook that puts the role
|
|
58
|
+
* instructions into the system prompt of every `bm-manager`, `bm-worker` and
|
|
59
|
+
* `bm-reviewer` agent, whoever creates it: Paseo's `create_agent` tool has no
|
|
60
|
+
* system-prompt parameter (bm-hld). It also registers an `on("agent.turn_ended")`
|
|
61
|
+
* hook that sends a stop notice to the running Reviewers of a Worker the user
|
|
62
|
+
* stopped (bm-wq6, REQ-026f). The returned cleanup removes every hook.
|
|
63
|
+
*
|
|
64
|
+
* This entry must never import from `client/`: that is a compile error.
|
|
65
|
+
*
|
|
66
|
+
* Declared as a hoisted `export default function`, not `const` + `export
|
|
67
|
+
* default`: Paseo 0.8 rewrites esbuild's export getters into eager copies
|
|
68
|
+
* (makeHermesInteropEager), so a late-bound default export is copied while
|
|
69
|
+
* still undefined and the daemon refuses the plugin ("must default export a
|
|
70
|
+
* function"). Guarded by test/plugin-bundle-cjs.test.ts.
|
|
71
|
+
*/
|
|
72
|
+
export default function contribute(server: PluginServerContext): () => void {
|
|
73
|
+
server.handle(managerEnsureRpc, async (input, { paseo }) => {
|
|
74
|
+
const result = await ensureManager(input, {
|
|
75
|
+
paseo,
|
|
76
|
+
// The base plus the user's additions from the Setup screen, when any.
|
|
77
|
+
readInstructions: () => currentInstructions("manager", paseo),
|
|
78
|
+
});
|
|
79
|
+
if (result.otherManagerIds.length > 0) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`[paseo-bm] workspace ${input.workspaceId} has ${result.otherManagerIds.length + 1} live Managers; using ${result.agentId} (labelled Managers first, then the newest). Left untouched: ${result.otherManagerIds.join(", ")}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (result.modeNotice !== null) console.warn(`[paseo-bm] ${result.modeNotice}`);
|
|
85
|
+
if (result.toolsNotice !== null) console.warn(`[paseo-bm] ${result.toolsNotice}`);
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
// Without this the Dashboard's settings screen cannot read or save anything:
|
|
89
|
+
// `useSettings` on the client calls `settings.paseo-bm.read`, an RPC the HOST
|
|
90
|
+
// only contributes once the server has declared the definition. The owner's
|
|
91
|
+
// daemon log carried eight `Plugin paseo-bm does not contribute RPC
|
|
92
|
+
// settings.paseo-bm.read` errors because this one line was missing
|
|
93
|
+
// (bm-settings-rpc-stgv).
|
|
94
|
+
server.registerSettings(dashboardSettings);
|
|
95
|
+
server.handle(agentsListRpc, async (input, { paseo }) => listWorkspaceAgents(input, { paseo, replacements: await replacementsFor(paseo) }));
|
|
96
|
+
server.handle(rolesDescribeRpc, (_input, { paseo }) => describeRoles({ paseo }));
|
|
97
|
+
registerDashboardRpcs(server, {
|
|
98
|
+
ensureManager: async (workspaceId, paseo) => {
|
|
99
|
+
const result = await ensureManager({ workspaceId }, { paseo: paseo as never, readInstructions: () => currentInstructions("manager", paseo) });
|
|
100
|
+
// This path shows no launcher notice, so the log is the only place a mode problem surfaces.
|
|
101
|
+
if (result.modeNotice !== null) console.warn(`[paseo-bm] ${result.modeNotice}`);
|
|
102
|
+
if (result.toolsNotice !== null) console.warn(`[paseo-bm] ${result.toolsNotice}`);
|
|
103
|
+
return result;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
registerSetupRpcs(server);
|
|
107
|
+
// delta 20260921 §4.3.2: the read-only data of Roles & models (`roles.settings`, `roles.options`).
|
|
108
|
+
registerRoleSettingsRpcs(server);
|
|
109
|
+
registerChatRpcs(server);
|
|
110
|
+
const removeRoleHook = registerRoleHook(server);
|
|
111
|
+
const removeStopPropagation = registerStopPropagation(server);
|
|
112
|
+
// delta 20260918g §4.5: a bm-* agent created without its bm.role label gets it.
|
|
113
|
+
const removeAgentLabels = registerAgentLabels(server);
|
|
114
|
+
// delta 20260921 §4.2.4 (F13): a plugin notice to an agent that may be running
|
|
115
|
+
// (BM-TOOLS, BM-SETTINGS, BM-FALLBACK) waits in memory for that agent's next
|
|
116
|
+
// turn end. The queue adds no hook of its own: it rides on the BM-FORMAT
|
|
117
|
+
// check's agent.turn_ended hook and runs after it, so a BM-FORMAT notice sent
|
|
118
|
+
// at the same turn end is never replaced by a queued one.
|
|
119
|
+
const noticeQueue = registerNoticeQueue(server);
|
|
120
|
+
// delta 20260918g §4.7: the sender of a BM-* block that breaks its template is told (BM-FORMAT).
|
|
121
|
+
const removeFormatCheck = registerFormatCheck(noticeQueue.host);
|
|
122
|
+
// delta 20260917c §4.7: the plugin counts the review budget and tells the
|
|
123
|
+
// Manager once per request; it never stops an agent.
|
|
124
|
+
const budgetTold = new Set<string>();
|
|
125
|
+
// Only an overrun found at a Worker's or Reviewer's turn end goes in here, and
|
|
126
|
+
// only what is in here may be sent at a Manager's turn end (review b2).
|
|
127
|
+
const budgetPending = new Map<string, BudgetOverrun>();
|
|
128
|
+
// delta 20260921 §4.4.4–§4.4.5: a Worker turn that ends on a provider-plan
|
|
129
|
+
// failure is classified and recorded as a fallback incident. Its own handler,
|
|
130
|
+
// not the collector's onRecorded, so detection never depends on the trace store.
|
|
131
|
+
// §4.4.9 (owner decision Q6 a): one timer per Wait click, set again after a
|
|
132
|
+
// reload at the first hook or fallback RPC that brings an SDK handle.
|
|
133
|
+
const fallbackWaiter = createFallbackWaiter();
|
|
134
|
+
const armWaits = (paseo: unknown) => void fallbackWaiter.ensureArmed(paseo);
|
|
135
|
+
const removeFallbackDetection = registerFallbackDetection(server, { onPaseo: armWaits });
|
|
136
|
+
// delta 20260921 §4.4.6: BM-FALLBACK to the Manager chat, fallback.incidents and fallback.act.
|
|
137
|
+
// "Switch": the plugin creates a replacement Worker (§4.4.7) or Manager
|
|
138
|
+
// (§4.5.2); for a Reviewer it tells the Worker how to create the
|
|
139
|
+
// replacement itself (§4.5.1).
|
|
140
|
+
const switches: Record<FallbackIncident["role"], FallbackAction> = {
|
|
141
|
+
worker: createWorkerSwitch(),
|
|
142
|
+
reviewer: createReviewerSwitch(),
|
|
143
|
+
manager: createManagerSwitch(),
|
|
144
|
+
};
|
|
145
|
+
const switchByRole: FallbackAction = (incident, paseo, deps) => switches[incident.role](incident, paseo, deps);
|
|
146
|
+
const removeFallbackNotices = registerFallbackRpcs(
|
|
147
|
+
server,
|
|
148
|
+
{ switch: switchByRole, wait: fallbackWaiter.wait, resend: createReviewerResend() },
|
|
149
|
+
{ onPaseo: armWaits },
|
|
150
|
+
);
|
|
151
|
+
const removeCollector = registerCollector(server, {
|
|
152
|
+
onRecorded: (event, { location, paseo }) =>
|
|
153
|
+
paseo === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: checkReviewBudget(event, { location, paseo: paseo as BudgetPaseo, told: budgetTold, pending: budgetPending }),
|
|
156
|
+
});
|
|
157
|
+
return () => {
|
|
158
|
+
removeRoleHook();
|
|
159
|
+
removeStopPropagation();
|
|
160
|
+
removeAgentLabels();
|
|
161
|
+
removeFormatCheck();
|
|
162
|
+
noticeQueue.remove();
|
|
163
|
+
removeFallbackDetection();
|
|
164
|
+
removeFallbackNotices();
|
|
165
|
+
fallbackWaiter.clear();
|
|
166
|
+
removeCollector();
|
|
167
|
+
};
|
|
168
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paseo-bm-plugin",
|
|
3
|
+
"version": "0.0.0-placeholder.0",
|
|
4
|
+
"description": "Beads Management for Paseo — the plugin payload: the Metric, Beads and Setup screens plus the agent role instructions. Install with npx paseo-bm, which also registers the three agent roles.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hieunt286/paseo-bm.git",
|
|
10
|
+
"directory": "plugin"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"paseo-plugin.json",
|
|
17
|
+
"index.client.tsx",
|
|
18
|
+
"index.server.ts",
|
|
19
|
+
"client/",
|
|
20
|
+
"server/",
|
|
21
|
+
"shared/",
|
|
22
|
+
"roles/",
|
|
23
|
+
"tsconfig.json",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"paseo",
|
|
32
|
+
"paseo-plugin",
|
|
33
|
+
"beads"
|
|
34
|
+
]
|
|
35
|
+
}
|