humanish 0.109.0 → 0.110.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 (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/analysis/automatic-config.d.ts +1 -1
  4. package/dist/cli/commands/comms.js +25 -7
  5. package/dist/cli/commands/migrate.js +15 -4
  6. package/dist/cli/commands/observe.js +4 -2
  7. package/dist/cli/commands/run-command.js +4 -3
  8. package/dist/cli/commands/runs.js +13 -3
  9. package/dist/cli/commands/setup.js +19 -5
  10. package/dist/cli/commands/study-format.js +29 -2
  11. package/dist/cli/commands/study.js +4 -3
  12. package/dist/cli/commands/tui.js +4 -3
  13. package/dist/cli/commands/watch.js +2 -2
  14. package/dist/cli/doctor.d.ts +3 -1
  15. package/dist/cli/doctor.js +5 -4
  16. package/dist/cli/io.d.ts +17 -3
  17. package/dist/cli/io.js +20 -3
  18. package/dist/comms/receiving-runtime.js +1 -1
  19. package/dist/comms/setup.js +1 -1
  20. package/dist/evidence/redaction.d.ts +15 -0
  21. package/dist/evidence/redaction.js +87 -0
  22. package/dist/feedback/export-bundle.js +11 -5
  23. package/dist/feedback/export.js +3 -0
  24. package/dist/keys/key-resolution.d.ts +7 -0
  25. package/dist/keys/key-resolution.js +10 -8
  26. package/dist/routes/computer-use/in-process-desktop.js +1 -1
  27. package/dist/routes/computer-use/live-phase.d.ts +1 -0
  28. package/dist/routes/computer-use/plan.js +3 -3
  29. package/dist/routes/computer-use/preflight.js +1 -1
  30. package/dist/routes/computer-use/setup.js +1 -1
  31. package/dist/routes/computer-use/types.d.ts +6 -0
  32. package/dist/routes/scripted/types.d.ts +1 -0
  33. package/dist/routes/shared-world/route.js +1 -1
  34. package/dist/routes/shared-world/types.d.ts +2 -0
  35. package/dist/routes/terminal/credentials.js +1 -1
  36. package/dist/routes/terminal/live-finish.js +8 -4
  37. package/dist/routes/terminal/types.d.ts +3 -0
  38. package/dist/run/bundle-shape.js +1 -0
  39. package/dist/run/bundle.d.ts +6 -0
  40. package/dist/run/reclaim.d.ts +3 -0
  41. package/dist/run/reclaim.js +10 -3
  42. package/dist/run/results.d.ts +3 -0
  43. package/dist/run/run.js +14 -8
  44. package/dist/run/sandbox-ids.d.ts +53 -0
  45. package/dist/run/sandbox-ids.js +198 -0
  46. package/dist/run/sandbox-receipts.d.ts +2 -0
  47. package/dist/run/sandbox-receipts.js +26 -0
  48. package/dist/run/status.js +5 -2
  49. package/dist/run/stored-runs.js +10 -1
  50. package/dist/run-study.js +6 -1
  51. package/dist/study/doctor.js +2 -2
  52. package/dist/study/init-templates.js +2 -2
  53. package/dist/study/migrate.d.ts +7 -2
  54. package/dist/study/migrate.js +60 -4
  55. package/dist/study/run-study-events.js +1 -1
  56. package/dist/study/run-study-options.js +3 -3
  57. package/dist/study/types.d.ts +1 -1
  58. package/dist/verify/artifacts.d.ts +4 -0
  59. package/dist/verify/artifacts.js +8 -0
  60. package/dist/verify/liveness.js +8 -4
  61. package/dist/verify/verify.d.ts +1 -1
  62. package/dist/verify/verify.js +21 -1
  63. package/docs/architecture/restricted-codex-analysis.md +1 -1
  64. package/docs/contracts/run-bundle.md +20 -1
  65. package/package.json +4 -2
  66. package/skills/humanish/SKILL.md +2 -2
package/CHANGELOG.md CHANGED
@@ -8,6 +8,26 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.110.0: --dotenv, and sandbox ids only in receipts (2026-10-03)
12
+
13
+ humanish 0.110.0 renames `--env-file` to `--dotenv`, so a missing file reaches humanish's exit 2
14
+ result where Node exited 9 first. `--env-file` stays as a hidden alias until 0.111.0. Raw sandbox
15
+ ids live only in a run's `sandbox-receipts.ndjson`: every other record, output and export copy names
16
+ a sandbox as `[redacted-sandbox-id]` with a digest, and `humanish verify` grades a run that still
17
+ holds a raw id `local_only` with `RAW_SANDBOX_ID`. The release notes list each changed field and
18
+ what to read instead.
19
+
20
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.0)
21
+
22
+ ## 0.109.1: Current option names in messages, reclaim --env-file (2026-10-03)
23
+
24
+ humanish 0.109.1 is a patch. Library refusals and warnings name `RunStudyOptions`, which 0.109.0
25
+ exports, where they named the removed `RunLabOptions`. `humanish export --format bundle` no longer
26
+ copies sandbox ids into its redacted workspace. `humanish reclaim` takes `--env-file`, and
27
+ `humanish migrate` removes a `labs/` directory that its moves emptied.
28
+
29
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.109.1)
30
+
11
31
  ## 0.109.0: Lab names removed, v2 study files refused (2026-10-03)
