humanish 0.15.0 → 0.15.2

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 (72) hide show
  1. package/README.md +33 -31
  2. package/dist/claude-agent-sdk.js +14 -7
  3. package/dist/claude-agent-sdk.js.map +1 -1
  4. package/dist/codex-app-server-ui.d.ts +1 -1
  5. package/dist/codex-app-server-ui.js +69 -39
  6. package/dist/codex-app-server-ui.js.map +1 -1
  7. package/dist/codex-app-server.d.ts +3 -0
  8. package/dist/codex-app-server.js +15 -8
  9. package/dist/codex-app-server.js.map +1 -1
  10. package/dist/concurrent-shared-world-lab.js +18 -16
  11. package/dist/concurrent-shared-world-lab.js.map +1 -1
  12. package/dist/core/git-state.d.ts +2 -0
  13. package/dist/core/git-state.js +152 -17
  14. package/dist/core/git-state.js.map +1 -1
  15. package/dist/core/git-workspace.d.ts +31 -0
  16. package/dist/core/git-workspace.js +447 -0
  17. package/dist/core/git-workspace.js.map +1 -0
  18. package/dist/cua-actor-lab.d.ts +3 -2
  19. package/dist/cua-actor-lab.js +34 -23
  20. package/dist/cua-actor-lab.js.map +1 -1
  21. package/dist/e2b-terminal-lab.js +111 -46
  22. package/dist/e2b-terminal-lab.js.map +1 -1
  23. package/dist/feedback.js +139 -101
  24. package/dist/feedback.js.map +1 -1
  25. package/dist/init.d.ts +1 -1
  26. package/dist/init.js +108 -22
  27. package/dist/init.js.map +1 -1
  28. package/dist/lab-config.d.ts +5 -0
  29. package/dist/lab-config.js +38 -0
  30. package/dist/lab-config.js.map +1 -1
  31. package/dist/labs.js +258 -52
  32. package/dist/labs.js.map +1 -1
  33. package/dist/observer-assets.js +3 -2
  34. package/dist/observer-assets.js.map +1 -1
  35. package/dist/observer-static.js +131 -17
  36. package/dist/observer-static.js.map +1 -1
  37. package/dist/observer.js +263 -66
  38. package/dist/observer.js.map +1 -1
  39. package/dist/openai-responses-cu.js +18 -4
  40. package/dist/openai-responses-cu.js.map +1 -1
  41. package/dist/oss-lab.js +59 -14
  42. package/dist/oss-lab.js.map +1 -1
  43. package/dist/oss-meta-lab.d.ts +2 -4
  44. package/dist/oss-meta-lab.js +117 -477
  45. package/dist/oss-meta-lab.js.map +1 -1
  46. package/dist/program.js +23 -21
  47. package/dist/program.js.map +1 -1
  48. package/dist/run-paths.d.ts +56 -0
  49. package/dist/run-paths.js +340 -0
  50. package/dist/run-paths.js.map +1 -0
  51. package/dist/run.d.ts +14 -4
  52. package/dist/run.js +595 -306
  53. package/dist/run.js.map +1 -1
  54. package/dist/scripted-browser-actor.d.ts +7 -4
  55. package/dist/scripted-browser-actor.js +154 -41
  56. package/dist/scripted-browser-actor.js.map +1 -1
  57. package/dist/scripted-browser-lab.js +95 -45
  58. package/dist/scripted-browser-lab.js.map +1 -1
  59. package/dist/selected-output-paths.d.ts +49 -0
  60. package/dist/selected-output-paths.js +354 -0
  61. package/dist/selected-output-paths.js.map +1 -0
  62. package/dist/shared-world-lab.js +13 -12
  63. package/dist/shared-world-lab.js.map +1 -1
  64. package/dist/source-archive.js +44 -1
  65. package/dist/source-archive.js.map +1 -1
  66. package/docs/architecture/oss-lab-poc.md +8 -0
  67. package/docs/assets/humanish-observer-hero.png +0 -0
  68. package/docs/contracts/policy.md +7 -1
  69. package/docs/goals/current.md +20 -1
  70. package/docs/product/open-source-install-experience.md +9 -1
  71. package/docs/ramp/README.md +14 -7
  72. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { type ActorCompletionReason, type ActorPersonaRef, type ActorStatus, type ActorTrace } from "./actor-contract.js";
