humanish 0.109.1 → 0.110.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/ARCHITECTURE.md +14 -11
  2. package/CHANGELOG.md +24 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +97 -48
  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/comms.js +25 -7
  16. package/dist/cli/commands/observe.js +4 -2
  17. package/dist/cli/commands/run-command.js +4 -3
  18. package/dist/cli/commands/run-signals.d.ts +2 -0
  19. package/dist/cli/commands/run-signals.js +96 -44
  20. package/dist/cli/commands/runs.d.ts +5 -0
  21. package/dist/cli/commands/runs.js +89 -67
  22. package/dist/cli/commands/setup.js +19 -5
  23. package/dist/cli/commands/study-format.js +29 -2
  24. package/dist/cli/commands/study-route-computer-use.js +2 -0
  25. package/dist/cli/commands/study-route-scripted.js +2 -0
  26. package/dist/cli/commands/study-route-shared-world.js +2 -0
  27. package/dist/cli/commands/study-route-terminal.js +2 -0
  28. package/dist/cli/commands/study.js +4 -3
  29. package/dist/cli/commands/tui.js +4 -3
  30. package/dist/cli/commands/watch.js +2 -2
  31. package/dist/cli/doctor.d.ts +7 -2
  32. package/dist/cli/doctor.js +47 -8
  33. package/dist/cli/findings.d.ts +103 -0
  34. package/dist/cli/findings.js +316 -0
  35. package/dist/cli/first-run-path.d.ts +5 -4
  36. package/dist/cli/first-run-path.js +14 -7
  37. package/dist/cli/io.d.ts +17 -3
  38. package/dist/cli/io.js +22 -4
  39. package/dist/cli/product-sentence.d.ts +3 -3
  40. package/dist/cli/product-sentence.js +3 -3
  41. package/dist/comms/receiving-runtime.js +1 -1
  42. package/dist/comms/setup.js +1 -1
  43. package/dist/evidence/redaction.d.ts +7 -3
  44. package/dist/evidence/redaction.js +68 -24
  45. package/dist/feedback/export-bundle.js +6 -1
  46. package/dist/feedback/export.js +3 -0
  47. package/dist/keys/env-file.d.ts +4 -0
  48. package/dist/keys/env-file.js +17 -0
  49. package/dist/keys/key-resolution.d.ts +7 -0
  50. package/dist/keys/key-resolution.js +10 -8
  51. package/dist/observer/data.d.ts +14 -1
  52. package/dist/observer/data.js +19 -9
  53. package/dist/observer/library.d.ts +4 -0
  54. package/dist/observer/library.js +18 -15
  55. package/dist/observer/render.js +8 -2
  56. package/dist/observer/run-routes.d.ts +2 -0
  57. package/dist/observer/run-routes.js +43 -34
  58. package/dist/observer-app.html +3 -3
  59. package/dist/routes/computer-use/bundle.js +3 -1
  60. package/dist/routes/computer-use/fanout-records.js +1 -1
  61. package/dist/routes/computer-use/live-phase.d.ts +1 -0
  62. package/dist/routes/computer-use/participant-execution.js +3 -1
  63. package/dist/routes/computer-use/participant-model.js +5 -2
  64. package/dist/routes/computer-use/plan.js +4 -4
  65. package/dist/routes/computer-use/preflight.js +2 -2
  66. package/dist/routes/computer-use/result.js +17 -28
  67. package/dist/routes/computer-use/single-bundle.js +7 -1
  68. package/dist/routes/computer-use/types.d.ts +6 -0
  69. package/dist/routes/scripted/bundle.js +4 -2
  70. package/dist/routes/scripted/plan.js +2 -2
  71. package/dist/routes/scripted/result.d.ts +3 -5
  72. package/dist/routes/scripted/result.js +16 -22
  73. package/dist/routes/scripted/scenario.js +4 -4
  74. package/dist/routes/scripted/types.d.ts +1 -0
  75. package/dist/routes/shared-world/bundle-records.js +6 -1
  76. package/dist/routes/shared-world/bundle.js +2 -0
  77. package/dist/routes/shared-world/plan.js +1 -1
  78. package/dist/routes/shared-world/result.js +22 -22
  79. package/dist/routes/shared-world/route.js +1 -1
  80. package/dist/routes/shared-world/types.d.ts +2 -0
  81. package/dist/routes/terminal/bundle.js +4 -2
  82. package/dist/routes/terminal/credentials.js +1 -1
  83. package/dist/routes/terminal/dry-run.js +8 -12
  84. package/dist/routes/terminal/ledger.d.ts +3 -3
  85. package/dist/routes/terminal/ledger.js +6 -6
  86. package/dist/routes/terminal/live-finish.js +23 -17
  87. package/dist/routes/terminal/plan.js +4 -4
  88. package/dist/routes/terminal/result.d.ts +6 -7
  89. package/dist/routes/terminal/result.js +6 -22
  90. package/dist/routes/terminal/session.js +1 -1
  91. package/dist/routes/terminal/types.d.ts +3 -0
  92. package/dist/run/bundle-shape.d.ts +3 -1
  93. package/dist/run/bundle-shape.js +26 -1
  94. package/dist/run/bundle.d.ts +30 -0
  95. package/dist/run/display.d.ts +59 -0
  96. package/dist/run/display.js +106 -0
  97. package/dist/run/dry-run.js +11 -7
  98. package/dist/run/judge.d.ts +3 -2
  99. package/dist/run/judge.js +10 -0
  100. package/dist/run/pricing.js +1 -1
  101. package/dist/run/projection.d.ts +0 -2
  102. package/dist/run/projection.js +0 -11
  103. package/dist/run/reclaim-outcomes.d.ts +89 -0
  104. package/dist/run/reclaim-outcomes.js +213 -0
  105. package/dist/run/reclaim.d.ts +18 -14
  106. package/dist/run/reclaim.js +179 -105
  107. package/dist/run/results.d.ts +11 -18
  108. package/dist/run/results.js +2 -2
  109. package/dist/run/run-index.d.ts +11 -1
  110. package/dist/run/run-index.js +11 -0
  111. package/dist/run/run.d.ts +34 -18
  112. package/dist/run/run.js +177 -72
  113. package/dist/run/sandbox-creates.d.ts +59 -0
  114. package/dist/run/sandbox-creates.js +138 -0
  115. package/dist/run/sandbox-ids.d.ts +53 -0
  116. package/dist/run/sandbox-ids.js +198 -0
  117. package/dist/run/sandbox-receipts.d.ts +16 -0
  118. package/dist/run/sandbox-receipts.js +70 -0
  119. package/dist/run/stats.d.ts +5 -0
  120. package/dist/run/stats.js +8 -4
  121. package/dist/run/status.d.ts +6 -9
  122. package/dist/run/status.js +7 -20
  123. package/dist/run/stored-runs.d.ts +5 -14
  124. package/dist/run/stored-runs.js +31 -137
  125. package/dist/run/streams.d.ts +3 -2
  126. package/dist/run-study.js +6 -1
  127. package/dist/study/doctor.d.ts +1 -1
  128. package/dist/study/doctor.js +7 -4
  129. package/dist/study/init-templates.js +2 -2
  130. package/dist/study/init.js +14 -3
  131. package/dist/study/keys.d.ts +12 -0
  132. package/dist/study/keys.js +32 -0
  133. package/dist/study/persona.d.ts +4 -0
  134. package/dist/study/persona.js +4 -2
  135. package/dist/study/types.d.ts +1 -1
  136. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  137. package/dist/substrates/e2b/peer-install.js +161 -0
  138. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  139. package/dist/substrates/e2b/sandbox.js +128 -35
  140. package/dist/substrates/e2b/sdk.d.ts +33 -0
  141. package/dist/substrates/e2b/sdk.js +21 -21
  142. package/dist/substrates/shell.d.ts +2 -0
  143. package/dist/substrates/shell.js +4 -0
  144. package/dist/tui-app.js +133 -133
  145. package/dist/verify/artifacts.d.ts +4 -0
  146. package/dist/verify/artifacts.js +8 -0
  147. package/dist/verify/liveness.d.ts +19 -3
  148. package/dist/verify/liveness.js +53 -28
  149. package/dist/verify/verify.d.ts +4 -1
  150. package/dist/verify/verify.js +27 -3
  151. package/docs/architecture/actor-contract.md +2 -1
  152. package/docs/architecture/observer.md +22 -7
  153. package/docs/architecture/restricted-codex-analysis.md +1 -1
  154. package/docs/architecture/task-protocol-support.md +13 -13
  155. package/docs/architecture/terminal-product-route.md +1 -1
  156. package/docs/contracts/run-bundle.md +90 -7
  157. package/docs/contracts/schemas.md +39 -22
  158. package/docs/contracts/study-analysis.md +45 -0
  159. package/docs/contracts/study-costs.md +8 -0
  160. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  161. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  162. package/docs/principles/invariants-and-defaults.md +11 -8
  163. package/package.json +5 -3
  164. package/skills/humanish/SKILL.md +18 -4
