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
|
@@ -0,0 +1,4529 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { runDesktopCommandOrThrow } from "./command-failure.js";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
|
|
9
|
+
import { loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
10
|
+
import { DEFAULT_OSS_REPOS, normalizeOssRepoSlugs, repoSlug, validateOssRepoSlug } from "./oss-lab.js";
|
|
11
|
+
import { redactOssRemoteTelemetryText } from "./oss-remote-telemetry.js";
|
|
12
|
+
import { bindExistingRunArtifactPaths, prepareExclusiveHumanishStorageDirectory, prepareReusableHumanishStorageDirectory, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
13
|
+
import { writeContainedOutputFile } from "./selected-output-paths.js";
|
|
14
|
+
import { buildRunSource, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, runDryRun } from "./run.js";
|
|
15
|
+
import { scoreOssMetaMeaningfulUse } from "./oss-meta-lab-scoring.js";
|
|
16
|
+
export const OSS_META_LAB_SCHEMA = "humanish.oss-meta-lab-result.v1";
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const liveRuntimeByResult = new WeakMap();
|
|
20
|
+
const OSS_META_LAB_PROVIDER_METADATA = {
|
|
21
|
+
mode: "oss-meta-lab",
|
|
22
|
+
tool: "humanish"
|
|
23
|
+
};
|
|
24
|
+
const OSS_META_LAB_REMOTE_ENV_NAMES = [
|
|
25
|
+
"HUMANISH_OSS_META_ACTOR_FIRST",
|
|
26
|
+
"HUMANISH_OSS_META_ACTOR_MODEL",
|
|
27
|
+
"HUMANISH_OSS_META_HOST_CODEX_ACTOR",
|
|
28
|
+
"HUMANISH_OSS_META_CODEX_APP_SERVER",
|
|
29
|
+
"HUMANISH_OSS_META_ACTOR_TIMEOUT_MS",
|
|
30
|
+
"HUMANISH_OSS_META_REQUIRE_ACTOR"
|
|
31
|
+
];
|
|
32
|
+
const OSS_META_LAB_ACTOR_AUTH_PLACEHOLDER = "CODEX_API_KEY or CODEX_ACCESS_TOKEN";
|
|
33
|
+
const OSS_META_LAB_ACTOR_PREFLIGHT_PLACEHOLDER = "Codex actor API quota/auth preflight";
|
|
34
|
+
const OSS_META_LAB_HOST_ACTOR_PLACEHOLDER = "host Codex actor plan";
|
|
35
|
+
export function buildOssRepoAssignments(repos, count) {
|
|
36
|
+
return Array.from({ length: count }, (_, index) => {
|
|
37
|
+
const repo = repos[index % repos.length];
|
|
38
|
+
if (!repo) {
|
|
39
|
+
throw new Error("At least one OSS repo is required.");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
index: index + 1,
|
|
43
|
+
repo,
|
|
44
|
+
scenarioId: `oss-meta-${repoSlug(repo)}`,
|
|
45
|
+
simId: `oss-${String(index + 1).padStart(2, "0")}`,
|
|
46
|
+
streamId: `oss-${String(index + 1).padStart(2, "0")}-desktop`
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function collectOssMetaLabRemoteEnv(env) {
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const name of OSS_META_LAB_REMOTE_ENV_NAMES) {
|
|
53
|
+
const value = env[name]?.trim();
|
|
54
|
+
if (value) {
|
|
55
|
+
result[name] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function githubTokenFromEnv(env) {
|
|
61
|
+
return env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim() || env.GITHUB_PAT?.trim() || "";
|
|
62
|
+
}
|
|
63
|
+
async function createGitHubAskPassEnv(root, env) {
|
|
64
|
+
await mkdir(root, { recursive: true });
|
|
65
|
+
const askPassPath = path.join(root, `git-askpass-${randomBytes(4).toString("hex")}.sh`);
|
|
66
|
+
await writeFile(askPassPath, [
|
|
67
|
+
"#!/usr/bin/env bash",
|
|
68
|
+
"case \"$1\" in",
|
|
69
|
+
" *Username*) echo \"x-access-token\" ;;",
|
|
70
|
+
" *Password*) echo \"${HUMANISH_GITHUB_TOKEN_RUNTIME:-}\" ;;",
|
|
71
|
+
" *) echo \"\" ;;",
|
|
72
|
+
"esac",
|
|
73
|
+
""
|
|
74
|
+
].join("\n"), { encoding: "utf8", mode: 0o700 });
|
|
75
|
+
const token = githubTokenFromEnv(env);
|
|
76
|
+
return {
|
|
77
|
+
...gitCredentialIsolatedEnv(env),
|
|
78
|
+
GIT_ASKPASS: askPassPath,
|
|
79
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
80
|
+
...(token ? { HUMANISH_GITHUB_TOKEN_RUNTIME: token } : {})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function gitEnvWithoutGitHubToken(env) {
|
|
84
|
+
return {
|
|
85
|
+
...gitCredentialIsolatedEnv(env),
|
|
86
|
+
GIT_ASKPASS: "false",
|
|
87
|
+
SSH_ASKPASS: "false",
|
|
88
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function gitCredentialIsolatedEnv(env) {
|
|
92
|
+
const isolated = { ...env };
|
|
93
|
+
for (const key of Object.keys(isolated)) {
|
|
94
|
+
if (key.startsWith("GIT_CONFIG_")) {
|
|
95
|
+
delete isolated[key];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
delete isolated.GH_TOKEN;
|
|
99
|
+
delete isolated.GITHUB_PAT;
|
|
100
|
+
delete isolated.GITHUB_TOKEN;
|
|
101
|
+
delete isolated.GIT_ASKPASS;
|
|
102
|
+
delete isolated.HUMANISH_GITHUB_TOKEN_RUNTIME;
|
|
103
|
+
delete isolated.SSH_ASKPASS;
|
|
104
|
+
return {
|
|
105
|
+
...isolated,
|
|
106
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
107
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
108
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function runGitRepoAccessProbe(execImpl, cwd, repoUrl, env, sourceEnv) {
|
|
112
|
+
await execImpl("git", ["-c", "credential.helper=", "ls-remote", "--exit-code", repoUrl, "HEAD"], {
|
|
113
|
+
cwd,
|
|
114
|
+
env,
|
|
115
|
+
maxBuffer: 256 * 1024,
|
|
116
|
+
timeout: readPositiveInt(sourceEnv.HUMANISH_OSS_META_REPO_PREFLIGHT_TIMEOUT_MS, 45_000)
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export async function preflightOssMetaRepoAccess(args) {
|
|
120
|
+
const execImpl = args.execFileImpl ?? execFileAsync;
|
|
121
|
+
const tokenPresent = Boolean(githubTokenFromEnv(args.env));
|
|
122
|
+
const rootId = `repo-access-${randomBytes(4).toString("hex")}`;
|
|
123
|
+
const preparedRoot = await prepareExclusiveHumanishStorageDirectory(args.cwd, "tmp", rootId);
|
|
124
|
+
const root = await pinCleanupDirectory(preparedRoot);
|
|
125
|
+
try {
|
|
126
|
+
await mkdir(root.physicalPath, { recursive: true });
|
|
127
|
+
const tokenGitEnv = tokenPresent ? await createGitHubAskPassEnv(root.physicalPath, args.env) : undefined;
|
|
128
|
+
const anonymousGitEnv = gitEnvWithoutGitHubToken(args.env);
|
|
129
|
+
const results = [];
|
|
130
|
+
for (const assignment of args.assignments) {
|
|
131
|
+
const repoUrl = `https://github.com/${assignment.repo}.git`;
|
|
132
|
+
let anonymousError;
|
|
133
|
+
try {
|
|
134
|
+
await runGitRepoAccessProbe(execImpl, root.physicalPath, repoUrl, anonymousGitEnv, args.env);
|
|
135
|
+
results.push({
|
|
136
|
+
ok: true,
|
|
137
|
+
reason: tokenPresent
|
|
138
|
+
? "GitHub repo clone access preflight passed with anonymous public clone access."
|
|
139
|
+
: "GitHub repo clone access preflight passed without token auth.",
|
|
140
|
+
repo: assignment.repo,
|
|
141
|
+
streamId: assignment.streamId,
|
|
142
|
+
tokenPresent
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
anonymousError = error;
|
|
148
|
+
}
|
|
149
|
+
let tokenError;
|
|
150
|
+
if (tokenGitEnv) {
|
|
151
|
+
try {
|
|
152
|
+
await runGitRepoAccessProbe(execImpl, root.physicalPath, repoUrl, tokenGitEnv, args.env);
|
|
153
|
+
results.push({
|
|
154
|
+
ok: true,
|
|
155
|
+
reason: "GitHub repo clone access preflight passed with token auth after anonymous clone access failed.",
|
|
156
|
+
repo: assignment.repo,
|
|
157
|
+
streamId: assignment.streamId,
|
|
158
|
+
tokenPresent: true
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
tokenError = error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
results.push({
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: repoAccessFailureReason({
|
|
169
|
+
anonymousError,
|
|
170
|
+
redactRepoName: args.redactRepoNames === true,
|
|
171
|
+
repo: assignment.repo,
|
|
172
|
+
tokenError,
|
|
173
|
+
tokenPresent
|
|
174
|
+
}),
|
|
175
|
+
repo: assignment.repo,
|
|
176
|
+
streamId: assignment.streamId,
|
|
177
|
+
tokenPresent
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (await validatePinnedCleanupDirectory(root).catch(() => false)) {
|
|
184
|
+
await rm(root.physicalPath, { recursive: true, force: true }).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function pinCleanupDirectory(directoryInput) {
|
|
189
|
+
const physicalPath = await realpath(path.resolve(directoryInput));
|
|
190
|
+
const stats = await lstat(physicalPath, { bigint: true });
|
|
191
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
192
|
+
throw new Error("OSS meta-lab cleanup roots must be physical directories.");
|
|
193
|
+
}
|
|
194
|
+
return Object.freeze({ birthtimeNs: stats.birthtimeNs, dev: stats.dev, ino: stats.ino, physicalPath });
|
|
195
|
+
}
|
|
196
|
+
async function validatePinnedCleanupDirectory(directory) {
|
|
197
|
+
const stats = await lstat(directory.physicalPath, { bigint: true });
|
|
198
|
+
return !stats.isSymbolicLink()
|
|
199
|
+
&& stats.isDirectory()
|
|
200
|
+
&& stats.birthtimeNs === directory.birthtimeNs
|
|
201
|
+
&& stats.dev === directory.dev
|
|
202
|
+
&& stats.ino === directory.ino
|
|
203
|
+
&& await realpath(directory.physicalPath) === directory.physicalPath;
|
|
204
|
+
}
|
|
205
|
+
export async function preflightOssMetaActorApiKey(args) {
|
|
206
|
+
const apiKey = args.env.CODEX_API_KEY?.trim() || args.env.OPENAI_API_KEY?.trim();
|
|
207
|
+
if (!apiKey) {
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
reason: "No API-key actor auth present; preflight skipped."
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
214
|
+
const model = args.env.HUMANISH_OSS_META_ACTOR_PREFLIGHT_MODEL?.trim() || "gpt-4.1-mini";
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetchImpl("https://api.openai.com/v1/responses", {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
Authorization: `Bearer ${apiKey}`,
|
|
220
|
+
"Content-Type": "application/json"
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
input: "Return a JSON object exactly like {\"status\":\"passed\"}. This is an actor auth preflight.",
|
|
224
|
+
max_output_tokens: 32,
|
|
225
|
+
model,
|
|
226
|
+
text: {
|
|
227
|
+
format: {
|
|
228
|
+
type: "json_object"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
if (response.ok) {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
reason: `OpenAI actor API-key preflight passed for ${model}.`,
|
|
237
|
+
status: response.status
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const body = await response.text().catch(() => "");
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: compactError(`OpenAI actor API-key preflight failed with HTTP ${response.status}: ${body}`),
|
|
244
|
+
status: response.status
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
reason: compactError(error)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function hostCodexActorRequested(env) {
|
|
255
|
+
return env.HUMANISH_OSS_META_HOST_CODEX_ACTOR === "1";
|
|
256
|
+
}
|
|
257
|
+
function codexAppServerModeRequested(env, explicit = false) {
|
|
258
|
+
return explicit || env.HUMANISH_OSS_META_CODEX_APP_SERVER === "1";
|
|
259
|
+
}
|
|
260
|
+
function remoteActorAuthRequested(env) {
|
|
261
|
+
return env.HUMANISH_OSS_META_ACTOR_FIRST === "1" || env.HUMANISH_OSS_META_REQUIRE_ACTOR === "1";
|
|
262
|
+
}
|
|
263
|
+
function actorRequired(env) {
|
|
264
|
+
return env.HUMANISH_OSS_META_REQUIRE_ACTOR === "1";
|
|
265
|
+
}
|
|
266
|
+
async function createHostActorPlans(_args) {
|
|
267
|
+
throw new Error("Host actor planning is disabled until live OSS meta-lab execution has an isolated credential boundary.");
|
|
268
|
+
}
|
|
269
|
+
function repoAccessFailureReason(args) {
|
|
270
|
+
const anonymous = sanitizeRepoAccessError(args.anonymousError, args.repo, args.redactRepoName);
|
|
271
|
+
const token = args.tokenError
|
|
272
|
+
? ` Token auth also failed: ${sanitizeRepoAccessError(args.tokenError, args.repo, args.redactRepoName)}`
|
|
273
|
+
: "";
|
|
274
|
+
const authHint = args.tokenPresent
|
|
275
|
+
? "A GitHub token was present, but `git ls-remote` could not read the repo with token or anonymous access. Check token repo access and scopes."
|
|
276
|
+
: "No GitHub token was present. Public repos should pass unauthenticated; private repos need GH_TOKEN, GITHUB_TOKEN, or GITHUB_PAT with read access.";
|
|
277
|
+
return `${authHint} Anonymous access failed: ${anonymous}${token}`.replace(/\s+/g, " ").trim().slice(0, 420);
|
|
278
|
+
}
|
|
279
|
+
function sanitizeRepoAccessError(error, repo, redactRepoName) {
|
|
280
|
+
let message = compactError(error)
|
|
281
|
+
.replace(/github_pat_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
282
|
+
.replace(/\bHUMANISH_GITHUB_TOKEN_RUNTIME=[^\s]+/g, "HUMANISH_GITHUB_TOKEN_RUNTIME=[redacted-github-token]");
|
|
283
|
+
if (redactRepoName) {
|
|
284
|
+
message = message
|
|
285
|
+
.replaceAll(`https://github.com/${repo}.git`, "https://github.com/[redacted-authorized-repo].git")
|
|
286
|
+
.replaceAll(`github.com/${repo}.git`, "github.com/[redacted-authorized-repo].git")
|
|
287
|
+
.replaceAll(repo, "[redacted-authorized-repo]");
|
|
288
|
+
}
|
|
289
|
+
return message;
|
|
290
|
+
}
|
|
291
|
+
function blockedLiveDesktopsForRepoAccess(args) {
|
|
292
|
+
const checkedAt = new Date().toISOString();
|
|
293
|
+
const preflightByStream = new Map(args.preflight.map((result) => [result.streamId, result]));
|
|
294
|
+
const failedCount = args.preflight.filter((result) => !result.ok).length;
|
|
295
|
+
return args.assignments.map((assignment) => {
|
|
296
|
+
const repoLabel = args.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo;
|
|
297
|
+
const preflight = preflightByStream.get(assignment.streamId);
|
|
298
|
+
const reason = preflight?.ok
|
|
299
|
+
? `GitHub repo clone access preflight passed for ${repoLabel}, but live launch was skipped because ${failedCount} assigned repo${failedCount === 1 ? "" : "s"} failed preflight.`
|
|
300
|
+
: preflight?.reason ?? `GitHub repo clone access preflight did not run for ${repoLabel}.`;
|
|
301
|
+
return {
|
|
302
|
+
completion: {
|
|
303
|
+
actorStatus: "blocked",
|
|
304
|
+
appReason: reason,
|
|
305
|
+
appStatus: "blocked",
|
|
306
|
+
checkedAt,
|
|
307
|
+
logTail: reason,
|
|
308
|
+
nestedObserverPresent: false,
|
|
309
|
+
nestedVerifyPassed: false,
|
|
310
|
+
reason,
|
|
311
|
+
status: "blocked",
|
|
312
|
+
visualReason: "No headed desktop was launched before repo clone access was proven.",
|
|
313
|
+
visualStatus: "not_started",
|
|
314
|
+
visualWindowCount: 0
|
|
315
|
+
},
|
|
316
|
+
repo: repoLabel,
|
|
317
|
+
simId: assignment.simId,
|
|
318
|
+
streamId: assignment.streamId
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
export function normalizeHostActorRecommendedProof(value) {
|
|
323
|
+
const proof = cleanHostActorText(value, "");
|
|
324
|
+
if (!/\bhumanish\s+run\b/.test(proof)) {
|
|
325
|
+
return "Start the target app on a loopback URL, then run `humanish run --app-url http://127.0.0.1:<port> --sims 2`.";
|
|
326
|
+
}
|
|
327
|
+
if (/\s--(?:browser|viewport|persona|scenario)\b/.test(proof)) {
|
|
328
|
+
return "Start the target app on a loopback URL, then run `humanish run --app-url http://127.0.0.1:<port> --sims 2`.";
|
|
329
|
+
}
|
|
330
|
+
return proof;
|
|
331
|
+
}
|
|
332
|
+
function cleanHostActorText(value, fallback) {
|
|
333
|
+
const text = String(typeof value === "string" ? value : fallback)
|
|
334
|
+
.replace(/sk-[A-Za-z0-9_-]{20,}/g, "[redacted-openai-key]")
|
|
335
|
+
.replace(/e2b_[A-Za-z0-9_-]{12,}/g, "[redacted-e2b-key]")
|
|
336
|
+
.replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
337
|
+
.replace(/github_pat_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
338
|
+
.replace(/\s+/g, " ")
|
|
339
|
+
.trim()
|
|
340
|
+
.slice(0, 500);
|
|
341
|
+
return text || fallback;
|
|
342
|
+
}
|
|
343
|
+
export async function runOssMetaLab(options) {
|
|
344
|
+
const cwd = path.resolve(options.cwd);
|
|
345
|
+
const dryRun = options.dryRun === true;
|
|
346
|
+
const liveRequested = !dryRun;
|
|
347
|
+
const warnings = [];
|
|
348
|
+
const repos = normalizeOssRepoSlugs(options.repos);
|
|
349
|
+
const count = options.count ?? DEFAULT_OSS_REPOS.length;
|
|
350
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
351
|
+
return {
|
|
352
|
+
schema: OSS_META_LAB_SCHEMA,
|
|
353
|
+
ok: false,
|
|
354
|
+
assignments: [],
|
|
355
|
+
cwd,
|
|
356
|
+
dryRun,
|
|
357
|
+
error: {
|
|
358
|
+
code: "HUMANISH_INVALID_OSS_COUNT",
|
|
359
|
+
message: "--count must be a positive integer."
|
|
360
|
+
},
|
|
361
|
+
liveRequested,
|
|
362
|
+
repos,
|
|
363
|
+
sandboxes: [],
|
|
364
|
+
warnings
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const invalid = repos.find((repo) => !validateOssRepoSlug(repo));
|
|
368
|
+
if (invalid) {
|
|
369
|
+
return {
|
|
370
|
+
schema: OSS_META_LAB_SCHEMA,
|
|
371
|
+
ok: false,
|
|
372
|
+
assignments: [],
|
|
373
|
+
count,
|
|
374
|
+
cwd,
|
|
375
|
+
dryRun,
|
|
376
|
+
error: {
|
|
377
|
+
code: "HUMANISH_INVALID_OSS_REPO",
|
|
378
|
+
message: `Only GitHub owner/repo slugs are supported: ${invalid}`
|
|
379
|
+
},
|
|
380
|
+
liveRequested,
|
|
381
|
+
repos,
|
|
382
|
+
sandboxes: [],
|
|
383
|
+
warnings
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const redactRepoNames = options.redactRepoNames ?? (liveRequested && (Boolean(githubTokenFromEnv(process.env)) || Boolean(options.repos?.length)));
|
|
387
|
+
const assignments = buildOssRepoAssignments(repos, count);
|
|
388
|
+
const publicAssignments = redactAssignments(assignments, redactRepoNames);
|
|
389
|
+
const publicRepos = redactRepoNames ? publicAssignments.map((assignment) => assignment.repo) : repos;
|
|
390
|
+
if (liveRequested) {
|
|
391
|
+
return {
|
|
392
|
+
schema: OSS_META_LAB_SCHEMA,
|
|
393
|
+
ok: false,
|
|
394
|
+
assignments: publicAssignments,
|
|
395
|
+
count,
|
|
396
|
+
cwd,
|
|
397
|
+
dryRun,
|
|
398
|
+
error: {
|
|
399
|
+
code: "HUMANISH_OSS_META_LIVE_ISOLATION_REQUIRED",
|
|
400
|
+
message: "Live OSS meta-lab execution is unavailable because repository-derived instructions require an isolated credential boundary. Use --dry-run."
|
|
401
|
+
},
|
|
402
|
+
liveRequested,
|
|
403
|
+
repos: publicRepos,
|
|
404
|
+
sandboxes: [],
|
|
405
|
+
warnings
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const codexAppServerMode = codexAppServerModeRequested(process.env, options.codexAppServer === true);
|
|
409
|
+
const hostActorMode = liveRequested && hostCodexActorRequested(process.env);
|
|
410
|
+
const missingKeys = missingLiveKeys(process.env);
|
|
411
|
+
const runId = options.runId ?? makeMetaRunId();
|
|
412
|
+
const physicalCwd = await realpath(cwd);
|
|
413
|
+
const runResult = await runDryRun({
|
|
414
|
+
cwd: physicalCwd,
|
|
415
|
+
dryRun: true,
|
|
416
|
+
runId,
|
|
417
|
+
simCount: count
|
|
418
|
+
});
|
|
419
|
+
if (!runResult.ok || !runResult.runId) {
|
|
420
|
+
return {
|
|
421
|
+
schema: OSS_META_LAB_SCHEMA,
|
|
422
|
+
ok: false,
|
|
423
|
+
assignments: publicAssignments,
|
|
424
|
+
count,
|
|
425
|
+
cwd,
|
|
426
|
+
dryRun,
|
|
427
|
+
error: {
|
|
428
|
+
code: "HUMANISH_META_RUN_FAILED",
|
|
429
|
+
message: runResult.error?.message ?? "Failed to create OSS meta-lab run bundle."
|
|
430
|
+
},
|
|
431
|
+
liveRequested,
|
|
432
|
+
repos: publicRepos,
|
|
433
|
+
sandboxes: [],
|
|
434
|
+
warnings: [...warnings, ...runResult.warnings]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const preparedRunPaths = await bindExistingRunArtifactPaths(physicalCwd, runId);
|
|
438
|
+
const artifactRoot = preparedRunPaths;
|
|
439
|
+
const createdAt = new Date().toISOString();
|
|
440
|
+
const source = await buildRunSource({
|
|
441
|
+
capturedAt: createdAt,
|
|
442
|
+
cwd: physicalCwd,
|
|
443
|
+
humanishSource: "present",
|
|
444
|
+
packageName: "humanish"
|
|
445
|
+
});
|
|
446
|
+
const persistScreenshots = liveRequested;
|
|
447
|
+
let liveDesktops = [];
|
|
448
|
+
let substrateMissingKeys = [...missingKeys];
|
|
449
|
+
const initialBundle = buildMetaBundle({
|
|
450
|
+
assignments,
|
|
451
|
+
createdAt,
|
|
452
|
+
cwd,
|
|
453
|
+
dryRun,
|
|
454
|
+
liveDesktops,
|
|
455
|
+
liveRequested,
|
|
456
|
+
missingKeys: substrateMissingKeys,
|
|
457
|
+
redactRepoNames,
|
|
458
|
+
runId,
|
|
459
|
+
source
|
|
460
|
+
});
|
|
461
|
+
await writeMetaBundleArtifacts(artifactRoot, initialBundle);
|
|
462
|
+
let observer = await renderObserver(physicalCwd, runId, { open: false });
|
|
463
|
+
if (observer.ok && options.onObserverReady) {
|
|
464
|
+
await options.onObserverReady(observer);
|
|
465
|
+
await validatePreparedRunArtifactPaths(preparedRunPaths);
|
|
466
|
+
}
|
|
467
|
+
const shouldPreflightRepoAccess = liveRequested
|
|
468
|
+
&& missingKeys.length === 0
|
|
469
|
+
&& process.env.HUMANISH_OSS_META_SKIP_REPO_ACCESS_PREFLIGHT !== "1"
|
|
470
|
+
&& (Boolean(githubTokenFromEnv(process.env)) || Boolean(options.repos?.length));
|
|
471
|
+
const repoAccessPreflight = shouldPreflightRepoAccess
|
|
472
|
+
? await preflightOssMetaRepoAccess({
|
|
473
|
+
assignments,
|
|
474
|
+
cwd,
|
|
475
|
+
env: process.env,
|
|
476
|
+
redactRepoNames
|
|
477
|
+
})
|
|
478
|
+
: [];
|
|
479
|
+
const repoAccessPreflightBlocked = repoAccessPreflight.some((result) => !result.ok);
|
|
480
|
+
if (repoAccessPreflight.length > 0) {
|
|
481
|
+
const passed = repoAccessPreflight.filter((result) => result.ok).length;
|
|
482
|
+
warnings.push(`GitHub repo clone access preflight passed ${passed}/${repoAccessPreflight.length} assigned repo${repoAccessPreflight.length === 1 ? "" : "s"}.`);
|
|
483
|
+
for (const failed of repoAccessPreflight.filter((result) => !result.ok)) {
|
|
484
|
+
const assignment = assignments.find((candidate) => candidate.streamId === failed.streamId);
|
|
485
|
+
const repoLabel = assignment && redactRepoNames ? repoArtifactLabel(assignment) : failed.repo;
|
|
486
|
+
warnings.push(`${repoLabel}: ${failed.reason}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const hostActorPlanResults = hostActorMode && missingKeys.length === 0 && !repoAccessPreflightBlocked
|
|
490
|
+
? await createHostActorPlans({
|
|
491
|
+
assignments,
|
|
492
|
+
cwd,
|
|
493
|
+
redactRepoNames,
|
|
494
|
+
runId
|
|
495
|
+
})
|
|
496
|
+
: [];
|
|
497
|
+
const hostActorPlansByStream = new Map(hostActorPlanResults.flatMap((result) => result.plan?.status === "passed" ? [[result.streamId, result]] : []));
|
|
498
|
+
if (hostActorPlanResults.length > 0) {
|
|
499
|
+
const passed = hostActorPlanResults.filter((result) => result.plan?.status === "passed").length;
|
|
500
|
+
warnings.push(`Host Codex actor authored ${passed}/${hostActorPlanResults.length} public-safe Humanish plan${hostActorPlanResults.length === 1 ? "" : "s"}.`);
|
|
501
|
+
for (const failed of hostActorPlanResults.filter((result) => result.plan?.status !== "passed")) {
|
|
502
|
+
warnings.push(`Host Codex actor plan failed for ${redactRepoNames ? "[redacted-authorized-repo]" : failed.repo}: ${failed.error ?? failed.plan?.summary ?? "unknown failure"}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const hostActorPlanBlocked = hostActorMode
|
|
506
|
+
&& actorRequired(process.env)
|
|
507
|
+
&& hostActorPlanResults.length > 0
|
|
508
|
+
&& hostActorPlanResults.some((result) => result.plan?.status !== "passed");
|
|
509
|
+
const actorAuthPreflight = liveRequested
|
|
510
|
+
&& missingKeys.length === 0
|
|
511
|
+
&& !hostActorMode
|
|
512
|
+
&& actorRequired(process.env)
|
|
513
|
+
&& process.env.HUMANISH_OSS_META_SKIP_ACTOR_PREFLIGHT !== "1"
|
|
514
|
+
? await preflightOssMetaActorApiKey({ env: process.env })
|
|
515
|
+
: undefined;
|
|
516
|
+
const actorAuthPreflightBlocked = actorAuthPreflight !== undefined && !actorAuthPreflight.ok;
|
|
517
|
+
substrateMissingKeys = [
|
|
518
|
+
...missingKeys,
|
|
519
|
+
...(hostActorPlanBlocked ? [OSS_META_LAB_HOST_ACTOR_PLACEHOLDER] : []),
|
|
520
|
+
...(actorAuthPreflightBlocked ? [OSS_META_LAB_ACTOR_PREFLIGHT_PLACEHOLDER] : [])
|
|
521
|
+
];
|
|
522
|
+
if (actorAuthPreflight) {
|
|
523
|
+
warnings.push(actorAuthPreflight.ok
|
|
524
|
+
? actorAuthPreflight.reason
|
|
525
|
+
: `Remote Codex actor API-key preflight blocked live launch: ${actorAuthPreflight.reason}`);
|
|
526
|
+
}
|
|
527
|
+
let localPackage;
|
|
528
|
+
if (liveRequested && missingKeys.length === 0 && !repoAccessPreflightBlocked && !hostActorPlanBlocked && !actorAuthPreflightBlocked) {
|
|
529
|
+
try {
|
|
530
|
+
localPackage = await packLocalHumanishPackage(cwd, runId);
|
|
531
|
+
warnings.push(`Packed local humanish package for sandbox install (${localPackage.fileName}).`);
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
warnings.push(`Local humanish package pack failed; sandbox bootstrap will try public npm fallback. ${compactError(error)}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (liveRequested && missingKeys.length === 0 && !repoAccessPreflightBlocked && !hostActorPlanBlocked && !actorAuthPreflightBlocked) {
|
|
538
|
+
try {
|
|
539
|
+
liveDesktops = await launchLiveDesktops(assignments, {
|
|
540
|
+
codexAppServerMode,
|
|
541
|
+
cwd,
|
|
542
|
+
hostActorPlansByStream,
|
|
543
|
+
...(localPackage ? { localPackage } : {}),
|
|
544
|
+
redactRepoNames
|
|
545
|
+
});
|
|
546
|
+
const completionSummary = await pollLiveDesktopCompletions(liveDesktops, {
|
|
547
|
+
...(options.completionTimeoutMs === undefined
|
|
548
|
+
? {}
|
|
549
|
+
: {
|
|
550
|
+
timeoutMs: options.completionTimeoutMs,
|
|
551
|
+
timeoutReason: "attached watch mode serves the Observer immediately after desktop streams are created"
|
|
552
|
+
})
|
|
553
|
+
});
|
|
554
|
+
warnings.push(...completionSummary.warnings);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
warnings.push(compactError(error));
|
|
558
|
+
liveDesktops = assignments.map((assignment) => {
|
|
559
|
+
const hostActorPlanResult = hostActorPlanResults.find((result) => result.streamId === assignment.streamId);
|
|
560
|
+
return {
|
|
561
|
+
error: compactError(error),
|
|
562
|
+
...(hostActorPlanResult?.plan ? { hostActorPlan: hostActorPlanResult.plan } : {}),
|
|
563
|
+
...(hostActorPlanResult?.artifactPath ? { hostActorPlanPath: hostActorPlanResult.artifactPath } : {}),
|
|
564
|
+
repo: redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo,
|
|
565
|
+
simId: assignment.simId,
|
|
566
|
+
streamId: assignment.streamId
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (repoAccessPreflightBlocked) {
|
|
572
|
+
liveDesktops = blockedLiveDesktopsForRepoAccess({
|
|
573
|
+
assignments,
|
|
574
|
+
preflight: repoAccessPreflight,
|
|
575
|
+
redactRepoNames
|
|
576
|
+
});
|
|
577
|
+
warnings.push("Live E2B launch skipped because GitHub repo clone access preflight failed.");
|
|
578
|
+
}
|
|
579
|
+
else if (hostActorPlanBlocked) {
|
|
580
|
+
warnings.push("Live E2B launch skipped because required host Codex actor plan evidence did not pass preflight.");
|
|
581
|
+
}
|
|
582
|
+
else if (actorAuthPreflightBlocked) {
|
|
583
|
+
warnings.push("Live E2B launch skipped because required Codex actor API-key quota/auth preflight failed.");
|
|
584
|
+
}
|
|
585
|
+
const liveDesktopCount = liveDesktops.filter((desktop) => desktop.url).length;
|
|
586
|
+
const failedLiveDesktopCount = liveDesktops.filter((desktop) => desktop.error).length;
|
|
587
|
+
const startedBootstrapCount = liveDesktops.filter((desktop) => desktop.bootstrap?.status === "started").length;
|
|
588
|
+
const terminalCompletionCount = liveDesktops.filter((desktop) => isTerminalCompletion(desktop.completion)).length;
|
|
589
|
+
const runningAppCount = liveDesktops.filter((desktop) => desktop.completion?.appStatus === "running").length;
|
|
590
|
+
const visibleDesktopCount = liveDesktops.filter((desktop) => desktop.completion?.visualStatus === "visible").length;
|
|
591
|
+
if (liveDesktops.length > 0) {
|
|
592
|
+
warnings.push(`Launched ${liveDesktopCount}/${liveDesktops.length} live E2B desktop stream${liveDesktops.length === 1 ? "" : "s"}.`);
|
|
593
|
+
if (startedBootstrapCount > 0) {
|
|
594
|
+
warnings.push(`Started ${startedBootstrapCount}/${liveDesktops.length} visible bootstrap terminal${liveDesktops.length === 1 ? "" : "s"} for target app startup, nested Humanish setup, and ${codexAppServerMode ? "Codex app-server client surface" : "Codex actor attempt"}.`);
|
|
595
|
+
if (terminalCompletionCount > 0) {
|
|
596
|
+
warnings.push(`Classified ${terminalCompletionCount}/${startedBootstrapCount} bootstrap terminal state${startedBootstrapCount === 1 ? "" : "s"} from remote public-safe evidence.`);
|
|
597
|
+
warnings.push(`Detected ${runningAppCount}/${terminalCompletionCount} target app HTTP-ready surface${terminalCompletionCount === 1 ? "" : "s"} from remote public-safe evidence.`);
|
|
598
|
+
warnings.push(`Detected ${visibleDesktopCount}/${terminalCompletionCount} headed desktop visual layout${terminalCompletionCount === 1 ? "" : "s"} from remote public-safe evidence.`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
warnings.push(codexAppServerMode
|
|
603
|
+
? "Codex app-server client surfacing and nested Humanish execution remain the next substrate slice behind these live desktops."
|
|
604
|
+
: "Codex TUI injection and nested Humanish execution remain the next substrate slice behind these live desktops.");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (failedLiveDesktopCount > 0) {
|
|
608
|
+
warnings.push(`${failedLiveDesktopCount} E2B desktop launch${failedLiveDesktopCount === 1 ? "" : "es"} failed; see stream events in the Observer.`);
|
|
609
|
+
}
|
|
610
|
+
if (persistScreenshots) {
|
|
611
|
+
const screenshotSummary = await captureLiveDesktopScreenshots(artifactRoot, liveDesktops, { redactRepoNames });
|
|
612
|
+
warnings.push(...screenshotSummary.warnings);
|
|
613
|
+
}
|
|
614
|
+
const actorEvidenceSummary = await writeActorEvidenceArtifacts(artifactRoot, liveDesktops, {
|
|
615
|
+
assignments,
|
|
616
|
+
redactRepoNames
|
|
617
|
+
});
|
|
618
|
+
warnings.push(...actorEvidenceSummary.warnings);
|
|
619
|
+
const bundle = buildMetaBundle({
|
|
620
|
+
assignments,
|
|
621
|
+
createdAt,
|
|
622
|
+
cwd,
|
|
623
|
+
dryRun,
|
|
624
|
+
liveDesktops,
|
|
625
|
+
liveRequested,
|
|
626
|
+
missingKeys: substrateMissingKeys,
|
|
627
|
+
redactRepoNames,
|
|
628
|
+
runId,
|
|
629
|
+
source
|
|
630
|
+
});
|
|
631
|
+
await writeMetaBundleArtifacts(artifactRoot, bundle);
|
|
632
|
+
const finalObserver = await renderObserver(physicalCwd, runId, { open: options.open === true });
|
|
633
|
+
Object.assign(observer, finalObserver);
|
|
634
|
+
if (observer.ok) {
|
|
635
|
+
attachObserverRuntimeStreamUrls(observer, liveDesktops
|
|
636
|
+
.filter((desktop) => desktop.url)
|
|
637
|
+
.map((desktop) => ({
|
|
638
|
+
streamId: desktop.streamId,
|
|
639
|
+
url: desktop.url
|
|
640
|
+
})));
|
|
641
|
+
}
|
|
642
|
+
const outcome = classifyMetaLabOutcome({
|
|
643
|
+
dryRun,
|
|
644
|
+
liveDesktops,
|
|
645
|
+
liveRequested,
|
|
646
|
+
missingKeys: substrateMissingKeys
|
|
647
|
+
});
|
|
648
|
+
const result = {
|
|
649
|
+
schema: OSS_META_LAB_SCHEMA,
|
|
650
|
+
ok: observer.ok && outcome.ok,
|
|
651
|
+
assignments: publicAssignments,
|
|
652
|
+
count,
|
|
653
|
+
cwd,
|
|
654
|
+
dryRun,
|
|
655
|
+
...(observer.ok && outcome.ok
|
|
656
|
+
? {}
|
|
657
|
+
: {
|
|
658
|
+
error: {
|
|
659
|
+
code: "HUMANISH_META_RUN_FAILED",
|
|
660
|
+
message: observer.ok ? outcome.reason : observer.error?.message ?? "OSS meta-lab Observer failed."
|
|
661
|
+
}
|
|
662
|
+
}),
|
|
663
|
+
liveRequested,
|
|
664
|
+
observer,
|
|
665
|
+
repos: publicRepos,
|
|
666
|
+
runId,
|
|
667
|
+
sandboxes: liveDesktops.map((desktop) => formatLiveDesktopForResult(desktop, redactRepoNames)),
|
|
668
|
+
warnings: [...warnings, ...observer.warnings]
|
|
669
|
+
};
|
|
670
|
+
if (observer.ok && liveDesktops.some((desktop) => desktop.desktop && desktop.bootstrap?.status === "started")) {
|
|
671
|
+
liveRuntimeByResult.set(result, {
|
|
672
|
+
artifactRoot,
|
|
673
|
+
assignments,
|
|
674
|
+
createdAt,
|
|
675
|
+
cwd,
|
|
676
|
+
dryRun,
|
|
677
|
+
liveDesktops,
|
|
678
|
+
liveRequested,
|
|
679
|
+
missingKeys: substrateMissingKeys,
|
|
680
|
+
persistScreenshots,
|
|
681
|
+
physicalCwd,
|
|
682
|
+
redactRepoNames,
|
|
683
|
+
runId,
|
|
684
|
+
source,
|
|
685
|
+
startedAt: Date.now()
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
async function writeMetaBundleArtifacts(artifactRoot, bundle) {
|
|
691
|
+
const publicBundle = publicSafeOssMetaBundle(bundle);
|
|
692
|
+
await writeJson(artifactRoot, "run.json", publicBundle);
|
|
693
|
+
await writeJson(artifactRoot, "review.json", publicBundle.review);
|
|
694
|
+
await writeContainedOutputFile(artifactRoot, "review.md", renderMetaReviewMarkdown(publicBundle), "utf8");
|
|
695
|
+
await writeContainedOutputFile(artifactRoot, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
696
|
+
}
|
|
697
|
+
export function publicSafeOssMetaBundle(bundle) {
|
|
698
|
+
return {
|
|
699
|
+
...bundle,
|
|
700
|
+
cwd: PUBLIC_TARGET_CWD,
|
|
701
|
+
streams: bundle.streams.map(publicSafeMetaStream)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function publicSafeMetaStream(stream) {
|
|
705
|
+
return {
|
|
706
|
+
...stream,
|
|
707
|
+
artifacts: uniqueStreamArtifacts(stream.artifacts.filter((artifact) => isLocalEvidenceArtifactPath(artifact.path))),
|
|
708
|
+
...(stream.terminal ? {
|
|
709
|
+
terminal: {
|
|
710
|
+
...stream.terminal,
|
|
711
|
+
tail: sanitizeRemoteLog(stream.terminal.tail)
|
|
712
|
+
}
|
|
713
|
+
} : {}),
|
|
714
|
+
...(stream.ui ? { ui: publicSafeMetaStreamUi(stream.ui) } : {})
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function publicSafeMetaStreamUi(ui) {
|
|
718
|
+
const { nestedObserverPath: rawNestedObserverPath, nestedObserverUrl: _rawNestedObserverUrl, ...rest } = ui;
|
|
719
|
+
const nestedObserverPath = rawNestedObserverPath && isLocalEvidenceArtifactPath(rawNestedObserverPath)
|
|
720
|
+
? rawNestedObserverPath
|
|
721
|
+
: undefined;
|
|
722
|
+
return {
|
|
723
|
+
...rest,
|
|
724
|
+
...(nestedObserverPath === undefined ? {} : { nestedObserverPath })
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
export function startOssMetaLabLiveRefresh(result, options = {}) {
|
|
728
|
+
const runtime = liveRuntimeByResult.get(result);
|
|
729
|
+
if (!runtime) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const intervalMs = options.intervalMs ?? readPositiveInt(process.env.HUMANISH_OSS_META_WATCH_REFRESH_MS, 5_000);
|
|
733
|
+
const screenshotIntervalMs = options.screenshotIntervalMs ?? readPositiveInt(process.env.HUMANISH_OSS_META_SCREENSHOT_REFRESH_MS, 15_000);
|
|
734
|
+
const timeoutMs = options.timeoutMs ?? readNonNegativeInt(process.env.HUMANISH_OSS_META_COMPLETION_TIMEOUT_MS, 240_000);
|
|
735
|
+
const deadline = timeoutMs === 0 ? null : runtime.startedAt + timeoutMs;
|
|
736
|
+
let lastScreenshotAt = 0;
|
|
737
|
+
let timer = null;
|
|
738
|
+
let stopped = false;
|
|
739
|
+
let running = false;
|
|
740
|
+
const tick = async () => {
|
|
741
|
+
if (stopped || running) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
running = true;
|
|
745
|
+
try {
|
|
746
|
+
const now = Date.now();
|
|
747
|
+
const timedOut = deadline !== null && now >= deadline;
|
|
748
|
+
const shouldCaptureScreenshot = runtime.persistScreenshots
|
|
749
|
+
&& (timedOut || lastScreenshotAt === 0 || now - lastScreenshotAt >= screenshotIntervalMs);
|
|
750
|
+
await refreshOssMetaLabLiveRuntime(runtime, {
|
|
751
|
+
captureScreenshots: shouldCaptureScreenshot,
|
|
752
|
+
timedOut,
|
|
753
|
+
timeoutMs
|
|
754
|
+
});
|
|
755
|
+
result.sandboxes = runtime.liveDesktops.map((desktop) => formatLiveDesktopForResult(desktop, runtime.redactRepoNames));
|
|
756
|
+
const outcome = classifyMetaLabOutcome({
|
|
757
|
+
dryRun: runtime.dryRun,
|
|
758
|
+
liveDesktops: runtime.liveDesktops,
|
|
759
|
+
liveRequested: runtime.liveRequested,
|
|
760
|
+
missingKeys: runtime.missingKeys
|
|
761
|
+
});
|
|
762
|
+
result.ok = result.observer?.ok === true && outcome.ok;
|
|
763
|
+
if (result.ok) {
|
|
764
|
+
delete result.error;
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
result.error = {
|
|
768
|
+
code: "HUMANISH_META_RUN_FAILED",
|
|
769
|
+
message: outcome.reason
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (shouldCaptureScreenshot) {
|
|
773
|
+
lastScreenshotAt = now;
|
|
774
|
+
}
|
|
775
|
+
if (runtime.liveDesktops.every((desktop) => isTerminalCompletion(desktop.completion))) {
|
|
776
|
+
if (timer) {
|
|
777
|
+
clearInterval(timer);
|
|
778
|
+
timer = null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Attached watch is best-effort telemetry. Keep serving the last verified
|
|
784
|
+
// bundle instead of crashing the user's observer process.
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
running = false;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
void tick();
|
|
791
|
+
timer = setInterval(() => {
|
|
792
|
+
void tick();
|
|
793
|
+
}, intervalMs);
|
|
794
|
+
const stop = async () => {
|
|
795
|
+
stopped = true;
|
|
796
|
+
if (timer) {
|
|
797
|
+
clearInterval(timer);
|
|
798
|
+
timer = null;
|
|
799
|
+
}
|
|
800
|
+
while (running) {
|
|
801
|
+
await wait(100);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
return {
|
|
805
|
+
async cleanup() {
|
|
806
|
+
await stop();
|
|
807
|
+
return cleanupOssMetaLabLiveDesktops(runtime.liveDesktops);
|
|
808
|
+
},
|
|
809
|
+
async stop() {
|
|
810
|
+
await stop();
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function cleanupOssMetaLabLiveDesktops(liveDesktops, options = {}) {
|
|
815
|
+
const ids = [...new Set(liveDesktops.flatMap((entry) => entry.desktop?.sandboxId ? [entry.desktop.sandboxId] : []))];
|
|
816
|
+
return killOssMetaLabProviderSandboxIds(ids, {
|
|
817
|
+
...(options.killSandbox === undefined ? {} : { killSandbox: options.killSandbox }),
|
|
818
|
+
...(options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: options.requestTimeoutMs }),
|
|
819
|
+
skipped: liveDesktops.length - ids.length
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
export async function cleanupStaleOssMetaLabSandboxes(options = {}) {
|
|
823
|
+
return cleanupOssMetaLabSandboxesAndProviderMatches({ sandboxes: [] }, options);
|
|
824
|
+
}
|
|
825
|
+
export async function cleanupOssMetaLabSandboxesAndProviderMatches(result, options = {}) {
|
|
826
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
827
|
+
const listed = await listOssMetaLabProviderSandboxIds({
|
|
828
|
+
...(options.listSandboxes === undefined ? {} : { listSandboxes: options.listSandboxes }),
|
|
829
|
+
requestTimeoutMs
|
|
830
|
+
});
|
|
831
|
+
const cleanup = await killOssMetaLabProviderSandboxIds(listed.ids, {
|
|
832
|
+
...(options.killSandbox === undefined ? {} : { killSandbox: options.killSandbox }),
|
|
833
|
+
redactIds: true,
|
|
834
|
+
requestTimeoutMs,
|
|
835
|
+
skipped: listed.skipped
|
|
836
|
+
});
|
|
837
|
+
const remaining = listed.errors.length > 0 || cleanup.errors.length > 0
|
|
838
|
+
? undefined
|
|
839
|
+
: (await listOssMetaLabProviderSandboxIds({
|
|
840
|
+
...(options.listSandboxes === undefined ? {} : { listSandboxes: options.listSandboxes }),
|
|
841
|
+
requestTimeoutMs
|
|
842
|
+
})).ids.length;
|
|
843
|
+
return {
|
|
844
|
+
killed: cleanup.killed,
|
|
845
|
+
matched: listed.ids.length,
|
|
846
|
+
...(remaining === undefined ? {} : { remaining }),
|
|
847
|
+
skipped: result.sandboxes.length + cleanup.skipped,
|
|
848
|
+
errors: [
|
|
849
|
+
...(result.sandboxes.length > 0
|
|
850
|
+
? ["Stored OSS meta-lab sandbox IDs were not used; cleanup requires verified provider metadata."]
|
|
851
|
+
: []),
|
|
852
|
+
...listed.errors,
|
|
853
|
+
...cleanup.errors
|
|
854
|
+
]
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
async function listOssMetaLabProviderSandboxIds(options) {
|
|
858
|
+
let listSandboxes = options.listSandboxes;
|
|
859
|
+
if (!listSandboxes) {
|
|
860
|
+
// humanish never enumerates an operator's E2B account by default (see
|
|
861
|
+
// docs/principles/invariants-and-defaults.md). Account-wide Sandbox.list discovery exists
|
|
862
|
+
// ONLY for a maintainer's explicit orphan sweep of a crashed
|
|
863
|
+
// prior process (`humanish lab cleanup oss`) and requires this opt-in, set deliberately by the
|
|
864
|
+
// maintainer running it against their own account -- never a default, never for a shared key.
|
|
865
|
+
if (process.env.HUMANISH_OSS_META_ALLOW_PROVIDER_LIST !== "1") {
|
|
866
|
+
return {
|
|
867
|
+
ids: [],
|
|
868
|
+
skipped: 0,
|
|
869
|
+
errors: ["Provider-wide Sandbox.list discovery is disabled by default (humanish never enumerates an E2B account); set HUMANISH_OSS_META_ALLOW_PROVIDER_LIST=1 to opt in for a maintainer-run orphan sweep."]
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
const e2bApiKey = process.env.E2B_API_KEY;
|
|
873
|
+
if (!e2bApiKey) {
|
|
874
|
+
return {
|
|
875
|
+
ids: [],
|
|
876
|
+
skipped: 0,
|
|
877
|
+
errors: ["E2B_API_KEY is not present; provider metadata cleanup readback skipped."]
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const desktopModule = await loadE2BDesktopModule();
|
|
881
|
+
if (!desktopModule.Sandbox.list) {
|
|
882
|
+
return {
|
|
883
|
+
ids: [],
|
|
884
|
+
skipped: 0,
|
|
885
|
+
errors: ["Installed @e2b/desktop SDK does not expose Sandbox.list; provider metadata cleanup readback skipped."]
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
listSandboxes = async (request) => {
|
|
889
|
+
const paginator = desktopModule.Sandbox.list?.({
|
|
890
|
+
metadata: request.metadata,
|
|
891
|
+
requestTimeoutMs: request.requestTimeoutMs
|
|
892
|
+
});
|
|
893
|
+
const sandboxes = [];
|
|
894
|
+
if (!paginator) {
|
|
895
|
+
return sandboxes;
|
|
896
|
+
}
|
|
897
|
+
while (true) {
|
|
898
|
+
sandboxes.push(...await paginator.nextItems({ requestTimeoutMs: request.requestTimeoutMs }));
|
|
899
|
+
if (!paginator.hasNext) {
|
|
900
|
+
return sandboxes;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const sandboxes = await listSandboxes({
|
|
907
|
+
metadata: { ...OSS_META_LAB_PROVIDER_METADATA },
|
|
908
|
+
requestTimeoutMs: options.requestTimeoutMs
|
|
909
|
+
});
|
|
910
|
+
let skipped = 0;
|
|
911
|
+
const ids = sandboxes.flatMap((sandbox) => {
|
|
912
|
+
const id = sandboxProviderId(sandbox);
|
|
913
|
+
if (!id || !isCleanupEligibleOssMetaLabSandbox(sandbox)) {
|
|
914
|
+
skipped += 1;
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
return [id];
|
|
918
|
+
});
|
|
919
|
+
return { ids: [...new Set(ids)], skipped, errors: [] };
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
return {
|
|
923
|
+
ids: [],
|
|
924
|
+
skipped: 0,
|
|
925
|
+
errors: [`provider metadata cleanup readback failed: ${compactError(error)}`]
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function sandboxProviderId(sandbox) {
|
|
930
|
+
for (const value of [sandbox.sandboxId, sandbox.sandboxID, sandbox.id]) {
|
|
931
|
+
if (typeof value === "string" && value.trim()) {
|
|
932
|
+
return value;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
function isCleanupEligibleOssMetaLabSandbox(sandbox) {
|
|
938
|
+
const metadata = sandbox.metadata;
|
|
939
|
+
if (!metadata || metadata.tool !== OSS_META_LAB_PROVIDER_METADATA.tool || metadata.mode !== OSS_META_LAB_PROVIDER_METADATA.mode) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const state = typeof sandbox.state === "string" ? sandbox.state.toLowerCase() : "";
|
|
943
|
+
return !["closed", "killed", "paused", "terminated"].includes(state);
|
|
944
|
+
}
|
|
945
|
+
export async function cleanupOssMetaLabSandboxes(result, _options = {}) {
|
|
946
|
+
return {
|
|
947
|
+
killed: 0,
|
|
948
|
+
skipped: result.sandboxes.length,
|
|
949
|
+
errors: result.sandboxes.some((sandbox) => sandbox.sandboxId)
|
|
950
|
+
? ["Stored OSS meta-lab sandbox IDs cannot authorize provider mutation; use the explicit metadata-verified orphan sweep."]
|
|
951
|
+
: []
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
async function killOssMetaLabProviderSandboxIds(idsInput, options = {}) {
|
|
955
|
+
const ids = [...new Set(idsInput.filter((id) => id.trim()))];
|
|
956
|
+
const skipped = options.skipped ?? 0;
|
|
957
|
+
if (ids.length === 0) {
|
|
958
|
+
return { killed: 0, skipped, errors: [] };
|
|
959
|
+
}
|
|
960
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
961
|
+
let killSandbox = options.killSandbox;
|
|
962
|
+
if (!killSandbox) {
|
|
963
|
+
const e2bApiKey = process.env.E2B_API_KEY;
|
|
964
|
+
if (!e2bApiKey) {
|
|
965
|
+
return {
|
|
966
|
+
killed: 0,
|
|
967
|
+
skipped: skipped + ids.length,
|
|
968
|
+
errors: ["E2B_API_KEY is not present; remote sandbox cleanup skipped."]
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const desktopModule = await loadE2BDesktopModule();
|
|
972
|
+
if (!desktopModule.Sandbox.kill) {
|
|
973
|
+
return {
|
|
974
|
+
killed: 0,
|
|
975
|
+
skipped: skipped + ids.length,
|
|
976
|
+
errors: ["Installed @e2b/desktop SDK does not expose Sandbox.kill; remote sandbox cleanup skipped."]
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
killSandbox = async (sandboxId, timeout) => desktopModule.Sandbox.kill?.(sandboxId, { requestTimeoutMs: timeout });
|
|
980
|
+
}
|
|
981
|
+
let killed = 0;
|
|
982
|
+
const errors = [];
|
|
983
|
+
for (const id of ids) {
|
|
984
|
+
try {
|
|
985
|
+
await killSandbox(id, requestTimeoutMs);
|
|
986
|
+
killed += 1;
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
const errorText = compactError(error);
|
|
990
|
+
errors.push(`${options.redactIds ? "[provider-runtime]" : id}: ${options.redactIds ? errorText.replaceAll(id, "[provider-runtime]") : errorText}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return { killed, skipped, errors };
|
|
994
|
+
}
|
|
995
|
+
export function buildOssMetaBundleFixture(args) {
|
|
996
|
+
return buildMetaBundle({
|
|
997
|
+
assignments: args.assignments,
|
|
998
|
+
createdAt: args.createdAt,
|
|
999
|
+
cwd: args.cwd,
|
|
1000
|
+
dryRun: args.dryRun,
|
|
1001
|
+
liveDesktops: args.lanes,
|
|
1002
|
+
liveRequested: args.liveRequested,
|
|
1003
|
+
missingKeys: args.missingKeys,
|
|
1004
|
+
redactRepoNames: args.redactRepoNames === true,
|
|
1005
|
+
runId: args.runId
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
export function buildOssMetaBootstrapScriptFixture() {
|
|
1009
|
+
const [assignment] = buildOssRepoAssignments(["maciekt07/TodoApp"], 1);
|
|
1010
|
+
if (!assignment) {
|
|
1011
|
+
throw new Error("Missing OSS meta-lab fixture assignment.");
|
|
1012
|
+
}
|
|
1013
|
+
return buildRemoteBootstrapScript({
|
|
1014
|
+
assignment,
|
|
1015
|
+
appDir: "/home/user/maciekt07-todoapp",
|
|
1016
|
+
completionPath: "/home/user/.humanish-oss-lab/maciekt07-todoapp/completion.json",
|
|
1017
|
+
displayRepo: "maciekt07/TodoApp",
|
|
1018
|
+
logPath: "/home/user/.humanish-oss-lab/maciekt07-todoapp/bootstrap.log",
|
|
1019
|
+
nestedObserverPath: "/home/user/maciekt07-todoapp/.humanish/runs/nested-maciekt07-todoapp/observer/index.html",
|
|
1020
|
+
remoteHostActorPlanPath: "/home/user/.humanish-oss-lab/maciekt07-todoapp/host-actor-plan.json",
|
|
1021
|
+
stateDir: "/home/user/.humanish-oss-lab/maciekt07-todoapp",
|
|
1022
|
+
token: "maciekt07-todoapp"
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function syntheticOssMetaRunSource(createdAt) {
|
|
1026
|
+
return {
|
|
1027
|
+
packageName: "humanish",
|
|
1028
|
+
humanishSource: "present",
|
|
1029
|
+
git: {
|
|
1030
|
+
schema: "humanish.git-state.v1",
|
|
1031
|
+
status: "clean",
|
|
1032
|
+
capturedAt: createdAt,
|
|
1033
|
+
head: {
|
|
1034
|
+
shortSha: null,
|
|
1035
|
+
refState: "unknown"
|
|
1036
|
+
},
|
|
1037
|
+
changes: {
|
|
1038
|
+
staged: 0,
|
|
1039
|
+
unstaged: 0,
|
|
1040
|
+
untracked: 0,
|
|
1041
|
+
total: 0
|
|
1042
|
+
},
|
|
1043
|
+
note: "public-safe synthetic OSS meta-lab fixture"
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function buildMetaBundle(args) {
|
|
1048
|
+
const simulations = [];
|
|
1049
|
+
const streams = [];
|
|
1050
|
+
const events = [
|
|
1051
|
+
{
|
|
1052
|
+
id: "event-000",
|
|
1053
|
+
at: args.createdAt,
|
|
1054
|
+
level: "info",
|
|
1055
|
+
type: "oss-meta.contract.created",
|
|
1056
|
+
message: "Created public-safe OSS meta-lab Observer-of-Observers contract."
|
|
1057
|
+
}
|
|
1058
|
+
];
|
|
1059
|
+
for (const assignment of args.assignments) {
|
|
1060
|
+
const prompt = buildCodexBootstrapPrompt(assignment, args.redactRepoNames);
|
|
1061
|
+
const rawLiveDesktop = args.liveDesktops.find((desktop) => desktop.streamId === assignment.streamId);
|
|
1062
|
+
const liveDesktop = rawLiveDesktop && args.redactRepoNames
|
|
1063
|
+
? redactLiveDesktopRepoMentions(rawLiveDesktop, assignment.repo)
|
|
1064
|
+
: rawLiveDesktop;
|
|
1065
|
+
const repoLabel = args.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo;
|
|
1066
|
+
const scenarioId = args.redactRepoNames ? `oss-meta-${repoLabel}` : assignment.scenarioId;
|
|
1067
|
+
const status = statusForMeta(args, liveDesktop);
|
|
1068
|
+
const completion = liveDesktop?.completion;
|
|
1069
|
+
const screenshot = liveDesktop?.screenshot;
|
|
1070
|
+
const terminalTail = terminalTailForMeta(prompt, liveDesktop);
|
|
1071
|
+
const liveStreamPresent = Boolean(liveDesktop?.url);
|
|
1072
|
+
const appServerMode = liveDesktop?.bootstrap?.codexMode === "app-server-client";
|
|
1073
|
+
simulations.push({
|
|
1074
|
+
id: assignment.simId,
|
|
1075
|
+
index: assignment.index,
|
|
1076
|
+
personaId: `codex-oss-operator-${String(assignment.index).padStart(2, "0")}`,
|
|
1077
|
+
scenarioId,
|
|
1078
|
+
status,
|
|
1079
|
+
streamKind: "browser",
|
|
1080
|
+
mode: "browser-sim",
|
|
1081
|
+
progress: progressForMeta(status, liveDesktop),
|
|
1082
|
+
currentStep: currentStepForMeta(args, assignment, liveDesktop),
|
|
1083
|
+
summary: completion
|
|
1084
|
+
? `Headed E2B desktop lane assigned to ${repoLabel}; ${completion.reason}`
|
|
1085
|
+
: liveDesktop?.bootstrap?.status === "started"
|
|
1086
|
+
? `Headed E2B desktop lane assigned to ${repoLabel}; bootstrap terminal launched to set up Humanish and open the nested Observer${appServerMode ? " plus Codex app-server client surface" : ""}.`
|
|
1087
|
+
: `Headed E2B desktop lane assigned to ${repoLabel}; remote bootstrap should set up Humanish and open a nested Observer inside that desktop.`,
|
|
1088
|
+
streamIds: [assignment.streamId],
|
|
1089
|
+
startedAt: args.createdAt,
|
|
1090
|
+
updatedAt: args.createdAt
|
|
1091
|
+
});
|
|
1092
|
+
const artifacts = uniqueStreamArtifacts([
|
|
1093
|
+
{ label: "run bundle", path: "run.json", kind: "bundle" },
|
|
1094
|
+
{ label: "review", path: "review.md", kind: "review" },
|
|
1095
|
+
{ label: "events", path: "events.ndjson", kind: "events" },
|
|
1096
|
+
...(completion?.appServerActorEvidence?.tracePath ? [{ label: "codex app-server trace", path: completion.appServerActorEvidence.tracePath, kind: "trace" }] : []),
|
|
1097
|
+
...(completion?.appServerActorEvidence?.eventsPath ? [{ label: "codex app-server events", path: completion.appServerActorEvidence.eventsPath, kind: "events" }] : []),
|
|
1098
|
+
...(completion?.appServerActorEvidence?.transcriptPath ? [{ label: "codex app-server transcript", path: completion.appServerActorEvidence.transcriptPath, kind: "log" }] : []),
|
|
1099
|
+
...(liveDesktop?.actorEvidence?.actorLastMessageTailPath ? [{ label: "actor last-message tail", path: liveDesktop.actorEvidence.actorLastMessageTailPath, kind: "log" }] : []),
|
|
1100
|
+
...(liveDesktop?.actorEvidence?.actorLogTailPath ? [{ label: "actor log tail", path: liveDesktop.actorEvidence.actorLogTailPath, kind: "log" }] : []),
|
|
1101
|
+
...(liveDesktop?.actorEvidence?.appServerTracePath ? [{ label: "codex app-server trace", path: liveDesktop.actorEvidence.appServerTracePath, kind: "trace" }] : []),
|
|
1102
|
+
...(liveDesktop?.actorEvidence?.appServerEventsPath ? [{ label: "codex app-server events", path: liveDesktop.actorEvidence.appServerEventsPath, kind: "events" }] : []),
|
|
1103
|
+
...(liveDesktop?.actorEvidence?.appServerTranscriptPath ? [{ label: "codex app-server transcript", path: liveDesktop.actorEvidence.appServerTranscriptPath, kind: "log" }] : []),
|
|
1104
|
+
...(liveDesktop?.actorEvidence?.nestedEvidencePath ? [{ label: "nested Humanish proof", path: liveDesktop.actorEvidence.nestedEvidencePath, kind: "trace" }] : []),
|
|
1105
|
+
...(liveDesktop?.actorEvidence?.setupQualityPath ? [{ label: "setup quality", path: liveDesktop.actorEvidence.setupQualityPath, kind: "filesystem" }] : []),
|
|
1106
|
+
...(liveDesktop?.hostActorPlanPath ? [{ label: "host Codex actor plan", path: liveDesktop.hostActorPlanPath, kind: "trace" }] : []),
|
|
1107
|
+
...(screenshot ? [{ label: "desktop screenshot", path: screenshot.path, kind: "screenshot" }] : [])
|
|
1108
|
+
]);
|
|
1109
|
+
const stream = {
|
|
1110
|
+
id: assignment.streamId,
|
|
1111
|
+
simId: assignment.simId,
|
|
1112
|
+
kind: "browser",
|
|
1113
|
+
label: `E2B desktop - ${repoLabel}`,
|
|
1114
|
+
status,
|
|
1115
|
+
transport: liveStreamPresent ? "sse" : screenshot ? "snapshot" : status === "contract_proof_only" ? "snapshot" : "sse",
|
|
1116
|
+
updatedAt: args.createdAt,
|
|
1117
|
+
embed: {
|
|
1118
|
+
kind: screenshot ? "screenshot" : "placeholder",
|
|
1119
|
+
...(screenshot ? { url: screenshot.observerUrl } : {}),
|
|
1120
|
+
title: `E2B desktop ${assignment.index}`
|
|
1121
|
+
},
|
|
1122
|
+
viewport: {
|
|
1123
|
+
width: 1440,
|
|
1124
|
+
height: 960,
|
|
1125
|
+
deviceScaleFactor: 1
|
|
1126
|
+
},
|
|
1127
|
+
terminal: {
|
|
1128
|
+
title: `${appServerMode ? "Codex app-server" : "Codex"} bootstrap - ${repoLabel}`,
|
|
1129
|
+
format: "plain",
|
|
1130
|
+
stdin: liveDesktop?.bootstrap ? "sent" : "planned",
|
|
1131
|
+
tail: terminalTail
|
|
1132
|
+
},
|
|
1133
|
+
ui: {
|
|
1134
|
+
route: completion?.appUrl ?? `e2b://desktop/${repoLabel}`,
|
|
1135
|
+
intent: appServerMode
|
|
1136
|
+
? "Watch the headed desktop where the bootstrap clones the repo, starts the target app, sets up Humanish, opens the nested Observer, and opens a Codex app-server client surface."
|
|
1137
|
+
: "Watch the headed desktop where the bootstrap clones the repo, starts the target app, sets up Humanish, opens the nested Observer, and attempts a Codex actor.",
|
|
1138
|
+
...(completion?.actorStatus ? { actorStatus: completion.actorStatus } : {}),
|
|
1139
|
+
...(completion?.appStatus ? { appStatus: completion.appStatus } : {}),
|
|
1140
|
+
...(completion?.appUrl ? { appUrl: completion.appUrl } : {}),
|
|
1141
|
+
...(liveDesktop?.actorEvidence?.nestedEvidencePath ? { nestedObserverPath: liveDesktop.actorEvidence.nestedEvidencePath } : {}),
|
|
1142
|
+
...(screenshot ? { screenshotUrl: screenshot.observerUrl } : {}),
|
|
1143
|
+
...(completion?.visualStatus ? { visualStatus: completion.visualStatus } : {}),
|
|
1144
|
+
state: completion
|
|
1145
|
+
? [
|
|
1146
|
+
completion.reason,
|
|
1147
|
+
completion.appStatus ? `app=${completion.appStatus}` : "",
|
|
1148
|
+
completion.actorStatus ? `actor=${completion.actorStatus}` : "",
|
|
1149
|
+
completion.visualStatus ? `visual=${completion.visualStatus}` : ""
|
|
1150
|
+
].filter(Boolean).join(" | ")
|
|
1151
|
+
: liveDesktop?.bootstrap?.status === "started"
|
|
1152
|
+
? "bootstrap terminal launched; target app and nested Observer setup running"
|
|
1153
|
+
: liveStreamPresent ? "live E2B desktop stream present; stream URL is runtime-only" : args.dryRun ? "contract desktop" : "headed E2B desktop"
|
|
1154
|
+
},
|
|
1155
|
+
...(completion ? { completion: completionForStream(completion, appServerMode) } : {}),
|
|
1156
|
+
artifacts
|
|
1157
|
+
};
|
|
1158
|
+
if (appServerMode) {
|
|
1159
|
+
stream.codex = codexMetadataForMetaStream(liveDesktop, completion);
|
|
1160
|
+
}
|
|
1161
|
+
streams.push(stream);
|
|
1162
|
+
events.push({
|
|
1163
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-assigned`,
|
|
1164
|
+
at: args.createdAt,
|
|
1165
|
+
level: "info",
|
|
1166
|
+
type: "oss-meta.repo.assigned",
|
|
1167
|
+
message: `Assigned ${repoLabel} to Codex desktop lane ${assignment.index}.`,
|
|
1168
|
+
simId: assignment.simId,
|
|
1169
|
+
streamId: assignment.streamId
|
|
1170
|
+
}, {
|
|
1171
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-prompt`,
|
|
1172
|
+
at: args.createdAt,
|
|
1173
|
+
level: "info",
|
|
1174
|
+
type: "oss-meta.codex.prompt.ready",
|
|
1175
|
+
message: appServerMode
|
|
1176
|
+
? "Codex app-server client hook is available in the stream logs tab."
|
|
1177
|
+
: "Codex bootstrap prompt is available in the stream logs tab.",
|
|
1178
|
+
simId: assignment.simId,
|
|
1179
|
+
streamId: assignment.streamId
|
|
1180
|
+
});
|
|
1181
|
+
if (liveStreamPresent && liveDesktop) {
|
|
1182
|
+
events.push({
|
|
1183
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-stream`,
|
|
1184
|
+
at: args.createdAt,
|
|
1185
|
+
level: "info",
|
|
1186
|
+
type: "oss-meta.e2b.stream.started",
|
|
1187
|
+
message: `Live E2B desktop stream started for ${repoLabel}; auth URL is runtime-only and not persisted in run artifacts.`,
|
|
1188
|
+
simId: assignment.simId,
|
|
1189
|
+
streamId: assignment.streamId
|
|
1190
|
+
});
|
|
1191
|
+
if (liveDesktop.bootstrap?.status === "started") {
|
|
1192
|
+
events.push({
|
|
1193
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-bootstrap-started`,
|
|
1194
|
+
at: args.createdAt,
|
|
1195
|
+
level: "info",
|
|
1196
|
+
type: "oss-meta.bootstrap.started",
|
|
1197
|
+
message: `Visible bootstrap terminal launched for ${repoLabel}.`,
|
|
1198
|
+
simId: assignment.simId,
|
|
1199
|
+
streamId: assignment.streamId
|
|
1200
|
+
});
|
|
1201
|
+
if (completion) {
|
|
1202
|
+
events.push({
|
|
1203
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-bootstrap-${completion.status}`,
|
|
1204
|
+
at: completion.checkedAt,
|
|
1205
|
+
level: eventLevelForCompletion(completion.status),
|
|
1206
|
+
type: `oss-meta.bootstrap.${completion.status}`,
|
|
1207
|
+
message: `${repoLabel}: ${completion.reason}`,
|
|
1208
|
+
simId: assignment.simId,
|
|
1209
|
+
streamId: assignment.streamId
|
|
1210
|
+
});
|
|
1211
|
+
if (completion.nestedStepTraceSummary) {
|
|
1212
|
+
events.push({
|
|
1213
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-nested-step-trace`,
|
|
1214
|
+
at: completion.checkedAt,
|
|
1215
|
+
level: completion.nestedStepTraceSummary.status === "passed" ? "info" : "warn",
|
|
1216
|
+
type: "oss-meta.nested.step_trace.summary",
|
|
1217
|
+
message: `${repoLabel}: Nested browser trace summary captured ${completion.nestedStepTraceSummary.counts.passedSteps}/${completion.nestedStepTraceSummary.counts.totalSteps} steps across ${completion.nestedStepTraceSummary.counts.surfaces} surface(s).`,
|
|
1218
|
+
simId: assignment.simId,
|
|
1219
|
+
streamId: assignment.streamId
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
else if (liveDesktop.bootstrap?.status === "failed") {
|
|
1225
|
+
events.push({
|
|
1226
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-bootstrap-failed`,
|
|
1227
|
+
at: args.createdAt,
|
|
1228
|
+
level: "error",
|
|
1229
|
+
type: "oss-meta.bootstrap.failed",
|
|
1230
|
+
message: `Bootstrap launcher failed for ${repoLabel}.`,
|
|
1231
|
+
simId: assignment.simId,
|
|
1232
|
+
streamId: assignment.streamId
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
else if (completion) {
|
|
1237
|
+
events.push({
|
|
1238
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-completion-${completion.status}`,
|
|
1239
|
+
at: completion.checkedAt,
|
|
1240
|
+
level: eventLevelForCompletion(completion.status),
|
|
1241
|
+
type: `oss-meta.bootstrap.${completion.status}`,
|
|
1242
|
+
message: `${repoLabel}: ${completion.reason}`,
|
|
1243
|
+
simId: assignment.simId,
|
|
1244
|
+
streamId: assignment.streamId
|
|
1245
|
+
});
|
|
1246
|
+
if (completion.nestedStepTraceSummary) {
|
|
1247
|
+
events.push({
|
|
1248
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-nested-step-trace`,
|
|
1249
|
+
at: completion.checkedAt,
|
|
1250
|
+
level: completion.nestedStepTraceSummary.status === "passed" ? "info" : "warn",
|
|
1251
|
+
type: "oss-meta.nested.step_trace.summary",
|
|
1252
|
+
message: `${repoLabel}: Nested browser trace summary captured ${completion.nestedStepTraceSummary.counts.passedSteps}/${completion.nestedStepTraceSummary.counts.totalSteps} steps across ${completion.nestedStepTraceSummary.counts.surfaces} surface(s).`,
|
|
1253
|
+
simId: assignment.simId,
|
|
1254
|
+
streamId: assignment.streamId
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
else if (liveDesktop?.error) {
|
|
1259
|
+
events.push({
|
|
1260
|
+
id: `event-${String(assignment.index).padStart(3, "0")}-stream-error`,
|
|
1261
|
+
at: args.createdAt,
|
|
1262
|
+
level: "error",
|
|
1263
|
+
type: "oss-meta.e2b.stream.failed",
|
|
1264
|
+
message: `E2B desktop stream failed for ${repoLabel}: ${liveDesktop.error}`,
|
|
1265
|
+
simId: assignment.simId,
|
|
1266
|
+
streamId: assignment.streamId
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (args.liveRequested && args.missingKeys.length > 0) {
|
|
1271
|
+
events.push({
|
|
1272
|
+
id: "event-live-keys-blocked",
|
|
1273
|
+
at: args.createdAt,
|
|
1274
|
+
level: "warn",
|
|
1275
|
+
type: "oss-meta.live.keys_missing",
|
|
1276
|
+
message: `Live launch is blocked until ${args.missingKeys.join(", ")} are present.`
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
if (args.liveRequested && args.liveDesktops.length === 0) {
|
|
1280
|
+
events.push({
|
|
1281
|
+
id: "event-live-substrate-planned",
|
|
1282
|
+
at: args.createdAt,
|
|
1283
|
+
level: "warn",
|
|
1284
|
+
type: "oss-meta.live.substrate_planned",
|
|
1285
|
+
message: args.missingKeys.length > 0
|
|
1286
|
+
? "E2B desktop launch is waiting on required environment variables."
|
|
1287
|
+
: "Codex TUI injection and nested Humanish execution are planned behind this Observer contract."
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if (args.liveDesktops.some((desktop) => desktop.url)) {
|
|
1291
|
+
events.push({
|
|
1292
|
+
id: "event-live-substrate-started",
|
|
1293
|
+
at: args.createdAt,
|
|
1294
|
+
level: "info",
|
|
1295
|
+
type: "oss-meta.live.substrate_started",
|
|
1296
|
+
message: args.liveDesktops.some((desktop) => desktop.bootstrap?.status === "started")
|
|
1297
|
+
? "E2B desktop streams are connected and bootstrap terminals are launched."
|
|
1298
|
+
: "E2B desktop streams are connected; Codex TUI injection is still pending."
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
const review = createMetaReview(args);
|
|
1302
|
+
const feedbackCandidates = buildMetaFeedbackCandidates({
|
|
1303
|
+
assignments: args.assignments,
|
|
1304
|
+
liveDesktops: args.liveDesktops,
|
|
1305
|
+
redactRepoNames: args.redactRepoNames,
|
|
1306
|
+
runId: args.runId
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
schema: RUN_BUNDLE_SCHEMA,
|
|
1310
|
+
runId: args.runId,
|
|
1311
|
+
mode: args.dryRun ? "dry-run" : "live",
|
|
1312
|
+
simCount: args.assignments.length,
|
|
1313
|
+
createdAt: args.createdAt,
|
|
1314
|
+
cwd: args.cwd,
|
|
1315
|
+
artifactRoot: path.join(".humanish", "runs", args.runId),
|
|
1316
|
+
source: args.source ?? syntheticOssMetaRunSource(args.createdAt),
|
|
1317
|
+
persona: {
|
|
1318
|
+
id: "oss-meta-codex-tui-operators",
|
|
1319
|
+
name: "Codex TUI OSS Setup Operators",
|
|
1320
|
+
source: "lab:oss:meta",
|
|
1321
|
+
sourceDigest: "public-safe"
|
|
1322
|
+
},
|
|
1323
|
+
scenario: {
|
|
1324
|
+
id: "oss-meta-observer-of-observers",
|
|
1325
|
+
title: "OSS Observer-of-Observers Meta-Lab",
|
|
1326
|
+
goal: "Launch headed E2B desktops where Codex agents clone authorized GitHub repos, set up Humanish, run nested Humanish proof commands, attempt Codex TUI, and keep each nested Observer visible.",
|
|
1327
|
+
source: "lab:oss:meta",
|
|
1328
|
+
sourceDigest: "public-safe"
|
|
1329
|
+
},
|
|
1330
|
+
lifecycle: [
|
|
1331
|
+
{
|
|
1332
|
+
at: args.createdAt,
|
|
1333
|
+
event: "oss-meta.run.created",
|
|
1334
|
+
message: `Created OSS meta-lab run with ${args.assignments.length} headed desktop lane${args.assignments.length === 1 ? "" : "s"}.`
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
at: args.createdAt,
|
|
1338
|
+
event: "oss-meta.repos.assigned",
|
|
1339
|
+
message: `Assigned repos: ${args.assignments.map((assignment) => args.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo).join(", ")}.`
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
at: args.createdAt,
|
|
1343
|
+
event: "oss-meta.observer.ready",
|
|
1344
|
+
message: "Top-level Observer is ready to watch nested Humanish Observers."
|
|
1345
|
+
}
|
|
1346
|
+
],
|
|
1347
|
+
simulations,
|
|
1348
|
+
streams,
|
|
1349
|
+
events,
|
|
1350
|
+
redaction: {
|
|
1351
|
+
status: "passed",
|
|
1352
|
+
notes: "OSS meta-lab artifacts contain GitHub slugs and redacted/synthetic bootstrap evidence only."
|
|
1353
|
+
},
|
|
1354
|
+
artifacts: {
|
|
1355
|
+
run: "run.json",
|
|
1356
|
+
reviewJson: "review.json",
|
|
1357
|
+
reviewMarkdown: "review.md",
|
|
1358
|
+
observerData: "observer/observer-data.json",
|
|
1359
|
+
events: "events.ndjson"
|
|
1360
|
+
},
|
|
1361
|
+
review,
|
|
1362
|
+
feedbackCandidates
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function uniqueStreamArtifacts(artifacts) {
|
|
1366
|
+
const seen = new Set();
|
|
1367
|
+
const unique = [];
|
|
1368
|
+
for (const artifact of artifacts) {
|
|
1369
|
+
const key = `${artifact.kind}:${artifact.path}`;
|
|
1370
|
+
if (seen.has(key))
|
|
1371
|
+
continue;
|
|
1372
|
+
seen.add(key);
|
|
1373
|
+
unique.push(artifact);
|
|
1374
|
+
}
|
|
1375
|
+
return unique;
|
|
1376
|
+
}
|
|
1377
|
+
function codexMetadataForMetaStream(liveDesktop, completion) {
|
|
1378
|
+
const trace = completion?.appServerActorEvidence?.traceJson;
|
|
1379
|
+
const traceRecord = isRecord(trace) ? trace : undefined;
|
|
1380
|
+
const counts = isRecord(traceRecord?.counts) ? traceRecord.counts : undefined;
|
|
1381
|
+
const eventCount = typeof counts?.envelopes === "number" ? counts.envelopes : undefined;
|
|
1382
|
+
return {
|
|
1383
|
+
provider: "codex-app-server",
|
|
1384
|
+
state: codexStateForMeta(completion),
|
|
1385
|
+
contract: "Codex app-server JSON-RPC actor telemetry projected from redacted event envelopes, trace summary, transcript tail, and filesystem setup evidence.",
|
|
1386
|
+
...(eventCount === undefined ? {} : { eventCount }),
|
|
1387
|
+
...(liveDesktop?.actorEvidence?.appServerTracePath ? { tracePath: liveDesktop.actorEvidence.appServerTracePath } : {}),
|
|
1388
|
+
...(typeof traceRecord?.threadId === "string" && traceRecord.threadId.trim() ? { threadId: traceRecord.threadId } : {}),
|
|
1389
|
+
...(typeof traceRecord?.turnId === "string" && traceRecord.turnId.trim() ? { turnId: traceRecord.turnId } : {}),
|
|
1390
|
+
...(typeof traceRecord?.sessionId === "string" && traceRecord.sessionId.trim() ? { sessionId: traceRecord.sessionId } : {}),
|
|
1391
|
+
...(typeof traceRecord?.model === "string" && traceRecord.model.trim() ? { model: traceRecord.model } : {})
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function codexStateForMeta(completion) {
|
|
1395
|
+
if (!completion)
|
|
1396
|
+
return "connecting";
|
|
1397
|
+
if (completion.actorStatus === "passed")
|
|
1398
|
+
return "completed";
|
|
1399
|
+
if (completion.actorStatus === "running")
|
|
1400
|
+
return "running";
|
|
1401
|
+
if (completion.actorStatus === "timed_out" || completion.status === "timed_out")
|
|
1402
|
+
return "timed_out";
|
|
1403
|
+
if (completion.actorStatus === "blocked" || completion.status === "blocked")
|
|
1404
|
+
return "blocked";
|
|
1405
|
+
if (completion.actorStatus === "failed" || completion.status === "failed")
|
|
1406
|
+
return "failed";
|
|
1407
|
+
return completion.status === "running" ? "running" : "watching";
|
|
1408
|
+
}
|
|
1409
|
+
function statusForMeta(args, liveDesktop) {
|
|
1410
|
+
if (args.dryRun)
|
|
1411
|
+
return "contract_proof_only";
|
|
1412
|
+
if (liveDesktop?.completion?.status === "passed" && liveDesktop.completion.appStatus !== "running")
|
|
1413
|
+
return "blocked";
|
|
1414
|
+
if (liveDesktop?.completion?.status === "passed" && liveDesktop.completion.visualStatus !== "visible")
|
|
1415
|
+
return "blocked";
|
|
1416
|
+
if (liveDesktop?.completion?.status === "passed"
|
|
1417
|
+
&& liveDesktop.bootstrap?.codexMode === "app-server-client"
|
|
1418
|
+
&& liveDesktop.completion.actorStatus !== "passed")
|
|
1419
|
+
return "blocked";
|
|
1420
|
+
if (liveDesktop?.completion?.status === "passed")
|
|
1421
|
+
return "passed";
|
|
1422
|
+
if (liveDesktop?.completion?.status === "failed")
|
|
1423
|
+
return "failed";
|
|
1424
|
+
if (liveDesktop?.completion?.status === "blocked")
|
|
1425
|
+
return "blocked";
|
|
1426
|
+
if (liveDesktop?.completion?.status === "timed_out")
|
|
1427
|
+
return "timed_out";
|
|
1428
|
+
if (liveDesktop?.bootstrap?.status === "failed")
|
|
1429
|
+
return "failed";
|
|
1430
|
+
if (liveDesktop?.url)
|
|
1431
|
+
return "running";
|
|
1432
|
+
if (liveDesktop?.error)
|
|
1433
|
+
return "failed";
|
|
1434
|
+
if (args.missingKeys.length > 0)
|
|
1435
|
+
return "blocked";
|
|
1436
|
+
return "preparing";
|
|
1437
|
+
}
|
|
1438
|
+
function progressForMeta(status, liveDesktop) {
|
|
1439
|
+
if (status === "contract_proof_only")
|
|
1440
|
+
return 100;
|
|
1441
|
+
if (status === "passed")
|
|
1442
|
+
return 100;
|
|
1443
|
+
if (status === "timed_out")
|
|
1444
|
+
return 100;
|
|
1445
|
+
if (status === "failed" && liveDesktop?.completion)
|
|
1446
|
+
return 100;
|
|
1447
|
+
if (status === "blocked")
|
|
1448
|
+
return 18;
|
|
1449
|
+
if (status === "failed")
|
|
1450
|
+
return 8;
|
|
1451
|
+
if (liveDesktop?.completion?.appStatus === "running")
|
|
1452
|
+
return 86;
|
|
1453
|
+
if (liveDesktop?.bootstrap?.status === "started")
|
|
1454
|
+
return 74;
|
|
1455
|
+
if (liveDesktop?.url)
|
|
1456
|
+
return 58;
|
|
1457
|
+
return 34;
|
|
1458
|
+
}
|
|
1459
|
+
function currentStepForMeta(args, assignment, liveDesktop) {
|
|
1460
|
+
const repoLabel = args.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo;
|
|
1461
|
+
if (args.dryRun) {
|
|
1462
|
+
return `Contract ready for ${repoLabel}; no E2B desktop launched.`;
|
|
1463
|
+
}
|
|
1464
|
+
if (liveDesktop?.completion) {
|
|
1465
|
+
return liveDesktop.completion.reason;
|
|
1466
|
+
}
|
|
1467
|
+
if (liveDesktop?.bootstrap?.status === "started") {
|
|
1468
|
+
return liveDesktop.bootstrap.codexMode === "app-server-client"
|
|
1469
|
+
? `Bootstrap terminal launched for ${repoLabel}; Codex app-server actor, Humanish setup, target app, and nested Observer run inside the desktop.`
|
|
1470
|
+
: `Bootstrap terminal launched for ${repoLabel}; Codex TUI attempt, Humanish setup, and nested Observer run inside the desktop.`;
|
|
1471
|
+
}
|
|
1472
|
+
if (liveDesktop?.bootstrap?.status === "failed") {
|
|
1473
|
+
return `Bootstrap launcher failed for ${repoLabel}.`;
|
|
1474
|
+
}
|
|
1475
|
+
if (liveDesktop?.url) {
|
|
1476
|
+
return `Live E2B desktop connected for ${repoLabel}; Codex TUI injection pending.`;
|
|
1477
|
+
}
|
|
1478
|
+
if (liveDesktop?.error) {
|
|
1479
|
+
return `E2B desktop launch failed for ${repoLabel}.`;
|
|
1480
|
+
}
|
|
1481
|
+
if (args.missingKeys.length > 0) {
|
|
1482
|
+
return `Waiting for ${args.missingKeys.join(", ")} before launching ${repoLabel}.`;
|
|
1483
|
+
}
|
|
1484
|
+
return `Ready to launch E2B desktop and run Codex actor setup for ${repoLabel}.`;
|
|
1485
|
+
}
|
|
1486
|
+
function createMetaReview(args) {
|
|
1487
|
+
const started = args.liveDesktops.filter((desktop) => desktop.bootstrap?.status === "started");
|
|
1488
|
+
const terminalCompletions = args.liveDesktops.filter((desktop) => isTerminalCompletion(desktop.completion));
|
|
1489
|
+
const appRunning = args.liveDesktops.filter((desktop) => desktop.completion?.appStatus === "running");
|
|
1490
|
+
const visualVisible = args.liveDesktops.filter((desktop) => desktop.completion?.visualStatus === "visible");
|
|
1491
|
+
const nestedLiveProof = args.liveDesktops.some((desktop) => desktop.completion?.nestedVerifyPassed === true
|
|
1492
|
+
&& /\bhumanish run live\b/.test(desktop.completion.logTail ?? ""));
|
|
1493
|
+
const outcome = classifyMetaLabOutcome(args);
|
|
1494
|
+
const gaps = [
|
|
1495
|
+
nestedLiveProof && appRunning.length > 0 && visualVisible.length > 0
|
|
1496
|
+
? "Target app browser surfaces, nested Observer windows, and nested Humanish live app-url proof are visible inside headed desktops."
|
|
1497
|
+
: appRunning.length > 0 && visualVisible.length > 0
|
|
1498
|
+
? "Target app browser surfaces and nested Observer windows are visible inside headed desktops; nested Humanish live proof is still missing."
|
|
1499
|
+
: appRunning.length > 0
|
|
1500
|
+
? "Target app surfaces responded over HTTP, but headed desktop browser-window visibility was not detected for every lane."
|
|
1501
|
+
: started.length > 0 && terminalCompletions.length === started.length
|
|
1502
|
+
? "OSS lane terminal states are classified from public-safe remote bootstrap evidence, but target app HTTP readiness was not detected."
|
|
1503
|
+
: args.liveDesktops.some((desktop) => desktop.bootstrap?.status === "started")
|
|
1504
|
+
? "Visible E2B bootstrap terminals are launched and run nested Humanish setup plus target app startup; completion is watched in the desktop stream until remote evidence is polled back."
|
|
1505
|
+
: "Nested Humanish Observer evidence is represented as a lane contract until Codex TUI injection and nested Humanish execution land.",
|
|
1506
|
+
nestedLiveProof
|
|
1507
|
+
? "Nested Humanish proof reached live app-url mode with desktop/mobile browser persona evidence; richer app-specific journey manifests remain the next adapter slice."
|
|
1508
|
+
: "Nested Humanish proof did not reach live app-url mode; target app startup or browser evidence is still missing.",
|
|
1509
|
+
"The top-level run does not clone, modify, commit, push, or file issues in target repos.",
|
|
1510
|
+
"Public runs may record GitHub owner/repo slugs; token-backed maintainer/private runs redact repo labels in durable artifacts by default."
|
|
1511
|
+
];
|
|
1512
|
+
if (args.liveRequested && args.missingKeys.length > 0) {
|
|
1513
|
+
gaps.unshift(`Live launch is blocked until ${args.missingKeys.join(", ")} are available in environment.`);
|
|
1514
|
+
}
|
|
1515
|
+
if (args.liveDesktops.some((desktop) => desktop.url) && !args.liveDesktops.some((desktop) => desktop.bootstrap?.status === "started")) {
|
|
1516
|
+
gaps.unshift("Live E2B desktop streams are connected, but Codex TUI injection and nested Humanish execution are not yet automated.");
|
|
1517
|
+
}
|
|
1518
|
+
return {
|
|
1519
|
+
schema: REVIEW_SCHEMA,
|
|
1520
|
+
verdict: outcome.verdict,
|
|
1521
|
+
summary: outcome.verdict === "fail" || outcome.verdict === "timed_out" || outcome.verdict === "blocked"
|
|
1522
|
+
? outcome.reason
|
|
1523
|
+
: args.dryRun
|
|
1524
|
+
? "OSS meta-lab dry-run rendered the Observer-of-Observers contract without provider spend."
|
|
1525
|
+
: terminalCompletions.length > 0
|
|
1526
|
+
? `OSS meta-lab launched live E2B desktop streams, classified ${terminalCompletions.length}/${started.length || terminalCompletions.length} bootstrap terminal state${terminalCompletions.length === 1 ? "" : "s"} from public-safe remote evidence, detected ${appRunning.length}/${terminalCompletions.length} target app HTTP-ready surface${terminalCompletions.length === 1 ? "" : "s"}, and detected ${visualVisible.length}/${terminalCompletions.length} headed desktop visual layout${terminalCompletions.length === 1 ? "" : "s"}.`
|
|
1527
|
+
: args.liveDesktops.some((desktop) => desktop.bootstrap?.status === "started")
|
|
1528
|
+
? "OSS meta-lab launched live E2B desktop streams, injected visible bootstrap terminals, and started target app plus nested Humanish setup inside each desktop."
|
|
1529
|
+
: args.liveDesktops.some((desktop) => desktop.url)
|
|
1530
|
+
? "OSS meta-lab launched live E2B desktop streams and rendered them in the top-level Observer."
|
|
1531
|
+
: "OSS meta-lab rendered the live headed-desktop control surface and marked the missing substrate truth in-lane.",
|
|
1532
|
+
gaps
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function classifyMetaLabOutcome(args) {
|
|
1536
|
+
if (args.dryRun) {
|
|
1537
|
+
return {
|
|
1538
|
+
ok: true,
|
|
1539
|
+
reason: "OSS meta-lab dry-run rendered the Observer-of-Observers contract without provider spend.",
|
|
1540
|
+
verdict: "contract_proof_only"
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (args.liveRequested && args.missingKeys.length > 0) {
|
|
1544
|
+
return {
|
|
1545
|
+
ok: true,
|
|
1546
|
+
reason: `Live launch is blocked until ${args.missingKeys.join(", ")} are available in environment.`,
|
|
1547
|
+
verdict: "blocked"
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
const launchFailures = args.liveDesktops.filter((desktop) => desktop.error || desktop.bootstrap?.status === "failed");
|
|
1551
|
+
if (launchFailures.length > 0) {
|
|
1552
|
+
return {
|
|
1553
|
+
ok: false,
|
|
1554
|
+
reason: `OSS meta-lab failed ${launchFailures.length}/${args.liveDesktops.length} live desktop or bootstrap launch${launchFailures.length === 1 ? "" : "es"}.`,
|
|
1555
|
+
verdict: "fail"
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const failedCompletions = args.liveDesktops.filter((desktop) => desktop.completion?.status === "failed");
|
|
1559
|
+
if (failedCompletions.length > 0) {
|
|
1560
|
+
return {
|
|
1561
|
+
ok: false,
|
|
1562
|
+
reason: `OSS meta-lab classified ${failedCompletions.length}/${args.liveDesktops.length} bootstrap terminal state${failedCompletions.length === 1 ? "" : "s"} as failed from public-safe remote evidence.`,
|
|
1563
|
+
verdict: "fail"
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
const timedOutCompletions = args.liveDesktops.filter((desktop) => desktop.completion?.status === "timed_out");
|
|
1567
|
+
if (timedOutCompletions.length > 0) {
|
|
1568
|
+
return {
|
|
1569
|
+
ok: false,
|
|
1570
|
+
reason: `OSS meta-lab timed out waiting for ${timedOutCompletions.length}/${args.liveDesktops.length} bootstrap completion marker${timedOutCompletions.length === 1 ? "" : "s"}.`,
|
|
1571
|
+
verdict: "timed_out"
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
const blockedCompletions = args.liveDesktops.filter((desktop) => desktop.completion?.status === "blocked");
|
|
1575
|
+
if (blockedCompletions.length > 0) {
|
|
1576
|
+
return {
|
|
1577
|
+
ok: false,
|
|
1578
|
+
reason: `OSS meta-lab classified ${blockedCompletions.length}/${args.liveDesktops.length} bootstrap terminal state${blockedCompletions.length === 1 ? "" : "s"} as blocked from public-safe remote evidence.`,
|
|
1579
|
+
verdict: "blocked"
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
const completedWithMissingApp = args.liveDesktops.filter((desktop) => desktop.completion?.status === "passed"
|
|
1583
|
+
&& desktop.completion.appStatus !== "running");
|
|
1584
|
+
if (completedWithMissingApp.length > 0) {
|
|
1585
|
+
return {
|
|
1586
|
+
ok: false,
|
|
1587
|
+
reason: `OSS meta-lab completed nested Humanish setup but did not detect ${completedWithMissingApp.length}/${args.liveDesktops.length} target app HTTP-ready surface${completedWithMissingApp.length === 1 ? "" : "s"}.`,
|
|
1588
|
+
verdict: "blocked"
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const completedWithMissingVisual = args.liveDesktops.filter((desktop) => desktop.completion?.status === "passed"
|
|
1592
|
+
&& desktop.completion.visualStatus !== "visible");
|
|
1593
|
+
if (completedWithMissingVisual.length > 0) {
|
|
1594
|
+
return {
|
|
1595
|
+
ok: false,
|
|
1596
|
+
reason: `OSS meta-lab completed nested Humanish setup but did not detect ${completedWithMissingVisual.length}/${args.liveDesktops.length} headed desktop visual layout${completedWithMissingVisual.length === 1 ? "" : "s"}.`,
|
|
1597
|
+
verdict: "blocked"
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
const completedWithMissingAppServerActor = args.liveDesktops.filter((desktop) => desktop.bootstrap?.codexMode === "app-server-client"
|
|
1601
|
+
&& desktop.completion?.status === "passed"
|
|
1602
|
+
&& desktop.completion.actorStatus !== "passed");
|
|
1603
|
+
if (completedWithMissingAppServerActor.length > 0) {
|
|
1604
|
+
return {
|
|
1605
|
+
ok: false,
|
|
1606
|
+
reason: `OSS meta-lab completed nested Humanish setup but did not capture passed Codex app-server actor evidence for ${completedWithMissingAppServerActor.length}/${args.liveDesktops.length} headed desktop lane${completedWithMissingAppServerActor.length === 1 ? "" : "s"}.`,
|
|
1607
|
+
verdict: "blocked"
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
if (args.liveDesktops.length > 0 && args.liveDesktops.every((desktop) => desktop.completion?.status === "passed")) {
|
|
1611
|
+
return {
|
|
1612
|
+
ok: true,
|
|
1613
|
+
reason: `OSS meta-lab passed ${args.liveDesktops.length}/${args.liveDesktops.length} bootstrap terminal states with target app HTTP readiness and headed desktop visual layout detected from public-safe remote evidence.`,
|
|
1614
|
+
verdict: "pass"
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
ok: true,
|
|
1619
|
+
reason: "OSS meta-lab rendered the live headed-desktop control surface and marked the missing substrate truth in-lane.",
|
|
1620
|
+
verdict: "contract_proof_only"
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
function terminalTailForMeta(prompt, liveDesktop) {
|
|
1624
|
+
if (!liveDesktop?.completion) {
|
|
1625
|
+
return liveDesktop?.bootstrap?.tail ?? prompt;
|
|
1626
|
+
}
|
|
1627
|
+
const lines = [
|
|
1628
|
+
`Remote bootstrap ${liveDesktop.completion.status}: ${liveDesktop.completion.reason}`,
|
|
1629
|
+
`checked_at: ${liveDesktop.completion.checkedAt}`,
|
|
1630
|
+
...(liveDesktop.completion.appStatus === undefined ? [] : [`app_status: ${liveDesktop.completion.appStatus}`]),
|
|
1631
|
+
...(liveDesktop.completion.appUrl === undefined ? [] : [`app_url: ${liveDesktop.completion.appUrl}`]),
|
|
1632
|
+
...(liveDesktop.completion.appReason === undefined ? [] : [`app_reason: ${liveDesktop.completion.appReason}`]),
|
|
1633
|
+
...(liveDesktop.completion.actorStatus === undefined ? [] : [`actor_status: ${liveDesktop.completion.actorStatus}`]),
|
|
1634
|
+
...(liveDesktop.completion.exitCode === undefined ? [] : [`exit_code: ${liveDesktop.completion.exitCode}`]),
|
|
1635
|
+
...(liveDesktop.completion.nestedVerifyPassed === undefined ? [] : [`nested_verify_passed: ${liveDesktop.completion.nestedVerifyPassed ? "true" : "false"}`]),
|
|
1636
|
+
...(liveDesktop.completion.nestedObserverPresent === undefined ? [] : [`nested_observer_present: ${liveDesktop.completion.nestedObserverPresent ? "true" : "false"}`]),
|
|
1637
|
+
...(liveDesktop.completion.visualStatus === undefined ? [] : [`visual_status: ${liveDesktop.completion.visualStatus}`]),
|
|
1638
|
+
...(liveDesktop.completion.visualWindowCount === undefined ? [] : [`visual_window_count: ${liveDesktop.completion.visualWindowCount}`]),
|
|
1639
|
+
...(liveDesktop.completion.visualReason === undefined ? [] : [`visual_reason: ${liveDesktop.completion.visualReason}`]),
|
|
1640
|
+
...(liveDesktop.completion.setupQuality === undefined ? [] : [
|
|
1641
|
+
`setup_quality: ${liveDesktop.completion.setupQuality.status}`,
|
|
1642
|
+
`setup_summary: ${liveDesktop.completion.setupQuality.summary}`,
|
|
1643
|
+
...(liveDesktop.completion.setupQuality.studyQuality === undefined ? [] : [
|
|
1644
|
+
`study_quality: ${liveDesktop.completion.setupQuality.studyQuality.rating}`,
|
|
1645
|
+
`study_summary: ${liveDesktop.completion.setupQuality.studyQuality.summary}`
|
|
1646
|
+
])
|
|
1647
|
+
]),
|
|
1648
|
+
"",
|
|
1649
|
+
"public-safe actor last message tail:",
|
|
1650
|
+
liveDesktop.completion.actorLastMessageTail?.trim() || "(no actor last-message captured)",
|
|
1651
|
+
"",
|
|
1652
|
+
"public-safe actor log tail:",
|
|
1653
|
+
liveDesktop.completion.actorLogTail?.trim() || "(no actor log tail captured)",
|
|
1654
|
+
"",
|
|
1655
|
+
"public-safe bootstrap log tail:",
|
|
1656
|
+
liveDesktop.completion.logTail?.trim() || "(no log tail captured)"
|
|
1657
|
+
];
|
|
1658
|
+
return lines.join("\n").trim();
|
|
1659
|
+
}
|
|
1660
|
+
function completionForStream(completion, appServerMode = false) {
|
|
1661
|
+
const effectiveStatus = appServerMode && completion.status === "passed" && completion.actorStatus !== "passed"
|
|
1662
|
+
? "blocked"
|
|
1663
|
+
: completion.status;
|
|
1664
|
+
const effectiveReason = effectiveStatus === "blocked" && completion.status === "passed" && completion.actorStatus !== "passed"
|
|
1665
|
+
? "Codex app-server mode requires passed app-server actor evidence; actor evidence did not reach passed."
|
|
1666
|
+
: completion.reason;
|
|
1667
|
+
const meaningfulUse = scoreOssMetaMeaningfulUse({
|
|
1668
|
+
...(completion.actorLastMessageTail === undefined ? {} : { actorLastMessageTail: completion.actorLastMessageTail }),
|
|
1669
|
+
...(completion.actorLogTail === undefined ? {} : { actorLogTail: completion.actorLogTail }),
|
|
1670
|
+
actorRequired: appServerMode,
|
|
1671
|
+
...(completion.actorStatus === undefined ? {} : { actorStatus: completion.actorStatus }),
|
|
1672
|
+
...(completion.appStatus === undefined ? {} : { appStatus: completion.appStatus }),
|
|
1673
|
+
...(completion.appUrl === undefined ? {} : { appUrl: completion.appUrl }),
|
|
1674
|
+
...(completion.nestedObserverPresent === undefined ? {} : { nestedObserverPresent: completion.nestedObserverPresent }),
|
|
1675
|
+
...(completion.nestedVerifyPassed === undefined ? {} : { nestedVerifyPassed: completion.nestedVerifyPassed }),
|
|
1676
|
+
...(completion.setupQuality === undefined ? {} : { setupQuality: completion.setupQuality }),
|
|
1677
|
+
status: effectiveStatus,
|
|
1678
|
+
...(completion.visualStatus === undefined ? {} : { visualStatus: completion.visualStatus })
|
|
1679
|
+
});
|
|
1680
|
+
return {
|
|
1681
|
+
...(completion.actorLogPath === undefined ? {} : { actorLogPath: completion.actorLogPath }),
|
|
1682
|
+
...(completion.actorLogTail === undefined ? {} : { actorLogTail: completion.actorLogTail }),
|
|
1683
|
+
...(completion.actorLastMessageTail === undefined ? {} : { actorLastMessageTail: completion.actorLastMessageTail }),
|
|
1684
|
+
...(completion.actorPid === undefined ? {} : { actorPid: completion.actorPid }),
|
|
1685
|
+
...(completion.actorStatus === undefined ? {} : { actorStatus: completion.actorStatus }),
|
|
1686
|
+
...(completion.appLogPath === undefined ? {} : { appLogPath: completion.appLogPath }),
|
|
1687
|
+
...(completion.appPid === undefined ? {} : { appPid: completion.appPid }),
|
|
1688
|
+
...(completion.appReason === undefined ? {} : { appReason: completion.appReason }),
|
|
1689
|
+
...(completion.appStatus === undefined ? {} : { appStatus: completion.appStatus }),
|
|
1690
|
+
...(completion.appUrl === undefined ? {} : { appUrl: completion.appUrl }),
|
|
1691
|
+
checkedAt: completion.checkedAt,
|
|
1692
|
+
...(completion.exitCode === undefined ? {} : { exitCode: completion.exitCode }),
|
|
1693
|
+
...(completion.logTail === undefined ? {} : { logTail: completion.logTail }),
|
|
1694
|
+
...(completion.nestedObserverPresent === undefined ? {} : { nestedObserverPresent: completion.nestedObserverPresent }),
|
|
1695
|
+
...(completion.nestedVerifyPassed === undefined ? {} : { nestedVerifyPassed: completion.nestedVerifyPassed }),
|
|
1696
|
+
reason: effectiveReason,
|
|
1697
|
+
status: effectiveStatus,
|
|
1698
|
+
meaningfulUse,
|
|
1699
|
+
...(completion.visualReason === undefined ? {} : { visualReason: completion.visualReason }),
|
|
1700
|
+
...(completion.visualStatus === undefined ? {} : { visualStatus: completion.visualStatus }),
|
|
1701
|
+
...(completion.visualWindowCount === undefined ? {} : { visualWindowCount: completion.visualWindowCount })
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function buildMetaFeedbackCandidates(args) {
|
|
1705
|
+
const candidates = [];
|
|
1706
|
+
for (const assignment of args.assignments) {
|
|
1707
|
+
const desktop = args.liveDesktops.find((lane) => lane.streamId === assignment.streamId);
|
|
1708
|
+
if (!desktop) {
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const repoLabel = args.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo;
|
|
1712
|
+
const scenarioId = args.redactRepoNames ? `oss-meta-${repoLabel}` : assignment.scenarioId;
|
|
1713
|
+
const baseEvidence = feedbackEvidenceForDesktop(desktop);
|
|
1714
|
+
const setupQualityPath = desktop.actorEvidence?.setupQualityPath;
|
|
1715
|
+
const failedSetupChecks = desktop.completion?.setupQuality?.checks.filter((check) => !check.ok) ?? [];
|
|
1716
|
+
const studyQuality = desktop.completion?.setupQuality?.studyQuality;
|
|
1717
|
+
if (setupQualityPath && failedSetupChecks.length > 0) {
|
|
1718
|
+
candidates.push({
|
|
1719
|
+
schema: "humanish.feedback-candidate.v1",
|
|
1720
|
+
id: `setup-quality-${safeArtifactToken(assignment.streamId)}`,
|
|
1721
|
+
run_id: args.runId,
|
|
1722
|
+
stream_id: assignment.streamId,
|
|
1723
|
+
adapter_id: "oss-meta-lab",
|
|
1724
|
+
scenario_id: scenarioId,
|
|
1725
|
+
persona_id: `codex-oss-operator-${String(assignment.index).padStart(2, "0")}`,
|
|
1726
|
+
actor: "codex-tui",
|
|
1727
|
+
substrate: "e2b-desktop",
|
|
1728
|
+
failure_owner: "actor",
|
|
1729
|
+
summary: `Generated Humanish setup for ${repoLabel} needs review`,
|
|
1730
|
+
expected: "The setup actor should create committed Humanish source files, useful personas/scenarios, a package script, and a .humanish/ runtime ignore without preserving private state.",
|
|
1731
|
+
actual: failedSetupChecks.map((check) => `${check.label}: ${check.detail}`).join(" "),
|
|
1732
|
+
evidence: [
|
|
1733
|
+
{
|
|
1734
|
+
path: setupQualityPath,
|
|
1735
|
+
kind: "filesystem",
|
|
1736
|
+
note: "Setup-quality snapshot with tree, checks, package scripts, and allowlisted previews."
|
|
1737
|
+
},
|
|
1738
|
+
...baseEvidence
|
|
1739
|
+
],
|
|
1740
|
+
redaction: {
|
|
1741
|
+
status: "passed",
|
|
1742
|
+
notes: "Feedback candidate references local public-safe run artifacts only."
|
|
1743
|
+
},
|
|
1744
|
+
idempotency_key: `humanish:${args.runId}:${assignment.streamId}:setup-quality`,
|
|
1745
|
+
proposed_next_state: "setup-quality-review",
|
|
1746
|
+
acceptance_proof: [
|
|
1747
|
+
`pnpm humanish -- verify --run ${args.runId} --json`,
|
|
1748
|
+
`pnpm humanish -- watch --run ${args.runId} --no-open`
|
|
1749
|
+
]
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
const actorText = `${desktop.completion?.actorLastMessageTail ?? ""}\n${desktop.completion?.actorLogTail ?? ""}`;
|
|
1753
|
+
if (hasAppUrlProofBlocker(actorText)) {
|
|
1754
|
+
candidates.push({
|
|
1755
|
+
schema: "humanish.feedback-candidate.v1",
|
|
1756
|
+
id: `published-cli-app-url-${safeArtifactToken(assignment.streamId)}`,
|
|
1757
|
+
run_id: args.runId,
|
|
1758
|
+
stream_id: assignment.streamId,
|
|
1759
|
+
adapter_id: "oss-meta-lab",
|
|
1760
|
+
scenario_id: scenarioId,
|
|
1761
|
+
persona_id: `codex-oss-operator-${String(assignment.index).padStart(2, "0")}`,
|
|
1762
|
+
actor: "codex-tui",
|
|
1763
|
+
substrate: "e2b-desktop",
|
|
1764
|
+
failure_owner: "harness",
|
|
1765
|
+
summary: "Published Humanish install path blocked app-url proof",
|
|
1766
|
+
expected: "A fresh npm-installed Humanish CLI should support the app-url live proof path documented for agents.",
|
|
1767
|
+
actual: "The actor evidence reports that the installed CLI did not accept or expose the app-url proof option.",
|
|
1768
|
+
evidence: baseEvidence,
|
|
1769
|
+
redaction: {
|
|
1770
|
+
status: "passed",
|
|
1771
|
+
notes: "Actor evidence was redacted before persistence."
|
|
1772
|
+
},
|
|
1773
|
+
idempotency_key: `humanish:${args.runId}:${assignment.streamId}:published-cli-app-url`,
|
|
1774
|
+
proposed_next_state: "adapter-hardening",
|
|
1775
|
+
acceptance_proof: [
|
|
1776
|
+
"npm view humanish version",
|
|
1777
|
+
"npx --yes --package humanish humanish run --help | grep -- --app-url",
|
|
1778
|
+
`pnpm humanish -- verify --run ${args.runId} --json`
|
|
1779
|
+
]
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
if (setupQualityPath && studyQuality && (studyQuality.rating === "none" || studyQuality.rating === "ceremonial")) {
|
|
1783
|
+
candidates.push({
|
|
1784
|
+
schema: "humanish.feedback-candidate.v1",
|
|
1785
|
+
id: `study-quality-${safeArtifactToken(assignment.streamId)}`,
|
|
1786
|
+
run_id: args.runId,
|
|
1787
|
+
stream_id: assignment.streamId,
|
|
1788
|
+
adapter_id: "oss-meta-lab",
|
|
1789
|
+
scenario_id: scenarioId,
|
|
1790
|
+
persona_id: `codex-oss-operator-${String(assignment.index).padStart(2, "0")}`,
|
|
1791
|
+
actor: "codex-tui",
|
|
1792
|
+
substrate: "e2b-desktop",
|
|
1793
|
+
failure_owner: "actor",
|
|
1794
|
+
summary: `Generated Humanish setup for ${repoLabel} was ${studyQuality.rating}`,
|
|
1795
|
+
expected: "The setup actor should turn Humanish init into an app-aware user-study plan with customized coverage, personas, scenarios, app-url proof, and public-safe feedback.",
|
|
1796
|
+
actual: studyQuality.summary,
|
|
1797
|
+
evidence: [
|
|
1798
|
+
{
|
|
1799
|
+
path: setupQualityPath,
|
|
1800
|
+
kind: "filesystem",
|
|
1801
|
+
note: "Setup-quality snapshot includes study-quality checks and public-safe structural signals."
|
|
1802
|
+
},
|
|
1803
|
+
...baseEvidence
|
|
1804
|
+
],
|
|
1805
|
+
redaction: {
|
|
1806
|
+
status: "passed",
|
|
1807
|
+
notes: "Study-quality feedback candidate references local public-safe artifacts only."
|
|
1808
|
+
},
|
|
1809
|
+
idempotency_key: `humanish:${args.runId}:${assignment.streamId}:study-quality`,
|
|
1810
|
+
proposed_next_state: "study-quality-review",
|
|
1811
|
+
acceptance_proof: [
|
|
1812
|
+
`pnpm humanish -- verify --run ${args.runId} --json`,
|
|
1813
|
+
`pnpm humanish -- watch --run ${args.runId} --no-open`,
|
|
1814
|
+
"Study-quality rating is useful or high_leverage, or the remaining ceremonial state is explicitly explained."
|
|
1815
|
+
]
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return candidates.slice(0, 20);
|
|
1820
|
+
}
|
|
1821
|
+
function feedbackEvidenceForDesktop(desktop) {
|
|
1822
|
+
const evidence = [];
|
|
1823
|
+
if (desktop.actorEvidence?.actorLastMessageTailPath) {
|
|
1824
|
+
evidence.push({
|
|
1825
|
+
path: desktop.actorEvidence.actorLastMessageTailPath,
|
|
1826
|
+
kind: "log",
|
|
1827
|
+
note: "Public-safe actor last-message tail."
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
if (desktop.actorEvidence?.actorLogTailPath) {
|
|
1831
|
+
evidence.push({
|
|
1832
|
+
path: desktop.actorEvidence.actorLogTailPath,
|
|
1833
|
+
kind: "log",
|
|
1834
|
+
note: "Public-safe actor log tail."
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
if (desktop.screenshot?.path) {
|
|
1838
|
+
evidence.push({
|
|
1839
|
+
path: desktop.screenshot.path,
|
|
1840
|
+
kind: "screenshot",
|
|
1841
|
+
note: "Headed desktop screenshot fallback."
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
if (desktop.hostActorPlanPath) {
|
|
1845
|
+
evidence.push({
|
|
1846
|
+
path: desktop.hostActorPlanPath,
|
|
1847
|
+
kind: "trace",
|
|
1848
|
+
note: "Host-authored public-safe actor plan."
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return evidence;
|
|
1852
|
+
}
|
|
1853
|
+
function eventLevelForCompletion(status) {
|
|
1854
|
+
if (status === "passed")
|
|
1855
|
+
return "info";
|
|
1856
|
+
if (status === "running")
|
|
1857
|
+
return "info";
|
|
1858
|
+
if (status === "blocked" || status === "timed_out")
|
|
1859
|
+
return "warn";
|
|
1860
|
+
return "error";
|
|
1861
|
+
}
|
|
1862
|
+
function isTerminalCompletion(completion) {
|
|
1863
|
+
return completion !== undefined && completion.status !== "running";
|
|
1864
|
+
}
|
|
1865
|
+
function buildCodexBootstrapPrompt(assignment, redactRepoName = false) {
|
|
1866
|
+
const repoLabel = redactRepoName ? "[redacted-authorized-repo]" : `https://github.com/${assignment.repo}.git`;
|
|
1867
|
+
return [
|
|
1868
|
+
`# Humanish OSS Meta-Lab Actor ${assignment.index}`,
|
|
1869
|
+
"",
|
|
1870
|
+
"You are running inside a disposable headed E2B desktop with a visible terminal and browser.",
|
|
1871
|
+
"Public-safety hard rails: use only authorized repo contents; never print keys; never commit, push, file issues, or preserve private artifacts.",
|
|
1872
|
+
"",
|
|
1873
|
+
`Target repo: ${repoLabel}`,
|
|
1874
|
+
"",
|
|
1875
|
+
"Mission:",
|
|
1876
|
+
"1. Clone the target repo into a clean disposable workspace.",
|
|
1877
|
+
"2. Inspect the package manager, dev scripts, README, and app shape.",
|
|
1878
|
+
"3. Get the repo into a local runnable dev mode if feasible.",
|
|
1879
|
+
"4. Discover the Humanish skill path and try installing it with `npx skills add danielgwilson/humanish --skill humanish`.",
|
|
1880
|
+
"5. Install Humanish as a dev dependency with the package manager the repo already uses. The package is `humanish`; the binary is `humanish`.",
|
|
1881
|
+
"6. Run `npx --no-install humanish init --yes` or the package-manager equivalent. Do not run bare `npx humanish` unless you have confirmed it resolves the local `humanish` binary.",
|
|
1882
|
+
"7. Replace starter coverage files with an app-aware `humanish/coverage-map.md` and `humanish/coverage-matrix.md` that name real screens, roles, states, happy paths, and at least one sad/friction path discovered from the repo/app.",
|
|
1883
|
+
"8. Author at least two public-safe, app-specific Humanish personas and two desktop/mobile browser scenarios. Avoid generic `first-run-smoke` only; each scenario should name a target journey, route/state, expected evidence, and a failure/friction check.",
|
|
1884
|
+
"9. Run `npx --no-install humanish run --help` and verify `--app-url` is available. If the local binary is missing, install `humanish`; one-shot fallback is `npx --yes --package humanish@latest humanish run --help`.",
|
|
1885
|
+
"10. If the app is running locally, run `npx --no-install humanish run --app-url <loopback-url> --sims 2`; do not use `humanish watch --sims` as app behavior proof.",
|
|
1886
|
+
"11. After `run --app-url`, render or open the nested Humanish Observer with `npx --no-install humanish watch --run latest --detach --no-open --json` and keep it visible.",
|
|
1887
|
+
"12. Final summary must be public-safe and include: personas/scenarios created, product journeys covered, one observed friction/improvement or `none observed`, and evidence paths. Do not stop at install/init proof.",
|
|
1888
|
+
"13. Record public-safe blockers and evidence paths only.",
|
|
1889
|
+
"",
|
|
1890
|
+
"Expected nested outcome: the top-level Humanish Observer shows this desktop, and this desktop shows its own nested Humanish Observer."
|
|
1891
|
+
].join("\n");
|
|
1892
|
+
}
|
|
1893
|
+
function renderMetaReviewMarkdown(bundle) {
|
|
1894
|
+
return `# Humanish OSS Meta-Lab Review
|
|
1895
|
+
|
|
1896
|
+
Run: ${bundle.runId}
|
|
1897
|
+
|
|
1898
|
+
Verdict: ${bundle.review.verdict}
|
|
1899
|
+
|
|
1900
|
+
${bundle.review.summary}
|
|
1901
|
+
|
|
1902
|
+
## Public-Safety
|
|
1903
|
+
|
|
1904
|
+
- Redaction: ${bundle.redaction.status}
|
|
1905
|
+
- Notes: ${bundle.redaction.notes}
|
|
1906
|
+
|
|
1907
|
+
## Assigned Repos
|
|
1908
|
+
|
|
1909
|
+
${bundle.streams.map((stream) => `- ${stream.label}: ${stream.simId}`).join("\n")}
|
|
1910
|
+
|
|
1911
|
+
## Gaps
|
|
1912
|
+
|
|
1913
|
+
${bundle.review.gaps.map((gap) => `- ${gap}`).join("\n")}
|
|
1914
|
+
`;
|
|
1915
|
+
}
|
|
1916
|
+
async function launchLiveDesktops(assignments, options = {}) {
|
|
1917
|
+
const e2bApiKey = process.env.E2B_API_KEY;
|
|
1918
|
+
if (!e2bApiKey) {
|
|
1919
|
+
return [];
|
|
1920
|
+
}
|
|
1921
|
+
const desktopModule = await loadE2BDesktopModule();
|
|
1922
|
+
const timeoutMs = readPositiveInt(process.env.HUMANISH_E2B_TIMEOUT_MS, 60 * 60 * 1000);
|
|
1923
|
+
const requestTimeoutMs = readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
1924
|
+
return Promise.all(assignments.map(async (assignment) => {
|
|
1925
|
+
const repoLabel = options.redactRepoNames ? repoArtifactLabel(assignment) : assignment.repo;
|
|
1926
|
+
const hostActorPlanResult = options.hostActorPlansByStream?.get(assignment.streamId);
|
|
1927
|
+
// Retain the provider object in process memory so attached cleanup can use a trusted handle.
|
|
1928
|
+
// Durable result IDs are evidence only and never authorize provider mutation.
|
|
1929
|
+
let desktop;
|
|
1930
|
+
try {
|
|
1931
|
+
desktop = await desktopModule.Sandbox.create({
|
|
1932
|
+
apiKey: e2bApiKey,
|
|
1933
|
+
requestTimeoutMs,
|
|
1934
|
+
timeoutMs,
|
|
1935
|
+
metadata: {
|
|
1936
|
+
...OSS_META_LAB_PROVIDER_METADATA,
|
|
1937
|
+
repo: repoLabel,
|
|
1938
|
+
simId: assignment.simId
|
|
1939
|
+
},
|
|
1940
|
+
envs: {
|
|
1941
|
+
...collectOssMetaLabRemoteEnv(process.env),
|
|
1942
|
+
...(options.codexAppServerMode ? { HUMANISH_OSS_META_CODEX_APP_SERVER: "1" } : {})
|
|
1943
|
+
},
|
|
1944
|
+
resolution: [1440, 960],
|
|
1945
|
+
dpi: 96,
|
|
1946
|
+
lifecycle: {
|
|
1947
|
+
onTimeout: "kill"
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
const bootstrap = await startOssBootstrap(desktop, assignment, options.localPackage, requestTimeoutMs, {
|
|
1951
|
+
...(options.codexAppServerMode === undefined ? {} : { codexAppServerMode: options.codexAppServerMode }),
|
|
1952
|
+
...(hostActorPlanResult === undefined ? {} : { hostActorPlanResult }),
|
|
1953
|
+
repoLabel,
|
|
1954
|
+
token: options.redactRepoNames ? repoLabel : repoSlug(assignment.repo)
|
|
1955
|
+
});
|
|
1956
|
+
await desktop.wait(750).catch(() => undefined);
|
|
1957
|
+
await desktop.stream.start({ requireAuth: true });
|
|
1958
|
+
const authKey = desktop.stream.getAuthKey();
|
|
1959
|
+
const url = desktop.stream.getUrl({
|
|
1960
|
+
authKey,
|
|
1961
|
+
autoConnect: true,
|
|
1962
|
+
viewOnly: true,
|
|
1963
|
+
resize: "scale"
|
|
1964
|
+
});
|
|
1965
|
+
return {
|
|
1966
|
+
bootstrap,
|
|
1967
|
+
desktop,
|
|
1968
|
+
...(hostActorPlanResult?.plan ? { hostActorPlan: hostActorPlanResult.plan } : {}),
|
|
1969
|
+
...(hostActorPlanResult?.artifactPath ? { hostActorPlanPath: hostActorPlanResult.artifactPath } : {}),
|
|
1970
|
+
repo: repoLabel,
|
|
1971
|
+
sandboxId: desktop.sandboxId,
|
|
1972
|
+
simId: assignment.simId,
|
|
1973
|
+
streamId: assignment.streamId,
|
|
1974
|
+
url
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
catch (error) {
|
|
1978
|
+
return {
|
|
1979
|
+
error: compactError(error),
|
|
1980
|
+
...(desktop ? { desktop } : {}),
|
|
1981
|
+
...(hostActorPlanResult?.plan ? { hostActorPlan: hostActorPlanResult.plan } : {}),
|
|
1982
|
+
...(hostActorPlanResult?.artifactPath ? { hostActorPlanPath: hostActorPlanResult.artifactPath } : {}),
|
|
1983
|
+
repo: repoLabel,
|
|
1984
|
+
// Keep the id as public-safe lifecycle evidence only. Cleanup authority comes from the
|
|
1985
|
+
// in-memory provider object above or a separately verified provider-metadata sweep.
|
|
1986
|
+
...(desktop?.sandboxId ? { sandboxId: desktop.sandboxId } : {}),
|
|
1987
|
+
simId: assignment.simId,
|
|
1988
|
+
streamId: assignment.streamId
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
}));
|
|
1992
|
+
}
|
|
1993
|
+
async function pollLiveDesktopCompletions(liveDesktops, options = {}) {
|
|
1994
|
+
const pollable = liveDesktops.filter((desktop) => desktop.desktop
|
|
1995
|
+
&& desktop.bootstrap?.status === "started"
|
|
1996
|
+
&& desktop.bootstrap.completionPath);
|
|
1997
|
+
if (pollable.length === 0) {
|
|
1998
|
+
return { warnings: [] };
|
|
1999
|
+
}
|
|
2000
|
+
const timeoutMs = options.timeoutMs ?? readNonNegativeInt(process.env.HUMANISH_OSS_META_COMPLETION_TIMEOUT_MS, 240_000);
|
|
2001
|
+
const intervalMs = readPositiveInt(process.env.HUMANISH_OSS_META_COMPLETION_INTERVAL_MS, 5_000);
|
|
2002
|
+
const requestTimeoutMs = readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
2003
|
+
const warnings = [];
|
|
2004
|
+
if (timeoutMs === 0) {
|
|
2005
|
+
warnings.push(`Initial OSS meta-lab completion wait skipped because ${options.timeoutReason ?? "HUMANISH_OSS_META_COMPLETION_TIMEOUT_MS=0"}; attached watch continues polling while the Observer is open.`);
|
|
2006
|
+
return { warnings };
|
|
2007
|
+
}
|
|
2008
|
+
const deadline = Date.now() + timeoutMs;
|
|
2009
|
+
while (Date.now() <= deadline) {
|
|
2010
|
+
await Promise.all(pollable.map(async (desktop) => {
|
|
2011
|
+
if (isTerminalCompletion(desktop.completion) || !desktop.desktop || !desktop.bootstrap?.completionPath) {
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const completion = await readRemoteCompletion(desktop.desktop, desktop.bootstrap, requestTimeoutMs);
|
|
2015
|
+
if (completion) {
|
|
2016
|
+
desktop.completion = completion;
|
|
2017
|
+
}
|
|
2018
|
+
}));
|
|
2019
|
+
if (pollable.every((desktop) => isTerminalCompletion(desktop.completion))) {
|
|
2020
|
+
return { warnings };
|
|
2021
|
+
}
|
|
2022
|
+
await wait(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
2023
|
+
}
|
|
2024
|
+
await Promise.all(pollable.map(async (desktop) => {
|
|
2025
|
+
if (isTerminalCompletion(desktop.completion) || !desktop.desktop || !desktop.bootstrap) {
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
desktop.completion = {
|
|
2029
|
+
checkedAt: new Date().toISOString(),
|
|
2030
|
+
logTail: await readRemoteLogTail(desktop.desktop, desktop.bootstrap, requestTimeoutMs),
|
|
2031
|
+
reason: `Timed out waiting ${timeoutMs}ms for remote bootstrap completion marker.`,
|
|
2032
|
+
status: "timed_out"
|
|
2033
|
+
};
|
|
2034
|
+
}));
|
|
2035
|
+
warnings.push(`Timed out waiting for ${pollable.filter((desktop) => desktop.completion?.status === "timed_out").length}/${pollable.length} OSS meta-lab bootstrap completion marker${pollable.length === 1 ? "" : "s"}.`);
|
|
2036
|
+
return { warnings };
|
|
2037
|
+
}
|
|
2038
|
+
async function refreshOssMetaLabLiveRuntime(runtime, options) {
|
|
2039
|
+
await refreshLiveDesktopProgress(runtime.liveDesktops, {
|
|
2040
|
+
timedOut: options.timedOut,
|
|
2041
|
+
timeoutMs: options.timeoutMs
|
|
2042
|
+
});
|
|
2043
|
+
if (options.captureScreenshots) {
|
|
2044
|
+
await captureLiveDesktopScreenshots(runtime.artifactRoot, runtime.liveDesktops);
|
|
2045
|
+
}
|
|
2046
|
+
await writeActorEvidenceArtifacts(runtime.artifactRoot, runtime.liveDesktops, {
|
|
2047
|
+
assignments: runtime.assignments,
|
|
2048
|
+
redactRepoNames: runtime.redactRepoNames
|
|
2049
|
+
});
|
|
2050
|
+
const bundle = buildMetaBundle({
|
|
2051
|
+
assignments: runtime.assignments,
|
|
2052
|
+
createdAt: runtime.createdAt,
|
|
2053
|
+
cwd: runtime.cwd,
|
|
2054
|
+
dryRun: runtime.dryRun,
|
|
2055
|
+
liveDesktops: runtime.liveDesktops,
|
|
2056
|
+
liveRequested: runtime.liveRequested,
|
|
2057
|
+
missingKeys: runtime.missingKeys,
|
|
2058
|
+
redactRepoNames: runtime.redactRepoNames,
|
|
2059
|
+
runId: runtime.runId,
|
|
2060
|
+
source: runtime.source
|
|
2061
|
+
});
|
|
2062
|
+
await writeMetaBundleArtifacts(runtime.artifactRoot, bundle);
|
|
2063
|
+
await renderObserver(runtime.physicalCwd, runtime.runId, { open: false });
|
|
2064
|
+
}
|
|
2065
|
+
async function refreshLiveDesktopProgress(liveDesktops, options) {
|
|
2066
|
+
const requestTimeoutMs = readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
2067
|
+
await Promise.all(liveDesktops.map(async (desktop) => {
|
|
2068
|
+
if (!desktop.desktop || desktop.bootstrap?.status !== "started") {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
if (isTerminalCompletion(desktop.completion)) {
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const completion = await readRemoteCompletion(desktop.desktop, desktop.bootstrap, requestTimeoutMs);
|
|
2075
|
+
if (completion) {
|
|
2076
|
+
desktop.completion = completion;
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
const logTail = await readRemoteLogTail(desktop.desktop, desktop.bootstrap, requestTimeoutMs);
|
|
2080
|
+
desktop.completion = {
|
|
2081
|
+
checkedAt: new Date().toISOString(),
|
|
2082
|
+
logTail,
|
|
2083
|
+
reason: options.timedOut
|
|
2084
|
+
? `Timed out waiting ${options.timeoutMs}ms for remote bootstrap completion marker.`
|
|
2085
|
+
: logTail
|
|
2086
|
+
? "Remote bootstrap is running; latest public-safe log tail is available."
|
|
2087
|
+
: "Remote bootstrap is running; waiting for first public-safe log output.",
|
|
2088
|
+
status: options.timedOut ? "timed_out" : "running"
|
|
2089
|
+
};
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
async function readRemoteCompletion(desktop, bootstrap, requestTimeoutMs) {
|
|
2093
|
+
if (!bootstrap.completionPath) {
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
const command = `if [ -f ${shellQuote(bootstrap.completionPath)} ]; then cat ${shellQuote(bootstrap.completionPath)}; else exit 3; fi`;
|
|
2097
|
+
const result = await desktop.commands.run(`bash -lc ${shellQuote(command)}`, {
|
|
2098
|
+
requestTimeoutMs,
|
|
2099
|
+
timeoutMs: 30_000
|
|
2100
|
+
}).catch(() => null);
|
|
2101
|
+
if (!result || (result.exitCode && result.exitCode !== 0) || !result.stdout) {
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
return parseRemoteCompletion(result.stdout);
|
|
2105
|
+
}
|
|
2106
|
+
async function readRemoteLogTail(desktop, bootstrap, requestTimeoutMs) {
|
|
2107
|
+
if (!bootstrap.logPath) {
|
|
2108
|
+
return "";
|
|
2109
|
+
}
|
|
2110
|
+
const command = `tail -n 80 ${shellQuote(bootstrap.logPath)} 2>/dev/null || true`;
|
|
2111
|
+
const result = await desktop.commands.run(`bash -lc ${shellQuote(command)}`, {
|
|
2112
|
+
requestTimeoutMs,
|
|
2113
|
+
timeoutMs: 30_000
|
|
2114
|
+
}).catch(() => null);
|
|
2115
|
+
return sanitizeRemoteLog(result?.stdout ?? "");
|
|
2116
|
+
}
|
|
2117
|
+
async function captureLiveDesktopScreenshots(artifactRoot, liveDesktops, options = { redactRepoNames: false }) {
|
|
2118
|
+
const candidates = liveDesktops.filter((desktop) => desktop.desktop && desktop.url);
|
|
2119
|
+
if (candidates.length === 0) {
|
|
2120
|
+
return { warnings: [] };
|
|
2121
|
+
}
|
|
2122
|
+
const warnings = [];
|
|
2123
|
+
await Promise.all(candidates.map(async (desktop) => {
|
|
2124
|
+
if (!desktop.desktop) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
try {
|
|
2128
|
+
await arrangeLiveDesktopForScreenshot(desktop.desktop, desktop.bootstrap?.terminalTitle, readPositiveInt(process.env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000));
|
|
2129
|
+
await desktop.desktop.wait(readPositiveInt(process.env.HUMANISH_OSS_META_SCREENSHOT_SETTLE_MS, 2_500)).catch(() => undefined);
|
|
2130
|
+
const bytes = await desktop.desktop.screenshot("bytes");
|
|
2131
|
+
const fileName = `${safeArtifactToken(desktop.streamId)}.png`;
|
|
2132
|
+
await writeContainedOutputFile(artifactRoot, path.join("screenshots", fileName), Buffer.from(bytes));
|
|
2133
|
+
desktop.screenshot = {
|
|
2134
|
+
capturedAt: new Date().toISOString(),
|
|
2135
|
+
observerUrl: `../screenshots/${fileName}`,
|
|
2136
|
+
path: path.join("screenshots", fileName)
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
catch (error) {
|
|
2140
|
+
const laneLabel = options.redactRepoNames ? desktop.streamId : desktop.repo;
|
|
2141
|
+
warnings.push(`Screenshot capture failed for ${laneLabel}: ${compactError(error)}`);
|
|
2142
|
+
}
|
|
2143
|
+
}));
|
|
2144
|
+
const capturedCount = liveDesktops.filter((desktop) => desktop.screenshot).length;
|
|
2145
|
+
if (capturedCount > 0) {
|
|
2146
|
+
warnings.push(options.redactRepoNames
|
|
2147
|
+
? `Captured ${capturedCount}/${candidates.length} local-only redacted E2B desktop screenshot fallback${candidates.length === 1 ? "" : "s"}; do not publish private screenshots.`
|
|
2148
|
+
: `Captured ${capturedCount}/${candidates.length} E2B desktop screenshot fallback${candidates.length === 1 ? "" : "s"}.`);
|
|
2149
|
+
}
|
|
2150
|
+
return { warnings };
|
|
2151
|
+
}
|
|
2152
|
+
async function writeActorEvidenceArtifacts(artifactRoot, liveDesktops, options) {
|
|
2153
|
+
const candidates = liveDesktops.filter((desktop) => desktop.completion?.actorLastMessageTail
|
|
2154
|
+
|| desktop.completion?.actorLogTail
|
|
2155
|
+
|| desktop.completion?.appServerActorEvidence
|
|
2156
|
+
|| desktop.completion?.nestedObserverPresent !== undefined
|
|
2157
|
+
|| desktop.completion?.nestedStepTraceSummary !== undefined
|
|
2158
|
+
|| desktop.completion?.nestedVerifyPassed !== undefined
|
|
2159
|
+
|| desktop.completion?.setupQuality);
|
|
2160
|
+
if (candidates.length === 0) {
|
|
2161
|
+
return { warnings: [] };
|
|
2162
|
+
}
|
|
2163
|
+
let written = 0;
|
|
2164
|
+
for (const desktop of candidates) {
|
|
2165
|
+
const assignment = options.assignments.find((candidate) => candidate.streamId === desktop.streamId);
|
|
2166
|
+
const repoForRedaction = options.redactRepoNames ? assignment?.repo : undefined;
|
|
2167
|
+
const baseName = safeArtifactToken(desktop.streamId);
|
|
2168
|
+
const actorEvidence = {};
|
|
2169
|
+
if (desktop.completion?.actorLastMessageTail) {
|
|
2170
|
+
const relativePath = path.join("actor-evidence", `${baseName}-actor-last-message-tail.txt`);
|
|
2171
|
+
await writeContainedOutputFile(artifactRoot, relativePath, renderPublicSafeActorEvidenceText("actor-last-message", desktop.streamId, desktop.completion.actorLastMessageTail, {
|
|
2172
|
+
providerRuntimeId: options.redactRepoNames ? desktop.sandboxId : undefined,
|
|
2173
|
+
repo: repoForRedaction
|
|
2174
|
+
}), "utf8");
|
|
2175
|
+
actorEvidence.actorLastMessageTailPath = relativePath;
|
|
2176
|
+
written += 1;
|
|
2177
|
+
}
|
|
2178
|
+
if (desktop.completion?.actorLogTail) {
|
|
2179
|
+
const relativePath = path.join("actor-evidence", `${baseName}-actor-log-tail.txt`);
|
|
2180
|
+
await writeContainedOutputFile(artifactRoot, relativePath, renderPublicSafeActorEvidenceText("actor-log", desktop.streamId, desktop.completion.actorLogTail, {
|
|
2181
|
+
providerRuntimeId: options.redactRepoNames ? desktop.sandboxId : undefined,
|
|
2182
|
+
repo: repoForRedaction
|
|
2183
|
+
}), "utf8");
|
|
2184
|
+
actorEvidence.actorLogTailPath = relativePath;
|
|
2185
|
+
written += 1;
|
|
2186
|
+
}
|
|
2187
|
+
if (desktop.completion?.setupQuality) {
|
|
2188
|
+
const relativePath = path.join("setup-quality", `${baseName}-setup-quality.json`);
|
|
2189
|
+
const snapshot = options.redactRepoNames
|
|
2190
|
+
? redactSetupQualityRepoMentions(suppressSetupQualityPreviews(desktop.completion.setupQuality), repoForRedaction)
|
|
2191
|
+
: desktop.completion.setupQuality;
|
|
2192
|
+
await writeJson(artifactRoot, relativePath, snapshot);
|
|
2193
|
+
actorEvidence.setupQualityPath = relativePath;
|
|
2194
|
+
written += 1;
|
|
2195
|
+
}
|
|
2196
|
+
if (desktop.completion && (desktop.completion.nestedObserverPresent !== undefined
|
|
2197
|
+
|| desktop.completion.nestedStepTraceSummary !== undefined
|
|
2198
|
+
|| desktop.completion.nestedVerifyPassed !== undefined)) {
|
|
2199
|
+
const relativePath = path.join("nested-evidence", `${baseName}-nested-proof.json`);
|
|
2200
|
+
await writeJson(artifactRoot, relativePath, {
|
|
2201
|
+
schema: "humanish.oss-meta-nested-proof.v1",
|
|
2202
|
+
streamId: desktop.streamId,
|
|
2203
|
+
redaction: {
|
|
2204
|
+
status: "passed",
|
|
2205
|
+
notes: "Nested proof summary contains booleans and redacted local artifact pointers only; remote sandbox paths are intentionally omitted."
|
|
2206
|
+
},
|
|
2207
|
+
status: desktop.completion.status,
|
|
2208
|
+
reason: desktop.completion.reason,
|
|
2209
|
+
checkedAt: desktop.completion.checkedAt,
|
|
2210
|
+
nestedObserverPresent: desktop.completion.nestedObserverPresent === true,
|
|
2211
|
+
nestedVerifyPassed: desktop.completion.nestedVerifyPassed === true,
|
|
2212
|
+
...(desktop.completion.nestedStepTraceSummary ? { stepTraceSummary: desktop.completion.nestedStepTraceSummary } : {}),
|
|
2213
|
+
appStatus: desktop.completion.appStatus ?? "unknown",
|
|
2214
|
+
actorStatus: desktop.completion.actorStatus ?? "unknown"
|
|
2215
|
+
});
|
|
2216
|
+
actorEvidence.nestedEvidencePath = relativePath;
|
|
2217
|
+
written += 1;
|
|
2218
|
+
}
|
|
2219
|
+
if (desktop.completion?.appServerActorEvidence) {
|
|
2220
|
+
const evidence = desktop.completion.appServerActorEvidence;
|
|
2221
|
+
const tracePath = path.join("codex-app-server", `${baseName}-summary.json`);
|
|
2222
|
+
const eventsPath = path.join("codex-app-server", `${baseName}-events.ndjson`);
|
|
2223
|
+
const transcriptPath = path.join("codex-app-server", `${baseName}-transcript.txt`);
|
|
2224
|
+
await writeJson(artifactRoot, tracePath, isRecord(evidence.traceJson)
|
|
2225
|
+
? evidence.traceJson
|
|
2226
|
+
: {
|
|
2227
|
+
schema: "humanish.codex-app-server-trace.missing.v1",
|
|
2228
|
+
redaction: { status: "passed" },
|
|
2229
|
+
status: evidence.status ?? "unknown",
|
|
2230
|
+
reason: evidence.reason ?? "Remote app-server trace JSON was not captured.",
|
|
2231
|
+
traceText: evidence.traceText ?? ""
|
|
2232
|
+
});
|
|
2233
|
+
await writeContainedOutputFile(artifactRoot, eventsPath, evidence.eventsText ?? "No app-server event envelope tail captured.\n", "utf8");
|
|
2234
|
+
await writeContainedOutputFile(artifactRoot, transcriptPath, evidence.transcriptText ?? "No app-server transcript tail captured.\n", "utf8");
|
|
2235
|
+
evidence.tracePath = tracePath;
|
|
2236
|
+
evidence.eventsPath = eventsPath;
|
|
2237
|
+
evidence.transcriptPath = transcriptPath;
|
|
2238
|
+
actorEvidence.appServerTracePath = tracePath;
|
|
2239
|
+
actorEvidence.appServerEventsPath = eventsPath;
|
|
2240
|
+
actorEvidence.appServerTranscriptPath = transcriptPath;
|
|
2241
|
+
written += 3;
|
|
2242
|
+
}
|
|
2243
|
+
desktop.actorEvidence = actorEvidence;
|
|
2244
|
+
}
|
|
2245
|
+
return {
|
|
2246
|
+
warnings: [
|
|
2247
|
+
`Persisted ${written} public-safe local actor evidence artifact${written === 1 ? "" : "s"}.`
|
|
2248
|
+
]
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function suppressSetupQualityPreviews(snapshot) {
|
|
2252
|
+
return {
|
|
2253
|
+
...snapshot,
|
|
2254
|
+
summary: sanitizeSetupQualityText(snapshot.summary),
|
|
2255
|
+
checks: snapshot.checks.map((check) => ({
|
|
2256
|
+
...check,
|
|
2257
|
+
label: sanitizeSetupQualityText(check.label),
|
|
2258
|
+
detail: sanitizeSetupQualityText(check.detail)
|
|
2259
|
+
})),
|
|
2260
|
+
...(snapshot.studyQuality ? { studyQuality: sanitizeStudyQualitySnapshot(snapshot.studyQuality) } : {}),
|
|
2261
|
+
previews: [],
|
|
2262
|
+
redaction: {
|
|
2263
|
+
status: "passed",
|
|
2264
|
+
rawPreviews: "suppressed",
|
|
2265
|
+
notes: "Raw file previews are suppressed for token-backed/private OSS meta-lab runs."
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function renderPublicSafeActorEvidenceText(kind, streamId, text, redaction) {
|
|
2270
|
+
const sanitized = sanitizeRemoteLog(redactPrivateActorEvidence(text, redaction));
|
|
2271
|
+
return [
|
|
2272
|
+
`schema: humanish.oss-meta-actor-evidence.v1`,
|
|
2273
|
+
`kind: ${kind}`,
|
|
2274
|
+
`stream: ${streamId}`,
|
|
2275
|
+
`redaction: passed`,
|
|
2276
|
+
"",
|
|
2277
|
+
sanitized || "(no actor evidence captured)"
|
|
2278
|
+
].join("\n").trimEnd() + "\n";
|
|
2279
|
+
}
|
|
2280
|
+
function redactLiveDesktopRepoMentions(desktop, repo) {
|
|
2281
|
+
return {
|
|
2282
|
+
...desktop,
|
|
2283
|
+
...(desktop.bootstrap
|
|
2284
|
+
? {
|
|
2285
|
+
bootstrap: {
|
|
2286
|
+
...desktop.bootstrap,
|
|
2287
|
+
tail: redactPrivateRuntimeMentions(desktop.bootstrap.tail, { providerRuntimeId: desktop.sandboxId, repo })
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
: {}),
|
|
2291
|
+
...(desktop.completion
|
|
2292
|
+
? { completion: redactCompletionRepoMentions(desktop.completion, repo, desktop.sandboxId) }
|
|
2293
|
+
: {})
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function redactCompletionRepoMentions(completion, repo, providerRuntimeId) {
|
|
2297
|
+
const redaction = { providerRuntimeId, repo };
|
|
2298
|
+
return {
|
|
2299
|
+
...completion,
|
|
2300
|
+
...(completion.actorLogTail === undefined ? {} : { actorLogTail: redactPrivateActorEvidence(completion.actorLogTail, redaction) }),
|
|
2301
|
+
...(completion.actorLastMessageTail === undefined ? {} : { actorLastMessageTail: redactPrivateActorEvidence(completion.actorLastMessageTail, redaction) }),
|
|
2302
|
+
...(completion.appServerActorEvidence === undefined ? {} : { appServerActorEvidence: redactAppServerActorEvidence(completion.appServerActorEvidence, redaction) }),
|
|
2303
|
+
...(completion.appReason === undefined ? {} : { appReason: redactPrivateRuntimeMentions(completion.appReason, redaction) }),
|
|
2304
|
+
...(completion.logTail === undefined ? {} : { logTail: redactPrivateRuntimeMentions(completion.logTail, redaction) }),
|
|
2305
|
+
...(completion.nestedStepTraceSummary === undefined ? {} : { nestedStepTraceSummary: redactNestedStepTraceSummary(completion.nestedStepTraceSummary, redaction) }),
|
|
2306
|
+
reason: redactPrivateRuntimeMentions(completion.reason, redaction),
|
|
2307
|
+
...(completion.setupQuality === undefined ? {} : { setupQuality: redactSetupQualityRepoMentions(completion.setupQuality, repo) }),
|
|
2308
|
+
...(completion.visualReason === undefined ? {} : { visualReason: redactPrivateRuntimeMentions(completion.visualReason, redaction) })
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
function redactNestedStepTraceSummary(summary, redaction) {
|
|
2312
|
+
return normalizeNestedStepTraceSummary(redactJsonValue(summary, redaction)) ?? summary;
|
|
2313
|
+
}
|
|
2314
|
+
function redactAppServerActorEvidence(evidence, redaction) {
|
|
2315
|
+
return {
|
|
2316
|
+
...evidence,
|
|
2317
|
+
...(evidence.eventsText === undefined ? {} : { eventsText: redactPrivateActorEvidence(evidence.eventsText, redaction) }),
|
|
2318
|
+
...(evidence.reason === undefined ? {} : { reason: redactPrivateRuntimeMentions(evidence.reason, redaction) }),
|
|
2319
|
+
...(evidence.status === undefined ? {} : { status: redactPrivateRuntimeMentions(evidence.status, redaction) }),
|
|
2320
|
+
...(evidence.traceJson === undefined ? {} : { traceJson: redactJsonValue(evidence.traceJson, redaction) }),
|
|
2321
|
+
...(evidence.traceText === undefined ? {} : { traceText: redactPrivateActorEvidence(evidence.traceText, redaction) }),
|
|
2322
|
+
...(evidence.transcriptText === undefined ? {} : { transcriptText: redactPrivateActorEvidence(evidence.transcriptText, redaction) })
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function redactJsonValue(value, redaction) {
|
|
2326
|
+
try {
|
|
2327
|
+
return JSON.parse(redactPrivateActorEvidence(JSON.stringify(value), redaction));
|
|
2328
|
+
}
|
|
2329
|
+
catch {
|
|
2330
|
+
return value;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
function redactPrivateActorEvidence(text, redaction) {
|
|
2334
|
+
const redacted = redactPrivateRuntimeMentions(text, redaction);
|
|
2335
|
+
return redaction?.repo ? stripSourceDiffBlocks(redacted) : redacted;
|
|
2336
|
+
}
|
|
2337
|
+
function stripSourceDiffBlocks(text) {
|
|
2338
|
+
const lines = text.split(/\r?\n/);
|
|
2339
|
+
const output = [];
|
|
2340
|
+
let inDiff = false;
|
|
2341
|
+
let emittedMarker = false;
|
|
2342
|
+
for (const line of lines) {
|
|
2343
|
+
if (/^diff --git\b/.test(line)) {
|
|
2344
|
+
if (!emittedMarker) {
|
|
2345
|
+
output.push("[redacted-source-diff]");
|
|
2346
|
+
emittedMarker = true;
|
|
2347
|
+
}
|
|
2348
|
+
inDiff = true;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (inDiff) {
|
|
2352
|
+
if (/^(tokens used|Installed\b|Humanish\b|Created personas:|Created browser scenarios:|Product journeys covered:|Observed friction|Evidence paths:|Verification:|If you want\b)/.test(line)) {
|
|
2353
|
+
inDiff = false;
|
|
2354
|
+
}
|
|
2355
|
+
else {
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
output.push(line);
|
|
2360
|
+
}
|
|
2361
|
+
return output.join("\n");
|
|
2362
|
+
}
|
|
2363
|
+
function redactSetupQualityRepoMentions(snapshot, repo) {
|
|
2364
|
+
if (!repo) {
|
|
2365
|
+
return snapshot;
|
|
2366
|
+
}
|
|
2367
|
+
return JSON.parse(JSON.stringify(snapshot, (_key, value) => typeof value === "string" ? redactRepoMentions(value, repo) : value));
|
|
2368
|
+
}
|
|
2369
|
+
function redactRepoMentions(text, repo) {
|
|
2370
|
+
const [owner, name] = repo.split("/");
|
|
2371
|
+
if (!owner || !name) {
|
|
2372
|
+
return text;
|
|
2373
|
+
}
|
|
2374
|
+
const replacement = "[redacted-authorized-repo]";
|
|
2375
|
+
let redacted = text
|
|
2376
|
+
.replace(new RegExp(`https://github\\.com/${escapeRegExp(owner)}/${escapeRegExp(name)}(?:\\.git)?`, "gi"), replacement)
|
|
2377
|
+
.replace(new RegExp(`git@github\\.com:${escapeRegExp(owner)}/${escapeRegExp(name)}(?:\\.git)?`, "gi"), replacement)
|
|
2378
|
+
.replace(new RegExp(`\\b${escapeRegExp(owner)}/${escapeRegExp(name)}\\b`, "gi"), replacement);
|
|
2379
|
+
if (name.length >= 4 && !isCommonRepoBasename(name)) {
|
|
2380
|
+
redacted = redacted.replace(new RegExp(`\\b${escapeRegExp(name)}\\b`, "gi"), replacement);
|
|
2381
|
+
}
|
|
2382
|
+
return redacted;
|
|
2383
|
+
}
|
|
2384
|
+
function redactPrivateRuntimeMentions(text, redaction) {
|
|
2385
|
+
let redacted = text;
|
|
2386
|
+
if (redaction?.repo) {
|
|
2387
|
+
redacted = redactRepoMentions(redacted, redaction.repo);
|
|
2388
|
+
}
|
|
2389
|
+
if (redaction?.providerRuntimeId) {
|
|
2390
|
+
redacted = redacted.replace(new RegExp(escapeRegExp(redaction.providerRuntimeId), "g"), "[redacted-provider-runtime-id]");
|
|
2391
|
+
}
|
|
2392
|
+
return redacted;
|
|
2393
|
+
}
|
|
2394
|
+
function isCommonRepoBasename(value) {
|
|
2395
|
+
return new Set(["app", "web", "api", "cli", "repo", "site", "docs", "main", "next", "demo", "test"]).has(value.toLowerCase());
|
|
2396
|
+
}
|
|
2397
|
+
function escapeRegExp(value) {
|
|
2398
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2399
|
+
}
|
|
2400
|
+
function hasAppUrlProofBlocker(text) {
|
|
2401
|
+
const normalized = normalizeActorEvidenceForPattern(text);
|
|
2402
|
+
const blocker = "(?:unknown option|unsupported|not available|does\\s+\\W*not\\W*(?:support|expose|accept)|did\\s+\\W*not\\W*(?:support|expose|accept)|doesnt\\s+(?:support|expose|accept)|didnt\\s+(?:support|expose|accept))";
|
|
2403
|
+
const appUrl = "(?:--app-url|run\\s+--app-url|app-url\\s+proof)";
|
|
2404
|
+
return new RegExp(`${blocker}[\\s\\S]{0,220}${appUrl}|${appUrl}[\\s\\S]{0,220}${blocker}`, "i").test(normalized);
|
|
2405
|
+
}
|
|
2406
|
+
function normalizeActorEvidenceForPattern(text) {
|
|
2407
|
+
return text
|
|
2408
|
+
.replace(/[*_`~[\](){}<>]/g, " ")
|
|
2409
|
+
.replace(/\s+/g, " ")
|
|
2410
|
+
.trim();
|
|
2411
|
+
}
|
|
2412
|
+
async function arrangeLiveDesktopForScreenshot(desktop, terminalTitle, requestTimeoutMs) {
|
|
2413
|
+
const command = buildRemoteScreenshotArrangeCommand(terminalTitle);
|
|
2414
|
+
await desktop.commands.run(`bash -lc ${shellQuote(command)}`, {
|
|
2415
|
+
requestTimeoutMs,
|
|
2416
|
+
timeoutMs: 15_000
|
|
2417
|
+
}).catch(() => undefined);
|
|
2418
|
+
}
|
|
2419
|
+
function parseRemoteCompletion(payload) {
|
|
2420
|
+
try {
|
|
2421
|
+
const parsed = JSON.parse(payload);
|
|
2422
|
+
const status = normalizeCompletionStatus(parsed.status);
|
|
2423
|
+
if (!status) {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
const nestedVerifyPassed = parsed.nestedVerifyStatus === "passed"
|
|
2427
|
+
? true
|
|
2428
|
+
: parsed.nestedVerifyStatus === "failed" ? false : undefined;
|
|
2429
|
+
const appStatus = normalizeAppStatus(parsed.appStatus);
|
|
2430
|
+
const actorStatus = normalizeActorStatus(parsed.actorStatus);
|
|
2431
|
+
const visualStatus = normalizeVisualStatus(parsed.visualStatus);
|
|
2432
|
+
const appServerActorEvidence = normalizeAppServerActorEvidence(parsed.appServerActorEvidence);
|
|
2433
|
+
const nestedStepTraceSummary = normalizeNestedStepTraceSummary(parsed.nestedStepTraceSummary);
|
|
2434
|
+
return {
|
|
2435
|
+
...(typeof parsed.actorLogPath === "string" && parsed.actorLogPath.trim() ? { actorLogPath: sanitizeRemoteLog(parsed.actorLogPath) } : {}),
|
|
2436
|
+
...(typeof parsed.actorLogTail === "string" && parsed.actorLogTail.trim() ? { actorLogTail: sanitizeRemoteLog(parsed.actorLogTail) } : {}),
|
|
2437
|
+
...(typeof parsed.actorLastMessageTail === "string" && parsed.actorLastMessageTail.trim() ? { actorLastMessageTail: sanitizeRemoteLog(parsed.actorLastMessageTail) } : {}),
|
|
2438
|
+
...(typeof parsed.actorPid === "number" && Number.isFinite(parsed.actorPid) ? { actorPid: parsed.actorPid } : {}),
|
|
2439
|
+
...(actorStatus ? { actorStatus } : {}),
|
|
2440
|
+
...(appServerActorEvidence ? { appServerActorEvidence } : {}),
|
|
2441
|
+
...(typeof parsed.appLogPath === "string" && parsed.appLogPath.trim() ? { appLogPath: sanitizeRemoteLog(parsed.appLogPath) } : {}),
|
|
2442
|
+
...(typeof parsed.appPid === "number" && Number.isFinite(parsed.appPid) ? { appPid: parsed.appPid } : {}),
|
|
2443
|
+
...(typeof parsed.appReason === "string" && parsed.appReason.trim() ? { appReason: sanitizeRemoteLog(parsed.appReason).replace(/\s+/g, " ").slice(0, 240) } : {}),
|
|
2444
|
+
...(appStatus ? { appStatus } : {}),
|
|
2445
|
+
...(typeof parsed.appUrl === "string" && parsed.appUrl.trim() ? { appUrl: sanitizeRemoteLog(parsed.appUrl).replace(/\s+/g, " ").slice(0, 240) } : {}),
|
|
2446
|
+
checkedAt: typeof parsed.completedAt === "string" ? parsed.completedAt : new Date().toISOString(),
|
|
2447
|
+
...(typeof parsed.exitCode === "number" ? { exitCode: parsed.exitCode } : {}),
|
|
2448
|
+
...(typeof parsed.logTail === "string" ? { logTail: sanitizeRemoteLog(parsed.logTail) } : {}),
|
|
2449
|
+
...(typeof parsed.nestedObserverPresent === "boolean" ? { nestedObserverPresent: parsed.nestedObserverPresent } : {}),
|
|
2450
|
+
...(nestedStepTraceSummary ? { nestedStepTraceSummary } : {}),
|
|
2451
|
+
...(nestedVerifyPassed === undefined ? {} : { nestedVerifyPassed }),
|
|
2452
|
+
reason: typeof parsed.reason === "string" && parsed.reason.trim()
|
|
2453
|
+
? sanitizeRemoteLog(parsed.reason).replace(/\s+/g, " ").slice(0, 240)
|
|
2454
|
+
: defaultReasonForCompletion(status),
|
|
2455
|
+
...(isRunSetupQualitySnapshot(parsed.setupQuality) ? { setupQuality: sanitizeSetupQualitySnapshot(parsed.setupQuality) } : {}),
|
|
2456
|
+
status,
|
|
2457
|
+
...(typeof parsed.visualReason === "string" && parsed.visualReason.trim() ? { visualReason: sanitizeRemoteLog(parsed.visualReason).replace(/\s+/g, " ").slice(0, 240) } : {}),
|
|
2458
|
+
...(visualStatus ? { visualStatus } : {}),
|
|
2459
|
+
...(typeof parsed.visualWindowCount === "number" && Number.isFinite(parsed.visualWindowCount) ? { visualWindowCount: parsed.visualWindowCount } : {})
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
catch {
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function normalizeAppStatus(value) {
|
|
2467
|
+
return value === "not_started"
|
|
2468
|
+
|| value === "running"
|
|
2469
|
+
|| value === "blocked"
|
|
2470
|
+
|| value === "failed"
|
|
2471
|
+
|| value === "missing"
|
|
2472
|
+
|| value === "unknown"
|
|
2473
|
+
? value
|
|
2474
|
+
: null;
|
|
2475
|
+
}
|
|
2476
|
+
function normalizeVisualStatus(value) {
|
|
2477
|
+
return value === "not_started"
|
|
2478
|
+
|| value === "visible"
|
|
2479
|
+
|| value === "blocked"
|
|
2480
|
+
|| value === "unknown"
|
|
2481
|
+
? value
|
|
2482
|
+
: null;
|
|
2483
|
+
}
|
|
2484
|
+
function normalizeActorStatus(value) {
|
|
2485
|
+
return value === "not_started"
|
|
2486
|
+
|| value === "running"
|
|
2487
|
+
|| value === "passed"
|
|
2488
|
+
|| value === "failed"
|
|
2489
|
+
|| value === "blocked"
|
|
2490
|
+
|| value === "timed_out"
|
|
2491
|
+
|| value === "suspended"
|
|
2492
|
+
|| value === "unknown"
|
|
2493
|
+
? value
|
|
2494
|
+
: null;
|
|
2495
|
+
}
|
|
2496
|
+
function normalizeNestedStepTraceSummary(value) {
|
|
2497
|
+
if (!isRecord(value) || !Array.isArray(value.surfaces)) {
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
const surfaces = value.surfaces
|
|
2501
|
+
.slice(0, 8)
|
|
2502
|
+
.filter((surface) => isRecord(surface))
|
|
2503
|
+
.map((surface) => {
|
|
2504
|
+
const steps = Array.isArray(surface.steps)
|
|
2505
|
+
? surface.steps
|
|
2506
|
+
.slice(0, 20)
|
|
2507
|
+
.filter((step) => isRecord(step))
|
|
2508
|
+
.map((step) => {
|
|
2509
|
+
const status = normalizeNestedStepStatus(step.status);
|
|
2510
|
+
const assertionStatuses = Array.isArray(step.assertionStatuses)
|
|
2511
|
+
? step.assertionStatuses
|
|
2512
|
+
.map((statusValue) => publicSafeSummaryToken(statusValue, "assertion"))
|
|
2513
|
+
.filter(Boolean)
|
|
2514
|
+
.slice(0, 12)
|
|
2515
|
+
: undefined;
|
|
2516
|
+
return {
|
|
2517
|
+
action: publicSafeSummaryToken(step.action, "action"),
|
|
2518
|
+
...(assertionStatuses && assertionStatuses.length > 0 ? { assertionStatuses } : {}),
|
|
2519
|
+
id: publicSafeSummaryToken(step.id, "step"),
|
|
2520
|
+
...(typeof step.label === "string" && step.label.trim() ? { label: sanitizeNestedTraceText(step.label, 140) } : {}),
|
|
2521
|
+
reason: sanitizeNestedTraceText(step.reason, 240),
|
|
2522
|
+
status
|
|
2523
|
+
};
|
|
2524
|
+
})
|
|
2525
|
+
: [];
|
|
2526
|
+
return {
|
|
2527
|
+
id: publicSafeSummaryToken(surface.id, "surface"),
|
|
2528
|
+
...(typeof surface.label === "string" && surface.label.trim() ? { label: sanitizeNestedTraceText(surface.label, 140) } : {}),
|
|
2529
|
+
ok: surface.ok === true,
|
|
2530
|
+
reason: sanitizeNestedTraceText(surface.reason, 240),
|
|
2531
|
+
steps
|
|
2532
|
+
};
|
|
2533
|
+
})
|
|
2534
|
+
.filter((surface) => surface.steps.length > 0);
|
|
2535
|
+
if (surfaces.length === 0) {
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
const totalSteps = surfaces.reduce((total, surface) => total + surface.steps.length, 0);
|
|
2539
|
+
const passedSteps = surfaces.reduce((total, surface) => total + surface.steps.filter((step) => step.status === "passed").length, 0);
|
|
2540
|
+
const blockedSteps = surfaces.reduce((total, surface) => total + surface.steps.filter((step) => step.status === "blocked").length, 0);
|
|
2541
|
+
const scenario = isRecord(value.scenario)
|
|
2542
|
+
? {
|
|
2543
|
+
id: publicSafeSummaryToken(value.scenario.id, "scenario"),
|
|
2544
|
+
...(typeof value.scenario.source === "string" && isSafeRepoRelativePath(value.scenario.source) ? { source: sanitizeSetupQualityPath(value.scenario.source) } : {}),
|
|
2545
|
+
...(typeof value.scenario.sourceDigest === "string" && value.scenario.sourceDigest.trim() ? { sourceDigest: publicSafeSummaryToken(value.scenario.sourceDigest, "digest") } : {}),
|
|
2546
|
+
...(typeof value.scenario.stepCount === "number" && Number.isFinite(value.scenario.stepCount) ? { stepCount: Math.max(0, Math.round(value.scenario.stepCount)) } : {}),
|
|
2547
|
+
...(typeof value.scenario.title === "string" && value.scenario.title.trim() ? { title: sanitizeNestedTraceText(value.scenario.title, 140) } : {})
|
|
2548
|
+
}
|
|
2549
|
+
: undefined;
|
|
2550
|
+
return {
|
|
2551
|
+
schema: "humanish.oss-meta-nested-step-trace-summary.v1",
|
|
2552
|
+
redaction: {
|
|
2553
|
+
status: "passed",
|
|
2554
|
+
notes: "Nested browser trace summary stores counts and redacted step metadata only; URLs, auth streams, remote paths, screenshots, and raw DOM text are omitted."
|
|
2555
|
+
},
|
|
2556
|
+
counts: {
|
|
2557
|
+
blockedSteps,
|
|
2558
|
+
passedSteps,
|
|
2559
|
+
surfaces: surfaces.length,
|
|
2560
|
+
totalSteps,
|
|
2561
|
+
traces: surfaces.length
|
|
2562
|
+
},
|
|
2563
|
+
...(scenario ? { scenario } : {}),
|
|
2564
|
+
status: blockedSteps > 0 ? "blocked" : passedSteps === totalSteps ? "passed" : "unknown",
|
|
2565
|
+
surfaces
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function normalizeNestedStepStatus(value) {
|
|
2569
|
+
return value === "passed" || value === "blocked" ? value : "unknown";
|
|
2570
|
+
}
|
|
2571
|
+
function publicSafeSummaryToken(value, fallback) {
|
|
2572
|
+
const token = sanitizeSetupQualityText(value)
|
|
2573
|
+
.toLowerCase()
|
|
2574
|
+
.replace(/[^a-z0-9_.-]+/g, "-")
|
|
2575
|
+
.replace(/^-+|-+$/g, "")
|
|
2576
|
+
.slice(0, 80);
|
|
2577
|
+
return token || fallback;
|
|
2578
|
+
}
|
|
2579
|
+
function sanitizeNestedTraceText(value, maxLength) {
|
|
2580
|
+
return sanitizeSetupQualityText(value)
|
|
2581
|
+
.replace(/https?:\/\/\S+/gi, "[redacted-url]")
|
|
2582
|
+
.replace(/(?:^|\s)\/(?:home|tmp|var|Users)\/\S+/g, " [redacted-path]")
|
|
2583
|
+
.replace(/\s+/g, " ")
|
|
2584
|
+
.trim()
|
|
2585
|
+
.slice(0, maxLength);
|
|
2586
|
+
}
|
|
2587
|
+
function normalizeAppServerActorEvidence(value) {
|
|
2588
|
+
if (!isRecord(value)) {
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2591
|
+
const tracePath = safeRelativeArtifactPath(typeof value.tracePath === "string" ? value.tracePath : "");
|
|
2592
|
+
const eventsPath = safeRelativeArtifactPath(typeof value.eventsPath === "string" ? value.eventsPath : "");
|
|
2593
|
+
const transcriptPath = safeRelativeArtifactPath(typeof value.transcriptPath === "string" ? value.transcriptPath : "");
|
|
2594
|
+
if (!tracePath || !eventsPath || !transcriptPath) {
|
|
2595
|
+
return null;
|
|
2596
|
+
}
|
|
2597
|
+
return {
|
|
2598
|
+
eventsPath,
|
|
2599
|
+
...(typeof value.eventsText === "string" && value.eventsText.trim() ? { eventsText: sanitizeRemoteLog(value.eventsText) } : {}),
|
|
2600
|
+
...(typeof value.reason === "string" && value.reason.trim() ? { reason: sanitizeRemoteLog(value.reason).replace(/\s+/g, " ").slice(0, 240) } : {}),
|
|
2601
|
+
...(typeof value.status === "string" && value.status.trim() ? { status: sanitizeRemoteLog(value.status).replace(/\s+/g, " ").slice(0, 80) } : {}),
|
|
2602
|
+
...(isRecord(value.traceJson) ? { traceJson: value.traceJson } : {}),
|
|
2603
|
+
tracePath,
|
|
2604
|
+
...(typeof value.traceText === "string" && value.traceText.trim() ? { traceText: sanitizeRemoteLog(value.traceText) } : {}),
|
|
2605
|
+
transcriptPath,
|
|
2606
|
+
...(typeof value.transcriptText === "string" && value.transcriptText.trim() ? { transcriptText: sanitizeRemoteLog(value.transcriptText) } : {})
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
function safeRelativeArtifactPath(value) {
|
|
2610
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
2611
|
+
if (!normalized || normalized.startsWith("/") || normalized.startsWith("../") || normalized.includes("://") || normalized.split("/").includes("..")) {
|
|
2612
|
+
return "";
|
|
2613
|
+
}
|
|
2614
|
+
return normalized;
|
|
2615
|
+
}
|
|
2616
|
+
function normalizeCompletionStatus(value) {
|
|
2617
|
+
return value === "running"
|
|
2618
|
+
|| value === "passed"
|
|
2619
|
+
|| value === "failed"
|
|
2620
|
+
|| value === "blocked"
|
|
2621
|
+
|| value === "timed_out"
|
|
2622
|
+
? value
|
|
2623
|
+
: null;
|
|
2624
|
+
}
|
|
2625
|
+
function defaultReasonForCompletion(status) {
|
|
2626
|
+
switch (status) {
|
|
2627
|
+
case "passed":
|
|
2628
|
+
return "Remote bootstrap completed successfully.";
|
|
2629
|
+
case "failed":
|
|
2630
|
+
return "Remote bootstrap failed.";
|
|
2631
|
+
case "blocked":
|
|
2632
|
+
return "Remote bootstrap is blocked.";
|
|
2633
|
+
case "timed_out":
|
|
2634
|
+
return "Remote bootstrap completion timed out.";
|
|
2635
|
+
case "running":
|
|
2636
|
+
return "Remote bootstrap is still running.";
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function isRunSetupQualitySnapshot(value) {
|
|
2640
|
+
if (!isRecord(value) || value.schema !== "humanish.setup-quality.v1") {
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
return Array.isArray(value.checks)
|
|
2644
|
+
&& Array.isArray(value.tree)
|
|
2645
|
+
&& Array.isArray(value.previews)
|
|
2646
|
+
&& isRecord(value.humanish)
|
|
2647
|
+
&& isRecord(value.packageScripts)
|
|
2648
|
+
&& typeof value.generatedAt === "string"
|
|
2649
|
+
&& typeof value.summary === "string"
|
|
2650
|
+
&& (value.status === "passed" || value.status === "needs_review" || value.status === "blocked");
|
|
2651
|
+
}
|
|
2652
|
+
function sanitizeSetupQualitySnapshot(snapshot) {
|
|
2653
|
+
const safeTree = snapshot.tree
|
|
2654
|
+
.filter((entry) => isSafeRepoRelativePath(entry.path) && (entry.type === "file" || entry.type === "directory"))
|
|
2655
|
+
.slice(0, 240)
|
|
2656
|
+
.map((entry) => ({
|
|
2657
|
+
path: sanitizeSetupQualityPath(entry.path),
|
|
2658
|
+
type: entry.type,
|
|
2659
|
+
...(typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes) ? { sizeBytes: Math.max(0, Math.round(entry.sizeBytes)) } : {})
|
|
2660
|
+
}));
|
|
2661
|
+
const safePreviews = snapshot.previews
|
|
2662
|
+
.filter((preview) => isSafeRepoRelativePath(preview.path))
|
|
2663
|
+
.slice(0, 20)
|
|
2664
|
+
.map((preview) => ({
|
|
2665
|
+
path: sanitizeSetupQualityPath(preview.path),
|
|
2666
|
+
language: sanitizePreviewLanguage(preview.language),
|
|
2667
|
+
truncated: preview.truncated === true,
|
|
2668
|
+
text: sanitizeSetupQualityText(preview.text).slice(0, 8_000)
|
|
2669
|
+
}));
|
|
2670
|
+
const safeScripts = {};
|
|
2671
|
+
for (const [key, value] of Object.entries(snapshot.packageScripts)) {
|
|
2672
|
+
const safeKey = sanitizeSetupQualityText(key).replace(/[^a-zA-Z0-9:_-]/g, "").slice(0, 80);
|
|
2673
|
+
if (!safeKey || typeof value !== "string") {
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
safeScripts[safeKey] = sanitizeSetupQualityText(value).slice(0, 300);
|
|
2677
|
+
}
|
|
2678
|
+
return {
|
|
2679
|
+
schema: "humanish.setup-quality.v1",
|
|
2680
|
+
generatedAt: sanitizeSetupQualityText(snapshot.generatedAt).slice(0, 80) || new Date().toISOString(),
|
|
2681
|
+
redaction: {
|
|
2682
|
+
status: "passed",
|
|
2683
|
+
rawPreviews: snapshot.redaction?.rawPreviews === "suppressed" ? "suppressed" : "included",
|
|
2684
|
+
notes: sanitizeSetupQualityText(snapshot.redaction?.notes ?? "Remote setup snapshot was redacted before persistence.").slice(0, 240)
|
|
2685
|
+
},
|
|
2686
|
+
summary: sanitizeSetupQualityText(snapshot.summary).slice(0, 320),
|
|
2687
|
+
status: snapshot.status,
|
|
2688
|
+
checks: snapshot.checks
|
|
2689
|
+
.filter((check) => isRecord(check) && typeof check.id === "string" && typeof check.label === "string" && typeof check.detail === "string")
|
|
2690
|
+
.slice(0, 40)
|
|
2691
|
+
.map((check) => ({
|
|
2692
|
+
id: sanitizeSetupQualityText(check.id).replace(/[^a-zA-Z0-9:_-]/g, "").slice(0, 80) || "check",
|
|
2693
|
+
label: sanitizeSetupQualityText(check.label).slice(0, 140),
|
|
2694
|
+
ok: check.ok === true,
|
|
2695
|
+
detail: sanitizeSetupQualityText(check.detail).slice(0, 320)
|
|
2696
|
+
})),
|
|
2697
|
+
tree: safeTree,
|
|
2698
|
+
previews: safePreviews,
|
|
2699
|
+
...(snapshot.studyQuality ? { studyQuality: sanitizeStudyQualitySnapshot(snapshot.studyQuality) } : {}),
|
|
2700
|
+
packageScripts: safeScripts,
|
|
2701
|
+
humanish: {
|
|
2702
|
+
configPresent: snapshot.humanish.configPresent === true,
|
|
2703
|
+
personaCount: typeof snapshot.humanish.personaCount === "number" && Number.isFinite(snapshot.humanish.personaCount) ? Math.max(0, Math.round(snapshot.humanish.personaCount)) : 0,
|
|
2704
|
+
scenarioCount: typeof snapshot.humanish.scenarioCount === "number" && Number.isFinite(snapshot.humanish.scenarioCount) ? Math.max(0, Math.round(snapshot.humanish.scenarioCount)) : 0,
|
|
2705
|
+
packageScriptPresent: snapshot.humanish.packageScriptPresent === true,
|
|
2706
|
+
gitignoreContainsRuntimeIgnore: snapshot.humanish.gitignoreContainsRuntimeIgnore === true
|
|
2707
|
+
}
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
function sanitizeStudyQualitySnapshot(studyQuality) {
|
|
2711
|
+
const rating = studyQuality.rating === "none"
|
|
2712
|
+
|| studyQuality.rating === "ceremonial"
|
|
2713
|
+
|| studyQuality.rating === "useful"
|
|
2714
|
+
|| studyQuality.rating === "high_leverage"
|
|
2715
|
+
? studyQuality.rating
|
|
2716
|
+
: "none";
|
|
2717
|
+
return {
|
|
2718
|
+
schema: "humanish.study-quality.v1",
|
|
2719
|
+
rating,
|
|
2720
|
+
summary: sanitizeSetupQualityText(studyQuality.summary).slice(0, 320),
|
|
2721
|
+
checks: Array.isArray(studyQuality.checks)
|
|
2722
|
+
? studyQuality.checks
|
|
2723
|
+
.filter((check) => isRecord(check) && typeof check.id === "string" && typeof check.label === "string" && typeof check.detail === "string")
|
|
2724
|
+
.slice(0, 20)
|
|
2725
|
+
.map((check) => ({
|
|
2726
|
+
id: sanitizeSetupQualityText(check.id).replace(/[^a-zA-Z0-9:_-]/g, "").slice(0, 80) || "study-check",
|
|
2727
|
+
label: sanitizeSetupQualityText(check.label).slice(0, 140),
|
|
2728
|
+
ok: check.ok === true,
|
|
2729
|
+
detail: sanitizeSetupQualityText(check.detail).slice(0, 320)
|
|
2730
|
+
}))
|
|
2731
|
+
: [],
|
|
2732
|
+
signals: {
|
|
2733
|
+
appUrlProofBlocked: studyQuality.signals?.appUrlProofBlocked === true,
|
|
2734
|
+
appUrlProofMentioned: studyQuality.signals?.appUrlProofMentioned === true,
|
|
2735
|
+
actorInsightCaptured: studyQuality.signals?.actorInsightCaptured === true,
|
|
2736
|
+
coverageCustomized: studyQuality.signals?.coverageCustomized === true,
|
|
2737
|
+
personaCustomized: studyQuality.signals?.personaCustomized === true,
|
|
2738
|
+
scenarioCustomized: studyQuality.signals?.scenarioCustomized === true
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function sanitizePreviewLanguage(value) {
|
|
2743
|
+
return value === "json" || value === "yaml" || value === "typescript" || value === "markdown" || value === "text"
|
|
2744
|
+
? value
|
|
2745
|
+
: "text";
|
|
2746
|
+
}
|
|
2747
|
+
function sanitizeSetupQualityPath(value) {
|
|
2748
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").slice(0, 240);
|
|
2749
|
+
}
|
|
2750
|
+
function sanitizeSetupQualityText(value) {
|
|
2751
|
+
return redactOssRemoteTelemetryText(String(value ?? ""))
|
|
2752
|
+
.replace(/\0/g, "")
|
|
2753
|
+
.replace(/https?:\/\/[^/\s]*e2b[^)\s]+/gi, "[redacted-e2b-url]")
|
|
2754
|
+
.trim();
|
|
2755
|
+
}
|
|
2756
|
+
function isSafeRepoRelativePath(value) {
|
|
2757
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2758
|
+
return false;
|
|
2759
|
+
}
|
|
2760
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
2761
|
+
return !path.isAbsolute(normalized)
|
|
2762
|
+
&& !normalized.includes("://")
|
|
2763
|
+
&& !normalized.startsWith("../")
|
|
2764
|
+
&& !normalized.split("/").includes("..")
|
|
2765
|
+
&& !normalized.split("/").some((segment) => /^(?:\.env(?:\..*)?|\.npmrc|\.git|node_modules|dist|build|\.next)$/.test(segment));
|
|
2766
|
+
}
|
|
2767
|
+
function isLocalEvidenceArtifactPath(value) {
|
|
2768
|
+
return isSafeRepoRelativePath(value);
|
|
2769
|
+
}
|
|
2770
|
+
function isRecord(value) {
|
|
2771
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2772
|
+
}
|
|
2773
|
+
function sanitizeRemoteLog(value) {
|
|
2774
|
+
return redactOssRemoteTelemetryText(value)
|
|
2775
|
+
.split(/\r?\n/)
|
|
2776
|
+
.slice(-80)
|
|
2777
|
+
.join("\n")
|
|
2778
|
+
.trim();
|
|
2779
|
+
}
|
|
2780
|
+
async function startOssBootstrap(desktop, assignment, localPackage, requestTimeoutMs, display = {
|
|
2781
|
+
repoLabel: assignment.repo,
|
|
2782
|
+
token: repoSlug(assignment.repo)
|
|
2783
|
+
}) {
|
|
2784
|
+
const token = display.token;
|
|
2785
|
+
const appDir = `/home/user/${token}`;
|
|
2786
|
+
const stateDir = `/home/user/.humanish-oss-lab/${token}`;
|
|
2787
|
+
const remotePackagePath = `/tmp/${localPackage?.fileName ?? "humanish.tgz"}`;
|
|
2788
|
+
const remoteHostActorPlanPath = `${stateDir}/host-actor-plan.json`;
|
|
2789
|
+
const bootstrapPath = `${stateDir}/bootstrap.sh`;
|
|
2790
|
+
const launcherPath = `${stateDir}/launch-terminal.sh`;
|
|
2791
|
+
const logPath = `${stateDir}/bootstrap.log`;
|
|
2792
|
+
const completionPath = `${stateDir}/completion.json`;
|
|
2793
|
+
const nestedObserverPath = `${appDir}/.humanish/runs/nested-${token}/observer/index.html`;
|
|
2794
|
+
const title = `Humanish ${assignment.index} ${display.repoLabel}`;
|
|
2795
|
+
const codexMode = codexAppServerModeRequested(process.env, display.codexAppServerMode === true) ? "app-server-client" : "tui-attempted";
|
|
2796
|
+
const baseTail = [
|
|
2797
|
+
`repo: ${display.repoLabel}`,
|
|
2798
|
+
`project: ${appDir}`,
|
|
2799
|
+
`sandbox: ${desktop.sandboxId}`,
|
|
2800
|
+
`remote package: ${localPackage ? remotePackagePath : "npm:humanish fallback"}`,
|
|
2801
|
+
`bootstrap: ${bootstrapPath}`,
|
|
2802
|
+
`completion: ${completionPath}`,
|
|
2803
|
+
`log: ${logPath}`,
|
|
2804
|
+
`nested observer: ${nestedObserverPath}`
|
|
2805
|
+
].join("\n");
|
|
2806
|
+
try {
|
|
2807
|
+
await runDesktopCommand(desktop, `mkdir -p ${shellQuote(stateDir)}`, {
|
|
2808
|
+
requestTimeoutMs,
|
|
2809
|
+
timeoutMs: 30_000
|
|
2810
|
+
});
|
|
2811
|
+
if (localPackage) {
|
|
2812
|
+
const packageBytes = await readFile(localPackage.path);
|
|
2813
|
+
await desktop.files.write(remotePackagePath, toArrayBuffer(packageBytes), {
|
|
2814
|
+
requestTimeoutMs,
|
|
2815
|
+
useOctetStream: true
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
if (display.hostActorPlanResult?.plan) {
|
|
2819
|
+
await desktop.files.write(remoteHostActorPlanPath, `${JSON.stringify(display.hostActorPlanResult.plan, null, 2)}\n`, {
|
|
2820
|
+
requestTimeoutMs
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
const bootstrapScript = buildRemoteBootstrapScript({
|
|
2824
|
+
assignment,
|
|
2825
|
+
appDir,
|
|
2826
|
+
completionPath,
|
|
2827
|
+
displayRepo: display.repoLabel,
|
|
2828
|
+
logPath,
|
|
2829
|
+
nestedObserverPath,
|
|
2830
|
+
...(display.hostActorPlanResult?.plan ? { remoteHostActorPlanPath } : {}),
|
|
2831
|
+
stateDir,
|
|
2832
|
+
token,
|
|
2833
|
+
...(localPackage ? { remotePackagePath } : {})
|
|
2834
|
+
});
|
|
2835
|
+
const launcherScript = buildRemoteLauncherScript({
|
|
2836
|
+
bootstrapPath,
|
|
2837
|
+
launcherPath,
|
|
2838
|
+
logPath,
|
|
2839
|
+
title
|
|
2840
|
+
});
|
|
2841
|
+
await desktop.files.write(bootstrapPath, bootstrapScript, { requestTimeoutMs });
|
|
2842
|
+
await desktop.files.write(launcherPath, launcherScript, { requestTimeoutMs });
|
|
2843
|
+
await runDesktopCommand(desktop, `chmod +x ${shellQuote(bootstrapPath)} ${shellQuote(launcherPath)}`, {
|
|
2844
|
+
requestTimeoutMs,
|
|
2845
|
+
timeoutMs: 30_000
|
|
2846
|
+
});
|
|
2847
|
+
await runDesktopCommand(desktop, `bash ${shellQuote(launcherPath)}`, {
|
|
2848
|
+
requestTimeoutMs,
|
|
2849
|
+
timeoutMs: 30_000
|
|
2850
|
+
});
|
|
2851
|
+
await desktop.wait(1200).catch(() => undefined);
|
|
2852
|
+
await runDesktopCommand(desktop, buildRemoteFocusCommand(title), {
|
|
2853
|
+
requestTimeoutMs,
|
|
2854
|
+
timeoutMs: 10_000
|
|
2855
|
+
}).catch(() => undefined);
|
|
2856
|
+
return {
|
|
2857
|
+
codexMode,
|
|
2858
|
+
completionPath,
|
|
2859
|
+
launcherPath,
|
|
2860
|
+
logPath,
|
|
2861
|
+
humanishPackageUploaded: Boolean(localPackage),
|
|
2862
|
+
nestedObserverPath,
|
|
2863
|
+
status: "started",
|
|
2864
|
+
tail: [
|
|
2865
|
+
"Visible E2B bootstrap terminal launched.",
|
|
2866
|
+
codexMode === "app-server-client"
|
|
2867
|
+
? "The terminal clones the authorized repo, installs this local humanish package tarball when available, runs nested Humanish proof commands, opens the nested Observer, and opens the Codex app-server client surface in Chrome when configured."
|
|
2868
|
+
: "The terminal clones the authorized repo, installs this local humanish package tarball when available, runs nested Humanish proof commands, attempts Codex TUI, then opens the nested Observer in Chrome.",
|
|
2869
|
+
baseTail
|
|
2870
|
+
].join("\n"),
|
|
2871
|
+
terminalTitle: title
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
catch (error) {
|
|
2875
|
+
return {
|
|
2876
|
+
codexMode,
|
|
2877
|
+
completionPath,
|
|
2878
|
+
launcherPath,
|
|
2879
|
+
logPath,
|
|
2880
|
+
humanishPackageUploaded: Boolean(localPackage),
|
|
2881
|
+
nestedObserverPath,
|
|
2882
|
+
status: "failed",
|
|
2883
|
+
tail: [
|
|
2884
|
+
"Bootstrap launcher failed before the remote terminal could start.",
|
|
2885
|
+
baseTail,
|
|
2886
|
+
`error: ${compactError(error)}`
|
|
2887
|
+
].join("\n"),
|
|
2888
|
+
terminalTitle: title
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function buildRemoteBootstrapScript(args) {
|
|
2893
|
+
const repoUrl = `https://github.com/${args.assignment.repo}.git`;
|
|
2894
|
+
const runId = `nested-${args.token}`;
|
|
2895
|
+
return `#!/usr/bin/env bash
|
|
2896
|
+
set -Eeuo pipefail
|
|
2897
|
+
export TERM=xterm-256color
|
|
2898
|
+
export HUMANISH_PUBLIC_SAFE=1
|
|
2899
|
+
HUMANISH_PRIVATE_CODEX_API_KEY="\${HUMANISH_CODEX_API_KEY:-}"
|
|
2900
|
+
HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN="\${HUMANISH_CODEX_ACCESS_TOKEN:-}"
|
|
2901
|
+
HUMANISH_PRIVATE_CODEX_APP_SERVER_URL="\${HUMANISH_CODEX_APP_SERVER_URL:-}"
|
|
2902
|
+
HUMANISH_PRIVATE_GITHUB_TOKEN="\${HUMANISH_GITHUB_TOKEN:-}"
|
|
2903
|
+
unset HUMANISH_CODEX_API_KEY HUMANISH_CODEX_ACCESS_TOKEN HUMANISH_CODEX_APP_SERVER_URL HUMANISH_GITHUB_TOKEN
|
|
2904
|
+
unset OPENAI_API_KEY CODEX_API_KEY CODEX_ACCESS_TOKEN E2B_API_KEY GH_TOKEN GITHUB_TOKEN
|
|
2905
|
+
STATE_DIR=${shellQuote(args.stateDir)}
|
|
2906
|
+
ROOT_DIR="$STATE_DIR"
|
|
2907
|
+
APP_DIR=${shellQuote(args.appDir)}
|
|
2908
|
+
LOG_PATH=${shellQuote(args.logPath)}
|
|
2909
|
+
COMPLETION_PATH=${shellQuote(args.completionPath)}
|
|
2910
|
+
NESTED_OBSERVER=${shellQuote(args.nestedObserverPath)}
|
|
2911
|
+
REMOTE_PACKAGE=${args.remotePackagePath ? shellQuote(args.remotePackagePath) : "''"}
|
|
2912
|
+
HOST_ACTOR_PLAN=${args.remoteHostActorPlanPath ? shellQuote(args.remoteHostActorPlanPath) : "''"}
|
|
2913
|
+
APP_LOG_PATH="$ROOT_DIR/app.log"
|
|
2914
|
+
ACTOR_LOG_PATH="$ROOT_DIR/actor.log"
|
|
2915
|
+
ACTOR_LAST_MESSAGE_PATH="$ROOT_DIR/actor-last-message.txt"
|
|
2916
|
+
CODEX_APP_SERVER_ROOT_PATH="$ROOT_DIR/codex-app-server"
|
|
2917
|
+
CODEX_APP_SERVER_STATE_PATH="$CODEX_APP_SERVER_ROOT_PATH/state.json"
|
|
2918
|
+
CODEX_APP_SERVER_LOG_PATH="$CODEX_APP_SERVER_ROOT_PATH/ui.log"
|
|
2919
|
+
CODEX_APP_SERVER_PROMPT_PATH="$ROOT_DIR/codex-app-server-prompt.txt"
|
|
2920
|
+
CODEX_APP_SERVER_PORT="\${HUMANISH_OSS_META_CODEX_APP_SERVER_PORT:-45137}"
|
|
2921
|
+
CODEX_APP_SERVER_UI_URL=""
|
|
2922
|
+
STREAM_ID=${shellQuote(args.assignment.streamId)}
|
|
2923
|
+
TERMINAL_TITLE=${shellQuote(`Humanish ${args.assignment.index} ${args.displayRepo}`)}
|
|
2924
|
+
mkdir -p "$ROOT_DIR"
|
|
2925
|
+
touch "$LOG_PATH"
|
|
2926
|
+
exec > >(tee -a "$LOG_PATH") 2>&1
|
|
2927
|
+
NESTED_VERIFY_STATUS=not_run
|
|
2928
|
+
APP_STATUS=not_started
|
|
2929
|
+
APP_REASON="Target app startup has not started."
|
|
2930
|
+
APP_URL=""
|
|
2931
|
+
APP_PID=""
|
|
2932
|
+
ACTOR_STATUS=not_started
|
|
2933
|
+
ACTOR_PID=""
|
|
2934
|
+
VISUAL_STATUS=not_started
|
|
2935
|
+
VISUAL_REASON="Browser windows have not been arranged."
|
|
2936
|
+
VISUAL_WINDOW_COUNT=0
|
|
2937
|
+
CODEX_APP_SERVER_CLIENT_OPENED=0
|
|
2938
|
+
|
|
2939
|
+
write_completion() {
|
|
2940
|
+
local status="$1"
|
|
2941
|
+
local reason="$2"
|
|
2942
|
+
local exit_code="$3"
|
|
2943
|
+
local nested_observer_present=false
|
|
2944
|
+
if [[ -f "$NESTED_OBSERVER" ]]; then
|
|
2945
|
+
nested_observer_present=true
|
|
2946
|
+
fi
|
|
2947
|
+
|
|
2948
|
+
node - "$COMPLETION_PATH" "$LOG_PATH" "$APP_DIR" "$status" "$reason" "$exit_code" "$nested_observer_present" "$NESTED_OBSERVER" "$NESTED_VERIFY_STATUS" "$APP_STATUS" "$APP_REASON" "$APP_URL" "$APP_PID" "$APP_LOG_PATH" "$ACTOR_STATUS" "$ACTOR_PID" "$ACTOR_LOG_PATH" "$ACTOR_LAST_MESSAGE_PATH" "$VISUAL_STATUS" "$VISUAL_REASON" "$VISUAL_WINDOW_COUNT" "$STREAM_ID" "$CODEX_APP_SERVER_STATE_PATH" "$CODEX_APP_SERVER_ROOT_PATH" <<'NODE' || true
|
|
2949
|
+
const fs = require("node:fs");
|
|
2950
|
+
const path = require("node:path");
|
|
2951
|
+
const [
|
|
2952
|
+
completionPath,
|
|
2953
|
+
logPath,
|
|
2954
|
+
appDir,
|
|
2955
|
+
status,
|
|
2956
|
+
reason,
|
|
2957
|
+
exitCode,
|
|
2958
|
+
nestedObserverPresent,
|
|
2959
|
+
nestedObserverPath,
|
|
2960
|
+
nestedVerifyStatus,
|
|
2961
|
+
appStatus,
|
|
2962
|
+
appReason,
|
|
2963
|
+
appUrl,
|
|
2964
|
+
appPid,
|
|
2965
|
+
appLogPath,
|
|
2966
|
+
actorStatus,
|
|
2967
|
+
actorPid,
|
|
2968
|
+
actorLogPath,
|
|
2969
|
+
actorLastMessagePath,
|
|
2970
|
+
visualStatus,
|
|
2971
|
+
visualReason,
|
|
2972
|
+
visualWindowCount,
|
|
2973
|
+
streamId,
|
|
2974
|
+
codexAppServerStatePath,
|
|
2975
|
+
codexAppServerRootPath
|
|
2976
|
+
] = process.argv.slice(2);
|
|
2977
|
+
const tailFile = (filePath, lines = 80) => fs.existsSync(filePath)
|
|
2978
|
+
? fs.readFileSync(filePath, "utf8").split(/\\r?\\n/).slice(-lines).join("\\n")
|
|
2979
|
+
: "";
|
|
2980
|
+
const redactText = (value) => String(value || "")
|
|
2981
|
+
.replace(/sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g, "[redacted-openai-key]")
|
|
2982
|
+
.replace(/e2b_[A-Za-z0-9_-]{12,}/g, "[redacted-e2b-key]")
|
|
2983
|
+
.replace(/(?:gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})/g, "[redacted-github-token]")
|
|
2984
|
+
.replace(/\\bBearer\\s+[A-Za-z0-9._~+/=-]{12,}\\b/gi, "Bearer [redacted-token]")
|
|
2985
|
+
.replace(/https?:\\/\\/[^/\\s]*e2b[^)\\s]+/gi, "[redacted-e2b-url]");
|
|
2986
|
+
const redactedTail = redactText(tailFile(logPath, 80));
|
|
2987
|
+
const actorLogTail = redactText(tailFile(actorLogPath, 80));
|
|
2988
|
+
const actorLastMessageTail = redactText(tailFile(actorLastMessagePath, 40));
|
|
2989
|
+
const numberOrNull = (value) => /^\\d+$/.test(String(value || "")) ? Number(value) : null;
|
|
2990
|
+
const cleanText = (value) => redactText(value)
|
|
2991
|
+
.replace(/\\s+/g, " ")
|
|
2992
|
+
.trim()
|
|
2993
|
+
.slice(0, 240);
|
|
2994
|
+
const safeRel = (value) => {
|
|
2995
|
+
const normalized = String(value || "").replace(/\\\\/g, "/").replace(/^\\.\\/+/, "");
|
|
2996
|
+
if (!normalized || normalized.startsWith("../") || normalized.includes("://") || normalized.split("/").includes("..")) return "";
|
|
2997
|
+
return normalized;
|
|
2998
|
+
};
|
|
2999
|
+
const languageFor = (rel) => {
|
|
3000
|
+
if (rel.endsWith(".json")) return "json";
|
|
3001
|
+
if (rel.endsWith(".yaml") || rel.endsWith(".yml")) return "yaml";
|
|
3002
|
+
if (rel.endsWith(".ts") || rel.endsWith(".tsx")) return "typescript";
|
|
3003
|
+
if (rel.endsWith(".md") || rel.endsWith(".markdown")) return "markdown";
|
|
3004
|
+
return "text";
|
|
3005
|
+
};
|
|
3006
|
+
const shouldPreview = (rel) => rel === "package.json"
|
|
3007
|
+
|| rel === ".gitignore"
|
|
3008
|
+
|| rel === "humanish/config.ts"
|
|
3009
|
+
|| rel === "humanish/coverage-map.md"
|
|
3010
|
+
|| rel === "humanish/coverage-matrix.md"
|
|
3011
|
+
|| rel.startsWith("humanish/personas/")
|
|
3012
|
+
|| rel.startsWith("humanish/scenarios/");
|
|
3013
|
+
const ignoredSegments = new Set([".git", "node_modules", ".humanish", "dist", "build", ".next", "coverage", ".turbo", ".cache"]);
|
|
3014
|
+
const readTextLimited = (filePath, maxBytes = 12000) => {
|
|
3015
|
+
try {
|
|
3016
|
+
const buffer = fs.readFileSync(filePath);
|
|
3017
|
+
return redactText(buffer.slice(0, maxBytes).toString("utf8")).replace(/\\0/g, "");
|
|
3018
|
+
} catch {
|
|
3019
|
+
return "";
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
const countFilesUnder = (root, prefix) => {
|
|
3023
|
+
try {
|
|
3024
|
+
return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).filter((entry) => entry.isFile()).length;
|
|
3025
|
+
} catch {
|
|
3026
|
+
return 0;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
const buildSetupQuality = (root) => {
|
|
3030
|
+
const tree = [];
|
|
3031
|
+
const previews = [];
|
|
3032
|
+
const walk = (dir, depth) => {
|
|
3033
|
+
if (depth > 4 || tree.length >= 240) return;
|
|
3034
|
+
let entries = [];
|
|
3035
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
3036
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3037
|
+
for (const entry of entries) {
|
|
3038
|
+
if (tree.length >= 240) break;
|
|
3039
|
+
if (ignoredSegments.has(entry.name)) continue;
|
|
3040
|
+
const absolute = path.join(dir, entry.name);
|
|
3041
|
+
const rel = safeRel(path.relative(root, absolute));
|
|
3042
|
+
if (!rel) continue;
|
|
3043
|
+
if (entry.isDirectory()) {
|
|
3044
|
+
tree.push({ path: rel, type: "directory" });
|
|
3045
|
+
walk(absolute, depth + 1);
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
if (!entry.isFile()) continue;
|
|
3049
|
+
const stats = fs.statSync(absolute);
|
|
3050
|
+
tree.push({ path: rel, type: "file", sizeBytes: stats.size });
|
|
3051
|
+
if (shouldPreview(rel) && previews.length < 20) {
|
|
3052
|
+
const text = readTextLimited(absolute);
|
|
3053
|
+
previews.push({ path: rel, language: languageFor(rel), truncated: stats.size > 12000 || text.length >= 8000, text: text.slice(0, 8000) });
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
3058
|
+
let packageScripts = {};
|
|
3059
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
3060
|
+
try {
|
|
3061
|
+
const parsedPackage = JSON.parse(readTextLimited(packageJsonPath, 20000));
|
|
3062
|
+
if (parsedPackage && typeof parsedPackage.scripts === "object" && parsedPackage.scripts) {
|
|
3063
|
+
packageScripts = Object.fromEntries(Object.entries(parsedPackage.scripts).filter(([, value]) => typeof value === "string"));
|
|
3064
|
+
}
|
|
3065
|
+
} catch {}
|
|
3066
|
+
}
|
|
3067
|
+
walk(root, 0);
|
|
3068
|
+
const readRel = (rel) => readTextLimited(path.join(root, rel), 20000);
|
|
3069
|
+
const listFilesUnder = (prefix) => {
|
|
3070
|
+
const absoluteRoot = path.join(root, prefix);
|
|
3071
|
+
const files = [];
|
|
3072
|
+
const visit = (dir, depth) => {
|
|
3073
|
+
if (depth > 2 || files.length >= 40) return;
|
|
3074
|
+
let entries = [];
|
|
3075
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
3076
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
3077
|
+
for (const entry of entries) {
|
|
3078
|
+
const absolute = path.join(dir, entry.name);
|
|
3079
|
+
const rel = safeRel(path.relative(root, absolute));
|
|
3080
|
+
if (!rel) continue;
|
|
3081
|
+
if (entry.isDirectory()) visit(absolute, depth + 1);
|
|
3082
|
+
else if (entry.isFile()) files.push(rel);
|
|
3083
|
+
}
|
|
3084
|
+
};
|
|
3085
|
+
visit(absoluteRoot, 0);
|
|
3086
|
+
return files;
|
|
3087
|
+
};
|
|
3088
|
+
const coverageMapText = readRel("humanish/coverage-map.md");
|
|
3089
|
+
const coverageMatrixText = readRel("humanish/coverage-matrix.md");
|
|
3090
|
+
const personaFiles = listFilesUnder(path.join("humanish", "personas"));
|
|
3091
|
+
const scenarioFiles = listFilesUnder(path.join("humanish", "scenarios"));
|
|
3092
|
+
const personaText = personaFiles.map((rel) => readRel(rel)).join("\\n");
|
|
3093
|
+
const scenarioText = scenarioFiles.map((rel) => readRel(rel)).join("\\n");
|
|
3094
|
+
const configText = readRel("humanish/config.ts");
|
|
3095
|
+
const defaultPersonaFiles = new Set(["humanish/personas/synthetic-new-user.yaml", "humanish/personas/skeptical-power-user.yaml"]);
|
|
3096
|
+
const defaultScenarioFiles = new Set(["humanish/scenarios/first-run-smoke.yaml", "humanish/scenarios/onboarding-regression.yaml"]);
|
|
3097
|
+
const personaEntries = personaFiles.map((rel) => ({ rel, text: readRel(rel) }));
|
|
3098
|
+
const scenarioEntries = scenarioFiles.map((rel) => ({ rel, text: readRel(rel) }));
|
|
3099
|
+
const coverageCustomized = (
|
|
3100
|
+
Boolean(coverageMapText.trim())
|
|
3101
|
+
&& !/Current starter coverage is intentionally minimal/i.test(coverageMapText)
|
|
3102
|
+
&& /\\b(screen|route|role|state|path|flow|journey|surface|happy|sad|friction)\\b/i.test(coverageMapText)
|
|
3103
|
+
) || (
|
|
3104
|
+
Boolean(coverageMatrixText.trim())
|
|
3105
|
+
&& !/\\bstarter\\b/i.test(coverageMatrixText)
|
|
3106
|
+
&& /\\b(screen|route|role|state|path|flow|journey|surface|happy|sad|friction)\\b/i.test(coverageMatrixText)
|
|
3107
|
+
);
|
|
3108
|
+
const personaCustomizedCount = personaEntries.filter(({ rel, text }) => !defaultPersonaFiles.has(rel)
|
|
3109
|
+
|| (Boolean(text.trim())
|
|
3110
|
+
&& !/privacy-safe first-time user evaluating the app|prefers clear affordances over novelty|starter persona/i.test(text)
|
|
3111
|
+
&& /\\b(role|workflow|creator|operator|admin|guest|player|customer|seller|designer|developer|founder|marketer|power user|novice|skeptical|accessibility|persona|job-to-be-done)\\b/i.test(text))).length;
|
|
3112
|
+
const scenarioCustomizedCount = scenarioEntries.filter(({ rel, text }) => !defaultScenarioFiles.has(rel)
|
|
3113
|
+
|| (Boolean(text.trim())
|
|
3114
|
+
&& !/Reach the first meaningful product state|Exercise onboarding friction|starter scenario/i.test(text)
|
|
3115
|
+
&& /\\b(route|screen|journey|state|happy|sad|friction|error|empty|mobile|desktop|evidence|expectation|selector|upload|preview|pricing|auth|settings|checkout|workflow)\\b/i.test(text))).length;
|
|
3116
|
+
const personaCustomized = personaCustomizedCount >= Math.min(2, personaFiles.length || 2);
|
|
3117
|
+
const scenarioCustomized = scenarioCustomizedCount >= Math.min(2, scenarioFiles.length || 2);
|
|
3118
|
+
const actorEvidenceText = [actorLogTail, actorLastMessageTail, redactedTail, scenarioText, configText].join("\\n");
|
|
3119
|
+
const actorEvidenceNormalized = actorEvidenceText.replace(/[*_\`~[\\](){}<>]/g, " ").replace(/\\s+/g, " ").trim();
|
|
3120
|
+
const appUrlProofBlocked = /(?:unknown option|unsupported|not available|does\\s+\\W*not\\W*(?:support|expose|accept)|did\\s+\\W*not\\W*(?:support|expose|accept)|doesnt\\s+(?:support|expose|accept)|didnt\\s+(?:support|expose|accept))[\\s\\S]{0,220}(?:--app-url|run\\s+--app-url|app-url\\s+proof)|(?:--app-url|run\\s+--app-url|app-url\\s+proof)[\\s\\S]{0,220}(?:unknown option|unsupported|not available|does\\s+\\W*not\\W*(?:support|expose|accept)|did\\s+\\W*not\\W*(?:support|expose|accept)|doesnt\\s+(?:support|expose|accept)|didnt\\s+(?:support|expose|accept))/i.test(actorEvidenceNormalized);
|
|
3121
|
+
const appUrlProofMentioned = !appUrlProofBlocked && (
|
|
3122
|
+
nestedVerifyStatus === "passed"
|
|
3123
|
+
|| /(?:humanish\\s+run[\\s\\S]{0,160}--app-url|--app-url[\\s\\S]{0,160}humanish\\s+run)/i.test(actorEvidenceText)
|
|
3124
|
+
);
|
|
3125
|
+
const actorInsightCaptured = /\\b(observed|found|friction|improvement|issue|gap|confusing|blocked|recommend|none observed|feedback|useful improvement|next harness upgrade)\\b/i.test(actorEvidenceNormalized);
|
|
3126
|
+
const gitignore = readTextLimited(path.join(root, ".gitignore"), 20000);
|
|
3127
|
+
const configPresent = fs.existsSync(path.join(root, "humanish", "config.ts"));
|
|
3128
|
+
const personaCount = countFilesUnder(root, path.join("humanish", "personas"));
|
|
3129
|
+
const scenarioCount = countFilesUnder(root, path.join("humanish", "scenarios"));
|
|
3130
|
+
const packageScriptPresent = Object.entries(packageScripts).some(([key, value]) => key.includes("humanish") || String(value).includes("humanish"));
|
|
3131
|
+
const gitignoreContainsRuntimeIgnore = /(^|\\n)\\s*\\.humanish\\/?\\s*(\\n|$)/.test(gitignore);
|
|
3132
|
+
const checks = [
|
|
3133
|
+
{ id: "humanish-config", label: "Humanish config", ok: configPresent, detail: configPresent ? "humanish/config.ts exists." : "humanish/config.ts was not created." },
|
|
3134
|
+
{ id: "personas", label: "Personas", ok: personaCount > 0, detail: personaCount + " persona file(s) detected." },
|
|
3135
|
+
{ id: "scenarios", label: "Scenarios", ok: scenarioCount > 0, detail: scenarioCount + " scenario file(s) detected." },
|
|
3136
|
+
{ id: "package-script", label: "Package script", ok: packageScriptPresent, detail: packageScriptPresent ? "package.json exposes a Humanish script." : "package.json does not expose a Humanish script." },
|
|
3137
|
+
{ id: "runtime-ignore", label: "Runtime ignore", ok: gitignoreContainsRuntimeIgnore, detail: gitignoreContainsRuntimeIgnore ? ".gitignore ignores .humanish/." : ".gitignore does not ignore .humanish/." }
|
|
3138
|
+
];
|
|
3139
|
+
const studyChecks = [
|
|
3140
|
+
{ id: "coverage-customized", label: "Coverage customized", ok: coverageCustomized, detail: coverageCustomized ? "Coverage map or matrix appears app-aware." : "Coverage map/matrix still appears starter-level or absent." },
|
|
3141
|
+
{ id: "personas-customized", label: "Personas customized", ok: personaCustomized, detail: personaCustomized ? "Personas appear app-specific or non-starter." : "Personas appear generic starter-level." },
|
|
3142
|
+
{ id: "scenarios-customized", label: "Scenarios customized", ok: scenarioCustomized, detail: scenarioCustomized ? "Scenarios appear app-specific or non-starter." : "Scenarios appear generic starter-level." },
|
|
3143
|
+
{ id: "app-url-proof", label: "App-url proof", ok: appUrlProofMentioned, detail: appUrlProofMentioned ? "Actor/setup evidence references app-url proof." : appUrlProofBlocked ? "Actor evidence reports that app-url proof was blocked." : "No app-url proof signal detected." },
|
|
3144
|
+
{ id: "actor-insight", label: "Actor insight", ok: actorInsightCaptured, detail: actorInsightCaptured ? "Actor evidence mentions feedback, friction, coverage, or observed product behavior." : "Actor evidence does not capture product feedback or coverage insight." }
|
|
3145
|
+
];
|
|
3146
|
+
const studySignalCount = studyChecks.filter((check) => check.ok).length;
|
|
3147
|
+
const usefulStudy = coverageCustomized && personaCustomized && scenarioCustomized;
|
|
3148
|
+
const concreteStudyInsight = actorInsightCaptured || appUrlProofBlocked;
|
|
3149
|
+
const studyRating = !configPresent || personaCount === 0 || scenarioCount === 0
|
|
3150
|
+
? "none"
|
|
3151
|
+
: usefulStudy && concreteStudyInsight && (appUrlProofMentioned || appUrlProofBlocked)
|
|
3152
|
+
? "high_leverage"
|
|
3153
|
+
: usefulStudy
|
|
3154
|
+
? "useful"
|
|
3155
|
+
: "ceremonial";
|
|
3156
|
+
const failed = checks.filter((check) => !check.ok);
|
|
3157
|
+
return {
|
|
3158
|
+
schema: "humanish.setup-quality.v1",
|
|
3159
|
+
generatedAt: new Date().toISOString(),
|
|
3160
|
+
redaction: {
|
|
3161
|
+
status: "passed",
|
|
3162
|
+
rawPreviews: "included",
|
|
3163
|
+
notes: "Only allowlisted setup files are previewed; generated state, browser profiles, secrets, .git, node_modules, and .humanish are excluded."
|
|
3164
|
+
},
|
|
3165
|
+
summary: failed.length === 0 ? "Humanish setup evidence looks complete." : failed.length + " setup-quality gap(s) need review.",
|
|
3166
|
+
status: failed.length === 0 ? "passed" : "needs_review",
|
|
3167
|
+
checks,
|
|
3168
|
+
tree,
|
|
3169
|
+
previews,
|
|
3170
|
+
studyQuality: {
|
|
3171
|
+
schema: "humanish.study-quality.v1",
|
|
3172
|
+
rating: studyRating,
|
|
3173
|
+
summary: "Study-quality rating " + studyRating + " from " + studySignalCount + "/5 app-specific leverage signals" + (appUrlProofBlocked ? "; app-url proof path was blocked by actor evidence." : "."),
|
|
3174
|
+
checks: studyChecks,
|
|
3175
|
+
signals: {
|
|
3176
|
+
appUrlProofBlocked,
|
|
3177
|
+
appUrlProofMentioned,
|
|
3178
|
+
actorInsightCaptured,
|
|
3179
|
+
coverageCustomized,
|
|
3180
|
+
personaCustomized,
|
|
3181
|
+
scenarioCustomized
|
|
3182
|
+
}
|
|
3183
|
+
},
|
|
3184
|
+
packageScripts,
|
|
3185
|
+
humanish: { configPresent, personaCount, scenarioCount, packageScriptPresent, gitignoreContainsRuntimeIgnore }
|
|
3186
|
+
};
|
|
3187
|
+
};
|
|
3188
|
+
const setupQuality = buildSetupQuality(appDir);
|
|
3189
|
+
const safeToken = (value) => String(value || "stream").replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "stream";
|
|
3190
|
+
const appServerActorEvidence = (() => {
|
|
3191
|
+
if (!fs.existsSync(codexAppServerStatePath)) return null;
|
|
3192
|
+
try {
|
|
3193
|
+
const state = JSON.parse(fs.readFileSync(codexAppServerStatePath, "utf8"));
|
|
3194
|
+
const result = state && typeof state === "object" ? state.result || {} : {};
|
|
3195
|
+
const readArtifact = (relativePath, maxBytes = 120000) => {
|
|
3196
|
+
if (!relativePath || String(relativePath).includes("..") || String(relativePath).includes("://")) return "";
|
|
3197
|
+
return readTextLimited(path.join(codexAppServerRootPath, String(relativePath)), maxBytes);
|
|
3198
|
+
};
|
|
3199
|
+
const readJsonArtifact = (relativePath) => {
|
|
3200
|
+
if (!relativePath || String(relativePath).includes("..") || String(relativePath).includes("://")) return null;
|
|
3201
|
+
try {
|
|
3202
|
+
return JSON.parse(fs.readFileSync(path.join(codexAppServerRootPath, String(relativePath)), "utf8"));
|
|
3203
|
+
} catch {
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
const compactList = (items, mapper, limit = 8) => Array.isArray(items)
|
|
3208
|
+
? items.slice(-limit).map(mapper).filter(Boolean)
|
|
3209
|
+
: undefined;
|
|
3210
|
+
const projectTraceJson = (value) => {
|
|
3211
|
+
if (!value || typeof value !== "object") return null;
|
|
3212
|
+
const messages = compactList(value.messages, (message) => message && typeof message === "object" ? {
|
|
3213
|
+
...(message.itemId ? { itemId: cleanText(message.itemId) } : {}),
|
|
3214
|
+
...(message.text ? { text: cleanText(message.text).slice(0, 4000) } : {})
|
|
3215
|
+
} : null, 10);
|
|
3216
|
+
const commands = compactList(value.commands, (command) => command && typeof command === "object" ? {
|
|
3217
|
+
...(command.itemId ? { itemId: cleanText(command.itemId) } : {}),
|
|
3218
|
+
...(command.command ? { command: cleanText(command.command).slice(0, 2000) } : {}),
|
|
3219
|
+
...(command.cwd ? { cwd: cleanText(command.cwd).slice(0, 500) } : {}),
|
|
3220
|
+
...(command.status ? { status: cleanText(command.status).slice(0, 80) } : {}),
|
|
3221
|
+
...(Number.isFinite(command.exitCode) ? { exitCode: command.exitCode } : {}),
|
|
3222
|
+
...(command.outputTail ? { outputTail: cleanText(command.outputTail).slice(-4000) } : {})
|
|
3223
|
+
} : null, 12);
|
|
3224
|
+
const fileChanges = compactList(value.fileChanges, (change) => change && typeof change === "object" ? {
|
|
3225
|
+
...(change.itemId ? { itemId: cleanText(change.itemId) } : {}),
|
|
3226
|
+
...(change.status ? { status: cleanText(change.status).slice(0, 80) } : {}),
|
|
3227
|
+
...(Number.isFinite(change.changeCount) ? { changeCount: change.changeCount } : {}),
|
|
3228
|
+
...(change.outputTail ? { outputTail: cleanText(change.outputTail).slice(-2000) } : {})
|
|
3229
|
+
} : null, 8);
|
|
3230
|
+
return {
|
|
3231
|
+
schema: "humanish.codex-app-server-trace.projected.v1",
|
|
3232
|
+
sourceSchema: cleanText(value.schema || ""),
|
|
3233
|
+
redaction: value.redaction && typeof value.redaction === "object" ? value.redaction : { status: "passed" },
|
|
3234
|
+
status: cleanText(value.status || result.status || state.status || ""),
|
|
3235
|
+
reason: cleanText(value.reason || result.reason || state.reason || ""),
|
|
3236
|
+
...(value.counts && typeof value.counts === "object" ? { counts: value.counts } : {}),
|
|
3237
|
+
...(messages && messages.length ? { messages } : {}),
|
|
3238
|
+
...(commands && commands.length ? { commands } : {}),
|
|
3239
|
+
...(fileChanges && fileChanges.length ? { fileChanges } : {}),
|
|
3240
|
+
...(value.tokenUsage && typeof value.tokenUsage === "object" ? { tokenUsage: value.tokenUsage } : {}),
|
|
3241
|
+
...(value.threadId ? { threadId: cleanText(value.threadId) } : {}),
|
|
3242
|
+
...(value.turnId ? { turnId: cleanText(value.turnId) } : {}),
|
|
3243
|
+
...(value.sessionId ? { sessionId: cleanText(value.sessionId) } : {}),
|
|
3244
|
+
...(value.completedAt ? { completedAt: cleanText(value.completedAt) } : {}),
|
|
3245
|
+
...(Number.isFinite(value.durationMs) ? { durationMs: value.durationMs } : {})
|
|
3246
|
+
};
|
|
3247
|
+
};
|
|
3248
|
+
const rawTraceJson = readJsonArtifact(result.tracePath);
|
|
3249
|
+
const traceJson = projectTraceJson(rawTraceJson);
|
|
3250
|
+
const traceText = traceJson ? "" : readArtifact(result.tracePath, 160000);
|
|
3251
|
+
const token = safeToken(streamId);
|
|
3252
|
+
return {
|
|
3253
|
+
eventsPath: path.join("codex-app-server", token + "-events.ndjson"),
|
|
3254
|
+
eventsText: readArtifact(result.eventsPath, 120000),
|
|
3255
|
+
reason: cleanText(state.reason || result.reason || ""),
|
|
3256
|
+
status: cleanText(state.status || result.status || ""),
|
|
3257
|
+
tracePath: path.join("codex-app-server", token + "-summary.json"),
|
|
3258
|
+
...(traceJson ? { traceJson } : { traceText }),
|
|
3259
|
+
transcriptPath: path.join("codex-app-server", token + "-transcript.txt"),
|
|
3260
|
+
transcriptText: readArtifact(result.transcriptPath, 80000)
|
|
3261
|
+
};
|
|
3262
|
+
} catch {
|
|
3263
|
+
return null;
|
|
3264
|
+
}
|
|
3265
|
+
})();
|
|
3266
|
+
const nestedStepTraceSummary = (() => {
|
|
3267
|
+
try {
|
|
3268
|
+
const runRoot = path.dirname(path.dirname(String(nestedObserverPath || "")));
|
|
3269
|
+
const tracesDir = path.join(runRoot, "traces");
|
|
3270
|
+
if (!fs.existsSync(tracesDir)) return null;
|
|
3271
|
+
const traceNames = fs.readdirSync(tracesDir)
|
|
3272
|
+
.filter((name) => name.endsWith(".json"))
|
|
3273
|
+
.sort()
|
|
3274
|
+
.slice(0, 8);
|
|
3275
|
+
if (!traceNames.length) return null;
|
|
3276
|
+
const surfaceSummaries = [];
|
|
3277
|
+
let scenario = null;
|
|
3278
|
+
for (const name of traceNames) {
|
|
3279
|
+
let trace = null;
|
|
3280
|
+
try {
|
|
3281
|
+
trace = JSON.parse(fs.readFileSync(path.join(tracesDir, name), "utf8"));
|
|
3282
|
+
} catch {
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
if (!trace || typeof trace !== "object" || !Array.isArray(trace.steps)) continue;
|
|
3286
|
+
if (!scenario && trace.scenario && typeof trace.scenario === "object") {
|
|
3287
|
+
scenario = {
|
|
3288
|
+
id: cleanText(trace.scenario.id || "scenario"),
|
|
3289
|
+
...(trace.scenario.source ? { source: safeRel(trace.scenario.source) } : {}),
|
|
3290
|
+
...(trace.scenario.sourceDigest ? { sourceDigest: cleanText(trace.scenario.sourceDigest) } : {}),
|
|
3291
|
+
...(Number.isFinite(trace.scenario.stepCount) ? { stepCount: trace.scenario.stepCount } : {}),
|
|
3292
|
+
...(trace.scenario.title ? { title: cleanText(trace.scenario.title) } : {})
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
const steps = trace.steps.slice(0, 20).map((step) => {
|
|
3296
|
+
const assertions = Array.isArray(step.assertions)
|
|
3297
|
+
? step.assertions.slice(0, 12).map((assertion) => cleanText((assertion.id || "assertion") + ":" + (assertion.status || "unknown")))
|
|
3298
|
+
: [];
|
|
3299
|
+
return {
|
|
3300
|
+
action: cleanText(step.action || "action"),
|
|
3301
|
+
...(assertions.length ? { assertionStatuses: assertions } : {}),
|
|
3302
|
+
id: cleanText(step.id || "step"),
|
|
3303
|
+
...(step.label ? { label: cleanText(step.label) } : {}),
|
|
3304
|
+
reason: cleanText(step.reason || ""),
|
|
3305
|
+
status: step.status === "passed" || step.status === "blocked" ? step.status : "unknown"
|
|
3306
|
+
};
|
|
3307
|
+
});
|
|
3308
|
+
if (!steps.length) continue;
|
|
3309
|
+
surfaceSummaries.push({
|
|
3310
|
+
id: cleanText(trace.surface && trace.surface.id ? trace.surface.id : path.basename(name, ".json")),
|
|
3311
|
+
...(trace.surface && trace.surface.label ? { label: cleanText(trace.surface.label) } : {}),
|
|
3312
|
+
ok: trace.ok === true,
|
|
3313
|
+
reason: cleanText(trace.reason || ""),
|
|
3314
|
+
steps
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
if (!surfaceSummaries.length) return null;
|
|
3318
|
+
const totalSteps = surfaceSummaries.reduce((total, surface) => total + surface.steps.length, 0);
|
|
3319
|
+
const passedSteps = surfaceSummaries.reduce((total, surface) => total + surface.steps.filter((step) => step.status === "passed").length, 0);
|
|
3320
|
+
const blockedSteps = surfaceSummaries.reduce((total, surface) => total + surface.steps.filter((step) => step.status === "blocked").length, 0);
|
|
3321
|
+
return {
|
|
3322
|
+
schema: "humanish.oss-meta-nested-step-trace-summary.v1",
|
|
3323
|
+
redaction: {
|
|
3324
|
+
status: "passed",
|
|
3325
|
+
notes: "Projected inside disposable sandbox from nested Humanish trace JSON; URLs, screenshots, and remote paths are omitted."
|
|
3326
|
+
},
|
|
3327
|
+
counts: {
|
|
3328
|
+
blockedSteps,
|
|
3329
|
+
passedSteps,
|
|
3330
|
+
surfaces: surfaceSummaries.length,
|
|
3331
|
+
totalSteps,
|
|
3332
|
+
traces: traceNames.length
|
|
3333
|
+
},
|
|
3334
|
+
...(scenario ? { scenario } : {}),
|
|
3335
|
+
status: blockedSteps > 0 ? "blocked" : passedSteps === totalSteps ? "passed" : "unknown",
|
|
3336
|
+
surfaces: surfaceSummaries
|
|
3337
|
+
};
|
|
3338
|
+
} catch {
|
|
3339
|
+
return null;
|
|
3340
|
+
}
|
|
3341
|
+
})();
|
|
3342
|
+
fs.writeFileSync(completionPath, JSON.stringify({
|
|
3343
|
+
schema: "humanish.oss-meta-bootstrap-completion.v1",
|
|
3344
|
+
status,
|
|
3345
|
+
reason,
|
|
3346
|
+
exitCode: Number(exitCode),
|
|
3347
|
+
appStatus,
|
|
3348
|
+
appReason: cleanText(appReason),
|
|
3349
|
+
appUrl: cleanText(appUrl),
|
|
3350
|
+
appPid: numberOrNull(appPid),
|
|
3351
|
+
appLogPath: cleanText(appLogPath),
|
|
3352
|
+
actorStatus,
|
|
3353
|
+
actorPid: numberOrNull(actorPid),
|
|
3354
|
+
actorLogPath: cleanText(actorLogPath),
|
|
3355
|
+
actorLogTail,
|
|
3356
|
+
actorLastMessageTail,
|
|
3357
|
+
...(appServerActorEvidence ? { appServerActorEvidence } : {}),
|
|
3358
|
+
nestedObserverPresent: nestedObserverPresent === "true",
|
|
3359
|
+
...(nestedStepTraceSummary ? { nestedStepTraceSummary } : {}),
|
|
3360
|
+
nestedVerifyStatus,
|
|
3361
|
+
setupQuality,
|
|
3362
|
+
visualStatus,
|
|
3363
|
+
visualReason: cleanText(visualReason),
|
|
3364
|
+
visualWindowCount: numberOrNull(visualWindowCount),
|
|
3365
|
+
logTail: redactedTail,
|
|
3366
|
+
completedAt: new Date().toISOString()
|
|
3367
|
+
}, null, 2) + "\\n");
|
|
3368
|
+
NODE
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
finish() {
|
|
3372
|
+
local exit_code="$?"
|
|
3373
|
+
trap - EXIT
|
|
3374
|
+
if [[ "$exit_code" -ne 0 ]]; then
|
|
3375
|
+
write_completion "failed" "Bootstrap exited before nested Humanish proof completed." "$exit_code"
|
|
3376
|
+
elif [[ "$NESTED_VERIFY_STATUS" != "passed" ]]; then
|
|
3377
|
+
write_completion "failed" "Nested Humanish verification did not pass." "$exit_code"
|
|
3378
|
+
elif [[ "$APP_STATUS" != "running" ]]; then
|
|
3379
|
+
write_completion "blocked" "Nested Humanish proof completed, but the target app surface was not proven running." "$exit_code"
|
|
3380
|
+
elif [[ "$VISUAL_STATUS" != "visible" ]]; then
|
|
3381
|
+
write_completion "blocked" "Nested Humanish proof completed, but headed desktop visual layout was not proven visible." "$exit_code"
|
|
3382
|
+
elif [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" && "$ACTOR_STATUS" != "passed" ]]; then
|
|
3383
|
+
write_completion "blocked" "Codex app-server mode requires passed Codex app-server actor evidence." "$exit_code"
|
|
3384
|
+
elif [[ "\${HUMANISH_OSS_META_REQUIRE_ACTOR:-0}" == "1" && "$ACTOR_STATUS" != "passed" ]]; then
|
|
3385
|
+
write_completion "blocked" "Required Codex actor evidence did not reach a passed terminal status." "$exit_code"
|
|
3386
|
+
else
|
|
3387
|
+
write_completion "passed" "Target app surface, nested Humanish proof, and nested Observer were checked." "$exit_code"
|
|
3388
|
+
fi
|
|
3389
|
+
exit "$exit_code"
|
|
3390
|
+
}
|
|
3391
|
+
trap finish EXIT
|
|
3392
|
+
|
|
3393
|
+
echo "== humanish oss meta-lab bootstrap =="
|
|
3394
|
+
echo "repo=${args.displayRepo}"
|
|
3395
|
+
echo "public_safe=1"
|
|
3396
|
+
echo "provider_secrets=isolated"
|
|
3397
|
+
echo "github_token=$([[ -n "$HUMANISH_PRIVATE_GITHUB_TOKEN" ]] && echo available-for-clone || echo absent)"
|
|
3398
|
+
echo "codex_actor_auth=$([[ -n "$HUMANISH_PRIVATE_CODEX_API_KEY$HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN" ]] && echo available-for-actor || echo absent)"
|
|
3399
|
+
echo "codex_app_server_mode=\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}"
|
|
3400
|
+
echo "codex_app_server_client=$([[ -n "$HUMANISH_PRIVATE_CODEX_APP_SERVER_URL" ]] && echo available || echo placeholder)"
|
|
3401
|
+
echo "host_actor_plan=$([[ -f "$HOST_ACTOR_PLAN" ]] && echo available || echo absent)"
|
|
3402
|
+
echo
|
|
3403
|
+
|
|
3404
|
+
need() {
|
|
3405
|
+
if ! command -v "$1" >/dev/null 2>&1; then
|
|
3406
|
+
echo "missing command: $1"
|
|
3407
|
+
return 1
|
|
3408
|
+
fi
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
ensure_node() {
|
|
3412
|
+
local major=0
|
|
3413
|
+
if command -v node >/dev/null 2>&1; then
|
|
3414
|
+
major="$(node -e 'console.log(Number(process.versions.node.split(".")[0]))' 2>/dev/null || echo 0)"
|
|
3415
|
+
fi
|
|
3416
|
+
|
|
3417
|
+
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 && [[ "$major" -ge 20 ]]; then
|
|
3418
|
+
return 0
|
|
3419
|
+
fi
|
|
3420
|
+
|
|
3421
|
+
echo "node_or_npm=missing_or_too_old"
|
|
3422
|
+
if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then
|
|
3423
|
+
echo "installing nodejs/npm via nodesource"
|
|
3424
|
+
sudo -n apt-get update
|
|
3425
|
+
sudo -n apt-get install -y ca-certificates curl gnupg
|
|
3426
|
+
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
|
3427
|
+
sudo -n apt-get install -y nodejs
|
|
3428
|
+
fi
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
need git
|
|
3432
|
+
ensure_node
|
|
3433
|
+
need node
|
|
3434
|
+
need npm
|
|
3435
|
+
|
|
3436
|
+
wait_for_http() {
|
|
3437
|
+
local url="$1"
|
|
3438
|
+
local timeout_ms="$2"
|
|
3439
|
+
local interval_ms="$3"
|
|
3440
|
+
node - "$url" "$timeout_ms" "$interval_ms" <<'NODE'
|
|
3441
|
+
const [url, timeoutArg, intervalArg] = process.argv.slice(2);
|
|
3442
|
+
const timeoutMs = Number(timeoutArg);
|
|
3443
|
+
const intervalMs = Number(intervalArg);
|
|
3444
|
+
const startedAt = Date.now();
|
|
3445
|
+
|
|
3446
|
+
async function probe() {
|
|
3447
|
+
const controller = new AbortController();
|
|
3448
|
+
const timer = setTimeout(() => controller.abort(), Math.min(intervalMs, 5_000));
|
|
3449
|
+
try {
|
|
3450
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
3451
|
+
return response.status < 500;
|
|
3452
|
+
} catch {
|
|
3453
|
+
return false;
|
|
3454
|
+
} finally {
|
|
3455
|
+
clearTimeout(timer);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
3460
|
+
if (await probe()) process.exit(0);
|
|
3461
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3462
|
+
}
|
|
3463
|
+
process.exit(1);
|
|
3464
|
+
NODE
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
wait_for_any_http() {
|
|
3468
|
+
local timeout_ms="$1"
|
|
3469
|
+
local interval_ms="$2"
|
|
3470
|
+
shift 2
|
|
3471
|
+
node - "$timeout_ms" "$interval_ms" "$@" <<'NODE'
|
|
3472
|
+
const [timeoutArg, intervalArg, ...urls] = process.argv.slice(2);
|
|
3473
|
+
const timeoutMs = Number(timeoutArg);
|
|
3474
|
+
const intervalMs = Number(intervalArg);
|
|
3475
|
+
const startedAt = Date.now();
|
|
3476
|
+
|
|
3477
|
+
async function probe(url) {
|
|
3478
|
+
const controller = new AbortController();
|
|
3479
|
+
const timer = setTimeout(() => controller.abort(), Math.min(intervalMs, 5_000));
|
|
3480
|
+
try {
|
|
3481
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
3482
|
+
return response.status < 500;
|
|
3483
|
+
} catch {
|
|
3484
|
+
return false;
|
|
3485
|
+
} finally {
|
|
3486
|
+
clearTimeout(timer);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
3491
|
+
for (const url of urls) {
|
|
3492
|
+
if (await probe(url)) {
|
|
3493
|
+
console.log(url);
|
|
3494
|
+
process.exit(0);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
3498
|
+
}
|
|
3499
|
+
process.exit(1);
|
|
3500
|
+
NODE
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
open_browser_url() {
|
|
3504
|
+
local url="$1"
|
|
3505
|
+
local profile="$2"
|
|
3506
|
+
local x="$3"
|
|
3507
|
+
local y="$4"
|
|
3508
|
+
local width="$5"
|
|
3509
|
+
local height="$6"
|
|
3510
|
+
if command -v google-chrome >/dev/null 2>&1; then
|
|
3511
|
+
nohup google-chrome --new-window --no-first-run --no-default-browser-check --disable-default-apps --user-data-dir="$ROOT_DIR/chrome-$profile" --window-position="$x,$y" --window-size="$width,$height" "$url" >/dev/null 2>&1 &
|
|
3512
|
+
elif command -v firefox >/dev/null 2>&1; then
|
|
3513
|
+
nohup firefox --width "$width" --height "$height" "$url" >/dev/null 2>&1 &
|
|
3514
|
+
else
|
|
3515
|
+
echo "browser_open=skipped profile=$profile url=$url"
|
|
3516
|
+
fi
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
open_nested_observer() {
|
|
3520
|
+
local label="$1"
|
|
3521
|
+
echo
|
|
3522
|
+
echo "== $label =="
|
|
3523
|
+
if [[ -f "$NESTED_OBSERVER" ]]; then
|
|
3524
|
+
open_browser_url "file://$NESTED_OBSERVER" nested-observer 760 0 680 940
|
|
3525
|
+
else
|
|
3526
|
+
echo "Nested observer missing: $NESTED_OBSERVER"
|
|
3527
|
+
fi
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
open_codex_app_server_client() {
|
|
3531
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" != "1" ]]; then
|
|
3532
|
+
return 0
|
|
3533
|
+
fi
|
|
3534
|
+
if [[ "$CODEX_APP_SERVER_CLIENT_OPENED" == "1" ]]; then
|
|
3535
|
+
return 0
|
|
3536
|
+
fi
|
|
3537
|
+
|
|
3538
|
+
echo
|
|
3539
|
+
echo "== codex app-server client surface =="
|
|
3540
|
+
local client_url="\${CODEX_APP_SERVER_UI_URL:-$HUMANISH_PRIVATE_CODEX_APP_SERVER_URL}"
|
|
3541
|
+
if [[ -z "$client_url" ]]; then
|
|
3542
|
+
ACTOR_STATUS=blocked
|
|
3543
|
+
echo "codex_app_server_client=blocked reason=no real app-server UI URL available"
|
|
3544
|
+
return 1
|
|
3545
|
+
fi
|
|
3546
|
+
echo "codex_app_server_client=provided"
|
|
3547
|
+
open_browser_url "$client_url" codex-app-server 430 520 330 420
|
|
3548
|
+
CODEX_APP_SERVER_CLIENT_OPENED=1
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
arrange_lab_windows() {
|
|
3552
|
+
echo
|
|
3553
|
+
echo "== visual desktop layout =="
|
|
3554
|
+
VISUAL_STATUS=unknown
|
|
3555
|
+
VISUAL_REASON="Window manager proof was not collected."
|
|
3556
|
+
VISUAL_WINDOW_COUNT=0
|
|
3557
|
+
local expected_chrome_windows=2
|
|
3558
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
3559
|
+
expected_chrome_windows=3
|
|
3560
|
+
fi
|
|
3561
|
+
if ! command -v xdotool >/dev/null 2>&1; then
|
|
3562
|
+
VISUAL_STATUS=unknown
|
|
3563
|
+
VISUAL_REASON="xdotool is unavailable; cannot prove headed browser window visibility."
|
|
3564
|
+
echo "visual_status=$VISUAL_STATUS reason=$VISUAL_REASON"
|
|
3565
|
+
return 0
|
|
3566
|
+
fi
|
|
3567
|
+
|
|
3568
|
+
local chrome_windows=()
|
|
3569
|
+
local observer_window=""
|
|
3570
|
+
for attempt in $(seq 1 40); do
|
|
3571
|
+
mapfile -t chrome_windows < <(xdotool search --onlyvisible --class google-chrome 2>/dev/null || true)
|
|
3572
|
+
observer_window="$(xdotool search --onlyvisible --name "Humanish Observer" 2>/dev/null | tail -n 1 || true)"
|
|
3573
|
+
VISUAL_WINDOW_COUNT="\${#chrome_windows[@]}"
|
|
3574
|
+
if [[ "$VISUAL_WINDOW_COUNT" -ge "$expected_chrome_windows" && -n "$observer_window" ]]; then
|
|
3575
|
+
break
|
|
3576
|
+
fi
|
|
3577
|
+
sleep 0.25
|
|
3578
|
+
done
|
|
3579
|
+
|
|
3580
|
+
if [[ -n "$observer_window" ]]; then
|
|
3581
|
+
xdotool windowsize "$observer_window" 680 933 >/dev/null 2>&1 || true
|
|
3582
|
+
xdotool windowmove "$observer_window" 760 27 >/dev/null 2>&1 || true
|
|
3583
|
+
fi
|
|
3584
|
+
|
|
3585
|
+
local slot=0
|
|
3586
|
+
local win
|
|
3587
|
+
for win in "\${chrome_windows[@]}"; do
|
|
3588
|
+
if [[ -n "$observer_window" && "$win" == "$observer_window" ]]; then
|
|
3589
|
+
continue
|
|
3590
|
+
fi
|
|
3591
|
+
if [[ "$slot" -eq 0 ]]; then
|
|
3592
|
+
xdotool windowsize "$win" 760 493 >/dev/null 2>&1 || true
|
|
3593
|
+
xdotool windowmove "$win" 0 27 >/dev/null 2>&1 || true
|
|
3594
|
+
elif [[ "$slot" -eq 1 ]]; then
|
|
3595
|
+
xdotool windowsize "$win" 430 420 >/dev/null 2>&1 || true
|
|
3596
|
+
xdotool windowmove "$win" 0 520 >/dev/null 2>&1 || true
|
|
3597
|
+
else
|
|
3598
|
+
xdotool windowsize "$win" 330 420 >/dev/null 2>&1 || true
|
|
3599
|
+
xdotool windowmove "$win" 430 520 >/dev/null 2>&1 || true
|
|
3600
|
+
fi
|
|
3601
|
+
slot=$((slot + 1))
|
|
3602
|
+
done
|
|
3603
|
+
|
|
3604
|
+
local terminal_window
|
|
3605
|
+
terminal_window="$(xdotool search --onlyvisible --name "$TERMINAL_TITLE" 2>/dev/null | head -n 1 || true)"
|
|
3606
|
+
if [[ -n "$terminal_window" ]]; then
|
|
3607
|
+
xdotool windowlower "$terminal_window" >/dev/null 2>&1 || xdotool windowminimize "$terminal_window" >/dev/null 2>&1 || true
|
|
3608
|
+
fi
|
|
3609
|
+
if [[ -n "$observer_window" ]]; then
|
|
3610
|
+
xdotool windowactivate "$observer_window" >/dev/null 2>&1 || true
|
|
3611
|
+
fi
|
|
3612
|
+
|
|
3613
|
+
mapfile -t chrome_windows < <(xdotool search --onlyvisible --class google-chrome 2>/dev/null || true)
|
|
3614
|
+
VISUAL_WINDOW_COUNT="\${#chrome_windows[@]}"
|
|
3615
|
+
if [[ "$VISUAL_WINDOW_COUNT" -ge "$expected_chrome_windows" && -n "$observer_window" ]]; then
|
|
3616
|
+
VISUAL_STATUS=visible
|
|
3617
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
3618
|
+
VISUAL_REASON="Detected $VISUAL_WINDOW_COUNT visible Chrome windows including target app, nested Observer, and Codex app-server client surface."
|
|
3619
|
+
else
|
|
3620
|
+
VISUAL_REASON="Detected $VISUAL_WINDOW_COUNT visible Chrome windows including nested Observer; app and Observer windows arranged for screenshot."
|
|
3621
|
+
fi
|
|
3622
|
+
else
|
|
3623
|
+
VISUAL_STATUS=blocked
|
|
3624
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
3625
|
+
VISUAL_REASON="Expected target app, nested Observer, and Codex app-server client browser windows, detected $VISUAL_WINDOW_COUNT visible Chrome window(s)."
|
|
3626
|
+
else
|
|
3627
|
+
VISUAL_REASON="Expected app and nested Observer browser windows, detected $VISUAL_WINDOW_COUNT visible Chrome window(s)."
|
|
3628
|
+
fi
|
|
3629
|
+
fi
|
|
3630
|
+
echo "visual_status=$VISUAL_STATUS window_count=$VISUAL_WINDOW_COUNT reason=$VISUAL_REASON"
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
detect_app_plan() {
|
|
3634
|
+
node <<'NODE'
|
|
3635
|
+
const fs = require("node:fs");
|
|
3636
|
+
const shell = (value) => "'" + String(value).replace(/'/g, "'\\"'\\"'") + "'";
|
|
3637
|
+
const emit = (key, value) => console.log(key + "=" + shell(value));
|
|
3638
|
+
|
|
3639
|
+
if (!fs.existsSync("package.json")) {
|
|
3640
|
+
console.log("APP_PLAN_OK='0'");
|
|
3641
|
+
emit("APP_PM", "npm");
|
|
3642
|
+
emit("APP_PLAN_REASON", "package.json not found");
|
|
3643
|
+
process.exit(0);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
let pkg;
|
|
3647
|
+
try {
|
|
3648
|
+
pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
3649
|
+
} catch {
|
|
3650
|
+
console.log("APP_PLAN_OK='0'");
|
|
3651
|
+
emit("APP_PM", "npm");
|
|
3652
|
+
emit("APP_PLAN_REASON", "package.json could not be parsed");
|
|
3653
|
+
process.exit(0);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
3657
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
3658
|
+
const lockfiles = fs.readdirSync(".").filter((name) => /^(pnpm-lock\\.yaml|yarn\\.lock|bun\\.lockb?|package-lock\\.json|npm-shrinkwrap\\.json)$/.test(name));
|
|
3659
|
+
const packageManagerHint = typeof pkg.packageManager === "string" ? pkg.packageManager : "";
|
|
3660
|
+
let pm = "npm";
|
|
3661
|
+
if (packageManagerHint.startsWith("pnpm@") || lockfiles.includes("pnpm-lock.yaml")) pm = "pnpm";
|
|
3662
|
+
else if (packageManagerHint.startsWith("yarn@") || lockfiles.includes("yarn.lock")) pm = "yarn";
|
|
3663
|
+
else if (packageManagerHint.startsWith("bun@") || lockfiles.includes("bun.lock") || lockfiles.includes("bun.lockb")) pm = "bun";
|
|
3664
|
+
|
|
3665
|
+
const names = ["dev", "start", "serve", "preview"];
|
|
3666
|
+
const entries = names
|
|
3667
|
+
.map((name) => ({ name, command: typeof scripts[name] === "string" ? scripts[name].trim() : "" }))
|
|
3668
|
+
.filter((entry) => entry.command && /^[A-Za-z0-9:_-]+$/.test(entry.name));
|
|
3669
|
+
|
|
3670
|
+
if (!entries.length) {
|
|
3671
|
+
console.log("APP_PLAN_OK='0'");
|
|
3672
|
+
emit("APP_PM", pm);
|
|
3673
|
+
emit("APP_PLAN_REASON", "no dev/start/serve/preview script found");
|
|
3674
|
+
process.exit(0);
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
const pickByCommand = (pattern) => entries.find((entry) => pattern.test(entry.command));
|
|
3678
|
+
let picked = null;
|
|
3679
|
+
let framework = "generic";
|
|
3680
|
+
if ((picked = pickByCommand(/\\bnext\\s+(dev|start)\\b/)) || deps.next) framework = "next";
|
|
3681
|
+
if (!picked && (picked = pickByCommand(/\\bvite(\\s|$)/))) framework = "vite";
|
|
3682
|
+
if (!picked && deps.vite) { picked = entries.find((entry) => entry.name === "dev") || entries[0]; framework = "vite"; }
|
|
3683
|
+
if (!picked && deps.next) picked = entries.find((entry) => entry.name === "dev" || entry.name === "start") || entries[0];
|
|
3684
|
+
if (!picked && (picked = pickByCommand(/\\bastro\\s+dev\\b/))) framework = "vite";
|
|
3685
|
+
if (!picked) picked = entries.find((entry) => entry.name === "dev") || entries.find((entry) => entry.name === "start") || entries[0];
|
|
3686
|
+
|
|
3687
|
+
const command = picked.command;
|
|
3688
|
+
const portFromCommand = /(?:^|\\s)(?:--port|-p)\\s+([0-9]{2,5})(?:\\s|$)/.exec(command)?.[1]
|
|
3689
|
+
|| /(?:^|\\s)PORT=([0-9]{2,5})(?:\\s|$)/.exec(command)?.[1];
|
|
3690
|
+
const defaultPort = framework === "vite" ? "5173" : "3000";
|
|
3691
|
+
|
|
3692
|
+
console.log("APP_PLAN_OK='1'");
|
|
3693
|
+
emit("APP_PM", pm);
|
|
3694
|
+
emit("APP_SCRIPT", picked.name);
|
|
3695
|
+
emit("APP_FRAMEWORK", framework);
|
|
3696
|
+
emit("APP_PORT", portFromCommand || defaultPort);
|
|
3697
|
+
emit("APP_PLAN_REASON", "selected " + picked.name + " script for " + framework + " app startup");
|
|
3698
|
+
NODE
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
ensure_package_manager() {
|
|
3702
|
+
local pm="$1"
|
|
3703
|
+
mkdir -p "$ROOT_DIR/npm-global"
|
|
3704
|
+
export NPM_CONFIG_PREFIX="$ROOT_DIR/npm-global"
|
|
3705
|
+
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
|
3706
|
+
if command -v "$pm" >/dev/null 2>&1; then
|
|
3707
|
+
return 0
|
|
3708
|
+
fi
|
|
3709
|
+
if command -v corepack >/dev/null 2>&1; then
|
|
3710
|
+
corepack enable >/dev/null 2>&1 || true
|
|
3711
|
+
if [[ "$pm" == "pnpm" ]]; then
|
|
3712
|
+
corepack prepare pnpm@latest --activate >/dev/null 2>&1 || true
|
|
3713
|
+
elif [[ "$pm" == "yarn" ]]; then
|
|
3714
|
+
corepack prepare yarn@stable --activate >/dev/null 2>&1 || true
|
|
3715
|
+
fi
|
|
3716
|
+
fi
|
|
3717
|
+
if command -v "$pm" >/dev/null 2>&1; then
|
|
3718
|
+
return 0
|
|
3719
|
+
fi
|
|
3720
|
+
case "$pm" in
|
|
3721
|
+
pnpm) npm i -g pnpm --no-audit --no-fund --prefix "$NPM_CONFIG_PREFIX" ;;
|
|
3722
|
+
yarn) npm i -g yarn --no-audit --no-fund --prefix "$NPM_CONFIG_PREFIX" ;;
|
|
3723
|
+
bun) npm i -g bun --no-audit --no-fund --prefix "$NPM_CONFIG_PREFIX" ;;
|
|
3724
|
+
npm) command -v npm >/dev/null 2>&1 ;;
|
|
3725
|
+
*) return 1 ;;
|
|
3726
|
+
esac
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
install_project_dependencies() {
|
|
3730
|
+
local pm="$1"
|
|
3731
|
+
if [[ "$pm" == "pnpm" ]]; then
|
|
3732
|
+
pnpm config set verify-deps-before-run false --location project >/dev/null 2>&1 || true
|
|
3733
|
+
pnpm config set dangerously-allow-all-builds true --location project >/dev/null 2>&1 || true
|
|
3734
|
+
fi
|
|
3735
|
+
if [[ -d node_modules ]]; then
|
|
3736
|
+
echo "dependencies=present"
|
|
3737
|
+
return 0
|
|
3738
|
+
fi
|
|
3739
|
+
echo "dependencies=installing manager=$pm"
|
|
3740
|
+
case "$pm" in
|
|
3741
|
+
pnpm)
|
|
3742
|
+
echo "pnpm_build_scripts=allowed disposable_e2b_lab=1"
|
|
3743
|
+
if [[ -f pnpm-lock.yaml ]]; then pnpm install --frozen-lockfile --dangerously-allow-all-builds || pnpm install --dangerously-allow-all-builds; else pnpm install --dangerously-allow-all-builds; fi
|
|
3744
|
+
;;
|
|
3745
|
+
yarn)
|
|
3746
|
+
if [[ -f yarn.lock ]]; then yarn install --frozen-lockfile --ignore-scripts || yarn install --ignore-scripts; else yarn install --ignore-scripts; fi
|
|
3747
|
+
;;
|
|
3748
|
+
bun)
|
|
3749
|
+
bun install
|
|
3750
|
+
;;
|
|
3751
|
+
npm)
|
|
3752
|
+
if [[ -f package-lock.json || -f npm-shrinkwrap.json ]]; then npm ci --ignore-scripts --no-audit --no-fund || npm install --ignore-scripts --no-audit --no-fund; else npm install --ignore-scripts --no-audit --no-fund; fi
|
|
3753
|
+
;;
|
|
3754
|
+
*) return 1 ;;
|
|
3755
|
+
esac
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
install_humanish_cli() {
|
|
3759
|
+
echo
|
|
3760
|
+
echo "== installing humanish =="
|
|
3761
|
+
local plan_output
|
|
3762
|
+
plan_output="$(detect_app_plan)"
|
|
3763
|
+
eval "$plan_output"
|
|
3764
|
+
local pm="\${APP_PM:-npm}"
|
|
3765
|
+
if ! ensure_package_manager "$pm"; then
|
|
3766
|
+
echo "humanish_install=blocked package_manager=$pm"
|
|
3767
|
+
return 1
|
|
3768
|
+
fi
|
|
3769
|
+
local spec="humanish"
|
|
3770
|
+
if [[ -n "$REMOTE_PACKAGE" && -f "$REMOTE_PACKAGE" ]]; then
|
|
3771
|
+
spec="$REMOTE_PACKAGE"
|
|
3772
|
+
fi
|
|
3773
|
+
|
|
3774
|
+
case "$pm" in
|
|
3775
|
+
pnpm)
|
|
3776
|
+
if pnpm root -w >/dev/null 2>&1; then
|
|
3777
|
+
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false npm_config_verify_deps_before_run=false pnpm add --save-dev --workspace-root "$spec" --ignore-scripts
|
|
3778
|
+
else
|
|
3779
|
+
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false npm_config_verify_deps_before_run=false pnpm add --save-dev "$spec" --ignore-scripts
|
|
3780
|
+
fi
|
|
3781
|
+
;;
|
|
3782
|
+
yarn)
|
|
3783
|
+
YARN_ENABLE_SCRIPTS=0 yarn add -D "$spec"
|
|
3784
|
+
;;
|
|
3785
|
+
bun)
|
|
3786
|
+
bun add -d "$spec"
|
|
3787
|
+
;;
|
|
3788
|
+
npm)
|
|
3789
|
+
npm i -D "$spec" --ignore-scripts --no-audit --no-fund
|
|
3790
|
+
;;
|
|
3791
|
+
*)
|
|
3792
|
+
return 1
|
|
3793
|
+
;;
|
|
3794
|
+
esac
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
apply_host_actor_plan() {
|
|
3798
|
+
if [[ "\${HUMANISH_OSS_META_HOST_CODEX_ACTOR:-0}" != "1" ]]; then
|
|
3799
|
+
return 0
|
|
3800
|
+
fi
|
|
3801
|
+
|
|
3802
|
+
echo
|
|
3803
|
+
echo "== host codex actor plan =="
|
|
3804
|
+
if [[ ! -f "$HOST_ACTOR_PLAN" ]]; then
|
|
3805
|
+
ACTOR_STATUS=blocked
|
|
3806
|
+
echo "host_actor_plan=missing"
|
|
3807
|
+
return 1
|
|
3808
|
+
fi
|
|
3809
|
+
|
|
3810
|
+
node - "$HOST_ACTOR_PLAN" <<'NODE'
|
|
3811
|
+
const fs = require("node:fs");
|
|
3812
|
+
const path = require("node:path");
|
|
3813
|
+
const [planPath] = process.argv.slice(2);
|
|
3814
|
+
const plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
3815
|
+
if (plan.schema !== "humanish.oss-host-actor-plan.v1" || plan.status !== "passed") {
|
|
3816
|
+
throw new Error("host actor plan is not passed");
|
|
3817
|
+
}
|
|
3818
|
+
const clean = (value, fallback) => String(value || fallback)
|
|
3819
|
+
.replace(/sk-[A-Za-z0-9_-]{20,}/g, "[redacted-openai-key]")
|
|
3820
|
+
.replace(/e2b_[A-Za-z0-9_-]{12,}/g, "[redacted-e2b-key]")
|
|
3821
|
+
.replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
3822
|
+
.replace(/github_pat_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
3823
|
+
.replace(/\\s+/g, " ")
|
|
3824
|
+
.trim()
|
|
3825
|
+
.slice(0, 300);
|
|
3826
|
+
const token = (value, fallback) => clean(value, fallback).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || fallback;
|
|
3827
|
+
const yamlScalar = (value) => clean(value, "").replace(/"/g, "\\\\\"");
|
|
3828
|
+
const yamlList = (values) => (Array.isArray(values) && values.length ? values : ["public_safe"]).map((value) => " - " + yamlScalar(value)).join("\\n");
|
|
3829
|
+
fs.mkdirSync("humanish/personas", { recursive: true });
|
|
3830
|
+
fs.mkdirSync("humanish/scenarios", { recursive: true });
|
|
3831
|
+
const personas = Array.isArray(plan.personas) ? plan.personas.slice(0, 2) : [];
|
|
3832
|
+
const scenarios = Array.isArray(plan.scenarios) ? plan.scenarios.slice(0, 2) : [];
|
|
3833
|
+
if (!personas.length || !scenarios.length) {
|
|
3834
|
+
throw new Error("host actor plan lacks personas or scenarios");
|
|
3835
|
+
}
|
|
3836
|
+
for (const persona of personas) {
|
|
3837
|
+
const id = token(persona.id, "host-codex-persona");
|
|
3838
|
+
fs.writeFileSync(path.join("humanish/personas", id + ".yaml"), [
|
|
3839
|
+
"schema: humanish.persona.v1",
|
|
3840
|
+
"id: " + id,
|
|
3841
|
+
"name: " + yamlScalar(persona.name || "Host Codex Persona"),
|
|
3842
|
+
"summary: " + yamlScalar(persona.intent || "Public-safe host Codex actor persona."),
|
|
3843
|
+
"traits:",
|
|
3844
|
+
yamlList(persona.traits),
|
|
3845
|
+
"constraints:",
|
|
3846
|
+
" - Do not use real personal data.",
|
|
3847
|
+
" - Do not use production accounts.",
|
|
3848
|
+
" - Treat all credentials as env var names only.",
|
|
3849
|
+
" - Source: local Codex host actor plan applied inside disposable E2B."
|
|
3850
|
+
].join("\\n") + "\\n");
|
|
3851
|
+
}
|
|
3852
|
+
for (const scenario of scenarios) {
|
|
3853
|
+
const id = token(scenario.id, "host-codex-scenario");
|
|
3854
|
+
const personaId = token(personas[0].id, "host-codex-persona");
|
|
3855
|
+
fs.writeFileSync(path.join("humanish/scenarios", id + ".yaml"), [
|
|
3856
|
+
"schema: humanish.scenario.v1",
|
|
3857
|
+
"id: " + id,
|
|
3858
|
+
"title: " + yamlScalar(scenario.title || "Host Codex Scenario"),
|
|
3859
|
+
"persona: " + personaId,
|
|
3860
|
+
"goal: " + yamlScalar(scenario.goal || "Exercise the app through a public-safe browser flow."),
|
|
3861
|
+
"mode: browser",
|
|
3862
|
+
"steps:",
|
|
3863
|
+
...(Array.isArray(scenario.steps) && scenario.steps.length ? scenario.steps : ["Open the app", "Reach a meaningful state"]).slice(0, 8).map((step, index) => [
|
|
3864
|
+
" - name: Step " + String(index + 1),
|
|
3865
|
+
" expectation: " + yamlScalar(step)
|
|
3866
|
+
].join("\\n"))
|
|
3867
|
+
].join("\\n") + "\\n");
|
|
3868
|
+
}
|
|
3869
|
+
console.log("host_actor_plan=applied personas=" + personas.length + " scenarios=" + scenarios.length);
|
|
3870
|
+
console.log("host_actor_recommended_proof=" + clean(plan.recommendedProof, "humanish run --app-url <loopback-url> --sims 2"));
|
|
3871
|
+
NODE
|
|
3872
|
+
local apply_exit=$?
|
|
3873
|
+
if [[ "$apply_exit" -eq 0 ]]; then
|
|
3874
|
+
ACTOR_STATUS=blocked
|
|
3875
|
+
echo "actor_status=$ACTOR_STATUS source=host-codex-plan reason=remote-actor-not-run"
|
|
3876
|
+
else
|
|
3877
|
+
ACTOR_STATUS=failed
|
|
3878
|
+
echo "actor_status=$ACTOR_STATUS source=host-codex-plan exit=$apply_exit"
|
|
3879
|
+
fi
|
|
3880
|
+
return "$apply_exit"
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
start_codex_app_server_actor_attempt() {
|
|
3884
|
+
echo "Codex app-server mode requested."
|
|
3885
|
+
mkdir -p "$CODEX_APP_SERVER_ROOT_PATH"
|
|
3886
|
+
cat > "$CODEX_APP_SERVER_PROMPT_PATH" <<'PROMPT'
|
|
3887
|
+
You are a Humanish meta-lab actor running through Codex app-server in a disposable authorized repo clone.
|
|
3888
|
+
|
|
3889
|
+
Goal: make Humanish useful for this app, then run at least one meaningful public-safe user-test proof.
|
|
3890
|
+
|
|
3891
|
+
Work requirements:
|
|
3892
|
+
- Inspect package.json, README, routes/components where lightweight, and the running app URL from APP_URL.
|
|
3893
|
+
- Improve Humanish setup beyond ceremonial init: app-aware coverage map/matrix, at least two app-specific synthetic personas, and at least two meaningful browser scenarios covering desktop/mobile or happy/friction states.
|
|
3894
|
+
- Do not leave only starter persona/scenario filenames, ids, or names. Write app-specific persona and scenario files with app-specific ids, names, goals, selectors, and expectations.
|
|
3895
|
+
- Use committed humanish/ source files for personas/scenarios. Keep runtime output under ignored .humanish/.
|
|
3896
|
+
- Run at least one meaningful Humanish proof against the running app URL when available: npx --no-install humanish run --app-url "$APP_URL" --sims 2.
|
|
3897
|
+
- Render or refresh the nested Observer with npx --no-install humanish watch --run latest --detach --no-open when feasible.
|
|
3898
|
+
- Finish with concise public-safe feedback: what user journey was tested, what evidence path proves it, and one useful product or Humanish harness improvement.
|
|
3899
|
+
|
|
3900
|
+
Safety:
|
|
3901
|
+
- Do not print secrets, environment values, raw private source, or private screenshots.
|
|
3902
|
+
- Do not commit, push, file issues, or mutate remotes.
|
|
3903
|
+
- Do not run provider-spend-heavy commands beyond the bounded Humanish/Codex proof.
|
|
3904
|
+
PROMPT
|
|
3905
|
+
|
|
3906
|
+
local timeout_ms="\${HUMANISH_OSS_META_ACTOR_TIMEOUT_MS:-480000}"
|
|
3907
|
+
local command
|
|
3908
|
+
command="APP_URL=$(printf '%q' "$APP_URL") HUMANISH_PRIVATE_CODEX_API_KEY=$(printf '%q' "$HUMANISH_PRIVATE_CODEX_API_KEY") HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN=$(printf '%q' "$HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN") npx --no-install humanish codex app-server --cwd $(printf '%q' "$APP_DIR") --run-root $(printf '%q' "$CODEX_APP_SERVER_ROOT_PATH") --state-file $(printf '%q' "$CODEX_APP_SERVER_STATE_PATH") --prompt-file $(printf '%q' "$CODEX_APP_SERVER_PROMPT_PATH") --timeout-ms $(printf '%q' "$timeout_ms") --port $(printf '%q' "$CODEX_APP_SERVER_PORT") --sandbox danger-full-access --actor-command 'npx -y @openai/codex@latest app-server --listen stdio://' --keep-open"
|
|
3909
|
+
nohup bash -lc "$command" > "$CODEX_APP_SERVER_LOG_PATH" 2>&1 &
|
|
3910
|
+
ACTOR_PID=$!
|
|
3911
|
+
ACTOR_STATUS=running
|
|
3912
|
+
CODEX_APP_SERVER_UI_URL="http://127.0.0.1:$CODEX_APP_SERVER_PORT/"
|
|
3913
|
+
echo "actor_status=$ACTOR_STATUS source=codex-app-server pid=$ACTOR_PID log=$CODEX_APP_SERVER_LOG_PATH state=$CODEX_APP_SERVER_STATE_PATH"
|
|
3914
|
+
echo "codex_app_server_trace=codex-app-server/summary.json"
|
|
3915
|
+
echo "codex_app_server_events=codex-app-server/events.ndjson"
|
|
3916
|
+
echo "codex_app_server_transcript=codex-app-server/transcript.txt"
|
|
3917
|
+
wait_for_http "$CODEX_APP_SERVER_UI_URL" 60000 500 || true
|
|
3918
|
+
open_codex_app_server_client || true
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
write_app_specific_browser_scenario() {
|
|
3922
|
+
if [[ "$APP_STATUS" != "running" || -z "$APP_URL" ]]; then
|
|
3923
|
+
return 0
|
|
3924
|
+
fi
|
|
3925
|
+
|
|
3926
|
+
node - "$APP_DIR" "$APP_URL" <<'NODE' || true
|
|
3927
|
+
const fs = require("node:fs");
|
|
3928
|
+
const path = require("node:path");
|
|
3929
|
+
const [appDir, appUrl] = process.argv.slice(2);
|
|
3930
|
+
const clean = (value, fallback = "") => String(value || fallback)
|
|
3931
|
+
.replace(/sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g, "[redacted-openai-key]")
|
|
3932
|
+
.replace(/e2b_[A-Za-z0-9_-]{12,}/g, "[redacted-e2b-key]")
|
|
3933
|
+
.replace(/(?:gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,})/g, "[redacted-github-token]")
|
|
3934
|
+
.replace(/https?:\\/\\/[^\\s]+/g, "[redacted-url]")
|
|
3935
|
+
.replace(/\\s+/g, " ")
|
|
3936
|
+
.trim()
|
|
3937
|
+
.slice(0, 160);
|
|
3938
|
+
const yamlScalar = (value) => clean(value, "").replace(/'/g, "''");
|
|
3939
|
+
const token = (value, fallback) => clean(value, fallback).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || fallback;
|
|
3940
|
+
const visibleTextFromHtml = (html) => clean(html
|
|
3941
|
+
.replace(/<script[\\s\\S]*?<\\/script>/gi, " ")
|
|
3942
|
+
.replace(/<style[\\s\\S]*?<\\/style>/gi, " ")
|
|
3943
|
+
.replace(/<[^>]+>/g, " "), "");
|
|
3944
|
+
const titleFromHtml = (html) => {
|
|
3945
|
+
const match = html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);
|
|
3946
|
+
return clean(match ? match[1] : "", "");
|
|
3947
|
+
};
|
|
3948
|
+
(async () => {
|
|
3949
|
+
try {
|
|
3950
|
+
const response = await fetch(appUrl);
|
|
3951
|
+
const html = await response.text();
|
|
3952
|
+
const title = titleFromHtml(html);
|
|
3953
|
+
const bodyText = visibleTextFromHtml(html);
|
|
3954
|
+
const expectedText = clean(bodyText.split(/[.!?\\n]/).find((part) => clean(part, "").length >= 4) || title, "");
|
|
3955
|
+
const scenarioId = token(title || "app-surface-browser", "app-surface-browser");
|
|
3956
|
+
const scenarioPath = path.join(appDir, "humanish", "scenarios", "app-surface-browser.yaml");
|
|
3957
|
+
fs.mkdirSync(path.dirname(scenarioPath), { recursive: true });
|
|
3958
|
+
const lines = [
|
|
3959
|
+
"schema: humanish.scenario.v1",
|
|
3960
|
+
"id: " + scenarioId,
|
|
3961
|
+
"title: App surface browser proof",
|
|
3962
|
+
"persona: synthetic-new-user",
|
|
3963
|
+
"goal: Prove the running app exposes a public-safe browser surface for synthetic users.",
|
|
3964
|
+
"mode: browser",
|
|
3965
|
+
"browser:",
|
|
3966
|
+
" startPath: /",
|
|
3967
|
+
" steps:",
|
|
3968
|
+
" - id: app-surface-load",
|
|
3969
|
+
" label: Load the running app surface",
|
|
3970
|
+
" action: goto",
|
|
3971
|
+
" path: /",
|
|
3972
|
+
" expect:",
|
|
3973
|
+
" selectorVisible: body",
|
|
3974
|
+
...(expectedText ? [
|
|
3975
|
+
" text: '" + yamlScalar(expectedText) + "'"
|
|
3976
|
+
] : []),
|
|
3977
|
+
" - id: app-surface-ready",
|
|
3978
|
+
" label: Confirm the browser can observe the app body",
|
|
3979
|
+
" action: waitForSelector",
|
|
3980
|
+
" selector: body"
|
|
3981
|
+
];
|
|
3982
|
+
fs.writeFileSync(scenarioPath, lines.join("\\n") + "\\n");
|
|
3983
|
+
console.log("browser_scenario=authored path=humanish/scenarios/app-surface-browser.yaml expected_text=" + (expectedText ? "present" : "absent"));
|
|
3984
|
+
} catch (error) {
|
|
3985
|
+
console.log("browser_scenario=skipped reason=" + clean(error && error.message ? error.message : error, "unknown"));
|
|
3986
|
+
}
|
|
3987
|
+
})();
|
|
3988
|
+
NODE
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
start_target_app_surface() {
|
|
3992
|
+
echo
|
|
3993
|
+
echo "== target app surface =="
|
|
3994
|
+
local plan_output
|
|
3995
|
+
plan_output="$(detect_app_plan)"
|
|
3996
|
+
eval "$plan_output"
|
|
3997
|
+
if [[ "\${APP_PLAN_OK:-0}" != "1" ]]; then
|
|
3998
|
+
APP_STATUS=missing
|
|
3999
|
+
APP_REASON="\${APP_PLAN_REASON:-no runnable app script detected}"
|
|
4000
|
+
echo "app_status=$APP_STATUS reason=$APP_REASON"
|
|
4001
|
+
return 0
|
|
4002
|
+
fi
|
|
4003
|
+
|
|
4004
|
+
if ! ensure_package_manager "$APP_PM"; then
|
|
4005
|
+
APP_STATUS=blocked
|
|
4006
|
+
APP_REASON="package manager $APP_PM is unavailable"
|
|
4007
|
+
echo "app_status=$APP_STATUS reason=$APP_REASON"
|
|
4008
|
+
return 0
|
|
4009
|
+
fi
|
|
4010
|
+
|
|
4011
|
+
if ! install_project_dependencies "$APP_PM"; then
|
|
4012
|
+
APP_STATUS=blocked
|
|
4013
|
+
APP_REASON="dependency install failed before app startup"
|
|
4014
|
+
echo "app_status=$APP_STATUS reason=$APP_REASON"
|
|
4015
|
+
return 0
|
|
4016
|
+
fi
|
|
4017
|
+
|
|
4018
|
+
local app_url_candidates=("http://localhost:$APP_PORT" "http://127.0.0.1:$APP_PORT" "http://[::1]:$APP_PORT")
|
|
4019
|
+
APP_URL="\${app_url_candidates[0]}"
|
|
4020
|
+
local start_command
|
|
4021
|
+
local command_prefix="HOST=0.0.0.0 PORT=$APP_PORT"
|
|
4022
|
+
local script_arg_separator="--"
|
|
4023
|
+
if [[ "$APP_PM" == "pnpm" ]]; then
|
|
4024
|
+
command_prefix="PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false npm_config_verify_deps_before_run=false $command_prefix"
|
|
4025
|
+
script_arg_separator=""
|
|
4026
|
+
fi
|
|
4027
|
+
case "$APP_FRAMEWORK" in
|
|
4028
|
+
next)
|
|
4029
|
+
start_command="$command_prefix $APP_PM run $APP_SCRIPT $script_arg_separator --hostname 0.0.0.0 --port $APP_PORT"
|
|
4030
|
+
;;
|
|
4031
|
+
vite)
|
|
4032
|
+
start_command="$command_prefix $APP_PM run $APP_SCRIPT $script_arg_separator --host 0.0.0.0 --port $APP_PORT"
|
|
4033
|
+
;;
|
|
4034
|
+
*)
|
|
4035
|
+
start_command="$command_prefix $APP_PM run $APP_SCRIPT"
|
|
4036
|
+
;;
|
|
4037
|
+
esac
|
|
4038
|
+
|
|
4039
|
+
echo "app_plan=$APP_PLAN_REASON"
|
|
4040
|
+
echo "app_url_candidates=\${app_url_candidates[*]}"
|
|
4041
|
+
echo "app_command=$start_command"
|
|
4042
|
+
nohup bash -lc "$start_command" > "$APP_LOG_PATH" 2>&1 &
|
|
4043
|
+
APP_PID=$!
|
|
4044
|
+
|
|
4045
|
+
local ready_url=""
|
|
4046
|
+
if ready_url="$(wait_for_any_http 120000 1000 "\${app_url_candidates[@]}")"; then
|
|
4047
|
+
APP_URL="$ready_url"
|
|
4048
|
+
APP_STATUS=running
|
|
4049
|
+
APP_REASON="target app responded at $APP_URL"
|
|
4050
|
+
open_browser_url "$APP_URL" app-desktop 0 0 760 520
|
|
4051
|
+
open_browser_url "$APP_URL" app-compact 0 520 430 420
|
|
4052
|
+
write_app_specific_browser_scenario || true
|
|
4053
|
+
else
|
|
4054
|
+
APP_STATUS=blocked
|
|
4055
|
+
APP_REASON="target app did not become HTTP-ready at $APP_URL"
|
|
4056
|
+
tail -n 40 "$APP_LOG_PATH" || true
|
|
4057
|
+
fi
|
|
4058
|
+
echo "app_status=$APP_STATUS reason=$APP_REASON pid=$APP_PID"
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
start_actor_attempt() {
|
|
4062
|
+
echo
|
|
4063
|
+
echo "== codex actor attempt =="
|
|
4064
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
4065
|
+
start_codex_app_server_actor_attempt
|
|
4066
|
+
return 0
|
|
4067
|
+
fi
|
|
4068
|
+
|
|
4069
|
+
local actor_script="$ROOT_DIR/actor-run.sh"
|
|
4070
|
+
cat > "$actor_script" <<ACTOR
|
|
4071
|
+
#!/usr/bin/env bash
|
|
4072
|
+
set +e
|
|
4073
|
+
APP_DIR="$APP_DIR"
|
|
4074
|
+
if [[ "\${HUMANISH_OSS_META_ACTOR_FIRST:-0}" == "1" ]]; then
|
|
4075
|
+
PROMPT='You are a Humanish meta-lab actor running in a disposable public-safe repo clone. Your goal is useful product-specific user-study setup, not ceremonial install proof. Inspect package.json, README, routes, scripts, and the running app shape. Install humanish as a dev dependency with the repo package manager if needed; package=humanish, binary=humanish. Run npx --no-install humanish init --yes if Humanish is not initialized. Replace starter humanish/coverage-map.md and humanish/coverage-matrix.md with app-aware screens, roles, states, happy paths, and at least one sad/friction path. Author at least two public-safe app-specific personas and two desktop/mobile browser scenarios; avoid generic first-run-smoke only. Start the local app if feasible. Run npx --no-install humanish run --help and verify --app-url is available. If the local binary is missing, install humanish; one-shot fallback is npx --yes --package humanish@latest humanish run --help. If the app is running locally, run npx --no-install humanish run --app-url <loopback-url> --sims 2; do not use humanish watch --sims as app behavior proof. After run --app-url, render or open the nested Humanish Observer with npx --no-install humanish watch --run latest --detach --no-open --json if feasible. Final summary must include personas/scenarios created, product journeys covered, one observed friction/improvement or none observed, and evidence paths. Do not stop at install/init proof. Do not wait on long-running watchers after rendering proof. Do not print secrets, do not commit, do not push, and do not file issues.'
|
|
4076
|
+
else
|
|
4077
|
+
PROMPT='You are a Humanish meta-lab actor. Inspect this repo, inspect the running app and Humanish artifacts already generated here, and draft the best next public-safe personas and desktop/mobile browser scenarios for human users of this app. Once the draft is written, exit successfully. Do not wait on long-running watchers. Do not print secrets, do not commit, do not push, and do not file issues.'
|
|
4078
|
+
fi
|
|
4079
|
+
printf -v app_dir_q '%q' "\\$APP_DIR"
|
|
4080
|
+
printf -v prompt_q '%q' "\\$PROMPT"
|
|
4081
|
+
printf -v actor_message_q '%q' "$ACTOR_LAST_MESSAGE_PATH"
|
|
4082
|
+
ACTOR_MODEL="\${HUMANISH_OSS_META_ACTOR_MODEL:-gpt-5.4-mini}"
|
|
4083
|
+
ACTOR_TIMEOUT_MS="\${HUMANISH_OSS_META_ACTOR_TIMEOUT_MS:-480000}"
|
|
4084
|
+
ACTOR_TIMEOUT_SECONDS=\\$(( (ACTOR_TIMEOUT_MS + 999) / 1000 ))
|
|
4085
|
+
printf -v actor_model_q '%q' "\\$ACTOR_MODEL"
|
|
4086
|
+
CODEX_COMMAND="npx -y @openai/codex@latest exec --ephemeral --ignore-user-config --skip-git-repo-check -m \\$actor_model_q -C \\$app_dir_q --dangerously-bypass-approvals-and-sandbox --output-last-message \\$actor_message_q \\$prompt_q"
|
|
4087
|
+
if [[ -n "\${HUMANISH_PRIVATE_CODEX_API_KEY:-}" && -n "\${HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN:-}" ]]; then
|
|
4088
|
+
CODEX_API_KEY="\\$HUMANISH_PRIVATE_CODEX_API_KEY" CODEX_ACCESS_TOKEN="\\$HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN" timeout "\\$ACTOR_TIMEOUT_SECONDS" bash -lc "\\$CODEX_COMMAND"
|
|
4089
|
+
elif [[ -n "\${HUMANISH_PRIVATE_CODEX_API_KEY:-}" ]]; then
|
|
4090
|
+
CODEX_API_KEY="\\$HUMANISH_PRIVATE_CODEX_API_KEY" timeout "\\$ACTOR_TIMEOUT_SECONDS" bash -lc "\\$CODEX_COMMAND"
|
|
4091
|
+
elif [[ -n "\${HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN:-}" ]]; then
|
|
4092
|
+
CODEX_ACCESS_TOKEN="\\$HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN" timeout "\\$ACTOR_TIMEOUT_SECONDS" bash -lc "\\$CODEX_COMMAND"
|
|
4093
|
+
else
|
|
4094
|
+
timeout "\\$ACTOR_TIMEOUT_SECONDS" bash -lc "\\$CODEX_COMMAND"
|
|
4095
|
+
fi
|
|
4096
|
+
code=\\$?
|
|
4097
|
+
echo "actor_exit=\\$code"
|
|
4098
|
+
exit "\\$code"
|
|
4099
|
+
ACTOR
|
|
4100
|
+
chmod +x "$actor_script"
|
|
4101
|
+
HUMANISH_PRIVATE_CODEX_API_KEY="$HUMANISH_PRIVATE_CODEX_API_KEY" HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN="$HUMANISH_PRIVATE_CODEX_ACCESS_TOKEN" nohup bash "$actor_script" > "$ACTOR_LOG_PATH" 2>&1 &
|
|
4102
|
+
ACTOR_PID=$!
|
|
4103
|
+
ACTOR_STATUS=running
|
|
4104
|
+
echo "actor_status=$ACTOR_STATUS pid=$ACTOR_PID log=$ACTOR_LOG_PATH"
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
wait_for_actor_attempt_if_required() {
|
|
4108
|
+
if [[ "\${HUMANISH_OSS_META_REQUIRE_ACTOR:-0}" != "1" && "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" != "1" ]]; then
|
|
4109
|
+
return 0
|
|
4110
|
+
fi
|
|
4111
|
+
|
|
4112
|
+
echo
|
|
4113
|
+
echo "== required codex actor readback =="
|
|
4114
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
4115
|
+
local timeout_ms="\${HUMANISH_OSS_META_ACTOR_TIMEOUT_MS:-480000}"
|
|
4116
|
+
local started_ms
|
|
4117
|
+
started_ms="$(date +%s%3N)"
|
|
4118
|
+
local next_heartbeat_ms=$((started_ms + 15000))
|
|
4119
|
+
while true; do
|
|
4120
|
+
local status_line
|
|
4121
|
+
status_line="$(node - "$CODEX_APP_SERVER_STATE_PATH" <<'NODE' 2>/dev/null || true
|
|
4122
|
+
const fs = require("node:fs");
|
|
4123
|
+
const [statePath] = process.argv.slice(2);
|
|
4124
|
+
if (!fs.existsSync(statePath)) process.exit(2);
|
|
4125
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
4126
|
+
console.log(String(state.status || "unknown"));
|
|
4127
|
+
NODE
|
|
4128
|
+
)"
|
|
4129
|
+
if [[ "$status_line" != "starting" && "$status_line" != "running" && "$status_line" != "unknown" && -n "$status_line" ]]; then
|
|
4130
|
+
ACTOR_STATUS="$status_line"
|
|
4131
|
+
break
|
|
4132
|
+
fi
|
|
4133
|
+
local now_ms
|
|
4134
|
+
now_ms="$(date +%s%3N)"
|
|
4135
|
+
if [[ "$now_ms" -ge "$next_heartbeat_ms" ]]; then
|
|
4136
|
+
echo "actor_status=running source=codex-app-server elapsed_ms=$((now_ms - started_ms)) state=$CODEX_APP_SERVER_STATE_PATH log=$CODEX_APP_SERVER_LOG_PATH"
|
|
4137
|
+
echo "actor_log_tail_begin"
|
|
4138
|
+
tail -n 20 "$CODEX_APP_SERVER_LOG_PATH" || true
|
|
4139
|
+
echo "actor_log_tail_end"
|
|
4140
|
+
next_heartbeat_ms=$((now_ms + 15000))
|
|
4141
|
+
fi
|
|
4142
|
+
if [[ $((now_ms - started_ms)) -ge "$timeout_ms" ]]; then
|
|
4143
|
+
ACTOR_STATUS=timed_out
|
|
4144
|
+
echo "actor_status=$ACTOR_STATUS timeout_ms=$timeout_ms state=$CODEX_APP_SERVER_STATE_PATH log=$CODEX_APP_SERVER_LOG_PATH"
|
|
4145
|
+
return 1
|
|
4146
|
+
fi
|
|
4147
|
+
sleep 1
|
|
4148
|
+
done
|
|
4149
|
+
echo "actor_status=$ACTOR_STATUS source=codex-app-server state=$CODEX_APP_SERVER_STATE_PATH log=$CODEX_APP_SERVER_LOG_PATH"
|
|
4150
|
+
echo "actor_log_tail_begin"
|
|
4151
|
+
tail -n 80 "$CODEX_APP_SERVER_LOG_PATH" || true
|
|
4152
|
+
echo "actor_log_tail_end"
|
|
4153
|
+
if [[ -f "$CODEX_APP_SERVER_ROOT_PATH/codex-app-server/summary.json" ]]; then
|
|
4154
|
+
node - "$CODEX_APP_SERVER_ROOT_PATH/codex-app-server/summary.json" "$ACTOR_LAST_MESSAGE_PATH" <<'NODE' || true
|
|
4155
|
+
const fs = require("node:fs");
|
|
4156
|
+
const [summaryPath, outPath] = process.argv.slice(2);
|
|
4157
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
|
|
4158
|
+
const messages = Array.isArray(summary.messages) ? summary.messages : [];
|
|
4159
|
+
const text = messages
|
|
4160
|
+
.slice(-4)
|
|
4161
|
+
.map((message) => typeof message?.text === "string" ? message.text : "")
|
|
4162
|
+
.filter(Boolean)
|
|
4163
|
+
.join("\\n\\n")
|
|
4164
|
+
.slice(-12000);
|
|
4165
|
+
fs.writeFileSync(outPath, text || "No final Codex app-server agent message captured.\\n");
|
|
4166
|
+
NODE
|
|
4167
|
+
elif [[ -f "$CODEX_APP_SERVER_ROOT_PATH/codex-app-server/transcript.txt" ]]; then
|
|
4168
|
+
tail -n 80 "$CODEX_APP_SERVER_ROOT_PATH/codex-app-server/transcript.txt" > "$ACTOR_LAST_MESSAGE_PATH" || true
|
|
4169
|
+
fi
|
|
4170
|
+
[[ "$ACTOR_STATUS" == "passed" ]]
|
|
4171
|
+
return $?
|
|
4172
|
+
fi
|
|
4173
|
+
|
|
4174
|
+
if [[ -z "$ACTOR_PID" ]]; then
|
|
4175
|
+
ACTOR_STATUS=blocked
|
|
4176
|
+
echo "actor_status=$ACTOR_STATUS reason=no actor process was started"
|
|
4177
|
+
return 1
|
|
4178
|
+
fi
|
|
4179
|
+
|
|
4180
|
+
local timeout_ms="\${HUMANISH_OSS_META_ACTOR_TIMEOUT_MS:-480000}"
|
|
4181
|
+
local started_ms
|
|
4182
|
+
started_ms="$(date +%s%3N)"
|
|
4183
|
+
while kill -0 "$ACTOR_PID" >/dev/null 2>&1; do
|
|
4184
|
+
local now_ms
|
|
4185
|
+
now_ms="$(date +%s%3N)"
|
|
4186
|
+
if [[ $((now_ms - started_ms)) -ge "$timeout_ms" ]]; then
|
|
4187
|
+
ACTOR_STATUS=timed_out
|
|
4188
|
+
kill "$ACTOR_PID" >/dev/null 2>&1 || true
|
|
4189
|
+
echo "actor_status=$ACTOR_STATUS timeout_ms=$timeout_ms log=$ACTOR_LOG_PATH"
|
|
4190
|
+
return 1
|
|
4191
|
+
fi
|
|
4192
|
+
sleep 1
|
|
4193
|
+
done
|
|
4194
|
+
|
|
4195
|
+
local actor_exit=0
|
|
4196
|
+
wait "$ACTOR_PID" || actor_exit=$?
|
|
4197
|
+
if [[ "$actor_exit" -eq 0 ]]; then
|
|
4198
|
+
ACTOR_STATUS=passed
|
|
4199
|
+
else
|
|
4200
|
+
ACTOR_STATUS=failed
|
|
4201
|
+
fi
|
|
4202
|
+
echo "actor_status=$ACTOR_STATUS exit=$actor_exit log=$ACTOR_LOG_PATH"
|
|
4203
|
+
echo "actor_log_tail_begin"
|
|
4204
|
+
tail -n 80 "$ACTOR_LOG_PATH" || true
|
|
4205
|
+
echo "actor_log_tail_end"
|
|
4206
|
+
[[ "$actor_exit" -eq 0 ]]
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
rm -rf "$APP_DIR"
|
|
4210
|
+
ASKPASS="$ROOT_DIR/git-askpass.sh"
|
|
4211
|
+
cat > "$ASKPASS" <<'ASKPASS'
|
|
4212
|
+
#!/usr/bin/env bash
|
|
4213
|
+
case "$1" in
|
|
4214
|
+
*Username*) echo "x-access-token" ;;
|
|
4215
|
+
*Password*) echo "\${HUMANISH_GITHUB_TOKEN_RUNTIME:-}" ;;
|
|
4216
|
+
*) echo "" ;;
|
|
4217
|
+
esac
|
|
4218
|
+
ASKPASS
|
|
4219
|
+
chmod 700 "$ASKPASS"
|
|
4220
|
+
clone_repo() {
|
|
4221
|
+
local repo_url=${shellQuote(repoUrl)}
|
|
4222
|
+
if GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_ASKPASS=false SSH_ASKPASS=false GIT_TERMINAL_PROMPT=0 git -c credential.helper= clone --depth=1 "$repo_url" "$APP_DIR"; then
|
|
4223
|
+
echo "clone_auth=anonymous"
|
|
4224
|
+
return 0
|
|
4225
|
+
fi
|
|
4226
|
+
echo "clone_auth=anonymous_failed retry=token_clone"
|
|
4227
|
+
rm -rf "$APP_DIR"
|
|
4228
|
+
|
|
4229
|
+
if [[ -n "$HUMANISH_PRIVATE_GITHUB_TOKEN" ]]; then
|
|
4230
|
+
if GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 HUMANISH_GITHUB_TOKEN_RUNTIME="$HUMANISH_PRIVATE_GITHUB_TOKEN" git -c credential.helper= clone --depth=1 "$repo_url" "$APP_DIR"; then
|
|
4231
|
+
echo "clone_auth=token"
|
|
4232
|
+
return 0
|
|
4233
|
+
fi
|
|
4234
|
+
echo "clone_auth=token_failed"
|
|
4235
|
+
rm -rf "$APP_DIR"
|
|
4236
|
+
fi
|
|
4237
|
+
|
|
4238
|
+
return 1
|
|
4239
|
+
}
|
|
4240
|
+
clone_repo
|
|
4241
|
+
cd "$APP_DIR"
|
|
4242
|
+
echo
|
|
4243
|
+
echo "== repo fingerprint =="
|
|
4244
|
+
git rev-parse --short HEAD || true
|
|
4245
|
+
node --version || true
|
|
4246
|
+
npm --version || true
|
|
4247
|
+
|
|
4248
|
+
echo
|
|
4249
|
+
echo "== humanish skill discovery =="
|
|
4250
|
+
if npx -y skills add danielgwilson/humanish --skill humanish --list >/tmp/humanish-skill-list.txt 2>&1; then
|
|
4251
|
+
echo "skill_discovery=listed"
|
|
4252
|
+
elif npx -y skills add danielgwilson/humanish --skill humanish >/tmp/humanish-skill-install.txt 2>&1; then
|
|
4253
|
+
echo "skill_install=attempted"
|
|
4254
|
+
else
|
|
4255
|
+
echo "skill_install=blocked"
|
|
4256
|
+
tail -n 20 /tmp/humanish-skill-list.txt /tmp/humanish-skill-install.txt 2>/dev/null || true
|
|
4257
|
+
fi
|
|
4258
|
+
|
|
4259
|
+
echo
|
|
4260
|
+
if [[ "\${HUMANISH_OSS_META_ACTOR_FIRST:-0}" == "1" && "\${HUMANISH_OSS_META_HOST_CODEX_ACTOR:-0}" != "1" && "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" != "1" ]]; then
|
|
4261
|
+
start_actor_attempt
|
|
4262
|
+
wait_for_actor_attempt_if_required || true
|
|
4263
|
+
fi
|
|
4264
|
+
|
|
4265
|
+
echo
|
|
4266
|
+
install_humanish_cli
|
|
4267
|
+
|
|
4268
|
+
echo
|
|
4269
|
+
echo "== humanish init =="
|
|
4270
|
+
npx --no-install humanish init --yes
|
|
4271
|
+
apply_host_actor_plan || true
|
|
4272
|
+
|
|
4273
|
+
start_target_app_surface
|
|
4274
|
+
|
|
4275
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" && "\${HUMANISH_OSS_META_HOST_CODEX_ACTOR:-0}" != "1" ]]; then
|
|
4276
|
+
start_actor_attempt
|
|
4277
|
+
wait_for_actor_attempt_if_required || true
|
|
4278
|
+
fi
|
|
4279
|
+
|
|
4280
|
+
echo
|
|
4281
|
+
echo "== nested humanish proof =="
|
|
4282
|
+
if [[ "$APP_STATUS" == "running" && -n "$APP_URL" ]]; then
|
|
4283
|
+
npx --no-install humanish run --app-url "$APP_URL" --sims 2 --run-id ${shellQuote(runId)}
|
|
4284
|
+
else
|
|
4285
|
+
echo "app_not_running_for_browser_proof=$APP_REASON"
|
|
4286
|
+
npx --no-install humanish run --dry-run --run-id ${shellQuote(runId)}
|
|
4287
|
+
fi
|
|
4288
|
+
if npx --no-install humanish verify --run latest; then
|
|
4289
|
+
NESTED_VERIFY_STATUS=passed
|
|
4290
|
+
else
|
|
4291
|
+
NESTED_VERIFY_STATUS=failed
|
|
4292
|
+
exit 1
|
|
4293
|
+
fi
|
|
4294
|
+
npx --no-install humanish watch --run latest --detach --no-open
|
|
4295
|
+
open_nested_observer "opening nested observer"
|
|
4296
|
+
open_codex_app_server_client
|
|
4297
|
+
arrange_lab_windows
|
|
4298
|
+
if [[ "\${HUMANISH_OSS_META_ACTOR_FIRST:-0}" != "1" && "\${HUMANISH_OSS_META_HOST_CODEX_ACTOR:-0}" != "1" && "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" != "1" ]]; then
|
|
4299
|
+
start_actor_attempt
|
|
4300
|
+
wait_for_actor_attempt_if_required || true
|
|
4301
|
+
fi
|
|
4302
|
+
|
|
4303
|
+
echo
|
|
4304
|
+
echo "== bootstrap complete =="
|
|
4305
|
+
echo "app_status=$APP_STATUS"
|
|
4306
|
+
echo "app_url=$APP_URL"
|
|
4307
|
+
echo "actor_status=$ACTOR_STATUS"
|
|
4308
|
+
echo "nested_observer=$NESTED_OBSERVER"
|
|
4309
|
+
`;
|
|
4310
|
+
}
|
|
4311
|
+
function buildRemoteLauncherScript(args) {
|
|
4312
|
+
const terminalCommand = `bash -lc ${shellQuote(`${args.bootstrapPath}; echo; echo 'Humanish bootstrap finished. Leave this terminal open for review.'; exec bash`)}`;
|
|
4313
|
+
return `#!/usr/bin/env bash
|
|
4314
|
+
set -u
|
|
4315
|
+
BOOTSTRAP=${shellQuote(args.bootstrapPath)}
|
|
4316
|
+
LOG_PATH=${shellQuote(args.logPath)}
|
|
4317
|
+
TITLE=${shellQuote(args.title)}
|
|
4318
|
+
echo "launching visible terminal for $BOOTSTRAP" >> "$LOG_PATH"
|
|
4319
|
+
if command -v xfce4-terminal >/dev/null 2>&1; then
|
|
4320
|
+
nohup xfce4-terminal --hold --title "$TITLE" --command ${shellQuote(terminalCommand)} >> "$LOG_PATH" 2>&1 &
|
|
4321
|
+
elif command -v xterm >/dev/null 2>&1; then
|
|
4322
|
+
nohup xterm -T "$TITLE" -e ${shellQuote(terminalCommand)} >> "$LOG_PATH" 2>&1 &
|
|
4323
|
+
else
|
|
4324
|
+
echo "No GUI terminal found; running bootstrap headless." >> "$LOG_PATH"
|
|
4325
|
+
nohup bash "$BOOTSTRAP" >> "$LOG_PATH" 2>&1 &
|
|
4326
|
+
fi
|
|
4327
|
+
`;
|
|
4328
|
+
}
|
|
4329
|
+
function buildRemoteFocusCommand(title) {
|
|
4330
|
+
return `TITLE=${shellQuote(title)}
|
|
4331
|
+
for attempt in $(seq 1 20); do
|
|
4332
|
+
if command -v wmctrl >/dev/null 2>&1; then
|
|
4333
|
+
wmctrl -a "$TITLE" >/dev/null 2>&1 && exit 0
|
|
4334
|
+
fi
|
|
4335
|
+
if command -v xdotool >/dev/null 2>&1; then
|
|
4336
|
+
WIN="$(xdotool search --name "$TITLE" 2>/dev/null | head -n 1 || true)"
|
|
4337
|
+
if [[ -n "$WIN" ]]; then
|
|
4338
|
+
xdotool windowactivate "$WIN" >/dev/null 2>&1 && exit 0
|
|
4339
|
+
fi
|
|
4340
|
+
fi
|
|
4341
|
+
sleep 0.25
|
|
4342
|
+
done
|
|
4343
|
+
exit 0`;
|
|
4344
|
+
}
|
|
4345
|
+
function buildRemoteScreenshotArrangeCommand(terminalTitle) {
|
|
4346
|
+
return `TERMINAL_TITLE=${shellQuote(terminalTitle ?? "")}
|
|
4347
|
+
EXPECTED_CHROME_WINDOWS=2
|
|
4348
|
+
if [[ "\${HUMANISH_OSS_META_CODEX_APP_SERVER:-0}" == "1" ]]; then
|
|
4349
|
+
EXPECTED_CHROME_WINDOWS=3
|
|
4350
|
+
fi
|
|
4351
|
+
if ! command -v xdotool >/dev/null 2>&1; then
|
|
4352
|
+
exit 0
|
|
4353
|
+
fi
|
|
4354
|
+
for attempt in $(seq 1 24); do
|
|
4355
|
+
CHROME_COUNT="$(xdotool search --onlyvisible --class google-chrome 2>/dev/null | wc -l | tr -d ' ')"
|
|
4356
|
+
OBSERVER_WINDOW="$(xdotool search --onlyvisible --name "Humanish Observer" 2>/dev/null | tail -n 1 || true)"
|
|
4357
|
+
if [[ "$CHROME_COUNT" -ge "$EXPECTED_CHROME_WINDOWS" && -n "$OBSERVER_WINDOW" ]]; then
|
|
4358
|
+
break
|
|
4359
|
+
fi
|
|
4360
|
+
sleep 0.25
|
|
4361
|
+
done
|
|
4362
|
+
OBSERVER_WINDOW="$(xdotool search --onlyvisible --name "Humanish Observer" 2>/dev/null | tail -n 1 || true)"
|
|
4363
|
+
if [[ -n "$OBSERVER_WINDOW" ]]; then
|
|
4364
|
+
xdotool windowsize "$OBSERVER_WINDOW" 680 933 >/dev/null 2>&1 || true
|
|
4365
|
+
xdotool windowmove "$OBSERVER_WINDOW" 760 27 >/dev/null 2>&1 || true
|
|
4366
|
+
fi
|
|
4367
|
+
slot=0
|
|
4368
|
+
while IFS= read -r win; do
|
|
4369
|
+
[[ -z "$win" ]] && continue
|
|
4370
|
+
if [[ -n "$OBSERVER_WINDOW" && "$win" == "$OBSERVER_WINDOW" ]]; then
|
|
4371
|
+
continue
|
|
4372
|
+
fi
|
|
4373
|
+
if [[ "$slot" -eq 0 ]]; then
|
|
4374
|
+
xdotool windowsize "$win" 760 493 >/dev/null 2>&1 || true
|
|
4375
|
+
xdotool windowmove "$win" 0 27 >/dev/null 2>&1 || true
|
|
4376
|
+
elif [[ "$slot" -eq 1 ]]; then
|
|
4377
|
+
xdotool windowsize "$win" 430 420 >/dev/null 2>&1 || true
|
|
4378
|
+
xdotool windowmove "$win" 0 520 >/dev/null 2>&1 || true
|
|
4379
|
+
else
|
|
4380
|
+
xdotool windowsize "$win" 330 420 >/dev/null 2>&1 || true
|
|
4381
|
+
xdotool windowmove "$win" 430 520 >/dev/null 2>&1 || true
|
|
4382
|
+
fi
|
|
4383
|
+
slot=$((slot + 1))
|
|
4384
|
+
done < <(xdotool search --onlyvisible --class google-chrome 2>/dev/null || true)
|
|
4385
|
+
if [[ -n "$TERMINAL_TITLE" ]]; then
|
|
4386
|
+
TERMINAL_WINDOW="$(xdotool search --onlyvisible --name "$TERMINAL_TITLE" 2>/dev/null | head -n 1 || true)"
|
|
4387
|
+
if [[ -n "$TERMINAL_WINDOW" ]]; then
|
|
4388
|
+
xdotool windowlower "$TERMINAL_WINDOW" >/dev/null 2>&1 || xdotool windowminimize "$TERMINAL_WINDOW" >/dev/null 2>&1 || true
|
|
4389
|
+
fi
|
|
4390
|
+
fi
|
|
4391
|
+
if [[ -n "$OBSERVER_WINDOW" ]]; then
|
|
4392
|
+
xdotool windowactivate "$OBSERVER_WINDOW" >/dev/null 2>&1 || true
|
|
4393
|
+
fi
|
|
4394
|
+
exit 0`;
|
|
4395
|
+
}
|
|
4396
|
+
async function runDesktopCommand(desktop, command, options) {
|
|
4397
|
+
const result = await runDesktopCommandOrThrow(() => desktop.commands.run(`bash -lc ${shellQuote(command)}`, options),
|
|
4398
|
+
// The real Sandbox throws CommandExitError on a non-zero exit, so recover the
|
|
4399
|
+
// exit code + output tail from the throw and preserve the formatted error.
|
|
4400
|
+
({ exitCode, stderrTail }) => new Error(`Remote command failed with exit code ${exitCode ?? "unknown"}.\noutput=${stderrTail}`));
|
|
4401
|
+
if (result.exitCode && result.exitCode !== 0) {
|
|
4402
|
+
throw new Error([
|
|
4403
|
+
`Remote command failed with exit code ${result.exitCode}.`,
|
|
4404
|
+
`stdout=${result.stdout ?? ""}`,
|
|
4405
|
+
`stderr=${result.stderr ?? ""}`
|
|
4406
|
+
].join("\n"));
|
|
4407
|
+
}
|
|
4408
|
+
return result;
|
|
4409
|
+
}
|
|
4410
|
+
async function packLocalHumanishPackage(cwd, runId) {
|
|
4411
|
+
const packageRoot = moduleRoot;
|
|
4412
|
+
await prepareReusableHumanishStorageDirectory(cwd, "tmp", "oss-meta", runId);
|
|
4413
|
+
const packDir = await prepareReusableHumanishStorageDirectory(cwd, "tmp", "oss-meta", runId, "package");
|
|
4414
|
+
await execFileAsync("pnpm", ["build"], {
|
|
4415
|
+
cwd: packageRoot,
|
|
4416
|
+
env: process.env,
|
|
4417
|
+
maxBuffer: 10 * 1024 * 1024
|
|
4418
|
+
});
|
|
4419
|
+
await execFileAsync("npm", ["pack", "--pack-destination", packDir], {
|
|
4420
|
+
cwd: packageRoot,
|
|
4421
|
+
env: process.env,
|
|
4422
|
+
maxBuffer: 10 * 1024 * 1024
|
|
4423
|
+
});
|
|
4424
|
+
const files = await readdir(packDir);
|
|
4425
|
+
const fileName = files.find((file) => /^humanish-.*\.tgz$/.test(file));
|
|
4426
|
+
if (!fileName) {
|
|
4427
|
+
throw new Error("npm pack did not produce a humanish tarball.");
|
|
4428
|
+
}
|
|
4429
|
+
const archivePath = path.join(packDir, fileName);
|
|
4430
|
+
const archiveStat = await stat(archivePath);
|
|
4431
|
+
return {
|
|
4432
|
+
fileName,
|
|
4433
|
+
path: archivePath,
|
|
4434
|
+
sizeBytes: archiveStat.size
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
function toArrayBuffer(buffer) {
|
|
4438
|
+
const copy = new ArrayBuffer(buffer.byteLength);
|
|
4439
|
+
new Uint8Array(copy).set(buffer);
|
|
4440
|
+
return copy;
|
|
4441
|
+
}
|
|
4442
|
+
function shellQuote(value) {
|
|
4443
|
+
if (value.length === 0) {
|
|
4444
|
+
return "''";
|
|
4445
|
+
}
|
|
4446
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
4447
|
+
}
|
|
4448
|
+
function formatLiveDesktopForResult(desktop, redactRepoNames) {
|
|
4449
|
+
return {
|
|
4450
|
+
...(desktop.completion?.actorStatus ? { actorStatus: desktop.completion.actorStatus } : {}),
|
|
4451
|
+
...(desktop.completion?.appStatus ? { appStatus: desktop.completion.appStatus } : {}),
|
|
4452
|
+
...(desktop.bootstrap ? { bootstrapStatus: desktop.bootstrap.status } : {}),
|
|
4453
|
+
...(desktop.completion ? { completionReason: desktop.completion.reason, completionStatus: desktop.completion.status } : {}),
|
|
4454
|
+
repo: desktop.repo,
|
|
4455
|
+
...(desktop.screenshot ? { screenshotPresent: true } : {}),
|
|
4456
|
+
...(!redactRepoNames && desktop.sandboxId ? { sandboxId: desktop.sandboxId } : {}),
|
|
4457
|
+
streamId: desktop.streamId,
|
|
4458
|
+
urlPresent: Boolean(desktop.url),
|
|
4459
|
+
...(desktop.completion?.visualStatus ? { visualStatus: desktop.completion.visualStatus } : {}),
|
|
4460
|
+
...(desktop.completion?.visualWindowCount === undefined ? {} : { visualWindowCount: desktop.completion.visualWindowCount })
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
function missingLiveKeys(env) {
|
|
4464
|
+
const missing = ["E2B_API_KEY"].filter((name) => !env[name]?.trim());
|
|
4465
|
+
const actorAuthRequested = remoteActorAuthRequested(env);
|
|
4466
|
+
const actorAuthPresent = Boolean(env.CODEX_API_KEY?.trim() || env.CODEX_ACCESS_TOKEN?.trim() || env.OPENAI_API_KEY?.trim());
|
|
4467
|
+
if (actorAuthRequested && !hostCodexActorRequested(env) && !actorAuthPresent) {
|
|
4468
|
+
missing.push(OSS_META_LAB_ACTOR_AUTH_PLACEHOLDER);
|
|
4469
|
+
}
|
|
4470
|
+
return missing;
|
|
4471
|
+
}
|
|
4472
|
+
function readPositiveInt(value, fallback) {
|
|
4473
|
+
if (!value || !/^\d+$/.test(value)) {
|
|
4474
|
+
return fallback;
|
|
4475
|
+
}
|
|
4476
|
+
const parsed = Number.parseInt(value, 10);
|
|
4477
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
4478
|
+
}
|
|
4479
|
+
function readNonNegativeInt(value, fallback) {
|
|
4480
|
+
if (!value || !/^\d+$/.test(value)) {
|
|
4481
|
+
return fallback;
|
|
4482
|
+
}
|
|
4483
|
+
const parsed = Number.parseInt(value, 10);
|
|
4484
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
4485
|
+
}
|
|
4486
|
+
async function wait(ms) {
|
|
4487
|
+
if (ms <= 0) {
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
4491
|
+
}
|
|
4492
|
+
function compactError(error) {
|
|
4493
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4494
|
+
return message
|
|
4495
|
+
.replace(/sk-[A-Za-z0-9_-]{20,}/g, "[redacted-openai-key]")
|
|
4496
|
+
.replace(/e2b_[A-Za-z0-9_-]{12,}/g, "[redacted-e2b-key]")
|
|
4497
|
+
.replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
4498
|
+
.replace(/github_pat_[A-Za-z0-9_]{12,}/g, "[redacted-github-token]")
|
|
4499
|
+
.replace(/https?:\/\/[^/\s]*e2b[^)\s]+/gi, "[redacted-e2b-url]")
|
|
4500
|
+
.replace(/\s+/g, " ")
|
|
4501
|
+
.slice(0, 240);
|
|
4502
|
+
}
|
|
4503
|
+
function makeMetaRunId() {
|
|
4504
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
4505
|
+
return `oss-meta-${stamp}-${randomBytes(4).toString("hex")}`;
|
|
4506
|
+
}
|
|
4507
|
+
function repoArtifactLabel(assignment) {
|
|
4508
|
+
return `repo-${String(assignment.index).padStart(2, "0")}`;
|
|
4509
|
+
}
|
|
4510
|
+
function redactAssignments(assignments, redactRepoNames) {
|
|
4511
|
+
if (!redactRepoNames) {
|
|
4512
|
+
return assignments;
|
|
4513
|
+
}
|
|
4514
|
+
return assignments.map((assignment) => ({
|
|
4515
|
+
...assignment,
|
|
4516
|
+
repo: repoArtifactLabel(assignment),
|
|
4517
|
+
scenarioId: `oss-meta-${repoArtifactLabel(assignment)}`
|
|
4518
|
+
}));
|
|
4519
|
+
}
|
|
4520
|
+
function safeArtifactToken(value) {
|
|
4521
|
+
const token = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4522
|
+
return token || "artifact";
|
|
4523
|
+
}
|
|
4524
|
+
async function writeJson(artifactRoot, relativePath, value) {
|
|
4525
|
+
await writeContainedOutputFile(artifactRoot, relativePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
4526
|
+
}
|
|
4527
|
+
// E2B desktop interfaces + the optional-peer loader now live in ./e2b-desktop-launch.js
|
|
4528
|
+
// (shared with the computer-use actor lane) and are imported at the top of this file.
|
|
4529
|
+
//# sourceMappingURL=oss-meta-lab.js.map
|