2
+ import { type PreparedOutputDirectory } from "./selected-output-paths.js";
2
3
  export interface ScriptedLocatorLike {
3
4
  first(): ScriptedLocatorLike;
4
5
  fill(value: string, options?: {
@@ -137,7 +138,7 @@ export interface BrowserPersonaJourney {
137
138
  }
138
139
  export declare const browserSurfaces: BrowserSurface[];
139
140
  export declare function captureBrowserSurface(args: {
140
- absoluteArtifactRoot: string;
141
+ absoluteArtifactRoot: PreparedOutputDirectory;
141
142
  appUrl: string;
142
143
  browserCommand: string;
143
144
  browserJourney: BrowserPersonaJourney;
@@ -145,7 +146,7 @@ export declare function captureBrowserSurface(args: {
145
146
  timeoutMs: number;
146
147
  }): Promise<BrowserSurfaceCapture>;
147
148
  export declare function captureBrowserSurfaceFixture(args: {
148
- absoluteArtifactRoot: string;
149
+ absoluteArtifactRoot: PreparedOutputDirectory;
149
150
  appUrl: string;
150
151
  browserCommand: string;
151
152
  browserJourney: BrowserPersonaJourney;
@@ -153,7 +154,7 @@ export declare function captureBrowserSurfaceFixture(args: {
153
154
  timeoutMs: number;
154
155
  }): Promise<BrowserSurfaceCapture>;
155
156
  export declare function captureBrowserSurfaceWithPlaywright(args: {
156
- absoluteArtifactRoot: string;
157
+ absoluteArtifactRoot: PreparedOutputDirectory;
157
158
  appUrl: string;
158
159
  browserCommand: string;
159
160
  browserJourney: BrowserPersonaJourney;
@@ -161,7 +162,7 @@ export declare function captureBrowserSurfaceWithPlaywright(args: {
161
162
  timeoutMs: number;
162
163
  }): Promise<BrowserSurfaceCapture>;
163
164
  export declare function executeBrowserPersonaStep(args: {
164
- absoluteArtifactRoot: string;
165
+ absoluteArtifactRoot: PreparedOutputDirectory;
165
166
  appUrl: string;
166
167
  browserJourney: BrowserPersonaJourney;
167
168
  page: ScriptedPageLike;
@@ -292,3 +293,5 @@ export interface ScriptedBrowserSessionResult {
292
293
  * approvals exist on a deterministic replay) — asserted in tests.
293
294
  */
294
295
  export declare function runScriptedBrowserSession(options: ScriptedBrowserSessionOptions): Promise<ScriptedBrowserSessionResult>;
296
+ /** Internal lab seam: the run root is already prepared and must stay bound to that identity. */
297
+ export declare function runScriptedBrowserSessionInPreparedRoot(options: ScriptedBrowserSessionOptions, preparedArtifactRoot: PreparedOutputDirectory): Promise<ScriptedBrowserSessionResult>;
@@ -18,13 +18,15 @@
18
18
  // importable from this code path. tokenUsage on every projected trace records zeros as an
19
19
  // affirmative $0 declaration that is TRUE by mechanism.
20
20
  import { execFile, spawn } from "node:child_process";
21
- import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
21
+ import { mkdtemp, rm, stat } from "node:fs/promises";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { promisify } from "node:util";
25
25
  import { ACTOR_TRACE_SCHEMA, SCRIPTED_BROWSER_CAPABILITIES } from "./actor-contract.js";
26
26
  import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS } from "./browser-evidence-hygiene.js";
27
+ import { assertScreenshotEvidence } from "./image-evidence.js";
27
28
  import { digestText, redactText, redactToSecretLabel } from "./redaction.js";
29
+ import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareContainedOutputFile, prepareSelectedOutputDirectory, readContainedRegularFile, writeContainedOutputFile } from "./selected-output-paths.js";
28
30
  const execFileAsync = promisify(execFile);
29
31
  const LOOPBACK_EVIDENCE_URL_POLICY = { kind: "loopback" };
30
32
  /** Production default: lazy playwright-core import + chromium.launch, exactly as the driver
@@ -80,7 +82,7 @@ export async function captureBrowserSurface(args) {
80
82
  export async function captureBrowserSurfaceFixture(args) {
81
83
  const started = Date.now();
82
84
  const tracePath = path.join("traces", `${args.surface.id}.json`);
83
- const absoluteTracePath = path.join(args.absoluteArtifactRoot, tracePath);
85
+ await assertScriptedOutputRoot(args.absoluteArtifactRoot);
84
86
  const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
85
87
  const capturedAt = new Date().toISOString();
86
88
  const profileDir = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-profile-"));
@@ -93,18 +95,16 @@ export async function captureBrowserSurfaceFixture(args) {
93
95
  }
94
96
  const stepStarted = Date.now();
95
97
  const screenshotPath = screenshotPathForBrowserStep(args.surface, step);
96
- const absoluteScreenshotPath = path.join(args.absoluteArtifactRoot, screenshotPath);
97
- await captureScreenshotWithBrowser({
98
- args: browserScreenshotArgs({
99
- appUrl: currentUrl,
100
- profileDir,
101
- screenshotPath: absoluteScreenshotPath,
102
- surface: args.surface
103
- }),
98
+ await prepareContainedOutputFile(args.absoluteArtifactRoot, screenshotPath);
99
+ const screenshotBytes = await captureBrowserCommandScreenshot({
100
+ appUrl: currentUrl,
104
101
  browserCommand: args.browserCommand,
105
- screenshotPath: absoluteScreenshotPath,
102
+ profileDir,
103
+ surface: args.surface,
106
104
  timeoutMs: args.timeoutMs
107
105
  });
106
+ assertScreenshotEvidence(screenshotPath, screenshotBytes);
107
+ await writeContainedOutputFile(args.absoluteArtifactRoot, screenshotPath, screenshotBytes);
108
108
  const assertions = fixtureAssertionsForBrowserStep(step, httpProbe.ok);
109
109
  steps.push({
110
110
  action: step.action,
@@ -132,7 +132,7 @@ export async function captureBrowserSurfaceFixture(args) {
132
132
  timestamp: capturedAt
133
133
  });
134
134
  const blockedScreenshotPath = surfaceScreenshotPath(blockedSteps);
135
- await writeJson(absoluteTracePath, buildBrowserTrace({
135
+ await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
136
136
  appUrl: args.appUrl,
137
137
  browserCommand: path.basename(args.browserCommand),
138
138
  browserJourney: args.browserJourney,
@@ -144,7 +144,7 @@ export async function captureBrowserSurfaceFixture(args) {
144
144
  ...(blockedScreenshotPath === undefined ? {} : { screenshotPath: blockedScreenshotPath }),
145
145
  steps: blockedSteps,
146
146
  surface: args.surface
147
- }));
147
+ }), null, 2)}\n`, "utf8");
148
148
  return {
149
149
  capturedAt,
150
150
  durationMs: Date.now() - started,
@@ -163,7 +163,12 @@ export async function captureBrowserSurfaceFixture(args) {
163
163
  // Every step in the success path attempts a screenshot, so each carries a
164
164
  // screenshotPath. A step claiming success whose screenshot is missing or empty
165
165
  // must still drag the capture out of `ok` — the strict verifier then catches it.
166
- const screenshotStats = await Promise.all(steps.map((step) => (step.screenshotPath ? stat(path.join(args.absoluteArtifactRoot, step.screenshotPath)) : Promise.resolve(null)).catch(() => null)));
166
+ const screenshotStats = await Promise.all(steps.map(async (step) => {
167
+ if (!step.screenshotPath)
168
+ return null;
169
+ const screenshotFile = await prepareContainedOutputFile(args.absoluteArtifactRoot, step.screenshotPath);
170
+ return stat(screenshotFile);
171
+ }).map((result) => result.catch(() => null)));
167
172
  const screenshotsOk = screenshotStats.every((stats) => stats?.isFile() && stats.size > 0);
168
173
  const ok = Boolean(screenshotsOk && httpProbe.ok && steps.every((step) => step.status === "passed"));
169
174
  const reason = ok
@@ -174,7 +179,7 @@ export async function captureBrowserSurfaceFixture(args) {
174
179
  const completedAt = new Date().toISOString();
175
180
  const durationMs = Date.now() - started;
176
181
  const fixtureScreenshotPath = surfaceScreenshotPath(steps);
177
- await writeJson(absoluteTracePath, buildBrowserTrace({
182
+ await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
178
183
  appUrl: args.appUrl,
179
184
  browserCommand: path.basename(args.browserCommand),
180
185
  browserJourney: args.browserJourney,
@@ -186,7 +191,7 @@ export async function captureBrowserSurfaceFixture(args) {
186
191
  ...(fixtureScreenshotPath === undefined ? {} : { screenshotPath: fixtureScreenshotPath }),
187
192
  steps,
188
193
  surface: args.surface
189
- }));
194
+ }), null, 2)}\n`, "utf8");
190
195
  return {
191
196
  capturedAt: completedAt,
192
197
  durationMs,
@@ -202,7 +207,7 @@ export async function captureBrowserSurfaceFixture(args) {
202
207
  export async function captureBrowserSurfaceWithPlaywright(args) {
203
208
  const started = Date.now();
204
209
  const tracePath = path.join("traces", `${args.surface.id}.json`);
205
- const absoluteTracePath = path.join(args.absoluteArtifactRoot, tracePath);
210
+ await assertScriptedOutputRoot(args.absoluteArtifactRoot);
206
211
  const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
207
212
  let browser = null;
208
213
  let page = null;
@@ -273,7 +278,7 @@ export async function captureBrowserSurfaceWithPlaywright(args) {
273
278
  ? `${args.surface.label} completed ${steps.length}/${steps.length} browser persona steps from ${args.appUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
274
279
  : `${args.surface.label} browser persona journey blocked: ${steps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`;
275
280
  const playwrightScreenshotPath = surfaceScreenshotPath(steps);
276
- await writeJson(absoluteTracePath, buildBrowserTrace({
281
+ await writeContainedOutputFile(args.absoluteArtifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
277
282
  appUrl: args.appUrl,
278
283
  browserCommand: path.basename(args.browserCommand),
279
284
  browserJourney: args.browserJourney,
@@ -285,7 +290,7 @@ export async function captureBrowserSurfaceWithPlaywright(args) {
285
290
  ...(playwrightScreenshotPath === undefined ? {} : { screenshotPath: playwrightScreenshotPath }),
286
291
  steps,
287
292
  surface: args.surface
288
- }));
293
+ }), null, 2)}\n`, "utf8");
289
294
  return {
290
295
  capturedAt: completedAt,
291
296
  durationMs,
@@ -352,10 +357,10 @@ export async function executeBrowserPersonaStep(args) {
352
357
  }
353
358
  const afterState = await browserPersonaPageState(args.page, urlPolicy);
354
359
  const screenshotPath = screenshotPathForBrowserStep(args.surface, args.step);
355
- await args.page.screenshot({
356
- path: path.join(args.absoluteArtifactRoot, screenshotPath),
357
- fullPage: true
358
- });
360
+ await prepareContainedOutputFile(args.absoluteArtifactRoot, screenshotPath);
361
+ const screenshotBytes = await captureScriptedPageScreenshot(args.page);
362
+ assertScreenshotEvidence(screenshotPath, screenshotBytes);
363
+ await writeContainedOutputFile(args.absoluteArtifactRoot, screenshotPath, screenshotBytes);
359
364
  const assertions = await evaluateBrowserStepExpectations({
360
365
  afterState,
361
366
  beforeState,
@@ -478,8 +483,22 @@ function fixtureAssertionsForBrowserStep(step, httpOk) {
478
483
  }));
479
484
  }
480
485
  export function screenshotPathForBrowserStep(surface, step) {
486
+ assertSafeOutputPathSegment(surface.id, "Browser surface id");
487
+ if (step) {
488
+ assertSafeOutputPathSegment(step.id, "Browser journey step id");
489
+ }
481
490
  return path.join("screenshots", `${surface.id}-${step?.id ?? "step"}.png`);
482
491
  }
492
+ function tracePathForBrowserSurface(surface) {
493
+ assertSafeOutputPathSegment(surface.id, "Browser surface id");
494
+ return path.join("traces", `${surface.id}.json`);
495
+ }
496
+ function assertScriptedSessionPathIds(options) {
497
+ assertSafeOutputPathSegment(options.surface.id, "Browser surface id");
498
+ for (const step of options.journey.steps) {
499
+ assertSafeOutputPathSegment(step.id, "Browser journey step id");
500
+ }
501
+ }
483
502
  /**
484
503
  * Best-effort screenshot of a blocked step. The step is blocked, so its failure is
485
504
  * the evidence; the shot is a bonus. Returns the relative path plus whether the
@@ -489,9 +508,19 @@ export function screenshotPathForBrowserStep(surface, step) {
489
508
  */
490
509
  async function captureBlockedStepScreenshot(page, artifactRoot, surface, step) {
491
510
  const screenshotPath = screenshotPathForBrowserStep(surface, step);
492
- await page?.screenshot({ path: path.join(artifactRoot, screenshotPath), fullPage: true }).catch(() => undefined);
493
- const stats = await stat(path.join(artifactRoot, screenshotPath)).catch(() => null);
494
- return { screenshotPath, written: Boolean(stats?.isFile() && stats.size > 0) };
511
+ await prepareContainedOutputFile(artifactRoot, screenshotPath);
512
+ if (!page) {
513
+ return { screenshotPath, written: false };
514
+ }
515
+ try {
516
+ const screenshotBytes = await captureScriptedPageScreenshot(page);
517
+ assertScreenshotEvidence(screenshotPath, screenshotBytes);
518
+ await writeContainedOutputFile(artifactRoot, screenshotPath, screenshotBytes);
519
+ return { screenshotPath, written: true };
520
+ }
521
+ catch {
522
+ return { screenshotPath, written: false };
523
+ }
495
524
  }
496
525
  /**
497
526
  * Surface-level screenshot path = the last step that actually wrote one. Returns
@@ -619,6 +648,34 @@ export async function captureScreenshotWithBrowser(args) {
619
648
  ? `browser exited before screenshot was written (exit=${exitCode ?? "null"} signal=${signal ?? "null"})`
620
649
  : `timed out after ${args.timeoutMs}ms waiting for screenshot`);
621
650
  }
651
+ async function captureBrowserCommandScreenshot(args) {
652
+ const stagingPath = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-command-shot-"));
653
+ const stagingRoot = await prepareSelectedOutputDirectory(path.dirname(stagingPath), stagingPath);
654
+ try {
655
+ const screenshotPath = path.join(stagingRoot.physicalPath, "capture.png");
656
+ await captureScreenshotWithBrowser({
657
+ args: browserScreenshotArgs({
658
+ appUrl: args.appUrl,
659
+ profileDir: args.profileDir,
660
+ screenshotPath,
661
+ surface: args.surface
662
+ }),
663
+ browserCommand: args.browserCommand,
664
+ screenshotPath,
665
+ timeoutMs: args.timeoutMs
666
+ });
667
+ const bytes = await readContainedRegularFile(stagingRoot, "capture.png");
668
+ if (!bytes) {
669
+ throw new Error("Browser screenshot command did not write a single-link staging file.");
670
+ }
671
+ return bytes;
672
+ }
673
+ finally {
674
+ await assertPreparedSelectedOutputDirectory(stagingRoot)
675
+ .then(() => rm(stagingRoot.physicalPath, { force: true, recursive: true }))
676
+ .catch(() => undefined);
677
+ }
678
+ }
622
679
  function terminateProcessGroup(pid, force = false) {
623
680
  if (!pid) {
624
681
  return;
@@ -925,6 +982,18 @@ class ScriptedJourneyTimeoutError extends Error {
925
982
  * approvals exist on a deterministic replay) — asserted in tests.
926
983
  */
927
984
  export async function runScriptedBrowserSession(options) {
985
+ assertScriptedSessionPathIds(options);
986
+ const preparedArtifactRoot = await prepareSelectedOutputDirectory(process.cwd(), options.artifactRoot);
987
+ return runScriptedBrowserSessionInPreparedRoot(options, preparedArtifactRoot);
988
+ }
989
+ /** Internal lab seam: the run root is already prepared and must stay bound to that identity. */
990
+ export async function runScriptedBrowserSessionInPreparedRoot(options, preparedArtifactRoot) {
991
+ assertScriptedSessionPathIds(options);
992
+ await prepareContainedOutputDirectory(preparedArtifactRoot, "screenshots");
993
+ await prepareContainedOutputDirectory(preparedArtifactRoot, "traces");
994
+ await prepareContainedOutputFile(preparedArtifactRoot, tracePathForBrowserSurface(options.surface));
995
+ await Promise.all(options.journey.steps.map((step) => prepareContainedOutputFile(preparedArtifactRoot, screenshotPathForBrowserStep(options.surface, step))));
996
+ await assertScriptedOutputRoot(preparedArtifactRoot);
928
997
  const now = options.now ?? (() => Date.now());
929
998
  const startedAtMs = now();
930
999
  const startedAt = new Date(startedAtMs).toISOString();
@@ -932,12 +1001,10 @@ export async function runScriptedBrowserSession(options) {
932
1001
  const browserCommand = options.browserCommand ?? (options.launchBrowser ? "injected-browser" : "");
933
1002
  const evidenceAppUrl = options.evidenceAppUrl ?? options.appUrl;
934
1003
  const urlPolicy = options.urlPolicy ?? LOOPBACK_EVIDENCE_URL_POLICY;
935
- await mkdir(path.join(options.artifactRoot, "screenshots"), { recursive: true });
936
- await mkdir(path.join(options.artifactRoot, "traces"), { recursive: true });
937
1004
  const finish = async (args) => {
938
1005
  const completedAtMs = now();
939
1006
  const trace = await projectScriptedActorTrace({
940
- artifactRoot: options.artifactRoot,
1007
+ artifactRoot: preparedArtifactRoot,
941
1008
  capture: args.capture,
942
1009
  completedAt: new Date(completedAtMs).toISOString(),
943
1010
  completionReason: args.completionReason,
@@ -966,7 +1033,7 @@ export async function runScriptedBrowserSession(options) {
966
1033
  const reason = `Scripted browser launch failed: ${compactBrowserError(error)}`;
967
1034
  const capture = await persistScriptedFailureCapture({
968
1035
  appUrl: options.appUrl,
969
- artifactRoot: options.artifactRoot,
1036
+ artifactRoot: preparedArtifactRoot,
970
1037
  browserCommand,
971
1038
  evidenceAppUrl,
972
1039
  journey: options.journey,
@@ -976,9 +1043,16 @@ export async function runScriptedBrowserSession(options) {
976
1043
  });
977
1044
  return finish({ capture, executedSteps: 0, status: "failed", completionReason: "harness_error", reason });
978
1045
  }
1046
+ try {
1047
+ await assertScriptedOutputRoot(preparedArtifactRoot);
1048
+ }
1049
+ catch (error) {
1050
+ await browser.close().catch(() => undefined);
1051
+ throw error;
1052
+ }
979
1053
  const journeyRun = await runScriptedJourney({
980
1054
  appUrl: options.appUrl,
981
- artifactRoot: options.artifactRoot,
1055
+ artifactRoot: preparedArtifactRoot,
982
1056
  browser,
983
1057
  browserCommand,
984
1058
  evidenceAppUrl,
@@ -1019,8 +1093,7 @@ export async function runScriptedBrowserSession(options) {
1019
1093
  async function runScriptedJourney(args) {
1020
1094
  const started = Date.now();
1021
1095
  const deadline = started + args.timeoutMs;
1022
- const tracePath = path.join("traces", `${args.surface.id}.json`);
1023
- const absoluteTracePath = path.join(args.artifactRoot, tracePath);
1096
+ const tracePath = tracePathForBrowserSurface(args.surface);
1024
1097
  const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
1025
1098
  let page = null;
1026
1099
  const steps = [];
@@ -1037,6 +1110,7 @@ async function runScriptedJourney(args) {
1037
1110
  });
1038
1111
  page = await context.newPage();
1039
1112
  for (const step of args.journey.steps) {
1113
+ await assertScriptedOutputRoot(args.artifactRoot);
1040
1114
  executedSteps += 1;
1041
1115
  steps.push(await withJourneyDeadline(executeBrowserPersonaStep({
1042
1116
  absoluteArtifactRoot: args.artifactRoot,
@@ -1051,6 +1125,7 @@ async function runScriptedJourney(args) {
1051
1125
  }
1052
1126
  }
1053
1127
  catch (error) {
1128
+ await assertScriptedOutputRoot(args.artifactRoot);
1054
1129
  timedOut = error instanceof ScriptedJourneyTimeoutError;
1055
1130
  const now = new Date().toISOString();
1056
1131
  const reason = compactBrowserError(error);
@@ -1092,7 +1167,7 @@ async function runScriptedJourney(args) {
1092
1167
  ? `${args.surface.label} completed ${steps.length}/${steps.length} scripted browser steps from ${args.evidenceAppUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
1093
1168
  : `${args.surface.label} scripted browser journey blocked: ${steps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`;
1094
1169
  const scriptedScreenshotPath = surfaceScreenshotPath(steps);
1095
- await writeJson(absoluteTracePath, buildBrowserTrace({
1170
+ await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
1096
1171
  appUrl: args.evidenceAppUrl,
1097
1172
  browserCommand: path.basename(args.browserCommand || "injected-browser"),
1098
1173
  browserJourney: args.journey,
@@ -1104,7 +1179,7 @@ async function runScriptedJourney(args) {
1104
1179
  ...(scriptedScreenshotPath === undefined ? {} : { screenshotPath: scriptedScreenshotPath }),
1105
1180
  steps,
1106
1181
  surface: args.surface
1107
- }));
1182
+ }), null, 2)}\n`, "utf8");
1108
1183
  return {
1109
1184
  capture: {
1110
1185
  capturedAt: completedAt,
@@ -1150,7 +1225,7 @@ async function withJourneyDeadline(promise, deadline, timeoutMs) {
1150
1225
  * journey actuation (browser launch crash). Mirrors the driver's failure shape. */
1151
1226
  async function persistScriptedFailureCapture(args) {
1152
1227
  const capturedAt = new Date().toISOString();
1153
- const tracePath = path.join("traces", `${args.surface.id}.json`);
1228
+ const tracePath = tracePathForBrowserSurface(args.surface);
1154
1229
  const blockedSteps = buildBlockedBrowserPersonaSteps({
1155
1230
  browserJourney: args.journey,
1156
1231
  currentUrl: args.appUrl,
@@ -1162,7 +1237,7 @@ async function persistScriptedFailureCapture(args) {
1162
1237
  // Pre-actuation failure: no screenshots were written, so the surface omits the
1163
1238
  // screenshot reference and the failure itself stands as the evidence.
1164
1239
  const screenshotPath = surfaceScreenshotPath(blockedSteps);
1165
- await writeJson(path.join(args.artifactRoot, tracePath), buildBrowserTrace({
1240
+ await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
1166
1241
  appUrl: args.evidenceAppUrl,
1167
1242
  browserCommand: path.basename(args.browserCommand || "injected-browser"),
1168
1243
  browserJourney: args.journey,
@@ -1173,7 +1248,7 @@ async function persistScriptedFailureCapture(args) {
1173
1248
  ...(screenshotPath === undefined ? {} : { screenshotPath }),
1174
1249
  steps: blockedSteps,
1175
1250
  surface: args.surface
1176
- }));
1251
+ }), null, 2)}\n`, "utf8");
1177
1252
  return {
1178
1253
  capturedAt,
1179
1254
  durationMs: 0,
@@ -1194,7 +1269,8 @@ async function projectScriptedActorTrace(args) {
1194
1269
  if (!step.screenshotPath) {
1195
1270
  continue;
1196
1271
  }
1197
- const stats = await stat(path.join(args.artifactRoot, step.screenshotPath)).catch(() => null);
1272
+ const screenshotFile = await prepareContainedOutputFile(args.artifactRoot, step.screenshotPath).catch(() => null);
1273
+ const stats = screenshotFile ? await stat(screenshotFile).catch(() => null) : null;
1198
1274
  if (stats?.isFile() && stats.size > 0) {
1199
1275
  writtenScreenshots.add(step.screenshotPath);
1200
1276
  }
@@ -1254,8 +1330,45 @@ async function projectScriptedActorTrace(args) {
1254
1330
  // Small private helpers (moved with the driver; run.ts keeps its own copies for
1255
1331
  // the code that stayed behind — this module must remain a leaf).
1256
1332
  // ---------------------------------------------------------------------------
1257
- async function writeJson(filePath, value) {
1258
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1333
+ async function assertScriptedOutputRoot(root) {
1334
+ if ("physicalRunRoot" in root) {
1335
+ await prepareContainedOutputDirectory(root, "");
1336
+ return root.physicalRunRoot;
1337
+ }
1338
+ await assertPreparedSelectedOutputDirectory(root);
1339
+ return root.physicalPath;
1340
+ }
1341
+ function browserScreenshotBytes(value) {
1342
+ if (Buffer.isBuffer(value)) {
1343
+ return value;
1344
+ }
1345
+ if (value instanceof Uint8Array) {
1346
+ return Buffer.from(value);
1347
+ }
1348
+ throw new Error("Browser screenshot did not return image bytes.");
1349
+ }
1350
+ async function captureScriptedPageScreenshot(page) {
1351
+ const stagingPath = await mkdtemp(path.join(os.tmpdir(), "humanish-browser-shot-"));
1352
+ const stagingRoot = await prepareSelectedOutputDirectory(path.dirname(stagingPath), stagingPath);
1353
+ try {
1354
+ const returned = await page.screenshot({
1355
+ path: path.join(stagingRoot.physicalPath, "capture.png"),
1356
+ fullPage: true
1357
+ });
1358
+ if (Buffer.isBuffer(returned) || returned instanceof Uint8Array) {
1359
+ return browserScreenshotBytes(returned);
1360
+ }
1361
+ const stagedBytes = await readContainedRegularFile(stagingRoot, "capture.png");
1362
+ if (!stagedBytes) {
1363
+ throw new Error("Browser screenshot did not return bytes or write a single-link staging file.");
1364
+ }
1365
+ return stagedBytes;
1366
+ }
1367
+ finally {
1368
+ await assertPreparedSelectedOutputDirectory(stagingRoot)
1369
+ .then(() => rm(stagingRoot.physicalPath, { force: true, recursive: true }))
1370
+ .catch(() => undefined);
1371
+ }
1259
1372
  }
1260
1373
  function shellQuote(value) {
1261
1374
  if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {