humanish 0.110.4 → 0.111.1

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 (145) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTEXT.md +2 -2
  3. package/dist/actors/codex/restricted-participant-policy.d.ts +2 -0
  4. package/dist/actors/codex/restricted-participant-policy.js +30 -2
  5. package/dist/actors/codex/restricted-participant.js +1 -1
  6. package/dist/actors/computer-use/executor-diagnostic.d.ts +15 -0
  7. package/dist/actors/computer-use/executor-diagnostic.js +105 -0
  8. package/dist/actors/computer-use/executor-error.d.ts +18 -3
  9. package/dist/actors/computer-use/executor-error.js +30 -4
  10. package/dist/actors/computer-use/loop/actions.d.ts +6 -3
  11. package/dist/actors/computer-use/loop/actions.js +27 -7
  12. package/dist/actors/computer-use/loop/ending.js +9 -1
  13. package/dist/actors/computer-use/loop/types.d.ts +14 -0
  14. package/dist/actors/computer-use/loop.d.ts +1 -1
  15. package/dist/actors/computer-use/loop.js +1 -1
  16. package/dist/actors/computer-use/openai-context.d.ts +8 -2
  17. package/dist/actors/computer-use/openai-context.js +91 -26
  18. package/dist/actors/computer-use/openai-provider.js +55 -12
  19. package/dist/actors/computer-use/openai-requests.d.ts +2 -1
  20. package/dist/actors/computer-use/openai-requests.js +4 -1
  21. package/dist/actors/contract.d.ts +7 -1
  22. package/dist/actors/local-agent/claude-session.js +1 -1
  23. package/dist/actors/local-agent/cli.js +1 -1
  24. package/dist/actors/scripted-browser/actor.d.ts +2 -2
  25. package/dist/browser-control/client.js +5 -5
  26. package/dist/browser-control/protocol.d.ts +30 -1
  27. package/dist/browser-control/protocol.js +19 -2
  28. package/dist/browser-control/transport.d.ts +3 -2
  29. package/dist/browser-control/transport.js +12 -7
  30. package/dist/cli/commands/study-route-computer-use.js +2 -2
  31. package/dist/cli/commands/study-route-shared-world.js +2 -2
  32. package/dist/cli/commands/study.js +1 -4
  33. package/dist/cli/doctor.js +11 -0
  34. package/dist/evidence/encoded-text.d.ts +3 -3
  35. package/dist/evidence/encoded-text.js +10 -5
  36. package/dist/evidence/redaction.d.ts +2 -2
  37. package/dist/evidence/redaction.js +18 -7
  38. package/dist/guest/browser-tools.js +5 -1
  39. package/dist/guest/chromium-text.js +42 -18
  40. package/dist/guest/desktop-executor.js +5 -1
  41. package/dist/keys/key-resolution.d.ts +3 -0
  42. package/dist/keys/key-resolution.js +15 -2
  43. package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
  44. package/dist/routes/computer-use/bundle-parts.js +7 -2
  45. package/dist/routes/computer-use/fanout-bundle.js +4 -2
  46. package/dist/routes/computer-use/local-vm.js +1 -2
  47. package/dist/routes/computer-use/participant-model.js +7 -2
  48. package/dist/routes/computer-use/plan.js +7 -10
  49. package/dist/routes/computer-use/preflight.js +1 -1
  50. package/dist/routes/computer-use/result.js +1 -1
  51. package/dist/routes/computer-use/route.js +2 -3
  52. package/dist/routes/computer-use/setup.js +2 -2
  53. package/dist/routes/computer-use/types.d.ts +4 -4
  54. package/dist/routes/preview.js +1 -0
  55. package/dist/routes/scripted/plan.js +4 -5
  56. package/dist/routes/scripted/route.js +3 -3
  57. package/dist/routes/scripted/scenario.d.ts +1 -1
  58. package/dist/routes/scripted/scenario.js +3 -3
  59. package/dist/routes/scripted/types.d.ts +2 -2
  60. package/dist/routes/shared-world/handoff.js +1 -1
  61. package/dist/routes/shared-world/participant-specs.js +1 -1
  62. package/dist/routes/shared-world/plan.js +6 -8
  63. package/dist/routes/shared-world/route.js +2 -2
  64. package/dist/routes/shared-world/setup.js +1 -0
  65. package/dist/routes/shared-world/types.d.ts +1 -1
  66. package/dist/routes/terminal/bundle.d.ts +3 -3
  67. package/dist/routes/terminal/dry-run.js +1 -0
  68. package/dist/routes/terminal/ledger.d.ts +2 -2
  69. package/dist/routes/terminal/lifetime.js +1 -1
  70. package/dist/routes/terminal/live-finish.js +25 -6
  71. package/dist/routes/terminal/live-sandbox.js +5 -1
  72. package/dist/routes/terminal/plan.js +3 -4
  73. package/dist/routes/terminal/recorder.d.ts +6 -0
  74. package/dist/routes/terminal/recorder.js +61 -6
  75. package/dist/routes/terminal/route.js +2 -3
  76. package/dist/routes/terminal/session.js +2 -1
  77. package/dist/routes/terminal/token-usage.d.ts +3 -0
  78. package/dist/routes/terminal/token-usage.js +5 -0
  79. package/dist/routes/terminal/types.d.ts +3 -2
  80. package/dist/routes/terminal/types.js +2 -1
  81. package/dist/run/dry-run.js +1 -0
  82. package/dist/run/results.d.ts +3 -1
  83. package/dist/run/run.d.ts +2 -0
  84. package/dist/run/run.js +16 -2
  85. package/dist/run/terminal-contract.d.ts +10 -0
  86. package/dist/run/terminal-contract.js +27 -9
  87. package/dist/run-study.d.ts +5 -3
  88. package/dist/run-study.js +7 -11
  89. package/dist/study/adapter-scorer-loader.d.ts +1 -1
  90. package/dist/study/adapter-scorer-loader.js +3 -3
  91. package/dist/study/composition-rules.js +36 -27
  92. package/dist/study/config.d.ts +29 -13
  93. package/dist/study/config.js +174 -157
  94. package/dist/study/discover.d.ts +1 -1
  95. package/dist/study/discover.js +1 -1
  96. package/dist/study/doctor.js +4 -5
  97. package/dist/study/keys.d.ts +250 -2
  98. package/dist/study/keys.js +32 -62
  99. package/dist/study/migrate/convert.js +3 -4
  100. package/dist/study/migrate/v2.d.ts +22 -7
  101. package/dist/study/migrate/v2.js +173 -23
  102. package/dist/study/parse/actors.d.ts +11 -32
  103. package/dist/study/parse/actors.js +11 -156
  104. package/dist/study/parse/comms.js +6 -2
  105. package/dist/study/parse/execution.d.ts +9 -4
  106. package/dist/study/parse/execution.js +4 -39
  107. package/dist/study/parse/{study-v3.d.ts → front.d.ts} +6 -17
  108. package/dist/study/parse/{study-v3.js → front.js} +24 -112
  109. package/dist/study/parse/subject.d.ts +2 -0
  110. package/dist/study/parse/subject.js +15 -18
  111. package/dist/study/parse/url-credentials.d.ts +3 -3
  112. package/dist/study/parse/url-credentials.js +155 -13
  113. package/dist/study/persona-resolve.js +7 -9
  114. package/dist/study/plan-base.d.ts +1 -1
  115. package/dist/study/plan-base.js +5 -7
  116. package/dist/study/plan-participants.d.ts +2 -2
  117. package/dist/study/plan-participants.js +3 -3
  118. package/dist/study/plan-types.d.ts +7 -5
  119. package/dist/study/plan.js +12 -6
  120. package/dist/study/preflight-probes.js +2 -2
  121. package/dist/study/preflight.d.ts +1 -1
  122. package/dist/study/preflight.js +1 -1
  123. package/dist/study/routing.d.ts +5 -5
  124. package/dist/study/routing.js +16 -16
  125. package/dist/study/run-study-options.d.ts +1 -1
  126. package/dist/study/run-study-options.js +2 -2
  127. package/dist/study/study-deps.d.ts +1 -1
  128. package/dist/study/study-fields.d.ts +4 -25
  129. package/dist/study/study-fields.js +15 -44
  130. package/dist/study/summary.js +13 -8
  131. package/dist/study/types.d.ts +51 -128
  132. package/dist/study/types.js +0 -1
  133. package/dist/study/url-credentials.js +3 -4
  134. package/dist/study/validation.d.ts +2 -10
  135. package/dist/study/validation.js +23 -53
  136. package/dist/study/warnings.d.ts +11 -3
  137. package/dist/study/warnings.js +56 -110
  138. package/dist/substrates/local/runtime-config.js +11 -13
  139. package/docs/architecture/browser-control.md +12 -2
  140. package/docs/architecture/guest-desktop.md +7 -1
  141. package/docs/architecture/terminal-product-route.md +3 -2
  142. package/docs/contracts/schemas.md +39 -9
  143. package/package.json +1 -1
  144. package/dist/study/parse/study.d.ts +0 -38
  145. package/dist/study/parse/study.js +0 -225
