humanish 0.110.1 → 0.110.2
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/ARCHITECTURE.md +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +8 -7
- package/dist/actors/local-agent/cli.d.ts +6 -0
- package/dist/actors/local-agent/cli.js +8 -0
- package/dist/actors/local-agent/readiness.js +2 -1
- package/dist/cli/commands/observe.js +15 -2
- package/dist/cli/commands/run-signals.js +13 -4
- package/dist/cli/commands/runs.js +36 -13
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +7 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +4 -2
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.js +9 -6
- package/dist/cli/findings.js +7 -6
- package/dist/cli/first-run-path.d.ts +27 -7
- package/dist/cli/first-run-path.js +112 -75
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +3 -1
- package/dist/cli/io.js +6 -14
- package/dist/cli/observer-follow.d.ts +20 -0
- package/dist/cli/observer-follow.js +36 -1
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +123 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.js +3 -2
- package/dist/observer/render.js +2 -1
- package/dist/observer/run-routes.js +8 -1
- package/dist/observer/serve.js +2 -1
- package/dist/routes/computer-use/plan.js +5 -0
- package/dist/routes/shared-world/plan.js +5 -1
- package/dist/routes/terminal/bundle.js +2 -1
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- package/dist/routes/terminal/plan.js +8 -0
- package/dist/routes/terminal/route.js +5 -4
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/run/dry-run.js +2 -1
- package/dist/run/judge.js +2 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -0
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +34 -2
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/sandbox-creates.js +5 -3
- package/dist/run/sandbox-receipts.d.ts +6 -5
- package/dist/run/sandbox-receipts.js +12 -7
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.js +0 -95
- package/dist/study/init.js +31 -34
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +12 -0
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- package/dist/substrates/e2b/sandbox.js +33 -15
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/tui/contract.js +5 -4
- package/dist/verify/liveness.js +2 -1
- package/dist/verify/verify.js +2 -1
- package/docs/architecture/terminal-product-route.md +14 -12
- package/docs/contracts/run-bundle.md +20 -4
- package/docs/contracts/schemas.md +6 -5
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +22 -18
package/ARCHITECTURE.md
CHANGED
|
@@ -107,7 +107,7 @@ not.
|
|
|
107
107
|
| `humanish/` | This repo's own studies, personas, scenarios, fixtures and coverage notes | `humanish/studies/first-run.yaml` |
|
|
108
108
|
| `examples/` | Library examples shipped in the npm package: a participant and a scorer | `examples/README.md` |
|
|
109
109
|
| `adapters/` | Adapter fixture sets that `tests/study/adapter-fixtures.test.ts` checks | `adapters/fixtures/README.md` |
|
|
110
|
-
| `bench/` | Benchmark apps with planted defects and
|
|
110
|
+
| `bench/` | Benchmark apps with planted defects, the scored runner behind `pnpm bench` and the September results | `bench/DEFECTS.md` |
|
|
111
111
|
| `fixtures/` | Synthetic apps and cases that tests and scripts copy | `fixtures/minimal-app/README.md` |
|
|
112
112
|
| `skills/` | The companion agent skill that `npx skills add` installs | `skills/humanish/SKILL.md` |
|
|
113
113
|
| `runtime/` | Desktop and browser image recipes | `runtime/browser-guest/README.md` |
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.110.2: Credential URLs refused, terminal key proxied, watch exits for agents (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.110.2 refuses a study URL that carries a user name, password or credential parameter,
|
|
14
|
+
and `humanish verify` catches all 41 secret formats its test lists, where 0.110.1 caught 13. A
|
|
15
|
+
terminal study that declares no `execution.runtimeAuth` keeps the OpenAI key in E2B's host-side
|
|
16
|
+
proxy rule (`openai-egress`) and gives Codex a placeholder. No Observer server hands out
|
|
17
|
+
`sandbox-receipts.ndjson` or `status.json`, and a Ctrl-C during desktop startup no longer writes
|
|
18
|
+
the raw sandbox id outside `sandbox-receipts.ndjson`. For a coding agent, `watch` and `observe`
|
|
19
|
+
print the Observer path and exit when nobody is at a terminal, a rerun of `init` rewrites only its
|
|
20
|
+
own `AGENTS.md` section, and suggested commands use one invocation style.
|
|
21
|
+
|
|
22
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.2)
|
|
23
|
+
|
|
11
24
|
## 0.110.1: Claude participants restricted, sandboxes reclaimed by tag (2026-10-04)
|
|
12
25
|
|
|
13
26
|
humanish 0.110.1 starts a Claude Code participant with Read as its only tool, in a scratch folder,
|
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ npx humanish run first-run
|
|
|
38
38
|
|
|
39
39
|
`init` writes starter studies and personas under `humanish/`, adds `.humanish/` to
|
|
40
40
|
`.gitignore`, and adds `humanish:*` scripts to `package.json` and a humanish section to
|
|
41
|
-
`AGENTS.md
|
|
41
|
+
`AGENTS.md`, and lists each file it created or changed. It never overwrites an existing study
|
|
42
|
+
file; a rerun replaces only its own `AGENTS.md` section. `run first-run` is a dry run: four
|
|
42
43
|
synthetic participants, with no browser, model, key or spend. It prints:
|
|
43
44
|
|
|
44
45
|
```text
|
|
@@ -357,12 +358,12 @@ never reads the subject app's own `.env` files.
|
|
|
357
358
|
**What leaves your machine.** [Trust boundaries](https://humanish.dev/docs/trust-boundaries)
|
|
358
359
|
has the providers' retention terms.
|
|
359
360
|
|
|
360
|
-
| Party | What it receives
|
|
361
|
-
| -------------------- |
|
|
362
|
-
| OpenAI | Each step's screenshot, the persona, the mission and the actions so far; after a run, selected text and captures for analysis; for an external-public shared-world study, the host participant's screen while its lobby code is read; for a terminal study, the agent's requests from the E2B sandbox
|
|
363
|
-
| Codex or Claude Code | The same screenshots and prompts, through your signed-in agent and its provider
|
|
364
|
-
| E2B | A desktop or shell running your app and everything on its screen; the repository a `clone` subject names; your working tree for a `local-tree` subject, minus gitignored files, `.env*` and other secret-shaped files; the credentials you declare for the subject app; for a terminal study
|
|
365
|
-
| PostHog | Telemetry events, described [below](#check-what-telemetry-sends)
|
|
361
|
+
| Party | What it receives | When |
|
|
362
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
363
|
+
| OpenAI | Each step's screenshot, the persona, the mission and the actions so far; after a run, selected text and captures for analysis; for an external-public shared-world study, the host participant's screen while its lobby code is read; for a terminal study, the agent's requests from the E2B sandbox | `openai-computer-use` participants; automatic or `humanish analyze` runs; external-public shared-world studies; terminal studies |
|
|
364
|
+
| Codex or Claude Code | The same screenshots and prompts, through your signed-in agent and its provider | `local-agent` participants, local browser studies, the Codex analyst |
|
|
365
|
+
| E2B | A desktop or shell running your app and everything on its screen; the repository a `clone` subject names; your working tree for a `local-tree` subject, minus gitignored files, `.env*` and other secret-shaped files; the credentials you declare for the subject app; for a terminal study with `execution.runtimeAuth: openai-env`, your OpenAI key | Every hosted study |
|
|
366
|
+
| PostHog | Telemetry events, described [below](#check-what-telemetry-sends) | By default |
|
|
366
367
|
|
|
367
368
|
## Find the main commands
|
|
368
369
|
|
|
@@ -123,6 +123,12 @@ export declare function detectLocalAgents(options?: DetectLocalAgentsOptions): P
|
|
|
123
123
|
export declare const NO_LOCAL_AGENT_MESSAGE = "no local coding agent found. openai-computer-use needs OPENAI_API_KEY; local-agent needs Codex or Claude Code installed and signed in. Hosted desktops also need E2B_API_KEY.";
|
|
124
124
|
/** Whether a signed-in agent can still not run participants here: an old Claude Code. */
|
|
125
125
|
export declare function localAgentVersionRefusal(agent: DetectedLocalAgent): string | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Whether an installed agent can run participants here: it reports signed in under the
|
|
128
|
+
* participant's environment, and a Claude Code is at or above the floor. Doctor's rows decide their
|
|
129
|
+
* status from this, so a row whose message says the agent cannot run participants is never "ok".
|
|
130
|
+
*/
|
|
131
|
+
export declare function localAgentReady(agent: DetectedLocalAgent): boolean;
|
|
126
132
|
/** Doctor's row for one installed agent, in the register the other rows use. */
|
|
127
133
|
export declare function localAgentDoctorMessage(agent: DetectedLocalAgent): string;
|
|
128
134
|
export {};
|
|
@@ -474,6 +474,14 @@ export function localAgentVersionRefusal(agent) {
|
|
|
474
474
|
? claudeCodeVersionMessage(agent.version)
|
|
475
475
|
: undefined;
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Whether an installed agent can run participants here: it reports signed in under the
|
|
479
|
+
* participant's environment, and a Claude Code is at or above the floor. Doctor's rows decide their
|
|
480
|
+
* status from this, so a row whose message says the agent cannot run participants is never "ok".
|
|
481
|
+
*/
|
|
482
|
+
export function localAgentReady(agent) {
|
|
483
|
+
return agent.authStatus === "authenticated" && localAgentVersionRefusal(agent) === undefined;
|
|
484
|
+
}
|
|
477
485
|
/** Doctor's row for one installed agent, in the register the other rows use. */
|
|
478
486
|
export function localAgentDoctorMessage(agent) {
|
|
479
487
|
const outdated = localAgentVersionRefusal(agent);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// error code.
|
|
6
6
|
import { describeCodexCliAdmission } from "../codex/codex-admission.js";
|
|
7
7
|
import { checkHostedCodexCompatibility, detectLocalAgents, localAgentVersionRefusal, } from "./cli.js";
|
|
8
|
+
import { cli } from "../../cli/invocation.js";
|
|
8
9
|
export async function localAgentRefusal(args) {
|
|
9
10
|
const { agent, env, caps } = args;
|
|
10
11
|
const available = await detectLocalAgents({ env, only: agent });
|
|
@@ -38,7 +39,7 @@ export async function localAgentRefusal(args) {
|
|
|
38
39
|
kind: "unsupported",
|
|
39
40
|
message: compatibility === "unsupported_platform"
|
|
40
41
|
? `Hosted Codex participants require Linux or macOS on x64 or arm64. This host is ${process.platform}/${process.arch}; no desktop was launched.`
|
|
41
|
-
: `Hosted Codex participants need a Codex CLI release humanish runs: ${describeCodexCliAdmission()}. Run
|
|
42
|
+
: `Hosted Codex participants need a Codex CLI release humanish runs: ${describeCodexCliAdmission()}. Run \`${cli("doctor")}\` for the release it found and the command that replaces it; no desktop was launched.`,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
if (chosen.billing === "account-unknown" &&
|
|
@@ -4,7 +4,7 @@ import { SERVE_SCHEMA, serveObserverLibrary } from "../../observer/serve.js";
|
|
|
4
4
|
import { startExposedObserver, validateExposure } from "../../observer/exposure.js";
|
|
5
5
|
import { ServeTunnelError } from "../../observer/tunnel.js";
|
|
6
6
|
import { collectRepeated, CWD_OPTION_DESCRIPTION, formatRunHuman, freePortOption, JSON_OPTION_DESCRIPTION, parseObserverPort, RUN_OPTION_DESCRIPTION, wantsJson, writeResult, } from "../io.js";
|
|
7
|
-
import { exitCodeForSignal, formatObserverHuman, } from "../observer-follow.js";
|
|
7
|
+
import { exitCodeForSignal, formatObserverHuman, personAtTerminal, unattendedObserverWarning, } from "../observer-follow.js";
|
|
8
8
|
export function registerObserveCommand(parent, io) {
|
|
9
9
|
addLibraryOptions(parent
|
|
10
10
|
.command("observe")
|
|
@@ -16,6 +16,7 @@ export function registerObserveCommand(parent, io) {
|
|
|
16
16
|
.option("--cwd <path>", CWD_OPTION_DESCRIPTION, ".")
|
|
17
17
|
.option("--open", "Open the Observer in the default browser.")
|
|
18
18
|
.option("--no-open", "Serve without opening a browser.")
|
|
19
|
+
.option("--serve", "Serve one run until Ctrl-C even without an interactive terminal; by default an agent's shell or a pipe gets the Observer path and an exit.")
|
|
19
20
|
.option("--json", JSON_OPTION_DESCRIPTION))
|
|
20
21
|
.addHelpText("after", [
|
|
21
22
|
"",
|
|
@@ -32,6 +33,8 @@ export function registerObserveCommand(parent, io) {
|
|
|
32
33
|
"The server binds 127.0.0.1 only. One run exposes just that run's bundle directory; --all",
|
|
33
34
|
"serves the run library. It stays attached until Ctrl-C; file:// security policy and live",
|
|
34
35
|
"refresh are why loopback http is preferred over opening the index.html path directly.",
|
|
36
|
+
"Without an interactive terminal, as in an agent's shell or a pipe, observe for one run",
|
|
37
|
+
"prints the Observer path and exits; --serve keeps it serving.",
|
|
35
38
|
"",
|
|
36
39
|
"--safe, --expose, --tunnel, --tunnel-domain, --oauth, --allow-email, --allow-domain and",
|
|
37
40
|
"--public-url need --all. Exposure only ever happens through an authenticated edge (ngrok",
|
|
@@ -119,9 +122,19 @@ async function observeRun(io, options, command) {
|
|
|
119
122
|
io.setExitCode(2);
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
125
|
+
const wantsMachine = wantsJson(command);
|
|
126
|
+
if (options.serve !== true && !personAtTerminal()) {
|
|
127
|
+
// Nobody can press Ctrl-C here, so a server would wait forever: print the path and exit.
|
|
128
|
+
const result = {
|
|
129
|
+
...rendered,
|
|
130
|
+
warnings: [...rendered.warnings, unattendedObserverWarning("observe")],
|
|
131
|
+
};
|
|
132
|
+
writeResult(command, io, result, formatObserverHuman);
|
|
133
|
+
io.setExitCode(0);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
122
136
|
// Reuse the contained current-data projection, scoped to this run. A raw static
|
|
123
137
|
// server would miss runtime status and could replay stored iframe grants.
|
|
124
|
-
const wantsMachine = wantsJson(command);
|
|
125
138
|
const shouldOpen = options.open === false
|
|
126
139
|
? false
|
|
127
140
|
: options.open === true
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// hangup signal prints one line saying humanish is stopping, refuses any further sandbox create,
|
|
3
3
|
// marks each unfinished run in this process interrupted, reclaims its sandboxes (by receipt, by
|
|
4
4
|
// the ids its in-flight creates report, and by its E2B tags), runs the registered shutdown
|
|
5
|
-
// cleanups (watch's Observer server and tunnel)
|
|
5
|
+
// cleanups (watch's Observer server and tunnel), sweeps the interrupted runs' files of their raw
|
|
6
|
+
// sandbox ids as Run.finish does for a finished run, and exits 128+n, all within one deadline. A
|
|
6
7
|
// second signal prints the reclaim command for each run not yet reclaimed and exits at once.
|
|
7
8
|
// Routes take no abort signal, so the bundle is not finished, but the record ends at once and no
|
|
8
9
|
// sandbox the run created waits for its timeout. Analysis has its own cancel handlers
|
|
@@ -12,10 +13,12 @@ import { activeRuns } from "../../run/active-runs.js";
|
|
|
12
13
|
import { LATEST_RUN_ALIAS } from "../../run/locate.js";
|
|
13
14
|
import { reclaimPinnedRunSandboxes, } from "../../run/reclaim.js";
|
|
14
15
|
import { stopSandboxCreates } from "../../run/sandbox-creates.js";
|
|
16
|
+
import { scrubRunSandboxIds } from "../../run/sandbox-ids.js";
|
|
15
17
|
import { SANDBOX_RECEIPTS_ARTIFACT } from "../../run/sandbox-receipts.js";
|
|
16
18
|
import { plural } from "../../run/text.js";
|
|
17
19
|
import { shellArg } from "../../substrates/shell.js";
|
|
18
20
|
import { exitCodeForSignal } from "../observer-follow.js";
|
|
21
|
+
import { cli } from "../invocation.js";
|
|
19
22
|
/** How long the handler waits for interrupt, reclaim and cleanups before it exits anyway. */
|
|
20
23
|
const INTERRUPT_RECLAIM_DEADLINE_MS = 10_000;
|
|
21
24
|
/** Each kill's own request timeout, inside the deadline. Kills run concurrently. */
|
|
@@ -125,7 +128,13 @@ async function stopActiveRuns(io, signal, stopping, options) {
|
|
|
125
128
|
stop.report = reclaimSummary(stop.run, reclaimed);
|
|
126
129
|
}),
|
|
127
130
|
...[...shutdownCleanups].map((cleanup) => cleanup().catch(() => undefined)),
|
|
128
|
-
])
|
|
131
|
+
]).then(() =>
|
|
132
|
+
// Last, so it also covers what the route recorded while reclaim ran, such as a desktop
|
|
133
|
+
// startup error that quotes the sandbox the kill cut short. A run stopped by the deadline is
|
|
134
|
+
// not swept; verify grades a file that names a receipt's id local_only.
|
|
135
|
+
Promise.all(stopping
|
|
136
|
+
.filter((stop) => stop.report !== null)
|
|
137
|
+
.map((stop) => scrubRunSandboxIds(stop.run.paths).catch(() => undefined))));
|
|
129
138
|
const finished = await withDeadline(work, deadlineMs, options.deadline);
|
|
130
139
|
for (const stop of stopping) {
|
|
131
140
|
if (stop.report === null)
|
|
@@ -159,9 +168,9 @@ function withDeadline(work, ms, wait) {
|
|
|
159
168
|
*/
|
|
160
169
|
function recovery(run) {
|
|
161
170
|
if (run.runId === LATEST_RUN_ALIAS)
|
|
162
|
-
return `its id is \`${LATEST_RUN_ALIAS}\`, which
|
|
171
|
+
return `its id is \`${LATEST_RUN_ALIAS}\`, which \`${cli("reclaim --run")}\` reads as the newest run, so kill the ids in ${path.join(run.paths.relativeRunRoot, SANDBOX_RECEIPTS_ARTIFACT)} by hand`;
|
|
163
172
|
const cwd = path.resolve(run.cwd) === process.cwd() ? "" : ` --cwd ${shellArg(run.cwd)}`;
|
|
164
|
-
return `run
|
|
173
|
+
return `run \`${cli(`reclaim --run ${shellArg(run.runId)}${cwd}`)}\``;
|
|
165
174
|
}
|
|
166
175
|
function reclaimSummary(run, reclaimed) {
|
|
167
176
|
if (reclaimed === undefined)
|
|
@@ -11,6 +11,7 @@ import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION,
|
|
|
11
11
|
import { plural } from "../../run/text.js";
|
|
12
12
|
import { warnAndQueue } from "../deprecations.js";
|
|
13
13
|
import { formatFindings, readRunFindings } from "../findings.js";
|
|
14
|
+
import { cli } from "../invocation.js";
|
|
14
15
|
export function registerRunCommand(parent, io) {
|
|
15
16
|
addRunOptions(parent
|
|
16
17
|
.command("run")
|
|
@@ -162,7 +163,7 @@ export function registerRuntimeCommands(parent, io) {
|
|
|
162
163
|
catch (error) {
|
|
163
164
|
const message = error instanceof Error
|
|
164
165
|
? error.message
|
|
165
|
-
:
|
|
166
|
+
: `Local runtime setup failed. Run ${cli("runtime setup")} to retry.`;
|
|
166
167
|
const result = {
|
|
167
168
|
schema: "humanish.runtime-result.v1",
|
|
168
169
|
ok: false,
|
|
@@ -208,7 +209,7 @@ async function runReclaimCommand(io, command, options) {
|
|
|
208
209
|
/** The command that finishes what a reclaim left open, with --cwd when it was given. */
|
|
209
210
|
function reclaimCommand(result, cwd, preflight) {
|
|
210
211
|
const target = preflight ? "--preflight" : `--run ${shellArg(result.runId)}`;
|
|
211
|
-
return `
|
|
212
|
+
return cli(`reclaim ${target}${cwd === "." ? "" : ` --cwd ${shellArg(cwd)}`}`);
|
|
212
213
|
}
|
|
213
214
|
function formatReclaimHuman(result, cwd, preflight) {
|
|
214
215
|
const lines = [];
|
|
@@ -216,7 +217,9 @@ function formatReclaimHuman(result, cwd, preflight) {
|
|
|
216
217
|
const search = result.tagSearch.status === "done"
|
|
217
218
|
? `E2B lists ${plural(result.tagSearch.found, "more sandbox", "more sandboxes")} tagged with it`
|
|
218
219
|
: `E2B tag search ${result.tagSearch.status}`;
|
|
219
|
-
lines.push(
|
|
220
|
+
lines.push(result.reason === "dry-run"
|
|
221
|
+
? `${check ? "Reclaim check" : "Reclaim"} ${result.runId}: ${result.state}. It was a dry run, which creates no sandboxes, so E2B was not contacted.`
|
|
222
|
+
: `${check ? "Reclaim check" : "Reclaim"} ${result.runId}: ${result.state}. ${plural(result.receiptCount, "sandbox receipt")}; ${search}.`);
|
|
220
223
|
for (const outcome of result.outcomes) {
|
|
221
224
|
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}, from ${outcome.source}): ${outcome.state}${outcome.detail ? `: ${outcome.detail}` : ""}`);
|
|
222
225
|
}
|
|
@@ -229,17 +232,41 @@ function formatReclaimHuman(result, cwd, preflight) {
|
|
|
229
232
|
const stdout = `${lines.join("\n")}\n`;
|
|
230
233
|
return result.error === undefined ? stdout : { stdout, error: result.error };
|
|
231
234
|
}
|
|
235
|
+
const NOTHING_TESTED = "no product behavior was tested";
|
|
232
236
|
const REVIEW_VERDICTS = {
|
|
233
237
|
// A dry run, or a live run with no participant result; review's result does not carry the mode.
|
|
234
|
-
contract_proof_only:
|
|
238
|
+
contract_proof_only: `no verdict; ${NOTHING_TESTED}`,
|
|
235
239
|
pass: "pass",
|
|
236
240
|
fail: "fail",
|
|
237
241
|
blocked: "blocked",
|
|
238
242
|
timed_out: "timed out",
|
|
239
243
|
};
|
|
244
|
+
/** The state runDisplay gives a finished run whose own ok agrees with its verdict. */
|
|
245
|
+
const VERDICT_STATES = {
|
|
246
|
+
pass: "passed",
|
|
247
|
+
fail: "failed",
|
|
248
|
+
blocked: "blocked",
|
|
249
|
+
timed_out: "timed_out",
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Review's headline: the run's display label, as `humanish runs`, review.md and the Observer show
|
|
253
|
+
* it. A verdict that names another state follows in parentheses, so an interrupted run whose last
|
|
254
|
+
* flush judged its participants `fail` reads "interrupted (verdict fail)". A run with no display,
|
|
255
|
+
* which verify does not pass, falls back to the verdict.
|
|
256
|
+
*/
|
|
257
|
+
function reviewHeadline(verdict, display) {
|
|
258
|
+
if (display === undefined)
|
|
259
|
+
return REVIEW_VERDICTS[verdict];
|
|
260
|
+
if (display.state === "no_verdict" || display.state === "dry_run")
|
|
261
|
+
return `${display.label}; ${NOTHING_TESTED}`;
|
|
262
|
+
const judged = VERDICT_STATES[verdict];
|
|
263
|
+
return judged === undefined || judged === display.state
|
|
264
|
+
? display.label
|
|
265
|
+
: `${display.label} (verdict ${REVIEW_VERDICTS[verdict]})`;
|
|
266
|
+
}
|
|
240
267
|
/**
|
|
241
|
-
* A run's review:
|
|
242
|
-
*
|
|
268
|
+
* A run's review: how it ended (runDisplay) and why it failed when it says, its summary and gaps,
|
|
269
|
+
* its analysis findings, and where review.json is.
|
|
243
270
|
*/
|
|
244
271
|
function formatReviewHuman(result) {
|
|
245
272
|
if (!("verdict" in result)) {
|
|
@@ -252,18 +279,14 @@ function formatReviewHuman(result) {
|
|
|
252
279
|
? {
|
|
253
280
|
error: {
|
|
254
281
|
...error,
|
|
255
|
-
message: `${error.message}
|
|
282
|
+
message: `${error.message} ${cli(`verify --run ${result.run}`)} shows why.`,
|
|
256
283
|
},
|
|
257
284
|
}
|
|
258
285
|
: humanError(error);
|
|
259
286
|
}
|
|
260
|
-
const verdict = REVIEW_VERDICTS[result.verdict];
|
|
261
287
|
const display = result.display;
|
|
262
|
-
const headline = result.verdict === "pass" && display !== undefined && display.state !== "passed"
|
|
263
|
-
? `${display.label} (verdict pass)`
|
|
264
|
-
: verdict;
|
|
265
288
|
return ([
|
|
266
|
-
`humanish review ${result.runId}: ${
|
|
289
|
+
`humanish review ${result.runId}: ${reviewHeadline(result.verdict, display)}`,
|
|
267
290
|
...(display?.reason === undefined ? [] : [`why: ${display.reason}`]),
|
|
268
291
|
"",
|
|
269
292
|
result.summary,
|
|
@@ -311,7 +334,7 @@ function formatVerifyHuman(result) {
|
|
|
311
334
|
lines.push(...shareSafetyLines(result), ...result.warnings.map((warning) => `warning: ${warning}`));
|
|
312
335
|
const cwdFlag = result.cwd === process.cwd() ? "" : ` --cwd ${result.cwd}`;
|
|
313
336
|
if (!result.ok)
|
|
314
|
-
lines.push(`every check:
|
|
337
|
+
lines.push(`every check: ${cli(`verify --run ${runId}${cwdFlag} --verbose`)}`);
|
|
315
338
|
return `${lines.join("\n")}\n`;
|
|
316
339
|
}
|
|
317
340
|
function formatVerifyVerbose(result) {
|
|
@@ -6,6 +6,7 @@ import { buildPayload, disabledByEnvironment, readTelemetryState, telemetryState
|
|
|
6
6
|
import { doctor } from "../doctor.js";
|
|
7
7
|
import { applyEnvFileOption, CLI_VERSION, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, markInvocationEnvelopeWritten, wantsJson, writeResult, formatCliError, } from "../io.js";
|
|
8
8
|
import { plural } from "../../run/text.js";
|
|
9
|
+
import { cli } from "../invocation.js";
|
|
9
10
|
export function registerInitCommand(parent, io) {
|
|
10
11
|
parent
|
|
11
12
|
.command("init")
|
|
@@ -283,7 +284,7 @@ export function registerKeysCommand(parent, io) {
|
|
|
283
284
|
store: storePath,
|
|
284
285
|
names,
|
|
285
286
|
message: names.length === 0
|
|
286
|
-
?
|
|
287
|
+
? `The store is empty. Add a key with \`${cli("keys set <vendor>")}\`.`
|
|
287
288
|
: `${plural(names.length, "key name")} stored. Values are never printed.`,
|
|
288
289
|
};
|
|
289
290
|
writeResult(command, io, result, formatKeysHuman);
|
|
@@ -304,19 +305,27 @@ function formatInitHuman(result) {
|
|
|
304
305
|
const title = result.ok ? `humanish init ${result.mode}` : `humanish init ${result.mode} blocked`;
|
|
305
306
|
const lines = [title, `cwd: ${result.cwd}`];
|
|
306
307
|
if (result.ok && result.mode === "applied") {
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
308
|
+
// Name each file init wrote, so the person or agent who ran it can review exactly those
|
|
309
|
+
// files; directories and preserved files are counts. JSON always carries every change.
|
|
310
|
+
const files = (action) => result.changes.filter((change) => change.action === action && change.target !== "runtime");
|
|
311
|
+
const created = files("create");
|
|
312
|
+
const updated = files("update");
|
|
313
|
+
const preserved = result.changes.filter((change) => change.action === "skip").length;
|
|
314
|
+
const directories = result.changes.filter((change) => change.action === "mkdir").length;
|
|
315
|
+
lines.push("");
|
|
316
|
+
if (created.length > 0)
|
|
317
|
+
lines.push("created:", ...created.map((change) => ` ${change.path}`));
|
|
318
|
+
if (updated.length > 0)
|
|
319
|
+
lines.push("updated:", ...updated.map((change) => ` ${change.path} (${change.reason})`));
|
|
320
|
+
if (created.length === 0 && updated.length === 0)
|
|
321
|
+
lines.push("no files changed");
|
|
322
|
+
if (preserved > 0 || directories > 0)
|
|
323
|
+
lines.push([
|
|
324
|
+
...(preserved > 0 ? [`${plural(preserved, "existing path")} unchanged`] : []),
|
|
325
|
+
...(directories > 0
|
|
326
|
+
? [`${plural(directories, "ignored directory", "ignored directories")} prepared`]
|
|
327
|
+
: []),
|
|
328
|
+
].join(", "));
|
|
320
329
|
}
|
|
321
330
|
else {
|
|
322
331
|
lines.push("", "changes:", ...result.changes.map(formatInitChange));
|
|
@@ -8,7 +8,7 @@ import { ServeTunnelError } from "../../observer/tunnel.js";
|
|
|
8
8
|
import { cliAnalysisOptions } from "./analysis-signals.js";
|
|
9
9
|
import { onRunShutdown } from "./run-signals.js";
|
|
10
10
|
import { parseStudyCount, parseParticipantIds, parseObserverPort, wantsJson, writeResult, } from "../io.js";
|
|
11
|
-
import { exposureRequestFromOptions, followObserver, planObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
11
|
+
import { exposureRequestFromOptions, followDecision, followObserver, planObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
12
12
|
import { writeRunFindings } from "../findings.js";
|
|
13
13
|
import { formatCuaStudyHuman } from "./study-format.js";
|
|
14
14
|
import { resolveRouteShouldOpen } from "./study-route-open.js";
|
|
@@ -87,7 +87,9 @@ function resolveCuaSettings(args) {
|
|
|
87
87
|
}
|
|
88
88
|
const dryRun = resolveStudyDryRun(args.config, args.options.dryRun, true) ?? true;
|
|
89
89
|
const port = parseObserverPort(args.options.port ?? "0");
|
|
90
|
-
const wantsFollow = args.mode === "watch" &&
|
|
90
|
+
const wantsFollow = args.mode === "watch" &&
|
|
91
|
+
dryRun !== true &&
|
|
92
|
+
followDecision({ wantsMachine, detach: args.options.detach, serve: args.options.serve }).follow;
|
|
91
93
|
if (port === null) {
|
|
92
94
|
refuseCua(args, dryRun, "HUMANISH_WATCH_OPTION_CONFLICT", "--port must be an integer between 0 and 65535.");
|
|
93
95
|
return undefined;
|
|
@@ -100,7 +102,8 @@ function prepareCuaWatch(args, settings) {
|
|
|
100
102
|
// one surface that serves runtime E2B stream URLs, so it must sit behind edge auth.
|
|
101
103
|
const exposeValidation = validateExposure("watch", exposureRequestFromOptions(args.options), {
|
|
102
104
|
dryRun: settings.dryRun,
|
|
103
|
-
|
|
105
|
+
// A watch that will not follow, because of --detach or no interactive terminal, cannot expose.
|
|
106
|
+
detach: args.options.detach === true || (!settings.wantsMachine && !settings.wantsFollow),
|
|
104
107
|
json: settings.wantsMachine,
|
|
105
108
|
});
|
|
106
109
|
if (!exposeValidation.ok) {
|
|
@@ -117,6 +120,7 @@ function prepareCuaWatch(args, settings) {
|
|
|
117
120
|
port: args.options.port ?? "0",
|
|
118
121
|
open: settings.shouldOpen,
|
|
119
122
|
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
123
|
+
...(args.options.serve === undefined ? {} : { serve: args.options.serve }),
|
|
120
124
|
})
|
|
121
125
|
: undefined;
|
|
122
126
|
if (finishedPlan === null)
|
|
@@ -27,6 +27,7 @@ export function watchFinishedPlan(args, wantsMachine, shouldOpen) {
|
|
|
27
27
|
port: args.options.port ?? "0",
|
|
28
28
|
open: shouldOpen,
|
|
29
29
|
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
30
|
+
...(args.options.serve === undefined ? {} : { serve: args.options.serve }),
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
/** The route's static render opens only when nothing else will open the Observer. */
|
|
@@ -35,6 +35,7 @@ export function previewRouteRun(args) {
|
|
|
35
35
|
io: args.io,
|
|
36
36
|
port: args.options.port ?? "0",
|
|
37
37
|
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
38
|
+
...(args.options.serve === undefined ? {} : { serve: args.options.serve }),
|
|
38
39
|
...(openOverride === undefined ? {} : { open: openOverride }),
|
|
39
40
|
})
|
|
40
41
|
: undefined;
|
|
@@ -5,7 +5,7 @@ import { serveObserver } from "../../observer/render.js";
|
|
|
5
5
|
import { redactText } from "../../evidence/redaction.js";
|
|
6
6
|
import { cliAnalysisOptions } from "./analysis-signals.js";
|
|
7
7
|
import { parseObserverPort, wantsJson, writeResult, } from "../io.js";
|
|
8
|
-
import { followObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
8
|
+
import { followDecision, followObserver, showObserver, staticObserverOpen, withObserverServer, } from "../observer-follow.js";
|
|
9
9
|
import { writeRunFindings } from "../findings.js";
|
|
10
10
|
import { formatConcurrentSharedWorldStudyHuman } from "./study-format.js";
|
|
11
11
|
import { resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
@@ -25,7 +25,9 @@ export function sharedWorldRouteRun(args) {
|
|
|
25
25
|
mode: args.mode,
|
|
26
26
|
wantsMachine,
|
|
27
27
|
});
|
|
28
|
-
const wantsFollow = args.mode === "watch" &&
|
|
28
|
+
const wantsFollow = args.mode === "watch" &&
|
|
29
|
+
dryRun !== true &&
|
|
30
|
+
followDecision({ wantsMachine, detach: args.options.detach, serve: args.options.serve }).follow;
|
|
29
31
|
const port = parseObserverPort(args.options.port ?? "0");
|
|
30
32
|
const failConcurrent = (message, runId) => {
|
|
31
33
|
const result = {
|
|
@@ -5,7 +5,8 @@ import { runDryRun } from "../../run/dry-run.js";
|
|
|
5
5
|
import { runStudyCommand } from "./study-run.js";
|
|
6
6
|
import { addRunOptions, studyOnlyFlags } from "./run-command.js";
|
|
7
7
|
import { applyEnvFileOption, collectRepeated, dotenvPathOf, formatRunHuman, parseObserverPort, parsePositiveInteger, wantsJson, writeResult, } from "../io.js";
|
|
8
|
-
import { followObserver, formatObserverHuman, watchExposeRequested } from "../observer-follow.js";
|
|
8
|
+
import { followDecision, followObserver, formatObserverHuman, unattendedObserverWarning, watchExposeRequested, } from "../observer-follow.js";
|
|
9
|
+
import { cli } from "../invocation.js";
|
|
9
10
|
export function registerWatchCommand(parent, io) {
|
|
10
11
|
// A run's flags come from addRunOptions, the helper `run` uses, so the two cannot drift.
|
|
11
12
|
addRunOptions(parent
|
|
@@ -23,6 +24,7 @@ export function registerWatchCommand(parent, io) {
|
|
|
23
24
|
.option("--allow-email <addr>", "Edge OAuth allow rule: permit this email. Repeatable. Requires --oauth.", collectRepeated)
|
|
24
25
|
.option("--allow-domain <domain>", "Edge OAuth allow rule: permit this domain. Repeatable. Requires --oauth.", collectRepeated)
|
|
25
26
|
.option("--public-url <origin>", "Bring-your-own authed edge (Cloudflare Access/Tailscale/manual). Binds loopback and trusts your edge. Requires --expose.")
|
|
27
|
+
.addOption(new Option("--serve", "Serve the Observer and stay attached until Ctrl-C even without an interactive terminal; by default an agent's shell or a pipe gets the result and an exit.").conflicts(["detach", "json"]))
|
|
26
28
|
// Hidden: watch rejects --safe with a pointer to the library filter and to edge auth, which
|
|
27
29
|
// a bare "unknown option" would lose.
|
|
28
30
|
.addOption(new Option("--safe").hideHelp())
|
|
@@ -37,7 +39,7 @@ export function registerWatchCommand(parent, io) {
|
|
|
37
39
|
"Watch a live computer-use run from your phone through an authenticated tunnel:",
|
|
38
40
|
" humanish watch my-browser-study --expose --tunnel ngrok --oauth google --allow-email you@example.com",
|
|
39
41
|
"",
|
|
40
|
-
"Agent/CI path:",
|
|
42
|
+
"Agent/CI path (without an interactive terminal, watch prints the result and exits):",
|
|
41
43
|
" humanish watch --json --no-open",
|
|
42
44
|
"",
|
|
43
45
|
"Saved runs:",
|
|
@@ -84,7 +86,7 @@ async function handleWatch(io, named, parsed, command) {
|
|
|
84
86
|
if (watchExposeRequested(options)) {
|
|
85
87
|
refuseWatch(command, io, options.cwd, {
|
|
86
88
|
code: "HUMANISH_WATCH_OPTION_CONFLICT",
|
|
87
|
-
message:
|
|
89
|
+
message: `--expose/--tunnel/--oauth apply only to a live computer-use run; to expose finished evidence use \`${cli("observe --all --expose")}\`.`,
|
|
88
90
|
});
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
@@ -107,7 +109,12 @@ async function handleWatch(io, named, parsed, command) {
|
|
|
107
109
|
: options.open === true
|
|
108
110
|
? true
|
|
109
111
|
: !wantsMachine && process.stdout.isTTY === true;
|
|
110
|
-
const
|
|
112
|
+
const decision = followDecision({
|
|
113
|
+
wantsMachine,
|
|
114
|
+
detach: options.detach,
|
|
115
|
+
serve: options.serve,
|
|
116
|
+
});
|
|
117
|
+
const wantsFollow = decision.follow;
|
|
111
118
|
const staticOpen = wantsFollow ? false : shouldOpen;
|
|
112
119
|
const rendered = await renderWatchEvidence(io, command, options, target.requestedParticipantCount, staticOpen);
|
|
113
120
|
if (rendered === undefined)
|
|
@@ -116,6 +123,7 @@ async function handleWatch(io, named, parsed, command) {
|
|
|
116
123
|
follow: wantsFollow,
|
|
117
124
|
open: shouldOpen,
|
|
118
125
|
port: target.port,
|
|
126
|
+
unattended: decision.unattended,
|
|
119
127
|
});
|
|
120
128
|
}
|
|
121
129
|
function refuseWatch(command, io, cwd, refusal) {
|
|
@@ -174,6 +182,8 @@ async function renderWatchEvidence(io, command, options, requestedParticipantCou
|
|
|
174
182
|
async function reportWatch(io, command, rendered, serve) {
|
|
175
183
|
let server = null;
|
|
176
184
|
let result = rendered;
|
|
185
|
+
if (rendered.ok && serve.unattended)
|
|
186
|
+
result = { ...rendered, warnings: [...rendered.warnings, unattendedObserverWarning("watch")] };
|
|
177
187
|
if (rendered.ok && serve.follow) {
|
|
178
188
|
server = await serveObserver(rendered, { open: serve.open, port: serve.port });
|
|
179
189
|
result = {
|
package/dist/cli/doctor.js
CHANGED
|
@@ -5,11 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { probeKeySources, } from "../keys/key-resolution.js";
|
|
6
6
|
import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
|
|
7
7
|
import { nodeSupportsTui, terminalSurfaceMessage, TUI_BUNDLE_URL } from "../tui/contract.js";
|
|
8
|
-
import { detectLocalAgents, localAgentDoctorMessage, NO_LOCAL_AGENT_MESSAGE, } from "../actors/local-agent/cli.js";
|
|
8
|
+
import { detectLocalAgents, localAgentDoctorMessage, localAgentReady, NO_LOCAL_AGENT_MESSAGE, } from "../actors/local-agent/cli.js";
|
|
9
9
|
import { leftoverClaudeTranscripts } from "../actors/local-agent/claude-participant.js";
|
|
10
10
|
import { studySetupChecks, studiesByRequiredKey, } from "../study/doctor.js";
|
|
11
11
|
import { prepareSelectedOutputDirectory, } from "../run/contained-output.js";
|
|
12
12
|
import { implicitProjectDirectoryExists, readImplicitProjectFile, validateCwd, } from "../run/project.js";
|
|
13
|
+
import { cli } from "./invocation.js";
|
|
13
14
|
const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
|
|
14
15
|
/**
|
|
15
16
|
* Version 2.3.1 stopped holding a launched background command's event stream open. Version 2.3.2
|
|
@@ -100,7 +101,7 @@ export async function doctor(cwdInput, options = {}) {
|
|
|
100
101
|
{
|
|
101
102
|
name: "setup route",
|
|
102
103
|
ok: true,
|
|
103
|
-
message:
|
|
104
|
+
message: `General capabilities only. Use ${cli("doctor --study <study>")} for the selected participant's requirements and separate analysis readiness.`,
|
|
104
105
|
},
|
|
105
106
|
]),
|
|
106
107
|
];
|
|
@@ -173,7 +174,7 @@ async function sourceCheck(projectRoot) {
|
|
|
173
174
|
ok,
|
|
174
175
|
message: ok
|
|
175
176
|
? "committed humanish/ source directory is present and safe to read"
|
|
176
|
-
:
|
|
177
|
+
: `no readable humanish/ source directory; run ${cli("init --yes")}`,
|
|
177
178
|
};
|
|
178
179
|
}
|
|
179
180
|
async function runtimeIgnoreCheck(projectRoot) {
|
|
@@ -183,7 +184,7 @@ async function runtimeIgnoreCheck(projectRoot) {
|
|
|
183
184
|
ok,
|
|
184
185
|
message: ok
|
|
185
186
|
? ".gitignore lists .humanish/"
|
|
186
|
-
:
|
|
187
|
+
: `.gitignore does not list .humanish/; run ${cli("init --yes")}`,
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
/**
|
|
@@ -271,7 +272,9 @@ async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
|
|
|
271
272
|
* The operator's own coding agents, one row each, reported as a capability and never a gate: a
|
|
272
273
|
* machine with none is not broken, it just needs a provider key. These rows exist because "go make
|
|
273
274
|
* an API key" is where most people trying humanish stop, and a developer very often already has one
|
|
274
|
-
* of these signed in. An agent that
|
|
275
|
+
* of these signed in. An agent that cannot run participants (not signed in under the participant's
|
|
276
|
+
* environment, sign-in unknown, or a Claude Code below the floor) is a note; `--study` fails on it
|
|
277
|
+
* when the study selects that agent.
|
|
275
278
|
*/
|
|
276
279
|
function localAgentChecks(agents) {
|
|
277
280
|
if (agents.length === 0)
|
|
@@ -279,7 +282,7 @@ function localAgentChecks(agents) {
|
|
|
279
282
|
return agents.map((agent) => ({
|
|
280
283
|
name: `local agent ${agent.bin}`,
|
|
281
284
|
ok: true,
|
|
282
|
-
...(agent
|
|
285
|
+
...(localAgentReady(agent) ? {} : { status: "note" }),
|
|
283
286
|
message: localAgentDoctorMessage(agent),
|
|
284
287
|
}));
|
|
285
288
|
}
|