humanish 0.142.0 → 0.144.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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -1
  3. package/dist/analysis/automatic-config.d.ts +12 -1
  4. package/dist/analysis/automatic-config.js +35 -6
  5. package/dist/analysis/execute.d.ts +1 -11
  6. package/dist/analysis/execute.js +4 -21
  7. package/dist/analysis/gemini-wire.d.ts +2 -1
  8. package/dist/analysis/gemini-wire.js +3 -2
  9. package/dist/analysis/models.d.ts +12 -0
  10. package/dist/analysis/models.js +47 -0
  11. package/dist/analysis/openai-wire.d.ts +2 -1
  12. package/dist/analysis/openai-wire.js +3 -2
  13. package/dist/analysis/provider.d.ts +4 -0
  14. package/dist/analysis/types.d.ts +4 -0
  15. package/dist/analysis/validation.js +4 -3
  16. package/dist/cli/commands/analyze.js +5 -3
  17. package/dist/cli/commands/run-command.js +2 -1
  18. package/dist/cli/commands/study-remote.d.ts +34 -2
  19. package/dist/cli/commands/study-remote.js +103 -52
  20. package/dist/cli/commands/study-route-run.d.ts +3 -2
  21. package/dist/cli/commands/study-route-run.js +7 -2
  22. package/dist/cli/commands/study-run.js +19 -9
  23. package/dist/cli/findings.d.ts +2 -1
  24. package/dist/cli/findings.js +31 -9
  25. package/dist/cli/io.d.ts +2 -0
  26. package/dist/remote/launch.d.ts +17 -2
  27. package/dist/remote/launch.js +108 -29
  28. package/dist/remote/plan.d.ts +2 -0
  29. package/dist/remote/plan.js +12 -0
  30. package/dist/remote/runner.d.ts +21 -0
  31. package/dist/remote/runner.js +42 -5
  32. package/dist/run/results.d.ts +9 -1
  33. package/dist/study/keys.d.ts +1 -0
  34. package/dist/study/keys.js +1 -0
  35. package/dist/study/parse/execution.js +5 -0
  36. package/dist/study/types.d.ts +6 -0
  37. package/dist/tui/launch.js +3 -0
  38. package/docs/contracts/study-analysis.md +35 -2
  39. package/docs/product/automatic-analysis.md +17 -7
  40. package/package.json +1 -1
  41. package/skills/humanish/SKILL.md +16 -1
@@ -1,81 +1,125 @@
1
1
  // `--remote` on run and watch: plan here, refuse what cannot run remotely, then launch and follow
2
2
  // the run in a runner sandbox (src/remote/launch.ts). The result and exit code are the run's own.
3
3
  import path from "node:path";
4
- import { launchRemoteRun } from "../../remote/launch.js";
4
+ import { forwardedEnv, launchRemoteRun, runsFromPackage, } from "../../remote/launch.js";
5
5
  import { planRemoteRun } from "../../remote/plan.js";
6
- import { planStudy } from "../../study/plan.js";
6
+ import { INSIDE_RUNNER_ENV } from "../../remote/runner.js";
7
+ import { planStudy, resolveStudyDryRun } from "../../study/plan.js";
7
8
  import { concurrentSandboxes, sandboxCeiling } from "../../substrates/e2b/lifetime.js";
8
9
  import { MODAL_MAX_SANDBOX_MS } from "../../substrates/modal/sdk.js";
9
10
  import { formatRunHuman, parseObserverPort, wantsJson, writeResult, } from "../io.js";
