humanish 0.110.4 → 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 (79) hide show
  1. package/CHANGELOG.md +15 -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/cli/commands/study.js +2 -4
  7. package/dist/routes/computer-use/participant-model.js +1 -1
  8. package/dist/routes/computer-use/plan.js +4 -6
  9. package/dist/routes/computer-use/preflight.js +1 -1
  10. package/dist/routes/computer-use/result.js +1 -1
  11. package/dist/routes/computer-use/route.js +1 -1
  12. package/dist/routes/computer-use/types.d.ts +4 -4
  13. package/dist/routes/scripted/route.js +1 -1
  14. package/dist/routes/scripted/scenario.d.ts +1 -1
  15. package/dist/routes/scripted/scenario.js +3 -3
  16. package/dist/routes/scripted/types.d.ts +2 -2
  17. package/dist/routes/shared-world/handoff.js +1 -1
  18. package/dist/routes/shared-world/participant-specs.js +1 -1
  19. package/dist/routes/shared-world/plan.js +3 -4
  20. package/dist/routes/shared-world/types.d.ts +1 -1
  21. package/dist/routes/terminal/bundle.d.ts +3 -3
  22. package/dist/routes/terminal/ledger.d.ts +2 -2
  23. package/dist/routes/terminal/lifetime.js +1 -1
  24. package/dist/routes/terminal/live-finish.js +1 -1
  25. package/dist/routes/terminal/plan.js +1 -1
  26. package/dist/routes/terminal/route.js +1 -1
  27. package/dist/routes/terminal/types.d.ts +1 -1
  28. package/dist/run/results.d.ts +1 -1
  29. package/dist/run-study.d.ts +5 -3
  30. package/dist/run-study.js +7 -11
  31. package/dist/study/adapter-scorer-loader.d.ts +1 -1
  32. package/dist/study/adapter-scorer-loader.js +3 -3
  33. package/dist/study/composition-rules.js +21 -11
  34. package/dist/study/config.d.ts +29 -13
  35. package/dist/study/config.js +174 -157
  36. package/dist/study/discover.d.ts +1 -1
  37. package/dist/study/discover.js +1 -1
  38. package/dist/study/keys.d.ts +250 -2
  39. package/dist/study/keys.js +32 -62
  40. package/dist/study/migrate/convert.js +3 -4
  41. package/dist/study/migrate/v2.d.ts +17 -7
  42. package/dist/study/migrate/v2.js +150 -23
  43. package/dist/study/parse/actors.d.ts +11 -32
  44. package/dist/study/parse/actors.js +11 -156
  45. package/dist/study/parse/comms.js +6 -2
  46. package/dist/study/parse/execution.d.ts +9 -4
  47. package/dist/study/parse/execution.js +4 -39
  48. package/dist/study/parse/{study-v3.d.ts → front.d.ts} +6 -17
  49. package/dist/study/parse/{study-v3.js → front.js} +22 -91
  50. package/dist/study/parse/subject.d.ts +2 -0
  51. package/dist/study/parse/subject.js +15 -18
  52. package/dist/study/persona-resolve.js +7 -9
  53. package/dist/study/plan-base.d.ts +1 -1
  54. package/dist/study/plan-base.js +3 -4
  55. package/dist/study/plan-participants.d.ts +2 -2
  56. package/dist/study/plan-types.d.ts +5 -5
  57. package/dist/study/plan.js +1 -1
  58. package/dist/study/preflight-probes.js +2 -2
  59. package/dist/study/preflight.d.ts +1 -1
  60. package/dist/study/preflight.js +1 -1
  61. package/dist/study/routing.d.ts +5 -5
  62. package/dist/study/routing.js +5 -5
  63. package/dist/study/run-study-options.d.ts +1 -1
  64. package/dist/study/study-deps.d.ts +1 -1
  65. package/dist/study/study-fields.d.ts +12 -21
  66. package/dist/study/study-fields.js +29 -34
  67. package/dist/study/summary.js +9 -4
  68. package/dist/study/types.d.ts +50 -128
  69. package/dist/study/types.js +0 -1
  70. package/dist/study/url-credentials.js +3 -4
  71. package/dist/study/validation.d.ts +2 -10
  72. package/dist/study/validation.js +16 -46
  73. package/dist/study/warnings.d.ts +3 -3
  74. package/dist/study/warnings.js +40 -109
  75. package/dist/substrates/local/runtime-config.js +9 -11
  76. package/docs/contracts/schemas.md +23 -4
  77. package/package.json +1 -1
  78. package/dist/study/parse/study.d.ts +0 -38
  79. package/dist/study/parse/study.js +0 -225
@@ -1,12 +1,5 @@
1
1
  import type { StudyRoute } from "../routing.js";
