humanish 0.110.3 → 0.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/dist/run/run.js
CHANGED
|
@@ -32,11 +32,13 @@ export class FinishedRun {
|
|
|
32
32
|
runId;
|
|
33
33
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
34
34
|
paths;
|
|
35
|
-
|
|
35
|
+
#participantsRan;
|
|
36
|
+
constructor(key, runId, paths, observer, outcome, recordFailure, interrupted, participantsRan) {
|
|
36
37
|
if (key !== issueKey)
|
|
37
38
|
throw new Error("Only Run.finish issues a FinishedRun.");
|
|
38
39
|
this.runId = runId;
|
|
39
40
|
this.paths = paths;
|
|
41
|
+
this.#participantsRan = participantsRan;
|
|
40
42
|
this.#observer = observer;
|
|
41
43
|
this.#outcome = outcome;
|
|
42
44
|
this.#recordFailure = recordFailure;
|
|
@@ -49,6 +51,10 @@ export class FinishedRun {
|
|
|
49
51
|
get interrupted() {
|
|
50
52
|
return this.#interrupted();
|
|
51
53
|
}
|
|
54
|
+
/** The route reported a participant's session started (Run.participantStarted). */
|
|
55
|
+
get participantsRan() {
|
|
56
|
+
return this.#participantsRan();
|
|
57
|
+
}
|
|
52
58
|
static isIssued(value) {
|
|
53
59
|
return typeof value === "object" && value !== null && #observer in value;
|
|
54
60
|
}
|
|
@@ -106,7 +112,7 @@ function observerTarget(options) {
|
|
|
106
112
|
};
|
|
107
113
|
}
|
|
108
114
|
function runPublisher(args) {
|
|
109
|
-
const { options, runId, paths, runStatus, now, admit } = args;
|
|
115
|
+
const { options, runId, paths, runStatus, now, admit, participantsRan } = args;
|
|
110
116
|
const observer = observerTarget(options);
|
|
111
117
|
let pointerWritten = false;
|
|
112
118
|
// The bundle of the last write asked for, and the outcome every later write carries once a
|
|
@@ -176,7 +182,7 @@ function runPublisher(args) {
|
|
|
176
182
|
await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
|
|
177
183
|
return current;
|
|
178
184
|
}));
|
|
179
|
-
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, () => runStatus.interrupted);
|
|
185
|
+
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, () => runStatus.interrupted, participantsRan);
|
|
180
186
|
};
|
|
181
187
|
const interrupt = async (signal) => {
|
|
182
188
|
interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
|
|
@@ -231,7 +237,8 @@ export async function runScope(fn) {
|
|
|
231
237
|
const created = await createRunArtifactPaths(options.cwd, runId);
|
|
232
238
|
if (!created.ok)
|
|
233
239
|
return created;
|
|
234
|
-
// beginRunStatus reads only the mode
|
|
240
|
+
// beginRunStatus reads only the mode, the study provenance and the sandboxes field from
|
|
241
|
+
// the run's options.
|
|
235
242
|
const runStatus = beginRunStatus(created.paths, { ...options, runId });
|
|
236
243
|
status = runStatus;
|
|
237
244
|
let interruptBundle;
|
|
@@ -260,7 +267,16 @@ export async function runScope(fn) {
|
|
|
260
267
|
const openRun = (options, runId, paths, runStatus) => {
|
|
261
268
|
const now = options.now ?? Date.now;
|
|
262
269
|
const createdAt = new Date(now()).toISOString();
|
|
263
|
-
|
|
270
|
+
let participantsRan = false;
|
|
271
|
+
const publisher = runPublisher({
|
|
272
|
+
options,
|
|
273
|
+
runId,
|
|
274
|
+
paths,
|
|
275
|
+
runStatus,
|
|
276
|
+
now,
|
|
277
|
+
admit,
|
|
278
|
+
participantsRan: () => participantsRan,
|
|
279
|
+
});
|
|
264
280
|
let finishCalled = false;
|
|
265
281
|
const checkIdentity = (bundle) => bundle.runId !== runId || bundle.mode !== options.mode
|
|
266
282
|
? "The bundle names another run or mode than this run."
|
|
@@ -271,6 +287,9 @@ export async function runScope(fn) {
|
|
|
271
287
|
mode: options.mode,
|
|
272
288
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
273
289
|
paths,
|
|
290
|
+
participantStarted() {
|
|
291
|
+
participantsRan = true;
|
|
292
|
+
},
|
|
274
293
|
writeSnapshot(bundle) {
|
|
275
294
|
if (closed)
|
|
276
295
|
return refuse("The run scope has closed.");
|
package/dist/run/status.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface RunStatusRecord {
|
|
|
53
53
|
* Read either through studyProvenanceOf.
|
|
54
54
|
*/
|
|
55
55
|
lab?: RunStudyProvenance;
|
|
56
|
+
/**
|
|
57
|
+
* `none` when the route recorded at start that this run creates no sandbox, as a scripted run
|
|
58
|
+
* against an app-url subject does. Absent when the run may create one. Reclaim reads it.
|
|
59
|
+
*/
|
|
60
|
+
sandboxes?: "none";
|
|
56
61
|
/** The pid that owns the run, for local liveness checks. */
|
|
57
62
|
pid: number;
|
|
58
63
|
startedAt: string;
|
|
@@ -87,6 +92,7 @@ export interface BeginRunStatusOptions {
|
|
|
87
92
|
runId: string;
|
|
88
93
|
mode: "dry-run" | "live";
|
|
89
94
|
study?: RunStudyProvenance | undefined;
|
|
95
|
+
sandboxes?: "none" | undefined;
|
|
90
96
|
}
|
|
91
97
|
/**
|
|
92
98
|
* Start a run's status record and keep it fresh. Fire-and-forget by design: a status write that
|
package/dist/run/status.js
CHANGED
|
@@ -64,6 +64,7 @@ export function beginRunStatus(runPaths, options) {
|
|
|
64
64
|
state: "running",
|
|
65
65
|
mode: options.mode,
|
|
66
66
|
...studyFields(options.study),
|
|
67
|
+
...(options.sandboxes === undefined ? {} : { sandboxes: options.sandboxes }),
|
|
67
68
|
pid: process.pid,
|
|
68
69
|
startedAt,
|
|
69
70
|
updatedAt: startedAt,
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -5,7 +5,7 @@ import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, r
|
|
|
5
5
|
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, } from "./contained-output.js";
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
7
|
import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
|
|
8
|
-
import { bundleDisplayFacts, runDisplay } from "./display.js";
|
|
8
|
+
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
9
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
10
10
|
import { readLatest, readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
@@ -101,22 +101,33 @@ export async function listRuns(cwdInput) {
|
|
|
101
101
|
* that is not an object naming a run gets none.
|
|
102
102
|
*/
|
|
103
103
|
async function listedRunDisplay(runPaths, bundle) {
|
|
104
|
-
|
|
104
|
+
const displayed = displayedBundle(bundle);
|
|
105
|
+
if (displayed === undefined)
|
|
105
106
|
return {};
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const status = displayed.outcome === undefined
|
|
108
|
+
? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE)
|
|
109
|
+
: undefined;
|
|
110
|
+
return { display: runDisplay(bundleDisplayFacts(displayed, status)) };
|
|
111
|
+
}
|
|
112
|
+
/** The fields runDisplay reads from an unchecked run.json; none when it does not name a run. */
|
|
113
|
+
function displayedBundle(bundle) {
|
|
114
|
+
if (!isRecord(bundle) || typeof bundle.runId !== "string")
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
109
117
|
runId: bundle.runId,
|
|
110
118
|
...(typeof bundle.mode === "string" ? { mode: bundle.mode } : {}),
|
|
111
119
|
...(isRecord(bundle.review) && typeof bundle.review.verdict === "string"
|
|
112
120
|
? { review: { verdict: bundle.review.verdict } }
|
|
113
121
|
: {}),
|
|
114
122
|
...(Array.isArray(bundle.simulations)
|
|
115
|
-
? {
|
|
123
|
+
? {
|
|
124
|
+
simulations: bundle.simulations
|
|
125
|
+
.filter(isRecord)
|
|
126
|
+
.map((record) => (typeof record.status === "string" ? { status: record.status } : {})),
|
|
127
|
+
}
|
|
116
128
|
: {}),
|
|
117
|
-
...(outcome
|
|
118
|
-
}
|
|
119
|
-
return { display: runDisplay(facts) };
|
|
129
|
+
...(isRunOutcome(bundle.outcome) ? { outcome: bundle.outcome } : {}),
|
|
130
|
+
};
|
|
120
131
|
}
|
|
121
132
|
function runsUnavailableResult(cwd, error) {
|
|
122
133
|
return {
|
|
@@ -159,10 +170,14 @@ export async function readReview(cwdInput, runInput) {
|
|
|
159
170
|
const projected = isRecord(bundle) && Array.isArray(bundle.streams)
|
|
160
171
|
? withCuaReviewProvenance(review, bundle.streams.filter(isRecord))
|
|
161
172
|
: review;
|
|
173
|
+
const displayed = displayedBundle(bundle);
|
|
174
|
+
const { display } = runPaths ? await listedRunDisplay(runPaths, bundle) : {};
|
|
162
175
|
return {
|
|
163
|
-
...
|
|
176
|
+
...(displayed === undefined || display === undefined
|
|
177
|
+
? projected
|
|
178
|
+
: displayedReview(projected, displayed, display)),
|
|
164
179
|
path: path.relative(cwd, path.join(runPaths.absoluteRunRoot, "review.json")),
|
|
165
180
|
runId: path.basename(runPaths.absoluteRunRoot),
|
|
166
|
-
...(
|
|
181
|
+
...(display === undefined ? {} : { display }),
|
|
167
182
|
};
|
|
168
183
|
}
|
package/dist/run-study.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type LateScorer } from "./study/route-inputs.js";
|
|
|
3
3
|
import { type StudyRoute } from "./study/plan.js";
|
|
4
4
|
import type { StudyDeps } from "./study/study-deps.js";
|
|
5
5
|
import type { RunStudyDriving, RunStudyHomes } from "./study/run-study-homes.js";
|
|
6
|
-
import {
|
|
6
|
+
import type { StudyConfig } from "./study/types.js";
|
|
7
7
|
import type { ObserverResult } from "./observer/render.js";
|
|
8
8
|
import { type CuaActorStudyResult } from "./routes/computer-use/types.js";
|
|
9
9
|
import { type PreviewStudyResult } from "./routes/preview.js";
|
|
@@ -20,8 +20,10 @@ import type { RunStudyProvenance } from "./run/study-provenance.js";
|
|
|
20
20
|
export declare function runStudyWith(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): Promise<StudyOutcome>;
|
|
21
21
|
/**
|
|
22
22
|
* The package's study runner (`src/index.ts`): runStudyWith with the public options only. A
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* config that skipped parseStudy is checked first (libraryConfigRefusal): a humanish.lab.v2 config
|
|
24
|
+
* or one with a humanish 0.110 field, no `actor`, or a `route` the config does not take. That, or a
|
|
25
|
+
* field RunStudyOptions no longer has, is refused in the route's own result envelope before
|
|
26
|
+
* anything runs.
|
|
25
27
|
*/
|
|
26
28
|
export declare function runStudy(config: StudyConfig, options: RunStudyOptions): Promise<StudyOutcome>;
|
|
27
29
|
/** A study planned once: the route's refusal, or the run of its plan. */
|
package/dist/run-study.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
import { computerUseInput, scriptedInput, sharedWorldInput, terminalInput, } from "./study/route-inputs.js";
|
|
8
8
|
import { planStudy, resolveStudyDryRun, routeOf } from "./study/plan.js";
|
|
9
9
|
import { normalizeRunStudyOptions, optionRefusalOutcome, removedOptionRefusal, } from "./study/run-study-options.js";
|
|
10
|
-
import {
|
|
11
|
-
import { V2_UNSUPPORTED_MESSAGE } from "./study/config.js";
|
|
10
|
+
import { libraryConfigRefusal } from "./study/config.js";
|
|
12
11
|
import { admitComputerUsePlan, computerUseStudyRefusal } from "./routes/computer-use/route.js";
|
|
13
12
|
import {} from "./routes/computer-use/types.js";
|
|
14
13
|
import { admitPreviewPlan, previewStudyRefusal, } from "./routes/preview.js";
|
|
@@ -32,17 +31,14 @@ export async function runStudyWith(config, options, deps = {}) {
|
|
|
32
31
|
}
|
|
33
32
|
/**
|
|
34
33
|
* The package's study runner (`src/index.ts`): runStudyWith with the public options only. A
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* config that skipped parseStudy is checked first (libraryConfigRefusal): a humanish.lab.v2 config
|
|
35
|
+
* or one with a humanish 0.110 field, no `actor`, or a `route` the config does not take. That, or a
|
|
36
|
+
* field RunStudyOptions no longer has, is refused in the route's own result envelope before
|
|
37
|
+
* anything runs.
|
|
37
38
|
*/
|
|
38
39
|
export async function runStudy(config, options) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
ok: false,
|
|
42
|
-
code: "HUMANISH_STUDY_V2_UNSUPPORTED",
|
|
43
|
-
message: V2_UNSUPPORTED_MESSAGE,
|
|
44
|
-
}
|
|
45
|
-
: removedOptionRefusal(options);
|
|
40
|
+
const rejected = libraryConfigRefusal(config);
|
|
41
|
+
const refusal = rejected === undefined ? removedOptionRefusal(options) : { ok: false, ...rejected };
|
|
46
42
|
if (refusal === undefined)
|
|
47
43
|
return runStudyWith(config, options);
|
|
48
44
|
const study = localBrowserDefaults(config);
|
|
@@ -50,7 +50,7 @@ export type AdapterScorerLoadResult = {
|
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
52
|
/**
|
|
53
|
-
* Resolve + load a config-declared scorer module. Resolution clones scenario
|
|
53
|
+
* Resolve + load a config-declared scorer module. Resolution clones the scripted `scenario` resolution exactly
|
|
54
54
|
* (prepareSelectedOutputDirectory → cwd-clamp → readContainedRegularFile), then `import()`s the
|
|
55
55
|
* entry file in a broad try/catch. The digest is over the readContainedRegularFile entry bytes.
|
|
56
56
|
*/
|
|
@@ -54,7 +54,7 @@ const SCORER_CAPABLE_ROUTES = new Set([
|
|
|
54
54
|
* package.json `type`. `.ts` is rejected: the compiled `dist/` CLI ships no TypeScript loader. */
|
|
55
55
|
const SCORER_EXTENSIONS = new Set([".mjs", ".js", ".cjs"]);
|
|
56
56
|
/**
|
|
57
|
-
* Resolve + load a config-declared scorer module. Resolution clones scenario
|
|
57
|
+
* Resolve + load a config-declared scorer module. Resolution clones the scripted `scenario` resolution exactly
|
|
58
58
|
* (prepareSelectedOutputDirectory → cwd-clamp → readContainedRegularFile), then `import()`s the
|
|
59
59
|
* entry file in a broad try/catch. The digest is over the readContainedRegularFile entry bytes.
|
|
60
60
|
*/
|
|
@@ -84,10 +84,10 @@ export async function loadAdapterScorer(args) {
|
|
|
84
84
|
if (!SCORER_EXTENSIONS.has(ext)) {
|
|
85
85
|
return fail("HUMANISH_STUDY_SCORER_BAD_REF", `review.scorer.ref "${trimmed}" must be a path relative to the project ending in .mjs (recommended), .js or .cjs.`);
|
|
86
86
|
}
|
|
87
|
-
// Root token: realpath(cwd) → prepareSelectedOutputDirectory(dirname, cwd). Mirrors scenario
|
|
87
|
+
// Root token: realpath(cwd) → prepareSelectedOutputDirectory(dirname, cwd). Mirrors the scripted `scenario`.
|
|
88
88
|
const physicalCwd = await realpath(path.resolve(args.cwd));
|
|
89
89
|
const root = await prepareSelectedOutputDirectory(path.dirname(physicalCwd), physicalCwd);
|
|
90
|
-
// Clamp verbatim from the scripted scenario
|
|
90
|
+
// Clamp verbatim from the scripted `scenario` branch: resolve → relative → reject escape/absolute.
|
|
91
91
|
const absolutePath = path.resolve(root.physicalPath, trimmed);
|
|
92
92
|
const relative = path.relative(root.physicalPath, absolutePath);
|
|
93
93
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
// a library caller's config itself, under its route's error codes. The planners call the shared
|
|
5
5
|
// checks in study/validation.ts and desktopCliProductReason below. The other subject rules here have
|
|
6
6
|
// planner counterparts whose conditions and wording differ (clone serve and repo, local-tree
|
|
7
|
-
// target, in-process fan-out, loopback targets, scripted scenario
|
|
7
|
+
// target, in-process fan-out, loopback targets, scripted `scenario`).
|
|
8
8
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
9
9
|
import { REPO_SLUG_PATTERN } from "./parse/values.js";
|
|
10
10
|
import { declaredTargets } from "./plan-participants.js";
|
|
11
11
|
import { actorResolvesToComputerUse, actorResolvesToScriptedBrowser, actorResolvesToTerminal, computerUseParticipantCount, registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, isComputerUseComposition, } from "./routing.js";
|
|
12
12
|
import { cloneTargetValidationReason, computerUseValidationReason, sharedWorldValidationReason, } from "./validation.js";
|
|
13
|
-
import {
|
|
13
|
+
import { actorOf, participantList, declaredParticipantCount, surfaceCount, modeOf, scenarioRefOf, declaresSharedWorld, } from "./study-fields.js";
|
|
14
|
+
/**
|
|
15
|
+
* The count a scripted or terminal composition reads: its surfaces, or a participant count.
|
|
16
|
+
* parse/front.ts refuses `participants` on route: scripted and route: terminal, and `surfaces` off
|
|
17
|
+
* route: scripted, but a study can declare a route its composition does not take, and these rules
|
|
18
|
+
* run before parseStudy compares the two. migrate's view of a v2 file reaches them the same way.
|
|
19
|
+
*/
|
|
20
|
+
function participantOrSurfaceCount(config) {
|
|
21
|
+
return declaredParticipantCount(config) ?? surfaceCount(config);
|
|
22
|
+
}
|
|
14
23
|
/** The first composition rule a config breaks, in the parser's order, or null. */
|
|
15
24
|
export function compositionReason(config) {
|
|
16
25
|
return (thisRepoValidationReason(config) ??
|
|
@@ -25,7 +34,7 @@ export function compositionReason(config) {
|
|
|
25
34
|
(isComputerUseComposition(config) ? computerUseValidationReason(config) : null) ??
|
|
26
35
|
// Whenever shared world is declared, not only when it routes, so a half-declared shared world
|
|
27
36
|
// fails with a precise reason instead of running as independent participants.
|
|
28
|
-
(config
|
|
37
|
+
(declaresSharedWorld(config) ? sharedWorldValidationReason(config) : null) ??
|
|
29
38
|
desktopCliValidationReason(config) ??
|
|
30
39
|
terminalValidationReason(config) ??
|
|
31
40
|
cloneActorValidationReason(config));
|
|
@@ -39,7 +48,7 @@ function thisRepoValidationReason(config) {
|
|
|
39
48
|
if (config.execution?.target) {
|
|
40
49
|
return "`execution.target` applies only to clone/app-url/local-app subjects; this-repo studies run locally.";
|
|
41
50
|
}
|
|
42
|
-
if (config
|
|
51
|
+
if (modeOf(config) === "live") {
|
|
43
52
|
return THIS_REPO_DRY_RUN_ONLY;
|
|
44
53
|
}
|
|
45
54
|
}
|
|
@@ -54,18 +63,18 @@ function thisRepoValidationReason(config) {
|
|
|
54
63
|
// planComputerUseStudy refuses it with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
|
|
55
64
|
function localAppValidationReason(config) {
|
|
56
65
|
if (config.subject.source === "local-app") {
|
|
57
|
-
const type = config
|
|
66
|
+
const type = actorOf(config)?.type ?? "";
|
|
58
67
|
if (config.execution?.target !== undefined && config.execution.target !== "local") {
|
|
59
68
|
return "A local-app subject drives a local dev server in this process, with no E2B desktop. Set `execution.target: local` or omit it. To run on a hosted desktop, use an app-url subject with `execution.target: e2b-desktop`.";
|
|
60
69
|
}
|
|
61
70
|
if (!actorResolvesToComputerUse(type)) {
|
|
62
|
-
return `
|
|
71
|
+
return `actor.type must be a registered computer-use actor for local-app subjects (one of: ${registeredComputerUseActors().join(", ")}); the caller's custom executor runs the computer-use loop. Got "${type}".`;
|
|
63
72
|
}
|
|
64
73
|
if (computerUseParticipantCount(config) > 1) {
|
|
65
|
-
return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips.
|
|
74
|
+
return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Remove `participants`, or set `participants: 1`. For fan-out, use an app-url or clone subject on execution.target: e2b-desktop.";
|
|
66
75
|
}
|
|
67
|
-
if (
|
|
68
|
-
return "`
|
|
76
|
+
if (participantList(config) !== undefined) {
|
|
77
|
+
return "`participants` (fan-out roster) is not supported on the in-process/local-app route: it provisions one E2B desktop per participant, which this route skips. Use an app-url or clone subject with execution.target: e2b-desktop.";
|
|
69
78
|
}
|
|
70
79
|
if (config.policies?.allowPublicTargets === true) {
|
|
71
80
|
return "`policies.allowPublicTargets` does not apply to a local-app subject: it is always a loopback dev server, so there is no public target to allow. Remove the setting.";
|
|
@@ -78,18 +87,18 @@ function localAppValidationReason(config) {
|
|
|
78
87
|
// app; a computer-use actor drives a hosted desktop browser. Fail closed on mis-configs.
|
|
79
88
|
function appUrlValidationReason(config) {
|
|
80
89
|
if (config.subject.source === "app-url") {
|
|
81
|
-
const type = config
|
|
90
|
+
const type = actorOf(config)?.type ?? "";
|
|
82
91
|
if (actorResolvesToScriptedBrowser(type)) {
|
|
83
92
|
// Scripted-browser route (all fail-closed, so claims match mechanism: a field that cannot act on
|
|
84
93
|
// this route is rejected, never silently ignored).
|
|
85
94
|
if (config.execution?.target !== undefined && config.execution.target !== "local") {
|
|
86
95
|
return "A scripted-browser actor on an app-url subject runs on this machine. Set `execution.target: local` or omit it. To run scripted steps on a hosted desktop, use a clone subject.";
|
|
87
96
|
}
|
|
88
|
-
if (!config
|
|
89
|
-
return "A scripted-browser study needs `scenario
|
|
97
|
+
if (!scenarioRefOf(config)) {
|
|
98
|
+
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
90
99
|
}
|
|
91
|
-
if ((config
|
|
92
|
-
return "A scripted-browser study takes `
|
|
100
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 2) {
|
|
101
|
+
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
93
102
|
}
|
|
94
103
|
if (config.policies?.redactScreenshots === true) {
|
|
95
104
|
return "`policies.redactScreenshots: true` is not supported on the scripted-browser route yet, so its screenshots would be stored unredacted in .humanish/. Remove the setting, or use a computer-use actor, which blurs screenshots as it takes them.";
|
|
@@ -106,17 +115,16 @@ function appUrlValidationReason(config) {
|
|
|
106
115
|
return "app-url computer-use subjects require `execution.target: local` or `e2b-desktop`.";
|
|
107
116
|
}
|
|
108
117
|
if (!actorResolvesToComputerUse(type)) {
|
|
109
|
-
return `
|
|
118
|
+
return `actor.type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
|
|
110
119
|
}
|
|
111
120
|
// Multi-participant fan-out is consumed on this route (`per-lane-worlds`; the shared cua
|
|
112
|
-
// cross-validation below enforces
|
|
113
|
-
//
|
|
114
|
-
// fan-out).
|
|
121
|
+
// cross-validation below enforces the 16 cap, the per-participant target gates, and the
|
|
122
|
+
// allowPublicTargets+N>1 rejection for ambiguous one-target fan-out).
|
|
115
123
|
// Loopback by default; an owner may declare a public/preview target via policies.
|
|
116
124
|
const targets = [config.subject.appUrl ?? "", ...declaredTargets(config)];
|
|
117
125
|
const unsafeTarget = targets.find((target) => !config.policies?.allowPublicTargets && !isLoopbackUrl(target));
|
|
118
126
|
if (unsafeTarget !== undefined) {
|
|
119
|
-
return "`subject.appUrl` and `
|
|
127
|
+
return "`subject.appUrl` and `participants[].target` must be loopback URLs (127.0.0.1 or localhost). To drive a deployed or preview URL you own, set `policies.allowPublicTargets: true`.";
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
}
|
|
@@ -125,7 +133,7 @@ function appUrlValidationReason(config) {
|
|
|
125
133
|
// Scripted-browser actors on any other subject: only a provisioned clone on e2b-desktop.
|
|
126
134
|
function scriptedBrowserValidationReason(config) {
|
|
127
135
|
if (config.subject.source !== "app-url" &&
|
|
128
|
-
actorResolvesToScriptedBrowser(config
|
|
136
|
+
actorResolvesToScriptedBrowser(actorOf(config)?.type)) {
|
|
129
137
|
if (config.subject.source !== "clone") {
|
|
130
138
|
return "scripted-browser actors require `subject.source: app-url` (a running app at a loopback URL) or `subject.source: clone` with `execution.target: e2b-desktop` (a provisioned synthetic subject).";
|
|
131
139
|
}
|
|
@@ -142,20 +150,22 @@ function scriptedBrowserValidationReason(config) {
|
|
|
142
150
|
if (!REPO_SLUG_PATTERN.test(repo)) {
|
|
143
151
|
return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
|
|
144
152
|
}
|
|
145
|
-
|
|
153
|
+
// migrate converts a v2 file's `subject.topology: shared-world` to `route: shared-world`, and
|
|
154
|
+
// refuses it here with the v2 parser's message.
|
|
155
|
+
if (declaresSharedWorld(config)) {
|
|
146
156
|
return "A clone scripted-browser study does not support `subject.topology` yet: it runs one synthetic subject for its scripted actor. Remove `subject.topology`.";
|
|
147
157
|
}
|
|
148
158
|
if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
|
|
149
159
|
return "A clone scripted-browser study does not support `subject.clone.fanout` or `subject.clone.keep` yet: its subject is always one sandbox, removed after the run.";
|
|
150
160
|
}
|
|
151
|
-
if (!config
|
|
152
|
-
return "A scripted-browser study needs `scenario
|
|
161
|
+
if (!scenarioRefOf(config)) {
|
|
162
|
+
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
153
163
|
}
|
|
154
|
-
if ((config
|
|
155
|
-
return "A scripted-browser study takes `
|
|
164
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 2) {
|
|
165
|
+
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
156
166
|
}
|
|
157
|
-
if (
|
|
158
|
-
return "`
|
|
167
|
+
if (participantList(config) !== undefined) {
|
|
168
|
+
return "`participants` is not supported on the scripted-browser route yet. Use `surfaces` to choose the desktop and mobile surfaces.";
|
|
159
169
|
}
|
|
160
170
|
if (config.policies?.redactScreenshots === true) {
|
|
161
171
|
return "`policies.redactScreenshots: true` is not supported on the scripted-browser route yet, so its screenshots would be stored unredacted in .humanish/. Remove the setting, or use a computer-use actor, which blurs screenshots as it takes them.";
|
|
@@ -183,7 +193,7 @@ function scriptedBrowserValidationReason(config) {
|
|
|
183
193
|
function cloneComputerUseValidationReason(config) {
|
|
184
194
|
if (config.subject.source === "clone" &&
|
|
185
195
|
config.execution?.target === "e2b-desktop" &&
|
|
186
|
-
actorResolvesToComputerUse(config
|
|
196
|
+
actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
187
197
|
if (!config.subject.serve) {
|
|
188
198
|
return "A clone subject on the computer-use route needs `subject.serve` (start and url): humanish starts the app in the sandbox before the participant opens it.";
|
|
189
199
|
}
|
|
@@ -195,8 +205,8 @@ function cloneComputerUseValidationReason(config) {
|
|
|
195
205
|
return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
|
|
196
206
|
}
|
|
197
207
|
// Fan-out is consumed here: N participants each clone the same single repo into their own
|
|
198
|
-
// E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the
|
|
199
|
-
//
|
|
208
|
+
// E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the 16 cap;
|
|
209
|
+
// the single-repo rule above is unchanged.
|
|
200
210
|
}
|
|
201
211
|
return null;
|
|
202
212
|
}
|
|
@@ -210,8 +220,8 @@ function localTreeValidationReason(config) {
|
|
|
210
220
|
if (config.execution?.target !== "e2b-desktop") {
|
|
211
221
|
return "local-tree subjects require `execution.target: e2b-desktop`: the packed working tree is provisioned and served inside a hosted desktop sandbox; there is no local route for a local-tree subject.";
|
|
212
222
|
}
|
|
213
|
-
if (!actorResolvesToComputerUse(config
|
|
214
|
-
return `
|
|
223
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
224
|
+
return `actor.type must be a registered computer-use actor for local-tree subjects (one of: ${registeredComputerUseActors().join(", ")}); the actor drives the hosted desktop that serves the packed working tree. Got "${actorOf(config)?.type ?? ""}".`;
|
|
215
225
|
}
|
|
216
226
|
}
|
|
217
227
|
return null;
|
|
@@ -238,7 +248,7 @@ function desktopCliValidationReason(config) {
|
|
|
238
248
|
if (config.execution?.target !== undefined && config.execution.target !== "e2b-desktop") {
|
|
239
249
|
return "A desktop-cli subject runs on a hosted desktop. Set `execution.target: e2b-desktop` or omit it.";
|
|
240
250
|
}
|
|
241
|
-
if (!actorResolvesToComputerUse(config
|
|
251
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type ?? "")) {
|
|
242
252
|
return "desktop-cli subjects need a registered computer-use actor: the participant reads the screen and types, which is what makes an interactive surface studiable at all.";
|
|
243
253
|
}
|
|
244
254
|
const install = config.subject.product.install;
|
|
@@ -255,15 +265,15 @@ function desktopCliValidationReason(config) {
|
|
|
255
265
|
// fan-out lands.
|
|
256
266
|
function terminalValidationReason(config) {
|
|
257
267
|
if (config.subject.source === "terminal-product") {
|
|
258
|
-
const type = config
|
|
268
|
+
const type = actorOf(config)?.type ?? "";
|
|
259
269
|
if (config.execution?.target !== undefined && config.execution.target !== "e2b-terminal") {
|
|
260
270
|
return "A terminal-product subject runs its agent in an E2B shell. Set `execution.target: e2b-terminal` or omit it; `local` and `e2b-desktop` are not supported.";
|
|
261
271
|
}
|
|
262
272
|
if (!actorResolvesToTerminal(type)) {
|
|
263
|
-
return `
|
|
273
|
+
return `actor.type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
|
|
264
274
|
}
|
|
265
|
-
if ((config
|
|
266
|
-
return "Terminal fan-out to more than one participant is not supported yet; set
|
|
275
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 1) {
|
|
276
|
+
return "Terminal fan-out to more than one participant is not supported yet; set participants to 1.";
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
else if (config.execution?.target === "e2b-terminal") {
|
|
@@ -271,7 +281,7 @@ function terminalValidationReason(config) {
|
|
|
271
281
|
// mis-config: reject, never silently mishandle (mirrors app-url's e2b-desktop pairing rule).
|
|
272
282
|
return "`execution.target: e2b-terminal` requires `subject.source: terminal-product` with a registered terminal actor.";
|
|
273
283
|
}
|
|
274
|
-
else if (actorResolvesToTerminal(config
|
|
284
|
+
else if (actorResolvesToTerminal(actorOf(config)?.type)) {
|
|
275
285
|
// A registered terminal actor on a non-terminal-product subject: rejected, never ignored (the
|
|
276
286
|
// terminal agent only studies a declared terminal-product from public surfaces).
|
|
277
287
|
return "terminal actors require `subject.source: terminal-product` (a CLI/product the agent studies from public surfaces); other subjects are not supported on this route.";
|
|
@@ -283,7 +293,7 @@ function terminalValidationReason(config) {
|
|
|
283
293
|
// Terminal actors were already refused above with their own message.
|
|
284
294
|
function cloneActorValidationReason(config) {
|
|
285
295
|
if (config.subject.source === "clone") {
|
|
286
|
-
const type = config
|
|
296
|
+
const type = actorOf(config)?.type ?? "";
|
|
287
297
|
if (!actorResolvesToComputerUse(type) && !actorResolvesToScriptedBrowser(type)) {
|
|
288
298
|
return `clone subjects need a registered computer-use actor (one of: ${registeredComputerUseActors().join(", ")}) or scripted-browser actor (one of: ${registeredScriptedBrowserActors().join(", ")}); humanish clones and serves the app for that participant to drive. Got "${type}".`;
|
|
289
299
|
}
|
package/dist/study/config.d.ts
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
|
-
import { type StudyParseResult } from "./types.js";
|
|
2
|
-
/** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
|
|
3
|
-
export declare const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
|
|
1
|
+
import { type StudyConfig, type StudyParseFailure, type StudyParseResult } from "./types.js";
|
|
4
2
|
/**
|
|
5
3
|
* The package's study parser (src/index.ts) and the one discovery uses: a humanish.study.v3
|
|
6
4
|
* document. It refuses a humanish.lab.v2 one with HUMANISH_STUDY_V2_UNSUPPORTED.
|
|
7
5
|
*/
|
|
8
6
|
export declare function parseStudy(raw: unknown): StudyParseResult;
|
|
7
|
+
interface StudyV3Options {
|
|
8
|
+
/**
|
|
9
|
+
* `report`, for `humanish migrate`: list the fields the route does not read in `inert`, in place
|
|
10
|
+
* of refusing the study for them. A shared world may then give `participants` as a count or
|
|
11
|
+
* leave it out, as a v2 file could (readStudyFront).
|
|
12
|
+
*/
|
|
13
|
+
readonly inert?: "refuse" | "report";
|
|
14
|
+
}
|
|
15
|
+
type StudyV3ParseResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
config: StudyConfig;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
inert?: string[];
|
|
20
|
+
} | StudyParseFailure;
|
|
9
21
|
/**
|
|
10
|
-
* Validate a
|
|
11
|
-
*
|
|
12
|
-
* accepted but reported in `warnings` so `study show` never silently swallows a setting that does
|
|
13
|
-
* nothing. migrate's conversion (convert.ts) calls it directly, since it reads v2 files.
|
|
22
|
+
* Validate a humanish.study.v3 document into a StudyConfig. Pure: the caller owns file IO. Fields the
|
|
23
|
+
* route does not read refuse the study; migrate's report mode lists them in `inert` instead.
|
|
14
24
|
*/
|
|
15
|
-
export declare function
|
|
25
|
+
export declare function parseStudyV3(raw: Record<string, unknown>, options?: StudyV3Options): StudyV3ParseResult;
|
|
26
|
+
/** Why runStudy refuses a config before anything runs. */
|
|
27
|
+
interface ConfigRefusal {
|
|
28
|
+
readonly code: "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID";
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Why runStudy refuses a config a library caller passes, or undefined: a humanish.lab.v2 config,
|
|
33
|
+
* one that still sets a StudyConfig field of humanish 0.110, one with no `actor` object, and one
|
|
34
|
+
* whose `route` is not the route its subject and actor take, with parseStudy's message. A planner
|
|
35
|
+
* reads only the v3 fields, so a budget left in `execution.caps` would otherwise run uncapped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function libraryConfigRefusal(config: StudyConfig): ConfigRefusal | undefined;
|
|
38
|
+
export {};
|