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
@@ -4,20 +4,113 @@
4
4
  // rules the two formats share. The converter (convert.ts) rewrites the YAML document separately and
5
5
  // compares what the two rewrites parse to.
6
6
  //
7
- // The v2-only rules are copies of the parser's and the planners' (the actor list and roster in
8
- // parse/actors.ts, the scenario object in parse/execution.ts, and checks in validation.ts and
9
- // composition-rules.ts), which stay there while a library config can still take the v2 shape.
7
+ // The v2 parser itself is gone. The rules here on keys v3 does not have (the actor list and roster,
8
+ // `laneFocus`, the `scenario` object, `subject.topology`, `execution.caps`, and the checks on their
9
+ // combinations) are kept with that parser's messages, so migrate refuses what it refused.
10
10
  import { isRecord } from "../../run/type-guards.js";
11
- import { findUnknownV2Key } from "../keys.js";
11
+ import { ACTOR, CAPS, EXECUTION, findUnknownKey, PARTICIPANT_ENTRY, SHARED_SECTIONS, SUBJECT, } from "../keys.js";
12
12
  import { parseActorFields, parseActorType, parseParticipantEntries, PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN, } from "../parse/actors.js";
13
13
  import { parseComms } from "../parse/comms.js";
14
- import { parseExecution, parsePolicies, parseReview } from "../parse/execution.js";
15
- import { parseCaps } from "../parse/study.js";
16
- import { parseSubject } from "../parse/subject.js";
14
+ import { parseCaps, parseExecution, parsePolicies, parseReview } from "../parse/execution.js";
15
+ import { hasSubjectSource, parseSubject } from "../parse/subject.js";
17
16
  import { posInt, str } from "../parse/values.js";
18
17
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, routeOf, } from "../routing.js";
19
18
  import { desktopMediaValidationReason, outputTokenLimitValidationReason, smtpValidationReason, } from "../validation.js";
