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,207 +1,233 @@
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, 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. migrate's conversion (convert.ts) calls it directly, since it reads v2 files.
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
- if (config.comms?.email?.smtp && config.subject.topology === "shared-world") {
194
- return invalid("SMTP capture is not supported yet for shared-world studies. Use the default per-lane-worlds topology for SMTP, or configure supported HTTP email capture for concurrent shared-world studies.");
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
+ });
195
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
+ };
216
+ }
217
+ /**
218
+ * The checks that read more than one section of a parsed config, and the defaults they fill:
219
+ * shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
220
+ */
221
+ function checkStudyConfig(config) {
222
+ const smtpReason = smtpValidationReason(config);
223
+ if (smtpReason)
224
+ return invalid(smtpReason);
196
225
  const mediaReason = desktopMediaValidationReason(config);
197
226
  if (mediaReason)
198
227
  return invalid(mediaReason);
199
228
  const outputLimitReason = outputTokenLimitValidationReason(config);
200
229
  if (outputLimitReason)
201
230
  return invalid(outputLimitReason);
202
- const scenarioCapsReason = scenarioCapsValidationReason(config);
203
- if (scenarioCapsReason)
204
- return invalid(scenarioCapsReason);
205
231
  // All-parallel default: a multi-participant study that does not declare execution.concurrency
206
232
  // runs every participant at once; the declared field is a cap the author chose, never a mode.
207
233
  // Independent computer-use participants resolve that default from the final participant count
@@ -209,7 +235,7 @@ function parseV2(raw) {
209
235
  // world's roster is fixed, so its default is filled here for the envelopes and warnings that read
210
236
  // the parsed config.
211
237
  {
212
- const participantCount = rosterOf(config.actors[0])?.length ?? config.actors[0]?.count ?? 1;
238
+ const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
213
239
  if (participantCount > 1 &&
214
240
  config.execution?.concurrency === undefined &&
215
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,6 +9,7 @@ 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";
12
13
  /** A ready row's message, with the schema values the launch recorded. */
13
14
  function readyMessage(message, readiness) {
14
15
  const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
@@ -151,7 +152,7 @@ export async function studySetupChecks(args) {
151
152
  {
152
153
  name: "study route",
153
154
  ok: true,
154
- message: `${config.id}: ${config.actors[0]?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
155
+ message: `${config.id}: ${actorOf(config)?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
155
156
  },
156
157
  ];
157
158
  if (dryRun)
@@ -246,16 +247,16 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
246
247
  ];
247
248
  }
248
249
  if ((route !== "computer-use" && route !== "shared-world") ||
249
- config.actors[0]?.type !== "local-agent")
250
+ actorOf(config)?.type !== "local-agent")
250
251
  return [];
251
- const choice = config.actors[0]?.localAgent ?? "codex";
252
+ const choice = actorOf(config)?.localAgent ?? "codex";
252
253
  const agent = args.agents.find((entry) => entry.id === choice);
253
254
  if (local)
254
255
  return [await localCodexParticipantCheck({ env: args.env, readiness: checkAccount })];
255
256
  // A signed-in hosted Codex gets the operator handshake; its sign-in status alone does not show
256
257
  // that the operator's config, release and model admit a launch.
257
258
  if (choice === "codex" && agent?.authStatus === "authenticated")
258
- return [await hostedCodexParticipantCheck(args, config.actors[0])];
259
+ return [await hostedCodexParticipantCheck(args, actorOf(config))];
259
260
  const outdated = agent === undefined ? undefined : localAgentVersionRefusal(agent);
260
261
  return [
261
262
  {