@@ -21,13 +21,15 @@
21
21
  // operator's machine. The same trick on the terminal route would be the opposite: it would move
22
22
  // code execution out of the sandbox and onto a real disk, which is why this is a computer-use
23
23
  // provider and nothing else. Even so, these are coding agents with their own shell and file tools,
24
- // so each one is spawned tool-restricted, in a scratch directory, with a per-turn timeout.
24
+ // so each one is spawned tool-restricted, in a scratch directory, with a per-turn timeout. Claude
25
+ // Code's flags, environment and stream checks are in claude-participant.ts.
25
26
  import { spawn } from "node:child_process";
26
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
27
+ import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
27
28
  import { tmpdir } from "node:os";
28
29
  import path from "node:path";
29
30
  import { admitsCodexCliVersion, parseCodexCliVersion } from "../codex/codex-admission.js";
30
31
  import { restrictedCodexNpmTarget } from "../codex/restricted-executable.js";
32
+ import { ClaudeStreamGuard, admitsClaudeCodeVersion, claudeCodeVersionMessage, claudeParticipantEnv, claudeParticipantFlags, parseClaudeCodeVersion, } from "./claude-participant.js";
31
33
  const LOCAL_AGENTS = [
32
34
  { id: "codex", bin: "codex", label: "Codex", credentialPath: ".codex/auth.json" },
33
35
  {
@@ -120,9 +122,14 @@ export function parseAgentJson(text) {
120
122
  throw new Error("the local agent did not return a JSON object");
121
123
  }
122
124
  const defaultSpawn = async (bin, args, options) => await new Promise((resolve) => {
123
- const child = spawn(bin, [...args], { cwd: options.cwd, stdio: ["ignore", "pipe", "pipe"] });
125
+ const child = spawn(bin, [...args], {
126
+ cwd: options.cwd,
127
+ env: options.env,
128
+ stdio: ["ignore", "pipe", "pipe"],
129
+ });
124
130
  let stdout = "";
125
131
  let stderr = "";
132
+ let scanned = 0;
126
133
  const timer = setTimeout(() => child.kill("SIGKILL"), options.timeoutMs);
127
134
  // Stopping a run must stop the thinking too: a local agent mid-turn can hold a terminal for
128
135
  // minutes, and a Stop that leaves it running is not a stop.
@@ -136,6 +143,12 @@ const defaultSpawn = async (bin, args, options) => await new Promise((resolve) =
136
143
  };
137
144
  child.stdout?.on("data", (chunk) => {
138
145
  stdout += chunk.toString("utf8");
146
+ for (let end = stdout.indexOf("\n", scanned); end >= 0; end = stdout.indexOf("\n", scanned)) {
147
+ const line = stdout.slice(scanned, end);
148
+ scanned = end + 1;
149
+ if (options.onLine?.(line) === true)
150
+ child.kill("SIGKILL");
151
+ }
139
152
  });
140
153
  child.stderr?.on("data", (chunk) => {
141
154
  stderr += chunk.toString("utf8");
@@ -214,35 +227,33 @@ export function createLocalAgentProvider(options) {
214
227
  const screenshotPath = path.join(work, "screen.png");
215
228
  await writeFile(screenshotPath, frame);
216
229
  const prompt = promptFor(request, screenshotPath);
217
- const args = [
218
- "-p",
219
- "--output-format",
220
- "json",
221
- // Read is the only tool it needs (the screenshot) and the only one it gets.
222
- "--allowedTools",
223
- "Read",
224
- ...(options.model === undefined ? [] : ["--model", options.model]),
225
- // `--allowedTools` takes a list, so a prompt placed right after it is read as a tool
226
- // name and Claude Code exits 1 with "Input must be provided". Three of three one-shot
227
- // runs failed on turn one that way on 2026-09-01 (Claude Code 2.1.257); `--` ends the
228
- // options so the prompt is the prompt.
229
- "--",
230
- prompt,
231
- ];
232
- const result = await spawnFn(descriptor.bin, args, {
230
+ const folders = [work, await realpath(work).catch(() => work)];
231
+ const watch = new ClaudeStreamGuard(folders);
232
+ const result = await spawnFn(descriptor.bin, claudeOneShotArgs(prompt, options.model), {
233
233
  cwd: work,
234
+ env: claudeParticipantEnv(options.env ?? process.env),
234
235
  timeoutMs,
235
236
  ...(signal === undefined ? {} : { signal }),
237
+ // Stops the process as soon as a line is refused; the full scan below decides the turn.
238
+ onLine: (line) => inspectLine(watch, line) !== undefined,
236
239
  });
240
+ const { refusal, result: final } = scanClaudeStream(result.stdout, folders);
241
+ if (refusal !== undefined)
242
+ throw refusal;
237
243
  if (result.code !== 0) {
238
244
  // Fail loud with the CLI's own words. A rate-limited plan says so here, and that is a
239
245
  // sentence the operator can act on, unlike "turn failed".
240
246
  const detail = (result.stderr || result.stdout).trim().slice(-400);
241
247
  throw new Error(`${descriptor.label} exited ${result.code ?? "on a signal"}: ${detail}`);
242
248
  }
243
- // Claude Code returns an envelope on stdout whose `result` field holds the text.
244
- const envelope = parseAgentJson(result.stdout);
245
- const payload = typeof envelope.result === "string" ? envelope.result : result.stdout;
249
+ if (final === undefined)
250
+ throw new Error(`${descriptor.label} returned no result message`);
251
+ if (final.is_error === true) {
252
+ const ending = typeof final.subtype === "string" ? final.subtype : "in error";
253
+ const said = typeof final.result === "string" ? final.result.slice(0, 160) : "";
254
+ throw new Error(`${descriptor.label} turn ended ${ending}: ${said}`);
255
+ }
256
+ const payload = typeof final.result === "string" ? final.result : "";
246
257
  const turn = parseAgentJson(payload);
247
258
  const actions = toCuaActions(Array.isArray(turn.actions) ? turn.actions : []);
248
259
  const done = turn.done === true || (actions.length === 0 && typeof turn.message === "string");
@@ -268,6 +279,53 @@ export function createLocalAgentProvider(options) {
268
279
  },
269
280
  };
270
281
  }
282
+ /** The one-shot argv: stream-json out so every message can be checked, then the prompt after `--`. */
283
+ function claudeOneShotArgs(prompt, model) {
284
+ return [
285
+ "-p",
286
+ "--output-format",
287
+ "stream-json",
288
+ // Required for stream-json output in -p mode.
289
+ "--verbose",
290
+ ...claudeParticipantFlags(model),
291
+ // `--tools` takes a list, so a prompt placed right after it would be read as a tool name.
292
+ // `--` ends the options so the prompt is the prompt.
293
+ "--",
294
+ prompt,
295
+ ];
296
+ }
297
+ /** The refusal one stdout line proves, if any. A line that is not JSON proves nothing. */
298
+ function inspectLine(guard, line) {
299
+ let message;
300
+ try {
301
+ message = JSON.parse(line);
302
+ }
303
+ catch {
304
+ return undefined;
305
+ }
306
+ return message !== null && typeof message === "object" && !Array.isArray(message)
307
+ ? guard.inspect(message)
308
+ : undefined;
309
+ }
310
+ /** The first refusal in a finished one-shot stream, and its last `result` message. */
311
+ function scanClaudeStream(stdout, folders) {
312
+ const guard = new ClaudeStreamGuard(folders);
313
+ let result;
314
+ for (const line of stdout.split("\n")) {
315
+ const refusal = inspectLine(guard, line);
316
+ if (refusal !== undefined)
317
+ return { refusal };
318
+ try {
319
+ const message = JSON.parse(line);
320
+ if (message?.type === "result")
321
+ result = message;
322
+ }
323
+ catch {
324
+ // not a message
325
+ }
326
+ }
327
+ return result === undefined ? {} : { result };
328
+ }
271
329
  /** No shell, prompts or model request. Bound time and output even for a broken CLI. */
272
330
  async function authProbe(bin, args, env) {
273
331
  return await new Promise((resolve) => {
@@ -375,7 +433,10 @@ export async function detectLocalAgents(options = {}) {
375
433
  .catch(() => false);
376
434
  });
377
435
  const found = [];
436
+ const probe = options.authProbe ?? authProbe;
378
437
  for (const descriptor of LOCAL_AGENTS) {
438
+ if (options.only !== undefined && descriptor.id !== options.only)
439
+ continue;
379
440
  const binPath = await which(descriptor.bin);
380
441
  if (binPath === undefined)
381
442
  continue;
@@ -384,24 +445,42 @@ export async function detectLocalAgents(options = {}) {
384
445
  : descriptor.id === "claude" && env.CLAUDE_CONFIG_DIR
385
446
  ? path.join(env.CLAUDE_CONFIG_DIR, ".credentials.json")
386
447
  : path.join(home, descriptor.credentialPath);
387
- const status = await (options.authProbe ?? authProbe)(binPath, descriptor.id === "codex" ? ["login", "status"] : ["auth", "status"], env)
448
+ // Claude Code is asked with the participant's own environment, so a login that only works
449
+ // through a dropped variable (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN) reads as signed out.
450
+ const probeEnv = descriptor.id === "claude" ? claudeParticipantEnv(env) : env;
451
+ const status = await probe(binPath, descriptor.id === "codex" ? ["login", "status"] : ["auth", "status"], probeEnv)
388
452
  .then((result) => classifyAuth(descriptor.id, result))
389
453
  .catch(() => ({ authStatus: "unknown" }));
454
+ const version = descriptor.id === "claude"
455
+ ? await probe(binPath, ["--version"], probeEnv)
456
+ .then((result) => result.code === 0 ? parseClaudeCodeVersion(result.stdout) : undefined)
457
+ .catch(() => undefined)
458
+ : undefined;
390
459
  found.push({
391
460
  ...descriptor,
392
461
  binPath,
393
462
  credentialsPresent: await exists(file).catch(() => false),
394
463
  ...status,
464
+ ...(version === undefined ? {} : { version }),
395
465
  });
396
466
  }
397
467
  return found;
398
468
  }
399
469
  /** Doctor's row when neither Codex nor Claude Code is installed. */
400
470
  export const NO_LOCAL_AGENT_MESSAGE = "no local coding agent found. openai-computer-use needs OPENAI_API_KEY; local-agent needs Codex or Claude Code installed and signed in. Hosted desktops also need E2B_API_KEY.";
471
+ /** Whether a signed-in agent can still not run participants here: an old Claude Code. */
472
+ export function localAgentVersionRefusal(agent) {
473
+ return agent.id === "claude" && !admitsClaudeCodeVersion(agent.version)
474
+ ? claudeCodeVersionMessage(agent.version)
475
+ : undefined;
476
+ }
401
477
  /** Doctor's row for one installed agent, in the register the other rows use. */
402
478
  export function localAgentDoctorMessage(agent) {
479
+ const outdated = localAgentVersionRefusal(agent);
480
+ if (agent.authStatus === "authenticated" && outdated !== undefined)
481
+ return `${agent.label} reports signed in, but cannot run participants yet. ${outdated}`;
403
482
  if (agent.authStatus === "authenticated")
404
- return `${agent.label} reports signed in; a study with actors[0].type: local-agent can use it instead of a provider API key. Account access and limits are untested.`;
483
+ return `${agent.label}${agent.version === undefined ? "" : ` ${agent.version}`} reports signed in; a study with actor.type: local-agent can use it instead of a provider API key. Account access and limits are untested.`;
405
484
  const status = agent.id === "codex" ? "codex login status" : "claude auth status";
406
485
  return agent.authStatus === "unauthenticated"
407
486
  ? `${agent.label} is installed and reports not signed in; run \`${agent.id === "codex" ? "codex login" : "claude auth login"}\``
@@ -11,12 +11,12 @@ export type LocalAgentRefusal =
11
11
  kind: "signin-required";
12
12
  message: string;
13
13
  }
14
- /** Hosted Codex on an unsupported platform or a CLI release the launcher refuses. */
14
+ /** Hosted Codex on an unsupported platform or release, or a Claude Code older than the floor. */
15
15
  | {
16
16
  kind: "unsupported";
17
17
  message: string;
18
18
  }
19
- /** A ChatGPT-account Codex with execution.caps, which it cannot price. */
19
+ /** A ChatGPT-account Codex with a dollar cap in `caps`, which it cannot price. */
20
20
  | {
21
21
  kind: "unpriced-cap";
22
22
  message: string;
@@ -1,30 +1,37 @@
1
1
  // Whether a local-agent participant can run on this machine before anything is acquired: its CLI
2
- // is on `PATH` and signed in, a hosted Codex CLI is a qualified release on a supported platform, and
3
- // a ChatGPT-account Codex is not asked to enforce a dollar cap it has no price for. Each route maps
4
- // the refusal's kind to its own error code.
2
+ // is on `PATH` and signed in, a hosted Codex CLI is a qualified release on a supported platform, a
3
+ // Claude Code CLI has the flags that restrict a participant, and a ChatGPT-account Codex is not
4
+ // asked to enforce a dollar cap it has no price for. Each route maps the refusal's kind to its own
5
+ // error code.
5
6
  import { describeCodexCliAdmission } from "../codex/codex-admission.js";
6
- import { checkHostedCodexCompatibility, detectLocalAgents } from "./cli.js";
7
+ import { checkHostedCodexCompatibility, detectLocalAgents, localAgentVersionRefusal, } from "./cli.js";
7
8
  export async function localAgentRefusal(args) {
8
9
  const { agent, env, caps } = args;
9
- const available = await detectLocalAgents({ env });
10
+ const available = await detectLocalAgents({ env, only: agent });
10
11
  const chosen = available.find((candidate) => candidate.id === agent);
11
12
  if (chosen === undefined) {
12
13
  return {
13
14
  kind: "agent-missing",
14
- message: `actors[0].type: local-agent needs the ${agent} CLI on PATH and signed in. ` +
15
- `Install it, or set OPENAI_API_KEY and use actors[0].type: openai-computer-use instead.`,
15
+ message: `actor.type: local-agent needs the ${agent} CLI on PATH and signed in. ` +
16
+ `Install it, or set OPENAI_API_KEY and use actor.type: openai-computer-use instead.`,
16
17
  };
17
18
  }
18
19
  if (chosen.authStatus !== "authenticated") {
19
20
  return {
20
21
  kind: "signin-required",
21
22
  message: chosen.authStatus === "unauthenticated"
22
- ? `${chosen.label} reports it is not signed in; run \`${chosen.id === "codex" ? "codex login" : "claude auth login"}\`, then retry.`
23
+ ? chosen.id === "codex"
24
+ ? "Codex reports it is not signed in; run `codex login`, then retry."
25
+ : `Claude Code reports it is not signed in with the environment a participant gets, which keeps its own login but not ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN. Run \`claude auth login\`, then retry.`
23
26
  : `${chosen.label} authentication status could not be checked. Run \`${chosen.id === "codex" ? "codex login status" : "claude auth status"}\` and update the CLI if needed. No desktop was launched.`,
24
27
  };
25
28
  }
26
- if (chosen.id !== "codex")
27
- return undefined;
29
+ if (chosen.id === "claude") {
30
+ const outdated = localAgentVersionRefusal(chosen);
31
+ return outdated === undefined
32
+ ? undefined
33
+ : { kind: "unsupported", message: `${outdated} No desktop was launched.` };
34
+ }
28
35
  const compatibility = await checkHostedCodexCompatibility(chosen.binPath, { env });
29
36
  if (compatibility !== "supported") {
30
37
  return {
@@ -38,7 +45,7 @@ export async function localAgentRefusal(args) {
38
45
  (caps.maxUsd !== undefined || caps.maxTotalUsd !== undefined)) {
39
46
  return {
40
47
  kind: "unpriced-cap",
41
- message: "A ChatGPT-account Codex participant has no API-dollar price, so execution.caps.maxUsd/maxTotalUsd cannot be enforced. Remove the dollar cap and use finite execution timeout/step limits, or use an API-backed participant; no desktop was launched.",
48
+ message: "A ChatGPT-account Codex participant has no API-dollar price, so caps.maxUsd/maxTotalUsd cannot be enforced. Remove the dollar cap and use finite execution timeout/step limits, or use an API-backed participant; no desktop was launched.",
42
49
  };
43
50
  }
44
51
  return undefined;
@@ -5,7 +5,8 @@ import { analyzeRun, correctAnalysis, dryRunBundleRefusal, showAnalysis, } from
5
5
  import { listAnalyses } from "../../analysis/store.js";
6
6
  import { listAnalysisExecutions } from "../../analysis/store-executions.js";
7
7
  import { resolveRunPath } from "../../run/locate.js";
8
- import { CWD_OPTION_DESCRIPTION, discoverCliKeys, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
8
+ import { CWD_OPTION_DESCRIPTION, discoverCliKeys, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, wantsJson, } from "../io.js";
9
+ import { analysisFindings, formatFindings, missingRunFindings, readRunAnalysis, } from "../findings.js";
9
10
  import { resolvePhysicalCwd } from "../../run/paths.js";
10
11
  /** Commander may collect a shared flag on the parent; only explicit values override leaf defaults. */
11
12
  function analysisSelection(options, command) {
@@ -46,7 +47,7 @@ export function registerAnalyzeCommand(parent, io) {
46
47
  .action((options, command) => handleAnalyzeList(io, options, command));
47
48
  analyze
48
49
  .command("show")
49
- .description("Read validated analysis and correction history. Defaults to the latest usable version.")
50
+ .description("Print a run's analysis findings with the evidence each one cites. --json prints the validated analysis record and its correction history. Defaults to the latest usable version.")
50
51
  .option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
51
52
  .option("--id <id>", "Exact analysis version.")
52
53
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
@@ -180,9 +181,24 @@ async function handleAnalyzeList(io, options, command) {
180
181
  }
181
182
  async function handleAnalyzeShow(io, options, command) {
182
183
  options = analysisSelection(options, command);
183
- const result = await showAnalysis(options.cwd, options.run, options.id);
184
- writeResult(command, io, result, (value) => forTerminal(JSON.stringify(value, null, 2) + "\n"));
185
- io.setExitCode(result.state === "invalid" ? 2 : 0);
184
+ if (wantsJson(command)) {
185
+ const result = await showAnalysis(options.cwd, options.run, options.id);
186
+ writeResult(command, io, result, () => "");
187
+ io.setExitCode(result.state === "invalid" ? 2 : 0);
188
+ return;
189
+ }
190
+ const source = await readRunAnalysis(options.cwd, options.run, options.id);
191
+ const findings = source ? analysisFindings(source) : missingRunFindings(options.cwd, options.run);
192
+ writeResult(command, io, findings, formatAnalysisShowHuman);
193
+ // The exit code the --json form gives for the same record.
194
+ io.setExitCode(source === null || source.loaded.state === "invalid" ? 2 : 0);
195
+ }
196
+ /**
197
+ * `analyze show` without --json: the findings, or why there are none and the command that gets
198
+ * them.
199
+ */
200
+ function formatAnalysisShowHuman(findings) {
201
+ return ([`humanish analyze show ${findings.runId}`, "", ...formatFindings(findings)].join("\n") + "\n");
186
202
  }
187
203
  async function handleAnalyzeCorrect(io, options, command) {
188
204
  options = analysisSelection(options, command);
@@ -9,7 +9,7 @@ import { resolveReceivingConnection } from "../../comms/receiving-runtime.js";
9
9
  import { resolveStudyManifest } from "../../study/discover.js";
10
10
  import { runCommsCatchHost } from "../../comms/catch-host.js";
11
11
  import { DEFAULT_SANDBOX_CATCH_PORT } from "../../comms/sandbox-catch.js";
12
- import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, PORT_OPTION_DESCRIPTION, writeResult, messageOutput, } from "../io.js";
12
+ import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, PORT_OPTION_DESCRIPTION, writeResult, messageOutput, } from "../io.js";
13
13
  export function registerCommsCommands(parent, io) {
14
14
  const comms = parent
15
15
  .command("comms")
@@ -27,7 +27,8 @@ export function registerCommsCommands(parent, io) {
27
27
  .command("list")
28
28
  .description("Show saved connections and local credential status; does not authenticate with a provider.")
29
29
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
30
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
30
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
31
+ .addOption(envFileAliasOption())
31
32
  .option("--json", JSON_OPTION_DESCRIPTION)
32
33
  .action((options, command) => handleCommsList(io, options, command));
33
34
  connections
@@ -46,7 +47,8 @@ export function registerCommsCommands(parent, io) {
46
47
  .option("--connection <name>", "Saved connection name.", "agentmail")
47
48
  .option("--study <path>", "Check the connection this study selects.")
48
49
  .option("--online", "Make a read-only provider authentication request.")
49
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
50
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
51
+ .addOption(envFileAliasOption())
50
52
  .option("--json", JSON_OPTION_DESCRIPTION)
51
53
  .action((options, command) => handleCommsCheck(io, options, command));
52
54
  comms
@@ -65,7 +67,8 @@ export function registerCommsCommands(parent, io) {
65
67
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
66
68
  .option("--run <id>", "One run to inspect or recover.")
67
69
  .option("--apply", "Recover the selected inactive run and verify mailbox deletion.")
68
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
70
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
71
+ .addOption(envFileAliasOption())
69
72
  .option("--json", JSON_OPTION_DESCRIPTION)
70
73
  .action((options, command) => handleCommsRecover(io, options, command));
71
74
  comms
@@ -89,7 +92,12 @@ function handleCommsProviders(io, _options, command) {
89
92
  writeResult(command, io, result, () => COMMS_PROVIDERS.map((provider) => `${provider.label}: ${provider.limitation}\nKey: ${provider.keyEnv}\nSetup: ${provider.setupUrl}\n`).join("\n"));
90
93
  }
91
94
  async function handleCommsList(io, options, command) {
92
- if (!(await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io })))
95
+ if (!(await applyEnvFileOption({
96
+ command,
97
+ cwd: options.cwd,
98
+ envFile: dotenvPathOf(options, command, io),
99
+ io,
100
+ })))
93
101
  return;
94
102
  const result = await readCommsSetup(resolve(options.cwd), process.env);
95
103
  writeResult(command, io, result, (value) => `${value.message}\nAgentMail key: ${value.credential.present ? "present" : "missing"}\n${value.connections.map((connection) => `${connection.name}: ${connection.provider} (${connection.apiKeyEnv})\n`).join("")}`);
@@ -106,7 +114,12 @@ async function handleCommsAdd(io, name, options, command) {
106
114
  io.setExitCode(result.ok ? 0 : 2);
107
115
  }
108
116
  async function handleCommsCheck(io, options, command) {
109
- if (!(await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io })))
117
+ if (!(await applyEnvFileOption({
118
+ command,
119
+ cwd: options.cwd,
120
+ envFile: dotenvPathOf(options, command, io),
121
+ io,
122
+ })))
110
123
  return;
111
124
  let connection = options.connection;
112
125
  const study = options.study;
@@ -150,7 +163,12 @@ async function handleCommsConfigure(io, options, command) {
150
163
  io.setExitCode(result.ok ? 0 : 2);
151
164
  }
152
165
  async function handleCommsRecover(io, options, command) {
153
- if (!(await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io })))
166
+ if (!(await applyEnvFileOption({
167
+ command,
168
+ cwd: options.cwd,
169
+ envFile: dotenvPathOf(options, command, io),
170
+ io,
171
+ })))
154
172
  return;
155
173
  const cwd = resolve(options.cwd);
156
174
  try {
@@ -352,8 +352,10 @@ function hiddenRunLines(groups) {
352
352
  const reasons = group.reasons.length > 0 ? ` (${group.reasons.join(", ")})` : "";
353
353
  lines.push(` ${runs(group.runs)} ${group.status}${reasons}`);
354
354
  }
355
- if (groups.some((group) => group.reasons.join(",") === "RAW_SCREENSHOTS"))
356
- lines.push("share: a run held back only for RAW_SCREENSHOTS can be copied with blurred screenshots: `humanish export --run <id> --format bundle --redact-screenshots --out <dir>`, then `humanish observe --all --safe --cwd <dir>`");
355
+ // Export clears both: it blurs screenshots and writes no raw sandbox id.
356
+ const exportable = new Set(["RAW_SCREENSHOTS", "RAW_SANDBOX_ID"]);
357
+ if (groups.some((group) => group.reasons.length > 0 && group.reasons.every((reason) => exportable.has(reason))))
358
+ lines.push("share: a run held back only for RAW_SCREENSHOTS or RAW_SANDBOX_ID can be copied with blurred screenshots and no sandbox ids: `humanish export --run <id> --format bundle --redact-screenshots --out <dir>`, then `humanish observe --all --safe --cwd <dir>`");
357
359
  lines.push("why: `humanish verify --run <id>` explains each reason");
358
360
  return lines;
359
361
  }
@@ -1,6 +1,6 @@
1
1
  import { runDryRun } from "../../run/dry-run.js";
2
2
  import { runStudyCommand } from "./study-run.js";
3
- import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, formatRunHuman, freePortOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, } from "../io.js";
3
+ import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, formatRunHuman, freePortOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, } from "../io.js";
4
4
  /** The flags a run takes, whichever command starts it. */
5
5
  export function addRunOptions(command) {
6
6
  return command
@@ -14,7 +14,8 @@ export function addRunOptions(command) {
14
14
  .option("--participants <ids>", "With --rerun-failed-from: comma-separated ids of the participants to rerun. The study's participants decide who takes part; ids are their id, or lane-01, lane-02, … by position.")
15
15
  .option("--scorer <path>", "Score with this .mjs module, a path inside the repo, in place of review.scorer.ref. It runs as code, so review it first. Terminal, computer-use and shared-world studies only.")
16
16
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
17
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
17
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
18
+ .addOption(envFileAliasOption())
18
19
  .option("--run-id <id>", "Explicit run id for deterministic fixture tests; refused when that run already exists.")
19
20
  .option("--json", JSON_OPTION_DESCRIPTION);
20
21
  }
@@ -52,7 +53,7 @@ export async function handleRun(io, named, options, command) {
52
53
  const loaded = await applyEnvFileOption({
53
54
  command,
54
55
  cwd: options.cwd,
55
- envFile: options.envFile,
56
+ envFile: dotenvPathOf(options, command, io),
56
57
  io,
57
58
  // runStudyCommand discovers keys for a live study; a preview without a study needs none.
58
59
  discoverKeys: false,
@@ -11,6 +11,8 @@ export interface RunSignalOptions {
11
11
  exit?: (code: number) => void;
12
12
  reclaim?: ReclaimHooks;
13
13
  deadlineMs?: number;
14
+ /** Resolves when the deadline has passed; defaults to a timer of `deadlineMs`. */
15
+ deadline?: (ms: number) => Promise<void>;
14
16
  }
15
17
  /**
16
18
  * Register work a committed shutdown finishes before it exits, such as closing watch's Observer