humanish 0.112.0 → 0.113.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 +19 -0
- package/README.md +11 -0
- package/TELEMETRY.md +21 -0
- package/dist/actors/codex/restricted-participant-policy.d.ts +21 -1
- package/dist/actors/codex/restricted-participant-policy.js +20 -2
- package/dist/actors/codex/restricted-participant.d.ts +1 -1
- package/dist/actors/codex/restricted-participant.js +2 -1
- package/dist/actors/computer-use/loop/debrief.d.ts +12 -4
- package/dist/actors/computer-use/loop/debrief.js +57 -20
- package/dist/actors/computer-use/loop/ending.d.ts +5 -2
- package/dist/actors/computer-use/loop/impressions.d.ts +14 -0
- package/dist/actors/computer-use/loop/impressions.js +40 -0
- package/dist/actors/computer-use/loop/session.d.ts +3 -1
- package/dist/actors/computer-use/loop/session.js +2 -0
- package/dist/actors/computer-use/loop/trace.js +2 -0
- package/dist/actors/computer-use/loop/types.d.ts +8 -1
- package/dist/actors/computer-use/loop.d.ts +2 -1
- package/dist/actors/computer-use/loop.js +15 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +9 -4
- package/dist/actors/computer-use/openai-conversation.js +53 -24
- package/dist/actors/computer-use/openai-provider.js +18 -12
- package/dist/actors/contract.d.ts +43 -3
- package/dist/actors/contract.js +13 -0
- package/dist/analysis/evidence-sources.d.ts +3 -1
- package/dist/analysis/evidence-sources.js +10 -1
- package/dist/analysis/evidence.js +2 -2
- package/dist/analysis/execute.d.ts +1 -1
- package/dist/analysis/execute.js +3 -1
- package/dist/cli/commands/runs.js +33 -10
- package/dist/cli/commands/setup.js +95 -6
- package/dist/cli/commands/study-format.js +7 -4
- package/dist/cli/commands/study.js +1 -1
- package/dist/cli/commands/tui.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +18 -0
- package/dist/cli/io.d.ts +2 -0
- package/dist/cli/program.js +35 -1
- package/dist/cli/secret-prompt.d.ts +5 -1
- package/dist/cli/secret-prompt.js +6 -2
- package/dist/cli/update-check-worker.d.ts +1 -0
- package/dist/cli/update-check-worker.js +5 -0
- package/dist/cli/update-check.d.ts +46 -0
- package/dist/cli/update-check.js +203 -0
- package/dist/feedback/draft.js +18 -0
- package/dist/keys/key-resolution.js +2 -0
- package/dist/keys/key-status.d.ts +37 -0
- package/dist/keys/key-status.js +68 -0
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/local-vm.js +9 -2
- package/dist/routes/computer-use/participant-desktop.d.ts +6 -0
- package/dist/routes/computer-use/participant-execution.js +9 -1
- package/dist/routes/computer-use/rerun-selection.js +23 -21
- package/dist/routes/computer-use/result.js +3 -2
- package/dist/routes/computer-use/self-report.js +7 -1
- package/dist/routes/computer-use/setup.js +14 -0
- package/dist/routes/computer-use/types.d.ts +4 -1
- package/dist/routes/shared-world/bundle-records.js +8 -7
- package/dist/routes/shared-world/bundle.js +1 -1
- package/dist/run/detail.js +4 -4
- package/dist/run/participant-caption.d.ts +2 -1
- package/dist/run/participant-caption.js +11 -1
- package/dist/run/participant-records.d.ts +3 -1
- package/dist/run/participant-records.js +6 -0
- package/dist/run-study.js +17 -1
- package/dist/study/discover.js +6 -1
- package/dist/study/doctor.d.ts +2 -0
- package/dist/study/doctor.js +45 -7
- package/dist/study/init-templates.js +10 -5
- package/dist/study/plan.js +3 -2
- package/dist/study/preflight-result.d.ts +1 -0
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/preflight.d.ts +2 -0
- package/dist/study/preflight.js +54 -14
- package/dist/study/warnings.d.ts +5 -0
- package/dist/study/warnings.js +79 -2
- package/dist/substrates/local/capacity.d.ts +75 -0
- package/dist/substrates/local/capacity.js +143 -0
- package/dist/substrates/local/firecracker-desktop.d.ts +8 -0
- package/dist/substrates/local/firecracker-desktop.js +34 -3
- package/dist/substrates/local/runtime-host.d.ts +16 -3
- package/dist/substrates/local/runtime-host.js +49 -4
- package/dist/substrates/local/runtime.d.ts +10 -0
- package/dist/substrates/local/runtime.js +44 -5
- package/dist/tui-app.js +58 -58
- package/docs/architecture/actor-contract.md +20 -6
- package/docs/contracts/feedback.md +8 -1
- package/docs/contracts/schemas.md +57 -34
- package/docs/contracts/study-analysis.md +15 -0
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +21 -10
|
@@ -3,8 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { inboxRecipientFor, } from "./participant-desktop.js";
|
|
4
4
|
import { createLocalFirecrackerDesktop, } from "../../substrates/local/firecracker-desktop.js";
|
|
5
5
|
import { localBrowserDefaults, localBrowserUnsupportedReason, } from "../../substrates/local/runtime-config.js";
|
|
6
|
-
import { prepareLocalRuntime } from "../../substrates/local/runtime.js";
|
|
7
|
-
import { dockerCommandLine } from "../../substrates/local/runtime-host.js";
|
|
6
|
+
import { localRuntimeCapacity, prepareLocalRuntime } from "../../substrates/local/runtime.js";
|
|
7
|
+
import { dockerCommandLine, usesLima } from "../../substrates/local/runtime-host.js";
|
|
8
|
+
import { outOfMemoryMessage } from "../../substrates/local/capacity.js";
|
|
8
9
|
import { checkRestrictedCodexAnalysisReadiness } from "../../analysis/restricted-codex.js";
|
|
9
10
|
import { createRestrictedCodexParticipant } from "../../actors/codex/restricted-participant.js";
|
|
10
11
|
import { withCloseReport } from "./participant-model.js";
|
|
@@ -110,6 +111,11 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
|
|
|
110
111
|
warnings.push("Desktop video/audio recording could not be retained. Screenshots and participant evidence remain available.");
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
// Asked before close, which removes the container; the answer comes from Docker's events.
|
|
115
|
+
if ((await session.killedForMemory?.()) === true) {
|
|
116
|
+
evidence.failureCode = "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY";
|
|
117
|
+
evidence.desktopFailure = outOfMemoryMessage(usesLima() ? "lima-vm" : "linux-host");
|
|
118
|
+
}
|
|
113
119
|
try {
|
|
114
120
|
evidence.released = (await session.close()).status === "released";
|
|
115
121
|
}
|
|
@@ -180,6 +186,7 @@ export function prepareLocalVmRun(options) {
|
|
|
180
186
|
options: account ? { ...runOptions, createProvider: accountProvider(state) } : runOptions,
|
|
181
187
|
localVm: {
|
|
182
188
|
desktop: (run, warnings, artifactRoot) => createLocalParticipantDesktop(context, run, warnings, artifactRoot),
|
|
189
|
+
capacity: () => localRuntimeCapacity(),
|
|
183
190
|
analysisRefusal: () => state.cleanupUnconfirmed ? "AUTOMATIC_ANALYSIS_CLEANUP_UNCONFIRMED" : undefined,
|
|
184
191
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
185
192
|
},
|
|
@@ -16,6 +16,12 @@ export type ParticipantDesktopEvidence = {
|
|
|
16
16
|
/** The substrate confirmed the desktop was released (an E2B kill or a local VM shutdown). The participant
|
|
17
17
|
* outcome records it as `killed`. */
|
|
18
18
|
released: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Why the desktop itself ended the session, in words a person reads, when the substrate knows:
|
|
21
|
+
* a local desktop Docker killed for memory. It replaces the session's error for a participant
|
|
22
|
+
* who did not pass.
|
|
23
|
+
*/
|
|
24
|
+
desktopFailure?: string;
|
|
19
25
|
} & Pick<ParticipantRunOutcome, "sandboxId" | "sandboxRelease" | "desktopDurationMs" | "desktopResources" | "streamUrlPresent" | "subjectCommit" | "desktopBrowser" | "desktopGeometry" | "stateStepRecords" | "phaseRecords" | "failureCode" | "commsArtifactPath" | "recording">;
|
|
20
26
|
/**
|
|
21
27
|
* The desktop one participant runs on, owned from preparation through final evidence collection.
|
|
@@ -107,6 +107,15 @@ export async function runCuaParticipant(spec, deps) {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
const { released, desktopFailure, ...desktopEvidence } = desktop.snapshot();
|
|
111
|
+
// A desktop that died under the participant explains the failure better than the transport
|
|
112
|
+
// error the session saw. A session that passed keeps its result.
|
|
113
|
+
if (desktopFailure !== undefined) {
|
|
114
|
+
if (session?.status === "passed")
|
|
115
|
+
warnings.push(desktopFailure);
|
|
116
|
+
else
|
|
117
|
+
sessionError = desktopFailure;
|
|
118
|
+
}
|
|
110
119
|
if (session)
|
|
111
120
|
await recordParticipantTrace(spec, deps, session, warnings);
|
|
112
121
|
const { noEngagement, selfReportedBlocker, reportedFriction } = judgeParticipantSession(session, deps, warnings);
|
|
@@ -116,7 +125,6 @@ export async function runCuaParticipant(spec, deps) {
|
|
|
116
125
|
providerCleanupError !== undefined ||
|
|
117
126
|
providerPolicyError !== undefined ||
|
|
118
127
|
session?.completionReason === "harness_error";
|
|
119
|
-
const { released, ...desktopEvidence } = desktop.snapshot();
|
|
120
128
|
return {
|
|
121
129
|
spec,
|
|
122
130
|
...(session ? { session } : {}),
|
|
@@ -4,42 +4,44 @@ import {} from "../../run/bundle.js";
|
|
|
4
4
|
import { loadRunBundle } from "../../run/locate.js";
|
|
5
5
|
import {} from "../../run/streams.js";
|
|
6
6
|
import { plural } from "../../run/text.js";
|
|
7
|
+
import { access } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { cli } from "../../cli/invocation.js";
|
|
7
10
|
export async function resolveCuaRerunSelection(args) {
|
|
8
|
-
const
|
|
11
|
+
const { studyId, sourceRunId } = args;
|
|
12
|
+
// Every refusal ends with the command that runs the whole study, which is what a user who
|
|
13
|
+
// reached for a rerun too early wants.
|
|
14
|
+
const instead = `To run every participant of ${studyId} instead, leave out --rerun-failed-from: ${cli(`watch ${studyId}`)} or ${cli(`run ${studyId}`)}.`;
|
|
15
|
+
const refuse = (what) => ({ ok: false, message: `${what} ${instead}` });
|
|
16
|
+
const source = await loadRunBundle(args.cwd, sourceRunId);
|
|
9
17
|
if (!source) {
|
|
10
|
-
|
|
18
|
+
const noRuns = sourceRunId === "latest" &&
|
|
19
|
+
!(await access(path.join(args.cwd, ".humanish", "runs", "latest.json")).then(() => true, () => false));
|
|
20
|
+
return refuse(noRuns
|
|
21
|
+
? "--rerun-failed-from latest found no run: this project has no runs yet, and a rerun repeats the failed participants of an earlier live run."
|
|
22
|
+
: `--rerun-failed-from ${sourceRunId} names no run humanish can read in this project. ${cli("runs")} lists its runs.`);
|
|
11
23
|
}
|
|
12
24
|
const bundle = source.bundle;
|
|
25
|
+
const run = `Run ${bundle.runId}${sourceRunId === bundle.runId ? "" : ` (the ${sourceRunId} run)`}`;
|
|
26
|
+
const notFanout = `${run} is not a live computer-use run with more than one participant, so it has no participants to rerun.`;
|
|
13
27
|
if (bundle.mode !== "live") {
|
|
14
|
-
return {
|
|
15
|
-
ok: false,
|
|
16
|
-
message: `source run ${bundle.runId} is ${bundle.mode}; rerun selection only applies to live CUA fan-out evidence.`,
|
|
17
|
-
};
|
|
28
|
+
return refuse(`${run} is a dry run: no participant ran in it, so none can be rerun. A rerun repeats the failed participants of a live run with more than one participant.`);
|
|
18
29
|
}
|
|
19
30
|
const fanoutEvent = bundle.events.some((event) => event.type === "cua-lab.fanout.plan");
|
|
20
|
-
if (!fanoutEvent || bundle.streams.length < 2)
|
|
21
|
-
return
|
|
22
|
-
}
|
|
31
|
+
if (!fanoutEvent || bundle.streams.length < 2)
|
|
32
|
+
return refuse(notFanout);
|
|
23
33
|
const prior = bundle.streams
|
|
24
34
|
.map(snapshotPriorParticipant)
|
|
25
35
|
.filter((entry) => entry !== null);
|
|
26
36
|
const priorById = new Map(prior.map((entry) => [entry.participantId, entry]));
|
|
27
|
-
if (priorById.size < 2)
|
|
28
|
-
return
|
|
29
|
-
ok: false,
|
|
30
|
-
message: `source run ${bundle.runId} does not expose multiple participant ids.`,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
37
|
+
if (priorById.size < 2)
|
|
38
|
+
return refuse(notFanout);
|
|
33
39
|
const explicitIds = uniqueIds(args.participantIds ?? []);
|
|
34
40
|
const selectedIds = explicitIds.length > 0
|
|
35
41
|
? explicitIds
|
|
36
42
|
: prior.filter((entry) => entry.rerunnable).map((entry) => entry.participantId);
|
|
37
|
-
if (selectedIds.length === 0)
|
|
38
|
-
return {
|
|
39
|
-
ok: false,
|
|
40
|
-
message: `source run ${bundle.runId} has no participant to rerun: none failed, was blocked, timed out or ended without engaging.`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
+
if (selectedIds.length === 0)
|
|
44
|
+
return refuse(`${run} has no participant to rerun: none failed, was blocked, timed out or ended without engaging.`);
|
|
43
45
|
const missingPrior = selectedIds.filter((id) => !priorById.has(id));
|
|
44
46
|
if (missingPrior.length > 0) {
|
|
45
47
|
return {
|
|
@@ -98,8 +98,9 @@ function cuaStudyResult(args) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
const failing = (outcomes ?? []).find((outcome) => !participantOk(outcome));
|
|
101
|
-
|
|
102
|
-
const
|
|
101
|
+
// A participant whose desktop failed for a known cause gives the run that cause's code.
|
|
102
|
+
const codedFailure = (outcomes ?? []).find((outcome) => outcome.failureCode !== undefined);
|
|
103
|
+
const code = codedFailure?.failureCode ?? "HUMANISH_COMPUTER_USE_FAILED";
|
|
103
104
|
return {
|
|
104
105
|
code,
|
|
105
106
|
message: observer.ok
|
|
@@ -200,8 +200,14 @@ export function resolveSelfReportedFriction(session) {
|
|
|
200
200
|
// reached the goal and described what was hard on the way has reported friction.
|
|
201
201
|
if (session.trace.declaredOutcome === "blocked")
|
|
202
202
|
return session.reason;
|
|
203
|
+
// Impressions are opinions about the product, not reports of what went wrong in the session.
|
|
204
|
+
const impressions = new Set(session.trace.impressions?.status === "collected"
|
|
205
|
+
? session.trace.impressions.items.map((item) => item.messageId)
|
|
206
|
+
: []);
|
|
203
207
|
const messages = session.trace.items
|
|
204
|
-
.filter((item) => item.kind === "message" &&
|
|
208
|
+
.filter((item) => item.kind === "message" &&
|
|
209
|
+
item.id !== session.trace.debrief?.messageId &&
|
|
210
|
+
!impressions.has(item.id))
|
|
205
211
|
.map((item) => item.text?.trim() ?? "")
|
|
206
212
|
.filter((text) => text.length > 0);
|
|
207
213
|
// A custom session may keep its closing report only in reason even when earlier messages exist.
|
|
@@ -22,6 +22,7 @@ import {} from "./bundle.js";
|
|
|
22
22
|
import { packRunLocalTree } from "./local-tree-pack.js";
|
|
23
23
|
import { projectParticipantSubjects, subjectProvenanceArg } from "./subject-projection.js";
|
|
24
24
|
import { participantSubjectEnv, } from "./types.js";
|
|
25
|
+
import { capacityShortfall } from "../../substrates/local/capacity.js";
|
|
25
26
|
import { studyPersonaIds } from "../../study/persona-resolve.js";
|
|
26
27
|
import { refusedResult } from "../../run/study-result.js";
|
|
27
28
|
/** The physical project, bound before any caller hook runs. */
|
|
@@ -145,6 +146,11 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
145
146
|
});
|
|
146
147
|
if (rejection)
|
|
147
148
|
return refuse(rejection.code, rejection.message, descriptor.id);
|
|
149
|
+
const shortfall = await localCapacityShortfall(input.localVm, participantPlan.concurrency);
|
|
150
|
+
if (shortfall !== undefined && "refusal" in shortfall)
|
|
151
|
+
return refuse("HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED", shortfall.refusal, descriptor.id);
|
|
152
|
+
if (shortfall !== undefined)
|
|
153
|
+
process.stderr.write(`warning: ${shortfall.warning}\n`);
|
|
148
154
|
}
|
|
149
155
|
// Pack the working tree once per run, on the host, before any sandbox or provider call: every
|
|
150
156
|
// fan-out participant below uploads this same archive, so one archiveSha256 describes every
|
|
@@ -192,6 +198,14 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
192
198
|
},
|
|
193
199
|
};
|
|
194
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Whether a local study's desktops at once fit the local runtime. Read before any desktop: a
|
|
203
|
+
* desktop past the VM's memory is killed mid-session, after its participant has spent.
|
|
204
|
+
*/
|
|
205
|
+
async function localCapacityShortfall(localVm, desktops) {
|
|
206
|
+
const capacity = await localVm?.capacity?.();
|
|
207
|
+
return capacity === undefined ? undefined : capacityShortfall(capacity, desktops);
|
|
208
|
+
}
|
|
195
209
|
/** Starts the run and builds what the participants and the finish read: the deps and the bundle base. */
|
|
196
210
|
export async function startCuaRun(plan, input, admitted, scope) {
|
|
197
211
|
const { cwd, descriptor, participantRuns, participantPlan, publicRepo } = admitted;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Verdict } from "../../run/judge.js";
|
|
2
|
+
import type { LocalCapacity } from "../../substrates/local/capacity.js";
|
|
2
3
|
import type { RunDesktopRecording } from "../../evidence/desktop-recording-types.js";
|
|
3
4
|
import type { CuaExecutor, CuaLiveMetadata, CuaLoopResult, CuaProvider } from "../../actors/computer-use/loop.js";
|
|
4
5
|
import type { CommsReceivingRun } from "../../comms/receiving.js";
|
|
@@ -38,6 +39,8 @@ export declare const MIN_DERIVED_SESSION_TIMEOUT_MS: number;
|
|
|
38
39
|
*/
|
|
39
40
|
export interface LocalVmInput {
|
|
40
41
|
readonly desktop: ParticipantDesktopFactory;
|
|
42
|
+
/** How many desktops the local runtime holds, or undefined when it cannot be read. */
|
|
43
|
+
readonly capacity?: () => Promise<LocalCapacity | undefined>;
|
|
41
44
|
/** The skip reason automatic analysis records instead of running, or undefined to run it. */
|
|
42
45
|
readonly analysisRefusal: () => AutomaticAnalysisRefusal | undefined;
|
|
43
46
|
readonly signal?: AbortSignal;
|
|
@@ -182,7 +185,7 @@ export interface CuaParticipantSummary {
|
|
|
182
185
|
concurrency: number;
|
|
183
186
|
waves: number;
|
|
184
187
|
}
|
|
185
|
-
export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
188
|
+
export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
186
189
|
/** Subject provenance projection: what the actor actually drove. */
|
|
187
190
|
export interface CuaSubjectProjection {
|
|
188
191
|
source: "app-url" | "clone" | "local-tree";
|
|
@@ -81,16 +81,17 @@ function sharedWorldStream(ctx, spec, index, view) {
|
|
|
81
81
|
const { args } = ctx;
|
|
82
82
|
const { name, session, screenshots, lastScreenshot, desktopGeometry, screenshotMode } = view;
|
|
83
83
|
const judged = view.judgment.judgedStatus;
|
|
84
|
+
const caption = participantCaption({
|
|
85
|
+
id: spec.planned.id,
|
|
86
|
+
personaId: spec.persona.id,
|
|
87
|
+
device: spec.planned.device,
|
|
88
|
+
});
|
|
84
89
|
return participantStream(spec, {
|
|
85
90
|
...(spec.evidenceAssignment === undefined
|
|
86
91
|
? {}
|
|
87
92
|
: { assignment: participantAssignment(spec.evidenceAssignment) }),
|
|
88
93
|
kind: "browser",
|
|
89
|
-
label:
|
|
90
|
-
id: spec.planned.id,
|
|
91
|
-
personaId: spec.persona.id,
|
|
92
|
-
device: spec.planned.device,
|
|
93
|
-
}),
|
|
94
|
+
label: caption,
|
|
94
95
|
status: view.status,
|
|
95
96
|
...(judged === undefined ? {} : { judgedStatus: judged }),
|
|
96
97
|
transport: "snapshot",
|
|
@@ -99,9 +100,9 @@ function sharedWorldStream(ctx, spec, index, view) {
|
|
|
99
100
|
? {
|
|
100
101
|
kind: "screenshot",
|
|
101
102
|
url: lastScreenshot,
|
|
102
|
-
title:
|
|
103
|
+
title: `${caption} (${screenshotMode})`,
|
|
103
104
|
}
|
|
104
|
-
: { kind: "placeholder", title:
|
|
105
|
+
: { kind: "placeholder", title: caption },
|
|
105
106
|
...(desktopGeometry.viewport === undefined
|
|
106
107
|
? {}
|
|
107
108
|
: {
|
|
@@ -252,7 +252,7 @@ function concurrencyReview(args, external, inProgress, evidence, events, nextEve
|
|
|
252
252
|
const overlaps = args.judgment.world.overlap;
|
|
253
253
|
const deltas = (stateSeries ?? []).filter((snapshot, i) => i > 0 && snapshot.digest !== (stateSeries ?? [])[i - 1].digest).length;
|
|
254
254
|
const stateSeriesLabel = external
|
|
255
|
-
? "
|
|
255
|
+
? "Shared app state was not recorded because the app is hosted outside humanish"
|
|
256
256
|
: `stateSeries ${plural((stateSeries ?? []).length, "snapshot")}, ${plural(deltas, "delta")}`;
|
|
257
257
|
const convergenceLabel = external
|
|
258
258
|
? `; lobby convergence ${args.lobbyConvergenceDigest ? "shown (all participants reached one /lobby/CODE)" : "not observed"}`
|
package/dist/run/detail.js
CHANGED
|
@@ -13,7 +13,7 @@ import { estimateActorCostForExecution } from "./pricing.js";
|
|
|
13
13
|
import { resolveRunPath } from "./locate.js";
|
|
14
14
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
15
15
|
import { savedCaption } from "./participant-caption.js";
|
|
16
|
-
import { streamParticipantIdOf } from "./participant-records.js";
|
|
16
|
+
import { recordedPersonaId, streamParticipantIdOf } from "./participant-records.js";
|
|
17
17
|
import { readContainedRegularFile } from "./contained-output.js";
|
|
18
18
|
import { isPathInside, resolvePhysicalCwd } from "./paths.js";
|
|
19
19
|
const RUN_DETAIL_SCHEMA = "humanish.run-detail.v1";
|
|
@@ -43,12 +43,12 @@ function latestThought(trace) {
|
|
|
43
43
|
}
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
46
|
-
function participantFrom(stream, index) {
|
|
46
|
+
function participantFrom(stream, index, savedPersonaId) {
|
|
47
47
|
// A live run's flush wins over the finished trace: while both exist, the live one is the newer
|
|
48
48
|
// account of what is happening.
|
|
49
49
|
const trace = stream.liveActor ?? stream.actor ?? {};
|
|
50
50
|
const id = stream.id ?? `stream-${index + 1}`;
|
|
51
|
-
const personaId = trace.persona?.id;
|
|
51
|
+
const personaId = trace.persona?.id ?? savedPersonaId;
|
|
52
52
|
const participantId = streamParticipantIdOf(stream);
|
|
53
53
|
// Computed once: calling it twice to test-then-use reads as though the two could differ.
|
|
54
54
|
const thought = latestThought(trace);
|
|
@@ -140,7 +140,7 @@ export async function readRunDetail(cwdInput, runId) {
|
|
|
140
140
|
...(automaticAnalysis === undefined ? {} : { automaticAnalysis }),
|
|
141
141
|
schema: RUN_DETAIL_SCHEMA,
|
|
142
142
|
runId: bundle.runId ?? runId,
|
|
143
|
-
participants: streams.map(participantFrom),
|
|
143
|
+
participants: streams.map((stream, index) => participantFrom(stream, index, recordedPersonaId(bundle, stream))),
|
|
144
144
|
...(isPathInside(cwd, observerAbsolute)
|
|
145
145
|
? { observerPath: path.relative(cwd, observerAbsolute) }
|
|
146
146
|
: { observerPath: observerAbsolute }),
|
|
@@ -25,6 +25,7 @@ export interface SavedCaptionFacts {
|
|
|
25
25
|
* The caption a saved stream shows: its label, which a route writes with `participantCaption`.
|
|
26
26
|
* Earlier releases wrote ids and taxonomy into the label (`lane-01 · browser`, `CUA participant
|
|
27
27
|
* <id>: <study>`, `Concurrent persona <id> (type:<t> / ...) · <study>`), so a stream saved that way
|
|
28
|
-
* is captioned again from the id it names
|
|
28
|
+
* is captioned again from the id it names. Older terminal and scripted labels use the
|
|
29
|
+
* recorded persona; a scripted mobile surface also identifies a phone.
|
|
29
30
|
*/
|
|
30
31
|
export declare function savedCaption(stream: SavedCaptionFacts): string;
|
|
@@ -28,11 +28,13 @@ export function participantCaption(participant) {
|
|
|
28
28
|
}
|
|
29
29
|
const OLDER_SHARED_WORLD_LABEL = /^Concurrent persona (\S+)/;
|
|
30
30
|
const OLDER_COMPUTER_USE_LABEL = /^CUA |^\S+ · browser$/;
|
|
31
|
+
const OLDER_TERMINAL_OR_SCRIPTED_LABEL = /^(Terminal agent|Desktop browser surface|Mobile browser surface) · /;
|
|
31
32
|
/**
|
|
32
33
|
* The caption a saved stream shows: its label, which a route writes with `participantCaption`.
|
|
33
34
|
* Earlier releases wrote ids and taxonomy into the label (`lane-01 · browser`, `CUA participant
|
|
34
35
|
* <id>: <study>`, `Concurrent persona <id> (type:<t> / ...) · <study>`), so a stream saved that way
|
|
35
|
-
* is captioned again from the id it names
|
|
36
|
+
* is captioned again from the id it names. Older terminal and scripted labels use the
|
|
37
|
+
* recorded persona; a scripted mobile surface also identifies a phone.
|
|
36
38
|
*/
|
|
37
39
|
export function savedCaption(stream) {
|
|
38
40
|
const { label, participantId, personaId } = stream;
|
|
@@ -40,6 +42,14 @@ export function savedCaption(stream) {
|
|
|
40
42
|
if (sharedWorldId !== undefined) {
|
|
41
43
|
return participantCaption({ id: sharedWorldId, personaId: personaId ?? sharedWorldId });
|
|
42
44
|
}
|
|
45
|
+
if (OLDER_TERMINAL_OR_SCRIPTED_LABEL.test(label) && personaId !== undefined) {
|
|
46
|
+
return participantCaption({
|
|
47
|
+
personaId,
|
|
48
|
+
...(label.startsWith("Mobile browser surface · ")
|
|
49
|
+
? { device: { name: "mobile", preset: { isMobile: true } } }
|
|
50
|
+
: {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
43
53
|
if (OLDER_COMPUTER_USE_LABEL.test(label) && personaId !== undefined) {
|
|
44
54
|
return participantCaption({
|
|
45
55
|
...(participantId === undefined ? {} : { id: participantId }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunEvent, RunProviderResource, RunSimulation } from "./bundle.js";
|
|
1
|
+
import type { RunBundle, RunEvent, RunProviderResource, RunSimulation } from "./bundle.js";
|
|
2
2
|
import type { RunStream } from "./streams.js";
|
|
3
3
|
/** The ids a participant's saved records carry: its record id and its stream id. */
|
|
4
4
|
export interface ParticipantIds {
|
|
@@ -26,3 +26,5 @@ export declare function recordIdOf(stream: RunStream): string;
|
|
|
26
26
|
export declare function eventRecordIdOf(event: RunEvent): string | undefined;
|
|
27
27
|
/** The participant id an adapter recorded on a saved stream (a fan-out `lane-NN` id), when it has one. */
|
|
28
28
|
export declare function streamParticipantIdOf(stream: Pick<RunStream, "laneId">): string | undefined;
|
|
29
|
+
/** The persona recorded for a stream, including dry runs without an actor trace. */
|
|
30
|
+
export declare function recordedPersonaId(bundle: Partial<Pick<RunBundle, "simulations">>, stream: Partial<Pick<RunStream, "simId">>): string | undefined;
|
|
@@ -61,3 +61,9 @@ export function eventRecordIdOf(event) {
|
|
|
61
61
|
export function streamParticipantIdOf(stream) {
|
|
62
62
|
return stream.laneId;
|
|
63
63
|
}
|
|
64
|
+
/** The persona recorded for a stream, including dry runs without an actor trace. */
|
|
65
|
+
export function recordedPersonaId(bundle, stream) {
|
|
66
|
+
if (stream.simId === undefined || !Array.isArray(bundle.simulations))
|
|
67
|
+
return undefined;
|
|
68
|
+
return bundle.simulations.find((record) => record?.id === stream.simId)?.personaId;
|
|
69
|
+
}
|
package/dist/run-study.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// which runs the route's local checks that need no scorer (keys, subject env, the local agent) and
|
|
5
5
|
// returns the route's run. prepareStudy is the same path in two steps, so the CLI can present either
|
|
6
6
|
// refusal before it loads a declared review scorer.
|
|
7
|
+
import { participantList } from "./study/study-fields.js";
|
|
8
|
+
import { personaBackgroundWarnings } from "./study/warnings.js";
|
|
9
|
+
import { resolveCommittedPersonasForCwd, studyPersonaIds } from "./study/persona-resolve.js";
|
|
7
10
|
import { computerUseInput, scriptedInput, sharedWorldInput, terminalInput, } from "./study/route-inputs.js";
|
|
8
11
|
import { planStudy, resolveStudyDryRun, routeOf } from "./study/plan.js";
|
|
9
12
|
import { normalizeRunStudyOptions, optionRefusalOutcome, removedOptionRefusal, } from "./study/run-study-options.js";
|
|
@@ -70,7 +73,9 @@ export async function prepareStudy(config, options, deps = {}) {
|
|
|
70
73
|
outcome.result.warnings.push(...normalized.warnings);
|
|
71
74
|
return { ok: false, outcome };
|
|
72
75
|
}
|
|
73
|
-
const
|
|
76
|
+
const runWarnings = [...(planned.planned.plan.warnings ?? [])];
|
|
77
|
+
const plan = { ...planned.planned.plan, warnings: runWarnings };
|
|
78
|
+
const admitted = await admitPlan(study, planning, plan, deps, normalized.emit);
|
|
74
79
|
if (!admitted.ok) {
|
|
75
80
|
await vm?.close();
|
|
76
81
|
admitted.outcome.result.warnings.push(...normalized.warnings);
|
|
@@ -92,6 +97,17 @@ export async function prepareStudy(config, options, deps = {}) {
|
|
|
92
97
|
return outcome;
|
|
93
98
|
}
|
|
94
99
|
}
|
|
100
|
+
// Admission keeps its refusal order; these warnings join the plan before its run scope opens.
|
|
101
|
+
const personas = route === "computer-use" || route === "shared-world"
|
|
102
|
+
? (await resolveCommittedPersonasForCwd(options.cwd, studyPersonaIds(study))).personas
|
|
103
|
+
: new Map();
|
|
104
|
+
const backgroundWarnings = personaBackgroundWarnings(route === "computer-use" &&
|
|
105
|
+
options.count !== undefined &&
|
|
106
|
+
participantList(study) === undefined
|
|
107
|
+
? { ...study, participants: options.count }
|
|
108
|
+
: study, personas);
|
|
109
|
+
runWarnings.splice(0, runWarnings.length, ...(planned.planned.plan.warnings ?? []), ...backgroundWarnings);
|
|
110
|
+
normalized.warnings.push(...backgroundWarnings);
|
|
95
111
|
// The route layers the scorer over the inputs it admitted, and the local study is not
|
|
96
112
|
// rebuilt for it. The admitted runSession, provider and participant desktop belong to this
|
|
97
113
|
// study, so a rebuilt one would leave the participants they start for no finally to close.
|
package/dist/study/discover.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personaBackgroundWarnings } from "./warnings.js";
|
|
1
2
|
import { studyPersonaIds, resolveCommittedPersonasForCwd } from "./persona-resolve.js";
|
|
2
3
|
import { personaBrief, PersonaConfigError } from "./persona.js";
|
|
3
4
|
import { constants } from "node:fs";
|
|
@@ -193,7 +194,11 @@ export async function inspectStudyManifest(cwd, study) {
|
|
|
193
194
|
config: resolved.config,
|
|
194
195
|
origin: resolved.origin,
|
|
195
196
|
path: resolved.path,
|
|
196
|
-
warnings: [
|
|
197
|
+
warnings: [
|
|
198
|
+
...resolved.warnings,
|
|
199
|
+
...personaResolution.warnings,
|
|
200
|
+
...personaBackgroundWarnings(resolved.config, personaResolution.personas),
|
|
201
|
+
],
|
|
197
202
|
};
|
|
198
203
|
}
|
|
199
204
|
function parseResolvedStudy(args) {
|
package/dist/study/doctor.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export declare function studySetupChecks(args: StudySetupCheckArgs): Promise<{
|
|
|
65
65
|
keys: string[];
|
|
66
66
|
reads?: ReadonlySet<string>;
|
|
67
67
|
checks: Check[];
|
|
68
|
+
/** The step to take first, which doctor prints before its rows. */
|
|
69
|
+
next?: string;
|
|
68
70
|
}>;
|
|
69
71
|
/**
|
|
70
72
|
* The ids of the project's labs that need each provider key for a live run, from the same plan
|
package/dist/study/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
3
3
|
import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
4
|
+
import { capacityShortfall, describeCapacity, } from "../substrates/local/capacity.js";
|
|
4
5
|
import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
5
6
|
import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
|
|
6
7
|
import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
|
|
@@ -178,8 +179,11 @@ export async function studySetupChecks(args) {
|
|
|
178
179
|
// One account check, shared by the local participant row and the Codex analysis row.
|
|
179
180
|
let accountReadiness;
|
|
180
181
|
const checkAccount = () => (accountReadiness ??= (args.codexAnalysisReadiness ?? codexAccountReadiness)(args.env).catch(() => ({ ready: false, errorCode: "codex_unavailable" })));
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
const plan = planned.planned.plan;
|
|
183
|
+
const desktopsAtOnce = plan.route === "computer-use" ? plan.concurrency : undefined;
|
|
184
|
+
const localChecks = local ? await localBrowserChecks(config, args, desktopsAtOnce) : undefined;
|
|
185
|
+
if (localChecks !== undefined)
|
|
186
|
+
checks.push(...localChecks.checks);
|
|
183
187
|
if (config.comms?.email?.kind === "real")
|
|
184
188
|
checks.push(await realEmailCheck(config.comms.email.connection, keys, args));
|
|
185
189
|
checks.push(...(await participantChecks(config, route, keys, local, args, checkAccount)));
|
|
@@ -193,10 +197,19 @@ export async function studySetupChecks(args) {
|
|
|
193
197
|
const reads = config.review?.scorer === undefined
|
|
194
198
|
? new Set([...keyNamesOf(planned.planned.plan), ...keys])
|
|
195
199
|
: undefined;
|
|
196
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
desktop,
|
|
202
|
+
keys,
|
|
203
|
+
...(reads === undefined ? {} : { reads }),
|
|
204
|
+
checks,
|
|
205
|
+
...(localChecks?.next === undefined ? {} : { next: localChecks.next }),
|
|
206
|
+
};
|
|
197
207
|
}
|
|
198
|
-
/**
|
|
199
|
-
|
|
208
|
+
/**
|
|
209
|
+
* A local browser study's runtime, how many of its desktops fit and, with an external catch, its
|
|
210
|
+
* captured inbox. `next` is the runtime's message when it is not ready, which doctor leads with.
|
|
211
|
+
*/
|
|
212
|
+
async function localBrowserChecks(config, args, desktopsAtOnce) {
|
|
200
213
|
const checks = [];
|
|
201
214
|
const runtime = await (args.localRuntimeReadiness ??
|
|
202
215
|
(() => localRuntimeStatus({
|
|
@@ -204,7 +217,14 @@ async function localBrowserChecks(config, args) {
|
|
|
204
217
|
media: config.execution?.desktop?.media !== undefined ||
|
|
205
218
|
config.execution?.desktop?.recording !== undefined,
|
|
206
219
|
})))();
|
|
207
|
-
checks.push({
|
|
220
|
+
checks.push({
|
|
221
|
+
name: "local browser runtime",
|
|
222
|
+
ok: runtime.ok,
|
|
223
|
+
message: runtime.message,
|
|
224
|
+
...(runtime.ok && !runtime.installed ? { status: "note" } : {}),
|
|
225
|
+
});
|
|
226
|
+
if (runtime.capacity !== undefined && desktopsAtOnce !== undefined)
|
|
227
|
+
checks.push(capacityCheck(runtime.capacity, desktopsAtOnce));
|
|
208
228
|
const email = config.comms?.email;
|
|
209
229
|
if (email?.kind === "fake" && email.external) {
|
|
210
230
|
const healthy = await externalCatchHealthy(email.external, { timeoutMs: 5000 });
|
|
@@ -216,7 +236,25 @@ async function localBrowserChecks(config, args) {
|
|
|
216
236
|
: `Captured inbox is unavailable or outdated. Start or upgrade and restart ${cli("comms catch")}, then check comms.email.external.catchBaseUrl (and inboxBaseUrl if set). No participant was allocated.`,
|
|
217
237
|
});
|
|
218
238
|
}
|
|
219
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
checks,
|
|
241
|
+
...(runtime.ok && runtime.installed ? {} : { next: runtime.message }),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Whether the desktops the study runs at once fit the VM or this machine, and why. */
|
|
245
|
+
function capacityCheck(capacity, desktopsAtOnce) {
|
|
246
|
+
const shortfall = capacityShortfall(capacity, desktopsAtOnce);
|
|
247
|
+
const name = "local desktop capacity";
|
|
248
|
+
if (shortfall === undefined)
|
|
249
|
+
return {
|
|
250
|
+
name,
|
|
251
|
+
ok: true,
|
|
252
|
+
message: `${describeCapacity(capacity)} This study runs ${desktopsAtOnce} at once, so it fits.`,
|
|
253
|
+
};
|
|
254
|
+
// Linux runs a study that is short of memory with a warning, so the row is advice there.
|
|
255
|
+
return "refusal" in shortfall
|
|
256
|
+
? { name, ok: false, message: shortfall.refusal }
|
|
257
|
+
: { name, ok: true, status: "note", message: shortfall.warning };
|
|
220
258
|
}
|
|
221
259
|
/** The saved real email connection. Adds the key it needs to `keys`. */
|
|
222
260
|
async function realEmailCheck(connection, keys, args) {
|
|
@@ -159,9 +159,11 @@ export const starterFiles = [
|
|
|
159
159
|
id: synthetic-new-user
|
|
160
160
|
name: Synthetic New User
|
|
161
161
|
summary: A privacy-safe first-time user evaluating the app with realistic but synthetic needs.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
background: >-
|
|
163
|
+
Alex keeps personal plans in a notebook and uses email and a few everyday apps without trouble.
|
|
164
|
+
They are trying an unfamiliar app to see whether it can make everyday tasks easier. They work
|
|
165
|
+
through some friction but stop when more effort no longer seems worth it, and they take the
|
|
166
|
+
straightforward path the screen offers.
|
|
165
167
|
constraints:
|
|
166
168
|
- Do not use real personal data.
|
|
167
169
|
- Do not use production accounts.
|
|
@@ -175,9 +177,12 @@ constraints:
|
|
|
175
177
|
id: skeptical-power-user
|
|
176
178
|
name: Skeptical Power User
|
|
177
179
|
summary: A privacy-safe experienced user looking for speed, reversibility, and clear proof.
|
|
180
|
+
background: >-
|
|
181
|
+
Robin manages several volunteer projects with spreadsheets and keyboard shortcuts and is at ease
|
|
182
|
+
with advanced options. They want faster tools but have lost work before and care about undoing
|
|
183
|
+
mistakes. They have little patience for repeated friction and stop when a tool keeps getting in
|
|
184
|
+
the way.
|
|
178
185
|
traits:
|
|
179
|
-
patience: low
|
|
180
|
-
technical_confidence: high
|
|
181
186
|
accessibility_needs: keyboard_first
|
|
182
187
|
constraints:
|
|
183
188
|
- Do not use real personal data.
|
package/dist/study/plan.js
CHANGED
|
@@ -12,7 +12,7 @@ import { planBase } from "./plan-base.js";
|
|
|
12
12
|
import { routeOf } from "./routing.js";
|
|
13
13
|
import { automaticAnalysisRouteReason, taskProtocolValidationReason } from "./validation.js";
|
|
14
14
|
import { declaredParticipantCount } from "./study-fields.js";
|
|
15
|
-
import { egressAllowIgnoredWarning } from "./warnings.js";
|
|
15
|
+
import { egressAllowIgnoredWarning, scriptedMissionWarnings } from "./warnings.js";
|
|
16
16
|
export { routeOf } from "./routing.js";
|
|
17
17
|
/** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
|
|
18
18
|
export function resolveStudyDryRun(config, override, fallback) {
|
|
@@ -76,10 +76,11 @@ export function planStudy(config, options, deps = {}) {
|
|
|
76
76
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
77
77
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
78
78
|
const warning = egressAllowIgnoredWarning(study);
|
|
79
|
+
const warnings = [...(warning === undefined ? [] : [warning]), ...scriptedMissionWarnings(study)];
|
|
79
80
|
const plan = {
|
|
80
81
|
...result.plan,
|
|
81
82
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
82
|
-
...(
|
|
83
|
+
...(warnings.length === 0 ? {} : { warnings }),
|
|
83
84
|
};
|
|
84
85
|
return { ok: true, planned: { plan } };
|
|
85
86
|
}
|
|
@@ -3,6 +3,7 @@ import type { StudyPreflightCheck, StudyPreflightResult, PreflightContext } from
|
|
|
3
3
|
export declare const STUDY_CHECK_SCHEMA = "humanish.study-check.v1";
|
|
4
4
|
export declare function finalize(ctx: PreflightContext, args?: {
|
|
5
5
|
check?: StudyPreflightCheck;
|
|
6
|
+
checks?: StudyPreflightCheck[];
|
|
6
7
|
}): StudyPreflightResult;
|
|
7
8
|
export declare function fail(ctx: PreflightContext, code: NonNullable<StudyPreflightResult["error"]>["code"], message: string, checks: StudyPreflightCheck[]): StudyPreflightResult;
|
|
8
9
|
export declare function digest(value: string): string;
|
|
@@ -5,7 +5,7 @@ import { digestText } from "../evidence/redaction.js";
|
|
|
5
5
|
/** The schema of `study check` results: whether a study is ready to run on this machine. */
|
|
6
6
|
export const STUDY_CHECK_SCHEMA = "humanish.study-check.v1";
|
|
7
7
|
export function finalize(ctx, args) {
|
|
8
|
-
const checks =
|
|
8
|
+
const checks = [...ctx.checks, ...(args?.check ? [args.check] : []), ...(args?.checks ?? [])];
|
|
9
9
|
const analysis = automaticAnalysisBudget(ctx.config.review?.analysis, ctx.route);
|
|
10
10
|
return {
|
|
11
11
|
schema: STUDY_CHECK_SCHEMA,
|
|
@@ -9,6 +9,8 @@ export interface StudyPreflightCheck {
|
|
|
9
9
|
name: string;
|
|
10
10
|
ok: boolean;
|
|
11
11
|
message: string;
|
|
12
|
+
/** `false` on a row the CLI could not check; its message names the command that checks it. */
|
|
13
|
+
checked?: false;
|
|
12
14
|
}
|
|
13
15
|
export interface StudyPreflightTarget {
|
|
14
16
|
label: string;
|