20
- import { ID_PATTERN, STUDY_SCHEMA, V2_SCHEMA, } from "../types.js";
19
+ import { ID_PATTERN, STUDY_SCHEMA, } from "../types.js";
20
+ /** The schema of the study format before humanish.study.v3. */
21
+ export const V2_SCHEMA = "humanish.lab.v2";
22
+ /** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
23
+ export const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
24
+ // The StudyConfig fields of humanish 0.110 and earlier, which a humanish.study.v3 config does not
25
+ // have, each with where its value goes now. A library caller may still build a config with them.
26
+ const V2_FIELDS = [
27
+ {
28
+ present: (config) => config.actors !== undefined,
29
+ move: "`actors[0]` is `actor`, and its `count`, `lanes`, `roster` and `laneFocus` are `participants` (`surfaces` on the scripted route)",
30
+ },
31
+ // A caller that moved `actors[0]` to `actor` as it was.
32
+ {
33
+ present: (config) => isRecord(config.actor) &&
34
+ ["count", "lanes", "roster", "laneFocus"].some((key) => config.actor[key] !== undefined),
35
+ move: "`actor.count`, `actor.lanes`, `actor.roster` and `actor.laneFocus` are `participants` (`surfaces` on the scripted route)",
36
+ },
37
+ {
38
+ present: (config) => isRecord(config.scenario),
39
+ move: "`scenario.ref` is `scenario`, a string, `scenario.mode` is `mode` and `scenario.caps` is `caps`",
40
+ },
41
+ {
42
+ present: (config) => isRecord(config.execution) && config.execution.caps !== undefined,
43
+ move: "`execution.caps` is `caps`",
44
+ },
45
+ {
46
+ present: (config) => isRecord(config.subject) && config.subject.topology !== undefined,
47
+ move: "`subject.topology: shared-world` is `route: shared-world`",
48
+ },
49
+ {
50
+ present: (config) => config.personas !== undefined,
51
+ move: "`personas` has no field now; remove it",
52
+ },
53
+ ];
54
+ /** Where each humanish 0.110 StudyConfig field a config still sets went; empty when it sets none. */
55
+ export function v2FieldMoves(config) {
56
+ if (!isRecord(config))
57
+ return [];
58
+ return V2_FIELDS.filter((field) => field.present(config)).map((field) => field.move);
59
+ }
60
+ /**
61
+ * The v2 key a hand-converted humanish.study.v3 file is most likely to keep, with where it went;
62
+ * undefined when the file keeps none. parse/front.ts refuses the file with it.
63
+ */
64
+ export function movedV2KeyReason(raw) {
65
+ if (raw.actors !== undefined) {
66
+ return "A study has one `actor:` object, and its participants go in `participants:`. `actors:` is the humanish.lab.v2 spelling.";
67
+ }
68
+ if (isRecord(raw.actor)) {
69
+ for (const key of ["count", "lanes", "roster", "laneFocus"]) {
70
+ if (raw.actor[key] !== undefined) {
71
+ return `\`actor.${key}\` moved to \`participants:\`: a count, \`{ count, instruction }\`, or a list of participants.`;
72
+ }
73
+ }
74
+ }
75
+ if (isRecord(raw.subject) && raw.subject.topology !== undefined) {
76
+ return "`subject.topology` moved to `route:`. A shared world is `route: shared-world`.";
77
+ }
78
+ if (isRecord(raw.execution) && raw.execution.caps !== undefined) {
79
+ return "`execution.caps` moved to a top-level `caps:` block.";
80
+ }
81
+ return undefined;
82
+ }
83
+ // The keys of a humanish.lab.v2 file, in the order its parser listed them: the v3 sections with
84
+ // `subject.topology`, the actor list with its participant keys, `execution.caps`, `personas` and
85
+ // the `scenario` object.
86
+ const V2_KEYS = {
87
+ schema: true,
88
+ id: true,
89
+ title: true,
90
+ description: true,
91
+ subject: { source: true, topology: true, ...without(SUBJECT, "source") },
92
+ actors: {
93
+ type: true,
94
+ count: true,
95
+ lanes: PARTICIPANT_ENTRY,
96
+ // Roster groups are participants with a count; the parser expanded them into `lanes[]`.
97
+ roster: { ...PARTICIPANT_ENTRY, count: true },
98
+ ...without(ACTOR, "type"),
99
+ laneFocus: { id: true, label: true, instruction: true },
100
+ },
101
+ execution: withAfter(EXECUTION, "egressAllow", { caps: CAPS }),
102
+ // Inline personas were validated by the persona resolver.
103
+ personas: true,
104
+ scenario: { ref: true, mode: true, inline: true, caps: CAPS },
105
+ ...SHARED_SECTIONS,
106
+ };
107
+ function without(shape, ...keys) {
108
+ return Object.fromEntries(Object.entries(shape).filter(([key]) => !keys.includes(key)));
109
+ }
110
+ // `shape` with `added` inserted after the key `after`.
111
+ function withAfter(shape, after, added) {
112
+ return Object.fromEntries(Object.entries(shape).flatMap((entry) => entry[0] === after ? [entry, ...Object.entries(added)] : [entry]));
113
+ }
21
114
  function refused(message) {
22
115
  return { ok: false, message };
23
116
  }
