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
@@ -4,24 +4,15 @@ import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
4
4
  import { isHttpUrl } from "./subject.js";
5
5
  import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
6
6
  import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
7
- import { invalid, posInt, str } from "./values.js";
7
+ import { invalid, str } from "./values.js";
8
8
  import { isRecord } from "../../run/type-guards.js";
9
9
  // A participant id interpolates into its evidence paths (screenshots/<id>/, actors/<id>.json), so
10
10
  // it must be a public-safe path token, same shape as a study id.
11
11
  export const PARTICIPANT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
12
12
  export const PARTICIPANT_ID_MAX_CHARS = 40;
13
13
  const METADATA_MAX_CHARS = 80;
14
- /** The participant entries an actor declares, after roster groups expand into them. The manifest
15
- * spells them `actors[].lanes`. Undefined when the actor declares none. */
16
- export function rosterOf(actor) {
17
- return actor?.lanes;
18
- }
19
- /** An actor's per-participant focus on the app-url route. The manifest spells it `laneFocus`. */
20
- export function focusOf(actor) {
21
- return actor?.laneFocus;
22
- }
23
14
  // Actor ids humanish no longer registers. Rejecting them at parse keeps a study that names one
24
- // from running on a route that ignores actors[0].type, such as a this-repo dry run.
15
+ // from running on a route that ignores actor.type, such as a this-repo dry run.
25
16
  const REMOVED_ACTOR_TYPES = new Set(["pi-agent-core", "claude-agent-sdk"]);
26
17
  // Only actors of a run kind a route dispatches. codex-app-server is registered but declares only
27
18
  // the "code" run kind, which no study route runs, so naming it would lead to the same dead end.
@@ -32,187 +23,98 @@ function routableActorTypes() {
32
23
  ...registeredTerminalActors(),
33
24
  ];
34
25
  }
