humanish 0.110.1 → 0.110.3
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 +11 -16
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +1 -1
- package/README.md +8 -7
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- 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/actors/registry.js +1 -1
- package/dist/analysis/automatic-completion.js +8 -0
- package/dist/cli/commands/observe.js +18 -4
- 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/study-run.js +9 -19
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.js +9 -6
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +28 -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 +5 -2
- package/dist/cli/io.js +9 -16
- package/dist/cli/observer-follow.d.ts +24 -0
- package/dist/cli/observer-follow.js +48 -3
- 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 +134 -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/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +6 -1
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +17 -78
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +10 -42
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/plan.js +5 -1
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +24 -63
- package/dist/routes/shared-world/types.d.ts +2 -6
- 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.d.ts +3 -13
- package/dist/routes/terminal/route.js +24 -63
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +2 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- 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/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run-not-found.js +3 -2
- package/dist/run/run.d.ts +6 -1
- package/dist/run/run.js +11 -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/run/status.d.ts +2 -0
- package/dist/run/status.js +5 -0
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -129
- package/dist/study/init.js +32 -35
- 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 +13 -1
- 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/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +26 -24
- package/docs/contracts/run-bundle.md +20 -4
- package/docs/contracts/schemas.md +6 -5
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +4 -2
- package/skills/humanish/SKILL.md +22 -18
package/ARCHITECTURE.md
CHANGED
|
@@ -9,25 +9,20 @@ longer declares that name. [CONTEXT.md](CONTEXT.md) defines the domain terms, an
|
|
|
9
9
|
|
|
10
10
|
The steps follow a live computer-use study on a hosted E2B desktop. Every route shares steps 1, 2, 7
|
|
11
11
|
and 8. Steps 3 to 6 run in the route's admit function and the `run()` it returns, which `runStudyWith`
|
|
12
|
-
reaches through `admitPlan` (`src/run-study.ts`)
|
|
12
|
+
reaches through `admitPlan` (`src/run-study.ts`). Four of the admit functions hand their checks and
|
|
13
|
+
the body they run inside the run scope to `admitRoute` (`src/run/route-shell.ts`). It completes the
|
|
14
|
+
automatic analysis of a refusal, and its `run()` opens the run scope, runs the body and completes
|
|
15
|
+
the analysis of the run:
|
|
13
16
|
|
|
14
|
-
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`)
|
|
15
|
-
`
|
|
16
|
-
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`); its `run()` calls
|
|
21
|
-
`runAdmittedSharedWorldRun` (`src/routes/shared-world/route.ts`).
|
|
17
|
+
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`), with the checks in
|
|
18
|
+
`admitCuaRun` (`src/routes/computer-use/setup.ts`).
|
|
19
|
+
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`), whose checks run inside the run.
|
|
20
|
+
- terminal: `admitTerminalPlan` (`src/routes/terminal/route.ts`), with a live plan's checks in
|
|
21
|
+
`checkLiveTerminalMachine` (`src/routes/terminal/session.ts`).
|
|
22
|
+
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`).
|
|
22
23
|
- preview: `admitPreviewPlan` (`src/routes/preview.ts`); its `run()` calls `runPreviewPlan`
|
|
23
24
|
(`src/routes/preview.ts`), which publishes a fixture bundle.
|
|
24
25
|
|
|
25
|
-
`runComputerUsePlan` (`src/routes/computer-use/route.ts`), `runTerminalPlan`
|
|
26
|
-
(`src/routes/terminal/route.ts`) and `runSharedWorldPlan` (`src/routes/shared-world/route.ts`)
|
|
27
|
-
admit and run in one call. Only tests call them, directly or through the route modules'
|
|
28
|
-
`runCuaActorStudy`, `runTerminalProductStudy` and `runConcurrentSharedWorld`. `runStudyWith` and the CLI do
|
|
29
|
-
not.
|
|
30
|
-
|
|
31
26
|
1. **Parse.** `runStudyCommand` (`src/cli/commands/study-run.ts`) calls `resolveStudyManifest`
|
|
32
27
|
(`src/study/discover.ts`), which calls `parseStudy` (`src/study/config.ts`). It rejects unknown
|
|
33
28
|
keys and the compositions the
|
|
@@ -107,7 +102,7 @@ not.
|
|
|
107
102
|
| `humanish/` | This repo's own studies, personas, scenarios, fixtures and coverage notes | `humanish/studies/first-run.yaml` |
|
|
108
103
|
| `examples/` | Library examples shipped in the npm package: a participant and a scorer | `examples/README.md` |
|
|
109
104
|
| `adapters/` | Adapter fixture sets that `tests/study/adapter-fixtures.test.ts` checks | `adapters/fixtures/README.md` |
|
|
110
|
-
| `bench/` | Benchmark apps with planted defects and
|
|
105
|
+
| `bench/` | Benchmark apps with planted defects, the scored runner behind `pnpm bench` and the September results | `bench/DEFECTS.md` |
|
|
111
106
|
| `fixtures/` | Synthetic apps and cases that tests and scripts copy | `fixtures/minimal-app/README.md` |
|
|
112
107
|
| `skills/` | The companion agent skill that `npx skills add` installs | `skills/humanish/SKILL.md` |
|
|
113
108
|
| `runtime/` | Desktop and browser image recipes | `runtime/browser-guest/README.md` |
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ 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.3: Ctrl-C at the watch prompt, review says analysis is off, E2B docs links kept (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.110.3 runs the `watch` and `observe` shutdown, and prints `watch stopped` or `observe
|
|
14
|
+
stopped`, when a Ctrl-C lands right at their prompt. It starts no automatic analysis for a run a
|
|
15
|
+
signal interrupted. `humanish review` says when a study's `review.analysis: false` is why a live
|
|
16
|
+
run has no findings. `humanish verify` no longer blocks a run whose text holds a URL followed by an
|
|
17
|
+
escaped line break and an `E2B_` variable, and redacted output keeps the E2B docs link that E2B's
|
|
18
|
+
401 names. The `--env-file` alias is now removed in 0.112.0. Inside, the computer-use, scripted,
|
|
19
|
+
terminal and shared-world routes run through one shell, `admitRoute` in `src/run/route-shell.ts`;
|
|
20
|
+
the live smoke ran each route on it.
|
|
21
|
+
|
|
22
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.3)
|
|
23
|
+
|
|
24
|
+
## 0.110.2: Credential URLs refused, terminal key proxied, watch exits for agents (2026-10-05)
|
|
25
|
+
|
|
26
|
+
humanish 0.110.2 refuses a study URL that carries a user name, password or credential parameter,
|
|
27
|
+
and `humanish verify` catches all 41 secret formats its test lists, where 0.110.1 caught 13. A
|
|
28
|
+
terminal study that declares no `execution.runtimeAuth` keeps the OpenAI key in E2B's host-side
|
|
29
|
+
proxy rule (`openai-egress`) and gives Codex a placeholder. No Observer server hands out
|
|
30
|
+
`sandbox-receipts.ndjson` or `status.json`, and a Ctrl-C during desktop startup no longer writes
|
|
31
|
+
the raw sandbox id outside `sandbox-receipts.ndjson`. For a coding agent, `watch` and `observe`
|
|
32
|
+
print the Observer path and exit when nobody is at a terminal, a rerun of `init` rewrites only its
|
|
33
|
+
own `AGENTS.md` section, and suggested commands use one invocation style.
|
|
34
|
+
|
|
35
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.2)
|
|
36
|
+
|
|
11
37
|
## 0.110.1: Claude participants restricted, sandboxes reclaimed by tag (2026-10-04)
|
|
12
38
|
|
|
13
39
|
humanish 0.110.1 starts a Claude Code participant with Read as its only tool, in a scratch folder,
|
package/CONTEXT.md
CHANGED
|
@@ -28,7 +28,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
28
28
|
| computer use | A model drives a participant by reading screenshots of a desktop and acting with mouse and keyboard. `openai-computer-use` and `local-agent` are the computer-use actors. CUA is the short form in code names such as `CuaExecutor` and in the older spellings listed under `cua` below. | `src/routes/computer-use/`, `src/actors/computer-use/` |
|
|
29
29
|
| fan-out | A computer-use run with more than one participant, each on its own desktop, or any rerun. It writes the fan-out bundle shape and passes only when every expected participant passes. One participant without a rerun keeps the single-participant shape. | `buildCuaFanoutBundle` in `src/routes/computer-use/fanout-bundle.ts`; `judgeComputerUseRun` in `src/routes/computer-use/bundle.ts` |
|
|
30
30
|
| route | The execution path for a study: `computer-use`, `shared-world`, `scripted`, `terminal` or `preview`. `preview` writes a dry-run bundle for `this-repo` subjects with no live participant. | `routeOf` and `StudyRoute` in `src/study/plan.ts`; predicates in `src/study/routing.ts`; one folder or file per route in `src/routes/` |
|
|
31
|
-
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's
|
|
31
|
+
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's admit function (`admitComputerUsePlan` and the others) takes the plan. | `StudyPlan` in `src/study/plan-types.ts`; `planStudy` in `src/study/plan.ts`; each route's planner in its `plan.ts` |
|
|
32
32
|
| substrate | Where a participant's desktop or shell runs: a hosted E2B sandbox, or a local Firecracker VM (inside Lima on macOS). | `src/substrates/e2b/`, `src/substrates/local/` |
|
|
33
33
|
| participant desktop | The desktop one computer-use or shared-world participant drives: a substrate plus the route's concerns, such as subject provisioning, comms and the participant's plan. It lives in the route, so `src/substrates/` holds only provider primitives ([ADR 0004](docs/decisions/0004-participant-desktops-stay-in-the-route.md)). | `ParticipantDesktop` in `src/routes/computer-use/participant-desktop.ts`; `createE2BParticipantDesktop` in `src/routes/computer-use/e2b-desktop/desktop.ts`; the local VM's in `src/routes/computer-use/local-vm.ts` |
|
|
34
34
|
| receipt | A line in a run's `sandbox-receipts.ndjson` that records a sandbox's id, provider, the participant or subject it belongs to (`laneId`), and its time limit as soon as the sandbox is created, before any work runs in it. `humanish reclaim` kills sandboxes by these ids after an interrupted run. | `SandboxReceipt` in `src/run/sandbox-receipts.ts`; `reclaimRunSandboxes` in `src/run/reclaim.ts` |
|
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
|
|
|
@@ -63,7 +63,7 @@ export async function runCuaActorSession(options) {
|
|
|
63
63
|
}
|
|
64
64
|
function defaultProvider(openai, strictSpend = false) {
|
|
65
65
|
if (!openai) {
|
|
66
|
-
throw new Error("
|
|
66
|
+
throw new Error("A computer-use session requires either `provider` (injected) or `openai` provider options.");
|
|
67
67
|
}
|
|
68
68
|
return createOpenAiResponsesProvider({
|
|
69
69
|
...openai,
|
|
@@ -72,7 +72,7 @@ function defaultProvider(openai, strictSpend = false) {
|
|
|
72
72
|
}
|
|
73
73
|
function defaultExecutor(desktop, executorOptions) {
|
|
74
74
|
if (!desktop) {
|
|
75
|
-
throw new Error("
|
|
75
|
+
throw new Error("A computer-use session requires either `executor` (injected) or `desktop` to build one.");
|
|
76
76
|
}
|
|
77
77
|
return createE2BDesktopExecutor(desktop, executorOptions ?? {});
|
|
78
78
|
}
|
|
@@ -96,7 +96,7 @@ export function spendLimit(session, estimate, maxUsd) {
|
|
|
96
96
|
// trip.
|
|
97
97
|
export const nonFiniteEstimate = {
|
|
98
98
|
completionReason: "harness_error",
|
|
99
|
-
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one
|
|
99
|
+
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one object with the running token totals. Failing closed instead of running uncapped.",
|
|
100
100
|
};
|
|
101
101
|
/** A study-level stop is a recruiting decision hitting its limit, not this participant's runaway. */
|
|
102
102
|
export function runSpendLimit(reason) {
|
|
@@ -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" &&
|
package/dist/actors/registry.js
CHANGED
|
@@ -74,7 +74,7 @@ export const actorRegistry = {
|
|
|
74
74
|
},
|
|
75
75
|
// The ActorId names the terminal-product dispatch slot; the live route records the concrete
|
|
76
76
|
// provider as "codex". keyPlacement "in-sandbox-command-scoped" is registry-declared and
|
|
77
|
-
// enforced by
|
|
77
|
+
// enforced by the terminal route before it creates a sandbox, which is where this actor runs.
|
|
78
78
|
"codex-exec": {
|
|
79
79
|
id: "codex-exec",
|
|
80
80
|
label: "Codex Exec (autonomous terminal agent, in-sandbox)",
|
|
@@ -25,6 +25,14 @@ export async function completeAutomaticAnalysis(result, finished, config, input,
|
|
|
25
25
|
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE" },
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
// A signal is stopping this process: the run is recorded interrupted, and the process exits
|
|
29
|
+
// once its sandboxes are reclaimed, so no analysis starts.
|
|
30
|
+
if (finished.interrupted)
|
|
31
|
+
return {
|
|
32
|
+
...result,
|
|
33
|
+
...origin,
|
|
34
|
+
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_ACTOR_CANCELLED" },
|
|
35
|
+
};
|
|
28
36
|
const prepared = finished.paths;
|
|
29
37
|
const seams = input?.deps?.analysis;
|
|
30
38
|
const signal = input?.analysisSignal;
|
|
@@ -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
|
|
@@ -151,8 +164,6 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
151
164
|
// Keep the JSON envelope on stdout clean: route attach/stop chatter to stderr
|
|
152
165
|
// for machine output, and to stdout for humans.
|
|
153
166
|
const note = options.json ? io.writeErr : io.writeOut;
|
|
154
|
-
note(`serving: ${server.url}\n`);
|
|
155
|
-
note("serving: press Ctrl-C to stop\n");
|
|
156
167
|
await new Promise((resolveWait) => {
|
|
157
168
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
158
169
|
const handlers = new Map();
|
|
@@ -182,6 +193,9 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
182
193
|
handlers.set(signal, handler);
|
|
183
194
|
process.once(signal, handler);
|
|
184
195
|
}
|
|
196
|
+
// The prompt follows the handlers, as in followObserver.
|
|
197
|
+
note(`serving: ${server.url}\n`);
|
|
198
|
+
note("serving: press Ctrl-C to stop\n");
|
|
185
199
|
});
|
|
186
200
|
}
|
|
187
201
|
async function handleServe(io, options, command) {
|
|
@@ -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 = {
|