humanish 0.0.1 → 0.15.1

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