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
package/dist/run.js ADDED
@@ -0,0 +1,4555 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { parse as parseYaml } from "yaml";
7
+ // The deterministic browser-persona driver lives in the scripted-browser-actor leaf module
8
+ // (moved there so the actor registry can reuse it without a run.ts import cycle). This file
9
+ // keeps the `run --app-url` orchestration; behavior is byte-identical.
10
+ import { browserSurfaces, builtinBrowserPersonaJourney, captureBrowserSurface, normalizeLocalAppUrl, parseBrowserPersonaJourneyFromScenario, resolveBrowserCommand } from "./scripted-browser-actor.js";
11
+ import { CODEX_APP_SERVER_TRACE_SCHEMA } from "./codex-app-server.js";
12
+ import { getActor } from "./actor-registry.js";
13
+ import { artifactReferenceIfWritten, hasWrittenScreenshot } from "./artifact-reference.js";
14
+ import { ACTOR_TRACE_SCHEMA } from "./actor-contract.js";
15
+ import { captureGitState, GIT_STATE_SCHEMA } from "./core/git-state.js";
16
+ import { mapWithConcurrency } from "./concurrency.js";
17
+ import { screenshotEvidenceError } from "./image-evidence.js";
18
+ import { buildObserverData } from "./observer-data.js";
19
+ import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
20
+ import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
21
+ import { loadE2BDesktopModule } from "./e2b-desktop-launch.js";
22
+ export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
23
+ export const SHARED_WORLD_SCHEMA = "humanish.shared-world.v1";
24
+ export const REVIEW_SCHEMA = "humanish.review.v1";
25
+ export const VERIFY_SCHEMA = "humanish.verify-result.v1";
26
+ export const RUNS_SCHEMA = "humanish.runs-result.v1";
27
+ export const DOCTOR_SCHEMA = "humanish.doctor-result.v1";
28
+ export const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
29
+ export const PUBLIC_TARGET_CWD = "[target-cwd]";
30
+ const SAFE_GIT_NOTES = new Set([
31
+ "Git command could not be started.",
32
+ "Git status command could not be captured.",
33
+ "Git status command could not be started.",
34
+ "Git work tree had changes; only counts were captured, not branch names, remotes, paths, or file names.",
35
+ "Git work tree was clean; branch names, remotes, paths, and file names were not captured.",
36
+ "No git work tree was detected.",
37
+ "public-safe synthetic fixture",
38
+ "public-safe synthetic OSS meta-lab fixture"
39
+ ]);
40
+ export async function buildRunSource(args) {
41
+ const gitOptions = args.capturedAt === undefined ? {} : { capturedAt: args.capturedAt };
42
+ return {
43
+ packageName: args.packageName,
44
+ humanishSource: args.humanishSource,
45
+ git: await captureGitState(args.cwd, gitOptions)
46
+ };
47
+ }
48
+ const CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA = "humanish.codex-app-server-trace.projected.v1";
49
+ const LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS = 240_000;
50
+ const LOCAL_CODEX_TUI_MAX_TIMEOUT_MS = 600_000;
51
+ const LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS = 80_000;
52
+ const LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY = 4;
53
+ const BROWSER_APP_DEFAULT_TIMEOUT_MS = 60_000;
54
+ const builtinPersona = {
55
+ id: "builtin-synthetic-new-user",
56
+ name: "Built-in Synthetic New User",
57
+ source: "builtin:synthetic-new-user",
58
+ sourceDigest: "builtin"
59
+ };
60
+ const builtinScenario = {
61
+ id: "builtin-first-run-smoke",
62
+ title: "Built-in First-Run Smoke",
63
+ goal: "Create a public-safe dry-run contract bundle from built-in defaults.",
64
+ source: "builtin:first-run-smoke",
65
+ sourceDigest: "builtin"
66
+ };
67
+ export async function runDryRun(options) {
68
+ const cwd = path.resolve(options.cwd);
69
+ const cwdError = await validateCwd(cwd);
70
+ const warnings = [];
71
+ if (cwdError) {
72
+ return {
73
+ schema: "humanish.run-result.v1",
74
+ ok: false,
75
+ cwd,
76
+ warnings,
77
+ error: cwdError
78
+ };
79
+ }
80
+ if (options.actor !== undefined && !isLocalCodexActor(options.actor)) {
81
+ return {
82
+ schema: "humanish.run-result.v1",
83
+ ok: false,
84
+ cwd,
85
+ warnings,
86
+ error: {
87
+ code: "HUMANISH_UNSUPPORTED_ACTOR",
88
+ message: `Unsupported actor: ${options.actor}`
89
+ }
90
+ };
91
+ }
92
+ const simCount = normalizeSimCount(options.appUrl ? options.simCount ?? 2 : options.simCount);
93
+ if (simCount === null) {
94
+ return {
95
+ schema: "humanish.run-result.v1",
96
+ ok: false,
97
+ cwd,
98
+ warnings,
99
+ error: {
100
+ code: "HUMANISH_INVALID_SIM_COUNT",
101
+ message: "--sims must be a positive integer."
102
+ }
103
+ };
104
+ }
105
+ if (options.appUrl !== undefined) {
106
+ if (options.dryRun) {
107
+ return {
108
+ schema: "humanish.run-result.v1",
109
+ ok: false,
110
+ cwd,
111
+ warnings,
112
+ error: {
113
+ code: "HUMANISH_APP_URL_OPTION_CONFLICT",
114
+ message: "Use --app-url for a live browser app proof; remove --dry-run."
115
+ }
116
+ };
117
+ }
118
+ if (simCount > 2) {
119
+ return {
120
+ schema: "humanish.run-result.v1",
121
+ ok: false,
122
+ cwd,
123
+ warnings,
124
+ error: {
125
+ code: "HUMANISH_INVALID_SIM_COUNT",
126
+ message: "--sims must be 1 or 2 when --app-url is used."
127
+ }
128
+ };
129
+ }
130
+ return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, simCount });
131
+ }
132
+ if (!options.dryRun) {
133
+ const actor = resolveRequestedLocalCodexActor(options.actor);
134
+ if (actor === "codex-tui") {
135
+ return runLocalCodexTui({ ...options, actor, cwd, simCount });
136
+ }
137
+ if (actor === "codex-exec") {
138
+ return runLocalCodexExec({ ...options, actor, cwd, simCount });
139
+ }
140
+ if (actor === "codex-app-server") {
141
+ return runLocalCodexAppServer({ ...options, actor, cwd, simCount });
142
+ }
143
+ return {
144
+ schema: "humanish.run-result.v1",
145
+ ok: false,
146
+ cwd,
147
+ warnings,
148
+ error: {
149
+ code: "HUMANISH_LIVE_RUN_UNIMPLEMENTED",
150
+ message: "Only run --dry-run is implemented unless --actor codex-tui, --actor codex-exec, --actor codex-app-server, or the matching HUMANISH_ENABLE_LOCAL_CODEX_* env var is set."
151
+ }
152
+ };
153
+ }
154
+ const now = new Date();
155
+ const createdAt = now.toISOString();
156
+ const runId = options.runId ?? `dryrun-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
157
+ const artifactRoot = path.join(".humanish", "runs", runId);
158
+ const absoluteArtifactRoot = path.join(cwd, artifactRoot);
159
+ const packageName = await readPackageName(cwd);
160
+ const humanishSource = await directoryExists(path.join(cwd, "humanish")) ? "present" : "missing";
161
+ const source = await buildRunSource({ cwd, capturedAt: createdAt, humanishSource, packageName });
162
+ const selection = await loadDryRunSelection(cwd, humanishSource);
163
+ if (humanishSource === "missing") {
164
+ warnings.push("Committed humanish/ source was not found; using built-in synthetic dry-run defaults.");
165
+ }
166
+ warnings.push(...selection.warnings);
167
+ const observerFixtures = buildSyntheticObserverFixtures({
168
+ createdAt,
169
+ personaId: selection.persona.id,
170
+ scenarioId: selection.scenario.id,
171
+ simCount
172
+ });
173
+ const bundle = {
174
+ schema: RUN_BUNDLE_SCHEMA,
175
+ runId,
176
+ mode: "dry-run",
177
+ simCount,
178
+ createdAt,
179
+ cwd,
180
+ artifactRoot,
181
+ source,
182
+ persona: selection.persona,
183
+ scenario: selection.scenario,
184
+ lifecycle: [
185
+ {
186
+ at: createdAt,
187
+ event: "run.created",
188
+ message: `Synthetic dry-run contract bundle created with ${simCount} sim${simCount === 1 ? "" : "s"}.`
189
+ },
190
+ {
191
+ at: createdAt,
192
+ event: "persona.selected",
193
+ message: "Selected public-safe synthetic persona."
194
+ },
195
+ {
196
+ at: createdAt,
197
+ event: "scenario.selected",
198
+ message: "Selected public-safe first-run scenario."
199
+ },
200
+ {
201
+ at: createdAt,
202
+ event: "review.skeleton.created",
203
+ message: "Created review skeleton without claiming product proof."
204
+ }
205
+ ],
206
+ simulations: observerFixtures.simulations,
207
+ streams: observerFixtures.streams,
208
+ events: observerFixtures.events,
209
+ redaction: {
210
+ status: "passed",
211
+ notes: "Dry-run bundle contains synthetic contract proof only."
212
+ },
213
+ artifacts: {
214
+ run: "run.json",
215
+ reviewJson: "review.json",
216
+ reviewMarkdown: "review.md",
217
+ observerData: "observer/observer-data.json",
218
+ events: "events.ndjson"
219
+ },
220
+ review: createReviewSummary(),
221
+ feedbackCandidates: []
222
+ };
223
+ await mkdir(absoluteArtifactRoot, { recursive: true });
224
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
225
+ await writeJson(path.join(cwd, ".humanish", "runs", "latest.json"), {
226
+ schema: "humanish.latest-run.v1",
227
+ runId,
228
+ path: artifactRoot,
229
+ updatedAt: createdAt
230
+ });
231
+ return {
232
+ schema: "humanish.run-result.v1",
233
+ ok: true,
234
+ runId,
235
+ mode: "dry-run",
236
+ simCount,
237
+ cwd,
238
+ artifactRoot,
239
+ bundlePath: path.join(artifactRoot, "run.json"),
240
+ reviewPath: path.join(artifactRoot, "review.md"),
241
+ latestPath: path.join(".humanish", "runs", "latest.json"),
242
+ warnings
243
+ };
244
+ }
245
+ async function runBrowserAppProof(options) {
246
+ const warnings = [];
247
+ const appUrl = normalizeLocalAppUrl(options.appUrl);
248
+ if (!appUrl) {
249
+ return {
250
+ schema: "humanish.run-result.v1",
251
+ ok: false,
252
+ cwd: options.cwd,
253
+ warnings,
254
+ error: {
255
+ code: "HUMANISH_INVALID_APP_URL",
256
+ message: "--app-url must be an http(s) loopback URL such as http://127.0.0.1:5173."
257
+ }
258
+ };
259
+ }
260
+ const browserCommand = await resolveBrowserCommand();
261
+ if (!browserCommand) {
262
+ return {
263
+ schema: "humanish.run-result.v1",
264
+ ok: false,
265
+ cwd: options.cwd,
266
+ warnings,
267
+ error: {
268
+ code: "HUMANISH_BROWSER_APP_CAPTURE_FAILED",
269
+ message: "No Chrome/Chromium browser command was found. Set HUMANISH_BROWSER_COMMAND to a browser binary that supports --headless and --screenshot."
270
+ }
271
+ };
272
+ }
273
+ const now = new Date();
274
+ const createdAt = now.toISOString();
275
+ const runId = options.runId ?? `browser-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
276
+ const artifactRoot = path.join(".humanish", "runs", runId);
277
+ const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
278
+ const packageName = await readPackageName(options.cwd);
279
+ const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
280
+ const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
281
+ const selection = await loadDryRunSelection(options.cwd, humanishSource);
282
+ if (selection.browserJourneyFailure) {
283
+ return {
284
+ schema: "humanish.run-result.v1",
285
+ ok: false,
286
+ cwd: options.cwd,
287
+ warnings: [...warnings, ...selection.warnings],
288
+ error: {
289
+ code: "HUMANISH_BROWSER_APP_CAPTURE_FAILED",
290
+ message: selection.browserJourneyFailure
291
+ }
292
+ };
293
+ }
294
+ const browserJourney = selection.browserJourney ?? builtinBrowserPersonaJourney();
295
+ if (humanishSource === "missing") {
296
+ warnings.push("Committed humanish/ source was not found; using built-in synthetic browser-app defaults.");
297
+ }
298
+ if (!selection.browserJourney) {
299
+ warnings.push("No executable browser scenario manifest was found; using built-in browser persona two-step journey.");
300
+ }
301
+ warnings.push(...selection.warnings);
302
+ await mkdir(path.join(absoluteArtifactRoot, "screenshots"), { recursive: true });
303
+ await mkdir(path.join(absoluteArtifactRoot, "traces"), { recursive: true });
304
+ const surfaces = browserSurfaces.slice(0, options.simCount);
305
+ const captures = await Promise.all(surfaces.map((surface) => captureBrowserSurface({
306
+ absoluteArtifactRoot,
307
+ appUrl,
308
+ browserCommand,
309
+ browserJourney,
310
+ surface,
311
+ timeoutMs: options.timeoutMs ?? BROWSER_APP_DEFAULT_TIMEOUT_MS
312
+ })));
313
+ const completedAt = new Date().toISOString();
314
+ const events = buildBrowserAppEvents({ appUrl, captures, createdAt });
315
+ const allPassed = captures.every((capture) => capture.ok);
316
+ const review = createBrowserAppReviewSummary({ appUrl, browserJourney, captures });
317
+ const bundle = {
318
+ schema: RUN_BUNDLE_SCHEMA,
319
+ runId,
320
+ mode: "live",
321
+ simCount: captures.length,
322
+ createdAt,
323
+ cwd: options.cwd,
324
+ artifactRoot,
325
+ source,
326
+ persona: {
327
+ id: selection.persona.id,
328
+ name: selection.persona.name,
329
+ source: selection.persona.source,
330
+ sourceDigest: selection.persona.sourceDigest
331
+ },
332
+ scenario: {
333
+ id: browserJourney.scenarioId,
334
+ title: browserJourney.scenarioTitle,
335
+ goal: browserJourney.goal,
336
+ source: browserJourney.source,
337
+ sourceDigest: browserJourney.sourceDigest
338
+ },
339
+ lifecycle: [
340
+ {
341
+ at: createdAt,
342
+ event: "run.created",
343
+ message: `Live browser persona proof created for ${appUrl}.`
344
+ },
345
+ {
346
+ at: createdAt,
347
+ event: "app.url.accepted",
348
+ message: "Accepted public-safe loopback app URL for browser persona journey."
349
+ },
350
+ {
351
+ at: completedAt,
352
+ event: "review.created",
353
+ message: allPassed
354
+ ? "Created review from desktop/mobile browser persona step evidence."
355
+ : "Created review with missing or blocked browser persona step evidence."
356
+ }
357
+ ],
358
+ simulations: captures.map((capture, index) => {
359
+ const simId = `browser-${capture.surface.id}`;
360
+ const streamId = `${simId}-stream`;
361
+ return {
362
+ id: simId,
363
+ index: index + 1,
364
+ personaId: selection.persona.id,
365
+ scenarioId: browserJourney.scenarioId,
366
+ status: capture.ok ? "passed" : "blocked",
367
+ streamKind: "browser",
368
+ mode: "browser-sim",
369
+ progress: 100,
370
+ currentStep: capture.ok
371
+ ? `${capture.surface.label} completed ${capture.steps.length} persona steps`
372
+ : `${capture.surface.label} journey blocked`,
373
+ summary: capture.reason,
374
+ streamIds: [streamId],
375
+ startedAt: createdAt,
376
+ updatedAt: capture.capturedAt
377
+ };
378
+ }),
379
+ streams: captures.map((capture) => {
380
+ const simId = `browser-${capture.surface.id}`;
381
+ const streamId = `${simId}-stream`;
382
+ // Never reference a screenshot the producer did not write (artifact-reference.ts).
383
+ // A blocked capture whose evidence IS the failure carries no surface screenshot, so
384
+ // we omit the embed URL + ui.screenshotUrl and keep the stream present with its
385
+ // blocked status — instead of claiming an artifact that verify would fail closed on.
386
+ // A capture that claims success but is missing its screenshot still keeps the
387
+ // reference so missingLocalEvidenceArtifacts can catch the broken producer.
388
+ const surfaceScreenshot = hasWrittenScreenshot(capture) ? capture.screenshotPath : undefined;
389
+ const screenshotUrl = surfaceScreenshot ? `../${surfaceScreenshot}` : undefined;
390
+ return {
391
+ id: streamId,
392
+ simId,
393
+ kind: "browser",
394
+ label: capture.surface.label,
395
+ status: capture.ok ? "passed" : "blocked",
396
+ transport: "snapshot",
397
+ updatedAt: capture.capturedAt,
398
+ embed: screenshotUrl
399
+ ? { kind: "screenshot", url: screenshotUrl, title: capture.surface.label }
400
+ : { kind: "placeholder", title: `${capture.surface.label} (blocked — no screenshot captured)` },
401
+ viewport: capture.surface.viewport,
402
+ ui: {
403
+ appStatus: capture.ok ? "running" : "blocked",
404
+ appUrl,
405
+ route: appUrl,
406
+ intent: browserJourney.goal,
407
+ ...(screenshotUrl ? { screenshotUrl } : {}),
408
+ state: capture.reason,
409
+ visualStatus: capture.ok ? "visible" : "blocked"
410
+ },
411
+ completion: {
412
+ checkedAt: capture.capturedAt,
413
+ exitCode: capture.ok ? 0 : 1,
414
+ reason: capture.reason,
415
+ status: capture.ok ? "passed" : "blocked"
416
+ },
417
+ artifacts: [
418
+ { label: "run bundle", path: "run.json", kind: "bundle" },
419
+ { label: "review", path: "review.md", kind: "review" },
420
+ { label: "event log", path: "events.ndjson", kind: "events" },
421
+ { label: `${capture.surface.id} browser trace`, path: capture.tracePath, kind: "trace" },
422
+ // Per-step screenshot artifacts only for steps whose screenshot was actually
423
+ // written; blocked-not-executed steps recorded no path and claim nothing.
424
+ ...capture.steps.flatMap((step) => {
425
+ const stepScreenshot = artifactReferenceIfWritten(step.screenshotPath, hasWrittenScreenshot(step));
426
+ return stepScreenshot
427
+ ? [{ label: `${capture.surface.id} ${step.id} screenshot`, path: stepScreenshot, kind: "screenshot" }]
428
+ : [];
429
+ })
430
+ ]
431
+ };
432
+ }),
433
+ events,
434
+ redaction: {
435
+ status: "passed",
436
+ notes: "Browser persona proof stores loopback app URLs, screenshots, and generated traces only; secret-like text is rejected by verify."
437
+ },
438
+ artifacts: {
439
+ run: "run.json",
440
+ reviewJson: "review.json",
441
+ reviewMarkdown: "review.md",
442
+ observerData: "observer/observer-data.json",
443
+ events: "events.ndjson"
444
+ },
445
+ review,
446
+ feedbackCandidates: []
447
+ };
448
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
449
+ await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
450
+ schema: "humanish.latest-run.v1",
451
+ runId,
452
+ path: artifactRoot,
453
+ updatedAt: completedAt
454
+ });
455
+ return {
456
+ schema: "humanish.run-result.v1",
457
+ ok: allPassed,
458
+ runId,
459
+ mode: "live",
460
+ simCount: captures.length,
461
+ cwd: options.cwd,
462
+ artifactRoot,
463
+ bundlePath: path.join(artifactRoot, "run.json"),
464
+ reviewPath: path.join(artifactRoot, "review.md"),
465
+ latestPath: path.join(".humanish", "runs", "latest.json"),
466
+ warnings,
467
+ ...(allPassed
468
+ ? {}
469
+ : {
470
+ error: {
471
+ code: "HUMANISH_BROWSER_APP_CAPTURE_FAILED",
472
+ message: review.summary
473
+ }
474
+ })
475
+ };
476
+ }
477
+ function buildBrowserAppEvents(args) {
478
+ const events = [
479
+ {
480
+ id: "event-001",
481
+ at: args.createdAt,
482
+ level: "info",
483
+ type: "browser-persona.run.created",
484
+ message: "Created live browser persona proof run against a loopback URL."
485
+ }
486
+ ];
487
+ args.captures.forEach((capture) => {
488
+ events.push({
489
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
490
+ at: capture.capturedAt,
491
+ level: capture.ok ? "info" : "warn",
492
+ type: capture.ok ? "browser-persona.journey.passed" : "browser-persona.journey.blocked",
493
+ message: `${capture.surface.id}: ${capture.reason}`,
494
+ simId: `browser-${capture.surface.id}`,
495
+ streamId: `browser-${capture.surface.id}-stream`
496
+ });
497
+ for (const step of capture.steps) {
498
+ events.push({
499
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
500
+ at: step.completedAt,
501
+ level: step.status === "passed" ? "info" : "warn",
502
+ type: step.status === "passed" ? "browser-persona.step.passed" : "browser-persona.step.blocked",
503
+ message: `${capture.surface.id} ${step.id}: ${step.reason}`,
504
+ simId: `browser-${capture.surface.id}`,
505
+ streamId: `browser-${capture.surface.id}-stream`
506
+ });
507
+ }
508
+ });
509
+ return events;
510
+ }
511
+ function createBrowserAppReviewSummary(args) {
512
+ const passed = args.captures.filter((capture) => capture.ok).length;
513
+ const allPassed = passed === args.captures.length;
514
+ const usedBuiltinFallback = args.browserJourney.source.startsWith("builtin:");
515
+ return {
516
+ schema: REVIEW_SCHEMA,
517
+ verdict: allPassed ? "pass" : "blocked",
518
+ summary: allPassed
519
+ ? `Completed ${passed}/${args.captures.length} live browser persona journey${args.captures.length === 1 ? "" : "s"} from ${args.appUrl} using ${args.browserJourney.scenarioId}.`
520
+ : `Completed ${passed}/${args.captures.length} live browser persona journeys from ${args.appUrl} using ${args.browserJourney.scenarioId}; at least one required journey was blocked.`,
521
+ gaps: [
522
+ usedBuiltinFallback
523
+ ? "This proof used the built-in two-step fallback because no executable browser scenario manifest was found."
524
+ : `This proof used executable browser steps from ${args.browserJourney.source}.`,
525
+ "Only loopback app URLs are accepted so generated bundles do not preserve private external targets.",
526
+ ...args.captures
527
+ .filter((capture) => !capture.ok)
528
+ .map((capture) => `${capture.surface.id}: ${capture.reason}`)
529
+ ]
530
+ };
531
+ }
532
+ function isLocalCodexActor(value) {
533
+ return value === "codex-tui" || value === "codex-exec" || value === "codex-app-server";
534
+ }
535
+ function resolveRequestedLocalCodexActor(actor) {
536
+ if (actor && isLocalCodexActor(actor)) {
537
+ return actor;
538
+ }
539
+ if (process.env.HUMANISH_ENABLE_LOCAL_CODEX_TUI === "1") {
540
+ return "codex-tui";
541
+ }
542
+ if (process.env.HUMANISH_ENABLE_LOCAL_CODEX_EXEC === "1") {
543
+ return "codex-exec";
544
+ }
545
+ if (process.env.HUMANISH_ENABLE_LOCAL_CODEX_APP_SERVER === "1") {
546
+ return "codex-app-server";
547
+ }
548
+ return undefined;
549
+ }
550
+ async function runLocalCodexTui(options) {
551
+ const warnings = [];
552
+ if (options.simCount !== 1) {
553
+ return {
554
+ schema: "humanish.run-result.v1",
555
+ ok: false,
556
+ cwd: options.cwd,
557
+ warnings,
558
+ error: {
559
+ code: "HUMANISH_ACTOR_FANOUT_UNIMPLEMENTED",
560
+ message: "Local Codex TUI actor support is currently single-lane because it owns one PTY/UI session. Use codex-exec for bounded-concurrency fanout."
561
+ }
562
+ };
563
+ }
564
+ const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("HUMANISH_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
565
+ if (timeoutMs === null) {
566
+ return {
567
+ schema: "humanish.run-result.v1",
568
+ ok: false,
569
+ cwd: options.cwd,
570
+ warnings,
571
+ error: {
572
+ code: "HUMANISH_INVALID_TIMEOUT",
573
+ message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
574
+ }
575
+ };
576
+ }
577
+ const now = new Date();
578
+ const createdAt = now.toISOString();
579
+ const runId = options.runId ?? `codex-tui-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
580
+ const artifactRoot = path.join(".humanish", "runs", runId);
581
+ const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
582
+ const transcriptPath = path.join(absoluteArtifactRoot, "transcripts", "codex-tui-sanitized.txt");
583
+ const actorTracePath = path.join(absoluteArtifactRoot, "actor.json");
584
+ const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
585
+ const packageName = await readPackageName(options.cwd);
586
+ const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
587
+ const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
588
+ const selection = await loadDryRunSelection(options.cwd, humanishSource);
589
+ if (humanishSource === "missing") {
590
+ warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
591
+ }
592
+ warnings.push(...selection.warnings);
593
+ const verdictNonce = randomUUID().slice(0, 12);
594
+ const prompt = buildLocalCodexTuiPrompt(selection, verdictNonce);
595
+ const promptDigest = digestText(prompt);
596
+ const command = resolveLocalCodexTuiCommand(options.cwd, prompt, options.actorCommand);
597
+ const usesDefaultCodexCommand = options.actorCommand === undefined && process.env.HUMANISH_CODEX_ACTOR_COMMAND === undefined;
598
+ const simId = "sim-01";
599
+ const streamId = "sim-01-codex-tui";
600
+ const events = [];
601
+ const appendEvent = async (type, message, level = "info") => {
602
+ events.push({
603
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
604
+ at: new Date().toISOString(),
605
+ level,
606
+ type,
607
+ message,
608
+ simId,
609
+ streamId
610
+ });
611
+ await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
612
+ };
613
+ await mkdir(path.dirname(transcriptPath), { recursive: true });
614
+ let actor;
615
+ const trustPreflight = usesDefaultCodexCommand ? await checkCodexWorkspaceTrust(options.cwd) : { ok: true };
616
+ if (!trustPreflight.ok) {
617
+ actor = {
618
+ durationMs: 0,
619
+ reason: trustPreflight.message,
620
+ status: "blocked",
621
+ transcript: `${trustPreflight.message}\nRecovery: ${trustPreflight.recoveryCommand}\n`,
622
+ transcriptBytes: Buffer.byteLength(trustPreflight.message)
623
+ };
624
+ await appendEvent("actor.preflight.blocked", redactSensitiveText(trustPreflight.message), "warn");
625
+ }
626
+ else {
627
+ await appendEvent("actor.spawned", `Spawned local Codex TUI actor command ${command.name} in explicit opt-in mode.`);
628
+ await appendEvent("actor.prompt.submitted", `Submitted bounded public-safe dogfood prompt digest ${promptDigest}; raw prompt omitted from event log.`);
629
+ await appendEvent("actor.running", "Published local Codex TUI running snapshot for Observer polling.");
630
+ const runningAt = new Date().toISOString();
631
+ const runningBundle = {
632
+ schema: RUN_BUNDLE_SCHEMA,
633
+ runId,
634
+ mode: "live",
635
+ simCount: 1,
636
+ createdAt,
637
+ cwd: options.cwd,
638
+ artifactRoot,
639
+ source,
640
+ persona: selection.persona,
641
+ scenario: selection.scenario,
642
+ lifecycle: [
643
+ {
644
+ at: createdAt,
645
+ event: "run.created",
646
+ message: "Live local Codex TUI run created with one explicit opt-in actor."
647
+ },
648
+ {
649
+ at: createdAt,
650
+ event: "actor.selected",
651
+ message: "Selected local codex-tui actor."
652
+ },
653
+ {
654
+ at: runningAt,
655
+ event: "actor.running",
656
+ message: "Local Codex TUI actor is running; Observer data will refresh with sanitized evidence after completion."
657
+ }
658
+ ],
659
+ simulations: [
660
+ {
661
+ id: simId,
662
+ index: 1,
663
+ personaId: selection.persona.id,
664
+ scenarioId: selection.scenario.id,
665
+ status: "running",
666
+ streamKind: "tui",
667
+ mode: "tui-sim",
668
+ progress: 35,
669
+ currentStep: "Local Codex TUI actor running",
670
+ summary: "Local Codex TUI actor is running.",
671
+ streamIds: [streamId],
672
+ startedAt: createdAt,
673
+ updatedAt: runningAt
674
+ }
675
+ ],
676
+ streams: [
677
+ {
678
+ id: streamId,
679
+ simId,
680
+ kind: "tui",
681
+ label: "Local Codex TUI actor",
682
+ status: "running",
683
+ transport: "pty",
684
+ updatedAt: runningAt,
685
+ embed: {
686
+ kind: "terminal",
687
+ title: "Local Codex TUI actor"
688
+ },
689
+ terminal: {
690
+ title: "Local Codex TUI actor",
691
+ format: "ansi",
692
+ stdin: "sent",
693
+ tail: "Codex TUI actor is running; sanitized transcript evidence will be linked after completion."
694
+ },
695
+ completion: {
696
+ checkedAt: runningAt,
697
+ reason: "actor process is still running",
698
+ status: "running"
699
+ },
700
+ artifacts: [
701
+ { label: "run bundle", path: "run.json", kind: "bundle" },
702
+ { label: "review", path: "review.md", kind: "review" },
703
+ { label: "event log", path: "events.ndjson", kind: "events" }
704
+ ]
705
+ }
706
+ ],
707
+ events,
708
+ redaction: {
709
+ status: "passed",
710
+ notes: "Running TUI bundle contains no raw transcript yet; final actor output will be redacted before persistence."
711
+ },
712
+ artifacts: {
713
+ run: "run.json",
714
+ reviewJson: "review.json",
715
+ reviewMarkdown: "review.md",
716
+ observerData: "observer/observer-data.json",
717
+ events: "events.ndjson"
718
+ },
719
+ review: createLocalActorRunningReviewSummary("Codex TUI"),
720
+ feedbackCandidates: []
721
+ };
722
+ await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
723
+ await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
724
+ schema: "humanish.latest-run.v1",
725
+ runId,
726
+ path: artifactRoot,
727
+ updatedAt: runningAt
728
+ });
729
+ actor = await executeLocalActorCommand(command, {
730
+ cwd: options.cwd,
731
+ timeoutMs,
732
+ verdictNonce
733
+ });
734
+ }
735
+ const completedAt = new Date().toISOString();
736
+ const redactedTranscript = redactSensitiveText(actor.transcript);
737
+ const tail = tailText(redactedTranscript, 6_000);
738
+ const status = actor.status;
739
+ // Redact the reason before it flows into the persisted event, simulation
740
+ // summary, completion reason, review, and lifecycle. For a trust-preflight
741
+ // block the reason embeds the absolute workspace path; the live CLI reason can
742
+ // still show the real path, but the public-safe bundle must not.
743
+ const verdictReason = redactSensitiveText(actor.reason);
744
+ await writeFile(transcriptPath, redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
745
+ await writeJson(actorTracePath, {
746
+ schema: "humanish.local-codex-tui-actor.v1",
747
+ actor: "codex-tui",
748
+ commandName: command.name,
749
+ promptDigest,
750
+ verdictNonce,
751
+ startedAt: createdAt,
752
+ completedAt,
753
+ durationMs: actor.durationMs,
754
+ exitCode: actor.exitCode,
755
+ signal: actor.signal,
756
+ status,
757
+ timeoutMs,
758
+ transcriptBytes: actor.transcriptBytes,
759
+ transcriptPath: "transcripts/codex-tui-sanitized.txt",
760
+ redaction: "passed"
761
+ });
762
+ await appendEvent("actor.observation", `Captured ${actor.transcriptBytes} output byte${actor.transcriptBytes === 1 ? "" : "s"}; sanitized transcript tail recorded with redaction=passed.`);
763
+ await appendEvent("actor.artifact", "Wrote sanitized Codex TUI transcript and actor trace artifacts under the ignored run directory.");
764
+ await appendEvent("actor.verdict", `Local Codex TUI actor verdict ${status}: ${verdictReason}`, status === "passed" ? "info" : status === "timed_out" ? "warn" : "error");
765
+ await appendEvent(status === "timed_out" ? "actor.timeout" : status === "blocked" && !trustPreflight.ok ? "actor.blocked" : "actor.exited", status === "timed_out"
766
+ ? `Actor timed out after ${timeoutMs}ms; last safe observation retained.`
767
+ : status === "blocked" && !trustPreflight.ok
768
+ ? "Actor launch was blocked by preflight before spawn."
769
+ : `Actor exited with code ${actor.exitCode ?? "null"}${actor.signal ? ` and signal ${actor.signal}` : ""}.`, status === "passed" ? "info" : "warn");
770
+ const bundle = {
771
+ schema: RUN_BUNDLE_SCHEMA,
772
+ runId,
773
+ mode: "live",
774
+ simCount: 1,
775
+ createdAt,
776
+ cwd: options.cwd,
777
+ artifactRoot,
778
+ source,
779
+ persona: selection.persona,
780
+ scenario: selection.scenario,
781
+ lifecycle: [
782
+ {
783
+ at: createdAt,
784
+ event: "run.created",
785
+ message: "Live local Codex TUI run created with one explicit opt-in actor."
786
+ },
787
+ {
788
+ at: createdAt,
789
+ event: "actor.selected",
790
+ message: "Selected local codex-tui actor."
791
+ },
792
+ {
793
+ at: completedAt,
794
+ event: "review.skeleton.created",
795
+ message: "Created review skeleton from sanitized actor lifecycle evidence."
796
+ }
797
+ ],
798
+ simulations: [
799
+ {
800
+ id: simId,
801
+ index: 1,
802
+ personaId: selection.persona.id,
803
+ scenarioId: selection.scenario.id,
804
+ status,
805
+ streamKind: "tui",
806
+ mode: "tui-sim",
807
+ progress: 100,
808
+ currentStep: status === "passed" ? "Local Codex TUI actor completed" : "Local Codex TUI actor needs review",
809
+ summary: `Local Codex TUI actor ${status}: ${verdictReason}`,
810
+ streamIds: [streamId],
811
+ startedAt: createdAt,
812
+ updatedAt: completedAt
813
+ }
814
+ ],
815
+ streams: [
816
+ {
817
+ id: streamId,
818
+ simId,
819
+ kind: "tui",
820
+ label: "Local Codex TUI actor",
821
+ status,
822
+ transport: "pty",
823
+ updatedAt: completedAt,
824
+ embed: {
825
+ kind: "terminal",
826
+ title: "Local Codex TUI actor"
827
+ },
828
+ terminal: {
829
+ title: "Local Codex TUI actor",
830
+ format: "ansi",
831
+ stdin: "sent",
832
+ tail
833
+ },
834
+ completion: {
835
+ checkedAt: completedAt,
836
+ ...(actor.exitCode === undefined ? {} : { exitCode: actor.exitCode }),
837
+ logTail: tail,
838
+ reason: verdictReason,
839
+ status
840
+ },
841
+ artifacts: [
842
+ { label: "run bundle", path: "run.json", kind: "bundle" },
843
+ { label: "review", path: "review.md", kind: "review" },
844
+ { label: "event log", path: "events.ndjson", kind: "events" },
845
+ { label: "sanitized transcript", path: "transcripts/codex-tui-sanitized.txt", kind: "log" },
846
+ { label: "actor trace", path: "actor.json", kind: "trace" }
847
+ ]
848
+ }
849
+ ],
850
+ events,
851
+ redaction: {
852
+ status: "passed",
853
+ notes: "Actor output was redacted before transcript and bundle persistence; raw prompt is omitted from event log."
854
+ },
855
+ artifacts: {
856
+ run: "run.json",
857
+ reviewJson: "review.json",
858
+ reviewMarkdown: "review.md",
859
+ observerData: "observer/observer-data.json",
860
+ events: "events.ndjson"
861
+ },
862
+ review: createLocalActorReviewSummary("Codex TUI", status, verdictReason),
863
+ feedbackCandidates: []
864
+ };
865
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
866
+ await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
867
+ schema: "humanish.latest-run.v1",
868
+ runId,
869
+ path: artifactRoot,
870
+ updatedAt: completedAt
871
+ });
872
+ return {
873
+ schema: "humanish.run-result.v1",
874
+ ok: status === "passed",
875
+ runId,
876
+ mode: "live",
877
+ simCount: 1,
878
+ cwd: options.cwd,
879
+ artifactRoot,
880
+ bundlePath: path.join(artifactRoot, "run.json"),
881
+ reviewPath: path.join(artifactRoot, "review.md"),
882
+ latestPath: path.join(".humanish", "runs", "latest.json"),
883
+ warnings,
884
+ ...(status === "passed"
885
+ ? {}
886
+ : {
887
+ error: {
888
+ code: "HUMANISH_LOCAL_CODEX_TUI_FAILED",
889
+ message: `Local Codex TUI actor ${status}: ${verdictReason}`
890
+ }
891
+ })
892
+ };
893
+ }
894
+ function buildLocalCodexExecBundle(args) {
895
+ return {
896
+ schema: RUN_BUNDLE_SCHEMA,
897
+ runId: args.runId,
898
+ mode: "live",
899
+ simCount: args.simCount,
900
+ createdAt: args.createdAt,
901
+ cwd: args.cwd,
902
+ artifactRoot: args.artifactRoot,
903
+ source: args.source,
904
+ persona: args.persona,
905
+ scenario: args.scenario,
906
+ lifecycle: args.lifecycle,
907
+ simulations: args.lanes.map((lane, index) => ({
908
+ id: lane.simId,
909
+ index: index + 1,
910
+ personaId: `codex-exec-${lane.focus.id}`,
911
+ scenarioId: args.scenario.id,
912
+ status: lane.status,
913
+ streamKind: "terminal",
914
+ mode: "cli-sim",
915
+ progress: lane.progress,
916
+ currentStep: lane.currentStep,
917
+ summary: lane.summary,
918
+ streamIds: [lane.streamId],
919
+ startedAt: args.createdAt,
920
+ updatedAt: lane.updatedAt
921
+ })),
922
+ streams: args.lanes.map((lane) => {
923
+ const artifacts = [
924
+ { label: "run bundle", path: "run.json", kind: "bundle" },
925
+ { label: "review", path: "review.md", kind: "review" },
926
+ { label: "event log", path: "events.ndjson", kind: "events" },
927
+ ...(lane.transcriptPath ? [{ label: "sanitized transcript", path: lane.transcriptPath, kind: "log" }] : []),
928
+ ...(lane.tracePath ? [{ label: "actor trace", path: lane.tracePath, kind: "trace" }] : [])
929
+ ];
930
+ return {
931
+ id: lane.streamId,
932
+ simId: lane.simId,
933
+ kind: "terminal",
934
+ label: `Local Codex exec - ${lane.focus.label}`,
935
+ status: lane.status,
936
+ transport: "snapshot",
937
+ updatedAt: lane.updatedAt,
938
+ embed: {
939
+ kind: "terminal",
940
+ title: `Local Codex exec - ${lane.focus.label}`
941
+ },
942
+ terminal: {
943
+ title: `Local Codex exec - ${lane.focus.label}`,
944
+ format: "plain",
945
+ stdin: "sent",
946
+ tail: lane.terminalTail
947
+ },
948
+ ...(lane.completion ? { completion: lane.completion } : {}),
949
+ artifacts
950
+ };
951
+ }),
952
+ events: args.events,
953
+ redaction: {
954
+ status: "passed",
955
+ notes: "Actor output was redacted before transcript and bundle persistence; raw prompt is omitted from event log."
956
+ },
957
+ artifacts: {
958
+ run: "run.json",
959
+ reviewJson: "review.json",
960
+ reviewMarkdown: "review.md",
961
+ observerData: "observer/observer-data.json",
962
+ events: "events.ndjson"
963
+ },
964
+ review: args.review,
965
+ feedbackCandidates: []
966
+ };
967
+ }
968
+ async function runLocalCodexExec(options) {
969
+ const warnings = [];
970
+ const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("HUMANISH_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
971
+ if (timeoutMs === null) {
972
+ return {
973
+ schema: "humanish.run-result.v1",
974
+ ok: false,
975
+ cwd: options.cwd,
976
+ warnings,
977
+ error: {
978
+ code: "HUMANISH_INVALID_TIMEOUT",
979
+ message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
980
+ }
981
+ };
982
+ }
983
+ const maxConcurrency = normalizePositiveInteger(readEnvInteger("HUMANISH_LOCAL_CODEX_EXEC_MAX_CONCURRENCY") ?? LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY);
984
+ if (maxConcurrency === null) {
985
+ return {
986
+ schema: "humanish.run-result.v1",
987
+ ok: false,
988
+ cwd: options.cwd,
989
+ warnings,
990
+ error: {
991
+ code: "HUMANISH_INVALID_ACTOR_CONCURRENCY",
992
+ message: "HUMANISH_LOCAL_CODEX_EXEC_MAX_CONCURRENCY must be a positive integer."
993
+ }
994
+ };
995
+ }
996
+ const now = new Date();
997
+ const createdAt = now.toISOString();
998
+ const runId = options.runId ?? `codex-exec-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
999
+ const artifactRoot = path.join(".humanish", "runs", runId);
1000
+ const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
1001
+ const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
1002
+ const packageName = await readPackageName(options.cwd);
1003
+ const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
1004
+ const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
1005
+ const selection = await loadDryRunSelection(options.cwd, humanishSource);
1006
+ if (humanishSource === "missing") {
1007
+ warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
1008
+ }
1009
+ warnings.push(...selection.warnings);
1010
+ const verdictNonce = randomUUID().slice(0, 12);
1011
+ const events = [];
1012
+ const pushEvent = (type, message, level = "info", simId, streamId) => {
1013
+ events.push({
1014
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
1015
+ at: new Date().toISOString(),
1016
+ level,
1017
+ type,
1018
+ message,
1019
+ ...(simId === undefined ? {} : { simId }),
1020
+ ...(streamId === undefined ? {} : { streamId })
1021
+ });
1022
+ };
1023
+ await mkdir(path.join(absoluteArtifactRoot, "transcripts"), { recursive: true });
1024
+ await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
1025
+ await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
1026
+ await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
1027
+ schema: "humanish.latest-run.v1",
1028
+ runId,
1029
+ path: artifactRoot,
1030
+ updatedAt: createdAt
1031
+ });
1032
+ const lanes = Array.from({ length: options.simCount }, (_, index) => {
1033
+ const focus = localCodexExecFocus(index);
1034
+ const simId = `sim-${String(index + 1).padStart(2, "0")}`;
1035
+ const streamId = `${simId}-codex-exec`;
1036
+ const prompt = buildLocalCodexExecPrompt(selection, verdictNonce, {
1037
+ focus,
1038
+ index: index + 1,
1039
+ total: options.simCount
1040
+ });
1041
+ const promptDigest = digestText(prompt);
1042
+ const command = resolveLocalCodexExecCommand(options.cwd, prompt, options.actorCommand);
1043
+ pushEvent("actor.spawned", `Spawned local Codex exec actor lane ${index + 1}/${options.simCount} (${focus.label}) command ${command.name} in explicit opt-in mode.`, "info", simId, streamId);
1044
+ pushEvent("actor.prompt.submitted", `Submitted bounded public-safe dogfood prompt digest ${promptDigest}; raw prompt omitted from event log.`, "info", simId, streamId);
1045
+ return { command, focus, promptDigest, simId, streamId };
1046
+ });
1047
+ for (const lane of lanes) {
1048
+ pushEvent("actor.running", "Published local Codex exec running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
1049
+ }
1050
+ await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1051
+ const baseLifecycle = [
1052
+ {
1053
+ at: createdAt,
1054
+ event: "run.created",
1055
+ message: `Live local Codex exec run created with ${options.simCount} explicit opt-in actor${options.simCount === 1 ? "" : "s"} and max concurrency ${maxConcurrency}.`
1056
+ },
1057
+ {
1058
+ at: createdAt,
1059
+ event: "actor.selected",
1060
+ message: "Selected local codex-exec actor."
1061
+ }
1062
+ ];
1063
+ const runningAt = new Date().toISOString();
1064
+ const runningBundle = buildLocalCodexExecBundle({
1065
+ runId,
1066
+ simCount: options.simCount,
1067
+ createdAt,
1068
+ cwd: options.cwd,
1069
+ artifactRoot,
1070
+ packageName,
1071
+ humanishSource,
1072
+ source,
1073
+ persona: selection.persona,
1074
+ scenario: selection.scenario,
1075
+ lifecycle: [
1076
+ ...baseLifecycle,
1077
+ {
1078
+ at: runningAt,
1079
+ event: "actor.running",
1080
+ message: `Local Codex exec actor lanes are running with max concurrency ${maxConcurrency}; Observer data will refresh as sanitized evidence arrives.`
1081
+ }
1082
+ ],
1083
+ lanes: lanes.map((lane) => ({
1084
+ focus: lane.focus,
1085
+ simId: lane.simId,
1086
+ streamId: lane.streamId,
1087
+ status: "running",
1088
+ progress: 35,
1089
+ currentStep: "Local Codex exec actor running",
1090
+ summary: `Local Codex exec actor ${lane.focus.label} is running.`,
1091
+ terminalTail: "Codex exec actor is running; sanitized transcript evidence will be linked after completion.",
1092
+ updatedAt: runningAt,
1093
+ completion: {
1094
+ checkedAt: runningAt,
1095
+ reason: "actor process is still running",
1096
+ status: "running"
1097
+ }
1098
+ })),
1099
+ events,
1100
+ review: createLocalActorRunningReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout")
1101
+ });
1102
+ await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
1103
+ const laneResults = await mapWithConcurrency(lanes, maxConcurrency, async (lane) => {
1104
+ const actor = await executeLocalActorCommand(lane.command, {
1105
+ cwd: options.cwd,
1106
+ timeoutMs,
1107
+ verdictNonce
1108
+ });
1109
+ const redactedTranscript = redactSensitiveText(actor.transcript);
1110
+ const tail = tailText(redactedTranscript, 6_000);
1111
+ const transcriptPath = options.simCount === 1
1112
+ ? "transcripts/codex-exec-sanitized.jsonl"
1113
+ : `transcripts/${lane.streamId}-sanitized.jsonl`;
1114
+ const tracePath = options.simCount === 1 ? "actor.json" : `actors/${lane.streamId}.json`;
1115
+ await writeFile(path.join(absoluteArtifactRoot, transcriptPath), redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
1116
+ await writeJson(path.join(absoluteArtifactRoot, tracePath), {
1117
+ schema: "humanish.local-codex-exec-actor.v1",
1118
+ actor: "codex-exec",
1119
+ commandName: lane.command.name,
1120
+ focusId: lane.focus.id,
1121
+ promptDigest: lane.promptDigest,
1122
+ verdictNonce,
1123
+ startedAt: createdAt,
1124
+ completedAt: new Date().toISOString(),
1125
+ durationMs: actor.durationMs,
1126
+ exitCode: actor.exitCode,
1127
+ signal: actor.signal,
1128
+ status: actor.status,
1129
+ timeoutMs,
1130
+ transcriptBytes: actor.transcriptBytes,
1131
+ transcriptPath,
1132
+ redaction: "passed"
1133
+ });
1134
+ return {
1135
+ actor,
1136
+ command: lane.command,
1137
+ focus: lane.focus,
1138
+ promptDigest: lane.promptDigest,
1139
+ simId: lane.simId,
1140
+ streamId: lane.streamId,
1141
+ tail,
1142
+ tracePath,
1143
+ transcriptPath
1144
+ };
1145
+ });
1146
+ const completedAt = new Date().toISOString();
1147
+ const laneStatuses = laneResults.map((result) => result.actor.status);
1148
+ const status = aggregateActorStatus(laneStatuses);
1149
+ const verdictReason = options.simCount === 1
1150
+ ? laneResults[0]?.actor.reason ?? "actor did not return a result"
1151
+ : summarizeExecFanout(laneStatuses);
1152
+ for (const result of laneResults) {
1153
+ pushEvent("actor.observation", `Captured ${result.actor.transcriptBytes} output byte${result.actor.transcriptBytes === 1 ? "" : "s"}; sanitized transcript tail recorded with redaction=passed.`, "info", result.simId, result.streamId);
1154
+ pushEvent("actor.artifact", "Wrote sanitized Codex exec transcript and actor trace artifacts under the ignored run directory.", "info", result.simId, result.streamId);
1155
+ pushEvent("actor.verdict", `Local Codex exec actor lane ${result.focus.label} verdict ${result.actor.status}: ${result.actor.reason}`, result.actor.status === "passed" ? "info" : result.actor.status === "timed_out" ? "warn" : "error", result.simId, result.streamId);
1156
+ pushEvent(result.actor.status === "timed_out" ? "actor.timeout" : "actor.exited", result.actor.status === "timed_out"
1157
+ ? `Actor timed out after ${timeoutMs}ms; last safe observation retained.`
1158
+ : `Actor exited with code ${result.actor.exitCode ?? "null"}${result.actor.signal ? ` and signal ${result.actor.signal}` : ""}.`, result.actor.status === "passed" ? "info" : "warn", result.simId, result.streamId);
1159
+ }
1160
+ await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1161
+ const bundle = buildLocalCodexExecBundle({
1162
+ runId,
1163
+ simCount: options.simCount,
1164
+ createdAt,
1165
+ cwd: options.cwd,
1166
+ artifactRoot,
1167
+ packageName,
1168
+ humanishSource,
1169
+ source,
1170
+ persona: selection.persona,
1171
+ scenario: selection.scenario,
1172
+ lifecycle: [
1173
+ ...baseLifecycle,
1174
+ {
1175
+ at: completedAt,
1176
+ event: "review.skeleton.created",
1177
+ message: "Created review skeleton from sanitized actor lifecycle evidence."
1178
+ }
1179
+ ],
1180
+ lanes: laneResults.map((result) => ({
1181
+ focus: result.focus,
1182
+ simId: result.simId,
1183
+ streamId: result.streamId,
1184
+ status: result.actor.status,
1185
+ progress: 100,
1186
+ currentStep: result.actor.status === "passed" ? "Local Codex exec actor completed" : "Local Codex exec actor needs review",
1187
+ summary: `Local Codex exec actor ${result.focus.label} ${result.actor.status}: ${result.actor.reason}`,
1188
+ terminalTail: result.tail,
1189
+ updatedAt: completedAt,
1190
+ transcriptPath: result.transcriptPath,
1191
+ tracePath: result.tracePath,
1192
+ completion: {
1193
+ checkedAt: completedAt,
1194
+ ...(result.actor.exitCode === undefined ? {} : { exitCode: result.actor.exitCode }),
1195
+ logTail: result.tail,
1196
+ reason: result.actor.reason,
1197
+ status: result.actor.status
1198
+ }
1199
+ })),
1200
+ events,
1201
+ review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout", status, verdictReason)
1202
+ });
1203
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
1204
+ return {
1205
+ schema: "humanish.run-result.v1",
1206
+ ok: status === "passed",
1207
+ runId,
1208
+ mode: "live",
1209
+ simCount: options.simCount,
1210
+ cwd: options.cwd,
1211
+ artifactRoot,
1212
+ bundlePath: path.join(artifactRoot, "run.json"),
1213
+ reviewPath: path.join(artifactRoot, "review.md"),
1214
+ latestPath: path.join(".humanish", "runs", "latest.json"),
1215
+ warnings,
1216
+ ...(status === "passed"
1217
+ ? {}
1218
+ : {
1219
+ error: {
1220
+ code: "HUMANISH_LOCAL_CODEX_EXEC_FAILED",
1221
+ message: `Local Codex exec actor ${status}: ${verdictReason}`
1222
+ }
1223
+ })
1224
+ };
1225
+ }
1226
+ function buildLocalCodexAppServerBundle(args) {
1227
+ return {
1228
+ schema: RUN_BUNDLE_SCHEMA,
1229
+ runId: args.runId,
1230
+ mode: "live",
1231
+ simCount: args.simCount,
1232
+ createdAt: args.createdAt,
1233
+ cwd: args.cwd,
1234
+ artifactRoot: args.artifactRoot,
1235
+ source: args.source,
1236
+ persona: args.persona,
1237
+ scenario: args.scenario,
1238
+ lifecycle: args.lifecycle,
1239
+ simulations: args.lanes.map((lane, index) => ({
1240
+ id: lane.simId,
1241
+ index: index + 1,
1242
+ personaId: `codex-app-server-${lane.focus.id}`,
1243
+ scenarioId: args.scenario.id,
1244
+ status: lane.status,
1245
+ streamKind: "codex-ui",
1246
+ mode: "codex-app-sim",
1247
+ progress: lane.progress,
1248
+ currentStep: lane.currentStep,
1249
+ summary: lane.summary,
1250
+ streamIds: [lane.streamId],
1251
+ startedAt: args.createdAt,
1252
+ updatedAt: lane.updatedAt
1253
+ })),
1254
+ streams: args.lanes.map((lane) => {
1255
+ const result = lane.result;
1256
+ // Provider-neutral projection of the actor evidence, with the persona's
1257
+ // applied traits threaded in. Only present once the lane has a result.
1258
+ const actor = result
1259
+ ? getActor("codex-app-server").toActorTrace(result, {
1260
+ id: args.persona.id,
1261
+ traitsApplied: personaToDirectives(args.resolvedPersona).traitsApplied,
1262
+ promptDigest: result.trace.promptDigest
1263
+ })
1264
+ : undefined;
1265
+ const artifacts = [
1266
+ { label: "run bundle", path: "run.json", kind: "bundle" },
1267
+ { label: "review", path: "review.md", kind: "review" },
1268
+ { label: "event log", path: "events.ndjson", kind: "events" },
1269
+ ...(result ? [
1270
+ { label: "codex app-server trace", path: result.tracePath, kind: "trace" },
1271
+ { label: "codex app-server events", path: result.eventsPath, kind: "events" },
1272
+ { label: "codex app-server transcript", path: result.transcriptPath, kind: "log" }
1273
+ ] : [])
1274
+ ];
1275
+ return {
1276
+ id: lane.streamId,
1277
+ simId: lane.simId,
1278
+ kind: "codex-ui",
1279
+ label: `Codex app-server - ${lane.focus.label}`,
1280
+ status: lane.status,
1281
+ transport: "app-server",
1282
+ updatedAt: lane.updatedAt,
1283
+ embed: {
1284
+ kind: "placeholder",
1285
+ title: `Codex app-server - ${lane.focus.label}`
1286
+ },
1287
+ terminal: {
1288
+ title: `Codex app-server - ${lane.focus.label}`,
1289
+ format: "plain",
1290
+ stdin: "sent",
1291
+ tail: lane.terminalTail
1292
+ },
1293
+ codex: {
1294
+ provider: "codex-app-server",
1295
+ contract: "Humanish captures Codex app-server Thread, Turn, Item, approval, command, file, tool, message, and reasoning evidence as redacted local artifacts.",
1296
+ state: codexStateForStream(lane.status),
1297
+ ...(result?.experimentalApi === undefined ? {} : { experimentalApi: result.experimentalApi }),
1298
+ ...(result?.counts === undefined ? {} : { eventCount: result.counts.envelopes }),
1299
+ ...(result?.model === undefined ? {} : { model: result.model }),
1300
+ ...(result?.sessionId === undefined ? {} : { sessionId: result.sessionId }),
1301
+ ...(result?.threadId === undefined ? {} : { threadId: result.threadId }),
1302
+ ...(result?.trace === undefined ? {} : { trace: result.trace }),
1303
+ ...(result?.tracePath === undefined ? {} : { tracePath: result.tracePath }),
1304
+ ...(result?.turnId === undefined ? {} : { turnId: result.turnId })
1305
+ },
1306
+ ...(actor === undefined ? {} : { actor }),
1307
+ ...(lane.completion ? { completion: lane.completion } : {}),
1308
+ artifacts
1309
+ };
1310
+ }),
1311
+ events: args.events,
1312
+ redaction: {
1313
+ status: "passed",
1314
+ notes: "Codex app-server envelopes and transcript summaries were redacted before persistence; raw prompts and secret-bearing fields are not stored in run events."
1315
+ },
1316
+ artifacts: {
1317
+ run: "run.json",
1318
+ reviewJson: "review.json",
1319
+ reviewMarkdown: "review.md",
1320
+ observerData: "observer/observer-data.json",
1321
+ events: "events.ndjson"
1322
+ },
1323
+ review: args.review,
1324
+ feedbackCandidates: []
1325
+ };
1326
+ }
1327
+ async function runLocalCodexAppServer(options) {
1328
+ const warnings = [];
1329
+ const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("HUMANISH_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
1330
+ if (timeoutMs === null) {
1331
+ return {
1332
+ schema: "humanish.run-result.v1",
1333
+ ok: false,
1334
+ cwd: options.cwd,
1335
+ warnings,
1336
+ error: {
1337
+ code: "HUMANISH_INVALID_TIMEOUT",
1338
+ message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
1339
+ }
1340
+ };
1341
+ }
1342
+ const now = new Date();
1343
+ const createdAt = now.toISOString();
1344
+ const runId = options.runId ?? `codex-app-server-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1345
+ const artifactRoot = path.join(".humanish", "runs", runId);
1346
+ const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
1347
+ const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
1348
+ const packageName = await readPackageName(options.cwd);
1349
+ const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
1350
+ const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
1351
+ const selection = await loadDryRunSelection(options.cwd, humanishSource);
1352
+ if (humanishSource === "missing") {
1353
+ warnings.push("Committed humanish/ source was not found; using built-in synthetic Codex app-server actor defaults.");
1354
+ }
1355
+ warnings.push(...selection.warnings);
1356
+ const events = [];
1357
+ const pushEvent = (type, message, level = "info", simId, streamId) => {
1358
+ events.push({
1359
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
1360
+ at: new Date().toISOString(),
1361
+ level,
1362
+ type,
1363
+ message,
1364
+ ...(simId === undefined ? {} : { simId }),
1365
+ ...(streamId === undefined ? {} : { streamId })
1366
+ });
1367
+ };
1368
+ await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
1369
+ await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
1370
+ await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
1371
+ schema: "humanish.latest-run.v1",
1372
+ runId,
1373
+ path: artifactRoot,
1374
+ updatedAt: createdAt
1375
+ });
1376
+ const lanes = Array.from({ length: options.simCount }, (_, index) => {
1377
+ const focus = localCodexExecFocus(index);
1378
+ const simId = `sim-${String(index + 1).padStart(2, "0")}`;
1379
+ const streamId = `${simId}-codex-app-server`;
1380
+ const prompt = buildLocalCodexAppServerPrompt(selection, {
1381
+ focus,
1382
+ index: index + 1,
1383
+ total: options.simCount
1384
+ });
1385
+ const promptDigest = digestText(prompt);
1386
+ pushEvent("codex-app-server.spawned", `Spawned Codex app-server lane ${index + 1}/${options.simCount} (${focus.label}) in explicit opt-in mode.`, "info", simId, streamId);
1387
+ pushEvent("codex-app-server.prompt.submitted", `Submitted bounded public-safe app-server prompt digest ${promptDigest}; raw prompt omitted from event log.`, "info", simId, streamId);
1388
+ return {
1389
+ focus,
1390
+ prefix: options.simCount === 1 ? "" : `actors/${streamId}/`,
1391
+ prompt,
1392
+ promptDigest,
1393
+ simId,
1394
+ streamId
1395
+ };
1396
+ });
1397
+ for (const lane of lanes) {
1398
+ pushEvent("codex-app-server.running", "Published Codex app-server running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
1399
+ }
1400
+ await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1401
+ const baseLifecycle = [
1402
+ {
1403
+ at: createdAt,
1404
+ event: "run.created",
1405
+ message: `Live Codex app-server run created with ${options.simCount} explicit opt-in lane${options.simCount === 1 ? "" : "s"}.`
1406
+ },
1407
+ {
1408
+ at: createdAt,
1409
+ event: "actor.selected",
1410
+ message: "Selected local codex-app-server actor."
1411
+ }
1412
+ ];
1413
+ const runningAt = new Date().toISOString();
1414
+ const runningBundle = buildLocalCodexAppServerBundle({
1415
+ runId,
1416
+ simCount: options.simCount,
1417
+ createdAt,
1418
+ cwd: options.cwd,
1419
+ artifactRoot,
1420
+ packageName,
1421
+ humanishSource,
1422
+ source,
1423
+ persona: selection.persona,
1424
+ resolvedPersona: selection.resolvedPersona,
1425
+ scenario: selection.scenario,
1426
+ lifecycle: [
1427
+ ...baseLifecycle,
1428
+ {
1429
+ at: runningAt,
1430
+ event: "codex-app-server.running",
1431
+ message: "Codex app-server lanes are running; Observer data will refresh as redacted app-server evidence arrives."
1432
+ }
1433
+ ],
1434
+ lanes: lanes.map((lane) => ({
1435
+ focus: lane.focus,
1436
+ simId: lane.simId,
1437
+ streamId: lane.streamId,
1438
+ status: "running",
1439
+ progress: 35,
1440
+ currentStep: "Codex app-server actor running",
1441
+ summary: `Codex app-server actor ${lane.focus.label} is running.`,
1442
+ terminalTail: "Codex app-server actor is running; redacted Thread/Turn/Item trace evidence will be linked after completion.",
1443
+ updatedAt: runningAt,
1444
+ completion: {
1445
+ checkedAt: runningAt,
1446
+ reason: "Codex app-server turn is still running",
1447
+ status: "running"
1448
+ }
1449
+ })),
1450
+ events,
1451
+ review: createLocalActorRunningReviewSummary("Codex app-server")
1452
+ });
1453
+ await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
1454
+ const laneResults = await mapWithConcurrency(lanes, options.simCount, async (lane) => {
1455
+ const laneRunRoot = lane.prefix ? path.join(absoluteArtifactRoot, lane.prefix) : absoluteArtifactRoot;
1456
+ const result = await getActor("codex-app-server").runSession({
1457
+ cwd: options.cwd,
1458
+ prompt: lane.prompt,
1459
+ runRoot: laneRunRoot,
1460
+ timeoutMs,
1461
+ ...(options.actorCommand === undefined ? {} : { actorCommand: options.actorCommand }),
1462
+ approvalPolicy: "never",
1463
+ experimentalApi: process.env.HUMANISH_CODEX_APP_SERVER_EXPERIMENTAL === "1",
1464
+ ...(process.env.HUMANISH_CODEX_APP_SERVER_MODEL ? { model: process.env.HUMANISH_CODEX_APP_SERVER_MODEL } : {}),
1465
+ sandbox: readCodexAppServerSandboxFromEnv(),
1466
+ serviceName: "humanish"
1467
+ });
1468
+ return {
1469
+ lane,
1470
+ result: prefixCodexAppServerResultPaths(result, lane.prefix)
1471
+ };
1472
+ });
1473
+ const completedAt = new Date().toISOString();
1474
+ const statuses = laneResults.map((entry) => entry.result.status);
1475
+ const status = aggregateActorStatus(statuses);
1476
+ const verdictReason = options.simCount === 1
1477
+ ? laneResults[0]?.result.reason ?? "Codex app-server actor did not return a result"
1478
+ : summarizeExecFanout(statuses);
1479
+ for (const entry of laneResults) {
1480
+ pushEvent("codex-app-server.artifact", `Captured ${entry.result.counts.envelopes} app-server envelope${entry.result.counts.envelopes === 1 ? "" : "s"}; trace summary and transcript were redacted before persistence.`, "info", entry.lane.simId, entry.lane.streamId);
1481
+ pushEvent("codex-app-server.verdict", `Codex app-server actor lane ${entry.lane.focus.label} verdict ${entry.result.status}: ${entry.result.reason}`, entry.result.status === "passed" ? "info" : entry.result.status === "timed_out" ? "warn" : "error", entry.lane.simId, entry.lane.streamId);
1482
+ }
1483
+ await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1484
+ const bundle = buildLocalCodexAppServerBundle({
1485
+ runId,
1486
+ simCount: options.simCount,
1487
+ createdAt,
1488
+ cwd: options.cwd,
1489
+ artifactRoot,
1490
+ packageName,
1491
+ humanishSource,
1492
+ source,
1493
+ persona: selection.persona,
1494
+ resolvedPersona: selection.resolvedPersona,
1495
+ scenario: selection.scenario,
1496
+ lifecycle: [
1497
+ ...baseLifecycle,
1498
+ {
1499
+ at: completedAt,
1500
+ event: "review.skeleton.created",
1501
+ message: "Created review skeleton from redacted Codex app-server lifecycle evidence."
1502
+ }
1503
+ ],
1504
+ lanes: laneResults.map((entry) => ({
1505
+ focus: entry.lane.focus,
1506
+ simId: entry.lane.simId,
1507
+ streamId: entry.lane.streamId,
1508
+ status: entry.result.status,
1509
+ progress: 100,
1510
+ currentStep: entry.result.status === "passed" ? "Codex app-server actor completed" : "Codex app-server actor needs review",
1511
+ summary: `Codex app-server actor ${entry.lane.focus.label} ${entry.result.status}: ${entry.result.reason}`,
1512
+ terminalTail: entry.result.tail,
1513
+ updatedAt: completedAt,
1514
+ result: entry.result,
1515
+ completion: {
1516
+ checkedAt: completedAt,
1517
+ ...(entry.result.exitCode === undefined ? {} : { exitCode: entry.result.exitCode }),
1518
+ logTail: entry.result.tail,
1519
+ reason: entry.result.reason,
1520
+ status: entry.result.status
1521
+ }
1522
+ })),
1523
+ events,
1524
+ review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex app-server" : "Codex app-server fanout", status, verdictReason)
1525
+ });
1526
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
1527
+ return {
1528
+ schema: "humanish.run-result.v1",
1529
+ ok: status === "passed",
1530
+ runId,
1531
+ mode: "live",
1532
+ simCount: options.simCount,
1533
+ cwd: options.cwd,
1534
+ artifactRoot,
1535
+ bundlePath: path.join(artifactRoot, "run.json"),
1536
+ reviewPath: path.join(artifactRoot, "review.md"),
1537
+ latestPath: path.join(".humanish", "runs", "latest.json"),
1538
+ warnings,
1539
+ ...(status === "passed"
1540
+ ? {}
1541
+ : {
1542
+ error: {
1543
+ code: "HUMANISH_CODEX_APP_SERVER_FAILED",
1544
+ message: `Codex app-server actor ${status}: ${verdictReason}`
1545
+ }
1546
+ })
1547
+ };
1548
+ }
1549
+ function buildSyntheticObserverFixtures(args) {
1550
+ const templates = [
1551
+ {
1552
+ kind: "ui",
1553
+ mode: "browser-sim",
1554
+ label: "UI journey",
1555
+ currentStep: "Route and viewport contract captured",
1556
+ summary: "Browser lane reserved for VNC playback, screenshots, route state, and interaction trace.",
1557
+ tail: "open target app\nresolve first-run route\ncapture viewport state\nrecord interaction trace",
1558
+ viewport: { width: 1440, height: 960, deviceScaleFactor: 1 }
1559
+ },
1560
+ {
1561
+ kind: "terminal",
1562
+ mode: "cli-sim",
1563
+ label: "CLI actor",
1564
+ currentStep: "Command transcript contract captured",
1565
+ summary: "CLI lane reserved for command-by-command persona runs with stdout/stderr and artifact links.",
1566
+ tail: "$ humanish doctor\nok target cwd\nok humanish source\n$ humanish run --scenario first-run-smoke\ncontract proof emitted",
1567
+ viewport: undefined
1568
+ },
1569
+ {
1570
+ kind: "tui",
1571
+ mode: "tui-sim",
1572
+ label: "TUI actor",
1573
+ currentStep: "Terminal UI frame contract captured",
1574
+ summary: "TUI lane reserved for PTY bytes, ANSI rendering, focus replay, and optional assisted attach.",
1575
+ tail: "\u001b[2mHumanish TUI frame\u001b[0m\n> persona: skeptical-power-user\n> scenario: onboarding-regression\nstatus: awaiting live PTY transport",
1576
+ viewport: undefined
1577
+ },
1578
+ {
1579
+ kind: "codex-ui",
1580
+ mode: "codex-app-sim",
1581
+ label: "Codex UI",
1582
+ currentStep: "App-server embed contract captured",
1583
+ summary: "Codex UI lane reserved for app-server sessions that can be watched beside terminal evidence.",
1584
+ tail: "codex-app-server session contract\nstate: not_connected\nembed: pending provider URL\nreceipts: planned",
1585
+ viewport: { width: 1280, height: 900, deviceScaleFactor: 1 }
1586
+ }
1587
+ ];
1588
+ const simulations = [];
1589
+ const streams = [];
1590
+ const events = [
1591
+ {
1592
+ id: "event-000",
1593
+ at: args.createdAt,
1594
+ level: "info",
1595
+ type: "observer.contract.created",
1596
+ message: "Created public-safe observer stream contract."
1597
+ }
1598
+ ];
1599
+ for (let index = 0; index < args.simCount; index += 1) {
1600
+ const template = templates[index % templates.length];
1601
+ if (!template) {
1602
+ throw new Error("Synthetic observer template missing.");
1603
+ }
1604
+ const simId = `sim-${String(index + 1).padStart(2, "0")}`;
1605
+ const streamId = `${simId}-${template.kind}`;
1606
+ const status = "contract_proof_only";
1607
+ simulations.push({
1608
+ id: simId,
1609
+ index: index + 1,
1610
+ personaId: args.personaId,
1611
+ scenarioId: args.scenarioId,
1612
+ status,
1613
+ streamKind: template.kind,
1614
+ mode: template.mode,
1615
+ progress: 100,
1616
+ currentStep: template.currentStep,
1617
+ summary: template.summary,
1618
+ streamIds: [streamId],
1619
+ startedAt: args.createdAt,
1620
+ updatedAt: args.createdAt
1621
+ });
1622
+ streams.push({
1623
+ id: streamId,
1624
+ simId,
1625
+ kind: template.kind,
1626
+ label: template.label,
1627
+ status,
1628
+ transport: streamTransport(template.kind),
1629
+ updatedAt: args.createdAt,
1630
+ embed: {
1631
+ kind: template.kind === "terminal" || template.kind === "tui" ? "terminal" : "placeholder",
1632
+ title: template.label
1633
+ },
1634
+ ...(template.viewport ? { viewport: template.viewport } : {}),
1635
+ terminal: {
1636
+ title: template.label,
1637
+ format: template.kind === "tui" ? "ansi" : "plain",
1638
+ stdin: "disabled",
1639
+ tail: template.tail
1640
+ },
1641
+ ...(template.kind === "ui" || template.kind === "codex-ui"
1642
+ ? {
1643
+ ui: {
1644
+ route: template.kind === "ui" ? "/first-run" : "/codex/session",
1645
+ intent: template.summary,
1646
+ state: "contract-only"
1647
+ }
1648
+ }
1649
+ : {}),
1650
+ ...(template.kind === "codex-ui"
1651
+ ? {
1652
+ codex: {
1653
+ provider: "codex-app-server",
1654
+ state: "not_connected",
1655
+ contract: "Observer accepts an app-server embed URL, session id, status feed, terminal receipt feed, and artifact links."
1656
+ }
1657
+ }
1658
+ : {}),
1659
+ artifacts: [
1660
+ { label: "run bundle", path: "run.json", kind: "bundle" },
1661
+ { label: "review", path: "review.md", kind: "review" },
1662
+ { label: "event log", path: "events.ndjson", kind: "events" }
1663
+ ]
1664
+ });
1665
+ events.push({
1666
+ id: `event-${String(index + 1).padStart(3, "0")}-a`,
1667
+ at: args.createdAt,
1668
+ level: "info",
1669
+ type: "sim.contract.ready",
1670
+ message: `${template.label} stream contract ready.`,
1671
+ simId,
1672
+ streamId
1673
+ }, {
1674
+ id: `event-${String(index + 1).padStart(3, "0")}-b`,
1675
+ at: args.createdAt,
1676
+ level: "warn",
1677
+ type: "sim.live-substrate.missing",
1678
+ message: "No live actor launched in dry-run mode; observer lane is ready for real substrate evidence.",
1679
+ simId,
1680
+ streamId
1681
+ });
1682
+ }
1683
+ return { events, simulations, streams };
1684
+ }
1685
+ function streamTransport(kind) {
1686
+ if (kind === "tui")
1687
+ return "pty";
1688
+ if (kind === "codex-ui")
1689
+ return "app-server";
1690
+ if (kind === "ui" || kind === "browser")
1691
+ return "polling";
1692
+ return "snapshot";
1693
+ }
1694
+ function resolveLocalCodexTuiCommand(cwd, prompt, overrideCommand) {
1695
+ const envCommand = process.env.HUMANISH_CODEX_ACTOR_COMMAND;
1696
+ const commandParts = overrideCommand && overrideCommand.length > 0
1697
+ ? overrideCommand
1698
+ : envCommand
1699
+ ? parseCommandLine(envCommand)
1700
+ : defaultLocalCodexTuiCommand(cwd, prompt);
1701
+ const [command, ...args] = commandParts;
1702
+ if (!command) {
1703
+ const [fallbackCommand, ...fallbackArgs] = defaultLocalCodexTuiCommand(cwd, prompt);
1704
+ return {
1705
+ command: fallbackCommand ?? "codex",
1706
+ args: fallbackArgs,
1707
+ name: fallbackCommand ? path.basename(fallbackCommand) : "codex"
1708
+ };
1709
+ }
1710
+ return {
1711
+ command,
1712
+ args,
1713
+ name: path.basename(command)
1714
+ };
1715
+ }
1716
+ function defaultLocalCodexTuiCommand(cwd, prompt) {
1717
+ const codexParts = [
1718
+ "codex",
1719
+ "--no-alt-screen",
1720
+ "-C",
1721
+ cwd,
1722
+ "--sandbox",
1723
+ "read-only",
1724
+ "--ask-for-approval",
1725
+ "never",
1726
+ prompt
1727
+ ];
1728
+ if (process.platform === "linux") {
1729
+ return ["script", "-qfec", shellJoin(codexParts), "/dev/null"];
1730
+ }
1731
+ return codexParts;
1732
+ }
1733
+ function resolveLocalCodexExecCommand(cwd, prompt, overrideCommand) {
1734
+ const envCommand = process.env.HUMANISH_CODEX_ACTOR_COMMAND;
1735
+ const commandParts = overrideCommand && overrideCommand.length > 0
1736
+ ? overrideCommand
1737
+ : envCommand
1738
+ ? parseCommandLine(envCommand)
1739
+ : defaultLocalCodexExecCommand(cwd, prompt);
1740
+ const [command, ...args] = commandParts;
1741
+ if (!command) {
1742
+ const [fallbackCommand, ...fallbackArgs] = defaultLocalCodexExecCommand(cwd, prompt);
1743
+ return {
1744
+ command: fallbackCommand ?? "codex",
1745
+ args: fallbackArgs,
1746
+ name: fallbackCommand ? path.basename(fallbackCommand) : "codex"
1747
+ };
1748
+ }
1749
+ return {
1750
+ command,
1751
+ args,
1752
+ name: path.basename(command)
1753
+ };
1754
+ }
1755
+ function defaultLocalCodexExecCommand(cwd, prompt) {
1756
+ return [
1757
+ "codex",
1758
+ "exec",
1759
+ "--skip-git-repo-check",
1760
+ "--ignore-rules",
1761
+ "--ephemeral",
1762
+ "-C",
1763
+ cwd,
1764
+ "--sandbox",
1765
+ "read-only",
1766
+ "--json",
1767
+ prompt
1768
+ ];
1769
+ }
1770
+ function executeLocalActorCommand(command, options) {
1771
+ const startedAt = Date.now();
1772
+ let transcript = "";
1773
+ let transcriptBytes = 0;
1774
+ let terminalQueryBuffer = "";
1775
+ let observedMarkerStatus = null;
1776
+ let stoppingAfterMarker = false;
1777
+ let timedOut = false;
1778
+ let settled = false;
1779
+ let timer;
1780
+ let markerKillTimer;
1781
+ return new Promise((resolve) => {
1782
+ const finish = (result) => {
1783
+ if (settled) {
1784
+ return;
1785
+ }
1786
+ settled = true;
1787
+ clearTimeout(timer);
1788
+ clearTimeout(markerKillTimer);
1789
+ const normalizedTranscript = normalizeLocalActorTranscript(transcript);
1790
+ resolve({
1791
+ ...result,
1792
+ durationMs: Date.now() - startedAt,
1793
+ transcript: redactSensitiveText(normalizedTranscript),
1794
+ transcriptBytes
1795
+ });
1796
+ };
1797
+ const child = spawn(command.command, command.args, {
1798
+ cwd: options.cwd,
1799
+ env: {
1800
+ ...process.env,
1801
+ TERM: process.env.TERM ?? "xterm-256color",
1802
+ COLUMNS: process.env.COLUMNS ?? "120",
1803
+ LINES: process.env.LINES ?? "40",
1804
+ HUMANISH_ACTOR_VERDICT_NONCE: options.verdictNonce
1805
+ },
1806
+ stdio: ["pipe", "pipe", "pipe"]
1807
+ });
1808
+ timer = setTimeout(() => {
1809
+ timedOut = true;
1810
+ child.kill("SIGTERM");
1811
+ setTimeout(() => {
1812
+ if (timedOut) {
1813
+ child.kill("SIGKILL");
1814
+ }
1815
+ }, 2_000).unref();
1816
+ }, options.timeoutMs);
1817
+ timer.unref();
1818
+ const capture = (chunk) => {
1819
+ transcriptBytes += chunk.byteLength;
1820
+ transcript = limitTranscript(transcript + chunk.toString("utf8"));
1821
+ terminalQueryBuffer = respondToTerminalQueries(terminalQueryBuffer, chunk, child.stdin);
1822
+ const markerStatus = observedMarkerStatus ?? extractLocalActorVerdict(normalizeLocalActorTranscript(transcript), options.verdictNonce);
1823
+ if (markerStatus && !observedMarkerStatus) {
1824
+ observedMarkerStatus = markerStatus;
1825
+ stoppingAfterMarker = true;
1826
+ child.kill("SIGTERM");
1827
+ markerKillTimer = setTimeout(() => {
1828
+ child.kill("SIGKILL");
1829
+ }, 2_000);
1830
+ markerKillTimer.unref();
1831
+ }
1832
+ };
1833
+ child.stdout?.on("data", capture);
1834
+ child.stderr?.on("data", capture);
1835
+ child.once("error", (error) => {
1836
+ finish({
1837
+ status: "blocked",
1838
+ reason: `actor command could not start: ${error.message}`
1839
+ });
1840
+ });
1841
+ child.once("close", (code, signal) => {
1842
+ if (timedOut || (Date.now() - startedAt >= options.timeoutMs && code === null)) {
1843
+ timedOut = false;
1844
+ finish({
1845
+ status: "timed_out",
1846
+ reason: `actor exceeded ${options.timeoutMs}ms timeout`,
1847
+ ...(signal === null ? {} : { signal })
1848
+ });
1849
+ return;
1850
+ }
1851
+ const markerStatus = observedMarkerStatus ?? extractLocalActorVerdict(normalizeLocalActorTranscript(transcript), options.verdictNonce);
1852
+ const processFailed = code !== 0 && !(stoppingAfterMarker && markerStatus);
1853
+ const status = processFailed
1854
+ ? markerStatus === "blocked" ? "blocked" : "failed"
1855
+ : markerStatus ?? "passed";
1856
+ finish({
1857
+ status,
1858
+ reason: processFailed
1859
+ ? markerStatus === "blocked"
1860
+ ? `actor reported blocked verdict marker and exited with code ${code ?? "null"}`
1861
+ : `actor process exited with code ${code ?? "null"}`
1862
+ : markerStatus
1863
+ ? `actor reported ${markerStatus} verdict marker`
1864
+ : "actor process exited successfully",
1865
+ ...(code === null ? {} : { exitCode: code }),
1866
+ ...(signal === null ? {} : { signal })
1867
+ });
1868
+ });
1869
+ });
1870
+ }
1871
+ function respondToTerminalQueries(currentBuffer, chunk, stdin) {
1872
+ if (!stdin || !stdin.writable) {
1873
+ return "";
1874
+ }
1875
+ let buffer = `${currentBuffer}${chunk.toString("latin1")}`;
1876
+ while (true) {
1877
+ const cprIndex = buffer.indexOf("\x1b[6n");
1878
+ const oscMatch = /\x1b\](10|11|12);\?(?:\x07|\x1b\\)/.exec(buffer);
1879
+ const oscIndex = oscMatch?.index ?? -1;
1880
+ if (cprIndex === -1 && oscIndex === -1) {
1881
+ break;
1882
+ }
1883
+ if (cprIndex !== -1 && (oscIndex === -1 || cprIndex < oscIndex)) {
1884
+ stdin.write("\x1b[24;120R");
1885
+ buffer = buffer.slice(cprIndex + "\x1b[6n".length);
1886
+ continue;
1887
+ }
1888
+ const colorSlot = oscMatch?.[1] ?? "10";
1889
+ stdin.write(terminalColorResponse(colorSlot));
1890
+ buffer = buffer.slice((oscIndex === -1 ? 0 : oscIndex) + (oscMatch?.[0].length ?? 0));
1891
+ }
1892
+ return buffer.slice(-128);
1893
+ }
1894
+ function terminalColorResponse(slot) {
1895
+ if (slot === "11") {
1896
+ return "\x1b]11;rgb:0000/0000/0000\x07";
1897
+ }
1898
+ return `\x1b]${slot};rgb:ffff/ffff/ffff\x07`;
1899
+ }
1900
+ /**
1901
+ * Strip ANSI/control noise from a captured terminal transcript into stable, scannable text.
1902
+ * Pure (no IO). Exported so the terminal-product lane (src/e2b-terminal-lab.ts) normalizes its
1903
+ * captured exec stream EXACTLY as the local-actor lanes do — the verdict-nonce scorer is only
1904
+ * sound against the same normalization the marker is matched on, so the logic must not diverge.
1905
+ */
1906
+ export function normalizeLocalActorTranscript(transcript) {
1907
+ return transcript
1908
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
1909
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
1910
+ .replace(/\x1b[78=>]/g, "")
1911
+ .replace(/\r\n/g, "\n")
1912
+ .replace(/\r/g, "\n")
1913
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
1914
+ }
1915
+ /**
1916
+ * Extract the per-run verdict from a normalized transcript: the agent must print exactly
1917
+ * `HUMANISH_ACTOR_VERDICT=<status> HUMANISH_ACTOR_NONCE=<nonce>`, and the nonce is mandatory so a
1918
+ * bare marker (echoed or replayed from untrusted text) can never forge a verdict. Pure (no IO).
1919
+ * Exported so the terminal-product lane scores its in-sandbox `codex exec` run by the SAME marker
1920
+ * — divergent verdict logic would let the two lanes disagree about what "passed" means.
1921
+ */
1922
+ export function extractLocalActorVerdict(transcript, verdictNonce) {
1923
+ const compactTranscript = transcript.replace(/\s+/g, "");
1924
+ // The per-run nonce is mandatory: a bare HUMANISH_ACTOR_VERDICT=<status>
1925
+ // marker echoed by an actor (or replayed from untrusted text) must never
1926
+ // satisfy verdict extraction.
1927
+ const match = new RegExp(`HUMANISH_ACTOR_VERDICT=(passed|blocked|failed)HUMANISH_ACTOR_NONCE=${escapeRegExp(verdictNonce)}`, "i").exec(compactTranscript);
1928
+ if (!match) {
1929
+ return null;
1930
+ }
1931
+ return match[1]?.toLowerCase();
1932
+ }
1933
+ async function checkCodexWorkspaceTrust(cwd) {
1934
+ if (process.env.HUMANISH_SKIP_CODEX_TRUST_PREFLIGHT === "1") {
1935
+ return { ok: true };
1936
+ }
1937
+ const trustRoot = await detectCodexTrustRoot(cwd);
1938
+ if (!trustRoot) {
1939
+ return { ok: true };
1940
+ }
1941
+ const configPath = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "config.toml");
1942
+ const configText = await readTextIfExists(configPath);
1943
+ if (configText && codexConfigTrustsProject(configText, trustRoot)) {
1944
+ return { ok: true };
1945
+ }
1946
+ return {
1947
+ ok: false,
1948
+ trustRoot,
1949
+ message: `Codex workspace trust preflight blocked local TUI launch; trust root is not explicitly trusted as an exact Codex project root: ${trustRoot}`,
1950
+ recoveryCommand: `codex --no-alt-screen -C ${shellQuote(trustRoot)}`
1951
+ };
1952
+ }
1953
+ async function detectCodexTrustRoot(cwd) {
1954
+ const worktreeRoot = await findGitWorktreeRoot(cwd);
1955
+ if (!worktreeRoot) {
1956
+ return null;
1957
+ }
1958
+ const dotGitPath = path.join(worktreeRoot, ".git");
1959
+ if (await directoryExists(dotGitPath)) {
1960
+ return worktreeRoot;
1961
+ }
1962
+ const gitFile = await readTextIfExists(dotGitPath);
1963
+ if (!gitFile?.startsWith("gitdir:")) {
1964
+ return worktreeRoot;
1965
+ }
1966
+ const gitDir = gitFile.slice("gitdir:".length).trim();
1967
+ const absoluteGitDir = path.isAbsolute(gitDir) ? gitDir : path.resolve(worktreeRoot, gitDir);
1968
+ const commonDirText = await readTextIfExists(path.join(absoluteGitDir, "commondir"));
1969
+ if (!commonDirText) {
1970
+ return worktreeRoot;
1971
+ }
1972
+ const commonDir = commonDirText.trim();
1973
+ const absoluteCommonDir = path.resolve(absoluteGitDir, commonDir);
1974
+ return path.basename(absoluteCommonDir) === ".git" ? path.dirname(absoluteCommonDir) : worktreeRoot;
1975
+ }
1976
+ async function findGitWorktreeRoot(cwd) {
1977
+ let current = path.resolve(cwd);
1978
+ while (true) {
1979
+ if (await fileExists(path.join(current, ".git")) || await directoryExists(path.join(current, ".git"))) {
1980
+ return current;
1981
+ }
1982
+ const parent = path.dirname(current);
1983
+ if (parent === current) {
1984
+ return null;
1985
+ }
1986
+ current = parent;
1987
+ }
1988
+ }
1989
+ function codexConfigTrustsProject(configText, trustRoot) {
1990
+ const sectionPattern = /^\[projects\."((?:\\.|[^"\\])*)"\]\s*$/gm;
1991
+ let sectionMatch;
1992
+ while ((sectionMatch = sectionPattern.exec(configText)) !== null) {
1993
+ const projectPath = unescapeTomlString(sectionMatch[1] ?? "");
1994
+ const afterSection = configText.slice(sectionMatch.index + sectionMatch[0].length);
1995
+ const nextSectionIndex = afterSection.search(/^\[/m);
1996
+ const sectionBody = nextSectionIndex === -1 ? afterSection : afterSection.slice(0, nextSectionIndex);
1997
+ if (/^trust_level\s*=\s*"trusted"\s*$/m.test(sectionBody) && isSamePath(projectPath, trustRoot)) {
1998
+ return true;
1999
+ }
2000
+ }
2001
+ return false;
2002
+ }
2003
+ function unescapeTomlString(value) {
2004
+ return value.replace(/\\(["\\])/g, "$1");
2005
+ }
2006
+ function isSamePath(candidatePath, targetPath) {
2007
+ const candidate = path.resolve(candidatePath);
2008
+ const target = path.resolve(targetPath);
2009
+ return candidate === target;
2010
+ }
2011
+ function normalizeActorTimeout(value) {
2012
+ if (normalizePositiveInteger(value) === null || value === undefined || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
2013
+ return null;
2014
+ }
2015
+ return value;
2016
+ }
2017
+ function normalizePositiveInteger(value) {
2018
+ if (!Number.isInteger(value) || value === undefined || value < 1) {
2019
+ return null;
2020
+ }
2021
+ return value;
2022
+ }
2023
+ function readEnvInteger(name) {
2024
+ const value = process.env[name];
2025
+ if (value === undefined || value.trim() === "") {
2026
+ return undefined;
2027
+ }
2028
+ return /^\d+$/.test(value) ? Number.parseInt(value, 10) : Number.NaN;
2029
+ }
2030
+ function personaPromptLine(selection) {
2031
+ return renderPersonaPromptSection(selection.resolvedPersona);
2032
+ }
2033
+ function buildLocalCodexTuiPrompt(selection, verdictNonce) {
2034
+ return [
2035
+ "You are a Humanish local Codex TUI dogfood actor.",
2036
+ personaPromptLine(selection),
2037
+ `Scenario: ${selection.scenario.title}.`,
2038
+ "Inspect this public repository's Humanish setup, run evidence, and Observer affordances.",
2039
+ "Run at most two read-only inspection commands; prefer file reads, `node dist/cli.js --help`, or `pnpm typecheck` when available.",
2040
+ "Do not run commands that write runtime artifacts or temp config, including `pnpm humanish`, `humanish watch`, `humanish feedback`, `humanish init`, tests, builds, installs, or commands that write `.humanish/`.",
2041
+ "If the strongest proof would require writes in this read-only sandbox, inspect existing artifacts instead and name the write-required proof as a follow-up.",
2042
+ "Use passed when read-only inspection confirms the committed harness and existing evidence contract; write-required follow-ups alone are not blockers.",
2043
+ "Do not print secrets, do not commit, do not push, do not open GitHub issues, and do not use private data.",
2044
+ "Finish by summarizing one public-safe harness improvement.",
2045
+ `Then print exactly one final machine-readable line in this format: HUMANISH_ACTOR_VERDICT=<status> HUMANISH_ACTOR_NONCE=${verdictNonce}.`,
2046
+ "Replace <status> with exactly one lowercase word: passed, blocked, or failed."
2047
+ ].join(" ");
2048
+ }
2049
+ function localCodexExecFocus(index) {
2050
+ const focuses = [
2051
+ {
2052
+ id: "install-readability",
2053
+ label: "install/readability",
2054
+ instruction: "audit whether a new user can understand the committed Humanish dogfood setup quickly",
2055
+ suggestedCommands: [
2056
+ "test -r humanish/README.md && sed -n '1,40p' humanish/README.md",
2057
+ "test -r humanish/config.ts && wc -l humanish/config.ts"
2058
+ ]
2059
+ },
2060
+ {
2061
+ id: "public-safety-trust",
2062
+ label: "public-safety/trust",
2063
+ instruction: "check the local actor boundaries, public-safety claims, and trust-bootstrap language",
2064
+ suggestedCommands: [
2065
+ "test -r humanish/coverage-map.md && sed -n '1,80p' humanish/coverage-map.md",
2066
+ "test -r docs/architecture/local-codex-tui-actor.md && sed -n '1,80p' docs/architecture/local-codex-tui-actor.md"
2067
+ ]
2068
+ },
2069
+ {
2070
+ id: "observer-evidence",
2071
+ label: "Observer/evidence",
2072
+ instruction: "inspect whether run evidence and Observer expectations are easy to verify",
2073
+ suggestedCommands: [
2074
+ "test -r humanish/coverage-matrix.md && sed -n '1,80p' humanish/coverage-matrix.md",
2075
+ "test -r humanish/scenarios/onboarding-regression.yaml && sed -n '1,120p' humanish/scenarios/onboarding-regression.yaml"
2076
+ ]
2077
+ },
2078
+ {
2079
+ id: "verification-release",
2080
+ label: "verification/release",
2081
+ instruction: "inspect the verification and release-gate promises exposed to local dogfood actors",
2082
+ suggestedCommands: [
2083
+ "test -r package.json && node -e \"const p=require('./package.json'); console.log(p.scripts.check); console.log(p.scripts['public-surface:scan']);\"",
2084
+ "test -r humanish/README.md && sed -n '1,80p' humanish/README.md"
2085
+ ]
2086
+ }
2087
+ ];
2088
+ return focuses[index % focuses.length] ?? focuses[0];
2089
+ }
2090
+ function aggregateActorStatus(statuses) {
2091
+ if (statuses.includes("failed"))
2092
+ return "failed";
2093
+ if (statuses.includes("timed_out"))
2094
+ return "timed_out";
2095
+ if (statuses.includes("blocked"))
2096
+ return "blocked";
2097
+ return statuses.length > 0 && statuses.every((status) => status === "passed") ? "passed" : "failed";
2098
+ }
2099
+ function summarizeExecFanout(statuses) {
2100
+ if (statuses.length === 1) {
2101
+ return statuses[0] === "passed" ? "actor process exited successfully" : `actor lane ${statuses[0]}`;
2102
+ }
2103
+ const counts = statuses.reduce((current, status) => ({ ...current, [status]: current[status] + 1 }), { passed: 0, failed: 0, blocked: 0, timed_out: 0 });
2104
+ const summary = Object.keys(counts)
2105
+ .filter((status) => counts[status] > 0)
2106
+ .map((status) => `${counts[status]} ${status}`)
2107
+ .join(", ");
2108
+ return statuses.every((status) => status === "passed")
2109
+ ? `all ${statuses.length} Codex exec lanes passed`
2110
+ : `${statuses.length} Codex exec lanes completed with ${summary}`;
2111
+ }
2112
+ function buildLocalCodexExecPrompt(selection, verdictNonce, lane) {
2113
+ const suggestedCommands = lane?.focus.suggestedCommands ?? [
2114
+ "test -r humanish/config.ts && wc -l humanish/config.ts",
2115
+ "test -r humanish/README.md && sed -n '1,40p' humanish/README.md"
2116
+ ];
2117
+ return [
2118
+ "You are a Humanish local Codex exec dogfood actor running noninteractively.",
2119
+ personaPromptLine(selection),
2120
+ `Scenario: ${selection.scenario.title}.`,
2121
+ ...(lane ? [`Fanout lane ${lane.index}/${lane.total}. Focus: ${lane.focus.instruction}.`] : []),
2122
+ "Run at most two read-only local inspection commands.",
2123
+ `Suggested commands: \`${suggestedCommands[0]}\` and \`${suggestedCommands[1]}\`.`,
2124
+ "Do not edit files, do not run network commands, do not commit, do not push, do not open GitHub issues, and do not print secrets.",
2125
+ "Do not inspect additional files unless one suggested command fails.",
2126
+ "Finish within three public-safe sentences.",
2127
+ `Then print exactly one final machine-readable line in this format: HUMANISH_ACTOR_VERDICT=<status> HUMANISH_ACTOR_NONCE=${verdictNonce}.`,
2128
+ "Replace <status> with exactly one lowercase word: passed, blocked, or failed."
2129
+ ].join(" ");
2130
+ }
2131
+ // The app-server lane intentionally carries no verdict nonce: its verdict
2132
+ // comes from the structured turn/completed status on the app-server JSON-RPC
2133
+ // channel (see codex-app-server.ts), never from HUMANISH_ACTOR_VERDICT marker
2134
+ // extraction, so transcript text cannot set the run verdict on that lane.
2135
+ function buildLocalCodexAppServerPrompt(selection, lane) {
2136
+ return [
2137
+ "You are a Humanish Codex app-server dogfood actor running through the official Codex app-server protocol.",
2138
+ personaPromptLine(selection),
2139
+ `Scenario: ${selection.scenario.title}.`,
2140
+ `Fanout lane ${lane.index}/${lane.total}. Focus: ${lane.focus.instruction}.`,
2141
+ "Work read-only unless the host explicitly configured a stronger sandbox.",
2142
+ "Inspect the current repository's Humanish setup, Observer proof contract, and public-safety posture.",
2143
+ "Use at most two lightweight local commands or file reads.",
2144
+ `Suggested commands: \`${lane.focus.suggestedCommands[0]}\` and \`${lane.focus.suggestedCommands[1]}\`.`,
2145
+ "Do not print secrets, keys, raw private transcripts, private screenshots, or private source snippets.",
2146
+ "Do not commit, push, open GitHub issues, mutate remote systems, or run provider-spend-heavy commands.",
2147
+ "End with one concise public-safe recommendation for improving Humanish as a closed-loop user-study harness."
2148
+ ].join(" ");
2149
+ }
2150
+ function readCodexAppServerSandboxFromEnv() {
2151
+ const value = process.env.HUMANISH_CODEX_APP_SERVER_SANDBOX;
2152
+ if (value === "workspace-write" || value === "danger-full-access") {
2153
+ return value;
2154
+ }
2155
+ return "read-only";
2156
+ }
2157
+ function codexStateForStream(status) {
2158
+ if (status === "running" || status === "preparing" || status === "queued") {
2159
+ return "running";
2160
+ }
2161
+ if (status === "passed" || status === "complete") {
2162
+ return "completed";
2163
+ }
2164
+ if (status === "failed") {
2165
+ return "failed";
2166
+ }
2167
+ if (status === "timed_out") {
2168
+ return "timed_out";
2169
+ }
2170
+ if (status === "blocked") {
2171
+ return "blocked";
2172
+ }
2173
+ return "watching";
2174
+ }
2175
+ function prefixCodexAppServerResultPaths(result, prefix) {
2176
+ if (!prefix) {
2177
+ return result;
2178
+ }
2179
+ return {
2180
+ ...result,
2181
+ eventsPath: `${prefix}${result.eventsPath}`,
2182
+ tracePath: `${prefix}${result.tracePath}`,
2183
+ transcriptPath: `${prefix}${result.transcriptPath}`
2184
+ };
2185
+ }
2186
+ function parseCommandLine(input) {
2187
+ const tokens = [];
2188
+ let current = "";
2189
+ let quote = null;
2190
+ let escaping = false;
2191
+ for (const char of input.trim()) {
2192
+ if (escaping) {
2193
+ current += char;
2194
+ escaping = false;
2195
+ continue;
2196
+ }
2197
+ if (char === "\\" && quote !== "'") {
2198
+ escaping = true;
2199
+ continue;
2200
+ }
2201
+ if (quote) {
2202
+ if (char === quote) {
2203
+ quote = null;
2204
+ }
2205
+ else {
2206
+ current += char;
2207
+ }
2208
+ continue;
2209
+ }
2210
+ if (char === "\"" || char === "'") {
2211
+ quote = char;
2212
+ continue;
2213
+ }
2214
+ if (/\s/.test(char)) {
2215
+ if (current !== "") {
2216
+ tokens.push(current);
2217
+ current = "";
2218
+ }
2219
+ continue;
2220
+ }
2221
+ current += char;
2222
+ }
2223
+ if (current !== "") {
2224
+ tokens.push(current);
2225
+ }
2226
+ return quote ? [] : tokens;
2227
+ }
2228
+ function shellJoin(parts) {
2229
+ return parts.map(shellQuote).join(" ");
2230
+ }
2231
+ function shellQuote(value) {
2232
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
2233
+ return value;
2234
+ }
2235
+ return `'${value.replace(/'/g, "'\\''")}'`;
2236
+ }
2237
+ function limitTranscript(value) {
2238
+ if (value.length <= LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS) {
2239
+ return value;
2240
+ }
2241
+ return `[...sanitized transcript truncated to last ${LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS} characters...]\n${value.slice(-LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS)}`;
2242
+ }
2243
+ function normalizeSimCount(value) {
2244
+ if (value === undefined) {
2245
+ return 1;
2246
+ }
2247
+ if (!Number.isSafeInteger(value) || value < 1) {
2248
+ return null;
2249
+ }
2250
+ return value;
2251
+ }
2252
+ export async function verifyRun(cwdInput, runInput) {
2253
+ const cwd = path.resolve(cwdInput);
2254
+ const checks = [];
2255
+ const resolved = await resolveRunPath(cwd, runInput);
2256
+ if (!resolved) {
2257
+ return {
2258
+ schema: VERIFY_SCHEMA,
2259
+ ok: false,
2260
+ cwd,
2261
+ run: runInput,
2262
+ checks,
2263
+ shareSafety: {
2264
+ status: "blocked",
2265
+ reasons: [
2266
+ {
2267
+ code: "VERIFY_FAILED",
2268
+ message: `Run not found: ${runInput}`
2269
+ }
2270
+ ]
2271
+ },
2272
+ warnings: [],
2273
+ error: {
2274
+ code: "HUMANISH_RUN_NOT_FOUND",
2275
+ message: `Run not found: ${runInput}`
2276
+ }
2277
+ };
2278
+ }
2279
+ const bundlePath = path.join(resolved, "run.json");
2280
+ const bundle = await readJsonIfExists(bundlePath);
2281
+ const cleanupJson = await readJsonIfExists(path.join(resolved, "cleanup.json"));
2282
+ const reviewJson = await readJsonIfExists(path.join(resolved, "review.json"));
2283
+ const reviewMarkdown = await readTextIfExists(path.join(resolved, "review.md"));
2284
+ checks.push({
2285
+ name: "run.json exists",
2286
+ ok: bundle !== null,
2287
+ message: bundle === null ? "run.json missing" : "run.json present"
2288
+ });
2289
+ checks.push({
2290
+ name: "run schema",
2291
+ ok: isRecord(bundle) && bundle.schema === RUN_BUNDLE_SCHEMA,
2292
+ message: "run bundle schema is humanish.run-bundle.v1"
2293
+ });
2294
+ checks.push({
2295
+ name: "run bundle shape",
2296
+ ok: isRunBundle(bundle),
2297
+ message: "run bundle must include source, persona, scenario, lifecycle, simulations, streams, events, artifacts, review, and feedback candidates"
2298
+ });
2299
+ checks.push({
2300
+ name: "redaction passed",
2301
+ ok: isRecord(bundle) && isRecord(bundle.redaction) && bundle.redaction.status === "passed",
2302
+ message: "redaction status must be passed"
2303
+ });
2304
+ checks.push({
2305
+ name: "review artifacts exist",
2306
+ ok: reviewJson !== null && reviewMarkdown !== null,
2307
+ message: "review.json and review.md must exist"
2308
+ });
2309
+ const publicSafetyFindings = await scanRunPublicSafetyArtifacts(resolved);
2310
+ checks.push({
2311
+ name: "public-safety scan",
2312
+ ok: publicSafetyFindings.length === 0,
2313
+ message: publicSafetyFindings.length === 0
2314
+ ? "run text artifacts and public-proof paths must not match known secret or browser-profile patterns"
2315
+ : `public-safety findings: ${publicSafetyFindings.slice(0, 5).join(", ")}`
2316
+ });
2317
+ const missingEvidenceArtifacts = isRunBundle(bundle)
2318
+ ? await missingLocalEvidenceArtifacts(resolved, bundle)
2319
+ : [];
2320
+ const invalidEvidenceReferences = isRunBundle(bundle)
2321
+ ? invalidRunEvidenceReferences(bundle)
2322
+ : [];
2323
+ checks.push({
2324
+ name: "local evidence artifacts exist",
2325
+ ok: missingEvidenceArtifacts.length === 0 && invalidEvidenceReferences.length === 0,
2326
+ message: missingEvidenceArtifacts.length === 0 && invalidEvidenceReferences.length === 0
2327
+ ? "referenced local screenshot/trace/log/filesystem artifacts are present"
2328
+ : invalidEvidenceReferences.length > 0
2329
+ ? `invalid evidence artifact references: ${invalidEvidenceReferences.join(", ")}`
2330
+ : `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`
2331
+ });
2332
+ const terminalProductFindings = isRunBundle(bundle)
2333
+ ? await validateTerminalProductEvidence(resolved, bundle)
2334
+ : [];
2335
+ checks.push({
2336
+ name: "terminal-product evidence",
2337
+ ok: terminalProductFindings.length === 0,
2338
+ message: terminalProductFindings.length === 0
2339
+ ? "live terminal-product streams either are absent or carry the substrate/cleanup/interventions/cost ledgers + a ledger-derived no-spend proof + redacted terminal evidence, with proven teardown and known spend within the declared cap"
2340
+ : `terminal-product findings: ${terminalProductFindings.join(", ")}`
2341
+ });
2342
+ const codexAppServerFindings = isRunBundle(bundle)
2343
+ ? await validateCodexAppServerEvidence(resolved, bundle)
2344
+ : [];
2345
+ checks.push({
2346
+ name: "codex app-server evidence",
2347
+ ok: codexAppServerFindings.length === 0,
2348
+ message: codexAppServerFindings.length === 0
2349
+ ? "live Codex app-server streams either are absent or include valid redacted trace evidence"
2350
+ : `codex app-server findings: ${codexAppServerFindings.join(", ")}`
2351
+ });
2352
+ const noEngagementFindings = isRunBundle(bundle) ? noEngagementActorFindings(bundle) : [];
2353
+ checks.push({
2354
+ name: "actor engagement",
2355
+ ok: noEngagementFindings.length === 0,
2356
+ message: noEngagementFindings.length === 0
2357
+ ? "live actor traces that claim goal_satisfied carry at least one action or message"
2358
+ : `no-engagement findings: ${noEngagementFindings.join(", ")} — a hollow run is not credible evidence`
2359
+ });
2360
+ const actorVerdictFindings = isRunBundle(bundle) ? actorVerdictConsistencyFindings(bundle) : [];
2361
+ checks.push({
2362
+ name: "actor verdict consistency",
2363
+ ok: actorVerdictFindings.length === 0,
2364
+ message: actorVerdictFindings.length === 0
2365
+ ? "live pass verdicts do not hide failed, blocked, or timed-out actor traces"
2366
+ : `actor verdict findings: ${actorVerdictFindings.join(", ")}`
2367
+ });
2368
+ const stateFindings = isRunBundle(bundle) ? subjectStateFindings(bundle) : [];
2369
+ checks.push({
2370
+ name: "subject state provenance",
2371
+ ok: stateFindings.length === 0,
2372
+ message: stateFindings.length === 0
2373
+ ? "subject state claims match the recorded seed/external evidence (or the subject block is honestly absent)"
2374
+ : `subject state findings: ${stateFindings.join(", ")}`
2375
+ });
2376
+ const sharedWorldFindings = isRunBundle(bundle) ? sharedWorldEvidenceFindings(bundle) : [];
2377
+ checks.push({
2378
+ name: "shared-world evidence",
2379
+ ok: sharedWorldFindings.length === 0,
2380
+ message: sharedWorldFindings.length === 0
2381
+ ? "live shared-world runs either are absent or carry a well-formed alternating timeline (cp-baseline → turn → cp), single-plane provenance, digest-only checkpoints, the mandatory attributionLimits, and a checkpoint delta on a passed run"
2382
+ : `shared-world findings: ${sharedWorldFindings.join(", ")}`
2383
+ });
2384
+ checks.push({
2385
+ name: "cleanup receipt",
2386
+ ok: cleanupJson === null || (isCleanupResult(cleanupJson) && cleanupJson.ok),
2387
+ message: cleanupJson === null
2388
+ ? "cleanup receipt not present; cleanup was not requested"
2389
+ : isCleanupResult(cleanupJson) && cleanupJson.ok
2390
+ ? "cleanup receipt is present and successful"
2391
+ : "cleanup receipt is present but malformed or failed"
2392
+ });
2393
+ const rerunFindings = isRunBundle(bundle) ? rerunLineageFindings(bundle) : [];
2394
+ checks.push({
2395
+ name: "rerun lineage",
2396
+ ok: rerunFindings.length === 0,
2397
+ message: rerunFindings.length === 0
2398
+ ? "rerun bundles either are absent or link selected lanes to prior lane status and a fan-out rerun event"
2399
+ : `rerun lineage findings: ${rerunFindings.join(", ")}`
2400
+ });
2401
+ const ok = checks.every((check) => check.ok);
2402
+ const warnings = isRunBundle(bundle)
2403
+ ? [...rawScreenshotPostureWarnings(bundle), ...undeclaredSubjectStateWarnings(bundle)]
2404
+ : [];
2405
+ const shareSafety = isRunBundle(bundle)
2406
+ ? buildShareSafety({ ok, bundle, publicSafetyFindings })
2407
+ : {
2408
+ status: "blocked",
2409
+ reasons: [
2410
+ {
2411
+ code: "VERIFY_FAILED",
2412
+ message: "Run bundle failed verification."
2413
+ }
2414
+ ]
2415
+ };
2416
+ return {
2417
+ schema: VERIFY_SCHEMA,
2418
+ ok,
2419
+ cwd,
2420
+ run: runInput,
2421
+ bundlePath: path.relative(cwd, bundlePath),
2422
+ checks,
2423
+ shareSafety,
2424
+ warnings,
2425
+ ...(ok
2426
+ ? {}
2427
+ : {
2428
+ error: {
2429
+ code: "HUMANISH_INVALID_RUN_BUNDLE",
2430
+ message: "Run bundle failed verification."
2431
+ }
2432
+ })
2433
+ };
2434
+ }
2435
+ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2436
+ const cwd = path.resolve(cwdInput);
2437
+ const checkedAt = (hooks.now ?? (() => new Date()))().toISOString();
2438
+ const resolved = await resolveRunPath(cwd, runInput);
2439
+ if (!resolved) {
2440
+ return {
2441
+ schema: CLEANUP_SCHEMA,
2442
+ ok: false,
2443
+ cwd,
2444
+ run: runInput,
2445
+ checkedAt,
2446
+ summary: { resources: 0, killed: 0, alreadyClean: 0, failed: 0, skipped: 0 },
2447
+ resources: [],
2448
+ adapterResults: [],
2449
+ warnings: [],
2450
+ error: {
2451
+ code: "HUMANISH_RUN_NOT_FOUND",
2452
+ message: `Run not found: ${runInput}`
2453
+ }
2454
+ };
2455
+ }
2456
+ const bundlePath = path.join(resolved, "run.json");
2457
+ const cleanupPath = path.join(resolved, "cleanup.json");
2458
+ const bundle = await readJsonIfExists(bundlePath);
2459
+ if (!isRunBundle(bundle)) {
2460
+ return {
2461
+ schema: CLEANUP_SCHEMA,
2462
+ ok: false,
2463
+ cwd,
2464
+ run: runInput,
2465
+ bundlePath: path.relative(cwd, bundlePath),
2466
+ checkedAt,
2467
+ summary: { resources: 0, killed: 0, alreadyClean: 0, failed: 0, skipped: 0 },
2468
+ resources: [],
2469
+ adapterResults: [],
2470
+ warnings: [],
2471
+ error: {
2472
+ code: "HUMANISH_INVALID_RUN_BUNDLE",
2473
+ message: "Run bundle failed cleanup shape validation."
2474
+ }
2475
+ };
2476
+ }
2477
+ const resources = [];
2478
+ const warnings = [];
2479
+ let desktopModule = null;
2480
+ const providerResources = bundle.providerResources ?? [];
2481
+ for (const resource of providerResources) {
2482
+ if (resource.provider !== "e2b-desktop" || resource.kind !== "sandbox") {
2483
+ resources.push({
2484
+ provider: resource.provider,
2485
+ kind: resource.kind,
2486
+ id: resource.id,
2487
+ status: "skipped",
2488
+ message: "cleanup only supports e2b-desktop sandbox resources"
2489
+ });
2490
+ continue;
2491
+ }
2492
+ if (resource.status === "killed" || resource.cleanup?.killed === true) {
2493
+ resources.push({
2494
+ provider: resource.provider,
2495
+ kind: resource.kind,
2496
+ id: resource.id,
2497
+ status: "already_clean",
2498
+ message: "resource was already recorded as killed"
2499
+ });
2500
+ continue;
2501
+ }
2502
+ try {
2503
+ desktopModule ??= await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
2504
+ if (typeof desktopModule.Sandbox.kill !== "function") {
2505
+ resources.push({
2506
+ provider: resource.provider,
2507
+ kind: resource.kind,
2508
+ id: resource.id,
2509
+ status: "failed",
2510
+ message: "installed @e2b/desktop SDK does not expose Sandbox.kill"
2511
+ });
2512
+ continue;
2513
+ }
2514
+ await desktopModule.Sandbox.kill(resource.id, { requestTimeoutMs: 60_000 });
2515
+ resources.push({
2516
+ provider: resource.provider,
2517
+ kind: resource.kind,
2518
+ id: resource.id,
2519
+ status: "killed",
2520
+ message: "resource killed by exact recorded id"
2521
+ });
2522
+ }
2523
+ catch (error) {
2524
+ resources.push({
2525
+ provider: resource.provider,
2526
+ kind: resource.kind,
2527
+ id: resource.id,
2528
+ status: "failed",
2529
+ message: error instanceof Error ? error.message : String(error)
2530
+ });
2531
+ }
2532
+ }
2533
+ let adapterResults = [];
2534
+ if (hooks.cleanupAdapterResources) {
2535
+ try {
2536
+ adapterResults = await hooks.cleanupAdapterResources({ cwd, runDir: resolved, bundle });
2537
+ }
2538
+ catch (error) {
2539
+ adapterResults = [{
2540
+ id: "adapter-cleanup",
2541
+ ok: false,
2542
+ message: error instanceof Error ? error.message : String(error)
2543
+ }];
2544
+ }
2545
+ }
2546
+ if (providerResources.length === 0 && adapterResults.length === 0) {
2547
+ warnings.push("Run bundle recorded no run-owned provider resources; nothing to clean.");
2548
+ }
2549
+ const summary = {
2550
+ resources: resources.length,
2551
+ killed: resources.filter((resource) => resource.status === "killed").length,
2552
+ alreadyClean: resources.filter((resource) => resource.status === "already_clean").length,
2553
+ failed: resources.filter((resource) => resource.status === "failed").length + adapterResults.filter((result) => !result.ok).length,
2554
+ skipped: resources.filter((resource) => resource.status === "skipped").length
2555
+ };
2556
+ const ok = summary.failed === 0;
2557
+ const result = {
2558
+ schema: CLEANUP_SCHEMA,
2559
+ ok,
2560
+ cwd: PUBLIC_TARGET_CWD,
2561
+ run: runInput,
2562
+ runId: bundle.runId,
2563
+ bundlePath: path.relative(cwd, bundlePath),
2564
+ cleanupPath: path.relative(cwd, cleanupPath),
2565
+ checkedAt,
2566
+ summary,
2567
+ resources,
2568
+ adapterResults,
2569
+ warnings
2570
+ };
2571
+ await writeJson(cleanupPath, result);
2572
+ return result;
2573
+ }
2574
+ export async function loadRunBundle(cwdInput, runInput) {
2575
+ const cwd = path.resolve(cwdInput);
2576
+ const resolved = await resolveRunPath(cwd, runInput);
2577
+ if (!resolved) {
2578
+ return null;
2579
+ }
2580
+ const bundlePath = path.join(resolved, "run.json");
2581
+ const bundle = await readJsonIfExists(bundlePath);
2582
+ if (!isRunBundle(bundle)) {
2583
+ return null;
2584
+ }
2585
+ return {
2586
+ bundle,
2587
+ bundlePath: path.relative(cwd, bundlePath),
2588
+ runDir: resolved
2589
+ };
2590
+ }
2591
+ export async function listRuns(cwdInput) {
2592
+ const cwd = path.resolve(cwdInput);
2593
+ const runsRoot = path.join(cwd, ".humanish", "runs");
2594
+ // ENOENT (no .humanish/runs yet) is a normal empty state: ok:true, no runs. Any
2595
+ // other readdir failure (e.g. permission denied) is a real I/O failure and must
2596
+ // not be swallowed into a false "no runs" report.
2597
+ let entries;
2598
+ try {
2599
+ entries = await readdir(runsRoot, { withFileTypes: true });
2600
+ }
2601
+ catch (error) {
2602
+ if (isNodeError(error) && error.code === "ENOENT") {
2603
+ entries = [];
2604
+ }
2605
+ else {
2606
+ return runsUnavailableResult(cwd, error);
2607
+ }
2608
+ }
2609
+ let latest;
2610
+ try {
2611
+ latest = await readLatest(cwd);
2612
+ }
2613
+ catch (error) {
2614
+ return runsUnavailableResult(cwd, error);
2615
+ }
2616
+ const runs = [];
2617
+ for (const entry of entries) {
2618
+ if (!entry.isDirectory()) {
2619
+ continue;
2620
+ }
2621
+ const bundle = await readJsonIfExists(path.join(runsRoot, entry.name, "run.json"));
2622
+ runs.push({
2623
+ runId: entry.name,
2624
+ createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
2625
+ mode: isRecord(bundle) && typeof bundle.mode === "string" ? bundle.mode : null,
2626
+ path: path.join(".humanish", "runs", entry.name)
2627
+ });
2628
+ }
2629
+ return {
2630
+ schema: RUNS_SCHEMA,
2631
+ ok: true,
2632
+ cwd,
2633
+ runs: runs.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")),
2634
+ latest: latest?.runId ?? null
2635
+ };
2636
+ }
2637
+ function runsUnavailableResult(cwd, error) {
2638
+ return {
2639
+ schema: RUNS_SCHEMA,
2640
+ ok: false,
2641
+ cwd,
2642
+ runs: [],
2643
+ latest: null,
2644
+ error: {
2645
+ code: "HUMANISH_RUNS_UNAVAILABLE",
2646
+ message: redactText(error instanceof Error ? error.message : String(error))
2647
+ }
2648
+ };
2649
+ }
2650
+ export async function readReview(cwdInput, runInput) {
2651
+ const verified = await verifyRun(cwdInput, runInput);
2652
+ if (!verified.ok || !verified.bundlePath) {
2653
+ return verified;
2654
+ }
2655
+ const cwd = path.resolve(cwdInput);
2656
+ const runDir = path.dirname(path.join(cwd, verified.bundlePath));
2657
+ const review = await readJsonIfExists(path.join(runDir, "review.json"));
2658
+ if (!isReviewSummary(review)) {
2659
+ return {
2660
+ ...verified,
2661
+ ok: false,
2662
+ error: {
2663
+ code: "HUMANISH_INVALID_RUN_BUNDLE",
2664
+ message: "review.json is missing or invalid."
2665
+ }
2666
+ };
2667
+ }
2668
+ return {
2669
+ ...review,
2670
+ path: path.relative(cwd, path.join(runDir, "review.json")),
2671
+ runId: path.basename(runDir)
2672
+ };
2673
+ }
2674
+ export async function doctor(cwdInput) {
2675
+ const cwd = path.resolve(cwdInput);
2676
+ const checks = [
2677
+ {
2678
+ name: "target cwd",
2679
+ ok: await directoryExists(cwd),
2680
+ message: "target directory exists"
2681
+ },
2682
+ {
2683
+ name: "package.json",
2684
+ ok: await fileExists(path.join(cwd, "package.json")),
2685
+ message: "package.json is present"
2686
+ },
2687
+ {
2688
+ name: "humanish source",
2689
+ ok: await directoryExists(path.join(cwd, "humanish")),
2690
+ message: "committed humanish/ source directory is present"
2691
+ },
2692
+ {
2693
+ name: "runtime ignore",
2694
+ ok: (await readTextIfExists(path.join(cwd, ".gitignore")))?.includes(".humanish/") ?? false,
2695
+ message: ".gitignore contains .humanish/"
2696
+ }
2697
+ ];
2698
+ return {
2699
+ schema: DOCTOR_SCHEMA,
2700
+ ok: checks.every((check) => check.ok),
2701
+ cwd,
2702
+ checks
2703
+ };
2704
+ }
2705
+ function createReviewSummary() {
2706
+ return {
2707
+ schema: REVIEW_SCHEMA,
2708
+ verdict: "contract_proof_only",
2709
+ summary: "Synthetic dry-run bundle was generated. This proves Humanish artifact plumbing, not product behavior.",
2710
+ gaps: [
2711
+ "No browser was launched.",
2712
+ "No product state was verified.",
2713
+ "No model, provider, or E2B substrate was used."
2714
+ ]
2715
+ };
2716
+ }
2717
+ function createLocalActorRunningReviewSummary(actorLabel) {
2718
+ return {
2719
+ schema: REVIEW_SCHEMA,
2720
+ verdict: "contract_proof_only",
2721
+ summary: `Live local ${actorLabel} actor is running. This is an in-progress Observer snapshot; final verdict will be written after sanitized actor evidence is captured.`,
2722
+ gaps: [
2723
+ "Final actor verdict and transcript artifacts are not available until the run completes.",
2724
+ "Live Observer follow depends on polling observer/observer-data.json while this run is active.",
2725
+ "No GitHub mutation, target OSS mutation, E2B substrate, or production data is used by this local actor contract."
2726
+ ]
2727
+ };
2728
+ }
2729
+ function createLocalActorReviewSummary(actorLabel, status, reason) {
2730
+ const verdict = status === "passed"
2731
+ ? "pass"
2732
+ : status === "timed_out"
2733
+ ? "timed_out"
2734
+ : status === "blocked"
2735
+ ? "blocked"
2736
+ : "fail";
2737
+ const isTui = actorLabel.toLowerCase().includes("tui");
2738
+ const isFanout = actorLabel.toLowerCase().includes("fanout");
2739
+ return {
2740
+ schema: REVIEW_SCHEMA,
2741
+ verdict,
2742
+ summary: `Live local ${actorLabel} actor ${status}: ${reason}. This proves the local actor lifecycle and sanitized evidence path${isFanout ? " across requested fanout lanes" : ""}, not target product behavior.`,
2743
+ gaps: [
2744
+ isTui
2745
+ ? "Only one local Codex TUI actor is supported in this slice."
2746
+ : "Codex TUI trust bootstrap, PTY rendering, and keyboard-focus proof remain separate from the noninteractive exec actor.",
2747
+ "Live follow uses polling Observer snapshots; raw interactive terminal streaming remains a follow-up hardening step.",
2748
+ "No GitHub mutation, target OSS mutation, E2B substrate, or production data was used by this local actor contract."
2749
+ ]
2750
+ };
2751
+ }
2752
+ async function loadDryRunSelection(cwd, humanishSource) {
2753
+ const warnings = [];
2754
+ if (humanishSource === "missing") {
2755
+ return {
2756
+ persona: builtinPersona,
2757
+ resolvedPersona: parseResolvedPersona({}, { id: builtinPersona.id, name: builtinPersona.name }),
2758
+ scenario: builtinScenario,
2759
+ warnings
2760
+ };
2761
+ }
2762
+ const personaPath = "humanish/personas/synthetic-new-user.yaml";
2763
+ const scenarioPath = "humanish/scenarios/first-run-smoke.yaml";
2764
+ const personaText = await readTextIfExists(path.join(cwd, personaPath));
2765
+ const scenarioText = await readTextIfExists(path.join(cwd, scenarioPath));
2766
+ const browserJourneySelection = await loadBrowserPersonaJourneySelection(cwd);
2767
+ if (personaText === null) {
2768
+ warnings.push(`${personaPath} was not found; using built-in persona defaults.`);
2769
+ }
2770
+ if (scenarioText === null) {
2771
+ warnings.push(`${scenarioPath} was not found; using built-in scenario defaults.`);
2772
+ }
2773
+ let resolvedPersona;
2774
+ if (personaText === null) {
2775
+ resolvedPersona = parseResolvedPersona({}, { id: builtinPersona.id, name: builtinPersona.name });
2776
+ }
2777
+ else {
2778
+ const parsedPersona = parsePersonaYaml(personaText);
2779
+ if (parsedPersona.failed) {
2780
+ warnings.push(`${personaPath} could not be parsed as YAML; using built-in persona trait defaults.`);
2781
+ }
2782
+ resolvedPersona = parseResolvedPersona(parsedPersona.value, {
2783
+ id: "synthetic-new-user",
2784
+ name: "Synthetic New User"
2785
+ });
2786
+ }
2787
+ return {
2788
+ ...(browserJourneySelection.journey ? { browserJourney: browserJourneySelection.journey } : {}),
2789
+ ...(browserJourneySelection.failure ? { browserJourneyFailure: browserJourneySelection.failure } : {}),
2790
+ persona: personaText === null
2791
+ ? builtinPersona
2792
+ : {
2793
+ id: readYamlScalar(personaText, "id") ?? "synthetic-new-user",
2794
+ name: readYamlScalar(personaText, "name") ?? "Synthetic New User",
2795
+ source: personaPath,
2796
+ sourceDigest: digestText(personaText)
2797
+ },
2798
+ resolvedPersona,
2799
+ scenario: scenarioText === null
2800
+ ? builtinScenario
2801
+ : {
2802
+ id: readYamlScalar(scenarioText, "id") ?? "first-run-smoke",
2803
+ title: readYamlScalar(scenarioText, "title") ?? "First-run smoke",
2804
+ goal: readYamlScalar(scenarioText, "goal") ?? "Run a public-safe first-run smoke scenario.",
2805
+ source: scenarioPath,
2806
+ sourceDigest: digestText(scenarioText)
2807
+ },
2808
+ warnings: [...warnings, ...browserJourneySelection.warnings]
2809
+ };
2810
+ }
2811
+ async function loadBrowserPersonaJourneySelection(cwd) {
2812
+ const warnings = [];
2813
+ const scenarioDir = path.join(cwd, "humanish", "scenarios");
2814
+ const names = await readdir(scenarioDir).catch((error) => {
2815
+ if (isNodeError(error) && error.code === "ENOENT") {
2816
+ return [];
2817
+ }
2818
+ throw error;
2819
+ });
2820
+ const files = names
2821
+ .filter((name) => name.endsWith(".yaml") || name.endsWith(".yml"))
2822
+ .sort((left, right) => {
2823
+ if (left === "first-run-smoke.yaml")
2824
+ return -1;
2825
+ if (right === "first-run-smoke.yaml")
2826
+ return 1;
2827
+ return left.localeCompare(right);
2828
+ });
2829
+ for (const name of files) {
2830
+ const relativePath = path.join("humanish", "scenarios", name);
2831
+ const absolutePath = path.join(cwd, relativePath);
2832
+ const text = await readTextIfExists(absolutePath);
2833
+ if (text === null) {
2834
+ continue;
2835
+ }
2836
+ let raw;
2837
+ try {
2838
+ raw = parseYaml(text);
2839
+ }
2840
+ catch (error) {
2841
+ return {
2842
+ failure: `${relativePath} could not be parsed as YAML; browser persona journey failed closed.`,
2843
+ warnings
2844
+ };
2845
+ }
2846
+ const parsed = parseBrowserPersonaJourneyFromScenario({
2847
+ raw,
2848
+ relativePath,
2849
+ sourceDigest: digestText(text)
2850
+ });
2851
+ if (parsed.failure) {
2852
+ return {
2853
+ failure: parsed.failure,
2854
+ warnings
2855
+ };
2856
+ }
2857
+ if (parsed.journey) {
2858
+ return {
2859
+ journey: parsed.journey,
2860
+ warnings
2861
+ };
2862
+ }
2863
+ }
2864
+ return { warnings };
2865
+ }
2866
+ function renderReviewMarkdown(bundle) {
2867
+ return `# Humanish Run Review
2868
+
2869
+ Run: ${bundle.runId}
2870
+
2871
+ Mode: ${bundle.mode}
2872
+
2873
+ Verdict: ${bundle.review.verdict}
2874
+
2875
+ ${bundle.review.summary}
2876
+
2877
+ ## Public-Safety
2878
+
2879
+ - Redaction: ${bundle.redaction.status}
2880
+ - Notes: ${bundle.redaction.notes}
2881
+
2882
+ ## Gaps
2883
+
2884
+ ${bundle.review.gaps.map((gap) => `- ${gap}`).join("\n")}
2885
+ `;
2886
+ }
2887
+ function readYamlScalar(text, key) {
2888
+ const match = text.match(new RegExp(`^${escapeRegExp(key)}:\\s*(.+?)\\s*$`, "m"));
2889
+ if (!match?.[1]) {
2890
+ return null;
2891
+ }
2892
+ return match[1].replace(/^["']|["']$/g, "");
2893
+ }
2894
+ function parsePersonaYaml(text) {
2895
+ try {
2896
+ return { value: parseYaml(text), failed: false };
2897
+ }
2898
+ catch {
2899
+ return { value: {}, failed: true };
2900
+ }
2901
+ }
2902
+ function escapeRegExp(value) {
2903
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2904
+ }
2905
+ async function resolveRunPath(cwd, runInput) {
2906
+ if (runInput === "latest") {
2907
+ const latest = await readLatest(cwd);
2908
+ return latest ? path.join(cwd, latest.path) : null;
2909
+ }
2910
+ const direct = path.join(cwd, ".humanish", "runs", runInput);
2911
+ return await directoryExists(direct) ? direct : null;
2912
+ }
2913
+ async function readLatest(cwd) {
2914
+ const latest = await readJsonIfExists(path.join(cwd, ".humanish", "runs", "latest.json"));
2915
+ if (isRunPointer(latest)) {
2916
+ return latest;
2917
+ }
2918
+ return null;
2919
+ }
2920
+ async function readPackageName(cwd) {
2921
+ const packageJson = await readJsonIfExists(path.join(cwd, "package.json"));
2922
+ return isRecord(packageJson) && typeof packageJson.name === "string" ? packageJson.name : null;
2923
+ }
2924
+ async function readJsonIfExists(filePath) {
2925
+ const text = await readTextIfExists(filePath);
2926
+ if (text === null) {
2927
+ return null;
2928
+ }
2929
+ try {
2930
+ return JSON.parse(text);
2931
+ }
2932
+ catch {
2933
+ return null;
2934
+ }
2935
+ }
2936
+ async function readTextIfExists(filePath) {
2937
+ try {
2938
+ return await readFile(filePath, "utf8");
2939
+ }
2940
+ catch (error) {
2941
+ if (isNodeError(error) && error.code === "ENOENT") {
2942
+ return null;
2943
+ }
2944
+ throw error;
2945
+ }
2946
+ }
2947
+ async function writeJson(filePath, value) {
2948
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
2949
+ }
2950
+ async function writeRunBundleArtifacts(absoluteArtifactRoot, bundle) {
2951
+ const publicBundle = {
2952
+ ...bundle,
2953
+ cwd: PUBLIC_TARGET_CWD
2954
+ };
2955
+ await writeJson(path.join(absoluteArtifactRoot, "run.json"), publicBundle);
2956
+ await writeJson(path.join(absoluteArtifactRoot, "review.json"), publicBundle.review);
2957
+ await writeFile(path.join(absoluteArtifactRoot, "review.md"), renderReviewMarkdown(publicBundle), "utf8");
2958
+ await writeFile(path.join(absoluteArtifactRoot, "events.ndjson"), `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
2959
+ await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
2960
+ await writeJson(path.join(absoluteArtifactRoot, "observer", "observer-data.json"), buildObserverData(publicBundle));
2961
+ }
2962
+ async function missingLocalEvidenceArtifacts(runRoot, bundle) {
2963
+ const requiredPaths = new Map();
2964
+ const addRequiredPath = (artifactPath, options = {}) => {
2965
+ const existing = requiredPaths.get(artifactPath);
2966
+ requiredPaths.set(artifactPath, { screenshot: Boolean(existing?.screenshot || options.screenshot) });
2967
+ };
2968
+ for (const stream of bundle.streams) {
2969
+ for (const artifact of stream.artifacts) {
2970
+ if (isLocalEvidenceArtifactPath(artifact.path)) {
2971
+ addRequiredPath(artifact.path, { screenshot: artifact.kind === "screenshot" });
2972
+ }
2973
+ }
2974
+ const embedPath = normalizeLocalEvidenceReference(stream.embed?.kind === "screenshot" ? stream.embed.url : undefined);
2975
+ if (embedPath) {
2976
+ addRequiredPath(embedPath, { screenshot: true });
2977
+ }
2978
+ const uiScreenshotPath = normalizeLocalEvidenceReference(stream.ui?.screenshotUrl);
2979
+ if (uiScreenshotPath) {
2980
+ addRequiredPath(uiScreenshotPath, { screenshot: true });
2981
+ }
2982
+ if (stream.ui?.nestedObserverPath && isLocalEvidenceArtifactPath(stream.ui.nestedObserverPath)) {
2983
+ addRequiredPath(stream.ui.nestedObserverPath);
2984
+ }
2985
+ }
2986
+ for (const artifact of bundle.adapterArtifacts ?? []) {
2987
+ if (isLocalEvidenceArtifactPath(artifact.path)) {
2988
+ addRequiredPath(artifact.path, { screenshot: artifact.kind === "screenshot" });
2989
+ }
2990
+ }
2991
+ const missing = [];
2992
+ for (const [artifactPath, requirements] of requiredPaths) {
2993
+ const absolutePath = path.join(runRoot, artifactPath);
2994
+ const stats = await stat(absolutePath).catch(() => null);
2995
+ if (!stats?.isFile() || stats.size <= 0) {
2996
+ missing.push(artifactPath);
2997
+ continue;
2998
+ }
2999
+ if (requirements.screenshot) {
3000
+ const bytes = await readFile(absolutePath).catch(() => null);
3001
+ const imageError = bytes ? screenshotEvidenceError(artifactPath, bytes) : "could not read screenshot bytes";
3002
+ if (imageError) {
3003
+ missing.push(`${artifactPath} (${imageError})`);
3004
+ }
3005
+ }
3006
+ }
3007
+ return missing;
3008
+ }
3009
+ function invalidRunEvidenceReferences(bundle) {
3010
+ const findings = [];
3011
+ if (path.isAbsolute(bundle.cwd)) {
3012
+ findings.push(`run bundle persists absolute cwd ${bundle.cwd}`);
3013
+ }
3014
+ const adapterArtifactKeys = new Set();
3015
+ for (const artifact of bundle.adapterArtifacts ?? []) {
3016
+ const key = `${artifact.namespace}:${artifact.kind}:${artifact.path}`;
3017
+ if (adapterArtifactKeys.has(key)) {
3018
+ findings.push(`adapter artifact duplicate ${artifact.namespace}:${artifact.kind}:${artifact.path}`);
3019
+ }
3020
+ adapterArtifactKeys.add(key);
3021
+ if (!isLocalEvidenceArtifactPath(artifact.path)) {
3022
+ findings.push(`adapter artifact ${artifact.namespace}:${artifact.kind} nonlocal artifact ${artifact.path}`);
3023
+ }
3024
+ }
3025
+ for (const stream of bundle.streams) {
3026
+ const seen = new Set();
3027
+ for (const artifact of stream.artifacts) {
3028
+ const key = `${artifact.kind}:${artifact.path}`;
3029
+ if (seen.has(key)) {
3030
+ findings.push(`${stream.id} duplicate artifact ${artifact.kind}:${artifact.path}`);
3031
+ }
3032
+ seen.add(key);
3033
+ if (!isLocalEvidenceArtifactPath(artifact.path)) {
3034
+ findings.push(`${stream.id} nonlocal artifact ${artifact.kind}:${artifact.path}`);
3035
+ }
3036
+ }
3037
+ if (stream.ui?.nestedObserverPath && !isLocalEvidenceArtifactPath(stream.ui.nestedObserverPath)) {
3038
+ findings.push(`${stream.id} nonlocal nested observer reference ${stream.ui.nestedObserverPath}`);
3039
+ }
3040
+ if (stream.embed?.kind === "screenshot" && stream.embed.url && !normalizeLocalEvidenceReference(stream.embed.url)) {
3041
+ findings.push(`${stream.id} nonlocal screenshot embed ${stream.embed.url}`);
3042
+ }
3043
+ if (stream.ui?.screenshotUrl && !normalizeLocalEvidenceReference(stream.ui.screenshotUrl)) {
3044
+ findings.push(`${stream.id} nonlocal screenshot reference ${stream.ui.screenshotUrl}`);
3045
+ }
3046
+ }
3047
+ return findings.slice(0, 50);
3048
+ }
3049
+ // The fixed artifact filenames the terminal-product lane (src/e2b-terminal-lab.ts) persists.
3050
+ // Kept in sync with TERMINAL_LEDGERS_ARTIFACT / TERMINAL_EVENTS_ARTIFACT / TERMINAL_TRANSCRIPT_ARTIFACT.
3051
+ const TERMINAL_LEDGERS_FILE = "terminal-ledgers.json";
3052
+ const TERMINAL_EVENTS_FILE = "terminal-events.ndjson";
3053
+ const TERMINAL_TRANSCRIPT_FILE = "terminal-transcript.txt";
3054
+ /**
3055
+ * Verifier for the terminal-product real-agent lane (issue #154, the in-sandbox command-scoped
3056
+ * key route). A LIVE terminal stream must carry the durable proof the safety contract requires,
3057
+ * and must FAIL CLOSED when any of it is missing — a blocked/failed agent run stays structurally
3058
+ * verifiable (the failure is the evidence) ONLY when the substrate/cleanup/interventions ledgers
3059
+ * are present; it must never become a hollow pass. Credential-shape leakage across every artifact
3060
+ * file is already caught by scanRunPublicSafetyArtifacts; this check enforces the STRUCTURAL
3061
+ * evidence + the proven-teardown invariant. Dry-run/contract bundles are exempt (mode !== live).
3062
+ */
3063
+ async function validateTerminalProductEvidence(runRoot, bundle) {
3064
+ if (bundle.mode !== "live") {
3065
+ return [];
3066
+ }
3067
+ const findings = [];
3068
+ // Detect the terminal-PRODUCT lane by its unique actor-trace protocol ("terminal-exec"), NOT by
3069
+ // the broad stream.kind "terminal" — the existing local codex-exec/TUI lanes also use terminal
3070
+ // streams (with a different protocol) and must not be held to this lane's ledger contract.
3071
+ const terminalStreams = bundle.streams.filter((stream) => stream.actor?.protocol === "terminal-exec" && stream.status !== "contract_proof_only");
3072
+ if (terminalStreams.length === 0) {
3073
+ return findings;
3074
+ }
3075
+ // The lane writes exactly one terminal run's ledgers/evidence at fixed paths in the run root.
3076
+ const ledgers = await readJsonIfExists(path.join(runRoot, TERMINAL_LEDGERS_FILE));
3077
+ if (!isRecord(ledgers) || ledgers.schema !== "humanish.terminal-ledgers.v1") {
3078
+ findings.push(`missing or malformed ${TERMINAL_LEDGERS_FILE} (humanish.terminal-ledgers.v1)`);
3079
+ return findings;
3080
+ }
3081
+ // Substrate lifecycle ledger: must record at least sandbox creation AND teardown.
3082
+ const lifecycle = Array.isArray(ledgers.lifecycle) ? ledgers.lifecycle : [];
3083
+ if (lifecycle.length === 0) {
3084
+ findings.push("substrate lifecycle ledger is empty (expected create -> ready -> exec -> cleanup events)");
3085
+ }
3086
+ // Command log: present (an array; empty is allowed only if the session never reached exec, which
3087
+ // the lifecycle/cleanup records still cover).
3088
+ if (!Array.isArray(ledgers.commandLog)) {
3089
+ findings.push("command log ledger is missing or not an array");
3090
+ }
3091
+ // Interventions ledger: must be PRESENT (an array). Empty is valid and expected (stdin disabled,
3092
+ // no assisted-input path) — but absent fails, so an assisted run can never masquerade as one
3093
+ // without an interventions record.
3094
+ if (!Array.isArray(ledgers.interventions)) {
3095
+ findings.push("interventions ledger is missing (an empty array is required-present, not optional)");
3096
+ }
3097
+ // Cleanup proof: the sandbox must be killed and proven reclaimed BY EXACT ID (remaining===0).
3098
+ // humanish never calls Sandbox.list to derive this field; a live run that cannot prove teardown
3099
+ // fails closed (remaining===1 still-present-unconfirmed, remaining===-1 kill(id) itself
3100
+ // failed -- the server-side kill-on-timeout is the backstop for both).
3101
+ const cleanup = isRecord(ledgers.cleanup) ? ledgers.cleanup : undefined;
3102
+ if (!cleanup) {
3103
+ findings.push("cleanup proof is missing");
3104
+ }
3105
+ else if (cleanup.killed !== true || cleanup.remaining !== 0) {
3106
+ findings.push(`cleanup not proven by id (killed=${String(cleanup.killed)}, remaining=${String(cleanup.remaining)}); a run that cannot prove sandbox teardown fails closed`);
3107
+ }
3108
+ // The redacted exec-stream + normalized transcript artifacts must be WRITTEN (the producer
3109
+ // always writes them on the live path, even empty for a no-output blocked run — so absence is a
3110
+ // real evidence gap, while emptiness is legitimate and keeps blocked runs verifiable).
3111
+ if (!(await fileExists(path.join(runRoot, TERMINAL_EVENTS_FILE)))) {
3112
+ findings.push(`missing terminal event stream artifact (${TERMINAL_EVENTS_FILE})`);
3113
+ }
3114
+ if (!(await fileExists(path.join(runRoot, TERMINAL_TRANSCRIPT_FILE)))) {
3115
+ findings.push(`missing normalized terminal transcript artifact (${TERMINAL_TRANSCRIPT_FILE})`);
3116
+ }
3117
+ // The provider-neutral actor trace must be on the terminal lane with redaction passed.
3118
+ for (const stream of terminalStreams) {
3119
+ const traceArtifact = stream.artifacts.find((artifact) => artifact.kind === "trace");
3120
+ const tracePath = traceArtifact?.path ?? "actor.json";
3121
+ const trace = await readJsonIfExists(path.join(runRoot, tracePath));
3122
+ if (!isRecord(trace) || trace.lane !== "terminal") {
3123
+ findings.push(`${stream.id} missing terminal-lane actor trace`);
3124
+ continue;
3125
+ }
3126
+ if (!isRecord(trace.redaction) || trace.redaction.status !== "passed") {
3127
+ findings.push(`${stream.id} actor trace redaction status must be passed`);
3128
+ }
3129
+ }
3130
+ // --- SLICE 3: the cost ledger + no-spend proof must be present + internally honest. ---
3131
+ findings.push(...validateTerminalCostEvidence(ledgers));
3132
+ return findings;
3133
+ }
3134
+ // The four cost categories the no-spend proof + cost ledger reason over. Kept in sync with
3135
+ // e2b-terminal-lab.ts COST_CATEGORIES (a missing category on either side is a finding).
3136
+ const TERMINAL_COST_CATEGORIES = ["product", "media", "payment", "provider"];
3137
+ /**
3138
+ * Verifier for the SLICE-3 cost ledger + no-spend proof (issue #154's cost/no-spend asks). A LIVE
3139
+ * terminal-product bundle MUST carry both (fail closed if absent on a live run). The load-bearing
3140
+ * honesty check: the no-spend proof may NOT claim zero on a line the ledger marks `null`
3141
+ * (UNMEASURED) — a proof can never claim more than the ledger measured. And the observed KNOWN
3142
+ * spend may not exceed the declared cap (the proof's own maxUsd) — fail-closed, not advisory.
3143
+ * The null discipline is enforced here too: a present line's `usd` must be a number OR literally
3144
+ * null (never undefined/omitted), so "not measured" can never be silently dropped.
3145
+ */
3146
+ function validateTerminalCostEvidence(ledgers) {
3147
+ const findings = [];
3148
+ const cost = isRecord(ledgers.cost) ? ledgers.cost : undefined;
3149
+ if (!cost || cost.schema !== "humanish.terminal-cost-ledger.v1") {
3150
+ findings.push("missing or malformed cost ledger (humanish.terminal-cost-ledger.v1) — a live terminal-product run must derive a cost ledger");
3151
+ return findings;
3152
+ }
3153
+ const lines = isRecord(cost.lines) ? cost.lines : undefined;
3154
+ if (!lines) {
3155
+ findings.push("cost ledger has no lines block");
3156
+ return findings;
3157
+ }
3158
+ // The null discipline: every applicable category line must be PRESENT with `usd` as a number or
3159
+ // literally null. `undefined`/omitted is forbidden — that would silently lose the "not measured"
3160
+ // distinction. Track which categories the ledger marks null so the no-spend proof cannot lie about them.
3161
+ const nullCategories = new Set();
3162
+ for (const category of TERMINAL_COST_CATEGORIES) {
3163
+ const line = isRecord(lines[category]) ? lines[category] : undefined;
3164
+ if (!line || !("usd" in line)) {
3165
+ findings.push(`cost ledger line "${category}" is missing its usd field (unknowns must be explicit null, never omitted)`);
3166
+ continue;
3167
+ }
3168
+ const usd = line.usd;
3169
+ if (usd === null) {
3170
+ nullCategories.add(category);
3171
+ }
3172
+ else if (typeof usd !== "number") {
3173
+ findings.push(`cost ledger line "${category}" usd must be a number or null (got ${typeof usd})`);
3174
+ }
3175
+ }
3176
+ const proof = isRecord(ledgers.noSpendProof) ? ledgers.noSpendProof : undefined;
3177
+ if (!proof || proof.schema !== "humanish.terminal-no-spend-proof.v1") {
3178
+ findings.push("missing or malformed no-spend proof (humanish.terminal-no-spend-proof.v1) — the no-spend proof must be derived from the ledger");
3179
+ return findings;
3180
+ }
3181
+ // HONESTY CHECK: the no-spend proof must NOT claim zero on a line the ledger marks `null`. A
3182
+ // knownZeroLines entry that is actually unmeasured in the ledger means the proof claimed more than
3183
+ // it measured — fail closed.
3184
+ const knownZeroLines = Array.isArray(proof.knownZeroLines) ? proof.knownZeroLines : [];
3185
+ for (const category of knownZeroLines) {
3186
+ if (nullCategories.has(String(category))) {
3187
+ findings.push(`no-spend proof claims zero on line "${String(category)}" but the cost ledger marks it null (UNMEASURED); a proof may not claim zero on a line it did not measure`);
3188
+ }
3189
+ }
3190
+ // FAIL-CLOSED CAP: observed KNOWN spend may not exceed the declared cap (the proof's maxUsd). The
3191
+ // ledger's knownTotalUsd is the measured spend; null lines do not count toward it (and the proof
3192
+ // reports them as unmeasured). A satisfied proof whose known total exceeds its cap is contradictory.
3193
+ const knownTotalUsd = typeof cost.knownTotalUsd === "number" ? cost.knownTotalUsd : Number.NaN;
3194
+ const maxUsd = typeof proof.maxUsd === "number" ? proof.maxUsd : null;
3195
+ if (maxUsd !== null && Number.isFinite(knownTotalUsd) && knownTotalUsd > maxUsd) {
3196
+ findings.push(`observed KNOWN spend ${knownTotalUsd} USD exceeds the declared cap maxUsd=${maxUsd}; the run must fail closed, not verify green`);
3197
+ }
3198
+ // A proof that asserts `satisfied:true` while a known line is non-zero (knownNonZeroLines) is
3199
+ // self-contradictory — reject it (the proof's own derived state must be internally consistent).
3200
+ const knownNonZeroLines = Array.isArray(proof.knownNonZeroLines) ? proof.knownNonZeroLines : [];
3201
+ if (proof.satisfied === true && knownNonZeroLines.length > 0) {
3202
+ findings.push(`no-spend proof claims satisfied:true but reports known non-zero spend lines (${knownNonZeroLines.map(String).join(", ")})`);
3203
+ }
3204
+ return findings;
3205
+ }
3206
+ async function validateCodexAppServerEvidence(runRoot, bundle) {
3207
+ if (bundle.mode !== "live") {
3208
+ return [];
3209
+ }
3210
+ const findings = [];
3211
+ const appServerStreams = bundle.streams.filter((stream) => stream.status !== "contract_proof_only"
3212
+ && (stream.codex?.provider === "codex-app-server"
3213
+ || stream.artifacts.some((artifact) => artifact.path.includes("codex-app-server"))));
3214
+ for (const stream of appServerStreams) {
3215
+ if (stream.codex?.provider !== "codex-app-server") {
3216
+ findings.push(`${stream.id} missing first-class codex app-server metadata`);
3217
+ }
3218
+ if (stream.status === "running" || stream.codex?.state === "connecting" || stream.codex?.state === "running") {
3219
+ continue;
3220
+ }
3221
+ const traceArtifact = stream.artifacts.find((artifact) => artifact.kind === "trace"
3222
+ && artifact.path.includes("codex-app-server"));
3223
+ const eventsArtifact = stream.artifacts.find((artifact) => artifact.kind === "events"
3224
+ && artifact.path.includes("codex-app-server"));
3225
+ const logArtifact = stream.artifacts.find((artifact) => artifact.kind === "log"
3226
+ && artifact.path.includes("codex-app-server"));
3227
+ if (!traceArtifact) {
3228
+ findings.push(`${stream.id} missing codex app-server trace artifact`);
3229
+ continue;
3230
+ }
3231
+ const trace = await readJsonIfExists(path.join(runRoot, traceArtifact.path));
3232
+ if (!isRecord(trace) || ![CODEX_APP_SERVER_TRACE_SCHEMA, CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA].includes(String(trace.schema))) {
3233
+ findings.push(`${stream.id} trace artifact must use ${CODEX_APP_SERVER_TRACE_SCHEMA} or ${CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA}`);
3234
+ }
3235
+ if (!isRecord(trace) || !isRecord(trace.redaction) || trace.redaction.status !== "passed") {
3236
+ findings.push(`${stream.id} trace redaction status must be passed`);
3237
+ }
3238
+ if (!eventsArtifact) {
3239
+ findings.push(`${stream.id} missing codex app-server event envelope log`);
3240
+ }
3241
+ if (!logArtifact) {
3242
+ findings.push(`${stream.id} missing codex app-server transcript summary log`);
3243
+ }
3244
+ }
3245
+ return findings;
3246
+ }
3247
+ // Trace item kinds that show the actor DID something (drove UI, ran a command, called a tool,
3248
+ // changed a file). reasoning/screenshot/plan/notice items are observation, not engagement.
3249
+ const ACTION_BEARING_ACTOR_ITEM_KINDS = new Set(["ui_action", "command", "tool_call", "file_change"]);
3250
+ /**
3251
+ * Independent mirror of the producer-side no-engagement guard (cua-actor-lab.ts): a LIVE actor
3252
+ * trace claiming goal_satisfied while carrying zero action-bearing items AND zero message items
3253
+ * is a hollow run — the actor neither did nor said anything — and must not verify as evidence
3254
+ * (invariant 4: evidence verifies fail-closed). Live-vs-dry-run is judged exactly as the
3255
+ * producer judges it, from bundle.mode alone; dry-run/contract bundles legitimately carry no
3256
+ * actions and stay exempt. Engagement is accepted from EITHER surface — itemized trace items or
3257
+ * the producer's counts — because providers differ in what they itemize; the hollow-run
3258
+ * regression class (the 0.3.0–0.6.0 CUA parser bug) reports zero on both. The trace is read
3259
+ * defensively: isRunStream does not validate the actor seam, and verify must not throw on a
3260
+ * malformed one.
3261
+ */
3262
+ function noEngagementActorFindings(bundle) {
3263
+ if (bundle.mode !== "live") {
3264
+ return [];
3265
+ }
3266
+ const findings = [];
3267
+ for (const stream of bundle.streams) {
3268
+ const trace = stream.actor;
3269
+ if (!isRecord(trace) || trace.schema !== ACTOR_TRACE_SCHEMA || trace.completionReason !== "goal_satisfied") {
3270
+ continue;
3271
+ }
3272
+ const items = Array.isArray(trace.items) ? trace.items : [];
3273
+ const counts = isRecord(trace.counts) ? trace.counts : {};
3274
+ const countOf = (key) => {
3275
+ const value = counts[key];
3276
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
3277
+ };
3278
+ const engaged = countOf("actions") > 0
3279
+ || countOf("messages") > 0
3280
+ || hasStopWhenObservationEvidence(items, countOf("screenshots"))
3281
+ || items.some((item) => isRecord(item)
3282
+ && typeof item.kind === "string"
3283
+ && (item.kind === "message" || ACTION_BEARING_ACTOR_ITEM_KINDS.has(item.kind)));
3284
+ if (!engaged) {
3285
+ const provider = typeof trace.provider === "string" ? trace.provider : "unknown provider";
3286
+ findings.push(`${stream.id} live actor trace (${provider}) claims goal_satisfied with zero actions and zero messages`);
3287
+ }
3288
+ }
3289
+ return findings;
3290
+ }
3291
+ function hasStopWhenObservationEvidence(items, screenshotCount) {
3292
+ const hasScreenshot = screenshotCount > 0 || items.some((item) => isRecord(item)
3293
+ && item.kind === "screenshot"
3294
+ && isRecord(item.screenshotRef)
3295
+ && typeof item.screenshotRef.path === "string"
3296
+ && item.screenshotRef.path.length > 0);
3297
+ if (!hasScreenshot)
3298
+ return false;
3299
+ return items.some((item) => isRecord(item)
3300
+ && item.kind === "notice"
3301
+ && item.status === "matched"
3302
+ && typeof item.title === "string"
3303
+ && item.title.startsWith("stopWhen matched"));
3304
+ }
3305
+ function actorVerdictConsistencyFindings(bundle) {
3306
+ if (bundle.mode !== "live" || bundle.review.verdict !== "pass") {
3307
+ return [];
3308
+ }
3309
+ const findings = [];
3310
+ for (const stream of bundle.streams) {
3311
+ const trace = stream.actor;
3312
+ if (!isRecord(trace) || trace.schema !== ACTOR_TRACE_SCHEMA) {
3313
+ continue;
3314
+ }
3315
+ if (trace.status !== "passed") {
3316
+ const provider = typeof trace.provider === "string" ? trace.provider : "unknown provider";
3317
+ const reason = typeof trace.reason === "string" ? trace.reason : "no actor reason";
3318
+ findings.push(`${stream.id} live actor trace (${provider}) has status ${String(trace.status)} under a pass review verdict: ${reason}`);
3319
+ }
3320
+ }
3321
+ return findings;
3322
+ }
3323
+ /**
3324
+ * redaction.screenshots: "raw" is the SUPPORTED local default (full-fidelity frames in
3325
+ * gitignored .humanish), not a verify failure — but ok: true must never read as "share-ready",
3326
+ * so verify surfaces the posture as a warning in both human and JSON output. Read defensively
3327
+ * for the same reason as noEngagementActorFindings.
3328
+ */
3329
+ function rawScreenshotPostureWarnings(bundle) {
3330
+ const rawStreamIds = rawScreenshotStreamIds(bundle);
3331
+ if (rawStreamIds.length === 0) {
3332
+ return [];
3333
+ }
3334
+ return [
3335
+ `Screenshots are FULL-FIDELITY (raw) on ${rawStreamIds.join(", ")} — supported for local use, NOT publish-safe as-is. Verify ok does not mean share-ready; set policies.redactScreenshots: true to blur a share-as-is bundle.`
3336
+ ];
3337
+ }
3338
+ function rawScreenshotStreamIds(bundle) {
3339
+ const rawStreamIds = [];
3340
+ for (const stream of bundle.streams) {
3341
+ const trace = stream.actor;
3342
+ if (isRecord(trace) && isRecord(trace.redaction) && trace.redaction.screenshots === "raw") {
3343
+ rawStreamIds.push(stream.id);
3344
+ }
3345
+ }
3346
+ return rawStreamIds;
3347
+ }
3348
+ function buildShareSafety(args) {
3349
+ const reasons = [];
3350
+ if (!args.ok) {
3351
+ reasons.push({
3352
+ code: "VERIFY_FAILED",
3353
+ message: "The run bundle is not valid enough to promote into public feedback."
3354
+ });
3355
+ }
3356
+ if (args.publicSafetyFindings.length > 0) {
3357
+ reasons.push({
3358
+ code: "PUBLIC_SAFETY_FINDINGS",
3359
+ message: "Text artifacts or public-proof paths matched known secret, token, local-path, browser-profile, or hosted-substrate URL patterns."
3360
+ });
3361
+ }
3362
+ const rawStreamIds = rawScreenshotStreamIds(args.bundle);
3363
+ if (rawStreamIds.length > 0) {
3364
+ reasons.push({
3365
+ code: "RAW_SCREENSHOTS",
3366
+ message: `Full-fidelity screenshots are present on ${rawStreamIds.join(", ")}. This is valid local evidence, but not share-ready as-is.`
3367
+ });
3368
+ }
3369
+ if (reasons.some((reason) => reason.code === "VERIFY_FAILED" || reason.code === "PUBLIC_SAFETY_FINDINGS")) {
3370
+ return { status: "blocked", reasons };
3371
+ }
3372
+ if (reasons.length > 0) {
3373
+ return { status: "local_only", reasons };
3374
+ }
3375
+ return { status: "share_ready", reasons: [] };
3376
+ }
3377
+ // The promptDigest convention: sha256 hex, first 16 chars. A "seeded" record without a real
3378
+ // digest cannot pin "same recipe" across bundles, so verify treats it as a hollow claim.
3379
+ const COMMAND_DIGEST_PATTERN = /^[0-9a-f]{16}$/;
3380
+ // Env var NAME shape (mirrors lab-config's ENV_NAME_PATTERN). externalEnvNames must hold
3381
+ // NAMES only — a value sneaking into the list trips this check (a free secret tripwire).
3382
+ const SUBJECT_ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
3383
+ /**
3384
+ * The `subject state provenance` check (invariant 5 + invariant 4): a bundle's subject CLAIM
3385
+ * must match its recorded evidence. Bundles without a subject block (all pre-existing and
3386
+ * non-cua bundles) pass untouched. Live-vs-dry-run is judged from bundle.mode, exactly like
3387
+ * noEngagementActorFindings. Covers both the state story (seed/external) and, for the
3388
+ * local-tree route, the archive content pin.
3389
+ */
3390
+ function subjectStateFindings(bundle) {
3391
+ const subject = bundle.subject;
3392
+ if (subject === undefined) {
3393
+ return [];
3394
+ }
3395
+ const findings = [];
3396
+ const state = subject.state;
3397
+ const seed = state.seed ?? [];
3398
+ const live = bundle.mode === "live";
3399
+ // Local-tree fail-closed pin: a LIVE local-tree subject must carry a well-formed archive
3400
+ // digest -- a dirty tree cannot be commit-pinned, so archiveSha256 is the only content pin
3401
+ // this route has. Mirrors the seeded-on-dry-run discriminator immediately below: judged by
3402
+ // bundle.mode, never by the presence/shape of other fields. Never echoes the malformed value
3403
+ // (it could itself be a leaked value, same discipline as the externalEnvNames check below).
3404
+ if (live && subject.source === "local-tree") {
3405
+ // Note: a malformed-but-present string is already rejected upstream by the
3406
+ // isRunSubjectProvenance shape gate, so in practice this branch fires for the
3407
+ // MISSING case; the pattern re-check stays as defense in depth for callers
3408
+ // that bypass the schema gate.
3409
+ const pin = subject.archiveSha256;
3410
+ if (typeof pin !== "string" || !ARCHIVE_SHA256_PATTERN.test(pin)) {
3411
+ findings.push("subject.source is local-tree on a live run but archiveSha256 is missing or malformed (a local-tree subject must carry a well-formed 64-hex archive digest)");
3412
+ }
3413
+ }
3414
+ // Marker-independent rule: a passed LIVE run can never ride on a seed step that did not
3415
+ // complete ok (closes the hollow-seeded × unpinned hole — an unpinned bundle still carries
3416
+ // its seed records, and a failed migration must not hide behind the external marker).
3417
+ if (live && bundle.review.verdict === "pass" && seed.some((record) => record.ok !== true)) {
3418
+ findings.push("review verdict is pass but a recorded seed step did not complete ok — a passed live run cannot carry failed or unexecuted state steps");
3419
+ }
3420
+ switch (state.provenance) {
3421
+ case "seeded": {
3422
+ if (!live) {
3423
+ findings.push('state marker "seeded" on a dry-run bundle — a contract bundle cannot claim executed state');
3424
+ }
3425
+ if (seed.length === 0) {
3426
+ findings.push('state marker "seeded" with zero seed step records is a hollow state claim');
3427
+ }
3428
+ for (const record of seed) {
3429
+ if (!COMMAND_DIGEST_PATTERN.test(record.commandDigest)) {
3430
+ findings.push(`seed step "${record.name}" lacks a sha256-16 commandDigest`);
3431
+ }
3432
+ if (live && record.ok !== true) {
3433
+ findings.push(`state marker "seeded" but step "${record.name}" did not complete ok`);
3434
+ }
3435
+ }
3436
+ break;
3437
+ }
3438
+ case "unpinned": {
3439
+ const externalEnvNames = state.externalEnvNames ?? [];
3440
+ if (externalEnvNames.length === 0) {
3441
+ findings.push('state marker "unpinned" requires non-empty externalEnvNames (the declaration must name the external channel)');
3442
+ }
3443
+ for (const name of externalEnvNames) {
3444
+ if (!SUBJECT_ENV_NAME_PATTERN.test(name)) {
3445
+ // Deliberately does NOT echo the entry: a malformed entry may BE a value.
3446
+ findings.push("externalEnvNames carries an entry that is not an env var NAME shape (values must never appear in evidence)");
3447
+ }
3448
+ }
3449
+ break;
3450
+ }
3451
+ case "declared-not-run": {
3452
+ if (live && bundle.review.verdict === "pass") {
3453
+ findings.push("a passed live run cannot claim its declared seed steps did not run (state marker \"declared-not-run\")");
3454
+ }
3455
+ break;
3456
+ }
3457
+ case "undeclared":
3458
+ break;
3459
+ default:
3460
+ findings.push("unknown subject state provenance marker");
3461
+ }
3462
+ return findings;
3463
+ }
3464
+ function rerunLineageFindings(bundle) {
3465
+ const rerun = bundle.rerun;
3466
+ if (!rerun) {
3467
+ return [];
3468
+ }
3469
+ const findings = [];
3470
+ const selectedLaneIds = rerun.selectedLaneIds;
3471
+ const selectedSet = new Set(selectedLaneIds);
3472
+ const previousLaneIds = rerun.previous.map((entry) => entry.laneId);
3473
+ const previousSet = new Set(previousLaneIds);
3474
+ const currentLaneIds = bundle.streams.map((stream) => stream.laneId);
3475
+ const currentConcreteLaneIds = currentLaneIds.filter((laneId) => typeof laneId === "string" && laneId.trim().length > 0);
3476
+ const currentSet = new Set(currentConcreteLaneIds);
3477
+ if (selectedSet.size !== selectedLaneIds.length) {
3478
+ findings.push("selectedLaneIds contains duplicate lane ids");
3479
+ }
3480
+ if (previousSet.size !== previousLaneIds.length) {
3481
+ findings.push("previous contains duplicate lane ids");
3482
+ }
3483
+ if (currentConcreteLaneIds.length !== bundle.streams.length) {
3484
+ findings.push("every rerun stream must carry a laneId");
3485
+ }
3486
+ for (const laneId of selectedLaneIds) {
3487
+ if (!previousSet.has(laneId)) {
3488
+ findings.push(`selected lane ${laneId} is missing prior status`);
3489
+ }
3490
+ if (!currentSet.has(laneId)) {
3491
+ findings.push(`selected lane ${laneId} is missing from current streams`);
3492
+ }
3493
+ }
3494
+ for (const laneId of previousLaneIds) {
3495
+ if (!selectedSet.has(laneId)) {
3496
+ findings.push(`previous lane ${laneId} was not selected`);
3497
+ }
3498
+ }
3499
+ for (const laneId of currentConcreteLaneIds) {
3500
+ if (!selectedSet.has(laneId)) {
3501
+ findings.push(`current stream lane ${laneId} was not selected`);
3502
+ }
3503
+ }
3504
+ if (!bundle.events.some((event) => event.type === "cua-lab.fanout.rerun")) {
3505
+ findings.push("missing cua-lab.fanout.rerun event");
3506
+ }
3507
+ return findings;
3508
+ }
3509
+ // SEQUENTIAL: the three disclosures a sequential shared-world bundle MUST pin (verify fails closed
3510
+ // if any is absent — omission overclaims): sequential turns only, no concurrency/races handled, and
3511
+ // a checkpoint delta is attributed to the TURN it followed, not a specific action (correlation).
3512
+ const MANDATORY_ATTRIBUTION_LIMITS = [
3513
+ "sequential-only",
3514
+ "no-concurrent-races",
3515
+ "delta-attributed-to-turn-not-action"
3516
+ ];
3517
+ // CONCURRENT (#164 phase 2, FIX-5): the REQUIRED set (all must be present) AND a FORBIDDEN set (any
3518
+ // present == a sequential claim leaking into a concurrent bundle == overclaim). verify needs BOTH
3519
+ // checks — presence-only would let an incoherent union pass.
3520
+ const CONCURRENT_REQUIRED_LIMITS = [
3521
+ "concurrent",
3522
+ "best-effort-causal-attribution",
3523
+ "non-deterministic-shared-state",
3524
+ "window-and-snapshot-granularity",
3525
+ "contention-observed-not-proven-safe",
3526
+ "state-change-not-isolated-to-actors"
3527
+ ];
3528
+ const CONCURRENT_FORBIDDEN_LIMITS = ["sequential-only", "no-concurrent-races"];
3529
+ // A shared-world checkpoint record persists DIGEST-ONLY: exactly these keys, nothing value-shaped.
3530
+ const SHARED_WORLD_CHECKPOINT_KEYS = new Set(["kind", "name", "digest", "deltaFromPrev"]);
3531
+ // CONCURRENT stateSeries record is DIGEST-ONLY too (FIX-7): permit ONLY a numeric timestamp + the
3532
+ // sha256-16 digest; any other key is a value-shaped leak / a smuggled per-delta→actor field.
3533
+ const SHARED_WORLD_STATESERIES_KEYS = new Set(["timestamp", "digest"]);
3534
+ /**
3535
+ * The `shared-world evidence` check (#164; invariant 4 + invariant 6): a LIVE shared-world bundle's
3536
+ * interaction CLAIM must match its recorded timeline + plane provenance, and its attribution
3537
+ * ceiling must be pinned. Mirrors validateTerminalProductEvidence: live-only (dry-run contract
3538
+ * bundles are skipped, exactly like the other live-only checks). Fail-closed on every overclaim.
3539
+ */
3540
+ function sharedWorldEvidenceFindings(bundle) {
3541
+ if (bundle.mode !== "live") {
3542
+ return [];
3543
+ }
3544
+ const sw = bundle.sharedWorld;
3545
+ if (!sw) {
3546
+ // A live bundle that DECLARES shared-world attribution but carries no evidence block is a
3547
+ // hollow claim — fail closed. (Absent attributionClass + absent block == an ordinary bundle.)
3548
+ return bundle.attributionClass === "shared-world"
3549
+ ? ["attributionClass is shared-world but the sharedWorld evidence block is missing"]
3550
+ : [];
3551
+ }
3552
+ // FIX-8: dispatch on topologyMode FIRST; unknown/missing → fail closed.
3553
+ const topologyMode = sw.topologyMode;
3554
+ if (topologyMode === "sequential") {
3555
+ return sequentialSharedWorldFindings(bundle, sw);
3556
+ }
3557
+ if (topologyMode === "concurrent") {
3558
+ return concurrentSharedWorldFindings(bundle, sw);
3559
+ }
3560
+ return ['sharedWorld.topologyMode must be "sequential" or "concurrent" (missing/unknown → fail closed)'];
3561
+ }
3562
+ /** Common shape findings shared by both topologyMode branches. */
3563
+ function sharedWorldCommonFindings(bundle, sw) {
3564
+ const findings = [];
3565
+ if (sw.schema !== SHARED_WORLD_SCHEMA) {
3566
+ findings.push(`sharedWorld.schema must be ${SHARED_WORLD_SCHEMA}`);
3567
+ }
3568
+ if (bundle.attributionClass !== "shared-world") {
3569
+ findings.push("a sharedWorld evidence block requires attributionClass: shared-world");
3570
+ }
3571
+ const plane = sw.plane;
3572
+ if (!isRecord(plane) || typeof plane.seedDigest !== "string" || !COMMAND_DIGEST_PATTERN.test(plane.seedDigest)) {
3573
+ findings.push("sharedWorld.plane.seedDigest must be a sha256-16 value");
3574
+ }
3575
+ if (isRecord(plane) && Array.isArray(plane.envNames)) {
3576
+ for (const name of plane.envNames) {
3577
+ if (typeof name !== "string" || !SUBJECT_ENV_NAME_PATTERN.test(name)) {
3578
+ // Does NOT echo the entry: a malformed entry may BE a value.
3579
+ findings.push("sharedWorld.plane.envNames carries an entry that is not an env var NAME shape (values must never appear in evidence)");
3580
+ }
3581
+ }
3582
+ }
3583
+ return findings;
3584
+ }
3585
+ /**
3586
+ * SEQUENTIAL branch (the PoC #164): the alternating timeline must be well-formed, single-plane,
3587
+ * digest-only, and carry the sequential attributionLimits. FIX-8: a sequential bundle must NOT
3588
+ * carry concurrent fields (laneWindows).
3589
+ */
3590
+ function sequentialSharedWorldFindings(bundle, sw) {
3591
+ const findings = sharedWorldCommonFindings(bundle, sw);
3592
+ // Read the raw record so an injected value-shaped field on a checkpoint is visible (the typed
3593
+ // view would hide unexpected keys).
3594
+ const rawTimeline = Array.isArray(sw.timeline)
3595
+ ? (sw.timeline)
3596
+ : [];
3597
+ if (!Array.isArray(sw.timeline)) {
3598
+ findings.push("a sequential shared-world bundle must carry a timeline");
3599
+ }
3600
+ if (Array.isArray(sw.laneWindows)) {
3601
+ findings.push("a sequential shared-world bundle must NOT carry concurrent laneWindows (topologyMode mismatch)");
3602
+ }
3603
+ const sequence = Array.isArray(sw.sequence) ? sw.sequence : [];
3604
+ // Attribution ceiling: every mandatory limit MUST be present (omission overclaims → fail).
3605
+ const limits = Array.isArray(sw.attributionLimits) ? sw.attributionLimits : [];
3606
+ for (const required of MANDATORY_ATTRIBUTION_LIMITS) {
3607
+ if (!limits.includes(required)) {
3608
+ findings.push(`attributionLimits is missing the mandatory disclosure "${required}" — an absent ceiling overclaims`);
3609
+ }
3610
+ }
3611
+ const checkpoints = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "checkpoint");
3612
+ const turns = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "turn");
3613
+ // Phantom/dropped role: sequence length == roleCount == executed-turn count.
3614
+ if (!(sequence.length === sw.roleCount && turns.length === sw.roleCount)) {
3615
+ findings.push(`phantom/dropped role: sequence length (${sequence.length}), roleCount (${sw.roleCount}), and timeline turn count (${turns.length}) must all match`);
3616
+ }
3617
+ // Timeline well-formed: starts with cp-baseline, strictly alternates checkpoint → turn →
3618
+ // checkpoint, ends on a checkpoint, and turn order == sequence.
3619
+ if (rawTimeline.length === 0) {
3620
+ findings.push("timeline is empty");
3621
+ }
3622
+ else {
3623
+ const first = rawTimeline[0];
3624
+ if (!isRecord(first) || first.kind !== "checkpoint" || first.name !== "cp-baseline") {
3625
+ findings.push('timeline must start with the "cp-baseline" checkpoint');
3626
+ }
3627
+ const last = rawTimeline[rawTimeline.length - 1];
3628
+ if (!isRecord(last) || last.kind !== "checkpoint") {
3629
+ findings.push("timeline must end on a checkpoint");
3630
+ }
3631
+ rawTimeline.forEach((entry, index) => {
3632
+ const expected = index % 2 === 0 ? "checkpoint" : "turn";
3633
+ if (!isRecord(entry) || entry.kind !== expected) {
3634
+ findings.push(`timeline must strictly alternate checkpoint → turn → checkpoint (index ${index} is not a ${expected})`);
3635
+ }
3636
+ });
3637
+ if (rawTimeline.length !== 1 + 2 * turns.length) {
3638
+ findings.push("timeline length must be 1 baseline checkpoint + 2 entries (turn + checkpoint) per role");
3639
+ }
3640
+ }
3641
+ turns.forEach((turn, index) => {
3642
+ if (turn.roleId !== sequence[index]) {
3643
+ findings.push(`turn order does not match the declared sequence at position ${index} (turn "${String(turn.roleId)}" vs sequence "${String(sequence[index])}")`);
3644
+ }
3645
+ });
3646
+ // Checkpoints: digest is sha256-16 and the record carries NO value-shaped field (digest-only).
3647
+ for (const checkpoint of checkpoints) {
3648
+ const name = typeof checkpoint.name === "string" ? checkpoint.name : "(unnamed)";
3649
+ if (typeof checkpoint.digest !== "string" || !COMMAND_DIGEST_PATTERN.test(checkpoint.digest)) {
3650
+ findings.push(`checkpoint "${name}" digest is not a sha256-16 value (a value-shaped checkpoint field is rejected)`);
3651
+ }
3652
+ for (const key of Object.keys(checkpoint)) {
3653
+ if (!SHARED_WORLD_CHECKPOINT_KEYS.has(key)) {
3654
+ findings.push(`checkpoint "${name}" carries an unexpected field "${key}" — checkpoints persist digest-only`);
3655
+ }
3656
+ }
3657
+ }
3658
+ // Turns: simId/streamId resolve to a real sim/stream.
3659
+ for (const turn of turns) {
3660
+ const roleId = typeof turn.roleId === "string" ? turn.roleId : "(unnamed)";
3661
+ if (!bundle.simulations.some((sim) => sim.id === turn.simId)) {
3662
+ findings.push(`turn "${roleId}" references unknown simId "${String(turn.simId)}"`);
3663
+ }
3664
+ if (!bundle.streams.some((stream) => stream.id === turn.streamId)) {
3665
+ findings.push(`turn "${roleId}" references unknown streamId "${String(turn.streamId)}"`);
3666
+ }
3667
+ }
3668
+ // Single-plane provenance: every turn shares ONE (commit, seedDigest), matching sharedWorld.plane.
3669
+ // (plane.seedDigest + plane.envNames shape are checked in sharedWorldCommonFindings.)
3670
+ const plane = sw.plane;
3671
+ const planeKeys = new Set(turns.map((turn) => `${String(turn.commit ?? "")}::${String(turn.seedDigest ?? "")}`));
3672
+ if (planeKeys.size > 1) {
3673
+ findings.push("turns reference divergent plane provenance (commit/seedDigest) — a shared-world run drives ONE plane");
3674
+ }
3675
+ if (isRecord(plane)) {
3676
+ for (const turn of turns) {
3677
+ if (String(turn.seedDigest ?? "") !== String(plane.seedDigest ?? "")
3678
+ || String(turn.commit ?? "") !== String(plane.commit ?? "")) {
3679
+ const roleId = typeof turn.roleId === "string" ? turn.roleId : "(unnamed)";
3680
+ findings.push(`turn "${roleId}" plane provenance diverges from sharedWorld.plane`);
3681
+ break;
3682
+ }
3683
+ }
3684
+ }
3685
+ // The delta-on-pass gate: a PASSED shared-world run MUST show at least one checkpoint delta —
3686
+ // otherwise the roles never interacted through shared state and the claim is hollow.
3687
+ if (bundle.review.verdict === "pass" && !checkpoints.some((checkpoint) => checkpoint.deltaFromPrev === true)) {
3688
+ findings.push("review verdict is pass but no checkpoint shows deltaFromPrev — the interaction is hollow (no observed shared-state change)");
3689
+ }
3690
+ return findings;
3691
+ }
3692
+ /**
3693
+ * CONCURRENT branch (#164 phase 2): N personas drove ONE getHost-exposed plane at once. Verify
3694
+ * fail-closed: the shape (laneWindows + stateSeries + outcomes, NO timeline — FIX-8); the
3695
+ * corrected required + forbidden attributionLimits (FIX-5); the harness-minted getHost target every
3696
+ * actor drove (FIX-2); the synthetic-subject provenance gate (FIX-3); digest-only state series with
3697
+ * the allowed-keys tripwire (FIX-7); single-plane provenance; and the concurrency-on-pass gate
3698
+ * (genuine overlap + a state delta AT/AFTER an overlap start — FIX-6).
3699
+ */
3700
+ function concurrentSharedWorldFindings(bundle, sw) {
3701
+ const findings = sharedWorldCommonFindings(bundle, sw);
3702
+ // FIX-8: shape coherence — concurrent carries laneWindows/stateSeries/outcomes, NOT a timeline.
3703
+ if (Array.isArray(sw.timeline)) {
3704
+ findings.push("a concurrent shared-world bundle must NOT carry a sequential timeline (topologyMode mismatch)");
3705
+ }
3706
+ const laneWindows = Array.isArray(sw.laneWindows) ? sw.laneWindows.filter(isRecord) : null;
3707
+ const stateSeries = Array.isArray(sw.stateSeries) ? sw.stateSeries.filter(isRecord) : null;
3708
+ const outcomes = Array.isArray(sw.outcomes) ? sw.outcomes.filter(isRecord) : null;
3709
+ if (laneWindows === null)
3710
+ findings.push("a concurrent shared-world bundle must carry laneWindows");
3711
+ if (stateSeries === null)
3712
+ findings.push("a concurrent shared-world bundle must carry stateSeries");
3713
+ if (outcomes === null)
3714
+ findings.push("a concurrent shared-world bundle must carry outcomes");
3715
+ if (laneWindows === null || stateSeries === null || outcomes === null) {
3716
+ return findings; // can't reason further without the core series
3717
+ }
3718
+ // FIX-5: required limits all present AND forbidden limits all absent.
3719
+ const limits = Array.isArray(sw.attributionLimits) ? sw.attributionLimits : [];
3720
+ for (const required of CONCURRENT_REQUIRED_LIMITS) {
3721
+ if (!limits.includes(required)) {
3722
+ findings.push(`attributionLimits is missing the mandatory concurrent disclosure "${required}" — an absent ceiling overclaims`);
3723
+ }
3724
+ }
3725
+ for (const forbidden of CONCURRENT_FORBIDDEN_LIMITS) {
3726
+ if (limits.includes(forbidden)) {
3727
+ findings.push(`attributionLimits carries the forbidden disclosure "${forbidden}" — a concurrent run cannot claim a sequential guarantee`);
3728
+ }
3729
+ }
3730
+ // Phantom/dropped role: laneWindows + outcomes each cover exactly roleCount (actors are
3731
+ // INDEPENDENT — none are blocked by another, so all N produce a window + outcome).
3732
+ if (laneWindows.length !== sw.roleCount) {
3733
+ findings.push(`phantom/dropped role: laneWindows count (${laneWindows.length}) must equal roleCount (${sw.roleCount})`);
3734
+ }
3735
+ if (outcomes.length !== sw.roleCount) {
3736
+ findings.push(`phantom/dropped role: outcomes count (${outcomes.length}) must equal roleCount (${sw.roleCount})`);
3737
+ }
3738
+ // laneWindows: numeric well-ordered windows; sim/stream resolve; route-host digest present.
3739
+ for (const window of laneWindows) {
3740
+ const roleId = typeof window.roleId === "string" ? window.roleId : "(unnamed)";
3741
+ const startedAt = window.startedAt;
3742
+ const endedAt = window.endedAt;
3743
+ if (typeof startedAt !== "number" || typeof endedAt !== "number" || !(startedAt <= endedAt)) {
3744
+ findings.push(`laneWindow "${roleId}" must carry numeric startedAt <= endedAt on one clock`);
3745
+ }
3746
+ if (typeof window.routeHostDigest !== "string" || !COMMAND_DIGEST_PATTERN.test(window.routeHostDigest)) {
3747
+ findings.push(`laneWindow "${roleId}" must record a sha256-16 routeHostDigest of the host it drove`);
3748
+ }
3749
+ if (!bundle.simulations.some((sim) => sim.id === window.simId)) {
3750
+ findings.push(`laneWindow "${roleId}" references unknown simId "${String(window.simId)}"`);
3751
+ }
3752
+ if (!bundle.streams.some((stream) => stream.id === window.streamId)) {
3753
+ findings.push(`laneWindow "${roleId}" references unknown streamId "${String(window.streamId)}"`);
3754
+ }
3755
+ }
3756
+ // FIX-2: the harness-minted getHost target. plane.hostDigest present (sha256-16) + every actor's
3757
+ // routeHostDigest equals it (every actor drove EXACTLY the harness-minted host — invariant 2).
3758
+ const plane = isRecord(sw.plane) ? sw.plane : {};
3759
+ const hostDigest = typeof plane.hostDigest === "string" ? plane.hostDigest : undefined;
3760
+ if (!hostDigest || !COMMAND_DIGEST_PATTERN.test(hostDigest)) {
3761
+ findings.push("sharedWorld.plane.hostDigest (sha256-16 of the harness-minted getHost origin) is required on the concurrent route");
3762
+ }
3763
+ else {
3764
+ for (const window of laneWindows) {
3765
+ const roleId = typeof window.roleId === "string" ? window.roleId : "(unnamed)";
3766
+ if (typeof window.routeHostDigest === "string" && window.routeHostDigest !== hostDigest) {
3767
+ findings.push(`laneWindow "${roleId}" drove a host that differs from the harness-minted plane.hostDigest (invariant 2)`);
3768
+ }
3769
+ }
3770
+ }
3771
+ // FIX-3: synthetic-subject provenance gate (a getHost URL is internet-reachable; real/external
3772
+ // data behind it is the hazard). Author attestation + a seeded provenance check.
3773
+ if (plane.exposure !== "synthetic") {
3774
+ findings.push('sharedWorld.plane.exposure must be "synthetic" — the getHost route requires the author attestation that the subject is synthetic seeded data (author-trust + provenance gate, not a no-real-data guarantee)');
3775
+ }
3776
+ if (bundle.subject?.state.provenance !== "seeded") {
3777
+ findings.push(`the concurrent getHost route requires subject.state.provenance == "seeded" (got "${bundle.subject?.state.provenance ?? "absent"}") — external/unpinned/undeclared data behind an internet-reachable URL is rejected`);
3778
+ }
3779
+ // Single-plane provenance: every laneWindow shares ONE (commit, seedDigest) matching plane.
3780
+ const planeKeys = new Set(laneWindows.map((window) => `${String(window.commit ?? "")}::${String(window.seedDigest ?? "")}`));
3781
+ if (planeKeys.size > 1) {
3782
+ findings.push("laneWindows reference divergent plane provenance (commit/seedDigest) — a concurrent run drives ONE plane");
3783
+ }
3784
+ for (const window of laneWindows) {
3785
+ if (String(window.seedDigest ?? "") !== String(plane.seedDigest ?? "")
3786
+ || String(window.commit ?? "") !== String(plane.commit ?? "")) {
3787
+ const roleId = typeof window.roleId === "string" ? window.roleId : "(unnamed)";
3788
+ findings.push(`laneWindow "${roleId}" plane provenance diverges from sharedWorld.plane`);
3789
+ break;
3790
+ }
3791
+ }
3792
+ // FIX-7: stateSeries is DIGEST-ONLY with the allowed-keys tripwire (no per-delta→actor field).
3793
+ for (const snapshot of stateSeries) {
3794
+ if (typeof snapshot.timestamp !== "number") {
3795
+ findings.push("a stateSeries snapshot must carry a numeric timestamp");
3796
+ }
3797
+ if (typeof snapshot.digest !== "string" || !COMMAND_DIGEST_PATTERN.test(snapshot.digest)) {
3798
+ findings.push("a stateSeries snapshot digest is not a sha256-16 value (a value-shaped field is rejected)");
3799
+ }
3800
+ for (const key of Object.keys(snapshot)) {
3801
+ if (!SHARED_WORLD_STATESERIES_KEYS.has(key)) {
3802
+ findings.push(`a stateSeries snapshot carries an unexpected field "${key}" — the series is digest-only (no per-delta attribution)`);
3803
+ }
3804
+ }
3805
+ }
3806
+ // The concurrency-on-pass gate (FIX-6): a PASSED concurrent run MUST show genuine overlap (≥2
3807
+ // laneWindows overlapping in time) AND a stateSeries delta whose timestamp is AT/AFTER the start
3808
+ // of an overlap interval — otherwise it was not actually concurrent, or the world never changed
3809
+ // under contention (a hollow concurrent claim).
3810
+ if (bundle.review.verdict === "pass") {
3811
+ const overlapStarts = [];
3812
+ for (let i = 0; i < laneWindows.length; i += 1) {
3813
+ for (let j = i + 1; j < laneWindows.length; j += 1) {
3814
+ const a = laneWindows[i];
3815
+ const b = laneWindows[j];
3816
+ const aStart = a.startedAt;
3817
+ const aEnd = a.endedAt;
3818
+ const bStart = b.startedAt;
3819
+ const bEnd = b.endedAt;
3820
+ if (typeof aStart === "number" && typeof aEnd === "number" && typeof bStart === "number" && typeof bEnd === "number"
3821
+ && aStart < bEnd && bStart < aEnd) {
3822
+ overlapStarts.push(Math.max(aStart, bStart));
3823
+ }
3824
+ }
3825
+ }
3826
+ if (overlapStarts.length === 0) {
3827
+ findings.push("review verdict is pass but no two laneWindows overlap in time — the run was not actually concurrent");
3828
+ }
3829
+ else {
3830
+ const earliestOverlapStart = Math.min(...overlapStarts);
3831
+ const sorted = [...stateSeries]
3832
+ .map((snapshot) => ({ timestamp: snapshot.timestamp, digest: String(snapshot.digest) }))
3833
+ .filter((snapshot) => typeof snapshot.timestamp === "number")
3834
+ .sort((x, y) => x.timestamp - y.timestamp);
3835
+ let deltaInWindow = false;
3836
+ for (let i = 1; i < sorted.length; i += 1) {
3837
+ if (sorted[i].digest !== sorted[i - 1].digest && sorted[i].timestamp >= earliestOverlapStart) {
3838
+ deltaInWindow = true;
3839
+ break;
3840
+ }
3841
+ }
3842
+ if (!deltaInWindow) {
3843
+ findings.push("review verdict is pass but no stateSeries delta occurs at/after an overlap interval start — the shared world did not change under concurrent load (hollow concurrent claim)");
3844
+ }
3845
+ }
3846
+ }
3847
+ return findings;
3848
+ }
3849
+ /**
3850
+ * Advisory (never flips ok): a LIVE clone bundle whose subject env is provisioned while its
3851
+ * state story is undeclared probably points at state the lab does not control. Emitted at
3852
+ * most ONCE per bundle (the subject block is bundle-level, never per stream). GITHUB_TOKEN
3853
+ * is mechanically excluded: the harness consumes that name for clone auth — it carries no
3854
+ * state implication.
3855
+ */
3856
+ function undeclaredSubjectStateWarnings(bundle) {
3857
+ const subject = bundle.subject;
3858
+ if (subject === undefined || bundle.mode !== "live" || subject.source !== "clone") {
3859
+ return [];
3860
+ }
3861
+ if (subject.state.provenance !== "undeclared") {
3862
+ return [];
3863
+ }
3864
+ const stateRelevantEnvNames = (subject.envNames ?? []).filter((name) => name !== "GITHUB_TOKEN");
3865
+ if (stateRelevantEnvNames.length === 0) {
3866
+ return [];
3867
+ }
3868
+ return [
3869
+ `Subject env is provisioned (${stateRelevantEnvNames.join(", ")}) but no state story is declared; if any name points at external state, declare subject.state.external (recorded UNPINNED) or seed in-sandbox state with subject.state.seed.`
3870
+ ];
3871
+ }
3872
+ const riskyPublicArtifactPathSegments = new Set([
3873
+ ".git",
3874
+ "Cookies",
3875
+ "Login Data",
3876
+ "Local Storage",
3877
+ "Preferences",
3878
+ "Secure Preferences",
3879
+ "profiles"
3880
+ ]);
3881
+ async function scanRunPublicSafetyArtifacts(runRoot) {
3882
+ const findings = [];
3883
+ await scanRunPublicSafetyDirectory(runRoot, runRoot, findings);
3884
+ return findings;
3885
+ }
3886
+ async function scanRunPublicSafetyDirectory(root, current, findings) {
3887
+ if (findings.length >= 50) {
3888
+ return;
3889
+ }
3890
+ const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
3891
+ for (const entry of entries) {
3892
+ const absolutePath = path.join(current, entry.name);
3893
+ const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
3894
+ if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
3895
+ findings.push(`risky artifact path ${relativePath}`);
3896
+ if (findings.length >= 50)
3897
+ return;
3898
+ }
3899
+ if (entry.isDirectory()) {
3900
+ await scanRunPublicSafetyDirectory(root, absolutePath, findings);
3901
+ if (findings.length >= 50)
3902
+ return;
3903
+ continue;
3904
+ }
3905
+ if (!entry.isFile() || !shouldScanTextArtifact(relativePath)) {
3906
+ continue;
3907
+ }
3908
+ const text = await readFile(absolutePath, "utf8").catch(() => null);
3909
+ if (text !== null && containsSensitivePattern(text)) {
3910
+ findings.push(`sensitive text ${relativePath}`);
3911
+ if (findings.length >= 50)
3912
+ return;
3913
+ }
3914
+ }
3915
+ }
3916
+ function isRiskyPublicArtifactPath(relativePath) {
3917
+ return relativePath.split(/[\\/]/).some((segment) => riskyPublicArtifactPathSegments.has(segment));
3918
+ }
3919
+ function shouldScanTextArtifact(relativePath) {
3920
+ const extension = path.extname(relativePath).toLowerCase();
3921
+ return ![".png", ".jpg", ".jpeg", ".webp", ".gif", ".tgz", ".gz", ".zip"].includes(extension);
3922
+ }
3923
+ function isLocalEvidenceArtifactPath(value) {
3924
+ const normalized = value.replace(/\\/g, "/");
3925
+ return value.length > 0
3926
+ && !/^\[[a-z0-9._-]+\]$/i.test(normalized)
3927
+ && !path.isAbsolute(normalized)
3928
+ && !normalized.includes("://")
3929
+ && !normalized.startsWith("..")
3930
+ && !normalized.split("/").includes("..")
3931
+ && !isRiskyPublicArtifactPath(normalized);
3932
+ }
3933
+ function normalizeLocalEvidenceReference(value) {
3934
+ if (!value || value.includes("://") || path.isAbsolute(value)) {
3935
+ return null;
3936
+ }
3937
+ const normalized = value.replace(/\\/g, "/");
3938
+ if (normalized.startsWith("../")) {
3939
+ return isLocalEvidenceArtifactPath(normalized.slice(3)) ? normalized.slice(3) : null;
3940
+ }
3941
+ return isLocalEvidenceArtifactPath(normalized) ? normalized : null;
3942
+ }
3943
+ async function validateCwd(cwd) {
3944
+ try {
3945
+ const stats = await stat(cwd);
3946
+ if (!stats.isDirectory()) {
3947
+ return {
3948
+ code: "HUMANISH_INVALID_CWD",
3949
+ message: `Target cwd is not a directory: ${cwd}`
3950
+ };
3951
+ }
3952
+ return null;
3953
+ }
3954
+ catch (error) {
3955
+ if (isNodeError(error) && error.code === "ENOENT") {
3956
+ return {
3957
+ code: "HUMANISH_INVALID_CWD",
3958
+ message: `Target cwd does not exist: ${cwd}`
3959
+ };
3960
+ }
3961
+ throw error;
3962
+ }
3963
+ }
3964
+ async function directoryExists(directoryPath) {
3965
+ try {
3966
+ return (await stat(directoryPath)).isDirectory();
3967
+ }
3968
+ catch (error) {
3969
+ if (isNodeError(error) && error.code === "ENOENT") {
3970
+ return false;
3971
+ }
3972
+ throw error;
3973
+ }
3974
+ }
3975
+ async function fileExists(filePath) {
3976
+ try {
3977
+ return (await stat(filePath)).isFile();
3978
+ }
3979
+ catch (error) {
3980
+ if (isNodeError(error) && error.code === "ENOENT") {
3981
+ return false;
3982
+ }
3983
+ throw error;
3984
+ }
3985
+ }
3986
+ function containsSensitivePattern(text) {
3987
+ return containsSensitive(text);
3988
+ }
3989
+ function redactSensitiveText(text) {
3990
+ return redactToSecretLabel(text);
3991
+ }
3992
+ function isRunBundle(value) {
3993
+ return isRecord(value)
3994
+ && value.schema === RUN_BUNDLE_SCHEMA
3995
+ && typeof value.runId === "string"
3996
+ && (value.mode === "dry-run" || value.mode === "live")
3997
+ && isPositiveSafeInteger(value.simCount)
3998
+ && typeof value.createdAt === "string"
3999
+ && value.cwd === PUBLIC_TARGET_CWD
4000
+ && typeof value.artifactRoot === "string"
4001
+ && isRunSource(value.source)
4002
+ && isPersonaSummary(value.persona)
4003
+ && isScenarioSummary(value.scenario)
4004
+ && Array.isArray(value.lifecycle)
4005
+ && value.lifecycle.every(isLifecycleEvent)
4006
+ && Array.isArray(value.simulations)
4007
+ && value.simulations.length === value.simCount
4008
+ && value.simulations.every(isRunSimulation)
4009
+ && Array.isArray(value.streams)
4010
+ && value.streams.every(isRunStream)
4011
+ && hasConsistentSimulationStreams(value.simulations, value.streams)
4012
+ && Array.isArray(value.events)
4013
+ && value.events.every(isRunEvent)
4014
+ && isRunArtifactIndex(value.artifacts)
4015
+ && isRecord(value.review)
4016
+ && isReviewSummary(value.review)
4017
+ && isRecord(value.redaction)
4018
+ && value.redaction.status === "passed"
4019
+ && Array.isArray(value.feedbackCandidates)
4020
+ && value.feedbackCandidates.every(isRunFeedbackCandidate)
4021
+ // Optional and additive: pre-existing bundles (and non-cua backends) carry no subject
4022
+ // block; when present it must be well-shaped (semantics are the verify check's job).
4023
+ && (value.subject === undefined || isRunSubjectProvenance(value.subject))
4024
+ && (value.desktopBrowser === undefined || isDesktopBrowserEvidence(value.desktopBrowser))
4025
+ && (value.rerun === undefined || isRunRerunLineage(value.rerun))
4026
+ // Optional + additive shared-world fields (#164). Tolerant SHAPE guard only — the interaction
4027
+ // semantics (timeline well-formedness, single-plane, delta-on-pass) are the verify check's job.
4028
+ && (value.attributionClass === undefined || value.attributionClass === "isolated" || value.attributionClass === "shared-world")
4029
+ && (value.sharedWorld === undefined || isSharedWorldEvidence(value.sharedWorld))
4030
+ // Optional, adapter-namespaced product score (the extension seam). When present, validate only
4031
+ // its SHAPE; core never reads the adapter's `data` payload.
4032
+ && (value.adapterScore === undefined || isRunAdapterScore(value.adapterScore))
4033
+ && (value.adapterArtifacts === undefined
4034
+ || (Array.isArray(value.adapterArtifacts) && value.adapterArtifacts.every(isRunAdapterArtifact)))
4035
+ && (value.providerResources === undefined
4036
+ || (Array.isArray(value.providerResources) && value.providerResources.every(isRunProviderResource)));
4037
+ }
4038
+ function isRunProviderResource(value) {
4039
+ return isRecord(value)
4040
+ && value.schema === "humanish.provider-resource.v1"
4041
+ && value.provider === "e2b-desktop"
4042
+ && value.kind === "sandbox"
4043
+ && typeof value.id === "string"
4044
+ && value.id.trim().length > 0
4045
+ && value.owner === "humanish"
4046
+ && (value.status === "running" || value.status === "killed" || value.status === "unknown")
4047
+ && (value.simId === undefined || typeof value.simId === "string")
4048
+ && (value.streamId === undefined || typeof value.streamId === "string")
4049
+ && (value.laneId === undefined || typeof value.laneId === "string")
4050
+ && (value.createdAt === undefined || typeof value.createdAt === "string")
4051
+ && (value.cleanup === undefined
4052
+ || (isRecord(value.cleanup)
4053
+ && typeof value.cleanup.killed === "boolean"
4054
+ && typeof value.cleanup.reason === "string"));
4055
+ }
4056
+ function isCleanupResult(value) {
4057
+ return isRecord(value)
4058
+ && value.schema === CLEANUP_SCHEMA
4059
+ && typeof value.ok === "boolean"
4060
+ && typeof value.cwd === "string"
4061
+ && typeof value.run === "string"
4062
+ && typeof value.checkedAt === "string"
4063
+ && (value.runId === undefined || typeof value.runId === "string")
4064
+ && (value.bundlePath === undefined || typeof value.bundlePath === "string")
4065
+ && (value.cleanupPath === undefined || typeof value.cleanupPath === "string")
4066
+ && isRecord(value.summary)
4067
+ && isNonNegativeSafeInteger(value.summary.resources)
4068
+ && isNonNegativeSafeInteger(value.summary.killed)
4069
+ && isNonNegativeSafeInteger(value.summary.alreadyClean)
4070
+ && isNonNegativeSafeInteger(value.summary.failed)
4071
+ && isNonNegativeSafeInteger(value.summary.skipped)
4072
+ && Array.isArray(value.resources)
4073
+ && value.resources.every(isCleanupResourceResult)
4074
+ && Array.isArray(value.adapterResults)
4075
+ && value.adapterResults.every(isCleanupAdapterResult)
4076
+ && Array.isArray(value.warnings)
4077
+ && value.warnings.every((warning) => typeof warning === "string");
4078
+ }
4079
+ function isCleanupResourceResult(value) {
4080
+ return isRecord(value)
4081
+ && value.provider === "e2b-desktop"
4082
+ && value.kind === "sandbox"
4083
+ && typeof value.id === "string"
4084
+ && value.id.trim().length > 0
4085
+ && (value.status === "killed" || value.status === "already_clean" || value.status === "failed" || value.status === "skipped")
4086
+ && typeof value.message === "string";
4087
+ }
4088
+ function isCleanupAdapterResult(value) {
4089
+ return isRecord(value)
4090
+ && typeof value.id === "string"
4091
+ && value.id.trim().length > 0
4092
+ && typeof value.ok === "boolean"
4093
+ && typeof value.message === "string";
4094
+ }
4095
+ function isRunRerunLineage(value) {
4096
+ return isRecord(value)
4097
+ && typeof value.sourceRunId === "string"
4098
+ && value.sourceRunId.trim().length > 0
4099
+ && Array.isArray(value.selectedLaneIds)
4100
+ && value.selectedLaneIds.length > 0
4101
+ && value.selectedLaneIds.every((laneId) => typeof laneId === "string" && laneId.trim().length > 0)
4102
+ && Array.isArray(value.previous)
4103
+ && value.previous.length > 0
4104
+ && value.previous.every((entry) => isRecord(entry)
4105
+ && typeof entry.laneId === "string"
4106
+ && entry.laneId.trim().length > 0
4107
+ && (entry.streamId === undefined || typeof entry.streamId === "string")
4108
+ && typeof entry.status === "string"
4109
+ && entry.status.trim().length > 0
4110
+ && (entry.reason === undefined || typeof entry.reason === "string")
4111
+ && (entry.actorStatus === undefined || typeof entry.actorStatus === "string")
4112
+ && (entry.completionReason === undefined || typeof entry.completionReason === "string"));
4113
+ }
4114
+ function isDesktopBrowserEvidence(value) {
4115
+ return isRecord(value)
4116
+ && (value.requested === "default" || value.requested === "chrome" || value.requested === "chromium" || value.requested === "firefox")
4117
+ && (value.resolved === undefined || typeof value.resolved === "string");
4118
+ }
4119
+ function isRunAdapterScore(value) {
4120
+ return isRecord(value)
4121
+ && value.schema === "humanish.adapter-score.v1"
4122
+ && typeof value.namespace === "string"
4123
+ && value.namespace.trim().length > 0
4124
+ && (value.status === "pass" || value.status === "partial" || value.status === "fail")
4125
+ && typeof value.score === "number"
4126
+ && Number.isFinite(value.score)
4127
+ && typeof value.summary === "string"
4128
+ && (value.data === undefined || isRecord(value.data));
4129
+ }
4130
+ /**
4131
+ * Tolerant SHAPE guard for the shared-world evidence block (#164). Validates required fields +
4132
+ * types but TOLERATES extra keys (additive): the strict value-shape/timeline checks are
4133
+ * sharedWorldEvidenceFindings' job (an injected value-shaped checkpoint field must pass the shape
4134
+ * guard so verify can catch it fail-closed, not silently bounce off isRunBundle).
4135
+ */
4136
+ function isSharedWorldEvidence(value) {
4137
+ if (!isRecord(value))
4138
+ return false;
4139
+ if (value.schema !== SHARED_WORLD_SCHEMA)
4140
+ return false;
4141
+ if (value.topology !== "shared-world")
4142
+ return false;
4143
+ if (!isNonNegativeSafeInteger(value.roleCount))
4144
+ return false;
4145
+ const plane = value.plane;
4146
+ if (!isRecord(plane))
4147
+ return false;
4148
+ if (plane.commit !== undefined && typeof plane.commit !== "string")
4149
+ return false;
4150
+ if (typeof plane.seedDigest !== "string")
4151
+ return false;
4152
+ if (!(Array.isArray(plane.envNames) && plane.envNames.every((name) => typeof name === "string")))
4153
+ return false;
4154
+ if (plane.hostDigest !== undefined && typeof plane.hostDigest !== "string")
4155
+ return false;
4156
+ if (plane.exposure !== undefined && typeof plane.exposure !== "string")
4157
+ return false;
4158
+ if (!(Array.isArray(value.attributionLimits) && value.attributionLimits.every((limit) => typeof limit === "string")))
4159
+ return false;
4160
+ // Tolerant: validate the TYPE of each present field only (the coherence + topologyMode dispatch
4161
+ // are validateSharedWorldEvidence's job — an injected value-shaped field must pass this guard so
4162
+ // verify catches it fail-closed). A bundle must carry at least one of the two shapes.
4163
+ if (value.sequence !== undefined && !(Array.isArray(value.sequence) && value.sequence.every((id) => typeof id === "string")))
4164
+ return false;
4165
+ if (value.timeline !== undefined && !(Array.isArray(value.timeline) && value.timeline.every(isSharedWorldTimelineEntry)))
4166
+ return false;
4167
+ if (value.laneWindows !== undefined && !(Array.isArray(value.laneWindows) && value.laneWindows.every(isSharedWorldLaneWindow)))
4168
+ return false;
4169
+ if (value.stateSeries !== undefined && !(Array.isArray(value.stateSeries) && value.stateSeries.every(isSharedWorldStateSnapshot)))
4170
+ return false;
4171
+ if (value.outcomes !== undefined && !(Array.isArray(value.outcomes) && value.outcomes.every(isSharedWorldOutcome)))
4172
+ return false;
4173
+ if (value.timeline === undefined && value.laneWindows === undefined)
4174
+ return false;
4175
+ return true;
4176
+ }
4177
+ function isSharedWorldTimelineEntry(value) {
4178
+ if (!isRecord(value))
4179
+ return false;
4180
+ if (value.kind === "checkpoint") {
4181
+ return typeof value.name === "string"
4182
+ && typeof value.digest === "string"
4183
+ && typeof value.deltaFromPrev === "boolean";
4184
+ }
4185
+ if (value.kind === "turn") {
4186
+ return typeof value.roleId === "string"
4187
+ && typeof value.simId === "string"
4188
+ && typeof value.streamId === "string"
4189
+ && typeof value.seedDigest === "string"
4190
+ && (value.commit === undefined || typeof value.commit === "string");
4191
+ }
4192
+ return false;
4193
+ }
4194
+ // Tolerant shape guards for the CONCURRENT series (extra keys tolerated — the digest-only /
4195
+ // allowed-keys tripwires are validateSharedWorldEvidence's strict job).
4196
+ function isSharedWorldLaneWindow(value) {
4197
+ return isRecord(value)
4198
+ && typeof value.roleId === "string"
4199
+ && (value.actorType === undefined || typeof value.actorType === "string")
4200
+ && (value.surface === undefined || typeof value.surface === "string")
4201
+ && (value.caseGroup === undefined || typeof value.caseGroup === "string")
4202
+ && typeof value.simId === "string"
4203
+ && typeof value.streamId === "string"
4204
+ && typeof value.startedAt === "number"
4205
+ && typeof value.endedAt === "number"
4206
+ && typeof value.verdict === "string"
4207
+ && typeof value.routeHostDigest === "string"
4208
+ && typeof value.seedDigest === "string"
4209
+ && (value.commit === undefined || typeof value.commit === "string");
4210
+ }
4211
+ function isSharedWorldStateSnapshot(value) {
4212
+ return isRecord(value) && typeof value.timestamp === "number" && typeof value.digest === "string";
4213
+ }
4214
+ function isSharedWorldOutcome(value) {
4215
+ return isRecord(value)
4216
+ && typeof value.roleId === "string"
4217
+ && (value.actorType === undefined || typeof value.actorType === "string")
4218
+ && (value.surface === undefined || typeof value.surface === "string")
4219
+ && (value.caseGroup === undefined || typeof value.caseGroup === "string")
4220
+ && typeof value.simId === "string"
4221
+ && typeof value.streamId === "string"
4222
+ && typeof value.status === "string"
4223
+ && typeof value.ok === "boolean";
4224
+ }
4225
+ // The local-tree archive content pin: sha256 hex, full 64 chars (NOT the repo's 16-char
4226
+ // display-digest convention -- this value is the provenance pin itself, persisted in full).
4227
+ const ARCHIVE_SHA256_PATTERN = /^[a-f0-9]{64}$/;
4228
+ function isRunSubjectProvenance(value) {
4229
+ if (!isRecord(value))
4230
+ return false;
4231
+ if (value.source !== "clone" && value.source !== "app-url" && value.source !== "local-tree")
4232
+ return false;
4233
+ if (value.repo !== undefined && typeof value.repo !== "string")
4234
+ return false;
4235
+ if (value.commit !== undefined && typeof value.commit !== "string")
4236
+ return false;
4237
+ if (value.archiveSha256 !== undefined
4238
+ && (typeof value.archiveSha256 !== "string" || !ARCHIVE_SHA256_PATTERN.test(value.archiveSha256))) {
4239
+ return false;
4240
+ }
4241
+ if (value.dirty !== undefined && typeof value.dirty !== "boolean")
4242
+ return false;
4243
+ if (value.envNames !== undefined
4244
+ && !(Array.isArray(value.envNames) && value.envNames.every((name) => typeof name === "string"))) {
4245
+ return false;
4246
+ }
4247
+ const state = value.state;
4248
+ if (!isRecord(state))
4249
+ return false;
4250
+ if (state.provenance !== "seeded" && state.provenance !== "unpinned"
4251
+ && state.provenance !== "declared-not-run" && state.provenance !== "undeclared") {
4252
+ return false;
4253
+ }
4254
+ if (state.seed !== undefined
4255
+ && !(Array.isArray(state.seed) && state.seed.every(isRunSubjectStateStepRecord))) {
4256
+ return false;
4257
+ }
4258
+ if (state.externalEnvNames !== undefined
4259
+ && !(Array.isArray(state.externalEnvNames) && state.externalEnvNames.every((name) => typeof name === "string"))) {
4260
+ return false;
4261
+ }
4262
+ return true;
4263
+ }
4264
+ function isRunSubjectStateStepRecord(value) {
4265
+ return isRecord(value)
4266
+ && typeof value.name === "string"
4267
+ && (value.when === "before-build" || value.when === "before-start" || value.when === "after-ready")
4268
+ && typeof value.commandDigest === "string"
4269
+ && (value.ok === undefined || typeof value.ok === "boolean")
4270
+ && (value.exitCode === undefined || typeof value.exitCode === "number")
4271
+ && (value.timedOut === undefined || typeof value.timedOut === "boolean")
4272
+ && (value.durationMs === undefined || typeof value.durationMs === "number");
4273
+ }
4274
+ function isRunSource(value) {
4275
+ return isRecord(value)
4276
+ && (typeof value.packageName === "string" || value.packageName === null)
4277
+ && (value.humanishSource === "present" || value.humanishSource === "missing")
4278
+ && isCapturedGitState(value.git);
4279
+ }
4280
+ function isCapturedGitState(value) {
4281
+ return isRecord(value)
4282
+ && value.schema === GIT_STATE_SCHEMA
4283
+ && (value.status === "clean" || value.status === "dirty" || value.status === "missing" || value.status === "unavailable")
4284
+ && typeof value.capturedAt === "string"
4285
+ && isRecord(value.head)
4286
+ && (isSafeGitShortSha(value.head.shortSha) || value.head.shortSha === null)
4287
+ && (value.head.refState === "attached" || value.head.refState === "detached" || value.head.refState === "unborn" || value.head.refState === "unknown")
4288
+ && isRecord(value.changes)
4289
+ && isNonNegativeSafeInteger(value.changes.staged)
4290
+ && isNonNegativeSafeInteger(value.changes.unstaged)
4291
+ && isNonNegativeSafeInteger(value.changes.untracked)
4292
+ && isNonNegativeSafeInteger(value.changes.total)
4293
+ && typeof value.note === "string"
4294
+ && SAFE_GIT_NOTES.has(value.note);
4295
+ }
4296
+ function isPersonaSummary(value) {
4297
+ return isRecord(value)
4298
+ && typeof value.id === "string"
4299
+ && typeof value.name === "string"
4300
+ && typeof value.source === "string"
4301
+ && typeof value.sourceDigest === "string";
4302
+ }
4303
+ function isScenarioSummary(value) {
4304
+ return isRecord(value)
4305
+ && typeof value.id === "string"
4306
+ && typeof value.title === "string"
4307
+ && typeof value.goal === "string"
4308
+ && typeof value.source === "string"
4309
+ && typeof value.sourceDigest === "string";
4310
+ }
4311
+ function isLifecycleEvent(value) {
4312
+ return isRecord(value)
4313
+ && typeof value.at === "string"
4314
+ && typeof value.event === "string"
4315
+ && typeof value.message === "string";
4316
+ }
4317
+ function isRunSimulation(value) {
4318
+ return isRecord(value)
4319
+ && typeof value.id === "string"
4320
+ && isPositiveSafeInteger(value.index)
4321
+ && typeof value.personaId === "string"
4322
+ && typeof value.scenarioId === "string"
4323
+ && isRunSimulationStatus(value.status)
4324
+ && isRunStreamKind(value.streamKind)
4325
+ && (value.mode === "browser-sim" || value.mode === "cli-sim" || value.mode === "tui-sim" || value.mode === "codex-app-sim")
4326
+ && typeof value.progress === "number"
4327
+ && typeof value.currentStep === "string"
4328
+ && typeof value.summary === "string"
4329
+ && Array.isArray(value.streamIds)
4330
+ && value.streamIds.every((streamId) => typeof streamId === "string")
4331
+ && typeof value.startedAt === "string"
4332
+ && typeof value.updatedAt === "string";
4333
+ }
4334
+ function isRunStream(value) {
4335
+ return isRecord(value)
4336
+ && typeof value.id === "string"
4337
+ && typeof value.simId === "string"
4338
+ && isRunStreamKind(value.kind)
4339
+ && typeof value.label === "string"
4340
+ && isRunSimulationStatus(value.status)
4341
+ && (value.transport === "snapshot" || value.transport === "polling" || value.transport === "sse" || value.transport === "pty" || value.transport === "app-server")
4342
+ && typeof value.updatedAt === "string"
4343
+ && Array.isArray(value.artifacts)
4344
+ && value.artifacts.every(isRunStreamArtifact);
4345
+ }
4346
+ function isRunStreamArtifact(value) {
4347
+ return isRecord(value)
4348
+ && typeof value.label === "string"
4349
+ && typeof value.path === "string"
4350
+ && (value.kind === "bundle"
4351
+ || value.kind === "review"
4352
+ || value.kind === "observer"
4353
+ || value.kind === "events"
4354
+ || value.kind === "screenshot"
4355
+ || value.kind === "trace"
4356
+ || value.kind === "log"
4357
+ || value.kind === "filesystem");
4358
+ }
4359
+ function isRunAdapterArtifact(value) {
4360
+ return isRecord(value)
4361
+ && value.schema === "humanish.adapter-artifact.v1"
4362
+ && typeof value.namespace === "string"
4363
+ && value.namespace.trim().length > 0
4364
+ && typeof value.label === "string"
4365
+ && value.label.trim().length > 0
4366
+ && typeof value.path === "string"
4367
+ && value.path.trim().length > 0
4368
+ && isLocalEvidenceArtifactPath(value.path)
4369
+ && (value.kind === "state"
4370
+ || value.kind === "review"
4371
+ || value.kind === "log"
4372
+ || value.kind === "trace"
4373
+ || value.kind === "screenshot"
4374
+ || value.kind === "filesystem"
4375
+ || value.kind === "summary")
4376
+ && typeof value.note === "string"
4377
+ && value.note.trim().length > 0;
4378
+ }
4379
+ function isRunEvent(value) {
4380
+ return isRecord(value)
4381
+ && typeof value.id === "string"
4382
+ && typeof value.at === "string"
4383
+ && (value.level === "info" || value.level === "warn" || value.level === "error")
4384
+ && typeof value.type === "string"
4385
+ && typeof value.message === "string";
4386
+ }
4387
+ function isRunArtifactIndex(value) {
4388
+ return isRecord(value)
4389
+ && typeof value.run === "string"
4390
+ && typeof value.reviewJson === "string"
4391
+ && typeof value.reviewMarkdown === "string"
4392
+ && typeof value.observerData === "string"
4393
+ && typeof value.events === "string";
4394
+ }
4395
+ function isRunFeedbackCandidate(value) {
4396
+ return isRecord(value)
4397
+ && value.schema === "humanish.feedback-candidate.v1"
4398
+ && typeof value.id === "string"
4399
+ && typeof value.run_id === "string"
4400
+ && (typeof value.stream_id === "string" || value.stream_id === undefined)
4401
+ && typeof value.adapter_id === "string"
4402
+ && typeof value.scenario_id === "string"
4403
+ && typeof value.persona_id === "string"
4404
+ && isFeedbackActor(value.actor)
4405
+ && isFeedbackSubstrate(value.substrate)
4406
+ && isFeedbackFailureOwner(value.failure_owner)
4407
+ && typeof value.summary === "string"
4408
+ && typeof value.expected === "string"
4409
+ && typeof value.actual === "string"
4410
+ && Array.isArray(value.evidence)
4411
+ && value.evidence.every(isRunFeedbackEvidence)
4412
+ && isRecord(value.redaction)
4413
+ && value.redaction.status === "passed"
4414
+ && typeof value.redaction.notes === "string"
4415
+ && typeof value.idempotency_key === "string"
4416
+ && isFeedbackNextState(value.proposed_next_state)
4417
+ && Array.isArray(value.acceptance_proof)
4418
+ && value.acceptance_proof.every((item) => typeof item === "string")
4419
+ // Optional, adapter-namespaced product-noun block: when present, validate only its SHAPE
4420
+ // (a non-empty namespace + a data record). Core never inspects the keys inside `data`.
4421
+ && (value.adapter === undefined || isFeedbackAdapterBlock(value.adapter));
4422
+ }
4423
+ function isFeedbackAdapterBlock(value) {
4424
+ return isRecord(value)
4425
+ && typeof value.namespace === "string"
4426
+ && value.namespace.trim().length > 0
4427
+ && isRecord(value.data);
4428
+ }
4429
+ function isRunFeedbackEvidence(value) {
4430
+ return isRecord(value)
4431
+ && typeof value.path === "string"
4432
+ && value.path.length > 0
4433
+ && !path.isAbsolute(value.path)
4434
+ && !value.path.includes("://")
4435
+ && !value.path.includes("..")
4436
+ && (value.kind === "review"
4437
+ || value.kind === "state"
4438
+ || value.kind === "log"
4439
+ || value.kind === "trace"
4440
+ || value.kind === "screenshot"
4441
+ || value.kind === "filesystem")
4442
+ && typeof value.note === "string";
4443
+ }
4444
+ function hasConsistentSimulationStreams(simulations, streams) {
4445
+ const simIds = new Set();
4446
+ const expectedStreamSimIds = new Map();
4447
+ const streamById = new Map();
4448
+ for (const simulation of simulations) {
4449
+ if (!isRunSimulation(simulation)) {
4450
+ return false;
4451
+ }
4452
+ simIds.add(simulation.id);
4453
+ for (const streamId of simulation.streamIds) {
4454
+ if (expectedStreamSimIds.has(streamId)) {
4455
+ return false;
4456
+ }
4457
+ expectedStreamSimIds.set(streamId, simulation.id);
4458
+ }
4459
+ }
4460
+ for (const stream of streams) {
4461
+ if (!isRunStream(stream) || !simIds.has(stream.simId) || streamById.has(stream.id)) {
4462
+ return false;
4463
+ }
4464
+ const expectedSimId = expectedStreamSimIds.get(stream.id);
4465
+ if (expectedSimId === undefined || stream.simId !== expectedSimId) {
4466
+ return false;
4467
+ }
4468
+ streamById.set(stream.id, stream);
4469
+ }
4470
+ return streamById.size === expectedStreamSimIds.size;
4471
+ }
4472
+ function isRunSimulationStatus(value) {
4473
+ return value === "queued"
4474
+ || value === "preparing"
4475
+ || value === "running"
4476
+ || value === "passed"
4477
+ || value === "complete"
4478
+ || value === "blocked"
4479
+ || value === "timed_out"
4480
+ || value === "failed"
4481
+ || value === "contract_proof_only";
4482
+ }
4483
+ function isRunStreamKind(value) {
4484
+ return value === "ui"
4485
+ || value === "browser"
4486
+ || value === "terminal"
4487
+ || value === "tui"
4488
+ || value === "codex-ui"
4489
+ || value === "artifact"
4490
+ || value === "summary";
4491
+ }
4492
+ function isSafeGitShortSha(value) {
4493
+ return typeof value === "string" && /^[a-f0-9]{7,12}$/.test(value);
4494
+ }
4495
+ function isFeedbackActor(value) {
4496
+ return value === "codex-tui"
4497
+ || value === "codex-exec"
4498
+ || value === "codex-app-server"
4499
+ || value === "synthetic-dry-run"
4500
+ || value === "unknown";
4501
+ }
4502
+ function isFeedbackSubstrate(value) {
4503
+ return value === "e2b-desktop"
4504
+ || value === "e2b-terminal"
4505
+ || value === "local-filesystem"
4506
+ || value === "codex-app-server"
4507
+ || value === "unknown";
4508
+ }
4509
+ function isFeedbackFailureOwner(value) {
4510
+ return value === "harness"
4511
+ || value === "target-app"
4512
+ || value === "actor"
4513
+ || value === "environment"
4514
+ || value === "unknown";
4515
+ }
4516
+ function isFeedbackNextState(value) {
4517
+ return value === "watch"
4518
+ || value === "adapter-hardening"
4519
+ || value === "target-app-setup"
4520
+ || value === "actor-auth"
4521
+ || value === "setup-quality-review"
4522
+ || value === "study-quality-review";
4523
+ }
4524
+ function isPositiveSafeInteger(value) {
4525
+ return Number.isSafeInteger(value) && typeof value === "number" && value >= 1;
4526
+ }
4527
+ function isNonNegativeSafeInteger(value) {
4528
+ return Number.isSafeInteger(value) && typeof value === "number" && value >= 0;
4529
+ }
4530
+ function isReviewSummary(value) {
4531
+ return isRecord(value)
4532
+ && value.schema === REVIEW_SCHEMA
4533
+ && (value.verdict === "contract_proof_only"
4534
+ || value.verdict === "pass"
4535
+ || value.verdict === "fail"
4536
+ || value.verdict === "blocked"
4537
+ || value.verdict === "timed_out")
4538
+ && typeof value.summary === "string"
4539
+ && Array.isArray(value.gaps)
4540
+ && value.gaps.every((gap) => typeof gap === "string");
4541
+ }
4542
+ function isRunPointer(value) {
4543
+ return isRecord(value)
4544
+ && value.schema === "humanish.latest-run.v1"
4545
+ && typeof value.runId === "string"
4546
+ && typeof value.path === "string"
4547
+ && typeof value.updatedAt === "string";
4548
+ }
4549
+ function isRecord(value) {
4550
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4551
+ }
4552
+ function isNodeError(error) {
4553
+ return error instanceof Error && "code" in error;
4554
+ }
4555
+ //# sourceMappingURL=run.js.map