2
- import { type StudyParseFailure } from "../types.js";
3
- export interface StudyDocument {
4
- readonly route: StudyRoute;
5
- /** The same document in the humanish.lab.v2 spelling. */
6
- readonly v2: Record<string, unknown>;
7
- /** For each expanded participant, the index of the `participants` entry it came from. */
8
- readonly participantSource: readonly number[];
9
- }
2
+ import type { StudyParseFailure, StudySurfaces } from "../types.js";
10
3
  /** A document's `participants`, read before the participant entries parse. */
11
4
  export interface Participants {
12
5
  count?: number;
@@ -21,15 +14,16 @@ export interface StudyFront {
21
14
  readonly route: StudyRoute;
22
15
  readonly actor: Record<string, unknown>;
23
16
  readonly participants: Participants;
24
- /** The scripted surfaces as a count: 1 is desktop, 2 adds mobile. */
25
- readonly surfaces: number | undefined;
17
+ readonly surfaces: StudySurfaces | undefined;
26
18
  }
27
19
  type Parsed<T> = {
28
20
  ok: true;
29
21
  value: T;
30
22
  } | StudyParseFailure;
31
- /** Rewrite a humanish.study.v3 document into humanish.lab.v2, or refuse it. */
32
- export declare function studyToV2(raw: Record<string, unknown>): Parsed<StudyDocument>;
23
+ /** The refusal for a study with no `actor` object. */
24
+ export declare const ACTOR_REQUIRED = "A study needs `actor:`, an object with at least `type`.";
25
+ /** Why `route` is not a route a study can declare, or undefined when it is one. */
26
+ export declare function routeNameReason(route: unknown): string | undefined;
33
27
  /**
34
28
  * The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
35
29
  * route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
@@ -39,9 +33,4 @@ export declare function studyToV2(raw: Record<string, unknown>): Parsed<StudyDoc
39
33
  * parser does.
40
34
  */
41
35
  export declare function readStudyFront(raw: Record<string, unknown>, anySharedWorldParticipants?: boolean): Parsed<StudyFront>;
42
- /**
43
- * Spell a v2 parser message the way the study file is written: `actors[0].lanes[3]` becomes the
44
- * `participants` entry it came from, `execution.caps` and `scenario.caps` become `caps`, and so on.
45
- */
46
- export declare function studySpelling(message: string, route: StudyRoute, participantSource: readonly number[]): string;
47
36
  export {};
@@ -1,10 +1,8 @@
1
- // humanish.study.v3, the study format. A study declares its route, one `actor`, its `participants`
2
- // and one `caps` block. studyToV2 rewrites a v3 document into the v2 spelling, so both formats run
3
- // through one parser and parse into the same StudyConfig. parseStudy then checks the declared
4
- // route against the one the config takes, and refuses any field that route does not read.
1
+ // What a humanish.study.v3 document declares before its sections parse: its route, one `actor`,
2
+ // its `participants` or `surfaces`, and the `caps` keys the route reads. config.ts parses the
3
+ // sections after these checks pass.
5
4
  import { isRecord } from "../../run/type-guards.js";
6
5
  import { findUnknownStudyKey } from "../keys.js";
7
- import { V2_SCHEMA } from "../types.js";
8
6
  import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./actors.js";
9
7
  import { invalid, posInt } from "./values.js";
10
8
  // The values `route:` takes: every route routeOf can return.
@@ -24,53 +22,13 @@ const CAPS_KEYS = {
24
22
  terminal: ["maxUsd", "maxJobs", "maxMinutes"],
25
23
  scripted: [],
26
24
  };
27
- /** Rewrite a humanish.study.v3 document into humanish.lab.v2, or refuse it. */
28
- export function studyToV2(raw) {
29
- const front = readStudyFront(raw);
30
- if (!front.ok)
31
- return front;
32
- const { route } = front.value;
33
- const { count, instruction, entries, source } = front.value.participants;
34
- // The v2 actor keys that hold participants.
35
- const v2Participants = [
36
- ["count", count ?? front.value.surfaces],
37
- ["laneFocus", instruction === undefined ? undefined : { instruction }],
38
- ["lanes", entries],
39
- ];
40
- const actor = { ...front.value.actor };
41
- for (const [key, value] of v2Participants)
42
- if (value !== undefined)
43
- actor[key] = value;
44
- const caps = raw.caps;
45
- const scenario = {};
46
- if (raw.scenario !== undefined)
47
- scenario.ref = raw.scenario;
48
- if (raw.mode !== undefined)
49
- scenario.mode = raw.mode;
50
- if (caps !== undefined && route === "terminal")
51
- scenario.caps = caps;
52
- const execution = caps !== undefined && route !== "terminal"
53
- ? withKey(raw.execution, "caps", caps)
54
- : raw.execution;
55
- const subject = route === "shared-world" ? withKey(raw.subject, "topology", "shared-world") : raw.subject;
56
- const v2 = { schema: V2_SCHEMA };
57
- const fields = [
58
- ["id", raw.id],
59
- ["title", raw.title],
60
- ["description", raw.description],
61
- ["subject", subject],
62
- ["actors", [actor]],
63
- ["execution", execution],
64
- ["scenario", Object.keys(scenario).length > 0 ? scenario : undefined],
65
- ["policies", raw.policies],
66
- ["review", raw.review],
67
- ["defaults", raw.defaults],
68
- ["comms", raw.comms],
69
- ];
70
- for (const [key, value] of fields)
71
- if (value !== undefined)
72
- v2[key] = value;
73
- return { ok: true, value: { route, v2, participantSource: source } };
25
+ /** The refusal for a study with no `actor` object. */
26
+ export const ACTOR_REQUIRED = "A study needs `actor:`, an object with at least `type`.";
27
+ /** Why `route` is not a route a study can declare, or undefined when it is one. */
28
+ export function routeNameReason(route) {
29
+ return ROUTES.some((name) => name === route)
30
+ ? undefined
31
+ : `A study needs \`route:\`, one of ${ROUTES.join(", ")}.`;
74
32
  }
75
33
  /**
76
34
  * The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
@@ -88,28 +46,26 @@ export function readStudyFront(raw, anySharedWorldParticipants = false) {
88
46
  if (unknownKey)
89
47
  return invalid(unknownKey);
90
48
  const route = ROUTES.find((name) => name === raw.route);
91
- if (route === undefined) {
92
- return invalid(`A study needs \`route:\`, one of ${ROUTES.join(", ")}.`);
93
- }
49
+ if (route === undefined)
50
+ return invalid(routeNameReason(raw.route));
94
51
  if (raw.mode !== undefined && raw.mode !== "dry-run" && raw.mode !== "live") {
95
52
  return invalid("`mode` must be `dry-run` or `live`. Leave it out for a dry run.");
96
53
  }
97
- if (!isRecord(raw.actor)) {
98
- return invalid("A study needs `actor:`, an object with at least `type`.");
99
- }
54
+ if (!isRecord(raw.actor))
55
+ return invalid(ACTOR_REQUIRED);
100
56
  if (raw.scenario !== undefined && typeof raw.scenario !== "string") {
101
57
  return invalid("`scenario` is a scenario id or a path to one, as a string.");
102
58
  }
103
59
  const participants = participantsOf(anySharedWorldParticipants && route === "shared-world" ? "computer-use" : route, raw.participants);
104
60
  if (!participants.ok)
105
61
  return participants;
106
- const surfaces = surfaceCount(route, raw.surfaces);
62
+ const surfaces = surfacesOf(route, raw.surfaces);
107
63
  if (!surfaces.ok)
108
64
  return surfaces;
109
65
  const capsReason = capsKeysReason(route, raw.caps);
110
66
  if (capsReason)
111
67
  return invalid(capsReason);
112
- // The v2 inert-field table does not list this one, but the terminal planner never reads it.
68
+ // The inert-field rows (warnings.ts) do not list this one; the terminal planner never reads it.
113
69
  if (route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined) {
114
70
  return invalid("route: terminal does not read `execution.timeoutMs`: `caps.maxMinutes` is the command deadline. Remove it.");
115
71
  }
@@ -118,24 +74,6 @@ export function readStudyFront(raw, anySharedWorldParticipants = false) {
118
74
  value: { route, actor: raw.actor, participants: participants.value, surfaces: surfaces.value },
119
75
  };
120
76
  }
121
- /**
122
- * Spell a v2 parser message the way the study file is written: `actors[0].lanes[3]` becomes the
123
- * `participants` entry it came from, `execution.caps` and `scenario.caps` become `caps`, and so on.
124
- */
125
- export function studySpelling(message, route, participantSource) {
126
- return (message
127
- .replaceAll(/actors\[0\]\.lanes\[(\d+)\]/g, (_match, index) => `participants[${participantSource[Number(index)] ?? index}]`)
128
- // "an `actors[0].lanes` roster" reads "a `participants` list" in a v3 file.
129
- .replaceAll(/\ban `actors\[0\]\.(lanes|roster)` roster\b/g, "a `participants` list")
130
- .replaceAll(/actors\[0\]\.(lanes|roster)\b/g, "participants")
131
- .replaceAll(/actors\[0\]\.laneFocus(\.instruction)?/g, "participants.instruction")
132
- .replaceAll(/actors\[0\]\.count\b/g, route === "scripted" ? "surfaces" : "participants")
133
- .replaceAll("actors[0]", "actor")
134
- .replaceAll(/\b(execution|scenario)\.caps\b/g, "caps")
135
- .replaceAll(/\bscenario\.mode\b/g, "mode")
136
- .replaceAll(/\bscenario\.ref\b/g, "scenario")
137
- .replaceAll("subject.topology: shared-world", "route: shared-world"));
138
- }
139
77
  // The v2 keys a hand-converted file is most likely to keep, each with where it went.
140
78
  function movedKeyReason(raw) {
141
79
  if (raw.actors !== undefined) {
@@ -212,7 +150,7 @@ function homogeneousOf(raw) {
212
150
  return { ok: true, value: participants };
213
151
  }
214
152
  // A list entry with `count: n` is a group: n participants `<id>-01` to `<id>-NN`, even when n is 1.
215
- // The v2 parser then checks every entry and the expanded ids for collisions.
153
+ // config.ts then checks every entry and the expanded ids for collisions.
216
154
  function entriesOf(raw) {
217
155
  const entries = [];
218
156
  const source = [];
@@ -241,17 +179,17 @@ function entriesOf(raw) {
241
179
  }
242
180
  return { ok: true, value: { entries, source } };
243
181
  }
244
- // Scripted surfaces become the v2 count: 1 is desktop, 2 adds mobile.
245
- function surfaceCount(route, raw) {
182
+ function surfacesOf(route, raw) {
246
183
  if (raw === undefined)
247
184
  return { ok: true, value: undefined };
248
185
  if (route !== "scripted") {
249
186
  return invalid(`\`surfaces\` belongs to route: scripted. This study's route is ${route}; remove it.`);
250
187
  }
251
- if (Array.isArray(raw) && raw.length === 1 && raw[0] === "desktop")
252
- return { ok: true, value: 1 };
188
+ if (Array.isArray(raw) && raw.length === 1 && raw[0] === "desktop") {
189
+ return { ok: true, value: ["desktop"] };
190
+ }
253
191
  if (Array.isArray(raw) && raw.length === 2 && raw[0] === "desktop" && raw[1] === "mobile") {
254
- return { ok: true, value: 2 };
192
+ return { ok: true, value: ["desktop", "mobile"] };
255
193
  }
256
194
  return invalid("`surfaces` must be [desktop] or [desktop, mobile].");
257
195
  }
@@ -268,10 +206,3 @@ function capsKeysReason(route, raw) {
268
206
  return undefined;
269
207
  return `route: ${route} reads only ${read.map((key) => `caps.${key}`).join(", ")}. Remove ${unread.map((key) => `caps.${key}`).join(", ")}.`;
270
208
  }
271
- // Add one key to a section. A section that is not an object passes through unchanged, so the v2
272
- // parser refuses it with its own message.
273
- function withKey(section, key, value) {
274
- if (section === undefined)
275
- return { [key]: value };
276
- return isRecord(section) ? { ...section, [key]: value } : section;
277
- }
@@ -1,4 +1,6 @@
1
1
  import type { StudyParseFailure, StudySubject } from "../types.js";
2
+ /** True when `raw` is a subject mapping whose `source` parseSubject accepts. */
3
+ export declare function hasSubjectSource(raw: unknown): boolean;
2
4
  export declare function parseSubject(raw: unknown): {
3
5
  ok: true;
4
6
  value: StudySubject;
@@ -3,31 +3,28 @@ import { ENV_NAME_PATTERN, invalid, posInt, str, strList } from "./values.js";
3
3
  import { parseEnvValues, parseState, subjectStateInvalidReason } from "./subject-state.js";
4
4
  import { isRecord } from "../../run/type-guards.js";
5
5
  import { urlCredentialReason } from "./url-credentials.js";
6
+ const SOURCES = [
7
+ "this-repo",
8
+ "clone",
9
+ "app-url",
10
+ "local-app",
11
+ "desktop-cli",
12
+ "terminal-product",
13
+ "local-tree",
14
+ ];
15
+ /** True when `raw` is a subject mapping whose `source` parseSubject accepts. */
16
+ export function hasSubjectSource(raw) {
17
+ return isRecord(raw) && SOURCES.some((name) => name === str(raw.source));
18
+ }
6
19
  export function parseSubject(raw) {
7
20
  if (!isRecord(raw)) {
8
21
  return invalid("`subject` is required and must be an object.");
9
22
  }
10
- const source = str(raw.source);
11
- if (source !== "this-repo" &&
12
- source !== "clone" &&
13
- source !== "app-url" &&
14
- source !== "local-app" &&
15
- source !== "desktop-cli" &&
16
- source !== "terminal-product" &&
17
- source !== "local-tree") {
23
+ const source = SOURCES.find((name) => name === str(raw.source));
24
+ if (source === undefined) {
18
25
  return invalid("`subject.source` must be one of: this-repo, clone, app-url, local-app, terminal-product, desktop-cli, local-tree.");
19
26
  }
20
27
  const subject = { source };
21
- // topology is enum-validated everywhere; its semantics (shared-world requires clone × e2b-desktop
22
- // × a ≥2 roster) are enforced in the shared-world cross-validation below, and a set-but-unconsumed
23
- // topology warns as inert off the shared-world route.
24
- if (raw.topology !== undefined) {
25
- const topology = str(raw.topology);
26
- if (topology !== "per-lane-worlds" && topology !== "shared-world") {
27
- return invalid("`subject.topology` must be per-lane-worlds (the default) or shared-world.");
28
- }
29
- subject.topology = topology;
30
- }
31
28
  // exposure is enum-validated everywhere; it is required on the concurrent shared-world route (the
32
29
  // getHost synthetic-subject attestation) and warns inert elsewhere.
33
30
  if (raw.exposure !== undefined) {
@@ -5,8 +5,7 @@ import { parseResolvedPersona, PersonaConfigError } from "./persona.js";
5
5
  import { prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
6
6
  import { digestText, redactText } from "../evidence/redaction.js";
7
7
  import { realpath } from "node:fs/promises";
8
- import { rosterOf } from "./parse/actors.js";
9
- import { actorsOf } from "./study-fields.js";
8
+ import { actorOf, participantList } from "./study-fields.js";
10
9
  /** Persona ids are file-name segments, never paths: the same grammar the terminal route enforces. */
11
10
  const PERSONA_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
12
11
  /** Title-case an id for the fallback display name (`skeptical-power-user` -> `Skeptical Power User`). */
@@ -87,13 +86,12 @@ export async function resolveCommittedPersonas(projectRoot, personaIds) {
87
86
  */
88
87
  export function studyPersonaIds(config) {
89
88
  const ids = [];
90
- for (const actor of actorsOf(config)) {
91
- if (actor.persona)
92
- ids.push(actor.persona);
93
- for (const entry of rosterOf(actor) ?? []) {
94
- if (entry.persona)
95
- ids.push(entry.persona);
96
- }
89
+ const persona = actorOf(config)?.persona;
90
+ if (persona)
91
+ ids.push(persona);
92
+ for (const entry of participantList(config) ?? []) {
93
+ if (entry.persona)
94
+ ids.push(entry.persona);
97
95
  }
98
96
  return [...new Set(ids)];
99
97
  }
@@ -2,7 +2,7 @@ import type { Brain, ComputerUsePlan, NonEmpty, PlannedAnalysis, ProvisionedSubj
2
2
  import type { StudyConfig } from "./types.js";
3
3
  export type Base = Omit<ComputerUsePlan, "route" | "actor" | "runner" | "concurrency" | "sessionBudgetMs" | "sandboxMs" | "caps" | "rerun">;
4
4
  export declare function isNonEmpty<T>(values: readonly T[]): values is NonEmpty<T>;
5
- export declare function planCaps(config: StudyConfig, route: "computer-use" | "shared-world"): ComputerUsePlan["caps"];
5
+ export declare function planCaps(config: StudyConfig): ComputerUsePlan["caps"];
6
6
  export declare function provisionedSubject(config: StudyConfig): ProvisionedSubject | undefined;
7
7
  /** Keys, env and local tools a live run checks right before it acquires anything. */
8
8
  export declare function desktopRequirements(config: StudyConfig, args: {
@@ -14,11 +14,10 @@ function deepFreeze(value, frozen = new WeakSet()) {
14
14
  return value;
15
15
  }
16
16
  function residualOf(config) {
17
- const { comms, policies, personas, defaults, review } = config;
17
+ const { comms, policies, defaults, review } = config;
18
18
  return deepFreeze(structuredClone({
19
19
  ...(comms === undefined ? {} : { comms }),
20
20
  ...(policies === undefined ? {} : { policies }),
21
- ...(personas === undefined ? {} : { personas }),
22
21
  ...(defaults === undefined ? {} : { defaults }),
23
22
  ...(review === undefined ? {} : { review }),
24
23
  ...(config.execution?.desktop === undefined && config.execution?.target === undefined
@@ -44,8 +43,8 @@ function residualOf(config) {
44
43
  export function isNonEmpty(values) {
45
44
  return values.length > 0;
46
45
  }
47
- export function planCaps(config, route) {
48
- const caps = capsOf(config, route);
46
+ export function planCaps(config) {
47
+ const caps = capsOf(config);
49
48
  return {
50
49
  ...(caps?.maxUsd === undefined ? {} : { maxUsd: caps.maxUsd }),
51
50
  ...(caps?.maxTotalUsd === undefined ? {} : { maxTotalUsd: caps.maxTotalUsd }),
@@ -9,7 +9,7 @@ export interface Participant {
9
9
  readonly id: string;
10
10
  /** 0-based position in the roster. Bundle `sim-NNN` and `stream-NNN` ids derive from it. */
11
11
  readonly index: number;
12
- /** `lanes[i].persona ?? actors[0].persona`. The route compiles the committed persona file. */
12
+ /** `participants[i].persona ?? actor.persona`. The route compiles the committed persona file. */
13
13
  readonly personaId: string | undefined;
14
14
  /** The declared mission and per-participant focus. An absent mission takes the route's default. */
15
15
  readonly assignment: {
@@ -30,7 +30,7 @@ interface DesktopParticipant extends Participant {
30
30
  readonly preset: DevicePreset;
31
31
  readonly resolution: [number, number];
32
32
  };
33
- /** The `lanes[]` entry's value, else actor value, else absent (the provider default is recorded in the trace). */
33
+ /** The `participants` entry's value, else actor value, else absent (the provider default is recorded in the trace). */
34
34
  readonly limits: {
35
35
  readonly stopWhen?: StopWhen;
36
36
  readonly dwell?: DwellWindow;
@@ -8,7 +8,7 @@ import type { RunStudyProvenance } from "../run/study-provenance.js";
8
8
  import type { ComputerUseRefusal } from "../routes/computer-use/plan.js";
9
9
  import type { SharedWorldRefusal } from "../routes/shared-world/plan.js";
10
10
  import type { ComputerUseParticipant, ExternalPublicParticipant, ProvisionedParticipant } from "./plan-participants.js";
11
- import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth, StudyScenarioCaps, StudySubjectProduct, StudySubjectServe, StudySubjectState, StudySubjectStateCheckpoint } from "./types.js";
11
+ import type { StudyConfig, StudyExecutionTerminal, StudyRuntimeAuth, StudyCaps, StudySubjectProduct, StudySubjectServe, StudySubjectState, StudySubjectStateCheckpoint } from "./types.js";
12
12
  export type NonEmpty<T> = readonly [T, ...T[]];
13
13
  type AtLeastTwo<T> = readonly [T, T, ...T[]];
14
14
  /**
@@ -48,7 +48,7 @@ export type Requirement = {
48
48
  * VM study read it before a plan exists. Shared world, terminal and scripted still read some config
49
49
  * fields at run time.
50
50
  */
51
- export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "personas" | "defaults" | "review"> & {
51
+ export type ResidualConfig = Pick<StudyConfig, "comms" | "policies" | "defaults" | "review"> & {
52
52
  readonly execution?: Pick<NonNullable<StudyConfig["execution"]>, "desktop" | "target">;
53
53
  readonly subject: Pick<StudyConfig["subject"], "clone" | "localTree" | "repos" | "envValues">;
54
54
  };
@@ -83,7 +83,7 @@ interface PreviewPlan extends PlanBase {
83
83
  }
84
84
  /**
85
85
  * The model driving a desktop participant. `caller` is the library caller's createProvider.
86
- * `declaredModel` is actors[0].model as the study wrote it, absent when undeclared: the providers
86
+ * `declaredModel` is actor.model as the study wrote it, absent when undeclared: the providers
87
87
  * take it as written, and spend is priced at it, else the default (pricedModel). An openai
88
88
  * brain's `model` is the one its provider runs, with that default applied.
89
89
  */
@@ -229,10 +229,10 @@ export type TerminalPlan = PlanBase & {
229
229
  readonly stdin?: NonNullable<StudyExecutionTerminal["stdin"]>;
230
230
  } & ({
231
231
  readonly dryRun: true;
232
- readonly caps?: StudyScenarioCaps;
232
+ readonly caps?: StudyCaps;
233
233
  } | {
234
234
  readonly dryRun: false;
235
- readonly caps: StudyScenarioCaps & {
235
+ readonly caps: StudyCaps & {
236
236
  readonly maxUsd: number;
237
237
  readonly maxMinutes: number;
238
238
  };
@@ -1,5 +1,5 @@
1
1
  // The route decision. A study's route follows from its composition (subject.source,
2
- // execution.target, the first actor's registered run kind, subject.topology), never from a
2
+ // execution.target, the actor's registered run kind, `route: shared-world`), never from a
3
3
  // declared kind. This is the only function that decides it.
4
4
  import { resolveAutomaticAnalysis } from "../analysis/automatic-config.js";
5
5
  import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/plan.js";
@@ -25,7 +25,7 @@ export async function runPublicPreviewPreflight(ctx) {
25
25
  { name: "route", ok: false, message: routeError },
26
26
  ]);
27
27
  }
28
- const rosterTargets = ctx.targets.filter((target) => target.kind === "actors[0].lanes[].target");
28
+ const rosterTargets = ctx.targets.filter((target) => target.kind === "participants[].target");
29
29
  const publicTargets = rosterTargets.length > 0
30
30
  ? rosterTargets
31
31
  : ctx.targets.filter((target) => target.kind === "subject.appUrl");
@@ -270,7 +270,7 @@ function targetUrlFor(config, target) {
270
270
  if (target.kind === "subject.appUrl" && config.subject.appUrl) {
271
271
  return config.subject.appUrl;
272
272
  }
273
- if (target.kind === "actors[0].lanes[].target") {
273
+ if (target.kind === "participants[].target") {
274
274
  const rosterTarget = participantList(config)?.find((entry) => entry.target && digest(entry.target) === target.targetDigest)?.target;
275
275
  if (rosterTarget)
276
276
  return rosterTarget;
@@ -12,7 +12,7 @@ export interface StudyPreflightCheck {
12
12
  }
13
13
  export interface StudyPreflightTarget {
14
14
  label: string;
15
- kind: "subject.appUrl" | "actors[0].lanes[].target" | "subject.serve.url" | "subject.product.publicSurface";
15
+ kind: "subject.appUrl" | "participants[].target" | "subject.serve.url" | "subject.product.publicSurface";
16
16
  targetDigest: string;
17
17
  originDigest?: string;
18
18
  loopback: boolean;
@@ -84,7 +84,7 @@ function collectTargets(config) {
84
84
  }
85
85
  for (const [index, entry] of (participantList(config) ?? []).entries()) {
86
86
  if (entry.target) {
87
- targets.push(makeTarget(`actors[0].lanes[${index}].target`, "actors[0].lanes[].target", entry.target));
87
+ targets.push(makeTarget(`participants[${index}].target`, "participants[].target", entry.target));
88
88
  }
89
89
  }
90
90
  if (config.subject.serve?.url) {
@@ -9,8 +9,8 @@ export declare function registeredScriptedBrowserActors(): string[];
9
9
  export declare function actorResolvesToTerminal(type: string | undefined): boolean;
10
10
  export declare function registeredTerminalActors(): string[];
11
11
  /**
12
- * The declared fan-out participant count on the computer-use route: a `lanes[]` roster's length, else
13
- * a homogeneous `count`, else 1. The single source of truth shared by the parser, the engine,
12
+ * The declared fan-out participant count on the computer-use route: a `participants` list's
13
+ * length, else a participant count, else 1. The single source of truth shared by the parser, the engine,
14
14
  * and the pre-flight plan so the participant count is computed the same way everywhere.
15
15
  */
16
16
  export declare function computerUseParticipantCount(config: StudyConfig): number;
@@ -36,15 +36,15 @@ export declare function isComputerUseComposition(config: StudyConfig): boolean;
36
36
  * `shared-world` topology. Mirror of isComputerUseComposition; the single source of truth shared by
37
37
  * routeOf (which checks it before the computer-use route) and the warning logic. Every
38
38
  * shared-world study runs its participants at once (`execution.concurrency` >= 2). The same
39
- * clone/local-tree × e2b-desktop × computer-use composition without `topology: shared-world` stays per-lane-worlds
40
- * (the computer-use route); the topology declaration is the override switch.
39
+ * clone/local-tree × e2b-desktop × computer-use composition without `route: shared-world` stays per-lane-worlds
40
+ * (the computer-use route); the route declaration is the override switch.
41
41
  */
42
42
  export declare function isSharedWorldComposition(config: StudyConfig): boolean;
43
43
  /** The getHost provisioned-subject shared-world shape (clone/local-tree served + exposed in-sandbox). */
44
44
  export declare function isProvisionedSharedWorldComposition(config: StudyConfig): boolean;
45
45
  /**
46
46
  * The external-public shared-world shape: a real public deployment used directly as
47
- * the shared plane: `source: app-url` + `topology: shared-world` + a computer-use actor on
47
+ * the shared plane: `source: app-url` + `route: shared-world` + a computer-use actor on
48
48
  * e2b-desktop + `policies.allowPublicTargets: true`. No getHost, no clone, no subject sandbox, no
49
49
  * seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
50
50
  * externalPublicSharedWorldValidationReason, not here; this predicate is the router only, so a
@@ -46,8 +46,8 @@ export function registeredTerminalActors() {
46
46
  return registeredActorsOn("terminal");
47
47
  }
48
48
  /**
49
- * The declared fan-out participant count on the computer-use route: a `lanes[]` roster's length, else
50
- * a homogeneous `count`, else 1. The single source of truth shared by the parser, the engine,
49
+ * The declared fan-out participant count on the computer-use route: a `participants` list's
50
+ * length, else a participant count, else 1. The single source of truth shared by the parser, the engine,
51
51
  * and the pre-flight plan so the participant count is computed the same way everywhere.
52
52
  */
53
53
  export function computerUseParticipantCount(config) {
@@ -96,8 +96,8 @@ export function isComputerUseComposition(config) {
96
96
  * `shared-world` topology. Mirror of isComputerUseComposition; the single source of truth shared by
97
97
  * routeOf (which checks it before the computer-use route) and the warning logic. Every
98
98
  * shared-world study runs its participants at once (`execution.concurrency` >= 2). The same
99
- * clone/local-tree × e2b-desktop × computer-use composition without `topology: shared-world` stays per-lane-worlds
100
- * (the computer-use route); the topology declaration is the override switch.
99
+ * clone/local-tree × e2b-desktop × computer-use composition without `route: shared-world` stays per-lane-worlds
100
+ * (the computer-use route); the route declaration is the override switch.
101
101
  */
102
102
  export function isSharedWorldComposition(config) {
103
103
  return (isProvisionedSharedWorldComposition(config) || isExternalPublicSharedWorldComposition(config));
@@ -111,7 +111,7 @@ export function isProvisionedSharedWorldComposition(config) {
111
111
  }
112
112
  /**
113
113
  * The external-public shared-world shape: a real public deployment used directly as
114
- * the shared plane: `source: app-url` + `topology: shared-world` + a computer-use actor on
114
+ * the shared plane: `source: app-url` + `route: shared-world` + a computer-use actor on
115
115
  * e2b-desktop + `policies.allowPublicTargets: true`. No getHost, no clone, no subject sandbox, no
116
116
  * seed. The operator-ownership attestation `subject.publicTarget` is required (validated in
117
117
  * externalPublicSharedWorldValidationReason, not here; this predicate is the router only, so a
@@ -4,7 +4,7 @@ import type { StudyConfig } from "./types.js";
4
4
  import { type StudyEvent } from "./run-study-events.js";
5
5
  type Refusal = {
6
6
  ok: false;
7
- code: "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED";
7
+ code: "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID";
8
8
  message: string;
9
9
  };
10
10
  type Normalized = {
@@ -27,7 +27,7 @@ export interface StudyDeps {
27
27
  readonly subjectPhaseSink?: (event: SubjectPhaseEvent, participant?: ParticipantRef) => void;
28
28
  /**
29
29
  * Shared world: runs each participant's computer-use session in place of the actor's own. The
30
- * planner reads whether it is set: a custom runner cannot enforce actors[0].maxOutputTokens.
30
+ * planner reads whether it is set: a custom runner cannot enforce actor.maxOutputTokens.
31
31
  */
32
32
  readonly runSession?: (options: CuaActorSessionOptions) => Promise<CuaLoopResult>;
33
33
  /**
@@ -1,28 +1,19 @@
1
- import type { StudyRoute } from "./routing.js";
2
- import type { StudyActor, StudyConfig, StudyParticipantEntry, StudyScenario, StudyScenarioCaps } from "./types.js";
3
- /** The actor that runs. A v3 file declares it as `actor`. */
1
+ import type { StudyActor, StudyCaps, StudyConfig, StudyParticipantEntry } from "./types.js";
2
+ /** The actor that runs. */
4
3
  export declare function actorOf(config: StudyConfig): StudyActor | undefined;
5
- /**
6
- * Every declared actor. parseStudy accepts exactly one; a library config may declare more, and the
7
- * checks that refuse a second actor's settings read them all.
8
- */
9
- export declare function actorsOf(config: StudyConfig): readonly StudyActor[];
10
- /** The participants listed one by one: a v3 `participants` list. Undefined when none is listed. */
4
+ /** The participants listed one by one: a `participants` list. Undefined when none is listed. */
11
5
  export declare function participantList(config: StudyConfig): readonly StudyParticipantEntry[] | undefined;
12
- /** The declared number of identical participants: v3 `participants: <n>` or `participants.count`. */
6
+ /** The declared number of identical participants: `participants: <n>` or `participants.count`. */
13
7
  export declare function declaredParticipantCount(config: StudyConfig): number | undefined;
14
- /** The steer each of a counted group of participants gets: v3 `participants.instruction`. */
8
+ /** The steer each of a counted group of participants gets: `participants.instruction`. */
15
9
  export declare function participantInstruction(config: StudyConfig): string | undefined;
16
- /** How many scripted surfaces run, 1 for desktop and 2 with mobile. A v3 file lists `surfaces`. */
10
+ /** How many scripted surfaces run, 1 for desktop and 2 with mobile. */
17
11
  export declare function surfaceCount(config: StudyConfig): number | undefined;
18
- /** The declared run mode. A v3 file declares it as `mode`. */
19
- export declare function modeOf(config: StudyConfig): StudyScenario["mode"];
20
- /** The scripted route's scenario id or path. A v3 file declares it as `scenario`. */
12
+ /** The declared run mode. */
13
+ export declare function modeOf(config: StudyConfig): StudyConfig["mode"];
14
+ /** The scripted route's scenario id or path. */
21
15
  export declare function scenarioRefOf(config: StudyConfig): string | undefined;
22
- /**
23
- * The spend caps a route enforces. A v3 file declares them as `caps`. The v2 shape keeps them in
24
- * `execution.caps` on computer use and shared world, and in `scenario.caps` on terminal.
25
- */
26
- export declare function capsOf(config: StudyConfig, route: Extract<StudyRoute, "computer-use" | "shared-world" | "terminal">): StudyScenarioCaps | undefined;
27
- /** True when the participants share one app. A v3 file declares `route: shared-world`. */
16
+ /** The spend caps the route enforces. parseStudy refuses a key the route does not read. */
17
+ export declare function capsOf(config: StudyConfig): StudyCaps | undefined;
18
+ /** True when the participants share one app: `route: shared-world`. */
28
19
  export declare function declaresSharedWorld(config: StudyConfig): boolean;