@@ -1,204 +1,224 @@
1
- // The normalized config, in the humanish.lab.v2 spelling: a study composes code primitives, with no
2
- // hardcoded study kind.
3
- // A humanish.study.v3 file is rewritten into this spelling first (parse/study-v3.ts), and then must
4
- // take the route it declares and set no field that route does not read.
1
+ // The study parser: a humanish.study.v3 document into a StudyConfig with the file's own keys.
2
+ // parse/front.ts checks what the document declares before its sections parse (the route, the
3
+ // actor object, the participants or surfaces form the route takes, and the caps keys it reads). The
4
+ // sections parse here, checkStudyConfig runs the checks that read more than one section, and the
5
+ // study must then take the route it declares and set no field that route does not read
6
+ // (warnings.ts). A humanish.lab.v2 document is refused; `humanish migrate` reads it
7
+ // (migrate/v2.ts).
5
8
  //
6
- // Scope (read before trusting field names): the engine routes by
7
- // subject.source × execution.target (disambiguated by the actor's run kind where both axes
8
- // collide) and consumes a deliberately small set of fields:
9
- // subject.source/repos/appUrl/serve/env/state/clone.{depth,fanout,keep}, actors[0].count,
10
- // execution.target + execution.desktop.codexAppServer, scenario.mode,
11
- // policies.redactRepos, defaults.open.
12
- // On the computer-use routes (app-url × e2b-desktop, and clone × e2b-desktop with a
13
- // computer-use actor), `actors[0].type` selects the actor: it must resolve to a registered
14
- // computer-use actor, and that descriptor runs the session. Those routes also consume
15
- // actors[0].{mission,persona,laneFocus.instruction,model,reasoningEffort}, execution.timeoutMs,
16
- // execution.desktop.{browser,resolution,sandboxTimeoutMs}, and (clone)
17
- // subject.{serve,env,state,clone.depth}.
18
- // On the scripted-browser route (app-url × local-or-absent, or clone × e2b-desktop, with a
19
- // registered scripted-browser actor), `actors[0].type` selects the actor too, and the route
20
- // consumes scenario.ref (required there, because the committed scenario's browser steps are what the
21
- // actor executes), actors[0].{persona,count}, and execution.timeoutMs. On the provisioned
22
- // clone slice it also consumes subject.{repos,serve,env,state,exposure,clone.depth} and
23
- // execution.desktop.template. actors[0].{mission,laneFocus,model} are inert on that route
24
- // because no model runs, and most execution.desktop.* fields remain forward-declared (device
25
- // presets belong to the computer-use route; scripted surfaces are the driver's own desktop/mobile
26
- // viewports where isMobile/DSF genuinely render via playwright emulation).
27
- // On the other routes those fields remain forward-declared and are not yet consumed:
28
- // parseStudy emits a warning listing any such field that is set, so `study show` shows
29
- // the truth.
9
+ // Scope (read before trusting field names): routeOf derives the route from
10
+ // subject.source × execution.target × the actor's run kind, with `route: shared-world` as the
11
+ // declaration that turns a computer-use composition into a shared world. On the computer-use
12
+ // routes (app-url × e2b-desktop or local, and clone, local-tree or desktop-cli on e2b-desktop with a
13
+ // computer-use actor), `actor.type` must resolve to a registered computer-use actor, and that
14
+ // descriptor runs the session. Those routes consume actor.{mission,persona,model,reasoningEffort},
15
+ // `participants`, `caps`, execution.timeoutMs and execution.desktop.*. The scripted route
16
+ // (app-url × local, or clone × e2b-desktop, with a registered scripted-browser actor) consumes
17
+ // `scenario` (required: its browser steps are what the actor runs), actor.persona and `surfaces`.
18
+ // The terminal route consumes subject.product, `caps` and execution.{terminal,runtimeAuth}.
30
19
  //
