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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/client/agent-tree.ts +308 -0
  4. package/client/answer-state.ts +62 -0
  5. package/client/bead-chips.tsx +147 -0
  6. package/client/beads-header-button.ts +108 -0
  7. package/client/beads-model.ts +581 -0
  8. package/client/beads-screen.tsx +516 -0
  9. package/client/beads-tab.tsx +58 -0
  10. package/client/chat-card.tsx +636 -0
  11. package/client/chat-cards.ts +1038 -0
  12. package/client/dashboard-actions.tsx +255 -0
  13. package/client/dashboard-model.ts +947 -0
  14. package/client/dashboard-view.ts +215 -0
  15. package/client/dashboard.tsx +318 -0
  16. package/client/launch-manager.ts +323 -0
  17. package/client/launcher.tsx +516 -0
  18. package/client/markdown-view.tsx +112 -0
  19. package/client/markdown.ts +145 -0
  20. package/client/settings.tsx +104 -0
  21. package/client/setup-model.ts +552 -0
  22. package/client/setup-screen.tsx +913 -0
  23. package/client/slot.ts +47 -0
  24. package/client/tree.tsx +204 -0
  25. package/client/ui.tsx +262 -0
  26. package/client/waiting-pills-model.ts +156 -0
  27. package/client/waiting-pills.tsx +201 -0
  28. package/index.client.tsx +232 -0
  29. package/index.server.ts +168 -0
  30. package/package.json +35 -0
  31. package/paseo-plugin.json +6 -0
  32. package/roles/manager.md +181 -0
  33. package/roles/reviewer.md +160 -0
  34. package/roles/worker.md +407 -0
  35. package/server/agent-labels.ts +194 -0
  36. package/server/agent-role.ts +102 -0
  37. package/server/answer-marks.ts +120 -0
  38. package/server/bead-actions.ts +88 -0
  39. package/server/bead-work.ts +80 -0
  40. package/server/beads-store.ts +342 -0
  41. package/server/bm-report.ts +433 -0
  42. package/server/chat-peers.ts +65 -0
  43. package/server/chat-rpc.ts +122 -0
  44. package/server/chat-waiting.ts +182 -0
  45. package/server/collector.ts +629 -0
  46. package/server/config-writer.ts +222 -0
  47. package/server/cost.ts +88 -0
  48. package/server/dashboard-rpc.ts +662 -0
  49. package/server/fallback-detect.ts +183 -0
  50. package/server/fallback-handover.ts +365 -0
  51. package/server/fallback-manager.ts +170 -0
  52. package/server/fallback-reviewer.ts +198 -0
  53. package/server/fallback-rpc.ts +306 -0
  54. package/server/fallback-settings.ts +322 -0
  55. package/server/fallback-state.ts +518 -0
  56. package/server/fallback-switch.ts +191 -0
  57. package/server/fallback-wait.ts +188 -0
  58. package/server/format-check.ts +352 -0
  59. package/server/install-home.ts +187 -0
  60. package/server/live-timeline.ts +129 -0
  61. package/server/manager-instructions.ts +9 -0
  62. package/server/manager.ts +647 -0
  63. package/server/model-costs.ts +238 -0
  64. package/server/notice-queue.ts +315 -0
  65. package/server/notices.ts +81 -0
  66. package/server/paseo-cli.ts +115 -0
  67. package/server/provider-id.ts +12 -0
  68. package/server/review-budget.ts +208 -0
  69. package/server/reviewer-instructions.ts +9 -0
  70. package/server/role-choices.ts +161 -0
  71. package/server/role-extras.ts +270 -0
  72. package/server/role-hook.ts +347 -0
  73. package/server/role-mode.ts +397 -0
  74. package/server/role-settings-rpc.ts +325 -0
  75. package/server/roles.ts +96 -0
  76. package/server/settings-notices.ts +112 -0
  77. package/server/setup-rpc.ts +70 -0
  78. package/server/setup-skills.ts +121 -0
  79. package/server/setup-tools.ts +162 -0
  80. package/server/shell.ts +68 -0
  81. package/server/stop-propagation.ts +365 -0
  82. package/server/tools-check.ts +118 -0
  83. package/server/trace-store.ts +1137 -0
  84. package/server/traces.ts +1356 -0
  85. package/server/worker-instructions.ts +9 -0
  86. package/server/workflow-steps.ts +422 -0
  87. package/shared/bead-ids.ts +25 -0
  88. package/shared/bm-fallback.ts +91 -0
  89. package/shared/bm-format.ts +424 -0
  90. package/shared/bm-questions.ts +213 -0
  91. package/shared/bm-report.ts +433 -0
  92. package/shared/contracts.ts +1371 -0
  93. package/shared/fallback-patterns.ts +201 -0
  94. package/shared/fallback.ts +46 -0
  95. package/shared/new-request.ts +20 -0
  96. package/shared/order.ts +22 -0
  97. package/shared/prices.ts +65 -0
  98. package/shared/settings.ts +57 -0
  99. package/shared/sole-worker.ts +20 -0
  100. package/shared/version.ts +6 -0
  101. 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
+ }