10
- export async function runRemoteStudy(args) {
11
- const { io, options, config } = args;
11
+ /**
12
+ * Whether this run executes here or in a runner sandbox. A hosted study runs remotely by default
13
+ * when it can; `--local` or `execution.remote: false` keeps it here, and `--remote` or
14
+ * `execution.remote: true` requires a remote run, refused with the reason when the study cannot
15
+ * have one. `--local` and `--remote` win over the study's field.
16
+ */
17
+ export function placeRun(args) {
18
+ const { options, config } = args;
19
+ if ((args.env ?? process.env)[INSIDE_RUNNER_ENV] === "1")
20
+ return options.remote === true
21
+ ? {
22
+ kind: "refused",
23
+ message: "This run is already inside a runner sandbox, so it runs here.",
24
+ }
25
+ : { kind: "local" };
26
+ const declared = config.execution?.remote;
27
+ if (options.local === true || (options.remote !== true && declared === false))
28
+ return { kind: "local" };
29
+ const required = options.remote === true || declared === true;
30
+ // A dry run creates no sandbox, so it never needs a runner, whatever asked for one.
31
+ if (resolveStudyDryRun(config, options.dryRun, true) !== false)
32
+ return required
33
+ ? { kind: "local", note: "A dry run creates no sandbox, so it runs here, not remotely." }
34
+ : { kind: "local" };
35
+ const local = (reason) => required
36
+ ? { kind: "refused", message: `This study cannot run remotely: ${reason}` }
37
+ : movable(config)
38
+ ? {
39
+ kind: "local",
40
+ note: `Not running remotely: ${reason} A run on this machine stops if it sleeps.`,
41
+ }
42
+ : { kind: "local" };
43
+ const flags = localOnlyFlags(options);
44
+ if (flags.length > 0)
45
+ return local(`${flags.join(", ")} ${flags.length === 1 ? "does" : "do"} not run remotely yet.`);
12
46
  const cwd = path.resolve(options.cwd);
13
- const refuse = (code, message, runId) => {
14
- const result = {
15
- schema: "humanish.run-result.v1",
16
- ok: false,
17
- cwd,
18
- ...(runId === undefined ? {} : { runId }),
19
- warnings: [],
20
- error: { code, message },
21
- };
22
- writeResult(args.command, io, result, formatRunHuman);
23
- io.setExitCode(2);
24
- };
25
- if (options.dryRun === true) {
26
- refuse("HUMANISH_REMOTE_REFUSED", "--remote runs live studies; a dry run needs no runner.");
27
- return;
28
- }
29
- const local = localOnlyFlags(options);
30
- if (local.length > 0) {
31
- refuse("HUMANISH_REMOTE_REFUSED", `${local.join(", ")} ${local.length === 1 ? "does" : "do"} not run remotely yet; run without --remote to use ${local.length === 1 ? "it" : "them"}.`);
32
- return;
33
- }
34
47
  const planned = planStudy(config, { cwd, dryRun: false });
35
- if (!planned.ok) {
36
- refuse("HUMANISH_REMOTE_REFUSED", planned.refusal.message);
37
- return;
38
- }
48
+ // A study that does not plan is refused by its route with the plan's own message.
49
+ if (!planned.ok)
50
+ return required ? { kind: "refused", message: planned.refusal.message } : { kind: "local" };
39
51
  const plan = planned.planned.plan;
40
- const e2bCeiling = sandboxCeiling(process.env);
41
52
  const provider = plan.route === "computer-use" && plan.runner.desktop === "e2b-desktop"
42
53
  ? plan.runner.provider
43
54
  : "e2b";
44
- const ceilingMs = provider === "modal" ? MODAL_MAX_SANDBOX_MS : e2bCeiling.ok ? e2bCeiling.ms : 0;
55
+ const e2bCeiling = sandboxCeiling(process.env);
45
56
  const sandboxes = concurrentSandboxes(process.env);
46
57
  const remote = planRemoteRun({
47
58
  config,
48
59
  plan,
49
60
  cwd,
50
61
  studyPath: args.studyPath,
51
- ceilingMs,
62
+ ceilingMs: provider === "modal" ? MODAL_MAX_SANDBOX_MS : e2bCeiling.ok ? e2bCeiling.ms : 0,
52
63
  concurrentSandboxes: sandboxes.ok ? sandboxes.count : 1,
53
64
  });
54
- if (!remote.ok) {
55
- refuse("HUMANISH_REMOTE_REFUSED", `This study cannot run remotely: ${remote.reason}`);
56
- return;
57
- }
65
+ if (!remote.ok)
66
+ return local(remote.reason);
67
+ // Checked last, so a study that could not run remotely anyway gives its own reason.
68
+ if (!(args.packaged ?? runsFromPackage()))
69
+ return local("this humanish runs from source, which a runner cannot install. Build it with pnpm build and run node dist/cli.js to run remotely.");
70
+ return { kind: "remote", plan: remote.plan };
71
+ }
72
+ /**
73
+ * Whether the study is of a kind that runs remotely, so a reason it does not this time is worth
74
+ * saying: a computer-use study on hosted desktops. Other routes never run remotely yet.
75
+ */
76
+ function movable(config) {
77
+ const target = config.execution?.target;
78
+ return (config.route === "computer-use" && (target === "e2b-desktop" || target === "modal-desktop"));
79
+ }
80
+ /** A refused placement as the command's result. */
81
+ export function refuseRemote(command, io, cwd, message, code = "HUMANISH_REMOTE_REFUSED", forwarded) {
82
+ const result = {
83
+ schema: "humanish.run-result.v1",
84
+ ok: false,
85
+ cwd: path.resolve(cwd),
86
+ warnings: [],
87
+ ...(forwarded === undefined ? {} : { remote: forwarded }),
88
+ error: { code, message },
89
+ };
90
+ writeResult(command, io, result, formatRunHuman);
91
+ io.setExitCode(2);
92
+ }
93
+ /** Launch `remote` in a runner sandbox and report how it ended. */
94
+ export async function runRemoteStudy(args) {
95
+ const { io, options } = args;
96
+ const cwd = path.resolve(options.cwd);
58
97
  const port = parseObserverPort(options.port ?? "0");
59
98
  if (port === null) {
60
- refuse("HUMANISH_REMOTE_REFUSED", "--port must be an integer between 0 and 65535.");
99
+ refuseRemote(args.command, io, cwd, "--port must be an integer between 0 and 65535.");
61
100
  return;
62
101
  }
102
+ const forwarded = {
103
+ provider: args.remote.provider,
104
+ keys: Object.keys(forwardedEnv(args.remote, process.env)).sort(),
105
+ };
106
+ io.writeErr(`The runner receives ${forwarded.keys.join(", ") || "no keys"} from this machine, in your own ${forwarded.provider === "modal" ? "Modal" : "E2B"} account. Run with --local to keep the study on this machine.\n`);
63
107
  let outcome;
64
108
  try {
65
109
  outcome = await launchRemoteRun({
66
110
  cwd,
67
111
  study: path.relative(cwd, path.resolve(cwd, args.studyPath)).split(path.sep).join("/"),
68
- remote: remote.plan,
112
+ remote: args.remote,
69
113
  env: process.env,
70
114
  port,
71
115
  say: (line) => io.writeErr(`${line}\n`),
72
116
  });
73
117
  }
74
118
  catch (error) {
75
- refuse("HUMANISH_REMOTE_FAILED", error instanceof Error ? error.message : String(error));
119
+ refuseRemote(args.command, io, cwd, error instanceof Error ? error.message : String(error), "HUMANISH_REMOTE_FAILED", forwarded);
76
120
  return;
77
121
  }
78
- reportRemoteOutcome(args.command, io, cwd, outcome);
122
+ reportRemoteOutcome(args.command, io, cwd, outcome, forwarded);
79
123
  }
80
124
  /** The run options a remote run does not carry to the runner yet. */
81
125
  function localOnlyFlags(options) {
@@ -90,28 +134,35 @@ function localOnlyFlags(options) {
90
134
  ].flat();
91
135
  }
92
136
  /** The command's result and exit code for a remote run that ended, stopped or detached. */
93
- export function reportRemoteOutcome(command, io, cwd, outcome) {
94
- if (outcome.detached === true) {
95
- io.writeErr(`The terminal closed, so run ${outcome.runId} goes on in its runner without this machine. Watch it again with humanish observe --run ${outcome.runId}, or stop it and its sandboxes with humanish reclaim --run ${outcome.runId}\n`);
96
- return;
97
- }
98
- if (outcome.result === undefined || typeof outcome.result !== "object") {
137
+ export function reportRemoteOutcome(command, io, cwd, outcome, forwarded) {
138
+ const remote = forwarded === undefined ? {} : { remote: forwarded };
139
+ if (outcome.detached === true ||
140
+ outcome.result === undefined ||
141
+ typeof outcome.result !== "object") {
142
+ const detached = outcome.detached === true;
99
143
  const failed = {
100
144
  schema: "humanish.run-result.v1",
101
145
  ok: false,
102
146
  cwd,
103
147
  runId: outcome.runId,
104
148
  warnings: [],
105
- error: {
106
- code: "HUMANISH_REMOTE_FAILED",
107
- message: `Run ${outcome.runId} ${outcome.error ?? "ended without a result"}.`,
108
- },
149
+ ...remote,
150
+ error: detached
151
+ ? {
152
+ code: "HUMANISH_REMOTE_DETACHED",
153
+ message: `The terminal closed, so run ${outcome.runId} goes on in its runner without this machine. Follow it and copy it back with humanish observe --run ${outcome.runId}, or stop it and its sandboxes with humanish reclaim --run ${outcome.runId}.`,
154
+ }
155
+ : {
156
+ code: "HUMANISH_REMOTE_FAILED",
157
+ message: `Run ${outcome.runId} ${outcome.error ?? "ended without a result"}.`,
158
+ },
109
159
  };
110
160
  writeResult(command, io, failed, formatRunHuman);
111
- io.setExitCode(outcome.exitCode === 0 ? 2 : outcome.exitCode);
161
+ // A detached run exits as a hung-up local command does; it is not finished, so not 0.
162
+ io.setExitCode(detached ? 129 : outcome.exitCode === 0 ? 2 : outcome.exitCode);
112
163
  return;
113
164
  }
114
- const result = { ...outcome.result, cwd };
165
+ const result = { ...outcome.result, cwd, ...remote };
115
166
  if (outcome.error !== undefined)
116
167
  result.warnings = [...(result.warnings ?? []), outcome.error];
117
168
  if (wantsJson(command))
@@ -12,8 +12,9 @@ export interface RouteRun {
12
12
  * only for a plan that will run: it loads the scorer, or writes its own refusal and returns
13
13
  * undefined, so a refused study never imports the scorer's host code. `afterRun` runs once runStudyWith
14
14
  * has returned or thrown, before presentation, which may hold its own signal handlers (watch's
15
- * Observer and tunnel).
15
+ * Observer and tunnel). `elsewhere`, when given, runs an admitted study somewhere else instead of
16
+ * here: the route's own checks have refused anything this machine would refuse first.
16
17
  */
17
18
  export declare function runRoute(config: StudyConfig, run: RouteRun, beforeRun?: () => Promise<{
18
19
  scorer?: LoadedAdapterScorer;
19
- } | undefined>, afterRun?: () => void): Promise<void>;
20
+ } | undefined>, afterRun?: () => void, elsewhere?: () => Promise<void>): Promise<void>;
@@ -6,12 +6,17 @@ import { prepareStudy } from "../../run-study.js";
6
6
  * only for a plan that will run: it loads the scorer, or writes its own refusal and returns
7
7
  * undefined, so a refused study never imports the scorer's host code. `afterRun` runs once runStudyWith
8
8
  * has returned or thrown, before presentation, which may hold its own signal handlers (watch's
9
- * Observer and tunnel).
9
+ * Observer and tunnel). `elsewhere`, when given, runs an admitted study somewhere else instead of
10
+ * here: the route's own checks have refused anything this machine would refuse first.
10
11
  */
11
- export async function runRoute(config, run, beforeRun = () => Promise.resolve({}), afterRun = () => undefined) {
12
+ export async function runRoute(config, run, beforeRun = () => Promise.resolve({}), afterRun = () => undefined, elsewhere) {
12
13
  let outcome;
13
14
  try {
14
15
  const prepared = await prepareStudy(config, run.options);
16
+ if (prepared.ok && elsewhere !== undefined) {
17
+ await elsewhere();
18
+ return;
19
+ }
15
20
  if (prepared.ok) {
16
21
  const ready = await beforeRun();
17
22
  if (ready === undefined)
@@ -15,7 +15,7 @@ import { sharedWorldRouteRun } from "./study-route-shared-world.js";
15
15
  import { previewRouteRun } from "./study-route-preview.js";
16
16
  import { terminalRouteRun } from "./study-route-terminal.js";
17
17
  import { maybeLoadAdapterScorer } from "./study-scorer.js";
18
- import { runRemoteStudy } from "./study-remote.js";
18
+ import { placeRun, refuseRemote, runRemoteStudy } from "./study-remote.js";
19
19
  import { discoverCliKeys, formatRunHuman, noteRunFacts, writeResult, } from "../io.js";
20
20
  import { watchExposeRequested } from "../observer-follow.js";
21
21
  import { beginRunSignalPhase } from "./run-signals.js";
@@ -112,16 +112,13 @@ export async function runStudyCommand(args) {
112
112
  ...(announced === undefined ? {} : { announced }),
113
113
  });
114
114
  }
115
- if (args.options.remote === true) {
116
- await runRemoteStudy({
117
- command: args.command,
118
- io: args.io,
119
- options: args.options,
120
- config,
121
- studyPath: resolved.path,
122
- });
115
+ const placement = placeRun({ options: args.options, config, studyPath: resolved.path });
116
+ if (placement.kind === "refused") {
117
+ refuseRemote(args.command, args.io, args.options.cwd, placement.message);
123
118
  return;
124
119
  }
120
+ if (placement.kind === "local" && placement.note !== undefined)
121
+ args.io.writeErr(`${placement.note}\n`);
125
122
  // The route's CLI setup refuses bad options, runStudyWith's plan refuses bad labs, and the route's local
126
123
  // checks (keys, runtime auth, subject env, the local agent, caps) refuse this machine, all before
127
124
  // the scorer loads, so none imports the scorer's host code. The scripted route's checks and the
@@ -131,6 +128,19 @@ export async function runStudyCommand(args) {
131
128
  return;
132
129
  // Every route's runStudyWith options carry the study's provenance, from here only.
133
130
  const run = { ...routeRun, options: { ...routeRun.options, study } };
131
+ // A remote run passes the same checks here first, so a study this machine would refuse is refused
132
+ // before any runner exists. The launcher holds its own signal handlers.
133
+ if (placement.kind === "remote") {
134
+ const remote = placement.plan;
135
+ await runRoute(config, run, undefined, undefined, () => runRemoteStudy({
136
+ command: args.command,
137
+ io: args.io,
138
+ options: args.options,
139
+ studyPath: resolved.path,
140
+ remote,
141
+ }));
142
+ return;
143
+ }
134
144
  // From here a signal marks the run interrupted and reclaims its sandboxes (run-signals.ts).
135
145
  const signals = beginRunSignalPhase(args.io);
136
146
  try {
@@ -158,10 +158,11 @@ interface LiveStudy {
158
158
  }
159
159
  /** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
160
160
  export declare function cwdFlag(cwd: string): string;
161
- /** An analysis provider and model, as a study or an analysis record names them. */
161
+ /** An analysis provider, model and effort, as a study or an analysis record names them. */
162
162
  interface Analyst {
163
163
  provider?: string | undefined;
164
164
  model?: string;
165
+ reasoningEffort?: string | undefined;
165
166
  }
166
167
  /** The findings view of a run's selected analysis, or why it has none. */
167
168
  export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
@@ -5,7 +5,7 @@
5
5
  // starts an analysis.
6
6
  import path from "node:path";
7
7
  import { costRefusal } from "../analysis/admission.js";
8
- import { DEFAULT_ANALYSIS_MAX_COST_USD, resolveAutomaticAnalysis, } from "../analysis/automatic-config.js";
8
+ import { DEFAULT_ANALYSIS_EFFORT, DEFAULT_ANALYSIS_MAX_COST_USD, DEFAULT_ANALYSIS_MODEL, resolveAutomaticAnalysis, } from "../analysis/automatic-config.js";
9
9
  import { ANALYSIS_USUALLY_TAKES } from "../analysis/analysis-limits.js";
10
10
  import { bySeverity } from "../analysis/finding-lead.js";
11
11
  import { loadAnalysis } from "../analysis/load.js";
@@ -35,15 +35,25 @@ const KEY_MISSING_REASONS = new Set([
35
35
  "AUTOMATIC_ANALYSIS_KEY_MISSING",
36
36
  "AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING",
37
37
  ]);
38
+ /** The effort an analysis configuration declares; Codex configurations declare none. */
39
+ const declaredEffort = (config) => "reasoningEffort" in config ? config.reasoningEffort : undefined;
38
40
  /** The `analyze` command that runs `analyst` on the run; undefined is the OpenAI default. */
39
41
  function analyzeCommand(source, analyst) {
40
42
  const run = cli(`analyze --run ${source.runId}${source.cwdFlag}`);
41
43
  if (analyst?.provider === "codex")
42
44
  return `${run} --provider codex`;
43
- const gemini = analyst?.provider === "gemini"
44
- ? ` --provider gemini${analyst.model === undefined ? "" : ` --model ${analyst.model}`}`
45
- : "";
46
- return `${run}${gemini} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
45
+ if (analyst?.provider === "gemini") {
46
+ const model = analyst.model === undefined ? "" : ` --model ${analyst.model}`;
47
+ return `${run} --provider gemini${model} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
48
+ }
49
+ // A named model or effort stays in the command, so the rerun is the same analyst. The default
50
+ // analyst needs neither: `analyze` without flags runs it.
51
+ const isDefault = (analyst?.model ?? DEFAULT_ANALYSIS_MODEL) === DEFAULT_ANALYSIS_MODEL &&
52
+ (analyst?.model === undefined || analyst.reasoningEffort === DEFAULT_ANALYSIS_EFFORT);
53
+ const flags = isDefault
54
+ ? ""
55
+ : ` --model ${analyst.model}${analyst.reasoningEffort === undefined ? "" : ` --reasoning-effort ${analyst.reasoningEffort}`}`;
56
+ return `${run}${flags} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
47
57
  }
48
58
  /** The cited items the analysis evidence resolves, each once, in citation order. */
49
59
  function citedEvidence(ids, analysis, runRoot) {
@@ -180,7 +190,7 @@ function missingKeyFindings(source, reason) {
180
190
  }));
181
191
  const opening = "The automatic analysis was skipped because OPENAI_API_KEY is not set.";
182
192
  if (source.codexSignedIn !== true)
183
- return withoutFindings(source, "skipped", reason, `${opening} Set it with ${cli("keys set openai")}, then run the next command.`, analyzeCommand(source, undefined));
193
+ return withoutFindings(source, "skipped", reason, `${opening} Set it with ${cli("keys set openai")}, then run the next command.`, analyzeCommand(source, source.analyst?.provider === "gemini" ? undefined : source.analyst));
184
194
  const file = source.study?.path === undefined ? "the study file" : source.study.path;
185
195
  return withoutFindings(source, "skipped", reason, `${opening} Codex is signed in on this machine, so its ChatGPT account can analyze the run instead, sending the evidence to OpenAI under that account. The next command does it once; review.analysis.provider: codex in ${file} makes it this study's analyst.`, analyzeCommand(source, { provider: "codex" }));
186
196
  }
@@ -234,7 +244,11 @@ async function studyAnalyst(cwd, study) {
234
244
  return undefined;
235
245
  const analysis = resolveAutomaticAnalysis(resolved.config.review?.analysis, resolved.config.actor);
236
246
  return analysis.ok && analysis.config !== undefined
237
- ? { provider: analysis.config.provider, model: analysis.config.model }
247
+ ? {
248
+ provider: analysis.config.provider,
249
+ model: analysis.config.model,
250
+ reasoningEffort: declaredEffort(analysis.config),
251
+ }
238
252
  : undefined;
239
253
  }
240
254
  /** The findings view of a run's selected analysis, or why it has none. */
@@ -255,7 +269,11 @@ export function analysisFindings(source) {
255
269
  ? `The run's evidence changed after analysis ${analysis.id}, so its findings may not match the run.`
256
270
  : `Analysis ${analysis.id} is ${analysis.status}, with ${plural(result.findings.length, "finding")}.`,
257
271
  next: stale
258
- ? analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model })
272
+ ? analyzeCommand(source, {
273
+ provider: analysis.provider,
274
+ model: analysis.config.model,
275
+ reasoningEffort: declaredEffort(analysis.config),
276
+ })
259
277
  : null,
260
278
  analysisId: analysis.id,
261
279
  status: analysis.status,
@@ -279,7 +297,11 @@ export function analysisFindings(source) {
279
297
  if (pending !== undefined)
280
298
  return pending;
281
299
  if (analysis && analysis.result === null)
282
- return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model }));
300
+ return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, {
301
+ provider: analysis.provider,
302
+ model: analysis.config.model,
303
+ reasoningEffort: declaredEffort(analysis.config),
304
+ }));
283
305
  if (job?.state === "failed" || job?.state === "cancelled") {
284
306
  const outcome = analysisOutcomeText(job);
285
307
  return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? { provider: "codex" } : undefined));
