humanish 0.117.0 → 0.118.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.
- package/CHANGELOG.md +21 -0
- package/dist/analysis/admission.d.ts +7 -2
- package/dist/analysis/admission.js +12 -3
- package/dist/analysis/automatic-config.d.ts +6 -1
- package/dist/analysis/automatic-config.js +12 -11
- package/dist/analysis/execute.d.ts +19 -7
- package/dist/analysis/execute.js +55 -17
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/study-format.js +8 -11
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +5 -3
- package/dist/cli/commands/study-route-terminal.js +2 -2
- package/dist/cli/commands/study-run.js +3 -3
- package/dist/cli/commands/study.js +2 -2
- package/dist/cli/doctor.js +11 -1
- package/dist/cli/io.js +3 -2
- package/dist/comms/lease-store.d.ts +1 -1
- package/dist/comms/lease-store.js +3 -2
- package/dist/comms/receiving-types.d.ts +2 -0
- package/dist/comms/receiving-types.js +2 -0
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/fanout-bundle.js +2 -1
- package/dist/routes/computer-use/fanout-records.js +2 -1
- package/dist/routes/computer-use/participant-runs.js +3 -9
- package/dist/routes/computer-use/plan.d.ts +3 -1
- package/dist/routes/computer-use/plan.js +25 -7
- package/dist/routes/computer-use/single-bundle.js +7 -4
- package/dist/routes/computer-use/subject-projection.js +2 -0
- package/dist/routes/scripted/plan.d.ts +4 -1
- package/dist/routes/scripted/plan.js +25 -2
- package/dist/routes/scripted/setup.js +1 -4
- package/dist/routes/scripted/types.d.ts +1 -1
- package/dist/routes/shared-world/participant-specs.js +2 -2
- package/dist/routes/shared-world/plan.d.ts +3 -1
- package/dist/routes/shared-world/plan.js +37 -6
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/run/bundle-shape.js +2 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/projection.js +7 -2
- package/dist/run/route-shell.js +10 -5
- package/dist/run/subject-name.d.ts +11 -0
- package/dist/run/subject-name.js +11 -0
- package/dist/study/automatic-analysis-plan.d.ts +28 -0
- package/dist/study/automatic-analysis-plan.js +57 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/config.js +6 -20
- package/dist/study/doctor.js +17 -3
- package/dist/study/parse/actors.d.ts +1 -1
- package/dist/study/parse/actors.js +1 -1
- package/dist/study/parse/front.d.ts +1 -1
- package/dist/study/parse/front.js +7 -1
- package/dist/study/plan-base.d.ts +2 -0
- package/dist/study/plan-base.js +3 -0
- package/dist/study/plan-participants.js +2 -5
- package/dist/study/plan-types.d.ts +8 -3
- package/dist/study/plan.d.ts +4 -4
- package/dist/study/plan.js +21 -12
- package/dist/study/preflight-result.js +2 -2
- package/dist/study/preflight.d.ts +2 -1
- package/dist/study/preflight.js +12 -3
- package/dist/study/routing.d.ts +5 -4
- package/dist/study/routing.js +16 -7
- package/dist/study/summary.d.ts +3 -3
- package/dist/study/summary.js +3 -5
- package/dist/study/validation.d.ts +2 -2
- package/dist/study/validation.js +22 -14
- package/dist/substrates/e2b/lifetime.d.ts +75 -4
- package/dist/substrates/e2b/lifetime.js +134 -10
- package/dist/substrates/e2b/subject-sandbox.js +2 -6
- package/dist/tui-app.js +112 -112
- package/dist/verify/artifacts.d.ts +19 -4
- package/dist/verify/artifacts.js +39 -8
- package/dist/verify/verify.js +6 -15
- package/docs/contracts/run-bundle.md +7 -6
- package/docs/contracts/schemas.md +14 -8
- package/docs/contracts/study-analysis.md +2 -2
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// What automatic analysis does for a study of N participants. One analysis reads at most
|
|
2
|
+
// EVIDENCE_LIMITS.participants of them (the analysis module owns that number). A study with more
|
|
3
|
+
// runs every participant and records its analysis as skipped for that reason, where the analysis
|
|
4
|
+
// would otherwise cover the first participants and drop the rest. The plan line, the run's
|
|
5
|
+
// analysis record and the CLI's exit code all read the skip from here. Once the analysis covers
|
|
6
|
+
// every participant a study may have, the skip never applies and this module can go.
|
|
7
|
+
import { automaticAnalysisSucceeded, } from "../analysis/automatic-completion.js";
|
|
8
|
+
import { automaticAnalysisBudget, formatAutomaticAnalysisBudget, } from "../analysis/automatic-config.js";
|
|
9
|
+
import { EVIDENCE_LIMITS } from "../analysis/evidence.js";
|
|
10
|
+
/** The reason a run records for an analysis its study has too many participants for. */
|
|
11
|
+
export const PARTICIPANT_LIMIT_SKIP = "AUTOMATIC_ANALYSIS_PARTICIPANT_LIMIT";
|
|
12
|
+
/** The CLI's words for that reason, after "analysis: ". */
|
|
13
|
+
export const PARTICIPANT_LIMIT_SKIP_TEXT = `not run, because the study has more than ${EVIDENCE_LIMITS.participants} participants and automatic analysis reads at most ${EVIDENCE_LIMITS.participants}`;
|
|
14
|
+
/** The skip a study of `participants` gets, or undefined when its analysis runs. */
|
|
15
|
+
export function analysisSkipFor(participants) {
|
|
16
|
+
return participants > EVIDENCE_LIMITS.participants ? PARTICIPANT_LIMIT_SKIP : undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The budget of a study's automatic analysis after a live run of `participants`, or undefined
|
|
20
|
+
* when none is planned. A skipped analysis carries no expected cost, since it spends nothing.
|
|
21
|
+
*/
|
|
22
|
+
export function studyAnalysisBudget(raw, route, participants) {
|
|
23
|
+
const skip = analysisSkipFor(participants);
|
|
24
|
+
if (skip === undefined)
|
|
25
|
+
return automaticAnalysisBudget(raw, route, participants);
|
|
26
|
+
const budget = automaticAnalysisBudget(raw, route);
|
|
27
|
+
return budget && { ...budget, participants, skip };
|
|
28
|
+
}
|
|
29
|
+
/** The line `run` and `study check` print about the analysis after a live run. */
|
|
30
|
+
export function formatStudyAnalysisBudget(budget) {
|
|
31
|
+
if (budget.skip === undefined)
|
|
32
|
+
return formatAutomaticAnalysisBudget(budget);
|
|
33
|
+
const next = budget.trigger === "default"
|
|
34
|
+
? "Set review.analysis: false to leave it out of this plan."
|
|
35
|
+
: "The analysis this study asks for will be recorded as skipped and the run will exit 2; set review.analysis: false or run fewer participants.";
|
|
36
|
+
return `After live runs: automatic analysis will not run. This study has ${budget.participants ?? "more"} participants, and automatic analysis reads at most ${EVIDENCE_LIMITS.participants}. ${next}`;
|
|
37
|
+
}
|
|
38
|
+
/** A run's result with its analysis recorded as skipped for its participant count. */
|
|
39
|
+
export function withParticipantLimitSkip(result, trigger) {
|
|
40
|
+
return {
|
|
41
|
+
...result,
|
|
42
|
+
...(trigger === "default" ? { automaticAnalysisTrigger: trigger } : {}),
|
|
43
|
+
automaticAnalysis: { state: "skipped", reason: PARTICIPANT_LIMIT_SKIP },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether a run's automatic analysis lets the command exit 0. A participant-count skip of the
|
|
48
|
+
* analysis a study did not ask for passes, like the default analysis's missing-key skip; a skip of
|
|
49
|
+
* an explicit request fails, like that request's missing key.
|
|
50
|
+
*/
|
|
51
|
+
export function studyAnalysisSucceeded(result) {
|
|
52
|
+
const value = result.automaticAnalysis;
|
|
53
|
+
return (automaticAnalysisSucceeded(result) ||
|
|
54
|
+
(result.automaticAnalysisTrigger === "default" &&
|
|
55
|
+
value?.state === "skipped" &&
|
|
56
|
+
value.reason === PARTICIPANT_LIMIT_SKIP));
|
|
57
|
+
}
|
|
@@ -118,7 +118,7 @@ function appUrlValidationReason(config) {
|
|
|
118
118
|
return `actor.type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
|
|
119
119
|
}
|
|
120
120
|
// Multi-participant fan-out is consumed on this route (`per-lane-worlds`; the shared cua
|
|
121
|
-
// cross-validation below enforces the
|
|
121
|
+
// cross-validation below enforces the participant bound, the per-participant target gates, and the
|
|
122
122
|
// allowPublicTargets+N>1 rejection for ambiguous one-target fan-out).
|
|
123
123
|
// Loopback by default; an owner may declare a public/preview target via policies.
|
|
124
124
|
const targets = [config.subject.appUrl ?? "", ...declaredTargets(config)];
|
package/dist/study/config.js
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
// The terminal route consumes subject.product, `caps` and execution.{terminal,runtimeAuth}.
|
|
19
19
|
//
|
|
20
20
|
// NOTE on `participants`: a count (preview: the simulated count; computer use: N identical
|
|
21
|
-
// participants, each its own desktop,
|
|
21
|
+
// participants, each its own desktop, at most 100), `{ count, instruction }` (identical
|
|
22
22
|
// participants with one steer), or a list of `{ id?, persona?, device?, instruction?, target? ... }`
|
|
23
23
|
// entries, each its own desktop. A list entry with a `count` is a group that expands into
|
|
24
24
|
// `<id>-01` to `<id>-NN`. A shared world takes a list of at least two. `participants[].device`
|
|
25
25
|
// excludes a raw `execution.desktop.resolution`, and `participants[].target` is app-url computer
|
|
26
26
|
// use only: an absolute browser URL this participant opens in place of `subject.appUrl`.
|
|
27
|
-
// `execution.concurrency` bounds in-flight participants (default: all at once
|
|
28
|
-
// HUMANISH_CUA_MAX_CONCURRENCY may only lower it).
|
|
27
|
+
// `execution.concurrency` bounds in-flight participants (default: all at once, up to the E2B
|
|
28
|
+
// plan's concurrent sandboxes; env HUMANISH_CUA_MAX_CONCURRENCY may only lower it).
|
|
29
29
|
import { isLocalBrowserStudy, localBrowserDefaults, localBrowserUnsupportedReason, } from "../substrates/local/runtime-config.js";
|
|
30
30
|
import { isRecord } from "../run/type-guards.js";
|
|
31
31
|
import { compositionReason } from "./composition-rules.js";
|
|
@@ -36,9 +36,8 @@ import { parseCaps, parseDefaults, parseExecution, parsePolicies, parseReview, }
|
|
|
36
36
|
import { ACTOR_REQUIRED, readStudyFront, routeNameReason, } from "./parse/front.js";
|
|
37
37
|
import { parseSubject } from "./parse/subject.js";
|
|
38
38
|
import { invalid, optionalStr, str } from "./parse/values.js";
|
|
39
|
-
import { isComputerUseComposition,
|
|
39
|
+
import { isComputerUseComposition, routeOf } from "./routing.js";
|
|
40
40
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
41
|
-
import { declaredParticipantCount, participantList } from "./study-fields.js";
|
|
42
41
|
import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
|
|
43
42
|
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "./validation.js";
|
|
44
43
|
import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
|
|
@@ -216,7 +215,8 @@ function parseParticipants(raw, read) {
|
|
|
216
215
|
}
|
|
217
216
|
/**
|
|
218
217
|
* The checks that read more than one section of a parsed config, and the defaults they fill:
|
|
219
|
-
*
|
|
218
|
+
* `comms.email.recipients` and the local browser's defaults. An omitted `execution.concurrency`
|
|
219
|
+
* stays unset: the planner resolves it from the participants and the E2B plan's limit.
|
|
220
220
|
*/
|
|
221
221
|
function checkStudyConfig(config) {
|
|
222
222
|
const smtpReason = smtpValidationReason(config);
|
|
@@ -231,20 +231,6 @@ function checkStudyConfig(config) {
|
|
|
231
231
|
const waitLimitReason = waitLimitValidationReason(config);
|
|
232
232
|
if (waitLimitReason)
|
|
233
233
|
return invalid(waitLimitReason);
|
|
234
|
-
// All-parallel default: a multi-participant study that does not declare execution.concurrency
|
|
235
|
-
// runs every participant at once; the declared field is a cap the author chose, never a mode.
|
|
236
|
-
// Independent computer-use participants resolve that default from the final participant count
|
|
237
|
-
// when they plan, after any --count override, so the parser leaves it unset for them. A shared
|
|
238
|
-
// world's roster is fixed, so its default is filled here for the envelopes and warnings that read
|
|
239
|
-
// the parsed config.
|
|
240
|
-
{
|
|
241
|
-
const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
|
|
242
|
-
if (participantCount > 1 &&
|
|
243
|
-
config.execution?.concurrency === undefined &&
|
|
244
|
-
isSharedWorldComposition(config)) {
|
|
245
|
-
config.execution = { ...config.execution, concurrency: participantCount };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
234
|
// Email that just works: the funnel's only handoff to an actor is the per-participant
|
|
249
235
|
// inbox instruction, gated on recipients[]. Guessed participant ids broke a field run:
|
|
250
236
|
// recipients copied from a single-participant example matched nothing, so every actor was left inbox-blind with zero
|
package/dist/study/doctor.js
CHANGED
|
@@ -5,7 +5,8 @@ import { capacityShortfall, describeCapacity, } from "../substrates/local/capaci
|
|
|
5
5
|
import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
6
6
|
import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
|
|
7
7
|
import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
|
|
8
|
-
import {
|
|
8
|
+
import { admissionRule } from "../analysis/admission.js";
|
|
9
|
+
import { studyAnalysisBudget } from "./automatic-analysis-plan.js";
|
|
9
10
|
import { externalCatchHealthy } from "../comms/sandbox-catch.js";
|
|
10
11
|
import { receivingRequiredKey } from "../comms/setup.js";
|
|
11
12
|
import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
|
|
@@ -190,7 +191,11 @@ export async function studySetupChecks(args) {
|
|
|
190
191
|
if (route === "scripted")
|
|
191
192
|
checks.push(await scriptedBrowserCheck());
|
|
192
193
|
checks.push(...subjectEnvChecks(requiredSubjectEnv(requirements), args));
|
|
193
|
-
const analysis =
|
|
194
|
+
const analysis = studyAnalysisBudget(config.review?.analysis, route, plan.route === "computer-use"
|
|
195
|
+
? plan.runner.participants.length
|
|
196
|
+
: plan.route === "shared-world"
|
|
197
|
+
? plan.plane.participants.length
|
|
198
|
+
: 1);
|
|
194
199
|
if (analysis)
|
|
195
200
|
checks.push(await analysisCheck(analysis, args, checkAccount));
|
|
196
201
|
checks.push(checkScope(analysis));
|
|
@@ -347,6 +352,15 @@ function subjectEnvChecks(names, args) {
|
|
|
347
352
|
}
|
|
348
353
|
/** The post-run analysis: the Codex account, or the OpenAI key and the analysis cost limit. */
|
|
349
354
|
async function analysisCheck(analysis, args, checkAccount) {
|
|
355
|
+
if (analysis.skip !== undefined) {
|
|
356
|
+
const explicit = analysis.trigger === "explicit";
|
|
357
|
+
return {
|
|
358
|
+
name: "post-run analysis",
|
|
359
|
+
ok: !explicit,
|
|
360
|
+
...(explicit ? {} : { status: "note" }),
|
|
361
|
+
message: `Will not run: this study has ${analysis.participants ?? "more"} participants, more than automatic analysis reads. The participants still run.${explicit ? " review.analysis asks for it, so the run will exit 2; set review.analysis: false or run fewer participants." : ""}`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
350
364
|
if (analysis.provider === "codex") {
|
|
351
365
|
const readiness = await checkAccount();
|
|
352
366
|
const recovery = readiness.errorCode === "codex_unsupported_platform"
|
|
@@ -368,7 +382,7 @@ async function analysisCheck(analysis, args, checkAccount) {
|
|
|
368
382
|
ok: true,
|
|
369
383
|
...(keyed ? {} : { status: "note" }),
|
|
370
384
|
message: keyed
|
|
371
|
-
? `OPENAI_API_KEY is present for the separate automatic analysis request; model access and quota are not tested. The analysis is refused before it starts if
|
|
385
|
+
? `OPENAI_API_KEY is present for the separate automatic analysis request; model access and quota are not tested. The analysis is refused before it starts if ${admissionRule(`$${analysis.maxCostUsd}`)}; this is not a billing cap. Participant readiness is independent.`
|
|
372
386
|
: "Will be skipped: OPENAI_API_KEY is missing. The participant may run, but there will be no automatic findings report. Add an OpenAI API key or set review.analysis: false deliberately.",
|
|
373
387
|
};
|
|
374
388
|
}
|
|
@@ -28,7 +28,7 @@ export interface ParticipantPaths {
|
|
|
28
28
|
* instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
29
29
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
30
30
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
31
|
-
* route-scoped cross-validation (device XOR raw resolution, targets,
|
|
31
|
+
* route-scoped cross-validation (device XOR raw resolution, targets, the participant bound) runs in parseStudy
|
|
32
32
|
* where the route is known.
|
|
33
33
|
*/
|
|
34
34
|
export declare function parseParticipantEntries(raw: unknown, paths: ParticipantPaths): {
|
|
@@ -99,7 +99,7 @@ export function parseActorFields(entry, path) {
|
|
|
99
99
|
* instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
100
100
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
101
101
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
102
|
-
* route-scoped cross-validation (device XOR raw resolution, targets,
|
|
102
|
+
* route-scoped cross-validation (device XOR raw resolution, targets, the participant bound) runs in parseStudy
|
|
103
103
|
* where the route is known.
|
|
104
104
|
*/
|
|
105
105
|
export function parseParticipantEntries(raw, paths) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type StudyRoute } from "../routing.js";
|
|
2
2
|
import type { StudyParseFailure, StudySurfaces } from "../types.js";
|
|
3
3
|
/** A document's `participants`, read before the participant entries parse. */
|
|
4
4
|
export interface Participants {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { isRecord } from "../../run/type-guards.js";
|
|
5
5
|
import { findUnknownStudyKey } from "../keys.js";
|
|
6
6
|
import { movedV2KeyReason } from "../migrate/v2.js";
|
|
7
|
+
import { participantCountReason } from "../routing.js";
|
|
7
8
|
import { PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN } from "./actors.js";
|
|
8
9
|
import { invalid, posInt } from "./values.js";
|
|
9
10
|
// The values `route:` takes: every route routeOf can return.
|
|
@@ -131,8 +132,13 @@ function homogeneousOf(raw) {
|
|
|
131
132
|
return { ok: true, value: participants };
|
|
132
133
|
}
|
|
133
134
|
// A list entry with `count: n` is a group: n participants `<id>-01` to `<id>-NN`, even when n is 1.
|
|
134
|
-
// config.ts then checks every entry and the expanded ids for collisions.
|
|
135
|
+
// config.ts then checks every entry and the expanded ids for collisions. The roster's size is
|
|
136
|
+
// checked before any group expands, so a large count is refused without building it.
|
|
135
137
|
function entriesOf(raw) {
|
|
138
|
+
const size = raw.reduce((sum, entry) => sum + (isRecord(entry) && typeof entry.count === "number" ? (posInt(entry.count) ?? 1) : 1), 0);
|
|
139
|
+
const sizeReason = participantCountReason(size);
|
|
140
|
+
if (sizeReason)
|
|
141
|
+
return invalid(sizeReason);
|
|
136
142
|
const entries = [];
|
|
137
143
|
const source = [];
|
|
138
144
|
for (const [index, entry] of raw.entries()) {
|
|
@@ -25,4 +25,6 @@ export declare function planBase(config: StudyConfig, input: {
|
|
|
25
25
|
readonly config?: PlannedAnalysis["config"] | undefined;
|
|
26
26
|
readonly preferLargerOutput?: boolean | undefined;
|
|
27
27
|
};
|
|
28
|
+
/** How many participants the run has, which decides whether its analysis can run. */
|
|
29
|
+
readonly participants?: number;
|
|
28
30
|
}): Base;
|
package/dist/study/plan-base.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// What every route planner shares: the plan fields common to all routes, the frozen residual config,
|
|
2
2
|
// and the requirement and subject helpers.
|
|
3
3
|
import { DEFAULT_OPENAI_CU_MODEL } from "../actors/computer-use/openai-provider.js";
|
|
4
|
+
import { analysisSkipFor } from "./automatic-analysis-plan.js";
|
|
4
5
|
// A YAML alias can make a parsed record contain itself (an inline persona that names its own
|
|
5
6
|
// anchor), and structuredClone keeps the cycle, so each object is frozen once.
|
|
6
7
|
function deepFreeze(value, frozen = new WeakSet()) {
|
|
@@ -108,6 +109,7 @@ export function pricedModel(brain) {
|
|
|
108
109
|
/** The fields every plan shares, from a config whose analysis already resolved. */
|
|
109
110
|
export function planBase(config, input) {
|
|
110
111
|
const analysis = input.analysis?.config;
|
|
112
|
+
const skip = analysisSkipFor(input.participants ?? 1);
|
|
111
113
|
return {
|
|
112
114
|
studyId: config.id,
|
|
113
115
|
...(config.title === undefined ? {} : { title: config.title }),
|
|
@@ -120,6 +122,7 @@ export function planBase(config, input) {
|
|
|
120
122
|
config: analysis,
|
|
121
123
|
trigger: config.review?.analysis === undefined ? "default" : "explicit",
|
|
122
124
|
preferLargerOutput: input.analysis?.preferLargerOutput === true,
|
|
125
|
+
...(skip === undefined ? {} : { skip }),
|
|
123
126
|
},
|
|
124
127
|
}),
|
|
125
128
|
requirements: [],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Who takes part in a study: one record per participant, built from the declared roster or a count.
|
|
2
2
|
// Computer-use and shared-world participants share the desktop fields; each kind adds its own.
|
|
3
3
|
import { resolveParticipantDevice } from "./device-presets.js";
|
|
4
|
-
import { isSharedWorldComposition, participantIdAt } from "./routing.js";
|
|
4
|
+
import { computerUseParticipantCount, isSharedWorldComposition, participantIdAt, } from "./routing.js";
|
|
5
5
|
import { participantList, declaredParticipantCount, participantInstruction, } from "./study-fields.js";
|
|
6
6
|
function desktopParticipant(config, entry, index, kind, focus) {
|
|
7
7
|
const actor = config.actor;
|
|
@@ -45,10 +45,7 @@ function desktopParticipant(config, entry, index, kind, focus) {
|
|
|
45
45
|
export function computerUseParticipants(config, countOverride) {
|
|
46
46
|
const actor = config.actor;
|
|
47
47
|
const roster = participantList(config);
|
|
48
|
-
|
|
49
|
-
? roster.length
|
|
50
|
-
: Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1);
|
|
51
|
-
return Array.from({ length: count }, (_, index) => {
|
|
48
|
+
return Array.from({ length: computerUseParticipantCount(config, countOverride) }, (_, index) => {
|
|
52
49
|
const entry = roster?.[index];
|
|
53
50
|
const focus = roster ? entry?.instruction : participantInstruction(config);
|
|
54
51
|
return {
|
|
@@ -57,13 +57,17 @@ export interface PlannedAnalysis {
|
|
|
57
57
|
/** "default" when the study declared no review.analysis. */
|
|
58
58
|
readonly trigger: "default" | "explicit";
|
|
59
59
|
readonly preferLargerOutput: boolean;
|
|
60
|
+
/** Set when the study has more participants than one analysis reads: a live run records the
|
|
61
|
+
* analysis as skipped for this reason and never starts it. */
|
|
62
|
+
readonly skip?: "AUTOMATIC_ANALYSIS_PARTICIPANT_LIMIT";
|
|
60
63
|
}
|
|
61
64
|
interface PlanBase {
|
|
62
65
|
readonly studyId: string;
|
|
63
66
|
/** The study's declared title, which bundles record. */
|
|
64
67
|
readonly title?: string;
|
|
65
68
|
readonly study?: RunStudyProvenance;
|
|
66
|
-
/** Warnings about the study's own fields
|
|
69
|
+
/** Warnings about the study's own fields and the plan made from them, which the run records in
|
|
70
|
+
* its bundle as warn events. */
|
|
67
71
|
readonly warnings?: readonly string[];
|
|
68
72
|
/** A frozen copy owned by the plan. */
|
|
69
73
|
readonly residual: Readonly<ResidualConfig>;
|
|
@@ -258,9 +262,10 @@ export interface ScriptedPlan extends PlanBase {
|
|
|
258
262
|
readonly scenarioRef: string;
|
|
259
263
|
/** Empty only for a library caller's `count: 0`, which the parser refuses; that run has no sessions. */
|
|
260
264
|
readonly surfaces: readonly BrowserSurface[];
|
|
261
|
-
/** The declared persona
|
|
265
|
+
/** The declared persona; the route supplies its default. */
|
|
262
266
|
readonly personaId?: string;
|
|
263
|
-
|
|
267
|
+
/** Each surface's journey budget: execution.timeoutMs, else 5 minutes. */
|
|
268
|
+
readonly sessionTimeoutMs: number;
|
|
264
269
|
}
|
|
265
270
|
export type StudyPlan = PreviewPlan | ComputerUsePlan | SharedWorldPlan | TerminalPlan | ScriptedPlan;
|
|
266
271
|
interface PlannedStudy {
|
package/dist/study/plan.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export { routeOf, type StudyRoute } from "./routing.js";
|
|
|
7
7
|
export declare function resolveStudyDryRun(config: StudyConfig, override: boolean | undefined, fallback: boolean | undefined): boolean | undefined;
|
|
8
8
|
/**
|
|
9
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
|
|
11
|
-
* ceiling, which no study file can know. Each route's
|
|
12
|
-
* makes, in the route's order and with its codes and
|
|
13
|
-
* the same planner.
|
|
10
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
11
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
12
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
13
|
+
* messages; the route's exported runner calls the same planner.
|
|
14
14
|
*/
|
|
15
15
|
export declare function planStudy(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): PlanResult;
|
package/dist/study/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
+
import { concurrentSandboxes, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
10
10
|
import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
|
|
11
11
|
import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
12
12
|
import { planBase } from "./plan-base.js";
|
|
@@ -65,21 +65,25 @@ function planPreview(config, options, input) {
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
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
|
|
69
|
-
* ceiling, which no study file can know. Each route's
|
|
70
|
-
* makes, in the route's order and with its codes and
|
|
71
|
-
* the same planner.
|
|
68
|
+
* (options.env, else process.env, as the routes read it) it reads the operator's E2B plan limits,
|
|
69
|
+
* the sandbox ceiling and the concurrent sandboxes, which no study file can know. Each route's
|
|
70
|
+
* planner makes every refusal that route makes, in the route's order and with its codes and
|
|
71
|
+
* messages; the route's exported runner calls the same planner.
|
|
72
72
|
*/
|
|
73
73
|
export function planStudy(config, options, deps = {}) {
|
|
74
74
|
const study = localBrowserDefaults(config);
|
|
75
75
|
const input = { dryRun: resolveStudyDryRun(study, options.dryRun, true) ?? true };
|
|
76
|
-
const
|
|
77
|
-
const
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
const e2b = {
|
|
78
|
+
sandboxCeiling: sandboxCeiling(env),
|
|
79
|
+
concurrentSandboxes: concurrentSandboxes(env),
|
|
80
|
+
};
|
|
81
|
+
const result = planRoute(routeOf(config), study, options, input, deps, e2b);
|
|
78
82
|
if (!result.ok)
|
|
79
83
|
return result;
|
|
80
84
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
81
85
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
82
|
-
const warnings = recordedStudyWarnings(study);
|
|
86
|
+
const warnings = [...recordedStudyWarnings(study), ...(result.plan.warnings ?? [])];
|
|
83
87
|
const plan = {
|
|
84
88
|
...result.plan,
|
|
85
89
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
@@ -87,7 +91,8 @@ export function planStudy(config, options, deps = {}) {
|
|
|
87
91
|
};
|
|
88
92
|
return { ok: true, planned: { plan } };
|
|
89
93
|
}
|
|
90
|
-
function planRoute(route, study, options, input, deps,
|
|
94
|
+
function planRoute(route, study, options, input, deps, e2b) {
|
|
95
|
+
const { sandboxCeiling } = e2b;
|
|
91
96
|
switch (route) {
|
|
92
97
|
case "preview":
|
|
93
98
|
return planPreview(study, options, input);
|
|
@@ -98,13 +103,13 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
98
103
|
driving: callerDrivingOf(options),
|
|
99
104
|
...(options.count === undefined ? {} : { countOverride: options.count }),
|
|
100
105
|
...(options.rerun === undefined ? {} : { rerun: options.rerun }),
|
|
101
|
-
|
|
106
|
+
...e2b,
|
|
102
107
|
});
|
|
103
108
|
case "shared-world":
|
|
104
109
|
return planSharedWorldStudy(study, {
|
|
105
110
|
...input,
|
|
106
111
|
hasRunSession: deps.runSession !== undefined,
|
|
107
|
-
|
|
112
|
+
...e2b,
|
|
108
113
|
});
|
|
109
114
|
case "terminal":
|
|
110
115
|
return planTerminalStudy(study, {
|
|
@@ -113,6 +118,10 @@ function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
|
113
118
|
sandboxCeiling,
|
|
114
119
|
});
|
|
115
120
|
case "scripted":
|
|
116
|
-
return planScriptedStudy(study, {
|
|
121
|
+
return planScriptedStudy(study, {
|
|
122
|
+
...input,
|
|
123
|
+
injectedBrowser: injectedBrowser(deps),
|
|
124
|
+
sandboxCeiling,
|
|
125
|
+
});
|
|
117
126
|
}
|
|
118
127
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// The preflight result a run returns, built from its context. preflight.ts and the probes in
|
|
2
2
|
// preflight-probes.ts both finish through these, so they live below both.
|
|
3
|
-
import {
|
|
3
|
+
import { studyAnalysisBudget } from "./automatic-analysis-plan.js";
|
|
4
4
|
import { digestText } from "../evidence/redaction.js";
|
|
5
5
|
import { computerUseParticipantCount } from "./routing.js";
|
|
6
6
|
/** The schema of `study check` results: whether a study is ready to run on this machine. */
|
|
7
7
|
export const STUDY_CHECK_SCHEMA = "humanish.study-check.v1";
|
|
8
8
|
export function finalize(ctx, args) {
|
|
9
9
|
const checks = [...ctx.checks, ...(args?.check ? [args.check] : []), ...(args?.checks ?? [])];
|
|
10
|
-
const analysis =
|
|
10
|
+
const analysis = studyAnalysisBudget(ctx.config.review?.analysis, ctx.route, computerUseParticipantCount(ctx.config));
|
|
11
11
|
return {
|
|
12
12
|
schema: STUDY_CHECK_SCHEMA,
|
|
13
13
|
...(analysis ? { analysis } : {}),
|
|
@@ -3,6 +3,7 @@ import type { E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
|
3
3
|
import { type StudyConfig } from "./types.js";
|
|
4
4
|
import { STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
5
5
|
import { type StudyRoute } from "./plan.js";
|
|
6
|
+
import type { PlanRefusal } from "./plan-types.js";
|
|
6
7
|
import { type StudyResolveFailure } from "./discover.js";
|
|
7
8
|
export type StudyPreflightReachabilityMode = "metadata" | "public-preview" | "sandbox-loopback" | "prepared-host";
|
|
8
9
|
export interface StudyPreflightCheck {
|
|
@@ -57,7 +58,7 @@ export interface StudyPreflightResult {
|
|
|
57
58
|
spend: StudyPreflightSpend;
|
|
58
59
|
warnings: string[];
|
|
59
60
|
error?: {
|
|
60
|
-
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED";
|
|
61
|
+
code: StudyResolveFailure["error"]["code"] | "HUMANISH_STUDY_PREFLIGHT_INVALID_OPTION" | "HUMANISH_STUDY_PREFLIGHT_UNSUPPORTED_ROUTE" | "HUMANISH_STUDY_PREFLIGHT_TARGET_POLICY" | "HUMANISH_STUDY_PREFLIGHT_ENV_MISSING" | "HUMANISH_STUDY_PREFLIGHT_E2B_REQUIRED" | "HUMANISH_STUDY_PREFLIGHT_TARGET_UNREACHABLE" | "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED" | "HUMANISH_STUDY_PREFLIGHT_TEARDOWN_FAILED" | PlanRefusal["code"];
|
|
61
62
|
message: string;
|
|
62
63
|
};
|
|
63
64
|
}
|
package/dist/study/preflight.js
CHANGED
|
@@ -4,8 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
5
5
|
import {} from "./types.js";
|
|
6
6
|
import { runPublicPreviewPreflight, runSandboxLoopbackPreflight } from "./preflight-probes.js";
|
|
7
|
-
import { digest, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
-
import { routeOf } from "./plan.js";
|
|
7
|
+
import { digest, fail, finalize, STUDY_CHECK_SCHEMA } from "./preflight-result.js";
|
|
8
|
+
import { planStudy, routeOf } from "./plan.js";
|
|
9
9
|
import { resolveStudyManifest } from "./discover.js";
|
|
10
10
|
import { participantList } from "./study-fields.js";
|
|
11
11
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
@@ -42,6 +42,7 @@ export async function runStudyPreflight(options) {
|
|
|
42
42
|
? new Map()
|
|
43
43
|
: (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas;
|
|
44
44
|
const route = routeOf(resolved.config);
|
|
45
|
+
const env = options.env ?? process.env;
|
|
45
46
|
const ctx = {
|
|
46
47
|
cwd,
|
|
47
48
|
study: options.study,
|
|
@@ -52,7 +53,7 @@ export async function runStudyPreflight(options) {
|
|
|
52
53
|
route,
|
|
53
54
|
reachability,
|
|
54
55
|
timeoutMs,
|
|
55
|
-
env
|
|
56
|
+
env,
|
|
56
57
|
hooks: options.hooks ?? {},
|
|
57
58
|
checks: [
|
|
58
59
|
{ name: "study file", ok: true, message: `resolved ${resolved.origin} study file` },
|
|
@@ -65,6 +66,14 @@ export async function runStudyPreflight(options) {
|
|
|
65
66
|
...personaBackgroundWarnings(resolved.config.id, participants, personas),
|
|
66
67
|
],
|
|
67
68
|
};
|
|
69
|
+
// The plan `humanish run <study>` makes before it creates anything: no run options, so the file's
|
|
70
|
+
// mode decides dry or live, and the same environment for the sandbox ceiling. A study the run
|
|
71
|
+
// would refuse fails here with the run's code, before any probe creates a sandbox.
|
|
72
|
+
const planned = planStudy(resolved.config, { cwd, env });
|
|
73
|
+
if (!planned.ok) {
|
|
74
|
+
const { code, message } = planned.refusal;
|
|
75
|
+
return fail(ctx, code, message, [{ name: "plan", ok: false, message }]);
|
|
76
|
+
}
|
|
68
77
|
const machine = machineCheck(ctx.config);
|
|
69
78
|
// A local study's app and desktops run on this machine, so the hosted probes do not apply.
|
|
70
79
|
if (reachability !== "metadata" && isLocalBrowserStudy(ctx.config))
|
package/dist/study/routing.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StudyConfig } from "./types.js";
|
|
2
|
-
|
|
2
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
3
|
+
export declare function participantCountReason(count: number): string | undefined;
|
|
3
4
|
export declare function actorResolvesToComputerUse(type: string | undefined): boolean;
|
|
4
5
|
export declare function registeredComputerUseActors(): string[];
|
|
5
6
|
export declare function actorResolvesToScriptedBrowser(type: string | undefined): boolean;
|
|
@@ -9,11 +10,11 @@ export declare function registeredScriptedBrowserActors(): string[];
|
|
|
9
10
|
export declare function actorResolvesToTerminal(type: string | undefined): boolean;
|
|
10
11
|
export declare function registeredTerminalActors(): string[];
|
|
11
12
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
13
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
14
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
14
15
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
15
16
|
*/
|
|
16
|
-
export declare function computerUseParticipantCount(config: StudyConfig): number;
|
|
17
|
+
export declare function computerUseParticipantCount(config: StudyConfig, countOverride?: number): number;
|
|
17
18
|
/**
|
|
18
19
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
|
19
20
|
* (shared-world participants) from its 0-based position. The parser's filled email recipients and
|
package/dist/study/routing.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { actorRegistry } from "../actors/registry.js";
|
|
2
2
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
3
3
|
import { participantList, declaredParticipantCount } from "./study-fields.js";
|
|
4
|
-
//
|
|
5
|
-
// and
|
|
6
|
-
|
|
4
|
+
// The most participants one study holds, on any route. One run keeps every participant's stream in
|
|
5
|
+
// one bundle, one Observer view and one analysis, and those are sized for 100: the analysis reads a
|
|
6
|
+
// bundle of at most 128 streams. How many run at once is a separate limit, the E2B plan's
|
|
7
|
+
// (concurrentSandboxes in substrates/e2b/lifetime.ts), and caps.maxUsd bounds what they spend.
|
|
8
|
+
const MAX_STUDY_PARTICIPANTS = 100;
|
|
9
|
+
/** Why a study of `count` participants cannot run, or undefined when it can. */
|
|
10
|
+
export function participantCountReason(count) {
|
|
11
|
+
return count > MAX_STUDY_PARTICIPANTS
|
|
12
|
+
? `A study runs at most ${MAX_STUDY_PARTICIPANTS} participants, and this one has ${count}. One run keeps every participant's evidence in one bundle and one Observer view, and humanish supports runs of up to ${MAX_STUDY_PARTICIPANTS}; split the participants across studies.`
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
7
15
|
// Run kinds whose evidence is the participant's screenshots. An actor that declares it produces none
|
|
8
16
|
// cannot run on them: a code-only actor would claim a GUI flow it only reached through a shell.
|
|
9
17
|
const SCREENSHOT_RUN_KINDS = new Set([
|
|
@@ -46,12 +54,13 @@ export function registeredTerminalActors() {
|
|
|
46
54
|
return registeredActorsOn("terminal");
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
57
|
+
* The fan-out participant count on the computer-use route: a `participants` list's length, else
|
|
58
|
+
* the caller's count override, else a participant count, else 1. The single source of truth shared by the parser, the engine,
|
|
51
59
|
* and the pre-flight plan so the participant count is computed the same way everywhere.
|
|
52
60
|
*/
|
|
53
|
-
export function computerUseParticipantCount(config) {
|
|
54
|
-
return participantList(config)?.length ??
|
|
61
|
+
export function computerUseParticipantCount(config, countOverride) {
|
|
62
|
+
return (participantList(config)?.length ??
|
|
63
|
+
Math.max(1, countOverride ?? declaredParticipantCount(config) ?? 1));
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* A participant's id: its declared roster id, else `lane-NN` (independent participants) or `role-NN`
|
package/dist/study/summary.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LocalRuntimeStatus } from "../substrates/local/runtime.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type StudyAnalysisBudget } from "./automatic-analysis-plan.js";
|
|
3
3
|
import { type KeyResolutionDeps } from "../keys/key-resolution.js";
|
|
4
4
|
export declare const STUDY_SUMMARY_SCHEMA = "humanish.study-summary.v1";
|
|
5
5
|
export interface StudyCaps {
|
|
@@ -15,8 +15,8 @@ export interface StudySummary {
|
|
|
15
15
|
message: string;
|
|
16
16
|
};
|
|
17
17
|
communications?: string;
|
|
18
|
-
analysis?:
|
|
19
|
-
/** The budget line `run` and `study check` print, from
|
|
18
|
+
analysis?: StudyAnalysisBudget & {
|
|
19
|
+
/** The budget line `run` and `study check` print, from formatStudyAnalysisBudget. */
|
|
20
20
|
line: string;
|
|
21
21
|
};
|
|
22
22
|
schema: typeof STUDY_SUMMARY_SCHEMA;
|
package/dist/study/summary.js
CHANGED
|
@@ -11,7 +11,7 @@ import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
|
11
11
|
import { planStudy, resolveStudyDryRun, routeOf } from "./plan.js";
|
|
12
12
|
import { localCodexParticipantCheck } from "./doctor.js";
|
|
13
13
|
import { requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
14
|
-
import {
|
|
14
|
+
import { formatStudyAnalysisBudget, studyAnalysisBudget, } from "./automatic-analysis-plan.js";
|
|
15
15
|
import { DEFAULT_OPENAI_CU_MODEL, DEFAULT_OPENAI_CU_REASONING_EFFORT, } from "../actors/computer-use/openai-provider.js";
|
|
16
16
|
import { inspectStudyManifest } from "./discover.js";
|
|
17
17
|
import { computerUseParticipantCount, isComputerUseComposition } from "./routing.js";
|
|
@@ -123,7 +123,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
123
123
|
missingKeys = missing;
|
|
124
124
|
}
|
|
125
125
|
// Computed once: a test-then-use pair reads as though the two calls could differ.
|
|
126
|
-
const analysis =
|
|
126
|
+
const analysis = studyAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
|
|
127
127
|
const subject = subjectOf(config);
|
|
128
128
|
const participants = participantsOf(inspected.config);
|
|
129
129
|
const caps = summaryCapsOf(inspected.config);
|
|
@@ -140,9 +140,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
140
140
|
? await localCodexParticipantCheck({ env: options.env ?? process.env })
|
|
141
141
|
: undefined;
|
|
142
142
|
return {
|
|
143
|
-
...(analysis
|
|
144
|
-
? { analysis: { ...analysis, line: formatAutomaticAnalysisBudget(analysis) } }
|
|
145
|
-
: {}),
|
|
143
|
+
...(analysis ? { analysis: { ...analysis, line: formatStudyAnalysisBudget(analysis) } } : {}),
|
|
146
144
|
...(runtime
|
|
147
145
|
? { runtime: { ok: runtime.ok, installed: runtime.installed, message: runtime.message } }
|
|
148
146
|
: {}),
|
|
@@ -2,8 +2,8 @@ import type { StudyConfig } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Cross-validate the computer-use fan-out declaration (`per-lane-worlds`). Returns the failure
|
|
4
4
|
* message, or null when valid. The parser enforces it, and runStudyWith checks it again for a config
|
|
5
|
-
* that skipped the parser. It runs rosterStructuralValidationReason (id and device
|
|
6
|
-
* validity, unique ids) first, then the route-scoped XOR,
|
|
5
|
+
* that skipped the parser. It runs rosterStructuralValidationReason (roster size, id and device
|
|
6
|
+
* validity, unique ids) first, then the route-scoped XOR, participant count and policy checks.
|
|
7
7
|
*/
|
|
8
8
|
export declare function computerUseValidationReason(config: StudyConfig): string | null;
|
|
9
9
|
/**
|