humanish 0.0.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/AGENTS.md +77 -0
  2. package/CONTRIBUTING.md +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +435 -8
  5. package/SECURITY.md +34 -0
  6. package/dist/actor-contract.d.ts +107 -0
  7. package/dist/actor-contract.js +286 -0
  8. package/dist/actor-contract.js.map +1 -0
  9. package/dist/actor-registry.d.ts +73 -0
  10. package/dist/actor-registry.js +93 -0
  11. package/dist/actor-registry.js.map +1 -0
  12. package/dist/adapter-extension.d.ts +54 -0
  13. package/dist/adapter-extension.js +214 -0
  14. package/dist/adapter-extension.js.map +1 -0
  15. package/dist/argv.d.ts +1 -0
  16. package/dist/argv.js +8 -0
  17. package/dist/argv.js.map +1 -0
  18. package/dist/artifact-reference.d.ts +22 -0
  19. package/dist/artifact-reference.js +47 -0
  20. package/dist/artifact-reference.js.map +1 -0
  21. package/dist/browser-evidence-hygiene.d.ts +7 -0
  22. package/dist/browser-evidence-hygiene.js +40 -0
  23. package/dist/browser-evidence-hygiene.js.map +1 -0
  24. package/dist/claude-agent-sdk.d.ts +94 -0
  25. package/dist/claude-agent-sdk.js +356 -0
  26. package/dist/claude-agent-sdk.js.map +1 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +5 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/codex-app-server-ui.d.ts +39 -0
  31. package/dist/codex-app-server-ui.js +327 -0
  32. package/dist/codex-app-server-ui.js.map +1 -0
  33. package/dist/codex-app-server.d.ts +141 -0
  34. package/dist/codex-app-server.js +747 -0
  35. package/dist/codex-app-server.js.map +1 -0
  36. package/dist/command-failure.d.ts +29 -0
  37. package/dist/command-failure.js +54 -0
  38. package/dist/command-failure.js.map +1 -0
  39. package/dist/computer-use-actor.d.ts +52 -0
  40. package/dist/computer-use-actor.js +48 -0
  41. package/dist/computer-use-actor.js.map +1 -0
  42. package/dist/computer-use.d.ts +232 -0
  43. package/dist/computer-use.js +615 -0
  44. package/dist/computer-use.js.map +1 -0
  45. package/dist/concurrency.d.ts +1 -0
  46. package/dist/concurrency.js +27 -0
  47. package/dist/concurrency.js.map +1 -0
  48. package/dist/concurrent-shared-world-lab.d.ts +115 -0
  49. package/dist/concurrent-shared-world-lab.js +1092 -0
  50. package/dist/concurrent-shared-world-lab.js.map +1 -0
  51. package/dist/core/git-state.d.ts +31 -0
  52. package/dist/core/git-state.js +142 -0
  53. package/dist/core/git-state.js.map +1 -0
  54. package/dist/core/index.d.ts +4 -0
  55. package/dist/core/index.js +3 -0
  56. package/dist/core/index.js.map +1 -0
  57. package/dist/core/run-primitives.d.ts +66 -0
  58. package/dist/core/run-primitives.js +120 -0
  59. package/dist/core/run-primitives.js.map +1 -0
  60. package/dist/cua-actor-lab.d.ts +620 -0
  61. package/dist/cua-actor-lab.js +2834 -0
  62. package/dist/cua-actor-lab.js.map +1 -0
  63. package/dist/device-presets.d.ts +67 -0
  64. package/dist/device-presets.js +50 -0
  65. package/dist/device-presets.js.map +1 -0
  66. package/dist/e2b-desktop-executor.d.ts +101 -0
  67. package/dist/e2b-desktop-executor.js +309 -0
  68. package/dist/e2b-desktop-executor.js.map +1 -0
  69. package/dist/e2b-desktop-launch.d.ts +144 -0
  70. package/dist/e2b-desktop-launch.js +59 -0
  71. package/dist/e2b-desktop-launch.js.map +1 -0
  72. package/dist/e2b-detached.d.ts +53 -0
  73. package/dist/e2b-detached.js +130 -0
  74. package/dist/e2b-detached.js.map +1 -0
  75. package/dist/e2b-terminal-lab.d.ts +345 -0
  76. package/dist/e2b-terminal-lab.js +1467 -0
  77. package/dist/e2b-terminal-lab.js.map +1 -0
  78. package/dist/env-file.d.ts +14 -0
  79. package/dist/env-file.js +108 -0
  80. package/dist/env-file.js.map +1 -0
  81. package/dist/feedback.d.ts +51 -0
  82. package/dist/feedback.js +313 -0
  83. package/dist/feedback.js.map +1 -0
  84. package/dist/image-evidence.d.ts +2 -0
  85. package/dist/image-evidence.js +33 -0
  86. package/dist/image-evidence.js.map +1 -0
  87. package/dist/index.d.ts +63 -0
  88. package/dist/index.js +33 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/init-templates.d.ts +12 -0
  91. package/dist/init-templates.js +369 -0
  92. package/dist/init-templates.js.map +1 -0
  93. package/dist/init.d.ts +26 -0
  94. package/dist/init.js +343 -0
  95. package/dist/init.js.map +1 -0
  96. package/dist/lab-app-runner.d.ts +78 -0
  97. package/dist/lab-app-runner.js +403 -0
  98. package/dist/lab-app-runner.js.map +1 -0
  99. package/dist/lab-config.d.ts +578 -0
  100. package/dist/lab-config.js +1751 -0
  101. package/dist/lab-config.js.map +1 -0
  102. package/dist/lab-engine.d.ts +77 -0
  103. package/dist/lab-engine.js +216 -0
  104. package/dist/lab-engine.js.map +1 -0
  105. package/dist/lab-preflight.d.ts +67 -0
  106. package/dist/lab-preflight.js +385 -0
  107. package/dist/lab-preflight.js.map +1 -0
  108. package/dist/labs.d.ts +53 -0
  109. package/dist/labs.js +176 -0
  110. package/dist/labs.js.map +1 -0
  111. package/dist/observer-assets.d.ts +2 -0
  112. package/dist/observer-assets.js +2630 -0
  113. package/dist/observer-assets.js.map +1 -0
  114. package/dist/observer-data.d.ts +63 -0
  115. package/dist/observer-data.js +145 -0
  116. package/dist/observer-data.js.map +1 -0
  117. package/dist/observer-static.d.ts +39 -0
  118. package/dist/observer-static.js +192 -0
  119. package/dist/observer-static.js.map +1 -0
  120. package/dist/observer.d.ts +46 -0
  121. package/dist/observer.js +353 -0
  122. package/dist/observer.js.map +1 -0
  123. package/dist/openai-responses-cu.d.ts +113 -0
  124. package/dist/openai-responses-cu.js +531 -0
  125. package/dist/openai-responses-cu.js.map +1 -0
  126. package/dist/oss-lab.d.ts +52 -0
  127. package/dist/oss-lab.js +299 -0
  128. package/dist/oss-lab.js.map +1 -0
  129. package/dist/oss-meta-lab-scoring.d.ts +15 -0
  130. package/dist/oss-meta-lab-scoring.js +145 -0
  131. package/dist/oss-meta-lab-scoring.js.map +1 -0
  132. package/dist/oss-meta-lab.d.ts +279 -0
  133. package/dist/oss-meta-lab.js +4889 -0
  134. package/dist/oss-meta-lab.js.map +1 -0
  135. package/dist/oss-remote-telemetry.d.ts +77 -0
  136. package/dist/oss-remote-telemetry.js +403 -0
  137. package/dist/oss-remote-telemetry.js.map +1 -0
  138. package/dist/persona.d.ts +35 -0
  139. package/dist/persona.js +133 -0
  140. package/dist/persona.js.map +1 -0
  141. package/dist/pi-agent-core.d.ts +77 -0
  142. package/dist/pi-agent-core.js +203 -0
  143. package/dist/pi-agent-core.js.map +1 -0
  144. package/dist/program.d.ts +54 -0
  145. package/dist/program.js +2347 -0
  146. package/dist/program.js.map +1 -0
  147. package/dist/redaction.d.ts +90 -0
  148. package/dist/redaction.js +292 -0
  149. package/dist/redaction.js.map +1 -0
  150. package/dist/run.d.ts +784 -0
  151. package/dist/run.js +4555 -0
  152. package/dist/run.js.map +1 -0
  153. package/dist/scripted-browser-actor.d.ts +294 -0
  154. package/dist/scripted-browser-actor.js +1286 -0
  155. package/dist/scripted-browser-actor.js.map +1 -0
  156. package/dist/scripted-browser-lab.d.ts +107 -0
  157. package/dist/scripted-browser-lab.js +738 -0
  158. package/dist/scripted-browser-lab.js.map +1 -0
  159. package/dist/shared-world-lab.d.ts +208 -0
  160. package/dist/shared-world-lab.js +1048 -0
  161. package/dist/shared-world-lab.js.map +1 -0
  162. package/dist/source-archive.d.ts +82 -0
  163. package/dist/source-archive.js +365 -0
  164. package/dist/source-archive.js.map +1 -0
  165. package/dist/stop-conditions.d.ts +26 -0
  166. package/dist/stop-conditions.js +66 -0
  167. package/dist/stop-conditions.js.map +1 -0
  168. package/dist/terminal-agent-actor.d.ts +36 -0
  169. package/dist/terminal-agent-actor.js +23 -0
  170. package/dist/terminal-agent-actor.js.map +1 -0
  171. package/docs/architecture/actor-contract.md +431 -0
  172. package/docs/architecture/github-feedback-loop.md +189 -0
  173. package/docs/architecture/local-codex-tui-actor.md +213 -0
  174. package/docs/architecture/observer.md +118 -0
  175. package/docs/architecture/oss-lab-poc.md +242 -0
  176. package/docs/architecture/project-layout.md +166 -0
  177. package/docs/architecture/state-driven-executor.md +203 -0
  178. package/docs/architecture/terminal-product-lane.md +148 -0
  179. package/docs/contracts/adapter-fixtures.md +80 -0
  180. package/docs/contracts/core.md +71 -0
  181. package/docs/contracts/feedback.md +145 -0
  182. package/docs/contracts/policy.md +305 -0
  183. package/docs/contracts/run-bundle.md +358 -0
  184. package/docs/contracts/schemas.md +984 -0
  185. package/docs/goals/current.md +409 -0
  186. package/docs/principles/invariants-and-defaults.md +135 -0
  187. package/docs/principles/self-driving-harness.md +129 -0
  188. package/docs/product/open-source-install-experience.md +229 -0
  189. package/docs/ramp/README.md +200 -0
  190. package/docs/release/open-source-readiness.md +208 -0
  191. package/docs/release/public-readiness-standard.md +205 -0
  192. package/docs/roadmap/world-class-open-source-v0.md +311 -0
  193. package/package.json +102 -8
  194. package/skills/humanish/SKILL.md +244 -0
  195. package/skills/humanish/agents/openai.yaml +7 -0
  196. package/index.js +0 -1
