humanish 0.115.0 → 0.117.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +7 -6
  3. package/TELEMETRY.md +6 -0
  4. package/dist/actors/codex/app-server-ui.js +11 -4
  5. package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
  6. package/dist/actors/codex/restricted-participant-policy.js +4 -32
  7. package/dist/actors/codex/restricted-participant.d.ts +5 -0
  8. package/dist/actors/codex/restricted-participant.js +3 -2
  9. package/dist/actors/computer-use/actor.d.ts +4 -0
  10. package/dist/actors/computer-use/actor.js +2 -0
  11. package/dist/actors/computer-use/loop/actions.d.ts +4 -4
  12. package/dist/actors/computer-use/loop/actions.js +25 -18
  13. package/dist/actors/computer-use/loop/debrief.js +61 -27
  14. package/dist/actors/computer-use/loop/session.d.ts +3 -0
  15. package/dist/actors/computer-use/loop/session.js +9 -1
  16. package/dist/actors/computer-use/loop/trace.js +1 -0
  17. package/dist/actors/computer-use/loop/types.d.ts +16 -14
  18. package/dist/actors/computer-use/loop.d.ts +1 -1
  19. package/dist/actors/computer-use/loop.js +1 -1
  20. package/dist/actors/computer-use/wait.d.ts +74 -0
  21. package/dist/actors/computer-use/wait.js +100 -0
  22. package/dist/actors/contract.d.ts +14 -0
  23. package/dist/actors/scripted-browser/steps.js +6 -5
  24. package/dist/analysis/analysis-limits.d.ts +35 -0
  25. package/dist/analysis/analysis-limits.js +41 -0
  26. package/dist/analysis/validation.js +14 -21
  27. package/dist/browser-control/protocol.d.ts +86 -6
  28. package/dist/browser-control/protocol.js +13 -6
  29. package/dist/cli/commands/analyze.js +2 -1
  30. package/dist/cli/commands/notes.js +2 -1
  31. package/dist/cli/commands/run-signals.js +5 -1
  32. package/dist/cli/commands/runs.js +2 -1
  33. package/dist/cli/commands/setup.js +3 -3
  34. package/dist/cli/commands/study-format.d.ts +5 -2
  35. package/dist/cli/commands/study-format.js +29 -9
  36. package/dist/cli/commands/study-route-computer-use.js +2 -2
  37. package/dist/cli/commands/study-route-scripted.js +1 -1
  38. package/dist/cli/commands/tui.js +59 -17
  39. package/dist/cli/doctor.js +9 -2
  40. package/dist/cli/findings-text.d.ts +5 -0
  41. package/dist/cli/findings-text.js +169 -0
  42. package/dist/cli/findings.d.ts +0 -4
  43. package/dist/cli/findings.js +5 -167
  44. package/dist/cli/io.js +6 -1
  45. package/dist/cli/update-check.js +4 -1
  46. package/dist/comms/connections.js +4 -3
  47. package/dist/comms/setup.js +18 -12
  48. package/dist/evidence/redaction.js +9 -1
  49. package/dist/evidence/secret-scrub.d.ts +15 -5
  50. package/dist/evidence/secret-scrub.js +67 -9
  51. package/dist/feedback/draft.js +1 -1
  52. package/dist/feedback/export-bundle.js +18 -14
  53. package/dist/feedback/feedback.js +25 -7
  54. package/dist/guest/desktop-executor.js +3 -1
  55. package/dist/guest/desktop-keys.js +59 -5
  56. package/dist/keys/key-resolution.d.ts +9 -0
  57. package/dist/keys/key-resolution.js +60 -48
  58. package/dist/keys/key-status.d.ts +2 -0
  59. package/dist/keys/key-status.js +9 -16
  60. package/dist/observer/pinned-files.js +1 -1
  61. package/dist/observer/render.js +3 -2
  62. package/dist/observer-app.html +5 -5
  63. package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
  64. package/dist/routes/computer-use/bundle-parts.js +2 -2
  65. package/dist/routes/computer-use/bundle.d.ts +4 -5
  66. package/dist/routes/computer-use/bundle.js +1 -3
  67. package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
  68. package/dist/routes/computer-use/fanout-records.js +10 -11
  69. package/dist/routes/computer-use/live-phase.d.ts +1 -1
  70. package/dist/routes/computer-use/live-phase.js +0 -1
  71. package/dist/routes/computer-use/local-vm.js +5 -2
  72. package/dist/routes/computer-use/participant-execution.d.ts +3 -2
  73. package/dist/routes/computer-use/participant-model.js +9 -0
  74. package/dist/routes/computer-use/participant-runs.d.ts +6 -2
  75. package/dist/routes/computer-use/participant-runs.js +9 -5
  76. package/dist/routes/computer-use/plan.d.ts +3 -0
  77. package/dist/routes/computer-use/plan.js +18 -13
  78. package/dist/routes/computer-use/rerun-selection.js +2 -2
  79. package/dist/routes/computer-use/result.js +3 -6
  80. package/dist/routes/computer-use/setup.d.ts +4 -6
  81. package/dist/routes/computer-use/setup.js +2 -6
  82. package/dist/routes/computer-use/single-bundle.d.ts +4 -5
  83. package/dist/routes/computer-use/single-bundle.js +22 -25
  84. package/dist/routes/computer-use/subject-projection.d.ts +4 -8
  85. package/dist/routes/computer-use/subject-projection.js +9 -31
  86. package/dist/routes/computer-use/types.d.ts +5 -42
  87. package/dist/routes/scripted/scenario.js +28 -12
  88. package/dist/routes/scripted/session-result.js +6 -3
  89. package/dist/routes/shared-world/bundle-records.js +2 -8
  90. package/dist/routes/shared-world/participant-specs.d.ts +0 -2
  91. package/dist/routes/shared-world/participant-specs.js +1 -20
  92. package/dist/routes/shared-world/plan.d.ts +3 -0
  93. package/dist/routes/shared-world/plan.js +25 -4
  94. package/dist/routes/shared-world/setup.js +2 -2
  95. package/dist/routes/terminal/ledger.d.ts +9 -9
  96. package/dist/routes/terminal/ledger.js +63 -40
  97. package/dist/routes/terminal/live-finish.js +7 -10
  98. package/dist/routes/terminal/live-sandbox.d.ts +1 -1
  99. package/dist/routes/terminal/live-sandbox.js +14 -11
  100. package/dist/routes/terminal/plan.d.ts +3 -0
  101. package/dist/routes/terminal/plan.js +8 -5
  102. package/dist/routes/terminal/sandbox.d.ts +4 -3
  103. package/dist/routes/terminal/sandbox.js +5 -5
  104. package/dist/routes/terminal/token-usage.d.ts +1 -2
  105. package/dist/routes/terminal/token-usage.js +4 -6
  106. package/dist/routes/terminal/types.d.ts +9 -1
  107. package/dist/run/bundle-shape.js +6 -1
  108. package/dist/run/bundle.d.ts +5 -3
  109. package/dist/run/contained-output.d.ts +53 -5
  110. package/dist/run/contained-output.js +141 -42
  111. package/dist/run/detail.d.ts +2 -1
  112. package/dist/run/detail.js +12 -7
  113. package/dist/run/escape-sequences.d.ts +11 -0
  114. package/dist/run/escape-sequences.js +93 -0
  115. package/dist/run/evidence-files.d.ts +8 -8
  116. package/dist/run/evidence-files.js +19 -23
  117. package/dist/run/locate.d.ts +32 -3
  118. package/dist/run/locate.js +45 -40
  119. package/dist/run/notes.d.ts +0 -8
  120. package/dist/run/notes.js +5 -51
  121. package/dist/run/outcomes.d.ts +24 -0
  122. package/dist/run/outcomes.js +19 -0
  123. package/dist/run/paths.d.ts +2 -0
  124. package/dist/run/paths.js +5 -1
  125. package/dist/run/preflight-receipts.js +13 -8
  126. package/dist/run/project.js +7 -4
  127. package/dist/run/reclaim-outcomes.d.ts +8 -6
  128. package/dist/run/reclaim-outcomes.js +10 -6
  129. package/dist/run/reclaim.d.ts +5 -0
  130. package/dist/run/reclaim.js +43 -16
  131. package/dist/run/run-clock.d.ts +50 -0
  132. package/dist/run/run-clock.js +66 -0
  133. package/dist/run/run.js +5 -2
  134. package/dist/run/sandbox-ids.d.ts +7 -2
  135. package/dist/run/sandbox-ids.js +27 -11
  136. package/dist/run/secrets.d.ts +2 -1
  137. package/dist/run/secrets.js +18 -22
  138. package/dist/run/stored-runs.js +14 -7
  139. package/dist/run/transient-comms-secrets.js +6 -17
  140. package/dist/run-study.js +22 -13
  141. package/dist/study/adapter-scorer-loader.js +6 -4
  142. package/dist/study/config.js +4 -1
  143. package/dist/study/discover.js +9 -6
  144. package/dist/study/init-paths.js +7 -5
  145. package/dist/study/keys.d.ts +2 -0
  146. package/dist/study/keys.js +2 -0
  147. package/dist/study/migrate/v2.js +2 -1
  148. package/dist/study/parse/actors.js +8 -0
  149. package/dist/study/persona-resolve.d.ts +2 -2
  150. package/dist/study/persona-resolve.js +9 -5
  151. package/dist/study/plan-participants.d.ts +4 -0
  152. package/dist/study/plan-participants.js +4 -0
  153. package/dist/study/plan-types.d.ts +2 -2
  154. package/dist/study/plan.d.ts +5 -3
  155. package/dist/study/plan.js +18 -9
  156. package/dist/study/preflight-probes.js +11 -3
  157. package/dist/study/preflight.js +9 -5
  158. package/dist/study/summary.d.ts +8 -2
  159. package/dist/study/summary.js +20 -22
  160. package/dist/study/types.d.ts +14 -0
  161. package/dist/study/validation.d.ts +5 -0
  162. package/dist/study/validation.js +8 -0
  163. package/dist/study/warnings.d.ts +19 -12
  164. package/dist/study/warnings.js +50 -17
  165. package/dist/subject/serve.d.ts +3 -0
  166. package/dist/subject/serve.js +4 -1
  167. package/dist/substrates/detached.js +14 -7
  168. package/dist/substrates/e2b/connection.d.ts +12 -0
  169. package/dist/substrates/e2b/connection.js +35 -0
  170. package/dist/substrates/e2b/desktop-executor.d.ts +1 -5
  171. package/dist/substrates/e2b/desktop-executor.js +45 -12
  172. package/dist/substrates/e2b/lifetime.d.ts +24 -4
  173. package/dist/substrates/e2b/lifetime.js +41 -4
  174. package/dist/substrates/e2b/sandbox.d.ts +4 -3
  175. package/dist/substrates/e2b/sandbox.js +13 -18
  176. package/dist/substrates/e2b/sdk.d.ts +20 -9
  177. package/dist/substrates/local/capacity.d.ts +18 -2
  178. package/dist/substrates/local/capacity.js +21 -2
  179. package/dist/substrates/local/runtime-host.js +21 -14
  180. package/dist/substrates/local/runtime.js +3 -11
  181. package/dist/tui/contract.d.ts +30 -5
  182. package/dist/tui/contract.js +4 -1
  183. package/dist/tui-app.js +150 -147
  184. package/dist/verify/artifacts.d.ts +9 -2
  185. package/dist/verify/artifacts.js +37 -13
  186. package/dist/verify/liveness.js +19 -5
  187. package/dist/verify/verify.js +54 -27
  188. package/docs/architecture/browser-control.md +10 -4
  189. package/docs/architecture/guest-desktop.md +7 -2
  190. package/docs/architecture/state-driven-executor.md +4 -2
  191. package/docs/contracts/run-bundle.md +18 -10
  192. package/docs/contracts/schemas.md +46 -29
  193. package/docs/contracts/study-analysis.md +10 -1
  194. package/docs/product/automatic-analysis.md +7 -1
  195. package/package.json +2 -2
