humanish 0.115.0 → 0.116.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 +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,28 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.116.0: Hosted keyboard input, E2B sandbox ceiling, bounded run reads, run header, TUI live starts (2026-10-09)
|
|
12
|
+
|
|
13
|
+
humanish 0.116.0 delivers a hosted participant's arrow keys and punctuation, lets the operator
|
|
14
|
+
raise the E2B sandbox lifetime, and reads run files within size limits. On hosted E2B desktops,
|
|
15
|
+
`ARROWDOWN`, Page Up, Page Down, `?`, `/` and other names missing from `@e2b/desktop`'s key table
|
|
16
|
+
reached xdotool unchanged, which ignored them while the run recorded the keypress as completed;
|
|
17
|
+
both desktops now translate key names through one table and refuse a name outside it.
|
|
18
|
+
`HUMANISH_E2B_MAX_SANDBOX_MINUTES` sets the longest sandbox lifetime the planners allow, 60 minutes
|
|
19
|
+
when unset and up to 1440 on E2B's Pro plan. A refusal names the value that admits the study, and
|
|
20
|
+
`humanish doctor` shows the ceiling. A route's sandbox kill, and the kills, checks and listings of
|
|
21
|
+
`humanish reclaim`, carry the run's E2B key. `verify` reads at most 32 MiB of a run file and lists
|
|
22
|
+
a larger file, or a run past 10,000 entries, as unscanned, and every contained read tells a missing
|
|
23
|
+
file from one it refused. A run whose analysis did not complete reads `live run finished; the
|
|
24
|
+
analysis did not complete`, a run refused before it was created prints no `analysis:` line, and a
|
|
25
|
+
`desktop-cli` run names its product on the `subject:` line and in run.json. In `humanish tui`, the
|
|
26
|
+
cursor follows the selected row through a refresh, a live start of a `mode: dry-run` file says
|
|
27
|
+
`needs mode: live`, Run again on a live run asks for a second Enter, the confirm prompt restates
|
|
28
|
+
the study's cost, and `c` lists every provider key. `NO_UPDATE_NOTIFIER` turns the update check
|
|
29
|
+
off.
|
|
30
|
+
|
|
31
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.116.0)
|
|
32
|
+
|
|
11
33
|
## 0.115.0: Closing report limits, admitted analysis cost, concurrent reviewer notes, known-value scrub (2026-10-08)
|
|
12
34
|
|
|
13
35
|
humanish 0.115.0 asks the OpenAI computer-use participant for its closing report and impressions
|
package/README.md
CHANGED
|
@@ -276,9 +276,10 @@ sensitive to capture in the first place.
|
|
|
276
276
|
**What the automated gate enforces.** `humanish verify` scans public-bound
|
|
277
277
|
artifacts and fails closed on secret, key, and token shapes and on known local
|
|
278
278
|
path shapes. It reads text only, judged by a file's bytes rather than its name:
|
|
279
|
-
any file in the run folder that is not UTF-8 text
|
|
280
|
-
screenshot or recording, keeps the run `local_only`
|
|
281
|
-
matches after undoing escapes, percent-encoding, HTML
|
|
279
|
+
any file in the run folder that is not UTF-8 text or is larger than 32 MiB,
|
|
280
|
+
other than a stream screenshot or recording, keeps the run `local_only`
|
|
281
|
+
(`UNSCANNED_ARTIFACT`). It matches after undoing escapes, percent-encoding, HTML
|
|
282
|
+
entities and base64. It
|
|
282
283
|
does not yet detect free-form PII or PHI such as names, emails,
|
|
283
284
|
phone numbers, dates of birth, or medical identifiers. Keeping those out depends
|
|
284
285
|
on using synthetic data and on review, so `redaction: passed` means the
|
|
@@ -483,9 +484,9 @@ At most once a day, a command run at a terminal also asks the npm registry for
|
|
|
483
484
|
the latest humanish version, and a later command prints one line on stderr when
|
|
484
485
|
yours is older. That request is a plain `GET` of humanish's dist-tags with no
|
|
485
486
|
machine id, version, command or path. It is skipped in CI, without a terminal,
|
|
486
|
-
for `--json`, and with `DO_NOT_TRACK=1`, `HUMANISH_TELEMETRY_DISABLED=1
|
|
487
|
-
`HUMANISH_NO_UPDATE_CHECK=1
|
|
488
|
-
describes it.
|
|
487
|
+
for `--json`, and with `DO_NOT_TRACK=1`, `HUMANISH_TELEMETRY_DISABLED=1`,
|
|
488
|
+
`HUMANISH_NO_UPDATE_CHECK=1` or update-notifier's `NO_UPDATE_NOTIFIER=1`.
|
|
489
|
+
[TELEMETRY.md](TELEMETRY.md#the-update-check) describes it.
|
|
489
490
|
|
|
490
491
|
## Contribute
|
|
491
492
|
|
package/TELEMETRY.md
CHANGED
|
@@ -132,4 +132,10 @@ participant, and when any of these is set:
|
|
|
132
132
|
DO_NOT_TRACK=1
|
|
133
133
|
HUMANISH_TELEMETRY_DISABLED=1
|
|
134
134
|
HUMANISH_NO_UPDATE_CHECK=1
|
|
135
|
+
NO_UPDATE_NOTIFIER=1
|
|
135
136
|
```
|
|
137
|
+
|
|
138
|
+
`NO_UPDATE_NOTIFIER` is the variable the update-notifier package reads, so a
|
|
139
|
+
machine set up to silence the CLIs built on it gets no humanish notice either.
|
|
140
|
+
humanish reads it as update-notifier does: any value turns the check off,
|
|
141
|
+
including an empty one, `0` or `false`.
|
|
@@ -2,7 +2,7 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { digestText } from "../../evidence/redaction.js";
|
|
4
4
|
import { CODEX_APP_SERVER_ARTIFACTS, CODEX_APP_SERVER_DIRECTORY, runCodexAppServerSessionInPreparedRoot, } from "./app-server.js";
|
|
5
|
-
import { prepareContainedOutputDirectory, prepareContainedOutputFile, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, prepareSelectedOutputFile, readContainedRegularFile, writeContainedOutputFile, writePreparedSelectedOutputFile, } from "../../run/contained-output.js";
|
|
5
|
+
import { prepareContainedOutputDirectory, prepareContainedOutputFile, prepareManagedHumanishOutputDirectory, prepareSelectedOutputDirectory, prepareSelectedOutputFile, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, writePreparedSelectedOutputFile, } from "../../run/contained-output.js";
|
|
6
6
|
const CODEX_APP_SERVER_UI_SCHEMA = "humanish.codex-app-server-ui.v1";
|
|
7
7
|
export async function startCodexAppServerUi(options) {
|
|
8
8
|
const cwd = path.resolve(options.cwd);
|
|
@@ -128,13 +128,20 @@ export async function startCodexAppServerUi(options) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
async function serveArtifact(args) {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
131
|
+
const read = await readContainedRegularFile(args.runRoot, args.requestPath, RUN_ARTIFACT_MAX_BYTES);
|
|
132
|
+
if (read.status === "read") {
|
|
133
133
|
args.response.writeHead(200, {
|
|
134
134
|
"cache-control": "no-store",
|
|
135
135
|
"content-type": contentTypeFor(args.requestPath),
|
|
136
136
|
});
|
|
137
|
-
args.response.end(
|
|
137
|
+
args.response.end(read.bytes);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// A file over the limit is there: a 404 would tell the page it does not exist. A link, or a
|
|
141
|
+
// path outside the run, is served as absent, as it always was.
|
|
142
|
+
if (read.status === "refused" && read.reason === "too-large") {
|
|
143
|
+
args.response.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
|
144
|
+
args.response.end(`${refusalText(args.requestPath, read)}.`);
|
|
138
145
|
return;
|
|
139
146
|
}
|
|
140
147
|
args.response.writeHead(404);
|
|
@@ -7,12 +7,27 @@ import { notice } from "./trace.js";
|
|
|
7
7
|
import { isCompleteTurnUsage } from "./usage.js";
|
|
8
8
|
const DEBRIEF_REQUEST_CAP_MS = 30_000;
|
|
9
9
|
const DEBRIEF_HINT = `The interactive session has ended. Return a closing account with summary, frictionReports and impressions. In summary, briefly describe only what you actually did and observed. In frictionReports, list only specific unexpected behavior, confusion, or recovery you personally encountered during this session. Preserve uncertainty. Use an empty list if you encountered none. ${IMPRESSIONS_ASK} Do not speculate, invent problems, quote instructions as observations, or describe planned actions. Do not request or take further actions. This is a closing account, not another attempt at the task.`;
|
|
10
|
+
const CLOSING_REPORT = {
|
|
11
|
+
call: (provider) => provider.debrief,
|
|
12
|
+
contextHint: DEBRIEF_HINT,
|
|
13
|
+
noticeLabel: "debrief",
|
|
14
|
+
deadlineLabel: "closing report",
|
|
15
|
+
read: readClosingReport,
|
|
16
|
+
};
|
|
17
|
+
const IMPRESSIONS_ONLY = {
|
|
18
|
+
call: (provider) => provider.requestImpressions,
|
|
19
|
+
contextHint: IMPRESSIONS_HINT,
|
|
20
|
+
noticeLabel: "impressions request",
|
|
21
|
+
deadlineLabel: "impressions request",
|
|
22
|
+
read: readImpressions,
|
|
23
|
+
};
|
|
10
24
|
/**
|
|
11
25
|
* Ask the participant for its closing report after a structured stop, or for its impressions only
|
|
12
26
|
* after it ended the session itself, or record why the request was skipped. Either way the result
|
|
13
27
|
* is the trace's debrief, and the watcher gets a final flush.
|
|
14
28
|
*/
|
|
15
29
|
export async function requestDebrief(session, context, trigger) {
|
|
30
|
+
const ask = trigger.kind === "participant_end" ? IMPRESSIONS_ONLY : CLOSING_REPORT;
|
|
16
31
|
const record = (status, detail, usageReported) => {
|
|
17
32
|
const debrief = {
|
|
18
33
|
trigger: trigger.kind,
|
|
@@ -20,23 +35,23 @@ export async function requestDebrief(session, context, trigger) {
|
|
|
20
35
|
reason: session.redactNarration(detail),
|
|
21
36
|
...(usageReported === undefined ? {} : { usageReported }),
|
|
22
37
|
};
|
|
23
|
-
session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant ${
|
|
38
|
+
session.trace.record("notice", () => notice(status === "completed" ? "ok" : "warn", `participant ${ask.noticeLabel} ${status}`, session.redactNarration(detail)));
|
|
24
39
|
return debrief;
|
|
25
40
|
};
|
|
26
|
-
const plan = planDebrief(session, trigger);
|
|
41
|
+
const plan = planDebrief(session, ask, trigger);
|
|
27
42
|
const debrief = "skip" in plan
|
|
28
43
|
? record("skipped", plan.skip)
|
|
29
|
-
: await debriefExchange(session,
|
|
44
|
+
: await debriefExchange(session, context, trigger, ask, plan, record);
|
|
30
45
|
session.flush();
|
|
31
46
|
return debrief;
|
|
32
47
|
}
|
|
33
|
-
function planDebrief(session, trigger) {
|
|
48
|
+
function planDebrief(session, ask, trigger) {
|
|
34
49
|
const { provider, signal, usage } = session;
|
|
35
50
|
const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
|
|
36
51
|
if (session.trace.counts.turns === 0)
|
|
37
52
|
return { skip: "the study stopped before any participant turn" };
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
53
|
+
const send = ask.call(provider)?.bind(provider);
|
|
54
|
+
if (send === undefined)
|
|
40
55
|
return { skip: "this provider does not support read-only closing reports" };
|
|
41
56
|
if (usage.cleanupUnconfirmed)
|
|
42
57
|
return { skip: "participant request cleanup is unconfirmed" };
|
|
@@ -68,9 +83,9 @@ function planDebrief(session, trigger) {
|
|
|
68
83
|
skip: signal?.aborted ? "the study was cancelled" : "the session deadline was reached",
|
|
69
84
|
};
|
|
70
85
|
}
|
|
71
|
-
return {
|
|
86
|
+
return { send, boundMs };
|
|
72
87
|
}
|
|
73
|
-
async function debriefExchange(session,
|
|
88
|
+
async function debriefExchange(session, context, trigger, ask, { send, boundMs }, record) {
|
|
74
89
|
const { signal, provider } = session;
|
|
75
90
|
const scope = requestScope(signal);
|
|
76
91
|
const timer = setTimeout(() => scope.end(), boundMs);
|
|
@@ -89,12 +104,12 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
|
|
|
89
104
|
...(context.acknowledgedSafetyChecks === undefined
|
|
90
105
|
? {}
|
|
91
106
|
: { acknowledgedSafetyChecks: context.acknowledgedSafetyChecks }),
|
|
92
|
-
contextHint:
|
|
107
|
+
contextHint: ask.contextHint,
|
|
93
108
|
};
|
|
94
109
|
const turn = provider.requestPolicy === "fail_closed"
|
|
95
|
-
? await singleDispatch(session, "debrief", (dispatchSignal) =>
|
|
96
|
-
: await raceSessionDeadline(
|
|
97
|
-
return acceptDebriefTurn(session, turn,
|
|
110
|
+
? await singleDispatch(session, "debrief", (dispatchSignal) => send(request, dispatchSignal), boundMs)
|
|
111
|
+
: await raceSessionDeadline(send(request, scope.signal), boundMs, signal);
|
|
112
|
+
return acceptDebriefTurn(session, turn, ask, record);
|
|
98
113
|
}
|
|
99
114
|
catch (error) {
|
|
100
115
|
// A failed optional report cannot rewrite the already observed structured completion.
|
|
@@ -104,7 +119,7 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
|
|
|
104
119
|
const detail = signal?.aborted
|
|
105
120
|
? "cancelled"
|
|
106
121
|
: scope.signal.aborted || error instanceof CuaDeadlineError
|
|
107
|
-
? `${
|
|
122
|
+
? `${ask.deadlineLabel} deadline reached`
|
|
108
123
|
: error instanceof Error
|
|
109
124
|
? error.message
|
|
110
125
|
: String(error);
|
|
@@ -117,7 +132,7 @@ async function debriefExchange(session, ask, context, trigger, boundMs, record)
|
|
|
117
132
|
scope.end();
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
|
-
function acceptDebriefTurn(session, turn,
|
|
135
|
+
function acceptDebriefTurn(session, turn, ask, record) {
|
|
121
136
|
const { maxUsd, overRunBudget, estimateTurnCostUsd } = session.settings;
|
|
122
137
|
session.usage.record(turn, "debrief");
|
|
123
138
|
session.lastResponseId = turn.responseId ?? session.lastResponseId;
|
|
@@ -133,19 +148,17 @@ function acceptDebriefTurn(session, turn, trigger, record) {
|
|
|
133
148
|
return record("failed", "the closing response requested actions or safety checks; none were executed and its report was not accepted", usageReported);
|
|
134
149
|
}
|
|
135
150
|
// First-party providers parse their replies already; these parses cover third-party ports.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
: "the reply did not contain valid impressions", usageReported);
|
|
142
|
-
}
|
|
143
|
-
session.impressions = recordImpressions(session, impressions.data);
|
|
144
|
-
return record("completed", "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged", usageReported);
|
|
145
|
-
}
|
|
151
|
+
const { status, detail, ...added } = ask.read(session, turn);
|
|
152
|
+
return { ...record(status, detail, usageReported), ...added };
|
|
153
|
+
}
|
|
154
|
+
/** A closing report: its summary and friction as one participant message, then its impressions. */
|
|
155
|
+
function readClosingReport(session, turn) {
|
|
146
156
|
const closing = closingReportSchema.safeParse(turn.closingReport);
|
|
147
157
|
if (!closing.success) {
|
|
148
|
-
return
|
|
158
|
+
return {
|
|
159
|
+
status: "failed",
|
|
160
|
+
detail: "the closing response did not contain a valid structured participant report",
|
|
161
|
+
};
|
|
149
162
|
}
|
|
150
163
|
const report = {
|
|
151
164
|
summary: session.redactNarration(closing.data.summary.trim()),
|
|
@@ -160,6 +173,27 @@ function acceptDebriefTurn(session, turn, trigger, record) {
|
|
|
160
173
|
}));
|
|
161
174
|
session.trace.bump("messages");
|
|
162
175
|
session.impressions = recordImpressions(session, closing.data.impressions);
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
return {
|
|
177
|
+
status: "completed",
|
|
178
|
+
detail: "one read-only report; no additional desktop actions; original stop and task outcomes preserved",
|
|
179
|
+
report,
|
|
180
|
+
messageId,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Impressions alone, after the participant ended the session itself. */
|
|
184
|
+
function readImpressions(session, turn) {
|
|
185
|
+
const impressions = participantImpressionsSchema.safeParse(turn.impressions);
|
|
186
|
+
if (!impressions.success) {
|
|
187
|
+
return {
|
|
188
|
+
status: "failed",
|
|
189
|
+
detail: turn.interruption === "output_limit"
|
|
190
|
+
? "the reply was cut off by the output limit"
|
|
191
|
+
: "the reply did not contain valid impressions",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
session.impressions = recordImpressions(session, impressions.data);
|
|
195
|
+
return {
|
|
196
|
+
status: "completed",
|
|
197
|
+
detail: "one read-only impressions request; no additional desktop actions; the participant's own ending is unchanged",
|
|
198
|
+
};
|
|
165
199
|
}
|
|
@@ -6,7 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { assertScreenshotEvidence, stripPngMetadataChunks } from "../../evidence/image.js";
|
|
8
8
|
import { digestText, redactToSecretLabel } from "../../evidence/redaction.js";
|
|
9
|
-
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile, } from "../../run/contained-output.js";
|
|
9
|
+
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "../../run/contained-output.js";
|
|
10
10
|
import { LOOPBACK_EVIDENCE_URL_POLICY, } from "./types.js";
|
|
11
11
|
export async function executeBrowserPersonaStep(args) {
|
|
12
12
|
const started = Date.now();
|
|
@@ -402,11 +402,12 @@ async function captureScriptedPageScreenshot(page) {
|
|
|
402
402
|
if (Buffer.isBuffer(returned) || returned instanceof Uint8Array) {
|
|
403
403
|
return stripPngMetadataChunks(browserScreenshotBytes(returned));
|
|
404
404
|
}
|
|
405
|
-
const
|
|
406
|
-
if (
|
|
405
|
+
const staged = await readContainedRegularFile(stagingRoot, "capture.png", RUN_ARTIFACT_MAX_BYTES);
|
|
406
|
+
if (staged.status === "missing")
|
|
407
407
|
throw new Error("Browser screenshot did not return bytes or write a single-link staging file.");
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
if (staged.status === "refused")
|
|
409
|
+
throw new Error(`Browser screenshot did not return bytes, and ${refusalText("its staging file capture.png", staged)}.`);
|
|
410
|
+
return stripPngMetadataChunks(staged.bytes);
|
|
410
411
|
}
|
|
411
412
|
finally {
|
|
412
413
|
await assertPreparedSelectedOutputDirectory(stagingRoot)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much an analysis result may hold, for the fields the Observer checks as well. Characters are
|
|
3
|
+
* counted as `String.length` counts them.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ANALYSIS_LIMITS: Readonly<{
|
|
6
|
+
/** A label: a finding's title and headline, a design finding's headline and screen, or a name. */
|
|
7
|
+
labelChars: 240;
|
|
8
|
+
experienceChars: 1200;
|
|
9
|
+
noticeChars: 1500;
|
|
10
|
+
whyItMattersChars: 1000;
|
|
11
|
+
suggestionChars: 1000;
|
|
12
|
+
/** Evidence one observation or design finding may cite. */
|
|
13
|
+
evidenceRefs: 100;
|
|
14
|
+
findings: 100;
|
|
15
|
+
designFindings: 40;
|
|
16
|
+
concernReviews: 60;
|
|
17
|
+
}>;
|
|
18
|
+
/** Whether text holds a control character other than tab, line feed and carriage return. */
|
|
19
|
+
export declare function hasControlCharacter(text: string): boolean;
|
|
20
|
+
/** The parts of an analysis result the revision boundaries read. */
|
|
21
|
+
interface RevisionFields {
|
|
22
|
+
readonly concernReviews?: unknown;
|
|
23
|
+
readonly designFindings?: unknown;
|
|
24
|
+
readonly findings: ReadonlyArray<{
|
|
25
|
+
readonly headline?: unknown;
|
|
26
|
+
readonly experience?: unknown;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether a result carries every field its prompt revision requires: concern reviews from
|
|
31
|
+
* study-evidence-5, and headlines, experiences and design findings from study-evidence-7. A prompt
|
|
32
|
+
* version without a study-evidence revision number requires none of them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function hasRevisionFields(promptVersion: string, result: RevisionFields): boolean;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The analysis result's limits and prompt revision boundaries, kept apart from the zod schemas in
|
|
2
|
+
// validation.ts so the Observer can read them: it bundles this module, and zod stays out of its
|
|
3
|
+
// page.
|
|
4
|
+
/**
|
|
5
|
+
* How much an analysis result may hold, for the fields the Observer checks as well. Characters are
|
|
6
|
+
* counted as `String.length` counts them.
|
|
7
|
+
*/
|
|
8
|
+
export const ANALYSIS_LIMITS = Object.freeze({
|
|
9
|
+
/** A label: a finding's title and headline, a design finding's headline and screen, or a name. */
|
|
10
|
+
labelChars: 240,
|
|
11
|
+
experienceChars: 1200,
|
|
12
|
+
noticeChars: 1500,
|
|
13
|
+
whyItMattersChars: 1000,
|
|
14
|
+
suggestionChars: 1000,
|
|
15
|
+
/** Evidence one observation or design finding may cite. */
|
|
16
|
+
evidenceRefs: 100,
|
|
17
|
+
findings: 100,
|
|
18
|
+
designFindings: 40,
|
|
19
|
+
concernReviews: 60,
|
|
20
|
+
});
|
|
21
|
+
/** Whether text holds a control character other than tab, line feed and carriage return. */
|
|
22
|
+
export function hasControlCharacter(text) {
|
|
23
|
+
return /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text);
|
|
24
|
+
}
|
|
25
|
+
// The prompt revision from which a result must carry each field. A boundary stays where it is when
|
|
26
|
+
// the prompt version advances, so a later prompt cannot bring back a legacy omission.
|
|
27
|
+
const CONCERN_REVIEWS_FROM = 5;
|
|
28
|
+
const HEADLINES_AND_DESIGN_FINDINGS_FROM = 7;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a result carries every field its prompt revision requires: concern reviews from
|
|
31
|
+
* study-evidence-5, and headlines, experiences and design findings from study-evidence-7. A prompt
|
|
32
|
+
* version without a study-evidence revision number requires none of them.
|
|
33
|
+
*/
|
|
34
|
+
export function hasRevisionFields(promptVersion, result) {
|
|
35
|
+
const revision = Number(/^study-evidence-(\d+)$/.exec(promptVersion)?.[1] ?? 0);
|
|
36
|
+
if (revision >= CONCERN_REVIEWS_FROM && result.concernReviews === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
return (revision < HEADLINES_AND_DESIGN_FINDINGS_FROM ||
|
|
39
|
+
(result.designFindings !== undefined &&
|
|
40
|
+
result.findings.every((finding) => finding.headline !== undefined && finding.experience !== undefined)));
|
|
41
|
+
}
|
|
@@ -2,12 +2,13 @@ import { validStoredCodexAnalysisConfig } from "./codex-config.js";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ACTOR_STATUSES, ACTOR_STOP_CAUSES } from "../actors/contract.js";
|
|
5
|
+
import { ANALYSIS_LIMITS, hasControlCharacter, hasRevisionFields } from "./analysis-limits.js";
|
|
5
6
|
import { SHA256_HEX_PATTERN, ACTION_CAPTURE_VERSION, ANALYSIS_SCHEMA, ANALYSIS_CORRECTION_SCHEMA, } from "./types.js";
|
|
6
7
|
const text = (max) => z
|
|
7
8
|
.string()
|
|
8
9
|
.max(max)
|
|
9
|
-
.refine((value) =>
|
|
10
|
-
const label = text(
|
|
10
|
+
.refine((value) => !hasControlCharacter(value), "Invalid text.");
|
|
11
|
+
const label = text(ANALYSIS_LIMITS.labelChars).min(1);
|
|
11
12
|
const id = z
|
|
12
13
|
.string()
|
|
13
14
|
.min(1)
|
|
@@ -17,7 +18,7 @@ const sourceId = text(256).min(1);
|
|
|
17
18
|
const digest = z.string().regex(SHA256_HEX_PATTERN);
|
|
18
19
|
const timestamp = z.string().datetime({ offset: true });
|
|
19
20
|
const sourceIds = z.array(sourceId).max(128);
|
|
20
|
-
const refs = z.array(id).min(1).max(
|
|
21
|
+
const refs = z.array(id).min(1).max(ANALYSIS_LIMITS.evidenceRefs);
|
|
21
22
|
const limitations = z.array(text(2000).min(1)).max(100);
|
|
22
23
|
const observationSchema = z
|
|
23
24
|
.object({
|
|
@@ -32,9 +33,9 @@ const designFindingSchema = z
|
|
|
32
33
|
id,
|
|
33
34
|
headline: label,
|
|
34
35
|
screen: label,
|
|
35
|
-
notice: text(
|
|
36
|
-
whyItMatters: text(
|
|
37
|
-
suggestion: text(
|
|
36
|
+
notice: text(ANALYSIS_LIMITS.noticeChars).min(1),
|
|
37
|
+
whyItMatters: text(ANALYSIS_LIMITS.whyItMattersChars).min(1),
|
|
38
|
+
suggestion: text(ANALYSIS_LIMITS.suggestionChars).min(1),
|
|
38
39
|
severity: z.enum(["minor", "moderate", "major"]),
|
|
39
40
|
confidence: z.enum(["low", "medium", "high"]),
|
|
40
41
|
seenByStreamIds: sourceIds.min(1),
|
|
@@ -66,7 +67,7 @@ const analysisResultSchema = z
|
|
|
66
67
|
id,
|
|
67
68
|
title: label,
|
|
68
69
|
headline: label.optional(),
|
|
69
|
-
experience: text(
|
|
70
|
+
experience: text(ANALYSIS_LIMITS.experienceChars).min(1).optional(),
|
|
70
71
|
summary: text(4000).min(1),
|
|
71
72
|
impact: z.enum(["blocked_task", "friction", "recovery", "uncertain"]),
|
|
72
73
|
affectedStreamIds: sourceIds.min(1),
|
|
@@ -79,8 +80,8 @@ const analysisResultSchema = z
|
|
|
79
80
|
priorityReason: text(2000).min(1),
|
|
80
81
|
})
|
|
81
82
|
.strict())
|
|
82
|
-
.max(
|
|
83
|
-
designFindings: z.array(designFindingSchema).max(
|
|
83
|
+
.max(ANALYSIS_LIMITS.findings),
|
|
84
|
+
designFindings: z.array(designFindingSchema).max(ANALYSIS_LIMITS.designFindings).optional(),
|
|
84
85
|
concernReviews: z
|
|
85
86
|
.array(observationSchema
|
|
86
87
|
.extend({
|
|
@@ -89,7 +90,7 @@ const analysisResultSchema = z
|
|
|
89
90
|
reason: text(2000).min(1),
|
|
90
91
|
})
|
|
91
92
|
.strict())
|
|
92
|
-
.max(
|
|
93
|
+
.max(ANALYSIS_LIMITS.concernReviews)
|
|
93
94
|
.optional(),
|
|
94
95
|
limitations,
|
|
95
96
|
})
|
|
@@ -100,8 +101,8 @@ export const analysisResponseSchema = analysisResultSchema
|
|
|
100
101
|
.extend({
|
|
101
102
|
findings: z
|
|
102
103
|
.array(analysisResultSchema.shape.findings.element.required({ headline: true, experience: true }))
|
|
103
|
-
.max(
|
|
104
|
-
designFindings: z.array(designFindingSchema).max(
|
|
104
|
+
.max(ANALYSIS_LIMITS.findings),
|
|
105
|
+
designFindings: z.array(designFindingSchema).max(ANALYSIS_LIMITS.designFindings),
|
|
105
106
|
})
|
|
106
107
|
.required({ concernReviews: true });
|
|
107
108
|
export const analysisResultJsonSchema = z.toJSONSchema(analysisResponseSchema);
|
|
@@ -469,15 +470,7 @@ export function validateAnalysisArtifact(value) {
|
|
|
469
470
|
(artifact.status === "complete" && !coverage.complete))
|
|
470
471
|
throw new Error("ANALYSIS_STATUS_INVALID");
|
|
471
472
|
if (artifact.result !== null) {
|
|
472
|
-
|
|
473
|
-
// findings with revision 7. Keep each boundary stable when the prompt version advances; a later
|
|
474
|
-
// prompt must not regain legacy omissions.
|
|
475
|
-
const revision = Number(/^study-evidence-(\d+)$/.exec(artifact.promptVersion)?.[1] ?? 0);
|
|
476
|
-
const result = artifact.result;
|
|
477
|
-
if ((revision >= 5 && result.concernReviews === undefined) ||
|
|
478
|
-
(revision >= 7 &&
|
|
479
|
-
(result.designFindings === undefined ||
|
|
480
|
-
result.findings.some((finding) => finding.headline === undefined || finding.experience === undefined))))
|
|
473
|
+
if (!hasRevisionFields(artifact.promptVersion, artifact.result))
|
|
481
474
|
throw new Error("ANALYSIS_RESULT_SCHEMA_INVALID");
|
|
482
475
|
validateAnalysisResult({ ...artifact, images: [] }, artifact.result);
|
|
483
476
|
}
|
|
@@ -6,7 +6,8 @@ import { listAnalyses } from "../../analysis/store.js";
|
|
|
6
6
|
import { listAnalysisExecutions } from "../../analysis/store-executions.js";
|
|
7
7
|
import { resolveRunPath } from "../../run/locate.js";
|
|
8
8
|
import { CWD_OPTION_DESCRIPTION, discoverCliKeys, JSON_OPTION_DESCRIPTION, RUN_OPTION_DESCRIPTION, writeResult, humanError, wantsJson, } from "../io.js";
|
|
9
|
-
import { analysisFindings,
|
|
9
|
+
import { analysisFindings, missingRunFindings, readRunAnalysis, } from "../findings.js";
|
|
10
|
+
import { formatFindings } from "../findings-text.js";
|
|
10
11
|
import { resolvePhysicalCwd } from "../../run/paths.js";
|
|
11
12
|
/** Commander may collect a shared flag on the parent; only explicit values override leaf defaults. */
|
|
12
13
|
function analysisSelection(options, command) {
|
|
@@ -2,9 +2,10 @@ import { renderObserver } from "../../observer/render.js";
|
|
|
2
2
|
import { loadRunBundlePrepared, resolveRunPath } from "../../run/locate.js";
|
|
3
3
|
import { readRunNotes } from "../../run/note-files.js";
|
|
4
4
|
import { byRunTime } from "../../run/note-shape.js";
|
|
5
|
-
import { addRunNote
|
|
5
|
+
import { addRunNote } from "../../run/notes.js";
|
|
6
6
|
import { runParticipantCaptions, streamParticipantIdOf } from "../../run/participant-records.js";
|
|
7
7
|
import { resolvePhysicalCwd, runIdOf } from "../../run/paths.js";
|
|
8
|
+
import { formatRunTime } from "../../run/run-clock.js";
|
|
8
9
|
import { runNotFoundMessage } from "../../run/run-not-found.js";
|
|
9
10
|
import { cli } from "../invocation.js";
|
|
10
11
|
import { CWD_OPTION_DESCRIPTION, humanError, JSON_OPTION_DESCRIPTION, writeResult, } from "../io.js";
|
|
@@ -134,7 +134,11 @@ async function stopActiveRuns(io, signal, stopping, options) {
|
|
|
134
134
|
// not swept; verify grades a file that names a receipt's id local_only.
|
|
135
135
|
Promise.all(stopping
|
|
136
136
|
.filter((stop) => stop.report !== null)
|
|
137
|
-
.map((stop) => scrubRunSandboxIds(stop.run.paths).catch(() =>
|
|
137
|
+
.map((stop) => scrubRunSandboxIds(stop.run.paths).catch((error) => {
|
|
138
|
+
// A file the sweep refused to read may still name a sandbox; say which.
|
|
139
|
+
if (error instanceof Error)
|
|
140
|
+
say(io, `humanish: ${signal}: ${error.message}`);
|
|
141
|
+
}))));
|
|
138
142
|
const finished = await withDeadline(work, deadlineMs, options.deadline);
|
|
139
143
|
for (const stop of stopping) {
|
|
140
144
|
if (stop.report === null)
|
|
@@ -11,7 +11,8 @@ import { addRunOptions, handleRun } from "./run-command.js";
|
|
|
11
11
|
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, RUN_OPTION_DESCRIPTION, writeResult, humanError, } from "../io.js";
|
|
12
12
|
import { plural } from "../../run/text.js";
|
|
13
13
|
import { warnAndQueue } from "../deprecations.js";
|
|
14
|
-
import {
|
|
14
|
+
import { readRunFindings } from "../findings.js";
|
|
15
|
+
import { formatFindings } from "../findings-text.js";
|
|
15
16
|
import { cli } from "../invocation.js";
|
|
16
17
|
export function registerRunCommand(parent, io) {
|
|
17
18
|
addRunOptions(parent
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath, } from "../../keys/key-resolution.js";
|
|
2
|
+
import { KEY_VENDORS, listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath, } from "../../keys/key-resolution.js";
|
|
3
3
|
import { askForMissingKeys, formatKeyStatus, keyStatus, otherStoredKeys, } from "../../keys/key-status.js";
|
|
4
4
|
import { promptSecret } from "../secret-prompt.js";
|
|
5
5
|
import { runInit } from "../../study/init.js";
|
|
@@ -270,7 +270,7 @@ export function registerKeysCommand(parent, io) {
|
|
|
270
270
|
.action(async (_options, command) => writeKeyStatus(command, io));
|
|
271
271
|
keys
|
|
272
272
|
.command("set")
|
|
273
|
-
.argument("[vendor-or-name]",
|
|
273
|
+
.argument("[vendor-or-name]", `A vendor alias (${KEY_VENDORS.join(", ")}) or a raw ENV_NAME. Without one, a terminal asks for each missing key in turn.`)
|
|
274
274
|
.description("Store one provider key in the user store (0600), prompted with hidden input. With no key named, ask for each missing key in turn.")
|
|
275
275
|
.option("--stdin", "Read the value from stdin instead of prompting (for agents/pipes).")
|
|
276
276
|
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
@@ -288,7 +288,7 @@ export function registerKeysCommand(parent, io) {
|
|
|
288
288
|
action: "set",
|
|
289
289
|
store: storePath,
|
|
290
290
|
names: [],
|
|
291
|
-
message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors:
|
|
291
|
+
message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: ${KEY_VENDORS.join(", ")}.`,
|
|
292
292
|
};
|
|
293
293
|
writeResult(command, io, result, formatKeysHuman);
|
|
294
294
|
io.setExitCode(2);
|
|
@@ -3,7 +3,10 @@ import type { CuaActorStudyResult } from "../../routes/computer-use/types.js";
|
|
|
3
3
|
import type { ScriptedBrowserStudyResult } from "../../routes/scripted/types.js";
|
|
4
4
|
import type { TerminalProductStudyResult } from "../../routes/terminal/types.js";
|
|
5
5
|
import type { ConcurrentSharedWorldStudyResult } from "../../routes/shared-world/types.js";
|
|
6
|
+
import type { StudySubject } from "../../study/types.js";
|
|
6
7
|
export declare function formatConcurrentSharedWorldStudyHuman(result: ConcurrentSharedWorldStudyResult): HumanOutput;
|
|
7
8
|
export declare function formatTerminalStudyHuman(result: TerminalProductStudyResult): HumanOutput;
|
|
8
|
-
|
|
9
|
-
export declare function
|
|
9
|
+
/** `subject` is the study's declared subject, which names it when the result has no URL. */
|
|
10
|
+
export declare function formatScriptedStudyHuman(result: ScriptedBrowserStudyResult, subject: StudySubject): HumanOutput;
|
|
11
|
+
/** `subject` is the study's declared subject, which names it when the result has no URL. */
|
|
12
|
+
export declare function formatCuaStudyHuman(result: CuaActorStudyResult, subject: StudySubject): HumanOutput;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { participantCaption } from "../../run/participant-caption.js";
|
|
2
2
|
import { formatCuaDiagnostics, formatCuaStopCause } from "../../routes/computer-use/diagnostics.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A run's first lines: the command that ran, whether it was a dry run, how it ended, and its route.
|
|
5
|
+
* `runOk` is the run's own ok, which automaticAnalysisEnvelope keeps when an analysis that did not
|
|
6
|
+
* complete turns `ok` false. That run finished, and the line says so.
|
|
7
|
+
*/
|
|
4
8
|
function runHeader(result, route) {
|
|
5
9
|
const kind = result.dryRun === true ? "dry run" : result.dryRun === false ? "live run" : "run";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const ending = result.ok
|
|
11
|
+
? "finished"
|
|
12
|
+
: result.runOk === true
|
|
13
|
+
? "finished; the analysis did not complete"
|
|
14
|
+
: "failed";
|
|
15
|
+
return [`humanish run ${result.studyId}: ${kind} ${ending}`, `route: ${route}`];
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* A run's human output: its lines on stdout and, when it failed with an error, that error on
|
|
@@ -31,6 +37,18 @@ function sandboxIdsLine(result) {
|
|
|
31
37
|
? [`sandbox ids: .humanish/runs/${result.runId}/sandbox-receipts.ndjson`]
|
|
32
38
|
: [];
|
|
33
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* How a run's output names its subject: by the URL its participants open. A product studied at a
|
|
42
|
+
* desktop has no URL, so it is named with its source, as in `humanish (desktop-cli)`. A refusal
|
|
43
|
+
* made before the route resolved a URL has none either, so it names the URL the study declares.
|
|
44
|
+
*/
|
|
45
|
+
function subjectName(appUrl, subject) {
|
|
46
|
+
if (appUrl !== "")
|
|
47
|
+
return appUrl;
|
|
48
|
+
if (subject.product !== undefined)
|
|
49
|
+
return `${subject.product.name} (${subject.source})`;
|
|
50
|
+
return subject.appUrl ?? subject.serve?.url ?? subject.source;
|
|
51
|
+
}
|
|
34
52
|
/** A participant's status in words: a dry run's placeholder status says that nothing ran live. */
|
|
35
53
|
function participantStatus(status) {
|
|
36
54
|
return status === "contract_proof_only" ? "dry run, nothing ran live" : status;
|
|
@@ -83,12 +101,13 @@ export function formatTerminalStudyHuman(result) {
|
|
|
83
101
|
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
84
102
|
]);
|
|
85
103
|
}
|
|
86
|
-
|
|
104
|
+
/** `subject` is the study's declared subject, which names it when the result has no URL. */
|
|
105
|
+
export function formatScriptedStudyHuman(result, subject) {
|
|
87
106
|
return withError(result.error, [
|
|
88
107
|
...runHeader(result, "scripted"),
|
|
89
108
|
`run: ${result.runId}`,
|
|
90
109
|
`actor: ${result.actor}`,
|
|
91
|
-
`subject: ${result.appUrl}`,
|
|
110
|
+
`subject: ${subjectName(result.appUrl, subject)}`,
|
|
92
111
|
...(result.scenario
|
|
93
112
|
? [
|
|
94
113
|
`scenario: ${result.scenario.id} @ ${result.scenario.sourceDigest.slice(0, 12)} (${result.scenario.source}, ${result.scenario.steps} step${result.scenario.steps === 1 ? "" : "s"})`,
|
|
@@ -108,12 +127,13 @@ export function formatScriptedStudyHuman(result) {
|
|
|
108
127
|
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
109
128
|
]);
|
|
110
129
|
}
|
|
111
|
-
|
|
130
|
+
/** `subject` is the study's declared subject, which names it when the result has no URL. */
|
|
131
|
+
export function formatCuaStudyHuman(result, subject) {
|
|
112
132
|
return withError(result.error, [
|
|
113
133
|
...runHeader(result, "computer-use"),
|
|
114
134
|
`run: ${result.runId}`,
|
|
115
135
|
`actor: ${result.actor}`,
|
|
116
|
-
`subject: ${result.appUrl}`,
|
|
136
|
+
`subject: ${subjectName(result.appUrl, subject)}`,
|
|
117
137
|
...(result.subject?.source === "clone"
|
|
118
138
|
? [
|
|
119
139
|
`repo: ${result.subject.repo}${result.subject.commit ? `@${result.subject.commit.slice(0, 12)}` : ""}${result.subject.envNames && result.subject.envNames.length > 0 ? ` env=[${result.subject.envNames.join(", ")}]` : ""}`,
|