humanish 0.110.3 → 0.111.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 (115) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/CONTEXT.md +2 -2
  3. package/dist/actors/local-agent/claude-session.js +1 -1
  4. package/dist/actors/local-agent/cli.js +1 -1
  5. package/dist/actors/scripted-browser/actor.d.ts +2 -2
  6. package/dist/actors/scripted-browser/actor.js +17 -8
  7. package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
  8. package/dist/actors/scripted-browser/launch-env.js +50 -0
  9. package/dist/analysis/automatic-completion.d.ts +2 -1
  10. package/dist/analysis/automatic-completion.js +7 -2
  11. package/dist/cli/commands/migrate.js +1 -1
  12. package/dist/cli/commands/runs.js +5 -2
  13. package/dist/cli/commands/study-route-computer-use.js +3 -2
  14. package/dist/cli/commands/study-route-preview.js +2 -1
  15. package/dist/cli/commands/study-route-shared-world.js +3 -3
  16. package/dist/cli/commands/study.js +2 -1
  17. package/dist/routes/computer-use/live-phase.js +8 -1
  18. package/dist/routes/computer-use/local-vm.js +2 -1
  19. package/dist/routes/computer-use/participant-model.js +1 -1
  20. package/dist/routes/computer-use/plan.js +9 -10
  21. package/dist/routes/computer-use/preflight.js +1 -1
  22. package/dist/routes/computer-use/result.js +1 -1
  23. package/dist/routes/computer-use/route.js +3 -2
  24. package/dist/routes/computer-use/setup.js +2 -1
  25. package/dist/routes/computer-use/types.d.ts +4 -4
  26. package/dist/routes/scripted/plan.js +5 -4
  27. package/dist/routes/scripted/route.js +6 -2
  28. package/dist/routes/scripted/scenario.d.ts +1 -1
  29. package/dist/routes/scripted/scenario.js +3 -3
  30. package/dist/routes/scripted/surface-sessions.d.ts +2 -0
  31. package/dist/routes/scripted/surface-sessions.js +2 -1
  32. package/dist/routes/scripted/types.d.ts +2 -2
  33. package/dist/routes/shared-world/handoff.js +1 -1
  34. package/dist/routes/shared-world/participant-specs.js +6 -2
  35. package/dist/routes/shared-world/plan.js +8 -8
  36. package/dist/routes/shared-world/route.js +3 -3
  37. package/dist/routes/shared-world/types.d.ts +1 -1
  38. package/dist/routes/terminal/bundle.d.ts +3 -3
  39. package/dist/routes/terminal/ledger.d.ts +2 -2
  40. package/dist/routes/terminal/lifetime.js +1 -1
  41. package/dist/routes/terminal/live-finish.js +1 -1
  42. package/dist/routes/terminal/plan.js +4 -3
  43. package/dist/routes/terminal/route.js +3 -2
  44. package/dist/routes/terminal/session.js +1 -0
  45. package/dist/routes/terminal/types.d.ts +1 -1
  46. package/dist/run/display.d.ts +10 -0
  47. package/dist/run/display.js +22 -0
  48. package/dist/run/reclaim.d.ts +4 -2
  49. package/dist/run/reclaim.js +61 -18
  50. package/dist/run/results.d.ts +1 -1
  51. package/dist/run/run.d.ts +11 -1
  52. package/dist/run/run.js +24 -5
  53. package/dist/run/status.d.ts +6 -0
  54. package/dist/run/status.js +1 -0
  55. package/dist/run/stored-runs.js +26 -11
  56. package/dist/run-study.d.ts +5 -3
  57. package/dist/run-study.js +7 -11
  58. package/dist/study/adapter-scorer-loader.d.ts +1 -1
  59. package/dist/study/adapter-scorer-loader.js +3 -3
  60. package/dist/study/composition-rules.js +49 -39
  61. package/dist/study/config.d.ts +31 -8
  62. package/dist/study/config.js +181 -155
  63. package/dist/study/discover.d.ts +1 -1
  64. package/dist/study/discover.js +1 -1
  65. package/dist/study/doctor.js +5 -4
  66. package/dist/study/keys.d.ts +250 -2
  67. package/dist/study/keys.js +32 -62
  68. package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
  69. package/dist/study/{convert.js → migrate/convert.js} +28 -31
  70. package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
  71. package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
  72. package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
  73. package/dist/study/migrate/v2.d.ts +78 -0
  74. package/dist/study/migrate/v2.js +607 -0
  75. package/dist/study/parse/actors.d.ts +34 -10
  76. package/dist/study/parse/actors.js +76 -174
  77. package/dist/study/parse/comms.js +6 -2
  78. package/dist/study/parse/execution.d.ts +9 -4
  79. package/dist/study/parse/execution.js +6 -41
  80. package/dist/study/parse/front.d.ts +36 -0
  81. package/dist/study/parse/{study-v3.js → front.js} +37 -88
  82. package/dist/study/parse/subject.d.ts +2 -0
  83. package/dist/study/parse/subject.js +15 -18
  84. package/dist/study/persona-resolve.d.ts +2 -4
  85. package/dist/study/persona-resolve.js +7 -8
  86. package/dist/study/plan-base.d.ts +1 -1
  87. package/dist/study/plan-base.js +6 -6
  88. package/dist/study/plan-participants.d.ts +2 -2
  89. package/dist/study/plan-participants.js +12 -11
  90. package/dist/study/plan-types.d.ts +5 -5
  91. package/dist/study/plan.js +5 -4
  92. package/dist/study/preflight-probes.js +4 -4
  93. package/dist/study/preflight.d.ts +1 -1
  94. package/dist/study/preflight.js +3 -3
  95. package/dist/study/routing.d.ts +5 -5
  96. package/dist/study/routing.js +17 -20
  97. package/dist/study/run-study-options.d.ts +1 -1
  98. package/dist/study/run-study-options.js +3 -3
  99. package/dist/study/study-deps.d.ts +1 -1
  100. package/dist/study/study-fields.d.ts +19 -0
  101. package/dist/study/study-fields.js +46 -0
  102. package/dist/study/summary.js +19 -18
  103. package/dist/study/types.d.ts +66 -104
  104. package/dist/study/types.js +0 -1
  105. package/dist/study/url-credentials.js +3 -3
  106. package/dist/study/validation.d.ts +4 -10
  107. package/dist/study/validation.js +54 -79
  108. package/dist/study/warnings.d.ts +3 -3
  109. package/dist/study/warnings.js +42 -110
  110. package/dist/substrates/local/runtime-config.js +14 -16
  111. package/dist/tui-app.js +3 -3
  112. package/docs/contracts/schemas.md +36 -8
  113. package/package.json +1 -1
  114. package/dist/study/parse/study-v3.d.ts +0 -21
  115. /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