12
32
 
13
33
  humanish 0.109.0 removes what 0.108.0 deprecated. A `humanish.lab.v2` file, or any study file in a
package/README.md CHANGED
@@ -285,7 +285,7 @@ why. A hosted participant on a release humanish has not tested records a warning
285
285
  the first of these sources that has it. It prints the name and source of a key it
286
286
  fills from sources 2 to 4, never the value:
287
287
 
288
- 1. the process environment, including a file passed with `--env-file`;
288
+ 1. the process environment, including a file passed with `--dotenv`;
289
289
  2. `.humanish/local/provider.env`;
290
290
  3. the vendor's own store: `~/.e2b/config.json` for `E2B_API_KEY`, and
291
291
  `gh auth token` for `GH_TOKEN`;
@@ -7,7 +7,7 @@ interface StudyAnalysisSettings {
7
7
  question?: string;
8
8
  timeoutMs?: number;
9
9
  }
10
- /** The study's review.analysis: a provider selection for a separate review after a live study. */
10
+ /** The study's review.analysis: a provider selection for a separate review after a live run. */
11
11
  export type StudyAnalysis = StudyAnalysisSettings & ({
12
12
  provider?: "openai";
13
13
  maxCostUsd: number;
@@ -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 {
@@ -4,8 +4,11 @@ function formatValue(value) {
4
4
  const text = JSON.stringify(value);
5
5
  return text.length > 80 ? `${text.slice(0, 77)}...` : text;
6
6
  }
7
- /** Each file's source, destination, moved keys and dropped keys, printed before any write. */
8
- function formatMigratePlan(files) {
7
+ /**
8
+ * Each file's source, destination, moved keys and dropped keys, and each labs/ directory the moves
9
+ * empty, printed before any write.
10
+ */
11
+ function formatMigratePlan(files, emptied) {
9
12
  const lines = [];
10
13
  for (const file of files) {
11
14
  if (file.action === "skip") {
@@ -22,6 +25,9 @@ function formatMigratePlan(files) {
22
25
  lines.push(` its comment: ${key.comments.replace(/\n/g, " ")}`);
23
26
  }
24
27
  }
28
+ for (const directory of emptied) {
29
+ lines.push(`${directory}/: removed after the move, since nothing else is in it`);
30
+ }
25
31
  return lines.length > 0 ? `${lines.join("\n")}\n` : "No study files to convert.\n";
26
32
  }
27
33
  function formatMigrateOutcome(result) {
@@ -33,7 +39,12 @@ function formatMigrateOutcome(result) {
33
39
  const files = written === 1 ? "1 study file" : `${written} study files`;
34
40
  if (result.dryRun)
35
41
  return `Dry run: nothing was written. ${files} would change.\n`;
36
- return written === 0 ? "No v2 study files to convert.\n" : `Converted ${files}.\n`;
42
+ if (written === 0)
43
+ return "No v2 study files to convert.\n";
44
+ const removed = result.removedDirectories ?? [];
45
+ return removed.length === 0
46
+ ? `Converted ${files}.\n`
47
+ : `Converted ${files}. Removed ${removed.map((directory) => `${directory}/`).join(", ")}, which the move emptied.\n`;
37
48
  }
38
49
  /** `humanish migrate`. */
39
50
  export function registerMigrateCommand(parent, io) {
@@ -57,7 +68,7 @@ export function registerMigrateCommand(parent, io) {
57
68
  dryRun: options.dryRun === true,
58
69
  // The plan is printed before anything is written; with --json it goes to stderr, so stdout
59
70
  // stays one JSON document.
60
- onPlan: (files) => (json ? io.writeErr : io.writeOut)(formatMigratePlan(files)),
71
+ onPlan: (files, emptied) => (json ? io.writeErr : io.writeOut)(formatMigratePlan(files, emptied)),
61
72
  });
62
73
  writeResult(command, io, result, formatMigrateOutcome);
63
74
  io.setExitCode(result.ok ? 0 : 2);
@@ -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,
@@ -7,7 +7,7 @@ import { cleanupRun, listRuns, readReview } from "../../run/stored-runs.js";
7
7
  import { verifyRun } from "../../verify/verify.js";
8
8
  import { reclaimPreflightSandboxes, reclaimRunSandboxes, } from "../../run/reclaim.js";
9
9
  import { addRunOptions, handleRun } from "./run-command.js";
10
- import { CWD_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
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
12
  export function registerRunCommand(parent, io) {
13
13
  addRunOptions(parent
@@ -165,13 +165,23 @@ export function registerRuntimeCommands(parent, io) {
165
165
  export function registerReclaimCommand(parent, io) {
166
166
  parent
167
167
  .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.")
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
169
  .summary("Stop an interrupted run's leftover sandboxes.")
170
170
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
171
171
  .option("--run <id>", RUN_OPTION_DESCRIPTION, "latest")
172
172
  .addOption(new Option("--preflight", "Reclaim sandboxes left by interrupted `humanish study check` probes (journaled in .humanish/preflight) instead of a run's.").conflicts("run"))
173
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
174
+ .addOption(envFileAliasOption())
173
175
  .option("--json", JSON_OPTION_DESCRIPTION)
174
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;
175
185
  const result = options.preflight
176
186
  ? await reclaimPreflightSandboxes(options.cwd)
177
187
  : await reclaimRunSandboxes(options.cwd, options.run);
@@ -183,7 +193,7 @@ function formatReclaimHuman(result) {
183
193
  const lines = [];
184
194
  lines.push(`Reclaim ${result.runId}: ${result.ok ? "ok" : "failed"}, ${plural(result.receiptCount, "sandbox receipt")}.`);
185
195
  for (const outcome of result.outcomes) {
186
- lines.push(` ${outcome.sandboxId} (${outcome.laneId}): ${outcome.state}${outcome.detail ? ` — ${outcome.detail}` : ""}`);
196
+ lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}): ${outcome.state}${outcome.detail ? ` — ${outcome.detail}` : ""}`);
187
197
  }
188
198
  for (const warning of result.warnings)
189
199
  lines.push(` warning: ${warning}`);
@@ -4,7 +4,7 @@ import { promptSecret } from "../secret-prompt.js";
4
4
  import { runInit } from "../../study/init.js";
5
5
  import { buildPayload, disabledByEnvironment, readTelemetryState, telemetryStatePath, writeTelemetryState, } from "../telemetry.js";
6
6
  import { doctor } from "../doctor.js";
7
- import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, writeResult, formatCliError, } from "../io.js";
7
+ import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, writeResult, formatCliError, } from "../io.js";
8
8
  import { plural } from "../../run/text.js";
9
9
  export function registerInitCommand(parent, io) {
10
10
  parent
@@ -51,13 +51,27 @@ export function registerDoctorCommand(parent, io) {
51
51
  .summary("Check what this project and machine need for a run.")
52
52
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
53
53
  .option("--study <study>", "Check the study's desktop, participant authentication and separate analysis requirements; no provider calls.")
54
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
54
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
55
+ .addOption(envFileAliasOption())
55
56
  .option("--json", JSON_OPTION_DESCRIPTION)
56
57
  .action(async (options, command) => {
57
- if (options.envFile &&
58
- !(await applyEnvFileOption({ command, cwd: options.cwd, envFile: options.envFile, io })))
58
+ const dotenv = dotenvPathOf(options, command, io);
59
+ let loaded;
60
+ if (dotenv &&
61
+ !(await applyEnvFileOption({
62
+ command,
63
+ cwd: options.cwd,
64
+ envFile: dotenv,
65
+ io,
66
+ onLoaded: (load) => {
67
+ loaded = load;
68
+ },
69
+ })))
59
70
  return;
60
- const result = await doctor(options.cwd, options.study ? { study: options.study } : {});
71
+ const result = await doctor(options.cwd, {
72
+ ...(options.study ? { study: options.study } : {}),
73
+ ...(loaded === undefined ? {} : { dotenv: loaded }),
74
+ });
61
75
  writeResult(command, io, result, formatDoctorHuman);
62
76
  // Behavioral change: was exit 1, every other structured command uses 2.
63
77
  io.setExitCode(result.ok ? 0 : 2);
@@ -15,6 +15,21 @@ function withError(error, lines) {
15
15
  const stdout = `${lines.join("\n")}\n`;
16
16
  return error === undefined ? stdout : { stdout, error };
17
17
  }
18
+ /** How a summary names a sandbox: by digest. The raw id is only in the run's receipts. */
19
+ function sandboxName(sandbox) {
20
+ return sandbox.sandboxIdDigest !== undefined
21
+ ? `[redacted-sandbox-id ${sandbox.sandboxIdDigest}]`
22
+ : sandbox.sandboxId;
23
+ }
24
+ /**
25
+ * One line per run naming the file that holds its raw sandbox ids, for the operator, when the
26
+ * result names a sandbox at all (each one carries a digest).
27
+ */
28
+ function sandboxIdsLine(result) {
29
+ return result.runId && JSON.stringify(result).includes('"sandboxIdDigest"')
30
+ ? [`sandbox ids: .humanish/runs/${result.runId}/sandbox-receipts.ndjson`]
31
+ : [];
32
+ }
18
33
  /** A participant's status in words: a dry run's placeholder status says that nothing ran live. */
19
34
  function participantStatus(status) {
20
35
  return status === "contract_proof_only" ? "dry run, nothing ran live" : status;
@@ -37,9 +52,10 @@ export function formatConcurrentSharedWorldStudyHuman(result) {
37
52
  ...result.roles.map((participant) => `persona ${participant.id} (${participant.persona}): ${participantStatus(participant.status)}${participant.session ? ` (${participant.session.completionReason})` : ""}${participant.ok ? "" : ", not ok"}`),
38
53
  ...(result.subjectSandbox
39
54
  ? [
40
- `subject sandbox: ${result.subjectSandbox.sandboxId} killed=${result.subjectSandbox.killed ? "yes" : "no"}`,
55
+ `subject sandbox: ${sandboxName(result.subjectSandbox)} killed=${result.subjectSandbox.killed ? "yes" : "no"}`,
41
56
  ]
42
57
  : []),
58
+ ...sandboxIdsLine(result),
43
59
  ...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
44
60
  ...(result.observer?.opened === undefined
45
61
  ? []
@@ -53,6 +69,10 @@ export function formatTerminalStudyHuman(result) {
53
69
  `run: ${result.runId}`,
54
70
  `actor: ${result.actor}`,
55
71
  `product: ${result.product}`,
72
+ ...(result.sandbox
73
+ ? [`sandbox: ${sandboxName(result.sandbox)} killed=${result.sandbox.killed ? "yes" : "no"}`]
74
+ : []),
75
+ ...sandboxIdsLine(result),
56
76
  ...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
57
77
  ...(result.observer?.opened === undefined
58
78
  ? []
@@ -72,6 +92,12 @@ export function formatScriptedStudyHuman(result) {
72
92
  ]
73
93
  : []),
74
94
  ...result.sessions.map((session) => `session ${session.surface}: ${session.status} (${session.completionReason}) · ${session.reason} [${session.screenshots} screenshot${session.screenshots === 1 ? "" : "s"}]`),
95
+ ...(result.subjectSandbox
96
+ ? [
97
+ `subject sandbox: ${sandboxName(result.subjectSandbox)} killed=${result.subjectSandbox.killed ? "yes" : "no"}`,
98
+ ]
99
+ : []),
100
+ ...sandboxIdsLine(result),
75
101
  ...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
76
102
  ...(result.observer?.opened === undefined
77
103
  ? []
@@ -105,9 +131,10 @@ export function formatCuaStudyHuman(result) {
105
131
  : []),
106
132
  ...(result.sandbox
107
133
  ? [
108
- `sandbox: ${result.sandbox.sandboxId} stream=${result.sandbox.streamUrlPresent ? "connected" : "missing"} killed=${result.sandbox.killed ? "yes" : "no"}`,
134
+ `sandbox: ${sandboxName(result.sandbox)} stream=${result.sandbox.streamUrlPresent ? "connected" : "missing"} killed=${result.sandbox.killed ? "yes" : "no"}`,
109
135
  ]
110
136
  : []),
137
+ ...sandboxIdsLine(result),
111
138
  ...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
112
139
  ...(result.observer?.opened === undefined
113
140
  ? []
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { Command, Option } from "commander";
4
4
  import { inspectStudyManifest, listStudyManifests } from "../../study/discover.js";
5
5
  import { runStudyPreflight, } from "../../study/preflight.js";
6
- import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, humanError, } from "../io.js";
6
+ import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, humanError, } from "../io.js";
7
7
  export function registerStudyCommands(parent, io) {
8
8
  const study = parent
9
9
  .command("study")
@@ -36,7 +36,8 @@ function checkCommand(command, io) {
36
36
  .choices(["metadata", "public-preview", "sandbox-loopback", "prepared-host"])
37
37
  .default("metadata"))
38
38
  .option("--timeout-ms <ms>", "Target reachability timeout.", String(30_000))
39
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
39
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
40
+ .addOption(envFileAliasOption())
40
41
  .option("--json", JSON_OPTION_DESCRIPTION)
41
42
  .action((name, options, command) => handleStudyCheck(io, name, options, command));
42
43
  }
@@ -54,7 +55,7 @@ async function handleStudyCheck(io, name, options, command) {
54
55
  if (!(await applyEnvFileOption({
55
56
  command,
56
57
  cwd: options.cwd,
57
- envFile: options.envFile,
58
+ envFile: dotenvPathOf(options, command, io),
58
59
  io,
59
60
  }))) {
60
61
  return;
@@ -19,7 +19,7 @@ import { readRunDetail } from "../../run/detail.js";
19
19
  import { launchRun, readLaunchLogTail } from "../../tui/launch.js";
20
20
  import { TUI_MIN_NODE_MAJOR, nodeSupportsTui, TUI_BUNDLE_URL, } from "../../tui/contract.js";
21
21
  import { detectAgentSession } from "../../actors/agent-session.js";
22
- import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, ENV_FILE_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, } from "../io.js";
22
+ import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, } from "../io.js";
23
23
  export const defaultTuiRuntime = {
24
24
  checkComms: checkCommsConnection,
25
25
  promptSecret,
@@ -59,7 +59,8 @@ export function registerTuiCommand(parent, io, runtime = defaultTuiRuntime) {
59
59
  .description("Browse studies and runs in an interactive terminal UI. It refuses detected agent sessions and non-TTY input or output; agents use humanish study list --json, humanish study show <study> --json and humanish runs --json.")
60
60
  .summary("Browse studies and runs in a terminal UI for people.")
61
61
  .option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
62
- .option("--env-file <path>", ENV_FILE_OPTION_DESCRIPTION)
62
+ .option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
63
+ .addOption(envFileAliasOption())
63
64
  .option("--force", "Open it anyway in a session that looks like an agent's.")
64
65
  .option("--json", JSON_OPTION_DESCRIPTION)
65
66
  .action((options, command) => handleTui(io, runtime, options, command));
@@ -89,7 +90,7 @@ async function handleTui(io, runtime, options, command) {
89
90
  if (!(await applyEnvFileOption({
90
91
  command,
91
92
  cwd: options.cwd,
92
- envFile: options.envFile,
93
+ envFile: dotenvPathOf(options, command, io),
93
94
  io,
94
95
  env: runtime.env,
95
96
  onDiscovered: (names) => names.forEach((name) => discoveredKeys.add(name)),
@@ -4,7 +4,7 @@ import { WATCH_SAFE_NOT_APPLICABLE_MESSAGE } from "../../observer/exposure.js";
4
4
  import { runDryRun } from "../../run/dry-run.js";
5
5
  import { runStudyCommand } from "./study-run.js";
6
6
  import { addRunOptions, studyOnlyFlags } from "./run-command.js";
7
- import { applyEnvFileOption, collectRepeated, formatRunHuman, parseObserverPort, parsePositiveInteger, wantsJson, writeResult, } from "../io.js";
7
+ import { applyEnvFileOption, collectRepeated, dotenvPathOf, formatRunHuman, parseObserverPort, parsePositiveInteger, wantsJson, writeResult, } from "../io.js";
8
8
  import { followObserver, formatObserverHuman, watchExposeRequested } from "../observer-follow.js";
9
9
  export function registerWatchCommand(parent, io) {
10
10
  // A run's flags come from addRunOptions, the helper `run` uses, so the two cannot drift.
@@ -59,7 +59,7 @@ async function handleWatch(io, named, parsed, command) {
59
59
  if (!(await applyEnvFileOption({
60
60
  command,
61
61
  cwd: options.cwd,
62
- envFile: options.envFile,
62
+ envFile: dotenvPathOf(options, command, io),
63
63
  io,
64
64
  // runStudyCommand discovers keys for a live study; a preview run needs none.
65
65
  discoverKeys: false,
@@ -1,4 +1,4 @@
1
- import { type KeyResolutionDeps } from "../keys/key-resolution.js";
1
+ import { type KeyResolutionDeps, type DotenvLoad } from "../keys/key-resolution.js";
2
2
  import { type DetectLocalAgentsOptions } from "../actors/local-agent/cli.js";
3
3
  import { type StudySetupCheckArgs } from "../study/doctor.js";
4
4
  declare const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
@@ -48,5 +48,7 @@ export declare function doctor(cwdInput: string, options?: {
48
48
  keyDeps?: KeyResolutionDeps;
49
49
  /** The hosted Codex participant's operator handshake; tests replace it. */
50
50
  codexParticipantReadiness?: StudySetupCheckArgs["codexParticipantReadiness"];
51
+ /** What `--dotenv` loaded, so a key row names the file that supplied it. */
52
+ dotenv?: DotenvLoad;
51
53
  }): Promise<DoctorResult>;
52
54
  export {};
@@ -65,7 +65,7 @@ export async function doctor(cwdInput, options = {}) {
65
65
  }
66
66
  const env = options.env ?? process.env;
67
67
  const agents = await detectLocalAgents({ ...options.localAgents, env });
68
- const { probes, receivingKey } = await probeDoctorKeys(cwd, env, options.study, options.keyDeps);
68
+ const { probes, receivingKey } = await probeDoctorKeys(cwd, env, options.study, options.keyDeps, options.dotenv);
69
69
  const keyPresent = (name) => probes.some((probe) => probe.name === name && probe.source !== null);
70
70
  const setup = options.study
71
71
  ? await studySetupChecks({
@@ -237,7 +237,7 @@ function terminalSurfaceCheck() {
237
237
  };
238
238
  }
239
239
  /** Probes the live-run keys, plus the receiving-email key a real-comms study names. */
240
- async function probeDoctorKeys(cwd, env, study, keyDeps) {
240
+ async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
241
241
  const keyNames = new Set(["OPENAI_API_KEY", "E2B_API_KEY", "GH_TOKEN", "CODEX_API_KEY"]);
242
242
  let receivingKey = null;
243
243
  if (study) {
@@ -254,6 +254,7 @@ async function probeDoctorKeys(cwd, env, study, keyDeps) {
254
254
  cwd,
255
255
  env,
256
256
  ...(keyDeps === undefined ? {} : { deps: keyDeps }),
257
+ ...(dotenv === undefined ? {} : { dotenv }),
257
258
  });
258
259
  return { probes, receivingKey };
259
260
  }
@@ -281,7 +282,7 @@ function studyList(studies) {
281
282
  }
282
283
  /**
283
284
  * Provider-key discovery: which source supplies each live-run key, through the same
284
- * chain a live command resolves (env/--env-file, project overlay, vendor stores, the
285
+ * chain a live command resolves (env/--dotenv, project overlay, vendor stores, the
285
286
  * humanish user store). Values never appear; sources and fill commands do.
286
287
  *
287
288
  * With --study, a key the selected route requires fails when missing. Without it, `keyUsers` maps
@@ -292,7 +293,7 @@ function keyChecks(probes, receivingKey, setup, keyUsers) {
292
293
  return probes.map((probe) => {
293
294
  const present = probe.source !== null;
294
295
  const hint = probe.name === receivingKey
295
- ? `provide ${probe.name} through process env or --env-file`
296
+ ? `provide ${probe.name} through process env or --dotenv`
296
297
  : probe.hint;
297
298
  if (!setup) {
298
299
  const users = keyUsers?.get(probe.name) ?? [];
package/dist/cli/io.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Command, Option } from "commander";
2
- import { discoverProviderKeys } from "../keys/key-resolution.js";
2
+ import { discoverProviderKeys, type DotenvLoad } from "../keys/key-resolution.js";
3
3
  import { type TelemetryProperties } from "./telemetry.js";
4
4
  import type { RunResult } from "../run/results.js";
5
5
  export declare const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
@@ -13,7 +13,18 @@ export interface CliIo {
13
13
  export declare const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
14
14
  export declare const CWD_OPTION_DESCRIPTION = "Project directory.";
15
15
  export declare const RUN_OPTION_DESCRIPTION = "Run id, or latest.";
16
- export declare const ENV_FILE_OPTION_DESCRIPTION = "Load unset variables from this env file. Values are never printed or saved.";
16
+ export declare const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this dotenv file. Values are never printed or saved.";
17
+ /**
18
+ * The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
19
+ * `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
20
+ * exits 9 through Node and never reaches humanish. `--dotenv` does not collide. Removed in 0.111.0.
21
+ */
22
+ export declare function envFileAliasOption(): Option;
23
+ /** The dotenv path a command was given: `--dotenv`, or the `--env-file` alias with its warning. */
24
+ export declare function dotenvPathOf(options: {
25
+ dotenv?: string | undefined;
26
+ envFile?: string | undefined;
27
+ }, command: Command, io: CliIo): string | undefined;
17
28
  export declare const PORT_OPTION_DESCRIPTION = "Port to listen on at 127.0.0.1.";
18
29
  /** `--port` for a loopback server whose default, 0, lets the OS pick a free port. */
19
30
  export declare function freePortOption(): Option;
@@ -22,6 +33,7 @@ export interface StudyCommandOptions {
22
33
  cwd: string;
23
34
  detach?: boolean | undefined;
24
35
  dryRun?: boolean | undefined;
36
+ dotenv?: string | undefined;
25
37
  envFile?: string | undefined;
26
38
  json?: boolean | undefined;
27
39
  open?: boolean | undefined;
@@ -50,13 +62,15 @@ export declare function applyEnvFileOption(args: {
50
62
  io: CliIo;
51
63
  env?: NodeJS.ProcessEnv;
52
64
  onDiscovered?: (names: string[]) => void;
65
+ /** Called with the dotenv path and the names it set, which were unset before. */
66
+ onLoaded?: (load: DotenvLoad) => void;
53
67
  /** False for the study-running commands, which discover only once the study resolves to live. */
54
68
  discoverKeys?: boolean;
55
69
  }): Promise<boolean>;
56
70
  /**
57
71
  * Provider-key discovery: fill still-missing keys from the documented project overlay, the
58
72
  * owning vendors' native stores, and the humanish user store. It is fill-only (an explicit
59
- * --env-file or process env always wins), and each fill is announced by name and source on
73
+ * --dotenv or process env always wins), and each fill is announced by name and source on
60
74
  * stderr, never by value. HUMANISH_STRICT_KEYS=1 restores env-only behavior.
61
75
  */
62
76
  export declare function discoverCliKeys(args: {
package/dist/cli/io.js CHANGED
@@ -7,7 +7,7 @@ import { Command, Option } from "commander";
7
7
  import { loadEnvFile } from "../keys/env-file.js";
8
8
  import { discoverProviderKeys } from "../keys/key-resolution.js";
9
9
  import { deriveRunFacts } from "./telemetry.js";
10
- import { withQueuedWarnings } from "./deprecations.js";
10
+ import { warnAndQueue, withQueuedWarnings } from "./deprecations.js";
11
11
  import { forTerminal } from "../routes/terminal/encoding.js";
12
12
  export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
13
13
  function readCliVersion() {
@@ -23,7 +23,23 @@ export const CLI_VERSION = readCliVersion();
23
23
  export const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
24
24
  export const CWD_OPTION_DESCRIPTION = "Project directory.";
25
25
  export const RUN_OPTION_DESCRIPTION = "Run id, or latest.";
26
- export const ENV_FILE_OPTION_DESCRIPTION = "Load unset variables from this env file. Values are never printed or saved.";
26
+ export const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this dotenv file. Values are never printed or saved.";
27
+ const ENV_FILE_RENAMED = "`--env-file` is now `--dotenv`; `--env-file` is removed in 0.111.0.";
28
+ /**
29
+ * The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
30
+ * `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
31
+ * exits 9 through Node and never reaches humanish. `--dotenv` does not collide. Removed in 0.111.0.
32
+ */
33
+ export function envFileAliasOption() {
34
+ return new Option("--env-file <path>").hideHelp().conflicts("dotenv");
35
+ }
36
+ /** The dotenv path a command was given: `--dotenv`, or the `--env-file` alias with its warning. */
37
+ export function dotenvPathOf(options, command, io) {
38
+ if (options.envFile === undefined)
39
+ return options.dotenv;
40
+ warnAndQueue(command, io, ENV_FILE_RENAMED);
41
+ return options.envFile;
42
+ }
27
43
  export const PORT_OPTION_DESCRIPTION = "Port to listen on at 127.0.0.1.";
28
44
  /** `--port` for a loopback server whose default, 0, lets the OS pick a free port. */
29
45
  export function freePortOption() {
@@ -74,6 +90,7 @@ export async function applyEnvFileOption(args) {
74
90
  }
75
91
  for (const name of result.loaded)
76
92
  env[name] = stagedEnv[name];
93
+ args.onLoaded?.({ path: args.envFile, names: result.loaded });
77
94
  }
78
95
  if (args.discoverKeys !== false) {
79
96
  await discoverCliKeys({
@@ -88,7 +105,7 @@ export async function applyEnvFileOption(args) {
88
105
  /**
89
106
  * Provider-key discovery: fill still-missing keys from the documented project overlay, the
90
107
  * owning vendors' native stores, and the humanish user store. It is fill-only (an explicit
91
- * --env-file or process env always wins), and each fill is announced by name and source on
108
+ * --dotenv or process env always wins), and each fill is announced by name and source on
92
109
  * stderr, never by value. HUMANISH_STRICT_KEYS=1 restores env-only behavior.
93
110
  */
94
111
  export async function discoverCliKeys(args) {
@@ -29,7 +29,7 @@ export async function resolveReceivingConnection(cwd, name, env, keyDeps) {
29
29
  });
30
30
  const apiKey = resolved[connection.apiKeyEnv]?.trim();
31
31
  if (!apiKey)
32
- throw new Error("Email connection credential is missing. Add it in Connections, or provide the configured key with --env-file.");
32
+ throw new Error("Email connection credential is missing. Add it in Connections, or provide the configured key with --dotenv.");
33
33
  return { connection, apiKey, adapter: createReceivingAdapter(connection, apiKey) };
34
34
  }
35
35
  export async function prepareReceivingRun(args) {
@@ -53,7 +53,7 @@ export async function checkCommsConnection(args) {
53
53
  ...base,
54
54
  ready: false,
55
55
  code: "credential_missing",
56
- message: "The configured credential is missing. Add it in Connections or provide it with --env-file.",
56
+ message: "The configured credential is missing. Add it in Connections or provide it with --dotenv.",
57
57
  };
58
58
  base.credentialPresent = true;
59
59
  if (!args.online)