31
- // NOTE on actors[0].count: it carries route-specific meanings. Preview route: simCount;
32
- // scripted-browser route: surface roster {1 = desktop, 2 = desktop + mobile}, default 1 (the
33
- // defaults-table single-participant row governs; count: 2 is the declared override); computer-use
34
- // E2B route: the homogeneous fan-out participant count (N identical participants, each its own E2B
35
- // desktop), capped at 16; the in-process/local-app computer-use route stays single-participant (no
36
- // E2B to fan out).
37
- //
38
- // NOTE on actors[0].lanes / actors[0].roster (computer-use E2B route): a
39
- // differentiated fan-out roster: each `{ id?, persona?, device?, instruction?, target? }` becomes one
40
- // independent E2B desktop (`per-lane-worlds`, the default topology). `roster[]` is parser sugar for
41
- // repeated groups and is normalized into `lanes[]` before the engine sees it. `lanes|roster` XOR
42
- // `count` (declare a differentiated roster or a homogeneous count, never both); `lanes|roster`
43
- // XOR `actors[0].laneFocus` (each entry's `instruction` is the roster's steer); `lanes[].device` XOR
44
- // raw `execution.desktop.resolution`. `execution.concurrency` bounds in-flight participants (default:
45
- // the participant count, all at once; env HUMANISH_CUA_MAX_CONCURRENCY may only lower it, per invariant
46
- // 3). On every non-computer-use route normalized `lanes` are inert (warned). subject.clone.fanout
47
- // is rejected on the computer-use route. `lanes[].target` is app-url × computer-use only: an
48
- // absolute browser URL this participant opens instead of `subject.appUrl`; it is the generic
49
- // setup-produced-target handoff, not a service topology primitive.
50
- //
51
- // There is deliberately no v1 compatibility: v1 had zero real users. Breaking schema changes
52
- // bump the version.
20
+ // NOTE on `participants`: a count (preview: the simulated count; computer use: N identical
21
+ // participants, each its own desktop, capped at 16), `{ count, instruction }` (identical
22
+ // participants with one steer), or a list of `{ id?, persona?, device?, instruction?, target? ... }`
23
+ // entries, each its own desktop. A list entry with a `count` is a group that expands into
24
+ // `<id>-01` to `<id>-NN`. A shared world takes a list of at least two. `participants[].device`
25
+ // excludes a raw `execution.desktop.resolution`, and `participants[].target` is app-url computer
26
+ // use only: an absolute browser URL this participant opens in place of `subject.appUrl`.
27
+ // `execution.concurrency` bounds in-flight participants (default: all at once; env
28
+ // HUMANISH_CUA_MAX_CONCURRENCY may only lower it).
53
29
  import { isLocalBrowserStudy, localBrowserDefaults, localBrowserUnsupportedReason, } from "../substrates/local/runtime-config.js";
