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
@@ -61,6 +61,7 @@ export async function runLiveTerminalSession(args) {
61
61
  // This entry point is the live terminal route; its dry-run sibling is a separate function.
62
62
  mode: "live",
63
63
  study: plan.study,
64
+ warnings: plan.warnings,
64
65
  renderReview: renderTerminalReviewMarkdown,
65
66
  observer: { open: input.open === true, render: deps.renderObserver },
66
67
  now,
@@ -77,7 +78,7 @@ export async function runLiveTerminalSession(args) {
77
78
  });
78
79
  const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
79
80
  // The ledgers + capture buffers, mutated through the live lifecycle.
80
- const recorder = createTerminalRecorder({ nowIso, sanitize, knownSecretValues });
81
+ const recorder = createTerminalRecorder({ nowIso, sanitize, knownSecretValues, verdictNonce });
81
82
  const { version, model, modelSource, reasoningEffort } = plan.runtime;
82
83
  const runtime = declaredRuntimeProvenance({
83
84
  ...(version === undefined ? {} : { version }),
@@ -1,4 +1,7 @@
1
1
  import type { ActorTokenUsage } from "../../actors/contract.js";
2
+ /** The usage records in complete stdout lines, kept from output past the transcript cap so
3
+ * parseTerminalTokenUsage can count them with the stored ones. */
4
+ export declare function terminalUsageRecords(lines: string): string[];
2
5
  /**
3
6
  * Accumulate runtime-turn usage from a captured `codex exec --json` stream. A Codex turn may
4
7
  * include multiple provider requests; these records cannot establish per-request pricing tiers.
@@ -19,6 +19,11 @@ const USAGE_RE = /"type"\s*:\s*"turn\.completed"\s*,\s*"usage"\s*:\s*(\{[^}\n]*\
19
19
  function num(value) {
20
20
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
21
21
  }
22
+ /** The usage records in complete stdout lines, kept from output past the transcript cap so
23
+ * parseTerminalTokenUsage can count them with the stored ones. */
24
+ export function terminalUsageRecords(lines) {
25
+ return Array.from(lines.matchAll(USAGE_RE), (match) => match[0]);
26
+ }
22
27
  /**
23
28
  * Accumulate runtime-turn usage from a captured `codex exec --json` stream. A Codex turn may
24
29
  * include multiple provider requests; these records cannot establish per-request pricing tiers.
@@ -53,7 +53,8 @@ export interface TerminalProductScoringContext {
53
53
  /**
54
54
  * The full normalized transcript of the in-sandbox agent session, scrubbed (literal known
55
55
  * values) then redacted (shape patterns) at the source, capped at MAX_TRANSCRIPT_BYTES, and
56
- * byte-identical to the persisted terminal-transcript.txt artifact. The trace's transcriptTail
56
+ * byte-identical to the persisted terminal-transcript.txt artifact. A capped transcript ends
57
+ * with a `[humanish]` line giving the byte count that was not stored. The trace's transcriptTail
57
58
  * is a ~2KB projection of this; a scorer needs the whole session so a rubric can find
58
59
  * command-tier evidence anywhere in it.
59
60
  */
@@ -171,7 +172,7 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
171
172
  observer?: ObserverResult;
172
173
  warnings: string[];
173
174
  error?: {
174
- code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN" | "HUMANISH_RUN_ID_IN_USE";
175
+ code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_TERMINAL_FAILED" | "HUMANISH_TERMINAL_ACTOR_UNSUPPORTED" | "HUMANISH_TERMINAL_SUBJECT_INVALID" | "HUMANISH_TERMINAL_KEYPLACEMENT_INVALID" | "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING" | "HUMANISH_TERMINAL_KEYS_MISSING" | "HUMANISH_TERMINAL_CAPS_MISSING" | "HUMANISH_TERMINAL_CAPS_INVALID" | "HUMANISH_TERMINAL_UNPRICED_CAP" | "HUMANISH_TERMINAL_CAPS_EXCEEDED" | "HUMANISH_TERMINAL_CREDENTIAL_DENIED" | "HUMANISH_TERMINAL_CLEANUP_UNPROVEN" | "HUMANISH_RUN_ID_IN_USE";
175
176
  message: string;
176
177
  };
177
178
  }
@@ -32,5 +32,6 @@ export const TEXT_ITEMS_BYTES = 128 * 1024;
32
32
  // The longest stdout line the participant reader holds while waiting for its newline.
33
33
  export const PENDING_LINE_CHARS = 1024 * 1024;
34
34
  // Hard cap on the retained event-stream + transcript size, so a runaway agent cannot balloon the
35
- // bundle. Redaction runs pre-truncation so a cut can never split a secret past the scrubber.
35
+ // bundle. Redaction runs pre-truncation so a cut can never split a secret past the scrubber. Output
36
+ // past the cap is still read for the verdict marker, token usage and the trace's agent text.
36
37
  export const MAX_TRANSCRIPT_BYTES = 512 * 1024;
@@ -67,6 +67,7 @@ async function runDryRunInScope(options, scope) {
67
67
  mintRunId: () => `dryrun-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`,
68
68
  mode: options.dryRun ? "dry-run" : "live",
69
69
  study: options.study,
70
+ warnings: options.warnings,
70
71
  renderReview: renderReviewMarkdown,
71
72
  // The run starts at the time its id and source were stamped with.
72
73
  now: () => Date.parse(createdAt),
@@ -5,6 +5,8 @@ export declare const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
5
5
  export interface RunOptions {
6
6
  /** Which manifest produced this run. */
7
7
  study?: RunStudyProvenance;
8
+ /** Warnings about the study's own fields, recorded in the bundle as warn events. */
9
+ warnings?: readonly string[];
8
10
  cwd: string;
9
11
  dryRun?: boolean;
10
12
  runId?: string;
@@ -30,7 +32,7 @@ export interface RunResult {
30
32
  observer?: ObserverResult;
31
33
  warnings: string[];
32
34
  error?: {
33
- code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_ANALYSIS_UNSUPPORTED" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_COMMS_UNSUPPORTED" | "HUMANISH_LIVE_RUN_UNIMPLEMENTED" | "HUMANISH_INVALID_CWD" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_INVALID_PARTICIPANT_COUNT" | "HUMANISH_INVALID_PORT" | "HUMANISH_UNSUPPORTED_RERUN_FLAGS" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_RUN_OPTION_CONFLICT" | "HUMANISH_OBSERVE_OPTION_CONFLICT" | "HUMANISH_STUDY_SCORER_BAD_REF" | "HUMANISH_STUDY_SCORER_NOT_FOUND" | "HUMANISH_STUDY_SCORER_LOAD_FAILED" | "HUMANISH_STUDY_SCORER_NO_HOOKS" | "HUMANISH_STUDY_SCORER_UNSUPPORTED_BACKEND";
35
+ code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_ANALYSIS_UNSUPPORTED" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_STUDY_COMMS_UNSUPPORTED" | "HUMANISH_LIVE_RUN_UNIMPLEMENTED" | "HUMANISH_INVALID_CWD" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_INVALID_PARTICIPANT_COUNT" | "HUMANISH_INVALID_PORT" | "HUMANISH_UNSUPPORTED_RERUN_FLAGS" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_RUN_OPTION_CONFLICT" | "HUMANISH_OBSERVE_OPTION_CONFLICT" | "HUMANISH_STUDY_SCORER_BAD_REF" | "HUMANISH_STUDY_SCORER_NOT_FOUND" | "HUMANISH_STUDY_SCORER_LOAD_FAILED" | "HUMANISH_STUDY_SCORER_NO_HOOKS" | "HUMANISH_STUDY_SCORER_UNSUPPORTED_BACKEND";
34
36
  message: string;
35
37
  };
36
38
  }
package/dist/run/run.d.ts CHANGED
@@ -22,6 +22,8 @@ interface StartRunOptions {
22
22
  };
23
23
  /** Clock for `createdAt` and the latest pointer. */
24
24
  now?: (() => number) | undefined;
25
+ /** Warnings about the study's own fields. Every bundle write records each as a warn event. */
26
+ warnings?: readonly string[] | undefined;
25
27
  }
26
28
  /** How a run ended as an execution, as run.json records it: the result's ok and execution outcome. */
27
29
  interface RecordedOutcome {
package/dist/run/run.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { redactText } from "../evidence/redaction.js";
6
6
  import { buildObserverData } from "../observer/data.js";
7
7
  import { renderObserver } from "../observer/render.js";
8
- import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD } from "./bundle.js";
8
+ import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD, } from "./bundle.js";
9
9
  import { judgeExecution, } from "./judge.js";
10
10
  import {} from "./results.js";
11
11
  import { createRunArtifactPaths, validatePreparedRunArtifactPaths, } from "./paths.js";
@@ -112,8 +112,20 @@ function observerTarget(options) {
112
112
  };
113
113
  }
