humanish 0.110.2 → 0.110.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/ARCHITECTURE.md +10 -15
  2. package/CHANGELOG.md +27 -0
  3. package/CONTEXT.md +1 -1
  4. package/dist/actors/computer-use/actor.js +2 -2
  5. package/dist/actors/computer-use/loop/ending.js +1 -1
  6. package/dist/actors/registry.js +1 -1
  7. package/dist/actors/scripted-browser/actor.js +17 -8
  8. package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
  9. package/dist/actors/scripted-browser/launch-env.js +50 -0
  10. package/dist/analysis/automatic-completion.d.ts +2 -1
  11. package/dist/analysis/automatic-completion.js +15 -2
  12. package/dist/cli/commands/migrate.js +1 -1
  13. package/dist/cli/commands/observe.js +3 -2
  14. package/dist/cli/commands/runs.js +5 -2
  15. package/dist/cli/commands/study-route-computer-use.js +3 -2
  16. package/dist/cli/commands/study-route-preview.js +2 -1
  17. package/dist/cli/commands/study-route-shared-world.js +3 -3
  18. package/dist/cli/commands/study-run.js +9 -19
  19. package/dist/cli/commands/study.js +4 -1
  20. package/dist/cli/findings.d.ts +5 -0
  21. package/dist/cli/findings.js +21 -0
  22. package/dist/cli/io.d.ts +2 -1
  23. package/dist/cli/io.js +3 -2
  24. package/dist/cli/observer-follow.d.ts +6 -2
  25. package/dist/cli/observer-follow.js +15 -5
  26. package/dist/evidence/redaction.js +15 -4
  27. package/dist/routes/computer-use/live-phase.js +8 -1
  28. package/dist/routes/computer-use/local-vm.js +2 -1
  29. package/dist/routes/computer-use/participant-runs.d.ts +6 -9
  30. package/dist/routes/computer-use/participant-runs.js +16 -36
  31. package/dist/routes/computer-use/plan.d.ts +2 -2
  32. package/dist/routes/computer-use/plan.js +7 -6
  33. package/dist/routes/computer-use/route.d.ts +5 -18
  34. package/dist/routes/computer-use/route.js +19 -79
  35. package/dist/routes/computer-use/setup.d.ts +3 -2
  36. package/dist/routes/computer-use/setup.js +2 -1
  37. package/dist/routes/computer-use/types.d.ts +6 -10
  38. package/dist/routes/scripted/plan.js +5 -4
  39. package/dist/routes/scripted/route.d.ts +4 -17
  40. package/dist/routes/scripted/route.js +15 -43
  41. package/dist/routes/scripted/surface-sessions.d.ts +2 -0
  42. package/dist/routes/scripted/surface-sessions.js +2 -1
  43. package/dist/routes/scripted/types.d.ts +1 -6
  44. package/dist/routes/shared-world/participant-specs.js +5 -1
  45. package/dist/routes/shared-world/plan.js +7 -6
  46. package/dist/routes/shared-world/route.d.ts +5 -11
  47. package/dist/routes/shared-world/route.js +27 -66
  48. package/dist/routes/shared-world/types.d.ts +2 -6
  49. package/dist/routes/terminal/plan.js +4 -3
  50. package/dist/routes/terminal/route.d.ts +3 -13
  51. package/dist/routes/terminal/route.js +21 -60
  52. package/dist/routes/terminal/session.d.ts +2 -3
  53. package/dist/routes/terminal/session.js +3 -3
  54. package/dist/routes/terminal/types.d.ts +2 -7
  55. package/dist/run/display.d.ts +10 -0
  56. package/dist/run/display.js +22 -0
  57. package/dist/run/reclaim.d.ts +4 -2
  58. package/dist/run/reclaim.js +61 -18
  59. package/dist/run/route-shell.d.ts +60 -0
  60. package/dist/run/route-shell.js +51 -0
  61. package/dist/run/run.d.ts +16 -1
  62. package/dist/run/run.js +33 -5
  63. package/dist/run/status.d.ts +8 -0
  64. package/dist/run/status.js +6 -0
  65. package/dist/run/stored-runs.js +26 -11
  66. package/dist/study/composition-rules.js +33 -33
  67. package/dist/study/config.d.ts +9 -2
  68. package/dist/study/config.js +14 -5
  69. package/dist/study/doctor.js +5 -4
  70. package/dist/study/init-templates.d.ts +1 -1
  71. package/dist/study/init-templates.js +43 -34
  72. package/dist/study/init.js +1 -1
  73. package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
  74. package/dist/study/{convert.js → migrate/convert.js} +29 -31
  75. package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
  76. package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
  77. package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
  78. package/dist/study/migrate/v2.d.ts +68 -0
  79. package/dist/study/migrate/v2.js +480 -0
  80. package/dist/study/parse/actors.d.ts +46 -1
  81. package/dist/study/parse/actors.js +116 -69
  82. package/dist/study/parse/execution.js +2 -2
  83. package/dist/study/parse/study-v3.d.ts +26 -0
  84. package/dist/study/parse/study-v3.js +53 -35
  85. package/dist/study/parse/study.d.ts +38 -0
  86. package/dist/study/parse/study.js +225 -0
  87. package/dist/study/parse/subject.js +1 -1
  88. package/dist/study/persona-resolve.d.ts +2 -4
  89. package/dist/study/persona-resolve.js +2 -1
  90. package/dist/study/plan-base.d.ts +1 -1
  91. package/dist/study/plan-base.js +5 -4
  92. package/dist/study/plan-participants.js +12 -11
  93. package/dist/study/plan.js +4 -3
  94. package/dist/study/preflight-probes.js +2 -2
  95. package/dist/study/preflight.js +2 -2
  96. package/dist/study/routing.js +12 -15
  97. package/dist/study/run-study-options.js +3 -3
  98. package/dist/study/study-fields.d.ts +28 -0
  99. package/dist/study/study-fields.js +51 -0
  100. package/dist/study/summary.js +13 -17
  101. package/dist/study/types.d.ts +40 -0
  102. package/dist/study/url-credentials.js +2 -1
  103. package/dist/study/validation.d.ts +2 -0
  104. package/dist/study/validation.js +46 -41
  105. package/dist/study/warnings.js +5 -4
  106. package/dist/substrates/local/runtime-config.js +9 -9
  107. package/dist/tui-app.js +3 -3
  108. package/docs/architecture/actor-contract.md +1 -1
  109. package/docs/architecture/external-public-shared-world.md +1 -1
  110. package/docs/architecture/terminal-product-route.md +13 -13
  111. package/docs/contracts/schemas.md +13 -4
  112. package/docs/contracts/study-analysis.md +5 -2
  113. package/docs/product/automatic-analysis.md +1 -1
  114. package/package.json +1 -1
  115. /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
