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