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.
Files changed (205) hide show
  1. package/AGENTS.md +77 -0
  2. package/CONTRIBUTING.md +39 -0
  3. package/LICENSE +21 -0
  4. package/README.md +437 -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 +363 -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 +357 -0
  32. package/dist/codex-app-server-ui.js.map +1 -0
  33. package/dist/codex-app-server.d.ts +144 -0
  34. package/dist/codex-app-server.js +754 -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 +1094 -0
  50. package/dist/concurrent-shared-world-lab.js.map +1 -0
  51. package/dist/core/git-state.d.ts +33 -0
  52. package/dist/core/git-state.js +277 -0
  53. package/dist/core/git-state.js.map +1 -0
  54. package/dist/core/git-workspace.d.ts +31 -0
  55. package/dist/core/git-workspace.js +447 -0
  56. package/dist/core/git-workspace.js.map +1 -0
  57. package/dist/core/index.d.ts +4 -0
  58. package/dist/core/index.js +3 -0
  59. package/dist/core/index.js.map +1 -0
  60. package/dist/core/run-primitives.d.ts +66 -0
  61. package/dist/core/run-primitives.js +120 -0
  62. package/dist/core/run-primitives.js.map +1 -0
  63. package/dist/cua-actor-lab.d.ts +621 -0
  64. package/dist/cua-actor-lab.js +2845 -0
  65. package/dist/cua-actor-lab.js.map +1 -0
  66. package/dist/device-presets.d.ts +67 -0
  67. package/dist/device-presets.js +50 -0
  68. package/dist/device-presets.js.map +1 -0
  69. package/dist/e2b-desktop-executor.d.ts +101 -0
  70. package/dist/e2b-desktop-executor.js +309 -0
  71. package/dist/e2b-desktop-executor.js.map +1 -0
  72. package/dist/e2b-desktop-launch.d.ts +144 -0
  73. package/dist/e2b-desktop-launch.js +59 -0
  74. package/dist/e2b-desktop-launch.js.map +1 -0
  75. package/dist/e2b-detached.d.ts +53 -0
  76. package/dist/e2b-detached.js +130 -0
  77. package/dist/e2b-detached.js.map +1 -0
  78. package/dist/e2b-terminal-lab.d.ts +345 -0
  79. package/dist/e2b-terminal-lab.js +1532 -0
  80. package/dist/e2b-terminal-lab.js.map +1 -0
  81. package/dist/env-file.d.ts +14 -0
  82. package/dist/env-file.js +108 -0
  83. package/dist/env-file.js.map +1 -0
  84. package/dist/feedback.d.ts +51 -0
  85. package/dist/feedback.js +351 -0
  86. package/dist/feedback.js.map +1 -0
  87. package/dist/image-evidence.d.ts +2 -0
  88. package/dist/image-evidence.js +33 -0
  89. package/dist/image-evidence.js.map +1 -0
  90. package/dist/index.d.ts +63 -0
  91. package/dist/index.js +33 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/init-templates.d.ts +12 -0
  94. package/dist/init-templates.js +369 -0
  95. package/dist/init-templates.js.map +1 -0
  96. package/dist/init.d.ts +26 -0
  97. package/dist/init.js +429 -0
  98. package/dist/init.js.map +1 -0
  99. package/dist/lab-app-runner.d.ts +78 -0
  100. package/dist/lab-app-runner.js +403 -0
  101. package/dist/lab-app-runner.js.map +1 -0
  102. package/dist/lab-config.d.ts +583 -0
  103. package/dist/lab-config.js +1789 -0
  104. package/dist/lab-config.js.map +1 -0
  105. package/dist/lab-engine.d.ts +77 -0
  106. package/dist/lab-engine.js +216 -0
  107. package/dist/lab-engine.js.map +1 -0
  108. package/dist/lab-preflight.d.ts +67 -0
  109. package/dist/lab-preflight.js +385 -0
  110. package/dist/lab-preflight.js.map +1 -0
  111. package/dist/labs.d.ts +53 -0
  112. package/dist/labs.js +382 -0
  113. package/dist/labs.js.map +1 -0
  114. package/dist/observer-assets.d.ts +2 -0
  115. package/dist/observer-assets.js +2630 -0
  116. package/dist/observer-assets.js.map +1 -0
  117. package/dist/observer-data.d.ts +63 -0
  118. package/dist/observer-data.js +145 -0
  119. package/dist/observer-data.js.map +1 -0
  120. package/dist/observer-static.d.ts +39 -0
  121. package/dist/observer-static.js +306 -0
  122. package/dist/observer-static.js.map +1 -0
  123. package/dist/observer.d.ts +46 -0
  124. package/dist/observer.js +550 -0
  125. package/dist/observer.js.map +1 -0
  126. package/dist/openai-responses-cu.d.ts +113 -0
  127. package/dist/openai-responses-cu.js +545 -0
  128. package/dist/openai-responses-cu.js.map +1 -0
  129. package/dist/oss-lab.d.ts +52 -0
  130. package/dist/oss-lab.js +344 -0
  131. package/dist/oss-lab.js.map +1 -0
  132. package/dist/oss-meta-lab-scoring.d.ts +15 -0
  133. package/dist/oss-meta-lab-scoring.js +145 -0
  134. package/dist/oss-meta-lab-scoring.js.map +1 -0
  135. package/dist/oss-meta-lab.d.ts +277 -0
  136. package/dist/oss-meta-lab.js +4529 -0
  137. package/dist/oss-meta-lab.js.map +1 -0
  138. package/dist/oss-remote-telemetry.d.ts +77 -0
  139. package/dist/oss-remote-telemetry.js +403 -0
  140. package/dist/oss-remote-telemetry.js.map +1 -0
  141. package/dist/persona.d.ts +35 -0
  142. package/dist/persona.js +133 -0
  143. package/dist/persona.js.map +1 -0
  144. package/dist/pi-agent-core.d.ts +77 -0
  145. package/dist/pi-agent-core.js +203 -0
  146. package/dist/pi-agent-core.js.map +1 -0
  147. package/dist/program.d.ts +54 -0
  148. package/dist/program.js +2349 -0
  149. package/dist/program.js.map +1 -0
  150. package/dist/redaction.d.ts +90 -0
  151. package/dist/redaction.js +292 -0
  152. package/dist/redaction.js.map +1 -0
  153. package/dist/run-paths.d.ts +56 -0
  154. package/dist/run-paths.js +340 -0
  155. package/dist/run-paths.js.map +1 -0
  156. package/dist/run.d.ts +794 -0
  157. package/dist/run.js +4844 -0
  158. package/dist/run.js.map +1 -0
  159. package/dist/scripted-browser-actor.d.ts +297 -0
  160. package/dist/scripted-browser-actor.js +1399 -0
  161. package/dist/scripted-browser-actor.js.map +1 -0
  162. package/dist/scripted-browser-lab.d.ts +107 -0
  163. package/dist/scripted-browser-lab.js +788 -0
  164. package/dist/scripted-browser-lab.js.map +1 -0
  165. package/dist/selected-output-paths.d.ts +49 -0
  166. package/dist/selected-output-paths.js +354 -0
  167. package/dist/selected-output-paths.js.map +1 -0
  168. package/dist/shared-world-lab.d.ts +208 -0
  169. package/dist/shared-world-lab.js +1049 -0
  170. package/dist/shared-world-lab.js.map +1 -0
  171. package/dist/source-archive.d.ts +82 -0
  172. package/dist/source-archive.js +408 -0
  173. package/dist/source-archive.js.map +1 -0
  174. package/dist/stop-conditions.d.ts +26 -0
  175. package/dist/stop-conditions.js +66 -0
  176. package/dist/stop-conditions.js.map +1 -0
  177. package/dist/terminal-agent-actor.d.ts +36 -0
  178. package/dist/terminal-agent-actor.js +23 -0
  179. package/dist/terminal-agent-actor.js.map +1 -0
  180. package/docs/architecture/actor-contract.md +431 -0
  181. package/docs/architecture/github-feedback-loop.md +189 -0
  182. package/docs/architecture/local-codex-tui-actor.md +213 -0
  183. package/docs/architecture/observer.md +118 -0
  184. package/docs/architecture/oss-lab-poc.md +250 -0
  185. package/docs/architecture/project-layout.md +166 -0
  186. package/docs/architecture/state-driven-executor.md +203 -0
  187. package/docs/architecture/terminal-product-lane.md +148 -0
  188. package/docs/contracts/adapter-fixtures.md +80 -0
  189. package/docs/contracts/core.md +71 -0
  190. package/docs/contracts/feedback.md +145 -0
  191. package/docs/contracts/policy.md +311 -0
  192. package/docs/contracts/run-bundle.md +358 -0
  193. package/docs/contracts/schemas.md +984 -0
  194. package/docs/goals/current.md +428 -0
  195. package/docs/principles/invariants-and-defaults.md +135 -0
  196. package/docs/principles/self-driving-harness.md +129 -0
  197. package/docs/product/open-source-install-experience.md +237 -0
  198. package/docs/ramp/README.md +207 -0
  199. package/docs/release/open-source-readiness.md +208 -0
  200. package/docs/release/public-readiness-standard.md +205 -0
  201. package/docs/roadmap/world-class-open-source-v0.md +311 -0
  202. package/package.json +102 -8
  203. package/skills/humanish/SKILL.md +244 -0
  204. package/skills/humanish/agents/openai.yaml +7 -0
  205. package/index.js +0 -1
