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
package/dist/run/run.js CHANGED
@@ -32,11 +32,13 @@ export class FinishedRun {
32
32
  runId;
33
33
  /** The paths created by startRun and validated at the start of `finish`. */
34
34
  paths;
35
- constructor(key, runId, paths, observer, outcome, recordFailure, interrupted) {
35
+ #participantsRan;
36
+ constructor(key, runId, paths, observer, outcome, recordFailure, interrupted, participantsRan) {
36
37
  if (key !== issueKey)
37
38
  throw new Error("Only Run.finish issues a FinishedRun.");
38
39
  this.runId = runId;
39
40
  this.paths = paths;
41
+ this.#participantsRan = participantsRan;
40
42
  this.#observer = observer;
41
43
  this.#outcome = outcome;
42
44
  this.#recordFailure = recordFailure;
@@ -49,6 +51,10 @@ export class FinishedRun {
49
51
  get interrupted() {
50
52
  return this.#interrupted();
51
53
  }
54
+ /** The route reported a participant's session started (Run.participantStarted). */
55
+ get participantsRan() {
56
+ return this.#participantsRan();
57
+ }
52
58
  static isIssued(value) {
53
59
  return typeof value === "object" && value !== null && #observer in value;
54
60
  }
@@ -106,7 +112,7 @@ function observerTarget(options) {
106
112
  };
107
113
  }
108
114
  function runPublisher(args) {
109
- const { options, runId, paths, runStatus, now, admit } = args;
115
+ const { options, runId, paths, runStatus, now, admit, participantsRan } = args;
110
116
  const observer = observerTarget(options);
111
117
  let pointerWritten = false;
112
118
  // The bundle of the last write asked for, and the outcome every later write carries once a
@@ -176,7 +182,7 @@ function runPublisher(args) {
176
182
  await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
177
183
  return current;
178
184
  }));
179
- return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, () => runStatus.interrupted);
185
+ return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, () => runStatus.interrupted, participantsRan);
180
186
  };
