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
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Spend: scripted steps make no model requests, and their traces record zero token usage. A local
8
8
  // app-url run spends no sandbox minutes; a live clone run spends E2B minutes to clone and serve the
9
- // subject. `scenario.mode: live` is required anyway, because a live run actuates a real browser
9
+ // subject. `mode: live` is required anyway, because a live run actuates a real browser
10
10
  // against a real app (it fills forms and clicks buttons). A dry run parses and digest-pins the
11
11
  // scenario and writes the contract bundle without touching anything.
12
12
  //
@@ -31,6 +31,7 @@ import { ScriptedSubject } from "./subject.js";
31
31
  import { runScriptedSessions, writeSurfaceTraces } from "./surface-sessions.js";
32
32
  import {} from "./types.js";
33
33
  import { studyResultIdentity } from "../../run/study-result.js";
34
+ import { actorOf } from "../../study/study-fields.js";
34
35
  /**
35
36
  * A refused scripted study's result, at the refusal's stage: a before-scope refusal has its own field
36
37
  * order and no analysis record; the others come after the output directory checks.
@@ -38,7 +39,7 @@ import { studyResultIdentity } from "../../run/study-result.js";
38
39
  export async function scriptedStudyRefusal(options, refusal) {
39
40
  const { config, dryRun } = options;
40
41
  const cwd = path.resolve(options.cwd);
41
- const actorType = config.actors[0]?.type ?? "";
42
+ const actorType = actorOf(config)?.type ?? "";
42
43
  if (refusal.beforeScope)
43
44
  return {
44
45
  ...studyResultIdentity("scripted", config.id),
@@ -94,6 +95,8 @@ async function runScriptedPlanInScope(plan, input, scope) {
94
95
  mintRunId: makeScriptedRunId,
95
96
  mode: dryRun ? "dry-run" : "live",
96
97
  study: plan.study,
98
+ // Only a clone subject is served from a sandbox; an app-url run drives a local browser.
99
+ sandboxes: setup.clone ? undefined : "none",
97
100
  renderReview: renderScriptedReviewMarkdown,
98
101
  observer: { open: input.open === true, render: setup.deps.renderObserver },
99
102
  });
@@ -144,6 +147,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
144
147
  deps: setup.deps,
145
148
  browserCommand: setup.browserCommand,
146
149
  runPaths,
150
+ onSessionStart: () => run.participantStarted(),
147
151
  });
148
152
  }
149
153
  catch (error) {
@@ -8,7 +8,7 @@ interface ResolvedScriptedScenario {
8
8
  sourceDigest: string;
9
9
  }
10
10
  /**
11
- * Resolve and consume `scenario.ref`. Path-style refs (contain a separator or end .yaml/.yml)
11
+ * Resolve and consume `scenario`. Path-style refs (contain a separator or end .yaml/.yml)
12
12
  * resolve against cwd and are clamped inside it: a ../../ escape is rejected, never recorded
13
13
  * as repo-relative provenance. Id-style refs must be public-safe tokens and resolve to
14
14
  * humanish/scenarios/<ref>.yaml (then .yml). Every failure mode is fail-closed. planScriptedStudy
@@ -1,4 +1,4 @@
1
- // Resolve a scripted-browser study's `scenario.ref` to a committed scenario file and parse its
1
+ // Resolve a scripted-browser study's `scenario` to a committed scenario file and parse its
2
2
  // `browser.steps` into the journey the actor runs. The ref is an id or a repo-relative path that must
3
3
  // stay inside the project; provenance records it repo-relative with the scenario's digest.
4
4
  import { parse as parseYaml } from "yaml";
@@ -6,11 +6,11 @@ import path from "node:path";
6
6
  import { parseBrowserPersonaJourneyFromScenario } from "../../actors/scripted-browser/journey.js";
7
7
  import { digestText } from "../../evidence/redaction.js";
8
8
  import { readContainedRegularFile, } from "../../run/contained-output.js";
9
- // Same public-safe token shape the study id uses; an id-style scenario.ref must match it before
9
+ // Same public-safe token shape the study id uses; an id-style `scenario` must match it before
10
10
  // it is interpolated into a repo path.
11
11
  const SCENARIO_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
12
12
  /**
13
- * Resolve and consume `scenario.ref`. Path-style refs (contain a separator or end .yaml/.yml)
13
+ * Resolve and consume `scenario`. Path-style refs (contain a separator or end .yaml/.yml)
14
14
  * resolve against cwd and are clamped inside it: a ../../ escape is rejected, never recorded
15
15
  * as repo-relative provenance. Id-style refs must be public-safe tokens and resolve to
16
16
  * humanish/scenarios/<ref>.yaml (then .yml). Every failure mode is fail-closed. planScriptedStudy
@@ -8,6 +8,8 @@ export declare function runScriptedSessions(session: Required<Pick<ScriptedBrows
8
8
  deps: StudyDeps;
9
9
  browserCommand: string | undefined;
10
10
  runPaths: PreparedRunArtifactPaths;
11
+ /** Called as each surface's session starts, before it can throw or return. */
12
+ onSessionStart: () => void;
11
13
  }): Promise<ScriptedBrowserSessionResult[]>;