@@ -0,0 +1,1094 @@
1
+ // The CONCURRENT shared-world lab backend (#164 phase 2): N persona lanes drive ONE shared,
2
+ // mutable service plane SIMULTANEOUSLY — the actual leverage of a sim. A recomposition of shipped
3
+ // pieces + the getHost wrapper:
4
+ //
5
+ // - ONE SUBJECT sandbox: provisionCloneSubject ONCE (clone+install+build+seed) + serve on
6
+ // 0.0.0.0, exposed via getHost(port) → a tokenless reachable URL (the headless service host;
7
+ // no GUI seat).
8
+ // - N ACTOR desktop sandboxes: fan-out's runCuaLane machinery (per-lane device/persona, by-id
9
+ // teardown) bounded by execution.concurrency, each browser pointed at the getHost URL —
10
+ // driving the shared service AT THE SAME TIME. INDEPENDENT (FIX-11): no pipeline gate, no
11
+ // fail-fast — one actor's failure must not block the swarm or corrupt the "M of N" outcomes.
12
+ // - A background prober snapshots the subject DB checkpoint digests on a cadence → a stateSeries
13
+ // of the shared world evolving under load.
14
+ // - ALL N+1 sandboxes torn down BY exact id in a finally — NEVER Sandbox.list.
15
+ //
16
+ // HONEST ATTRIBUTION (verify-enforced, doctrine-audit fixes incorporated): the bundle declares
17
+ // attributionClass: shared-world + a CONCURRENT humanish.shared-world.v1 block (topologyMode
18
+ // "concurrent"; laneWindows + stateSeries + outcomes; NO timeline) whose attributionLimits drop
19
+ // `sequential-only`/`no-concurrent-races` and add `concurrent`,
20
+ // `best-effort-causal-attribution`, `non-deterministic-shared-state`,
21
+ // `window-and-snapshot-granularity`, `contention-observed-not-proven-safe`,
22
+ // `state-change-not-isolated-to-actors`. laneWindows + stateSeries are INDEPENDENT series with NO
23
+ // per-delta→actor field — causation under concurrency is structurally inexpressible.
24
+ //
25
+ // CAPABILITY vs PROOF (FIX-1): the deterministic $0 gate proves the PLUMBING + the honesty
26
+ // contract — the real mapWithConcurrency produces genuinely overlapping laneWindows (a rendezvous
27
+ // latch in the fake session forces two lane fns in-flight while the REAL orchestrator clock
28
+ // measures the windows). It does NOT prove "we ran many concurrent users at scale" — that
29
+ // CAPABILITY is backed only by the deferred, separately-authorized live receipt.
30
+ //
31
+ // Synthetic-subject (FIX-3): a getHost URL is internet-reachable for the run, so this route is
32
+ // synthetic-seeded-subjects ONLY. Verify fail-closes on subject.state.provenance != "seeded" and
33
+ // requires the author attestation subject.exposure: synthetic. This is author-trust + a provenance
34
+ // gate, NOT a no-real-data guarantee (Humanish cannot tell synthetic from real data).
35
+ import { randomBytes } from "node:crypto";
36
+ import path from "node:path";
37
+ import { pathToFileURL } from "node:url";
38
+ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
39
+ import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
40
+ import { toErrorMessage } from "./command-failure.js";
41
+ import { mapWithConcurrency } from "./concurrency.js";
42
+ import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
43
+ import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
44
+ import { concurrentSharedWorldValidationReason } from "./lab-config.js";
45
+ import { buildObserverData } from "./observer-data.js";
46
+ import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
47
+ import { redactText } from "./redaction.js";
48
+ import { prepareRunArtifactPaths, validatePreparedRunArtifactPaths } from "./run-paths.js";
49
+ import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
50
+ import { combineCheckpointDigest, runCheckpointSnapshot, seedRecipeDigest } from "./shared-world-lab.js";
51
+ import { buildRunSource, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, SHARED_WORLD_SCHEMA } from "./run.js";
52
+ export const CONCURRENT_SHARED_WORLD_LAB_SCHEMA = "humanish.concurrent-shared-world-lab-result.v1";
53
+ export const CONCURRENT_SHARED_WORLD_PROVIDER_METADATA = {
54
+ mode: "concurrent-shared-world-lab",
55
+ tool: "humanish"
56
+ };
57
+ // The verify-enforced CONCURRENT attribution ceiling (FIX-5). Mirrored in run.ts's required set.
58
+ export const CONCURRENT_ATTRIBUTION_LIMITS = [
59
+ "concurrent",
60
+ "best-effort-causal-attribution",
61
+ "non-deterministic-shared-state",
62
+ "window-and-snapshot-granularity",
63
+ "contention-observed-not-proven-safe",
64
+ "state-change-not-isolated-to-actors"
65
+ ];
66
+ const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
67
+ const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
68
+ const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
69
+ const DEFAULT_STATE_STEP_TIMEOUT_MS = 5 * 60_000;
70
+ const DEFAULT_PROBER_CADENCE_MS = 1000;
71
+ const DEFAULT_MISSION = "You are one of MANY users hitting a shared web application at the same time. The browser is already open at the app. Accomplish your role's task, then stop.";
72
+ function readPositiveInt(value, fallback) {
73
+ if (value === undefined)
74
+ return fallback;
75
+ const parsed = Number.parseInt(value, 10);
76
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
77
+ }
78
+ function makeRunId() {
79
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
80
+ return `concurrent-shared-world-${stamp}-${randomBytes(4).toString("hex")}`;
81
+ }
82
+ /** Extract the in-sandbox port from the (loopback) serve.url so getHost can expose it. */
83
+ function servePort(serveUrl) {
84
+ const url = new URL(serveUrl);
85
+ if (url.port)
86
+ return Number(url.port);
87
+ return url.protocol === "https:" ? 443 : 80;
88
+ }
89
+ /** Resolve an actor's seat URL against the harness-minted getHost base (entry is a same-origin
90
+ * relative path, validated at parse against serve.url). */
91
+ function resolveActorSeatUrl(baseUrl, entry) {
92
+ if (!entry)
93
+ return baseUrl;
94
+ try {
95
+ return new URL(entry, baseUrl).toString();
96
+ }
97
+ catch {
98
+ return baseUrl;
99
+ }
100
+ }
101
+ /** A getHost URL must be TOKENLESS (no userinfo, no query — no authKey; invariant 1). */
102
+ function isTokenlessHost(value) {
103
+ try {
104
+ const url = new URL(value);
105
+ return url.username === "" && url.password === "" && url.search === "";
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ /** sha256-16 of a URL's ORIGIN — the publish-safe host identity persisted in the bundle (the raw
112
+ * getHost URL embeds the live sandbox id + matches the e2b-URL redaction, so it never lands raw). */
113
+ function hostOriginDigest(url) {
114
+ try {
115
+ return commandDigestOf(new URL(url).origin);
116
+ }
117
+ catch {
118
+ return commandDigestOf(url);
119
+ }
120
+ }
121
+ /** A public-safe, human-readable route label for the bundle (host redacted to a placeholder; the
122
+ * entry path kept). Never contains the raw getHost URL. */
123
+ function publicSafeRouteLabel(entry) {
124
+ return `[provisioned-subject]${entry ?? "/"}`;
125
+ }
126
+ /**
127
+ * Build the ONE subject sandbox's provenance (invariant 5): clone (repo + optional commit) or
128
+ * local-tree (archiveSha256 + optional commit/dirty from the once-per-run host-packed archive -
129
+ * archiveSha256 IS the pin; there is only ONE archive, so no per-lane unanimity math applies,
130
+ * unlike the cua fan-out route). Used for both the in-progress and final bundle: the archive
131
+ * never changes mid-run (packed before any sandbox exists).
132
+ */
133
+ function buildSubjectProvenance(args) {
134
+ if (args.localTreeRoute) {
135
+ return {
136
+ source: "local-tree",
137
+ ...(args.localTreeArchive === undefined ? {} : { archiveSha256: args.localTreeArchive.archiveSha256 }),
138
+ ...(args.subjectCommit === undefined ? {} : { commit: args.subjectCommit }),
139
+ ...(args.localTreeArchive?.git === undefined ? {} : { dirty: args.localTreeArchive.git.dirty }),
140
+ envNames: args.subjectEnvNames,
141
+ state: args.state
142
+ };
143
+ }
144
+ return {
145
+ source: "clone",
146
+ repo: args.publicRepo,
147
+ ...(args.subjectCommit === undefined ? {} : { commit: args.subjectCommit }),
148
+ envNames: args.subjectEnvNames,
149
+ state: args.state
150
+ };
151
+ }
152
+ function laneTaxonomyLabel(spec) {
153
+ const parts = [
154
+ spec.actorType ? `type:${spec.actorType}` : undefined,
155
+ spec.surface ? `surface:${spec.surface}` : undefined,
156
+ spec.caseGroup ? `case:${spec.caseGroup}` : undefined
157
+ ].filter((part) => part !== undefined);
158
+ return parts.length > 0 ? ` (${parts.join(" / ")})` : "";
159
+ }
160
+ /** Build one actor lane's CuaLaneSpec from a roster role (per-actor device IS honored here — each
161
+ * actor has its OWN desktop, unlike the sequential one-sandbox PoC). */
162
+ function buildActorSpec(config, role, index) {
163
+ const mission = config.actors[0]?.mission ?? DEFAULT_MISSION;
164
+ const device = resolveLaneDevice(config, role);
165
+ const composed = composeLaneInstructions({
166
+ mission,
167
+ ...(role.persona === undefined ? {} : { persona: role.persona }),
168
+ ...(role.instruction === undefined ? {} : { instruction: role.instruction }),
169
+ device: { name: device.name, preset: device.preset }
170
+ });
171
+ const roleId = role.id ?? `role-${String(index + 1).padStart(2, "0")}`;
172
+ const streamId = `stream-${String(index + 1).padStart(3, "0")}`;
173
+ return {
174
+ laneId: roleId,
175
+ ...(role.actorType === undefined ? {} : { actorType: role.actorType }),
176
+ ...(role.surface === undefined ? {} : { surface: role.surface }),
177
+ ...(role.caseGroup === undefined ? {} : { caseGroup: role.caseGroup }),
178
+ laneIndex: index,
179
+ simId: `sim-${String(index + 1).padStart(3, "0")}`,
180
+ streamId,
181
+ persona: composed.persona,
182
+ instructions: composed.instructions,
183
+ ...((role.stopWhen ?? config.actors[0]?.stopWhen) === undefined ? {} : { stopWhen: (role.stopWhen ?? config.actors[0]?.stopWhen) }),
184
+ deviceName: device.name,
185
+ devicePreset: device.preset,
186
+ resolution: device.resolution,
187
+ screenshotDir: roleId,
188
+ traceArtifactPath: `actors/${streamId}.json`
189
+ };
190
+ }
191
+ async function writeConcurrentRunArtifacts(bundle, preparedRunPaths) {
192
+ const runPaths = await validatePreparedRunArtifactPaths(preparedRunPaths);
193
+ const publicBundle = {
194
+ ...bundle,
195
+ cwd: PUBLIC_TARGET_CWD
196
+ };
197
+ await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(publicBundle, null, 2)}\n`, "utf8");
198
+ await writeContainedOutputFile(runPaths, "review.json", `${JSON.stringify(publicBundle.review, null, 2)}\n`, "utf8");
199
+ await writeContainedOutputFile(runPaths, "review.md", renderConcurrentReviewMarkdown(publicBundle), "utf8");
200
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
201
+ await writeContainedOutputFile(runPaths, "observer/observer-data.json", `${JSON.stringify(buildObserverData(publicBundle), null, 2)}\n`, "utf8");
202
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
203
+ schema: "humanish.latest-run.v1",
204
+ runId: publicBundle.runId,
205
+ path: runPaths.relativeRunRoot,
206
+ updatedAt: new Date().toISOString()
207
+ }, null, 2)}\n`, "utf8");
208
+ }
209
+ function observerResultForConcurrentArtifacts(cwd, runId, artifactRoot, warnings = []) {
210
+ const observerPath = path.join(artifactRoot, "observer", "index.html");
211
+ const observerDataPath = path.join(artifactRoot, "observer", "observer-data.json");
212
+ const eventsPath = path.join(artifactRoot, "events.ndjson");
213
+ return {
214
+ schema: "humanish.observer-result.v1",
215
+ ok: true,
216
+ cwd,
217
+ run: runId,
218
+ observerPath: path.relative(cwd, observerPath),
219
+ observerDataPath: path.relative(cwd, observerDataPath),
220
+ eventsPath: path.relative(cwd, eventsPath),
221
+ observerUrl: pathToFileURL(observerPath).href,
222
+ bundlePath: path.join(artifactRoot, "run.json"),
223
+ opened: false,
224
+ warnings
225
+ };
226
+ }
227
+ export async function runConcurrentSharedWorld(options) {
228
+ const { config, dryRun } = options;
229
+ const cwd = path.resolve(options.cwd);
230
+ const hooks = options.hooks ?? {};
231
+ const env = hooks.env ?? process.env;
232
+ const render = hooks.renderObserverFn ?? renderObserver;
233
+ const actorType = config.actors[0]?.type ?? "";
234
+ const concurrency = config.execution?.concurrency ?? 1;
235
+ const roles = config.actors[0]?.lanes ?? [];
236
+ const fail = (code, message, actorLabel) => ({
237
+ schema: CONCURRENT_SHARED_WORLD_LAB_SCHEMA,
238
+ ok: false,
239
+ cwd,
240
+ labId: config.id,
241
+ actor: actorLabel ?? actorType,
242
+ topology: "shared-world",
243
+ topologyMode: "concurrent",
244
+ roleCount: roles.length,
245
+ concurrency,
246
+ dryRun,
247
+ runId: options.runId ?? "not-created",
248
+ roles: [],
249
+ warnings: [],
250
+ error: { code, message }
251
+ });
252
+ const descriptor = actorRegistry[actorType];
253
+ if (!descriptor || !isCuaActorDescriptor(descriptor)) {
254
+ return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
255
+ }
256
+ // Re-enforce the concurrent cross-validation (library API surface).
257
+ const invalidReason = concurrentSharedWorldValidationReason(config);
258
+ if (invalidReason) {
259
+ return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_INVALID", invalidReason, descriptor.id);
260
+ }
261
+ const serve = config.subject.serve;
262
+ const localTreeRoute = config.subject.source === "local-tree";
263
+ const subjectRepo = config.subject.repos?.[0] ?? "";
264
+ const subjectEnvNames = config.subject.env ?? [];
265
+ const checkpoints = config.subject.state?.checkpoint ?? [];
266
+ const runSession = hooks.runSession ?? descriptor.runSession;
267
+ const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
268
+ const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
269
+ const knownSecretValues = [
270
+ openaiApiKey,
271
+ e2bApiKey,
272
+ ...subjectEnvNames.map((name) => env[name] ?? ""),
273
+ ...checkpoints.flatMap((probe) => probe.redact ?? [])
274
+ ].filter((value) => value.length >= 4);
275
+ const scrubKnownValues = (text) => knownSecretValues.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
276
+ const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
277
+ const publicRepo = redactRepoLabel ? "repo-01" : subjectRepo;
278
+ const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
279
+ if (!dryRun) {
280
+ const missingKeys = [
281
+ ...(openaiApiKey ? [] : ["OPENAI_API_KEY"]),
282
+ ...(e2bApiKey ? [] : ["E2B_API_KEY"])
283
+ ];
284
+ if (missingKeys.length > 0) {
285
+ return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_KEYS_MISSING", `Live concurrent shared-world labs need ${missingKeys.join(" and ")} in the environment (pass via --env-file; values are never persisted).`, descriptor.id);
286
+ }
287
+ const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
288
+ if (missingSubjectEnv.length > 0) {
289
+ return fail("HUMANISH_CONCURRENT_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);
290
+ }
291
+ }
292
+ const runId = options.runId ?? makeRunId();
293
+ const runPaths = await prepareRunArtifactPaths(cwd, runId);
294
+ const artifactRoot = runPaths.absoluteRunRoot;
295
+ const physicalArtifactRoot = runPaths.physicalRunRoot;
296
+ const createdAt = new Date().toISOString();
297
+ const timeoutMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
298
+ const requestTimeoutMs = readPositiveInt(env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
299
+ const redactScreenshots = config.policies?.redactScreenshots === true;
300
+ const timers = hooks.detachedTimers ?? {};
301
+ const now = hooks.now ?? Date.now;
302
+ const proberCadenceMs = hooks.proberCadenceMs ?? DEFAULT_PROBER_CADENCE_MS;
303
+ const seedDigest = seedRecipeDigest(config);
304
+ const source = await buildRunSource({ capturedAt: createdAt, cwd, humanishSource: "present", packageName: "humanish" });
305
+ const warnings = [];
306
+ const stateStepRecords = [];
307
+ const stateSnapshots = [];
308
+ const actorSpecs = roles.map((role, i) => buildActorSpec(config, role, i));
309
+ let actorResults = [];
310
+ let subjectCommit;
311
+ let subjectSandboxId;
312
+ let subjectKilled = false;
313
+ let getHostUrl;
314
+ let runError;
315
+ let snapshotIndex = 0;
316
+ let liveObserver;
317
+ const runtimeStreamUrls = [];
318
+ // Pack the working tree ONCE per run, on the host, BEFORE the subject sandbox is created
319
+ // (mirrors the sequential route + the cua route's ordering): a packing failure fails the run
320
+ // closed here, never spending sandbox cost. Dry-run packs nothing.
321
+ let localTreeArchive;
322
+ let localTreeArchiveBuffer;
323
+ if (localTreeRoute && !dryRun) {
324
+ const packLocalTree = hooks.packLocalTree ?? defaultPackLocalTree;
325
+ try {
326
+ const packed = await packLocalTree({
327
+ root: cwd,
328
+ ...(config.subject.localTree?.exclude === undefined ? {} : { extraExclude: config.subject.localTree.exclude }),
329
+ ...(config.subject.localTree?.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: config.subject.localTree.maxArchiveBytes })
330
+ });
331
+ localTreeArchive = packed.archive;
332
+ localTreeArchiveBuffer = packed.buffer;
333
+ process.stderr.write(`humanish concurrent shared-world local-tree: packed ${packed.archive.fileCount} entries, ${packed.archive.totalBytes} bytes, archiveSha256 ${packed.archive.archiveSha256}`
334
+ + `${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`);
335
+ }
336
+ catch (error) {
337
+ return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
338
+ }
339
+ }
340
+ if (!dryRun) {
341
+ let subjectModule;
342
+ let subjectDesktop;
343
+ // Background prober dispose signal (FIX-9: cleared in finally).
344
+ let proberDisposed = false;
345
+ let releaseDispose = () => { };
346
+ const disposeSignal = new Promise((resolve) => { releaseDispose = resolve; });
347
+ let proberLoop;
348
+ const proberSnapshot = async () => {
349
+ if (!subjectDesktop)
350
+ return;
351
+ const timestamp = now();
352
+ const idx = snapshotIndex;
353
+ snapshotIndex += 1;
354
+ const snapshot = await runCheckpointSnapshot({
355
+ desktop: subjectDesktop,
356
+ snapshotIndex: idx,
357
+ name: `state-${idx}`,
358
+ checkpoints,
359
+ prevDigest: undefined,
360
+ scrub: scrubKnownValues,
361
+ requestTimeoutMs,
362
+ timers
363
+ });
364
+ stateSnapshots.push({ timestamp, digest: snapshot.digest });
365
+ };
366
+ try {
367
+ subjectModule = await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
368
+ // The ONE subject sandbox: headless service host (no GUI seat). The SUBJECT env is provisioned
369
+ // HERE; the actor sandboxes get NONE of it (FIX-10). A custom desktop template (image) is
370
+ // honored on BOTH the subject sandbox (here) and every actor sandbox (via runCuaLane, which
371
+ // reads the same config); absent keeps the byte-stable Sandbox.create(opts) default.
372
+ subjectDesktop = await createDesktopSandbox(subjectModule, {
373
+ apiKey: e2bApiKey,
374
+ requestTimeoutMs,
375
+ timeoutMs: timeoutMs + SUBJECT_PROVISION_BUDGET_MS
376
+ + (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0)
377
+ + SANDBOX_TIMEOUT_BUFFER_MS,
378
+ metadata: {
379
+ ...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
380
+ labId: config.id,
381
+ topology: "shared-world",
382
+ topologyMode: "concurrent",
383
+ role: "subject",
384
+ roleCount: String(roles.length)
385
+ },
386
+ ...(subjectEnvNames.length > 0
387
+ ? { envs: Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])) }
388
+ : {}),
389
+ dpi: 96,
390
+ lifecycle: { onTimeout: "kill" }
391
+ }, config.execution?.desktop?.template);
392
+ subjectSandboxId = subjectDesktop.sandboxId;
393
+ if (hooks.prepareDesktop) {
394
+ await hooks.prepareDesktop(subjectDesktop);
395
+ }
396
+ // Provision the ONE shared plane: clone + install/build + seed + serve on 0.0.0.0 + probe
397
+ // (clone route), or upload/extract the once-per-run packed archive + the SAME shared serve
398
+ // pipeline (local-tree route).
399
+ const onSubjectPhase = hooks.onPhase ?? ((event) => {
400
+ process.stderr.write(`humanish shared-world (concurrent): ${event.message}${event.durationMs === undefined ? "" : ` (${event.durationMs}ms)`}\n`);
401
+ });
402
+ if (localTreeRoute) {
403
+ await provisionLocalTreeSubject(subjectDesktop, {
404
+ archiveBuffer: localTreeArchiveBuffer,
405
+ serve,
406
+ ...(config.subject.state === undefined ? {} : { state: config.subject.state }),
407
+ requestTimeoutMs,
408
+ scrub: scrubKnownValues,
409
+ onStateStep: (record) => { stateStepRecords.push(record); },
410
+ onPhase: onSubjectPhase,
411
+ ...timers
412
+ });
413
+ }
414
+ else {
415
+ subjectCommit = await provisionCloneSubject(subjectDesktop, {
416
+ repo: subjectRepo,
417
+ depth: config.subject.clone?.depth ?? 1,
418
+ serve,
419
+ ...(config.subject.state === undefined ? {} : { state: config.subject.state }),
420
+ hasGithubToken,
421
+ requestTimeoutMs,
422
+ scrub: scrubKnownValues,
423
+ onCommit: (commit) => { subjectCommit = commit; },
424
+ onStateStep: (record) => { stateStepRecords.push(record); },
425
+ onPhase: onSubjectPhase,
426
+ ...timers
427
+ });
428
+ }
429
+ // Expose the served port via getHost (FIX-2). Fail closed if the SDK lacks it.
430
+ if (typeof subjectDesktop.getHost !== "function") {
431
+ throw new Error("the installed @e2b/desktop SDK does not expose getHost(port); the concurrent shared-world route requires it to reach the subject plane");
432
+ }
433
+ // getHost returns a BARE host (e.g. "3000-<sandboxId>.e2b.app", no scheme); e2b exposes the
434
+ // port over https. Normalize to a full URL before the tokenless check + before persisting.
435
+ const rawHost = subjectDesktop.getHost(servePort(serve.url));
436
+ const hostUrl = /^https?:\/\//i.test(rawHost) ? rawHost : `https://${rawHost}`;
437
+ if (!isTokenlessHost(hostUrl)) {
438
+ throw new Error("getHost returned a non-tokenless URL; refusing to persist a host URL that may carry a credential (invariant 1)");
439
+ }
440
+ getHostUrl = hostUrl;
441
+ // Baseline state snapshot, then start the background cadence prober.
442
+ await proberSnapshot();
443
+ if (options.onObserverReady) {
444
+ const inProgressPlaneCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
445
+ const inProgressSubject = buildSubjectProvenance({
446
+ localTreeRoute,
447
+ publicRepo,
448
+ subjectCommit: inProgressPlaneCommit,
449
+ localTreeArchive,
450
+ subjectEnvNames,
451
+ state: resolveSubjectState({ declared: config.subject.state, dryRun: false, executed: stateStepRecords })
452
+ });
453
+ const inProgressBundle = buildConcurrentSharedWorldBundle({
454
+ config,
455
+ descriptor,
456
+ createdAt,
457
+ dryRun: false,
458
+ inProgress: true,
459
+ runId,
460
+ source,
461
+ roles,
462
+ actorSpecs,
463
+ actorResults: [],
464
+ stateSnapshots,
465
+ subject: inProgressSubject,
466
+ seedDigest,
467
+ ...(inProgressPlaneCommit === undefined ? {} : { subjectCommit: inProgressPlaneCommit }),
468
+ hostDigest: hostOriginDigest(getHostUrl)
469
+ });
470
+ await writeConcurrentRunArtifacts(inProgressBundle, runPaths);
471
+ liveObserver = observerResultForConcurrentArtifacts(cwd, runId, artifactRoot, [
472
+ "Live concurrent shared-world Observer is attached before final verification; stream auth URLs are runtime-only and are not persisted."
473
+ ]);
474
+ await options.onObserverReady(liveObserver);
475
+ }
476
+ proberLoop = (async () => {
477
+ while (!proberDisposed) {
478
+ let timer;
479
+ await Promise.race([
480
+ new Promise((resolve) => { timer = setTimeout(resolve, proberCadenceMs); }),
481
+ disposeSignal
482
+ ]);
483
+ if (timer)
484
+ clearTimeout(timer); // FIX-9: no dangling prober timer.
485
+ if (proberDisposed)
486
+ break;
487
+ await proberSnapshot().catch(() => undefined);
488
+ }
489
+ })();
490
+ // Launch N actor sandboxes CONCURRENTLY, INDEPENDENT (FIX-11: runCuaLane + mapWithConcurrency,
491
+ // NOT runCuaLanes — no pipeline gate / fail-fast). Each actor's window is measured on the ONE
492
+ // orchestrator clock (FIX-1). cloneRoute=false + subjectEnvNames=[] keep subject creds out of
493
+ // every actor sandbox (FIX-10).
494
+ const cuaHooks = {
495
+ ...(hooks.loadDesktopModule ? { loadDesktopModule: hooks.loadDesktopModule } : {}),
496
+ ...(hooks.detachedTimers ? { detachedTimers: hooks.detachedTimers } : {}),
497
+ ...(hooks.env ? { env: hooks.env } : {}),
498
+ ...(hooks.prepareDesktop ? { prepareDesktop: (desktop) => hooks.prepareDesktop(desktop) } : {}),
499
+ onRuntimeStreamReady: (stream) => {
500
+ runtimeStreamUrls.push({ streamId: stream.streamId, url: stream.url });
501
+ if (liveObserver) {
502
+ attachObserverRuntimeStreamUrls(liveObserver, runtimeStreamUrls);
503
+ }
504
+ }
505
+ };
506
+ const baseActorDeps = {
507
+ config,
508
+ descriptor,
509
+ cloneRoute: false,
510
+ subjectEnvNames: [],
511
+ hasGithubToken: false,
512
+ env,
513
+ openaiApiKey,
514
+ e2bApiKey,
515
+ requestTimeoutMs,
516
+ perLaneSandboxMs: timeoutMs + SANDBOX_TIMEOUT_BUFFER_MS,
517
+ timeoutMs,
518
+ laneCount: roles.length,
519
+ artifactRoot: runPaths,
520
+ redactScreenshots,
521
+ scrubKnownValues,
522
+ runSession,
523
+ hooks: cuaHooks
524
+ };
525
+ actorResults = await mapWithConcurrency(actorSpecs, Math.max(1, concurrency), async (spec, i) => {
526
+ const route = resolveActorSeatUrl(getHostUrl, roles[i]?.entry);
527
+ const startedAt = now();
528
+ const outcome = await runCuaLane(spec, { ...baseActorDeps, appUrl: route });
529
+ const endedAt = now();
530
+ return { spec, outcome, startedAt, endedAt, route };
531
+ });
532
+ }
533
+ catch (error) {
534
+ runError = redactText(scrubKnownValues(toErrorMessage(error)));
535
+ warnings.push(`Concurrent shared-world run failed before completion: ${runError}`);
536
+ }
537
+ finally {
538
+ // FIX-9: stop the prober, take a final snapshot while the subject is still alive, then tear
539
+ // down the ONE subject sandbox BY id (the actor sandboxes are torn down inside runCuaLane).
540
+ proberDisposed = true;
541
+ releaseDispose();
542
+ if (proberLoop) {
543
+ await proberLoop.catch(() => undefined);
544
+ }
545
+ if (subjectDesktop && getHostUrl) {
546
+ await proberSnapshot().catch(() => undefined);
547
+ }
548
+ if (subjectDesktop && subjectModule) {
549
+ if (typeof subjectModule.Sandbox.kill === "function") {
550
+ try {
551
+ await subjectModule.Sandbox.kill(subjectDesktop.sandboxId, { requestTimeoutMs: 60_000 });
552
+ subjectKilled = true;
553
+ }
554
+ catch (error) {
555
+ warnings.push(`Subject sandbox teardown failed (server-side kill-on-timeout will reclaim it): ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
556
+ }
557
+ }
558
+ else {
559
+ warnings.push("Installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the subject sandbox.");
560
+ }
561
+ }
562
+ }
563
+ }
564
+ const subjectState = resolveSubjectState({ declared: config.subject.state, dryRun, executed: stateStepRecords });
565
+ const planeCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
566
+ const subject = buildSubjectProvenance({
567
+ localTreeRoute,
568
+ publicRepo,
569
+ subjectCommit: planeCommit,
570
+ localTreeArchive,
571
+ subjectEnvNames,
572
+ state: subjectState
573
+ });
574
+ // Collect per-actor warnings (each lane's own teardown/raw-screenshot notes).
575
+ for (const result of actorResults) {
576
+ warnings.push(...result.outcome.warnings);
577
+ }
578
+ const bundle = buildConcurrentSharedWorldBundle({
579
+ config,
580
+ descriptor,
581
+ createdAt,
582
+ dryRun,
583
+ runId,
584
+ source,
585
+ roles,
586
+ actorSpecs,
587
+ actorResults,
588
+ stateSnapshots,
589
+ subject,
590
+ seedDigest,
591
+ ...(planeCommit === undefined ? {} : { subjectCommit: planeCommit }),
592
+ ...(getHostUrl === undefined ? {} : { hostDigest: hostOriginDigest(getHostUrl) }),
593
+ ...(runError === undefined ? {} : { runError })
594
+ });
595
+ const adapterWarnings = [];
596
+ await applyBrowserAdapterHooks({
597
+ hooks,
598
+ bundle,
599
+ context: {
600
+ bundle,
601
+ runDir: physicalArtifactRoot,
602
+ labId: config.id,
603
+ runId,
604
+ actor: descriptor.id,
605
+ backend: "concurrent-shared-world",
606
+ dryRun,
607
+ laneCount: roles.length
608
+ },
609
+ sanitize: (text) => redactText(scrubKnownValues(text)),
610
+ warnings: adapterWarnings,
611
+ hookLabel: "sharedWorldHooks"
612
+ });
613
+ await writeConcurrentRunArtifacts(bundle, runPaths);
614
+ const observer = await render(cwd, runId, { open: options.open === true });
615
+ if (observer.ok && liveObserver) {
616
+ attachObserverRuntimeStreamUrls(observer, runtimeStreamUrls);
617
+ }
618
+ const roleOk = (result) => {
619
+ if (dryRun)
620
+ return true;
621
+ return actorLanePassed(result);
622
+ };
623
+ // Concurrent "ok": every actor must produce a terminal, engaged PASSED session. Per-persona
624
+ // mission success is still the "M of N" headline, but a failed actor trace cannot make the
625
+ // route green just because the harness got a terminal.
626
+ const swarmRan = !dryRun && actorResults.length === roles.length
627
+ && actorResults.every(actorLanePassed);
628
+ const adapterFailure = adapterScoreFailureMessage(bundle);
629
+ const ok = observer.ok && runError === undefined && (dryRun || swarmRan) && adapterFailure === undefined;
630
+ const overlapProven = !dryRun && actorWindowsOverlap(actorResults);
631
+ const roleResults = actorSpecs.map((spec, index) => {
632
+ const result = actorResults[index];
633
+ const base = { id: spec.laneId, index: index + 1, persona: spec.persona.id };
634
+ if (dryRun || !result) {
635
+ return { ...base, status: "contract_proof_only", ok: dryRun };
636
+ }
637
+ const session = result.outcome.session;
638
+ const thisOk = roleOk(result);
639
+ return {
640
+ ...base,
641
+ status: session ? session.status : "failed",
642
+ ok: thisOk,
643
+ window: { startedAt: result.startedAt, endedAt: result.endedAt },
644
+ ...(session
645
+ ? { session: { status: session.status, completionReason: session.completionReason, reason: session.reason, screenshots: result.outcome.screenshots.length } }
646
+ : {}),
647
+ ...(result.outcome.sandboxId === undefined
648
+ ? {}
649
+ : { sandbox: { sandboxId: result.outcome.sandboxId, killed: result.outcome.killed } }),
650
+ ...(thisOk
651
+ ? {}
652
+ : {
653
+ error: {
654
+ code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED",
655
+ message: result.outcome.sessionError
656
+ ?? (result.outcome.noEngagement
657
+ ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
658
+ : result.outcome.selfReportedBlocker
659
+ ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
660
+ : session?.completionReason === "harness_error"
661
+ ? `Actor seat ended with a harness error: ${session.reason}`
662
+ : "Actor did not produce a terminal session.")
663
+ }
664
+ })
665
+ };
666
+ });
667
+ const errorResult = (() => {
668
+ if (ok)
669
+ return undefined;
670
+ if (!observer.ok) {
671
+ return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: observer.error?.message ?? "Observer failed for the concurrent shared-world run." };
672
+ }
673
+ if (runError) {
674
+ return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: runError };
675
+ }
676
+ if (adapterFailure !== undefined) {
677
+ return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: adapterFailure };
678
+ }
679
+ const passed = roleResults.filter((role) => role.ok).length;
680
+ return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: `Concurrent shared-world run did not run coherently: ${passed}/${roles.length} actor(s) reached a terminal, engaged passed session.` };
681
+ })();
682
+ return {
683
+ schema: CONCURRENT_SHARED_WORLD_LAB_SCHEMA,
684
+ ok,
685
+ cwd,
686
+ labId: config.id,
687
+ actor: descriptor.id,
688
+ topology: "shared-world",
689
+ topologyMode: "concurrent",
690
+ roleCount: roles.length,
691
+ concurrency,
692
+ dryRun,
693
+ runId,
694
+ ...(getHostUrl === undefined ? {} : { host: getHostUrl }),
695
+ ...(subjectSandboxId === undefined ? {} : { subjectSandbox: { sandboxId: subjectSandboxId, killed: subjectKilled } }),
696
+ ...(dryRun ? {} : { overlapProven }),
697
+ subject,
698
+ roles: roleResults,
699
+ observer,
700
+ warnings: [...warnings, ...adapterWarnings, ...observer.warnings],
701
+ ...(errorResult === undefined ? {} : { error: errorResult })
702
+ };
703
+ }
704
+ /** True when ≥2 actor windows overlap in time (the proven-concurrency signal). */
705
+ function actorWindowsOverlap(results) {
706
+ for (let i = 0; i < results.length; i += 1) {
707
+ for (let j = i + 1; j < results.length; j += 1) {
708
+ const a = results[i];
709
+ const b = results[j];
710
+ if (a.startedAt < b.endedAt && b.startedAt < a.endedAt) {
711
+ return true;
712
+ }
713
+ }
714
+ }
715
+ return false;
716
+ }
717
+ function actorLanePassed(result) {
718
+ if (!result)
719
+ return false;
720
+ const session = result.outcome.session;
721
+ return session !== undefined
722
+ && session.status === "passed"
723
+ && session.completionReason !== "harness_error"
724
+ && result.outcome.sessionError === undefined
725
+ && !result.outcome.noEngagement
726
+ && !result.outcome.selfReportedBlocker;
727
+ }
728
+ /** Project the concurrent run into a humanish.run-bundle.v1 with the CONCURRENT shared-world block. */
729
+ export function buildConcurrentSharedWorldBundle(args) {
730
+ const { config, descriptor, createdAt, dryRun, actorSpecs, actorResults, roles } = args;
731
+ const inProgress = args.inProgress === true;
732
+ const simulations = [];
733
+ const streams = [];
734
+ const events = [];
735
+ // Public-safe label only — the raw getHost URL never lands in the bundle (it embeds the live
736
+ // sandbox id + matches the e2b-URL redaction). The host identity is carried by plane.hostDigest.
737
+ const appUrl = "[provisioned-subject]";
738
+ const planeCommit = dryRun ? undefined : args.subjectCommit;
739
+ events.push({
740
+ id: "event-000-created",
741
+ at: createdAt,
742
+ level: "info",
743
+ type: "concurrent-shared-world.run.created",
744
+ message: `Created CONCURRENT shared-world run for ${config.id} (actor ${descriptor.id}, ${actorSpecs.length} persona(s) vs ONE shared plane, max ${config.execution?.concurrency ?? 1} concurrent).`
745
+ });
746
+ // Human-readable plane label, byte-stable for the clone route (see shared-world-lab.ts's
747
+ // buildSharedWorldBundle for the same pattern). local-tree has no repo slug: it labels the
748
+ // packed archive instead (archiveSha256 + dirty/clean when the packed root was a git work tree).
749
+ const dryRunPlaneLabel = args.subject.source === "local-tree"
750
+ ? "packed working tree"
751
+ : `clone of ${args.subject.repo}`;
752
+ const livePlaneLabel = args.subject.source === "local-tree"
753
+ ? (args.subject.archiveSha256
754
+ ? `packed working tree (archiveSha256 ${args.subject.archiveSha256}${args.subject.dirty === true ? ", dirty working tree" : args.subject.dirty === false ? ", clean working tree" : ""})`
755
+ : "packed working tree (archive digest unresolved; provisioning failed before resolution)")
756
+ : `clone of ${args.subject.repo}${args.subjectCommit ? `@${args.subjectCommit}` : ""}`;
757
+ events.push({
758
+ id: "event-001-plane",
759
+ at: createdAt,
760
+ level: "info",
761
+ type: "concurrent-shared-world.plane.provenance",
762
+ message: dryRun
763
+ ? `Shared plane declared: ${dryRunPlaneLabel}, served + getHost-exposed in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
764
+ : `Shared plane: ${livePlaneLabel}, served + exposed at the harness-minted getHost URL; seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
765
+ simId: actorSpecs[0]?.simId ?? "sim-001",
766
+ streamId: actorSpecs[0]?.streamId ?? "stream-001"
767
+ });
768
+ let eventSeq = 2;
769
+ const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
770
+ actorSpecs.forEach((spec, index) => {
771
+ const taxonomy = laneTaxonomyLabel(spec);
772
+ const result = actorResults[index];
773
+ const outcome = result?.outcome;
774
+ const session = outcome?.session;
775
+ const screenshots = outcome?.screenshots ?? [];
776
+ const lastScreenshot = screenshots[screenshots.length - 1];
777
+ const route = publicSafeRouteLabel(roles[index]?.entry); // public-safe (host redacted)
778
+ const status = session
779
+ ? session.status
780
+ : outcome?.sessionError
781
+ ? "failed"
782
+ : inProgress
783
+ ? "running"
784
+ : "contract_proof_only";
785
+ const reason = session?.reason
786
+ ?? outcome?.sessionError
787
+ ?? (inProgress
788
+ ? "Actor desktop is running; the attached Observer hydrates the runtime stream URL without persisting it."
789
+ : "Contract actor only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.");
790
+ const traceScreenshotMode = session?.trace.redaction.screenshots;
791
+ const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
792
+ ? traceScreenshotMode
793
+ : config.policies?.redactScreenshots === true ? "blurred" : "raw";
794
+ simulations.push({
795
+ id: spec.simId,
796
+ index: index + 1,
797
+ personaId: spec.persona.id,
798
+ scenarioId: `concurrent-shared-world-${config.id}`,
799
+ status,
800
+ streamKind: "browser",
801
+ mode: "browser-sim",
802
+ progress: inProgress ? 35 : 100,
803
+ currentStep: reason,
804
+ summary: session
805
+ ? `Persona ${spec.laneId}${taxonomy} (${spec.persona.id}): drove the shared plane concurrently; ${session.completionReason}.`
806
+ : outcome?.sessionError
807
+ ? `Persona ${spec.laneId}${taxonomy} failed before a terminal session verdict: ${outcome.sessionError}`
808
+ : inProgress
809
+ ? `Persona ${spec.laneId}${taxonomy} (${spec.persona.id}) is running against the shared plane.`
810
+ : `Contract persona ${spec.laneId}${taxonomy} (${spec.persona.id}) for ${descriptor.id} against the shared plane at ${appUrl}.`,
811
+ streamIds: [spec.streamId],
812
+ startedAt: createdAt,
813
+ updatedAt: createdAt
814
+ });
815
+ streams.push({
816
+ id: spec.streamId,
817
+ simId: spec.simId,
818
+ kind: "browser",
819
+ label: `Concurrent persona ${spec.laneId}${taxonomy} — ${config.id}`,
820
+ status,
821
+ transport: "snapshot",
822
+ updatedAt: createdAt,
823
+ embed: lastScreenshot
824
+ ? { kind: "screenshot", url: lastScreenshot, title: `Shared plane, persona ${spec.laneId} (${screenshotMode})` }
825
+ : { kind: "placeholder", title: `Shared plane, persona ${spec.laneId}` },
826
+ viewport: {
827
+ width: spec.resolution[0],
828
+ height: spec.resolution[1],
829
+ deviceScaleFactor: spec.devicePreset.deviceScaleFactor,
830
+ isMobile: spec.devicePreset.isMobile
831
+ },
832
+ ui: {
833
+ route,
834
+ intent: `Watch persona ${spec.laneId}${taxonomy} (${spec.persona.id}) drive the SHARED plane concurrently with the other personas.`,
835
+ state: reason,
836
+ ...(session ? { actorStatus: session.status } : {}),
837
+ ...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
838
+ },
839
+ ...(session ? { actor: session.trace } : {}),
840
+ artifacts: [
841
+ { label: "run bundle", path: "run.json", kind: "bundle" },
842
+ { label: "review", path: "review.md", kind: "review" },
843
+ { label: "events", path: "events.ndjson", kind: "events" },
844
+ ...(session
845
+ ? [{ label: `persona ${spec.laneId} actor trace`, path: spec.traceArtifactPath, kind: "trace" }]
846
+ : []),
847
+ ...screenshots.map((screenshot, screenshotIndex) => ({
848
+ label: `persona ${spec.laneId} screenshot ${String(screenshotIndex + 1).padStart(2, "0")} (${screenshotMode})`,
849
+ path: screenshot,
850
+ kind: "screenshot"
851
+ }))
852
+ ]
853
+ });
854
+ for (const warning of outcome?.warnings ?? []) {
855
+ events.push({
856
+ id: nextEventId(`warning-${spec.laneId}`),
857
+ at: createdAt,
858
+ level: "warn",
859
+ type: "concurrent-shared-world.actor.warning",
860
+ message: `Persona ${spec.laneId}: ${warning}`,
861
+ simId: spec.simId,
862
+ streamId: spec.streamId
863
+ });
864
+ }
865
+ if (session) {
866
+ events.push({
867
+ id: nextEventId(`session-${spec.laneId}`),
868
+ at: createdAt,
869
+ level: session.status === "passed" ? "info" : "warn",
870
+ type: `concurrent-shared-world.session.${session.completionReason}`,
871
+ message: `Persona ${spec.laneId}: ${session.status} — ${session.reason}`,
872
+ simId: spec.simId,
873
+ streamId: spec.streamId
874
+ });
875
+ }
876
+ else if (outcome?.sessionError) {
877
+ events.push({
878
+ id: nextEventId(`session-error-${spec.laneId}`),
879
+ at: createdAt,
880
+ level: "error",
881
+ type: "concurrent-shared-world.session.error",
882
+ message: `Persona ${spec.laneId}: ${outcome.sessionError}`,
883
+ simId: spec.simId,
884
+ streamId: spec.streamId
885
+ });
886
+ }
887
+ else if (inProgress) {
888
+ events.push({
889
+ id: nextEventId(`running-${spec.laneId}`),
890
+ at: createdAt,
891
+ level: "info",
892
+ type: "actor.running",
893
+ message: `Persona ${spec.laneId}: desktop actor is running; live stream URL is runtime-only and not persisted.`,
894
+ simId: spec.simId,
895
+ streamId: spec.streamId
896
+ });
897
+ }
898
+ else {
899
+ events.push({
900
+ id: nextEventId(`contract-${spec.laneId}`),
901
+ at: createdAt,
902
+ level: "info",
903
+ type: "concurrent-shared-world.contract.ready",
904
+ message: `Persona ${spec.laneId}: dry-run contract actor ready; switch scenario.mode to live for a real concurrent session.`,
905
+ simId: spec.simId,
906
+ streamId: spec.streamId
907
+ });
908
+ }
909
+ });
910
+ // Build the concurrent shared-world evidence block. routeHostDigest is sha256-16 of the ORIGIN of
911
+ // the getHost seat URL each actor drove (publish-safe; verify confirms it == plane.hostDigest).
912
+ const fallbackHostDigest = args.hostDigest ?? commandDigestOf("[provisioned-subject]");
913
+ const laneWindows = actorSpecs.map((spec, index) => {
914
+ const result = actorResults[index];
915
+ const session = result?.outcome.session;
916
+ const routeHostDigest = result ? hostOriginDigest(result.route) : fallbackHostDigest;
917
+ return {
918
+ roleId: spec.laneId,
919
+ ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
920
+ ...(spec.surface === undefined ? {} : { surface: spec.surface }),
921
+ ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
922
+ simId: spec.simId,
923
+ streamId: spec.streamId,
924
+ startedAt: result?.startedAt ?? 0,
925
+ endedAt: result?.endedAt ?? 0,
926
+ verdict: session ? session.status : result?.outcome.sessionError ? "failed" : inProgress ? "running" : "contract_proof_only",
927
+ routeHostDigest,
928
+ ...(planeCommit === undefined ? {} : { commit: planeCommit }),
929
+ seedDigest: args.seedDigest
930
+ };
931
+ });
932
+ const stateSeries = dryRun
933
+ ? [{ timestamp: 0, digest: declaredStateDigest(config) }]
934
+ : [...args.stateSnapshots].sort((a, b) => a.timestamp - b.timestamp);
935
+ const outcomes = actorSpecs.map((spec, index) => {
936
+ const result = actorResults[index];
937
+ const session = result?.outcome.session;
938
+ const ok = !dryRun && actorLanePassed(result);
939
+ return {
940
+ roleId: spec.laneId,
941
+ ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
942
+ ...(spec.surface === undefined ? {} : { surface: spec.surface }),
943
+ ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
944
+ simId: spec.simId,
945
+ streamId: spec.streamId,
946
+ status: session ? session.status : result?.outcome.sessionError ? "failed" : inProgress ? "running" : "contract_proof_only",
947
+ ...(session ? { completionReason: session.completionReason } : {}),
948
+ ok
949
+ };
950
+ });
951
+ const sharedWorld = {
952
+ schema: SHARED_WORLD_SCHEMA,
953
+ topology: "shared-world",
954
+ topologyMode: "concurrent",
955
+ roleCount: actorSpecs.length,
956
+ plane: {
957
+ ...(planeCommit === undefined ? {} : { commit: planeCommit }),
958
+ seedDigest: args.seedDigest,
959
+ envNames: args.subject.envNames ?? [],
960
+ ...(args.hostDigest === undefined ? {} : { hostDigest: args.hostDigest }),
961
+ exposure: "synthetic"
962
+ },
963
+ attributionLimits: [...CONCURRENT_ATTRIBUTION_LIMITS],
964
+ laneWindows,
965
+ stateSeries,
966
+ outcomes
967
+ };
968
+ const overlaps = actorWindowsOverlap(actorResults);
969
+ const deltas = stateSeries.filter((snapshot, i) => i > 0 && snapshot.digest !== stateSeries[i - 1].digest).length;
970
+ events.push({
971
+ id: nextEventId("concurrency"),
972
+ at: createdAt,
973
+ level: "info",
974
+ type: "concurrent-shared-world.concurrency",
975
+ message: `Concurrency: ${laneWindows.length} actor window(s)${dryRun ? " (dry-run contract; $0)" : `, overlap ${overlaps ? "PROVEN" : "not observed"}`}; stateSeries ${stateSeries.length} snapshot(s), ${deltas} delta(s). Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}. CAPABILITY at scale is backed only by the deferred live receipt.`
976
+ });
977
+ // Concurrent verdict: dryRun → contract; else every actor produced a terminal, engaged PASSED
978
+ // session → pass; otherwise fail. Per-persona mission success is the M-of-N in outcomes[].
979
+ const verdict = dryRun
980
+ ? "contract_proof_only"
981
+ : inProgress
982
+ ? "contract_proof_only"
983
+ : (actorResults.length === actorSpecs.length
984
+ && actorResults.every(actorLanePassed)
985
+ ? "pass"
986
+ : "fail");
987
+ const passedMissions = outcomes.filter((outcome) => outcome.ok).length;
988
+ const review = {
989
+ schema: REVIEW_SCHEMA,
990
+ verdict,
991
+ summary: dryRun
992
+ ? `Dry-run concurrent shared-world contract: ${actorSpecs.length} persona(s) declared against ONE getHost-exposed plane (${descriptor.id}); no sandboxes launched, $0 spend.`
993
+ : inProgress
994
+ ? `In-progress concurrent shared-world Observer snapshot: ${actorSpecs.length} persona(s) running against ONE shared plane; final verification is pending.`
995
+ : `Concurrent shared-world (ONE plane, ${actorSpecs.length} simultaneous personas): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${passedMissions}/${actorSpecs.length} reached their goal; overlap ${overlaps ? "proven" : "not observed"}; ${deltas} state delta(s) under load.`,
996
+ gaps: dryRun
997
+ ? ["Live concurrent shared-world session not yet run (dry-run contract only); the concurrency capability at scale is backed only by the deferred live receipt."]
998
+ : inProgress
999
+ ? ["Final actor traces, screenshots, state deltas, and verification are pending; this Observer is for live watch only."]
1000
+ : actorResults
1001
+ .filter((result) => result.outcome.sessionError !== undefined
1002
+ || result.outcome.noEngagement
1003
+ || result.outcome.selfReportedBlocker
1004
+ || result.outcome.session === undefined
1005
+ || result.outcome.session.status !== "passed")
1006
+ .map((result) => `${result.spec.laneId}: ${result.outcome.sessionError ?? result.outcome.session?.reason ?? "did not pass"}`)
1007
+ };
1008
+ const anyRaw = actorResults.some((result) => result.outcome.session?.trace.redaction.screenshots === "raw");
1009
+ const ranLive = actorResults.some((result) => result.outcome.session !== undefined || result.outcome.sessionError !== undefined);
1010
+ return {
1011
+ schema: RUN_BUNDLE_SCHEMA,
1012
+ runId: args.runId,
1013
+ mode: dryRun ? "dry-run" : "live",
1014
+ simCount: actorSpecs.length,
1015
+ createdAt,
1016
+ cwd: PUBLIC_TARGET_CWD,
1017
+ artifactRoot: path.join(".humanish", "runs", args.runId),
1018
+ source: args.source,
1019
+ persona: {
1020
+ id: actorSpecs[0]?.persona.id ?? "concurrent-persona",
1021
+ name: `Concurrent shared-world swarm (${actorSpecs.length} personas)`,
1022
+ source: `lab:${config.id}`,
1023
+ sourceDigest: actorSpecs[0]?.persona.promptDigest ?? args.seedDigest
1024
+ },
1025
+ scenario: {
1026
+ id: `concurrent-shared-world-${config.id}`,
1027
+ title: config.title ?? `Concurrent shared-world: ${config.id}`,
1028
+ goal: actorSpecs[0]?.instructions ?? "Concurrent shared-world interaction.",
1029
+ source: `lab:${config.id}`,
1030
+ sourceDigest: actorSpecs[0]?.persona.promptDigest ?? args.seedDigest
1031
+ },
1032
+ lifecycle: [
1033
+ {
1034
+ at: createdAt,
1035
+ event: "concurrent-shared-world.run.created",
1036
+ message: `Created concurrent shared-world run with ONE shared plane and ${actorSpecs.length} simultaneous actor seats (actor ${descriptor.id}).`
1037
+ }
1038
+ ],
1039
+ simulations,
1040
+ streams,
1041
+ events,
1042
+ redaction: {
1043
+ status: "passed",
1044
+ notes: ranLive
1045
+ ? anyRaw
1046
+ ? "Typed text recorded as length only and reasoning/messages pass through text redaction. Some personas captured FULL-FIDELITY (raw) screenshots, retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle. stateSeries persists digest-only."
1047
+ : "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. stateSeries persists digest-only."
1048
+ : inProgress
1049
+ ? "In-progress live Observer snapshot: runtime stream auth URLs are process-local only and are not persisted. Final typed text, traces, and screenshots are pending. stateSeries persists digest-only."
1050
+ : "Dry-run concurrent shared-world contract bundle: no sandboxes launched and no screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs. stateSeries persists digest-only."
1051
+ },
1052
+ artifacts: {
1053
+ run: "run.json",
1054
+ reviewJson: "review.json",
1055
+ reviewMarkdown: "review.md",
1056
+ observerData: "observer/observer-data.json",
1057
+ events: "events.ndjson"
1058
+ },
1059
+ review,
1060
+ feedbackCandidates: [],
1061
+ // Custom desktop image provenance (subject + every actor sandbox launched on it); omitted on the default.
1062
+ ...(config.execution?.desktop?.template === undefined ? {} : { desktopTemplate: config.execution.desktop.template }),
1063
+ subject: args.subject,
1064
+ attributionClass: "shared-world",
1065
+ sharedWorld
1066
+ };
1067
+ }
1068
+ /** The declared (dry-run) state digest: the probe RECIPE (command digests), no run. */
1069
+ function declaredStateDigest(config) {
1070
+ const probes = config.subject.state?.checkpoint ?? [];
1071
+ return combineCheckpointDigest(probes.map((probe) => `${probe.name}=${commandDigestOf(probe.command)}`));
1072
+ }
1073
+ function renderConcurrentReviewMarkdown(bundle) {
1074
+ const plane = bundle.events.find((event) => event.type === "concurrent-shared-world.plane.provenance");
1075
+ const concurrency = bundle.events.find((event) => event.type === "concurrent-shared-world.concurrency");
1076
+ const sw = bundle.sharedWorld;
1077
+ return [
1078
+ `# ${bundle.scenario.title}`,
1079
+ "",
1080
+ `- run: ${bundle.runId}`,
1081
+ `- mode: ${bundle.mode}`,
1082
+ `- attribution class: ${bundle.attributionClass ?? "isolated"}`,
1083
+ `- topology: ${sw?.topology ?? "(none)"} / ${sw?.topologyMode ?? "(none)"}`,
1084
+ `- personas: ${sw?.roleCount ?? 0}`,
1085
+ `- verdict: ${bundle.review.verdict}`,
1086
+ `- summary: ${bundle.review.summary}`,
1087
+ ...(plane ? [`- plane: ${plane.message}`] : []),
1088
+ ...(concurrency ? [`- concurrency: ${concurrency.message}`] : []),
1089
+ ...(sw ? [`- attribution limits: ${sw.attributionLimits.join(", ")}`] : []),
1090
+ ...(bundle.review.gaps.length > 0 ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)] : []),
1091
+ ""
1092
+ ].join("\n");
1093
+ }
1094
+ //# sourceMappingURL=concurrent-shared-world-lab.js.map