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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which start mode each paseo-bm role gets, and how it is chosen.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the `agent.create` hook (which sets `config.modeId`) and by the
|
|
5
|
+
* role instructions (which tell the Manager the Worker mode to pass), so both
|
|
6
|
+
* name the same value. Kept apart from both so neither has to import the other.
|
|
7
|
+
*
|
|
8
|
+
* The rule is owner decision bm-msy (2026-09-15), which the role files used to
|
|
9
|
+
* teach in prose. Paseo resolves a new agent's mode BEFORE the hook runs and
|
|
10
|
+
* refuses a cross-provider child whose creator is not unattended and passes no
|
|
11
|
+
* mode (delta 20260917c K10), so the hook can correct a mode but cannot rescue
|
|
12
|
+
* a creation the daemon already refused: that is why the Manager still passes
|
|
13
|
+
* one, taken from its Runtime facts.
|
|
14
|
+
*
|
|
15
|
+
* Every lookup here is on the path of a real agent creation, and the plugin
|
|
16
|
+
* host rejects a `before` hook after 30 s and then FAILS the creation (read in
|
|
17
|
+
* the installed Paseo.app bundle), while the SDK's own `listModes` timeout is
|
|
18
|
+
* 90 s and the daemon waits for provider warm-up. So each lookup is raced
|
|
19
|
+
* against `LOOKUP_TIMEOUT_MS` and a slow answer costs the mode, never the
|
|
20
|
+
* agent.
|
|
21
|
+
*/
|
|
22
|
+
import type { Role } from "./role-extras";
|
|
23
|
+
|
|
24
|
+
/** A mode as `providers.listModes` returns it (`AgentMode` in @getpaseo/protocol). */
|
|
25
|
+
export type ProviderMode = { id: string; label?: string; description?: string; colorTier?: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Roles whose start mode the hook chooses (delta 20260917c §4.6). The Manager
|
|
29
|
+
* is left out on purpose: `manager.ensure` already sets its mode, and two
|
|
30
|
+
* places deciding one value is worse than one.
|
|
31
|
+
*/
|
|
32
|
+
export const ROLE_GETS_MODE: Readonly<Record<Role, boolean>> = { manager: false, worker: true, reviewer: true };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How long the hook waits for one lookup. Well under the host's 30 s hook
|
|
36
|
+
* budget, and generous for a local daemon RPC.
|
|
37
|
+
*/
|
|
38
|
+
export const LOOKUP_TIMEOUT_MS = 5000;
|
|
39
|
+
|
|
40
|
+
/** Returned instead of the work's value when it took longer than `ms`. A
|
|
41
|
+
* sentinel, not `null`: a lookup may legitimately answer `null`. */
|
|
42
|
+
export const TIMED_OUT = Symbol("paseo-bm lookup timed out");
|
|
43
|
+
|
|
44
|
+
/** The work's result, or `TIMED_OUT`. */
|
|
45
|
+
export async function withTimeout<T>(work: Promise<T>, ms: number = LOOKUP_TIMEOUT_MS): Promise<T | typeof TIMED_OUT> {
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
try {
|
|
48
|
+
return await Promise.race([
|
|
49
|
+
work,
|
|
50
|
+
new Promise<typeof TIMED_OUT>((resolve) => {
|
|
51
|
+
timer = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
} finally {
|
|
56
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tierOf = (mode: ProviderMode): string => (typeof mode.colorTier === "string" ? mode.colorTier.toLowerCase() : "");
|
|
61
|
+
const firstWithTier = (modes: readonly ProviderMode[], tier: string): string | undefined =>
|
|
62
|
+
modes.find((mode) => tierOf(mode) === tier)?.id;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The mode a paseo-bm role should start in, or `undefined` to leave the request
|
|
66
|
+
* alone.
|
|
67
|
+
*
|
|
68
|
+
* - Worker: the first `dangerous` mode, i.e. the one that runs without approval
|
|
69
|
+
* prompts (`bypassPermissions` on Claude, `full-access` on Codex); else the
|
|
70
|
+
* first `moderate`; else the first `safe`. Never `planning`. A mode the caller
|
|
71
|
+
* (or the profile) already chose is kept.
|
|
72
|
+
* Before either chain: a `modeId` set by hand on the role's own profile wins
|
|
73
|
+
* (bm-msy, "the profile's mode if it has one"), except that a dangerous or
|
|
74
|
+
* planning profile mode is not used for the Reviewer.
|
|
75
|
+
*
|
|
76
|
+
* - Reviewer: `auto` when present; else the first `moderate` whose id, label and
|
|
77
|
+
* description mention neither "full" nor "network"; else the first `safe`. Never
|
|
78
|
+
* `dangerous` or `planning`: a caller-chosen mode of either tier is
|
|
79
|
+
* downgraded, any other caller choice is kept, and a mode id the provider does
|
|
80
|
+
* not list is kept rather than guessed about.
|
|
81
|
+
*/
|
|
82
|
+
export function chooseModeId(
|
|
83
|
+
role: Role,
|
|
84
|
+
modes: readonly ProviderMode[],
|
|
85
|
+
current: string | undefined,
|
|
86
|
+
profileModeId?: string | null,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
if (!ROLE_GETS_MODE[role]) return undefined;
|
|
89
|
+
const usable = modes.filter((mode) => typeof mode?.id === "string" && mode.id !== "");
|
|
90
|
+
if (usable.length === 0) return undefined;
|
|
91
|
+
// "The profile's mode if it has one" is part of the bm-msy rule: a mode the
|
|
92
|
+
// owner set on the bm-worker / bm-reviewer profile by hand wins over the
|
|
93
|
+
// plugin's own pick. For the Reviewer it still may not be dangerous.
|
|
94
|
+
const fromProfile = usable.find((mode) => mode.id === profileModeId);
|
|
95
|
+
const profileSafe = fromProfile !== undefined && tierOf(fromProfile) !== "dangerous" && tierOf(fromProfile) !== "planning";
|
|
96
|
+
|
|
97
|
+
if (role === "worker") {
|
|
98
|
+
if (current !== undefined) return undefined;
|
|
99
|
+
if (fromProfile !== undefined) return fromProfile.id;
|
|
100
|
+
return firstWithTier(usable, "dangerous") ?? firstWithTier(usable, "moderate") ?? firstWithTier(usable, "safe");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const reviewerMode = profileSafe
|
|
104
|
+
? fromProfile!.id
|
|
105
|
+
: usable.find((mode) => mode.id === "auto")?.id ??
|
|
106
|
+
usable.find((mode) => tierOf(mode) === "moderate" && !/full|network/i.test(`${mode.id} ${mode.label ?? ""} ${mode.description ?? ""}`))?.id ??
|
|
107
|
+
firstWithTier(usable, "safe");
|
|
108
|
+
if (current === undefined) return reviewerMode;
|
|
109
|
+
const chosen = usable.find((mode) => mode.id === current);
|
|
110
|
+
if (chosen === undefined) return undefined;
|
|
111
|
+
return tierOf(chosen) === "dangerous" || tierOf(chosen) === "planning" ? reviewerMode : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The mode `manager.ensure` gives a Manager, or `undefined` to leave the
|
|
116
|
+
* provider's default (delta 20260918 §4.1, owner decisions Q31 and Q36).
|
|
117
|
+
*
|
|
118
|
+
* 1. A mode set by hand on the `bm-manager` profile, when the provider lists
|
|
119
|
+
* it, always wins — even a `planning` one: the owner's rule is that the
|
|
120
|
+
* profile's own mode still wins.
|
|
121
|
+
* 2. Otherwise the first `dangerous` mode, the one that runs without permission
|
|
122
|
+
* prompts (`bypassPermissions` on Claude, `full-access` on Codex).
|
|
123
|
+
* 3. Otherwise nothing. There is deliberately no fallback to `moderate`: the
|
|
124
|
+
* request is "no prompts", and a provider's automatic mode is not that.
|
|
125
|
+
*/
|
|
126
|
+
export function managerModeFor(modes: readonly ProviderMode[], profileModeId: string | null): string | undefined {
|
|
127
|
+
const usable = modes.filter((mode) => typeof mode?.id === "string" && mode.id !== "");
|
|
128
|
+
const fromProfile = usable.find((mode) => mode.id === profileModeId);
|
|
129
|
+
if (fromProfile !== undefined) return fromProfile.id;
|
|
130
|
+
return firstWithTier(usable, "dangerous");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What a paseo-bm profile sets by hand (delta 20260921 §4.1.1). `null` fields
|
|
135
|
+
* are fields the profile does not set; `featureValues` is `null` when the
|
|
136
|
+
* profile sets no feature.
|
|
137
|
+
*/
|
|
138
|
+
export interface RoleProfile {
|
|
139
|
+
model: string | null;
|
|
140
|
+
modeId: string | null;
|
|
141
|
+
thinkingOptionId: string | null;
|
|
142
|
+
featureValues: Record<string, unknown> | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const nonEmpty = (value: unknown): string | null => (typeof value === "string" && value.trim() !== "" ? value : null);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The settings of one paseo-bm profile, from ONE `config.get()` under the
|
|
149
|
+
* lookup budget, or `null` when it cannot be read or does not exist. The
|
|
150
|
+
* plugin SDK's view is flat: `config.agentProfiles` is `daemon.agentProfiles`.
|
|
151
|
+
* Never throws.
|
|
152
|
+
*/
|
|
153
|
+
export async function profileOf(
|
|
154
|
+
paseo: unknown,
|
|
155
|
+
profileId: string,
|
|
156
|
+
log: (message: string) => void = (message) => console.warn(message),
|
|
157
|
+
): Promise<RoleProfile | null> {
|
|
158
|
+
const get = (paseo as { config?: { get?: unknown } } | null | undefined)?.config?.get;
|
|
159
|
+
if (typeof get !== "function") return null;
|
|
160
|
+
try {
|
|
161
|
+
const result = await withTimeout(
|
|
162
|
+
(get.call((paseo as { config: unknown }).config) as Promise<{
|
|
163
|
+
config?: { agentProfiles?: Array<Record<string, unknown> | null | undefined> };
|
|
164
|
+
}>),
|
|
165
|
+
);
|
|
166
|
+
if (result === TIMED_OUT) {
|
|
167
|
+
log(`[paseo-bm] reading the ${profileId} profile took longer than ${LOOKUP_TIMEOUT_MS} ms; its own settings are ignored.`);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const profile = result?.config?.agentProfiles?.find((entry) => entry?.id === profileId);
|
|
171
|
+
if (profile === null || profile === undefined || typeof profile !== "object") return null;
|
|
172
|
+
const features = profile.featureValues;
|
|
173
|
+
return {
|
|
174
|
+
model: nonEmpty(profile.model),
|
|
175
|
+
modeId: nonEmpty(profile.modeId),
|
|
176
|
+
thinkingOptionId: nonEmpty(profile.thinkingOptionId),
|
|
177
|
+
featureValues:
|
|
178
|
+
features !== null && typeof features === "object" && !Array.isArray(features) && Object.keys(features).length > 0
|
|
179
|
+
? { ...(features as Record<string, unknown>) }
|
|
180
|
+
: null,
|
|
181
|
+
};
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The `modeId` set by hand on a paseo-bm profile, or `null`. Read the same way
|
|
189
|
+
* `manager.ensure` reads the Manager's profile. Never throws.
|
|
190
|
+
*/
|
|
191
|
+
export async function profileModeOf(
|
|
192
|
+
paseo: unknown,
|
|
193
|
+
profileId: string,
|
|
194
|
+
log: (message: string) => void = (message) => console.warn(message),
|
|
195
|
+
): Promise<string | null> {
|
|
196
|
+
return (await profileOf(paseo, profileId, log))?.modeId ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The last mode list read successfully per provider in this plugin run, and
|
|
201
|
+
* when (delta 20260918g §4.9): the Worker's Runtime facts fall back on it when
|
|
202
|
+
* a lookup fails.
|
|
203
|
+
*/
|
|
204
|
+
const lastModes = new Map<string, { modes: ProviderMode[]; at: string }>();
|
|
205
|
+
|
|
206
|
+
/** The last list `modesFor` read for `provider` in this run, or null. */
|
|
207
|
+
export function lastModesOf(provider: string): { modes: ProviderMode[]; at: string } | null {
|
|
208
|
+
return lastModes.get(provider) ?? null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Forgets every remembered list; for tests. */
|
|
212
|
+
export function forgetModes(): void {
|
|
213
|
+
lastModes.clear();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The provider's modes, or `null` when they cannot be read. Every `null` is
|
|
218
|
+
* logged: a silent miss would leave a Reviewer in the full-access mode it
|
|
219
|
+
* inherits from a no-prompt Worker, or a Manager without the Worker mode it
|
|
220
|
+
* must pass. A list that was read is remembered (`lastModesOf`).
|
|
221
|
+
*/
|
|
222
|
+
export async function modesFor(
|
|
223
|
+
paseo: unknown,
|
|
224
|
+
provider: string,
|
|
225
|
+
log: (message: string) => void = (message) => console.warn(message),
|
|
226
|
+
cwd?: string,
|
|
227
|
+
): Promise<ProviderMode[] | null> {
|
|
228
|
+
const providers = (paseo as { providers?: { listModes?: unknown } } | null | undefined)?.providers;
|
|
229
|
+
const listModes = providers?.listModes;
|
|
230
|
+
if (typeof listModes !== "function") {
|
|
231
|
+
log(`[paseo-bm] this Paseo host cannot list provider modes; the mode of ${provider} is left to its creator.`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
// `cwd` lets the daemon answer from the snapshot it already warmed for this
|
|
236
|
+
// directory instead of waiting for a cold provider discovery.
|
|
237
|
+
const call = cwd === undefined ? listModes.call(providers, provider) : listModes.call(providers, provider, { cwd });
|
|
238
|
+
const result = await withTimeout(call as Promise<{ modes?: unknown; error?: unknown } | null | undefined>);
|
|
239
|
+
if (result === TIMED_OUT) {
|
|
240
|
+
log(`[paseo-bm] reading the modes of ${provider} took longer than ${LOOKUP_TIMEOUT_MS} ms; its mode is left to its creator.`);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const modes = Array.isArray(result?.modes) ? (result.modes as ProviderMode[]) : [];
|
|
244
|
+
const failure = typeof result?.error === "string" && result.error !== "" ? result.error : null;
|
|
245
|
+
if (failure !== null || !Array.isArray(result?.modes)) {
|
|
246
|
+
log(`[paseo-bm] could not read the modes of ${provider} (${failure ?? "no modes listed"}); its mode is left to its creator.`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
// An empty list WITHOUT an error is an answer, not a failure: the provider
|
|
250
|
+
// has no modes at all (Pi, delta 20260921 §4.2.1 / F3). Nothing to log,
|
|
251
|
+
// and nothing worth remembering for the Runtime-facts fallback.
|
|
252
|
+
if (modes.length === 0) return [];
|
|
253
|
+
lastModes.set(provider, { modes, at: new Date().toISOString() });
|
|
254
|
+
return modes;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
log(`[paseo-bm] could not read the modes of ${provider} (${error instanceof Error ? error.message : String(error)}); its mode is left to its creator.`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Run posture by provider capability (delta 20260921 §4.2.1–§4.2.2, REQ-063).
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* How a provider lets paseo-bm choose a role's start mode:
|
|
267
|
+
* - `tiered`: its modes carry a `colorTier` (Claude, Codex) — the bm-msy rules apply;
|
|
268
|
+
* - `untiered`: modes without any `colorTier` (OpenCode's modes are the user's
|
|
269
|
+
* own OpenCode agents) — a listed mode is always passed, plus auto-approve;
|
|
270
|
+
* - `none`: an empty list without an error (Pi) — no mode is ever passed;
|
|
271
|
+
* - `unknown`: the list could not be read — today's behaviour, unchanged.
|
|
272
|
+
*/
|
|
273
|
+
export type ProviderCapability = "tiered" | "untiered" | "none" | "unknown";
|
|
274
|
+
|
|
275
|
+
/** The capability class of a provider from what `modesFor` returned. */
|
|
276
|
+
export function capabilityOf(modes: readonly ProviderMode[] | null): ProviderCapability {
|
|
277
|
+
if (modes === null) return "unknown";
|
|
278
|
+
const usable = modes.filter((mode) => typeof mode?.id === "string" && mode.id !== "");
|
|
279
|
+
if (usable.length === 0) return "none";
|
|
280
|
+
return usable.some((mode) => tierOf(mode) !== "") ? "tiered" : "untiered";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The toggle that auto-approves a provider's tool-permission prompts (OpenCode, design F1). */
|
|
284
|
+
export const AUTO_APPROVE_FEATURE = "auto_accept";
|
|
285
|
+
|
|
286
|
+
/** A feature as `providers.listFeatures` returns it (toggle or select). */
|
|
287
|
+
export type ProviderFeature = { type?: string; id?: string; value?: unknown };
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The features a provider offers for a draft config, or `null` when they
|
|
291
|
+
* cannot be read. `provider` is the selection the agent is created with
|
|
292
|
+
* (`bm-worker` or `bm-worker/<model>`); `cwd` is required by the daemon.
|
|
293
|
+
* Raced against the lookup budget; every `null` is logged. Never throws.
|
|
294
|
+
*/
|
|
295
|
+
export async function featuresFor(
|
|
296
|
+
paseo: unknown,
|
|
297
|
+
provider: string,
|
|
298
|
+
cwd: string | undefined,
|
|
299
|
+
log: (message: string) => void = (message) => console.warn(message),
|
|
300
|
+
): Promise<ProviderFeature[] | null> {
|
|
301
|
+
const providers = (paseo as { providers?: { listFeatures?: unknown } } | null | undefined)?.providers;
|
|
302
|
+
const listFeatures = providers?.listFeatures;
|
|
303
|
+
if (typeof listFeatures !== "function" || cwd === undefined) {
|
|
304
|
+
log(`[paseo-bm] cannot read the features of ${provider}; its auto-approve is left as it is.`);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const result = await withTimeout(
|
|
309
|
+
listFeatures.call(providers, { provider, cwd }) as Promise<{ features?: unknown; error?: unknown } | null | undefined>,
|
|
310
|
+
);
|
|
311
|
+
if (result === TIMED_OUT) {
|
|
312
|
+
log(`[paseo-bm] reading the features of ${provider} took longer than ${LOOKUP_TIMEOUT_MS} ms; its auto-approve is left as it is.`);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (typeof result?.error === "string" && result.error !== "") {
|
|
316
|
+
log(`[paseo-bm] could not read the features of ${provider} (${result.error}); its auto-approve is left as it is.`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return Array.isArray(result?.features) ? (result.features as ProviderFeature[]) : [];
|
|
320
|
+
} catch (error) {
|
|
321
|
+
log(`[paseo-bm] could not read the features of ${provider} (${error instanceof Error ? error.message : String(error)}); its auto-approve is left as it is.`);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** True when the provider offers the auto-approve toggle. */
|
|
327
|
+
export function offersAutoApprove(features: readonly ProviderFeature[] | null): boolean {
|
|
328
|
+
return Array.isArray(features) && features.some((feature) => feature?.type === "toggle" && feature.id === AUTO_APPROVE_FEATURE);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* What `runPostureOf` asks to change: a start mode and/or the complete feature
|
|
333
|
+
* values. `null` means REMOVE the key (a provider without modes gets neither).
|
|
334
|
+
*/
|
|
335
|
+
export interface RunPosture {
|
|
336
|
+
modeId?: string | null;
|
|
337
|
+
featureValues?: Record<string, unknown> | null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The start mode and auto-approve a role should get on a provider of this
|
|
342
|
+
* capability (delta 20260921 §4.2.2), or `undefined` for `tiered` and
|
|
343
|
+
* `unknown`, whose rules stay where they were (`chooseModeId` in the hook,
|
|
344
|
+
* `managerModeFor` in `manager.ensure`). `current` is what the request already
|
|
345
|
+
* carries: the creator's mode and the feature values merged so far (the
|
|
346
|
+
* profile's under the creator's).
|
|
347
|
+
*
|
|
348
|
+
* - `untiered`: a listed mode is always passed — the creator's, else the
|
|
349
|
+
* profile's, else the FIRST listed one: the daemon refuses a cross-provider
|
|
350
|
+
* child without a mode (design F11). The Manager and the Worker get
|
|
351
|
+
* `auto_accept: true` when the provider offers it and nobody set it; the
|
|
352
|
+
* Reviewer ALWAYS gets `auto_accept: false`, over the profile and over a
|
|
353
|
+
* value the daemon may have set by itself (owner decision Q7 a).
|
|
354
|
+
* - `none`: no mode and no feature (Pi has neither; its lack of a permission
|
|
355
|
+
* layer was accepted by the owner, Q7 a): both are REMOVED from the request,
|
|
356
|
+
* whether the creator or the profile set them (review b4).
|
|
357
|
+
* The Reviewer's `auto_accept: false` is written even when the features could
|
|
358
|
+
* not be read or do not list the toggle, so no provider default can turn
|
|
359
|
+
* auto-approve on for it (review b4).
|
|
360
|
+
*/
|
|
361
|
+
export function runPostureOf(
|
|
362
|
+
role: Role,
|
|
363
|
+
capability: ProviderCapability,
|
|
364
|
+
modes: readonly ProviderMode[],
|
|
365
|
+
features: readonly ProviderFeature[] | null,
|
|
366
|
+
profileModeId: string | null,
|
|
367
|
+
current: { modeId?: string; featureValues?: Record<string, unknown> } = {},
|
|
368
|
+
): RunPosture | undefined {
|
|
369
|
+
if (capability === "none") {
|
|
370
|
+
const posture: RunPosture = {};
|
|
371
|
+
if (current.modeId !== undefined) posture.modeId = null;
|
|
372
|
+
if (current.featureValues !== undefined) posture.featureValues = null;
|
|
373
|
+
return posture;
|
|
374
|
+
}
|
|
375
|
+
if (capability !== "untiered") return undefined;
|
|
376
|
+
const listed = modes.filter((mode) => typeof mode?.id === "string" && mode.id !== "").map((mode) => mode.id);
|
|
377
|
+
const posture: RunPosture = {};
|
|
378
|
+
const keep = current.modeId !== undefined && listed.includes(current.modeId);
|
|
379
|
+
if (!keep) {
|
|
380
|
+
const modeId = profileModeId !== null && listed.includes(profileModeId) ? profileModeId : listed[0];
|
|
381
|
+
if (modeId !== undefined) posture.modeId = modeId;
|
|
382
|
+
}
|
|
383
|
+
if (offersAutoApprove(features) || role === "reviewer") {
|
|
384
|
+
const values = { ...(current.featureValues ?? {}) };
|
|
385
|
+
const set = Object.prototype.hasOwnProperty.call(values, AUTO_APPROVE_FEATURE);
|
|
386
|
+
if (role === "reviewer") {
|
|
387
|
+
if (values[AUTO_APPROVE_FEATURE] !== false) {
|
|
388
|
+
values[AUTO_APPROVE_FEATURE] = false;
|
|
389
|
+
posture.featureValues = values;
|
|
390
|
+
}
|
|
391
|
+
} else if (!set) {
|
|
392
|
+
values[AUTO_APPROVE_FEATURE] = true;
|
|
393
|
+
posture.featureValues = values;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return posture;
|
|
397
|
+
}
|