12
14
  /** Writes each surface's actor-<surface>.json, as the computer-use route writes actor.json. */
13
15
  export declare function writeSurfaceTraces(runPaths: PreparedRunArtifactPaths, results: readonly ScriptedBrowserSessionResult[]): Promise<void>;
@@ -7,9 +7,10 @@ import { validateScriptedSessionResult } from "./session-result.js";
7
7
  /** One session per surface, in parallel. */
8
8
  export function runScriptedSessions(session, run) {
9
9
  const { appUrl, evidenceAppUrl, urlPolicy, journey, persona, timeoutMs, artifactRoot } = session;
10
- const { surfaces, deps, browserCommand, runPaths } = run;
10
+ const { surfaces, deps, browserCommand, runPaths, onSessionStart } = run;
11
11
  const runSession = deps.runScriptedSession;
12
12
  return Promise.all(surfaces.map((surface) => {
13
+ onSessionStart();
13
14
  const sessionOptions = {
14
15
  appUrl,
15
16
  evidenceAppUrl,
@@ -47,7 +47,7 @@ export interface ScriptedBrowserStudyResult extends AutomaticAnalysisResult, Stu
47
47
  killed: boolean;
48
48
  };
49
49
  hostDigest?: string;
50
- /** The consumed scenario.ref: digest-pinned provenance of the executable steps. */
50
+ /** The consumed `scenario`: digest-pinned provenance of the executable steps. */
51
51
  scenario?: {
52
52
  id: string;
53
53
  source: string;
@@ -58,7 +58,7 @@ export interface ScriptedBrowserStudyResult extends AutomaticAnalysisResult, Stu
58
58
  observer?: ObserverResult;
59
59
  warnings: string[];
60
60
  error?: {
61
- code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE" | "HUMANISH_RUN_ID_IN_USE";
61
+ code: "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_SCRIPTED_FAILED" | "HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED" | "HUMANISH_SCRIPTED_SCENARIO_INVALID" | "HUMANISH_SCRIPTED_SUBJECT_UNSAFE" | "HUMANISH_SCRIPTED_BROWSER_MISSING" | "HUMANISH_SCRIPTED_KEYS_MISSING" | "HUMANISH_SCRIPTED_SUBJECT_ENV_MISSING" | "HUMANISH_SCRIPTED_GETHOST_UNAVAILABLE" | "HUMANISH_RUN_ID_IN_USE";
62
62
  message: string;
63
63
  };
64
64
  }
@@ -30,7 +30,7 @@ const HANDOFF_DEADLINE_BUDGET_FRACTION = 0.4;
30
30
  // convergence proof): at most this many single-frame reads before the participant is assumed to be
31
31
  // somewhere without a code. Each reader stops the instant it has what it needs, so in practice only
32
32
  // a handful fire (a participant reaches its /lobby within a few turns). NOTE: these reads are
33
- // out-of-band OpenAI calls (external-public route only) and are left out of execution.caps.maxUsd,
33
+ // out-of-band OpenAI calls (external-public route only) and are left out of caps.maxUsd,
34
34
  // so this hard cap is what bounds their spend (each read is one cheap single-frame OCR call). If
35
35
  // this route ever runs under a strict budget, fold the estimate in.
36
36
  const MAX_LOBBY_CODE_VISION_READS = 30;
@@ -167,7 +167,11 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
167
167
  studyCwd: ctx.cwd,
168
168
  redactScreenshots: ctx.redactScreenshots,
169
169
  scrubKnownValues,
170
- runSession: ctx.runSession,
170
+ // A session that throws returns no trace; the run still records that it started.
171
+ runSession: (options) => {
172
+ ctx.run.participantStarted();
173
+ return ctx.runSession(options);
174
+ },
171
175
  // A local-agent brain runs each participant on the operator's signed-in agent, as on computer
172
176
  // use.
173
177
  brain: ctx.plan.brain,
@@ -196,7 +200,7 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
196
200
  * scrubbed of the run's known secret values.
197
201
  */
198
202
  export async function buildParticipantSpecs(participants, cwd, scrubKnownValues) {
199
- // Only the personas the participants use: an actors[0].persona that every participant overrides
203
+ // Only the personas the participants use: an actor.persona that every participant overrides
200
204
  // is never applied, so it is not read.
201
205
  const personaResolution = await resolveCommittedPersonasForCwd(cwd, participants.map((participant) => participant.personaId));
202
206
  const actorSpecs = participants.map((participant) => buildActorSpec(participant, personaResolution.personas));
@@ -6,12 +6,13 @@ import { DEFAULT_OPENAI_CU_MODEL } from "../../actors/computer-use/openai-provid
6
6
  import { studyUrlCredentialReason } from "../../study/url-credentials.js";
7
7
  import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
8
8
  import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
9
- import { brainOf, capsOf, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
9
+ import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
10
10
  import { sharedWorldParticipants } from "../../study/plan-participants.js";
11
11
  import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
12
12
  import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
13
- import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
13
+ import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
14
14
  import { MODEL_RATES, unpricedCapMessage } from "../../run/pricing.js";
15
+ import { actorOf, capsOf } from "../../study/study-fields.js";
15
16
  /** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
16
17
  export function sharedWorldDescriptorOf(actorType) {
17
18
  const descriptor = actorRegistry[actorType];
@@ -38,7 +39,7 @@ export function planSharedWorldStudy(config, input) {
38
39
  const tasksReason = taskProtocolValidationReason(config, false);
39
40
  if (tasksReason)
40
41
  return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
41
- const actorType = config.actors[0]?.type ?? "";
42
+ const actorType = actorOf(config)?.type ?? "";
42
43
  const descriptor = actorRegistry[actorType];
43
44
  if (!descriptor || !isCuaActorDescriptor(descriptor))
44
45
  return refuse("HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
@@ -53,17 +54,16 @@ export function planSharedWorldStudy(config, input) {
53
54
  // A library caller's config skips the parser, which refuses these first.
54
55
  studyUrlCredentialReason(config) ??
55
56
  outputTokenLimitValidationReason(config) ??
56
- scenarioCapsValidationReason(config) ??
57
57
  (externalPublic
58
58
  ? externalPublicSharedWorldValidationReason(config)
59
59
  : concurrentSharedWorldValidationReason(config));
60
60
  if (invalidReason)
61
61
  return refuse(invalid, invalidReason, actor);
62
- if (config.actors[0]?.maxOutputTokens !== undefined && input.hasRunSession === true)
62
+ if (actorOf(config)?.maxOutputTokens !== undefined && input.hasRunSession === true)
63
63
  return refuse(invalid, "maxOutputTokens cannot be enforced by a custom runSession.", actor);
64
- const caps = config.execution?.caps;
64
+ const caps = capsOf(config);
65
65
  if (!input.dryRun && (caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
66
- const model = (config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
66
+ const model = (actorOf(config)?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
67
67
  if (!MODEL_RATES[model])
68
68
  return refuse("HUMANISH_SHARED_WORLD_UNPRICED_CAP", unpricedCapMessage(model), actor);
69
69
  }
@@ -106,7 +106,7 @@ export function planSharedWorldStudy(config, input) {
106
106
  ? {}
107
107
  : { sessionTimeoutMs: config.execution.timeoutMs }),
108
108
  brain,
109
- caps: capsOf(config),
109
+ caps: planCaps(config),
110
110
  requirements: base.dryRun
111
111
  ? []
112
112
  : [
@@ -48,7 +48,7 @@ import { prepareConcurrentRun } from "./setup.js";
48
48
  import { browserRouteScorer } from "../../study/adapter-scorer-loader.js";
49
49
  import { withLateScorer } from "../../study/route-inputs.js";
50
50
  import {} from "./types.js";
51
- import { rosterOf } from "../../study/parse/actors.js";
51
+ import { actorOf, participantList } from "../../study/study-fields.js";
52
52
  /** The shared-world code for each local-agent refusal, kind for kind with computer-use. */
53
53
  const LOCAL_AGENT_REFUSAL_CODES = {
54
54
  "agent-missing": "HUMANISH_SHARED_WORLD_AGENT_MISSING",
@@ -59,11 +59,11 @@ const LOCAL_AGENT_REFUSAL_CODES = {
59
59
  /** A refused shared-world study's result: the route's envelope, and a refusal's analysis record. */
60
60
  export function sharedWorldStudyRefusal(options, refusal) {
61
61
  const { config, dryRun } = options;
62
- const declared = rosterOf(config.actors[0]) ?? [];
62
+ const declared = participantList(config) ?? [];
63
63
  const fail = concurrentStudyFailure({
64
64
  cwd: path.resolve(options.cwd),
65
65
  studyId: config.id,
66
- actor: config.actors[0]?.type ?? "",
66
+ actor: actorOf(config)?.type ?? "",
67
67
  participantCount: declared.length,
68
68
  // The parser fills concurrency for multi-participant studies, so this fallback serves only
69
69
  // library callers: every declared participant runs at once unless the author declared a cap.
@@ -54,7 +54,7 @@ export interface SharedWorldRunInput {
54
54
  * core-stamped onto the bundle as evidence. Absent for library callers. */
55
55
  scorerProvenance?: RunScorerProvenance;
56
56
  }
57
- export type ConcurrentSharedWorldStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
57
+ export type ConcurrentSharedWorldStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_SHARED_WORLD_FAILED" | "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED" | "HUMANISH_SHARED_WORLD_INVALID" | "HUMANISH_SHARED_WORLD_KEYS_MISSING"
58
58
  /** A local-agent brain's CLI is not on `PATH`. */
59
59
  | "HUMANISH_SHARED_WORLD_AGENT_MISSING"
60
60
  /** A local-agent brain's CLI is signed out, or could not report its sign-in status. */
@@ -1,5 +1,5 @@
1
1
  import type { ActorPersonaRef, ActorTrace } from "../../actors/contract.js";
2
- import type { StudyScenarioCaps, StudyRuntimeAuth } from "../../study/types.js";
2
+ import type { StudyCaps, StudyRuntimeAuth } from "../../study/types.js";
3
3
  import { type RunBundle, type RunCostSummary, type BundleRun } from "../../run/bundle.js";
4
4
  import { type Verdict } from "../../run/judge.js";
5
5
  import type { TerminalLedgers } from "./types.js";
@@ -21,7 +21,7 @@ export declare function buildTerminalProductBundle(args: {
21
21
  persona: ActorPersonaRef;
22
22
  productName: string;
23
23
  publicSurfaces: string[];
24
- caps?: StudyScenarioCaps;
24
+ caps?: StudyCaps;
25
25
  runtimeAuth?: string;
26
26
  stdin: "disabled" | "planned" | "sent";
27
27
  policies: {
@@ -51,7 +51,7 @@ export declare function buildLiveTerminalProductBundle(args: {
51
51
  persona: ActorPersonaRef;
52
52
  productName: string;
53
53
  publicSurfaces: string[];
54
- caps?: StudyScenarioCaps;
54
+ caps?: StudyCaps;
55
55
  runtimeAuthKeyName: string;
56
56
  runtimeAuth?: StudyRuntimeAuth;
57
57
  policies: {
@@ -1,5 +1,5 @@
1
1
  import type { ActorTokenUsage } from "../../actors/contract.js";
2
- import type { StudyScenarioCaps } from "../../study/types.js";
2
+ import type { StudyCaps } from "../../study/types.js";
3
3
  import { type CostCategory } from "../../run/terminal-contract.js";
4
4
  import type { CostLine, NoSpendProof, TerminalCostLedger } from "./types.js";
5
5
  /**
@@ -41,7 +41,7 @@ export declare function buildNoSpendProof(ledger: TerminalCostLedger, maxUsd: nu
41
41
  * grant a green pass: the no-spend proof reports them as unmeasured. maxMinutes is wall-clock and is
42
42
  * enforced separately (runWithWallClock); it is not a ledger-derived cap.
43
43
  */
44
- export declare function evaluateCapsAgainstLedger(ledger: TerminalCostLedger, caps: StudyScenarioCaps): {
44
+ export declare function evaluateCapsAgainstLedger(ledger: TerminalCostLedger, caps: StudyCaps): {
45
45
  ok: true;
46
46
  } | {
47
47
  ok: false;
@@ -1,5 +1,5 @@
1
1
  // How long a live terminal sandbox may live. Its server-side timeout has to cover everything that
2
- // runs in it before the codex command, the command's own wall clock (scenario.caps.maxMinutes) and
2
+ // runs in it before the codex command, the command's own wall clock (caps.maxMinutes) and
3
3
  // the reclamation buffer after that clock's kill. A timeout shorter than that lets E2B kill the
4
4
  // sandbox before the command's deadline and before the evidence is read.
5
5
  import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
@@ -117,7 +117,7 @@ export async function finishLiveTerminalSession(inputs) {
117
117
  const judgment = judgeTerminal({ dryRun: false, participant });
118
118
  // The run cost summary, as the computer-use route records it: the sandbox's compute time from
119
119
  // its span and observed size, and the participant's tokens (unpriced for Codex). It is not part
120
- // of the cap ledger above, whose lines sum against scenario.caps.maxUsd.
120
+ // of the cap ledger above, whose lines sum against caps.maxUsd.
121
121
  const desktops = session.runCostDesktops();
122
122
  const runCost = buildRunCostSummary({
123
123
  participants: [{ trace }],
@@ -13,6 +13,7 @@ import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
13
13
  import { terminalSandboxTimeoutMs } from "./lifetime.js";
14
14
  import { isExactRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./runtime.js";
15
15
  import { PRODUCT_SETUP_TIMEOUT_MS, TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS, } from "./types.js";
16
+ import { actorOf, capsOf } from "../../study/study-fields.js";
16
17
  /**
17
18
  * Plan a terminal-product study. It is called for any config handed to the terminal runner, not
18
19
  * only one routeOf sends here, so a config for another route gets this route's refusal.
@@ -40,12 +41,12 @@ export function planTerminalStudy(config, input) {
40
41
  const tasksReason = taskProtocolValidationReason(config, false);
41
42
  if (tasksReason)
42
43
  return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
43
- const actorType = config.actors[0]?.type ?? "";
44
+ const actorType = actorOf(config)?.type ?? "";
44
45
  const descriptor = actorRegistry[actorType];
45
46
  if (!descriptor || !isTerminalActorDescriptor(descriptor))
46
47
  return refuse("HUMANISH_TERMINAL_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered terminal actor.`);
47
48
  const runtimeVersion = config.execution?.runtime?.version;
48
- const actor = config.actors[0];
49
+ const actor = actorOf(config);
49
50
  if ((config.execution?.runtime !== undefined && !isExactRuntimeVersion(runtimeVersion)) ||
50
51
  (actor?.model !== undefined &&
51
52
  (typeof actor.model !== "string" || actor.model.trim().length === 0)) ||
@@ -83,7 +84,7 @@ export function planTerminalStudy(config, input) {
83
84
  ...(egressAllow === undefined ? {} : { egressAllow }),
84
85
  ...(stdin === undefined ? {} : { stdin }),
85
86
  };
86
- const caps = config.scenario?.caps;
87
+ const caps = capsOf(config);
87
88
  if (input.dryRun)
88
89
  return { ok: true, plan: { ...shared, dryRun: true, ...(caps ? { caps } : {}) } };
89
90
  // The live route injects the runtime key into one command only, and only a registered actor
@@ -17,7 +17,7 @@
17
17
  // spending. Opt-in openai-env injects the raw key command-scoped, never through
18
18
  // Sandbox.create({envs}).
19
19
  // Enforced in credentials.ts and runtime-auth.ts.
20
- // 2. Fail-closed cap. The live key is never exercised without scenario.caps in force: maxUsd
20
+ // 2. Fail-closed cap. The live key is never exercised without `caps` in force: maxUsd
21
21
  // (default/require 0 = no-spend) + maxMinutes (wall-clock kill of the codex command).
22
22
  // Enforced in plan.ts (caps required), live-sandbox.ts and lifetime.ts (the wall clock).
23
23
  // 3. Public surfaces only. The mission references only subject.product.publicSurfaces + the
@@ -54,6 +54,7 @@ import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
54
54
  import { withLateScorer } from "../../study/route-inputs.js";
55
55
  import {} from "./types.js";
56
56
  import { studyResultIdentity } from "../../run/study-result.js";
57
+ import { actorOf } from "../../study/study-fields.js";
57
58
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
58
59
  export function terminalStudyRefusal(options, refusal) {
59
60
  const { config, dryRun } = options;
@@ -61,7 +62,7 @@ export function terminalStudyRefusal(options, refusal) {
61
62
  ...studyResultIdentity("terminal", config.id),
62
63
  ok: false,
63
64
  cwd: path.resolve(options.cwd),
64
- actor: refusal.actor ?? config.actors[0]?.type ?? "",
65
+ actor: refusal.actor ?? actorOf(config)?.type ?? "",
65
66
  product: config.subject.product?.name ?? "",
66
67
  dryRun,
67
68
  runId: options.runId ?? "not-created",
@@ -111,6 +111,7 @@ export async function runLiveTerminalSession(args) {
111
111
  if ((await session.bootstrap()) &&
112
112
  (await session.verifyRuntimeVersion()) &&
113
113
  (await session.prepareProduct())) {
114
+ run.participantStarted();
114
115
  await session.execCodex();
115
116
  }
116
117
  }
@@ -171,7 +171,7 @@ export interface TerminalProductStudyResult extends AutomaticAnalysisResult, Stu
171
171
  observer?: ObserverResult;
172
172
  warnings: string[];
173
173
  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";
174
+ 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
175
  message: string;
176
176
  };
177
177
  }
@@ -57,3 +57,13 @@ export declare function bundleDisplayFacts(bundle: DisplayedBundle, status?: unk
57
57
  * execution failed never reads as a pass beside its participants' verdict.
58
58
  */
59
59
  export declare function reviewOutcome(bundle: DisplayedBundle): string;
60
+ /**
61
+ * The summary and gaps a review shows beside its run's display. A run stopped while a participant
62
+ * was still running keeps the review its last live flush wrote, which describes that session as
63
+ * running. Once runDisplay reads the run as interrupted, the review says how many participants the
64
+ * stop cut off instead.
65
+ */
66
+ export declare function displayedReview<T extends {
67
+ summary: string;
68
+ gaps: string[];
69
+ }>(review: T, bundle: DisplayedBundle, display: RunDisplay): T;
@@ -3,6 +3,7 @@
3
3
  // same facts, so a run whose own ok is false shows as a pass on none of them, and two surfaces
4
4
  // cannot disagree about one run.
5
5
  import { classifyRunStatus, isRunStatusRecord } from "./status.js";
6
+ import { plural } from "./text.js";
6
7
  const LABELS = {
7
8
  running: "running",
8
9
  interrupted: "interrupted",
@@ -104,3 +105,24 @@ export function bundleDisplayFacts(bundle, status, nowMs = Date.now()) {
104
105
  export function reviewOutcome(bundle) {
105
106
  return runDisplayLine(runDisplay(bundleDisplayFacts(bundle)));
106
107
  }
108
+ /**
109
+ * The summary and gaps a review shows beside its run's display. A run stopped while a participant
110
+ * was still running keeps the review its last live flush wrote, which describes that session as
111
+ * running. Once runDisplay reads the run as interrupted, the review says how many participants the
112
+ * stop cut off instead.
113
+ */
114
+ export function displayedReview(review, bundle, display) {
115
+ const records = bundle.simulations ?? [];
116
+ const running = records.filter((record) => record?.status === "running").length;
117
+ if (display.state !== "interrupted" || running === 0)
118
+ return review;
119
+ const signal = bundle.outcome?.state === "interrupted" ? ` by ${bundle.outcome.signal}` : "";
120
+ const one = running === 1;
121
+ return {
122
+ ...review,
123
+ summary: `Interrupted${signal} while ${running} of ${plural(records.length, "participant")} ${one ? "was" : "were"} still running.`,
124
+ gaps: [
125
+ `${plural(running, "participant")} ${one ? "has" : "have"} no verdict: the run stopped before ${one ? "its session" : "their sessions"} ended.`,
126
+ ],
127
+ };
128
+ }
@@ -20,9 +20,10 @@ export interface ReclaimResult {
20
20
  createsInFlight: number;
21
21
  /**
22
22
  * Why there was nothing to look for: `dry-run` when run.json or status.json records the run as
23
- * a dry run, which creates no sandboxes. Reclaim then contacts E2B for nothing.
23
+ * a dry run, which creates no sandboxes, and `no-sandbox` when status.json records that the
24
+ * route created none for this run. Reclaim then contacts E2B for nothing.
24
25
  */
25
- reason?: "dry-run";
26
+ reason?: NoSandboxReason;
26
27
  warnings: string[];
27
28
  error?: {
28
29
  code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
@@ -47,6 +48,7 @@ export declare function reclaimRunSandboxes(cwd: string, runInput: string, hooks
47
48
  * also an alias (a run named `latest`) cannot reach another run's receipts.
48
49
  */
49
50
  export declare function reclaimPinnedRunSandboxes(cwd: string, runPaths: PreparedRunArtifactPaths, hooks?: ReclaimHooks): Promise<ReclaimResult>;
51
+ type NoSandboxReason = "dry-run" | "no-sandbox";
50
52
  /**
51
53
  * Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
52
54
  * .humanish/preflight/<probe-id>/ and removes the journal after a confirmed kill, so what is left
@@ -15,6 +15,7 @@ import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
15
15
  import { RUN_BUNDLE_FILE } from "./bundle.js";
16
16
  import { RUN_STATUS_FILE } from "./status.js";
17
17
  import { isRecord } from "./type-guards.js";
18
+ import { isRunOutcome } from "./bundle-shape.js";
18
19
  import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
19
20
  import { sandboxOwnerTags, } from "./sandbox-creates.js";
20
21
  import { earlierOutcomes, GONE, RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, receiptWriter, rootState, searchTagSets, settledWithin, targetSet, } from "./reclaim-outcomes.js";
@@ -64,8 +65,9 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
64
65
  }
65
66
  const runId = runIdOf(runPaths);
66
67
  // The signal handler passes the creates it stopped and decides from what they reported.
67
- if (hooks.creates === undefined && (await recordedDryRun(runPaths)))
68
- return { ...base, ok: true, state: "clean", runId, reason: "dry-run" };
68
+ const reason = hooks.creates === undefined ? await recordedNoSandbox(runPaths) : undefined;
69
+ if (reason !== undefined)
70
+ return { ...base, ok: true, state: "clean", runId, reason };
69
71
  const reclaimed = await reclaimRoot(runPaths, runId, hooks, warnings);
70
72
  if (reclaimed.kind === "module-unavailable")
71
73
  return {
@@ -93,29 +95,70 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
93
95
  };
94
96
  }
95
97
  /**
96
- * Whether the run records that it was a dry run: run.json and status.json, each one present, say
97
- * `dry-run`. A run with neither record is not taken for one. A journal with any line in it, or an
98
- * earlier reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a
99
- * live one whatever its records say.
98
+ * Why the run's own records say it created no sandbox. `dry-run`: run.json and status.json, each
99
+ * one present, say `dry-run`. `no-sandbox`: status.json, written at start, names this run and
100
+ * records `sandboxes: none`, which the route sets only when it has no way to create one (a
101
+ * scripted run against an app-url subject), and run.json agrees (scriptedAppUrlBundle). A status
102
+ * record alone can be stale or copied from another run, so it never decides on its own, and an
103
+ * empty journal alone never makes a run clean. A journal with any line in it, or an earlier
104
+ * reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a live one
105
+ * whatever its records say.
100
106
  */
101
- async function recordedDryRun(runPaths) {
102
- const modes = [];
103
- for (const file of [RUN_BUNDLE_FILE, RUN_STATUS_FILE]) {
104
- const record = await readRunJsonIfExists(runPaths, file);
105
- if (isRecord(record))
106
- modes.push(record.mode);
107
- }
108
- if (modes.length === 0 || modes.some((mode) => mode !== "dry-run"))
109
- return false;
107
+ async function recordedNoSandbox(runPaths) {
108
+ const bundle = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
109
+ const status = await readRunJsonIfExists(runPaths, RUN_STATUS_FILE);
110
+ const records = [bundle, status].filter(isRecord);
111
+ const reason = records.length > 0 && records.every((record) => record.mode === "dry-run")
112
+ ? "dry-run"
113
+ : isRecord(status) &&
114
+ status.runId === runIdOf(runPaths) &&
115
+ status.sandboxes === "none" &&
116
+ scriptedAppUrlBundle(bundle, runIdOf(runPaths))
117
+ ? "no-sandbox"
118
+ : undefined;
119
+ if (reason === undefined)
120
+ return undefined;
110
121
  if (!(await containedPathAbsent(runPaths, RECLAIM_RECEIPT_ARTIFACT))) {
111
122
  const earlier = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
112
123
  if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
113
- return false;
124
+ return undefined;
114
125
  }
115
126
  if (await containedPathAbsent(runPaths, SANDBOX_RECEIPTS_ARTIFACT))
116
- return true;
127
+ return reason;
117
128
  const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT);
118
- return journal !== null && journal.toString("utf8").trim() === "";
129
+ return journal !== null && journal.toString("utf8").trim() === "" ? reason : undefined;
130
+ }
131
+ /** The execution failure kinds that leave a sandbox or a provider's resources unconfirmed. */
132
+ const CLEANUP_KINDS = new Set(["sandbox-cleanup", "provider-cleanup"]);
133
+ /**
134
+ * run.json for `runId` agrees that its run could create no sandbox: the scripted route wrote it
135
+ * (each lifecycle event is a `scripted-lab.` one), its subject is not a provisioned clone (a clone
136
+ * records `subject`), it lists no provider resource and no desktop minutes, and its outcome marks
137
+ * no sandbox or provider cleanup unconfirmed. A run.json that is missing, unreadable or says
138
+ * otherwise does not agree.
139
+ */
140
+ function scriptedAppUrlBundle(bundle, runId) {
141
+ if (!isRecord(bundle) || bundle.runId !== runId || bundle.subject !== undefined)
142
+ return false;
143
+ const { lifecycle, providerResources, cost, outcome } = bundle;
144
+ const scripted = Array.isArray(lifecycle) &&
145
+ lifecycle.length > 0 &&
146
+ lifecycle.every((entry) => isRecord(entry) &&
147
+ typeof entry.event === "string" &&
148
+ entry.event.startsWith("scripted-lab."));
149
+ if (!scripted)
150
+ return false;
151
+ if (providerResources !== undefined &&
152
+ !(Array.isArray(providerResources) && providerResources.length === 0))
153
+ return false;
154
+ if (cost !== undefined && !(isRecord(cost) && cost.desktopMinutes === null))
155
+ return false;
156
+ if (!isRunOutcome(outcome))
157
+ return false;
158
+ if (outcome.state !== "finished")
159
+ return true;
160
+ const { failures, warnings = [] } = outcome.execution;
161
+ return ![...failures, ...warnings].some((failure) => CLEANUP_KINDS.has(failure.kind));
119
162
  }
120
163
  /**
121
164
  * Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
@@ -30,7 +30,7 @@ export interface RunResult {
30
30
  observer?: ObserverResult;
31
31
  warnings: string[];
32
32
  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";
33
+ 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
34
  message: string;
35
35
  };
36
36
  }
package/dist/run/run.d.ts CHANGED
@@ -11,6 +11,8 @@ interface StartRunOptions {
11
11
  mintRunId: () => string;
12
12
  mode: "dry-run" | "live";
13
13
  study?: RunStudyProvenance | undefined;
14
+ /** `none` when the route creates no sandbox for this run; status.json records it for reclaim. */
15
+ sandboxes?: "none" | undefined;
14
16
  /** review.md for the published bundle. */
15
17
  renderReview: (bundle: RunBundle) => string;
16
18
  /** Used by `FinishedRun.renderObserver`; `render` is the `StudyDeps.renderObserver` seam. */
@@ -52,6 +54,12 @@ interface Run {
52
54
  * can never overwrite the final bundle. Routes that throttle snapshots own their timers.
53
55
  */
54
56
  writeSnapshot(bundle: RunBundle): Promise<void>;
57
+ /**
58
+ * A participant's session started: its actor began to act on the subject. FinishedRun's
59
+ * participantsRan reads this, so a session that throws before it returns a trace still counts,
60
+ * and a run that failed before any session (a refused E2B key) does not.
61
+ */
62
+ participantStarted(): void;
55
63
  /**
56
64
  * The one final publication: run.json with `outcome` set from `outcome`, then the status
57
65
  * outcome copied from it, then review.json, review.md, events.ndjson,
@@ -92,12 +100,14 @@ export declare class FinishedRun {
92
100
  readonly runId: string;
93
101
  /** The paths created by startRun and validated at the start of `finish`. */
94
102
  readonly paths: PreparedRunArtifactPaths;
95
- constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean);
103
+ constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean, participantsRan: () => boolean);
96
104
  /**
97
105
  * A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
98
106
  * when asked, since the signal can arrive after the bundle was published.
99
107
  */
100
108
  get interrupted(): boolean;
109
+ /** The route reported a participant's session started (Run.participantStarted). */
110
+ get participantsRan(): boolean;
101
111
  static isIssued(value: unknown): value is FinishedRun;
102
112
  /**
103
113
  * The run's ok and execution outcome as run.json records them: what the route passed to