@@ -1,72 +1,54 @@
1
1
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
2
2
  import { declaredParticipantIds } from "./plan-participants.js";
3
3
  import { addressedRecipients } from "./parse/comms.js";
4
- import { focusOf, rosterOf } from "./parse/actors.js";
5
- // The computer-use and terminal routes consume mission/persona/model and laneFocus.instruction (they
6
- // compose the agent prompt and bundle provenance); laneFocus.id/label stay inert. On the computer-use
7
- // E2B route actors[0].lanes is consumed (the fan-out roster).
4
+ import { actorOf, participantList, declaredParticipantCount, scenarioRefOf, } from "./study-fields.js";
5
+ // The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
6
+ // and bundle provenance).
8
7
  const promptRoute = (routes) => routes.cua || routes.terminal;
9
- // The scripted-browser route consumes persona and count (trace/bundle provenance; surface roster).
10
- // The prompt fields can never act there: the scripted actor runs no model.
8
+ // The scripted-browser route consumes persona and surfaces (trace/bundle provenance). The prompt
9
+ // fields can never act there: the scripted actor runs no model.
11
10
  const scriptedRoute = (routes) => !promptRoute(routes) && routes.scripted;
12
11
  const otherRoute = (routes) => !promptRoute(routes) && !routes.scripted;
13
- /** Rows for each actor, reported as `actors[<index>].<field>`, in this order per actor. */
12
+ /** Rows for the actor and its participants, reported first, in this order. */
14
13
  const ACTOR_ROWS = [
15
14
  // Shared-world-only fields on the roster: a participant's `entry` is inert anywhere else
16
15
  // (claims match mechanism).
17
16
  {
18
- field: "lanes[].entry",
19
- reason: "the per-participant loopback entry is a shared-world capability; needs subject.topology: shared-world",
20
- applies: (actor, routes) => Boolean(rosterOf(actor)?.some((entry) => entry.entry !== undefined)) && !routes.shared,
17
+ field: "participants[].entry",
18
+ reason: "the per-participant loopback entry is a shared-world capability; needs route: shared-world",
19
+ applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.entry !== undefined)) &&
20
+ !routes.shared,
21
21
  },