181
187
  const interrupt = async (signal) => {
182
188
  interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
@@ -231,7 +237,8 @@ export async function runScope(fn) {
231
237
  const created = await createRunArtifactPaths(options.cwd, runId);
232
238
  if (!created.ok)
233
239
  return created;
234
- // beginRunStatus reads only the mode and the study provenance from the run's options.
240
+ // beginRunStatus reads only the mode, the study provenance and the sandboxes field from
241
+ // the run's options.
235
242
  const runStatus = beginRunStatus(created.paths, { ...options, runId });
236
243
  status = runStatus;
237
244
  let interruptBundle;
@@ -260,7 +267,16 @@ export async function runScope(fn) {
260
267
  const openRun = (options, runId, paths, runStatus) => {
261
268
  const now = options.now ?? Date.now;
262
269
  const createdAt = new Date(now()).toISOString();
263
- const publisher = runPublisher({ options, runId, paths, runStatus, now, admit });
270
+ let participantsRan = false;
271
+ const publisher = runPublisher({
272
+ options,
273
+ runId,
274
+ paths,
275
+ runStatus,
276
+ now,
277
+ admit,
278
+ participantsRan: () => participantsRan,
279
+ });
264
280
  let finishCalled = false;
265
281
  const checkIdentity = (bundle) => bundle.runId !== runId || bundle.mode !== options.mode
266
282
  ? "The bundle names another run or mode than this run."
@@ -271,6 +287,9 @@ export async function runScope(fn) {
271
287
  mode: options.mode,
272
288
  ...(options.study === undefined ? {} : { study: options.study }),
273
289
  paths,
290
+ participantStarted() {
291
+ participantsRan = true;
292
+ },
274
293
  writeSnapshot(bundle) {
275
294
  if (closed)
276
295
  return refuse("The run scope has closed.");
@@ -53,6 +53,11 @@ export interface RunStatusRecord {
53
53
  * Read either through studyProvenanceOf.
54
54
  */
55
55
  lab?: RunStudyProvenance;
56
+ /**
57
+ * `none` when the route recorded at start that this run creates no sandbox, as a scripted run
58
+ * against an app-url subject does. Absent when the run may create one. Reclaim reads it.
59
+ */
60
+ sandboxes?: "none";
56
61
  /** The pid that owns the run, for local liveness checks. */
57
62
  pid: number;
58
63
  startedAt: string;
@@ -87,6 +92,7 @@ export interface BeginRunStatusOptions {
87
92
  runId: string;
88
93
  mode: "dry-run" | "live";
89
94
  study?: RunStudyProvenance | undefined;
95
+ sandboxes?: "none" | undefined;
90
96
  }
91
97
  /**
92
98
  * Start a run's status record and keep it fresh. Fire-and-forget by design: a status write that
@@ -64,6 +64,7 @@ export function beginRunStatus(runPaths, options) {
64
64
  state: "running",
65
65
  mode: options.mode,
66
66
  ...studyFields(options.study),
67
+ ...(options.sandboxes === undefined ? {} : { sandboxes: options.sandboxes }),
67
68
  pid: process.pid,
68
69
  startedAt,
69
70
  updatedAt: startedAt,
@@ -5,7 +5,7 @@ import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, r
5
5
  import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, } from "./contained-output.js";
6
6
  import { RUN_BUNDLE_FILE } from "./bundle.js";
7
7
  import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
8
- import { bundleDisplayFacts, runDisplay } from "./display.js";
8
+ import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
9
9
  import { RUN_STATUS_FILE } from "./status.js";
10
10
  import { readLatest, readRunJsonIfExists, resolveRunPath } from "./locate.js";
11
11
  import { withCuaReviewProvenance } from "./outcomes.js";
@@ -101,22 +101,33 @@ export async function listRuns(cwdInput) {
101
101
  * that is not an object naming a run gets none.
102
102
  */
103
103
  async function listedRunDisplay(runPaths, bundle) {
104
- if (!isRecord(bundle) || typeof bundle.runId !== "string")
104
+ const displayed = displayedBundle(bundle);
105
+ if (displayed === undefined)
105
106
  return {};
106
- const outcome = isRunOutcome(bundle.outcome) ? bundle.outcome : undefined;
107
- const status = outcome === undefined ? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE) : undefined;
108
- const facts = bundleDisplayFacts({
107
+ const status = displayed.outcome === undefined
108
+ ? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE)
109
+ : undefined;
110
+ return { display: runDisplay(bundleDisplayFacts(displayed, status)) };
111
+ }
112
+ /** The fields runDisplay reads from an unchecked run.json; none when it does not name a run. */
113
+ function displayedBundle(bundle) {
114
+ if (!isRecord(bundle) || typeof bundle.runId !== "string")
115
+ return undefined;
116
+ return {
109
117
  runId: bundle.runId,
110
118
  ...(typeof bundle.mode === "string" ? { mode: bundle.mode } : {}),
111
119
  ...(isRecord(bundle.review) && typeof bundle.review.verdict === "string"
112
120
  ? { review: { verdict: bundle.review.verdict } }
113
121
  : {}),
114
122
  ...(Array.isArray(bundle.simulations)
115
- ? { simulations: bundle.simulations.filter(isRecord) }
123
+ ? {
124
+ simulations: bundle.simulations
125
+ .filter(isRecord)
126
+ .map((record) => (typeof record.status === "string" ? { status: record.status } : {})),
127
+ }
116
128
  : {}),
117
- ...(outcome === undefined ? {} : { outcome }),
118
- }, status);
119
- return { display: runDisplay(facts) };
129
+ ...(isRunOutcome(bundle.outcome) ? { outcome: bundle.outcome } : {}),
130
+ };
120
131
  }
121
132
  function runsUnavailableResult(cwd, error) {
122
133
  return {
@@ -159,10 +170,14 @@ export async function readReview(cwdInput, runInput) {
159
170
  const projected = isRecord(bundle) && Array.isArray(bundle.streams)
160
171
  ? withCuaReviewProvenance(review, bundle.streams.filter(isRecord))
161
172
  : review;
173
+ const displayed = displayedBundle(bundle);
174
+ const { display } = runPaths ? await listedRunDisplay(runPaths, bundle) : {};
162
175
  return {
163
- ...projected,
176
+ ...(displayed === undefined || display === undefined
177
+ ? projected
178
+ : displayedReview(projected, displayed, display)),
164
179
  path: path.relative(cwd, path.join(runPaths.absoluteRunRoot, "review.json")),
165
180
  runId: path.basename(runPaths.absoluteRunRoot),
166
- ...(runPaths ? await listedRunDisplay(runPaths, bundle) : {}),
181
+ ...(display === undefined ? {} : { display }),
167
182
  };
168
183
  }
@@ -3,7 +3,7 @@ import { type LateScorer } from "./study/route-inputs.js";
3
3
  import { type StudyRoute } from "./study/plan.js";
4
4
  import type { StudyDeps } from "./study/study-deps.js";
5
5
  import type { RunStudyDriving, RunStudyHomes } from "./study/run-study-homes.js";
6
- import { type StudyConfig } from "./study/types.js";
6
+ import type { StudyConfig } from "./study/types.js";
7
7
  import type { ObserverResult } from "./observer/render.js";
8
8
  import { type CuaActorStudyResult } from "./routes/computer-use/types.js";
9
9
  import { type PreviewStudyResult } from "./routes/preview.js";
@@ -20,8 +20,10 @@ import type { RunStudyProvenance } from "./run/study-provenance.js";
20
20
  export declare function runStudyWith(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): Promise<StudyOutcome>;
21
21
  /**
22
22
  * The package's study runner (`src/index.ts`): runStudyWith with the public options only. A
23
- * JavaScript caller that passes a field RunStudyOptions no longer has, or a humanish.lab.v2 config,
24
- * is refused in the route's own result envelope before anything runs.
23
+ * config that skipped parseStudy is checked first (libraryConfigRefusal): a humanish.lab.v2 config
24
+ * or one with a humanish 0.110 field, no `actor`, or a `route` the config does not take. That, or a
25
+ * field RunStudyOptions no longer has, is refused in the route's own result envelope before
26
+ * anything runs.
25
27
  */
26
28
  export declare function runStudy(config: StudyConfig, options: RunStudyOptions): Promise<StudyOutcome>;
27
29
  /** A study planned once: the route's refusal, or the run of its plan. */
package/dist/run-study.js CHANGED
@@ -7,8 +7,7 @@
7
7
  import { computerUseInput, scriptedInput, sharedWorldInput, terminalInput, } from "./study/route-inputs.js";
8
8
  import { planStudy, resolveStudyDryRun, routeOf } from "./study/plan.js";
9
9
  import { normalizeRunStudyOptions, optionRefusalOutcome, removedOptionRefusal, } from "./study/run-study-options.js";
10
- import { V2_SCHEMA } from "./study/types.js";
11
- import { V2_UNSUPPORTED_MESSAGE } from "./study/config.js";
10
+ import { libraryConfigRefusal } from "./study/config.js";
12
11
  import { admitComputerUsePlan, computerUseStudyRefusal } from "./routes/computer-use/route.js";
13
12
  import {} from "./routes/computer-use/types.js";
14
13
  import { admitPreviewPlan, previewStudyRefusal, } from "./routes/preview.js";
@@ -32,17 +31,14 @@ export async function runStudyWith(config, options, deps = {}) {
32
31
  }
33
32
  /**
34
33
  * The package's study runner (`src/index.ts`): runStudyWith with the public options only. A
35
- * JavaScript caller that passes a field RunStudyOptions no longer has, or a humanish.lab.v2 config,
36
- * is refused in the route's own result envelope before anything runs.
34
+ * config that skipped parseStudy is checked first (libraryConfigRefusal): a humanish.lab.v2 config
35
+ * or one with a humanish 0.110 field, no `actor`, or a `route` the config does not take. That, or a
36
+ * field RunStudyOptions no longer has, is refused in the route's own result envelope before
37
+ * anything runs.
37
38
  */
38
39
  export async function runStudy(config, options) {
39
- const refusal = config.schema === V2_SCHEMA
40
- ? {
41
- ok: false,
42
- code: "HUMANISH_STUDY_V2_UNSUPPORTED",
43
- message: V2_UNSUPPORTED_MESSAGE,
44
- }
45
- : removedOptionRefusal(options);
40
+ const rejected = libraryConfigRefusal(config);
41
+ const refusal = rejected === undefined ? removedOptionRefusal(options) : { ok: false, ...rejected };
46
42
  if (refusal === undefined)
47
43
  return runStudyWith(config, options);
48
44
  const study = localBrowserDefaults(config);
@@ -50,7 +50,7 @@ export type AdapterScorerLoadResult = {
50
50
  };
51
51
  };
52
52
  /**
53
- * Resolve + load a config-declared scorer module. Resolution clones scenario.ref exactly
53
+ * Resolve + load a config-declared scorer module. Resolution clones the scripted `scenario` resolution exactly
54
54
  * (prepareSelectedOutputDirectory → cwd-clamp → readContainedRegularFile), then `import()`s the
55
55
  * entry file in a broad try/catch. The digest is over the readContainedRegularFile entry bytes.
56
56
  */
@@ -54,7 +54,7 @@ const SCORER_CAPABLE_ROUTES = new Set([
54
54
  * package.json `type`. `.ts` is rejected: the compiled `dist/` CLI ships no TypeScript loader. */
55
55
  const SCORER_EXTENSIONS = new Set([".mjs", ".js", ".cjs"]);
56
56
  /**
57
- * Resolve + load a config-declared scorer module. Resolution clones scenario.ref exactly
57
+ * Resolve + load a config-declared scorer module. Resolution clones the scripted `scenario` resolution exactly
58
58
  * (prepareSelectedOutputDirectory → cwd-clamp → readContainedRegularFile), then `import()`s the
59
59
  * entry file in a broad try/catch. The digest is over the readContainedRegularFile entry bytes.
60
60
  */
@@ -84,10 +84,10 @@ export async function loadAdapterScorer(args) {
84
84
  if (!SCORER_EXTENSIONS.has(ext)) {
85
85
  return fail("HUMANISH_STUDY_SCORER_BAD_REF", `review.scorer.ref "${trimmed}" must be a path relative to the project ending in .mjs (recommended), .js or .cjs.`);
86
86
  }
87
- // Root token: realpath(cwd) → prepareSelectedOutputDirectory(dirname, cwd). Mirrors scenario.ref.
87
+ // Root token: realpath(cwd) → prepareSelectedOutputDirectory(dirname, cwd). Mirrors the scripted `scenario`.
88
88
  const physicalCwd = await realpath(path.resolve(args.cwd));
89
89
  const root = await prepareSelectedOutputDirectory(path.dirname(physicalCwd), physicalCwd);
90
- // Clamp verbatim from the scripted scenario.ref branch: resolve → relative → reject escape/absolute.
90
+ // Clamp verbatim from the scripted `scenario` branch: resolve → relative → reject escape/absolute.
91
91
  const absolutePath = path.resolve(root.physicalPath, trimmed);
92
92
  const relative = path.relative(root.physicalPath, absolutePath);
93
93
  if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
@@ -4,13 +4,22 @@
4
4
  // a library caller's config itself, under its route's error codes. The planners call the shared
5
5
  // checks in study/validation.ts and desktopCliProductReason below. The other subject rules here have
6
6
  // planner counterparts whose conditions and wording differ (clone serve and repo, local-tree
7
- // target, in-process fan-out, loopback targets, scripted scenario.ref).
7
+ // target, in-process fan-out, loopback targets, scripted `scenario`).
8
8
  import { isLoopbackUrl } from "./parse/subject.js";
9
9
  import { REPO_SLUG_PATTERN } from "./parse/values.js";
10
10
  import { declaredTargets } from "./plan-participants.js";
11
11
  import { actorResolvesToComputerUse, actorResolvesToScriptedBrowser, actorResolvesToTerminal, computerUseParticipantCount, registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, isComputerUseComposition, } from "./routing.js";
12
12
  import { cloneTargetValidationReason, computerUseValidationReason, sharedWorldValidationReason, } from "./validation.js";
13
- import { rosterOf } from "./parse/actors.js";
13
+ import { actorOf, participantList, declaredParticipantCount, surfaceCount, modeOf, scenarioRefOf, declaresSharedWorld, } from "./study-fields.js";
14
+ /**
15
+ * The count a scripted or terminal composition reads: its surfaces, or a participant count.
16
+ * parse/front.ts refuses `participants` on route: scripted and route: terminal, and `surfaces` off
17
+ * route: scripted, but a study can declare a route its composition does not take, and these rules
18
+ * run before parseStudy compares the two. migrate's view of a v2 file reaches them the same way.
19
+ */
20
+ function participantOrSurfaceCount(config) {
21
+ return declaredParticipantCount(config) ?? surfaceCount(config);
22
+ }
14
23
  /** The first composition rule a config breaks, in the parser's order, or null. */
15
24
  export function compositionReason(config) {
16
25
  return (thisRepoValidationReason(config) ??
@@ -25,7 +34,7 @@ export function compositionReason(config) {
25
34
  (isComputerUseComposition(config) ? computerUseValidationReason(config) : null) ??
26
35
  // Whenever shared world is declared, not only when it routes, so a half-declared shared world
27
36
  // fails with a precise reason instead of running as independent participants.
28
- (config.subject.topology === "shared-world" ? sharedWorldValidationReason(config) : null) ??
37
+ (declaresSharedWorld(config) ? sharedWorldValidationReason(config) : null) ??
29
38
  desktopCliValidationReason(config) ??
30
39
  terminalValidationReason(config) ??
31
40
  cloneActorValidationReason(config));
@@ -39,7 +48,7 @@ function thisRepoValidationReason(config) {
39
48
  if (config.execution?.target) {
40
49
  return "`execution.target` applies only to clone/app-url/local-app subjects; this-repo studies run locally.";
41
50
  }
42
- if (config.scenario?.mode === "live") {
51
+ if (modeOf(config) === "live") {
43
52
  return THIS_REPO_DRY_RUN_ONLY;
44
53
  }
45
54
  }
@@ -54,18 +63,18 @@ function thisRepoValidationReason(config) {
54
63
  // planComputerUseStudy refuses it with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
55
64
  function localAppValidationReason(config) {
56
65
  if (config.subject.source === "local-app") {
57
- const type = config.actors[0]?.type ?? "";
66
+ const type = actorOf(config)?.type ?? "";
58
67
  if (config.execution?.target !== undefined && config.execution.target !== "local") {
59
68
  return "A local-app subject drives a local dev server in this process, with no E2B desktop. Set `execution.target: local` or omit it. To run on a hosted desktop, use an app-url subject with `execution.target: e2b-desktop`.";
60
69
  }
61
70
  if (!actorResolvesToComputerUse(type)) {
62
- return `actors[0].type must be a registered computer-use actor for local-app subjects (one of: ${registeredComputerUseActors().join(", ")}); the caller's custom executor runs the computer-use loop. Got "${type}".`;
71
+ return `actor.type must be a registered computer-use actor for local-app subjects (one of: ${registeredComputerUseActors().join(", ")}); the caller's custom executor runs the computer-use loop. Got "${type}".`;
63
72
  }
64
73
  if (computerUseParticipantCount(config) > 1) {
65
- return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Set actors[0].count to 1 and drop actors[0].lanes (use an app-url or clone subject on execution.target: e2b-desktop for fan-out).";
74
+ return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Remove `participants`, or set `participants: 1`. For fan-out, use an app-url or clone subject on execution.target: e2b-desktop.";
66
75
  }
67
- if (rosterOf(config.actors[0]) !== undefined) {
68
- return "`actors[0].lanes` (fan-out roster) is not supported on the in-process/local-app route: it provisions one E2B desktop per participant, which this route skips. Use an app-url or clone subject with execution.target: e2b-desktop.";
76
+ if (participantList(config) !== undefined) {
77
+ return "`participants` (fan-out roster) is not supported on the in-process/local-app route: it provisions one E2B desktop per participant, which this route skips. Use an app-url or clone subject with execution.target: e2b-desktop.";
69
78
  }
70
79
  if (config.policies?.allowPublicTargets === true) {
71
80
  return "`policies.allowPublicTargets` does not apply to a local-app subject: it is always a loopback dev server, so there is no public target to allow. Remove the setting.";
@@ -78,18 +87,18 @@ function localAppValidationReason(config) {
78
87
  // app; a computer-use actor drives a hosted desktop browser. Fail closed on mis-configs.
79
88
  function appUrlValidationReason(config) {
80
89
  if (config.subject.source === "app-url") {
81
- const type = config.actors[0]?.type ?? "";
90
+ const type = actorOf(config)?.type ?? "";
82
91
  if (actorResolvesToScriptedBrowser(type)) {
83
92
  // Scripted-browser route (all fail-closed, so claims match mechanism: a field that cannot act on
84
93
  // this route is rejected, never silently ignored).
85
94
  if (config.execution?.target !== undefined && config.execution.target !== "local") {
86
95
  return "A scripted-browser actor on an app-url subject runs on this machine. Set `execution.target: local` or omit it. To run scripted steps on a hosted desktop, use a clone subject.";
87
96
  }
88
- if (!config.scenario?.ref) {
89
- return "A scripted-browser study needs `scenario.ref`: the actor runs the browser steps in that scenario file.";
97
+ if (!scenarioRefOf(config)) {
98
+ return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
90
99
  }
91
- if ((config.actors[0]?.count ?? 1) > 2) {
92
- return "A scripted-browser study takes `actors[0].count` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
100
+ if ((participantOrSurfaceCount(config) ?? 1) > 2) {
101
+ return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
93
102
  }
94
103
  if (config.policies?.redactScreenshots === true) {
95
104
  return "`policies.redactScreenshots: true` is not supported on the scripted-browser route yet, so its screenshots would be stored unredacted in .humanish/. Remove the setting, or use a computer-use actor, which blurs screenshots as it takes them.";
@@ -106,17 +115,16 @@ function appUrlValidationReason(config) {
106
115
  return "app-url computer-use subjects require `execution.target: local` or `e2b-desktop`.";
107
116
  }
108
117
  if (!actorResolvesToComputerUse(type)) {
109
- return `actors[0].type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
118
+ return `actor.type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
110
119
  }
111
120
  // Multi-participant fan-out is consumed on this route (`per-lane-worlds`; the shared cua
112
- // cross-validation below enforces `lanes`/`count` XOR rules, the 16 cap, and the
113
- // per-participant target gates, and the allowPublicTargets+N>1 rejection for ambiguous one-target
114
- // fan-out).
121
+ // cross-validation below enforces the 16 cap, the per-participant target gates, and the
122
+ // allowPublicTargets+N>1 rejection for ambiguous one-target fan-out).
115
123
  // Loopback by default; an owner may declare a public/preview target via policies.
116
124
  const targets = [config.subject.appUrl ?? "", ...declaredTargets(config)];
117
125
  const unsafeTarget = targets.find((target) => !config.policies?.allowPublicTargets && !isLoopbackUrl(target));
118
126
  if (unsafeTarget !== undefined) {
119
- return "`subject.appUrl` and `actors[0].lanes[].target` must be loopback URLs (127.0.0.1 or localhost). To drive a deployed or preview URL you own, set `policies.allowPublicTargets: true`.";
127
+ return "`subject.appUrl` and `participants[].target` must be loopback URLs (127.0.0.1 or localhost). To drive a deployed or preview URL you own, set `policies.allowPublicTargets: true`.";
120
128
  }
121
129
  }
122
130
  }
@@ -125,7 +133,7 @@ function appUrlValidationReason(config) {
125
133
  // Scripted-browser actors on any other subject: only a provisioned clone on e2b-desktop.
126
134
  function scriptedBrowserValidationReason(config) {
127
135
  if (config.subject.source !== "app-url" &&
128
- actorResolvesToScriptedBrowser(config.actors[0]?.type)) {
136
+ actorResolvesToScriptedBrowser(actorOf(config)?.type)) {
129
137
  if (config.subject.source !== "clone") {
130
138
  return "scripted-browser actors require `subject.source: app-url` (a running app at a loopback URL) or `subject.source: clone` with `execution.target: e2b-desktop` (a provisioned synthetic subject).";
131
139
  }
@@ -142,20 +150,22 @@ function scriptedBrowserValidationReason(config) {
142
150
  if (!REPO_SLUG_PATTERN.test(repo)) {
143
151
  return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
144
152
  }
145
- if (config.subject.topology !== undefined) {
153
+ // migrate converts a v2 file's `subject.topology: shared-world` to `route: shared-world`, and
154
+ // refuses it here with the v2 parser's message.
155
+ if (declaresSharedWorld(config)) {
146
156
  return "A clone scripted-browser study does not support `subject.topology` yet: it runs one synthetic subject for its scripted actor. Remove `subject.topology`.";
147
157
  }
148
158
  if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
149
159
  return "A clone scripted-browser study does not support `subject.clone.fanout` or `subject.clone.keep` yet: its subject is always one sandbox, removed after the run.";
150
160
  }
151
- if (!config.scenario?.ref) {
152
- return "A scripted-browser study needs `scenario.ref`: the actor runs the browser steps in that scenario file.";
161
+ if (!scenarioRefOf(config)) {
162
+ return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
153
163
  }
154
- if ((config.actors[0]?.count ?? 1) > 2) {
155
- return "A scripted-browser study takes `actors[0].count` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
164
+ if ((participantOrSurfaceCount(config) ?? 1) > 2) {
165
+ return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
156
166
  }
157
- if (rosterOf(config.actors[0]) !== undefined) {
158
- return "`actors[0].lanes` is not supported on the scripted-browser route yet. Use `actors[0].count` to choose the desktop and mobile surfaces.";
167
+ if (participantList(config) !== undefined) {
168
+ return "`participants` is not supported on the scripted-browser route yet. Use `surfaces` to choose the desktop and mobile surfaces.";
159
169
  }
160
170
  if (config.policies?.redactScreenshots === true) {
161
171
  return "`policies.redactScreenshots: true` is not supported on the scripted-browser route yet, so its screenshots would be stored unredacted in .humanish/. Remove the setting, or use a computer-use actor, which blurs screenshots as it takes them.";
@@ -183,7 +193,7 @@ function scriptedBrowserValidationReason(config) {
183
193
  function cloneComputerUseValidationReason(config) {
184
194
  if (config.subject.source === "clone" &&
185
195
  config.execution?.target === "e2b-desktop" &&
186
- actorResolvesToComputerUse(config.actors[0]?.type)) {
196
+ actorResolvesToComputerUse(actorOf(config)?.type)) {
187
197
  if (!config.subject.serve) {
188
198
  return "A clone subject on the computer-use route needs `subject.serve` (start and url): humanish starts the app in the sandbox before the participant opens it.";
189
199
  }
@@ -195,8 +205,8 @@ function cloneComputerUseValidationReason(config) {
195
205
  return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
196
206
  }
197
207
  // Fan-out is consumed here: N participants each clone the same single repo into their own
198
- // E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the
199
- // `lanes`/`count` rules and the 16 cap; the single-repo rule above is unchanged.
208
+ // E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the 16 cap;
209
+ // the single-repo rule above is unchanged.
200
210
  }
201
211
  return null;
202
212
  }
@@ -210,8 +220,8 @@ function localTreeValidationReason(config) {
210
220
  if (config.execution?.target !== "e2b-desktop") {
211
221
  return "local-tree subjects require `execution.target: e2b-desktop`: the packed working tree is provisioned and served inside a hosted desktop sandbox; there is no local route for a local-tree subject.";
212
222
  }
213
- if (!actorResolvesToComputerUse(config.actors[0]?.type)) {
214
- return `actors[0].type must be a registered computer-use actor for local-tree subjects (one of: ${registeredComputerUseActors().join(", ")}); the actor drives the hosted desktop that serves the packed working tree. Got "${config.actors[0]?.type ?? ""}".`;
223
+ if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
224
+ return `actor.type must be a registered computer-use actor for local-tree subjects (one of: ${registeredComputerUseActors().join(", ")}); the actor drives the hosted desktop that serves the packed working tree. Got "${actorOf(config)?.type ?? ""}".`;
215
225
  }
216
226
  }
217
227
  return null;
@@ -238,7 +248,7 @@ function desktopCliValidationReason(config) {
238
248
  if (config.execution?.target !== undefined && config.execution.target !== "e2b-desktop") {
239
249
  return "A desktop-cli subject runs on a hosted desktop. Set `execution.target: e2b-desktop` or omit it.";
240
250
  }
241
- if (!actorResolvesToComputerUse(config.actors[0]?.type ?? "")) {
251
+ if (!actorResolvesToComputerUse(actorOf(config)?.type ?? "")) {
242
252
  return "desktop-cli subjects need a registered computer-use actor: the participant reads the screen and types, which is what makes an interactive surface studiable at all.";
243
253
  }
244
254
  const install = config.subject.product.install;
@@ -255,15 +265,15 @@ function desktopCliValidationReason(config) {
255
265
  // fan-out lands.
256
266
  function terminalValidationReason(config) {
257
267
  if (config.subject.source === "terminal-product") {
258
- const type = config.actors[0]?.type ?? "";
268
+ const type = actorOf(config)?.type ?? "";
259
269
  if (config.execution?.target !== undefined && config.execution.target !== "e2b-terminal") {
260
270
  return "A terminal-product subject runs its agent in an E2B shell. Set `execution.target: e2b-terminal` or omit it; `local` and `e2b-desktop` are not supported.";
261
271
  }
262
272
  if (!actorResolvesToTerminal(type)) {
263
- return `actors[0].type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
273
+ return `actor.type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
264
274
  }
265
- if ((config.actors[0]?.count ?? 1) > 1) {
266
- return "Terminal fan-out to more than one participant is not supported yet; set actors[0].count to 1.";
275
+ if ((participantOrSurfaceCount(config) ?? 1) > 1) {
276
+ return "Terminal fan-out to more than one participant is not supported yet; set participants to 1.";
267
277
  }
268
278
  }
269
279
  else if (config.execution?.target === "e2b-terminal") {
@@ -271,7 +281,7 @@ function terminalValidationReason(config) {
271
281
  // mis-config: reject, never silently mishandle (mirrors app-url's e2b-desktop pairing rule).
272
282
  return "`execution.target: e2b-terminal` requires `subject.source: terminal-product` with a registered terminal actor.";
273
283
  }
274
- else if (actorResolvesToTerminal(config.actors[0]?.type)) {
284
+ else if (actorResolvesToTerminal(actorOf(config)?.type)) {
275
285
  // A registered terminal actor on a non-terminal-product subject: rejected, never ignored (the
276
286
  // terminal agent only studies a declared terminal-product from public surfaces).
277
287
  return "terminal actors require `subject.source: terminal-product` (a CLI/product the agent studies from public surfaces); other subjects are not supported on this route.";
@@ -283,7 +293,7 @@ function terminalValidationReason(config) {
283
293
  // Terminal actors were already refused above with their own message.
284
294
  function cloneActorValidationReason(config) {
285
295
  if (config.subject.source === "clone") {
286
- const type = config.actors[0]?.type ?? "";
296
+ const type = actorOf(config)?.type ?? "";
287
297
  if (!actorResolvesToComputerUse(type) && !actorResolvesToScriptedBrowser(type)) {
288
298
  return `clone subjects need a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}) or scripted-browser actor (one of: ${registeredScriptedBrowserActors().join(", ")}); humanish clones and serves the app for that participant to drive. Got "${type}".`;
289
299
  }
@@ -1,15 +1,38 @@
1
- import { type StudyParseResult } from "./types.js";
2
- /** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
3
- export declare const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
1
+ import { type StudyConfig, type StudyParseFailure, type StudyParseResult } from "./types.js";
4
2
  /**
5
3
  * The package's study parser (src/index.ts) and the one discovery uses: a humanish.study.v3
6
4
  * document. It refuses a humanish.lab.v2 one with HUMANISH_STUDY_V2_UNSUPPORTED.
7
5
  */
8
6
  export declare function parseStudy(raw: unknown): StudyParseResult;
7
+ interface StudyV3Options {
8
+ /**
9
+ * `report`, for `humanish migrate`: list the fields the route does not read in `inert`, in place
10
+ * of refusing the study for them. A shared world may then give `participants` as a count or
11
+ * leave it out, as a v2 file could (readStudyFront).
12
+ */
13
+ readonly inert?: "refuse" | "report";
14
+ }
15
+ type StudyV3ParseResult = {
16
+ ok: true;
17
+ config: StudyConfig;
18
+ warnings: string[];
19
+ inert?: string[];
20
+ } | StudyParseFailure;
9
21
  /**
10
- * Validate a parsed YAML object, humanish.study.v3 or humanish.lab.v2, into a StudyConfig. Pure:
11
- * the caller owns file IO. Structural validation only. Fields the engine does not yet consume are
12
- * accepted but reported in `warnings` so `study show` never silently swallows a setting that does
13
- * nothing. migrate's conversion (convert.ts) calls it directly, since it reads v2 files.
22
+ * Validate a humanish.study.v3 document into a StudyConfig. Pure: the caller owns file IO. Fields the
23
+ * route does not read refuse the study; migrate's report mode lists them in `inert` instead.
14
24
  */
15
- export declare function parseStudyDocument(raw: unknown): StudyParseResult;
25
+ export declare function parseStudyV3(raw: Record<string, unknown>, options?: StudyV3Options): StudyV3ParseResult;
26
+ /** Why runStudy refuses a config before anything runs. */
27
+ interface ConfigRefusal {
28
+ readonly code: "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID";
29
+ readonly message: string;
30
+ }
31
+ /**
32
+ * Why runStudy refuses a config a library caller passes, or undefined: a humanish.lab.v2 config,
33
+ * one that still sets a StudyConfig field of humanish 0.110, one with no `actor` object, and one
34
+ * whose `route` is not the route its subject and actor take, with parseStudy's message. A planner
35
+ * reads only the v3 fields, so a budget left in `execution.caps` would otherwise run uncapped.
36
+ */
37
+ export declare function libraryConfigRefusal(config: StudyConfig): ConfigRefusal | undefined;
38
+ export {};