humanish 0.110.3 → 0.110.4

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 (77) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/actors/scripted-browser/actor.js +17 -8
  3. package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
  4. package/dist/actors/scripted-browser/launch-env.js +50 -0
  5. package/dist/analysis/automatic-completion.d.ts +2 -1
  6. package/dist/analysis/automatic-completion.js +7 -2
  7. package/dist/cli/commands/migrate.js +1 -1
  8. package/dist/cli/commands/runs.js +5 -2
  9. package/dist/cli/commands/study-route-computer-use.js +3 -2
  10. package/dist/cli/commands/study-route-preview.js +2 -1
  11. package/dist/cli/commands/study-route-shared-world.js +3 -3
  12. package/dist/cli/commands/study.js +4 -1
  13. package/dist/routes/computer-use/live-phase.js +8 -1
  14. package/dist/routes/computer-use/local-vm.js +2 -1
  15. package/dist/routes/computer-use/plan.js +6 -5
  16. package/dist/routes/computer-use/route.js +2 -1
  17. package/dist/routes/computer-use/setup.js +2 -1
  18. package/dist/routes/scripted/plan.js +5 -4
  19. package/dist/routes/scripted/route.js +5 -1
  20. package/dist/routes/scripted/surface-sessions.d.ts +2 -0
  21. package/dist/routes/scripted/surface-sessions.js +2 -1
  22. package/dist/routes/shared-world/participant-specs.js +5 -1
  23. package/dist/routes/shared-world/plan.js +7 -6
  24. package/dist/routes/shared-world/route.js +3 -3
  25. package/dist/routes/terminal/plan.js +4 -3
  26. package/dist/routes/terminal/route.js +2 -1
  27. package/dist/routes/terminal/session.js +1 -0
  28. package/dist/run/display.d.ts +10 -0
  29. package/dist/run/display.js +22 -0
  30. package/dist/run/reclaim.d.ts +4 -2
  31. package/dist/run/reclaim.js +61 -18
  32. package/dist/run/run.d.ts +11 -1
  33. package/dist/run/run.js +24 -5
  34. package/dist/run/status.d.ts +6 -0
  35. package/dist/run/status.js +1 -0
  36. package/dist/run/stored-runs.js +26 -11
  37. package/dist/study/composition-rules.js +32 -32
  38. package/dist/study/config.d.ts +9 -2
  39. package/dist/study/config.js +14 -5
  40. package/dist/study/doctor.js +5 -4
  41. package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
  42. package/dist/study/{convert.js → migrate/convert.js} +29 -31
  43. package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
  44. package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
  45. package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
  46. package/dist/study/migrate/v2.d.ts +68 -0
  47. package/dist/study/migrate/v2.js +480 -0
  48. package/dist/study/parse/actors.d.ts +46 -1
  49. package/dist/study/parse/actors.js +116 -69
  50. package/dist/study/parse/execution.js +2 -2
  51. package/dist/study/parse/study-v3.d.ts +26 -0
  52. package/dist/study/parse/study-v3.js +53 -35
  53. package/dist/study/parse/study.d.ts +38 -0
  54. package/dist/study/parse/study.js +225 -0
  55. package/dist/study/persona-resolve.d.ts +2 -4
  56. package/dist/study/persona-resolve.js +2 -1
  57. package/dist/study/plan-base.d.ts +1 -1
  58. package/dist/study/plan-base.js +5 -4
  59. package/dist/study/plan-participants.js +12 -11
  60. package/dist/study/plan.js +4 -3
  61. package/dist/study/preflight-probes.js +2 -2
  62. package/dist/study/preflight.js +2 -2
  63. package/dist/study/routing.js +12 -15
  64. package/dist/study/run-study-options.js +3 -3
  65. package/dist/study/study-fields.d.ts +28 -0
  66. package/dist/study/study-fields.js +51 -0
  67. package/dist/study/summary.js +13 -17
  68. package/dist/study/types.d.ts +40 -0
  69. package/dist/study/url-credentials.js +2 -1
  70. package/dist/study/validation.d.ts +2 -0
  71. package/dist/study/validation.js +46 -41
  72. package/dist/study/warnings.js +5 -4
  73. package/dist/substrates/local/runtime-config.js +9 -9
  74. package/dist/tui-app.js +3 -3
  75. package/docs/contracts/schemas.md +13 -4
  76. package/package.json +1 -1
  77. /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