22
22
  // The host marker acts only on the external-public shared-world route; inert elsewhere.
23
23
  {
24
- field: "lanes[].host",
24
+ field: "participants[].host",
25
25
  reason: "the designated host-participant marker; needs the external-public shared-world route: app-url × topology shared-world × allowPublicTargets",
26
- applies: (actor, routes) => Boolean(rosterOf(actor)?.some((entry) => entry.host === true)) && !routes.externalPublic,
26
+ applies: (config, routes) => Boolean(participantList(config)?.some((entry) => entry.host === true)) &&
27
+ !routes.externalPublic,
27
28
  },
28
29
  {
29
- field: "laneFocus.id",
30
- applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.id),
31
- },
32
- {
33
- field: "laneFocus.label",
34
- applies: (actor, routes) => promptRoute(routes) && Boolean(focusOf(actor)?.label),
35
- },
36
- {
37
- field: "lanes",
38
- reason: "only the computer-use route runs more than one participant",
39
- applies: (actor, routes) => routes.terminal && Boolean(rosterOf(actor)),
40
- },
41
- {
42
- field: "mission",
30
+ field: "actor.mission",
43
31
  reason: "the scripted-browser actor runs no model",
44
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(actor.mission),
32
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.mission),
45
33
  },
46
34
  {
47
- field: "laneFocus",
35
+ field: "actor.model",
48
36
  reason: "the scripted-browser actor runs no model",
49
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(focusOf(actor)),
37
+ applies: (config, routes) => scriptedRoute(routes) && Boolean(actorOf(config)?.model),
50
38
  },
39
+ // On every other route the prompt fields and persona are inert.
51
40
  {
52
- field: "model",
53
- reason: "the scripted-browser actor runs no model",
54
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(actor.model),
41
+ field: "actor.mission",
42
+ applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.mission),
55
43
  },
56
44
  {
57
- field: "lanes",
58
- reason: "the scripted-browser route fans out via actors[0].count, not an actors[0].lanes roster",
59
- applies: (actor, routes) => scriptedRoute(routes) && Boolean(rosterOf(actor)),
45
+ field: "actor.persona",
46
+ applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.persona),
60
47
  },
61
- // On every other route the prompt fields, persona and the `lanes[]` roster are inert.
62
- { field: "mission", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.mission) },
63
48
  {
64
- field: "laneFocus",
65
- applies: (actor, routes) => otherRoute(routes) && Boolean(focusOf(actor)),
49
+ field: "actor.model",
50
+ applies: (config, routes) => otherRoute(routes) && Boolean(actorOf(config)?.model),
66
51
  },
67
- { field: "persona", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.persona) },
68
- { field: "model", applies: (actor, routes) => otherRoute(routes) && Boolean(actor.model) },
69
- { field: "lanes", applies: (actor, routes) => otherRoute(routes) && Boolean(rosterOf(actor)) },
70
52
  ];
71
53
  const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
72
54
  const RESERVED = "reserved; no route reads it yet";
@@ -90,17 +72,10 @@ const CONFIG_ROWS = [
90
72
  field: "subject.state",
91
73
  applies: (config, routes) => Boolean(config.subject.state) && !routes.cua && !routes.scripted,
92
74
  },
93
- // topology + checkpoint act only on the shared-world route; a set-but-unconsumed value
94
- // (incl. an explicit per-lane-worlds, which the computer-use route already is by mechanism) warns
95
- // inert.
96
- {
97
- field: "subject.topology",
98
- reason: "drives behavior only on the shared-world route; needs subject.topology: shared-world + clone × e2b-desktop × a computer-use actor + a roster of ≥2 participants",
99
- applies: (config, routes) => config.subject.topology !== undefined && !routes.shared,
100
- },
75
+ // The checkpoint acts only on the shared-world route.
101
76
  {
102
77
  field: "subject.state.checkpoint",
103
- reason: "the shared-world state-checkpoint probe; needs subject.topology: shared-world",
78
+ reason: "the shared-world state-checkpoint probe; needs route: shared-world",
104
79
  applies: (config, routes) => config.subject.state?.checkpoint !== undefined && !routes.shared,
105
80
  },
