humanish 0.109.1 → 0.110.1
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/ARCHITECTURE.md +14 -11
- package/CHANGELOG.md +24 -0
- package/CONTEXT.md +8 -6
- package/README.md +97 -48
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +11 -0
- package/dist/actors/local-agent/cli.js +103 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +18 -11
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +96 -44
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +89 -67
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/commands/study-format.js +29 -2
- package/dist/cli/commands/study-route-computer-use.js +2 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +2 -0
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/study.js +4 -3
- package/dist/cli/commands/tui.js +4 -3
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/doctor.d.ts +7 -2
- package/dist/cli/doctor.js +47 -8
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +316 -0
- package/dist/cli/first-run-path.d.ts +5 -4
- package/dist/cli/first-run-path.js +14 -7
- package/dist/cli/io.d.ts +17 -3
- package/dist/cli/io.js +22 -4
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/comms/receiving-runtime.js +1 -1
- package/dist/comms/setup.js +1 -1
- package/dist/evidence/redaction.d.ts +7 -3
- package/dist/evidence/redaction.js +68 -24
- package/dist/feedback/export-bundle.js +6 -1
- package/dist/feedback/export.js +3 -0
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.d.ts +7 -0
- package/dist/keys/key-resolution.js +10 -8
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +18 -15
- package/dist/observer/render.js +8 -2
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +43 -34
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +4 -4
- package/dist/routes/computer-use/preflight.js +2 -2
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/computer-use/types.d.ts +6 -0
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/scripted/types.d.ts +1 -0
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/bundle.js +4 -2
- package/dist/routes/terminal/credentials.js +1 -1
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +23 -17
- package/dist/routes/terminal/plan.js +4 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/session.js +1 -1
- package/dist/routes/terminal/types.d.ts +3 -0
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +26 -1
- package/dist/run/bundle.d.ts +30 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +11 -7
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +10 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +18 -14
- package/dist/run/reclaim.js +179 -105
- package/dist/run/results.d.ts +11 -18
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +177 -72
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +138 -0
- package/dist/run/sandbox-ids.d.ts +53 -0
- package/dist/run/sandbox-ids.js +198 -0
- package/dist/run/sandbox-receipts.d.ts +16 -0
- package/dist/run/sandbox-receipts.js +70 -0
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -20
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -137
- package/dist/run/streams.d.ts +3 -2
- package/dist/run-study.js +6 -1
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +7 -4
- package/dist/study/init-templates.js +2 -2
- package/dist/study/init.js +14 -3
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/types.d.ts +1 -1
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +128 -35
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui-app.js +133 -133
- package/dist/verify/artifacts.d.ts +4 -0
- package/dist/verify/artifacts.js +8 -0
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +53 -28
- package/dist/verify/verify.d.ts +4 -1
- package/dist/verify/verify.js +27 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +90 -7
- package/docs/contracts/schemas.md +39 -22
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +5 -3
- package/skills/humanish/SKILL.md +18 -4
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// The run command's signal handling outside post-run analysis. The first interrupt, terminate or
|
|
2
|
-
// hangup signal
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// hangup signal prints one line saying humanish is stopping, refuses any further sandbox create,
|
|
3
|
+
// marks each unfinished run in this process interrupted, reclaims its sandboxes (by receipt, by
|
|
4
|
+
// the ids its in-flight creates report, and by its E2B tags), runs the registered shutdown
|
|
5
|
+
// cleanups (watch's Observer server and tunnel) and exits 128+n, all within one deadline. A
|
|
6
|
+
// second signal prints the reclaim command for each run not yet reclaimed and exits at once.
|
|
7
|
+
// Routes take no abort signal, so the bundle is not finished, but the record ends at once and no
|
|
8
|
+
// sandbox the run created waits for its timeout. Analysis has its own cancel handlers
|
|
7
9
|
// (analysis-signals.ts) and takes over when it starts.
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
import { activeRuns } from "../../run/active-runs.js";
|
|
10
12
|
import { LATEST_RUN_ALIAS } from "../../run/locate.js";
|
|
11
13
|
import { reclaimPinnedRunSandboxes, } from "../../run/reclaim.js";
|
|
14
|
+
import { stopSandboxCreates } from "../../run/sandbox-creates.js";
|
|
12
15
|
import { SANDBOX_RECEIPTS_ARTIFACT } from "../../run/sandbox-receipts.js";
|
|
16
|
+
import { plural } from "../../run/text.js";
|
|
17
|
+
import { shellArg } from "../../substrates/shell.js";
|
|
13
18
|
import { exitCodeForSignal } from "../observer-follow.js";
|
|
14
19
|
/** How long the handler waits for interrupt, reclaim and cleanups before it exits anyway. */
|
|
15
20
|
const INTERRUPT_RECLAIM_DEADLINE_MS = 10_000;
|
|
16
|
-
/** Each kill's own request timeout, inside the deadline. */
|
|
21
|
+
/** Each kill's own request timeout, inside the deadline. Kills run concurrently. */
|
|
17
22
|
const KILL_REQUEST_TIMEOUT_MS = 8_000;
|
|
23
|
+
/** How long reclaim waits for in-flight creates before it searches E2B by tag anyway. */
|
|
24
|
+
const CREATES_WAIT_MS = 6_000;
|
|
18
25
|
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
19
26
|
let current;
|
|
20
27
|
const shutdownCleanups = new Set();
|
|
@@ -39,7 +46,7 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
39
46
|
process.exit(code);
|
|
40
47
|
});
|
|
41
48
|
const handlers = new Map();
|
|
42
|
-
let stopping
|
|
49
|
+
let stopping;
|
|
43
50
|
const end = () => {
|
|
44
51
|
for (const [signal, handler] of handlers)
|
|
45
52
|
target.removeListener(signal, handler);
|
|
@@ -48,16 +55,23 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
48
55
|
current = undefined;
|
|
49
56
|
};
|
|
50
57
|
const release = () => {
|
|
51
|
-
if (
|
|
58
|
+
if (stopping === undefined)
|
|
52
59
|
end();
|
|
53
60
|
};
|
|
54
61
|
const onSignal = (signal) => {
|
|
55
|
-
if (stopping) {
|
|
62
|
+
if (stopping !== undefined) {
|
|
63
|
+
const open = stopping.filter((stop) => stop.report !== null && stop.report?.clean !== true);
|
|
64
|
+
for (const stop of open)
|
|
65
|
+
say(io, `humanish: second ${signal}: exiting before run ${stop.run.runId}'s sandboxes were confirmed stopped; ${recovery(stop.run)} to stop what is left.`);
|
|
66
|
+
if (open.length === 0)
|
|
67
|
+
say(io, `humanish: second ${signal}: exiting.`);
|
|
56
68
|
exit(exitCodeForSignal(signal));
|
|
57
69
|
return;
|
|
58
70
|
}
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// Synchronous, before anything is awaited: no create can start after this point.
|
|
72
|
+
stopping = activeRuns().map((run) => ({ run, creates: stopSandboxCreates(run.paths) }));
|
|
73
|
+
say(io, stoppingLine(signal, stopping, options.deadlineMs ?? INTERRUPT_RECLAIM_DEADLINE_MS));
|
|
74
|
+
void stopActiveRuns(io, signal, stopping, options)
|
|
61
75
|
.catch(() => undefined)
|
|
62
76
|
.finally(() => exit(exitCodeForSignal(signal)));
|
|
63
77
|
};
|
|
@@ -74,48 +88,65 @@ export function beginRunSignalPhase(io, options = {}) {
|
|
|
74
88
|
export function handOverRunSignals() {
|
|
75
89
|
current?.release();
|
|
76
90
|
}
|
|
91
|
+
function say(io, line) {
|
|
92
|
+
try {
|
|
93
|
+
io.writeErr(`${line}\n`);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// A hung-up terminal cannot take the line; the status record and reclaim receipt hold it.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function stoppingLine(signal, stopping, deadlineMs) {
|
|
100
|
+
const again = `a second ${signal} exits without waiting`;
|
|
101
|
+
if (stopping.length === 0)
|
|
102
|
+
return `humanish: ${signal}: stopping; no run had started.`;
|
|
103
|
+
const runs = stopping.map((stop) => stop.run.runId).join(", ");
|
|
104
|
+
return `humanish: ${signal}: stopping run ${runs} and reclaiming its sandboxes (up to ${deadlineMs / 1000} s); ${again}.`;
|
|
105
|
+
}
|
|
77
106
|
/**
|
|
78
107
|
* Interrupt, reclaim and the registered cleanups all run inside one deadline that starts before
|
|
79
108
|
* the first status write is awaited, so a stalled write or kill still ends in a bounded exit.
|
|
80
109
|
*/
|
|
81
|
-
async function stopActiveRuns(io, signal, options) {
|
|
110
|
+
async function stopActiveRuns(io, signal, stopping, options) {
|
|
82
111
|
const deadlineMs = options.deadlineMs ?? INTERRUPT_RECLAIM_DEADLINE_MS;
|
|
83
|
-
const runs = activeRuns();
|
|
84
|
-
// Absent: still pending at the deadline. Null: the run had finished, so there is nothing to say.
|
|
85
|
-
const reports = new Map();
|
|
86
112
|
const work = Promise.all([
|
|
87
|
-
...
|
|
113
|
+
...stopping.map(async (stop) => {
|
|
88
114
|
// False when the run had already finished: its route released what it held.
|
|
89
|
-
if (!(await run.status.interrupt(signal))) {
|
|
90
|
-
|
|
115
|
+
if (!(await stop.run.status.interrupt(signal))) {
|
|
116
|
+
stop.report = null;
|
|
91
117
|
return;
|
|
92
118
|
}
|
|
93
|
-
const reclaimed = await reclaimPinnedRunSandboxes(run.cwd, run.paths, {
|
|
119
|
+
const reclaimed = await reclaimPinnedRunSandboxes(stop.run.cwd, stop.run.paths, {
|
|
94
120
|
requestTimeoutMs: KILL_REQUEST_TIMEOUT_MS,
|
|
121
|
+
createsWaitMs: Math.min(CREATES_WAIT_MS, deadlineMs / 2),
|
|
95
122
|
...options.reclaim,
|
|
123
|
+
creates: stop.creates,
|
|
96
124
|
}).catch(() => undefined);
|
|
97
|
-
|
|
125
|
+
stop.report = reclaimSummary(stop.run, reclaimed);
|
|
98
126
|
}),
|
|
99
127
|
...[...shutdownCleanups].map((cleanup) => cleanup().catch(() => undefined)),
|
|
100
128
|
]);
|
|
101
|
-
const finished = await withDeadline(work, deadlineMs);
|
|
102
|
-
for (const
|
|
103
|
-
|
|
104
|
-
if (report === null)
|
|
129
|
+
const finished = await withDeadline(work, deadlineMs, options.deadline);
|
|
130
|
+
for (const stop of stopping) {
|
|
131
|
+
if (stop.report === null)
|
|
105
132
|
continue;
|
|
106
|
-
const text = report ??
|
|
107
|
-
|
|
108
|
-
? `stopping did not finish within ${deadlineMs / 1000} s; ${recovery(run)}.`
|
|
109
|
-
: `reclaim failed; ${recovery(run)}.`);
|
|
110
|
-
try {
|
|
111
|
-
io.writeErr(`humanish: ${signal}: run ${run.runId} marked interrupted; ${text}\n`);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// A hung-up terminal cannot take the line; the status record and reclaim receipt hold it.
|
|
115
|
-
}
|
|
133
|
+
const text = stop.report?.text ?? deadlineText(stop, finished, deadlineMs);
|
|
134
|
+
say(io, `humanish: ${signal}: run ${stop.run.runId} marked interrupted; ${text}`);
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
|
-
function
|
|
137
|
+
function deadlineText(stop, finished, deadlineMs) {
|
|
138
|
+
if (finished !== "deadline")
|
|
139
|
+
return `reclaim failed; sandboxes unknown; ${recovery(stop.run)}.`;
|
|
140
|
+
const inFlight = stop.creates.inFlight();
|
|
141
|
+
const creates = inFlight === 0 ? "" : ` Still waiting on E2B: ${plural(inFlight, "sandbox create")}.`;
|
|
142
|
+
return `stopping did not finish within ${deadlineMs / 1000} s; sandboxes unknown.${creates} ${capitalize(recovery(stop.run))}; it also finds sandboxes by this run's E2B tags.`;
|
|
143
|
+
}
|
|
144
|
+
function capitalize(text) {
|
|
145
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
146
|
+
}
|
|
147
|
+
function withDeadline(work, ms, wait) {
|
|
148
|
+
if (wait !== undefined)
|
|
149
|
+
return Promise.race([work, wait(ms).then(() => "deadline")]);
|
|
119
150
|
let timer;
|
|
120
151
|
const deadline = new Promise((resolve) => {
|
|
121
152
|
timer = setTimeout(() => resolve("deadline"), ms);
|
|
@@ -127,20 +158,41 @@ function withDeadline(work, ms) {
|
|
|
127
158
|
* to reclaim, which would resolve the newest run instead, so it gets its receipts path.
|
|
128
159
|
*/
|
|
129
160
|
function recovery(run) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
161
|
+
if (run.runId === LATEST_RUN_ALIAS)
|
|
162
|
+
return `its id is \`${LATEST_RUN_ALIAS}\`, which \`humanish reclaim --run\` reads as the newest run, so kill the ids in ${path.join(run.paths.relativeRunRoot, SANDBOX_RECEIPTS_ARTIFACT)} by hand`;
|
|
163
|
+
const cwd = path.resolve(run.cwd) === process.cwd() ? "" : ` --cwd ${shellArg(run.cwd)}`;
|
|
164
|
+
return `run \`humanish reclaim --run ${shellArg(run.runId)}${cwd}\``;
|
|
133
165
|
}
|
|
134
166
|
function reclaimSummary(run, reclaimed) {
|
|
135
167
|
if (reclaimed === undefined)
|
|
136
|
-
return `reclaim failed; ${recovery(run)}
|
|
168
|
+
return { text: `reclaim failed; sandboxes unknown; ${recovery(run)}.`, clean: false };
|
|
137
169
|
if (reclaimed.error !== undefined)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
return {
|
|
171
|
+
text: `${reclaimed.error.message} Sandboxes unknown; ${recovery(run)}.`,
|
|
172
|
+
clean: false,
|
|
173
|
+
};
|
|
141
174
|
const counts = new Map();
|
|
142
175
|
for (const outcome of reclaimed.outcomes)
|
|
143
176
|
counts.set(outcome.state, (counts.get(outcome.state) ?? 0) + 1);
|
|
144
|
-
const tally =
|
|
145
|
-
|
|
177
|
+
const tally = counts.size === 0
|
|
178
|
+
? "none created"
|
|
179
|
+
: [...counts].map(([state, count]) => `${count} ${state}`).join(", ");
|
|
180
|
+
switch (reclaimed.state) {
|
|
181
|
+
case "clean":
|
|
182
|
+
return {
|
|
183
|
+
text: `sandboxes clean: ${tally}; E2B lists none still tagged with this run.`,
|
|
184
|
+
clean: true,
|
|
185
|
+
};
|
|
186
|
+
case "unconfirmed":
|
|
187
|
+
return { text: `sandboxes unconfirmed: ${tally}; ${recovery(run)} to retry.`, clean: false };
|
|
188
|
+
default: {
|
|
189
|
+
const why = reclaimed.createsInFlight > 0
|
|
190
|
+
? `${plural(reclaimed.createsInFlight, "sandbox create")} still waiting on E2B`
|
|
191
|
+
: `the E2B tag search ${reclaimed.tagSearch.status}${reclaimed.tagSearch.detail === undefined ? "" : ` (${reclaimed.tagSearch.detail})`}`;
|
|
192
|
+
return {
|
|
193
|
+
text: `sandboxes unknown: ${tally}, but ${why}; ${recovery(run)}.`,
|
|
194
|
+
clean: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
146
198
|
}
|
|
@@ -2,6 +2,11 @@ import { Command } from "commander";
|
|
|
2
2
|
import { type CliIo } from "../io.js";
|
|
3
3
|
export declare function registerRunCommand(parent: Command, io: CliIo): void;
|
|
4
4
|
export declare function registerVerifyCommand(parent: Command, io: CliIo): void;
|
|
5
|
+
/**
|
|
6
|
+
* The deprecated `cleanup`, hidden from help: one warning, then exactly `reclaim --check`, with
|
|
7
|
+
* its output and exit code. The old command read only what computer-use runs recorded and passed
|
|
8
|
+
* when it found no record; the check asks E2B.
|
|
9
|
+
*/
|
|
5
10
|
export declare function registerCleanupCommand(parent: Command, io: CliIo): void;
|
|
6
11
|
export declare function registerReviewCommand(parent: Command, io: CliIo): void;
|
|
7
12
|
export declare function registerExportCommand(parent: Command, io: CliIo): void;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
|
-
import {
|
|
3
|
+
import { shellArg } from "../../substrates/shell.js";
|
|
4
4
|
import { computeStats, formatStatsHuman } from "../../run/stats.js";
|
|
5
5
|
import { DEFAULT_EXPORT_MAX_BYTES, exportRun, formatExportHuman } from "../../feedback/export.js";
|
|
6
|
-
import {
|
|
6
|
+
import { listRuns, readReview } from "../../run/stored-runs.js";
|
|
7
7
|
import { verifyRun } from "../../verify/verify.js";
|
|
8
8
|
import { reclaimPreflightSandboxes, reclaimRunSandboxes, } from "../../run/reclaim.js";
|
|
9
9
|
import { addRunOptions, handleRun } from "./run-command.js";
|
|
10
|
-
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION,
|
|
10
|
+
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
|
|
11
11
|
import { plural } from "../../run/text.js";
|
|
12
|
+
import { warnAndQueue } from "../deprecations.js";
|
|
13
|
+
import { formatFindings, readRunFindings } from "../findings.js";
|
|
12
14
|
export function registerRunCommand(parent, io) {
|
|
13
15
|
addRunOptions(parent
|
|
14
16
|
.command("run")
|
|
@@ -30,30 +32,40 @@ export function registerVerifyCommand(parent, io) {
|
|
|
30
32
|
io.setExitCode(result.ok ? 0 : 2);
|
|
31
33
|
});
|
|
32
34
|
}
|
|
35
|
+
const CLEANUP_RENAMED = "`cleanup` is now `reclaim --check`; `cleanup` is removed in 0.112.0.";
|
|
36
|
+
/**
|
|
37
|
+
* The deprecated `cleanup`, hidden from help: one warning, then exactly `reclaim --check`, with
|
|
38
|
+
* its output and exit code. The old command read only what computer-use runs recorded and passed
|
|
39
|
+
* when it found no record; the check asks E2B.
|
|
40
|
+
*/
|
|
33
41
|
export function registerCleanupCommand(parent, io) {
|
|
34
42
|
parent
|
|
35
|
-
.command("cleanup")
|
|
36
|
-
.description(
|
|
37
|
-
.summary("Check that a run's resources were stopped.")
|
|
38
|
-
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
43
|
+
.command("cleanup", { hidden: true })
|
|
44
|
+
.description(`Deprecated: ${CLEANUP_RENAMED}`)
|
|
39
45
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
46
|
+
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
47
|
+
.option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
|
|
48
|
+
.addOption(envFileAliasOption())
|
|
40
49
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
41
50
|
.action(async (options, command) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
io.setExitCode(result.ok ? 0 : 2);
|
|
51
|
+
warnAndQueue(command, io, CLEANUP_RENAMED);
|
|
52
|
+
await runReclaimCommand(io, command, { ...options, check: true });
|
|
45
53
|
});
|
|
46
54
|
}
|
|
47
55
|
export function registerReviewCommand(parent, io) {
|
|
48
56
|
parent
|
|
49
57
|
.command("review")
|
|
50
|
-
.description("Show a run's review: verdict, summary and
|
|
51
|
-
.summary("
|
|
58
|
+
.description("Show a run's review: verdict, summary, gaps and the findings of its analysis, with the evidence each one cites.")
|
|
59
|
+
.summary("Show a run's outcome and analysis findings.")
|
|
52
60
|
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
53
61
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
54
62
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
55
63
|
.action(async (options, command) => {
|
|
56
|
-
const
|
|
64
|
+
const review = await readReview(options.cwd, options.run);
|
|
65
|
+
// The verified run's exact id, so a run that moves `latest` meanwhile cannot swap the analysis.
|
|
66
|
+
const result = "verdict" in review
|
|
67
|
+
? { ...review, analysis: await readRunFindings(options.cwd, review.runId) }
|
|
68
|
+
: review;
|
|
57
69
|
writeResult(command, io, result, formatReviewHuman);
|
|
58
70
|
io.setExitCode("ok" in result && result.ok === false ? 2 : 0);
|
|
59
71
|
});
|
|
@@ -165,34 +177,57 @@ export function registerRuntimeCommands(parent, io) {
|
|
|
165
177
|
export function registerReclaimCommand(parent, io) {
|
|
166
178
|
parent
|
|
167
179
|
.command("reclaim")
|
|
168
|
-
.description("Kill an interrupted run's sandboxes
|
|
169
|
-
.summary("Stop an interrupted run's
|
|
180
|
+
.description("Kill an interrupted run's sandboxes: the exact ids journaled in its sandbox-receipts.ndjson, and any sandbox E2B lists with this run's owner tags (a sandbox whose id never reached a receipt). It lists only sandboxes that match every tag of this run. --check asks E2B whether each still exists and kills nothing. Needs E2B_API_KEY in the environment or in --dotenv.")
|
|
181
|
+
.summary("Stop or check an interrupted run's sandboxes.")
|
|
170
182
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
171
183
|
.option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
|
|
172
184
|
.addOption(new Option("--preflight", "Reclaim sandboxes left by interrupted `humanish study check` probes (journaled in .humanish/preflight) instead of a run's.").conflicts("run"))
|
|
173
|
-
.option("--
|
|
185
|
+
.option("--check", "Ask E2B whether each sandbox still exists; kill nothing and write nothing. Exits 0 only when all are gone.")
|
|
186
|
+
.option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
|
|
187
|
+
.addOption(envFileAliasOption())
|
|
174
188
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
175
|
-
.action(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
.action((options, command) => runReclaimCommand(io, command, options));
|
|
190
|
+
}
|
|
191
|
+
/** `reclaim`, and the deprecated `cleanup`, which runs it with `check`. */
|
|
192
|
+
async function runReclaimCommand(io, command, options) {
|
|
193
|
+
// The kill calls read E2B_API_KEY from the environment: the env file, then discovered keys.
|
|
194
|
+
if (!(await applyEnvFileOption({
|
|
195
|
+
command,
|
|
196
|
+
cwd: options.cwd,
|
|
197
|
+
envFile: dotenvPathOf(options, command, io),
|
|
198
|
+
io,
|
|
199
|
+
})))
|
|
200
|
+
return;
|
|
201
|
+
const hooks = { check: options.check === true };
|
|
202
|
+
const result = options.preflight
|
|
203
|
+
? await reclaimPreflightSandboxes(options.cwd, hooks)
|
|
204
|
+
: await reclaimRunSandboxes(options.cwd, options.run, hooks);
|
|
205
|
+
writeResult(command, io, result, (value) => formatReclaimHuman(value, options.cwd, options.preflight === true));
|
|
206
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
185
207
|
}
|
|
186
|
-
|
|
208
|
+
/** The command that finishes what a reclaim left open, with --cwd when it was given. */
|
|
209
|
+
function reclaimCommand(result, cwd, preflight) {
|
|
210
|
+
const target = preflight ? "--preflight" : `--run ${shellArg(result.runId)}`;
|
|
211
|
+
return `humanish reclaim ${target}${cwd === "." ? "" : ` --cwd ${shellArg(cwd)}`}`;
|
|
212
|
+
}
|
|
213
|
+
function formatReclaimHuman(result, cwd, preflight) {
|
|
187
214
|
const lines = [];
|
|
188
|
-
|
|
215
|
+
const check = result.mode === "check";
|
|
216
|
+
const search = result.tagSearch.status === "done"
|
|
217
|
+
? `E2B lists ${plural(result.tagSearch.found, "more sandbox", "more sandboxes")} tagged with it`
|
|
218
|
+
: `E2B tag search ${result.tagSearch.status}`;
|
|
219
|
+
lines.push(`${check ? "Reclaim check" : "Reclaim"} ${result.runId}: ${result.state}. ${plural(result.receiptCount, "sandbox receipt")}; ${search}.`);
|
|
189
220
|
for (const outcome of result.outcomes) {
|
|
190
|
-
lines.push(` ${outcome.
|
|
221
|
+
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}, from ${outcome.source}): ${outcome.state}${outcome.detail ? `: ${outcome.detail}` : ""}`);
|
|
191
222
|
}
|
|
192
223
|
for (const warning of result.warnings)
|
|
193
224
|
lines.push(` warning: ${warning}`);
|
|
194
|
-
|
|
195
|
-
|
|
225
|
+
if (result.error === undefined && result.state !== "clean")
|
|
226
|
+
lines.push(result.state === "running"
|
|
227
|
+
? `To stop them, run ${reclaimCommand(result, cwd, preflight)}.`
|
|
228
|
+
: `Run ${reclaimCommand(result, cwd, preflight)} again once E2B is reachable; each sandbox's create-time timeout is the backstop.`);
|
|
229
|
+
const stdout = `${lines.join("\n")}\n`;
|
|
230
|
+
return result.error === undefined ? stdout : { stdout, error: result.error };
|
|
196
231
|
}
|
|
197
232
|
const REVIEW_VERDICTS = {
|
|
198
233
|
// A dry run, or a live run with no participant result; review's result does not carry the mode.
|
|
@@ -202,7 +237,10 @@ const REVIEW_VERDICTS = {
|
|
|
202
237
|
blocked: "blocked",
|
|
203
238
|
timed_out: "timed out",
|
|
204
239
|
};
|
|
205
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* A run's review: its verdict, summary and gaps, its analysis findings, and where review.json is.
|
|
242
|
+
* A pass verdict on a run whose own ok is false leads with how the run ended (runDisplay), then why.
|
|
243
|
+
*/
|
|
206
244
|
function formatReviewHuman(result) {
|
|
207
245
|
if (!("verdict" in result)) {
|
|
208
246
|
// A run that did not pass verify has no review to show; verify says why.
|
|
@@ -219,12 +257,19 @@ function formatReviewHuman(result) {
|
|
|
219
257
|
}
|
|
220
258
|
: humanError(error);
|
|
221
259
|
}
|
|
260
|
+
const verdict = REVIEW_VERDICTS[result.verdict];
|
|
261
|
+
const display = result.display;
|
|
262
|
+
const headline = result.verdict === "pass" && display !== undefined && display.state !== "passed"
|
|
263
|
+
? `${display.label} (verdict pass)`
|
|
264
|
+
: verdict;
|
|
222
265
|
return ([
|
|
223
|
-
`humanish review ${result.runId}: ${
|
|
266
|
+
`humanish review ${result.runId}: ${headline}`,
|
|
267
|
+
...(display?.reason === undefined ? [] : [`why: ${display.reason}`]),
|
|
224
268
|
"",
|
|
225
269
|
result.summary,
|
|
226
270
|
...(result.gaps.length === 0 ? [] : ["", "gaps:", ...result.gaps.map((gap) => `- ${gap}`)]),
|
|
227
271
|
"",
|
|
272
|
+
...formatFindings(result.analysis),
|
|
228
273
|
`review: ${result.path}`,
|
|
229
274
|
].join("\n") + "\n");
|
|
230
275
|
}
|
|
@@ -240,6 +285,12 @@ function shareSafetyLines(result) {
|
|
|
240
285
|
.filter((reason) => reason.code !== "VERIFY_FAILED")
|
|
241
286
|
.map((reason) => `share-safety: ${reason.code}: ${reason.message}`);
|
|
242
287
|
}
|
|
288
|
+
/** For a run that is not finished, its liveness and sandbox state, so the line leads with them. */
|
|
289
|
+
function unfinishedNote(result) {
|
|
290
|
+
return result.unfinished === undefined
|
|
291
|
+
? ""
|
|
292
|
+
: ` (${result.unfinished.liveness}, sandboxes ${result.unfinished.sandboxes})`;
|
|
293
|
+
}
|
|
243
294
|
/**
|
|
244
295
|
* One line for a pass. A failure lists only the failing checks. `--verbose` prints every check.
|
|
245
296
|
* A run that does not exist prints only that.
|
|
@@ -248,12 +299,13 @@ function formatVerifyHuman(result) {
|
|
|
248
299
|
if (result.error?.code === "HUMANISH_RUN_NOT_FOUND")
|
|
249
300
|
return humanError(result.error);
|
|
250
301
|
const runId = verifiedRunId(result);
|
|
302
|
+
const named = `${runId}${unfinishedNote(result)}`;
|
|
251
303
|
const total = result.checks.length;
|
|
252
304
|
const failed = result.checks.filter((check) => !check.ok);
|
|
253
305
|
const lines = result.ok
|
|
254
|
-
? [`verified ${
|
|
306
|
+
? [`verified ${named} · ${result.shareSafety.status} · ${total} checks passed`]
|
|
255
307
|
: [
|
|
256
|
-
`verify failed: ${
|
|
308
|
+
`verify failed: ${named} · ${result.shareSafety.status} · ${failed.length} of ${total} checks failed`,
|
|
257
309
|
...failed.map((check) => `- ${check.message}`),
|
|
258
310
|
];
|
|
259
311
|
lines.push(...shareSafetyLines(result), ...result.warnings.map((warning) => `warning: ${warning}`));
|
|
@@ -267,43 +319,13 @@ function formatVerifyVerbose(result) {
|
|
|
267
319
|
return humanError(result.error);
|
|
268
320
|
return ([
|
|
269
321
|
`humanish verify ${result.ok ? "passed" : "failed"}`,
|
|
270
|
-
`run: ${verifiedRunId(result)}`,
|
|
322
|
+
`run: ${verifiedRunId(result)}${unfinishedNote(result)}`,
|
|
271
323
|
`share-safety: ${result.shareSafety.status}`,
|
|
272
324
|
...result.shareSafety.reasons.map((reason) => `share-safety reason: ${reason.code}: ${reason.message}`),
|
|
273
325
|
...result.checks.map((check) => `- ${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`),
|
|
274
326
|
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
275
327
|
].join("\n") + "\n");
|
|
276
328
|
}
|
|
277
|
-
/** A command-line argument, single-quoted only when the shell would split or expand it. */
|
|
278
|
-
function shellArg(value) {
|
|
279
|
-
return /^[\w@%+=:,./-]+$/.test(value) ? value : shellQuote(value);
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Cleanup only reads evidence and never stops a sandbox. A failed resource is an E2B sandbox not
|
|
283
|
-
* recorded as stopped, which reclaim can stop from the run's sandbox receipts, so the last line
|
|
284
|
-
* names it. A skipped resource is a provider reclaim does not handle, so it gets no line. `cwd` is
|
|
285
|
-
* the --cwd the command was given, since cleanup's result masks it.
|
|
286
|
-
*/
|
|
287
|
-
function reclaimPointer(result, cwd) {
|
|
288
|
-
if (result.runId === undefined || result.summary.failed === 0)
|
|
289
|
-
return [];
|
|
290
|
-
const cwdFlag = cwd === "." ? "" : ` --cwd ${shellArg(cwd)}`;
|
|
291
|
-
return [
|
|
292
|
-
`To stop leftover sandboxes, run humanish reclaim --run ${shellArg(result.runId)}${cwdFlag}.`,
|
|
293
|
-
];
|
|
294
|
-
}
|
|
295
|
-
function formatCleanupHuman(result, cwd) {
|
|
296
|
-
if (!result.ok && result.error)
|
|
297
|
-
return humanError(result.error);
|
|
298
|
-
return ([
|
|
299
|
-
`humanish cleanup ${result.ok ? "passed" : "failed"}`,
|
|
300
|
-
`run: ${result.runId ?? result.run}`,
|
|
301
|
-
`resources: already-clean ${result.summary.alreadyClean}, skipped ${result.summary.skipped}, failed ${result.summary.failed}`,
|
|
302
|
-
...(result.cleanupPath ? [`cleanup: ${result.cleanupPath}`] : []),
|
|
303
|
-
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
304
|
-
...reclaimPointer(result, cwd),
|
|
305
|
-
].join("\n") + "\n");
|
|
306
|
-
}
|
|
307
329
|
function formatRunsHuman(result) {
|
|
308
330
|
if (!result.ok)
|
|
309
331
|
return humanError(result.error);
|
|
@@ -312,6 +334,6 @@ function formatRunsHuman(result) {
|
|
|
312
334
|
}
|
|
313
335
|
return ([
|
|
314
336
|
`latest: ${result.latest ?? "none"}`,
|
|
315
|
-
...result.runs.map((run) => `- ${run.runId} ${run.mode ?? "unknown"} ${run.createdAt ?? "unknown"} ${run.path}`),
|
|
337
|
+
...result.runs.map((run) => `- ${run.runId} ${run.mode ?? "unknown"} ${run.display?.label ?? "unreadable"} ${run.createdAt ?? "unknown"} ${run.path}`),
|
|
316
338
|
].join("\n") + "\n");
|
|
317
339
|
}
|
|
@@ -4,7 +4,7 @@ import { promptSecret } from "../secret-prompt.js";
|
|
|
4
4
|
import { runInit } from "../../study/init.js";
|
|
5
5
|
import { buildPayload, disabledByEnvironment, readTelemetryState, telemetryStatePath, writeTelemetryState, } from "../telemetry.js";
|
|
6
6
|
import { doctor } from "../doctor.js";
|
|
7
|
-
import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION,
|
|
7
|
+
import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, writeResult, formatCliError, } from "../io.js";
|
|
8
8
|
import { plural } from "../../run/text.js";
|
|
9
9
|
export function registerInitCommand(parent, io) {
|
|
10
10
|
parent
|
|
@@ -51,13 +51,27 @@ export function registerDoctorCommand(parent, io) {
|
|
|
51
51
|
.summary("Check what this project and machine need for a run.")
|
|
52
52
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
53
53
|
.option("--study <study>", "Check the study's desktop, participant authentication and separate analysis requirements; no provider calls.")
|
|
54
|
-
.option("--
|
|
54
|
+
.option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
|
|
55
|
+
.addOption(envFileAliasOption())
|
|
55
56
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
56
57
|
.action(async (options, command) => {
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const dotenv = dotenvPathOf(options, command, io);
|
|
59
|
+
let loaded;
|
|
60
|
+
if (dotenv &&
|
|
61
|
+
!(await applyEnvFileOption({
|
|
62
|
+
command,
|
|
63
|
+
cwd: options.cwd,
|
|
64
|
+
envFile: dotenv,
|
|
65
|
+
io,
|
|
66
|
+
onLoaded: (load) => {
|
|
67
|
+
loaded = load;
|
|
68
|
+
},
|
|
69
|
+
})))
|
|
59
70
|
return;
|
|
60
|
-
const result = await doctor(options.cwd,
|
|
71
|
+
const result = await doctor(options.cwd, {
|
|
72
|
+
...(options.study ? { study: options.study } : {}),
|
|
73
|
+
...(loaded === undefined ? {} : { dotenv: loaded }),
|
|
74
|
+
});
|
|
61
75
|
writeResult(command, io, result, formatDoctorHuman);
|
|
62
76
|
// Behavioral change: was exit 1, every other structured command uses 2.
|
|
63
77
|
io.setExitCode(result.ok ? 0 : 2);
|
|
@@ -15,6 +15,21 @@ function withError(error, lines) {
|
|
|
15
15
|
const stdout = `${lines.join("\n")}\n`;
|
|
16
16
|
return error === undefined ? stdout : { stdout, error };
|
|
17
17
|
}
|
|
18
|
+
/** How a summary names a sandbox: by digest. The raw id is only in the run's receipts. */
|
|
19
|
+
function sandboxName(sandbox) {
|
|
20
|
+
return sandbox.sandboxIdDigest !== undefined
|
|
21
|
+
? `[redacted-sandbox-id ${sandbox.sandboxIdDigest}]`
|
|
22
|
+
: sandbox.sandboxId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One line per run naming the file that holds its raw sandbox ids, for the operator, when the
|
|
26
|
+
* result names a sandbox at all (each one carries a digest).
|
|
27
|
+
*/
|
|
28
|
+
function sandboxIdsLine(result) {
|
|
29
|
+
return result.runId && JSON.stringify(result).includes('"sandboxIdDigest"')
|
|
30
|
+
? [`sandbox ids: .humanish/runs/${result.runId}/sandbox-receipts.ndjson`]
|
|
31
|
+
: [];
|
|
32
|
+
}
|
|
18
33
|
/** A participant's status in words: a dry run's placeholder status says that nothing ran live. */
|
|
19
34
|
function participantStatus(status) {
|
|
20
35
|
return status === "contract_proof_only" ? "dry run, nothing ran live" : status;
|
|
@@ -37,9 +52,10 @@ export function formatConcurrentSharedWorldStudyHuman(result) {
|
|
|
37
52
|
...result.roles.map((participant) => `persona ${participant.id} (${participant.persona}): ${participantStatus(participant.status)}${participant.session ? ` (${participant.session.completionReason})` : ""}${participant.ok ? "" : ", not ok"}`),
|
|
38
53
|
...(result.subjectSandbox
|
|
39
54
|
? [
|
|
40
|
-
`subject sandbox: ${result.subjectSandbox
|
|
55
|
+
`subject sandbox: ${sandboxName(result.subjectSandbox)} killed=${result.subjectSandbox.killed ? "yes" : "no"}`,
|
|
41
56
|
]
|
|
42
57
|
: []),
|
|
58
|
+
...sandboxIdsLine(result),
|
|
43
59
|
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
44
60
|
...(result.observer?.opened === undefined
|
|
45
61
|
? []
|
|
@@ -53,6 +69,10 @@ export function formatTerminalStudyHuman(result) {
|
|
|
53
69
|
`run: ${result.runId}`,
|
|
54
70
|
`actor: ${result.actor}`,
|
|
55
71
|
`product: ${result.product}`,
|
|
72
|
+
...(result.sandbox
|
|
73
|
+
? [`sandbox: ${sandboxName(result.sandbox)} killed=${result.sandbox.killed ? "yes" : "no"}`]
|
|
74
|
+
: []),
|
|
75
|
+
...sandboxIdsLine(result),
|
|
56
76
|
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
57
77
|
...(result.observer?.opened === undefined
|
|
58
78
|
? []
|
|
@@ -72,6 +92,12 @@ export function formatScriptedStudyHuman(result) {
|
|
|
72
92
|
]
|
|
73
93
|
: []),
|
|
74
94
|
...result.sessions.map((session) => `session ${session.surface}: ${session.status} (${session.completionReason}) · ${session.reason} [${session.screenshots} screenshot${session.screenshots === 1 ? "" : "s"}]`),
|
|
95
|
+
...(result.subjectSandbox
|
|
96
|
+
? [
|
|
97
|
+
`subject sandbox: ${sandboxName(result.subjectSandbox)} killed=${result.subjectSandbox.killed ? "yes" : "no"}`,
|
|
98
|
+
]
|
|
99
|
+
: []),
|
|
100
|
+
...sandboxIdsLine(result),
|
|
75
101
|
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
76
102
|
...(result.observer?.opened === undefined
|
|
77
103
|
? []
|
|
@@ -105,9 +131,10 @@ export function formatCuaStudyHuman(result) {
|
|
|
105
131
|
: []),
|
|
106
132
|
...(result.sandbox
|
|
107
133
|
? [
|
|
108
|
-
`sandbox: ${result.sandbox
|
|
134
|
+
`sandbox: ${sandboxName(result.sandbox)} stream=${result.sandbox.streamUrlPresent ? "connected" : "missing"} killed=${result.sandbox.killed ? "yes" : "no"}`,
|
|
109
135
|
]
|
|
110
136
|
: []),
|
|
137
|
+
...sandboxIdsLine(result),
|
|
111
138
|
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
112
139
|
...(result.observer?.opened === undefined
|
|
113
140
|
? []
|
|
@@ -9,6 +9,7 @@ import { cliAnalysisOptions } from "./analysis-signals.js";
|
|
|
9
9
|
import { onRunShutdown } from "./run-signals.js";
|
|
10
10
|
import { parseStudyCount, parseParticipantIds, parseObserverPort, wantsJson, writeResult, } from "../io.js";
|
|
11
11
|
import { exposureRequestFromOptions, followObserver, planObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
12
|
+
import { writeRunFindings } from "../findings.js";
|
|
12
13
|
import { formatCuaStudyHuman } from "./study-format.js";
|
|
13
14
|
import { resolveRouteShouldOpen } from "./study-route-open.js";
|
|
14
15
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
@@ -237,6 +238,7 @@ async function reportCuaRun(args, prepared, result, live) {
|
|
|
237
238
|
}
|
|
238
239
|
writeResult(args.command, args.io, output, formatCuaStudyHuman);
|
|
239
240
|
args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
|
|
241
|
+
await writeRunFindings(args.command, args.io, args.options.cwd, result);
|
|
240
242
|
if (server && (result.observer?.ok || attachedObserver)) {
|
|
241
243
|
const followResult = output.observer?.ok
|
|
242
244
|
? output.observer
|
|
@@ -2,6 +2,7 @@ import { automaticAnalysisSucceeded } from "../../analysis/automatic-completion.
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { cliAnalysisOptions } from "./analysis-signals.js";
|
|
4
4
|
import { wantsJson, writeResult } from "../io.js";
|
|
5
|
+
import { writeRunFindings } from "../findings.js";
|
|
5
6
|
import { showObserver } from "../observer-follow.js";
|
|
6
7
|
import { formatScriptedStudyHuman } from "./study-format.js";
|
|
7
8
|
import { observerOpen, resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
@@ -35,6 +36,7 @@ export function scriptedRouteRun(args) {
|
|
|
35
36
|
const result = outcome.result;
|
|
36
37
|
writeResult(args.command, args.io, result, formatScriptedStudyHuman);
|
|
37
38
|
args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
|
|
39
|
+
await writeRunFindings(args.command, args.io, args.options.cwd, result);
|
|
38
40
|
// Watch mode serves the Observer the route rendered through its finished run.
|
|
39
41
|
if (finishedPlan !== undefined && result.ok && result.observer !== undefined) {
|
|
40
42
|
await showObserver({
|