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
package/dist/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { parse as parseYaml } from "yaml";
@@ -8,17 +8,19 @@ import { parse as parseYaml } from "yaml";
8
8
  // (moved there so the actor registry can reuse it without a run.ts import cycle). This file
9
9
  // keeps the `run --app-url` orchestration; behavior is byte-identical.
10
10
  import { browserSurfaces, builtinBrowserPersonaJourney, captureBrowserSurface, normalizeLocalAppUrl, parseBrowserPersonaJourneyFromScenario, resolveBrowserCommand } from "./scripted-browser-actor.js";
11
- import { CODEX_APP_SERVER_TRACE_SCHEMA } from "./codex-app-server.js";
11
+ import { CODEX_APP_SERVER_TRACE_SCHEMA, runCodexAppServerSessionInPreparedRoot } from "./codex-app-server.js";
12
12
  import { getActor } from "./actor-registry.js";
13
13
  import { artifactReferenceIfWritten, hasWrittenScreenshot } from "./artifact-reference.js";
14
14
  import { ACTOR_TRACE_SCHEMA } from "./actor-contract.js";
15
15
  import { captureGitState, GIT_STATE_SCHEMA } from "./core/git-state.js";
16
+ import { inspectVerifiedGitWorkspace } from "./core/git-workspace.js";
16
17
  import { mapWithConcurrency } from "./concurrency.js";
17
18
  import { screenshotEvidenceError } from "./image-evidence.js";
18
19
  import { buildObserverData } from "./observer-data.js";
19
20
  import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
20
21
  import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
21
- import { loadE2BDesktopModule } from "./e2b-desktop-launch.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";
22
24
  export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
23
25
  export const SHARED_WORLD_SCHEMA = "humanish.shared-world.v1";
24
26
  export const REVIEW_SCHEMA = "humanish.review.v1";
@@ -29,8 +31,17 @@ export const CLEANUP_SCHEMA = "humanish.cleanup-result.v1";
29
31
  export const PUBLIC_TARGET_CWD = "[target-cwd]";
30
32
  const SAFE_GIT_NOTES = new Set([
31
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.",
32
37
  "Git status command could not be captured.",
33
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.",
34
45
  "Git work tree had changes; only counts were captured, not branch names, remotes, paths, or file names.",
35
46
  "Git work tree was clean; branch names, remotes, paths, and file names were not captured.",
36
47
  "No git work tree was detected.",
@@ -102,6 +113,7 @@ export async function runDryRun(options) {
102
113
  }
103
114
  };
104
115
  }
116
+ const projectRoot = await prepareSelectedOutputDirectory(path.dirname(cwd), cwd);
105
117
  if (options.appUrl !== undefined) {
106
118
  if (options.dryRun) {
107
119
  return {
@@ -127,18 +139,18 @@ export async function runDryRun(options) {
127
139
  }
128
140
  };
129
141
  }
130
- return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, simCount });
142
+ return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, projectRoot, simCount });
131
143
  }