package/dist/cli/io.d.ts CHANGED
@@ -47,6 +47,8 @@ export interface StudyCommandOptions {
47
47
  runId?: string | undefined;
48
48
  /** Run the study in a runner sandbox on its participants' provider. */
49
49
  remote?: boolean | undefined;
50
+ /** Run the study on this machine even when it could run remotely. */
51
+ local?: boolean | undefined;
50
52
  /** Hidden: the run key the run's sandboxes carry, chosen by a remote run's launcher. */
51
53
  ownerKey?: string | undefined;
52
54
  /** Hidden, watch only: bind the served Observer on every interface (a remote run's runner). */
@@ -38,12 +38,20 @@ export declare function followRemoteRun(args: {
38
38
  signals: Signals;
39
39
  proxyUrl: string;
40
40
  }): Promise<RemoteOutcome>;
41
- /** Two sandbox journals as one: every line of each, in order, one per line. */
41
+ /**
42
+ * Two sandbox journals as one: every line of each, in order, once. A copy that runs again adds
43
+ * nothing it already added.
44
+ */
42
45
  export declare function joinJournals(...journals: string[]): string;
43
46
  export interface Signals {
44
47
  readonly requested: "stop" | "detach" | undefined;
45
- /** The exit code a local run gives for the stop signal received: 130 for SIGINT, 143 for SIGTERM. */
48
+ /**
49
+ * The exit code for the first signal received: 130 for SIGINT and 143 for SIGTERM, as a local
50
+ * run gives, and 129 for a closed terminal.
51
+ */
46
52
  readonly exitCode: number;
53
+ /** Settles when the first signal arrives, so a long step can stop waiting for itself. */
54
+ readonly received: Promise<void>;
47
55
  dispose(): void;
48
56
  }
49
57
  /**
@@ -51,3 +59,10 @@ export interface Signals {
51
59
  * running; the runner ends it at its deadline.
52
60
  */
53
61
  export declare function watchSignals(): Signals;
62
+ /**
63
+ * The names and values the runner's run receives: each of the plan's names that is set here, and of
64
+ * names the run reads only the first of, only the first one set.
65
+ */
66
+ export declare function forwardedEnv(remote: Pick<RemotePlan, "envNames" | "alternatives">, env: Readonly<Record<string, string | undefined>>): Record<string, string>;
67
+ /** Whether this humanish runs from a built package, which a runner can install. */
68
+ export declare function runsFromPackage(): boolean;