@@ -0,0 +1,1048 @@
1
+ // The shared-world lab backend (#164): the SEQUENTIAL deterministic proof-of-concept of the
2
+ // shared-world topology. ONE sandbox provisions a mutable service plane ONCE (clone or packed
3
+ // working tree + serve + seed), then N role SEATS take turns IN DECLARED ORDER (each an isolated browser
4
+ // profile + identity) against the shared loopback app. A read-only state
5
+ // CHECKPOINT (digest probe) runs at baseline + after each role's turn, producing a
6
+ // harness-clocked timeline that PROVES role B acted on a world already containing role A's
7
+ // mutation (the checkpoint after A strictly precedes B's turn in one clock).
8
+ //
9
+ // Doctrine (docs/goals/shared-world-topology/goal.md): the bundle declares a VERIFIED, weaker
10
+ // `attributionClass: shared-world` + a `humanish.shared-world.v1` block whose `attributionLimits`
11
+ // pin the attribution ceiling (sequential-only, no-concurrent-races,
12
+ // delta-attributed-to-turn-not-action). verifyRun fails closed on any overclaim.
13
+ //
14
+ // Safety rails (same as the fan-out packet + the 2026-06-16 prod incident): ONE Sandbox.create;
15
+ // ONE teardown BY exact sandboxId in a finally — NEVER Sandbox.list (account-wide ops are
16
+ // forbidden). Provisioned values are literal-scrubbed before any error/log persists; checkpoints
17
+ // persist DIGEST-ONLY. The concurrent (getHost) topology, a handoff/barrier grammar, an onTurn
18
+ // hook, and real per-role login are named NON-GOALS (PR2+).
19
+ //
20
+ // FIDELITY NOTE: one sandbox has ONE desktop geometry, so a role's `device` is a PROMPT SIGNAL
21
+ // (composed into its persona context) — physical per-role geometry is the concurrent topology's
22
+ // job. Each role's stream viewport records the sandbox's actual rendered resolution (honest).
23
+ import { mkdir, writeFile } from "node:fs/promises";
24
+ import { randomBytes } from "node:crypto";
25
+ import path from "node:path";
26
+ import { runDesktopCommandOrThrow, toErrorMessage } from "./command-failure.js";
27
+ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
28
+ import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
29
+ import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS, chromiumEvidenceProfilePreferencesJson } from "./browser-evidence-hygiene.js";
30
+ import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, makeChromeBrowserStateObserver, makeLaneWriteScreenshot, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, SUBJECT_DIR } from "./cua-actor-lab.js";
31
+ import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
32
+ import { runDetachedStep } from "./e2b-detached.js";
33
+ import { resolveSeatUrl, sharedWorldValidationReason } from "./lab-config.js";
34
+ import { renderObserver } from "./observer.js";
35
+ import { redactText } from "./redaction.js";
36
+ import { buildRunSource, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, SHARED_WORLD_SCHEMA } from "./run.js";
37
+ export const SHARED_WORLD_LAB_SCHEMA = "humanish.shared-world-lab-result.v1";
38
+ export const SHARED_WORLD_LAB_PROVIDER_METADATA = {
39
+ mode: "shared-world-lab",
40
+ tool: "humanish"
41
+ };
42
+ const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
43
+ // Settle after opening a seat's browser, before the session's first screenshot.
44
+ const BROWSER_SETTLE_MS = 8_000;
45
+ // Server-side reclamation buffer past the loop's own wall-clock stop.
46
+ const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
47
+ // Room for the one-time clone/install/build/start/probe + the sequential per-role sessions.
48
+ const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
49
+ // Per-checkpoint probe budget (read-only aggregate probes are fast).
50
+ const CHECKPOINT_TIMEOUT_MS = 60_000;
51
+ const DEFAULT_STATE_STEP_TIMEOUT_MS = 5 * 60_000;
52
+ const DEFAULT_MISSION = "You are testing a shared web application other roles also use. The browser is already open at your entry URL. Accomplish what your role asks, then stop.";
53
+ function readPositiveInt(value, fallback) {
54
+ if (value === undefined)
55
+ return fallback;
56
+ const parsed = Number.parseInt(value, 10);
57
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
58
+ }
59
+ function makeSharedWorldRunId() {
60
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
61
+ return `shared-world-${stamp}-${randomBytes(4).toString("hex")}`;
62
+ }
63
+ /** Single-quote a value for safe interpolation into the seat-launch shell command. */
64
+ function shellQuote(value) {
65
+ return `'${value.replaceAll("'", "'\\''")}'`;
66
+ }
67
+ /**
68
+ * Launch ONE seat's browser with its OWN isolated profile — the shared-world topology needs a
69
+ * fresh browser identity boundary per role (cookies/session isolated per seat). Absent/default
70
+ * preserves the historical shared-world opener (Chrome best-effort). A concrete preference is
71
+ * fail-closed and records the resolved in-sandbox command.
72
+ */
73
+ async function launchSeatBrowser(desktop, args) {
74
+ const requested = args.browserPreference ?? "default";
75
+ const chromiumFlags = CHROMIUM_EVIDENCE_HYGIENE_FLAGS.map(shellQuote).join(" ");
76
+ const command = [
77
+ "set -euo pipefail",
78
+ `browser_preference=${shellQuote(requested)}`,
79
+ `profile_dir=${shellQuote(args.profileDir)}`,
80
+ `seat_url=${shellQuote(args.seatUrl)}`,
81
+ `chrome_preferences_json=${shellQuote(chromiumEvidenceProfilePreferencesJson())}`,
82
+ 'mkdir -p "$profile_dir"',
83
+ "chrome_debug_flags=(" + chromiumFlags + ")",
84
+ "prepare_chrome_profile() {",
85
+ " mkdir -p \"$profile_dir/Default\"",
86
+ " printf '%s\\n' \"$chrome_preferences_json\" > \"$profile_dir/Default/Preferences\"",
87
+ "}",
88
+ "launch_chrome() {",
89
+ " local label=\"$1\"",
90
+ " local binary=\"$2\"",
91
+ " if ! command -v \"$binary\" >/dev/null 2>&1; then return 127; fi",
92
+ " echo \"HUMANISH_BROWSER_RESOLVED=$label\"",
93
+ " pkill -f '[r]emote-debugging-port=9222' 2>/dev/null || true",
94
+ " prepare_chrome_profile",
95
+ " setsid -f \"$binary\" --new-window --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --user-data-dir=\"$profile_dir\" \"${chrome_debug_flags[@]}\" \"$seat_url\" > /dev/null 2>&1 < /dev/null",
96
+ "}",
97
+ "launch_firefox() {",
98
+ " if ! command -v firefox >/dev/null 2>&1; then return 127; fi",
99
+ " echo \"HUMANISH_BROWSER_RESOLVED=firefox\"",
100
+ " setsid -f firefox --new-window --profile \"$profile_dir\" \"$seat_url\" > /dev/null 2>&1 < /dev/null",
101
+ "}",
102
+ "case \"$browser_preference\" in",
103
+ " chrome)",
104
+ " launch_chrome google-chrome google-chrome || launch_chrome google-chrome-stable google-chrome-stable",
105
+ " ;;",
106
+ " chromium)",
107
+ " launch_chrome chromium chromium || launch_chrome chromium-browser chromium-browser",
108
+ " ;;",
109
+ " firefox)",
110
+ " launch_firefox",
111
+ " ;;",
112
+ " default)",
113
+ " launch_chrome google-chrome google-chrome || true",
114
+ " ;;",
115
+ "esac"
116
+ ].join("\n");
117
+ const result = await runDesktopCommandOrThrow(() => desktop.commands.run(command, { requestTimeoutMs: args.requestTimeoutMs }), (_info, error) => args.browserPreference !== undefined && args.browserPreference !== "default"
118
+ ? new Error(`requested desktop browser "${args.browserPreference}" could not be launched for shared-world seat`)
119
+ : error);
120
+ if (args.browserPreference !== undefined && args.browserPreference !== "default" && result.exitCode !== undefined && result.exitCode !== 0) {
121
+ throw new Error(`requested desktop browser "${args.browserPreference}" could not be launched for shared-world seat`);
122
+ }
123
+ if (args.browserPreference === undefined) {
124
+ return undefined;
125
+ }
126
+ const resolved = (result.stdout ?? "").match(/^HUMANISH_BROWSER_RESOLVED=(\S+)$/m)?.[1];
127
+ return {
128
+ requested,
129
+ ...(resolved === undefined ? {} : { resolved })
130
+ };
131
+ }
132
+ /** Combine a snapshot's per-probe digests into ONE sha256-16 (digest-only; no raw value). */
133
+ export function combineCheckpointDigest(parts) {
134
+ return commandDigestOf(parts.join("\n"));
135
+ }
136
+ /**
137
+ * Run ONE checkpoint snapshot LIVE: each declared probe runs read-only via the detached
138
+ * primitive; its stdout is literal-scrubbed (provisioned values + the probe's declared redact
139
+ * literals, folded into `scrub`) then pattern-redacted, then digested. Only the COMBINED digest
140
+ * persists — never the raw value (the seed-step lockdown). Unique step names per snapshot prevent
141
+ * stale-status reuse across snapshots.
142
+ */
143
+ export async function runCheckpointSnapshot(args) {
144
+ const parts = [];
145
+ for (const probe of args.checkpoints) {
146
+ const result = await runDetachedStep(args.desktop, {
147
+ name: `checkpoint-${args.snapshotIndex}-${probe.name}`,
148
+ command: probe.command,
149
+ cwd: SUBJECT_DIR,
150
+ timeoutMs: CHECKPOINT_TIMEOUT_MS,
151
+ requestTimeoutMs: args.requestTimeoutMs,
152
+ ...args.timers
153
+ });
154
+ const scrubbed = redactText(args.scrub(result.logTail));
155
+ parts.push(`${probe.name}=${commandDigestOf(scrubbed)}`);
156
+ }
157
+ const digest = combineCheckpointDigest(parts);
158
+ return {
159
+ kind: "checkpoint",
160
+ name: args.name,
161
+ digest,
162
+ deltaFromPrev: args.prevDigest !== undefined && digest !== args.prevDigest
163
+ };
164
+ }
165
+ /** The DECLARED (dry-run) checkpoint snapshot: digest the probe RECIPE (command digests), no run. */
166
+ export function declaredCheckpointSnapshot(name, checkpoints) {
167
+ const parts = checkpoints.map((probe) => `${probe.name}=${commandDigestOf(probe.command)}`);
168
+ return { kind: "checkpoint", name, digest: combineCheckpointDigest(parts), deltaFromPrev: false };
169
+ }
170
+ /** sha256-16 over the ordered seed-step command digests — the seeded-state RECIPE identity. */
171
+ export function seedRecipeDigest(config) {
172
+ const seed = config.subject.state?.seed ?? [];
173
+ return commandDigestOf(seed.map((step) => `${step.name}:${commandDigestOf(step.command)}`).join("\n"));
174
+ }
175
+ /** Build the resolved role roster from actors[0].lanes (the role roster). */
176
+ function buildRoleSpecs(config, serveUrl) {
177
+ const actor = config.actors[0];
178
+ const mission = actor?.mission ?? DEFAULT_MISSION;
179
+ const roster = actor?.lanes ?? [];
180
+ return roster.map((lane, i) => {
181
+ const roleId = lane.id ?? `role-${String(i + 1).padStart(2, "0")}`;
182
+ const device = resolveLaneDevice(config, lane);
183
+ const composed = composeLaneInstructions({
184
+ mission,
185
+ ...(lane.persona === undefined ? {} : { persona: lane.persona }),
186
+ ...(lane.instruction === undefined ? {} : { instruction: lane.instruction }),
187
+ device: { name: device.name, preset: device.preset }
188
+ });
189
+ return {
190
+ roleId,
191
+ roleIndex: i,
192
+ simId: `sim-${String(i + 1).padStart(3, "0")}`,
193
+ streamId: `stream-${String(i + 1).padStart(3, "0")}`,
194
+ persona: composed.persona,
195
+ instructions: composed.instructions,
196
+ deviceName: device.name,
197
+ ...((lane.stopWhen ?? actor?.stopWhen) === undefined ? {} : { stopWhen: (lane.stopWhen ?? actor?.stopWhen) }),
198
+ ...(lane.entry === undefined ? {} : { entry: lane.entry }),
199
+ seatUrl: resolveSeatUrl(serveUrl, lane.entry) ?? serveUrl,
200
+ screenshotDir: roleId,
201
+ traceArtifactPath: `actors/${`stream-${String(i + 1).padStart(3, "0")}`}.json`,
202
+ profileDir: `/tmp/seat-${roleId}`
203
+ };
204
+ });
205
+ }
206
+ export async function runSharedWorldLab(options) {
207
+ const { config, dryRun } = options;
208
+ const cwd = path.resolve(options.cwd);
209
+ const hooks = options.hooks ?? {};
210
+ const env = hooks.env ?? process.env;
211
+ const render = hooks.renderObserverFn ?? renderObserver;
212
+ const actorType = config.actors[0]?.type ?? "";
213
+ const fail = (code, message, actorLabel) => ({
214
+ schema: SHARED_WORLD_LAB_SCHEMA,
215
+ ok: false,
216
+ cwd,
217
+ labId: config.id,
218
+ actor: actorLabel ?? actorType,
219
+ topology: "shared-world",
220
+ roleCount: config.actors[0]?.lanes?.length ?? 0,
221
+ sequence: [],
222
+ dryRun,
223
+ runId: options.runId ?? "not-created",
224
+ roles: [],
225
+ warnings: [],
226
+ error: { code, message }
227
+ });
228
+ // Resolve the actor through the registry — the parser validated this, but the engine fails closed
229
+ // rather than trusting a config that arrived through the library door (this fn is npm surface).
230
+ const descriptor = actorRegistry[actorType];
231
+ if (!descriptor || !isCuaActorDescriptor(descriptor)) {
232
+ return fail("HUMANISH_SHARED_WORLD_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
233
+ }
234
+ // Re-enforce the shared-world cross-validation (library API surface).
235
+ const invalidReason = sharedWorldValidationReason(config);
236
+ if (invalidReason) {
237
+ return fail("HUMANISH_SHARED_WORLD_LAB_INVALID", invalidReason, descriptor.id);
238
+ }
239
+ const serve = config.subject.serve;
240
+ const localTreeRoute = config.subject.source === "local-tree";
241
+ const subjectRepo = config.subject.repos?.[0] ?? "";
242
+ const subjectEnvNames = config.subject.env ?? [];
243
+ const checkpoints = config.subject.state?.checkpoint ?? [];
244
+ const roleSpecs = buildRoleSpecs(config, serve.url);
245
+ const roleCount = roleSpecs.length;
246
+ const runSession = hooks.runSession ?? descriptor.runSession;
247
+ // Read keys once into locals (names only; values never logged or persisted).
248
+ const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
249
+ const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
250
+ // Literal scrubber for every known provisioned value (no secret "shape" to pattern-match):
251
+ // provider/E2B keys, subject env values, AND each checkpoint's declared redact literals.
252
+ const knownSecretValues = [
253
+ openaiApiKey,
254
+ e2bApiKey,
255
+ ...subjectEnvNames.map((name) => env[name] ?? ""),
256
+ ...checkpoints.flatMap((probe) => probe.redact ?? [])
257
+ ].filter((value) => value.length >= 4);
258
+ const scrubKnownValues = (text) => knownSecretValues.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
259
+ const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
260
+ const publicRepo = redactRepoLabel ? "repo-01" : subjectRepo;
261
+ const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
262
+ if (!dryRun) {
263
+ const missingKeys = [
264
+ ...(openaiApiKey ? [] : ["OPENAI_API_KEY"]),
265
+ ...(e2bApiKey ? [] : ["E2B_API_KEY"])
266
+ ];
267
+ if (missingKeys.length > 0) {
268
+ return fail("HUMANISH_SHARED_WORLD_LAB_KEYS_MISSING", `Live shared-world labs need ${missingKeys.join(" and ")} in the environment (pass them via --env-file; values are never persisted).`, descriptor.id);
269
+ }
270
+ const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
271
+ if (missingSubjectEnv.length > 0) {
272
+ return fail("HUMANISH_SHARED_WORLD_LAB_SUBJECT_ENV_MISSING", `subject.env declares ${missingSubjectEnv.join(", ")} but the environment does not provide ${missingSubjectEnv.length === 1 ? "it" : "them"} (pass via --env-file; values are never persisted).`, descriptor.id);
273
+ }
274
+ }
275
+ const runId = options.runId ?? makeSharedWorldRunId();
276
+ const artifactRoot = path.join(cwd, ".humanish", "runs", runId);
277
+ const createdAt = new Date().toISOString();
278
+ const timeoutMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
279
+ const requestTimeoutMs = readPositiveInt(env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
280
+ const redactScreenshots = config.policies?.redactScreenshots === true;
281
+ const timers = hooks.detachedTimers ?? {};
282
+ // ONE sandbox geometry for the shared desktop (run-wide; per-role device is a prompt signal).
283
+ const sandboxDevice = resolveLaneDevice(config, undefined);
284
+ const sandboxResolution = sandboxDevice.resolution;
285
+ const sandboxPreset = sandboxDevice.preset;
286
+ const perRunSandboxMs = config.execution?.desktop?.sandboxTimeoutMs
287
+ ?? timeoutMs * Math.max(1, roleCount)
288
+ + SUBJECT_PROVISION_BUDGET_MS
289
+ + (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0)
290
+ + SANDBOX_TIMEOUT_BUFFER_MS;
291
+ await mkdir(artifactRoot, { recursive: true });
292
+ const source = await buildRunSource({ capturedAt: createdAt, cwd, humanishSource: "present", packageName: "humanish" });
293
+ const warnings = [];
294
+ const stateStepRecords = [];
295
+ const roleOutcomes = [];
296
+ const baselineDeclared = declaredCheckpointSnapshot("cp-baseline", checkpoints);
297
+ let baselineCheckpoint = baselineDeclared;
298
+ let subjectCommit;
299
+ let sandboxId;
300
+ let killed = false;
301
+ let failFastReason;
302
+ // Pack the working tree ONCE per run, on the host, BEFORE any sandbox is created (mirrors the
303
+ // cua route's ordering): a packing failure fails the run closed here, never spending sandbox
304
+ // cost. Dry-run packs nothing (no fs side effects; the contract bundle carries no archiveSha256).
305
+ let localTreeArchive;
306
+ let localTreeArchiveBuffer;
307
+ if (localTreeRoute && !dryRun) {
308
+ const packLocalTree = hooks.packLocalTree ?? defaultPackLocalTree;
309
+ try {
310
+ const packed = await packLocalTree({
311
+ root: cwd,
312
+ ...(config.subject.localTree?.exclude === undefined ? {} : { extraExclude: config.subject.localTree.exclude }),
313
+ ...(config.subject.localTree?.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: config.subject.localTree.maxArchiveBytes })
314
+ });
315
+ localTreeArchive = packed.archive;
316
+ localTreeArchiveBuffer = packed.buffer;
317
+ process.stderr.write(`humanish shared-world local-tree: packed ${packed.archive.fileCount} entries, ${packed.archive.totalBytes} bytes, archiveSha256 ${packed.archive.archiveSha256}`
318
+ + `${packed.archive.git ? ` (commit ${packed.archive.git.commit.slice(0, 12)}, ${packed.archive.git.dirty ? "dirty" : "clean"} working tree)` : " (not a git work tree)"}\n`);
319
+ }
320
+ catch (error) {
321
+ return fail("HUMANISH_SHARED_WORLD_LAB_FAILED", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
322
+ }
323
+ }
324
+ if (!dryRun) {
325
+ let desktopModule;
326
+ let desktop;
327
+ let runFailed = false;
328
+ try {
329
+ desktopModule = await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
330
+ // ONE Sandbox.create for the whole run (metadata.topology + roleCount; the SUBJECT env is
331
+ // provisioned here on the clone route — the ACTOR key never enters the sandbox). An optional
332
+ // custom desktop template (image) selects Sandbox.create(template, opts); absent keeps the
333
+ // byte-stable Sandbox.create(opts) default.
334
+ desktop = await createDesktopSandbox(desktopModule, {
335
+ apiKey: e2bApiKey,
336
+ requestTimeoutMs,
337
+ timeoutMs: perRunSandboxMs,
338
+ metadata: {
339
+ ...SHARED_WORLD_LAB_PROVIDER_METADATA,
340
+ labId: config.id,
341
+ topology: "shared-world",
342
+ roleCount: String(roleCount)
343
+ },
344
+ ...(subjectEnvNames.length > 0
345
+ ? { envs: Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])) }
346
+ : {}),
347
+ resolution: sandboxResolution,
348
+ dpi: 96,
349
+ lifecycle: { onTimeout: "kill" }
350
+ }, config.execution?.desktop?.template);
351
+ sandboxId = desktop.sandboxId;
352
+ if (hooks.prepareDesktop) {
353
+ await hooks.prepareDesktop(desktop);
354
+ }
355
+ // Provision the shared plane ONCE: clone + install/build + seed + serve + readiness probe
356
+ // (clone route), or upload/extract the once-per-run packed archive + the SAME shared serve
357
+ // pipeline (local-tree route). One stderr line per phase boundary by default; hooks.onPhase
358
+ // (DI seam, mirrors CuaActorLabHooks) overrides it so tests capture instead of writing to
359
+ // real stderr.
360
+ const onSubjectPhase = hooks.onPhase ?? ((event) => {
361
+ process.stderr.write(`humanish shared-world: ${event.message}${event.durationMs === undefined ? "" : ` (${event.durationMs}ms)`}\n`);
362
+ });
363
+ if (localTreeRoute) {
364
+ await provisionLocalTreeSubject(desktop, {
365
+ archiveBuffer: localTreeArchiveBuffer,
366
+ serve,
367
+ ...(config.subject.state === undefined ? {} : { state: config.subject.state }),
368
+ requestTimeoutMs,
369
+ scrub: scrubKnownValues,
370
+ onStateStep: (record) => { stateStepRecords.push(record); },
371
+ onPhase: onSubjectPhase,
372
+ ...timers
373
+ });
374
+ }
375
+ else {
376
+ subjectCommit = await provisionCloneSubject(desktop, {
377
+ repo: subjectRepo,
378
+ depth: config.subject.clone?.depth ?? 1,
379
+ serve,
380
+ ...(config.subject.state === undefined ? {} : { state: config.subject.state }),
381
+ hasGithubToken,
382
+ requestTimeoutMs,
383
+ scrub: scrubKnownValues,
384
+ onCommit: (commit) => { subjectCommit = commit; },
385
+ onStateStep: (record) => { stateStepRecords.push(record); },
386
+ onPhase: onSubjectPhase,
387
+ ...timers
388
+ });
389
+ }
390
+ // Baseline checkpoint (before any role acts).
391
+ baselineCheckpoint = await runCheckpointSnapshot({
392
+ desktop,
393
+ snapshotIndex: 0,
394
+ name: "cp-baseline",
395
+ checkpoints,
396
+ prevDigest: undefined,
397
+ scrub: scrubKnownValues,
398
+ requestTimeoutMs,
399
+ timers
400
+ });
401
+ let prevDigest = baselineCheckpoint.digest;
402
+ // Sequential per-role loop (DECLARED order). A HARNESS error blocks the REMAINING roles
403
+ // (the shared-state premise is broken); a MISSION failure is data, never trips fail-fast.
404
+ for (const [index, spec] of roleSpecs.entries()) {
405
+ if (failFastReason) {
406
+ roleOutcomes.push({
407
+ spec,
408
+ screenshots: [],
409
+ skippedReason: `skipped: ${failFastReason}`,
410
+ noEngagement: false,
411
+ harnessError: false
412
+ });
413
+ continue;
414
+ }
415
+ const screenshots = [];
416
+ const writeScreenshot = makeLaneWriteScreenshot(artifactRoot, { screenshotDir: spec.screenshotDir }, screenshots);
417
+ let session;
418
+ let sessionError;
419
+ let desktopBrowser;
420
+ try {
421
+ // Fresh isolated browser profile per seat, opened at the role's same-origin loopback entry.
422
+ desktopBrowser = await launchSeatBrowser(desktop, {
423
+ ...(config.execution?.desktop?.browser === undefined ? {} : { browserPreference: config.execution.desktop.browser }),
424
+ profileDir: spec.profileDir,
425
+ seatUrl: spec.seatUrl,
426
+ requestTimeoutMs
427
+ });
428
+ await desktop.wait(BROWSER_SETTLE_MS).catch(() => undefined);
429
+ const sessionOptions = {
430
+ instructions: spec.instructions,
431
+ persona: spec.persona,
432
+ timeoutMs,
433
+ openai: {
434
+ apiKey: openaiApiKey,
435
+ ...(config.actors[0]?.model ? { model: config.actors[0].model } : {})
436
+ },
437
+ desktop: desktop,
438
+ executorOptions: {
439
+ observeBrowserState: makeChromeBrowserStateObserver(desktop, requestTimeoutMs)
440
+ },
441
+ redactScreenshots,
442
+ scrubText: scrubKnownValues,
443
+ writeScreenshot,
444
+ ...(spec.stopWhen === undefined ? {} : { stopWhen: spec.stopWhen })
445
+ };
446
+ session = await runSession(sessionOptions);
447
+ }
448
+ catch (error) {
449
+ sessionError = redactText(scrubKnownValues(toErrorMessage(error)));
450
+ }
451
+ if (session) {
452
+ await mkdir(path.dirname(path.join(artifactRoot, spec.traceArtifactPath)), { recursive: true });
453
+ await writeFile(path.join(artifactRoot, spec.traceArtifactPath), `${JSON.stringify(session.trace, null, 2)}\n`, "utf8");
454
+ if (session.trace.redaction.screenshots === "raw") {
455
+ warnings.push("Screenshots are full-fidelity (raw) for local use — the bundle stays in gitignored .humanish and nothing scans these pixels; review them before sharing anywhere. Set policies.redactScreenshots: true to blur a share-as-is bundle.");
456
+ }
457
+ }
458
+ const noEngagement = session !== undefined
459
+ && session.completionReason === "goal_satisfied"
460
+ && (session.trace.counts.actions ?? 0) === 0
461
+ && (session.trace.counts.messages ?? 0) === 0;
462
+ if (noEngagement) {
463
+ warnings.push(`Role ${spec.roleId} returned goal_satisfied with ZERO actions and ZERO messages — likely a blank/still-loading screen; NOT counted as a pass.`);
464
+ }
465
+ const harnessError = sessionError !== undefined || session?.completionReason === "harness_error";
466
+ // Checkpoint AFTER this role's turn (the interaction-proof snapshot). Runs even on a
467
+ // harness-errored turn (the probe is read-only state, independent of the browser seat).
468
+ const afterCheckpoint = await runCheckpointSnapshot({
469
+ desktop,
470
+ snapshotIndex: index + 1,
471
+ name: `cp-after-${spec.roleId}`,
472
+ checkpoints,
473
+ prevDigest,
474
+ scrub: scrubKnownValues,
475
+ requestTimeoutMs,
476
+ timers
477
+ });
478
+ prevDigest = afterCheckpoint.digest;
479
+ roleOutcomes.push({
480
+ spec,
481
+ ...(session ? { session } : {}),
482
+ ...(sessionError === undefined ? {} : { sessionError }),
483
+ screenshots,
484
+ ...(desktopBrowser === undefined ? {} : { desktopBrowser }),
485
+ noEngagement,
486
+ harnessError,
487
+ afterCheckpoint
488
+ });
489
+ if (harnessError && !failFastReason) {
490
+ failFastReason = `role "${spec.roleId}" ended in a harness error — the shared-state premise is broken (fail-fast)`;
491
+ }
492
+ }
493
+ }
494
+ catch (error) {
495
+ runFailed = true;
496
+ warnings.push(`Shared-world run failed before completion: ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
497
+ }
498
+ finally {
499
+ // ONE teardown BY exact sandboxId — NEVER Sandbox.list (the 2026-06-16 prod-incident rail).
500
+ if (desktop && desktopModule) {
501
+ const anyRoleFailed = runFailed
502
+ || failFastReason !== undefined
503
+ || roleOutcomes.some((outcome) => outcome.harnessError || outcome.sessionError !== undefined);
504
+ // Each route's own keep flag gates its own run only: a clone.keep can never leak into a
505
+ // local-tree run's teardown decision, and vice versa (mirrors runCuaLane's keepReason).
506
+ const keepReason = config.subject.clone?.keep === true
507
+ ? "subject.clone.keep"
508
+ : config.subject.localTree?.keep === true
509
+ ? "subject.localTree.keep"
510
+ : undefined;
511
+ const keepForDebug = keepReason !== undefined && anyRoleFailed;
512
+ if (keepForDebug) {
513
+ warnings.push(`Sandbox ${desktop.sandboxId} kept for debugging (${keepReason} on failure); reclaim it via E2B or it will be killed on its server-side timeout.`);
514
+ }
515
+ else if (typeof desktopModule.Sandbox.kill === "function") {
516
+ try {
517
+ await desktopModule.Sandbox.kill(desktop.sandboxId, { requestTimeoutMs: 60_000 });
518
+ killed = true;
519
+ }
520
+ catch (error) {
521
+ warnings.push(`Sandbox teardown failed (server-side kill-on-timeout will reclaim it): ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
522
+ }
523
+ }
524
+ else {
525
+ warnings.push("Installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox.");
526
+ }
527
+ }
528
+ }
529
+ }
530
+ // Subject provenance (invariant 5): the ONE shared plane. Local-tree carries archiveSha256 (the
531
+ // pin - one archive, so no per-lane unanimity math is needed, unlike the cua fan-out route) plus
532
+ // the host-side commit/dirty when the packed root was a git work tree; clone carries repo/commit
533
+ // as before. Mirrors laneSubjectProjection's local-tree branch in cua-actor-lab.ts.
534
+ const subjectState = resolveSubjectState({
535
+ declared: config.subject.state,
536
+ dryRun,
537
+ executed: stateStepRecords
538
+ });
539
+ const planeCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
540
+ const subject = localTreeRoute
541
+ ? {
542
+ source: "local-tree",
543
+ ...(localTreeArchive === undefined ? {} : { archiveSha256: localTreeArchive.archiveSha256 }),
544
+ ...(planeCommit === undefined ? {} : { commit: planeCommit }),
545
+ ...(localTreeArchive?.git === undefined ? {} : { dirty: localTreeArchive.git.dirty }),
546
+ envNames: subjectEnvNames,
547
+ state: subjectState
548
+ }
549
+ : {
550
+ source: "clone",
551
+ repo: publicRepo,
552
+ ...(subjectCommit === undefined ? {} : { commit: subjectCommit }),
553
+ envNames: subjectEnvNames,
554
+ state: subjectState
555
+ };
556
+ const bundle = buildSharedWorldBundle({
557
+ config,
558
+ descriptor,
559
+ createdAt,
560
+ dryRun,
561
+ runId,
562
+ source,
563
+ roleSpecs,
564
+ roleOutcomes,
565
+ baselineCheckpoint,
566
+ subject,
567
+ sandboxResolution,
568
+ sandboxPreset,
569
+ seedDigest: seedRecipeDigest(config),
570
+ ...(planeCommit === undefined ? {} : { subjectCommit: planeCommit }),
571
+ ...(failFastReason === undefined ? {} : { failFastReason })
572
+ });
573
+ const adapterWarnings = [];
574
+ await applyBrowserAdapterHooks({
575
+ hooks,
576
+ bundle,
577
+ context: {
578
+ bundle,
579
+ runDir: artifactRoot,
580
+ labId: config.id,
581
+ runId,
582
+ actor: descriptor.id,
583
+ backend: "shared-world",
584
+ dryRun,
585
+ laneCount: roleSpecs.length
586
+ },
587
+ sanitize: (text) => redactText(scrubKnownValues(text)),
588
+ warnings: adapterWarnings,
589
+ hookLabel: "sharedWorldHooks"
590
+ });
591
+ await writeFile(path.join(artifactRoot, "run.json"), `${JSON.stringify(bundle, null, 2)}\n`, "utf8");
592
+ await writeFile(path.join(artifactRoot, "review.json"), `${JSON.stringify(bundle.review, null, 2)}\n`, "utf8");
593
+ await writeFile(path.join(artifactRoot, "review.md"), renderSharedWorldReviewMarkdown(bundle), "utf8");
594
+ await writeFile(path.join(artifactRoot, "events.ndjson"), `${bundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
595
+ await writeFile(path.join(cwd, ".humanish", "runs", "latest.json"), `${JSON.stringify({ schema: "humanish.latest-run.v1", runId, path: path.join(".humanish", "runs", runId), updatedAt: createdAt }, null, 2)}\n`, "utf8");
596
+ const observer = await render(cwd, runId, { open: options.open === true });
597
+ const roleOk = (outcome) => {
598
+ return sharedWorldRoleOutcomeOk(outcome, dryRun);
599
+ };
600
+ const allRolesOk = roleSpecs.every((_, index) => roleOk(roleOutcomes[index]));
601
+ const adapterFailure = adapterScoreFailureMessage(bundle);
602
+ const ok = observer.ok && allRolesOk && failFastReason === undefined && adapterFailure === undefined;
603
+ const allWarnings = [...warnings, ...adapterWarnings, ...observer.warnings];
604
+ const roles = roleSpecs.map((spec, index) => {
605
+ const outcome = roleOutcomes[index];
606
+ const base = { id: spec.roleId, index: spec.roleIndex + 1, persona: spec.persona.id, profileDir: spec.profileDir };
607
+ if (dryRun || !outcome) {
608
+ return { ...base, status: "contract_proof_only", ok: dryRun };
609
+ }
610
+ if (outcome.skippedReason !== undefined) {
611
+ return {
612
+ ...base,
613
+ status: "blocked",
614
+ ok: false,
615
+ skippedReason: outcome.skippedReason,
616
+ error: { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: outcome.skippedReason }
617
+ };
618
+ }
619
+ const session = outcome.session;
620
+ const thisOk = roleOk(outcome);
621
+ return {
622
+ ...base,
623
+ status: session ? session.status : "failed",
624
+ ok: thisOk,
625
+ ...(session
626
+ ? { session: { status: session.status, completionReason: session.completionReason, reason: session.reason, screenshots: outcome.screenshots.length } }
627
+ : {}),
628
+ ...(thisOk
629
+ ? {}
630
+ : {
631
+ error: {
632
+ code: "HUMANISH_SHARED_WORLD_LAB_FAILED",
633
+ message: outcome.sessionError
634
+ ?? (outcome.noEngagement
635
+ ? "Role took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
636
+ : session?.completionReason === "harness_error"
637
+ ? `Role seat ended with a harness error: ${session.reason}`
638
+ : "Role did not produce a terminal session.")
639
+ }
640
+ })
641
+ };
642
+ });
643
+ const sequence = roleOutcomes
644
+ .filter((outcome) => outcome.skippedReason === undefined && outcome.afterCheckpoint !== undefined)
645
+ .map((outcome) => outcome.spec.roleId);
646
+ const errorResult = (() => {
647
+ if (ok)
648
+ return undefined;
649
+ if (!observer.ok) {
650
+ return { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: observer.error?.message ?? "Observer failed for the shared-world run." };
651
+ }
652
+ if (adapterFailure !== undefined) {
653
+ return { code: "HUMANISH_SHARED_WORLD_LAB_FAILED", message: adapterFailure };
654
+ }
655
+ const passed = roles.filter((role) => role.ok).length;
656
+ return {
657
+ code: "HUMANISH_SHARED_WORLD_LAB_FAILED",
658
+ message: `Shared-world run failed: ${passed}/${roleCount} role(s) passed${failFastReason ? ` (fail-fast: ${failFastReason})` : ""}.`
659
+ };
660
+ })();
661
+ return {
662
+ schema: SHARED_WORLD_LAB_SCHEMA,
663
+ ok,
664
+ cwd,
665
+ labId: config.id,
666
+ actor: descriptor.id,
667
+ topology: "shared-world",
668
+ roleCount,
669
+ sequence,
670
+ dryRun,
671
+ runId,
672
+ ...(sandboxId === undefined ? {} : { sandbox: { sandboxId, killed } }),
673
+ subject,
674
+ roles,
675
+ observer,
676
+ warnings: allWarnings,
677
+ ...(errorResult === undefined ? {} : { error: errorResult })
678
+ };
679
+ }
680
+ /** Project the shared-world run into a humanish.run-bundle.v1 with the sharedWorld evidence block. */
681
+ export function buildSharedWorldBundle(args) {
682
+ const { config, descriptor, createdAt, dryRun, roleSpecs, roleOutcomes } = args;
683
+ const simulations = [];
684
+ const streams = [];
685
+ const events = [];
686
+ const appUrl = config.subject.serve?.url ?? "";
687
+ events.push({
688
+ id: "event-000-created",
689
+ at: createdAt,
690
+ level: "info",
691
+ type: "shared-world.run.created",
692
+ message: `Created shared-world run for ${config.id} (actor ${descriptor.id}, ${roleSpecs.length} role(s), ONE shared plane, sequential turns).`
693
+ });
694
+ // Human-readable plane label, byte-stable for the clone route: "clone of <repo>[@<commit>]".
695
+ // The local-tree route has no repo slug, so it labels the packed archive instead (archiveSha256
696
+ // + dirty/clean when the packed root was a git work tree).
697
+ const dryRunPlaneLabel = args.subject.source === "local-tree"
698
+ ? "packed working tree"
699
+ : `clone of ${args.subject.repo}`;
700
+ const livePlaneLabel = args.subject.source === "local-tree"
701
+ ? (args.subject.archiveSha256
702
+ ? `packed working tree (archiveSha256 ${args.subject.archiveSha256}${args.subject.dirty === true ? ", dirty working tree" : args.subject.dirty === false ? ", clean working tree" : ""})`
703
+ : "packed working tree (archive digest unresolved; provisioning failed before resolution)")
704
+ : `clone of ${args.subject.repo}${args.subjectCommit ? `@${args.subjectCommit}` : ""}`;
705
+ events.push({
706
+ id: "event-001-plane",
707
+ at: createdAt,
708
+ level: "info",
709
+ type: "shared-world.plane.provenance",
710
+ message: dryRun
711
+ ? `Shared plane declared: ${dryRunPlaneLabel}, served at ${appUrl} in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
712
+ : `Shared plane: ${livePlaneLabel}, served at ${appUrl} in-sandbox; seed recipe ${args.seedDigest}; env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
713
+ simId: roleSpecs[0]?.simId ?? "sim-001",
714
+ streamId: roleSpecs[0]?.streamId ?? "stream-001"
715
+ });
716
+ let eventSeq = 2;
717
+ const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
718
+ roleSpecs.forEach((spec, index) => {
719
+ const outcome = roleOutcomes[index];
720
+ const session = outcome?.session;
721
+ const screenshots = outcome?.screenshots ?? [];
722
+ const lastScreenshot = screenshots[screenshots.length - 1];
723
+ const status = outcome?.skippedReason !== undefined
724
+ ? "blocked"
725
+ : session
726
+ ? session.status
727
+ : outcome?.sessionError
728
+ ? "failed"
729
+ : "contract_proof_only";
730
+ const reason = outcome?.skippedReason
731
+ ?? session?.reason
732
+ ?? outcome?.sessionError
733
+ ?? "Contract role only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.";
734
+ const traceScreenshotMode = session?.trace.redaction.screenshots;
735
+ const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
736
+ ? traceScreenshotMode
737
+ : config.policies?.redactScreenshots === true ? "blurred" : "raw";
738
+ simulations.push({
739
+ id: spec.simId,
740
+ index: index + 1,
741
+ personaId: spec.persona.id,
742
+ scenarioId: `shared-world-${config.id}`,
743
+ status,
744
+ streamKind: "browser",
745
+ mode: "browser-sim",
746
+ progress: 100,
747
+ currentStep: reason,
748
+ summary: session
749
+ ? `Role ${spec.roleId} (${spec.persona.id}): drove the shared app; ${session.completionReason}.`
750
+ : outcome?.skippedReason !== undefined
751
+ ? `Role ${spec.roleId} ${outcome.skippedReason}.`
752
+ : outcome?.sessionError
753
+ ? `Role ${spec.roleId} failed before a terminal session verdict: ${outcome.sessionError}`
754
+ : `Contract role ${spec.roleId} (${spec.persona.id}) for ${descriptor.id} against the shared plane at ${appUrl}.`,
755
+ streamIds: [spec.streamId],
756
+ startedAt: createdAt,
757
+ updatedAt: createdAt
758
+ });
759
+ streams.push({
760
+ id: spec.streamId,
761
+ simId: spec.simId,
762
+ kind: "browser",
763
+ label: `Shared-world role ${spec.roleId} — ${config.id}`,
764
+ status,
765
+ transport: "snapshot",
766
+ updatedAt: createdAt,
767
+ embed: lastScreenshot
768
+ ? { kind: "screenshot", url: lastScreenshot, title: `Shared desktop, role ${spec.roleId} (${screenshotMode})` }
769
+ : { kind: "placeholder", title: `Shared desktop, role ${spec.roleId}` },
770
+ viewport: {
771
+ width: args.sandboxResolution[0],
772
+ height: args.sandboxResolution[1],
773
+ deviceScaleFactor: args.sandboxPreset.deviceScaleFactor,
774
+ isMobile: args.sandboxPreset.isMobile
775
+ },
776
+ ui: {
777
+ route: spec.seatUrl,
778
+ intent: `Watch role ${spec.roleId} (${spec.persona.id}) drive the SHARED app (one plane; sequential turn).`,
779
+ state: reason,
780
+ ...(session ? { actorStatus: session.status } : {}),
781
+ ...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
782
+ },
783
+ ...(session ? { actor: session.trace } : {}),
784
+ artifacts: [
785
+ { label: "run bundle", path: "run.json", kind: "bundle" },
786
+ { label: "review", path: "review.md", kind: "review" },
787
+ { label: "events", path: "events.ndjson", kind: "events" },
788
+ ...(session
789
+ ? [{ label: `role ${spec.roleId} actor trace`, path: spec.traceArtifactPath, kind: "trace" }]
790
+ : []),
791
+ ...screenshots.map((screenshot, screenshotIndex) => ({
792
+ label: `role ${spec.roleId} screenshot ${String(screenshotIndex + 1).padStart(2, "0")} (${screenshotMode})`,
793
+ path: screenshot,
794
+ kind: "screenshot"
795
+ }))
796
+ ]
797
+ });
798
+ // Per-role session event.
799
+ if (session) {
800
+ events.push({
801
+ id: nextEventId(`session-${spec.roleId}`),
802
+ at: createdAt,
803
+ level: session.status === "passed" ? "info" : "warn",
804
+ type: `shared-world.session.${session.completionReason}`,
805
+ message: `Role ${spec.roleId}: ${session.status} — ${session.reason}`,
806
+ simId: spec.simId,
807
+ streamId: spec.streamId
808
+ });
809
+ }
810
+ else if (outcome?.skippedReason !== undefined) {
811
+ events.push({
812
+ id: nextEventId(`blocked-${spec.roleId}`),
813
+ at: createdAt,
814
+ level: "warn",
815
+ type: "shared-world.session.blocked",
816
+ message: `Role ${spec.roleId} ${outcome.skippedReason}.`,
817
+ simId: spec.simId,
818
+ streamId: spec.streamId
819
+ });
820
+ }
821
+ else if (outcome?.sessionError) {
822
+ events.push({
823
+ id: nextEventId(`session-error-${spec.roleId}`),
824
+ at: createdAt,
825
+ level: "error",
826
+ type: "shared-world.session.error",
827
+ message: `Role ${spec.roleId}: ${outcome.sessionError}`,
828
+ simId: spec.simId,
829
+ streamId: spec.streamId
830
+ });
831
+ }
832
+ else {
833
+ events.push({
834
+ id: nextEventId(`contract-${spec.roleId}`),
835
+ at: createdAt,
836
+ level: "info",
837
+ type: "shared-world.contract.ready",
838
+ message: `Role ${spec.roleId}: dry-run contract role ready; switch scenario.mode to live for a real shared-world session.`,
839
+ simId: spec.simId,
840
+ streamId: spec.streamId
841
+ });
842
+ }
843
+ });
844
+ // Build the shared-world evidence block (the timeline + plane + attribution ceiling).
845
+ const seedDigest = args.seedDigest;
846
+ const planeCommit = dryRun ? undefined : args.subjectCommit;
847
+ const timeline = [args.baselineCheckpoint];
848
+ const sequence = [];
849
+ if (dryRun) {
850
+ // Declared (contract) timeline: every role would take a turn; recipe-digest checkpoints, no delta.
851
+ roleSpecs.forEach((spec) => {
852
+ timeline.push({
853
+ kind: "turn",
854
+ roleId: spec.roleId,
855
+ simId: spec.simId,
856
+ streamId: spec.streamId,
857
+ seedDigest
858
+ });
859
+ timeline.push(declaredCheckpointSnapshot(`cp-after-${spec.roleId}`, config.subject.state?.checkpoint ?? []));
860
+ sequence.push(spec.roleId);
861
+ });
862
+ }
863
+ else {
864
+ // Executed timeline: only roles that took a turn (skipped roles contribute nothing).
865
+ for (const outcome of roleOutcomes) {
866
+ if (outcome.skippedReason !== undefined || outcome.afterCheckpoint === undefined) {
867
+ continue;
868
+ }
869
+ timeline.push({
870
+ kind: "turn",
871
+ roleId: outcome.spec.roleId,
872
+ simId: outcome.spec.simId,
873
+ streamId: outcome.spec.streamId,
874
+ ...(planeCommit === undefined ? {} : { commit: planeCommit }),
875
+ seedDigest
876
+ });
877
+ timeline.push(outcome.afterCheckpoint);
878
+ sequence.push(outcome.spec.roleId);
879
+ }
880
+ }
881
+ const sharedWorld = {
882
+ schema: SHARED_WORLD_SCHEMA,
883
+ topology: "shared-world",
884
+ topologyMode: "sequential",
885
+ roleCount: roleSpecs.length,
886
+ plane: {
887
+ ...(planeCommit === undefined ? {} : { commit: planeCommit }),
888
+ seedDigest,
889
+ envNames: args.subject.envNames ?? []
890
+ },
891
+ sequence,
892
+ timeline,
893
+ attributionLimits: ["sequential-only", "no-concurrent-races", "delta-attributed-to-turn-not-action"]
894
+ };
895
+ events.push({
896
+ id: nextEventId("timeline"),
897
+ at: createdAt,
898
+ level: "info",
899
+ type: "shared-world.timeline",
900
+ message: `Interaction timeline: ${timeline.length} entries (${sequence.length} turn(s) interleaved with checkpoints); deltas observed on ${timeline.filter((entry) => entry.kind === "checkpoint" && entry.deltaFromPrev).map((entry) => entry.name).join(", ") || "none"}. Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}.`
901
+ });
902
+ if (args.failFastReason) {
903
+ events.push({
904
+ id: nextEventId("fail-fast"),
905
+ at: createdAt,
906
+ level: "warn",
907
+ type: "shared-world.fail-fast",
908
+ message: `Fail-fast: ${args.failFastReason}. Remaining roles were blocked; completed evidence is retained.`
909
+ });
910
+ }
911
+ // Worst-of review verdict across roles.
912
+ const verdict = dryRun
913
+ ? "contract_proof_only"
914
+ : (() => {
915
+ const allPassed = roleOutcomes.length === roleSpecs.length
916
+ && roleOutcomes.every((outcome) => sharedWorldRoleOutcomeOk(outcome, false));
917
+ if (allPassed)
918
+ return "pass";
919
+ const anyFail = roleOutcomes.some((outcome) => outcome.skippedReason !== undefined
920
+ || outcome.harnessError
921
+ || outcome.noEngagement
922
+ || outcome.sessionError !== undefined
923
+ || outcome.session === undefined
924
+ || outcome.session.status === "failed"
925
+ || outcome.session.status === "blocked");
926
+ if (anyFail)
927
+ return "fail";
928
+ if (roleOutcomes.some((outcome) => outcome.session?.status === "timed_out"))
929
+ return "timed_out";
930
+ return "fail";
931
+ })();
932
+ const passedRoles = roleOutcomes.filter((outcome) => sharedWorldRoleOutcomeOk(outcome, dryRun)).length;
933
+ const configuredBrowser = config.execution?.desktop?.browser;
934
+ const resolvedBrowsers = roleOutcomes
935
+ .map((outcome) => outcome.desktopBrowser?.resolved)
936
+ .filter((value) => value !== undefined);
937
+ const unanimousResolvedBrowser = resolvedBrowsers.length > 0 && new Set(resolvedBrowsers).size === 1
938
+ ? resolvedBrowsers[0]
939
+ : undefined;
940
+ const review = {
941
+ schema: REVIEW_SCHEMA,
942
+ verdict,
943
+ summary: dryRun
944
+ ? `Dry-run shared-world contract: ${roleSpecs.length} role(s) declared against ONE plane (${descriptor.id}) at ${appUrl}; no desktop launched, $0 spend.`
945
+ : `Shared-world run (ONE plane, sequential): ${passedRoles}/${roleSpecs.length} role(s) reached a terminal, engaged verdict; ${timeline.filter((entry) => entry.kind === "checkpoint" && entry.deltaFromPrev).length} checkpoint delta(s) observed.`,
946
+ gaps: dryRun
947
+ ? ["Live shared-world session not yet run (dry-run contract only)."]
948
+ : roleOutcomes
949
+ .filter((outcome) => outcome.skippedReason !== undefined
950
+ || outcome.sessionError !== undefined
951
+ || outcome.noEngagement
952
+ || outcome.session === undefined
953
+ || outcome.session.status !== "passed")
954
+ .map((outcome) => `${outcome.spec.roleId}: ${outcome.skippedReason ?? outcome.sessionError ?? outcome.session?.reason ?? "did not pass"}`)
955
+ };
956
+ const anyRaw = roleOutcomes.some((outcome) => outcome.session?.trace.redaction.screenshots === "raw");
957
+ const ranLive = roleOutcomes.some((outcome) => outcome.session !== undefined || outcome.sessionError !== undefined);
958
+ return {
959
+ schema: RUN_BUNDLE_SCHEMA,
960
+ runId: args.runId,
961
+ mode: dryRun ? "dry-run" : "live",
962
+ simCount: roleSpecs.length,
963
+ createdAt,
964
+ cwd: PUBLIC_TARGET_CWD,
965
+ artifactRoot: path.join(".humanish", "runs", args.runId),
966
+ source: args.source,
967
+ persona: {
968
+ id: roleSpecs[0]?.persona.id ?? "shared-world-role",
969
+ name: `Shared-world roster (${roleSpecs.length} roles)`,
970
+ source: `lab:${config.id}`,
971
+ sourceDigest: roleSpecs[0]?.persona.promptDigest ?? seedDigest
972
+ },
973
+ scenario: {
974
+ id: `shared-world-${config.id}`,
975
+ title: config.title ?? `Shared-world: ${config.id}`,
976
+ goal: roleSpecs[0]?.instructions ?? "Shared-world sequential interaction.",
977
+ source: `lab:${config.id}`,
978
+ sourceDigest: roleSpecs[0]?.persona.promptDigest ?? seedDigest
979
+ },
980
+ lifecycle: [
981
+ {
982
+ at: createdAt,
983
+ event: "shared-world.run.created",
984
+ message: `Created shared-world run with ONE shared plane and ${roleSpecs.length} sequential role seats (actor ${descriptor.id}).`
985
+ }
986
+ ],
987
+ simulations,
988
+ streams,
989
+ events,
990
+ redaction: {
991
+ status: "passed",
992
+ notes: ranLive
993
+ ? anyRaw
994
+ ? "Typed text recorded as length only and reasoning/messages pass through text redaction. Some roles captured FULL-FIDELITY (raw) screenshots, retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle. Checkpoints persist digest-only."
995
+ : "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are blurred at capture (policies.redactScreenshots: true) for a share-as-is bundle. Checkpoints persist digest-only."
996
+ : "Dry-run shared-world contract bundle: no desktop launched and no screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs. Checkpoints persist digest-only."
997
+ },
998
+ artifacts: {
999
+ run: "run.json",
1000
+ reviewJson: "review.json",
1001
+ reviewMarkdown: "review.md",
1002
+ observerData: "observer/observer-data.json",
1003
+ events: "events.ndjson"
1004
+ },
1005
+ review,
1006
+ feedbackCandidates: [],
1007
+ // Custom desktop image provenance (the ONE shared plane launched on it); omitted on the default.
1008
+ ...(config.execution?.desktop?.template === undefined ? {} : { desktopTemplate: config.execution.desktop.template }),
1009
+ ...(configuredBrowser === undefined
1010
+ ? {}
1011
+ : { desktopBrowser: { requested: configuredBrowser, ...(unanimousResolvedBrowser === undefined ? {} : { resolved: unanimousResolvedBrowser }) } }),
1012
+ subject: args.subject,
1013
+ attributionClass: "shared-world",
1014
+ sharedWorld
1015
+ };
1016
+ }
1017
+ function sharedWorldRoleOutcomeOk(outcome, dryRun) {
1018
+ if (dryRun)
1019
+ return true;
1020
+ if (!outcome || outcome.skippedReason !== undefined)
1021
+ return false;
1022
+ return outcome.session !== undefined
1023
+ && outcome.session.status === "passed"
1024
+ && outcome.session.completionReason !== "harness_error"
1025
+ && outcome.sessionError === undefined
1026
+ && !outcome.noEngagement;
1027
+ }
1028
+ function renderSharedWorldReviewMarkdown(bundle) {
1029
+ const plane = bundle.events.find((event) => event.type === "shared-world.plane.provenance");
1030
+ const timeline = bundle.events.find((event) => event.type === "shared-world.timeline");
1031
+ return [
1032
+ `# ${bundle.scenario.title}`,
1033
+ "",
1034
+ `- run: ${bundle.runId}`,
1035
+ `- mode: ${bundle.mode}`,
1036
+ `- attribution class: ${bundle.attributionClass ?? "isolated"}`,
1037
+ `- topology: ${bundle.sharedWorld?.topology ?? "(none)"}`,
1038
+ `- roles: ${bundle.sharedWorld?.roleCount ?? 0}; sequence: ${(bundle.sharedWorld?.sequence ?? []).join(" → ") || "(none)"}`,
1039
+ `- verdict: ${bundle.review.verdict}`,
1040
+ `- summary: ${bundle.review.summary}`,
1041
+ ...(plane ? [`- plane: ${plane.message}`] : []),
1042
+ ...(timeline ? [`- timeline: ${timeline.message}`] : []),
1043
+ ...(bundle.sharedWorld ? [`- attribution limits: ${bundle.sharedWorld.attributionLimits.join(", ")}`] : []),
1044
+ ...(bundle.review.gaps.length > 0 ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)] : []),
1045
+ ""
1046
+ ].join("\n");
1047
+ }
1048
+ //# sourceMappingURL=shared-world-lab.js.map