@@ -63,7 +63,7 @@ import { invalid, optionalStr, str } from "./parse/values.js";
63
63
  import { isComputerUseComposition, isSharedWorldComposition, routeOf } from "./routing.js";
64
64
  import { declaredParticipantIds } from "./plan-participants.js";
65
65
  import { ID_PATTERN, V2_SCHEMA, STUDY_SCHEMA, } from "./types.js";
66
- import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, taskProtocolValidationReason, } from "./validation.js";
66
+ import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
67
67
  import { forwardDeclaredWarnings, inertFieldLabels } from "./warnings.js";
68
68
  import { plural } from "../run/text.js";
69
69
  /** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
@@ -84,7 +84,7 @@ export function parseStudy(raw) {
84
84
  * Validate a parsed YAML object, humanish.study.v3 or humanish.lab.v2, into a StudyConfig. Pure:
85
85
  * the caller owns file IO. Structural validation only. Fields the engine does not yet consume are
86
86
  * accepted but reported in `warnings` so `study show` never silently swallows a setting that does
87
- * nothing. migrate's conversion (convert.ts) calls it directly, since it reads v2 files.
87
+ * nothing. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
88
88
  */
89
89
  export function parseStudyDocument(raw) {
90
90
  if (!isRecord(raw)) {
@@ -190,9 +190,18 @@ function parseV2(raw) {
190
190
  return commsResult;
191
191
  if (commsResult.value)
192
192
  config.comms = commsResult.value;
193
- if (config.comms?.email?.smtp && config.subject.topology === "shared-world") {
194
- return invalid("SMTP capture is not supported yet for shared-world studies. Use the default per-lane-worlds topology for SMTP, or configure supported HTTP email capture for concurrent shared-world studies.");
195
- }
193
+ return checkStudyConfig(config);
194
+ }
195
+ /**
196
+ * The checks that read more than one section of a parsed config, and the defaults they fill:
197
+ * shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
198
+ * parseV2 runs them once every section has parsed. parse/study.ts runs them on its v2 view of a v3
199
+ * study.
200
+ */
201
+ export function checkStudyConfig(config) {
202
+ const smtpReason = smtpValidationReason(config);
203
+ if (smtpReason)
204
+ return invalid(smtpReason);
196
205
  const mediaReason = desktopMediaValidationReason(config);
197
206
  if (mediaReason)
198
207
  return invalid(mediaReason);
@@ -9,6 +9,7 @@ import { externalCatchHealthy } from "../comms/sandbox-catch.js";
9
9
  import { receivingRequiredKey } from "../comms/setup.js";
10
10
  import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
11
11
  import { cli } from "../cli/invocation.js";
12
+ import { actorOf } from "./study-fields.js";
12
13
  /** A ready row's message, with the schema values the launch recorded. */
13
14
  function readyMessage(message, readiness) {
14
15
  const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
@@ -151,7 +152,7 @@ export async function studySetupChecks(args) {
151
152
  {
152
153
  name: "study route",
153
154
  ok: true,
154
- message: `${config.id}: ${config.actors[0]?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
155
+ message: `${config.id}: ${actorOf(config)?.type ?? "synthetic"} / ${route} / ${dryRun ? "dry-run (no live participant)" : "live"}`,
155
156
  },
156
157
  ];
157
158
  if (dryRun)
@@ -246,16 +247,16 @@ async function participantChecks(config, route, keys, local, args, checkAccount)
246
247
  ];
247
248
  }
248
249
  if ((route !== "computer-use" && route !== "shared-world") ||
249
- config.actors[0]?.type !== "local-agent")
250
+ actorOf(config)?.type !== "local-agent")
250
251
  return [];
251
- const choice = config.actors[0]?.localAgent ?? "codex";
252
+ const choice = actorOf(config)?.localAgent ?? "codex";
252
253
  const agent = args.agents.find((entry) => entry.id === choice);
253
254
  if (local)
254
255
  return [await localCodexParticipantCheck({ env: args.env, readiness: checkAccount })];
255
256
  // A signed-in hosted Codex gets the operator handshake; its sign-in status alone does not show
256
257
  // that the operator's config, release and model admit a launch.
257
258
  if (choice === "codex" && agent?.authStatus === "authenticated")
258
- return [await hostedCodexParticipantCheck(args, config.actors[0])];
259
+ return [await hostedCodexParticipantCheck(args, actorOf(config))];
259
260
  const outdated = agent === undefined ? undefined : localAgentVersionRefusal(agent);
260
261
  return [
261
262
  {
@@ -1,4 +1,4 @@
1
- import { type StudyRoute } from "./routing.js";
1
+ import type { StudyRoute } from "../routing.js";
2
2
  /** A key the conversion moved, by its v2 path and its v3 path. */
3
3
  export interface MovedKey {
4
4
  readonly from: string;
@@ -1,16 +1,15 @@
1
1
  // Convert a humanish.lab.v2 study file to humanish.study.v3. It edits the parsed YAML document in
2
2
  // place and moves each pair node, so the comments on a moved key move with it. The result lists
3
3
  // every key it moved and every key it dropped. It refuses a file whose v3 form would parse or plan
4
- // differently from the v2 source with the dropped keys removed.
4
+ // differently from the v2 source with the dropped keys removed. v2.ts reads the v2 source.
5
5
  import { isAlias, isMap, isScalar, isSeq, LineCounter, Pair, parseDocument, Scalar, visit, YAMLMap, YAMLSeq, } from "yaml";
6
- import { parseStudyDocument } from "./config.js";
7
- import { focusOf } from "./parse/actors.js";
8
- import { posInt } from "./parse/values.js";
9
- import { planStudy } from "./plan.js";
10
- import { routeOf } from "./routing.js";
11
- import { V2_SCHEMA, STUDY_SCHEMA } from "./types.js";
12
- import { inertFieldPaths } from "./warnings.js";
13
- import { deleteNodeFieldPath, deletePlainFieldPath, pairIndex, readFieldPath, } from "./field-paths.js";
6
+ import { parseStudy } from "../config.js";
7
+ import { parseStudyV3 } from "../parse/study.js";
8
+ import { posInt } from "../parse/values.js";
9
+ import { planStudy } from "../plan.js";
10
+ import { V2_SCHEMA, STUDY_SCHEMA } from "../types.js";
11
+ import { deleteNodeFieldPath, deletePlainFieldPath, pairIndex, readFieldPath, } from "../field-paths.js";
12
+ import { checkV2, droppedPaths, v2ShapeReason, v2ToV3Raw } from "./v2.js";
14
13
  function isPlainRecord(value) {
15
14
  return typeof value === "object" && value !== null && !Array.isArray(value);
16
15
  }
@@ -36,10 +35,19 @@ export function convertStudyText(text, cwd) {
36
35
  return refuse("it is not a YAML mapping.");
37
36
  if (raw.schema !== V2_SCHEMA)
38
37
  return refuse(`its schema is not ${V2_SCHEMA}.`);
39
- const source = parseStudyDocument(raw);
38
+ // The v2 parser's checks: the sections and the v2-only budget rule, then the checks the formats
39
+ // share on the v3 form of the file, then the rules on v2 shapes the v3 form cannot hold.
40
+ const source = checkV2(raw);
40
41
  if (!source.ok)
41
- return refuse(`it does not parse: ${source.error.message}`);
42
- const route = routeOf(source.config);
42
+ return refuse(`it does not parse: ${source.message}`);
43
+ const study = source.value;
44
+ const { route } = study;
45
+ const view = parseStudyV3(v2ToV3Raw(raw, route), { inert: "report" });
46
+ if (!view.ok)
47
+ return refuse(`it does not parse: ${view.error.message}`);
48
+ const shape = v2ShapeReason(study);
49
+ if (shape)
50
+ return refuse(`it does not parse: ${shape}`);
43
51
  const actorRaw = Array.isArray(raw.actors) ? raw.actors[0] : undefined;
44
52
  if (route === "terminal" && isPlainRecord(actorRaw)) {
45
53
  for (const key of ["count", "laneFocus"]) {
@@ -50,7 +58,7 @@ export function convertStudyText(text, cwd) {
50
58
  }
51
59
  const root = doc.contents;
52
60
  const dropped = [];
53
- for (const path of droppedPaths(source.config, route, raw)) {
61
+ for (const path of droppedPaths(study, view.inert ?? [])) {
54
62
  const value = readFieldPath(raw, path);
55
63
  const pairs = deleteNodeFieldPath(root, path);
56
64
  if (value === undefined || pairs.length === 0)
@@ -60,8 +68,7 @@ export function convertStudyText(text, cwd) {
60
68
  }
61
69
  const moved = [];
62
70
  // v2 reads some laneFocus values as no focus at all ({}, a valueless key, valueless fields).
63
- const focused = focusOf(source.config.actors[0]) !== undefined;
64
- const converted = convertDocument(root, route, focused, moved);
71
+ const converted = convertDocument(root, route, study.focus !== undefined, moved);
65
72
  if (!converted.ok)
66
73
  return converted;
67
74
  const out = doc.toString({ lineWidth: foldWidth(text), flowCollectionPadding: false });
@@ -69,7 +76,7 @@ export function convertStudyText(text, cwd) {
69
76
  const lost = lostComment(text, out, dropped);
70
77
  if (lost !== undefined)
71
78
  return refuse(`it would lose the comment "${lost}".`);
72
- const check = samePlans(raw, dropped.map((key) => key.path), out, cwd);
79
+ const check = samePlans(raw, route, dropped.map((key) => key.path), out, cwd);
73
80
  if (!check.ok)
74
81
  return check;
75
82
  return { ok: true, conversion: { text: out, route, moved, dropped } };
@@ -105,16 +112,6 @@ function lostComment(source, output, dropped) {
105
112
  }
106
113
  return undefined;
107
114
  }
108
- // The inert-field table's paths for the route, plus terminal `execution.timeoutMs`, which no
109
- // terminal code reads. Parents come before their children, so a child under a dropped parent is
110
- // reported with the parent.
111
- function droppedPaths(config, route, raw) {
112
- const paths = inertFieldPaths(config);
113
- if (route === "terminal" && readFieldPath(raw, "execution.timeoutMs") !== undefined)
114
- paths.push("execution.timeoutMs");
115
- const depth = (path) => path.split(".").length;
116
- return [...new Set(paths)].sort((left, right) => depth(left) - depth(right));
117
- }
118
115
  // Folded text is folded again on output. Most of a file's long lines end near the width its author
119
116
  // folded at, so the 90th percentile of their lengths stands for it, between 80 and 120.
120
117
  function foldWidth(text) {
@@ -406,19 +403,20 @@ function numericCount(pair) {
406
403
  function plain(value) {
407
404
  return JSON.parse(JSON.stringify(value));
408
405
  }
409
- function samePlans(raw, dropped, text, cwd) {
406
+ // The YAML rewrite against v2ToV3Raw of the source without the dropped keys: two rewrites written
407
+ // separately, which must parse to the same study and plan the same runs.
408
+ function samePlans(raw, route, dropped, text, cwd) {
410
409
  const projected = structuredClone(raw);
411
410
  for (const path of dropped)
412
411
  deletePlainFieldPath(projected, path);
413
- const before = parseStudyDocument(projected);
412
+ const before = parseStudy(v2ToV3Raw(projected, route));
414
413
  if (!before.ok) {
415
414
  return refuse(`it does not parse once the unread keys are dropped: ${before.error.message}`);
416
415
  }
417
- const after = parseStudyDocument(parseDocument(text).toJS());
416
+ const after = parseStudy(parseDocument(text).toJS());
418
417
  if (!after.ok)
419
418
  return refuse(`its v3 form does not parse: ${after.error.message}`);
420
- if (JSON.stringify(plain({ ...after.config, schema: V2_SCHEMA })) !==
421
- JSON.stringify(plain(before.config))) {
419
+ if (JSON.stringify(plain(after.config)) !== JSON.stringify(plain(before.config))) {
422
420
  return refuse("its v3 form parses to a different study.");
423
421
  }
424
422
  for (const dryRun of [true, false]) {
@@ -1,4 +1,4 @@
1
- import type { StudyRoute } from "./routing.js";
1
+ import type { StudyRoute } from "../routing.js";
2
2
  import { type DroppedKey, type MovedKey } from "./convert.js";
3
3
  /** The schema of `humanish migrate --json`. */
4
4
  declare const MIGRATE_SCHEMA = "humanish.migrate-result.v1";
@@ -14,9 +14,9 @@ import { randomUUID } from "node:crypto";
14
14
  import { link, lstat, readdir, readFile, realpath, rename, rmdir } from "node:fs/promises";
15
15
  import path from "node:path";
16
16
  import { parseDocument } from "yaml";
17
- import { isNodeError } from "../run/type-guards.js";
17
+ import { isNodeError } from "../../run/type-guards.js";
18
18
  import { convertStudyText, isStudyV3 } from "./convert.js";
19
- import { otherStudyFiles, STUDY_DIRECTORIES, studyFileStem } from "./files.js";
19
+ import { otherStudyFiles, STUDY_DIRECTORIES, studyFileStem } from "../files.js";
20
20
  import { assertDirectory, bindDirectory, exists, fileIdentity, NO_HARD_LINKS, removeChecked, removeOwned, restoreName, sameFile, sha256, writeOwned, } from "./owned-files.js";
21
21
  /** The schema of `humanish migrate --json`. */
22
22
  const MIGRATE_SCHEMA = "humanish.migrate-result.v1";
@@ -6,7 +6,7 @@ import { createHash, randomUUID } from "node:crypto";
6
6
  import { constants } from "node:fs";
7
7
  import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
8
8
  import path from "node:path";
9
- import { isNodeError } from "../run/type-guards.js";
9
+ import { isNodeError } from "../../run/type-guards.js";
10
10
  export function sha256(bytes) {
11
11
  return createHash("sha256").update(bytes).digest("hex");
12
12
  }
@@ -0,0 +1,68 @@
1
+ import { type StudyRoute } from "../routing.js";
2
+ import { type StudyConfig, type StudyParticipantEntry, type StudyScenarioCaps } from "../types.js";
3
+ type Raw = Record<string, unknown>;
4
+ type Read<T> = {
5
+ ok: true;
6
+ value: T;
7
+ } | {
8
+ ok: false;
9
+ message: string;
10
+ };
11
+ /** `laneFocus`: the steer each of a counted group of participants gets. */
12
+ interface V2Focus {
13
+ id?: string;
14
+ label?: string;
15
+ instruction?: string;
16
+ }
17
+ /** The `scenario` object. */
18
+ interface V2Scenario {
19
+ ref?: string;
20
+ mode?: "dry-run" | "live";
21
+ inline?: Raw;
22
+ caps?: StudyScenarioCaps;
23
+ }
24
+ /** A v2 file that checkV2 accepted, as the v2 parser reads it. */
25
+ export interface V2Study {
26
+ readonly route: StudyRoute;
27
+ /**
28
+ * The sections that decide the route and that the v2 parser's first cross-section checks read:
29
+ * subject, the actor without its participant keys, execution, policies and comms.
30
+ */
31
+ readonly composition: StudyConfig;
32
+ readonly count: number | undefined;
33
+ /** `actors[0].lanes`, or the participants `actors[0].roster` expands into. */
34
+ readonly entries: readonly StudyParticipantEntry[] | undefined;
35
+ readonly focus: V2Focus | undefined;
36
+ readonly executionCaps: StudyScenarioCaps | undefined;
37
+ readonly scenario: V2Scenario | undefined;
38
+ readonly personas: boolean;
39
+ readonly terminalTimeout: boolean;
40
+ }
41
+ /**
42
+ * Check a humanish.lab.v2 file the way the v2 parser does, up to the checks that read more than
43
+ * one section, and then the budget rule those checks apply first. Each refusal is that parser's
44
+ * message.
45
+ */
46
+ export declare function checkV2(raw: Raw): Read<V2Study>;
47
+ /**
48
+ * The refusals the v2 parser gives for shapes a v3 file cannot express, once the checks both
49
+ * formats share have passed on the study without them. A file that also breaks a shared check
50
+ * gets that check's message.
51
+ */
52
+ export declare function v2ShapeReason(study: V2Study): string | undefined;
53
+ /**
54
+ * A v2 file as a plain humanish.study.v3 record, for parseStudyV3 and parseStudy. It reads each key
55
+ * it moves the way the v2 parser reads it (`count` as a positive integer, `laneFocus` through its
56
+ * string fields, `roster` groups expanded, `scenario.ref` and `scenario.mode` as strings), and
57
+ * leaves out every key no v3 file can hold, so it parses whenever the v2 parser would. A caps
58
+ * block keeps the keys its route reads. A shared world keeps a count or no list, which only
59
+ * parseStudyV3's report mode takes.
60
+ */
61
+ export declare function v2ToV3Raw(raw: Raw, route: StudyRoute): Raw;
62
+ /**
63
+ * The keys migrate drops: the v2-only rows above and `inert`, the fields parseStudyV3 reports the
64
+ * route does not read, then terminal `execution.timeoutMs`, which no terminal code reads. Parents
65
+ * come before their children, so a child under a dropped parent is reported with the parent.
66
+ */
67
+ export declare function droppedPaths(study: V2Study, inert: readonly string[]): string[];
68
+ export {};