132
144
  if (!options.dryRun) {
133
145
  const actor = resolveRequestedLocalCodexActor(options.actor);
134
146
  if (actor === "codex-tui") {
135
- return runLocalCodexTui({ ...options, actor, cwd, simCount });
147
+ return runLocalCodexTui({ ...options, actor, cwd, projectRoot, simCount });
136
148
  }
137
149
  if (actor === "codex-exec") {
138
- return runLocalCodexExec({ ...options, actor, cwd, simCount });
150
+ return runLocalCodexExec({ ...options, actor, cwd, projectRoot, simCount });
139
151
  }
140
152
  if (actor === "codex-app-server") {
141
- return runLocalCodexAppServer({ ...options, actor, cwd, simCount });
153
+ return runLocalCodexAppServer({ ...options, actor, cwd, projectRoot, simCount });
142
154
  }
143
155
  return {
144
156
  schema: "humanish.run-result.v1",
@@ -154,12 +166,13 @@ export async function runDryRun(options) {
154
166
  const now = new Date();
155
167
  const createdAt = now.toISOString();
156
168
  const runId = options.runId ?? `dryrun-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
157
- const artifactRoot = path.join(".humanish", "runs", runId);
158
- const absoluteArtifactRoot = path.join(cwd, artifactRoot);
159
- const packageName = await readPackageName(cwd);
160
- const humanishSource = await directoryExists(path.join(cwd, "humanish")) ? "present" : "missing";
169
+ const packageName = await readPackageName(projectRoot);
170
+ const humanishSource = await implicitProjectDirectoryExists(projectRoot, "humanish") ? "present" : "missing";
161
171
  const source = await buildRunSource({ cwd, capturedAt: createdAt, humanishSource, packageName });
162
- const selection = await loadDryRunSelection(cwd, humanishSource);
172
+ const selection = await loadDryRunSelection(projectRoot, humanishSource);
173
+ await assertPreparedSelectedOutputDirectory(projectRoot);
174
+ const runPaths = await prepareRunArtifactPaths(cwd, runId);
175
+ const artifactRoot = runPaths.relativeRunRoot;
163
176
  if (humanishSource === "missing") {
164
177
  warnings.push("Committed humanish/ source was not found; using built-in synthetic dry-run defaults.");
165
178
  }
@@ -220,14 +233,13 @@ export async function runDryRun(options) {
220
233
  review: createReviewSummary(),
221
234
  feedbackCandidates: []
222
235
  };
223
- await mkdir(absoluteArtifactRoot, { recursive: true });
224
- await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
225
- await writeJson(path.join(cwd, ".humanish", "runs", "latest.json"), {
236
+ await writeRunBundleArtifacts(runPaths, bundle);
237
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
226
238
  schema: "humanish.latest-run.v1",
227
239
  runId,
228
240
  path: artifactRoot,
229
241
  updatedAt: createdAt
230
- });
242
+ }, null, 2)}\n`, "utf8");
231
243
  return {
232
244
  schema: "humanish.run-result.v1",
233
245
  ok: true,
@@ -238,7 +250,7 @@ export async function runDryRun(options) {
238
250
  artifactRoot,
239
251
  bundlePath: path.join(artifactRoot, "run.json"),
240
252
  reviewPath: path.join(artifactRoot, "review.md"),
241
- latestPath: path.join(".humanish", "runs", "latest.json"),
253
+ latestPath: runPaths.relativeLatestPointer,
242
254
  warnings
243
255
  };
244
256
  }
@@ -273,12 +285,13 @@ async function runBrowserAppProof(options) {
273
285
  const now = new Date();
274
286
  const createdAt = now.toISOString();
275
287
  const runId = options.runId ?? `browser-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
276
- const artifactRoot = path.join(".humanish", "runs", runId);
277
- const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
278
- const packageName = await readPackageName(options.cwd);
279
- const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
288
+ const packageName = await readPackageName(options.projectRoot);
289
+ const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
280
290
  const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
281
- const selection = await loadDryRunSelection(options.cwd, humanishSource);
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;
282
295
  if (selection.browserJourneyFailure) {
283
296
  return {
284
297
  schema: "humanish.run-result.v1",
@@ -299,17 +312,18 @@ async function runBrowserAppProof(options) {
299
312
  warnings.push("No executable browser scenario manifest was found; using built-in browser persona two-step journey.");
300
313
  }
301
314
  warnings.push(...selection.warnings);
302
- await mkdir(path.join(absoluteArtifactRoot, "screenshots"), { recursive: true });
303
- await mkdir(path.join(absoluteArtifactRoot, "traces"), { recursive: true });
315
+ await prepareContainedOutputDirectory(runPaths, "screenshots");
316
+ await prepareContainedOutputDirectory(runPaths, "traces");
304
317
  const surfaces = browserSurfaces.slice(0, options.simCount);
305
318
  const captures = await Promise.all(surfaces.map((surface) => captureBrowserSurface({
306
- absoluteArtifactRoot,
319
+ absoluteArtifactRoot: runPaths,
307
320
  appUrl,
308
321
  browserCommand,
309
322
  browserJourney,
310
323
  surface,
311
324
  timeoutMs: options.timeoutMs ?? BROWSER_APP_DEFAULT_TIMEOUT_MS
312
325
  })));
326
+ await validatePreparedRunArtifactPaths(runPaths);
313
327
  const completedAt = new Date().toISOString();
314
328
  const events = buildBrowserAppEvents({ appUrl, captures, createdAt });
315
329
  const allPassed = captures.every((capture) => capture.ok);
@@ -445,13 +459,13 @@ async function runBrowserAppProof(options) {
445
459
  review,
446
460
  feedbackCandidates: []
447
461
  };
448
- await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
449
- await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
462
+ await writeRunBundleArtifacts(runPaths, bundle);
463
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
450
464
  schema: "humanish.latest-run.v1",
451
465
  runId,
452
466
  path: artifactRoot,
453
467
  updatedAt: completedAt
454
- });
468
+ }, null, 2)}\n`, "utf8");
455
469
  return {
456
470
  schema: "humanish.run-result.v1",
457
471
  ok: allPassed,
@@ -462,7 +476,7 @@ async function runBrowserAppProof(options) {
462
476
  artifactRoot,
463
477
  bundlePath: path.join(artifactRoot, "run.json"),
464
478
  reviewPath: path.join(artifactRoot, "review.md"),
465
- latestPath: path.join(".humanish", "runs", "latest.json"),
479
+ latestPath: runPaths.relativeLatestPointer,
466
480
  warnings,
467
481
  ...(allPassed
468
482
  ? {}
@@ -577,15 +591,28 @@ async function runLocalCodexTui(options) {
577
591
  const now = new Date();
578
592
  const createdAt = now.toISOString();
579
593
  const runId = options.runId ?? `codex-tui-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
580
- const artifactRoot = path.join(".humanish", "runs", runId);
581
- const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
582
- const transcriptPath = path.join(absoluteArtifactRoot, "transcripts", "codex-tui-sanitized.txt");
583
- const actorTracePath = path.join(absoluteArtifactRoot, "actor.json");
584
- const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
585
- const packageName = await readPackageName(options.cwd);
586
- const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
587
- const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
588
- const selection = await loadDryRunSelection(options.cwd, humanishSource);
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;
589
616
  if (humanishSource === "missing") {
590
617
  warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
591
618
  }
@@ -594,7 +621,6 @@ async function runLocalCodexTui(options) {
594
621
  const prompt = buildLocalCodexTuiPrompt(selection, verdictNonce);
595
622
  const promptDigest = digestText(prompt);
596
623
  const command = resolveLocalCodexTuiCommand(options.cwd, prompt, options.actorCommand);
597
- const usesDefaultCodexCommand = options.actorCommand === undefined && process.env.HUMANISH_CODEX_ACTOR_COMMAND === undefined;
598
624
  const simId = "sim-01";
599
625
  const streamId = "sim-01-codex-tui";
600
626
  const events = [];
@@ -608,11 +634,9 @@ async function runLocalCodexTui(options) {
608
634
  simId,
609
635
  streamId
610
636
  });
611
- await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
637
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
612
638
  };
613
- await mkdir(path.dirname(transcriptPath), { recursive: true });
614
639
  let actor;
615
- const trustPreflight = usesDefaultCodexCommand ? await checkCodexWorkspaceTrust(options.cwd) : { ok: true };
616
640
  if (!trustPreflight.ok) {
617
641
  actor = {
618
642
  durationMs: 0,
@@ -719,19 +743,20 @@ async function runLocalCodexTui(options) {
719
743
  review: createLocalActorRunningReviewSummary("Codex TUI"),
720
744
  feedbackCandidates: []
721
745
  };
722
- await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
723
- await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
746
+ await writeRunBundleArtifacts(runPaths, runningBundle);
747
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
724
748
  schema: "humanish.latest-run.v1",
725
749
  runId,
726
750
  path: artifactRoot,
727
751
  updatedAt: runningAt
728
- });
752
+ }, null, 2)}\n`, "utf8");
729
753
  actor = await executeLocalActorCommand(command, {
730
754
  cwd: options.cwd,
731
755
  timeoutMs,
732
756
  verdictNonce
733
757
  });
734
758
  }
759
+ await validatePreparedRunArtifactPaths(runPaths);
735
760
  const completedAt = new Date().toISOString();
736
761
  const redactedTranscript = redactSensitiveText(actor.transcript);
737
762
  const tail = tailText(redactedTranscript, 6_000);
@@ -741,8 +766,8 @@ async function runLocalCodexTui(options) {
741
766
  // block the reason embeds the absolute workspace path; the live CLI reason can
742
767
  // still show the real path, but the public-safe bundle must not.
743
768
  const verdictReason = redactSensitiveText(actor.reason);
744
- await writeFile(transcriptPath, redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
745
- await writeJson(actorTracePath, {
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({
746
771
  schema: "humanish.local-codex-tui-actor.v1",
747
772
  actor: "codex-tui",
748
773
  commandName: command.name,
@@ -758,7 +783,7 @@ async function runLocalCodexTui(options) {
758
783
  transcriptBytes: actor.transcriptBytes,
759
784
  transcriptPath: "transcripts/codex-tui-sanitized.txt",
760
785
  redaction: "passed"
761
- });
786
+ }, null, 2)}\n`, "utf8");
762
787
  await appendEvent("actor.observation", `Captured ${actor.transcriptBytes} output byte${actor.transcriptBytes === 1 ? "" : "s"}; sanitized transcript tail recorded with redaction=passed.`);
763
788
  await appendEvent("actor.artifact", "Wrote sanitized Codex TUI transcript and actor trace artifacts under the ignored run directory.");
764
789
  await appendEvent("actor.verdict", `Local Codex TUI actor verdict ${status}: ${verdictReason}`, status === "passed" ? "info" : status === "timed_out" ? "warn" : "error");
@@ -862,13 +887,13 @@ async function runLocalCodexTui(options) {
862
887
  review: createLocalActorReviewSummary("Codex TUI", status, verdictReason),
863
888
  feedbackCandidates: []
864
889
  };
865
- await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
866
- await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
890
+ await writeRunBundleArtifacts(runPaths, bundle);
891
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
867
892
  schema: "humanish.latest-run.v1",
868
893
  runId,
869
894
  path: artifactRoot,
870
895
  updatedAt: completedAt
871
- });
896
+ }, null, 2)}\n`, "utf8");
872
897
  return {
873
898
  schema: "humanish.run-result.v1",
874
899
  ok: status === "passed",
@@ -879,7 +904,7 @@ async function runLocalCodexTui(options) {
879
904
  artifactRoot,
880
905
  bundlePath: path.join(artifactRoot, "run.json"),
881
906
  reviewPath: path.join(artifactRoot, "review.md"),
882
- latestPath: path.join(".humanish", "runs", "latest.json"),
907
+ latestPath: runPaths.relativeLatestPointer,
883
908
  warnings,
884
909
  ...(status === "passed"
885
910
  ? {}
@@ -996,13 +1021,13 @@ async function runLocalCodexExec(options) {
996
1021
  const now = new Date();
997
1022
  const createdAt = now.toISOString();
998
1023
  const runId = options.runId ?? `codex-exec-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
999
- const artifactRoot = path.join(".humanish", "runs", runId);
1000
- const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
1001
- const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
1002
- const packageName = await readPackageName(options.cwd);
1003
- const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
1024
+ const packageName = await readPackageName(options.projectRoot);
1025
+ const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
1004
1026
  const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
1005
- const selection = await loadDryRunSelection(options.cwd, humanishSource);
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;
1006
1031
  if (humanishSource === "missing") {
1007
1032
  warnings.push("Committed humanish/ source was not found; using built-in synthetic local actor defaults.");
1008
1033
  }
@@ -1020,15 +1045,12 @@ async function runLocalCodexExec(options) {
1020
1045
  ...(streamId === undefined ? {} : { streamId })
1021
1046
  });
1022
1047
  };
1023
- await mkdir(path.join(absoluteArtifactRoot, "transcripts"), { recursive: true });
1024
- await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
1025
- await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
1026
- await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
1048
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
1027
1049
  schema: "humanish.latest-run.v1",
1028
1050
  runId,
1029
1051
  path: artifactRoot,
1030
1052
  updatedAt: createdAt
1031
- });
1053
+ }, null, 2)}\n`, "utf8");
1032
1054
  const lanes = Array.from({ length: options.simCount }, (_, index) => {
1033
1055
  const focus = localCodexExecFocus(index);
1034
1056
  const simId = `sim-${String(index + 1).padStart(2, "0")}`;
@@ -1047,7 +1069,7 @@ async function runLocalCodexExec(options) {
1047
1069
  for (const lane of lanes) {
1048
1070
  pushEvent("actor.running", "Published local Codex exec running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
1049
1071
  }
1050
- await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1072
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1051
1073
  const baseLifecycle = [
1052
1074
  {
1053
1075
  at: createdAt,
@@ -1099,7 +1121,7 @@ async function runLocalCodexExec(options) {
1099
1121
  events,
1100
1122
  review: createLocalActorRunningReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout")
1101
1123
  });
1102
- await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
1124
+ await writeRunBundleArtifacts(runPaths, runningBundle);
1103
1125
  const laneResults = await mapWithConcurrency(lanes, maxConcurrency, async (lane) => {
1104
1126
  const actor = await executeLocalActorCommand(lane.command, {
1105
1127
  cwd: options.cwd,
@@ -1112,30 +1134,12 @@ async function runLocalCodexExec(options) {
1112
1134
  ? "transcripts/codex-exec-sanitized.jsonl"
1113
1135
  : `transcripts/${lane.streamId}-sanitized.jsonl`;
1114
1136
  const tracePath = options.simCount === 1 ? "actor.json" : `actors/${lane.streamId}.json`;
1115
- await writeFile(path.join(absoluteArtifactRoot, transcriptPath), redactedTranscript.length > 0 ? redactedTranscript : "No transcript output captured.\n", "utf8");
1116
- await writeJson(path.join(absoluteArtifactRoot, tracePath), {
1117
- schema: "humanish.local-codex-exec-actor.v1",
1118
- actor: "codex-exec",
1119
- commandName: lane.command.name,
1120
- focusId: lane.focus.id,
1121
- promptDigest: lane.promptDigest,
1122
- verdictNonce,
1123
- startedAt: createdAt,
1124
- completedAt: new Date().toISOString(),
1125
- durationMs: actor.durationMs,
1126
- exitCode: actor.exitCode,
1127
- signal: actor.signal,
1128
- status: actor.status,
1129
- timeoutMs,
1130
- transcriptBytes: actor.transcriptBytes,
1131
- transcriptPath,
1132
- redaction: "passed"
1133
- });
1134
1137
  return {
1135
1138
  actor,
1136
1139
  command: lane.command,
1137
1140
  focus: lane.focus,
1138
1141
  promptDigest: lane.promptDigest,
1142
+ redactedTranscript,
1139
1143
  simId: lane.simId,
1140
1144
  streamId: lane.streamId,
1141
1145
  tail,
@@ -1144,6 +1148,28 @@ async function runLocalCodexExec(options) {
1144
1148
  };
1145
1149
  });
1146
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
+ }
1147
1173
  const laneStatuses = laneResults.map((result) => result.actor.status);
1148
1174
  const status = aggregateActorStatus(laneStatuses);
1149
1175
  const verdictReason = options.simCount === 1
@@ -1157,7 +1183,7 @@ async function runLocalCodexExec(options) {
1157
1183
  ? `Actor timed out after ${timeoutMs}ms; last safe observation retained.`
1158
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);
1159
1185
  }
1160
- await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1186
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1161
1187
  const bundle = buildLocalCodexExecBundle({
1162
1188
  runId,
1163
1189
  simCount: options.simCount,
@@ -1200,7 +1226,13 @@ async function runLocalCodexExec(options) {
1200
1226
  events,
1201
1227
  review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout", status, verdictReason)
1202
1228
  });
1203
- await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
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");
1204
1236
  return {
1205
1237
  schema: "humanish.run-result.v1",
1206
1238
  ok: status === "passed",
@@ -1211,7 +1243,7 @@ async function runLocalCodexExec(options) {
1211
1243
  artifactRoot,
1212
1244
  bundlePath: path.join(artifactRoot, "run.json"),
1213
1245
  reviewPath: path.join(artifactRoot, "review.md"),
1214
- latestPath: path.join(".humanish", "runs", "latest.json"),
1246
+ latestPath: runPaths.relativeLatestPointer,
1215
1247
  warnings,
1216
1248
  ...(status === "passed"
1217
1249
  ? {}
@@ -1342,13 +1374,13 @@ async function runLocalCodexAppServer(options) {
1342
1374
  const now = new Date();
1343
1375
  const createdAt = now.toISOString();
1344
1376
  const runId = options.runId ?? `codex-app-server-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
1345
- const artifactRoot = path.join(".humanish", "runs", runId);
1346
- const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
1347
- const eventsPath = path.join(absoluteArtifactRoot, "events.ndjson");
1348
- const packageName = await readPackageName(options.cwd);
1349
- const humanishSource = await directoryExists(path.join(options.cwd, "humanish")) ? "present" : "missing";
1377
+ const packageName = await readPackageName(options.projectRoot);
1378
+ const humanishSource = await implicitProjectDirectoryExists(options.projectRoot, "humanish") ? "present" : "missing";
1350
1379
  const source = await buildRunSource({ cwd: options.cwd, capturedAt: createdAt, humanishSource, packageName });
1351
- const selection = await loadDryRunSelection(options.cwd, humanishSource);
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;
1352
1384
  if (humanishSource === "missing") {
1353
1385
  warnings.push("Committed humanish/ source was not found; using built-in synthetic Codex app-server actor defaults.");
1354
1386
  }
@@ -1365,14 +1397,12 @@ async function runLocalCodexAppServer(options) {
1365
1397
  ...(streamId === undefined ? {} : { streamId })
1366
1398
  });
1367
1399
  };
1368
- await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
1369
- await mkdir(path.join(absoluteArtifactRoot, "actors"), { recursive: true });
1370
- await writeJson(path.join(options.cwd, ".humanish", "runs", "latest.json"), {
1400
+ await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
1371
1401
  schema: "humanish.latest-run.v1",
1372
1402
  runId,
1373
1403
  path: artifactRoot,
1374
1404
  updatedAt: createdAt
1375
- });
1405
+ }, null, 2)}\n`, "utf8");
1376
1406
  const lanes = Array.from({ length: options.simCount }, (_, index) => {
1377
1407
  const focus = localCodexExecFocus(index);
1378
1408
  const simId = `sim-${String(index + 1).padStart(2, "0")}`;
@@ -1397,7 +1427,7 @@ async function runLocalCodexAppServer(options) {
1397
1427
  for (const lane of lanes) {
1398
1428
  pushEvent("codex-app-server.running", "Published Codex app-server running snapshot for Observer polling.", "info", lane.simId, lane.streamId);
1399
1429
  }
1400
- await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1430
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1401
1431
  const baseLifecycle = [
1402
1432
  {
1403
1433
  at: createdAt,
@@ -1450,13 +1480,15 @@ async function runLocalCodexAppServer(options) {
1450
1480
  events,
1451
1481
  review: createLocalActorRunningReviewSummary("Codex app-server")
1452
1482
  });
1453
- await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
1483
+ await writeRunBundleArtifacts(runPaths, runningBundle);
1454
1484
  const laneResults = await mapWithConcurrency(lanes, options.simCount, async (lane) => {
1455
- const laneRunRoot = lane.prefix ? path.join(absoluteArtifactRoot, lane.prefix) : absoluteArtifactRoot;
1456
- const result = await getActor("codex-app-server").runSession({
1485
+ const laneRunRoot = lane.prefix
1486
+ ? await prepareContainedOutputDirectoryRoot(runPaths, lane.prefix)
1487
+ : runPaths;
1488
+ const sessionOptions = {
1457
1489
  cwd: options.cwd,
1458
1490
  prompt: lane.prompt,
1459
- runRoot: laneRunRoot,
1491
+ runRoot: "physicalRunRoot" in laneRunRoot ? laneRunRoot.physicalRunRoot : laneRunRoot.physicalPath,
1460
1492
  timeoutMs,
1461
1493
  ...(options.actorCommand === undefined ? {} : { actorCommand: options.actorCommand }),
1462
1494
  approvalPolicy: "never",
@@ -1464,12 +1496,14 @@ async function runLocalCodexAppServer(options) {
1464
1496
  ...(process.env.HUMANISH_CODEX_APP_SERVER_MODEL ? { model: process.env.HUMANISH_CODEX_APP_SERVER_MODEL } : {}),
1465
1497
  sandbox: readCodexAppServerSandboxFromEnv(),
1466
1498
  serviceName: "humanish"
1467
- });
1499
+ };
1500
+ const result = await runCodexAppServerSessionInPreparedRoot(sessionOptions, laneRunRoot);
1468
1501
  return {
1469
1502
  lane,
1470
1503
  result: prefixCodexAppServerResultPaths(result, lane.prefix)
1471
1504
  };
1472
1505
  });
1506
+ await validatePreparedRunArtifactPaths(runPaths);
1473
1507
  const completedAt = new Date().toISOString();
1474
1508
  const statuses = laneResults.map((entry) => entry.result.status);
1475
1509
  const status = aggregateActorStatus(statuses);
@@ -1480,7 +1514,7 @@ async function runLocalCodexAppServer(options) {
1480
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);
1481
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);
1482
1516
  }
1483
- await writeFile(eventsPath, `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1517
+ await writeContainedOutputFile(runPaths, "events.ndjson", `${events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1484
1518
  const bundle = buildLocalCodexAppServerBundle({
1485
1519
  runId,
1486
1520
  simCount: options.simCount,
@@ -1523,7 +1557,13 @@ async function runLocalCodexAppServer(options) {
1523
1557
  events,
1524
1558
  review: createLocalActorReviewSummary(options.simCount === 1 ? "Codex app-server" : "Codex app-server fanout", status, verdictReason)
1525
1559
  });
1526
- await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
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");
1527
1567
  return {
1528
1568
  schema: "humanish.run-result.v1",
1529
1569
  ok: status === "passed",
@@ -1534,7 +1574,7 @@ async function runLocalCodexAppServer(options) {
1534
1574
  artifactRoot,
1535
1575
  bundlePath: path.join(artifactRoot, "run.json"),
1536
1576
  reviewPath: path.join(artifactRoot, "review.md"),
1537
- latestPath: path.join(".humanish", "runs", "latest.json"),
1577
+ latestPath: runPaths.relativeLatestPointer,
1538
1578
  warnings,
1539
1579
  ...(status === "passed"
1540
1580
  ? {}
@@ -1934,13 +1974,23 @@ async function checkCodexWorkspaceTrust(cwd) {
1934
1974
  if (process.env.HUMANISH_SKIP_CODEX_TRUST_PREFLIGHT === "1") {
1935
1975
  return { ok: true };
1936
1976
  }
1937
- const trustRoot = await detectCodexTrustRoot(cwd);
1938
- if (!trustRoot) {
1977
+ const detected = await detectCodexTrustRoot(cwd);
1978
+ if (!detected) {
1939
1979
  return { ok: true };
1940
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;
1941
1991
  const configPath = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "config.toml");
1942
1992
  const configText = await readTextIfExists(configPath);
1943
- if (configText && codexConfigTrustsProject(configText, trustRoot)) {
1993
+ if (configText && await codexConfigTrustsProject(configText, trustRoot)) {
1944
1994
  return { ok: true };
1945
1995
  }
1946
1996
  return {
@@ -1951,42 +2001,16 @@ async function checkCodexWorkspaceTrust(cwd) {
1951
2001
  };
1952
2002
  }
1953
2003
  async function detectCodexTrustRoot(cwd) {
1954
- const worktreeRoot = await findGitWorktreeRoot(cwd);
1955
- if (!worktreeRoot) {
2004
+ const inspection = await inspectVerifiedGitWorkspace(cwd);
2005
+ if (inspection.status === "missing") {
1956
2006
  return null;
1957
2007
  }
1958
- const dotGitPath = path.join(worktreeRoot, ".git");
1959
- if (await directoryExists(dotGitPath)) {
1960
- return worktreeRoot;
1961
- }
1962
- const gitFile = await readTextIfExists(dotGitPath);
1963
- if (!gitFile?.startsWith("gitdir:")) {
1964
- return worktreeRoot;
1965
- }
1966
- const gitDir = gitFile.slice("gitdir:".length).trim();
1967
- const absoluteGitDir = path.isAbsolute(gitDir) ? gitDir : path.resolve(worktreeRoot, gitDir);
1968
- const commonDirText = await readTextIfExists(path.join(absoluteGitDir, "commondir"));
1969
- if (!commonDirText) {
1970
- return worktreeRoot;
1971
- }
1972
- const commonDir = commonDirText.trim();
1973
- const absoluteCommonDir = path.resolve(absoluteGitDir, commonDir);
1974
- return path.basename(absoluteCommonDir) === ".git" ? path.dirname(absoluteCommonDir) : worktreeRoot;
1975
- }
1976
- async function findGitWorktreeRoot(cwd) {
1977
- let current = path.resolve(cwd);
1978
- while (true) {
1979
- if (await fileExists(path.join(current, ".git")) || await directoryExists(path.join(current, ".git"))) {
1980
- return current;
1981
- }
1982
- const parent = path.dirname(current);
1983
- if (parent === current) {
1984
- return null;
1985
- }
1986
- current = parent;
2008
+ if (inspection.status === "unsafe") {
2009
+ return { unsafe: true, worktreeRoot: inspection.worktreeRoot };
1987
2010
  }
2011
+ return { unsafe: false, trustRoot: inspection.workspace.trustRoot };
1988
2012
  }
1989
- function codexConfigTrustsProject(configText, trustRoot) {
2013
+ async function codexConfigTrustsProject(configText, trustRoot) {
1990
2014
  const sectionPattern = /^\[projects\."((?:\\.|[^"\\])*)"\]\s*$/gm;
1991
2015
  let sectionMatch;
1992
2016
  while ((sectionMatch = sectionPattern.exec(configText)) !== null) {
@@ -1994,7 +2018,7 @@ function codexConfigTrustsProject(configText, trustRoot) {
1994
2018
  const afterSection = configText.slice(sectionMatch.index + sectionMatch[0].length);
1995
2019
  const nextSectionIndex = afterSection.search(/^\[/m);
1996
2020
  const sectionBody = nextSectionIndex === -1 ? afterSection : afterSection.slice(0, nextSectionIndex);
1997
- if (/^trust_level\s*=\s*"trusted"\s*$/m.test(sectionBody) && isSamePath(projectPath, trustRoot)) {
2021
+ if (/^trust_level\s*=\s*"trusted"\s*$/m.test(sectionBody) && await isSamePhysicalPath(projectPath, trustRoot)) {
1998
2022
  return true;
1999
2023
  }
2000
2024
  }
@@ -2003,10 +2027,17 @@ function codexConfigTrustsProject(configText, trustRoot) {
2003
2027
  function unescapeTomlString(value) {
2004
2028
  return value.replace(/\\(["\\])/g, "$1");
2005
2029
  }
2006
- function isSamePath(candidatePath, targetPath) {
2007
- const candidate = path.resolve(candidatePath);
2008
- const target = path.resolve(targetPath);
2009
- return candidate === target;
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
+ }
2010
2041
  }
2011
2042
  function normalizeActorTimeout(value) {
2012
2043
  if (normalizePositiveInteger(value) === null || value === undefined || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
@@ -2251,9 +2282,40 @@ function normalizeSimCount(value) {
2251
2282
  }
2252
2283
  export async function verifyRun(cwdInput, runInput) {
2253
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) {
2254
2317
  const checks = [];
2255
- const resolved = await resolveRunPath(cwd, runInput);
2256
- if (!resolved) {
2318
+ if (!runPaths) {
2257
2319
  return {
2258
2320
  schema: VERIFY_SCHEMA,
2259
2321
  ok: false,
@@ -2276,11 +2338,11 @@ export async function verifyRun(cwdInput, runInput) {
2276
2338
  }
2277
2339
  };
2278
2340
  }
2279
- const bundlePath = path.join(resolved, "run.json");
2280
- const bundle = await readJsonIfExists(bundlePath);
2281
- const cleanupJson = await readJsonIfExists(path.join(resolved, "cleanup.json"));
2282
- const reviewJson = await readJsonIfExists(path.join(resolved, "review.json"));
2283
- const reviewMarkdown = await readTextIfExists(path.join(resolved, "review.md"));
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");
2284
2346
  checks.push({
2285
2347
  name: "run.json exists",
2286
2348
  ok: bundle !== null,
@@ -2306,7 +2368,7 @@ export async function verifyRun(cwdInput, runInput) {
2306
2368
  ok: reviewJson !== null && reviewMarkdown !== null,
2307
2369
  message: "review.json and review.md must exist"
2308
2370
  });
2309
- const publicSafetyFindings = await scanRunPublicSafetyArtifacts(resolved);
2371
+ const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths);
2310
2372
  checks.push({
2311
2373
  name: "public-safety scan",
2312
2374
  ok: publicSafetyFindings.length === 0,
@@ -2315,7 +2377,7 @@ export async function verifyRun(cwdInput, runInput) {
2315
2377
  : `public-safety findings: ${publicSafetyFindings.slice(0, 5).join(", ")}`
2316
2378
  });
2317
2379
  const missingEvidenceArtifacts = isRunBundle(bundle)
2318
- ? await missingLocalEvidenceArtifacts(resolved, bundle)
2380
+ ? await missingLocalEvidenceArtifacts(runPaths, bundle)
2319
2381
  : [];
2320
2382
  const invalidEvidenceReferences = isRunBundle(bundle)
2321
2383
  ? invalidRunEvidenceReferences(bundle)
@@ -2330,7 +2392,7 @@ export async function verifyRun(cwdInput, runInput) {
2330
2392
  : `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`
2331
2393
  });
2332
2394
  const terminalProductFindings = isRunBundle(bundle)
2333
- ? await validateTerminalProductEvidence(resolved, bundle)
2395
+ ? await validateTerminalProductEvidence(runPaths, bundle)
2334
2396
  : [];
2335
2397
  checks.push({
2336
2398
  name: "terminal-product evidence",
@@ -2340,7 +2402,7 @@ export async function verifyRun(cwdInput, runInput) {
2340
2402
  : `terminal-product findings: ${terminalProductFindings.join(", ")}`
2341
2403
  });
2342
2404
  const codexAppServerFindings = isRunBundle(bundle)
2343
- ? await validateCodexAppServerEvidence(resolved, bundle)
2405
+ ? await validateCodexAppServerEvidence(runPaths, bundle)
2344
2406
  : [];
2345
2407
  checks.push({
2346
2408
  name: "codex app-server evidence",
@@ -2435,7 +2497,27 @@ export async function verifyRun(cwdInput, runInput) {
2435
2497
  export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2436
2498
  const cwd = path.resolve(cwdInput);
2437
2499
  const checkedAt = (hooks.now ?? (() => new Date()))().toISOString();
2438
- const resolved = await resolveRunPath(cwd, runInput);
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
+ }
2439
2521
  if (!resolved) {
2440
2522
  return {
2441
2523
  schema: CLEANUP_SCHEMA,
@@ -2453,9 +2535,20 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2453
2535
  }
2454
2536
  };
2455
2537
  }
2456
- const bundlePath = path.join(resolved, "run.json");
2457
- const cleanupPath = path.join(resolved, "cleanup.json");
2458
- const bundle = await readJsonIfExists(bundlePath);
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
+ }
2459
2552
  if (!isRunBundle(bundle)) {
2460
2553
  return {
2461
2554
  schema: CLEANUP_SCHEMA,
@@ -2476,7 +2569,6 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2476
2569
  }
2477
2570
  const resources = [];
2478
2571
  const warnings = [];
2479
- let desktopModule = null;
2480
2572
  const providerResources = bundle.providerResources ?? [];
2481
2573
  for (const resource of providerResources) {
2482
2574
  if (resource.provider !== "e2b-desktop" || resource.kind !== "sandbox") {
@@ -2499,41 +2591,22 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2499
2591
  });
2500
2592
  continue;
2501
2593
  }
2502
- try {
2503
- desktopModule ??= await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
2504
- if (typeof desktopModule.Sandbox.kill !== "function") {
2505
- resources.push({
2506
- provider: resource.provider,
2507
- kind: resource.kind,
2508
- id: resource.id,
2509
- status: "failed",
2510
- message: "installed @e2b/desktop SDK does not expose Sandbox.kill"
2511
- });
2512
- continue;
2513
- }
2514
- await desktopModule.Sandbox.kill(resource.id, { requestTimeoutMs: 60_000 });
2515
- resources.push({
2516
- provider: resource.provider,
2517
- kind: resource.kind,
2518
- id: resource.id,
2519
- status: "killed",
2520
- message: "resource killed by exact recorded id"
2521
- });
2522
- }
2523
- catch (error) {
2524
- resources.push({
2525
- provider: resource.provider,
2526
- kind: resource.kind,
2527
- id: resource.id,
2528
- status: "failed",
2529
- message: error instanceof Error ? error.message : String(error)
2530
- });
2531
- }
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
+ });
2532
2601
  }
2533
2602
  let adapterResults = [];
2534
2603
  if (hooks.cleanupAdapterResources) {
2535
2604
  try {
2536
- adapterResults = await hooks.cleanupAdapterResources({ cwd, runDir: resolved, bundle });
2605
+ adapterResults = await hooks.cleanupAdapterResources({
2606
+ cwd,
2607
+ runDir: runPaths.physicalRunRoot,
2608
+ bundle
2609
+ });
2537
2610
  }
2538
2611
  catch (error) {
2539
2612
  adapterResults = [{
@@ -2542,9 +2615,10 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2542
2615
  message: error instanceof Error ? error.message : String(error)
2543
2616
  }];
2544
2617
  }
2618
+ await validatePreparedRunArtifactPaths(runPaths);
2545
2619
  }
2546
2620
  if (providerResources.length === 0 && adapterResults.length === 0) {
2547
- warnings.push("Run bundle recorded no run-owned provider resources; nothing to clean.");
2621
+ warnings.push("Run bundle recorded no provider resource evidence; nothing to inspect.");
2548
2622
  }
2549
2623
  const summary = {
2550
2624
  resources: resources.length,
@@ -2568,35 +2642,58 @@ export async function cleanupRun(cwdInput, runInput, hooks = {}) {
2568
2642
  adapterResults,
2569
2643
  warnings
2570
2644
  };
2571
- await writeJson(cleanupPath, result);
2645
+ await validatePreparedRunArtifactPaths(runPaths);
2646
+ await writeContainedOutputFile(runPaths, "cleanup.json", `${JSON.stringify(result, null, 2)}\n`, "utf8");
2572
2647
  return result;
2573
2648
  }
2574
2649
  export async function loadRunBundle(cwdInput, runInput) {
2575
2650
  const cwd = path.resolve(cwdInput);
2576
- const resolved = await resolveRunPath(cwd, runInput);
2577
- if (!resolved) {
2651
+ const runPaths = await resolveRunPath(cwd, runInput).catch(() => null);
2652
+ if (!runPaths) {
2578
2653
  return null;
2579
2654
  }
2580
- const bundlePath = path.join(resolved, "run.json");
2581
- const bundle = await readJsonIfExists(bundlePath);
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");
2582
2663
  if (!isRunBundle(bundle)) {
2583
2664
  return null;
2584
2665
  }
2585
2666
  return {
2586
2667
  bundle,
2587
2668
  bundlePath: path.relative(cwd, bundlePath),
2588
- runDir: resolved
2669
+ runDir: runPaths.absoluteRunRoot
2589
2670
  };
2590
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
+ }
2591
2683
  export async function listRuns(cwdInput) {
2592
2684
  const cwd = path.resolve(cwdInput);
2593
- const runsRoot = path.join(cwd, ".humanish", "runs");
2685
+ const runsRootPath = resolveRunsRoot(cwd);
2594
2686
  // ENOENT (no .humanish/runs yet) is a normal empty state: ok:true, no runs. Any
2595
2687
  // other readdir failure (e.g. permission denied) is a real I/O failure and must
2596
2688
  // not be swallowed into a false "no runs" report.
2597
2689
  let entries;
2690
+ let runsRoot = null;
2598
2691
  try {
2599
- entries = await readdir(runsRoot, { withFileTypes: true });
2692
+ runsRoot = await bindExistingManagedHumanishOutputDirectory(cwd, "runs");
2693
+ entries = runsRoot ? await readdir(runsRoot.physicalPath) : [];
2694
+ if (runsRoot) {
2695
+ await assertPreparedSelectedOutputDirectory(runsRoot);
2696
+ }
2600
2697
  }
2601
2698
  catch (error) {
2602
2699
  if (isNodeError(error) && error.code === "ENOENT") {
@@ -2608,24 +2705,53 @@ export async function listRuns(cwdInput) {
2608
2705
  }
2609
2706
  let latest;
2610
2707
  try {
2611
- latest = await readLatest(cwd);
2708
+ latest = runsRoot ? await readLatest(runsRoot) : null;
2612
2709
  }
2613
2710
  catch (error) {
2614
2711
  return runsUnavailableResult(cwd, error);
2615
2712
  }
2616
2713
  const runs = [];
2617
- for (const entry of entries) {
2618
- if (!entry.isDirectory()) {
2714
+ for (const entryName of entries) {
2715
+ if (entryName === "latest.json" || !isSafeRunIdSegment(entryName)) {
2619
2716
  continue;
2620
2717
  }
2621
- const bundle = await readJsonIfExists(path.join(runsRoot, entry.name, "run.json"));
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");
2622
2740
  runs.push({
2623
- runId: entry.name,
2741
+ runId: entryName,
2624
2742
  createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
2625
2743
  mode: isRecord(bundle) && typeof bundle.mode === "string" ? bundle.mode : null,
2626
- path: path.join(".humanish", "runs", entry.name)
2744
+ path: path.join(RUNS_RELATIVE_ROOT, entryName)
2627
2745
  });
2628
2746
  }
2747
+ if (runsRoot) {
2748
+ try {
2749
+ await assertPreparedSelectedOutputDirectory(runsRoot);
2750
+ }
2751
+ catch (error) {
2752
+ return runsUnavailableResult(cwd, error);
2753
+ }
2754
+ }
2629
2755
  return {
2630
2756
  schema: RUNS_SCHEMA,
2631
2757
  ok: true,
@@ -2648,13 +2774,19 @@ function runsUnavailableResult(cwd, error) {
2648
2774
  };
2649
2775
  }
2650
2776
  export async function readReview(cwdInput, runInput) {
2651
- const verified = await verifyRun(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);
2652
2786
  if (!verified.ok || !verified.bundlePath) {
2653
2787
  return verified;
2654
2788
  }
2655
- const cwd = path.resolve(cwdInput);
2656
- const runDir = path.dirname(path.join(cwd, verified.bundlePath));
2657
- const review = await readJsonIfExists(path.join(runDir, "review.json"));
2789
+ const review = runPaths ? await readRunJsonIfExists(runPaths, "review.json") : null;
2658
2790
  if (!isReviewSummary(review)) {
2659
2791
  return {
2660
2792
  ...verified,
@@ -2667,32 +2799,63 @@ export async function readReview(cwdInput, runInput) {
2667
2799
  }
2668
2800
  return {
2669
2801
  ...review,
2670
- path: path.relative(cwd, path.join(runDir, "review.json")),
2671
- runId: path.basename(runDir)
2802
+ path: path.relative(cwd, path.join(runPaths.absoluteRunRoot, "review.json")),
2803
+ runId: path.basename(runPaths.absoluteRunRoot)
2672
2804
  };
2673
2805
  }
2674
2806
  export async function doctor(cwdInput) {
2675
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
+ };
2676
2839
  const checks = [
2677
2840
  {
2678
2841
  name: "target cwd",
2679
- ok: await directoryExists(cwd),
2842
+ ok: true,
2680
2843
  message: "target directory exists"
2681
2844
  },
2682
2845
  {
2683
2846
  name: "package.json",
2684
- ok: await fileExists(path.join(cwd, "package.json")),
2685
- message: "package.json is present"
2847
+ ok: await safeCheck(async () => await readImplicitProjectFile(projectRoot, "package.json") !== null),
2848
+ message: "package.json is present and safe to read"
2686
2849
  },
2687
2850
  {
2688
2851
  name: "humanish source",
2689
- ok: await directoryExists(path.join(cwd, "humanish")),
2690
- message: "committed humanish/ source directory is present"
2852
+ ok: await safeCheck(() => implicitProjectDirectoryExists(projectRoot, "humanish")),
2853
+ message: "committed humanish/ source directory is present and safe to read"
2691
2854
  },
2692
2855
  {
2693
2856
  name: "runtime ignore",
2694
- ok: (await readTextIfExists(path.join(cwd, ".gitignore")))?.includes(".humanish/") ?? false,
2695
- message: ".gitignore contains .humanish/"
2857
+ ok: await safeCheck(async () => (await readImplicitProjectFile(projectRoot, ".gitignore"))?.includes(".humanish/") ?? false),
2858
+ message: ".gitignore safely contains .humanish/"
2696
2859
  }
2697
2860
  ];
2698
2861
  return {
@@ -2749,7 +2912,83 @@ function createLocalActorReviewSummary(actorLabel, status, reason) {
2749
2912
  ]
2750
2913
  };
2751
2914
  }
2752
- async function loadDryRunSelection(cwd, humanishSource) {
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) {
2753
2992
  const warnings = [];
2754
2993
  if (humanishSource === "missing") {
2755
2994
  return {
@@ -2761,9 +3000,9 @@ async function loadDryRunSelection(cwd, humanishSource) {
2761
3000
  }
2762
3001
  const personaPath = "humanish/personas/synthetic-new-user.yaml";
2763
3002
  const scenarioPath = "humanish/scenarios/first-run-smoke.yaml";
2764
- const personaText = await readTextIfExists(path.join(cwd, personaPath));
2765
- const scenarioText = await readTextIfExists(path.join(cwd, scenarioPath));
2766
- const browserJourneySelection = await loadBrowserPersonaJourneySelection(cwd);
3003
+ const personaText = await readImplicitProjectFile(projectRoot, personaPath);
3004
+ const scenarioText = await readImplicitProjectFile(projectRoot, scenarioPath);
3005
+ const browserJourneySelection = await loadBrowserPersonaJourneySelection(projectRoot);
2767
3006
  if (personaText === null) {
2768
3007
  warnings.push(`${personaPath} was not found; using built-in persona defaults.`);
2769
3008
  }
@@ -2808,15 +3047,9 @@ async function loadDryRunSelection(cwd, humanishSource) {
2808
3047
  warnings: [...warnings, ...browserJourneySelection.warnings]
2809
3048
  };
2810
3049
  }
2811
- async function loadBrowserPersonaJourneySelection(cwd) {
3050
+ async function loadBrowserPersonaJourneySelection(projectRoot) {
2812
3051
  const warnings = [];
2813
- const scenarioDir = path.join(cwd, "humanish", "scenarios");
2814
- const names = await readdir(scenarioDir).catch((error) => {
2815
- if (isNodeError(error) && error.code === "ENOENT") {
2816
- return [];
2817
- }
2818
- throw error;
2819
- });
3052
+ const names = await listImplicitProjectDirectory(projectRoot, "humanish/scenarios");
2820
3053
  const files = names
2821
3054
  .filter((name) => name.endsWith(".yaml") || name.endsWith(".yml"))
2822
3055
  .sort((left, right) => {
@@ -2828,8 +3061,7 @@ async function loadBrowserPersonaJourneySelection(cwd) {
2828
3061
  });
2829
3062
  for (const name of files) {
2830
3063
  const relativePath = path.join("humanish", "scenarios", name);
2831
- const absolutePath = path.join(cwd, relativePath);
2832
- const text = await readTextIfExists(absolutePath);
3064
+ const text = await readImplicitProjectFile(projectRoot, relativePath);
2833
3065
  if (text === null) {
2834
3066
  continue;
2835
3067
  }
@@ -2904,25 +3136,71 @@ function escapeRegExp(value) {
2904
3136
  }
2905
3137
  async function resolveRunPath(cwd, runInput) {
2906
3138
  if (runInput === "latest") {
2907
- const latest = await readLatest(cwd);
2908
- return latest ? path.join(cwd, latest.path) : null;
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;
2909
3158
  }
2910
- const direct = path.join(cwd, ".humanish", "runs", runInput);
2911
- return await directoryExists(direct) ? direct : null;
3159
+ return bindExistingRunArtifactPaths(cwd, runInput);
2912
3160
  }
2913
- async function readLatest(cwd) {
2914
- const latest = await readJsonIfExists(path.join(cwd, ".humanish", "runs", "latest.json"));
2915
- if (isRunPointer(latest)) {
2916
- return latest;
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 });
2917
3166
  }
2918
- return null;
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;
2919
3188
  }
2920
- async function readPackageName(cwd) {
2921
- const packageJson = await readJsonIfExists(path.join(cwd, "package.json"));
2922
- return isRecord(packageJson) && typeof packageJson.name === "string" ? packageJson.name : null;
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
+ }
2923
3201
  }
2924
- async function readJsonIfExists(filePath) {
2925
- const text = await readTextIfExists(filePath);
3202
+ async function readRunJsonIfExists(runPaths, ...segments) {
3203
+ const text = await readRunTextIfExists(runPaths, ...segments);
2926
3204
  if (text === null) {
2927
3205
  return null;
2928
3206
  }
@@ -2933,6 +3211,33 @@ async function readJsonIfExists(filePath) {
2933
3211
  return null;
2934
3212
  }
2935
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
+ }
2936
3241
  async function readTextIfExists(filePath) {
2937
3242
  try {
2938
3243
  return await readFile(filePath, "utf8");
@@ -2944,22 +3249,18 @@ async function readTextIfExists(filePath) {
2944
3249
  throw error;
2945
3250
  }
2946
3251
  }
2947
- async function writeJson(filePath, value) {
2948
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
2949
- }
2950
- async function writeRunBundleArtifacts(absoluteArtifactRoot, bundle) {
3252
+ async function writeRunBundleArtifacts(runPaths, bundle) {
2951
3253
  const publicBundle = {
2952
3254
  ...bundle,
2953
3255
  cwd: PUBLIC_TARGET_CWD
2954
3256
  };
2955
- await writeJson(path.join(absoluteArtifactRoot, "run.json"), publicBundle);
2956
- await writeJson(path.join(absoluteArtifactRoot, "review.json"), publicBundle.review);
2957
- await writeFile(path.join(absoluteArtifactRoot, "review.md"), renderReviewMarkdown(publicBundle), "utf8");
2958
- await writeFile(path.join(absoluteArtifactRoot, "events.ndjson"), `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
2959
- await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
2960
- await writeJson(path.join(absoluteArtifactRoot, "observer", "observer-data.json"), buildObserverData(publicBundle));
2961
- }
2962
- async function missingLocalEvidenceArtifacts(runRoot, bundle) {
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) {
2963
3264
  const requiredPaths = new Map();
2964
3265
  const addRequiredPath = (artifactPath, options = {}) => {
2965
3266
  const existing = requiredPaths.get(artifactPath);
@@ -2990,15 +3291,13 @@ async function missingLocalEvidenceArtifacts(runRoot, bundle) {
2990
3291
  }
2991
3292
  const missing = [];
2992
3293
  for (const [artifactPath, requirements] of requiredPaths) {
2993
- const absolutePath = path.join(runRoot, artifactPath);
2994
- const stats = await stat(absolutePath).catch(() => null);
2995
- if (!stats?.isFile() || stats.size <= 0) {
3294
+ const bytes = await readSafeRunArtifactBytes(runPaths, artifactPath);
3295
+ if (!bytes || bytes.length === 0) {
2996
3296
  missing.push(artifactPath);
2997
3297
  continue;
2998
3298
  }
2999
3299
  if (requirements.screenshot) {
3000
- const bytes = await readFile(absolutePath).catch(() => null);
3001
- const imageError = bytes ? screenshotEvidenceError(artifactPath, bytes) : "could not read screenshot bytes";
3300
+ const imageError = screenshotEvidenceError(artifactPath, bytes);
3002
3301
  if (imageError) {
3003
3302
  missing.push(`${artifactPath} (${imageError})`);
3004
3303
  }
@@ -3060,7 +3359,7 @@ const TERMINAL_TRANSCRIPT_FILE = "terminal-transcript.txt";
3060
3359
  * file is already caught by scanRunPublicSafetyArtifacts; this check enforces the STRUCTURAL
3061
3360
  * evidence + the proven-teardown invariant. Dry-run/contract bundles are exempt (mode !== live).
3062
3361
  */
3063
- async function validateTerminalProductEvidence(runRoot, bundle) {
3362
+ async function validateTerminalProductEvidence(runPaths, bundle) {
3064
3363
  if (bundle.mode !== "live") {
3065
3364
  return [];
3066
3365
  }
@@ -3073,7 +3372,7 @@ async function validateTerminalProductEvidence(runRoot, bundle) {
3073
3372
  return findings;
3074
3373
  }
3075
3374
  // The lane writes exactly one terminal run's ledgers/evidence at fixed paths in the run root.
3076
- const ledgers = await readJsonIfExists(path.join(runRoot, TERMINAL_LEDGERS_FILE));
3375
+ const ledgers = await readSafeRunArtifactJson(runPaths, TERMINAL_LEDGERS_FILE);
3077
3376
  if (!isRecord(ledgers) || ledgers.schema !== "humanish.terminal-ledgers.v1") {
3078
3377
  findings.push(`missing or malformed ${TERMINAL_LEDGERS_FILE} (humanish.terminal-ledgers.v1)`);
3079
3378
  return findings;
@@ -3108,17 +3407,17 @@ async function validateTerminalProductEvidence(runRoot, bundle) {
3108
3407
  // The redacted exec-stream + normalized transcript artifacts must be WRITTEN (the producer
3109
3408
  // always writes them on the live path, even empty for a no-output blocked run — so absence is a
3110
3409
  // real evidence gap, while emptiness is legitimate and keeps blocked runs verifiable).
3111
- if (!(await fileExists(path.join(runRoot, TERMINAL_EVENTS_FILE)))) {
3410
+ if (!(await readSafeRunArtifactBytes(runPaths, TERMINAL_EVENTS_FILE))) {
3112
3411
  findings.push(`missing terminal event stream artifact (${TERMINAL_EVENTS_FILE})`);
3113
3412
  }
3114
- if (!(await fileExists(path.join(runRoot, TERMINAL_TRANSCRIPT_FILE)))) {
3413
+ if (!(await readSafeRunArtifactBytes(runPaths, TERMINAL_TRANSCRIPT_FILE))) {
3115
3414
  findings.push(`missing normalized terminal transcript artifact (${TERMINAL_TRANSCRIPT_FILE})`);
3116
3415
  }
3117
3416
  // The provider-neutral actor trace must be on the terminal lane with redaction passed.
3118
3417
  for (const stream of terminalStreams) {
3119
3418
  const traceArtifact = stream.artifacts.find((artifact) => artifact.kind === "trace");
3120
3419
  const tracePath = traceArtifact?.path ?? "actor.json";
3121
- const trace = await readJsonIfExists(path.join(runRoot, tracePath));
3420
+ const trace = await readSafeRunArtifactJson(runPaths, tracePath);
3122
3421
  if (!isRecord(trace) || trace.lane !== "terminal") {
3123
3422
  findings.push(`${stream.id} missing terminal-lane actor trace`);
3124
3423
  continue;
@@ -3203,7 +3502,7 @@ function validateTerminalCostEvidence(ledgers) {
3203
3502
  }
3204
3503
  return findings;
3205
3504
  }
3206
- async function validateCodexAppServerEvidence(runRoot, bundle) {
3505
+ async function validateCodexAppServerEvidence(runPaths, bundle) {
3207
3506
  if (bundle.mode !== "live") {
3208
3507
  return [];
3209
3508
  }
@@ -3228,7 +3527,7 @@ async function validateCodexAppServerEvidence(runRoot, bundle) {
3228
3527
  findings.push(`${stream.id} missing codex app-server trace artifact`);
3229
3528
  continue;
3230
3529
  }
3231
- const trace = await readJsonIfExists(path.join(runRoot, traceArtifact.path));
3530
+ const trace = await readSafeRunArtifactJson(runPaths, traceArtifact.path);
3232
3531
  if (!isRecord(trace) || ![CODEX_APP_SERVER_TRACE_SCHEMA, CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA].includes(String(trace.schema))) {
3233
3532
  findings.push(`${stream.id} trace artifact must use ${CODEX_APP_SERVER_TRACE_SCHEMA} or ${CODEX_APP_SERVER_PROJECTED_TRACE_SCHEMA}`);
3234
3533
  }
@@ -3878,34 +4177,46 @@ const riskyPublicArtifactPathSegments = new Set([
3878
4177
  "Secure Preferences",
3879
4178
  "profiles"
3880
4179
  ]);
3881
- async function scanRunPublicSafetyArtifacts(runRoot) {
4180
+ async function scanRunPublicSafetyArtifacts(runPaths) {
3882
4181
  const findings = [];
3883
- await scanRunPublicSafetyDirectory(runRoot, runRoot, findings);
4182
+ await validatePreparedRunArtifactPaths(runPaths);
4183
+ await scanRunPublicSafetyDirectory(runPaths, "", findings);
4184
+ await validatePreparedRunArtifactPaths(runPaths);
3884
4185
  return findings;
3885
4186
  }
3886
- async function scanRunPublicSafetyDirectory(root, current, findings) {
4187
+ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings) {
3887
4188
  if (findings.length >= 50) {
3888
4189
  return;
3889
4190
  }
3890
- const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
3891
- for (const entry of entries) {
3892
- const absolutePath = path.join(current, entry.name);
3893
- const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
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;
3894
4197
  if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
3895
4198
  findings.push(`risky artifact path ${relativePath}`);
3896
4199
  if (findings.length >= 50)
3897
4200
  return;
3898
4201
  }
3899
- if (entry.isDirectory()) {
3900
- await scanRunPublicSafetyDirectory(root, absolutePath, findings);
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);
3901
4211
  if (findings.length >= 50)
3902
4212
  return;
3903
4213
  continue;
3904
4214
  }
3905
- if (!entry.isFile() || !shouldScanTextArtifact(relativePath)) {
4215
+ if (!shouldScanTextArtifact(relativePath)) {
3906
4216
  continue;
3907
4217
  }
3908
- const text = await readFile(absolutePath, "utf8").catch(() => null);
4218
+ const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
4219
+ const text = bytes?.toString("utf8") ?? null;
3909
4220
  if (text !== null && containsSensitivePattern(text)) {
3910
4221
  findings.push(`sensitive text ${relativePath}`);
3911
4222
  if (findings.length >= 50)
@@ -3961,28 +4272,6 @@ async function validateCwd(cwd) {
3961
4272
  throw error;
3962
4273
  }
3963
4274
  }
3964
- async function directoryExists(directoryPath) {
3965
- try {
3966
- return (await stat(directoryPath)).isDirectory();
3967
- }
3968
- catch (error) {
3969
- if (isNodeError(error) && error.code === "ENOENT") {
3970
- return false;
3971
- }
3972
- throw error;
3973
- }
3974
- }
3975
- async function fileExists(filePath) {
3976
- try {
3977
- return (await stat(filePath)).isFile();
3978
- }
3979
- catch (error) {
3980
- if (isNodeError(error) && error.code === "ENOENT") {
3981
- return false;
3982
- }
3983
- throw error;
3984
- }
3985
- }
3986
4275
  function containsSensitivePattern(text) {
3987
4276
  return containsSensitive(text);
3988
4277
  }