humanish 0.110.3 → 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.
- package/CHANGELOG.md +14 -0
- 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 +4 -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/plan.js +6 -5
- package/dist/routes/computer-use/route.js +2 -1
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +5 -1
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/shared-world/participant-specs.js +5 -1
- package/dist/routes/shared-world/plan.js +7 -6
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +2 -1
- package/dist/routes/terminal/session.js +1 -0
- 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/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/study/composition-rules.js +32 -32
- package/dist/study/config.d.ts +9 -2
- package/dist/study/config.js +14 -5
- package/dist/study/doctor.js +5 -4
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +29 -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 +68 -0
- package/dist/study/migrate/v2.js +480 -0
- package/dist/study/parse/actors.d.ts +46 -1
- package/dist/study/parse/actors.js +116 -69
- package/dist/study/parse/execution.js +2 -2
- package/dist/study/parse/study-v3.d.ts +26 -0
- package/dist/study/parse/study-v3.js +53 -35
- package/dist/study/parse/study.d.ts +38 -0
- package/dist/study/parse/study.js +225 -0
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +2 -1
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -4
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan.js +4 -3
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.js +2 -2
- package/dist/study/routing.js +12 -15
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-fields.d.ts +28 -0
- package/dist/study/study-fields.js +51 -0
- package/dist/study/summary.js +13 -17
- package/dist/study/types.d.ts +40 -0
- package/dist/study/url-credentials.js +2 -1
- package/dist/study/validation.d.ts +2 -0
- package/dist/study/validation.js +46 -41
- package/dist/study/warnings.js +5 -4
- package/dist/substrates/local/runtime-config.js +9 -9
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +13 -4
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/dist/run/display.d.ts
CHANGED
|
@@ -57,3 +57,13 @@ export declare function bundleDisplayFacts(bundle: DisplayedBundle, status?: unk
|
|
|
57
57
|
* execution failed never reads as a pass beside its participants' verdict.
|
|
58
58
|
*/
|
|
59
59
|
export declare function reviewOutcome(bundle: DisplayedBundle): string;
|
|
60
|
+
/**
|
|
61
|
+
* The summary and gaps a review shows beside its run's display. A run stopped while a participant
|
|
62
|
+
* was still running keeps the review its last live flush wrote, which describes that session as
|
|
63
|
+
* running. Once runDisplay reads the run as interrupted, the review says how many participants the
|
|
64
|
+
* stop cut off instead.
|
|
65
|
+
*/
|
|
66
|
+
export declare function displayedReview<T extends {
|
|
67
|
+
summary: string;
|
|
68
|
+
gaps: string[];
|
|
69
|
+
}>(review: T, bundle: DisplayedBundle, display: RunDisplay): T;
|
package/dist/run/display.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// same facts, so a run whose own ok is false shows as a pass on none of them, and two surfaces
|
|
4
4
|
// cannot disagree about one run.
|
|
5
5
|
import { classifyRunStatus, isRunStatusRecord } from "./status.js";
|
|
6
|
+
import { plural } from "./text.js";
|
|
6
7
|
const LABELS = {
|
|
7
8
|
running: "running",
|
|
8
9
|
interrupted: "interrupted",
|
|
@@ -104,3 +105,24 @@ export function bundleDisplayFacts(bundle, status, nowMs = Date.now()) {
|
|
|
104
105
|
export function reviewOutcome(bundle) {
|
|
105
106
|
return runDisplayLine(runDisplay(bundleDisplayFacts(bundle)));
|
|
106
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The summary and gaps a review shows beside its run's display. A run stopped while a participant
|
|
110
|
+
* was still running keeps the review its last live flush wrote, which describes that session as
|
|
111
|
+
* running. Once runDisplay reads the run as interrupted, the review says how many participants the
|
|
112
|
+
* stop cut off instead.
|
|
113
|
+
*/
|
|
114
|
+
export function displayedReview(review, bundle, display) {
|
|
115
|
+
const records = bundle.simulations ?? [];
|
|
116
|
+
const running = records.filter((record) => record?.status === "running").length;
|
|
117
|
+
if (display.state !== "interrupted" || running === 0)
|
|
118
|
+
return review;
|
|
119
|
+
const signal = bundle.outcome?.state === "interrupted" ? ` by ${bundle.outcome.signal}` : "";
|
|
120
|
+
const one = running === 1;
|
|
121
|
+
return {
|
|
122
|
+
...review,
|
|
123
|
+
summary: `Interrupted${signal} while ${running} of ${plural(records.length, "participant")} ${one ? "was" : "were"} still running.`,
|
|
124
|
+
gaps: [
|
|
125
|
+
`${plural(running, "participant")} ${one ? "has" : "have"} no verdict: the run stopped before ${one ? "its session" : "their sessions"} ended.`,
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -20,9 +20,10 @@ export interface ReclaimResult {
|
|
|
20
20
|
createsInFlight: number;
|
|
21
21
|
/**
|
|
22
22
|
* Why there was nothing to look for: `dry-run` when run.json or status.json records the run as
|
|
23
|
-
* a dry run, which creates no sandboxes
|
|
23
|
+
* a dry run, which creates no sandboxes, and `no-sandbox` when status.json records that the
|
|
24
|
+
* route created none for this run. Reclaim then contacts E2B for nothing.
|
|
24
25
|
*/
|
|
25
|
-
reason?:
|
|
26
|
+
reason?: NoSandboxReason;
|
|
26
27
|
warnings: string[];
|
|
27
28
|
error?: {
|
|
28
29
|
code: "HUMANISH_RECLAIM_RUN_NOT_FOUND" | "HUMANISH_RECLAIM_E2B_DEBUG" | "HUMANISH_RECLAIM_MODULE_UNAVAILABLE" | "HUMANISH_RECLAIM_RECEIPTS_UNREADABLE";
|
|
@@ -47,6 +48,7 @@ export declare function reclaimRunSandboxes(cwd: string, runInput: string, hooks
|
|
|
47
48
|
* also an alias (a run named `latest`) cannot reach another run's receipts.
|
|
48
49
|
*/
|
|
49
50
|
export declare function reclaimPinnedRunSandboxes(cwd: string, runPaths: PreparedRunArtifactPaths, hooks?: ReclaimHooks): Promise<ReclaimResult>;
|
|
51
|
+
type NoSandboxReason = "dry-run" | "no-sandbox";
|
|
50
52
|
/**
|
|
51
53
|
* Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
|
|
52
54
|
* .humanish/preflight/<probe-id>/ and removes the journal after a confirmed kill, so what is left
|
package/dist/run/reclaim.js
CHANGED
|
@@ -15,6 +15,7 @@ import { readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
|
15
15
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
16
16
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
17
17
|
import { isRecord } from "./type-guards.js";
|
|
18
|
+
import { isRunOutcome } from "./bundle-shape.js";
|
|
18
19
|
import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
19
20
|
import { sandboxOwnerTags, } from "./sandbox-creates.js";
|
|
20
21
|
import { earlierOutcomes, GONE, RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, receiptWriter, rootState, searchTagSets, settledWithin, targetSet, } from "./reclaim-outcomes.js";
|
|
@@ -64,8 +65,9 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
64
65
|
}
|
|
65
66
|
const runId = runIdOf(runPaths);
|
|
66
67
|
// The signal handler passes the creates it stopped and decides from what they reported.
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const reason = hooks.creates === undefined ? await recordedNoSandbox(runPaths) : undefined;
|
|
69
|
+
if (reason !== undefined)
|
|
70
|
+
return { ...base, ok: true, state: "clean", runId, reason };
|
|
69
71
|
const reclaimed = await reclaimRoot(runPaths, runId, hooks, warnings);
|
|
70
72
|
if (reclaimed.kind === "module-unavailable")
|
|
71
73
|
return {
|
|
@@ -93,29 +95,70 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
/**
|
|
96
|
-
*
|
|
97
|
-
* `dry-run`.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
98
|
+
* Why the run's own records say it created no sandbox. `dry-run`: run.json and status.json, each
|
|
99
|
+
* one present, say `dry-run`. `no-sandbox`: status.json, written at start, names this run and
|
|
100
|
+
* records `sandboxes: none`, which the route sets only when it has no way to create one (a
|
|
101
|
+
* scripted run against an app-url subject), and run.json agrees (scriptedAppUrlBundle). A status
|
|
102
|
+
* record alone can be stale or copied from another run, so it never decides on its own, and an
|
|
103
|
+
* empty journal alone never makes a run clean. A journal with any line in it, or an earlier
|
|
104
|
+
* reclaim receipt that names a sandbox, means a create ran, so that run is reclaimed as a live one
|
|
105
|
+
* whatever its records say.
|
|
100
106
|
*/
|
|
101
|
-
async function
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
async function recordedNoSandbox(runPaths) {
|
|
108
|
+
const bundle = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
|
|
109
|
+
const status = await readRunJsonIfExists(runPaths, RUN_STATUS_FILE);
|
|
110
|
+
const records = [bundle, status].filter(isRecord);
|
|
111
|
+
const reason = records.length > 0 && records.every((record) => record.mode === "dry-run")
|
|
112
|
+
? "dry-run"
|
|
113
|
+
: isRecord(status) &&
|
|
114
|
+
status.runId === runIdOf(runPaths) &&
|
|
115
|
+
status.sandboxes === "none" &&
|
|
116
|
+
scriptedAppUrlBundle(bundle, runIdOf(runPaths))
|
|
117
|
+
? "no-sandbox"
|
|
118
|
+
: undefined;
|
|
119
|
+
if (reason === undefined)
|
|
120
|
+
return undefined;
|
|
110
121
|
if (!(await containedPathAbsent(runPaths, RECLAIM_RECEIPT_ARTIFACT))) {
|
|
111
122
|
const earlier = await readRunJsonIfExists(runPaths, RECLAIM_RECEIPT_ARTIFACT);
|
|
112
123
|
if (!isRecord(earlier) || !Array.isArray(earlier.outcomes) || earlier.outcomes.length > 0)
|
|
113
|
-
return
|
|
124
|
+
return undefined;
|
|
114
125
|
}
|
|
115
126
|
if (await containedPathAbsent(runPaths, SANDBOX_RECEIPTS_ARTIFACT))
|
|
116
|
-
return
|
|
127
|
+
return reason;
|
|
117
128
|
const journal = await readContainedRegularFile(runPaths, SANDBOX_RECEIPTS_ARTIFACT);
|
|
118
|
-
return journal !== null && journal.toString("utf8").trim() === "";
|
|
129
|
+
return journal !== null && journal.toString("utf8").trim() === "" ? reason : undefined;
|
|
130
|
+
}
|
|
131
|
+
/** The execution failure kinds that leave a sandbox or a provider's resources unconfirmed. */
|
|
132
|
+
const CLEANUP_KINDS = new Set(["sandbox-cleanup", "provider-cleanup"]);
|
|
133
|
+
/**
|
|
134
|
+
* run.json for `runId` agrees that its run could create no sandbox: the scripted route wrote it
|
|
135
|
+
* (each lifecycle event is a `scripted-lab.` one), its subject is not a provisioned clone (a clone
|
|
136
|
+
* records `subject`), it lists no provider resource and no desktop minutes, and its outcome marks
|
|
137
|
+
* no sandbox or provider cleanup unconfirmed. A run.json that is missing, unreadable or says
|
|
138
|
+
* otherwise does not agree.
|
|
139
|
+
*/
|
|
140
|
+
function scriptedAppUrlBundle(bundle, runId) {
|
|
141
|
+
if (!isRecord(bundle) || bundle.runId !== runId || bundle.subject !== undefined)
|
|
142
|
+
return false;
|
|
143
|
+
const { lifecycle, providerResources, cost, outcome } = bundle;
|
|
144
|
+
const scripted = Array.isArray(lifecycle) &&
|
|
145
|
+
lifecycle.length > 0 &&
|
|
146
|
+
lifecycle.every((entry) => isRecord(entry) &&
|
|
147
|
+
typeof entry.event === "string" &&
|
|
148
|
+
entry.event.startsWith("scripted-lab."));
|
|
149
|
+
if (!scripted)
|
|
150
|
+
return false;
|
|
151
|
+
if (providerResources !== undefined &&
|
|
152
|
+
!(Array.isArray(providerResources) && providerResources.length === 0))
|
|
153
|
+
return false;
|
|
154
|
+
if (cost !== undefined && !(isRecord(cost) && cost.desktopMinutes === null))
|
|
155
|
+
return false;
|
|
156
|
+
if (!isRunOutcome(outcome))
|
|
157
|
+
return false;
|
|
158
|
+
if (outcome.state !== "finished")
|
|
159
|
+
return true;
|
|
160
|
+
const { failures, warnings = [] } = outcome.execution;
|
|
161
|
+
return ![...failures, ...warnings].some((failure) => CLEANUP_KINDS.has(failure.kind));
|
|
119
162
|
}
|
|
120
163
|
/**
|
|
121
164
|
* Kill the sandboxes left by interrupted `humanish study check` probes. A probe journals under
|
package/dist/run/run.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface StartRunOptions {
|
|
|
11
11
|
mintRunId: () => string;
|
|
12
12
|
mode: "dry-run" | "live";
|
|
13
13
|
study?: RunStudyProvenance | undefined;
|
|
14
|
+
/** `none` when the route creates no sandbox for this run; status.json records it for reclaim. */
|
|
15
|
+
sandboxes?: "none" | undefined;
|
|
14
16
|
/** review.md for the published bundle. */
|
|
15
17
|
renderReview: (bundle: RunBundle) => string;
|
|
16
18
|
/** Used by `FinishedRun.renderObserver`; `render` is the `StudyDeps.renderObserver` seam. */
|
|
@@ -52,6 +54,12 @@ interface Run {
|
|
|
52
54
|
* can never overwrite the final bundle. Routes that throttle snapshots own their timers.
|
|
53
55
|
*/
|
|
54
56
|
writeSnapshot(bundle: RunBundle): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* A participant's session started: its actor began to act on the subject. FinishedRun's
|
|
59
|
+
* participantsRan reads this, so a session that throws before it returns a trace still counts,
|
|
60
|
+
* and a run that failed before any session (a refused E2B key) does not.
|
|
61
|
+
*/
|
|
62
|
+
participantStarted(): void;
|
|
55
63
|
/**
|
|
56
64
|
* The one final publication: run.json with `outcome` set from `outcome`, then the status
|
|
57
65
|
* outcome copied from it, then review.json, review.md, events.ndjson,
|
|
@@ -92,12 +100,14 @@ export declare class FinishedRun {
|
|
|
92
100
|
readonly runId: string;
|
|
93
101
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
94
102
|
readonly paths: PreparedRunArtifactPaths;
|
|
95
|
-
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean);
|
|
103
|
+
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, interrupted: () => boolean, participantsRan: () => boolean);
|
|
96
104
|
/**
|
|
97
105
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
98
106
|
* when asked, since the signal can arrive after the bundle was published.
|
|
99
107
|
*/
|
|
100
108
|
get interrupted(): boolean;
|
|
109
|
+
/** The route reported a participant's session started (Run.participantStarted). */
|
|
110
|
+
get participantsRan(): boolean;
|
|
101
111
|
static isIssued(value: unknown): value is FinishedRun;
|
|
102
112
|
/**
|
|
103
113
|
* The run's ok and execution outcome as run.json records them: what the route passed to
|
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
|
}
|
|
@@ -10,7 +10,7 @@ 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
14
|
/** The first composition rule a config breaks, in the parser's order, or null. */
|
|
15
15
|
export function compositionReason(config) {
|
|
16
16
|
return (thisRepoValidationReason(config) ??
|
|
@@ -25,7 +25,7 @@ export function compositionReason(config) {
|
|
|
25
25
|
(isComputerUseComposition(config) ? computerUseValidationReason(config) : null) ??
|
|
26
26
|
// Whenever shared world is declared, not only when it routes, so a half-declared shared world
|
|
27
27
|
// fails with a precise reason instead of running as independent participants.
|
|
28
|
-
(config
|
|
28
|
+
(declaresSharedWorld(config) ? sharedWorldValidationReason(config) : null) ??
|
|
29
29
|
desktopCliValidationReason(config) ??
|
|
30
30
|
terminalValidationReason(config) ??
|
|
31
31
|
cloneActorValidationReason(config));
|
|
@@ -39,7 +39,7 @@ function thisRepoValidationReason(config) {
|
|
|
39
39
|
if (config.execution?.target) {
|
|
40
40
|
return "`execution.target` applies only to clone/app-url/local-app subjects; this-repo studies run locally.";
|
|
41
41
|
}
|
|
42
|
-
if (config
|
|
42
|
+
if (modeOf(config) === "live") {
|
|
43
43
|
return THIS_REPO_DRY_RUN_ONLY;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -54,18 +54,18 @@ function thisRepoValidationReason(config) {
|
|
|
54
54
|
// planComputerUseStudy refuses it with HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR.
|
|
55
55
|
function localAppValidationReason(config) {
|
|
56
56
|
if (config.subject.source === "local-app") {
|
|
57
|
-
const type = config
|
|
57
|
+
const type = actorOf(config)?.type ?? "";
|
|
58
58
|
if (config.execution?.target !== undefined && config.execution.target !== "local") {
|
|
59
59
|
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
60
|
}
|
|
61
61
|
if (!actorResolvesToComputerUse(type)) {
|
|
62
|
-
return `
|
|
62
|
+
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
63
|
}
|
|
64
64
|
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. Set
|
|
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. Set participants to 1 and drop participants (use an app-url or clone subject on execution.target: e2b-desktop for fan-out).";
|
|
66
66
|
}
|
|
67
|
-
if (
|
|
68
|
-
return "`
|
|
67
|
+
if (participantList(config) !== undefined) {
|
|
68
|
+
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
69
|
}
|
|
70
70
|
if (config.policies?.allowPublicTargets === true) {
|
|
71
71
|
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 +78,18 @@ function localAppValidationReason(config) {
|
|
|
78
78
|
// app; a computer-use actor drives a hosted desktop browser. Fail closed on mis-configs.
|
|
79
79
|
function appUrlValidationReason(config) {
|
|
80
80
|
if (config.subject.source === "app-url") {
|
|
81
|
-
const type = config
|
|
81
|
+
const type = actorOf(config)?.type ?? "";
|
|
82
82
|
if (actorResolvesToScriptedBrowser(type)) {
|
|
83
83
|
// Scripted-browser route (all fail-closed, so claims match mechanism: a field that cannot act on
|
|
84
84
|
// this route is rejected, never silently ignored).
|
|
85
85
|
if (config.execution?.target !== undefined && config.execution.target !== "local") {
|
|
86
86
|
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
87
|
}
|
|
88
|
-
if (!config
|
|
89
|
-
return "A scripted-browser study needs `scenario
|
|
88
|
+
if (!scenarioRefOf(config)) {
|
|
89
|
+
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
90
90
|
}
|
|
91
|
-
if ((config
|
|
92
|
-
return "A scripted-browser study takes `
|
|
91
|
+
if ((surfaceCount(config) ?? 1) > 2) {
|
|
92
|
+
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
93
93
|
}
|
|
94
94
|
if (config.policies?.redactScreenshots === true) {
|
|
95
95
|
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,7 +106,7 @@ function appUrlValidationReason(config) {
|
|
|
106
106
|
return "app-url computer-use subjects require `execution.target: local` or `e2b-desktop`.";
|
|
107
107
|
}
|
|
108
108
|
if (!actorResolvesToComputerUse(type)) {
|
|
109
|
-
return `
|
|
109
|
+
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
110
|
}
|
|
111
111
|
// Multi-participant fan-out is consumed on this route (`per-lane-worlds`; the shared cua
|
|
112
112
|
// cross-validation below enforces `lanes`/`count` XOR rules, the 16 cap, and the
|
|
@@ -116,7 +116,7 @@ function appUrlValidationReason(config) {
|
|
|
116
116
|
const targets = [config.subject.appUrl ?? "", ...declaredTargets(config)];
|
|
117
117
|
const unsafeTarget = targets.find((target) => !config.policies?.allowPublicTargets && !isLoopbackUrl(target));
|
|
118
118
|
if (unsafeTarget !== undefined) {
|
|
119
|
-
return "`subject.appUrl` and `
|
|
119
|
+
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
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}
|
|
@@ -125,7 +125,7 @@ function appUrlValidationReason(config) {
|
|
|
125
125
|
// Scripted-browser actors on any other subject: only a provisioned clone on e2b-desktop.
|
|
126
126
|
function scriptedBrowserValidationReason(config) {
|
|
127
127
|
if (config.subject.source !== "app-url" &&
|
|
128
|
-
actorResolvesToScriptedBrowser(config
|
|
128
|
+
actorResolvesToScriptedBrowser(actorOf(config)?.type)) {
|
|
129
129
|
if (config.subject.source !== "clone") {
|
|
130
130
|
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
131
|
}
|
|
@@ -148,14 +148,14 @@ function scriptedBrowserValidationReason(config) {
|
|
|
148
148
|
if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
|
|
149
149
|
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
150
|
}
|
|
151
|
-
if (!config
|
|
152
|
-
return "A scripted-browser study needs `scenario
|
|
151
|
+
if (!scenarioRefOf(config)) {
|
|
152
|
+
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
153
153
|
}
|
|
154
|
-
if ((config
|
|
155
|
-
return "A scripted-browser study takes `
|
|
154
|
+
if ((surfaceCount(config) ?? 1) > 2) {
|
|
155
|
+
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
156
156
|
}
|
|
157
|
-
if (
|
|
158
|
-
return "`
|
|
157
|
+
if (participantList(config) !== undefined) {
|
|
158
|
+
return "`participants` is not supported on the scripted-browser route yet. Use `surfaces` to choose the desktop and mobile surfaces.";
|
|
159
159
|
}
|
|
160
160
|
if (config.policies?.redactScreenshots === true) {
|
|
161
161
|
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 +183,7 @@ function scriptedBrowserValidationReason(config) {
|
|
|
183
183
|
function cloneComputerUseValidationReason(config) {
|
|
184
184
|
if (config.subject.source === "clone" &&
|
|
185
185
|
config.execution?.target === "e2b-desktop" &&
|
|
186
|
-
actorResolvesToComputerUse(config
|
|
186
|
+
actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
187
187
|
if (!config.subject.serve) {
|
|
188
188
|
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
189
|
}
|
|
@@ -210,8 +210,8 @@ function localTreeValidationReason(config) {
|
|
|
210
210
|
if (config.execution?.target !== "e2b-desktop") {
|
|
211
211
|
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
212
|
}
|
|
213
|
-
if (!actorResolvesToComputerUse(config
|
|
214
|
-
return `
|
|
213
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type)) {
|
|
214
|
+
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
215
|
}
|
|
216
216
|
}
|
|
217
217
|
return null;
|
|
@@ -238,7 +238,7 @@ function desktopCliValidationReason(config) {
|
|
|
238
238
|
if (config.execution?.target !== undefined && config.execution.target !== "e2b-desktop") {
|
|
239
239
|
return "A desktop-cli subject runs on a hosted desktop. Set `execution.target: e2b-desktop` or omit it.";
|
|
240
240
|
}
|
|
241
|
-
if (!actorResolvesToComputerUse(config
|
|
241
|
+
if (!actorResolvesToComputerUse(actorOf(config)?.type ?? "")) {
|
|
242
242
|
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
243
|
}
|
|
244
244
|
const install = config.subject.product.install;
|
|
@@ -255,15 +255,15 @@ function desktopCliValidationReason(config) {
|
|
|
255
255
|
// fan-out lands.
|
|
256
256
|
function terminalValidationReason(config) {
|
|
257
257
|
if (config.subject.source === "terminal-product") {
|
|
258
|
-
const type = config
|
|
258
|
+
const type = actorOf(config)?.type ?? "";
|
|
259
259
|
if (config.execution?.target !== undefined && config.execution.target !== "e2b-terminal") {
|
|
260
260
|
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
261
|
}
|
|
262
262
|
if (!actorResolvesToTerminal(type)) {
|
|
263
|
-
return `
|
|
263
|
+
return `actor.type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
|
|
264
264
|
}
|
|
265
|
-
if ((config
|
|
266
|
-
return "Terminal fan-out to more than one participant is not supported yet; set
|
|
265
|
+
if ((declaredParticipantCount(config) ?? 1) > 1) {
|
|
266
|
+
return "Terminal fan-out to more than one participant is not supported yet; set participants to 1.";
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
else if (config.execution?.target === "e2b-terminal") {
|
|
@@ -271,7 +271,7 @@ function terminalValidationReason(config) {
|
|
|
271
271
|
// mis-config: reject, never silently mishandle (mirrors app-url's e2b-desktop pairing rule).
|
|
272
272
|
return "`execution.target: e2b-terminal` requires `subject.source: terminal-product` with a registered terminal actor.";
|
|
273
273
|
}
|
|
274
|
-
else if (actorResolvesToTerminal(config
|
|
274
|
+
else if (actorResolvesToTerminal(actorOf(config)?.type)) {
|
|
275
275
|
// A registered terminal actor on a non-terminal-product subject: rejected, never ignored (the
|
|
276
276
|
// terminal agent only studies a declared terminal-product from public surfaces).
|
|
277
277
|
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 +283,7 @@ function terminalValidationReason(config) {
|
|
|
283
283
|
// Terminal actors were already refused above with their own message.
|
|
284
284
|
function cloneActorValidationReason(config) {
|
|
285
285
|
if (config.subject.source === "clone") {
|
|
286
|
-
const type = config
|
|
286
|
+
const type = actorOf(config)?.type ?? "";
|
|
287
287
|
if (!actorResolvesToComputerUse(type) && !actorResolvesToScriptedBrowser(type)) {
|
|
288
288
|
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
289
|
}
|
package/dist/study/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type StudyParseResult } from "./types.js";
|
|
1
|
+
import { type StudyConfig, type StudyParseResult } from "./types.js";
|
|
2
2
|
/** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
|
|
3
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>.";
|
|
4
4
|
/**
|
|
@@ -10,6 +10,13 @@ export declare function parseStudy(raw: unknown): StudyParseResult;
|
|
|
10
10
|
* Validate a parsed YAML object, humanish.study.v3 or humanish.lab.v2, into a StudyConfig. Pure:
|
|
11
11
|
* the caller owns file IO. Structural validation only. Fields the engine does not yet consume are
|
|
12
12
|
* accepted but reported in `warnings` so `study show` never silently swallows a setting that does
|
|
13
|
-
* nothing.
|
|
13
|
+
* nothing. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
|
|
14
14
|
*/
|
|
15
15
|
export declare function parseStudyDocument(raw: unknown): StudyParseResult;
|
|
16
|
+
/**
|
|
17
|
+
* The checks that read more than one section of a parsed config, and the defaults they fill:
|
|
18
|
+
* shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
|
|
19
|
+
* parseV2 runs them once every section has parsed. parse/study.ts runs them on its v2 view of a v3
|
|
20
|
+
* study.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkStudyConfig(config: StudyConfig): StudyParseResult;
|