humanish 0.0.1 → 0.15.1
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/AGENTS.md +77 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +437 -8
- package/SECURITY.md +34 -0
- package/dist/actor-contract.d.ts +107 -0
- package/dist/actor-contract.js +286 -0
- package/dist/actor-contract.js.map +1 -0
- package/dist/actor-registry.d.ts +73 -0
- package/dist/actor-registry.js +93 -0
- package/dist/actor-registry.js.map +1 -0
- package/dist/adapter-extension.d.ts +54 -0
- package/dist/adapter-extension.js +214 -0
- package/dist/adapter-extension.js.map +1 -0
- package/dist/argv.d.ts +1 -0
- package/dist/argv.js +8 -0
- package/dist/argv.js.map +1 -0
- package/dist/artifact-reference.d.ts +22 -0
- package/dist/artifact-reference.js +47 -0
- package/dist/artifact-reference.js.map +1 -0
- package/dist/browser-evidence-hygiene.d.ts +7 -0
- package/dist/browser-evidence-hygiene.js +40 -0
- package/dist/browser-evidence-hygiene.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +94 -0
- package/dist/claude-agent-sdk.js +363 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-app-server-ui.d.ts +39 -0
- package/dist/codex-app-server-ui.js +357 -0
- package/dist/codex-app-server-ui.js.map +1 -0
- package/dist/codex-app-server.d.ts +144 -0
- package/dist/codex-app-server.js +754 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/command-failure.d.ts +29 -0
- package/dist/command-failure.js +54 -0
- package/dist/command-failure.js.map +1 -0
- package/dist/computer-use-actor.d.ts +52 -0
- package/dist/computer-use-actor.js +48 -0
- package/dist/computer-use-actor.js.map +1 -0
- package/dist/computer-use.d.ts +232 -0
- package/dist/computer-use.js +615 -0
- package/dist/computer-use.js.map +1 -0
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +27 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/concurrent-shared-world-lab.d.ts +115 -0
- package/dist/concurrent-shared-world-lab.js +1094 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -0
- package/dist/core/git-state.d.ts +33 -0
- package/dist/core/git-state.js +277 -0
- package/dist/core/git-state.js.map +1 -0
- package/dist/core/git-workspace.d.ts +31 -0
- package/dist/core/git-workspace.js +447 -0
- package/dist/core/git-workspace.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/run-primitives.d.ts +66 -0
- package/dist/core/run-primitives.js +120 -0
- package/dist/core/run-primitives.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +621 -0
- package/dist/cua-actor-lab.js +2845 -0
- package/dist/cua-actor-lab.js.map +1 -0
- package/dist/device-presets.d.ts +67 -0
- package/dist/device-presets.js +50 -0
- package/dist/device-presets.js.map +1 -0
- package/dist/e2b-desktop-executor.d.ts +101 -0
- package/dist/e2b-desktop-executor.js +309 -0
- package/dist/e2b-desktop-executor.js.map +1 -0
- package/dist/e2b-desktop-launch.d.ts +144 -0
- package/dist/e2b-desktop-launch.js +59 -0
- package/dist/e2b-desktop-launch.js.map +1 -0
- package/dist/e2b-detached.d.ts +53 -0
- package/dist/e2b-detached.js +130 -0
- package/dist/e2b-detached.js.map +1 -0
- package/dist/e2b-terminal-lab.d.ts +345 -0
- package/dist/e2b-terminal-lab.js +1532 -0
- package/dist/e2b-terminal-lab.js.map +1 -0
- package/dist/env-file.d.ts +14 -0
- package/dist/env-file.js +108 -0
- package/dist/env-file.js.map +1 -0
- package/dist/feedback.d.ts +51 -0
- package/dist/feedback.js +351 -0
- package/dist/feedback.js.map +1 -0
- package/dist/image-evidence.d.ts +2 -0
- package/dist/image-evidence.js +33 -0
- package/dist/image-evidence.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/init-templates.d.ts +12 -0
- package/dist/init-templates.js +369 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.d.ts +26 -0
- package/dist/init.js +429 -0
- package/dist/init.js.map +1 -0
- package/dist/lab-app-runner.d.ts +78 -0
- package/dist/lab-app-runner.js +403 -0
- package/dist/lab-app-runner.js.map +1 -0
- package/dist/lab-config.d.ts +583 -0
- package/dist/lab-config.js +1789 -0
- package/dist/lab-config.js.map +1 -0
- package/dist/lab-engine.d.ts +77 -0
- package/dist/lab-engine.js +216 -0
- package/dist/lab-engine.js.map +1 -0
- package/dist/lab-preflight.d.ts +67 -0
- package/dist/lab-preflight.js +385 -0
- package/dist/lab-preflight.js.map +1 -0
- package/dist/labs.d.ts +53 -0
- package/dist/labs.js +382 -0
- package/dist/labs.js.map +1 -0
- package/dist/observer-assets.d.ts +2 -0
- package/dist/observer-assets.js +2630 -0
- package/dist/observer-assets.js.map +1 -0
- package/dist/observer-data.d.ts +63 -0
- package/dist/observer-data.js +145 -0
- package/dist/observer-data.js.map +1 -0
- package/dist/observer-static.d.ts +39 -0
- package/dist/observer-static.js +306 -0
- package/dist/observer-static.js.map +1 -0
- package/dist/observer.d.ts +46 -0
- package/dist/observer.js +550 -0
- package/dist/observer.js.map +1 -0
- package/dist/openai-responses-cu.d.ts +113 -0
- package/dist/openai-responses-cu.js +545 -0
- package/dist/openai-responses-cu.js.map +1 -0
- package/dist/oss-lab.d.ts +52 -0
- package/dist/oss-lab.js +344 -0
- package/dist/oss-lab.js.map +1 -0
- package/dist/oss-meta-lab-scoring.d.ts +15 -0
- package/dist/oss-meta-lab-scoring.js +145 -0
- package/dist/oss-meta-lab-scoring.js.map +1 -0
- package/dist/oss-meta-lab.d.ts +277 -0
- package/dist/oss-meta-lab.js +4529 -0
- package/dist/oss-meta-lab.js.map +1 -0
- package/dist/oss-remote-telemetry.d.ts +77 -0
- package/dist/oss-remote-telemetry.js +403 -0
- package/dist/oss-remote-telemetry.js.map +1 -0
- package/dist/persona.d.ts +35 -0
- package/dist/persona.js +133 -0
- package/dist/persona.js.map +1 -0
- package/dist/pi-agent-core.d.ts +77 -0
- package/dist/pi-agent-core.js +203 -0
- package/dist/pi-agent-core.js.map +1 -0
- package/dist/program.d.ts +54 -0
- package/dist/program.js +2349 -0
- package/dist/program.js.map +1 -0
- package/dist/redaction.d.ts +90 -0
- package/dist/redaction.js +292 -0
- package/dist/redaction.js.map +1 -0
- package/dist/run-paths.d.ts +56 -0
- package/dist/run-paths.js +340 -0
- package/dist/run-paths.js.map +1 -0
- package/dist/run.d.ts +794 -0
- package/dist/run.js +4844 -0
- package/dist/run.js.map +1 -0
- package/dist/scripted-browser-actor.d.ts +297 -0
- package/dist/scripted-browser-actor.js +1399 -0
- package/dist/scripted-browser-actor.js.map +1 -0
- package/dist/scripted-browser-lab.d.ts +107 -0
- package/dist/scripted-browser-lab.js +788 -0
- package/dist/scripted-browser-lab.js.map +1 -0
- package/dist/selected-output-paths.d.ts +49 -0
- package/dist/selected-output-paths.js +354 -0
- package/dist/selected-output-paths.js.map +1 -0
- package/dist/shared-world-lab.d.ts +208 -0
- package/dist/shared-world-lab.js +1049 -0
- package/dist/shared-world-lab.js.map +1 -0
- package/dist/source-archive.d.ts +82 -0
- package/dist/source-archive.js +408 -0
- package/dist/source-archive.js.map +1 -0
- package/dist/stop-conditions.d.ts +26 -0
- package/dist/stop-conditions.js +66 -0
- package/dist/stop-conditions.js.map +1 -0
- package/dist/terminal-agent-actor.d.ts +36 -0
- package/dist/terminal-agent-actor.js +23 -0
- package/dist/terminal-agent-actor.js.map +1 -0
- package/docs/architecture/actor-contract.md +431 -0
- package/docs/architecture/github-feedback-loop.md +189 -0
- package/docs/architecture/local-codex-tui-actor.md +213 -0
- package/docs/architecture/observer.md +118 -0
- package/docs/architecture/oss-lab-poc.md +250 -0
- package/docs/architecture/project-layout.md +166 -0
- package/docs/architecture/state-driven-executor.md +203 -0
- package/docs/architecture/terminal-product-lane.md +148 -0
- package/docs/contracts/adapter-fixtures.md +80 -0
- package/docs/contracts/core.md +71 -0
- package/docs/contracts/feedback.md +145 -0
- package/docs/contracts/policy.md +311 -0
- package/docs/contracts/run-bundle.md +358 -0
- package/docs/contracts/schemas.md +984 -0
- package/docs/goals/current.md +428 -0
- package/docs/principles/invariants-and-defaults.md +135 -0
- package/docs/principles/self-driving-harness.md +129 -0
- package/docs/product/open-source-install-experience.md +237 -0
- package/docs/ramp/README.md +207 -0
- package/docs/release/open-source-readiness.md +208 -0
- package/docs/release/public-readiness-standard.md +205 -0
- package/docs/roadmap/world-class-open-source-v0.md +311 -0
- package/package.json +102 -8
- package/skills/humanish/SKILL.md +244 -0
- package/skills/humanish/agents/openai.yaml +7 -0
- package/index.js +0 -1
package/dist/program.js
ADDED
|
@@ -0,0 +1,2349 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import { startCodexAppServerUi } from "./codex-app-server-ui.js";
|
|
7
|
+
import { loadEnvFile } from "./env-file.js";
|
|
8
|
+
import { redactText } from "./redaction.js";
|
|
9
|
+
import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
|
|
10
|
+
import { runInit } from "./init.js";
|
|
11
|
+
import { inspectLabManifest, listLabManifests, resolveLabManifest } from "./labs.js";
|
|
12
|
+
import { runLabPreflight } from "./lab-preflight.js";
|
|
13
|
+
import { runLab, resolveLabDryRun, selectLabBackend } from "./lab-engine.js";
|
|
14
|
+
import { openTarget, renderObserver, serveObserver } from "./observer.js";
|
|
15
|
+
import { serveObserverStatic } from "./observer-static.js";
|
|
16
|
+
import { DEFAULT_OSS_REPOS, runOssLab } from "./oss-lab.js";
|
|
17
|
+
import { cleanupOssMetaLabSandboxes, cleanupStaleOssMetaLabSandboxes, runOssMetaLab, startOssMetaLabLiveRefresh } from "./oss-meta-lab.js";
|
|
18
|
+
import { cleanupRun, doctor, listRuns, readReview, runDryRun, verifyRun } from "./run.js";
|
|
19
|
+
export const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
|
|
20
|
+
function readCliVersion() {
|
|
21
|
+
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
22
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
23
|
+
return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.0.0";
|
|
24
|
+
}
|
|
25
|
+
const CLI_VERSION = readCliVersion();
|
|
26
|
+
// Shared so the ~20 leaf commands that declare their own --json flag cannot drift
|
|
27
|
+
// from each other in wording.
|
|
28
|
+
const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response.";
|
|
29
|
+
const defaultIo = {
|
|
30
|
+
writeOut: (text) => process.stdout.write(text),
|
|
31
|
+
writeErr: (text) => process.stderr.write(text),
|
|
32
|
+
setExitCode: (code) => {
|
|
33
|
+
process.exitCode = code;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
// Command boundary catch-all (fix set point 1): every leaf command is created
|
|
37
|
+
// through HumanishCommand.createCommand, and HumanishCommand.action wraps the caller's
|
|
38
|
+
// handler so an uncaught throw or promise rejection can never escape as a raw
|
|
39
|
+
// Node crash. On failure it emits the same structured envelope --json commands
|
|
40
|
+
// already promise (schema humanish.cli-response.v1, ok: false, error.code
|
|
41
|
+
// HUMANISH_UNEXPECTED) or a single concise stderr line otherwise, then sets exit
|
|
42
|
+
// code 2 through the existing CliIo.setExitCode seam. This is the single seam:
|
|
43
|
+
// none of the ~20 .action() handlers below need their own try/catch for this.
|
|
44
|
+
class HumanishCommand extends Command {
|
|
45
|
+
cliIo;
|
|
46
|
+
constructor(name, cliIo) {
|
|
47
|
+
super(name);
|
|
48
|
+
this.cliIo = cliIo;
|
|
49
|
+
}
|
|
50
|
+
createCommand(name) {
|
|
51
|
+
return new HumanishCommand(name, this.cliIo);
|
|
52
|
+
}
|
|
53
|
+
action(fn) {
|
|
54
|
+
const cliIo = this.cliIo;
|
|
55
|
+
const wrapped = function (...args) {
|
|
56
|
+
try {
|
|
57
|
+
// Reflect.apply (not fn.apply) sidesteps TS's special CallableFunction
|
|
58
|
+
// overload for functions typed with an explicit `this` parameter, which
|
|
59
|
+
// would otherwise reject the plain `Command` thisArg below.
|
|
60
|
+
const result = Reflect.apply(fn, this, args);
|
|
61
|
+
if (result && typeof result.then === "function") {
|
|
62
|
+
return result.catch((error) => {
|
|
63
|
+
reportUnexpectedActionError(this, cliIo, error);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
reportUnexpectedActionError(this, cliIo, error);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return super.action(wrapped);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Fix set point 1 (general guard): writeResult marks its own invocation's root
|
|
77
|
+
// command the moment it flushes a result to stdout. Keyed by the root Command
|
|
78
|
+
// of the invocation (a fresh object every createProgram() call) rather than a
|
|
79
|
+
// module-level boolean, so tests that construct multiple `createProgram()`
|
|
80
|
+
// instances in the same process never see one instance's writes bleed into
|
|
81
|
+
// another's, and nothing needs explicit cleanup -- the entry is released once
|
|
82
|
+
// that Command tree is garbage collected.
|
|
83
|
+
const invocationEnvelopeWritten = new WeakSet();
|
|
84
|
+
function rootCommandOf(command) {
|
|
85
|
+
let current = command;
|
|
86
|
+
while (current.parent) {
|
|
87
|
+
current = current.parent;
|
|
88
|
+
}
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
function markInvocationEnvelopeWritten(command) {
|
|
92
|
+
invocationEnvelopeWritten.add(rootCommandOf(command));
|
|
93
|
+
}
|
|
94
|
+
function invocationEnvelopeAlreadyWritten(command) {
|
|
95
|
+
return invocationEnvelopeWritten.has(rootCommandOf(command));
|
|
96
|
+
}
|
|
97
|
+
function reportUnexpectedActionError(command, io, error) {
|
|
98
|
+
const message = redactText(error instanceof Error ? error.message : String(error));
|
|
99
|
+
if (wantsJson(command)) {
|
|
100
|
+
if (invocationEnvelopeAlreadyWritten(command)) {
|
|
101
|
+
// Some result already went to stdout for this invocation before the
|
|
102
|
+
// failure landed -- e.g. `codex app-server --keep-open --json` writes its
|
|
103
|
+
// "running" envelope via writeResult, then a later `await` can still
|
|
104
|
+
// reject (codex-app-server-ui.ts's persistState() write can fail on
|
|
105
|
+
// either branch of that command's completion handling). Appending a
|
|
106
|
+
// second JSON document to stdout would break every JSON.parse(stdout)
|
|
107
|
+
// consumer, so this failure goes to stderr instead, same as the non-json
|
|
108
|
+
// branch below.
|
|
109
|
+
io.writeErr(`HUMANISH_UNEXPECTED: ${message}\n`);
|
|
110
|
+
io.setExitCode(2);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const envelope = {
|
|
114
|
+
schema: CLI_RESPONSE_SCHEMA,
|
|
115
|
+
ok: false,
|
|
116
|
+
error: {
|
|
117
|
+
code: "HUMANISH_UNEXPECTED",
|
|
118
|
+
message
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
io.writeOut(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
io.writeErr(`HUMANISH_UNEXPECTED: ${message}\n`);
|
|
125
|
+
}
|
|
126
|
+
io.setExitCode(2);
|
|
127
|
+
}
|
|
128
|
+
export function createProgram(io = {}) {
|
|
129
|
+
const cliIo = { ...defaultIo, ...io };
|
|
130
|
+
const program = new HumanishCommand(undefined, cliIo);
|
|
131
|
+
program
|
|
132
|
+
.name("humanish")
|
|
133
|
+
.description("Open-source-safe persona simulation CLI and proof harness.")
|
|
134
|
+
.version(CLI_VERSION)
|
|
135
|
+
.showHelpAfterError()
|
|
136
|
+
.option("--json", "Print machine-readable JSON responses where supported.")
|
|
137
|
+
.configureOutput({
|
|
138
|
+
writeOut: (text) => cliIo.writeOut(text),
|
|
139
|
+
writeErr: (text) => cliIo.writeErr(text)
|
|
140
|
+
})
|
|
141
|
+
.addHelpText("after", [
|
|
142
|
+
"",
|
|
143
|
+
"Examples:",
|
|
144
|
+
" humanish watch",
|
|
145
|
+
" humanish watch first-run",
|
|
146
|
+
" humanish watch --lab .humanish/labs/local.yaml",
|
|
147
|
+
" humanish watch --run latest --detach",
|
|
148
|
+
" humanish watch --json --no-open",
|
|
149
|
+
" humanish lab list",
|
|
150
|
+
" humanish lab run first-run --json --no-open",
|
|
151
|
+
" humanish verify --run latest --json",
|
|
152
|
+
"",
|
|
153
|
+
"Public-safety boundary:",
|
|
154
|
+
" Humanish must not commit or emit PII, PHI, secrets, keys, raw private transcripts,",
|
|
155
|
+
" private screenshots, or private upstream artifacts."
|
|
156
|
+
].join("\n"));
|
|
157
|
+
registerInitCommand(program, cliIo);
|
|
158
|
+
registerDoctorCommand(program, cliIo);
|
|
159
|
+
registerRunCommand(program, cliIo);
|
|
160
|
+
registerVerifyCommand(program, cliIo);
|
|
161
|
+
registerCleanupCommand(program, cliIo);
|
|
162
|
+
registerReviewCommand(program, cliIo);
|
|
163
|
+
registerRunsCommand(program, cliIo);
|
|
164
|
+
registerWatchCommand(program, cliIo);
|
|
165
|
+
registerObserveCommand(program, cliIo);
|
|
166
|
+
registerCodexCommands(program, cliIo);
|
|
167
|
+
registerLabCommands(program, cliIo);
|
|
168
|
+
registerFeedbackCommands(program, cliIo);
|
|
169
|
+
return program;
|
|
170
|
+
}
|
|
171
|
+
function registerInitCommand(parent, io) {
|
|
172
|
+
parent
|
|
173
|
+
.command("init")
|
|
174
|
+
.description("Set up committed humanish/ source files and ignored .humanish/ runtime state.")
|
|
175
|
+
.summary("Set up humanish/ source and .humanish/ runtime state.")
|
|
176
|
+
.option("--dry-run", "Print planned changes without writing files.")
|
|
177
|
+
.option("--yes", "Apply safe generated changes without prompting.")
|
|
178
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
179
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
180
|
+
.action(async (options, command) => {
|
|
181
|
+
const initOptions = {
|
|
182
|
+
cwd: options.cwd,
|
|
183
|
+
...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
184
|
+
...(options.yes === undefined ? {} : { yes: options.yes })
|
|
185
|
+
};
|
|
186
|
+
const result = await runInit(initOptions);
|
|
187
|
+
if (wantsJson(command)) {
|
|
188
|
+
io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
189
|
+
}
|
|
190
|
+
else if (result.ok) {
|
|
191
|
+
io.writeOut(formatInitHuman(result));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
io.writeErr(formatInitHuman(result));
|
|
195
|
+
}
|
|
196
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function registerDoctorCommand(parent, io) {
|
|
200
|
+
parent
|
|
201
|
+
.command("doctor")
|
|
202
|
+
.description("Explain project readiness and missing Humanish setup.")
|
|
203
|
+
.summary("Explain project readiness and missing setup.")
|
|
204
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
205
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
206
|
+
.action(async (options, command) => {
|
|
207
|
+
const result = await doctor(options.cwd);
|
|
208
|
+
writeResult(command, io, result, formatDoctorHuman);
|
|
209
|
+
// Behavioral change: was exit 1, every other structured command uses 2.
|
|
210
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function registerRunCommand(parent, io) {
|
|
214
|
+
parent
|
|
215
|
+
.command("run")
|
|
216
|
+
.argument("[lab]", "Optional lab id or .yaml path.")
|
|
217
|
+
.description("Run a persona/scenario simulation or synthetic dry-run bundle.")
|
|
218
|
+
.summary("Run a persona/scenario simulation or dry-run bundle.")
|
|
219
|
+
.option("--dry-run", "Generate contract proof without browser, keys, or provider spend.")
|
|
220
|
+
.option("--app-url <url>", "Capture live desktop/mobile browser evidence against a running loopback app URL.")
|
|
221
|
+
.addOption(new Option("--actor <actor>", "Explicit live actor to run.").choices(["codex-tui", "codex-exec", "codex-app-server"]))
|
|
222
|
+
.option("--sims <count>", "Simulation count. Codex exec runs requested lanes with bounded concurrency; Codex TUI supports 1.")
|
|
223
|
+
.option("--timeout-ms <ms>", "Local actor timeout in milliseconds.", String(240_000))
|
|
224
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
225
|
+
.option("--env-file <path>", "Load a local env file for this run without persisting values.")
|
|
226
|
+
.option("--run-id <id>", "Explicit run id for deterministic fixture tests.")
|
|
227
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
228
|
+
.action(async (lab, options, command) => {
|
|
229
|
+
if (!await applyEnvFileOption({
|
|
230
|
+
command,
|
|
231
|
+
cwd: options.cwd,
|
|
232
|
+
envFile: options.envFile,
|
|
233
|
+
io
|
|
234
|
+
})) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (lab) {
|
|
238
|
+
if (options.appUrl !== undefined || options.actor !== undefined) {
|
|
239
|
+
const result = {
|
|
240
|
+
schema: "humanish.run-result.v1",
|
|
241
|
+
ok: false,
|
|
242
|
+
cwd: options.cwd,
|
|
243
|
+
warnings: [],
|
|
244
|
+
error: {
|
|
245
|
+
code: "HUMANISH_APP_URL_OPTION_CONFLICT",
|
|
246
|
+
message: "Use lab manifests with lab-compatible options only; --app-url and --actor belong to direct `humanish run`."
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
writeResult(command, io, result, formatRunHuman);
|
|
250
|
+
io.setExitCode(2);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await runLabCommand({
|
|
254
|
+
command,
|
|
255
|
+
io,
|
|
256
|
+
lab,
|
|
257
|
+
mode: "run",
|
|
258
|
+
options: {
|
|
259
|
+
cwd: options.cwd,
|
|
260
|
+
...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
261
|
+
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
262
|
+
...(options.sims === undefined ? {} : { sims: options.sims })
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const simCount = options.sims === undefined ? undefined : parsePositiveInteger(options.sims);
|
|
268
|
+
const timeoutMs = options.timeoutMs === undefined ? undefined : parseTimeoutMs(options.timeoutMs);
|
|
269
|
+
if (options.sims !== undefined && simCount === null) {
|
|
270
|
+
const result = {
|
|
271
|
+
schema: "humanish.run-result.v1",
|
|
272
|
+
ok: false,
|
|
273
|
+
cwd: options.cwd,
|
|
274
|
+
warnings: [],
|
|
275
|
+
error: {
|
|
276
|
+
code: "HUMANISH_INVALID_SIM_COUNT",
|
|
277
|
+
message: "--sims must be a positive integer."
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
writeResult(command, io, result, formatRunHuman);
|
|
281
|
+
io.setExitCode(2);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (options.timeoutMs !== undefined && timeoutMs === null) {
|
|
285
|
+
const result = {
|
|
286
|
+
schema: "humanish.run-result.v1",
|
|
287
|
+
ok: false,
|
|
288
|
+
cwd: options.cwd,
|
|
289
|
+
warnings: [],
|
|
290
|
+
error: {
|
|
291
|
+
code: "HUMANISH_INVALID_TIMEOUT",
|
|
292
|
+
message: "--timeout-ms must be an integer between 1 and 600000."
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
writeResult(command, io, result, formatRunHuman);
|
|
296
|
+
io.setExitCode(2);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const result = await runDryRun({
|
|
300
|
+
cwd: options.cwd,
|
|
301
|
+
...(options.actor === undefined ? {} : { actor: options.actor }),
|
|
302
|
+
...(options.appUrl === undefined ? {} : { appUrl: options.appUrl }),
|
|
303
|
+
...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
304
|
+
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
305
|
+
...(simCount === undefined || simCount === null ? {} : { simCount }),
|
|
306
|
+
...(timeoutMs === undefined || timeoutMs === null ? {} : { timeoutMs })
|
|
307
|
+
});
|
|
308
|
+
writeResult(command, io, result, formatRunHuman);
|
|
309
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function registerVerifyCommand(parent, io) {
|
|
313
|
+
parent
|
|
314
|
+
.command("verify")
|
|
315
|
+
.description("Validate a run bundle and public-safety gates.")
|
|
316
|
+
.summary("Validate a run bundle and public-safety gates.")
|
|
317
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
318
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
319
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
320
|
+
.action(async (options, command) => {
|
|
321
|
+
const result = await verifyRun(options.cwd, options.run);
|
|
322
|
+
writeResult(command, io, result, formatVerifyHuman);
|
|
323
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function registerCleanupCommand(parent, io) {
|
|
327
|
+
parent
|
|
328
|
+
.command("cleanup")
|
|
329
|
+
.description("Inspect recorded resource evidence and write cleanup.json; stored ids do not authorize provider mutation.")
|
|
330
|
+
.summary("Write a resource cleanup inspection receipt.")
|
|
331
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
332
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
333
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
334
|
+
.action(async (options, command) => {
|
|
335
|
+
const result = await cleanupRun(options.cwd, options.run);
|
|
336
|
+
writeResult(command, io, result, formatCleanupHuman);
|
|
337
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function registerReviewCommand(parent, io) {
|
|
341
|
+
parent
|
|
342
|
+
.command("review")
|
|
343
|
+
.description("Build a review packet from verified run evidence.")
|
|
344
|
+
.summary("Build a review packet from verified run evidence.")
|
|
345
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
346
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
347
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
348
|
+
.action(async (options, command) => {
|
|
349
|
+
const result = await readReview(options.cwd, options.run);
|
|
350
|
+
writeResult(command, io, result, (value) => `${JSON.stringify(value, null, 2)}\n`);
|
|
351
|
+
io.setExitCode("ok" in result && result.ok === false ? 2 : 0);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function registerRunsCommand(parent, io) {
|
|
355
|
+
parent
|
|
356
|
+
.command("runs")
|
|
357
|
+
.description("List local Humanish runs and latest pointers.")
|
|
358
|
+
.summary("List local Humanish runs and latest pointers.")
|
|
359
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
360
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
361
|
+
.action(async (options, command) => {
|
|
362
|
+
const result = await listRuns(options.cwd);
|
|
363
|
+
writeResult(command, io, result, formatRunsHuman);
|
|
364
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function registerCodexCommands(parent, io) {
|
|
368
|
+
const codex = parent
|
|
369
|
+
.command("codex")
|
|
370
|
+
.description("Run Codex-native Humanish integration surfaces.")
|
|
371
|
+
.summary("Run Codex-native Humanish integration surfaces.");
|
|
372
|
+
codex
|
|
373
|
+
.command("app-server")
|
|
374
|
+
.description("Run a browser-visible Codex app-server actor surface and write redacted protocol artifacts.")
|
|
375
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
376
|
+
.option("--prompt <text>", "Prompt to submit to Codex app-server.")
|
|
377
|
+
.option("--prompt-file <path>", "Read the Codex app-server prompt from a file.")
|
|
378
|
+
.option("--run-root <path>", "Artifact directory for redacted app-server evidence.")
|
|
379
|
+
.option("--state-file <path>", "State JSON file for external observers.")
|
|
380
|
+
.option("--timeout-ms <ms>", "Actor timeout in milliseconds.", String(240_000))
|
|
381
|
+
.option("--port <port>", "Local browser UI port.", "0")
|
|
382
|
+
.option("--model <model>", "Optional Codex model override.")
|
|
383
|
+
.addOption(new Option("--sandbox <mode>", "Turn sandbox policy.").choices(["read-only", "workspace-write", "danger-full-access"]).default("read-only"))
|
|
384
|
+
.option("--actor-command <command>", "Override app-server command. Defaults to codex app-server --listen stdio://.")
|
|
385
|
+
.option("--keep-open", "Keep the browser UI process alive after the actor finishes.")
|
|
386
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
387
|
+
.action(async (options, command) => {
|
|
388
|
+
const timeoutMs = parseTimeoutMs(options.timeoutMs);
|
|
389
|
+
const port = parseObserverPort(options.port);
|
|
390
|
+
if (timeoutMs === null) {
|
|
391
|
+
const result = codexAppServerUiError(options.cwd, "HUMANISH_INVALID_TIMEOUT", "--timeout-ms must be an integer between 1 and 600000.");
|
|
392
|
+
writeResult(command, io, result, formatCodexAppServerUiHuman);
|
|
393
|
+
io.setExitCode(2);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (port === null) {
|
|
397
|
+
const result = codexAppServerUiError(options.cwd, "HUMANISH_INVALID_PORT", "--port must be an integer between 0 and 65535.");
|
|
398
|
+
writeResult(command, io, result, formatCodexAppServerUiHuman);
|
|
399
|
+
io.setExitCode(2);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const prompt = await readCodexAppServerPrompt(options);
|
|
403
|
+
if (!prompt) {
|
|
404
|
+
const result = codexAppServerUiError(options.cwd, "HUMANISH_CODEX_APP_SERVER_PROMPT_REQUIRED", "Provide --prompt or --prompt-file.");
|
|
405
|
+
writeResult(command, io, result, formatCodexAppServerUiHuman);
|
|
406
|
+
io.setExitCode(2);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const controller = await startCodexAppServerUi({
|
|
410
|
+
...(options.actorCommand === undefined ? {} : { actorCommand: options.actorCommand }),
|
|
411
|
+
cwd: options.cwd,
|
|
412
|
+
keepOpen: options.keepOpen === true,
|
|
413
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
414
|
+
port,
|
|
415
|
+
prompt,
|
|
416
|
+
...(options.runRoot === undefined ? {} : { runRoot: options.runRoot }),
|
|
417
|
+
sandbox: options.sandbox,
|
|
418
|
+
...(options.stateFile === undefined ? {} : { stateFile: options.stateFile }),
|
|
419
|
+
timeoutMs
|
|
420
|
+
});
|
|
421
|
+
const initial = {
|
|
422
|
+
schema: "humanish.codex-app-server-ui-result.v1",
|
|
423
|
+
ok: true,
|
|
424
|
+
cwd: resolve(options.cwd),
|
|
425
|
+
stateFile: controller.stateFile,
|
|
426
|
+
url: controller.url,
|
|
427
|
+
status: controller.initialState.status,
|
|
428
|
+
reason: controller.initialState.reason
|
|
429
|
+
};
|
|
430
|
+
if (options.keepOpen === true) {
|
|
431
|
+
writeResult(command, io, initial, formatCodexAppServerUiHuman);
|
|
432
|
+
io.setExitCode(0);
|
|
433
|
+
try {
|
|
434
|
+
// Local hardening for the known double-envelope path: the "running"
|
|
435
|
+
// envelope above has already reached stdout, so a rejection here
|
|
436
|
+
// (codex-app-server-ui.ts's persistState() write can fail on either
|
|
437
|
+
// branch of session completion) must not go through the
|
|
438
|
+
// command-boundary catch-all's --json branch, which would otherwise
|
|
439
|
+
// append a second JSON document to stdout. Handling it here directly
|
|
440
|
+
// means this known path stays correct even if that general guard is
|
|
441
|
+
// ever weakened; it does not replace it.
|
|
442
|
+
await controller.completion;
|
|
443
|
+
await new Promise((resolveWait) => {
|
|
444
|
+
process.once("SIGINT", () => {
|
|
445
|
+
void controller.close().finally(resolveWait);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
io.writeErr(`HUMANISH_UNEXPECTED: ${redactText(error instanceof Error ? error.message : String(error))}\n`);
|
|
451
|
+
io.setExitCode(2);
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const completed = await controller.completion;
|
|
456
|
+
const output = codexAppServerUiResultFromState(completed);
|
|
457
|
+
writeResult(command, io, output, formatCodexAppServerUiHuman);
|
|
458
|
+
io.setExitCode(output.ok ? 0 : 2);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function registerWatchCommand(parent, io) {
|
|
462
|
+
parent
|
|
463
|
+
.command("watch")
|
|
464
|
+
.argument("[lab]", "Optional lab id or .yaml path to run and observe.")
|
|
465
|
+
.description("Run sims, open the observer, and keep the shell attached.")
|
|
466
|
+
.summary("Run sims, open the observer, keep the shell attached.")
|
|
467
|
+
.option("--lab <id-or-path>", "Explicit lab id or .yaml path.")
|
|
468
|
+
.option("--run <id>", "Watch an existing run id or latest pointer.")
|
|
469
|
+
.option("--dry-run", "Lab only: render contract evidence without live provider spend.")
|
|
470
|
+
.option("--codex-app-server", "Lab only: use Codex app-server client mode for OSS headed desktops.")
|
|
471
|
+
.option("--sims <count>", "Start a fresh synthetic run with this many sims before rendering. Defaults to 4 when --run is omitted.")
|
|
472
|
+
.option("--count <count>", "Lab only: override headed desktop lane count.")
|
|
473
|
+
.option("--limit <count>", "Lab only: override smoke lab repo limit.")
|
|
474
|
+
.option("--repo <owner/repo>", "Lab only: GitHub repo slug. Repeatable.", collectRepeated, [])
|
|
475
|
+
.option("--repos <owner/repo,...>", "Lab only: comma-separated GitHub repo slugs.")
|
|
476
|
+
.option("--redact-repos", "Lab only: redact repo labels in durable artifacts.")
|
|
477
|
+
.option("--no-redact-repos", "Lab only: persist repo labels. Use only for public-safe runs.")
|
|
478
|
+
.option("--keep", "Lab only: keep disposable clone sandbox for debugging.")
|
|
479
|
+
.option("--run-id <id>", "Explicit run id for deterministic fixture tests.")
|
|
480
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
481
|
+
.option("--env-file <path>", "Load a local env file for this watch without persisting values.")
|
|
482
|
+
.option("--open", "Open the observer in the default browser.")
|
|
483
|
+
.option("--no-open", "Render without opening a browser.")
|
|
484
|
+
.addOption(new Option("--follow", "Deprecated; human output follows by default.").hideHelp())
|
|
485
|
+
.option("--detach", "Render/open once and exit without attached watch server.")
|
|
486
|
+
.option("--port <port>", "Local observer server port when following.", "0")
|
|
487
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
488
|
+
.addHelpText("after", [
|
|
489
|
+
"",
|
|
490
|
+
"Happy path:",
|
|
491
|
+
" humanish watch",
|
|
492
|
+
" humanish watch first-run",
|
|
493
|
+
" humanish watch --lab .humanish/labs/local.yaml",
|
|
494
|
+
"",
|
|
495
|
+
"Agent/CI path:",
|
|
496
|
+
" humanish watch --json --no-open",
|
|
497
|
+
"",
|
|
498
|
+
"Existing evidence:",
|
|
499
|
+
" humanish watch --run latest --detach"
|
|
500
|
+
].join("\n"))
|
|
501
|
+
.action(async (labArg, options, command) => {
|
|
502
|
+
const lab = options.lab ?? labArg;
|
|
503
|
+
if (options.lab !== undefined && labArg !== undefined) {
|
|
504
|
+
const result = {
|
|
505
|
+
schema: "humanish.run-result.v1",
|
|
506
|
+
ok: false,
|
|
507
|
+
cwd: options.cwd,
|
|
508
|
+
warnings: [],
|
|
509
|
+
error: {
|
|
510
|
+
code: "HUMANISH_WATCH_OPTION_CONFLICT",
|
|
511
|
+
message: "Use either positional lab or --lab, not both."
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
writeResult(command, io, result, formatRunHuman);
|
|
515
|
+
io.setExitCode(2);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!await applyEnvFileOption({
|
|
519
|
+
command,
|
|
520
|
+
cwd: options.cwd,
|
|
521
|
+
envFile: options.envFile,
|
|
522
|
+
io
|
|
523
|
+
})) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (lab) {
|
|
527
|
+
if (options.run !== undefined) {
|
|
528
|
+
const result = {
|
|
529
|
+
schema: "humanish.run-result.v1",
|
|
530
|
+
ok: false,
|
|
531
|
+
cwd: options.cwd,
|
|
532
|
+
warnings: [],
|
|
533
|
+
error: {
|
|
534
|
+
code: "HUMANISH_WATCH_OPTION_CONFLICT",
|
|
535
|
+
message: "Use either a lab to start evidence or --run to watch existing evidence, not both."
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
writeResult(command, io, result, formatRunHuman);
|
|
539
|
+
io.setExitCode(2);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await runLabCommand({
|
|
543
|
+
command,
|
|
544
|
+
io,
|
|
545
|
+
lab,
|
|
546
|
+
mode: "watch",
|
|
547
|
+
options: {
|
|
548
|
+
cwd: options.cwd,
|
|
549
|
+
...(options.count === undefined ? {} : { count: options.count }),
|
|
550
|
+
...(options.codexAppServer === undefined ? {} : { codexAppServer: options.codexAppServer }),
|
|
551
|
+
...(options.detach === undefined ? {} : { detach: options.detach }),
|
|
552
|
+
...(options.dryRun === undefined ? {} : { dryRun: options.dryRun }),
|
|
553
|
+
...(options.keep === undefined ? {} : { keep: options.keep }),
|
|
554
|
+
...(options.limit === undefined ? {} : { limit: options.limit }),
|
|
555
|
+
...(options.open === undefined ? {} : { open: options.open }),
|
|
556
|
+
port: options.port,
|
|
557
|
+
...(options.redactRepos === undefined ? {} : { redactRepos: options.redactRepos }),
|
|
558
|
+
repo: options.repo,
|
|
559
|
+
...(options.repos === undefined ? {} : { repos: options.repos }),
|
|
560
|
+
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
561
|
+
...(options.sims === undefined ? {} : { sims: options.sims })
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const runOptionSource = typeof command.getOptionValueSource === "function"
|
|
567
|
+
? command.getOptionValueSource("run")
|
|
568
|
+
: undefined;
|
|
569
|
+
const runWasOmitted = runOptionSource === undefined || runOptionSource === "default";
|
|
570
|
+
const simCount = options.sims === undefined ? undefined : parsePositiveInteger(options.sims);
|
|
571
|
+
const port = parseObserverPort(options.port);
|
|
572
|
+
if (options.sims !== undefined && simCount === null) {
|
|
573
|
+
const result = {
|
|
574
|
+
schema: "humanish.run-result.v1",
|
|
575
|
+
ok: false,
|
|
576
|
+
cwd: options.cwd,
|
|
577
|
+
warnings: [],
|
|
578
|
+
error: {
|
|
579
|
+
code: "HUMANISH_INVALID_SIM_COUNT",
|
|
580
|
+
message: "--sims must be a positive integer."
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
writeResult(command, io, result, formatRunHuman);
|
|
584
|
+
io.setExitCode(2);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (!runWasOmitted && options.sims !== undefined) {
|
|
588
|
+
const result = {
|
|
589
|
+
schema: "humanish.run-result.v1",
|
|
590
|
+
ok: false,
|
|
591
|
+
cwd: options.cwd,
|
|
592
|
+
warnings: [],
|
|
593
|
+
error: {
|
|
594
|
+
code: "HUMANISH_WATCH_OPTION_CONFLICT",
|
|
595
|
+
message: "Use either --run to watch existing evidence or --sims to start a fresh run, not both."
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
writeResult(command, io, result, formatRunHuman);
|
|
599
|
+
io.setExitCode(2);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!runWasOmitted && options.runId !== undefined) {
|
|
603
|
+
const result = {
|
|
604
|
+
schema: "humanish.run-result.v1",
|
|
605
|
+
ok: false,
|
|
606
|
+
cwd: options.cwd,
|
|
607
|
+
warnings: [],
|
|
608
|
+
error: {
|
|
609
|
+
code: "HUMANISH_WATCH_OPTION_CONFLICT",
|
|
610
|
+
message: "--run-id only applies to fresh watch runs; remove --run or remove --run-id."
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
writeResult(command, io, result, formatRunHuman);
|
|
614
|
+
io.setExitCode(2);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (port === null) {
|
|
618
|
+
const result = {
|
|
619
|
+
schema: "humanish.run-result.v1",
|
|
620
|
+
ok: false,
|
|
621
|
+
cwd: options.cwd,
|
|
622
|
+
warnings: [],
|
|
623
|
+
error: {
|
|
624
|
+
code: "HUMANISH_INVALID_PORT",
|
|
625
|
+
message: "--port must be an integer between 0 and 65535."
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
writeResult(command, io, result, formatRunHuman);
|
|
629
|
+
io.setExitCode(2);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const requestedSimCount = simCount ?? (runWasOmitted ? 4 : undefined);
|
|
633
|
+
let runInput = options.run ?? "latest";
|
|
634
|
+
if (requestedSimCount !== undefined && requestedSimCount !== null) {
|
|
635
|
+
const runResult = await runDryRun({
|
|
636
|
+
cwd: options.cwd,
|
|
637
|
+
dryRun: true,
|
|
638
|
+
simCount: requestedSimCount,
|
|
639
|
+
...(options.runId === undefined ? {} : { runId: options.runId })
|
|
640
|
+
});
|
|
641
|
+
if (!runResult.ok || !runResult.runId) {
|
|
642
|
+
writeResult(command, io, runResult, formatRunHuman);
|
|
643
|
+
io.setExitCode(2);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
runInput = runResult.runId;
|
|
647
|
+
}
|
|
648
|
+
const wantsMachine = wantsJson(command);
|
|
649
|
+
const shouldOpen = options.open === false ? false : options.open === true ? true : !wantsMachine && process.stdout.isTTY === true;
|
|
650
|
+
const wantsFollow = !wantsMachine && options.detach !== true && (options.follow !== false);
|
|
651
|
+
const rendered = await renderObserver(options.cwd, runInput, { open: wantsFollow ? false : shouldOpen });
|
|
652
|
+
let server = null;
|
|
653
|
+
let result = rendered;
|
|
654
|
+
if (rendered.ok && wantsFollow) {
|
|
655
|
+
server = await serveObserver(rendered, { open: shouldOpen, port });
|
|
656
|
+
result = {
|
|
657
|
+
...rendered,
|
|
658
|
+
observerUrl: server.url,
|
|
659
|
+
serverUrl: server.url,
|
|
660
|
+
opened: server.opened,
|
|
661
|
+
...(server.openCommand ? { openCommand: server.openCommand } : {}),
|
|
662
|
+
warnings: [
|
|
663
|
+
...rendered.warnings,
|
|
664
|
+
"Live observer server is polling observer-data.json with no-store caching.",
|
|
665
|
+
...(server.warning ? [server.warning] : [])
|
|
666
|
+
]
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
writeResult(command, io, result, formatObserverHuman);
|
|
670
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
671
|
+
if (result.ok && server) {
|
|
672
|
+
await followObserver(io, result, server);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
function registerObserveCommand(parent, io) {
|
|
677
|
+
parent
|
|
678
|
+
.command("observe")
|
|
679
|
+
.description("Serve a finished run's Observer over loopback http://127.0.0.1 instead of a file:// path.")
|
|
680
|
+
.summary("Serve a finished run's Observer over loopback http.")
|
|
681
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
682
|
+
.option("--port <port>", "Loopback port to bind on 127.0.0.1. Defaults to an ephemeral port.", "0")
|
|
683
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
684
|
+
.option("--open", "Open the observer in the default browser.")
|
|
685
|
+
.option("--no-open", "Serve without opening a browser.")
|
|
686
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
687
|
+
.addHelpText("after", [
|
|
688
|
+
"",
|
|
689
|
+
"Examples:",
|
|
690
|
+
" humanish observe",
|
|
691
|
+
" humanish observe --run latest",
|
|
692
|
+
" humanish observe --run <runId> --port 8732",
|
|
693
|
+
" humanish observe --no-open --json",
|
|
694
|
+
"",
|
|
695
|
+
"The server binds 127.0.0.1 only and exposes just the run's bundle directory.",
|
|
696
|
+
"It stays attached until Ctrl-C; file:// security policy and live refresh are why",
|
|
697
|
+
"loopback http is preferred over opening the index.html path directly."
|
|
698
|
+
].join("\n"))
|
|
699
|
+
.action(async (options, command) => {
|
|
700
|
+
const port = parseObserverPort(options.port);
|
|
701
|
+
if (port === null) {
|
|
702
|
+
const result = {
|
|
703
|
+
schema: "humanish.run-result.v1",
|
|
704
|
+
ok: false,
|
|
705
|
+
cwd: options.cwd,
|
|
706
|
+
warnings: [],
|
|
707
|
+
error: {
|
|
708
|
+
code: "HUMANISH_INVALID_PORT",
|
|
709
|
+
message: "--port must be an integer between 0 and 65535."
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
writeResult(command, io, result, formatRunHuman);
|
|
713
|
+
io.setExitCode(2);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const rendered = await renderObserver(options.cwd, options.run, { open: false });
|
|
717
|
+
if (!rendered.ok || !rendered.observerPath) {
|
|
718
|
+
writeResult(command, io, rendered, formatObserverHuman);
|
|
719
|
+
io.setExitCode(2);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
// Serve the run's bundle directory so the Observer's relative artifact
|
|
723
|
+
// links (../run.json, ../review.json, ../events.ndjson) resolve, then land
|
|
724
|
+
// visitors on observer/index.html. The loopback root is the run dir; the
|
|
725
|
+
// traversal guard still refuses anything above it (sibling runs, the
|
|
726
|
+
// .humanish/runs/ parent, etc.).
|
|
727
|
+
const observerIndexAbs = join(resolve(options.cwd), rendered.observerPath);
|
|
728
|
+
const runDir = dirname(dirname(observerIndexAbs));
|
|
729
|
+
const wantsMachine = wantsJson(command);
|
|
730
|
+
const shouldOpen = options.open === false
|
|
731
|
+
? false
|
|
732
|
+
: options.open === true
|
|
733
|
+
? true
|
|
734
|
+
: !wantsMachine && process.stdout.isTTY === true;
|
|
735
|
+
const server = await serveObserverStatic({ root: runDir, port, entryPath: "observer/index.html" });
|
|
736
|
+
const openResult = shouldOpen ? openTarget(server.url) : { opened: false };
|
|
737
|
+
const result = {
|
|
738
|
+
...rendered,
|
|
739
|
+
observerUrl: server.url,
|
|
740
|
+
serverUrl: server.url,
|
|
741
|
+
opened: openResult.opened,
|
|
742
|
+
...(openResult.command ? { openCommand: openResult.command } : {}),
|
|
743
|
+
warnings: [
|
|
744
|
+
...rendered.warnings,
|
|
745
|
+
"Observer is served read-only over loopback http on 127.0.0.1; only this run's bundle directory is exposed.",
|
|
746
|
+
...(openResult.warning ? [openResult.warning] : [])
|
|
747
|
+
]
|
|
748
|
+
};
|
|
749
|
+
writeResult(command, io, result, formatObserverHuman);
|
|
750
|
+
io.setExitCode(0);
|
|
751
|
+
await serveObserveUntilSignal(io, server, { json: wantsMachine });
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
async function serveObserveUntilSignal(io, server, options) {
|
|
755
|
+
// Keep the JSON envelope on stdout clean: route attach/stop chatter to stderr
|
|
756
|
+
// for machine output, and to stdout for humans.
|
|
757
|
+
const note = options.json ? io.writeErr : io.writeOut;
|
|
758
|
+
note(`serving: ${server.url}\n`);
|
|
759
|
+
note("serving: press Ctrl-C to stop\n");
|
|
760
|
+
await new Promise((resolveWait) => {
|
|
761
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
762
|
+
const handlers = new Map();
|
|
763
|
+
let stopping = false;
|
|
764
|
+
const stop = (signal) => {
|
|
765
|
+
if (stopping) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
stopping = true;
|
|
769
|
+
for (const [registeredSignal, handler] of handlers.entries()) {
|
|
770
|
+
process.removeListener(registeredSignal, handler);
|
|
771
|
+
}
|
|
772
|
+
io.setExitCode(exitCodeForSignal(signal));
|
|
773
|
+
void (async () => {
|
|
774
|
+
try {
|
|
775
|
+
await server.close();
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
io.writeErr(`observe cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
779
|
+
}
|
|
780
|
+
note("observe stopped\n");
|
|
781
|
+
resolveWait();
|
|
782
|
+
})();
|
|
783
|
+
};
|
|
784
|
+
for (const signal of signals) {
|
|
785
|
+
const handler = () => stop(signal);
|
|
786
|
+
handlers.set(signal, handler);
|
|
787
|
+
process.once(signal, handler);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function registerFeedbackCommands(parent, io) {
|
|
792
|
+
const feedback = parent
|
|
793
|
+
.command("feedback")
|
|
794
|
+
.description("Create public-safe feedback drafts without GitHub API mutation.")
|
|
795
|
+
.summary("Create public-safe feedback drafts, no GitHub API.");
|
|
796
|
+
feedback
|
|
797
|
+
.command("list")
|
|
798
|
+
.description("List feedback draft state for a run.")
|
|
799
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
800
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
801
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
802
|
+
.action(async (options, command) => {
|
|
803
|
+
const result = await listFeedback(options.cwd, options.run);
|
|
804
|
+
writeResult(command, io, result, formatFeedbackHuman);
|
|
805
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
806
|
+
});
|
|
807
|
+
feedback
|
|
808
|
+
.command("draft")
|
|
809
|
+
.description("Generate a public-safe feedback draft from verified evidence.")
|
|
810
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
811
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
812
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
813
|
+
.action(async (options, command) => {
|
|
814
|
+
const result = await draftFeedback(options.cwd, options.run);
|
|
815
|
+
writeResult(command, io, result, formatFeedbackHuman);
|
|
816
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
817
|
+
});
|
|
818
|
+
feedback
|
|
819
|
+
.command("verify")
|
|
820
|
+
.description("Verify the feedback draft for public issue eligibility.")
|
|
821
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
822
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
823
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
824
|
+
.action(async (options, command) => {
|
|
825
|
+
const result = await verifyFeedback(options.cwd, options.run);
|
|
826
|
+
writeResult(command, io, result, formatFeedbackHuman);
|
|
827
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
828
|
+
});
|
|
829
|
+
feedback
|
|
830
|
+
.command("issue")
|
|
831
|
+
.description("Print Markdown for a public GitHub issue. Does not mutate GitHub.")
|
|
832
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
833
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
834
|
+
.requiredOption("--repo <owner/repo>", "Repository slug used in rendered filing instructions.")
|
|
835
|
+
.option("--format <format>", "Output format.", "markdown")
|
|
836
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
837
|
+
.action(async (options, command) => {
|
|
838
|
+
const result = await renderIssueMarkdown(options.cwd, options.run, options.repo);
|
|
839
|
+
if (wantsJson(command)) {
|
|
840
|
+
io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
841
|
+
}
|
|
842
|
+
else if (options.format !== "markdown") {
|
|
843
|
+
io.writeErr("Only --format markdown is supported.\n");
|
|
844
|
+
io.setExitCode(2);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
else if (result.ok && result.issueMarkdown) {
|
|
848
|
+
io.writeOut(result.issueMarkdown);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
io.writeErr(formatFeedbackHuman(result));
|
|
852
|
+
}
|
|
853
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
854
|
+
});
|
|
855
|
+
feedback
|
|
856
|
+
.command("issue-url")
|
|
857
|
+
.description("Print a prefilled public issue URL. Does not mutate GitHub.")
|
|
858
|
+
.option("--run <id>", "Run id or latest pointer.", "latest")
|
|
859
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
860
|
+
.requiredOption("--repo <owner/repo>", "Repository slug used in the generated URL.")
|
|
861
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
862
|
+
.action(async (options, command) => {
|
|
863
|
+
const result = await renderIssueUrl(options.cwd, options.run, options.repo);
|
|
864
|
+
if (wantsJson(command)) {
|
|
865
|
+
io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
866
|
+
}
|
|
867
|
+
else if (result.ok && result.issueUrl) {
|
|
868
|
+
io.writeOut(`${result.issueUrl}\n`);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
io.writeErr(formatFeedbackHuman(result));
|
|
872
|
+
}
|
|
873
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function registerLabCommands(parent, io) {
|
|
877
|
+
const lab = parent
|
|
878
|
+
.command("lab")
|
|
879
|
+
.description("List, inspect, and run Humanish lab manifests.")
|
|
880
|
+
.summary("List, inspect, and run Humanish lab manifests.");
|
|
881
|
+
lab
|
|
882
|
+
.command("list")
|
|
883
|
+
.description("List committed and ignored Humanish lab manifests.")
|
|
884
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
885
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
886
|
+
.action(async (options, command) => {
|
|
887
|
+
const result = await listLabManifests(options.cwd);
|
|
888
|
+
writeResult(command, io, result, formatLabListHuman);
|
|
889
|
+
io.setExitCode(0);
|
|
890
|
+
});
|
|
891
|
+
lab
|
|
892
|
+
.command("inspect")
|
|
893
|
+
.argument("<lab>", "Lab id or .yaml path.")
|
|
894
|
+
.description("Inspect a Humanish lab manifest without running it.")
|
|
895
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
896
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
897
|
+
.action(async (labName, options, command) => {
|
|
898
|
+
const result = await inspectLabManifest(options.cwd, labName);
|
|
899
|
+
writeResult(command, io, result, formatLabInspectHuman);
|
|
900
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
901
|
+
});
|
|
902
|
+
lab
|
|
903
|
+
.command("preflight")
|
|
904
|
+
.argument("<lab>", "Lab id or .yaml path.")
|
|
905
|
+
.description("Check a lab manifest and optional target reachability before actor/model spend.")
|
|
906
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
907
|
+
.addOption(new Option("--reachability <mode>", "Reachability mode.").choices(["metadata", "public-preview", "sandbox-loopback", "prepared-host"]).default("metadata"))
|
|
908
|
+
.option("--timeout-ms <ms>", "Target reachability timeout.", String(30_000))
|
|
909
|
+
.option("--env-file <path>", "Load a local env file for this preflight without persisting values.")
|
|
910
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
911
|
+
.action(async (labName, options, command) => {
|
|
912
|
+
if (!await applyEnvFileOption({
|
|
913
|
+
command,
|
|
914
|
+
cwd: options.cwd,
|
|
915
|
+
envFile: options.envFile,
|
|
916
|
+
io
|
|
917
|
+
})) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const timeoutMs = parsePositiveInteger(options.timeoutMs);
|
|
921
|
+
if (timeoutMs === null) {
|
|
922
|
+
const result = {
|
|
923
|
+
schema: "humanish.lab-preflight-result.v1",
|
|
924
|
+
ok: false,
|
|
925
|
+
cwd: resolve(options.cwd),
|
|
926
|
+
lab: labName,
|
|
927
|
+
reachability: options.reachability,
|
|
928
|
+
checks: [{ name: "timeout", ok: false, message: "--timeout-ms must be a positive integer." }],
|
|
929
|
+
targets: [],
|
|
930
|
+
sandbox: { created: false },
|
|
931
|
+
spend: { e2bDesktop: false, model: false },
|
|
932
|
+
warnings: [],
|
|
933
|
+
error: {
|
|
934
|
+
code: "HUMANISH_LAB_PREFLIGHT_INVALID_OPTION",
|
|
935
|
+
message: "--timeout-ms must be a positive integer."
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
writeResult(command, io, result, formatLabPreflightHuman);
|
|
939
|
+
io.setExitCode(2);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const result = await runLabPreflight({
|
|
943
|
+
cwd: options.cwd,
|
|
944
|
+
lab: labName,
|
|
945
|
+
reachability: options.reachability,
|
|
946
|
+
timeoutMs
|
|
947
|
+
});
|
|
948
|
+
writeResult(command, io, result, formatLabPreflightHuman);
|
|
949
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
950
|
+
});
|
|
951
|
+
lab
|
|
952
|
+
.command("cleanup")
|
|
953
|
+
.argument("[lab]", "Provider-backed lab to clean up.", "oss")
|
|
954
|
+
.description("Sweep stale provider resources from a crashed prior process, by provider metadata, without printing provider ids. humanish never enumerates an account by default: set HUMANISH_OSS_META_ALLOW_PROVIDER_LIST=1 to opt in for this maintainer-only sweep.")
|
|
955
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
956
|
+
.action(async (labName, _options, command) => {
|
|
957
|
+
if (labName !== "oss") {
|
|
958
|
+
const result = {
|
|
959
|
+
schema: "humanish.oss-meta-lab-cleanup-result.v1",
|
|
960
|
+
ok: false,
|
|
961
|
+
lab: labName,
|
|
962
|
+
cleanup: { killed: 0, skipped: 0, errors: [`Unsupported cleanup lab '${labName}'.`] }
|
|
963
|
+
};
|
|
964
|
+
writeResult(command, io, result, formatOssMetaLabCleanupHuman);
|
|
965
|
+
io.setExitCode(2);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const cleanup = await cleanupStaleOssMetaLabSandboxes();
|
|
969
|
+
const result = {
|
|
970
|
+
schema: "humanish.oss-meta-lab-cleanup-result.v1",
|
|
971
|
+
ok: cleanup.errors.length === 0,
|
|
972
|
+
lab: "oss",
|
|
973
|
+
cleanup
|
|
974
|
+
};
|
|
975
|
+
writeResult(command, io, result, formatOssMetaLabCleanupHuman);
|
|
976
|
+
io.setExitCode(result.ok ? 0 : 2);
|
|
977
|
+
});
|
|
978
|
+
lab
|
|
979
|
+
.command("run")
|
|
980
|
+
.argument("<lab>", "Lab id or .yaml path.")
|
|
981
|
+
.description("Run a Humanish lab manifest.")
|
|
982
|
+
.option("--env-file <path>", "Load a local env file for this lab without persisting values.")
|
|
983
|
+
.option("--dry-run", "Render contract evidence without live provider spend. The bundled OSS lab defaults to this mode.")
|
|
984
|
+
.option("--codex-app-server", "Meta only: use Codex app-server client mode for headed desktop actor surfaces.")
|
|
985
|
+
.option("--open", "Open the observer in the default browser.")
|
|
986
|
+
.option("--no-open", "Render without opening a browser.")
|
|
987
|
+
.option("--detach", "Render/open once and exit without attached watch server.")
|
|
988
|
+
.option("--port <port>", "Local observer server port when following.", "0")
|
|
989
|
+
.option("--sims <count>", "Override synthetic sims or headed desktop lanes.")
|
|
990
|
+
.option("--count <count>", "CUA/meta only: override headed desktop lane count.")
|
|
991
|
+
.option("--rerun-failed-from <run>", "CUA fan-out only: create a new run for failed lanes from a prior run.")
|
|
992
|
+
.option("--lanes <lane-ids>", "CUA rerun only: comma-separated lane ids to rerun from the source run.")
|
|
993
|
+
.option("--limit <count>", "Smoke labs only: override repo limit.")
|
|
994
|
+
.option("--run-id <id>", "Explicit lab run id.")
|
|
995
|
+
.option("--cwd <path>", "Target project directory.", ".")
|
|
996
|
+
.option("--repo <owner/repo>", "Smoke/meta only: GitHub repo slug. Repeatable.", collectRepeated, [])
|
|
997
|
+
.option("--repos <owner/repo,...>", "Smoke/meta only: comma-separated GitHub repo slugs.")
|
|
998
|
+
.option("--redact-repos", "Meta only: redact repo labels in durable lab artifacts.")
|
|
999
|
+
.option("--no-redact-repos", "Meta only: persist repo labels in durable lab artifacts. Use only for public-safe runs.")
|
|
1000
|
+
.option("--keep", "Smoke labs only: keep disposable clone sandbox for debugging.")
|
|
1001
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1002
|
+
.addHelpText("after", [
|
|
1003
|
+
"",
|
|
1004
|
+
"Examples:",
|
|
1005
|
+
" humanish lab run first-run",
|
|
1006
|
+
" humanish lab run fanout-demo --rerun-failed-from latest --lanes lane-02,lane-04",
|
|
1007
|
+
" humanish lab run oss --dry-run --json --no-open",
|
|
1008
|
+
" humanish lab run .humanish/labs/private-dogfood.yaml --env-file .humanish/local/provider.env",
|
|
1009
|
+
"",
|
|
1010
|
+
"Human watch path:",
|
|
1011
|
+
" humanish watch first-run",
|
|
1012
|
+
" humanish watch --lab .humanish/labs/local.yaml",
|
|
1013
|
+
"",
|
|
1014
|
+
"OSS safety:",
|
|
1015
|
+
" Live OSS meta-lab manifests fail closed pending credential isolation."
|
|
1016
|
+
].join("\n"))
|
|
1017
|
+
.action(async (labName, options, command) => {
|
|
1018
|
+
if (!await applyEnvFileOption({
|
|
1019
|
+
command,
|
|
1020
|
+
cwd: options.cwd,
|
|
1021
|
+
envFile: options.envFile,
|
|
1022
|
+
io
|
|
1023
|
+
})) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
await runLabCommand({
|
|
1027
|
+
command,
|
|
1028
|
+
io,
|
|
1029
|
+
lab: labName,
|
|
1030
|
+
mode: "run",
|
|
1031
|
+
options
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
lab
|
|
1035
|
+
.command("oss", { hidden: true })
|
|
1036
|
+
.description("Alias: run the bundled OSS meta-lab dry-run contract.")
|
|
1037
|
+
.option("--env-file <path>", "Load a local env file for this lab without persisting values.")
|
|
1038
|
+
.option("--repos <owner/repo,...>", "Comma-separated GitHub repo slugs.")
|
|
1039
|
+
.option("--repo <owner/repo>", "GitHub repo slug. Repeatable.", collectRepeated, [])
|
|
1040
|
+
.option("--count <count>", "Number of contract lanes to assign.", String(DEFAULT_OSS_REPOS.length))
|
|
1041
|
+
.option("--sims <count>", "Alias for --count.")
|
|
1042
|
+
.option("--run-id <id>", "Explicit lab run id.")
|
|
1043
|
+
.option("--cwd <path>", "Host directory for ignored .humanish lab report.", ".")
|
|
1044
|
+
.option("--dry-run", "Render the Observer-of-Observers contract without provider spend or live E2B launch (default).")
|
|
1045
|
+
.option("--open", "Open the observer in the default browser.")
|
|
1046
|
+
.option("--no-open", "Render without opening a browser.")
|
|
1047
|
+
.option("--detach", "Render/open once and exit without attached watch server.")
|
|
1048
|
+
.option("--redact-repos", "Redact repo labels in durable lab artifacts.")
|
|
1049
|
+
.option("--no-redact-repos", "Persist repo labels in durable lab artifacts. Defaults to redacted when a GitHub token is present.")
|
|
1050
|
+
.option("--port <port>", "Local observer server port when following.", "0")
|
|
1051
|
+
.option("--smoke", "Run the disposable local clone smoke harness instead of headed meta-sims.")
|
|
1052
|
+
.option("--limit <count>", "Smoke mode only: number of selected repos to trial.", String(DEFAULT_OSS_REPOS.length))
|
|
1053
|
+
.option("--keep", "Smoke mode only: keep disposable clone sandbox for debugging.")
|
|
1054
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1055
|
+
.addHelpText("after", [
|
|
1056
|
+
"",
|
|
1057
|
+
"Preferred paths:",
|
|
1058
|
+
" humanish watch oss",
|
|
1059
|
+
" humanish lab run oss --dry-run",
|
|
1060
|
+
"",
|
|
1061
|
+
"Repo selection:",
|
|
1062
|
+
" humanish watch --lab .humanish/labs/local-oss.yaml",
|
|
1063
|
+
" humanish lab run oss --repos CorentinTh/it-tools,drawdb-io/drawdb,maciekt07/TodoApp,lissy93/dashy",
|
|
1064
|
+
" humanish lab run oss --repo CorentinTh/it-tools --repo drawdb-io/drawdb --count 4",
|
|
1065
|
+
"",
|
|
1066
|
+
"Agent/CI path:",
|
|
1067
|
+
" humanish lab run oss --dry-run --json --no-open",
|
|
1068
|
+
"",
|
|
1069
|
+
"Disposable clone smoke:",
|
|
1070
|
+
" humanish lab run oss-smoke --limit 1 --keep",
|
|
1071
|
+
" humanish lab oss-smoke --limit 1 --keep",
|
|
1072
|
+
"",
|
|
1073
|
+
"Shape:",
|
|
1074
|
+
" The top-level Observer shows contract-only lanes for the selected repo labels.",
|
|
1075
|
+
" No repo clone, provider sandbox, credential forwarding, or Codex actor runs.",
|
|
1076
|
+
"",
|
|
1077
|
+
"Safety:",
|
|
1078
|
+
" Only GitHub owner/repo slugs are accepted. Live OSS meta-lab execution",
|
|
1079
|
+
" fails closed pending credential isolation. Repo labels are redacted by",
|
|
1080
|
+
" default when overridden; use --no-redact-repos only for public-safe repos."
|
|
1081
|
+
].join("\n"))
|
|
1082
|
+
.action(async (options, command) => {
|
|
1083
|
+
if (!await applyEnvFileOption({
|
|
1084
|
+
command,
|
|
1085
|
+
cwd: options.cwd,
|
|
1086
|
+
envFile: options.envFile,
|
|
1087
|
+
io
|
|
1088
|
+
})) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (options.smoke) {
|
|
1092
|
+
await runOssSmokeAction({ command, io, options });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const countInput = options.sims ?? options.count;
|
|
1096
|
+
const count = parsePositiveInteger(countInput);
|
|
1097
|
+
const dryRun = options.dryRun ?? true;
|
|
1098
|
+
const port = parseObserverPort(options.port);
|
|
1099
|
+
if (port === null) {
|
|
1100
|
+
const result = {
|
|
1101
|
+
schema: "humanish.oss-meta-lab-result.v1",
|
|
1102
|
+
ok: false,
|
|
1103
|
+
assignments: [],
|
|
1104
|
+
cwd: options.cwd,
|
|
1105
|
+
dryRun,
|
|
1106
|
+
error: {
|
|
1107
|
+
code: "HUMANISH_META_RUN_FAILED",
|
|
1108
|
+
message: "--port must be an integer between 0 and 65535."
|
|
1109
|
+
},
|
|
1110
|
+
liveRequested: !dryRun,
|
|
1111
|
+
repos: [...options.repo, ...(options.repos ? [options.repos] : [])],
|
|
1112
|
+
sandboxes: [],
|
|
1113
|
+
warnings: []
|
|
1114
|
+
};
|
|
1115
|
+
writeResult(command, io, result, formatOssMetaLabHuman);
|
|
1116
|
+
io.setExitCode(2);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const wantsMachine = wantsJson(command);
|
|
1120
|
+
const shouldOpen = options.open === false ? false : options.open === true ? true : !wantsMachine && process.stdout.isTTY === true;
|
|
1121
|
+
const wantsFollow = !wantsMachine && options.detach !== true && !dryRun;
|
|
1122
|
+
const repoOverrideRequested = options.repo.length > 0 || options.repos !== undefined;
|
|
1123
|
+
const redactRepoNames = options.redactRepos ?? (repoOverrideRequested ? true : undefined);
|
|
1124
|
+
let server = null;
|
|
1125
|
+
let liveRefresh = null;
|
|
1126
|
+
let result;
|
|
1127
|
+
try {
|
|
1128
|
+
result = await runOssMetaLab({
|
|
1129
|
+
...(wantsFollow ? { completionTimeoutMs: 0 } : {}),
|
|
1130
|
+
...(options.codexAppServer === undefined ? {} : { codexAppServer: options.codexAppServer }),
|
|
1131
|
+
cwd: options.cwd,
|
|
1132
|
+
...(wantsFollow
|
|
1133
|
+
? {
|
|
1134
|
+
onObserverReady: async (observer) => {
|
|
1135
|
+
if (!server) {
|
|
1136
|
+
server = await serveObserver(observer, { open: shouldOpen, port });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
: {}),
|
|
1141
|
+
open: wantsFollow ? false : shouldOpen,
|
|
1142
|
+
...(redactRepoNames === undefined ? {} : { redactRepoNames }),
|
|
1143
|
+
repos: [...options.repo, ...(options.repos ? [options.repos] : [])],
|
|
1144
|
+
...(count === null ? { count: Number.NaN } : { count }),
|
|
1145
|
+
dryRun,
|
|
1146
|
+
...(options.runId === undefined ? {} : { runId: options.runId })
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
const earlyServer = server;
|
|
1151
|
+
await earlyServer?.close().catch((cleanupError) => {
|
|
1152
|
+
io.writeErr(`watch cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
|
|
1153
|
+
});
|
|
1154
|
+
server = null;
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
let output = result;
|
|
1158
|
+
if (server && shouldServeOssMetaLabObserver(result, { wantsFollow: true })) {
|
|
1159
|
+
liveRefresh = startOssMetaLabLiveRefresh(result);
|
|
1160
|
+
output = withOssMetaLabServer(result, server);
|
|
1161
|
+
}
|
|
1162
|
+
else if (shouldServeOssMetaLabObserver(result, { wantsFollow })) {
|
|
1163
|
+
server = await serveObserver(result.observer, { open: shouldOpen, port });
|
|
1164
|
+
liveRefresh = startOssMetaLabLiveRefresh(result);
|
|
1165
|
+
output = withOssMetaLabServer(result, server);
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const earlyServer = server;
|
|
1169
|
+
await earlyServer?.close().catch((error) => {
|
|
1170
|
+
io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1171
|
+
});
|
|
1172
|
+
server = null;
|
|
1173
|
+
}
|
|
1174
|
+
const exitCode = exitCodeForOssMetaLab(output);
|
|
1175
|
+
writeResult(command, io, output, formatOssMetaLabHuman);
|
|
1176
|
+
io.setExitCode(exitCode);
|
|
1177
|
+
if (shouldForceExitAfterOssMetaLab(output, { detach: options.detach === true, wantsMachine })) {
|
|
1178
|
+
// The E2B SDK keeps local handles open after stream URL creation. Detach should
|
|
1179
|
+
// return the user's shell, and JSON mode should exit after printing the result.
|
|
1180
|
+
setTimeout(() => process.exit(exitCode), 50);
|
|
1181
|
+
}
|
|
1182
|
+
if (server && output.observer?.ok) {
|
|
1183
|
+
await followObserver(io, output.observer, server, output.liveRequested
|
|
1184
|
+
? {
|
|
1185
|
+
onStop: async () => {
|
|
1186
|
+
const cleanup = liveRefresh
|
|
1187
|
+
? await liveRefresh.cleanup()
|
|
1188
|
+
: await cleanupOssMetaLabSandboxes(output);
|
|
1189
|
+
return [
|
|
1190
|
+
`E2B sandbox cleanup killed ${cleanup.killed}, skipped ${cleanup.skipped}.`,
|
|
1191
|
+
...cleanup.errors.map((error) => `E2B sandbox cleanup error: ${error}`)
|
|
1192
|
+
];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
: {});
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
lab
|
|
1199
|
+
.command("oss-smoke", { hidden: true })
|
|
1200
|
+
.description("Clone lightweight public OSS repos, try Humanish setup/proof, then discard clones.")
|
|
1201
|
+
.option("--repos <owner/repo,...>", "Comma-separated public GitHub repo slugs.")
|
|
1202
|
+
.option("--repo <owner/repo>", "Public GitHub repo slug. Repeatable.", collectRepeated, [])
|
|
1203
|
+
.option("--limit <count>", "Number of selected repos to trial.", String(DEFAULT_OSS_REPOS.length))
|
|
1204
|
+
.option("--run-id <id>", "Explicit lab run id.")
|
|
1205
|
+
.option("--cwd <path>", "Host directory for ignored .humanish lab report.", ".")
|
|
1206
|
+
.option("--keep", "Keep disposable clone sandbox for debugging.")
|
|
1207
|
+
.option("--json", JSON_OPTION_DESCRIPTION)
|
|
1208
|
+
.addHelpText("after", [
|
|
1209
|
+
"",
|
|
1210
|
+
"Examples:",
|
|
1211
|
+
" humanish lab oss-smoke",
|
|
1212
|
+
" humanish lab oss-smoke --repos CorentinTh/it-tools,drawdb-io/drawdb",
|
|
1213
|
+
" humanish lab oss-smoke --limit 1 --keep --json",
|
|
1214
|
+
"",
|
|
1215
|
+
"Safety:",
|
|
1216
|
+
" Only public GitHub owner/repo slugs are accepted. Clones live under ignored .humanish/",
|
|
1217
|
+
" runtime state and are removed by default."
|
|
1218
|
+
].join("\n"))
|
|
1219
|
+
.action(async (options, command) => {
|
|
1220
|
+
await runOssSmokeAction({ command, io, options });
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
async function runOssSmokeAction(args) {
|
|
1224
|
+
const limit = parsePositiveInteger(args.options.limit);
|
|
1225
|
+
const labOptions = {
|
|
1226
|
+
cwd: args.options.cwd,
|
|
1227
|
+
limit: limit ?? Number.NaN,
|
|
1228
|
+
repos: [...args.options.repo, ...(args.options.repos ? [args.options.repos] : [])],
|
|
1229
|
+
...(args.options.keep === undefined ? {} : { keep: args.options.keep }),
|
|
1230
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1231
|
+
};
|
|
1232
|
+
const result = await runOssLab(labOptions);
|
|
1233
|
+
writeResult(args.command, args.io, result, formatOssLabHuman);
|
|
1234
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1235
|
+
}
|
|
1236
|
+
async function runLabCommand(args) {
|
|
1237
|
+
const resolved = await resolveLabManifest(args.options.cwd, args.lab);
|
|
1238
|
+
if (!resolved.ok) {
|
|
1239
|
+
writeResult(args.command, args.io, resolved, formatLabResolveFailureHuman);
|
|
1240
|
+
args.io.setExitCode(2);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
// Surface forward-declared-field + .yml warnings on run/watch too, not only on inspect.
|
|
1244
|
+
// Otherwise a setting that does nothing is silently swallowed on the path users actually run.
|
|
1245
|
+
for (const warning of resolved.warnings) {
|
|
1246
|
+
args.io.writeErr(`warning: ${warning}\n`);
|
|
1247
|
+
}
|
|
1248
|
+
const config = resolved.config;
|
|
1249
|
+
const backend = selectLabBackend(config);
|
|
1250
|
+
if (backend !== "cua" && labRerunFlagsRequested(args.options)) {
|
|
1251
|
+
writeUnsupportedRerunFlagsResult(args, backend);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
switch (backend) {
|
|
1255
|
+
case "synthetic":
|
|
1256
|
+
await runSyntheticBackend({ ...args, config });
|
|
1257
|
+
return;
|
|
1258
|
+
case "meta":
|
|
1259
|
+
await runMetaBackend({ ...args, config });
|
|
1260
|
+
return;
|
|
1261
|
+
case "smoke":
|
|
1262
|
+
await runSmokeBackend({ ...args, config });
|
|
1263
|
+
return;
|
|
1264
|
+
case "cua":
|
|
1265
|
+
await runCuaBackend({ ...args, config });
|
|
1266
|
+
return;
|
|
1267
|
+
case "scripted":
|
|
1268
|
+
await runScriptedBackend({ ...args, config });
|
|
1269
|
+
return;
|
|
1270
|
+
case "terminal":
|
|
1271
|
+
await runTerminalBackend({ ...args, config });
|
|
1272
|
+
return;
|
|
1273
|
+
case "shared-world":
|
|
1274
|
+
await runSharedWorldBackend({ ...args, config });
|
|
1275
|
+
return;
|
|
1276
|
+
case "concurrent-shared-world":
|
|
1277
|
+
await runConcurrentSharedWorldBackend({ ...args, config });
|
|
1278
|
+
return;
|
|
1279
|
+
default:
|
|
1280
|
+
// Compile-time exhaustiveness: a future backend must be handled here, not silently no-op.
|
|
1281
|
+
throw new Error(`Unhandled lab backend: ${String(backend)}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function labRerunFlagsRequested(options) {
|
|
1285
|
+
return options.rerunFailedFrom !== undefined || options.lanes !== undefined;
|
|
1286
|
+
}
|
|
1287
|
+
function writeUnsupportedRerunFlagsResult(args, backend) {
|
|
1288
|
+
const result = {
|
|
1289
|
+
schema: "humanish.run-result.v1",
|
|
1290
|
+
ok: false,
|
|
1291
|
+
cwd: resolve(args.options.cwd),
|
|
1292
|
+
warnings: [],
|
|
1293
|
+
error: {
|
|
1294
|
+
code: "HUMANISH_UNSUPPORTED_RERUN_FLAGS",
|
|
1295
|
+
message: `--rerun-failed-from/--lanes apply only to CUA fan-out labs; this lab resolved to ${backend}.`
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
writeResult(args.command, args.io, result, formatRunHuman);
|
|
1299
|
+
args.io.setExitCode(2);
|
|
1300
|
+
}
|
|
1301
|
+
async function runSyntheticBackend(args) {
|
|
1302
|
+
const simCount = parseLabCount(args.options.sims, args.config.actors[0]?.count ?? 4);
|
|
1303
|
+
if (simCount === null) {
|
|
1304
|
+
const result = {
|
|
1305
|
+
schema: "humanish.run-result.v1",
|
|
1306
|
+
ok: false,
|
|
1307
|
+
cwd: args.options.cwd,
|
|
1308
|
+
warnings: [],
|
|
1309
|
+
error: {
|
|
1310
|
+
code: "HUMANISH_INVALID_SIM_COUNT",
|
|
1311
|
+
message: "--sims must be a positive integer."
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
writeResult(args.command, args.io, result, formatRunHuman);
|
|
1315
|
+
args.io.setExitCode(2);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const outcome = await runLab(args.config, {
|
|
1319
|
+
cwd: args.options.cwd,
|
|
1320
|
+
count: simCount,
|
|
1321
|
+
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
1322
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1323
|
+
});
|
|
1324
|
+
if (outcome.backend !== "synthetic") {
|
|
1325
|
+
throw new Error(`Expected synthetic backend, got ${outcome.backend}.`);
|
|
1326
|
+
}
|
|
1327
|
+
const runResult = outcome.result;
|
|
1328
|
+
if (args.mode === "run") {
|
|
1329
|
+
writeResult(args.command, args.io, runResult, formatRunHuman);
|
|
1330
|
+
args.io.setExitCode(runResult.ok ? 0 : 2);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (!runResult.ok || !runResult.runId) {
|
|
1334
|
+
writeResult(args.command, args.io, runResult, formatRunHuman);
|
|
1335
|
+
args.io.setExitCode(2);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const openOverride = args.options.open ?? args.config.defaults?.open;
|
|
1339
|
+
await renderAndMaybeFollowObserver({
|
|
1340
|
+
command: args.command,
|
|
1341
|
+
cwd: args.options.cwd,
|
|
1342
|
+
io: args.io,
|
|
1343
|
+
port: args.options.port ?? "0",
|
|
1344
|
+
runInput: runResult.runId,
|
|
1345
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1346
|
+
...(openOverride === undefined ? {} : { open: openOverride })
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Default browser-open policy for a lab backend run. Mirrors the observe/watch gate:
|
|
1351
|
+
* an explicit --open/--no-open wins; --json (machine mode) never auto-opens; otherwise a
|
|
1352
|
+
* lab-config `defaults.open` wins, and the final fallback opens only for an interactive
|
|
1353
|
+
* `watch` on a real TTY. Extracted so all lab backends share one gate (and one test).
|
|
1354
|
+
*/
|
|
1355
|
+
export function resolveBackendShouldOpen(args) {
|
|
1356
|
+
if (args.optionOpen === false)
|
|
1357
|
+
return false;
|
|
1358
|
+
if (args.wantsMachine)
|
|
1359
|
+
return args.optionOpen === true;
|
|
1360
|
+
return args.optionOpen ?? args.defaultsOpen ?? (process.stdout.isTTY === true && args.mode === "watch");
|
|
1361
|
+
}
|
|
1362
|
+
async function runCuaBackend(args) {
|
|
1363
|
+
const wantsMachine = wantsJson(args.command);
|
|
1364
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1365
|
+
optionOpen: args.options.open,
|
|
1366
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1367
|
+
mode: args.mode,
|
|
1368
|
+
wantsMachine
|
|
1369
|
+
});
|
|
1370
|
+
const laneIds = parseLaneIds(args.options.lanes);
|
|
1371
|
+
if (laneIds.length > 0 && !args.options.rerunFailedFrom) {
|
|
1372
|
+
args.io.writeErr("error: --lanes requires --rerun-failed-from.\n");
|
|
1373
|
+
args.io.setExitCode(2);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const count = parseLabCount(args.options.count ?? args.options.sims, args.config.actors[0]?.count ?? 1);
|
|
1377
|
+
if (count === null) {
|
|
1378
|
+
args.io.writeErr("error: --count/--sims must be a positive integer.\n");
|
|
1379
|
+
args.io.setExitCode(2);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const outcome = await runLab(args.config, {
|
|
1383
|
+
cwd: args.options.cwd,
|
|
1384
|
+
// Watch mode opens the served Observer below instead of the static render.
|
|
1385
|
+
open: args.mode === "watch" ? false : shouldOpen,
|
|
1386
|
+
count,
|
|
1387
|
+
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
1388
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
|
|
1389
|
+
...(args.options.rerunFailedFrom === undefined
|
|
1390
|
+
? {}
|
|
1391
|
+
: {
|
|
1392
|
+
rerun: {
|
|
1393
|
+
sourceRunId: args.options.rerunFailedFrom,
|
|
1394
|
+
...(laneIds.length === 0 ? {} : { laneIds })
|
|
1395
|
+
}
|
|
1396
|
+
})
|
|
1397
|
+
});
|
|
1398
|
+
if (outcome.backend !== "cua") {
|
|
1399
|
+
throw new Error(`Expected cua backend, got ${outcome.backend}.`);
|
|
1400
|
+
}
|
|
1401
|
+
const result = outcome.result;
|
|
1402
|
+
writeResult(args.command, args.io, result, formatCuaLabHuman);
|
|
1403
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1404
|
+
// Watch mode serves the freshly rendered Observer (and opens it unless told not to).
|
|
1405
|
+
if (args.mode === "watch" && result.ok && !wantsMachine) {
|
|
1406
|
+
await renderAndMaybeFollowObserver({
|
|
1407
|
+
command: args.command,
|
|
1408
|
+
cwd: args.options.cwd,
|
|
1409
|
+
io: args.io,
|
|
1410
|
+
port: args.options.port ?? "0",
|
|
1411
|
+
runInput: result.runId,
|
|
1412
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1413
|
+
...(shouldOpen === undefined ? {} : { open: shouldOpen })
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
// Mirror of runCuaBackend: open semantics from defaults.open/--no-open/watch-mode, writeResult
|
|
1418
|
+
// with the scripted human formatter, exit code result.ok ? 0 : 2, watch-mode Observer follow.
|
|
1419
|
+
async function runScriptedBackend(args) {
|
|
1420
|
+
const wantsMachine = wantsJson(args.command);
|
|
1421
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1422
|
+
optionOpen: args.options.open,
|
|
1423
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1424
|
+
mode: args.mode,
|
|
1425
|
+
wantsMachine
|
|
1426
|
+
});
|
|
1427
|
+
const outcome = await runLab(args.config, {
|
|
1428
|
+
cwd: args.options.cwd,
|
|
1429
|
+
// Watch mode opens the served Observer below instead of the static render.
|
|
1430
|
+
open: args.mode === "watch" ? false : shouldOpen,
|
|
1431
|
+
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
1432
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1433
|
+
});
|
|
1434
|
+
if (outcome.backend !== "scripted") {
|
|
1435
|
+
throw new Error(`Expected scripted backend, got ${outcome.backend}.`);
|
|
1436
|
+
}
|
|
1437
|
+
const result = outcome.result;
|
|
1438
|
+
writeResult(args.command, args.io, result, formatScriptedLabHuman);
|
|
1439
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1440
|
+
// Watch mode serves the freshly rendered Observer (and opens it unless told not to).
|
|
1441
|
+
if (args.mode === "watch" && result.ok && !wantsMachine) {
|
|
1442
|
+
await renderAndMaybeFollowObserver({
|
|
1443
|
+
command: args.command,
|
|
1444
|
+
cwd: args.options.cwd,
|
|
1445
|
+
io: args.io,
|
|
1446
|
+
port: args.options.port ?? "0",
|
|
1447
|
+
runInput: result.runId,
|
|
1448
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1449
|
+
...(shouldOpen === undefined ? {} : { open: shouldOpen })
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
// Mirror of runCuaBackend/runScriptedBackend: open semantics from defaults.open/--no-open/watch,
|
|
1454
|
+
// writeResult with the terminal human formatter, exit code result.ok ? 0 : 2, watch-mode follow.
|
|
1455
|
+
async function runTerminalBackend(args) {
|
|
1456
|
+
const wantsMachine = wantsJson(args.command);
|
|
1457
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1458
|
+
optionOpen: args.options.open,
|
|
1459
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1460
|
+
mode: args.mode,
|
|
1461
|
+
wantsMachine
|
|
1462
|
+
});
|
|
1463
|
+
const outcome = await runLab(args.config, {
|
|
1464
|
+
cwd: args.options.cwd,
|
|
1465
|
+
open: args.mode === "watch" ? false : shouldOpen,
|
|
1466
|
+
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
1467
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1468
|
+
});
|
|
1469
|
+
if (outcome.backend !== "terminal") {
|
|
1470
|
+
throw new Error(`Expected terminal backend, got ${outcome.backend}.`);
|
|
1471
|
+
}
|
|
1472
|
+
const result = outcome.result;
|
|
1473
|
+
writeResult(args.command, args.io, result, formatTerminalLabHuman);
|
|
1474
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1475
|
+
if (args.mode === "watch" && result.ok && !wantsMachine) {
|
|
1476
|
+
await renderAndMaybeFollowObserver({
|
|
1477
|
+
command: args.command,
|
|
1478
|
+
cwd: args.options.cwd,
|
|
1479
|
+
io: args.io,
|
|
1480
|
+
port: args.options.port ?? "0",
|
|
1481
|
+
runInput: result.runId,
|
|
1482
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1483
|
+
...(shouldOpen === undefined ? {} : { open: shouldOpen })
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async function runSharedWorldBackend(args) {
|
|
1488
|
+
const wantsMachine = wantsJson(args.command);
|
|
1489
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1490
|
+
optionOpen: args.options.open,
|
|
1491
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1492
|
+
mode: args.mode,
|
|
1493
|
+
wantsMachine
|
|
1494
|
+
});
|
|
1495
|
+
const outcome = await runLab(args.config, {
|
|
1496
|
+
cwd: args.options.cwd,
|
|
1497
|
+
open: args.mode === "watch" ? false : shouldOpen,
|
|
1498
|
+
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
1499
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1500
|
+
});
|
|
1501
|
+
if (outcome.backend !== "shared-world") {
|
|
1502
|
+
throw new Error(`Expected shared-world backend, got ${outcome.backend}.`);
|
|
1503
|
+
}
|
|
1504
|
+
const result = outcome.result;
|
|
1505
|
+
writeResult(args.command, args.io, result, formatSharedWorldLabHuman);
|
|
1506
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1507
|
+
if (args.mode === "watch" && result.ok && !wantsMachine) {
|
|
1508
|
+
await renderAndMaybeFollowObserver({
|
|
1509
|
+
command: args.command,
|
|
1510
|
+
cwd: args.options.cwd,
|
|
1511
|
+
io: args.io,
|
|
1512
|
+
port: args.options.port ?? "0",
|
|
1513
|
+
runInput: result.runId,
|
|
1514
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1515
|
+
...(shouldOpen === undefined ? {} : { open: shouldOpen })
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function formatSharedWorldLabHuman(result) {
|
|
1520
|
+
return [
|
|
1521
|
+
`humanish lab shared-world ${result.ok ? (result.dryRun ? "dry-run" : "live") : "failed"}`,
|
|
1522
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
1523
|
+
`run: ${result.runId}`,
|
|
1524
|
+
`lab: ${result.labId}`,
|
|
1525
|
+
`actor: ${result.actor}`,
|
|
1526
|
+
`topology: ${result.topology} (${result.roleCount} role${result.roleCount === 1 ? "" : "s"})`,
|
|
1527
|
+
`sequence: ${result.sequence.join(" -> ") || "(none)"}`,
|
|
1528
|
+
...(result.subject?.commit ? [`plane: ${result.subject.repo}@${result.subject.commit.slice(0, 12)}`] : []),
|
|
1529
|
+
...result.roles.map((role) => `role ${role.id} (${role.persona}): ${role.status}${role.session ? ` (${role.session.completionReason})` : ""}${role.skippedReason ? ` · ${role.skippedReason}` : ""}`),
|
|
1530
|
+
...(result.sandbox ? [`sandbox: ${result.sandbox.sandboxId} killed=${result.sandbox.killed ? "yes" : "no"}`] : []),
|
|
1531
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
1532
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
1533
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
1534
|
+
].join("\n") + "\n";
|
|
1535
|
+
}
|
|
1536
|
+
async function runConcurrentSharedWorldBackend(args) {
|
|
1537
|
+
const wantsMachine = wantsJson(args.command);
|
|
1538
|
+
const dryRun = resolveLabDryRun(args.config, args.options.dryRun, true) ?? true;
|
|
1539
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1540
|
+
optionOpen: args.options.open,
|
|
1541
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1542
|
+
mode: args.mode,
|
|
1543
|
+
wantsMachine
|
|
1544
|
+
});
|
|
1545
|
+
const wantsFollow = args.mode === "watch" && !wantsMachine && args.options.detach !== true && dryRun !== true;
|
|
1546
|
+
const port = parseObserverPort(args.options.port ?? "0");
|
|
1547
|
+
if (wantsFollow && port === null) {
|
|
1548
|
+
const result = {
|
|
1549
|
+
schema: "humanish.concurrent-shared-world-lab-result.v1",
|
|
1550
|
+
ok: false,
|
|
1551
|
+
cwd: args.options.cwd,
|
|
1552
|
+
labId: args.config.id,
|
|
1553
|
+
actor: args.config.actors[0]?.type ?? "",
|
|
1554
|
+
topology: "shared-world",
|
|
1555
|
+
topologyMode: "concurrent",
|
|
1556
|
+
roleCount: args.config.actors[0]?.lanes?.length ?? 0,
|
|
1557
|
+
concurrency: args.config.execution?.concurrency ?? 1,
|
|
1558
|
+
dryRun,
|
|
1559
|
+
runId: args.options.runId ?? "not-created",
|
|
1560
|
+
roles: [],
|
|
1561
|
+
warnings: [],
|
|
1562
|
+
error: {
|
|
1563
|
+
code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED",
|
|
1564
|
+
message: "--port must be an integer between 0 and 65535."
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
writeResult(args.command, args.io, result, formatConcurrentSharedWorldLabHuman);
|
|
1568
|
+
args.io.setExitCode(2);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
let server = null;
|
|
1572
|
+
let attachedObserver = null;
|
|
1573
|
+
let outcome;
|
|
1574
|
+
try {
|
|
1575
|
+
outcome = await runLab(args.config, {
|
|
1576
|
+
cwd: args.options.cwd,
|
|
1577
|
+
open: wantsFollow ? false : shouldOpen,
|
|
1578
|
+
dryRun,
|
|
1579
|
+
...(wantsFollow
|
|
1580
|
+
? {
|
|
1581
|
+
onObserverReady: async (observer) => {
|
|
1582
|
+
attachedObserver = observer;
|
|
1583
|
+
if (!server) {
|
|
1584
|
+
server = await serveObserver(observer, { open: shouldOpen, port: port ?? 0 });
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
: {}),
|
|
1589
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
const earlyServer = server;
|
|
1594
|
+
await earlyServer?.close().catch((cleanupError) => {
|
|
1595
|
+
args.io.writeErr(`watch cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
|
|
1596
|
+
});
|
|
1597
|
+
server = null;
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
if (outcome.backend !== "concurrent-shared-world") {
|
|
1601
|
+
throw new Error(`Expected concurrent-shared-world backend, got ${outcome.backend}.`);
|
|
1602
|
+
}
|
|
1603
|
+
const result = outcome.result;
|
|
1604
|
+
let output = result;
|
|
1605
|
+
if (server && attachedObserver) {
|
|
1606
|
+
const activeServer = server;
|
|
1607
|
+
output = {
|
|
1608
|
+
...result,
|
|
1609
|
+
observer: result.observer?.ok
|
|
1610
|
+
? withObserverServer(result.observer, activeServer)
|
|
1611
|
+
: withObserverServer(attachedObserver, activeServer),
|
|
1612
|
+
warnings: [
|
|
1613
|
+
...result.warnings,
|
|
1614
|
+
"Live concurrent shared-world server is polling observer-data.json with no-store caching.",
|
|
1615
|
+
...(activeServer.warning ? [activeServer.warning] : [])
|
|
1616
|
+
]
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
writeResult(args.command, args.io, output, formatConcurrentSharedWorldLabHuman);
|
|
1620
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1621
|
+
if (server && output.observer?.ok) {
|
|
1622
|
+
await followObserver(args.io, output.observer, server);
|
|
1623
|
+
}
|
|
1624
|
+
else if (args.mode === "watch" && result.ok && !wantsMachine) {
|
|
1625
|
+
await renderAndMaybeFollowObserver({
|
|
1626
|
+
command: args.command,
|
|
1627
|
+
cwd: args.options.cwd,
|
|
1628
|
+
io: args.io,
|
|
1629
|
+
port: args.options.port ?? "0",
|
|
1630
|
+
runInput: result.runId,
|
|
1631
|
+
...(args.options.detach === undefined ? {} : { detach: args.options.detach }),
|
|
1632
|
+
...(shouldOpen === undefined ? {} : { open: shouldOpen })
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function formatConcurrentSharedWorldLabHuman(result) {
|
|
1637
|
+
return [
|
|
1638
|
+
`humanish lab concurrent-shared-world ${result.ok ? (result.dryRun ? "dry-run" : "live") : "failed"}`,
|
|
1639
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
1640
|
+
`run: ${result.runId}`,
|
|
1641
|
+
`lab: ${result.labId}`,
|
|
1642
|
+
`actor: ${result.actor}`,
|
|
1643
|
+
`topology: ${result.topology}/${result.topologyMode} (${result.roleCount} persona${result.roleCount === 1 ? "" : "s"}, concurrency ${result.concurrency})`,
|
|
1644
|
+
...(result.host ? [`host: ${result.host}`] : []),
|
|
1645
|
+
...(result.subject?.commit ? [`plane: ${result.subject.repo}@${result.subject.commit.slice(0, 12)}`] : []),
|
|
1646
|
+
...(result.overlapProven === undefined ? [] : [`overlap: ${result.overlapProven ? "proven" : "not observed"} (capability at scale is live-backed only)`]),
|
|
1647
|
+
...result.roles.map((role) => `persona ${role.id} (${role.persona}): ${role.status}${role.session ? ` (${role.session.completionReason})` : ""} ${role.ok ? "ok" : "not-ok"}`),
|
|
1648
|
+
...(result.subjectSandbox ? [`subject sandbox: ${result.subjectSandbox.sandboxId} killed=${result.subjectSandbox.killed ? "yes" : "no"}`] : []),
|
|
1649
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
1650
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
1651
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
1652
|
+
].join("\n") + "\n";
|
|
1653
|
+
}
|
|
1654
|
+
function formatTerminalLabHuman(result) {
|
|
1655
|
+
return [
|
|
1656
|
+
`humanish lab terminal ${result.ok ? (result.dryRun ? "dry-run" : "live") : "failed"}`,
|
|
1657
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
1658
|
+
`run: ${result.runId}`,
|
|
1659
|
+
`lab: ${result.labId}`,
|
|
1660
|
+
`actor: ${result.actor}`,
|
|
1661
|
+
`product: ${result.product}`,
|
|
1662
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
1663
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
1664
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
1665
|
+
].join("\n") + "\n";
|
|
1666
|
+
}
|
|
1667
|
+
function formatScriptedLabHuman(result) {
|
|
1668
|
+
return [
|
|
1669
|
+
`humanish lab scripted ${result.ok ? (result.dryRun ? "dry-run" : "live") : "failed"}`,
|
|
1670
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
1671
|
+
`run: ${result.runId}`,
|
|
1672
|
+
`lab: ${result.labId}`,
|
|
1673
|
+
`actor: ${result.actor}`,
|
|
1674
|
+
`subject: ${result.appUrl}`,
|
|
1675
|
+
...(result.scenario
|
|
1676
|
+
? [`scenario: ${result.scenario.id} @ ${result.scenario.sourceDigest.slice(0, 12)} (${result.scenario.source}, ${result.scenario.steps} step${result.scenario.steps === 1 ? "" : "s"})`]
|
|
1677
|
+
: []),
|
|
1678
|
+
...result.sessions.map((session) => `session ${session.surface}: ${session.status} (${session.completionReason}) · ${session.reason} [${session.screenshots} screenshot${session.screenshots === 1 ? "" : "s"}]`),
|
|
1679
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
1680
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
1681
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
1682
|
+
].join("\n") + "\n";
|
|
1683
|
+
}
|
|
1684
|
+
function formatCuaLabHuman(result) {
|
|
1685
|
+
return [
|
|
1686
|
+
`humanish lab cua ${result.ok ? (result.dryRun ? "dry-run" : "live") : "failed"}`,
|
|
1687
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
1688
|
+
`run: ${result.runId}`,
|
|
1689
|
+
`lab: ${result.labId}`,
|
|
1690
|
+
`actor: ${result.actor}`,
|
|
1691
|
+
`subject: ${result.appUrl}`,
|
|
1692
|
+
...(result.subject?.source === "clone"
|
|
1693
|
+
? [`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(", ")}]` : ""}`]
|
|
1694
|
+
: []),
|
|
1695
|
+
...(result.rerun
|
|
1696
|
+
? [`rerun: ${result.rerun.selectedLaneIds.join(", ")} from ${result.rerun.sourceRunId}`]
|
|
1697
|
+
: []),
|
|
1698
|
+
...(result.session
|
|
1699
|
+
? [`session: ${result.session.status} (${result.session.completionReason}) · ${result.session.reason}`,
|
|
1700
|
+
`screenshots: ${result.session.screenshots}`]
|
|
1701
|
+
: []),
|
|
1702
|
+
...(result.sandbox
|
|
1703
|
+
? [`sandbox: ${result.sandbox.sandboxId} stream=${result.sandbox.streamUrlPresent ? "connected" : "missing"} killed=${result.sandbox.killed ? "yes" : "no"}`]
|
|
1704
|
+
: []),
|
|
1705
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
1706
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
1707
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
1708
|
+
].join("\n") + "\n";
|
|
1709
|
+
}
|
|
1710
|
+
async function runSmokeBackend(args) {
|
|
1711
|
+
const fanout = args.config.subject.clone?.fanout ?? args.config.subject.repos?.length ?? DEFAULT_OSS_REPOS.length;
|
|
1712
|
+
const limit = parseLabCount(args.options.limit ?? args.options.sims, fanout);
|
|
1713
|
+
if (limit === null) {
|
|
1714
|
+
const result = {
|
|
1715
|
+
schema: "humanish.oss-lab-result.v1",
|
|
1716
|
+
ok: false,
|
|
1717
|
+
cleanup: { kept: Boolean(args.options.keep), sandboxRemoved: false },
|
|
1718
|
+
completedAt: new Date().toISOString(),
|
|
1719
|
+
cwd: args.options.cwd,
|
|
1720
|
+
error: {
|
|
1721
|
+
code: "HUMANISH_INVALID_OSS_LIMIT",
|
|
1722
|
+
message: "--limit must be a positive integer."
|
|
1723
|
+
},
|
|
1724
|
+
repos: [],
|
|
1725
|
+
runId: args.options.runId ?? "not-created",
|
|
1726
|
+
sandboxPath: ".humanish/tmp/oss-lab/not-created",
|
|
1727
|
+
startedAt: new Date().toISOString(),
|
|
1728
|
+
warnings: []
|
|
1729
|
+
};
|
|
1730
|
+
writeResult(args.command, args.io, result, formatOssLabHuman);
|
|
1731
|
+
args.io.setExitCode(2);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const repos = labReposOverride(args.options);
|
|
1735
|
+
const outcome = await runLab(args.config, {
|
|
1736
|
+
cwd: args.options.cwd,
|
|
1737
|
+
count: limit,
|
|
1738
|
+
...(repos === undefined ? {} : { repos }),
|
|
1739
|
+
...(args.options.keep === undefined ? {} : { keep: args.options.keep }),
|
|
1740
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1741
|
+
});
|
|
1742
|
+
if (outcome.backend !== "smoke") {
|
|
1743
|
+
throw new Error(`Expected smoke backend, got ${outcome.backend}.`);
|
|
1744
|
+
}
|
|
1745
|
+
const result = outcome.result;
|
|
1746
|
+
writeResult(args.command, args.io, result, formatOssLabHuman);
|
|
1747
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1748
|
+
}
|
|
1749
|
+
async function runMetaBackend(args) {
|
|
1750
|
+
const metaCountDefault = args.config.subject.clone?.fanout ?? args.config.subject.repos?.length ?? DEFAULT_OSS_REPOS.length;
|
|
1751
|
+
const count = parseLabCount(args.options.count ?? args.options.sims, metaCountDefault);
|
|
1752
|
+
const repos = labReposOverride(args.options);
|
|
1753
|
+
const port = parseObserverPort(args.options.port ?? "0");
|
|
1754
|
+
if (port === null) {
|
|
1755
|
+
const result = {
|
|
1756
|
+
schema: "humanish.oss-meta-lab-result.v1",
|
|
1757
|
+
ok: false,
|
|
1758
|
+
assignments: [],
|
|
1759
|
+
cwd: args.options.cwd,
|
|
1760
|
+
dryRun: args.options.dryRun === true,
|
|
1761
|
+
error: {
|
|
1762
|
+
code: "HUMANISH_META_RUN_FAILED",
|
|
1763
|
+
message: "--port must be an integer between 0 and 65535."
|
|
1764
|
+
},
|
|
1765
|
+
liveRequested: args.options.dryRun !== true,
|
|
1766
|
+
repos: repos ?? args.config.subject.repos ?? [],
|
|
1767
|
+
sandboxes: [],
|
|
1768
|
+
warnings: []
|
|
1769
|
+
};
|
|
1770
|
+
writeResult(args.command, args.io, result, formatOssMetaLabHuman);
|
|
1771
|
+
args.io.setExitCode(2);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const wantsMachine = wantsJson(args.command);
|
|
1775
|
+
const dryRun = resolveLabDryRun(args.config, args.options.dryRun, undefined);
|
|
1776
|
+
const shouldOpen = resolveBackendShouldOpen({
|
|
1777
|
+
optionOpen: args.options.open,
|
|
1778
|
+
defaultsOpen: args.config.defaults?.open,
|
|
1779
|
+
mode: args.mode,
|
|
1780
|
+
wantsMachine
|
|
1781
|
+
});
|
|
1782
|
+
const wantsFollow = args.mode === "watch" && !wantsMachine && args.options.detach !== true && dryRun !== true;
|
|
1783
|
+
const codexAppServer = args.options.codexAppServer ?? args.config.execution?.desktop?.codexAppServer;
|
|
1784
|
+
const repoOverrideRequested = (args.options.repo?.length ?? 0) > 0 || args.options.repos !== undefined;
|
|
1785
|
+
const defaultRedactRepos = repoOverrideRequested ? true : args.config.policies?.redactRepos;
|
|
1786
|
+
const redactRepoNames = args.options.redactRepos ?? defaultRedactRepos;
|
|
1787
|
+
let server = null;
|
|
1788
|
+
let liveRefresh = null;
|
|
1789
|
+
let result;
|
|
1790
|
+
try {
|
|
1791
|
+
const outcome = await runLab(args.config, {
|
|
1792
|
+
cwd: args.options.cwd,
|
|
1793
|
+
...(wantsFollow ? { completionTimeoutMs: 0 } : {}),
|
|
1794
|
+
...(codexAppServer === undefined ? {} : { codexAppServer }),
|
|
1795
|
+
...(wantsFollow
|
|
1796
|
+
? {
|
|
1797
|
+
onObserverReady: async (observer) => {
|
|
1798
|
+
if (!server) {
|
|
1799
|
+
server = await serveObserver(observer, { open: shouldOpen, port });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
: {}),
|
|
1804
|
+
open: wantsFollow ? false : shouldOpen,
|
|
1805
|
+
...(dryRun === undefined ? {} : { dryRun }),
|
|
1806
|
+
...(redactRepoNames === undefined ? {} : { redactRepos: redactRepoNames }),
|
|
1807
|
+
...(repos === undefined ? {} : { repos }),
|
|
1808
|
+
count: count === null ? Number.NaN : count,
|
|
1809
|
+
...(args.options.runId === undefined ? {} : { runId: args.options.runId })
|
|
1810
|
+
});
|
|
1811
|
+
if (outcome.backend !== "meta") {
|
|
1812
|
+
throw new Error(`Expected meta backend, got ${outcome.backend}.`);
|
|
1813
|
+
}
|
|
1814
|
+
result = outcome.result;
|
|
1815
|
+
}
|
|
1816
|
+
catch (error) {
|
|
1817
|
+
const earlyServer = server;
|
|
1818
|
+
await earlyServer?.close().catch((cleanupError) => {
|
|
1819
|
+
args.io.writeErr(`watch cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
|
|
1820
|
+
});
|
|
1821
|
+
server = null;
|
|
1822
|
+
throw error;
|
|
1823
|
+
}
|
|
1824
|
+
let output = result;
|
|
1825
|
+
if (server && shouldServeOssMetaLabObserver(result, { wantsFollow: true })) {
|
|
1826
|
+
liveRefresh = startOssMetaLabLiveRefresh(result);
|
|
1827
|
+
output = withOssMetaLabServer(result, server);
|
|
1828
|
+
}
|
|
1829
|
+
else if (shouldServeOssMetaLabObserver(result, { wantsFollow })) {
|
|
1830
|
+
server = await serveObserver(result.observer, { open: shouldOpen, port });
|
|
1831
|
+
liveRefresh = startOssMetaLabLiveRefresh(result);
|
|
1832
|
+
output = withOssMetaLabServer(result, server);
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
const earlyServer = server;
|
|
1836
|
+
await earlyServer?.close().catch((error) => {
|
|
1837
|
+
args.io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1838
|
+
});
|
|
1839
|
+
server = null;
|
|
1840
|
+
}
|
|
1841
|
+
const exitCode = exitCodeForOssMetaLab(output);
|
|
1842
|
+
writeResult(args.command, args.io, output, formatOssMetaLabHuman);
|
|
1843
|
+
args.io.setExitCode(exitCode);
|
|
1844
|
+
if (shouldForceExitAfterOssMetaLab(output, { detach: args.options.detach === true, wantsMachine })) {
|
|
1845
|
+
setTimeout(() => process.exit(exitCode), 50);
|
|
1846
|
+
}
|
|
1847
|
+
if (server && output.observer?.ok) {
|
|
1848
|
+
await followObserver(args.io, output.observer, server, output.liveRequested
|
|
1849
|
+
? {
|
|
1850
|
+
onStop: async () => {
|
|
1851
|
+
const cleanup = liveRefresh
|
|
1852
|
+
? await liveRefresh.cleanup()
|
|
1853
|
+
: await cleanupOssMetaLabSandboxes(output);
|
|
1854
|
+
return [
|
|
1855
|
+
`E2B sandbox cleanup killed ${cleanup.killed}, skipped ${cleanup.skipped}.`,
|
|
1856
|
+
...cleanup.errors.map((error) => `E2B sandbox cleanup error: ${error}`)
|
|
1857
|
+
];
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
: {});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function renderAndMaybeFollowObserver(args) {
|
|
1864
|
+
const port = parseObserverPort(args.port);
|
|
1865
|
+
if (port === null) {
|
|
1866
|
+
const result = {
|
|
1867
|
+
schema: "humanish.run-result.v1",
|
|
1868
|
+
ok: false,
|
|
1869
|
+
cwd: args.cwd,
|
|
1870
|
+
warnings: [],
|
|
1871
|
+
error: {
|
|
1872
|
+
code: "HUMANISH_INVALID_PORT",
|
|
1873
|
+
message: "--port must be an integer between 0 and 65535."
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
writeResult(args.command, args.io, result, formatRunHuman);
|
|
1877
|
+
args.io.setExitCode(2);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const wantsMachine = wantsJson(args.command);
|
|
1881
|
+
const shouldOpen = args.open === false ? false : args.open === true ? true : !wantsMachine && process.stdout.isTTY === true;
|
|
1882
|
+
const wantsFollow = !wantsMachine && args.detach !== true;
|
|
1883
|
+
const rendered = await renderObserver(args.cwd, args.runInput, { open: wantsFollow ? false : shouldOpen });
|
|
1884
|
+
let server = null;
|
|
1885
|
+
let result = rendered;
|
|
1886
|
+
if (rendered.ok && wantsFollow) {
|
|
1887
|
+
server = await serveObserver(rendered, { open: shouldOpen, port });
|
|
1888
|
+
result = withObserverServer(rendered, server);
|
|
1889
|
+
}
|
|
1890
|
+
writeResult(args.command, args.io, result, formatObserverHuman);
|
|
1891
|
+
args.io.setExitCode(result.ok ? 0 : 2);
|
|
1892
|
+
if (result.ok && server) {
|
|
1893
|
+
await followObserver(args.io, result, server);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
async function applyEnvFileOption(args) {
|
|
1897
|
+
if (!args.envFile) {
|
|
1898
|
+
return true;
|
|
1899
|
+
}
|
|
1900
|
+
const result = await loadEnvFile(args.cwd, args.envFile);
|
|
1901
|
+
if (result.ok) {
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
writeResult(args.command, args.io, result, formatEnvFileHuman);
|
|
1905
|
+
args.io.setExitCode(2);
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
function labReposOverride(options) {
|
|
1909
|
+
const override = [
|
|
1910
|
+
...(options.repo ?? []),
|
|
1911
|
+
...(options.repos ? [options.repos] : [])
|
|
1912
|
+
];
|
|
1913
|
+
return override.length > 0 ? override : undefined;
|
|
1914
|
+
}
|
|
1915
|
+
function parseLabCount(value, fallback) {
|
|
1916
|
+
return value === undefined ? fallback : parsePositiveInteger(value);
|
|
1917
|
+
}
|
|
1918
|
+
function parseLaneIds(value) {
|
|
1919
|
+
if (value === undefined) {
|
|
1920
|
+
return [];
|
|
1921
|
+
}
|
|
1922
|
+
const seen = new Set();
|
|
1923
|
+
const laneIds = [];
|
|
1924
|
+
for (const raw of value.split(",")) {
|
|
1925
|
+
const laneId = raw.trim();
|
|
1926
|
+
if (!laneId || seen.has(laneId))
|
|
1927
|
+
continue;
|
|
1928
|
+
seen.add(laneId);
|
|
1929
|
+
laneIds.push(laneId);
|
|
1930
|
+
}
|
|
1931
|
+
return laneIds;
|
|
1932
|
+
}
|
|
1933
|
+
function withObserverServer(rendered, server) {
|
|
1934
|
+
return {
|
|
1935
|
+
...rendered,
|
|
1936
|
+
observerUrl: server.url,
|
|
1937
|
+
serverUrl: server.url,
|
|
1938
|
+
opened: server.opened,
|
|
1939
|
+
...(server.openCommand ? { openCommand: server.openCommand } : {}),
|
|
1940
|
+
warnings: [
|
|
1941
|
+
...rendered.warnings,
|
|
1942
|
+
"Live observer server is polling observer-data.json with no-store caching.",
|
|
1943
|
+
...(server.warning ? [server.warning] : [])
|
|
1944
|
+
]
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function withOssMetaLabServer(result, server) {
|
|
1948
|
+
return {
|
|
1949
|
+
...result,
|
|
1950
|
+
observer: {
|
|
1951
|
+
...result.observer,
|
|
1952
|
+
observerUrl: server.url,
|
|
1953
|
+
serverUrl: server.url,
|
|
1954
|
+
opened: server.opened,
|
|
1955
|
+
...(server.openCommand ? { openCommand: server.openCommand } : {}),
|
|
1956
|
+
warnings: [
|
|
1957
|
+
...result.observer.warnings,
|
|
1958
|
+
"Live OSS meta-lab server is polling observer-data.json with no-store caching.",
|
|
1959
|
+
...(server.warning ? [server.warning] : [])
|
|
1960
|
+
]
|
|
1961
|
+
},
|
|
1962
|
+
warnings: [
|
|
1963
|
+
...result.warnings,
|
|
1964
|
+
"Live OSS meta-lab server is polling observer-data.json with no-store caching.",
|
|
1965
|
+
...(server.warning ? [server.warning] : [])
|
|
1966
|
+
]
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
// Exported so tests/program.test.ts can drive the command-boundary catch-all's
|
|
1970
|
+
// post-write guard (invocationEnvelopeAlreadyWritten above) through the same
|
|
1971
|
+
// funnel every real command uses, without duplicating its stdout-vs-formatHuman
|
|
1972
|
+
// branching. Not re-exported from src/index.ts; this stays an internal seam.
|
|
1973
|
+
export function writeResult(command, io, result, formatHuman) {
|
|
1974
|
+
if (wantsJson(command)) {
|
|
1975
|
+
io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
io.writeOut(formatHuman(result));
|
|
1979
|
+
}
|
|
1980
|
+
markInvocationEnvelopeWritten(command);
|
|
1981
|
+
}
|
|
1982
|
+
function formatDoctorHuman(result) {
|
|
1983
|
+
return [
|
|
1984
|
+
`humanish doctor ${result.ok ? "ok" : "needs setup"}`,
|
|
1985
|
+
`cwd: ${result.cwd}`,
|
|
1986
|
+
...result.checks.map((check) => `- ${check.ok ? "ok" : "missing"} ${check.name}: ${check.message}`)
|
|
1987
|
+
].join("\n") + "\n";
|
|
1988
|
+
}
|
|
1989
|
+
function formatRunHuman(result) {
|
|
1990
|
+
if (!result.ok) {
|
|
1991
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
1992
|
+
}
|
|
1993
|
+
return [
|
|
1994
|
+
`humanish run ${result.mode}`,
|
|
1995
|
+
`run: ${result.runId}`,
|
|
1996
|
+
...(result.simCount === undefined ? [] : [`sims: ${result.simCount}`]),
|
|
1997
|
+
`bundle: ${result.bundlePath}`,
|
|
1998
|
+
`review: ${result.reviewPath}`,
|
|
1999
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2000
|
+
].join("\n") + "\n";
|
|
2001
|
+
}
|
|
2002
|
+
function formatVerifyHuman(result) {
|
|
2003
|
+
return [
|
|
2004
|
+
`humanish verify ${result.ok ? "passed" : "failed"}`,
|
|
2005
|
+
`run: ${result.run}`,
|
|
2006
|
+
`share-safety: ${result.shareSafety.status}`,
|
|
2007
|
+
...result.shareSafety.reasons.map((reason) => `share-safety reason: ${reason.code}: ${reason.message}`),
|
|
2008
|
+
...result.checks.map((check) => `- ${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`),
|
|
2009
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2010
|
+
].join("\n") + "\n";
|
|
2011
|
+
}
|
|
2012
|
+
function formatCleanupHuman(result) {
|
|
2013
|
+
if (!result.ok && result.error) {
|
|
2014
|
+
return `${result.error.code}: ${result.error.message}\n`;
|
|
2015
|
+
}
|
|
2016
|
+
return [
|
|
2017
|
+
`humanish cleanup ${result.ok ? "passed" : "failed"}`,
|
|
2018
|
+
`run: ${result.runId ?? result.run}`,
|
|
2019
|
+
`resources: killed ${result.summary.killed}, already-clean ${result.summary.alreadyClean}, skipped ${result.summary.skipped}, failed ${result.summary.failed}`,
|
|
2020
|
+
...(result.cleanupPath ? [`cleanup: ${result.cleanupPath}`] : []),
|
|
2021
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2022
|
+
].join("\n") + "\n";
|
|
2023
|
+
}
|
|
2024
|
+
function formatRunsHuman(result) {
|
|
2025
|
+
if (!result.ok) {
|
|
2026
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2027
|
+
}
|
|
2028
|
+
if (result.runs.length === 0) {
|
|
2029
|
+
return `No Humanish runs found in ${result.cwd}\n`;
|
|
2030
|
+
}
|
|
2031
|
+
return [
|
|
2032
|
+
`latest: ${result.latest ?? "none"}`,
|
|
2033
|
+
...result.runs.map((run) => `- ${run.runId} ${run.mode ?? "unknown"} ${run.createdAt ?? "unknown"} ${run.path}`)
|
|
2034
|
+
].join("\n") + "\n";
|
|
2035
|
+
}
|
|
2036
|
+
function formatObserverHuman(result) {
|
|
2037
|
+
if (!result.ok) {
|
|
2038
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2039
|
+
}
|
|
2040
|
+
return [
|
|
2041
|
+
"humanish observer rendered",
|
|
2042
|
+
`run: ${result.run}`,
|
|
2043
|
+
`observer: ${result.observerPath}`,
|
|
2044
|
+
...(result.observerUrl ? [`url: ${result.observerUrl}`] : []),
|
|
2045
|
+
...(result.opened === undefined ? [] : [`opened: ${result.opened ? "yes" : "no"}`]),
|
|
2046
|
+
`bundle: ${result.bundlePath}`,
|
|
2047
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2048
|
+
].join("\n") + "\n";
|
|
2049
|
+
}
|
|
2050
|
+
function formatCodexAppServerUiHuman(result) {
|
|
2051
|
+
if (!result.ok) {
|
|
2052
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2053
|
+
}
|
|
2054
|
+
return [
|
|
2055
|
+
"humanish codex app-server",
|
|
2056
|
+
`url: ${result.url ?? "not-started"}`,
|
|
2057
|
+
`status: ${result.status ?? "unknown"}`,
|
|
2058
|
+
`state: ${result.stateFile ?? "none"}`,
|
|
2059
|
+
`reason: ${result.reason}`
|
|
2060
|
+
].join("\n") + "\n";
|
|
2061
|
+
}
|
|
2062
|
+
function formatEnvFileHuman(result) {
|
|
2063
|
+
if (!result.ok) {
|
|
2064
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2065
|
+
}
|
|
2066
|
+
return [
|
|
2067
|
+
"humanish env-file loaded",
|
|
2068
|
+
`env-file: ${result.envFile}`,
|
|
2069
|
+
`loaded: ${result.loaded.length ? result.loaded.join(", ") : "none"}`,
|
|
2070
|
+
`skipped-existing: ${result.skipped.length ? result.skipped.join(", ") : "none"}`
|
|
2071
|
+
].join("\n") + "\n";
|
|
2072
|
+
}
|
|
2073
|
+
function formatLabListHuman(result) {
|
|
2074
|
+
if (result.labs.length === 0) {
|
|
2075
|
+
return [
|
|
2076
|
+
`No Humanish labs found in ${result.cwd}`,
|
|
2077
|
+
"Create one under humanish/labs/*.yaml, .humanish/labs/*.yaml, or pass a .yaml path.",
|
|
2078
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2079
|
+
].join("\n") + "\n";
|
|
2080
|
+
}
|
|
2081
|
+
return [
|
|
2082
|
+
"humanish labs",
|
|
2083
|
+
...result.labs.map((lab) => `- ${lab.id} ${lab.source} ${lab.origin} ${lab.path}${lab.title ? ` (${lab.title})` : ""}`),
|
|
2084
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2085
|
+
].join("\n") + "\n";
|
|
2086
|
+
}
|
|
2087
|
+
function formatLabInspectHuman(result) {
|
|
2088
|
+
if (!result.ok || !result.config) {
|
|
2089
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2090
|
+
}
|
|
2091
|
+
const config = result.config;
|
|
2092
|
+
return [
|
|
2093
|
+
"humanish lab",
|
|
2094
|
+
`id: ${config.id}`,
|
|
2095
|
+
`subject: ${config.subject.source}`,
|
|
2096
|
+
...(config.execution?.target ? [`execution: ${config.execution.target}`] : []),
|
|
2097
|
+
`actors: ${config.actors.map((actor) => actor.type).join(", ")}`,
|
|
2098
|
+
...(config.title ? [`title: ${config.title}`] : []),
|
|
2099
|
+
...(config.description ? [`description: ${config.description}`] : []),
|
|
2100
|
+
...(result.path ? [`path: ${result.path}`] : []),
|
|
2101
|
+
...(result.origin ? [`origin: ${result.origin}`] : []),
|
|
2102
|
+
...(config.subject.repos?.length ? [`repos: ${config.subject.repos.join(", ")}`] : []),
|
|
2103
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2104
|
+
].join("\n") + "\n";
|
|
2105
|
+
}
|
|
2106
|
+
function formatLabPreflightHuman(result) {
|
|
2107
|
+
const checkedTargets = result.targets.filter((target) => target.checked);
|
|
2108
|
+
const reachableTargets = checkedTargets.filter((target) => target.reachable === true);
|
|
2109
|
+
const blockedTargets = result.targets.filter((target) => target.status === "blocked");
|
|
2110
|
+
return [
|
|
2111
|
+
`humanish lab preflight ${result.ok ? "passed" : "failed"}`,
|
|
2112
|
+
`lab: ${result.labId ?? result.lab}`,
|
|
2113
|
+
...(result.backend ? [`backend: ${result.backend}`] : []),
|
|
2114
|
+
`reachability: ${result.reachability}`,
|
|
2115
|
+
`targets: ${checkedTargets.length ? `${reachableTargets.length}/${checkedTargets.length} reachable` : `${result.targets.length} declared, not checked`}`,
|
|
2116
|
+
...(blockedTargets.length ? [`blocked-targets: ${blockedTargets.length}`] : []),
|
|
2117
|
+
`spend: ${result.spend.e2bDesktop ? "one e2b desktop, no model calls" : "none"}`,
|
|
2118
|
+
...(result.sandbox.created
|
|
2119
|
+
? [`sandbox: created=yes killed=${result.sandbox.killed === true ? "yes" : "no"}`]
|
|
2120
|
+
: []),
|
|
2121
|
+
...result.checks.map((check) => `- ${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`),
|
|
2122
|
+
...(result.error ? [`error: ${result.error.code}: ${result.error.message}`] : []),
|
|
2123
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2124
|
+
].join("\n") + "\n";
|
|
2125
|
+
}
|
|
2126
|
+
function formatLabResolveFailureHuman(result) {
|
|
2127
|
+
return [
|
|
2128
|
+
`${result.error.code}: ${result.error.message}`,
|
|
2129
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2130
|
+
].join("\n") + "\n";
|
|
2131
|
+
}
|
|
2132
|
+
async function readCodexAppServerPrompt(options) {
|
|
2133
|
+
if (options.prompt !== undefined && options.prompt.trim()) {
|
|
2134
|
+
return options.prompt;
|
|
2135
|
+
}
|
|
2136
|
+
if (options.promptFile !== undefined && options.promptFile.trim()) {
|
|
2137
|
+
const promptPath = resolve(options.cwd, options.promptFile);
|
|
2138
|
+
const text = await readFile(promptPath, "utf8");
|
|
2139
|
+
return text.trim() || null;
|
|
2140
|
+
}
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
function codexAppServerUiError(cwd, code, message) {
|
|
2144
|
+
return {
|
|
2145
|
+
schema: "humanish.codex-app-server-ui-result.v1",
|
|
2146
|
+
ok: false,
|
|
2147
|
+
cwd: resolve(cwd),
|
|
2148
|
+
reason: message,
|
|
2149
|
+
error: { code, message }
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
function codexAppServerUiResultFromState(state) {
|
|
2153
|
+
return {
|
|
2154
|
+
schema: "humanish.codex-app-server-ui-result.v1",
|
|
2155
|
+
ok: state.status === "passed",
|
|
2156
|
+
cwd: state.cwd,
|
|
2157
|
+
reason: state.reason,
|
|
2158
|
+
stateFile: state.stateFile,
|
|
2159
|
+
status: state.status,
|
|
2160
|
+
...(state.url === undefined ? {} : { url: state.url })
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
function parsePositiveInteger(value) {
|
|
2164
|
+
if (!/^\d+$/.test(value)) {
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
const parsed = Number.parseInt(value, 10);
|
|
2168
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : null;
|
|
2169
|
+
}
|
|
2170
|
+
function parseTimeoutMs(value) {
|
|
2171
|
+
if (!/^\d+$/.test(value)) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
const parsed = Number.parseInt(value, 10);
|
|
2175
|
+
return parsed >= 1 && parsed <= 600_000 ? parsed : null;
|
|
2176
|
+
}
|
|
2177
|
+
function parseObserverPort(value) {
|
|
2178
|
+
if (!/^\d+$/.test(value)) {
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
const parsed = Number.parseInt(value, 10);
|
|
2182
|
+
return parsed >= 0 && parsed <= 65535 ? parsed : null;
|
|
2183
|
+
}
|
|
2184
|
+
export async function followObserver(io, result, server, options = {}) {
|
|
2185
|
+
io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
|
|
2186
|
+
io.writeOut("watching: press Ctrl-C to stop\n");
|
|
2187
|
+
await new Promise((resolve) => {
|
|
2188
|
+
const signalTarget = options.signalTarget ?? process;
|
|
2189
|
+
const signals = options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2190
|
+
const handlers = new Map();
|
|
2191
|
+
let stopping = false;
|
|
2192
|
+
const stop = (signal) => {
|
|
2193
|
+
if (stopping) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
stopping = true;
|
|
2197
|
+
for (const [registeredSignal, handler] of handlers.entries()) {
|
|
2198
|
+
signalTarget.removeListener(registeredSignal, handler);
|
|
2199
|
+
}
|
|
2200
|
+
io.setExitCode(exitCodeForSignal(signal));
|
|
2201
|
+
(async () => {
|
|
2202
|
+
try {
|
|
2203
|
+
await server.close();
|
|
2204
|
+
}
|
|
2205
|
+
catch (error) {
|
|
2206
|
+
io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2207
|
+
}
|
|
2208
|
+
if (options.onStop) {
|
|
2209
|
+
try {
|
|
2210
|
+
const messages = await options.onStop();
|
|
2211
|
+
for (const message of messages) {
|
|
2212
|
+
io.writeOut(`watch cleanup: ${message}\n`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
catch (error) {
|
|
2216
|
+
io.writeErr(`watch cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
io.writeOut("watch stopped\n");
|
|
2220
|
+
resolve();
|
|
2221
|
+
})();
|
|
2222
|
+
};
|
|
2223
|
+
for (const signal of signals) {
|
|
2224
|
+
const handler = () => stop(signal);
|
|
2225
|
+
handlers.set(signal, handler);
|
|
2226
|
+
signalTarget.once(signal, handler);
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
function exitCodeForSignal(signal) {
|
|
2231
|
+
switch (signal) {
|
|
2232
|
+
case "SIGINT":
|
|
2233
|
+
return 130;
|
|
2234
|
+
case "SIGTERM":
|
|
2235
|
+
return 143;
|
|
2236
|
+
case "SIGHUP":
|
|
2237
|
+
return 129;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
function formatFeedbackHuman(result) {
|
|
2241
|
+
if (!result.ok) {
|
|
2242
|
+
return `${result.error?.code}: ${result.error?.message}\n`;
|
|
2243
|
+
}
|
|
2244
|
+
return [
|
|
2245
|
+
"humanish feedback ready",
|
|
2246
|
+
`run: ${result.run}`,
|
|
2247
|
+
...(result.draftPath ? [`draft: ${result.draftPath}`] : []),
|
|
2248
|
+
...(result.issuePath ? [`issue: ${result.issuePath}`] : [])
|
|
2249
|
+
].join("\n") + "\n";
|
|
2250
|
+
}
|
|
2251
|
+
function formatOssLabHuman(result) {
|
|
2252
|
+
if (!result.ok && result.error) {
|
|
2253
|
+
return `${result.error.code}: ${result.error.message}\n`;
|
|
2254
|
+
}
|
|
2255
|
+
return [
|
|
2256
|
+
`humanish lab oss-smoke ${result.ok ? "passed" : "failed"}`,
|
|
2257
|
+
`run: ${result.runId}`,
|
|
2258
|
+
...(result.reportMarkdownPath ? [`report: ${result.reportMarkdownPath}`] : []),
|
|
2259
|
+
`sandbox: ${result.cleanup.kept ? result.sandboxPath : "removed"}`,
|
|
2260
|
+
...result.repos.map((repo) => {
|
|
2261
|
+
const passed = repo.steps.filter((step) => step.ok).length;
|
|
2262
|
+
return `- ${repo.ok ? "ok" : "fail"} ${repo.repo}: ${passed}/${repo.steps.length} steps, ${repo.changedFiles.length} changed files in disposable clone`;
|
|
2263
|
+
}),
|
|
2264
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2265
|
+
].join("\n") + "\n";
|
|
2266
|
+
}
|
|
2267
|
+
function formatOssMetaLabHuman(result) {
|
|
2268
|
+
return [
|
|
2269
|
+
`humanish lab oss ${result.ok ? (result.dryRun ? "dry-run" : "watch") : "failed"}`,
|
|
2270
|
+
...(result.error ? [`${result.error.code}: ${result.error.message}`] : []),
|
|
2271
|
+
`run: ${result.runId ?? "not-created"}`,
|
|
2272
|
+
`repos: ${result.repos.join(", ")}`,
|
|
2273
|
+
...(result.count === undefined ? [] : [`desktops: ${result.count}`]),
|
|
2274
|
+
...(result.observer?.observerPath ? [`observer: ${result.observer.observerPath}`] : []),
|
|
2275
|
+
...(result.observer?.observerUrl ? [`url: ${result.observer.observerUrl}`] : []),
|
|
2276
|
+
...(result.observer?.opened === undefined ? [] : [`opened: ${result.observer.opened ? "yes" : "no"}`]),
|
|
2277
|
+
...(result.observer?.bundlePath ? [`bundle: ${result.observer.bundlePath}`] : []),
|
|
2278
|
+
...result.assignments.map((assignment) => `- ${String(assignment.index).padStart(2, "0")} ${assignment.repo}: top-level desktop lane -> nested Humanish Observer`),
|
|
2279
|
+
...result.sandboxes.map((sandbox) => {
|
|
2280
|
+
const sandboxLabel = sandbox.sandboxId ? ` sandbox=${sandbox.sandboxId}` : "";
|
|
2281
|
+
const bootstrapLabel = sandbox.bootstrapStatus ? ` bootstrap=${sandbox.bootstrapStatus}` : "";
|
|
2282
|
+
const completionLabel = sandbox.completionStatus ? ` completion=${sandbox.completionStatus}` : "";
|
|
2283
|
+
const screenshotLabel = sandbox.screenshotPresent ? " screenshot=yes" : "";
|
|
2284
|
+
return `sandbox ${sandbox.streamId}: ${sandbox.repo} stream=${sandbox.urlPresent ? "connected" : "missing"}${bootstrapLabel}${completionLabel}${screenshotLabel}${sandboxLabel}`;
|
|
2285
|
+
}),
|
|
2286
|
+
...result.warnings.map((warning) => `warning: ${warning}`)
|
|
2287
|
+
].join("\n") + "\n";
|
|
2288
|
+
}
|
|
2289
|
+
function formatOssMetaLabCleanupHuman(result) {
|
|
2290
|
+
return [
|
|
2291
|
+
`humanish lab cleanup ${result.lab} ${result.ok ? "passed" : "failed"}`,
|
|
2292
|
+
...(result.cleanup.matched === undefined ? [] : [`matched: ${result.cleanup.matched}`]),
|
|
2293
|
+
`killed: ${result.cleanup.killed}`,
|
|
2294
|
+
`skipped: ${result.cleanup.skipped}`,
|
|
2295
|
+
...(result.cleanup.remaining === undefined ? [] : [`remaining: ${result.cleanup.remaining}`]),
|
|
2296
|
+
...result.cleanup.errors.map((error) => `error: ${error}`)
|
|
2297
|
+
].join("\n") + "\n";
|
|
2298
|
+
}
|
|
2299
|
+
function collectRepeated(value, previous) {
|
|
2300
|
+
return [...previous, value];
|
|
2301
|
+
}
|
|
2302
|
+
function formatInitHuman(result) {
|
|
2303
|
+
const title = result.ok
|
|
2304
|
+
? `humanish init ${result.mode}`
|
|
2305
|
+
: `humanish init ${result.mode} blocked`;
|
|
2306
|
+
const lines = [
|
|
2307
|
+
title,
|
|
2308
|
+
`cwd: ${result.cwd}`,
|
|
2309
|
+
"",
|
|
2310
|
+
"changes:",
|
|
2311
|
+
...result.changes.map(formatInitChange)
|
|
2312
|
+
];
|
|
2313
|
+
if (result.warnings.length > 0) {
|
|
2314
|
+
lines.push("", "warnings:", ...result.warnings.map((warning) => `- ${warning}`));
|
|
2315
|
+
}
|
|
2316
|
+
if (result.error) {
|
|
2317
|
+
lines.push("", `${result.error.code}: ${result.error.message}`);
|
|
2318
|
+
}
|
|
2319
|
+
if (result.mode === "needs-confirmation") {
|
|
2320
|
+
lines.push("", "Run with --dry-run --json to inspect or --yes to apply.");
|
|
2321
|
+
}
|
|
2322
|
+
return `${lines.join("\n")}\n`;
|
|
2323
|
+
}
|
|
2324
|
+
function formatInitChange(change) {
|
|
2325
|
+
return `- ${change.action.padEnd(6)} ${change.path} (${change.target}: ${change.reason})`;
|
|
2326
|
+
}
|
|
2327
|
+
export function shouldForceExitAfterOssMetaLab(output, options) {
|
|
2328
|
+
return output.liveRequested === true
|
|
2329
|
+
&& (options.detach || options.wantsMachine)
|
|
2330
|
+
&& output.sandboxes.some((sandbox) => sandbox.urlPresent);
|
|
2331
|
+
}
|
|
2332
|
+
export function shouldServeOssMetaLabObserver(output, options) {
|
|
2333
|
+
return options.wantsFollow
|
|
2334
|
+
&& output.observer?.ok === true;
|
|
2335
|
+
}
|
|
2336
|
+
export function exitCodeForOssMetaLab(output) {
|
|
2337
|
+
return output.ok ? 0 : 2;
|
|
2338
|
+
}
|
|
2339
|
+
function wantsJson(command) {
|
|
2340
|
+
let current = command;
|
|
2341
|
+
while (current) {
|
|
2342
|
+
if (current.opts().json === true) {
|
|
2343
|
+
return true;
|
|
2344
|
+
}
|
|
2345
|
+
current = current.parent ?? null;
|
|
2346
|
+
}
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
//# sourceMappingURL=program.js.map
|