@@ -17,14 +17,13 @@
17
17
  import { randomBytes } from "node:crypto";
18
18
  import { realpath } from "node:fs/promises";
19
19
  import path from "node:path";
20
- import { completeAutomaticAnalysis } from "../../analysis/automatic-completion.js";
21
- import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
22
20
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
23
21
  import { buildRunSource } from "../../run/bundle.js";
24
22
  import { prepareSelectedOutputDirectory } from "../../run/contained-output.js";
25
- import { runScope } from "../../run/run.js";
23
+ import {} from "../../run/run.js";
24
+ import { admitRoute, completeRefusalAnalysis } from "../../run/route-shell.js";
26
25
  import { renderScriptedReviewMarkdown } from "./bundle.js";
27
- import { injectedBrowser, planScriptedStudy } from "./plan.js";
26
+ import {} from "./plan.js";
28
27
  import { finishScriptedRun } from "./result.js";
29
28
  import { UnsafeScriptedSessionResultError } from "./session-result.js";
30
29
  import { prepareScriptedRun } from "./setup.js";
@@ -32,21 +31,7 @@ import { ScriptedSubject } from "./subject.js";
32
31
  import { runScriptedSessions, writeSurfaceTraces } from "./surface-sessions.js";
33
32
  import {} from "./types.js";
34
33
  import { studyResultIdentity } from "../../run/study-result.js";
35
- /**
36
- * The config-taking entry point. It plans, returns a refusal with the envelope the route has always
37
- * returned at that refusal's stage, and otherwise runs the plan.
38
- */
39
- export async function runScriptedBrowserStudy(options) {
40
- const { config, dryRun, ...input } = options;
41
- // planScriptedStudy makes every configuration refusal, in the order this route always has.
42
- const planned = planScriptedStudy(config, {
43
- dryRun,
44
- injectedBrowser: injectedBrowser(input.deps),
45
- });
46
- if (planned.ok)
47
- return runScriptedPlan(planned.plan, input);
48
- return scriptedStudyRefusal(options, planned.refusal);
49
- }
34
+ import { actorOf } from "../../study/study-fields.js";
50
35
  /**
51
36
  * A refused scripted study's result, at the refusal's stage: a before-scope refusal has its own field
52
37
  * order and no analysis record; the others come after the output directory checks.
@@ -54,7 +39,7 @@ export async function runScriptedBrowserStudy(options) {
54
39
  export async function scriptedStudyRefusal(options, refusal) {
55
40
  const { config, dryRun } = options;
56
41
  const cwd = path.resolve(options.cwd);
57
- const actorType = config.actors[0]?.type ?? "";
42
+ const actorType = actorOf(config)?.type ?? "";
58
43
  if (refusal.beforeScope)
59
44
  return {
60
45
  ...studyResultIdentity("scripted", config.id),
@@ -83,35 +68,19 @@ export async function scriptedStudyRefusal(options, refusal) {
83
68
  warnings: [],
84
69
  error: { code: refusal.code, message: refusal.message },
85
70
  };
86
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
87
- return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, options, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
71
+ return completeRefusalAnalysis(refused, config, options);
88
72
  }
89
73
  /**
90
74
  * runStudyWith's step for a scripted plan. It takes no scorer, and its local checks (keys, a browser)
91
75
  * run inside the run, so it returns the run.
92
76
  */