54
30
  import { isRecord } from "../run/type-guards.js";
55
31
  import { compositionReason } from "./composition-rules.js";
56
- import { findUnknownV2Key } from "./keys.js";
57
- import { parseActors, rosterOf } from "./parse/actors.js";
32
+ import { V2_SCHEMA, V2_UNSUPPORTED_MESSAGE, v2FieldMoves } from "./migrate/v2.js";
33
+ import { parseActorFields, parseActorType, parseParticipantEntries } from "./parse/actors.js";
58
34
  import { parseComms, recipientParticipantId } from "./parse/comms.js";
59
- import { parseDefaults, parseExecution, parsePersonas, parsePolicies, parseReview, parseScenario, } from "./parse/execution.js";
60
- import { studySpelling, studyToV2 } from "./parse/study-v3.js";
35
+ import { parseCaps, parseDefaults, parseExecution, parsePolicies, parseReview, } from "./parse/execution.js";
36
+ import { ACTOR_REQUIRED, readStudyFront, routeNameReason, } from "./parse/front.js";
61
37
  import { parseSubject } from "./parse/subject.js";
62
38
  import { invalid, optionalStr, str } from "./parse/values.js";
63
39
  import { isComputerUseComposition, isSharedWorldComposition, routeOf } from "./routing.js";
64
40
  import { declaredParticipantIds } from "./plan-participants.js";
65
- import { ID_PATTERN, V2_SCHEMA, STUDY_SCHEMA, } from "./types.js";
66
- import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
67
- import { forwardDeclaredWarnings, inertFieldLabels } from "./warnings.js";
41
+ import { declaredParticipantCount, participantList } from "./study-fields.js";
42
+ import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
43
+ import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
44
+ import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
68
45
  import { plural } from "../run/text.js";