106
81
  // publicTarget (the external-public ownership attestation) acts only on the external-public
@@ -160,18 +135,10 @@ const CONFIG_ROWS = [
160
135
  field: "execution.concurrency",
161
136
  applies: (config, routes) => config.execution?.concurrency !== undefined && !routes.cua,
162
137
  },
163
- // execution.caps is consumed on the computer-use route (maxUsd is the fail-closed spend abort);
164
- // inert (warned) everywhere else so a misplaced budget field is never trusted to cap a route it
165
- // cannot.
166
- {
167
- field: "execution.caps",
168
- reason: "only the computer-use route enforces it, with a computer-use actor on e2b-desktop",
169
- applies: (config, routes) => Boolean(config.execution?.caps) && !routes.cua,
170
- },
171
- // terminal-product consumes subject.product, scenario.caps, execution.{terminal,runtimeAuth}:
172
- // dry-run records the contract; live execution enforces caps and command-scoped auth. On every
173
- // other route they are inert and must warn so a
174
- // misplaced safety/budget field is never trusted to do something it cannot.
138
+ // parse/front.ts refuses a `caps` key the declared route does not read, so no row lists `caps`.
139
+ // terminal-product consumes subject.product and execution.{terminal,runtimeAuth}: dry-run records
140
+ // the contract; live execution enforces command-scoped auth. On every other route they are inert
141
+ // and must warn so a misplaced safety field is never trusted to do something it cannot.
175
142
  {
176
143
  field: "subject.product",
177
144
  reason: "needs subject.source: terminal-product or desktop-cli with the matching actor",
@@ -179,18 +146,6 @@ const CONFIG_ROWS = [
179
146
  !routes.terminal &&
180
147
  config.subject.source !== "desktop-cli",
181
148
  },
182
- {
183
- field: "scenario.caps",
184
- reason: TERMINAL_ONLY,
185
- applies: (config, routes) => Boolean(config.scenario?.caps) && !routes.terminal,
186
- },
187
- // The study-level budget is a CUA-route capability; the terminal route is a single agent whose
188
- // maxUsd already caps the whole run, so a maxTotalUsd there would be trusted and unenforced.
189
- {
190
- field: "scenario.caps.maxTotalUsd",
191
- reason: "the study-level budget is a computer-use route capability; the terminal route's maxUsd already caps the whole run",
192
- applies: (config, routes) => config.scenario?.caps?.maxTotalUsd !== undefined && routes.terminal,
193
- },
194
149
  {
195
150
  field: "execution.terminal",
196
151
  reason: TERMINAL_ONLY,
@@ -246,13 +201,12 @@ const CONFIG_ROWS = [
246
201
  reason: "no current route reads it",
247
202
  applies: (config) => config.execution?.desktop?.codexAppServer !== undefined,
248
203
  },
249
- // scenario.ref is consumed on the scripted-browser route (required there); forward-declared
204
+ // `scenario` is consumed on the scripted-browser route (required there); forward-declared
250
205
  // everywhere else.
251
206
  {
252
- field: "scenario.ref",
253
- applies: (config, routes) => Boolean(config.scenario?.ref) && !routes.scripted,
207
+ field: "scenario",
208
+ applies: (config, routes) => Boolean(scenarioRefOf(config)) && !routes.scripted,
254
209
  },
255
- { field: "scenario.inline", applies: (config) => Boolean(config.scenario?.inline) },
256
210
  // review.{scoring,milestones,vocabulary} stay forward-declared on every route.
257
211
  // review.scorer is consumed (loaded + wired, or fail-closed at load) on every scorer-capable
258
212
  // route, so it does not warn there; on the scripted-browser route the actor carries no scorer seam,
@@ -277,7 +231,6 @@ const CONFIG_ROWS = [
277
231
  reason: "the scripted-browser actor has no adopter-scorer seam; declare it on a terminal / computer-use / shared-world route",
278
232
  applies: (config, routes) => Boolean(config.review?.scorer) && routes.scripted,
279
233
  },
280
- { field: "personas", applies: (config) => Boolean(config.personas) },
281
234
  ];
282
235
  function routesOf(config) {
283
236
  const cua = isComputerUseComposition(config);
@@ -294,44 +247,23 @@ function routesOf(config) {
294
247
  config.execution?.target === "e2b-desktop",
295
248
  };
296
249
  }
297
- function rowLabel(row, prefix = "") {
298
- return row.reason === undefined
299
- ? `${prefix}${row.field}`
300
- : `${prefix}${row.field} (${row.reason})`;
250
+ function inertRows(config) {
251
+ const routes = routesOf(config);
252
+ return [...ACTOR_ROWS, ...CONFIG_ROWS].filter((row) => row.applies(config, routes));
301
253
  }
302
254
  /**
303
- * The fields a config sets that its route does not read, each as `field` or `field (reason)`. A v2
304
- * file gets them as one warning; a v3 study is refused for any of them.
255
+ * The fields a config sets that its route does not read, each as `field` or `field (reason)`.
256
+ * parseStudy refuses a study for any of them.
305
257
  */
306
258
  export function inertFieldLabels(config) {
307
- const routes = routesOf(config);
308
- const inert = [];
309
- for (const [index, actor] of config.actors.entries()) {
310
- for (const row of ACTOR_ROWS)
311
- if (row.applies(actor, routes))
312
- inert.push(rowLabel(row, `actors[${index}].`));
313
- }
314
- for (const row of CONFIG_ROWS)
315
- if (row.applies(config, routes))
316
- inert.push(rowLabel(row));
317
- return inert;
259
+ return inertRows(config).map((row) => row.reason === undefined ? row.field : `${row.field} (${row.reason})`);
318
260
  }
319
261
  /**
320
262
  * The same fields as `inertFieldLabels`, as paths without their reasons, such as
321
- * `actors[0].lanes[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
263
+ * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
322
264
  */
323
265
  export function inertFieldPaths(config) {
324
- const routes = routesOf(config);
325
- const paths = [];
326
- for (const [index, actor] of config.actors.entries()) {
327
- for (const row of ACTOR_ROWS)
328
- if (row.applies(actor, routes))
329
- paths.push(`actors[${index}].${row.field}`);
330
- }
331
- for (const row of CONFIG_ROWS)
332
- if (row.applies(config, routes))
333
- paths.push(row.field);
334
- return paths;
266
+ return inertRows(config).map((row) => row.field);
335
267
  }
336
268
  // Report fields that are present but not yet consumed by the engine, so a user never trusts a
337
269
  // setting that silently does nothing.
@@ -347,7 +279,7 @@ export function forwardDeclaredWarnings(config) {
347
279
  // and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
348
280
  // waves is otherwise indistinguishable from the all-live run the author meant.
349
281
  {
350
- const participantCount = rosterOf(config.actors[0])?.length ?? config.actors[0]?.count ?? 1;
282
+ const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
351
283
  const cap = config.execution?.concurrency;
352
284
  if (routes.cua && cap !== undefined && participantCount > 1 && cap < participantCount) {
353
285
  warnings.push(`execution.concurrency ${cap} caps a ${participantCount}-participant roster: participants run in waves of ${cap}, never all live at once. Remove execution.concurrency (the default runs all ${participantCount} participants simultaneously) or set it to ${participantCount}; declare a lower cap only to bound simultaneous paid desktops.`);
@@ -1,23 +1,24 @@
1
- import { rosterOf } from "../../study/parse/actors.js";
1
+ import { actorOf, capsOf, participantList } from "../../study/study-fields.js";
2
2
  // The runtime image enforces a 30-minute lifetime; reserve setup/teardown room.
3
3
  export const LOCAL_BROWSER_LIFETIME_MS = 30 * 60_000;
4
4
  const MAX_SESSION_MS = 20 * 60_000;
5
5
  export function isLocalBrowserStudy(config) {
6
6
  return (config.subject.source === "app-url" &&
7
7
  config.execution?.target === "local" &&
8
- ["local-agent", "openai-computer-use"].includes(config.actors[0]?.type ?? ""));
8
+ ["local-agent", "openai-computer-use"].includes(actorOf(config)?.type ?? ""));
9
9
  }
10
10
  /** Defaults for the explicitly selected local substrate; hosted configurations are untouched. */
11
11
  export function localBrowserDefaults(config) {
12
- if (!isLocalBrowserStudy(config))
12
+ const actor = actorOf(config);
13
+ if (!isLocalBrowserStudy(config) || actor === undefined)
13
14
  return config;
14
15
  return {
15
16
  ...config,
16
- actors: config.actors.map((actor) => ({
17
+ actor: {
17
18
  ...actor,
18
19
  model: actor.model ?? "gpt-6-astra",
19
20
  reasoningEffort: actor.reasoningEffort ?? "low",
20
- })),
21
+ },
21
22
  execution: {
22
23
  ...config.execution,
23
24
  timeoutMs: config.execution?.timeoutMs ?? MAX_SESSION_MS,
@@ -26,19 +27,18 @@ export function localBrowserDefaults(config) {
26
27
  resolution: config.execution?.desktop?.resolution ?? [960, 720],
27
28
  },
28
29
  },
29
- ...(config.review?.analysis === undefined && config.actors[0]?.type === "local-agent"
30
+ ...(config.review?.analysis === undefined && actor.type === "local-agent"
30
31
  ? { review: { ...config.review, analysis: { provider: "codex" } } }
31
32
  : {}),
32
33
  };
33
34
  }
34
35
  export function localBrowserUnsupportedReason(config) {
35
- const actor = config.actors[0];
36
+ const actor = actorOf(config);
36
37
  const desktop = config.execution?.desktop;
37
38
  if ((config.execution?.timeoutMs ?? MAX_SESSION_MS) > MAX_SESSION_MS) {
38
39
  return "Local browser sessions currently support at most 20 minutes, within the runtime's 30-minute lifetime.";
39
40
  }
40
- if (config.actors.length !== 1 ||
41
- !actor ||
41
+ if (!actor ||
42
42
  (actor.type === "local-agent" && actor.localAgent !== undefined && actor.localAgent !== "codex")) {
43
43
  return "Local browser studies support only Codex local-agent or openai-computer-use participants.";
44
44
  }
@@ -46,7 +46,7 @@ export function localBrowserUnsupportedReason(config) {
46
46
  desktop.resolution[1] !== 720 ||
47
47
  desktop.device !== undefined ||
48
48
  (desktop.browser !== undefined && !["default", "chromium"].includes(desktop.browser)) ||
49
- rosterOf(actor)?.some((entry) => entry.device !== undefined) ||
49
+ participantList(config)?.some((entry) => entry.device !== undefined) ||
50
50
  desktop.template !== undefined ||
51
51
  desktop.sandboxTimeoutMs !== undefined) {
52
52
  return "The local browser runtime currently uses a 960×720 Chromium desktop. Omit hosted templates, device presets and sandboxTimeoutMs.";
@@ -61,17 +61,15 @@ export function localBrowserUnsupportedReason(config) {
61
61
  if (actor.type === "local-agent" &&
62
62
  (actor.model !== "gpt-6-astra" ||
63
63
  actor.reasoningEffort !== "low" ||
64
- rosterOf(actor)?.some((entry) => entry.reasoningEffort !== undefined && entry.reasoningEffort !== "low") ||
64
+ participantList(config)?.some((entry) => entry.reasoningEffort !== undefined && entry.reasoningEffort !== "low") ||
65
65
  actor.maxOutputTokens !== undefined ||
66
- config.execution?.caps?.maxUsd !== undefined ||
67
- config.execution?.caps?.maxTotalUsd !== undefined ||
68
- config.scenario?.caps?.maxUsd !== undefined ||
69
- config.scenario?.caps?.maxTotalUsd !== undefined)) {
66
+ capsOf(config)?.maxUsd !== undefined ||
67
+ capsOf(config)?.maxTotalUsd !== undefined)) {
70
68
  return "Local Codex participants currently use gpt-6-astra at low effort. Account dollar/output-token caps are unavailable; use API participants for those controls.";
71
69
  }
72
70
  for (const target of [
73
71
  config.subject.appUrl,
74
- ...(rosterOf(actor) ?? []).map((entry) => entry.target),
72
+ ...(participantList(config) ?? []).map((entry) => entry.target),
75
73
  ].filter(Boolean)) {
76
74
  let url;
77
75
  try {