humanish 0.110.0 → 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 +13 -0
- package/CONTEXT.md +8 -6
- package/README.md +96 -47
- 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/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 +86 -70
- 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/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +42 -4
- 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.js +2 -1
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- 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/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 +1 -1
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- 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/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/terminal/bundle.js +4 -2
- 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 +15 -13
- 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/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +25 -1
- package/dist/run/bundle.d.ts +24 -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 -17
- package/dist/run/reclaim.js +180 -113
- package/dist/run/results.d.ts +10 -20
- 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 +166 -67
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +138 -0
- package/dist/run/sandbox-receipts.d.ts +14 -0
- package/dist/run/sandbox-receipts.js +44 -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 -23
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -146
- package/dist/run/streams.d.ts +3 -2
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +5 -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/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/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +46 -25
- package/dist/verify/verify.d.ts +3 -0
- package/dist/verify/verify.js +6 -2
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +75 -6
- 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 +4 -3
- package/skills/humanish/SKILL.md +16 -2
|
@@ -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
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,40 +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"))
|
|
185
|
+
.option("--check", "Ask E2B whether each sandbox still exists; kill nothing and write nothing. Exits 0 only when all are gone.")
|
|
173
186
|
.option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
|
|
174
187
|
.addOption(envFileAliasOption())
|
|
175
188
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
176
|
-
.action(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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);
|
|
207
|
+
}
|
|
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)}`}`;
|
|
191
212
|
}
|
|
192
|
-
function formatReclaimHuman(result) {
|
|
213
|
+
function formatReclaimHuman(result, cwd, preflight) {
|
|
193
214
|
const lines = [];
|
|
194
|
-
|
|
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}.`);
|
|
195
220
|
for (const outcome of result.outcomes) {
|
|
196
|
-
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}): ${outcome.state}${outcome.detail ?
|
|
221
|
+
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}, from ${outcome.source}): ${outcome.state}${outcome.detail ? `: ${outcome.detail}` : ""}`);
|
|
197
222
|
}
|
|
198
223
|
for (const warning of result.warnings)
|
|
199
224
|
lines.push(` warning: ${warning}`);
|
|
200
|
-
|
|
201
|
-
|
|
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 };
|
|
202
231
|
}
|
|
203
232
|
const REVIEW_VERDICTS = {
|
|
204
233
|
// A dry run, or a live run with no participant result; review's result does not carry the mode.
|
|
@@ -208,7 +237,10 @@ const REVIEW_VERDICTS = {
|
|
|
208
237
|
blocked: "blocked",
|
|
209
238
|
timed_out: "timed out",
|
|
210
239
|
};
|
|
211
|
-
/**
|
|
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
|
+
*/
|
|
212
244
|
function formatReviewHuman(result) {
|
|
213
245
|
if (!("verdict" in result)) {
|
|
214
246
|
// A run that did not pass verify has no review to show; verify says why.
|
|
@@ -225,12 +257,19 @@ function formatReviewHuman(result) {
|
|
|
225
257
|
}
|
|
226
258
|
: humanError(error);
|
|
227
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;
|
|
228
265
|
return ([
|
|
229
|
-
`humanish review ${result.runId}: ${
|
|
266
|
+
`humanish review ${result.runId}: ${headline}`,
|
|
267
|
+
...(display?.reason === undefined ? [] : [`why: ${display.reason}`]),
|
|
230
268
|
"",
|
|
231
269
|
result.summary,
|
|
232
270
|
...(result.gaps.length === 0 ? [] : ["", "gaps:", ...result.gaps.map((gap) => `- ${gap}`)]),
|
|
233
271
|
"",
|
|
272
|
+
...formatFindings(result.analysis),
|
|
234
273
|
`review: ${result.path}`,
|
|
235
274
|
].join("\n") + "\n");
|
|
236
275
|
}
|
|
@@ -246,6 +285,12 @@ function shareSafetyLines(result) {
|
|
|
246
285
|
.filter((reason) => reason.code !== "VERIFY_FAILED")
|
|
247
286
|
.map((reason) => `share-safety: ${reason.code}: ${reason.message}`);
|
|
248
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
|
+
}
|
|
249
294
|
/**
|
|
250
295
|
* One line for a pass. A failure lists only the failing checks. `--verbose` prints every check.
|
|
251
296
|
* A run that does not exist prints only that.
|
|
@@ -254,12 +299,13 @@ function formatVerifyHuman(result) {
|
|
|
254
299
|
if (result.error?.code === "HUMANISH_RUN_NOT_FOUND")
|
|
255
300
|
return humanError(result.error);
|
|
256
301
|
const runId = verifiedRunId(result);
|
|
302
|
+
const named = `${runId}${unfinishedNote(result)}`;
|
|
257
303
|
const total = result.checks.length;
|
|
258
304
|
const failed = result.checks.filter((check) => !check.ok);
|
|
259
305
|
const lines = result.ok
|
|
260
|
-
? [`verified ${
|
|
306
|
+
? [`verified ${named} · ${result.shareSafety.status} · ${total} checks passed`]
|
|
261
307
|
: [
|
|
262
|
-
`verify failed: ${
|
|
308
|
+
`verify failed: ${named} · ${result.shareSafety.status} · ${failed.length} of ${total} checks failed`,
|
|
263
309
|
...failed.map((check) => `- ${check.message}`),
|
|
264
310
|
];
|
|
265
311
|
lines.push(...shareSafetyLines(result), ...result.warnings.map((warning) => `warning: ${warning}`));
|
|
@@ -273,43 +319,13 @@ function formatVerifyVerbose(result) {
|
|
|
273
319
|
return humanError(result.error);
|
|
274
320
|
return ([
|
|
275
321
|
`humanish verify ${result.ok ? "passed" : "failed"}`,
|
|
276
|
-
`run: ${verifiedRunId(result)}`,
|
|
322
|
+
`run: ${verifiedRunId(result)}${unfinishedNote(result)}`,
|
|
277
323
|
`share-safety: ${result.shareSafety.status}`,
|
|
278
324
|
...result.shareSafety.reasons.map((reason) => `share-safety reason: ${reason.code}: ${reason.message}`),
|
|
279
325
|
...result.checks.map((check) => `- ${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`),
|
|
280
326
|
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
281
327
|
].join("\n") + "\n");
|
|
282
328
|
}
|
|
283
|
-
/** A command-line argument, single-quoted only when the shell would split or expand it. */
|
|
284
|
-
function shellArg(value) {
|
|
285
|
-
return /^[\w@%+=:,./-]+$/.test(value) ? value : shellQuote(value);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Cleanup only reads evidence and never stops a sandbox. A failed resource is an E2B sandbox not
|
|
289
|
-
* recorded as stopped, which reclaim can stop from the run's sandbox receipts, so the last line
|
|
290
|
-
* names it. A skipped resource is a provider reclaim does not handle, so it gets no line. `cwd` is
|
|
291
|
-
* the --cwd the command was given, since cleanup's result masks it.
|
|
292
|
-
*/
|
|
293
|
-
function reclaimPointer(result, cwd) {
|
|
294
|
-
if (result.runId === undefined || result.summary.failed === 0)
|
|
295
|
-
return [];
|
|
296
|
-
const cwdFlag = cwd === "." ? "" : ` --cwd ${shellArg(cwd)}`;
|
|
297
|
-
return [
|
|
298
|
-
`To stop leftover sandboxes, run humanish reclaim --run ${shellArg(result.runId)}${cwdFlag}.`,
|
|
299
|
-
];
|
|
300
|
-
}
|
|
301
|
-
function formatCleanupHuman(result, cwd) {
|
|
302
|
-
if (!result.ok && result.error)
|
|
303
|
-
return humanError(result.error);
|
|
304
|
-
return ([
|
|
305
|
-
`humanish cleanup ${result.ok ? "passed" : "failed"}`,
|
|
306
|
-
`run: ${result.runId ?? result.run}`,
|
|
307
|
-
`resources: already-clean ${result.summary.alreadyClean}, skipped ${result.summary.skipped}, failed ${result.summary.failed}`,
|
|
308
|
-
...(result.cleanupPath ? [`cleanup: ${result.cleanupPath}`] : []),
|
|
309
|
-
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
310
|
-
...reclaimPointer(result, cwd),
|
|
311
|
-
].join("\n") + "\n");
|
|
312
|
-
}
|
|
313
329
|
function formatRunsHuman(result) {
|
|
314
330
|
if (!result.ok)
|
|
315
331
|
return humanError(result.error);
|
|
@@ -318,6 +334,6 @@ function formatRunsHuman(result) {
|
|
|
318
334
|
}
|
|
319
335
|
return ([
|
|
320
336
|
`latest: ${result.latest ?? "none"}`,
|
|
321
|
-
...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}`),
|
|
322
338
|
].join("\n") + "\n");
|
|
323
339
|
}
|
|
@@ -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({
|
|
@@ -6,6 +6,7 @@ import { redactText } from "../../evidence/redaction.js";
|
|
|
6
6
|
import { cliAnalysisOptions } from "./analysis-signals.js";
|
|
7
7
|
import { parseObserverPort, wantsJson, writeResult, } from "../io.js";
|
|
8
8
|
import { followObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
9
|
+
import { writeRunFindings } from "../findings.js";
|
|
9
10
|
import { formatConcurrentSharedWorldStudyHuman } from "./study-format.js";
|
|
10
11
|
import { resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
11
12
|
import { rosterOf } from "../../study/parse/actors.js";
|
|
@@ -118,6 +119,7 @@ export function sharedWorldRouteRun(args) {
|
|
|
118
119
|
}
|
|
119
120
|
writeResult(args.command, args.io, output, formatConcurrentSharedWorldStudyHuman);
|
|
120
121
|
args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
|
|
122
|
+
await writeRunFindings(args.command, args.io, args.options.cwd, result);
|
|
121
123
|
if (server && output.observer?.ok) {
|
|
122
124
|
await followObserver(args.io, output.observer, server);
|
|
123
125
|
}
|
|
@@ -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 { formatTerminalStudyHuman } from "./study-format.js";
|
|
7
8
|
import { observerOpen, resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
@@ -35,6 +36,7 @@ export function terminalRouteRun(args) {
|
|
|
35
36
|
const result = outcome.result;
|
|
36
37
|
writeResult(args.command, args.io, result, formatTerminalStudyHuman);
|
|
37
38
|
args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
|
|
39
|
+
await writeRunFindings(args.command, args.io, args.options.cwd, result);
|
|
38
40
|
if (finishedPlan !== undefined && result.ok && result.observer !== undefined) {
|
|
39
41
|
await showObserver({
|
|
40
42
|
command: args.command,
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type KeyResolutionDeps, type DotenvLoad } from "../keys/key-resolution.js";
|
|
2
|
+
import { type HumanishInstall } from "../substrates/e2b/peer-install.js";
|
|
2
3
|
import { type DetectLocalAgentsOptions } from "../actors/local-agent/cli.js";
|
|
3
4
|
import { type StudySetupCheckArgs } from "../study/doctor.js";
|
|
4
5
|
declare const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
|
|
@@ -35,7 +36,9 @@ export interface DoctorResult {
|
|
|
35
36
|
*/
|
|
36
37
|
export declare const DESKTOP_SDK_FLOOR = "2.3.2";
|
|
37
38
|
/** The advisory `doctor` attaches to an installed desktop SDK older than the floor, else undefined. */
|
|
38
|
-
export declare function desktopSdkAdvisory(version: string | undefined): string | undefined;
|
|
39
|
+
export declare function desktopSdkAdvisory(version: string | undefined, install?: () => HumanishInstall): string | undefined;
|
|
40
|
+
/** The doctor line for an absent desktop SDK, with the install command for where humanish runs from. */
|
|
41
|
+
export declare function missingDesktopSdkMessage(install: HumanishInstall): string;
|
|
39
42
|
/** A row as a check writes it. Doctor derives `status`; an advisory row sets "note" itself. */
|
|
40
43
|
export type DoctorCheckDraft = Omit<DoctorResult["checks"][number], "status"> & {
|
|
41
44
|
status?: "note";
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { probeKeySources, } from "../keys/key-resolution.js";
|
|
6
|
+
import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
|
|
5
7
|
import { nodeSupportsTui, terminalSurfaceMessage, TUI_BUNDLE_URL } from "../tui/contract.js";
|
|
6
8
|
import { detectLocalAgents, localAgentDoctorMessage, NO_LOCAL_AGENT_MESSAGE, } from "../actors/local-agent/cli.js";
|
|
9
|
+
import { leftoverClaudeTranscripts } from "../actors/local-agent/claude-participant.js";
|
|
7
10
|
import { studySetupChecks, studiesByRequiredKey, } from "../study/doctor.js";
|
|
8
11
|
import { prepareSelectedOutputDirectory, } from "../run/contained-output.js";
|
|
9
12
|
import { implicitProjectDirectoryExists, readImplicitProjectFile, validateCwd, } from "../run/project.js";
|
|
@@ -16,7 +19,7 @@ const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
|
|
|
16
19
|
*/
|
|
17
20
|
export const DESKTOP_SDK_FLOOR = "2.3.2";
|
|
18
21
|
/** The advisory `doctor` attaches to an installed desktop SDK older than the floor, else undefined. */
|
|
19
|
-
export function desktopSdkAdvisory(version) {
|
|
22
|
+
export function desktopSdkAdvisory(version, install = humanishInstall) {
|
|
20
23
|
if (version === undefined)
|
|
21
24
|
return undefined;
|
|
22
25
|
const parse = (value) => value
|
|
@@ -31,9 +34,14 @@ export function desktopSdkAdvisory(version) {
|
|
|
31
34
|
(have[0] === floor[0] &&
|
|
32
35
|
(have[1] < floor[1] || (have[1] === floor[1] && have[2] < floor[2])));
|
|
33
36
|
return older
|
|
34
|
-
? `@e2b/desktop ${version} is older than ${DESKTOP_SDK_FLOOR}, the supported floor for background command cleanup and stdin handles (older releases could keep the CLI alive for minutes after its result).
|
|
37
|
+
? `@e2b/desktop ${version} is older than ${DESKTOP_SDK_FLOOR}, the supported floor for background command cleanup and stdin handles (older releases could keep the CLI alive for minutes after its result). To update it: ${desktopPeerAdvice(install(), "@e2b/desktop@latest").advice}`
|
|
35
38
|
: undefined;
|
|
36
39
|
}
|
|
40
|
+
/** The doctor line for an absent desktop SDK, with the install command for where humanish runs from. */
|
|
41
|
+
export function missingDesktopSdkMessage(install) {
|
|
42
|
+
const { where, advice } = desktopPeerAdvice(install);
|
|
43
|
+
return `optional peer @e2b/desktop is not installed${where}: dry runs work, but a live run with a desktop participant fails when it starts. ${advice}`;
|
|
44
|
+
}
|
|
37
45
|
/** The version of the @e2b/desktop that `import("@e2b/desktop")` resolves to from here, if readable. */
|
|
38
46
|
async function installedDesktopSdkVersion() {
|
|
39
47
|
try {
|
|
@@ -86,6 +94,7 @@ export async function doctor(cwdInput, options = {}) {
|
|
|
86
94
|
await desktopSdkCheck(setup),
|
|
87
95
|
terminalSurfaceCheck(),
|
|
88
96
|
...localAgentChecks(agents),
|
|
97
|
+
await claudeTranscriptCheck(env),
|
|
89
98
|
...keyChecks(probes, receivingKey, setup, keyUsers),
|
|
90
99
|
...(setup?.checks ?? [
|
|
91
100
|
{
|
|
@@ -205,7 +214,7 @@ async function desktopSdkCheck(setup) {
|
|
|
205
214
|
? `optional peer @e2b/desktop ${version ?? "(version unread)"} is installed; provider access is not tested${advisory === undefined ? "" : `. ${advisory}`}`
|
|
206
215
|
: setup?.desktop === false
|
|
207
216
|
? "optional peer @e2b/desktop is absent; not required by the selected route"
|
|
208
|
-
:
|
|
217
|
+
: missingDesktopSdkMessage(humanishInstall()),
|
|
209
218
|
};
|
|
210
219
|
}
|
|
211
220
|
/**
|
|
@@ -274,6 +283,35 @@ function localAgentChecks(agents) {
|
|
|
274
283
|
message: localAgentDoctorMessage(agent),
|
|
275
284
|
}));
|
|
276
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Earlier releases let Claude Code save a transcript for every participant, with each screenshot it
|
|
288
|
+
* read, under its projects folder and outside `.humanish/`. Participants now run with
|
|
289
|
+
* `--no-session-persistence`; this row counts what earlier runs left and prints the command that
|
|
290
|
+
* removes it. doctor never deletes anything.
|
|
291
|
+
*/
|
|
292
|
+
async function claudeTranscriptCheck(env) {
|
|
293
|
+
const found = await leftoverClaudeTranscripts(env, env.HOME ?? homedir(), (directory) => readdir(directory));
|
|
294
|
+
const name = "claude participant transcripts";
|
|
295
|
+
if ("unreadable" in found)
|
|
296
|
+
return {
|
|
297
|
+
name,
|
|
298
|
+
ok: true,
|
|
299
|
+
status: "note",
|
|
300
|
+
message: `could not list ${found.directory} (${found.unreadable}), so transcripts from earlier Claude Code participants were not counted. Check the folder for names ending in -humanish-claude-session-XXXXXX.`,
|
|
301
|
+
};
|
|
302
|
+
const count = found.names.length;
|
|
303
|
+
const folders = `${count} transcript folder${count === 1 ? "" : "s"} from earlier Claude Code participants under ${found.directory} hold every screenshot those runs read, unblurred.`;
|
|
304
|
+
return {
|
|
305
|
+
name,
|
|
306
|
+
ok: true,
|
|
307
|
+
...(count === 0 ? {} : { status: "note" }),
|
|
308
|
+
message: count === 0
|
|
309
|
+
? `no transcript from an earlier Claude Code participant under ${found.directory}`
|
|
310
|
+
: found.removeCommand === undefined
|
|
311
|
+
? `${folders} Remove these folders there: ${found.names.join(", ")}`
|
|
312
|
+
: `${folders} Remove them with: ${found.removeCommand}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
277
315
|
/** "try-live", "try-live and local-browser", "a, b and c". */
|
|
278
316
|
function studyList(studies) {
|
|
279
317
|
return studies.length < 2
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { AutomaticAnalysisOutcome } from "../analysis/job.js";
|
|
3
|
+
import type { AnalysisArtifact, LoadedAnalysis } from "../analysis/types.js";
|
|
4
|
+
import { type CliIo } from "./io.js";
|
|
5
|
+
export declare const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
|
|
6
|
+
type Finding = NonNullable<AnalysisArtifact["result"]>["findings"][number];
|
|
7
|
+
/** `ready` and `stale` carry findings; every other state carries none and says why in `message`. */
|
|
8
|
+
type FindingsState = "ready" | "stale" | "running" | "none" | "skipped" | "failed" | "dry_run" | "unavailable";
|
|
9
|
+
interface FindingEvidence {
|
|
10
|
+
id: string;
|
|
11
|
+
streamId: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
/** How the finding's observations used this item: visual, action, participant_statement, inference. */
|
|
14
|
+
bases: string[];
|
|
15
|
+
/** Index of the latest retained capture at this item, from 0; null before any capture. */
|
|
16
|
+
frame: number | null;
|
|
17
|
+
/** Time since the first retained capture, not a video offset. */
|
|
18
|
+
elapsedMs: number | null;
|
|
19
|
+
at: string | null;
|
|
20
|
+
/** The capture file relative to the project directory, or null for nonvisual evidence. */
|
|
21
|
+
capture: string | null;
|
|
22
|
+
}
|
|
23
|
+
interface FindingView {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
summary: string;
|
|
27
|
+
impact: Finding["impact"];
|
|
28
|
+
confidence: Finding["confidence"];
|
|
29
|
+
recovery: Finding["recovery"];
|
|
30
|
+
affected: {
|
|
31
|
+
streamId: string;
|
|
32
|
+
label: string;
|
|
33
|
+
}[];
|
|
34
|
+
exposedCount: number;
|
|
35
|
+
evidence: FindingEvidence[];
|
|
36
|
+
nextStep: string;
|
|
37
|
+
/** The latest human review note on this finding version (`analyze correct`), or null. */
|
|
38
|
+
correction: {
|
|
39
|
+
status: "confirmed" | "dismissed" | "amended";
|
|
40
|
+
reason: string;
|
|
41
|
+
replacementClaim: string | null;
|
|
42
|
+
} | null;
|
|
43
|
+
}
|
|
44
|
+
export interface AnalysisFindings {
|
|
45
|
+
schema: typeof ANALYSIS_FINDINGS_SCHEMA;
|
|
46
|
+
runId: string;
|
|
47
|
+
state: FindingsState;
|
|
48
|
+
/** The stable code behind a state other than ready, when one is recorded. */
|
|
49
|
+
reason: string | null;
|
|
50
|
+
message: string;
|
|
51
|
+
/** The command that gets findings for this run, or null when none can. */
|
|
52
|
+
next: string | null;
|
|
53
|
+
analysisId: string | null;
|
|
54
|
+
status: AnalysisArtifact["status"] | null;
|
|
55
|
+
provider: AnalysisArtifact["provider"] | null;
|
|
56
|
+
model: string | null;
|
|
57
|
+
completedAt: string | null;
|
|
58
|
+
estimatedCostUsd: number | null;
|
|
59
|
+
/** The run directory relative to the project directory; null when the run was not found. */
|
|
60
|
+
runPath: string | null;
|
|
61
|
+
/** analysis.json relative to the project directory. */
|
|
62
|
+
path: string | null;
|
|
63
|
+
summary: string | null;
|
|
64
|
+
/** Highest priority first, as the analysis ranked them. */
|
|
65
|
+
findings: FindingView[];
|
|
66
|
+
limitations: string[];
|
|
67
|
+
warnings: string[];
|
|
68
|
+
}
|
|
69
|
+
interface FindingsSource {
|
|
70
|
+
runId: string;
|
|
71
|
+
/** The bundle's mode; undefined when run.json could not be read. */
|
|
72
|
+
mode: string | undefined;
|
|
73
|
+
/** The run directory relative to the project directory. */
|
|
74
|
+
runRoot: string;
|
|
75
|
+
loaded: LoadedAnalysis;
|
|
76
|
+
/** ` --cwd <dir>` when the commands this view names need it, or "". */
|
|
77
|
+
cwdFlag: string;
|
|
78
|
+
}
|
|
79
|
+
/** The findings view of a run's selected analysis, or why it has none. */
|
|
80
|
+
export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
|
|
81
|
+
/**
|
|
82
|
+
* The run's bundle mode and its selected analysis: `analysisId`, or the version `analyze show`
|
|
83
|
+
* defaults to. Null when the run is missing or its storage is unsafe.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readRunAnalysis(cwd: string, run: string, analysisId?: string): Promise<FindingsSource | null>;
|
|
86
|
+
/** The view of a run that could not be found. */
|
|
87
|
+
export declare function missingRunFindings(cwd: string, run: string): AnalysisFindings;
|
|
88
|
+
/** The findings of a run's selected analysis, or why it has none. */
|
|
89
|
+
export declare function readRunFindings(cwd: string, run: string, analysisId?: string): Promise<AnalysisFindings>;
|
|
90
|
+
/** The full findings block: the analysis, every finding with its evidence, and its limitations. */
|
|
91
|
+
export declare function formatFindings(view: AnalysisFindings): string[];
|
|
92
|
+
/** At most `limit` findings, one line each, and the command that prints all of them. */
|
|
93
|
+
export declare function formatFindingsSummary(view: AnalysisFindings, fullCommand: string, limit?: number): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The end of a live run's human output, after its `analysis:` line: the findings of the analysis
|
|
96
|
+
* the run just completed. Prints nothing for --json, or when the run's analysis did not complete,
|
|
97
|
+
* since that line already says why.
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeRunFindings(command: Command, io: CliIo, cwd: string, result: {
|
|
100
|
+
runId?: string;
|
|
101
|
+
automaticAnalysis?: AutomaticAnalysisOutcome;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
export {};
|