@@ -32,22 +125,26 @@ const ENTRY_PATHS = {
32
125
  * message.
33
126
  */
34
127
  export function checkV2(raw) {
35
- const unknownKey = findUnknownV2Key(raw);
128
+ const unknownKey = findUnknownKey(raw, V2_KEYS);
36
129
  if (unknownKey)
37
130
  return refused(unknownKey);
38
131
  const id = str(raw.id);
39
132
  if (!id || !ID_PATTERN.test(id)) {
40
133
  return refused("The study id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
41
134
  }
135
+ // The v2 parser checked `subject.topology` right after `subject.source`.
136
+ const topology = hasSubjectSource(raw.subject) ? readTopology(raw.subject) : undefined;
137
+ if (topology?.ok === false)
138
+ return topology;
42
139
  const subject = parseSubject(raw.subject);
43
140
  if (!subject.ok)
44
141
  return refused(subject.error.message);
45
142
  const actor = readActor(raw.actors);
46
143
  if (!actor.ok)
47
144
  return actor;
48
- const execution = parseExecution(raw.execution);
145
+ const execution = readExecution(raw.execution);
49
146
  if (!execution.ok)
50
- return refused(execution.error.message);
147
+ return execution;
51
148
  const scenario = readScenario(raw.scenario);
52
149
  if (!scenario.ok)
53
150
  return scenario;
@@ -71,22 +168,26 @@ export function checkV2(raw) {
71
168
  if (!comms.ok)
72
169
  return refused(comms.error.message);
73
170
  const composition = {
74
- schema: V2_SCHEMA,
171
+ schema: STUDY_SCHEMA,
75
172
  id,
173
+ // A shared world is declared; any other route is derived below.
174
+ route: topology?.value === "shared-world" ? "shared-world" : "computer-use",
76
175
  subject: subject.value,
77
- actors: [{ type: actor.value.type, ...actor.value.fields }],
78
- ...(execution.value === undefined ? {} : { execution: execution.value }),
176
+ actor: { type: actor.value.type, ...actor.value.fields },
177
+ ...(execution.value.execution === undefined ? {} : { execution: execution.value.execution }),
79
178
  ...(policies.value === undefined ? {} : { policies: policies.value }),
80
179
  ...(comms.value === undefined ? {} : { comms: comms.value }),
81
180
  };
82
181
  const route = v2RouteOf(composition);
182
+ composition.route = route;
83
183
  const study = {
84
184
  route,
85
185
  composition,
186
+ topology: topology?.value,
86
187
  count: actor.value.count,
87
188
  entries: actor.value.entries,
88
189
  focus: actor.value.focus,
89
- executionCaps: execution.value?.caps,
190
+ executionCaps: execution.value.caps,
90
191
  scenario: scenario.value,
91
192
  personas: Array.isArray(raw.personas) && raw.personas.some(isRecord),
92
193
  terminalTimeout: route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined,
@@ -107,7 +208,37 @@ export function checkV2(raw) {
107
208
  * the shared-world checks' refusal, as it does from the v2 parser.
108
209
  */
109
210
  function v2RouteOf(composition) {
110
- return composition.subject.topology === "shared-world" ? "shared-world" : routeOf(composition);
211
+ return composition.route === "shared-world" ? "shared-world" : routeOf(composition);
212
+ }
213
+ // parse/subject.ts read `subject.topology` as one of two values.
214
+ function readTopology(subject) {
215
+ if (!isRecord(subject) || subject.topology === undefined)
216
+ return { ok: true, value: undefined };
217
+ const topology = str(subject.topology);
218
+ if (topology !== "per-lane-worlds" && topology !== "shared-world") {
219
+ return refused("`subject.topology` must be per-lane-worlds (the default) or shared-world.");
220
+ }
221
+ return { ok: true, value: topology };
222
+ }
223
+ // parse/execution.ts parseExecution, which read `execution.caps` after `execution.desktop` and
224
+ // before `execution.terminal`.
225
+ function readExecution(raw) {
226
+ let caps;
227
+ if (isRecord(raw)) {
228
+ const { target, timeoutMs, completionTimeoutMs, concurrency, desktop } = raw;
229
+ const head = parseExecution({ target, timeoutMs, completionTimeoutMs, concurrency, desktop });
230
+ if (!head.ok)
231
+ return refused(head.error.message);
232
+ const parsed = parseCaps(raw.caps);
233
+ if (!parsed.ok)
234
+ return refused(parsed.error.message);
235
+ caps = parsed.value;
236
+ }
237
+ // parseExecution reads every key but `caps`.
238
+ const execution = parseExecution(raw);
239
+ if (!execution.ok)
240
+ return refused(execution.error.message);
241
+ return { ok: true, value: { execution: execution.value, caps } };
111
242
  }
112
243
  // parse/actors.ts parseActors, for the one actor it accepts.
113
244
  function readActor(raw) {
@@ -252,7 +383,7 @@ export function v2ShapeReason(study) {
252
383
  const { route, composition, count, entries, focus } = study;
253
384
  if (route === "scripted") {
254
385
  // composition-rules.ts scriptedBrowserValidationReason, in its order.
255
- if (composition.subject.source === "clone" && composition.subject.topology !== undefined) {
386
+ if (composition.subject.source === "clone" && study.topology !== undefined) {
256
387
  return "A clone scripted-browser study does not support `subject.topology` yet: it runs one synthetic subject for its scripted actor. Remove `subject.topology`.";
257
388
  }
258
389
  if ((count ?? 1) > 2) {
@@ -280,13 +411,13 @@ export function v2ShapeReason(study) {
280
411
  const caps = study.scenario?.caps;
281
412
  if (composition.subject.source === "app-url" &&
282
413
  composition.execution?.target === "local" &&
283
- composition.actors[0]?.type === "local-agent" &&
414
+ composition.actor.type === "local-agent" &&
284
415
  (caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
285
416
  return "Local Codex participants currently use gpt-6-astra at low effort. Account dollar/output-token caps are unavailable; use API participants for those controls.";
286
417
  }
287
418
  return undefined;
288
419
  }
289
- // The caps keys each route reads, as study-v3.ts CAPS_KEYS has them.
420
+ // The caps keys each route reads, as parse/front.ts CAPS_KEYS has them.
290
421
  const CAPS_READ = {
291
422
  preview: [],
292
423
  "computer-use": ["maxUsd", "maxTotalUsd"],
@@ -394,7 +525,7 @@ const V2_DROP_ROWS = [
394
525
  },
395
526
  {
396
527
  path: "subject.topology",
397
- applies: (study, routes) => !routes.shared && study.composition.subject.topology !== undefined,
528
+ applies: (study, routes) => !routes.shared && study.topology !== undefined,
398
529
  },
399
530
  {
400
531
  path: "execution.caps",
@@ -458,15 +589,34 @@ const DROP_ORDER = [
458
589
  "review.scorer",
459
590
  "personas",
460
591
  ];
592
+ /**
593
+ * The v2 path of a field parseStudyV3 reports in report mode. Each of these keys exists in both
594
+ * formats: `actor.<key>` is `actors[0].<key>`, a `participants` entry's key is
595
+ * `actors[0].lanes[].<key>` (field-paths.ts finds it under `roster` when the file has one), `mode`
596
+ * is `scenario.mode` and `scenario` is `scenario.ref`. Every other path is the same.
597
+ */
598
+ function v2PathOf(path) {
599
+ if (path.startsWith("actor."))
600
+ return `actors[0].${path.slice("actor.".length)}`;
601
+ if (path.startsWith("participants[]")) {
602
+ return `actors[0].lanes[]${path.slice("participants[]".length)}`;
603
+ }
604
+ if (path === "mode")
605
+ return "scenario.mode";
606
+ if (path === "scenario")
607
+ return "scenario.ref";
608
+ return path;
609
+ }
461
610
  /**
462
611
  * The keys migrate drops: the v2-only rows above and `inert`, the fields parseStudyV3 reports the
463
- * route does not read, then terminal `execution.timeoutMs`, which no terminal code reads. Parents
464
- * come before their children, so a child under a dropped parent is reported with the parent.
612
+ * route does not read (v3 paths, mapped back to the file's), then terminal `execution.timeoutMs`,
613
+ * which no terminal code reads. Parents come before their children, so a child under a dropped
614
+ * parent is reported with the parent.
465
615
  */
466
616
  export function droppedPaths(study, inert) {
467
617
  const routes = routesOf(study);
468
618
  const found = new Set([
469
- ...inert,
619
+ ...inert.map(v2PathOf),
470
620
  ...V2_DROP_ROWS.filter((row) => row.applies(study, routes)).map((row) => row.path),
471
621
  ]);
472
622
  const paths = [
@@ -1,56 +1,35 @@
1
- import type { StudyActor, StudyParticipantEntry, StudyParticipantFocus, StudyParseFailure, StudyV3 } from "../types.js";
1
+ import type { StudyActor, StudyParticipantEntry, StudyParseFailure } from "../types.js";
2
2
  export declare const PARTICIPANT_ID_PATTERN: RegExp;
3
3
  export declare const PARTICIPANT_ID_MAX_CHARS = 40;
4
- /** The participant entries an actor declares, after roster groups expand into them. The manifest
5
- * spells them `actors[].lanes`. Undefined when the actor declares none. */
6
- export declare function rosterOf<Entry = StudyParticipantEntry>(actor: {
7
- readonly lanes?: readonly Entry[];
8
- } | undefined): readonly Entry[] | undefined;
9
- /** An actor's per-participant focus on the app-url route. The manifest spells it `laneFocus`. */
10
- export declare function focusOf(actor: Pick<StudyActor, "laneFocus"> | undefined): StudyParticipantFocus | undefined;
11
- export declare function parseActors(raw: unknown): {
12
- ok: true;
13
- value: StudyActor[];
14
- } | StudyParseFailure;
15
4
  /** An actor's `type`, which a study route must run. `path` names the actor in messages. */
16
5
  export declare function parseActorType(entry: Record<string, unknown>, path: string): {
17
6
  ok: true;
18
7
  value: string;
19
8
  } | StudyParseFailure;
20
- /** An actor's keys other than `type` and the keys that declare its participants. */
21
- type ActorFields = Omit<StudyV3["actor"], "type">;
9
+ /** An actor's keys other than `type`. */
10
+ type ActorFields = Omit<StudyActor, "type">;
22
11
  /**
23
- * Every actor key other than `type` and the participant keys. The v2 parser reads them after the
24
- * participant entries, so a file with errors in both reports the entry first.
12
+ * Every actor key other than `type`. The parser reads them after the participant entries, so a file
13
+ * with errors in both reports the entry first.
25
14
  */
26
15
  export declare function parseActorFields(entry: Record<string, unknown>, path: string): {
27
16
  ok: true;
28
17
  value: ActorFields;
29
18
  } | StudyParseFailure;
30
- /** The participants a v2 actor declares, which a v3 study declares outside its actor. */
31
- interface ActorParticipants {
32
- readonly count?: number | undefined;
33
- readonly entries?: StudyParticipantEntry[] | undefined;
34
- readonly instruction?: string | undefined;
35
- }
36
- /** A v3 study's actor in the v2 spelling: the actor holds its participants. */
37
- export declare function withParticipants(actor: StudyV3["actor"], { count, entries, instruction }: ActorParticipants): StudyActor;
38
- /** A v2 actor without the keys that declare its participants: the actor of a v3 study. */
39
- export declare function withoutParticipants(actor: StudyActor): StudyV3["actor"];
40
19
  /** How a participant list's messages name the list and each entry in it. */
41
20
  export interface ParticipantPaths {
42
21
  readonly list: string;
43
22
  readonly entry: (index: number) => string;
44
23
  }
45
24
  /**
46
- * Parse a participant list into a fan-out roster (computer-use E2B route): `actors[index].lanes` in
47
- * a v2 file, `participants` in a v3 one, each named in messages by `paths`. Structural only:
48
- * each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?,
49
- * entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
25
+ * Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
26
+ * study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
27
+ * Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
28
+ * instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
50
29
  * Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
51
30
  * must be public-safe tokens; a device must be a known preset name. The
52
- * route-scoped cross-validation (`lanes` XOR `count`/`laneFocus`, device XOR raw resolution, cap 16)
53
- * runs in parseStudy where the route is known.
31
+ * route-scoped cross-validation (device XOR raw resolution, targets, cap 16) runs in parseStudy
32
+ * where the route is known.
54
33
  */
55
34
  export declare function parseParticipantEntries(raw: unknown, paths: ParticipantPaths): {
56
35
  ok: true;
@@ -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,56 +23,6 @@ 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 path = `actors[${index}]`;
50
- const type = parseActorType(entry, path);
51
- if (!type.ok)
52
- return type;
53
- const actor = { type: type.value };
54
- const count = posInt(entry.count);
55
- if (count !== undefined)
56
- actor.count = count;
57
- if (entry.lanes !== undefined && entry.roster !== undefined) {
58
- 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\`.`);
59
- }
60
- if (entry.roster !== undefined && count !== undefined) {
61
- return invalid(`Set either actors[${index}].roster or actors[${index}].count, not both: \`roster\` declares groups of distinct participants, and \`count\` declares identical ones.`);
62
- }
63
- if (entry.roster !== undefined && entry.laneFocus !== undefined) {
64
- return invalid(`actors[${index}].roster and actors[${index}].laneFocus are mutually exclusive: a roster group's instruction is each participant's steer.`);
65
- }
66
- const rosterResult = entry.roster !== undefined
67
- ? parseRosterGroups(entry.roster, index)
68
- : parseParticipantEntries(entry.lanes, lanePaths(index));
69
- if (!rosterResult.ok) {
70
- return rosterResult;
71
- }
72
- if (rosterResult.value)
73
- actor.lanes = rosterResult.value;
74
- const fields = parseActorFields(entry, path);
75
- if (!fields.ok)
76
- return fields;
77
- Object.assign(actor, fields.value);
78
- const focus = parseFocus(entry.laneFocus);
79
- if (focus)
80
- actor.laneFocus = focus;
81
- actors.push(actor);
82
- }
83
- return { ok: true, value: actors };
84
- }
85
26
  /** An actor's `type`, which a study route must run. `path` names the actor in messages. */
86
27
  export function parseActorType(entry, path) {
87
28
  const type = str(entry.type);
@@ -94,8 +35,8 @@ export function parseActorType(entry, path) {
94
35
  return { ok: true, value: type };
95
36
  }
96
37
  /**
97
- * Every actor key other than `type` and the participant keys. The v2 parser reads them after the
98
- * participant entries, so a file with errors in both reports the entry first.
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.
99
40
  */
100
41
  export function parseActorFields(entry, path) {
101
42
  const actor = {};
@@ -143,101 +84,15 @@ export function parseActorFields(entry, path) {
143
84
  actor.tasks = tasksResult.value;
144
85
  return { ok: true, value: actor };
145
86
  }
146
- /** A v3 study's actor in the v2 spelling: the actor holds its participants. */
147
- export function withParticipants(actor, { count, entries, instruction }) {
148
- const { type, ...fields } = actor;
149
- return {
150
- type,
151
- ...(count === undefined ? {} : { count }),
152
- ...(entries === undefined ? {} : { lanes: entries }),
153
- ...fields,
154
- ...(instruction === undefined ? {} : { laneFocus: { instruction } }),
155
- };
156
- }
157
- /** A v2 actor without the keys that declare its participants: the actor of a v3 study. */
158
- export function withoutParticipants(actor) {
159
- const fields = { ...actor };
160
- delete fields.count;
161
- delete fields.lanes;
162
- delete fields.laneFocus;
163
- return fields;
164
- }
165
- function parseFocus(raw) {
166
- if (!isRecord(raw)) {
167
- return undefined;
168
- }
169
- const focus = {};
170
- const id = str(raw.id);
171
- if (id)
172
- focus.id = id;
173
- const label = str(raw.label);
174
- if (label)
175
- focus.label = label;
176
- const instruction = str(raw.instruction);
177
- if (instruction)
178
- focus.instruction = instruction;
179
- return Object.keys(focus).length > 0 ? focus : undefined;
180
- }
181
- /**
182
- * Parse `actors[index].roster` compact groups into concrete `lanes[]` entries. This is authoring sugar for
183
- * "N users of M adapter-owned types across S surfaces"; the runtime receives only `lanes[]`.
184
- */
185
- function parseRosterGroups(raw, actorIndex) {
186
- if (raw === undefined) {
187
- return { ok: true, value: undefined };
188
- }
189
- if (!Array.isArray(raw) || raw.length === 0) {
190
- 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.`);
191
- }
192
- const expanded = [];
193
- const seenGroupIds = new Set();
194
- for (const [groupIndex, entry] of raw.entries()) {
195
- if (!isRecord(entry)) {
196
- return invalid(`actors[${actorIndex}].roster[${groupIndex}] must be an object ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
197
- }
198
- const groupId = str(entry.id);
199
- if (groupId === undefined) {
200
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].id is required and must be a public-safe token matching ${PARTICIPANT_ID_PATTERN}.`);
201
- }
202
- if (!PARTICIPANT_ID_PATTERN.test(groupId) || groupId.length > PARTICIPANT_ID_MAX_CHARS - 3) {
203
- 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.`);
204
- }
205
- if (seenGroupIds.has(groupId)) {
206
- return invalid(`actors[${actorIndex}].roster group ids must be unique (duplicate "${groupId}").`);
207
- }
208
- seenGroupIds.add(groupId);
209
- const count = posInt(entry.count);
210
- if (count === undefined) {
211
- return invalid(`actors[${actorIndex}].roster[${groupIndex}].count is required and must be a positive integer.`);
212
- }
213
- const groupEntryInput = { ...entry };
214
- delete groupEntryInput.id;
215
- delete groupEntryInput.count;
216
- for (let i = 1; i <= count; i += 1) {
217
- expanded.push({
218
- ...groupEntryInput,
219
- id: `${groupId}-${String(i).padStart(2, "0")}`,
220
- });
221
- }
222
- }
223
- return parseParticipantEntries(expanded, lanePaths(actorIndex));
224
- }
225
- // The v2 spelling, `actors[0].lanes[3]`. A roster group's participants are named the same way.
226
- function lanePaths(actorIndex) {
227
- return {
228
- list: `actors[${actorIndex}].lanes`,
229
- entry: (index) => `actors[${actorIndex}].lanes[${index}]`,
230
- };
231
- }
232
87
  /**
233
- * Parse a participant list into a fan-out roster (computer-use E2B route): `actors[index].lanes` in
234
- * a v2 file, `participants` in a v3 one, each named in messages by `paths`. Structural only:
235
- * each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?,
236
- * 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? }`.
237
92
  * Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
238
93
  * must be public-safe tokens; a device must be a known preset name. The
239
- * route-scoped cross-validation (`lanes` XOR `count`/`laneFocus`, device XOR raw resolution, cap 16)
240
- * 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.
241
96
  */
242
97
  export function parseParticipantEntries(raw, paths) {
243
98
  if (raw === undefined) {
@@ -415,7 +270,7 @@ function parseDwell(raw, field) {
415
270
  ...(whenResult.value === undefined ? {} : { when: whenResult.value }),
416
271
  ms,
417
272
  everyMs,
418
- // 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
419
274
  then,
420
275
  },
421
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;