93
77
  export function admitScriptedPlan(plan, input) {
94
- return {
95
- ok: true,
96
- run: async () => ({
97
- route: "scripted",
98
- result: await runScriptedPlan(plan, input),
99
- }),
100
- };
101
- }
102
- /**
103
- * Run a scripted plan. The run scope gives a direct library caller the same status-record lifetime
104
- * the CLI gets: returning from this function finalizes any record the run opened, whichever of its
105
- * fail-closed exits it took. Each route opens its own scope; `runStudyWith` opens none. Without this a
106
- * test or an adopter calling the route directly leaves
107
- * the 5s cadence ticking into a directory something else is deleting, which surfaces as an
108
- * unrelated ENOTEMPTY.
109
- */
110
- export async function runScriptedPlan(plan, input) {
111
- const { result, finished } = await runScope((scope) => runScriptedPlanInScope(plan, input, scope));
112
- return completeAutomaticAnalysis(result, finished, plan.analysis?.config, input, {
113
- ...(plan.analysis === undefined ? {} : { trigger: plan.analysis.trigger }),
114
- preferLargerOutput: plan.analysis?.preferLargerOutput === true,
78
+ return admitRoute({
79
+ route: "scripted",
80
+ analysis: plan.analysis,
81
+ input,
82
+ admit: () => ({ ok: true, admitted: plan }),
83
+ runInScope: runScriptedPlanInScope,
115
84
  });
116
85
  }
117
86
  async function runScriptedPlanInScope(plan, input, scope) {
@@ -126,6 +95,8 @@ async function runScriptedPlanInScope(plan, input, scope) {
126
95
  mintRunId: makeScriptedRunId,
127
96
  mode: dryRun ? "dry-run" : "live",
128
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",
129
100
  renderReview: renderScriptedReviewMarkdown,
130
101
  observer: { open: input.open === true, render: setup.deps.renderObserver },
131
102
  });
@@ -176,6 +147,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
176
147
  deps: setup.deps,
177
148
  browserCommand: setup.browserCommand,
178
149
  runPaths,
150
+ onSessionStart: () => run.participantStarted(),
179
151
  });
180
152
  }
