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.
Files changed (132) hide show
  1. package/ARCHITECTURE.md +14 -11
  2. package/CHANGELOG.md +13 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +96 -47
  5. package/dist/actors/computer-use/loop/ending.js +4 -4
  6. package/dist/actors/local-agent/claude-participant.d.ts +74 -0
  7. package/dist/actors/local-agent/claude-participant.js +243 -0
  8. package/dist/actors/local-agent/claude-session.d.ts +16 -3
  9. package/dist/actors/local-agent/claude-session.js +64 -23
  10. package/dist/actors/local-agent/cli.d.ts +11 -0
  11. package/dist/actors/local-agent/cli.js +103 -24
  12. package/dist/actors/local-agent/readiness.d.ts +2 -2
  13. package/dist/actors/local-agent/readiness.js +18 -11
  14. package/dist/cli/commands/analyze.js +21 -5
  15. package/dist/cli/commands/run-signals.d.ts +2 -0
  16. package/dist/cli/commands/run-signals.js +96 -44
  17. package/dist/cli/commands/runs.d.ts +5 -0
  18. package/dist/cli/commands/runs.js +86 -70
  19. package/dist/cli/commands/study-route-computer-use.js +2 -0
  20. package/dist/cli/commands/study-route-scripted.js +2 -0
  21. package/dist/cli/commands/study-route-shared-world.js +2 -0
  22. package/dist/cli/commands/study-route-terminal.js +2 -0
  23. package/dist/cli/doctor.d.ts +4 -1
  24. package/dist/cli/doctor.js +42 -4
  25. package/dist/cli/findings.d.ts +103 -0
  26. package/dist/cli/findings.js +316 -0
  27. package/dist/cli/first-run-path.d.ts +5 -4
  28. package/dist/cli/first-run-path.js +14 -7
  29. package/dist/cli/io.js +2 -1
  30. package/dist/cli/product-sentence.d.ts +3 -3
  31. package/dist/cli/product-sentence.js +3 -3
  32. package/dist/keys/env-file.d.ts +4 -0
  33. package/dist/keys/env-file.js +17 -0
  34. package/dist/observer/data.d.ts +14 -1
  35. package/dist/observer/data.js +19 -9
  36. package/dist/observer/library.d.ts +4 -0
  37. package/dist/observer/library.js +18 -15
  38. package/dist/observer/render.js +8 -2
  39. package/dist/observer/run-routes.d.ts +2 -0
  40. package/dist/observer/run-routes.js +43 -34
  41. package/dist/observer-app.html +3 -3
  42. package/dist/routes/computer-use/bundle.js +3 -1
  43. package/dist/routes/computer-use/fanout-records.js +1 -1
  44. package/dist/routes/computer-use/participant-execution.js +3 -1
  45. package/dist/routes/computer-use/participant-model.js +5 -2
  46. package/dist/routes/computer-use/plan.js +4 -4
  47. package/dist/routes/computer-use/preflight.js +1 -1
  48. package/dist/routes/computer-use/result.js +17 -28
  49. package/dist/routes/computer-use/single-bundle.js +7 -1
  50. package/dist/routes/scripted/bundle.js +4 -2
  51. package/dist/routes/scripted/plan.js +2 -2
  52. package/dist/routes/scripted/result.d.ts +3 -5
  53. package/dist/routes/scripted/result.js +16 -22
  54. package/dist/routes/scripted/scenario.js +4 -4
  55. package/dist/routes/shared-world/bundle-records.js +6 -1
  56. package/dist/routes/shared-world/bundle.js +2 -0
  57. package/dist/routes/shared-world/plan.js +1 -1
  58. package/dist/routes/shared-world/result.js +22 -22
  59. package/dist/routes/terminal/bundle.js +4 -2
  60. package/dist/routes/terminal/dry-run.js +8 -12
  61. package/dist/routes/terminal/ledger.d.ts +3 -3
  62. package/dist/routes/terminal/ledger.js +6 -6
  63. package/dist/routes/terminal/live-finish.js +15 -13
  64. package/dist/routes/terminal/plan.js +4 -4
  65. package/dist/routes/terminal/result.d.ts +6 -7
  66. package/dist/routes/terminal/result.js +6 -22
  67. package/dist/routes/terminal/session.js +1 -1
  68. package/dist/run/bundle-shape.d.ts +3 -1
  69. package/dist/run/bundle-shape.js +25 -1
  70. package/dist/run/bundle.d.ts +24 -0
  71. package/dist/run/display.d.ts +59 -0
  72. package/dist/run/display.js +106 -0
  73. package/dist/run/dry-run.js +11 -7
  74. package/dist/run/judge.d.ts +3 -2
  75. package/dist/run/judge.js +10 -0
  76. package/dist/run/pricing.js +1 -1
  77. package/dist/run/projection.d.ts +0 -2
  78. package/dist/run/projection.js +0 -11
  79. package/dist/run/reclaim-outcomes.d.ts +89 -0
  80. package/dist/run/reclaim-outcomes.js +213 -0
  81. package/dist/run/reclaim.d.ts +18 -17
  82. package/dist/run/reclaim.js +180 -113
  83. package/dist/run/results.d.ts +10 -20
  84. package/dist/run/results.js +2 -2
  85. package/dist/run/run-index.d.ts +11 -1
  86. package/dist/run/run-index.js +11 -0
  87. package/dist/run/run.d.ts +34 -18
  88. package/dist/run/run.js +166 -67
  89. package/dist/run/sandbox-creates.d.ts +59 -0
  90. package/dist/run/sandbox-creates.js +138 -0
  91. package/dist/run/sandbox-receipts.d.ts +14 -0
  92. package/dist/run/sandbox-receipts.js +44 -0
  93. package/dist/run/stats.d.ts +5 -0
  94. package/dist/run/stats.js +8 -4
  95. package/dist/run/status.d.ts +6 -9
  96. package/dist/run/status.js +7 -23
  97. package/dist/run/stored-runs.d.ts +5 -14
  98. package/dist/run/stored-runs.js +31 -146
  99. package/dist/run/streams.d.ts +3 -2
  100. package/dist/study/doctor.d.ts +1 -1
  101. package/dist/study/doctor.js +5 -2
  102. package/dist/study/init.js +14 -3
  103. package/dist/study/keys.d.ts +12 -0
  104. package/dist/study/keys.js +32 -0
  105. package/dist/study/persona.d.ts +4 -0
  106. package/dist/study/persona.js +4 -2
  107. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  108. package/dist/substrates/e2b/peer-install.js +161 -0
  109. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  110. package/dist/substrates/e2b/sandbox.js +128 -35
  111. package/dist/substrates/e2b/sdk.d.ts +33 -0
  112. package/dist/substrates/e2b/sdk.js +21 -21
  113. package/dist/substrates/shell.d.ts +2 -0
  114. package/dist/substrates/shell.js +4 -0
  115. package/dist/tui-app.js +133 -133
  116. package/dist/verify/liveness.d.ts +19 -3
  117. package/dist/verify/liveness.js +46 -25
  118. package/dist/verify/verify.d.ts +3 -0
  119. package/dist/verify/verify.js +6 -2
  120. package/docs/architecture/actor-contract.md +2 -1
  121. package/docs/architecture/observer.md +22 -7
  122. package/docs/architecture/task-protocol-support.md +13 -13
  123. package/docs/architecture/terminal-product-route.md +1 -1
  124. package/docs/contracts/run-bundle.md +75 -6
  125. package/docs/contracts/schemas.md +39 -22
  126. package/docs/contracts/study-analysis.md +45 -0
  127. package/docs/contracts/study-costs.md +8 -0
  128. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  129. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  130. package/docs/principles/invariants-and-defaults.md +11 -8
  131. package/package.json +4 -3
  132. 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 { shellQuote } from "../../substrates/shell.js";
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 { cleanupRun, listRuns, readReview } from "../../run/stored-runs.js";
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("Check a run's recorded resources and write cleanup.json. It stops nothing; humanish reclaim stops leftover sandboxes.")
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
- const result = await cleanupRun(options.cwd, options.run);
43
- writeResult(command, io, result, (value) => formatCleanupHuman(value, options.cwd));
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 gaps.")
51
- .summary("Build a review packet from verified run evidence.")
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 result = await readReview(options.cwd, options.run);
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 by the exact ids journaled in its sandbox-receipts.ndjson; never enumerates the E2B account. Needs E2B_API_KEY in the environment or in --dotenv.")
169
- .summary("Stop an interrupted run's leftover sandboxes.")
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(async (options, command) => {
177
- // The kill calls read E2B_API_KEY from the environment: the env file, then discovered keys.
178
- if (!(await applyEnvFileOption({
179
- command,
180
- cwd: options.cwd,
181
- envFile: dotenvPathOf(options, command, io),
182
- io,
183
- })))
184
- return;
185
- const result = options.preflight
186
- ? await reclaimPreflightSandboxes(options.cwd)
187
- : await reclaimRunSandboxes(options.cwd, options.run);
188
- writeResult(command, io, result, formatReclaimHuman);
189
- io.setExitCode(result.ok ? 0 : 2);
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
- lines.push(`Reclaim ${result.runId}: ${result.ok ? "ok" : "failed"}, ${plural(result.receiptCount, "sandbox receipt")}.`);
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 ? ` — ${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
- const stdout = lines.join("\n");
201
- return result.error === undefined ? stdout : { stdout: `${stdout}\n`, error: result.error };
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
- /** A run's review: its verdict, summary and gaps, and where review.json is. */
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}: ${REVIEW_VERDICTS[result.verdict]}`,
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 ${runId} · ${result.shareSafety.status} · ${total} checks passed`]
306
+ ? [`verified ${named} · ${result.shareSafety.status} · ${total} checks passed`]
261
307
  : [
262
- `verify failed: ${runId} · ${result.shareSafety.status} · ${failed.length} of ${total} checks 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,
@@ -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";
@@ -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). Update with \`npm i -D @e2b/desktop@latest\`.`
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
- : "optional peer @e2b/desktop is not installed: dry runs work, but a live run with a desktop participant fails when it starts. Install it with `npm i -D @e2b/desktop`.",
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 {};