humanish 0.139.1 → 0.141.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 +23 -0
- package/README.md +5 -4
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/external-handoff.d.ts +22 -0
- package/dist/actors/computer-use/external-handoff.js +83 -0
- package/dist/actors/computer-use/loop/observation.d.ts +7 -0
- package/dist/actors/computer-use/loop/observation.js +17 -1
- package/dist/actors/computer-use/loop/types.d.ts +6 -0
- package/dist/analysis/analysis-limits.d.ts +6 -0
- package/dist/analysis/analysis-limits.js +6 -0
- package/dist/analysis/analysis-lock.d.ts +8 -0
- package/dist/analysis/analysis-lock.js +129 -0
- package/dist/analysis/automatic-completion.js +31 -0
- package/dist/analysis/automatic-config.d.ts +15 -6
- package/dist/analysis/automatic-config.js +25 -9
- package/dist/analysis/execute.js +1 -1
- package/dist/analysis/job.d.ts +1 -0
- package/dist/analysis/job.js +6 -2
- package/dist/analysis/recording-ended.d.ts +16 -0
- package/dist/analysis/recording-ended.js +29 -0
- package/dist/analysis/service.d.ts +1 -3
- package/dist/analysis/service.js +23 -50
- package/dist/analysis/source.d.ts +1 -1
- package/dist/browser-control/protocol.d.ts +8 -0
- package/dist/browser-control/protocol.js +21 -0
- package/dist/cli/commands/analysis-signals.d.ts +10 -4
- package/dist/cli/commands/analysis-signals.js +28 -6
- package/dist/cli/commands/runs.js +8 -2
- package/dist/cli/commands/study-route-computer-use.js +4 -5
- package/dist/cli/commands/study-route-scripted.js +5 -6
- package/dist/cli/commands/study-route-shared-world.js +4 -5
- package/dist/cli/commands/study-route-terminal.js +5 -6
- package/dist/cli/commands/study-run.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +3 -0
- package/dist/cli/findings.d.ts +40 -14
- package/dist/cli/findings.js +117 -35
- package/dist/cli/io.d.ts +5 -1
- package/dist/cli/io.js +12 -2
- package/dist/cli/run-findings.d.ts +19 -0
- package/dist/cli/run-findings.js +106 -0
- package/dist/evidence/browser-hygiene.d.ts +12 -1
- package/dist/evidence/browser-hygiene.js +45 -2
- package/dist/feedback/export-bundle.js +1 -1
- package/dist/guest/bootstrap.d.ts +4 -2
- package/dist/guest/bootstrap.js +15 -7
- package/dist/guest/display-config.d.ts +31 -0
- package/dist/guest/display-config.js +35 -0
- package/dist/guest/external-links.d.ts +14 -0
- package/dist/guest/external-links.js +48 -0
- package/dist/guest/runtime-desktop.d.ts +9 -3
- package/dist/guest/runtime-desktop.js +64 -27
- package/dist/guest/runtime.d.ts +2 -1
- package/dist/guest/runtime.js +1 -1
- package/dist/guest-runtime-main.js +2 -1
- package/dist/observer/data.d.ts +4 -0
- package/dist/observer/run-routes.js +12 -3
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/e2b-desktop/desktop.js +27 -17
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +10 -17
- package/dist/routes/computer-use/e2b-desktop/teardown.js +1 -1
- package/dist/routes/computer-use/in-process-desktop.js +2 -2
- package/dist/routes/computer-use/live-phase.js +1 -1
- package/dist/routes/computer-use/local-vm.js +34 -1
- package/dist/routes/computer-use/plan.js +1 -1
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/scripted/plan.js +1 -1
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/run/display.d.ts +6 -1
- package/dist/run/display.js +11 -0
- package/dist/run/host-suspension.js +4 -1
- package/dist/run/preflight-receipts.d.ts +3 -12
- package/dist/run/preflight-receipts.js +7 -67
- package/dist/run/process-owner.d.ts +28 -0
- package/dist/run/process-owner.js +83 -0
- package/dist/run/projection.js +1 -1
- package/dist/run/reclaim.d.ts +6 -0
- package/dist/run/reclaim.js +32 -2
- package/dist/run/route-shell.js +2 -2
- package/dist/run/run-index.d.ts +3 -1
- package/dist/run/run-index.js +23 -11
- package/dist/run/run.d.ts +16 -5
- package/dist/run/run.js +25 -10
- package/dist/run/status.d.ts +57 -2
- package/dist/run/status.js +87 -1
- package/dist/run/stored-runs.d.ts +3 -0
- package/dist/run/stored-runs.js +9 -4
- package/dist/study/doctor.js +11 -6
- package/dist/study/init-templates.js +9 -3
- package/dist/study/mobile-emulation.d.ts +27 -0
- package/dist/study/mobile-emulation.js +60 -0
- package/dist/study/plan.js +1 -1
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/run-study-events.d.ts +7 -1
- package/dist/study/summary.js +1 -1
- package/dist/study/warnings.js +5 -2
- package/dist/substrates/e2b/cdp-probe.d.ts +2 -6
- package/dist/substrates/e2b/desktop-browser.js +3 -1
- package/dist/substrates/e2b/desktop-cdp.d.ts +0 -2
- package/dist/substrates/e2b/desktop-cdp.js +0 -2
- package/dist/substrates/e2b/desktop-executor.d.ts +7 -0
- package/dist/substrates/e2b/desktop-executor.js +3 -0
- package/dist/substrates/e2b/desktop-handoff.d.ts +22 -0
- package/dist/substrates/e2b/desktop-handoff.js +86 -0
- package/dist/substrates/local/firecracker-desktop.d.ts +5 -0
- package/dist/substrates/local/firecracker-desktop.js +7 -1
- package/dist/substrates/local/runtime-config.d.ts +12 -0
- package/dist/substrates/local/runtime-config.js +55 -13
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/local/runtime.js +1 -0
- package/dist/tui-app.js +115 -115
- package/docs/architecture/observer.md +7 -3
- package/docs/contracts/schemas.md +24 -3
- package/docs/contracts/study-analysis.md +15 -9
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +75 -18
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +153 -31
package/dist/run/reclaim.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
import { e2bAccount, e2bConnection } from "../substrates/e2b/connection.js";
|
|
13
13
|
import { modalAccount } from "../substrates/modal/sandbox.js";
|
|
14
14
|
import { E2B_DEBUG_ENV, e2bDebugMode, loadE2BDesktopModule, } from "../substrates/e2b/sdk.js";
|
|
15
|
-
import { readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
15
|
+
import { readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
16
16
|
import { discardPreflightJournal, listPreflightJournals, preflightReclaimDecision, } from "./preflight-receipts.js";
|
|
17
17
|
import { readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
18
18
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
19
|
-
import { RUN_STATUS_FILE } from "./status.js";
|
|
19
|
+
import { classifyRunStatus, isRunStatusRecord, RUN_STATUS_FILE, } from "./status.js";
|
|
20
20
|
import { isRecord } from "./type-guards.js";
|
|
21
21
|
import { isRunOutcome } from "./bundle-shape.js";
|
|
22
22
|
import { parseSandboxOwners, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
@@ -66,6 +66,15 @@ async function reclaimRun(cwd, runInput, locate, hooks) {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
// A kill by hand, not a check and not the signal handler (whose run is alive and records its
|
|
70
|
+
// own interrupt): a run whose heartbeat stopped is over once its sandboxes are stopped.
|
|
71
|
+
const marked = hooks.check !== true &&
|
|
72
|
+
hooks.creates === undefined &&
|
|
73
|
+
(await markStaleRunInterrupted(runPaths, Date.now()).catch(() => false));
|
|
74
|
+
const result = await reclaimLocated(runPaths, base, hooks, warnings);
|
|
75
|
+
return marked ? { ...result, markedInterrupted: true } : result;
|
|
76
|
+
}
|
|
77
|
+
async function reclaimLocated(runPaths, base, hooks, warnings) {
|
|
69
78
|
const runId = runIdOf(runPaths);
|
|
70
79
|
// The signal handler passes the creates it stopped and decides from what they reported.
|
|
71
80
|
const reason = hooks.creates === undefined ? await recordedNoSandbox(runPaths) : undefined;
|
|
@@ -424,3 +433,24 @@ async function reclaimRoot(root, label, hooks, warnings) {
|
|
|
424
433
|
createsInFlight,
|
|
425
434
|
};
|
|
426
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* For `humanish reclaim`, which stops the run's sandboxes: a `running` record whose heartbeat is
|
|
438
|
+
* stale becomes `interrupted` with reason `reclaimed` and `completedAt` at its last heartbeat. A
|
|
439
|
+
* fresh record belongs to a live owner and is left alone, as is one that already ended. True when
|
|
440
|
+
* it wrote one.
|
|
441
|
+
*/
|
|
442
|
+
async function markStaleRunInterrupted(runPaths, nowMs) {
|
|
443
|
+
const record = runJsonValue(await readRunJsonIfExists(runPaths, RUN_STATUS_FILE));
|
|
444
|
+
if (!isRunStatusRecord(record) || record.state !== "running")
|
|
445
|
+
return false;
|
|
446
|
+
if (classifyRunStatus(record, nowMs) === "running")
|
|
447
|
+
return false;
|
|
448
|
+
const marked = {
|
|
449
|
+
...record,
|
|
450
|
+
state: "interrupted",
|
|
451
|
+
reason: "reclaimed",
|
|
452
|
+
completedAt: record.updatedAt,
|
|
453
|
+
};
|
|
454
|
+
await writeContainedOutputFile(runPaths, RUN_STATUS_FILE, `${JSON.stringify(marked, null, 2)}\n`, "utf8");
|
|
455
|
+
return true;
|
|
456
|
+
}
|
package/dist/run/route-shell.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function admitRoute(shell) {
|
|
|
30
30
|
// The scorer joins the input only; whatever the checks admitted stays what the run uses.
|
|
31
31
|
const input = shell.withScorer?.(shell.input, scorer) ?? shell.input;
|
|
32
32
|
const run = async () => {
|
|
33
|
-
const { result, finished } = await runScope((scope) => shell.runInScope(admitted, input, scope));
|
|
33
|
+
const { result, finished } = await runScope((scope) => shell.runInScope(admitted, input, scope), { onStart: (runId) => input.emit?.({ type: "run-started", runId }) });
|
|
34
34
|
return complete(input, result, finished);
|
|
35
35
|
};
|
|
36
36
|
return routeOutcome(shell.route, await (shell.commsSecrets ? withTransientCommsSecrets(run) : run()));
|
|
@@ -42,7 +42,7 @@ export async function admitRoute(shell) {
|
|
|
42
42
|
* or default analysis is recorded as skipped.
|
|
43
43
|
*/
|
|
44
44
|
export function completeRefusalAnalysis(refused, config, input) {
|
|
45
|
-
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
45
|
+
const analysis = resolveAutomaticAnalysis(config.review?.analysis, config.actor);
|
|
46
46
|
return completeAutomaticAnalysis(refused, undefined, analysis.ok ? analysis.config : undefined, input, { trigger: config.review?.analysis === undefined ? "default" : "explicit" });
|
|
47
47
|
}
|
|
48
48
|
/** A result as its route's outcome. StudyOutcome pairs each route with its result type. */
|
package/dist/run/run-index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AnalysisCostCache } from "./run-index-analysis.js";
|
|
2
2
|
import type { RunAnalysisCost } from "./run-cost.js";
|
|
3
|
-
import { type RunLiveness } from "./status.js";
|
|
3
|
+
import { type RunAnalysisPhase, type RunLiveness } from "./status.js";
|
|
4
4
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
5
5
|
import type { ExecutionFailure } from "./judge.js";
|
|
6
6
|
declare const RUN_INDEX_SCHEMA = "humanish.run-index.v1";
|
|
@@ -42,6 +42,8 @@ export interface RunIndexEntry {
|
|
|
42
42
|
analysisCost?: RunAnalysisCost;
|
|
43
43
|
/** Wall-clock span when both ends are known; used for per-study medians. */
|
|
44
44
|
durationMs?: number;
|
|
45
|
+
/** The automatic analysis the run's own process started after the run ended, from status.json. */
|
|
46
|
+
analysis?: RunAnalysisPhase;
|
|
45
47
|
}
|
|
46
48
|
export interface RunIndexResult {
|
|
47
49
|
schema: typeof RUN_INDEX_SCHEMA;
|
package/dist/run/run-index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { runLiveness } from "./liveness.js";
|
|
7
7
|
import { AnalysisCostCache, readIndexedAnalysisCost } from "./run-index-analysis.js";
|
|
8
|
-
import { RUN_STATUS_FILE, classifyRunStatus, isRunStatusRecord, } from "./status.js";
|
|
8
|
+
import { RUN_STATUS_FILE, classifyRunStatus, isRunStatusRecord, runAnalysisPhase, } from "./status.js";
|
|
9
9
|
import { studyProvenanceOf } from "./study-provenance.js";
|
|
10
10
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
11
11
|
import { isRunOutcome } from "./bundle-shape.js";
|
|
@@ -63,6 +63,7 @@ async function readJson(file) {
|
|
|
63
63
|
function entryFromStatus(record, nowMs) {
|
|
64
64
|
const started = Date.parse(record.startedAt);
|
|
65
65
|
const ended = record.completedAt === undefined ? Number.NaN : Date.parse(record.completedAt);
|
|
66
|
+
const analysis = runAnalysisPhase(record, nowMs);
|
|
66
67
|
return {
|
|
67
68
|
runId: record.runId,
|
|
68
69
|
derivedFrom: "status",
|
|
@@ -88,6 +89,7 @@ function entryFromStatus(record, nowMs) {
|
|
|
88
89
|
? { estimatedCostComplete: record.outcome.estimatedCostComplete }
|
|
89
90
|
: {}),
|
|
90
91
|
...(Number.isFinite(started) && Number.isFinite(ended) ? { durationMs: ended - started } : {}),
|
|
92
|
+
...(analysis === undefined ? {} : { analysis }),
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
/** A status record this run can be classified from: well formed and naming this run. */
|
|
@@ -168,16 +170,8 @@ async function readEntry(runId, runDir, cache, nowMs) {
|
|
|
168
170
|
const [statusKey, bundleKey] = await Promise.all([statKey(statusFile), statKey(bundleFile)]);
|
|
169
171
|
const key = JSON.stringify([statusKey, bundleKey]);
|
|
170
172
|
const cached = cache?.get(runId, key);
|
|
171
|
-
if (cached !== undefined)
|
|
172
|
-
return cached
|
|
173
|
-
cached.updatedAt !== undefined &&
|
|
174
|
-
cached.completedAt === undefined
|
|
175
|
-
? {
|
|
176
|
-
...cached,
|
|
177
|
-
liveness: classifyRunStatus({ state: "running", updatedAt: cached.updatedAt }, nowMs),
|
|
178
|
-
}
|
|
179
|
-
: cached;
|
|
180
|
-
}
|
|
173
|
+
if (cached !== undefined)
|
|
174
|
+
return withCurrentLiveness(cached, nowMs);
|
|
181
175
|
const [status, raw] = await Promise.all([
|
|
182
176
|
statusKey === null ? undefined : readJson(statusFile),
|
|
183
177
|
bundleKey === null ? undefined : readJson(bundleFile),
|
|
@@ -196,6 +190,7 @@ async function readEntry(runId, runDir, cache, nowMs) {
|
|
|
196
190
|
updatedAt: metadata.updatedAt,
|
|
197
191
|
...(metadata.completedAt === undefined ? {} : { completedAt: metadata.completedAt }),
|
|
198
192
|
...(metadata.durationMs === undefined ? {} : { durationMs: metadata.durationMs }),
|
|
193
|
+
...(metadata.analysis === undefined ? {} : { analysis: metadata.analysis }),
|
|
199
194
|
...studyEntry(metadata.study),
|
|
200
195
|
}),
|
|
201
196
|
...entryFromBundle(runId, bundle),
|
|
@@ -216,6 +211,23 @@ async function readEntry(runId, runDir, cache, nowMs) {
|
|
|
216
211
|
cache?.set(runId, key, entry);
|
|
217
212
|
return entry;
|
|
218
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* A cached entry read again at `nowMs`. Its file has not changed, but a live run or a running
|
|
216
|
+
* analysis goes stale with time alone, so both are classified again from the cached heartbeat.
|
|
217
|
+
*/
|
|
218
|
+
function withCurrentLiveness(cached, nowMs) {
|
|
219
|
+
if (cached.updatedAt === undefined)
|
|
220
|
+
return cached;
|
|
221
|
+
if (cached.derivedFrom === "status" && cached.completedAt === undefined)
|
|
222
|
+
return {
|
|
223
|
+
...cached,
|
|
224
|
+
liveness: classifyRunStatus({ state: "running", updatedAt: cached.updatedAt }, nowMs),
|
|
225
|
+
};
|
|
226
|
+
if (cached.analysis === undefined || cached.analysis.state === "ended")
|
|
227
|
+
return cached;
|
|
228
|
+
const analysis = runAnalysisPhase({ updatedAt: cached.updatedAt, analysis: { startedAt: cached.analysis.startedAt } }, nowMs);
|
|
229
|
+
return analysis === undefined ? cached : { ...cached, analysis };
|
|
230
|
+
}
|
|
219
231
|
/** Newest-first ordering: the most recent thing known about a run, else its id's own timestamp. */
|
|
220
232
|
function sortKey(entry) {
|
|
221
233
|
for (const candidate of [entry.completedAt, entry.updatedAt, entry.startedAt]) {
|
package/dist/run/run.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type RunBundle } from "./bundle.js";
|
|
|
3
3
|
import { type ExecutionFailure, type ExecutionOutcome, type OutcomePolicy } from "./judge.js";
|
|
4
4
|
import { type PreparedRunArtifactPaths, type RunIdInUse } from "./paths.js";
|
|
5
5
|
import { RunSecrets } from "./secrets.js";
|
|
6
|
+
import { type RunStatusHandle } from "./status.js";
|
|
6
7
|
import { type HostClock, type HostSuspension } from "./host-suspension.js";
|
|
7
8
|
import type { RunStudyProvenance } from "./study-provenance.js";
|
|
8
9
|
interface StartRunOptions {
|
|
@@ -170,7 +171,7 @@ export declare class FinishedRun {
|
|
|
170
171
|
readonly runId: string;
|
|
171
172
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
172
173
|
readonly paths: PreparedRunArtifactPaths;
|
|
173
|
-
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>,
|
|
174
|
+
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, status: Pick<RunStatusHandle, "interrupted" | "beginAnalysis">, participantsRan: () => boolean);
|
|
174
175
|
/**
|
|
175
176
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
176
177
|
* when asked, since the signal can arrive after the bundle was published.
|
|
@@ -178,6 +179,12 @@ export declare class FinishedRun {
|
|
|
178
179
|
get interrupted(): boolean;
|
|
179
180
|
/** The route reported a participant's session started (Run.participantStarted). */
|
|
180
181
|
get participantsRan(): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Automatic analysis of this run starts in this process: status.json records when, and its
|
|
184
|
+
* heartbeat runs again, so every surface can say the run is analyzing. Call the returned
|
|
185
|
+
* function with the analysis outcome's state when it ends. After an interrupt both do nothing.
|
|
186
|
+
*/
|
|
187
|
+
beginAnalysis(): ReturnType<RunStatusHandle["beginAnalysis"]>;
|
|
181
188
|
static isIssued(value: unknown): value is FinishedRun;
|
|
182
189
|
/**
|
|
183
190
|
* The run's ok and execution outcome as run.json records them: what the route passed to
|
|
@@ -195,12 +202,16 @@ export declare class FinishedRun {
|
|
|
195
202
|
/**
|
|
196
203
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
197
204
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
198
|
-
* timers or callbacks. It then waits for the calls it admitted and, unless the run
|
|
199
|
-
*
|
|
200
|
-
*
|
|
205
|
+
* timers or callbacks. It then waits for the calls it admitted and, unless the run published,
|
|
206
|
+
* records status.json `interrupted` with reason `no-publish`: a run with no published bundle did
|
|
207
|
+
* not finish, so no surface shows it as passed or counts it in a study's durations. The close cannot replace `fn`'s exception: admitted calls
|
|
208
|
+
* are awaited settled and the status write swallows its own errors. `finished` is set only
|
|
201
209
|
* when `Run.finish` resolved inside the scope, so a refusal cannot hand analysis another run.
|
|
202
210
|
*/
|
|
203
|
-
export declare function runScope<T>(fn: (scope: RunScope) => Promise<T
|
|
211
|
+
export declare function runScope<T>(fn: (scope: RunScope) => Promise<T>, hooks?: {
|
|
212
|
+
/** The run exists: its directory and first status.json write landed. A throw is ignored. */
|
|
213
|
+
onStart?: (runId: string) => void;
|
|
214
|
+
}): Promise<{
|
|
204
215
|
result: T;
|
|
205
216
|
finished: FinishedRun | undefined;
|
|
206
217
|
}>;
|
package/dist/run/run.js
CHANGED
|
@@ -35,34 +35,42 @@ const issueKey = Symbol("FinishedRun");
|
|
|
35
35
|
export class FinishedRun {
|
|
36
36
|
#observer;
|
|
37
37
|
#recordFailure;
|
|
38
|
-
#interrupted;
|
|
39
38
|
#outcome;
|
|
40
39
|
runId;
|
|
41
40
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
42
41
|
paths;
|
|
43
42
|
#participantsRan;
|
|
44
|
-
|
|
43
|
+
#status;
|
|
44
|
+
constructor(key, runId, paths, observer, outcome, recordFailure, status, participantsRan) {
|
|
45
45
|
if (key !== issueKey)
|
|
46
46
|
throw new Error("Only Run.finish issues a FinishedRun.");
|
|
47
47
|
this.runId = runId;
|
|
48
48
|
this.paths = paths;
|
|
49
49
|
this.#participantsRan = participantsRan;
|
|
50
|
+
this.#status = status;
|
|
50
51
|
this.#observer = observer;
|
|
51
52
|
this.#outcome = outcome;
|
|
52
53
|
this.#recordFailure = recordFailure;
|
|
53
|
-
this.#interrupted = interrupted;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
57
57
|
* when asked, since the signal can arrive after the bundle was published.
|
|
58
58
|
*/
|
|
59
59
|
get interrupted() {
|
|
60
|
-
return this.#interrupted
|
|
60
|
+
return this.#status.interrupted;
|
|
61
61
|
}
|
|
62
62
|
/** The route reported a participant's session started (Run.participantStarted). */
|
|
63
63
|
get participantsRan() {
|
|
64
64
|
return this.#participantsRan();
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Automatic analysis of this run starts in this process: status.json records when, and its
|
|
68
|
+
* heartbeat runs again, so every surface can say the run is analyzing. Call the returned
|
|
69
|
+
* function with the analysis outcome's state when it ends. After an interrupt both do nothing.
|
|
70
|
+
*/
|
|
71
|
+
beginAnalysis() {
|
|
72
|
+
return this.#status.beginAnalysis();
|
|
73
|
+
}
|
|
66
74
|
static isIssued(value) {
|
|
67
75
|
return typeof value === "object" && value !== null && #observer in value;
|
|
68
76
|
}
|
|
@@ -262,7 +270,7 @@ function runPublisher(args) {
|
|
|
262
270
|
await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
|
|
263
271
|
return current;
|
|
264
272
|
}));
|
|
265
|
-
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure,
|
|
273
|
+
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, runStatus, participantsRan);
|
|
266
274
|
};
|
|
267
275
|
const interrupt = async (signal) => {
|
|
268
276
|
interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
|
|
@@ -287,12 +295,13 @@ function runPublisher(args) {
|
|
|
287
295
|
/**
|
|
288
296
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
289
297
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
290
|
-
* timers or callbacks. It then waits for the calls it admitted and, unless the run
|
|
291
|
-
*
|
|
292
|
-
*
|
|
298
|
+
* timers or callbacks. It then waits for the calls it admitted and, unless the run published,
|
|
299
|
+
* records status.json `interrupted` with reason `no-publish`: a run with no published bundle did
|
|
300
|
+
* not finish, so no surface shows it as passed or counts it in a study's durations. The close cannot replace `fn`'s exception: admitted calls
|
|
301
|
+
* are awaited settled and the status write swallows its own errors. `finished` is set only
|
|
293
302
|
* when `Run.finish` resolved inside the scope, so a refusal cannot hand analysis another run.
|
|
294
303
|
*/
|
|
295
|
-
export async function runScope(fn) {
|
|
304
|
+
export async function runScope(fn, hooks = {}) {
|
|
296
305
|
let closed = false;
|
|
297
306
|
let started = false;
|
|
298
307
|
let status;
|
|
@@ -339,6 +348,12 @@ export async function runScope(fn) {
|
|
|
339
348
|
},
|
|
340
349
|
});
|
|
341
350
|
await runStatus.started;
|
|
351
|
+
try {
|
|
352
|
+
hooks.onStart?.(runId);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// An announcement cannot stop the run it announces.
|
|
356
|
+
}
|
|
342
357
|
const run = openRun(options, runId, created.paths, runStatus);
|
|
343
358
|
interruptBundle = run.interrupt;
|
|
344
359
|
return { ok: true, run: run.run };
|
|
@@ -414,7 +429,7 @@ export async function runScope(fn) {
|
|
|
414
429
|
}
|
|
415
430
|
finally {
|
|
416
431
|
if (finished === undefined)
|
|
417
|
-
await status?.
|
|
432
|
+
await status?.abandon();
|
|
418
433
|
unregister?.();
|
|
419
434
|
}
|
|
420
435
|
}
|
package/dist/run/status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AutomaticAnalysisOutcome } from "../analysis/job.js";
|
|
1
2
|
import type { RunBundle } from "./bundle.js";
|
|
2
3
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
3
4
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
@@ -10,11 +11,26 @@ export declare const RUN_STATUS_FILE = "status.json";
|
|
|
10
11
|
export declare const RUN_STATUS_TOUCH_MS = 5000;
|
|
11
12
|
/**
|
|
12
13
|
* A `running` record whose `updatedAt` is older than this is interrupted, not alive: the process
|
|
13
|
-
* died
|
|
14
|
+
* died before the run ended (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
14
15
|
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
15
16
|
*/
|
|
16
17
|
export declare const RUN_STATUS_STALE_MS: number;
|
|
17
18
|
type RunStatusState = "running" | "finished" | "interrupted";
|
|
19
|
+
/**
|
|
20
|
+
* Why a run is `interrupted` without a signal. `no-publish`: its process left the run without
|
|
21
|
+
* publishing a bundle, as a route that throws or returns a refusal after the run started does.
|
|
22
|
+
* `reclaimed`: `humanish reclaim` found its heartbeat stale and stopped its sandboxes.
|
|
23
|
+
*/
|
|
24
|
+
type RunInterruptReason = "no-publish" | "reclaimed";
|
|
25
|
+
/** How automatic analysis ended, as its job records it. */
|
|
26
|
+
type RunAnalysisEndState = AutomaticAnalysisOutcome["state"];
|
|
27
|
+
/** Automatic analysis in the run's own process, after the run ended. */
|
|
28
|
+
interface RunStatusAnalysis {
|
|
29
|
+
startedAt: string;
|
|
30
|
+
/** Absent while it runs, and on the record of a process that stopped during it. */
|
|
31
|
+
endedAt?: string;
|
|
32
|
+
state?: RunAnalysisEndState;
|
|
33
|
+
}
|
|
18
34
|
/** The signals the CLI's run handler records when it stops a run. */
|
|
19
35
|
export type RunInterruptSignal = "SIGINT" | "SIGTERM" | "SIGHUP";
|
|
20
36
|
/** The outcome summary a finalized record carries. Derived from the bundle; never authoritative. */
|
|
@@ -67,7 +83,14 @@ export interface RunStatusRecord {
|
|
|
67
83
|
completedAt?: string;
|
|
68
84
|
/** The signal that stopped the run, on an `interrupted` record. */
|
|
69
85
|
signal?: RunInterruptSignal;
|
|
86
|
+
/** Why an `interrupted` record has no signal. */
|
|
87
|
+
reason?: RunInterruptReason;
|
|
70
88
|
outcome?: RunStatusOutcome;
|
|
89
|
+
/**
|
|
90
|
+
* Set on a `finished` record when automatic analysis starts in the same process. While it
|
|
91
|
+
* runs, the heartbeat keeps `updatedAt` fresh; `completedAt` stays when the participants ended.
|
|
92
|
+
*/
|
|
93
|
+
analysis?: RunStatusAnalysis;
|
|
71
94
|
}
|
|
72
95
|
export interface RunStatusHandle {
|
|
73
96
|
/** Resolves once the initial record has landed on disk, and never rejects: a failed write is
|
|
@@ -75,7 +98,8 @@ export interface RunStatusHandle {
|
|
|
75
98
|
* it before a route can acquire a sandbox, so a run killed after its sandbox receipt lands
|
|
76
99
|
* still has a record to classify. */
|
|
77
100
|
readonly started: Promise<void>;
|
|
78
|
-
/**
|
|
101
|
+
/** The run ended with a published bundle: state `finished`, `completedAt`, and the derived
|
|
102
|
+
* outcome. Stops the cadence.
|
|
79
103
|
* Idempotent: a second call is a no-op, so a route with several exit paths is safe. */
|
|
80
104
|
finish(outcome?: RunStatusOutcome): Promise<void>;
|
|
81
105
|
/** After finish, replace the finished record's outcome with one read from a rewritten run.json,
|
|
@@ -88,6 +112,15 @@ export interface RunStatusHandle {
|
|
|
88
112
|
interrupt(signal: RunInterruptSignal): Promise<boolean>;
|
|
89
113
|
/** True from the moment `interrupt` accepts a signal, before its write lands. */
|
|
90
114
|
readonly interrupted: boolean;
|
|
115
|
+
/** The run's scope closed without a publish: state `interrupted` with reason `no-publish`. A
|
|
116
|
+
* no-op after finish or an interrupt, so it never overwrites a record that says more. */
|
|
117
|
+
abandon(): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* After finish, automatic analysis starts: `analysis.startedAt` is written and the heartbeat
|
|
120
|
+
* runs again until the returned function records the end and its state. Before finish or after
|
|
121
|
+
* an interrupt it writes nothing, and neither does the returned function.
|
|
122
|
+
*/
|
|
123
|
+
beginAnalysis(): Promise<(state: RunAnalysisEndState) => Promise<void>>;
|
|
91
124
|
/** The host suspensions the cadence saw until now, or until it stopped. */
|
|
92
125
|
suspensions(): readonly HostSuspension[];
|
|
93
126
|
}
|
|
@@ -114,6 +147,28 @@ export type RunLiveness = "running" | "interrupted" | "finished";
|
|
|
114
147
|
* `nowMs` is passed in rather than read, so a classification is reproducible.
|
|
115
148
|
*/
|
|
116
149
|
export declare function classifyRunStatus(record: Pick<RunStatusRecord, "state" | "updatedAt">, nowMs: number, staleMs?: number): RunLiveness;
|
|
150
|
+
/** Automatic analysis as a status record shows it: running, ended, or stopped with its process. */
|
|
151
|
+
export type RunAnalysisPhase = {
|
|
152
|
+
state: "running" | "stopped";
|
|
153
|
+
startedAt: string;
|
|
154
|
+
} | {
|
|
155
|
+
state: "ended";
|
|
156
|
+
startedAt: string;
|
|
157
|
+
endedAt: string;
|
|
158
|
+
outcome?: RunAnalysisEndState;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* The record's automatic analysis. `stopped` is an analysis with no end whose heartbeat went
|
|
162
|
+
* stale: the process ended during it. Undefined when the record has none, or one this version
|
|
163
|
+
* cannot read.
|
|
164
|
+
*/
|
|
165
|
+
export declare function runAnalysisPhase(record: Pick<RunStatusRecord, "updatedAt" | "analysis">, nowMs: number): RunAnalysisPhase | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* A `running` record whose heartbeat is stale and whose pid no process on this host holds: its
|
|
168
|
+
* owner is gone, so the run cannot still be writing. A live pid proves nothing either way (pids
|
|
169
|
+
* are reused), so such a record stays unconfirmed.
|
|
170
|
+
*/
|
|
171
|
+
export declare function runOwnerGone(record: Pick<RunStatusRecord, "state" | "pid" | "updatedAt">, nowMs: number, running?: (pid: number) => boolean): boolean;
|
|
117
172
|
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
118
173
|
export declare function isRunStatusRecord(value: unknown): value is RunStatusRecord;
|
|
119
174
|
export {};
|
package/dist/run/status.js
CHANGED
|
@@ -20,7 +20,7 @@ export const RUN_STATUS_FILE = "status.json";
|
|
|
20
20
|
export const RUN_STATUS_TOUCH_MS = 5_000;
|
|
21
21
|
/**
|
|
22
22
|
* A `running` record whose `updatedAt` is older than this is interrupted, not alive: the process
|
|
23
|
-
* died
|
|
23
|
+
* died before the run ended (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
24
24
|
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
25
25
|
*/
|
|
26
26
|
export const RUN_STATUS_STALE_MS = RUN_STATUS_TOUCH_MS * 3;
|
|
@@ -132,6 +132,48 @@ export function beginRunStatus(runPaths, options) {
|
|
|
132
132
|
get interrupted() {
|
|
133
133
|
return interrupted;
|
|
134
134
|
},
|
|
135
|
+
async abandon() {
|
|
136
|
+
if (finished)
|
|
137
|
+
return;
|
|
138
|
+
finished = true;
|
|
139
|
+
heartbeat.stop();
|
|
140
|
+
const completedAt = iso();
|
|
141
|
+
await write({
|
|
142
|
+
...base,
|
|
143
|
+
state: "interrupted",
|
|
144
|
+
updatedAt: completedAt,
|
|
145
|
+
completedAt,
|
|
146
|
+
reason: "no-publish",
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
async beginAnalysis() {
|
|
150
|
+
const ended = async () => { };
|
|
151
|
+
if (finishedRecord === undefined || interrupted)
|
|
152
|
+
return ended;
|
|
153
|
+
const startedAt = iso();
|
|
154
|
+
finishedRecord = { ...finishedRecord, updatedAt: startedAt, analysis: { startedAt } };
|
|
155
|
+
await write(finishedRecord);
|
|
156
|
+
const analysisHeartbeat = startHeartbeat(clock, RUN_STATUS_TOUCH_MS, () => {
|
|
157
|
+
if (finishedRecord === undefined)
|
|
158
|
+
return;
|
|
159
|
+
finishedRecord = { ...finishedRecord, updatedAt: iso() };
|
|
160
|
+
void write(finishedRecord);
|
|
161
|
+
});
|
|
162
|
+
let done = false;
|
|
163
|
+
return async (state) => {
|
|
164
|
+
if (done || finishedRecord?.analysis === undefined)
|
|
165
|
+
return;
|
|
166
|
+
done = true;
|
|
167
|
+
analysisHeartbeat.stop();
|
|
168
|
+
const endedAt = iso();
|
|
169
|
+
finishedRecord = {
|
|
170
|
+
...finishedRecord,
|
|
171
|
+
updatedAt: endedAt,
|
|
172
|
+
analysis: { ...finishedRecord.analysis, endedAt, state },
|
|
173
|
+
};
|
|
174
|
+
await write(finishedRecord);
|
|
175
|
+
};
|
|
176
|
+
},
|
|
135
177
|
suspensions: () => heartbeat.suspensions(),
|
|
136
178
|
};
|
|
137
179
|
return handle;
|
|
@@ -150,6 +192,50 @@ export function classifyRunStatus(record, nowMs, staleMs = RUN_STATUS_STALE_MS)
|
|
|
150
192
|
return "interrupted";
|
|
151
193
|
return nowMs - updated <= staleMs ? "running" : "interrupted";
|
|
152
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The record's automatic analysis. `stopped` is an analysis with no end whose heartbeat went
|
|
197
|
+
* stale: the process ended during it. Undefined when the record has none, or one this version
|
|
198
|
+
* cannot read.
|
|
199
|
+
*/
|
|
200
|
+
export function runAnalysisPhase(record, nowMs) {
|
|
201
|
+
const analysis = record.analysis;
|
|
202
|
+
if (analysis === null || typeof analysis !== "object")
|
|
203
|
+
return undefined;
|
|
204
|
+
const { startedAt, endedAt, state } = analysis;
|
|
205
|
+
if (typeof startedAt !== "string" || !Number.isFinite(Date.parse(startedAt)))
|
|
206
|
+
return undefined;
|
|
207
|
+
if (typeof endedAt === "string")
|
|
208
|
+
return {
|
|
209
|
+
state: "ended",
|
|
210
|
+
startedAt,
|
|
211
|
+
endedAt,
|
|
212
|
+
...(typeof state === "string" ? { outcome: state } : {}),
|
|
213
|
+
};
|
|
214
|
+
const updated = Date.parse(record.updatedAt);
|
|
215
|
+
return Number.isFinite(updated) && nowMs - updated <= RUN_STATUS_STALE_MS
|
|
216
|
+
? { state: "running", startedAt }
|
|
217
|
+
: { state: "stopped", startedAt };
|
|
218
|
+
}
|
|
219
|
+
/** Whether `pid` names a process on this host. EPERM means one exists that this user cannot signal. */
|
|
220
|
+
function processRunning(pid) {
|
|
221
|
+
try {
|
|
222
|
+
process.kill(pid, 0);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return error.code === "EPERM";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A `running` record whose heartbeat is stale and whose pid no process on this host holds: its
|
|
231
|
+
* owner is gone, so the run cannot still be writing. A live pid proves nothing either way (pids
|
|
232
|
+
* are reused), so such a record stays unconfirmed.
|
|
233
|
+
*/
|
|
234
|
+
export function runOwnerGone(record, nowMs, running = processRunning) {
|
|
235
|
+
if (record.state !== "running" || classifyRunStatus(record, nowMs) === "running")
|
|
236
|
+
return false;
|
|
237
|
+
return record.pid !== process.pid && !running(record.pid);
|
|
238
|
+
}
|
|
153
239
|
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
154
240
|
export function isRunStatusRecord(value) {
|
|
155
241
|
if (value === null || typeof value !== "object")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReviewSummary } from "./bundle.js";
|
|
2
2
|
import { type RunDisplay } from "./display.js";
|
|
3
|
+
import { type RunAnalysisPhase } from "./status.js";
|
|
3
4
|
import { type VerifyResult } from "../verify/verify.js";
|
|
4
5
|
declare const RUNS_SCHEMA = "humanish.runs-result.v1";
|
|
5
6
|
export interface RunsResult {
|
|
@@ -13,6 +14,8 @@ export interface RunsResult {
|
|
|
13
14
|
path: string;
|
|
14
15
|
/** How the run reads (runDisplay in src/run/display.ts); absent when it has no readable run.json. */
|
|
15
16
|
display?: RunDisplay;
|
|
17
|
+
/** The automatic analysis its own process started after the run ended, from status.json. */
|
|
18
|
+
analysis?: RunAnalysisPhase;
|
|
16
19
|
}>;
|
|
17
20
|
latest: string | null;
|
|
18
21
|
error?: {
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -6,7 +6,7 @@ import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutpu
|
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
7
|
import { isReviewSummary, isRunBundle, isRunOutcome } from "./bundle-shape.js";
|
|
8
8
|
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
|
-
import { RUN_STATUS_FILE } from "./status.js";
|
|
9
|
+
import { isRunStatusRecord, RUN_STATUS_FILE, runAnalysisPhase, } from "./status.js";
|
|
10
10
|
import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
12
12
|
import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
|
|
@@ -106,10 +106,15 @@ async function listedRunDisplay(runPaths, bundle) {
|
|
|
106
106
|
const displayed = displayedBundle(bundle);
|
|
107
107
|
if (displayed === undefined)
|
|
108
108
|
return {};
|
|
109
|
-
const status =
|
|
110
|
-
|
|
109
|
+
const status = runJsonValue(await readRunJsonIfExists(runPaths, RUN_STATUS_FILE));
|
|
110
|
+
const analysis = isRunStatusRecord(status) && status.runId === displayed.runId
|
|
111
|
+
? runAnalysisPhase(status, Date.now())
|
|
111
112
|
: undefined;
|
|
112
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
// A bundle with an outcome decides how the run ended; status.json only adds its analysis.
|
|
115
|
+
display: runDisplay(bundleDisplayFacts(displayed, displayed.outcome === undefined ? status : undefined)),
|
|
116
|
+
...(analysis === undefined ? {} : { analysis }),
|
|
117
|
+
};
|
|
113
118
|
}
|
|
114
119
|
/** The fields runDisplay reads from an unchecked run.json; none when it does not name a run. */
|
|
115
120
|
function displayedBundle(bundle) {
|
package/dist/study/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { isLocalBrowserStudy } from "../substrates/local/runtime-config.js";
|
|
2
|
+
import { isLocalBrowserStudy, localStudySetsScreens } from "../substrates/local/runtime-config.js";
|
|
3
3
|
import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
4
4
|
import { capacityShortfall, describeCapacity, } from "../substrates/local/capacity.js";
|
|
5
5
|
import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
@@ -204,7 +204,7 @@ export async function studySetupChecks(args) {
|
|
|
204
204
|
if (route === "scripted")
|
|
205
205
|
checks.push(await scriptedBrowserCheck());
|
|
206
206
|
checks.push(...subjectEnvChecks(requiredSubjectEnv(requirements), args));
|
|
207
|
-
const analysis = automaticAnalysisBudget(config.review?.analysis, route);
|
|
207
|
+
const analysis = automaticAnalysisBudget(config.review?.analysis, route, undefined, config.actor);
|
|
208
208
|
if (analysis)
|
|
209
209
|
checks.push(await analysisCheck(analysis, args, checkAccount));
|
|
210
210
|
checks.push(checkScope(analysis));
|
|
@@ -231,10 +231,13 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
|
|
|
231
231
|
media: config.execution?.desktop?.media !== undefined ||
|
|
232
232
|
config.execution?.desktop?.recording !== undefined,
|
|
233
233
|
})))();
|
|
234
|
+
const screens = runtime.installed && runtime.assets?.displaySizes !== true && localStudySetsScreens(config)
|
|
235
|
+
? "The installed local runtime starts every participant at 960×720, and this study sets other screen sizes. Omit device and resolution, use a hosted desktop, or set HUMANISH_LOCAL_RUNTIME_IMAGE to a runtime built from current source."
|
|
236
|
+
: undefined;
|
|
234
237
|
checks.push({
|
|
235
238
|
name: "local browser runtime",
|
|
236
|
-
ok: runtime.ok,
|
|
237
|
-
message: runtime.message,
|
|
239
|
+
ok: runtime.ok && screens === undefined,
|
|
240
|
+
message: screens ?? runtime.message,
|
|
238
241
|
...(runtime.ok && !runtime.installed ? { status: "note" } : {}),
|
|
239
242
|
});
|
|
240
243
|
if (runtime.capacity !== undefined && desktopsAtOnce !== undefined)
|
|
@@ -252,7 +255,9 @@ async function localBrowserChecks(config, args, desktopsAtOnce) {
|
|
|
252
255
|
}
|
|
253
256
|
return {
|
|
254
257
|
checks,
|
|
255
|
-
...(runtime.ok && runtime.installed
|
|
258
|
+
...(runtime.ok && runtime.installed && screens === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { next: screens ?? runtime.message }),
|
|
256
261
|
};
|
|
257
262
|
}
|
|
258
263
|
/** Whether the desktops the study runs at once fit the VM or this machine, and why. */
|
|
@@ -384,7 +389,7 @@ async function analysisCheck(analysis, args, checkAccount) {
|
|
|
384
389
|
...(keyed ? {} : { status: "note" }),
|
|
385
390
|
message: keyed
|
|
386
391
|
? `OPENAI_API_KEY is present for the separate automatic analysis request; model access and quota are not tested. The analysis is refused before it starts if ${admissionRule(`$${analysis.maxCostUsd}`)}; this is not a billing cap. Participant readiness is independent.`
|
|
387
|
-
: "Will be skipped: OPENAI_API_KEY is missing. The participant may run, but there will be no automatic findings report. Add an OpenAI API key or set review.analysis: false deliberately.",
|
|
392
|
+
: "Will be skipped: OPENAI_API_KEY is missing. The participant may run, but there will be no automatic findings report. Add an OpenAI API key, or set review.analysis.provider: codex to have a signed-in Codex account analyze the run instead, or set review.analysis: false deliberately.",
|
|
388
393
|
};
|
|
389
394
|
}
|
|
390
395
|
/** What doctor checked and what it did not. */
|
|
@@ -62,6 +62,7 @@ Studies:
|
|
|
62
62
|
- private or machine-local studies live in ignored .humanish/studies/*.yaml or
|
|
63
63
|
.humanish/local/studies/*.yaml;
|
|
64
64
|
- run a study with \`${command} run <study>\`, or \`${command} watch <study>\` to stay attached;
|
|
65
|
+
- a live run ends with findings from a separate analyst; \`${command} review --run latest\` shows them;
|
|
65
66
|
- files under humanish/labs/ move there with \`${command} migrate\`.
|
|
66
67
|
|
|
67
68
|
Format standard:
|
|
@@ -79,12 +80,17 @@ Format standard:
|
|
|
79
80
|
*/
|
|
80
81
|
function tryLiveStudy(actor, localAgent = "codex") {
|
|
81
82
|
const account = actor === "local-agent";
|
|
83
|
+
const analyst = localAgent === "codex"
|
|
84
|
+
? `The analysis after the run uses the same Codex account. Set review.analysis: false to skip
|
|
85
|
+
it.`
|
|
86
|
+
: `When OPENAI_API_KEY is set, the analysis after the run bills it: humanish refuses that request
|
|
87
|
+
before it starts if its estimate is over $${DEFAULT_ANALYSIS_MAX_COST_USD}. With Codex also signed
|
|
88
|
+
in, review.analysis.provider: codex has its account analyze the run instead. Set
|
|
89
|
+
review.analysis: false to skip it.`;
|
|
82
90
|
const needs = account
|
|
83
91
|
? ` Needs E2B_API_KEY and the coding agent signed in on this machine. Cost: one small task, a few
|
|
84
92
|
E2B desktop minutes and the agent's account usage. humanish cannot put a dollar cap on account
|
|
85
|
-
usage, so the ten-minute session limit below bounds the run.
|
|
86
|
-
analysis after the run bills it: humanish refuses that request before it starts if its estimate
|
|
87
|
-
is over $${DEFAULT_ANALYSIS_MAX_COST_USD}. Set review.analysis: false to skip it.`
|
|
93
|
+
usage, so the ten-minute session limit below bounds the run. ${analyst}`
|
|
88
94
|
: ` Needs E2B_API_KEY and OPENAI_API_KEY. Cost: one small task with a $2 cap on estimated model
|
|
89
95
|
spend. The run stops before its next model request once the estimate passes $2, so the last
|
|
90
96
|
request can go slightly over, and hosted desktop time is billed separately. The analysis after
|