114
114
  function runPublisher(args) {
115
- const { options, runId, paths, runStatus, now, admit, participantsRan } = args;
115
+ const { options, runId, createdAt, paths, runStatus, now, admit, participantsRan } = args;
116
116
  const observer = observerTarget(options);
117
+ // The study's warnings join whatever events the route wrote, once, in every write.
118
+ const studyWarnings = (options.warnings ?? []).map((message, index) => ({
119
+ id: `event-study-warning-${String(index + 1).padStart(3, "0")}`,
120
+ at: createdAt,
121
+ level: "warn",
122
+ type: "study.warning",
123
+ message,
124
+ }));
125
+ const withStudyWarnings = (events) => [
126
+ ...events.filter((event) => !studyWarnings.some((warning) => warning.id === event.id)),
127
+ ...studyWarnings,
128
+ ];
117
129
  let pointerWritten = false;
118
130
  // The bundle of the last write asked for, and the outcome every later write carries once a
119
131
  // signal stopped the run, so no write after the interrupt can drop it.
@@ -145,6 +157,7 @@ function runPublisher(args) {
145
157
  const recorded = interrupted ?? outcome;
146
158
  const publicBundle = await withPublicSandboxIds(paths, {
147
159
  ...evidence,
160
+ events: withStudyWarnings(evidence.events),
148
161
  cwd: PUBLIC_TARGET_CWD,
149
162
  ...(recorded === undefined ? {} : { outcome: recorded }),
150
163
  });
@@ -271,6 +284,7 @@ export async function runScope(fn) {
271
284
  const publisher = runPublisher({
272
285
  options,
273
286
  runId,
287
+ createdAt,
274
288
  paths,
275
289
  runStatus,
276
290
  now,
@@ -22,4 +22,14 @@ type ActorVerdict = "passed" | "blocked" | "failed";
22
22
  * divergent verdict logic would let the two routes disagree about what "passed" means.
23
23
  */
24
24
  export declare function extractLocalActorVerdict(transcript: string, verdictNonce: string): ActorVerdict | null;
25
+ /**
26
+ * The same search over text that arrives in pieces and is not kept: the terminal route reads
27
+ * output past its transcript cap through this. Each piece is normalized and loses its whitespace
28
+ * as the whole transcript does above, and the end of the previous piece is carried so a marker
29
+ * split between pieces is found. The first marker wins, as above.
30
+ */
31
+ export declare function createLocalActorVerdictScanner(verdictNonce: string): {
32
+ push(text: string): void;
33
+ verdict(): ActorVerdict | null;
34
+ };
25
35
  export {};
@@ -27,6 +27,9 @@ export function normalizeLocalActorTranscript(transcript) {
27
27
  .replace(/\r/g, "\n")
28
28
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
29
29
  }
30
+ // The nonce is mandatory: a bare HUMANISH_ACTOR_VERDICT=<status> marker echoed by an actor (or
31
+ // replayed from untrusted text) must never satisfy verdict extraction.
32
+ const verdictPattern = (verdictNonce) => new RegExp(`HUMANISH_ACTOR_VERDICT=(passed|blocked|failed)HUMANISH_ACTOR_NONCE=${escapeRegExp(verdictNonce)}`, "i");
30
33
  /**
31
34
  * Extract the per-run verdict from a normalized transcript: the agent must print exactly
32
35
  * `HUMANISH_ACTOR_VERDICT=<status> HUMANISH_ACTOR_NONCE=<nonce>`, and the nonce is mandatory so a
@@ -35,13 +38,28 @@ export function normalizeLocalActorTranscript(transcript) {
35
38
  * divergent verdict logic would let the two routes disagree about what "passed" means.
36
39
  */
37
40
  export function extractLocalActorVerdict(transcript, verdictNonce) {
38
- const compactTranscript = transcript.replace(/\s+/g, "");
39
- // The per-run nonce is mandatory: a bare HUMANISH_ACTOR_VERDICT=<status>
40
- // marker echoed by an actor (or replayed from untrusted text) must never
41
- // satisfy verdict extraction.
42
- const match = new RegExp(`HUMANISH_ACTOR_VERDICT=(passed|blocked|failed)HUMANISH_ACTOR_NONCE=${escapeRegExp(verdictNonce)}`, "i").exec(compactTranscript);
43
- if (!match) {
44
- return null;
45
- }
46
- return match[1]?.toLowerCase();
41
+ const match = verdictPattern(verdictNonce).exec(transcript.replace(/\s+/g, ""));
42
+ return match?.[1]?.toLowerCase() ?? null;
43
+ }
44
+ /**
45
+ * The same search over text that arrives in pieces and is not kept: the terminal route reads
46
+ * output past its transcript cap through this. Each piece is normalized and loses its whitespace
47
+ * as the whole transcript does above, and the end of the previous piece is carried so a marker
48
+ * split between pieces is found. The first marker wins, as above.
49
+ */
50
+ export function createLocalActorVerdictScanner(verdictNonce) {
51
+ const pattern = verdictPattern(verdictNonce);
52
+ const carried = `HUMANISH_ACTOR_VERDICT=blockedHUMANISH_ACTOR_NONCE=${verdictNonce}`.length - 1;
53
+ let tail = "";
54
+ let found = null;
55
+ return {
56
+ push(text) {
57
+ if (found)
58
+ return;
59
+ const window = tail + normalizeLocalActorTranscript(text).replace(/\s+/g, "");
60
+ found = pattern.exec(window)?.[1]?.toLowerCase() ?? null;
61
+ tail = window.slice(-carried);
62
+ },
63
+ verdict: () => found,
64
+ };
47
65
  }
@@ -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 { actorOf, participantList, declaredParticipantCount, surfaceCount, modeOf, scenarioRefOf, declaresSharedWorld, } from "./study-fields.js";
13
+ import { participantList, declaredParticipantCount } 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) ?? config.surfaces?.length;
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
- (declaresSharedWorld(config) ? sharedWorldValidationReason(config) : null) ??
37
+ (config.route === "shared-world" ? 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 (modeOf(config) === "live") {
51
+ if (config.mode === "live") {
43
52
  return THIS_REPO_DRY_RUN_ONLY;
44
53
  }
45
54
  }
@@ -54,7 +63,7 @@ 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 = actorOf(config)?.type ?? "";
66
+ const type = config.actor?.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
  }
@@ -62,7 +71,7 @@ function localAppValidationReason(config) {
62
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 participants to 1 and drop participants (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
76
  if (participantList(config) !== undefined) {
68
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.";
@@ -78,17 +87,17 @@ 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 = actorOf(config)?.type ?? "";
90
+ const type = config.actor?.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 (!scenarioRefOf(config)) {
97
+ if (!config.scenario) {
89
98
  return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
90
99
  }
91
- if ((surfaceCount(config) ?? 1) > 2) {
100
+ if ((participantOrSurfaceCount(config) ?? 1) > 2) {
92
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) {
@@ -109,9 +118,8 @@ function appUrlValidationReason(config) {
109
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));
@@ -124,8 +132,7 @@ function appUrlValidationReason(config) {
124
132
  }
125
133
  // Scripted-browser actors on any other subject: only a provisioned clone on e2b-desktop.
126
134
  function scriptedBrowserValidationReason(config) {
127
- if (config.subject.source !== "app-url" &&
128
- actorResolvesToScriptedBrowser(actorOf(config)?.type)) {
135
+ if (config.subject.source !== "app-url" && actorResolvesToScriptedBrowser(config.actor?.type)) {
129
136
  if (config.subject.source !== "clone") {
130
137
  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
138
  }
@@ -142,16 +149,18 @@ function scriptedBrowserValidationReason(config) {
142
149
  if (!REPO_SLUG_PATTERN.test(repo)) {
143
150
  return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
144
151
  }
145
- if (config.subject.topology !== undefined) {
152
+ // migrate converts a v2 file's `subject.topology: shared-world` to `route: shared-world`, and
153
+ // refuses it here with the v2 parser's message.
154
+ if (config.route === "shared-world") {
146
155
  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
156
  }
148
157
  if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
149
158
  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
159
  }
151
- if (!scenarioRefOf(config)) {
160
+ if (!config.scenario) {
152
161
  return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
153
162
  }
154
- if ((surfaceCount(config) ?? 1) > 2) {
163
+ if ((participantOrSurfaceCount(config) ?? 1) > 2) {
155
164
  return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
156
165
  }
157
166
  if (participantList(config) !== undefined) {
@@ -183,7 +192,7 @@ function scriptedBrowserValidationReason(config) {
183
192
  function cloneComputerUseValidationReason(config) {
184
193
  if (config.subject.source === "clone" &&
185
194
  config.execution?.target === "e2b-desktop" &&
186
- actorResolvesToComputerUse(actorOf(config)?.type)) {
195
+ actorResolvesToComputerUse(config.actor?.type)) {
187
196
  if (!config.subject.serve) {
188
197
  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
198
  }
@@ -195,8 +204,8 @@ function cloneComputerUseValidationReason(config) {
195
204
  return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
196
205
  }
197
206
  // 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.
207
+ // E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the 16 cap;
208
+ // the single-repo rule above is unchanged.
200
209
  }
201
210
  return null;
202
211
  }
@@ -210,8 +219,8 @@ function localTreeValidationReason(config) {
210
219
  if (config.execution?.target !== "e2b-desktop") {
211
220
  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
221
  }
213
- if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
214
- 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 ?? ""}".`;
222
+ if (!actorResolvesToComputerUse(config.actor?.type)) {
223
+ 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 "${config.actor?.type ?? ""}".`;
215
224
  }
216
225
  }
217
226
  return null;
@@ -238,7 +247,7 @@ function desktopCliValidationReason(config) {
238
247
  if (config.execution?.target !== undefined && config.execution.target !== "e2b-desktop") {
239
248
  return "A desktop-cli subject runs on a hosted desktop. Set `execution.target: e2b-desktop` or omit it.";
240
249
  }
241
- if (!actorResolvesToComputerUse(actorOf(config)?.type ?? "")) {
250
+ if (!actorResolvesToComputerUse(config.actor?.type ?? "")) {
242
251
  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
252
  }
244
253
  const install = config.subject.product.install;
@@ -255,14 +264,14 @@ function desktopCliValidationReason(config) {
255
264
  // fan-out lands.
256
265
  function terminalValidationReason(config) {
257
266
  if (config.subject.source === "terminal-product") {
258
- const type = actorOf(config)?.type ?? "";
267
+ const type = config.actor?.type ?? "";
259
268
  if (config.execution?.target !== undefined && config.execution.target !== "e2b-terminal") {
260
269
  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
270
  }
262
271
  if (!actorResolvesToTerminal(type)) {
263
272
  return `actor.type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
264
273
  }
265
- if ((declaredParticipantCount(config) ?? 1) > 1) {
274
+ if ((participantOrSurfaceCount(config) ?? 1) > 1) {
266
275
  return "Terminal fan-out to more than one participant is not supported yet; set participants to 1.";
267
276
  }
268
277
  }
@@ -271,7 +280,7 @@ function terminalValidationReason(config) {
271
280
  // mis-config: reject, never silently mishandle (mirrors app-url's e2b-desktop pairing rule).
272
281
  return "`execution.target: e2b-terminal` requires `subject.source: terminal-product` with a registered terminal actor.";
273
282
  }
274
- else if (actorResolvesToTerminal(actorOf(config)?.type)) {
283
+ else if (actorResolvesToTerminal(config.actor?.type)) {
275
284
  // A registered terminal actor on a non-terminal-product subject: rejected, never ignored (the
276
285
  // terminal agent only studies a declared terminal-product from public surfaces).
277
286
  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 +292,7 @@ function terminalValidationReason(config) {
283
292
  // Terminal actors were already refused above with their own message.
284
293
  function cloneActorValidationReason(config) {
285
294
  if (config.subject.source === "clone") {
286
- const type = actorOf(config)?.type ?? "";
295
+ const type = config.actor?.type ?? "";
287
296
  if (!actorResolvesToComputerUse(type) && !actorResolvesToScriptedBrowser(type)) {
288
297
  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
298
  }
@@ -1,22 +1,38 @@
1
- import { type StudyConfig, 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. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
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
+ }
16
31
  /**
17
- * The checks that read more than one section of a parsed config, and the defaults they fill:
18
- * shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
19
- * parseV2 runs them once every section has parsed. parse/study.ts runs them on its v2 view of a v3
20
- * study.
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.
21
36
  */
22
- export declare function checkStudyConfig(config: StudyConfig): StudyParseResult;
37
+ export declare function libraryConfigRefusal(config: StudyConfig): ConfigRefusal | undefined;
38
+ export {};