@@ -134,7 +134,11 @@ async function stopActiveRuns(io, signal, stopping, options) {
134
134
  // not swept; verify grades a file that names a receipt's id local_only.
135
135
  Promise.all(stopping
136
136
  .filter((stop) => stop.report !== null)
137
- .map((stop) => scrubRunSandboxIds(stop.run.paths).catch(() => undefined))));
137
+ .map((stop) => scrubRunSandboxIds(stop.run.paths).catch((error) => {
138
+ // A file the sweep refused to read may still name a sandbox; say which.
139
+ if (error instanceof Error)
140
+ say(io, `humanish: ${signal}: ${error.message}`);
141
+ }))));
138
142
  const finished = await withDeadline(work, deadlineMs, options.deadline);
139
143
  for (const stop of stopping) {
140
144
  if (stop.report === null)
@@ -11,7 +11,8 @@ import { addRunOptions, handleRun } from "./run-command.js";
11
11
  import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
12
12
  import { plural } from "../../run/text.js";
13
13
  import { warnAndQueue } from "../deprecations.js";
14
- import { formatFindings, readRunFindings } from "../findings.js";
14
+ import { readRunFindings } from "../findings.js";
15
+ import { formatFindings } from "../findings-text.js";
15
16
  import { cli } from "../invocation.js";
16
17
  export function registerRunCommand(parent, io) {
17
18
  addRunOptions(parent
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath, } from "../../keys/key-resolution.js";
2
+ import { KEY_VENDORS, listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath, } from "../../keys/key-resolution.js";
3
3
  import { askForMissingKeys, formatKeyStatus, keyStatus, otherStoredKeys, } from "../../keys/key-status.js";
4
4
  import { promptSecret } from "../secret-prompt.js";
5
5
  import { runInit } from "../../study/init.js";
@@ -270,7 +270,7 @@ export function registerKeysCommand(parent, io) {
270
270
  .action(async (_options, command) => writeKeyStatus(command, io));
271
271
  keys
272
272
  .command("set")
273
- .argument("[vendor-or-name]", "A vendor alias (openai, e2b, anthropic, github, agentmail) or a raw ENV_NAME. Without one, a terminal asks for each missing key in turn.")
273
+ .argument("[vendor-or-name]", `A vendor alias (${KEY_VENDORS.join(", ")}) or a raw ENV_NAME. Without one, a terminal asks for each missing key in turn.`)
274
274
  .description("Store one provider key in the user store (0600), prompted with hidden input. With no key named, ask for each missing key in turn.")
275
275
  .option("--stdin", "Read the value from stdin instead of prompting (for agents/pipes).")
276
276
  .option("--json", JSON_OPTION_DESCRIPTION)
@@ -288,7 +288,7 @@ export function registerKeysCommand(parent, io) {
288
288
  action: "set",
289
289
  store: storePath,
290
290
  names: [],
291
- message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: openai, e2b, anthropic, github, agentmail.`,
291
+ message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: ${KEY_VENDORS.join(", ")}.`,
292
292
  };
293
293
  writeResult(command, io, result, formatKeysHuman);
294
294
  io.setExitCode(2);
@@ -3,7 +3,10 @@ import type { CuaActorStudyResult } from "../../routes/computer-use/types.js";
3
3
  import type { ScriptedBrowserStudyResult } from "../../routes/scripted/types.js";
4
4
  import type { TerminalProductStudyResult } from "../../routes/terminal/types.js";
5
5
  import type { ConcurrentSharedWorldStudyResult } from "../../routes/shared-world/types.js";
6
+ import type { StudySubject } from "../../study/types.js";
6
7
  export declare function formatConcurrentSharedWorldStudyHuman(result: ConcurrentSharedWorldStudyResult): HumanOutput;
7
8
  export declare function formatTerminalStudyHuman(result: TerminalProductStudyResult): HumanOutput;
8
- export declare function formatScriptedStudyHuman(result: ScriptedBrowserStudyResult): HumanOutput;
9
- export declare function formatCuaStudyHuman(result: CuaActorStudyResult): HumanOutput;
9
+ /** `subject` is the study's declared subject, which names it when the result has no URL. */
10
+ export declare function formatScriptedStudyHuman(result: ScriptedBrowserStudyResult, subject: StudySubject): HumanOutput;
11
+ /** `subject` is the study's declared subject, which names it when the result has no URL. */
12
+ export declare function formatCuaStudyHuman(result: CuaActorStudyResult, subject: StudySubject): HumanOutput;
@@ -1,12 +1,18 @@
1
1
  import { participantCaption } from "../../run/participant-caption.js";
2
2
  import { formatCuaDiagnostics, formatCuaStopCause } from "../../routes/computer-use/diagnostics.js";
3
- /** A run's first lines: the command that ran, whether it was a dry run, how it ended, and its route. */
3
+ /**
4
+ * A run's first lines: the command that ran, whether it was a dry run, how it ended, and its route.
5
+ * `runOk` is the run's own ok, which automaticAnalysisEnvelope keeps when an analysis that did not
6
+ * complete turns `ok` false. That run finished, and the line says so.
7
+ */
4
8
  function runHeader(result, route) {
5
9
  const kind = result.dryRun === true ? "dry run" : result.dryRun === false ? "live run" : "run";
6
- return [
7
- `humanish run ${result.studyId}: ${kind} ${result.ok ? "finished" : "failed"}`,
8
- `route: ${route}`,
9
- ];
10
+ const ending = result.ok
11
+ ? "finished"
12
+ : result.runOk === true
13
+ ? "finished; the analysis did not complete"
14
+ : "failed";
15
+ return [`humanish run ${result.studyId}: ${kind} ${ending}`, `route: ${route}`];
10
16
  }
11
17
  /**
12
18
  * A run's human output: its lines on stdout and, when it failed with an error, that error on
@@ -31,6 +37,18 @@ function sandboxIdsLine(result) {
31
37
  ? [`sandbox ids: .humanish/runs/${result.runId}/sandbox-receipts.ndjson`]
32
38
  : [];
33
39
  }
40
+ /**
41
+ * How a run's output names its subject: by the URL its participants open. A product studied at a
42
+ * desktop has no URL, so it is named with its source, as in `humanish (desktop-cli)`. A refusal
43
+ * made before the route resolved a URL has none either, so it names the URL the study declares.
44
+ */
45
+ function subjectName(appUrl, subject) {
46
+ if (appUrl !== "")
47
+ return appUrl;
48
+ if (subject.product !== undefined)
49
+ return `${subject.product.name} (${subject.source})`;
50
+ return subject.appUrl ?? subject.serve?.url ?? subject.source;
51
+ }
34
52
  /** A participant's status in words: a dry run's placeholder status says that nothing ran live. */
35
53
  function participantStatus(status) {
36
54
  return status === "contract_proof_only" ? "dry run, nothing ran live" : status;
@@ -83,12 +101,13 @@ export function formatTerminalStudyHuman(result) {
83
101
  ...result.warnings.map((warning) => `warning: ${warning}`),
84
102
  ]);
85
103
  }
86
- export function formatScriptedStudyHuman(result) {
104
+ /** `subject` is the study's declared subject, which names it when the result has no URL. */
105
+ export function formatScriptedStudyHuman(result, subject) {
87
106
  return withError(result.error, [
88
107
  ...runHeader(result, "scripted"),
89
108
  `run: ${result.runId}`,
90
109
  `actor: ${result.actor}`,
91
- `subject: ${result.appUrl}`,
110
+ `subject: ${subjectName(result.appUrl, subject)}`,
92
111
  ...(result.scenario
93
112
  ? [
94
113
  `scenario: ${result.scenario.id} @ ${result.scenario.sourceDigest.slice(0, 12)} (${result.scenario.source}, ${result.scenario.steps} step${result.scenario.steps === 1 ? "" : "s"})`,
@@ -108,12 +127,13 @@ export function formatScriptedStudyHuman(result) {
108
127
  ...result.warnings.map((warning) => `warning: ${warning}`),
109
128
  ]);
110
129
  }
111
- export function formatCuaStudyHuman(result) {
130
+ /** `subject` is the study's declared subject, which names it when the result has no URL. */
131
+ export function formatCuaStudyHuman(result, subject) {
112
132
  return withError(result.error, [
113
133
  ...runHeader(result, "computer-use"),
114
134
  `run: ${result.runId}`,
115
135
  `actor: ${result.actor}`,
116
- `subject: ${result.appUrl}`,
136
+ `subject: ${subjectName(result.appUrl, subject)}`,
117
137
  ...(result.subject?.source === "clone"
118
138
  ? [
119
139
  `repo: ${result.subject.repo}${result.subject.commit ? `@${result.subject.commit.slice(0, 12)}` : ""}${result.subject.envNames && result.subject.envNames.length > 0 ? ` env=[${result.subject.envNames.join(", ")}]` : ""}`,
@@ -57,7 +57,7 @@ function refuseCua(args, dryRun, code, message) {
57
57
  dryRun,
58
58
  runId: args.options.runId ?? "not-created",
59
59
  });
60
- writeResult(args.command, args.io, result, formatCuaStudyHuman);
60
+ writeResult(args.command, args.io, result, (value) => formatCuaStudyHuman(value, args.config.subject));
61
61
  args.io.setExitCode(2);
62
62
  }
63
63
  /** Parses the options, or writes the refusal and returns undefined. */
@@ -236,7 +236,7 @@ async function reportCuaRun(args, prepared, result, live) {
236
236
  ],
237
237
  };
238
238
  }
239
- writeResult(args.command, args.io, output, formatCuaStudyHuman);
239
+ writeResult(args.command, args.io, output, (value) => formatCuaStudyHuman(value, args.config.subject));
240
240
  args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
241
241
  await writeRunFindings(args.command, args.io, args.options.cwd, result);
242
242
  if (server && (result.observer?.ok || attachedObserver)) {
@@ -34,7 +34,7 @@ export function scriptedRouteRun(args) {
34
34
  throw new Error(`Expected the scripted route, got ${outcome.route}.`);
35
35
  }
36
36
  const result = outcome.result;
37
- writeResult(args.command, args.io, result, formatScriptedStudyHuman);
37
+ writeResult(args.command, args.io, result, (value) => formatScriptedStudyHuman(value, args.config.subject));
38
38
  args.io.setExitCode(result.ok && automaticAnalysisSucceeded(result) ? 0 : 2);
39
39
  await writeRunFindings(args.command, args.io, args.options.cwd, result);
40
40
  // Watch mode serves the Observer the route rendered through its finished run.
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { Command } from "commander";
4
- import { setUserKey } from "../../keys/key-resolution.js";
4
+ import { LISTED_KEYS, setUserKey } from "../../keys/key-resolution.js";
5
+ import { keyStatus, keyStatusLine } from "../../keys/key-status.js";
5
6
  import { saveCommsConnection } from "../../comms/connections.js";
6
7
  import { readCommsSetup } from "../../comms/setup.js";
7
8
  import { checkCommsConnection, configureCommsStudy, } from "../../comms/setup.js";
@@ -160,13 +161,11 @@ async function runTuiSession(loaded, runtime, cwd, discoveredKeys) {
160
161
  runIndexCache: new RunIndexCache(),
161
162
  observerSession: createTuiObserverSession(cwd),
162
163
  };
163
- let connectionNotice;
164
+ let returnTo;
164
165
  try {
165
166
  for (;;) {
166
167
  const outcome = await loaded.startTui({
167
- ...(connectionNotice === undefined
168
- ? {}
169
- : { initialScreen: "connections", connectionNotice }),
168
+ ...returnTo,
170
169
  cwd,
171
170
  version: { cli: CLI_VERSION },
172
171
  capabilities: tuiCapabilities(session),
@@ -175,9 +174,17 @@ async function runTuiSession(loaded, runtime, cwd, discoveredKeys) {
175
174
  });
176
175
  if (typeof outcome === "number")
177
176
  return outcome;
178
- if (outcome.action !== "agentmail-key")
177
+ returnTo =
178
+ outcome.action === "agentmail-key"
179
+ ? { initialScreen: "connections", connectionNotice: await storeAgentmailKey(session) }
180
+ : outcome.action === "provider-key"
181
+ ? {
182
+ initialScreen: "keys",
183
+ connectionNotice: await storeProviderKey(session, outcome.name),
184
+ }
185
+ : undefined;
186
+ if (returnTo === undefined)
179
187
  return 1;
180
- connectionNotice = await storeAgentmailKey(session);
181
188
  }
182
189
  }
183
190
  finally {
@@ -197,6 +204,14 @@ function connectionEnv(session) {
197
204
  function tuiCapabilities(session) {
198
205
  const { cwd, runtime, sessionEnv, runIndexCache, observerSession } = session;
199
206
  return {
207
+ keys: {
208
+ // The project's overlay and the stores, as `humanish keys` reads them from this directory.
209
+ status: async () => (await keyStatus({ cwd, env: connectionEnv(session) })).map((row) => ({
210
+ name: row.name,
211
+ set: row.source !== null,
212
+ line: keyStatusLine(row),
213
+ })),
214
+ },
200
215
  comms: {
201
216
  read: async () => ({
202
217
  ...(await readCommsSetup(cwd, connectionEnv(session))),
@@ -270,21 +285,48 @@ function tuiCapabilities(session) {
270
285
  },
271
286
  };
272
287
  }
288
+ const ENTRY_CANCELLED = "Key entry cancelled. Nothing was changed.";
289
+ const ENTRY_FAILED = "Could not store the key. Use a single non-empty line and check key-store permissions.";
290
+ /**
291
+ * Asks for one key with hidden input, after the surface has unmounted, and stores it in the user
292
+ * store. Only the host reads the credential. A value that discovery filled into this session is
293
+ * dropped so the stored one is read; an explicit env or file value still wins.
294
+ */
295
+ async function promptAndStoreKey(session, name, label) {
296
+ const { runtime, sessionEnv } = session;
297
+ const value = await runtime.promptSecret(label, runtime.stdin, runtime.stdout);
298
+ if (value === null || value === "")
299
+ return "cancelled";
300
+ setUserKey(name, value, sessionEnv);
301
+ if (session.discoveredKeys.has(name))
302
+ delete sessionEnv[name];
303
+ return "stored";
304
+ }
305
+ /**
306
+ * A key from the keys screen, asked for and stored as `humanish keys set <vendor>` does. Resolves to
307
+ * the notice the remounted keys screen shows.
308
+ */
309
+ async function storeProviderKey(session, name) {
310
+ if (!LISTED_KEYS.some((key) => key.name === name))
311
+ return `${name} is not a key humanish keys lists. Nothing was changed.`;
312
+ try {
313
+ return (await promptAndStoreKey(session, name, `Value for ${name}`)) === "stored"
314
+ ? `${name} stored.`
315
+ : ENTRY_CANCELLED;
316
+ }
317
+ catch {
318
+ return ENTRY_FAILED;
319
+ }
320
+ }
273
321
  /**
274
322
  * Prompts for the AgentMail key after the surface has unmounted, stores it and checks it. Resolves
275
323
  * to the notice the remounted connections screen shows.
276
324
  */
277
325
  async function storeAgentmailKey(session) {
278
- const { cwd, runtime, sessionEnv } = session;
279
- // startTui has unmounted: only the host reads the credential, then remounts the view.
280
- const value = await runtime.promptSecret("AgentMail API key", runtime.stdin, runtime.stdout);
281
- if (value === null || value === "")
282
- return "Key entry cancelled. Nothing was changed.";
326
+ const { cwd, runtime } = session;
283
327
  try {
284
- setUserKey("AGENTMAIL_API_KEY", value, sessionEnv);
285
- // Refresh only a value filled implicitly by discovery; explicit env/file wins.
286
- if (session.discoveredKeys.has("AGENTMAIL_API_KEY"))
287
- delete sessionEnv.AGENTMAIL_API_KEY;
328
+ if ((await promptAndStoreKey(session, "AGENTMAIL_API_KEY", "AgentMail API key")) !== "stored")
329
+ return ENTRY_CANCELLED;
288
330
  const saved = await saveCommsConnection(cwd);
289
331
  const storedNotice = saved.ok
290
332
  ? "Key stored. Project connection saved."
@@ -301,6 +343,6 @@ async function storeAgentmailKey(session) {
301
343
  : `${storedNotice} ${check.message}`;
302
344
  }
303
345
  catch {
304
- return "Could not store the key. Use a single non-empty line and check key-store permissions.";
346
+ return ENTRY_FAILED;
305
347
  }
306
348
  }
@@ -2,8 +2,9 @@ import { readFile, readdir } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { probeKeySources, } from "../keys/key-resolution.js";
5
+ import { DOCTOR_KEYS, probeKeySources, } from "../keys/key-resolution.js";
6
6
  import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
7
+ import { describeSandboxCeiling, sandboxCeiling } from "../substrates/e2b/lifetime.js";
7
8
  import { nodeSupportsTui, terminalSurfaceMessage, TUI_BUNDLE_URL } from "../tui/contract.js";
8
9
  import { detectLocalAgents, localAgentDoctorMessage, localAgentReady, NO_LOCAL_AGENT_MESSAGE, } from "../actors/local-agent/cli.js";
9
10
  import { leftoverClaudeTranscripts } from "../actors/local-agent/claude-participant.js";
@@ -95,6 +96,7 @@ export async function doctor(cwdInput, options = {}) {
95
96
  const checks = [
96
97
  ...(await projectChecks(projectRoot)),
97
98
  await desktopSdkCheck(setup),
99
+ sandboxCeilingCheck(env),
98
100
  terminalSurfaceCheck(),
99
101
  ...versionCheck(env),
100
102
  ...localAgentChecks(agents),
@@ -191,6 +193,11 @@ async function runtimeIgnoreCheck(projectRoot) {
191
193
  : `.gitignore does not list .humanish/; run ${cli("init --yes")}`,
192
194
  };
193
195
  }
196
+ /** The longest sandbox lifetime the E2B planners allow, and where that number came from. */
197
+ function sandboxCeilingCheck(env) {
198
+ const ceiling = sandboxCeiling(env);
199
+ return { name: "e2b sandbox ceiling", ok: ceiling.ok, message: describeSandboxCeiling(ceiling) };
200
+ }
194
201
  /**
195
202
  * The optional peer dep every live browser and terminal route needs. `npx -y humanish` does not
196
203
  * pull optional peers, so without this check an adopter's first live run fails on it, safely and at
@@ -266,7 +273,7 @@ function versionCheck(env) {
266
273
  }
267
274
  /** Probes the live-run keys, plus the receiving-email key a real-comms study names. */
268
275
  async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
269
- const keyNames = new Set(["OPENAI_API_KEY", "E2B_API_KEY", "GH_TOKEN", "CODEX_API_KEY"]);
276
+ const keyNames = new Set(DOCTOR_KEYS);
270
277
  let receivingKey = null;
271
278
  if (study) {
272
279
  const { resolveStudyManifest } = await import("../study/discover.js");
@@ -0,0 +1,5 @@
1
+ import type { AnalysisFindings } from "./findings.js";
2
+ /** The full findings block: the analysis, every finding with its evidence, and its limitations. */
3
+ export declare function formatFindings(view: AnalysisFindings): string[];
4
+ /** At most `limit` findings, one line each, and the command that prints all of them. */
5
+ export declare function formatFindingsSummary(view: AnalysisFindings, fullCommand: string, limit?: number): string[];
@@ -0,0 +1,169 @@
1
+ // The text of a run's `humanish.analysis-findings.v1` view (findings.ts): the full block `review`
2
+ // and `analyze show` print, and the summary at the end of a live `run`. It reads only the view.
3
+ import path from "node:path";
4
+ import { findingLead } from "../analysis/finding-lead.js";
5
+ import { plural } from "../run/text.js";
6
+ const IMPACT_TEXT = {
7
+ blocked_task: "blocked task",
8
+ friction: "friction",
9
+ recovery: "recovery",
10
+ uncertain: "uncertain",
11
+ };
12
+ const RECOVERY_TEXT = {
13
+ recovered: "recovered",
14
+ not_observed: "no recovery observed",
15
+ unknown: "recovery unknown",
16
+ };
17
+ /** `+1:05` from milliseconds since the first retained capture. */
18
+ function elapsedText(ms) {
19
+ const seconds = Math.floor(ms / 1000);
20
+ const minutes = Math.floor(seconds / 60);
21
+ const hours = Math.floor(minutes / 60);
22
+ const ss = String(seconds % 60).padStart(2, "0");
23
+ return hours > 0
24
+ ? `+${hours}:${String(minutes % 60).padStart(2, "0")}:${ss}`
25
+ : `+${minutes}:${ss}`;
26
+ }
27
+ /** One participant's cited moments: each frame once, at its earliest cited time. */
28
+ function momentsText(evidence) {
29
+ const frames = new Map();
30
+ let unframed = 0;
31
+ for (const entry of evidence) {
32
+ if (entry.frame === null) {
33
+ unframed++;
34
+ continue;
35
+ }
36
+ const earlier = frames.get(entry.frame);
37
+ if (earlier === undefined ||
38
+ (entry.elapsedMs !== null && (earlier ?? Infinity) > entry.elapsedMs))
39
+ frames.set(entry.frame, entry.elapsedMs);
40
+ }
41
+ const parts = [...frames]
42
+ .sort(([a], [b]) => a - b)
43
+ .map(([frame, ms]) => `frame ${frame}${ms === null ? "" : ` at ${elapsedText(ms)}`}`);
44
+ if (unframed > 0)
45
+ parts.push(`${plural(unframed, "item")} with no frame`);
46
+ return parts.join(", ");
47
+ }
48
+ /** The capture files, relative to the run directory, in frame order. */
49
+ function captureFiles(evidence, runRoot) {
50
+ return [
51
+ ...new Set(evidence
52
+ .filter((entry) => entry.capture !== null)
53
+ .sort((a, b) => (a.frame ?? 0) - (b.frame ?? 0))
54
+ .map((entry) => (runRoot ? path.relative(runRoot, entry.capture) : entry.capture))),
55
+ ];
56
+ }
57
+ function findingLines(finding, runRoot) {
58
+ const cited = [...new Set(finding.evidence.map((entry) => entry.streamId))];
59
+ const labels = new Map(finding.affected.map((p) => [p.streamId, p.label]));
60
+ const affectedIds = finding.affected.map((p) => p.streamId);
61
+ const streams = [...affectedIds, ...cited.filter((id) => !labels.has(id))];
62
+ const captures = captureFiles(finding.evidence, runRoot);
63
+ const correction = finding.correction;
64
+ const lead = findingLead(finding, correction);
65
+ return [
66
+ ...(lead === null
67
+ ? [`${finding.id} ${finding.title}`]
68
+ : [
69
+ `${finding.id} ${lead.headline}`,
70
+ ...(lead.account === null ? [] : [` ${lead.account}`]),
71
+ ` evidence: ${finding.title}`,
72
+ ]),
73
+ ` impact: ${IMPACT_TEXT[finding.impact]} · confidence: ${finding.confidence} · recovery: ${RECOVERY_TEXT[finding.recovery]}`,
74
+ ` ${finding.summary}`,
75
+ ` affected: ${finding.affected.length} of ${plural(finding.exposedCount, "exposed participant")}`,
76
+ ...streams.map((streamId) => {
77
+ const moments = momentsText(finding.evidence.filter((entry) => entry.streamId === streamId));
78
+ const label = `${labels.get(streamId) ?? streamId}${labels.has(streamId) ? "" : " (cited, not affected)"}`;
79
+ return ` - ${label}${moments ? `: ${moments}` : ""}`;
80
+ }),
81
+ ...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
82
+ ` next step: ${finding.nextStep}`,
83
+ ...(correction === null
84
+ ? []
85
+ : [
86
+ ` human review: ${correction.status}, ${correction.reason}${correction.replacementClaim ? ` Amended claim: ${correction.replacementClaim}` : ""}`,
87
+ ]),
88
+ ];
89
+ }
90
+ function designFindingLines(finding, runRoot) {
91
+ const captures = captureFiles(finding.evidence, runRoot);
92
+ return [
93
+ `${finding.id} ${finding.severity}: ${finding.headline}`,
94
+ ` screen: ${finding.screen}`,
95
+ ` notice: ${finding.notice}`,
96
+ ` why it matters: ${finding.whyItMatters}`,
97
+ ` suggestion: ${finding.suggestion}`,
98
+ ` seen by: ${finding.seenBy.map((p) => p.label).join(", ")} · confidence: ${finding.confidence}`,
99
+ ...(captures.length === 0 ? [] : [` captures: ${captures.join(", ")}`]),
100
+ ];
101
+ }
102
+ /** Nothing for an analysis written before design findings. */
103
+ function designSection(view) {
104
+ const design = view.designFindings;
105
+ if (design === null)
106
+ return [];
107
+ if (design.length === 0)
108
+ return ["", "design findings: none in the reviewed captures"];
109
+ return [
110
+ "",
111
+ `design findings: ${design.length}, most severe first`,
112
+ ...design.flatMap((finding, index) => [
113
+ ...(index === 0 ? [] : [""]),
114
+ ...designFindingLines(finding, view.runPath ?? ""),
115
+ ]),
116
+ ];
117
+ }
118
+ /** The full findings block: the analysis, every finding with its evidence, and its limitations. */
119
+ export function formatFindings(view) {
120
+ if (view.state !== "ready" && view.state !== "stale")
121
+ return [`findings: none. ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
122
+ const model = [view.provider, view.model].filter(Boolean).join(" ");
123
+ const cost = view.estimatedCostUsd === null ? "" : `, estimated $${view.estimatedCostUsd.toFixed(2)}`;
124
+ return [
125
+ `findings: ${view.findings.length} from analysis ${view.analysisId} (${view.status}, ${model}${cost})`,
126
+ ...(view.state === "stale" ? [`warning: ${view.message}`, `next: ${view.next}`] : []),
127
+ ...(view.summary ? [view.summary] : []),
128
+ ...view.findings.flatMap((finding) => ["", ...findingLines(finding, view.runPath ?? "")]),
129
+ ...designSection(view),
130
+ ...(view.limitations.length === 0
131
+ ? []
132
+ : ["", "limitations:", ...view.limitations.map((limitation) => `- ${limitation}`)]),
133
+ ...view.warnings.map((warning) => `warning: ${warning}`),
134
+ "",
135
+ `analysis: ${view.path}`,
136
+ ];
137
+ }
138
+ /** At most `limit` findings, one line each, and the command that prints all of them. */
139
+ export function formatFindingsSummary(view, fullCommand, limit = 3) {
140
+ if (view.state !== "ready" && view.state !== "stale")
141
+ return [`findings: ${view.message}`, ...(view.next ? [`next: ${view.next}`] : [])];
142
+ const shown = view.findings.slice(0, limit);
143
+ const design = view.designFindings;
144
+ return [
145
+ `findings: ${view.findings.length === 0 ? "none" : view.findings.length}${view.state === "stale" ? " (stale)" : ""}`,
146
+ ...shown.map((finding) => {
147
+ const qualities = `${IMPACT_TEXT[finding.impact]}, ${finding.confidence} confidence, ${RECOVERY_TEXT[finding.recovery]}`;
148
+ const lead = findingLead(finding, finding.correction);
149
+ if (lead === null)
150
+ return `- ${finding.id} ${qualities}: ${finding.title}`;
151
+ return `- ${finding.id} ${lead.headline} (${lead.corrected ? "corrected in human review" : qualities})`;
152
+ }),
153
+ ...(view.findings.length > limit
154
+ ? [`- and ${plural(view.findings.length - limit, "more finding")}`]
155
+ : []),
156
+ ...(design === null
157
+ ? []
158
+ : [
159
+ `design findings: ${design.length === 0 ? "none" : design.length}`,
160
+ ...design
161
+ .slice(0, limit)
162
+ .map((finding) => `- ${finding.id} ${finding.severity}: ${finding.headline}`),
163
+ ...(design.length > limit
164
+ ? [`- and ${plural(design.length - limit, "more design finding")}`]
165
+ : []),
166
+ ]),
167
+ `all findings: ${fullCommand}`,
168
+ ];
169
+ }
@@ -137,10 +137,6 @@ export declare function readRunAnalysis(cwd: string, run: string, analysisId?: s
137
137
  export declare function missingRunFindings(cwd: string, run: string): AnalysisFindings;
138
138
  /** The findings of a run's selected analysis, or why it has none. */
139
139
  export declare function readRunFindings(cwd: string, run: string, analysisId?: string): Promise<AnalysisFindings>;
140
- /** The full findings block: the analysis, every finding with its evidence, and its limitations. */
141
- export declare function formatFindings(view: AnalysisFindings): string[];
142
- /** At most `limit` findings, one line each, and the command that prints all of them. */
143
- export declare function formatFindingsSummary(view: AnalysisFindings, fullCommand: string, limit?: number): string[];
144
140
  /**
145
141
  * The end of a live run's human output, after its `analysis:` line: the findings of the analysis
146
142
  * the run just completed. Prints nothing for --json, or when the run's analysis did not complete,