35
- export function parseActors(raw) {
36
- if (!Array.isArray(raw) || raw.length === 0) {
37
- return invalid("`actors` must be a non-empty array.");
38
- }
39
- // Multi-actor fan-out is not wired yet (only actors[0] is consumed). Fail closed rather than
40
- // silently ignore actors[1..]; multi-actor support lands in a later slice.
41
- if (raw.length > 1) {
42
- return invalid("Multiple actors are not supported yet (only the first actor runs); declare a single actor.");
43
- }
44
- const actors = [];
45
- for (const [index, entry] of raw.entries()) {
46
- if (!isRecord(entry)) {
47
- return invalid(`actors[${index}] must be an object.`);
48
- }
49
- const type = str(entry.type);
50
- if (!type) {
51
- return invalid(`actors[${index}].type is required.`);
52
- }
53
- if (REMOVED_ACTOR_TYPES.has(type)) {
54
- return invalid(`actors[${index}].type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
55
- }
56
- const actor = { type };
57
- const count = posInt(entry.count);
58
- if (count !== undefined)
59
- actor.count = count;
60
- if (entry.lanes !== undefined && entry.roster !== undefined) {
61
- return invalid(`Set either actors[${index}].lanes or actors[${index}].roster, not both: list participants one by one in \`lanes\`, or in groups in \`roster\`.`);
62
- }
63
- if (entry.roster !== undefined && count !== undefined) {
64
- return invalid(`Set either actors[${index}].roster or actors[${index}].count, not both: \`roster\` declares groups of distinct participants, and \`count\` declares identical ones.`);
65
- }
66
- if (entry.roster !== undefined && entry.laneFocus !== undefined) {
67
- return invalid(`actors[${index}].roster and actors[${index}].laneFocus are mutually exclusive: a roster group's instruction is each participant's steer.`);
68
- }
69
- const rosterResult = entry.roster !== undefined
70
- ? parseRosterGroups(entry.roster, index)
71
- : parseParticipantEntries(entry.lanes, index);
72
- if (!rosterResult.ok) {
73
- return rosterResult;
74
- }
75
- if (rosterResult.value)
76
- actor.lanes = rosterResult.value;
77
- const persona = str(entry.persona);
78
- if (persona)
79
- actor.persona = persona;
80
- const mission = str(entry.mission);
81
- if (mission)
82
- actor.mission = mission;
83
- const model = str(entry.model);
84
- if (model)
85
- actor.model = model;
86
- if (entry.maxOutputTokens !== undefined) {
87
- if (!isMaxOutputTokens(entry.maxOutputTokens))
88
- return invalid(`actors[${index}].maxOutputTokens must be a positive safe integer.`);
89
- actor.maxOutputTokens = entry.maxOutputTokens;
90
- }
91
- const localAgent = str(entry.localAgent);
92
- if (entry.localAgent !== undefined) {
93
- if (localAgent !== "codex" && localAgent !== "claude") {
94
- return invalid(`actors[${index}].localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
95
- }
96
- actor.localAgent = localAgent;
97
- }
98
- if (entry.reasoningEffort !== undefined) {
99
- if (!isReasoningEffort(entry.reasoningEffort)) {
100
- return invalid(`actors[${index}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
101
- }
102
- actor.reasoningEffort = entry.reasoningEffort;
103
- }
104
- const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${index}].stopWhen`);
105
- if (!stopWhenResult.ok)
106
- return stopWhenResult;
107
- if (stopWhenResult.value !== undefined)
108
- actor.stopWhen = stopWhenResult.value;
109
- const dwellResult = parseDwell(entry.dwell, `actors[${index}].dwell`);
110
- if (!dwellResult.ok)
111
- return dwellResult;
112
- if (dwellResult.value !== undefined)
113
- actor.dwell = dwellResult.value;
114
- const tasksResult = parseTasks(entry.tasks, `actors[${index}].tasks`);
115
- if (!tasksResult.ok)
116
- return tasksResult;
117
- if (tasksResult.value !== undefined)
118
- actor.tasks = tasksResult.value;
119
- const focus = parseFocus(entry.laneFocus);
120
- if (focus)
121
- actor.laneFocus = focus;
122
- actors.push(actor);
26
+ /** An actor's `type`, which a study route must run. `path` names the actor in messages. */
27
+ export function parseActorType(entry, path) {
28
+ const type = str(entry.type);
29
+ if (!type) {
30
+ return invalid(`${path}.type is required.`);
123
31
  }
124
- return { ok: true, value: actors };
125
- }
126
- function parseFocus(raw) {
127
- if (!isRecord(raw)) {
128
- return undefined;
32
+ if (REMOVED_ACTOR_TYPES.has(type)) {
33
+ return invalid(`${path}.type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
129
34
  }
130
- const focus = {};
131
- const id = str(raw.id);
132
- if (id)
133
- focus.id = id;
134
- const label = str(raw.label);
135
- if (label)
136
- focus.label = label;
137
- const instruction = str(raw.instruction);
138
- if (instruction)
139
- focus.instruction = instruction;
140
- return Object.keys(focus).length > 0 ? focus : undefined;
35
+ return { ok: true, value: type };
141
36
  }
142
37
  /**
143
- * Parse `actors[index].roster` compact groups into concrete `lanes[]` entries. This is authoring sugar for
144
- * "N users of M adapter-owned types across S surfaces"; the runtime receives only `lanes[]`.
38
+ * Every actor key other than `type`. The parser reads them after the participant entries, so a file
39
+ * with errors in both reports the entry first.
145
40
  */
146
- function parseRosterGroups(raw, actorIndex) {
147
- if (raw === undefined) {
148
- return { ok: true, value: undefined };
41
+ export function parseActorFields(entry, path) {
42
+ const actor = {};
43
+ const persona = str(entry.persona);
44
+ if (persona)
45
+ actor.persona = persona;
46
+ const mission = str(entry.mission);
47
+ if (mission)
48
+ actor.mission = mission;
49
+ const model = str(entry.model);
50
+ if (model)
51
+ actor.model = model;
52
+ if (entry.maxOutputTokens !== undefined) {
53
+ if (!isMaxOutputTokens(entry.maxOutputTokens))
54
+ return invalid(`${path}.maxOutputTokens must be a positive safe integer.`);
55
+ actor.maxOutputTokens = entry.maxOutputTokens;
149
56
  }
150
- if (!Array.isArray(raw) || raw.length === 0) {
151
- return invalid(`actors[${actorIndex}].roster must be a non-empty array of group objects ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
152
- }
153
- const expanded = [];
154
- const seenGroupIds = new Set();
155
- for (const [groupIndex, entry] of raw.entries()) {
156
- if (!isRecord(entry)) {
157
- return invalid(`actors[${actorIndex}].roster[${groupIndex}] must be an object ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
57
+ const localAgent = str(entry.localAgent);
58
+ if (entry.localAgent !== undefined) {
59
+ if (localAgent !== "codex" && localAgent !== "claude") {
60
+ return invalid(`${path}.localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
158
61
  }
159
- const groupId = str(entry.id);
160
- if (groupId === undefined) {
161
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].id is required and must be a public-safe token matching ${PARTICIPANT_ID_PATTERN}.`);
162
- }
163
- if (!PARTICIPANT_ID_PATTERN.test(groupId) || groupId.length > PARTICIPANT_ID_MAX_CHARS - 3) {
164
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].id must match ${PARTICIPANT_ID_PATTERN} and be at most ${PARTICIPANT_ID_MAX_CHARS - 3} characters, because each generated participant id adds a suffix like "-01"; "${groupId}" is not.`);
165
- }
166
- if (seenGroupIds.has(groupId)) {
167
- return invalid(`actors[${actorIndex}].roster group ids must be unique (duplicate "${groupId}").`);
168
- }
169
- seenGroupIds.add(groupId);
170
- const count = posInt(entry.count);
171
- if (count === undefined) {
172
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].count is required and must be a positive integer.`);
173
- }
174
- const groupEntryInput = { ...entry };
175
- delete groupEntryInput.id;
176
- delete groupEntryInput.count;
177
- for (let i = 1; i <= count; i += 1) {
178
- expanded.push({
179
- ...groupEntryInput,
180
- id: `${groupId}-${String(i).padStart(2, "0")}`,
181
- });
62
+ actor.localAgent = localAgent;
63
+ }
64
+ if (entry.reasoningEffort !== undefined) {
65
+ if (!isReasoningEffort(entry.reasoningEffort)) {
66
+ return invalid(`${path}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
182
67
  }
68
+ actor.reasoningEffort = entry.reasoningEffort;
183
69
  }
184
- return parseParticipantEntries(expanded, actorIndex);
70
+ const stopWhenResult = parseStopWhen(entry.stopWhen, `${path}.stopWhen`);
71
+ if (!stopWhenResult.ok)
72
+ return stopWhenResult;
73
+ if (stopWhenResult.value !== undefined)
74
+ actor.stopWhen = stopWhenResult.value;
75
+ const dwellResult = parseDwell(entry.dwell, `${path}.dwell`);
76
+ if (!dwellResult.ok)
77
+ return dwellResult;
78
+ if (dwellResult.value !== undefined)
79
+ actor.dwell = dwellResult.value;
80
+ const tasksResult = parseTasks(entry.tasks, `${path}.tasks`);
81
+ if (!tasksResult.ok)
82
+ return tasksResult;
83
+ if (tasksResult.value !== undefined)
84
+ actor.tasks = tasksResult.value;
85
+ return { ok: true, value: actor };
185
86
  }
186
87
  /**
187
- * Parse `actors[index].lanes` into a fan-out roster (computer-use E2B route). Structural only:
188
- * each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?,
189
- * entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
88
+ * Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
89
+ * study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
90
+ * Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
91
+ * instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
190
92
  * Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
191
93
  * must be public-safe tokens; a device must be a known preset name. The
192
- * route-scoped cross-validation (`lanes` XOR `count`/`laneFocus`, device XOR raw resolution, cap 16)
193
- * runs in parseStudy where the route is known.
94
+ * route-scoped cross-validation (device XOR raw resolution, targets, cap 16) runs in parseStudy
95
+ * where the route is known.
194
96
  */
195
- function parseParticipantEntries(raw, actorIndex) {
97
+ export function parseParticipantEntries(raw, paths) {
196
98
  if (raw === undefined) {
197
99
  return { ok: true, value: undefined };
198
100
  }
199
101
  if (!Array.isArray(raw) || raw.length === 0) {
200
- return invalid(`actors[${actorIndex}].lanes must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
102
+ return invalid(`${paths.list} must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
201
103
  }
202
104
  const entries = [];
203
105
  const seenIds = new Set();
204
106
  for (const [entryIndex, entry] of raw.entries()) {
205
107
  if (!isRecord(entry)) {
206
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}] must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
108
+ return invalid(`${paths.entry(entryIndex)} must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
207
109
  }
208
110
  const parsedEntry = {};
209
111
  const id = str(entry.id);
210
112
  if (id !== undefined) {
211
113
  if (!PARTICIPANT_ID_PATTERN.test(id) || id.length > PARTICIPANT_ID_MAX_CHARS) {
212
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}].id must be a public-safe token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars (it names the participant's evidence paths); got "${id}".`);
114
+ return invalid(`${paths.entry(entryIndex)}.id must be a public-safe token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars (it names the participant's evidence paths); got "${id}".`);
213
115
  }
214
116
  if (seenIds.has(id)) {
215
- return invalid(`actors[${actorIndex}].lanes ids must be unique (duplicate "${id}").`);
117
+ return invalid(`${paths.list} ids must be unique (duplicate "${id}").`);
216
118
  }
217
119
  seenIds.add(id);
218
120
  parsedEntry.id = id;
@@ -220,24 +122,24 @@ function parseParticipantEntries(raw, actorIndex) {
220
122
  const device = str(entry.device);
221
123
  if (device !== undefined) {
222
124
  if (!isDevicePresetName(device)) {
223
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}].device must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
125
+ return invalid(`${paths.entry(entryIndex)}.device must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
224
126
  }
225
127
  parsedEntry.device = device;
226
128
  }
227
129
  const persona = str(entry.persona);
228
130
  if (persona !== undefined)
229
131
  parsedEntry.persona = persona;
230
- const actorType = parseEntryMetadata(entry.actorType, `actors[${actorIndex}].lanes[${entryIndex}].actorType`);
132
+ const actorType = parseEntryMetadata(entry.actorType, `${paths.entry(entryIndex)}.actorType`);
231
133
  if (!actorType.ok)
232
134
  return actorType;
233
135
  if (actorType.value !== undefined)
234
136
  parsedEntry.actorType = actorType.value;
235
- const surface = parseEntryMetadata(entry.surface, `actors[${actorIndex}].lanes[${entryIndex}].surface`);
137
+ const surface = parseEntryMetadata(entry.surface, `${paths.entry(entryIndex)}.surface`);
236
138
  if (!surface.ok)
237
139
  return surface;
238
140
  if (surface.value !== undefined)
239
141
  parsedEntry.surface = surface.value;
240
- const caseGroup = parseEntryMetadata(entry.caseGroup, `actors[${actorIndex}].lanes[${entryIndex}].caseGroup`);
142
+ const caseGroup = parseEntryMetadata(entry.caseGroup, `${paths.entry(entryIndex)}.caseGroup`);
241
143
  if (!caseGroup.ok)
242
144
  return caseGroup;
243
145
  if (caseGroup.value !== undefined)
@@ -245,28 +147,28 @@ function parseParticipantEntries(raw, actorIndex) {
245
147
  const instruction = str(entry.instruction);
246
148
  if (instruction !== undefined)
247
149
  parsedEntry.instruction = instruction;
248
- const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${actorIndex}].lanes[${entryIndex}].stopWhen`);
150
+ const stopWhenResult = parseStopWhen(entry.stopWhen, `${paths.entry(entryIndex)}.stopWhen`);
249
151
  if (!stopWhenResult.ok)
250
152
  return stopWhenResult;
251
153
  if (stopWhenResult.value !== undefined)
252
154
  parsedEntry.stopWhen = stopWhenResult.value;
253
- const dwellResult = parseDwell(entry.dwell, `actors[${actorIndex}].lanes[${entryIndex}].dwell`);
155
+ const dwellResult = parseDwell(entry.dwell, `${paths.entry(entryIndex)}.dwell`);
254
156
  if (!dwellResult.ok)
255
157
  return dwellResult;
256
158
  if (dwellResult.value !== undefined)
257
159
  parsedEntry.dwell = dwellResult.value;
258
160
  if (entry.reasoningEffort !== undefined) {
259
161
  if (!isReasoningEffort(entry.reasoningEffort)) {
260
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
162
+ return invalid(`${paths.entry(entryIndex)}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
261
163
  }
262
164
  parsedEntry.reasoningEffort = entry.reasoningEffort;
263
165
  }
264
166
  const target = str(entry.target);
265
167
  if (target !== undefined) {
266
168
  if (!isHttpUrl(target)) {
267
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}].target must be an absolute http(s) URL.`);
169
+ return invalid(`${paths.entry(entryIndex)}.target must be an absolute http(s) URL.`);
268
170
  }
269
- const credential = urlCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].target`, target);
171
+ const credential = urlCredentialReason(`${paths.entry(entryIndex)}.target`, target);
270
172
  if (credential)
271
173
  return invalid(credential);
272
174
  parsedEntry.target = target;
@@ -275,7 +177,7 @@ function parseParticipantEntries(raw, actorIndex) {
275
177
  // it runs in sharedWorldValidationReason (where the route + serve.url are known).
276
178
  const entryPath = str(entry.entry);
277
179
  if (entryPath !== undefined) {
278
- const credential = entryCredentialReason(`actors[${actorIndex}].lanes[${entryIndex}].entry`, entryPath);
180
+ const credential = entryCredentialReason(`${paths.entry(entryIndex)}.entry`, entryPath);
279
181
  if (credential)
280
182
  return invalid(credential);
281
183
  parsedEntry.entry = entryPath;
@@ -284,7 +186,7 @@ function parseParticipantEntries(raw, actorIndex) {
284
186
  // exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
285
187
  if (entry.host !== undefined) {
286
188
  if (typeof entry.host !== "boolean") {
287
- return invalid(`actors[${actorIndex}].lanes[${entryIndex}].host must be a boolean (marks the designated host participant on the external-public shared-world route).`);
189
+ return invalid(`${paths.entry(entryIndex)}.host must be a boolean (marks the designated host participant on the external-public shared-world route).`);
288
190
  }
289
191
  if (entry.host)
290
192
  parsedEntry.host = true;
@@ -368,7 +270,7 @@ function parseDwell(raw, field) {
368
270
  ...(whenResult.value === undefined ? {} : { when: whenResult.value }),
369
271
  ms,
370
272
  everyMs,
371
- // oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of humanish.lab.v2
273
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of a study
372
274
  then,
373
275
  },
374
276
  };
@@ -76,9 +76,13 @@ function parseCommsEmail(raw) {
76
76
  }
77
77
  return { ok: true, value: email };
78
78
  }
79
- /** A real inbox: a saved connection, with optional link and allowed origins and nothing else. */
79
+ /**
80
+ * A real inbox: a saved connection, with optional link and allowed origins and nothing else. The
81
+ * parsed config adds `kind: real`, so a config parseStudy returned parses again with it.
82
+ */
80
83
  function parseEmailConnection(raw) {
81
- const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key));
84
+ const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key) &&
85
+ !(key === "kind" && raw.kind === "real"));
82
86
  if (unsupported.length)
83
87
  return invalid("An email connection cannot be mixed with capture settings, kind, recipients, or provider options. Use only connection, optional linkOrigin and allowedOrigins.");
84
88
  if (typeof raw.connection !== "string" || !/^[a-z][a-z0-9-]{0,47}$/.test(raw.connection))
@@ -1,12 +1,17 @@
1
- import type { StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview, StudyScenario } from "../types.js";
1
+ import type { StudyCaps, StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview } from "../types.js";
2
2
  export declare function parseExecution(raw: unknown): {
3
3
  ok: true;
4
4
  value: StudyExecution | undefined;
5
5
  } | StudyParseFailure;
6
- export declare function parsePersonas(raw: unknown): Record<string, unknown>[] | undefined;
7
- export declare function parseScenario(raw: unknown): {
6
+ /**
7
+ * Parse a `caps` block's values. Returns a parse failure on a malformed value rather than silently
8
+ * dropping a budget declaration (a cap that silently does nothing would claim protection it
9
+ * does not give). Each cap must be a non-negative finite number. readStudyFront has checked the
10
+ * keys against the route; migrate checks a v2 file's two caps blocks with it.
11
+ */
12
+ export declare function parseCaps(raw: unknown): {
8
13
  ok: true;
9
- value: StudyScenario | undefined;
14
+ value: StudyCaps | undefined;
10
15
  } | StudyParseFailure;
11
16
  export declare function parsePolicies(raw: unknown): {
12
17
  ok: true;
@@ -33,14 +33,6 @@ export function parseExecution(raw) {
33
33
  }
34
34
  if (desktopResult.value)
35
35
  execution.desktop = desktopResult.value;
36
- // Reuse the terminal route's caps parser (same shape, not a fork); a malformed budget is a hard
37
- // error, never silently dropped (a cap that silently does nothing would be a safety lie).
38
- const capsResult = parseCaps(raw.caps);
39
- if (!capsResult.ok) {
40
- return capsResult;
41
- }
42
- if (capsResult.value)
43
- execution.caps = capsResult.value;
44
36
  const terminalResult = parseTerminal(raw.terminal);
45
37
  if (!terminalResult.ok) {
46
38
  return terminalResult;
@@ -217,45 +209,18 @@ function parseDesktop(raw) {
217
209
  }
218
210
  return { ok: true, value: Object.keys(desktop).length > 0 ? desktop : undefined };
219
211
  }
220
- export function parsePersonas(raw) {
221
- if (!Array.isArray(raw)) {
222
- return undefined;
223
- }
224
- const personas = raw.filter(isRecord);
225
- return personas.length > 0 ? personas : undefined;
226
- }
227
- export function parseScenario(raw) {
228
- if (!isRecord(raw)) {
229
- return { ok: true, value: undefined };
230
- }
231
- const scenario = {};
232
- const ref = str(raw.ref);
233
- if (ref)
234
- scenario.ref = ref;
235
- if (isRecord(raw.inline))
236
- scenario.inline = raw.inline;
237
- const mode = str(raw.mode);
238
- if (mode === "dry-run" || mode === "live")
239
- scenario.mode = mode;
240
- const capsResult = parseCaps(raw.caps);
241
- if (!capsResult.ok) {
242
- return capsResult;
243
- }
244
- if (capsResult.value)
245
- scenario.caps = capsResult.value;
246
- return { ok: true, value: Object.keys(scenario).length > 0 ? scenario : undefined };
247
- }
248
212
  /**
249
- * Parse `scenario.caps`. Returns a parse failure on a malformed value rather than silently
213
+ * Parse a `caps` block's values. Returns a parse failure on a malformed value rather than silently
250
214
  * dropping a budget declaration (a cap that silently does nothing would claim protection it
251
- * does not give). Each cap must be a non-negative finite number.
215
+ * does not give). Each cap must be a non-negative finite number. readStudyFront has checked the
216
+ * keys against the route; migrate checks a v2 file's two caps blocks with it.
252
217
  */
253
- function parseCaps(raw) {
218
+ export function parseCaps(raw) {
254
219
  if (raw === undefined) {
255
220
  return { ok: true, value: undefined };
256
221
  }
257
222
  if (!isRecord(raw)) {
258
- return invalid("`scenario.caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
223
+ return invalid("`caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
259
224
  }
260
225
  const caps = {};
261
226
  for (const key of ["maxUsd", "maxTotalUsd", "maxJobs", "maxMinutes"]) {
@@ -263,7 +228,7 @@ function parseCaps(raw) {
263
228
  continue;
264
229
  const value = nonNegNumber(raw[key]);
265
230
  if (value === undefined) {
266
- return invalid(`\`scenario.caps.${key}\` must be a non-negative number.`);
231
+ return invalid(`\`caps.${key}\` must be a non-negative number.`);
267
232
  }
268
233
  caps[key] = value;
269
234
  }
@@ -0,0 +1,36 @@
1
+ import type { StudyRoute } from "../routing.js";
2
+ import type { StudyParseFailure, StudySurfaces } from "../types.js";
3
+ /** A document's `participants`, read before the participant entries parse. */
4
+ export interface Participants {
5
+ count?: number;
6
+ instruction?: unknown;
7
+ /** The list entries, with each entry that has a count expanded into its participants. */
8
+ entries?: unknown[];
9
+ /** For each expanded entry, the index of the `participants` entry it came from. */
10
+ source: number[];
11
+ }
12
+ /** What a v3 document declares before its sections parse. */
13
+ export interface StudyFront {
14
+ readonly route: StudyRoute;
15
+ readonly actor: Record<string, unknown>;
16
+ readonly participants: Participants;
17
+ readonly surfaces: StudySurfaces | undefined;
18
+ }
19
+ type Parsed<T> = {
20
+ ok: true;
21
+ value: T;
22
+ } | StudyParseFailure;
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;
27
+ /**
28
+ * The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
29
+ * route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
30
+ * `caps` keys its route takes. `anySharedWorldParticipants`, for migrate's v3 form of a v2 file:
31
+ * a shared world takes `participants` in every form computer use takes, or none, because a v2
32
+ * shared world could declare a count or no list. The shared-world checks then refuse it, as the v2
33
+ * parser does.
34
+ */
35
+ export declare function readStudyFront(raw: Record<string, unknown>, anySharedWorldParticipants?: boolean): Parsed<StudyFront>;
36
+ export {};