181
153
  catch (error) {
@@ -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,
@@ -1,23 +1,18 @@
1
1
  import type { ActorCompletionReason, ActorStatus } from "../../actors/contract.js";
2
2
  import type { StudyEvent } from "../../study/run-study-events.js";
3
3
  import type { AutomaticAnalysisResult } from "../../analysis/automatic-completion.js";
4
- import type { StudyConfig } from "../../study/types.js";
5
4
  import type { ObserverResult } from "../../observer/render.js";
6
5
  import type { RunSubjectProvenance } from "../../run/bundle.js";
7
6
  import type { StudyDeps } from "../../study/study-deps.js";
8
7
  import type { RunStudyHomes } from "../../study/run-study-homes.js";
9
8
  import { type StudyResultIdentity } from "../../run/study-result.js";
10
9
  /** What a scripted run takes besides its plan. The plan carries the config, dry run and study. */
11
- export type ScriptedRunInput = Omit<RunScriptedBrowserStudyOptions, "config" | "dryRun">;
12
- export interface RunScriptedBrowserStudyOptions {
10
+ export interface ScriptedRunInput {
13
11
  /** Cancels post-run analysis only. */
14
12
  analysisSignal?: AbortSignal;
15
13
  /** Reports the analysis window to onEvent; built by normalizeRunStudyOptions. */
16
14
  emit?: (event: StudyEvent) => void;
17
15
  cwd: string;
18
- config: StudyConfig;
19
- /** Resolved upstream (scenario.mode + CLI override); defaults safe (dry-run). */
20
- dryRun: boolean;
21
16
  open?: boolean;
22
17
  runId?: string;
23
18
  /** Keys and subject env for the run. Defaults to process.env. Values are scrubbed; names persist. */
@@ -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,
@@ -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
13
  import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, 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.`);
@@ -59,11 +60,11 @@ export function planSharedWorldStudy(config, input) {
59
60
  : concurrentSharedWorldValidationReason(config));
60
61
  if (invalidReason)
61
62
  return refuse(invalid, invalidReason, actor);
62
- if (config.actors[0]?.maxOutputTokens !== undefined && input.hasRunSession === true)
63
+ if (actorOf(config)?.maxOutputTokens !== undefined && input.hasRunSession === true)
63
64
  return refuse(invalid, "maxOutputTokens cannot be enforced by a custom runSession.", actor);
64
- const caps = config.execution?.caps;
65
+ const caps = capsOf(config, "shared-world");
65
66
  if (!input.dryRun && (caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
66
- const model = (config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
67
+ const model = (actorOf(config)?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
67
68
  if (!MODEL_RATES[model])
68
69
  return refuse("HUMANISH_SHARED_WORLD_UNPRICED_CAP", unpricedCapMessage(model), actor);
69
70
  }
@@ -106,7 +107,7 @@ export function planSharedWorldStudy(config, input) {
106
107
  ? {}
107
108
  : { sessionTimeoutMs: config.execution.timeoutMs }),
108
109
  brain,
109
- caps: capsOf(config),
110
+ caps: planCaps(config, "shared-world"),
110
111
  requirements: base.dryRun
111
112
  ? []
112
113
  : [
@@ -1,21 +1,15 @@
1
+ import { type RefusedStudy } from "../../run/route-shell.js";
1
2
  import { type SharedWorldRefusal } from "./plan.js";
2
3
  import type { SharedWorldPlan } from "../../study/plan-types.js";
3
4
  import type { AdmittedPlan } from "../../run-study.js";
4
5
  import type { StudyConfig } from "../../study/types.js";
5
- import { type ConcurrentSharedWorldStudyResult, type SharedWorldRunInput, type RunConcurrentSharedWorldStudyOptions } from "./types.js";
6
- /**
7
- * The library entry for a shared-world study. It plans the config with planSharedWorldStudy and runs
8
- * the plan with runSharedWorldPlan, whose run scope and withTransientCommsSecrets wrapper cover the
9
- * run and its analysis.
10
- */
11
- export declare function runConcurrentSharedWorld(options: RunConcurrentSharedWorldStudyOptions): Promise<ConcurrentSharedWorldStudyResult>;
6
+ import { type ConcurrentSharedWorldStudyResult, type SharedWorldRunInput } from "./types.js";
12
7
  /** A refused shared-world study's result: the route's envelope, and a refusal's analysis record. */
13
- export declare function sharedWorldStudyRefusal(options: RunConcurrentSharedWorldStudyOptions, refusal: SharedWorldRefusal): Promise<ConcurrentSharedWorldStudyResult>;
8
+ export declare function sharedWorldStudyRefusal(options: SharedWorldRunInput & RefusedStudy, refusal: SharedWorldRefusal): Promise<ConcurrentSharedWorldStudyResult>;
14
9
  /**
15
10
  * runStudyWith's step for a shared-world plan. It runs a live plan's local checks (the keys, a local
16
11
  * agent's sign-in and the subject env) before any run scope opens, so the CLI can present their refusal before it loads a
17
- * declared scorer, and returns the run that continues from them with that scorer.
12
+ * declared scorer, and returns the run that continues from them with that scorer. The run and its
13
+ * analysis share one scope for the email secrets it registers.
18
14
  */
19
15
  export declare function admitSharedWorldPlan(plan: SharedWorldPlan, input: SharedWorldRunInput, config: StudyConfig): Promise<AdmittedPlan<"shared-world">>;
20
- /** Run a shared-world plan: its local checks, then the run. */
21
- export declare function runSharedWorldPlan(plan: SharedWorldPlan, input: SharedWorldRunInput, config: StudyConfig): Promise<ConcurrentSharedWorldStudyResult>;
@@ -34,15 +34,13 @@
34
34
  // gate; humanish cannot tell synthetic from real data, so it makes no claim about real data.
35
35
  import path from "node:path";
36
36
  import { missingKeys, missingSubjectEnv } from "../../study/requirements.js";
37
- import { completeAutomaticAnalysis } from "../../analysis/automatic-completion.js";
38
- import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
39
37
  import { scrubLiterals } from "../../evidence/redaction.js";
40
38
  import { describeMissingKeys } from "../../keys/key-resolution.js";
41
- import { withTransientCommsSecrets } from "../../run/transient-comms-secrets.js";
42
- import { runScope } from "../../run/run.js";
39
+ import {} from "../../run/run.js";
40
+ import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
43
41
  import { makeCuaRunBudget } from "../computer-use/participant-model.js";
44
42
  import { runExternalPublicPlane } from "./external-public.js";
45
- import { planSharedWorldStudy, sharedWorldDescriptorOf } from "./plan.js";
43
+ import { sharedWorldDescriptorOf } from "./plan.js";
46
44
  import { localAgentRefusal } from "../../actors/local-agent/readiness.js";
47
45
  import { runProvisionedPlane } from "./provisioned.js";
48
46
  import { concurrentStudyFailure, finishConcurrentRun } from "./result.js";
@@ -50,7 +48,7 @@ import { prepareConcurrentRun } from "./setup.js";
50
48
  import { browserRouteScorer } from "../../study/adapter-scorer-loader.js";
51
49
  import { withLateScorer } from "../../study/route-inputs.js";
52
50
  import {} from "./types.js";
53
- import { rosterOf } from "../../study/parse/actors.js";
51
+ import { actorOf, participantList } from "../../study/study-fields.js";
54
52
  /** The shared-world code for each local-agent refusal, kind for kind with computer-use. */
55
53
  const LOCAL_AGENT_REFUSAL_CODES = {
56
54
  "agent-missing": "HUMANISH_SHARED_WORLD_AGENT_MISSING",
@@ -58,30 +56,14 @@ const LOCAL_AGENT_REFUSAL_CODES = {
58
56
  unsupported: "HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED",
59
57
  "unpriced-cap": "HUMANISH_SHARED_WORLD_UNPRICED_CAP",
60
58
  };
61
- /**
62
- * The library entry for a shared-world study. It plans the config with planSharedWorldStudy and runs
63
- * the plan with runSharedWorldPlan, whose run scope and withTransientCommsSecrets wrapper cover the
64
- * run and its analysis.
65
- */
66
- export async function runConcurrentSharedWorld(options) {
67
- const { config, dryRun, ...input } = options;
68
- // planSharedWorldStudy makes every configuration refusal, in the order this route always has.
69
- const planned = planSharedWorldStudy(config, {
70
- dryRun,
71
- hasRunSession: input.deps?.runSession !== undefined,
72
- });
73
- if (planned.ok)
74
- return runSharedWorldPlan(planned.plan, input, config);
75
- return sharedWorldStudyRefusal(options, planned.refusal);
76
- }
77
59
  /** A refused shared-world study's result: the route's envelope, and a refusal's analysis record. */
78
60
  export function sharedWorldStudyRefusal(options, refusal) {
79
61
  const { config, dryRun } = options;
80
- const declared = rosterOf(config.actors[0]) ?? [];
62
+ const declared = participantList(config) ?? [];
81
63
  const fail = concurrentStudyFailure({
82
64
  cwd: path.resolve(options.cwd),
83
65
  studyId: config.id,
84
- actor: config.actors[0]?.type ?? "",
66
+ actor: actorOf(config)?.type ?? "",
85
67
  participantCount: declared.length,
86
68
  // The parser fills concurrency for multi-participant studies, so this fallback serves only
87
69
  // library callers: every declared participant runs at once unless the author declared a cap.
@@ -89,42 +71,38 @@ export function sharedWorldStudyRefusal(options, refusal) {
89
71
  dryRun,
90
72
  runId: options.runId,
91
73
  });
92
- // A refusal starts no run, so a declared or default analysis is recorded as skipped.
93
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
94
- return completeAutomaticAnalysis(fail(refusal.code, refusal.message, refusal.actor), undefined, analysis.ok ? analysis.config : undefined, options, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
74
+ return completeRefusalAnalysis(fail(refusal.code, refusal.message, refusal.actor), config, options);
95
75
  }
96
76
  /**
97
77
  * runStudyWith's step for a shared-world plan. It runs a live plan's local checks (the keys, a local
98
78
  * agent's sign-in and the subject env) before any run scope opens, so the CLI can present their refusal before it loads a
99
- * declared scorer, and returns the run that continues from them with that scorer.
79
+ * declared scorer, and returns the run that continues from them with that scorer. The run and its
80
+ * analysis share one scope for the email secrets it registers.
100
81
  */
101
- export async function admitSharedWorldPlan(plan, input, config) {
102
- const refused = await admitSharedWorldRun(plan, input);
103
- if (refused)
104
- return { ok: false, outcome: sharedWorldOutcome(refused) };
105
- return {
106
- ok: true,
107
- run: async (scorer) => sharedWorldOutcome(await runAdmittedSharedWorldRun(plan, withLateScorer(input, scorer, browserRouteScorer), config)),
108
- };
109
- }
110
- function sharedWorldOutcome(result) {
111
- return { route: "shared-world", result };
112
- }
113
- /** Run a shared-world plan: its local checks, then the run. */
114
- export async function runSharedWorldPlan(plan, input, config) {
115
- const refused = await admitSharedWorldRun(plan, input);
116
- return refused ?? runAdmittedSharedWorldRun(plan, input, config);
82
+ export function admitSharedWorldPlan(plan, input, config) {
83
+ return admitRoute({
84
+ route: "shared-world",
85
+ analysis: plan.analysis,
86
+ input,
87
+ admit: () => admitSharedWorldRun(plan, input),
88
+ withScorer: (base, scorer) => withLateScorer(base, scorer, browserRouteScorer),
89
+ runInScope: (_admitted, running, scope) => runPlanInScope(plan, running, config, scope),
90
+ commsSecrets: true,
91
+ });
117
92
  }
118
93
  /**
119
94
  * A live plan's local checks, made outside any run scope: the keys, a local agent's sign-in, then
120
- * the subject env. A refusal is the result runSharedWorldPlan returns for it, with the analysis
121
- * record of a run that never started.
95
+ * the subject env.
122
96
  */
123
97
  async function admitSharedWorldRun(plan, input) {
98
+ const admitted = { ok: true, admitted: undefined };
124
99
  if (plan.dryRun)
125
- return undefined;
100
+ return admitted;
126
101
  const env = input.env ?? process.env;
127
- const fail = (code, message) => completeSharedWorldAnalysis(plan, input, sharedWorldFailure(plan, input)(code, message, sharedWorldDescriptorOf(plan.actor).id), undefined);
102
+ const fail = (code, message) => ({
103
+ ok: false,
104
+ result: sharedWorldFailure(plan, input)(code, message, sharedWorldDescriptorOf(plan.actor).id),
105
+ });
128
106
  // The plan lists OPENAI_API_KEY for an openai brain's participants and the external-public
129
107
  // plane's lobby-code reader; a local-agent brain's participants run on the operator's signed-in
130
108
  // agent instead.
@@ -143,24 +121,7 @@ async function admitSharedWorldRun(plan, input) {
143
121
  if (unsetSubjectEnv.length > 0) {
144
122
  return fail("HUMANISH_SHARED_WORLD_SUBJECT_ENV_MISSING", `subject.env declares ${unsetSubjectEnv.join(", ")} but the environment does not provide ${unsetSubjectEnv.length === 1 ? "it" : "them"} (pass via --dotenv; values are never persisted).`);
145
123
  }
146
- return undefined;
147
- }
148
- /**
149
- * Runs an admitted plan in its own run scope, then its automatic analysis. The
150
- * withTransientCommsSecrets wrapper scopes any email secret the run registers to this run and its
151
- * analysis.
152
- */
153
- function runAdmittedSharedWorldRun(plan, input, config) {
154
- return withTransientCommsSecrets(async () => {
155
- const { result, finished } = await runScope((scope) => runPlanInScope(plan, input, config, scope));
156
- return completeSharedWorldAnalysis(plan, input, result, finished);
157
- });
158
- }
159
- function completeSharedWorldAnalysis(plan, input, result, finished) {
160
- return completeAutomaticAnalysis(result, finished, plan.analysis?.config, input, {
161
- ...(plan.analysis === undefined ? {} : { trigger: plan.analysis.trigger }),
162
- preferLargerOutput: plan.analysis?.preferLargerOutput === true,
163
- });
124
+ return admitted;
164
125
  }
165
126
  /** The route's envelope for a run that stops before its bundle. */
166
127
  function sharedWorldFailure(plan, input) {
@@ -24,13 +24,11 @@ export declare const CONCURRENT_SHARED_WORLD_PROVIDER_METADATA: {
24
24
  readonly tool: "humanish";
25
25
  };
26
26
  export declare const CONCURRENT_ATTRIBUTION_LIMITS: readonly ["concurrent", "best-effort-causal-attribution", "non-deterministic-shared-state", "window-and-snapshot-granularity", "contention-observed-not-proven-safe", "state-change-not-isolated-to-actors"];
27
- export interface RunConcurrentSharedWorldStudyOptions {
27
+ /** What a shared-world run takes besides its plan. The plan carries the config, dry run and study. */
28
+ export interface SharedWorldRunInput {
28
29
  /** Cancels post-run analysis only. */
29
30
  analysisSignal?: AbortSignal;
30
31
  cwd: string;
31
- config: StudyConfig;
32
- /** Resolved upstream (scenario.mode + CLI override); defaults safe (dry-run). */
33
- dryRun: boolean;
34
32
  open?: boolean;
35
33
  runId?: string;
36
34
  onObserverReady?: (observer: ObserverResult & {
@@ -56,8 +54,6 @@ export interface RunConcurrentSharedWorldStudyOptions {
56
54
  * core-stamped onto the bundle as evidence. Absent for library callers. */
57
55
  scorerProvenance?: RunScorerProvenance;
58
56
  }
59
- /** What a shared-world run takes besides its plan. The plan carries the config, dry run and study. */
60
- export type SharedWorldRunInput = Omit<RunConcurrentSharedWorldStudyOptions, "config" | "dryRun">;
61
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"
62
58
  /** A local-agent brain's CLI is not on `PATH`. */
63
59
  | "HUMANISH_SHARED_WORLD_AGENT_MISSING"
@@ -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, "terminal");
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
@@ -1,20 +1,10 @@
1
+ import { type RefusedStudy } from "../../run/route-shell.js";
1
2
  import { type TerminalRefusal } from "./plan.js";
2
3
  import type { TerminalPlan } from "../../study/plan-types.js";
3
4
  import type { AdmittedPlan } from "../../run-study.js";
4
- import { type RunTerminalProductStudyOptions, type TerminalProductStudyResult, type TerminalRunInput } from "./types.js";
5
- /**
6
- * The config-taking entry point. It plans, returns a refusal with the same envelope and analysis
7
- * record the route has always returned, and otherwise runs the plan.
8
- */
9
- export declare function runTerminalProductStudy(options: RunTerminalProductStudyOptions): Promise<TerminalProductStudyResult>;
5
+ import { type TerminalProductStudyResult, type TerminalRunInput } from "./types.js";
10
6
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
11
- export declare function terminalStudyRefusal(options: RunTerminalProductStudyOptions, refusal: TerminalRefusal): Promise<TerminalProductStudyResult>;
12
- /**
13
- * Run a terminal plan. The run scope gives a direct library caller the same run lifetime the CLI
14
- * gets: whichever of its fail-closed exits the run takes, the run it started is closed, and only a
15
- * run that published its final bundle reaches automatic analysis.
16
- */
17
- export declare function runTerminalPlan(plan: TerminalPlan, input: TerminalRunInput): Promise<TerminalProductStudyResult>;
7
+ export declare function terminalStudyRefusal(options: TerminalRunInput & RefusedStudy, refusal: TerminalRefusal): Promise<TerminalProductStudyResult>;
18
8
  /**
19
9
  * runStudyWith's step for a terminal plan. It runs a live plan's local checks (checkLiveTerminalMachine)
20
10
  * before any run scope opens, so the CLI can present their refusal before it loads a declared
@@ -44,31 +44,17 @@
44
44
  // humanish never calls Sandbox.list to prove cleanup, so a shared operator key never reaches a
45
45
  // sandbox it did not create. A live run that cannot prove teardown fails closed.
46
46
  // Enforced in sandbox.ts.
47
- import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
48
- import { completeAutomaticAnalysis } from "../../analysis/automatic-completion.js";
49
47
  import path from "node:path";
50
- import { runScope } from "../../run/run.js";
51
- import { planTerminalStudy } from "./plan.js";
48
+ import {} from "../../run/run.js";
49
+ import { admitRoute, completeRefusalAnalysis, } from "../../run/route-shell.js";
50
+ import {} from "./plan.js";
52
51
  import { runDryTerminalStudy } from "./dry-run.js";
53
52
  import { checkLiveTerminalMachine, runLiveTerminalSession } from "./session.js";
54
53
  import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
55
54
  import { withLateScorer } from "../../study/route-inputs.js";
56
55
  import {} from "./types.js";
57
56
  import { studyResultIdentity } from "../../run/study-result.js";
58
- /**
59
- * The config-taking entry point. It plans, returns a refusal with the same envelope and analysis
60
- * record the route has always returned, and otherwise runs the plan.
61
- */
62
- export async function runTerminalProductStudy(options) {
63
- const { config, dryRun, ...input } = options;
64
- const planned = planTerminalStudy(config, {
65
- dryRun,
66
- hasCostProbe: input.deps?.costProbe !== undefined,
67
- });
68
- if (planned.ok)
69
- return runTerminalPlan(planned.plan, input);
70
- return terminalStudyRefusal(options, planned.refusal);
71
- }
57
+ import { actorOf } from "../../study/study-fields.js";
72
58
  /** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
73
59
  export function terminalStudyRefusal(options, refusal) {
74
60
  const { config, dryRun } = options;
@@ -76,67 +62,42 @@ export function terminalStudyRefusal(options, refusal) {
76
62
  ...studyResultIdentity("terminal", config.id),
77
63
  ok: false,
78
64
  cwd: path.resolve(options.cwd),
79
- actor: refusal.actor ?? config.actors[0]?.type ?? "",
65
+ actor: refusal.actor ?? actorOf(config)?.type ?? "",
80
66
  product: config.subject.product?.name ?? "",
81
67
  dryRun,
82
68
  runId: options.runId ?? "not-created",
83
69
  warnings: [],
84
70
  error: { code: refusal.code, message: refusal.message },
85
71
  };
86
- // A refusal starts no run, so a declared or default analysis is recorded as skipped.
87
- const analysis = resolveAutomaticAnalysis(config.review?.analysis);
88
- return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, options, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
89
- }
90
- /**
91
- * Run a terminal plan. The run scope gives a direct library caller the same run lifetime the CLI
92
- * gets: whichever of its fail-closed exits the run takes, the run it started is closed, and only a
93
- * run that published its final bundle reaches automatic analysis.
94
- */
95
- export async function runTerminalPlan(plan, input) {
96
- const admission = await admitTerminalRun(plan, input);
97
- return admission.ok ? runAdmittedTerminalRun(admission.admitted, input) : admission.result;
72
+ return completeRefusalAnalysis(refused, config, options);
98
73
  }
99
74
  /**
100
75
  * runStudyWith's step for a terminal plan. It runs a live plan's local checks (checkLiveTerminalMachine)
101
76
  * before any run scope opens, so the CLI can present their refusal before it loads a declared
102
77
  * scorer, and returns the run that continues from them with that scorer.
103
78
  */
104
- export async function admitTerminalPlan(plan, input) {
105
- const admission = await admitTerminalRun(plan, input);
106
- if (!admission.ok)
107
- return { ok: false, outcome: terminalOutcome(admission.result) };
108
- return {
109
- ok: true,
110
- run: async (scorer) => terminalOutcome(await runAdmittedTerminalRun(admission.admitted, withLateScorer(input, scorer, terminalRouteScorer))),
111
- };
112
- }
113
- function terminalOutcome(result) {
114
- return { route: "terminal", result };
79
+ export function admitTerminalPlan(plan, input) {
80
+ return admitRoute({
81
+ route: "terminal",
82
+ analysis: plan.analysis,
83
+ input,
84
+ admit: () => admitTerminalRun(plan, input),
85
+ withScorer: (base, scorer) => withLateScorer(base, scorer, terminalRouteScorer),
86
+ runInScope: runTerminalPlanInScope,
87
+ });
115
88
  }
116
- /**
117
- * A live plan's local checks, made outside any run scope. A refusal is the result runTerminalPlan
118
- * returns for it, with the analysis record of a run that never started.
119
- */
120
- async function admitTerminalRun(plan, input) {
89
+ /** A live plan's local checks, made outside any run scope. */
90
+ function admitTerminalRun(plan, input) {
121
91
  const warnings = [];
122
92
  if (plan.dryRun)
123
93
  return { ok: true, admitted: { plan, warnings } };
124
94
  const checked = checkLiveTerminalMachine(plan, input, warnings);
125
95
  if (checked.ok)
126
96
  return { ok: true, admitted: { plan, warnings, runtimeEnv: checked.runtimeEnv } };
127
- const refused = terminalFailure(plan, input, warnings)(checked.code, checked.message);
128
- return { ok: false, result: await completeTerminalAnalysis(plan, input, refused, undefined) };
129
- }
130
- /** Runs an admitted plan in its own run scope, then its automatic analysis. */
131
- async function runAdmittedTerminalRun(admitted, input) {
132
- const { result, finished } = await runScope((scope) => runTerminalPlanInScope(admitted, input, scope));
133
- return completeTerminalAnalysis(admitted.plan, input, result, finished);
134
- }
135
- function completeTerminalAnalysis(plan, input, result, finished) {
136
- return completeAutomaticAnalysis(result, finished, plan.analysis?.config, input, {
137
- ...(plan.analysis === undefined ? {} : { trigger: plan.analysis.trigger }),
138
- preferLargerOutput: plan.analysis?.preferLargerOutput === true,
139
- });
97
+ return {
98
+ ok: false,
99
+ result: terminalFailure(plan, input, warnings)(checked.code, checked.message),
100
+ };
140
101
  }
141
102
  /** The route's envelope for a run that stops before its bundle. */
142
103
  function terminalFailure(plan, input, warnings) {
@@ -13,9 +13,8 @@ export declare function checkLiveTerminalMachine(plan: LiveTerminalPlan, input:
13
13
  readonly message: string;
14
14
  };
15
15
  /**
16
- * The live in-sandbox agent session orchestrator (mirror of runCuaActorStudy's E2B branch). Enforces
17
- * the 8-point safety contract by construction; fails closed before any sandbox/key/spend on any
18
- * precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
16
+ * The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
17
+ * construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
19
18
  * the redacted terminal event stream + normalized transcript, the agent report, and the
20
19
  * provider-neutral actor trace; tears the sandbox down in a finally and proves the teardown.
21
20
  */
@@ -38,9 +38,8 @@ export function checkLiveTerminalMachine(plan, input, warnings) {
38
38
  return { ok: true, runtimeEnv };
39
39
  }
40
40
  /**
41
- * The live in-sandbox agent session orchestrator (mirror of runCuaActorStudy's E2B branch). Enforces
42
- * the 8-point safety contract by construction; fails closed before any sandbox/key/spend on any
43
- * precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
41
+ * The live in-sandbox agent session orchestrator. Enforces the 8-point safety contract by
42
+ * construction; fails closed before any sandbox/key/spend on any precondition miss. Persists the substrate-lifecycle/command-log/interventions/cleanup ledgers,
44
43
  * the redacted terminal event stream + normalized transcript, the agent report, and the
45
44
  * provider-neutral actor trace; tears the sandbox down in a finally and proves the teardown.
46
45
  */
@@ -112,6 +111,7 @@ export async function runLiveTerminalSession(args) {
112
111
  if ((await session.bootstrap()) &&
113
112
  (await session.verifyRuntimeVersion()) &&
114
113
  (await session.prepareProduct())) {
114
+ run.participantStarted();
115
115
  await session.execCodex();
116
116
  }
117
117
  }