humanish 0.115.0 → 0.117.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 (195) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +7 -6
  3. package/TELEMETRY.md +6 -0
  4. package/dist/actors/codex/app-server-ui.js +11 -4
  5. package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
  6. package/dist/actors/codex/restricted-participant-policy.js +4 -32
  7. package/dist/actors/codex/restricted-participant.d.ts +5 -0
  8. package/dist/actors/codex/restricted-participant.js +3 -2
  9. package/dist/actors/computer-use/actor.d.ts +4 -0
  10. package/dist/actors/computer-use/actor.js +2 -0
  11. package/dist/actors/computer-use/loop/actions.d.ts +4 -4
  12. package/dist/actors/computer-use/loop/actions.js +25 -18
  13. package/dist/actors/computer-use/loop/debrief.js +61 -27
  14. package/dist/actors/computer-use/loop/session.d.ts +3 -0
  15. package/dist/actors/computer-use/loop/session.js +9 -1
  16. package/dist/actors/computer-use/loop/trace.js +1 -0
  17. package/dist/actors/computer-use/loop/types.d.ts +16 -14
  18. package/dist/actors/computer-use/loop.d.ts +1 -1
  19. package/dist/actors/computer-use/loop.js +1 -1
  20. package/dist/actors/computer-use/wait.d.ts +74 -0
  21. package/dist/actors/computer-use/wait.js +100 -0
  22. package/dist/actors/contract.d.ts +14 -0
  23. package/dist/actors/scripted-browser/steps.js +6 -5
  24. package/dist/analysis/analysis-limits.d.ts +35 -0
  25. package/dist/analysis/analysis-limits.js +41 -0
  26. package/dist/analysis/validation.js +14 -21
  27. package/dist/browser-control/protocol.d.ts +86 -6
  28. package/dist/browser-control/protocol.js +13 -6
  29. package/dist/cli/commands/analyze.js +2 -1
  30. package/dist/cli/commands/notes.js +2 -1
  31. package/dist/cli/commands/run-signals.js +5 -1
  32. package/dist/cli/commands/runs.js +2 -1
  33. package/dist/cli/commands/setup.js +3 -3
  34. package/dist/cli/commands/study-format.d.ts +5 -2
  35. package/dist/cli/commands/study-format.js +29 -9
  36. package/dist/cli/commands/study-route-computer-use.js +2 -2
  37. package/dist/cli/commands/study-route-scripted.js +1 -1
  38. package/dist/cli/commands/tui.js +59 -17
  39. package/dist/cli/doctor.js +9 -2
  40. package/dist/cli/findings-text.d.ts +5 -0
  41. package/dist/cli/findings-text.js +169 -0
  42. package/dist/cli/findings.d.ts +0 -4
  43. package/dist/cli/findings.js +5 -167
  44. package/dist/cli/io.js +6 -1
  45. package/dist/cli/update-check.js +4 -1
  46. package/dist/comms/connections.js +4 -3
  47. package/dist/comms/setup.js +18 -12
  48. package/dist/evidence/redaction.js +9 -1
  49. package/dist/evidence/secret-scrub.d.ts +15 -5
  50. package/dist/evidence/secret-scrub.js +67 -9
  51. package/dist/feedback/draft.js +1 -1
  52. package/dist/feedback/export-bundle.js +18 -14
  53. package/dist/feedback/feedback.js +25 -7
  54. package/dist/guest/desktop-executor.js +3 -1
  55. package/dist/guest/desktop-keys.js +59 -5
  56. package/dist/keys/key-resolution.d.ts +9 -0
  57. package/dist/keys/key-resolution.js +60 -48
  58. package/dist/keys/key-status.d.ts +2 -0
  59. package/dist/keys/key-status.js +9 -16
  60. package/dist/observer/pinned-files.js +1 -1
  61. package/dist/observer/render.js +3 -2
  62. package/dist/observer-app.html +5 -5
  63. package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
  64. package/dist/routes/computer-use/bundle-parts.js +2 -2
  65. package/dist/routes/computer-use/bundle.d.ts +4 -5
  66. package/dist/routes/computer-use/bundle.js +1 -3
  67. package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
  68. package/dist/routes/computer-use/fanout-records.js +10 -11
  69. package/dist/routes/computer-use/live-phase.d.ts +1 -1
  70. package/dist/routes/computer-use/live-phase.js +0 -1
  71. package/dist/routes/computer-use/local-vm.js +5 -2
  72. package/dist/routes/computer-use/participant-execution.d.ts +3 -2
  73. package/dist/routes/computer-use/participant-model.js +9 -0
  74. package/dist/routes/computer-use/participant-runs.d.ts +6 -2
  75. package/dist/routes/computer-use/participant-runs.js +9 -5
  76. package/dist/routes/computer-use/plan.d.ts +3 -0
  77. package/dist/routes/computer-use/plan.js +18 -13
  78. package/dist/routes/computer-use/rerun-selection.js +2 -2
  79. package/dist/routes/computer-use/result.js +3 -6
  80. package/dist/routes/computer-use/setup.d.ts +4 -6
  81. package/dist/routes/computer-use/setup.js +2 -6
  82. package/dist/routes/computer-use/single-bundle.d.ts +4 -5
  83. package/dist/routes/computer-use/single-bundle.js +22 -25
  84. package/dist/routes/computer-use/subject-projection.d.ts +4 -8
  85. package/dist/routes/computer-use/subject-projection.js +9 -31
  86. package/dist/routes/computer-use/types.d.ts +5 -42
  87. package/dist/routes/scripted/scenario.js +28 -12
  88. package/dist/routes/scripted/session-result.js +6 -3
  89. package/dist/routes/shared-world/bundle-records.js +2 -8
  90. package/dist/routes/shared-world/participant-specs.d.ts +0 -2
  91. package/dist/routes/shared-world/participant-specs.js +1 -20
  92. package/dist/routes/shared-world/plan.d.ts +3 -0
  93. package/dist/routes/shared-world/plan.js +25 -4
  94. package/dist/routes/shared-world/setup.js +2 -2
  95. package/dist/routes/terminal/ledger.d.ts +9 -9
  96. package/dist/routes/terminal/ledger.js +63 -40
  97. package/dist/routes/terminal/live-finish.js +7 -10
  98. package/dist/routes/terminal/live-sandbox.d.ts +1 -1
  99. package/dist/routes/terminal/live-sandbox.js +14 -11
  100. package/dist/routes/terminal/plan.d.ts +3 -0
  101. package/dist/routes/terminal/plan.js +8 -5
  102. package/dist/routes/terminal/sandbox.d.ts +4 -3
  103. package/dist/routes/terminal/sandbox.js +5 -5
  104. package/dist/routes/terminal/token-usage.d.ts +1 -2
  105. package/dist/routes/terminal/token-usage.js +4 -6
  106. package/dist/routes/terminal/types.d.ts +9 -1
  107. package/dist/run/bundle-shape.js +6 -1
  108. package/dist/run/bundle.d.ts +5 -3
  109. package/dist/run/contained-output.d.ts +53 -5
  110. package/dist/run/contained-output.js +141 -42
  111. package/dist/run/detail.d.ts +2 -1
  112. package/dist/run/detail.js +12 -7
  113. package/dist/run/escape-sequences.d.ts +11 -0
  114. package/dist/run/escape-sequences.js +93 -0
  115. package/dist/run/evidence-files.d.ts +8 -8
  116. package/dist/run/evidence-files.js +19 -23
  117. package/dist/run/locate.d.ts +32 -3
  118. package/dist/run/locate.js +45 -40
  119. package/dist/run/notes.d.ts +0 -8
  120. package/dist/run/notes.js +5 -51
  121. package/dist/run/outcomes.d.ts +24 -0
  122. package/dist/run/outcomes.js +19 -0
  123. package/dist/run/paths.d.ts +2 -0
  124. package/dist/run/paths.js +5 -1
  125. package/dist/run/preflight-receipts.js +13 -8
  126. package/dist/run/project.js +7 -4
  127. package/dist/run/reclaim-outcomes.d.ts +8 -6
  128. package/dist/run/reclaim-outcomes.js +10 -6
  129. package/dist/run/reclaim.d.ts +5 -0
  130. package/dist/run/reclaim.js +43 -16
  131. package/dist/run/run-clock.d.ts +50 -0
  132. package/dist/run/run-clock.js +66 -0
  133. package/dist/run/run.js +5 -2
  134. package/dist/run/sandbox-ids.d.ts +7 -2
  135. package/dist/run/sandbox-ids.js +27 -11
  136. package/dist/run/secrets.d.ts +2 -1
  137. package/dist/run/secrets.js +18 -22
  138. package/dist/run/stored-runs.js +14 -7
  139. package/dist/run/transient-comms-secrets.js +6 -17
  140. package/dist/run-study.js +22 -13
  141. package/dist/study/adapter-scorer-loader.js +6 -4
  142. package/dist/study/config.js +4 -1
  143. package/dist/study/discover.js +9 -6
  144. package/dist/study/init-paths.js +7 -5
  145. package/dist/study/keys.d.ts +2 -0
  146. package/dist/study/keys.js +2 -0
  147. package/dist/study/migrate/v2.js +2 -1
  148. package/dist/study/parse/actors.js +8 -0
  149. package/dist/study/persona-resolve.d.ts +2 -2
  150. package/dist/study/persona-resolve.js +9 -5
  151. package/dist/study/plan-participants.d.ts +4 -0
  152. package/dist/study/plan-participants.js +4 -0
  153. package/dist/study/plan-types.d.ts +2 -2
  154. package/dist/study/plan.d.ts +5 -3
  155. package/dist/study/plan.js +18 -9
  156. package/dist/study/preflight-probes.js +11 -3
  157. package/dist/study/preflight.js +9 -5
  158. package/dist/study/summary.d.ts +8 -2
  159. package/dist/study/summary.js +20 -22
  160. package/dist/study/types.d.ts +14 -0
  161. package/dist/study/validation.d.ts +5 -0
  162. package/dist/study/validation.js +8 -0
  163. package/dist/study/warnings.d.ts +19 -12
  164. package/dist/study/warnings.js +50 -17
  165. package/dist/subject/serve.d.ts +3 -0
  166. package/dist/subject/serve.js +4 -1
  167. package/dist/substrates/detached.js +14 -7
  168. package/dist/substrates/e2b/connection.d.ts +12 -0
  169. package/dist/substrates/e2b/connection.js +35 -0
  170. package/dist/substrates/e2b/desktop-executor.d.ts +1 -5
  171. package/dist/substrates/e2b/desktop-executor.js +45 -12
  172. package/dist/substrates/e2b/lifetime.d.ts +24 -4
  173. package/dist/substrates/e2b/lifetime.js +41 -4
  174. package/dist/substrates/e2b/sandbox.d.ts +4 -3
  175. package/dist/substrates/e2b/sandbox.js +13 -18
  176. package/dist/substrates/e2b/sdk.d.ts +20 -9
  177. package/dist/substrates/local/capacity.d.ts +18 -2
  178. package/dist/substrates/local/capacity.js +21 -2
  179. package/dist/substrates/local/runtime-host.js +21 -14
  180. package/dist/substrates/local/runtime.js +3 -11
  181. package/dist/tui/contract.d.ts +30 -5
  182. package/dist/tui/contract.js +4 -1
  183. package/dist/tui-app.js +150 -147
  184. package/dist/verify/artifacts.d.ts +9 -2
  185. package/dist/verify/artifacts.js +37 -13
  186. package/dist/verify/liveness.js +19 -5
  187. package/dist/verify/verify.js +54 -27
  188. package/docs/architecture/browser-control.md +10 -4
  189. package/docs/architecture/guest-desktop.md +7 -2
  190. package/docs/architecture/state-driven-executor.md +4 -2
  191. package/docs/contracts/run-bundle.md +18 -10
  192. package/docs/contracts/schemas.md +46 -29
  193. package/docs/contracts/study-analysis.md +10 -1
  194. package/docs/product/automatic-analysis.md +7 -1
  195. package/package.json +2 -2
@@ -12,6 +12,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
12
12
  const dwell = entry?.dwell ?? actor?.dwell;
13
13
  const reasoningEffort = entry?.reasoningEffort ?? actor?.reasoningEffort;
14
14
  const maxOutputTokens = actor?.maxOutputTokens;
15
+ const maxWaitMs = actor?.maxWaitMs;
16
+ const idleWaitMs = actor?.idleWaitMs;
15
17
  return {
16
18
  id: participantIdAt(index, entry?.id, kind),
17
19
  index,
@@ -31,6 +33,8 @@ function desktopParticipant(config, entry, index, kind, focus) {
31
33
  ...(dwell === undefined ? {} : { dwell }),
32
34
  ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
33
35
  ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
36
+ ...(maxWaitMs === undefined ? {} : { maxWaitMs }),
37
+ ...(idleWaitMs === undefined ? {} : { idleWaitMs }),
34
38
  },
35
39
  };
36
40
  }
@@ -196,8 +196,8 @@ export interface SharedWorldPlan extends PlanBase {
196
196
  readonly plane: SharedWorldPlane;
197
197
  /** At least 2. */
198
198
  readonly concurrency: number;
199
- /** The declared participant session timeout; the route supplies its default. */
200
- readonly sessionTimeoutMs?: number;
199
+ /** Each participant's session budget: execution.timeoutMs, else the plane's derived default. */
200
+ readonly sessionTimeoutMs: number;
201
201
  readonly brain: Extract<Brain, {
202
202
  kind: "openai" | "local-agent";
203
203
  }>;
@@ -6,8 +6,10 @@ export { routeOf, type StudyRoute } from "./routing.js";
6
6
  /** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
7
7
  export declare function resolveStudyDryRun(config: StudyConfig, override: boolean | undefined, fallback: boolean | undefined): boolean | undefined;
8
8
  /**
9
- * The plan a study runs under, built without reading files, env or the network. Each route's planner
10
- * makes every refusal that route makes, in the route's order and with its codes and messages; the
11
- * route's exported runner calls the same planner.
9
+ * The plan a study runs under, built without reading files or the network. From the environment
10
+ * (options.env, else process.env, as the routes read it) it reads one value, the E2B sandbox
11
+ * ceiling, which no study file can know. Each route's planner makes every refusal that route
12
+ * makes, in the route's order and with its codes and messages; the route's exported runner calls
13
+ * the same planner.
12
14
  */
13
15
  export declare function planStudy(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): PlanResult;
@@ -6,13 +6,14 @@ import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/pl
6
6
  import { injectedBrowser, planScriptedStudy } from "../routes/scripted/plan.js";
7
7
  import { planSharedWorldStudy } from "../routes/shared-world/plan.js";
8
8
  import { planTerminalStudy } from "../routes/terminal/plan.js";
9
+ import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
9
10
  import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
10
11
  import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
11
12
  import { planBase } from "./plan-base.js";
12
13
  import { routeOf } from "./routing.js";
13
14
  import { automaticAnalysisRouteReason, taskProtocolValidationReason } from "./validation.js";
14
15
  import { declaredParticipantCount } from "./study-fields.js";
15
- import { egressAllowIgnoredWarning, scriptedMissionWarnings } from "./warnings.js";
16
+ import { recordedStudyWarnings } from "./warnings.js";
16
17
  export { routeOf } from "./routing.js";
17
18
  /** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
18
19
  export function resolveStudyDryRun(config, override, fallback) {
@@ -63,20 +64,22 @@ function planPreview(config, options, input) {
63
64
  };
64
65
  }
65
66
  /**
66
- * The plan a study runs under, built without reading files, env or the network. Each route's planner
67
- * makes every refusal that route makes, in the route's order and with its codes and messages; the
68
- * route's exported runner calls the same planner.
67
+ * The plan a study runs under, built without reading files or the network. From the environment
68
+ * (options.env, else process.env, as the routes read it) it reads one value, the E2B sandbox
69
+ * ceiling, which no study file can know. Each route's planner makes every refusal that route
70
+ * makes, in the route's order and with its codes and messages; the route's exported runner calls
71
+ * the same planner.
69
72
  */
70
73
  export function planStudy(config, options, deps = {}) {
71
74
  const study = localBrowserDefaults(config);
72
75
  const input = { dryRun: resolveStudyDryRun(study, options.dryRun, true) ?? true };
73
- const result = planRoute(routeOf(config), study, options, input, deps);
76
+ const ceiling = sandboxCeiling(options.env ?? process.env);
77
+ const result = planRoute(routeOf(config), study, options, input, deps, ceiling);
74
78
  if (!result.ok)
75
79
  return result;
76
80
  // The manifest the CLI resolved enters the plan here and nowhere else; the routes read
77
81
  // plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
78
- const warning = egressAllowIgnoredWarning(study);
79
- const warnings = [...(warning === undefined ? [] : [warning]), ...scriptedMissionWarnings(study)];
82
+ const warnings = recordedStudyWarnings(study);
80
83
  const plan = {
81
84
  ...result.plan,
82
85
  ...(options.study === undefined ? {} : { study: options.study }),
@@ -84,7 +87,7 @@ export function planStudy(config, options, deps = {}) {
84
87
  };
85
88
  return { ok: true, planned: { plan } };
86
89
  }
87
- function planRoute(route, study, options, input, deps) {
90
+ function planRoute(route, study, options, input, deps, sandboxCeiling) {
88
91
  switch (route) {
89
92
  case "preview":
90
93
  return planPreview(study, options, input);
@@ -95,14 +98,20 @@ function planRoute(route, study, options, input, deps) {
95
98
  driving: callerDrivingOf(options),
96
99
  ...(options.count === undefined ? {} : { countOverride: options.count }),
97
100
  ...(options.rerun === undefined ? {} : { rerun: options.rerun }),
101
+ sandboxCeiling,
98
102
  });
99
103
  case "shared-world":
100
104
  return planSharedWorldStudy(study, {
101
105
  ...input,
102
106
  hasRunSession: deps.runSession !== undefined,
107
+ sandboxCeiling,
103
108
  });
104
109
  case "terminal":
105
- return planTerminalStudy(study, { ...input, hasCostProbe: deps.costProbe !== undefined });
110
+ return planTerminalStudy(study, {
111
+ ...input,
112
+ hasCostProbe: deps.costProbe !== undefined,
113
+ sandboxCeiling,
114
+ });
106
115
  case "scripted":
107
116
  return planScriptedStudy(study, { ...input, injectedBrowser: injectedBrowser(deps) });
108
117
  }
@@ -4,7 +4,7 @@
4
4
  // the study and dispatches here.
5
5
  import { cloneProvisioningBudgetMs, provisionCloneSubject } from "../subject/clone.js";
6
6
  import { CUA_ACTOR_STUDY_PROVIDER_METADATA } from "../routes/computer-use/e2b-desktop/prepare.js";
7
- import { MAX_SANDBOX_MS } from "../substrates/e2b/lifetime.js";
7
+ import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
8
8
  import { abandonPreflightJournal, discardPreflightJournal, openPreflightJournal, } from "../run/preflight-receipts.js";
9
9
  import { probeUrl } from "../substrates/detached.js";
10
10
  import { loadE2BDesktopModule } from "../substrates/e2b/sdk.js";
@@ -147,8 +147,16 @@ async function withPreflightSandbox(ctx, args, callback) {
147
147
  let allocation;
148
148
  let failureMessage;
149
149
  // The lease is sized to the probe's work, never longer than a declared sandbox timeout (the run
150
- // gets no more than that either) or E2B's maximum.
151
- const timeoutMs = Math.min(args.leaseMs, ctx.config.execution?.desktop?.sandboxTimeoutMs ?? MAX_SANDBOX_MS);
150
+ // gets no more than that either) or the sandbox ceiling.
151
+ const ceiling = sandboxCeiling(ctx.env);
152
+ if (!ceiling.ok)
153
+ return {
154
+ ok: false,
155
+ result: fail(ctx, "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED", ceiling.message, [
156
+ { name: "sandbox preflight", ok: false, message: ceiling.message },
157
+ ]),
158
+ };
159
+ const timeoutMs = Math.min(args.leaseMs, ctx.config.execution?.desktop?.sandboxTimeoutMs ?? ceiling.ms);
152
160
  // The receipt goes to a journal under .humanish/preflight, so `humanish reclaim --preflight`
153
161
  // can kill the probe if this process dies before the finally block does.
154
162
  let journal;
@@ -1,4 +1,4 @@
1
- import { personaBackgroundWarnings } from "./warnings.js";
1
+ import { personaBackgroundWarnings, plannedParticipants } from "./warnings.js";
2
2
  import { resolveCommittedPersonasForCwd, studyPersonaIds } from "./persona-resolve.js";
3
3
  import path from "node:path";
4
4
  import { isLoopbackUrl } from "./parse/subject.js";
@@ -37,9 +37,10 @@ export async function runStudyPreflight(options) {
37
37
  error: resolved.error,
38
38
  };
39
39
  }
40
- const personas = resolved.config.route === "computer-use" || resolved.config.route === "shared-world"
41
- ? (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas
42
- : new Map();
40
+ const participants = plannedParticipants(resolved.config);
41
+ const personas = participants.length === 0
42
+ ? new Map()
43
+ : (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas;
43
44
  const route = routeOf(resolved.config);
44
45
  const ctx = {
45
46
  cwd,
@@ -59,7 +60,10 @@ export async function runStudyPreflight(options) {
59
60
  ],
60
61
  targets: collectTargets(resolved.config),
61
62
  sandbox: { created: false },
62
- warnings: [...resolved.warnings, ...personaBackgroundWarnings(resolved.config, personas)],
63
+ warnings: [
64
+ ...resolved.warnings,
65
+ ...personaBackgroundWarnings(resolved.config.id, participants, personas),
66
+ ],
63
67
  };
64
68
  const machine = machineCheck(ctx.config);
65
69
  // A local study's app and desktops run on this machine, so the hosted probes do not apply.
@@ -38,9 +38,15 @@ export interface StudySummary {
38
38
  * default. A study variable you cannot see is one nobody chose.
39
39
  */
40
40
  reasoningEffort?: string;
41
- caps: StudyCaps;
41
+ /** Absent on routes whose dollar caps the summary does not show; `{}` when none is declared. */
42
+ caps?: StudyCaps;
42
43
  /**
43
- * Whether the configured route's required keys resolve. Dry runs require none. This checks key
44
+ * What `humanish run <study>` runs this file as: its `mode`, a dry run when it sets none. The TUI
45
+ * cannot start a live run of a dry-run file, because no `run` flag overrides the file to live.
46
+ */
47
+ mode: "live" | "dry-run";
48
+ /**
49
+ * Whether the keys a live run of this study needs resolve, whatever its mode. This checks key
44
50
  * presence, not local CLI authentication or provider validity. Undefined when not checked.
45
51
  */
46
52
  keysReady?: boolean;
@@ -7,10 +7,9 @@ import { localRuntimeStatus } from "../substrates/local/runtime.js";
7
7
  // what a stakeholder reads on the screen where they decide whether to press Start.
8
8
  //
9
9
  // Resolved analysis defaults are shown independently of declared participant caps. A cap that is not
10
- // declared is not "unlimited" and not "$0"; it is a line the screen does not draw.
11
- import { resolveStudyDryRun } from "./plan.js";
12
- import { routeOf } from "./plan.js";
13
- import { localCodexParticipantCheck, planCliRun } from "./doctor.js";
10
+ // declared is not "unlimited" and not "$0": the screen says that no cap is set.
11
+ import { planStudy, resolveStudyDryRun, routeOf } from "./plan.js";
12
+ import { localCodexParticipantCheck } from "./doctor.js";
14
13
  import { requiredKeys, requiredSubjectEnv } from "./requirements.js";
15
14
  import { automaticAnalysisBudget, formatAutomaticAnalysisBudget, } from "../analysis/automatic-config.js";
16
15
  import { DEFAULT_OPENAI_CU_MODEL, DEFAULT_OPENAI_CU_REASONING_EFFORT, } from "../actors/computer-use/openai-provider.js";
@@ -70,7 +69,7 @@ function participantsOf(config) {
70
69
  /** The computer-use caps. Other routes draw none. */
71
70
  function summaryCapsOf(config) {
72
71
  if (!isComputerUseComposition(config))
73
- return {};
72
+ return undefined;
74
73
  const caps = config.caps;
75
74
  return {
76
75
  ...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
@@ -89,18 +88,17 @@ export async function readStudySummary(cwd, study, options = {}) {
89
88
  const route = routeOf(inspected.config);
90
89
  let keysReady;
91
90
  let missingKeys;
92
- const dryRun = resolveStudyDryRun(inspected.config, undefined, true) === true;
93
- // A live key check reads the plan's requirements. A study the planner refuses has none to check,
94
- // so the summary reports the refusal in place of its keys.
95
- const planned = options.checkKeys === true && !dryRun ? await planCliRun(inspected.config, cwd) : undefined;
91
+ const mode = resolveStudyDryRun(inspected.config, undefined, true) === true ? "dry-run" : "live";
92
+ // The keys are the live plan's, whatever the file's mode: the screen offers a live run of every
93
+ // study, and a dry run needs no keys to report. A study the planner refuses live has none to
94
+ // check, so the summary reports the refusal in place of its keys.
95
+ const planned = options.checkKeys === true ? planStudy(inspected.config, { cwd, dryRun: false }) : undefined;
96
96
  const planRefusal = planned?.ok === false ? planned.refusal.message : undefined;
97
- if (options.checkKeys === true && planned?.ok !== false) {
98
- const requirements = planned?.planned.plan.requirements ?? [];
97
+ if (planned?.ok === true) {
98
+ const requirements = planned.planned.plan.requirements;
99
99
  const subjectKeys = requiredSubjectEnv(requirements);
100
100
  const email = inspected.config.comms?.email;
101
- const receivingKey = !dryRun && email?.kind === "real"
102
- ? await receivingRequiredKey(cwd, email.connection)
103
- : undefined;
101
+ const receivingKey = email?.kind === "real" ? await receivingRequiredKey(cwd, email.connection) : undefined;
104
102
  const candidates = [
105
103
  "OPENAI_API_KEY",
106
104
  "CODEX_API_KEY",
@@ -108,13 +106,11 @@ export async function readStudySummary(cwd, study, options = {}) {
108
106
  ...subjectKeys,
109
107
  ...(receivingKey ? [receivingKey] : []),
110
108
  ];
111
- const probes = dryRun
112
- ? []
113
- : await probeKeySources(candidates, {
114
- cwd,
115
- env: options.env ?? process.env,
116
- ...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
117
- }).catch(() => []);
109
+ const probes = await probeKeySources(candidates, {
110
+ cwd,
111
+ env: options.env ?? process.env,
112
+ ...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
113
+ }).catch(() => []);
118
114
  const present = new Set(probes.filter((probe) => probe.source !== null).map((probe) => probe.name));
119
115
  const required = requiredKeys(requirements, (name) => present.has(name));
120
116
  const missing = [
@@ -130,6 +126,7 @@ export async function readStudySummary(cwd, study, options = {}) {
130
126
  const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
131
127
  const subject = subjectOf(config);
132
128
  const participants = participantsOf(inspected.config);
129
+ const caps = summaryCapsOf(inspected.config);
133
130
  const runtime = options.checkKeys === true && isLocalBrowserStudy(inspected.config)
134
131
  ? await localRuntimeStatus({
135
132
  ...(options.env ? { env: options.env } : {}),
@@ -170,7 +167,8 @@ export async function readStudySummary(cwd, study, options = {}) {
170
167
  ...(participants === undefined ? {} : { participants }),
171
168
  model: inspected.config.actor?.model ?? DEFAULT_OPENAI_CU_MODEL,
172
169
  reasoningEffort: reasoningEffortOf(inspected.config),
173
- caps: summaryCapsOf(inspected.config),
170
+ ...(caps === undefined ? {} : { caps }),
171
+ mode,
174
172
  ...(keysReady === undefined ? {} : { keysReady }),
175
173
  ...(missingKeys === undefined ? {} : { missingKeys }),
176
174
  ...(planRefusal === undefined ? {} : { planRefusal }),
@@ -349,6 +349,20 @@ export interface StudyActor {
349
349
  model?: string;
350
350
  /** First-party OpenAI CUA only: per-response output limit including reasoning, not a dollar cap. */
351
351
  maxOutputTokens?: number;
352
+ /**
353
+ * Computer-use and shared-world: the longest one `wait` action lasts, in whole milliseconds from
354
+ * 1000 to 600000. Default 120000, or 30000 on a desktop with speech. A longer wait is shortened
355
+ * to it, the trace records a `wait shortened` notice, and the participant is told on its next
356
+ * turn.
357
+ */
358
+ maxWaitMs?: number;
359
+ /**
360
+ * Computer-use and shared-world: how long a `wait` that names no duration lasts when the
361
+ * participant's turn only waits or takes screenshots, in whole milliseconds from 1000 to 600000
362
+ * and no longer than maxWaitMs. Default 5000. After an action in the same turn such a wait lasts
363
+ * 500 ms. OpenAI's computer-use `wait` never names a duration.
364
+ */
365
+ idleWaitMs?: number;
352
366
  /**
353
367
  * `local-agent` only: which locally signed-in coding agent is the brain. Absent = codex.
354
368
  *
@@ -36,6 +36,11 @@ export declare function taskProtocolValidationReason(config: StudyConfig, suppor
36
36
  export declare function cloneTargetValidationReason(config: StudyConfig): string | null;
37
37
  /** Refuse a claimed output bound when the route cannot pass it to the first-party provider. */
38
38
  export declare function outputTokenLimitValidationReason(config: StudyConfig): string | null;
39
+ /**
40
+ * Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
41
+ * participants take no wait actions the fields are inert (warnings.ts).
42
+ */
43
+ export declare function waitLimitValidationReason(config: StudyConfig): string | null;
39
44
  /**
40
45
  * Cross-validate a concurrent shared-world declaration. Returns the failure message,
41
46
  * or null when valid. Includes the base shared-world checks plus the concurrent extras: a synthetic
@@ -1,4 +1,5 @@
1
1
  import { isMaxOutputTokens } from "../actors/output-token-limit.js";
2
+ import { waitFieldsReason } from "../actors/computer-use/wait.js";
2
3
  import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./parse/actors.js";
3
4
  import { isHttpUrl, isLoopbackUrl } from "./parse/subject.js";
4
5
  import { declaredTargets } from "./plan-participants.js";
@@ -235,6 +236,13 @@ export function outputTokenLimitValidationReason(config) {
235
236
  }
236
237
  return null;
237
238
  }
239
+ /**
240
+ * Refuse a wait field out of range, or an idle wait longer than the longest wait. On a route whose
241
+ * participants take no wait actions the fields are inert (warnings.ts).
242
+ */
243
+ export function waitLimitValidationReason(config) {
244
+ return config.actor === undefined ? null : waitFieldsReason(config.actor, "actor.");
245
+ }
238
246
  /**
239
247
  * Shared-world participants share one live app, so at least two must be live at once. The
240
248
  * sequential shared-world route (`execution.concurrency: 1`) was removed in 0.106.0; the parser
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedPersona } from "./persona.js";
2
2
  import type { StudyConfig } from "./types.js";
3
+ import { type Participant } from "./plan-participants.js";
3
4
  /**
4
5
  * The fields a config sets that its route does not read, each as `field` or `field (reason)`.
5
6
  * parseStudy refuses a study for any of them.
@@ -10,17 +11,23 @@ export declare function inertFieldLabels(config: StudyConfig): string[];
10
11
  * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
11
12
  */
12
13
  export declare function inertFieldPaths(config: StudyConfig): string[];
14
+ export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
13
15
  /**
14
- * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
15
- * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
16
- * study author would trust a list that blocks nothing. `humanish study check` and every run print
17
- * it, and the run records it in its bundle.
18
- * Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
19
- * route.
16
+ * The warnings about a study's own fields that a run records in its bundle as `study.warning`
17
+ * events: `execution.egressAllow` off the terminal route, and participant text that reads like a
18
+ * script. parseStudy reports them too, after its other warnings, so `study check` and every run
19
+ * print them.
20
20
  */
21
- export declare function egressAllowIgnoredWarning(config: StudyConfig): string | undefined;
22
- export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
23
- /** Authored participant text is checked before runtime instructions or researcher criteria join it. */
24
- export declare function scriptedMissionWarnings(config: StudyConfig): string[];
25
- /** Backgrounds give participants a reason to act beyond the assigned task. */
26
- export declare function personaBackgroundWarnings(config: StudyConfig, personas: ReadonlyMap<string, ResolvedPersona>): string[];
21
+ export declare function recordedStudyWarnings(config: StudyConfig): string[];
22
+ /**
23
+ * A warning for each participant whose persona has no background, which gives a participant a
24
+ * reason to act beyond the assigned task. `participants` are the records the planner builds: a
25
+ * computer-use or shared-world plan's participants, or `plannedParticipants` before a plan exists.
26
+ */
27
+ export declare function personaBackgroundWarnings(studyId: string, participants: readonly Pick<Participant, "id" | "personaId">[], personas: ReadonlyMap<string, ResolvedPersona>): string[];
28
+ /**
29
+ * The participant records planStudy builds for a study's declared participants, for `study check`
30
+ * and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
31
+ * `--count` is not applied.
32
+ */
33
+ export declare function plannedParticipants(config: StudyConfig): readonly Participant[];
@@ -1,6 +1,6 @@
1
1
  import { cli } from "../cli/invocation.js";
2
2
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
3
- import { declaredParticipantIds } from "./plan-participants.js";
3
+ import { computerUseParticipants, declaredParticipantIds, sharedWorldParticipants, } from "./plan-participants.js";
4
4
  import { addressedRecipients } from "./parse/comms.js";
5
5
  import { participantList, participantInstruction, declaredParticipantCount, } from "./study-fields.js";
6
6
  // The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
@@ -50,6 +50,18 @@ const ACTOR_ROWS = [
50
50
  field: "actor.model",
51
51
  applies: (config, routes) => otherRoute(routes) && Boolean(config.actor?.model),
52
52
  },
53
+ // Only a computer-use loop dispatches wait actions; terminal, scripted and preview participants
54
+ // take none.
55
+ {
56
+ field: "actor.maxWaitMs",
57
+ reason: "the longest wait action of a computer-use participant; needs route: computer-use or shared-world",
58
+ applies: (config, routes) => config.actor?.maxWaitMs !== undefined && !routes.cua && !routes.shared,
59
+ },
60
+ {
61
+ field: "actor.idleWaitMs",
62
+ reason: "how long a computer-use participant's wait with no duration lasts; needs route: computer-use or shared-world",
63
+ applies: (config, routes) => config.actor?.idleWaitMs !== undefined && !routes.cua && !routes.shared,
64
+ },
53
65
  ];
54
66
  const TERMINAL_ONLY = "needs subject.source: terminal-product + a registered terminal actor";
55
67
  const RESERVED = "reserved; no route reads it yet";
@@ -269,12 +281,11 @@ export function inertFieldPaths(config) {
269
281
  /**
270
282
  * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
271
283
  * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
272
- * study author would trust a list that blocks nothing. `humanish study check` and every run print
273
- * it, and the run records it in its bundle.
284
+ * study author would trust a list that blocks nothing.
274
285
  * Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
275
286
  * route.
276
287
  */
277
- export function egressAllowIgnoredWarning(config) {
288
+ function egressAllowIgnoredWarning(config) {
278
289
  if (config.execution?.egressAllow === undefined || config.route === "terminal")
279
290
  return undefined;
280
291
  return `\`execution.egressAllow\` is ignored on route: ${config.route}. Only route: terminal creates its sandbox with that outbound allowlist, and this route's browser desktops can reach any site. Remove the field: the first minor release on or after 2026-11-04 refuses it on every route except terminal.`;
@@ -289,9 +300,6 @@ export function forwardDeclaredWarnings(config) {
289
300
  : [
290
301
  `These fields are set, but no route reads them yet, so they have no effect: ${inert.join(", ")}.`,
291
302
  ];
292
- const egressIgnored = egressAllowIgnoredWarning(config);
293
- if (egressIgnored !== undefined)
294
- warnings.push(egressIgnored);
295
303
  // A declared cap below the participant count is legal but loud: the roster promises N live actors
296
304
  // and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
297
305
  // waves is otherwise indistinguishable from the all-live run the author meant.
@@ -312,7 +320,20 @@ export function forwardDeclaredWarnings(config) {
312
320
  warnings.push(`comms.email covers ${covered.size} of ${participantIds.length} participants. These get no inbox and are never told one exists: ${uncovered.join(", ")}. Add a recipient with an address for each one whose flow needs email.`);
313
321
  }
314
322
  }
315
- return [...warnings, ...scriptedMissionWarnings(config)];
323
+ return [...warnings, ...recordedStudyWarnings(config)];
324
+ }
325
+ /**
326
+ * The warnings about a study's own fields that a run records in its bundle as `study.warning`
327
+ * events: `execution.egressAllow` off the terminal route, and participant text that reads like a
328
+ * script. parseStudy reports them too, after its other warnings, so `study check` and every run
329
+ * print them.
330
+ */
331
+ export function recordedStudyWarnings(config) {
332
+ const egressIgnored = egressAllowIgnoredWarning(config);
333
+ return [
334
+ ...(egressIgnored === undefined ? [] : [egressIgnored]),
335
+ ...scriptedMissionWarnings(config),
336
+ ];
316
337
  }
317
338
  const UI_ACTION = /\b(?:click|tap|press|type\b[^.!?\n]*?\binto|select\b[^.!?\n]*?\bfrom)\b/gi;
318
339
  function normalizedUrl(value) {
@@ -343,7 +364,7 @@ function actionChain(text) {
343
364
  /\bthen\b/i.test(text.slice((actions[0]?.index ?? 0) + (actions[0]?.[0].length ?? 0)))));
344
365
  }
345
366
  /** Authored participant text is checked before runtime instructions or researcher criteria join it. */
346
- export function scriptedMissionWarnings(config) {
367
+ function scriptedMissionWarnings(config) {
347
368
  if (config.route !== "computer-use" && config.route !== "shared-world")
348
369
  return [];
349
370
  const fields = [
@@ -374,20 +395,32 @@ export function scriptedMissionWarnings(config) {
374
395
  ];
375
396
  });
376
397
  }
377
- /** Backgrounds give participants a reason to act beyond the assigned task. */
378
- export function personaBackgroundWarnings(config, personas) {
379
- if (config.route !== "computer-use" && config.route !== "shared-world")
380
- return [];
381
- const roster = participantList(config);
382
- return declaredParticipantIds(config).flatMap((id, index) => {
383
- const personaId = roster?.[index]?.persona ?? config.actor?.persona;
398
+ /**
399
+ * A warning for each participant whose persona has no background, which gives a participant a
400
+ * reason to act beyond the assigned task. `participants` are the records the planner builds: a
401
+ * computer-use or shared-world plan's participants, or `plannedParticipants` before a plan exists.
402
+ */
403
+ export function personaBackgroundWarnings(studyId, participants, personas) {
404
+ return participants.flatMap(({ id, personaId }) => {
384
405
  if (personaId && personas.get(personaId)?.background)
385
406
  return [];
386
407
  const reason = personaId
387
408
  ? `persona ${personaId} has no readable background`
388
409
  : "no persona is assigned";
389
410
  return [
390
- `Participant ${id} has no persona background because ${reason}. Add a short, fictional background describing their experience and situation. Run ${cli(`study show ${config.id} --json`)} to see what the participant receives.`,
411
+ `Participant ${id} has no persona background because ${reason}. Add a short, fictional background describing their experience and situation. Run ${cli(`study show ${studyId} --json`)} to see what the participant receives.`,
391
412
  ];
392
413
  });
393
414
  }
415
+ /**
416
+ * The participant records planStudy builds for a study's declared participants, for `study check`
417
+ * and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
418
+ * `--count` is not applied.
419
+ */
420
+ export function plannedParticipants(config) {
421
+ if (config.route === "computer-use")
422
+ return computerUseParticipants(config);
423
+ if (config.route === "shared-world")
424
+ return sharedWorldParticipants(config).participants;
425
+ return [];
426
+ }
@@ -37,3 +37,6 @@ export declare function runSubjectServePipeline(shell: Shell, args: {
37
37
  * an in-sandbox git refresh, because the archive excludes .git). */
38
38
  onPhaseComplete?: () => Promise<void>;
39
39
  } & DetachedTimers): Promise<void>;
40
+ /** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
41
+ export declare class SubjectBuildError extends Error {
42
+ }
@@ -196,6 +196,9 @@ async function installSubject(step, command) {
196
196
  throw new Error(`${headline}: ${failureTail(step.scrub(install.logTail))}`);
197
197
  }
198
198
  }
199
+ /** A serve.build that exited non-zero or ran out of time; the message carries its redacted log tail. */
200
+ export class SubjectBuildError extends Error {
201
+ }
199
202
  /** serve.build, once. */
200
203
  async function buildSubject(step, command) {
201
204
  const { now } = step;
@@ -211,7 +214,7 @@ async function buildSubject(step, command) {
211
214
  });
212
215
  emitPhaseCompleted(step.onPhase, now, buildStartedAt, "build", build.ok, build.ok ? "subject build complete" : "subject build failed");
213
216
  if (!build.ok) {
214
- throw new Error(`subject build ${build.timedOut ? "timed out" : `failed (exit ${build.exitCode})`}: ${failureTail(step.scrub(build.logTail))}`);
217
+ throw new SubjectBuildError(`subject build ${build.timedOut ? "timed out" : `failed (exit ${build.exitCode})`}: ${failureTail(step.scrub(build.logTail))}`);
215
218
  }
216
219
  }
217
220
  /** serve.start as a detached process that keeps running. */
@@ -3,7 +3,9 @@
3
3
  //
4
4
  // - Scripts are written via `writeFile`, never heredocs, which eliminates the
5
5
  // sentinel-collision bug class (a command line that equals the heredoc terminator) by
6
- // construction.
6
+ // construction. The author's command is a script file of its own, so bash parses it apart
7
+ // from the wrapper: a syntax error in it is that script's exit 2, logged, and the wrapper
8
+ // still writes the status.
7
9
  // - Bounded steps (install/build) run detached with an atomically-written status file
8
10
  // (write tmp + mv), polled by short foreground commands; a timeout kills the process
9
11
  // group and surfaces a capped log tail for the caller to redact and persist.
@@ -34,10 +36,11 @@ function assertName(name) {
34
36
  function stepDir(name) {
35
37
  return `${WORK_ROOT}/${name}`;
36
38
  }
37
- // The wrapper script: runs the command from its own session (Shell.start makes the script the
38
- // process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything, and
39
- // writes the exit code atomically so a poller can never read a half-written status.
40
- function wrapperScript(name, command, cwd) {
39
+ // The wrapper script: runs the command script from its own session (Shell.start makes the
40
+ // wrapper the process-group leader, so `kill -- -PID` reclaims the whole tree), logs everything,
41
+ // and writes the exit code atomically so a poller can never read a half-written status. It holds
42
+ // no author text, so it always parses and always reaches the status write.
43
+ function wrapperScript(name, cwd) {
41
44
  const dir = stepDir(name);
42
45
  return [
43
46
  "#!/bin/bash",
@@ -46,7 +49,7 @@ function wrapperScript(name, command, cwd) {
46
49
  cwd === undefined
47
50
  ? ": # no cwd override"
48
51
  : `cd ${shellQuote(cwd)} || { echo 127 > ${shellQuote(`${dir}/status.tmp`)}; mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}; exit 127; }`,
49
- `( ${command} ) > ${shellQuote(`${dir}/log.txt`)} 2>&1`,
52
+ `bash ${shellQuote(`${dir}/command.sh`)} > ${shellQuote(`${dir}/log.txt`)} 2>&1`,
50
53
  "code=$?",
51
54
  `echo $code > ${shellQuote(`${dir}/status.tmp`)}`,
52
55
  `mv ${shellQuote(`${dir}/status.tmp`)} ${shellQuote(`${dir}/status`)}`,
@@ -59,7 +62,11 @@ async function writeAndLaunch(shell, name, command, cwd, requestTimeoutMs) {
59
62
  const dir = stepDir(name);
60
63
  const scriptPath = `${dir}/run.sh`;
61
64
  await runOrThrow(shell, `mkdir -p ${shellQuote(dir)}`, { requestTimeoutMs });
62
- await shell.writeFile(scriptPath, wrapperScript(name, command, cwd));
65
+ // The study parser trims each command, so a YAML `|` block arrives without its final newline.
66
+ // bash closes a heredoc whose delimiter is a script's last line either way; the newline makes
67
+ // the file plain text.
68
+ await shell.writeFile(`${dir}/command.sh`, `${command}\n`);
69
+ await shell.writeFile(scriptPath, wrapperScript(name, cwd));
63
70
  throwOnExit(await shell.start(`bash ${shellQuote(scriptPath)}`, { requestTimeoutMs }));
64
71
  }
65
72
  /** Read the capped log tail for a step (raw; caller redacts). */
@@ -0,0 +1,12 @@
1
+ import type { E2BConnection, E2BDesktopModule } from "./sdk.js";
2
+ /** The connection an env names: E2B_API_KEY and E2B_DOMAIN, trimmed, each only when set. */
3
+ export declare function e2bConnection(env: Readonly<Record<string, string | undefined>>): E2BConnection;
4
+ /** The calls humanish makes on sandboxes that already exist. */
5
+ export type E2BAccount = Pick<E2BDesktopModule["Sandbox"], "kill" | "getInfo" | "list">;
6
+ /**
7
+ * The kill, getInfo and list of `module`, each carrying `connection`'s key and domain over the
8
+ * call's own options. `connection` may be a create's options, so the release of a sandbox reaches
9
+ * the account that created it. Each method is bound now: a hook that later replaces one on the
10
+ * shared module does not change this account. A method the installed SDK lacks stays absent.
11
+ */
12
+ export declare function e2bAccount(module: E2BDesktopModule, connection: E2BConnection): E2BAccount;