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.
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/dist/analysis/automatic-config.d.ts +12 -1
- package/dist/analysis/automatic-config.js +35 -6
- package/dist/analysis/execute.d.ts +1 -11
- package/dist/analysis/execute.js +4 -21
- package/dist/analysis/gemini-wire.d.ts +2 -1
- package/dist/analysis/gemini-wire.js +3 -2
- package/dist/analysis/models.d.ts +12 -0
- package/dist/analysis/models.js +47 -0
- package/dist/analysis/openai-wire.d.ts +2 -1
- package/dist/analysis/openai-wire.js +3 -2
- package/dist/analysis/provider.d.ts +4 -0
- package/dist/analysis/types.d.ts +4 -0
- package/dist/analysis/validation.js +4 -3
- package/dist/cli/commands/analyze.js +5 -3
- package/dist/cli/commands/run-command.js +2 -1
- package/dist/cli/commands/study-remote.d.ts +34 -2
- package/dist/cli/commands/study-remote.js +103 -52
- package/dist/cli/commands/study-route-run.d.ts +3 -2
- package/dist/cli/commands/study-route-run.js +7 -2
- package/dist/cli/commands/study-run.js +19 -9
- package/dist/cli/findings.d.ts +2 -1
- package/dist/cli/findings.js +31 -9
- package/dist/cli/io.d.ts +2 -0
- package/dist/remote/launch.d.ts +17 -2
- package/dist/remote/launch.js +108 -29
- package/dist/remote/plan.d.ts +2 -0
- package/dist/remote/plan.js +12 -0
- package/dist/remote/runner.d.ts +21 -0
- package/dist/remote/runner.js +42 -5
- package/dist/run/results.d.ts +9 -1
- package/dist/study/keys.d.ts +1 -0
- package/dist/study/keys.js +1 -0
- package/dist/study/parse/execution.js +5 -0
- package/dist/study/types.d.ts +6 -0
- package/dist/tui/launch.js +3 -0
- package/docs/contracts/study-analysis.md +35 -2
- package/docs/product/automatic-analysis.md +17 -7
- package/package.json +1 -1
- 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 {
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 {
|
package/dist/cli/findings.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/cli/findings.js
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
? {
|
|
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, {
|
|
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, {
|
|
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). */
|
package/dist/remote/launch.d.ts
CHANGED
|
@@ -38,12 +38,20 @@ export declare function followRemoteRun(args: {
|
|
|
38
38
|
signals: Signals;
|
|
39
39
|
proxyUrl: string;
|
|
40
40
|
}): Promise<RemoteOutcome>;
|
|
41
|
-
/**
|
|
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
|
-
/**
|
|
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;
|