69
- /** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
70
- export const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
71
46
  /**
72
47
  * The package's study parser (src/index.ts) and the one discovery uses: a humanish.study.v3
73
48
  * document. It refuses a humanish.lab.v2 one with HUMANISH_STUDY_V2_UNSUPPORTED.
74
49
  */
75
50
  export function parseStudy(raw) {
76
- if (isRecord(raw) && raw.schema === V2_SCHEMA)
51
+ if (!isRecord(raw))
52
+ return invalid("A study file must be a YAML object.");
53
+ if (raw.schema === V2_SCHEMA)
77
54
  return {
78
55
  ok: false,
79
56
  error: { code: "HUMANISH_STUDY_V2_UNSUPPORTED", message: V2_UNSUPPORTED_MESSAGE },
80
57
  };
81
- return parseStudyDocument(raw);
82
- }
83
- /**
84
- * Validate a parsed YAML object, humanish.study.v3 or humanish.lab.v2, into a StudyConfig. Pure:
85
- * the caller owns file IO. Structural validation only. Fields the engine does not yet consume are
86
- * accepted but reported in `warnings` so `study show` never silently swallows a setting that does
87
- * nothing. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
88
- */
89
- export function parseStudyDocument(raw) {
90
- if (!isRecord(raw)) {
91
- return invalid("A study file must be a YAML object.");
92
- }
93
- if (raw.schema === STUDY_SCHEMA)
94
- return parseV3(raw);
95
- if (raw.schema !== V2_SCHEMA) {
58
+ if (raw.schema !== STUDY_SCHEMA) {
96
59
  return invalid(`The study schema must be ${STUDY_SCHEMA} or ${V2_SCHEMA}.`);
97
60
  }
98
- return parseV2(raw);
61
+ return parseStudyV3(raw);
99
62
  }
100
63
  /**
101
- * A v3 study parses through the v2 parser, then must take the route it declares and set no field
102
- * that route does not read. A v2 file gets those fields as a warning instead.
64
+ * Validate a humanish.study.v3 document into a StudyConfig. Pure: the caller owns file IO. Fields the
65
+ * route does not read refuse the study; migrate's report mode lists them in `inert` instead.
103
66
  */
104
- function parseV3(raw) {
105
- const document = studyToV2(raw);
106
- if (!document.ok)
107
- return document;
108
- const { route, v2, participantSource } = document.value;
109
- const spell = (message) => studySpelling(message, route, participantSource);
110
- const parsed = parseV2(v2);
111
- if (!parsed.ok)
112
- return invalid(spell(parsed.error.message));
113
- const taken = routeOf(parsed.config);
114
- if (taken !== route) {
115
- return invalid(`This study declares route: ${route}, but its subject (source: ${parsed.config.subject.source}) and actor (type: ${parsed.config.actors[0]?.type}) take the ${taken} route. Change \`route\`, or change the subject or actor.`);
67
+ export function parseStudyV3(raw, options = {}) {
68
+ const front = readStudyFront(raw, options.inert === "report");
69
+ if (!front.ok)
70
+ return front;
71
+ const study = parseSections(raw, front.value);
72
+ if (!study.ok)
73
+ return study;
74
+ const checked = checkStudyConfig(study.value);
75
+ if (!checked.ok)
76
+ return checked;
77
+ const { config, warnings } = checked;
78
+ const { route } = config;
79
+ const mismatch = routeMismatchReason(config);
80
+ if (mismatch)
81
+ return invalid(mismatch);
82
+ if (options.inert === "report") {
83
+ return { ok: true, config, warnings, inert: inertFieldPaths(config) };
116
84
  }
117
- const inert = inertFieldLabels(parsed.config);
85
+ const inert = inertFieldLabels(config);
118
86
  if (inert.length > 0) {
119
- return invalid(spell(`route: ${route} does not read ${inert.join(", ")}. Remove ${inert.length === 1 ? "it" : "them"}.`));
87
+ return invalid(`route: ${route} does not read ${inert.join(", ")}. Remove ${inert.length === 1 ? "it" : "them"}.`);
120
88
  }
121
- return {
122
- ok: true,
123
- config: { ...parsed.config, schema: STUDY_SCHEMA },
124
- warnings: parsed.warnings.map(spell),
125
- };
89
+ return { ok: true, config, warnings };
90
+ }
91
+ /** Why a config's declared route is not the one its subject and actor take, or undefined. */
92
+ function routeMismatchReason(config) {
93
+ const taken = routeOf(config);
94
+ if (taken === config.route)
95
+ return undefined;
96
+ return `This study declares route: ${config.route}, but its subject (source: ${config.subject.source}) and actor (type: ${config.actor.type}) take the ${taken} route. Change \`route\`, or change the subject or actor.`;
97
+ }
98
+ /**
99
+ * Why runStudy refuses a config a library caller passes, or undefined: a humanish.lab.v2 config,
100
+ * one that still sets a StudyConfig field of humanish 0.110, one with no `actor` object, and one
101
+ * whose `route` is not the route its subject and actor take, with parseStudy's message. A planner
102
+ * reads only the v3 fields, so a budget left in `execution.caps` would otherwise run uncapped.
103
+ */
104
+ export function libraryConfigRefusal(config) {
105
+ const raw = config;
106
+ if (!isRecord(raw))
107
+ return undefined;
108
+ if (raw.schema === V2_SCHEMA)
109
+ return { code: "HUMANISH_STUDY_V2_UNSUPPORTED", message: V2_UNSUPPORTED_MESSAGE };
110
+ const moves = v2FieldMoves(raw);
111
+ if (moves.length > 0)
112
+ return {
113
+ code: "HUMANISH_STUDY_V2_UNSUPPORTED",
114
+ message: `This StudyConfig sets fields that humanish 0.111.0 renamed: ${moves.join("; ")}. Set the humanish.study.v3 fields, or parse the study file with parseStudy. The 0.111.0 release notes list each renamed field.`,
115
+ };
116
+ if (!isRecord(raw.actor))
117
+ return { code: "HUMANISH_STUDY_INVALID", message: ACTOR_REQUIRED };
118
+ const message = routeNameReason(raw.route) ?? routeMismatchReason(config);
119
+ return message === undefined ? undefined : { code: "HUMANISH_STUDY_INVALID", message };
126
120
  }
127
- function parseV2(raw) {
128
- const unknownKey = findUnknownV2Key(raw);
129
- if (unknownKey)
130
- return invalid(unknownKey);
121
+ // Each section in a fixed order, so a file with more than one error always reports the same one
122
+ // first. migrate's checkV2 reads a v2 file's sections in this order too.
123
+ function parseSections(raw, front) {
131
124
  const id = str(raw.id);
132
125
  if (!id || !ID_PATTERN.test(id)) {
133
126
  return invalid("The study id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
134
127
  }
135
- const subjectResult = parseSubject(raw.subject);
136
- if (!subjectResult.ok) {
137
- return subjectResult;
138
- }
139
- const actorsResult = parseActors(raw.actors);
140
- if (!actorsResult.ok) {
141
- return actorsResult;
142
- }
143
- const executionResult = parseExecution(raw.execution);
144
- if (!executionResult.ok) {
145
- return executionResult;
146
- }
147
- const config = {
148
- schema: V2_SCHEMA,
149
- id,
150
- ...optionalStr("title", raw.title),
151
- ...optionalStr("description", raw.description),
152
- subject: subjectResult.value,
153
- actors: actorsResult.value,
154
- ...(executionResult.value ? { execution: executionResult.value } : {}),
155
- };
156
- const personas = parsePersonas(raw.personas);
157
- if (personas)
158
- config.personas = personas;
159
- const scenarioResult = parseScenario(raw.scenario);
160
- if (!scenarioResult.ok) {
161
- return scenarioResult;
162
- }
163
- if (scenarioResult.value)
164
- config.scenario = scenarioResult.value;
128
+ const subject = parseSubject(raw.subject);
129
+ if (!subject.ok)
130
+ return subject;
131
+ const type = parseActorType(front.actor, "actor");
132
+ if (!type.ok)
133
+ return type;
134
+ const participants = parseParticipants(raw.participants, front.participants);
135
+ if (!participants.ok)
136
+ return participants;
137
+ const fields = parseActorFields(front.actor, "actor");
138
+ if (!fields.ok)
139
+ return fields;
140
+ const execution = parseExecution(raw.execution);
141
+ if (!execution.ok)
142
+ return execution;
143
+ const caps = parseCaps(raw.caps);
144
+ if (!caps.ok)
145
+ return caps;
165
146
  if (isRecord(raw.policies) &&
166
147
  raw.policies.mediaPermission !== undefined &&
167
148
  raw.policies.mediaPermission !== "prompt" &&
168
149
  raw.policies.mediaPermission !== "granted") {
169
150
  return invalid("`policies.mediaPermission` must be `prompt` (the participant answers the browser's own dialog) or `granted`.");
170
151
  }
171
- const policiesResult = parsePolicies(raw.policies);
172
- if (!policiesResult.ok)
173
- return policiesResult;
174
- if (policiesResult.value)
175
- config.policies = policiesResult.value;
176
- const reviewResult = parseReview(raw.review);
177
- if (!reviewResult.ok)
178
- return reviewResult;
179
- if (reviewResult.value)
180
- config.review = reviewResult.value;
181
- if (raw.defaults !== undefined && isRecord(raw.defaults) && raw.defaults.open !== undefined) {
152
+ const policies = parsePolicies(raw.policies);
153
+ if (!policies.ok)
154
+ return policies;
155
+ const review = parseReview(raw.review);
156
+ if (!review.ok)
157
+ return review;
158
+ if (isRecord(raw.defaults) && raw.defaults.open !== undefined) {
182
159
  if (typeof raw.defaults.open !== "boolean")
183
160
  return invalid("`defaults.open` must be true or false.");
184
161
  }
185
162
  const defaults = parseDefaults(raw.defaults);
186
- if (defaults)
187
- config.defaults = defaults;
188
- const commsResult = parseComms(raw.comms);
189
- if (!commsResult.ok)
190
- return commsResult;
191
- if (commsResult.value)
192
- config.comms = commsResult.value;
193
- return checkStudyConfig(config);
163
+ const comms = parseComms(raw.comms);
164
+ if (!comms.ok)
165
+ return comms;
166
+ const mode = raw.mode === "dry-run" || raw.mode === "live" ? raw.mode : undefined;
167
+ const { surfaces } = front;
168
+ const scenario = str(raw.scenario);
169
+ return {
170
+ ok: true,
171
+ value: {
172
+ schema: STUDY_SCHEMA,
173
+ id,
174
+ ...optionalStr("title", raw.title),
175
+ ...optionalStr("description", raw.description),
176
+ route: front.route,
177
+ ...(mode === undefined ? {} : { mode }),
178
+ subject: subject.value,
179
+ actor: { type: type.value, ...fields.value },
180
+ ...(participants.value === undefined ? {} : { participants: participants.value }),
181
+ ...(surfaces === undefined ? {} : { surfaces }),
182
+ ...(caps.value === undefined ? {} : { caps: caps.value }),
183
+ ...(execution.value === undefined ? {} : { execution: execution.value }),
184
+ ...(scenario === undefined ? {} : { scenario }),
185
+ ...(policies.value === undefined ? {} : { policies: policies.value }),
186
+ ...(review.value === undefined ? {} : { review: review.value }),
187
+ ...(defaults === undefined ? {} : { defaults }),
188
+ ...(comms.value === undefined ? {} : { comms: comms.value }),
189
+ },
190
+ };
191
+ }
192
+ // `participants` in the form the file wrote it. readStudyFront has checked the form against the
193
+ // route and expanded each list entry that has a count; the entries parse here, each named by the
194
+ // `participants` entry it came from. An instruction that is not a non-empty string is dropped, as
195
+ // the v2 parser drops it.
196
+ function parseParticipants(raw, read) {
197
+ if (read.entries !== undefined) {
198
+ const { source } = read;
199
+ return parseParticipantEntries(read.entries, {
200
+ list: "participants",
201
+ entry: (index) => `participants[${source[index] ?? index}]`,
202
+ });
203
+ }
204
+ if (typeof raw === "number")
205
+ return { ok: true, value: read.count };
206
+ if (!isRecord(raw))
207
+ return { ok: true, value: undefined };
208
+ const instruction = str(read.instruction);
209
+ return {
210
+ ok: true,
211
+ value: {
212
+ ...(read.count === undefined ? {} : { count: read.count }),
213
+ ...(instruction === undefined ? {} : { instruction }),
214
+ },
215
+ };
194
216
  }
195
217
  /**
196
218
  * The checks that read more than one section of a parsed config, and the defaults they fill:
197
219
  * shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
198
- * parseV2 runs them once every section has parsed. parse/study.ts runs them on its v2 view of a v3
199
- * study.
200
220
  */
201
- export function checkStudyConfig(config) {
221
+ function checkStudyConfig(config) {
202
222
  const smtpReason = smtpValidationReason(config);
203
223
  if (smtpReason)
204
224
  return invalid(smtpReason);
@@ -208,9 +228,6 @@ export function checkStudyConfig(config) {
208
228
  const outputLimitReason = outputTokenLimitValidationReason(config);
209
229
  if (outputLimitReason)
210
230
  return invalid(outputLimitReason);
211
- const scenarioCapsReason = scenarioCapsValidationReason(config);
212
- if (scenarioCapsReason)
213
- return invalid(scenarioCapsReason);
214
231
  // All-parallel default: a multi-participant study that does not declare execution.concurrency
215
232
  // runs every participant at once; the declared field is a cap the author chose, never a mode.
216
233
  // Independent computer-use participants resolve that default from the final participant count
@@ -218,7 +235,7 @@ export function checkStudyConfig(config) {
218
235
  // world's roster is fixed, so its default is filled here for the envelopes and warnings that read
219
236
  // the parsed config.
220
237
  {
221
- const participantCount = rosterOf(config.actors[0])?.length ?? config.actors[0]?.count ?? 1;
238
+ const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
222
239
  if (participantCount > 1 &&
223
240
  config.execution?.concurrency === undefined &&
224
241
  isSharedWorldComposition(config)) {
@@ -1,5 +1,5 @@
1
1
  import type { ActorPersonaRef } from "../actors/contract.js";
2
- import { type StudyConfig } from "./types.js";
2
+ import type { StudyConfig } from "./types.js";
3
3
  declare const STUDY_LIST_SCHEMA = "humanish.study-list.v1";
4
4
  declare const STUDY_SHOW_SCHEMA = "humanish.study-show.v1";
5
5
  type StudyOrigin = "committed" | "ignored" | "explicit";
@@ -5,7 +5,7 @@ import { lstat, open, readdir, realpath } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { parse } from "yaml";
7
7
  import { parseStudy } from "./config.js";
8
- import { V2_SCHEMA } from "./types.js";
8
+ import { V2_SCHEMA } from "./migrate/v2.js";
9
9
  import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
10
10
  import { isNodeError } from "../run/type-guards.js";
11
11
  import { studyFileCandidates, STUDY_DIRECTORIES } from "./files.js";
@@ -9,7 +9,6 @@ import { externalCatchHealthy } from "../comms/sandbox-catch.js";
9
9
  import { receivingRequiredKey } from "../comms/setup.js";
10
10
  import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
11
11
  import { cli } from "../cli/invocation.js";
12
- import { actorOf } from "./study-fields.js";
13
12
  /** A ready row's message, with the schema values the launch recorded. */
14
13
  function readyMessage(message, readiness) {
15
14
  const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
@@ -152,7 +151,7 @@ export async function studySetupChecks(args) {
152
151
  {
153
152
  name: "study route",
154
153
  ok: true,
155
- message: `${config.id}: ${actorOf(config)?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
154
+ message: `${config.id}: ${config.actor?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
156
155
  },
157
156
  ];
158
157
  if (dryRun)
@@ -247,16 +246,16 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
247
246
  ];
248
247
  }
249
248
  if ((route !== "computer-use" && route !== "shared-world") ||
250
- actorOf(config)?.type !== "local-agent")
249
+ config.actor?.type !== "local-agent")
251
250
  return [];
252
- const choice = actorOf(config)?.localAgent ?? "codex";
251
+ const choice = config.actor?.localAgent ?? "codex";
253
252
  const agent = args.agents.find((entry) => entry.id === choice);
254
253
  if (local)
255
254
  return [await localCodexParticipantCheck({ env: args.env, readiness: checkAccount })];
256
255
  // A signed-in hosted Codex gets the operator handshake; its sign-in status alone does not show
257
256
  // that the operator's config, release and model admit a launch.
258
257
  if (choice === "codex" && agent?.authStatus === "authenticated")
259
- return [await hostedCodexParticipantCheck(args, actorOf(config))];
258
+ return [await hostedCodexParticipantCheck(args, config.actor)];
260
259
  const outdated = agent === undefined ? undefined : localAgentVersionRefusal(agent);
261
260
  return [
262
261
  {