humanish 0.121.0 → 0.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTEXT.md +1 -1
  3. package/README.md +33 -6
  4. package/dist/actors/computer-use/actor.d.ts +3 -0
  5. package/dist/actors/computer-use/actor.js +1 -0
  6. package/dist/actors/computer-use/loop/backstop.d.ts +6 -1
  7. package/dist/actors/computer-use/loop/backstop.js +13 -1
  8. package/dist/actors/computer-use/loop/clock.d.ts +20 -0
  9. package/dist/actors/computer-use/loop/clock.js +64 -0
  10. package/dist/actors/computer-use/loop/ending.d.ts +1 -1
  11. package/dist/actors/computer-use/loop/session.d.ts +1 -1
  12. package/dist/actors/computer-use/loop/session.js +2 -1
  13. package/dist/actors/computer-use/loop/types.d.ts +16 -0
  14. package/dist/actors/computer-use/loop.d.ts +1 -1
  15. package/dist/actors/computer-use/loop.js +43 -8
  16. package/dist/actors/computer-use/wait.d.ts +3 -2
  17. package/dist/actors/computer-use/wait.js +5 -4
  18. package/dist/actors/contract.d.ts +3 -3
  19. package/dist/actors/contract.js +3 -0
  20. package/dist/actors/stop-cause.js +1 -0
  21. package/dist/analysis/automatic.js +2 -1
  22. package/dist/analysis/evidence.js +41 -21
  23. package/dist/cli/commands/study-run.js +9 -3
  24. package/dist/feedback/derivative-text.d.ts +12 -0
  25. package/dist/feedback/derivative-text.js +130 -0
  26. package/dist/feedback/export-bundle.js +38 -131
  27. package/dist/feedback/export.js +9 -1
  28. package/dist/observer/run-routes.js +61 -18
  29. package/dist/routes/computer-use/bundle.d.ts +0 -1
  30. package/dist/routes/computer-use/bundle.js +0 -1
  31. package/dist/routes/computer-use/diagnostics.d.ts +1 -1
  32. package/dist/routes/computer-use/diagnostics.js +4 -0
  33. package/dist/routes/computer-use/fanout-bundle.d.ts +2 -3
  34. package/dist/routes/computer-use/fanout-bundle.js +2 -12
  35. package/dist/routes/computer-use/live-phase.d.ts +0 -2
  36. package/dist/routes/computer-use/live-phase.js +2 -4
  37. package/dist/routes/computer-use/participant-execution.d.ts +9 -15
  38. package/dist/routes/computer-use/participant-execution.js +22 -31
  39. package/dist/routes/computer-use/participant-model.js +17 -0
  40. package/dist/routes/computer-use/participant-runs.d.ts +11 -1
  41. package/dist/routes/computer-use/participant-runs.js +31 -4
  42. package/dist/routes/computer-use/plan.js +8 -1
  43. package/dist/routes/computer-use/result.js +1 -2
  44. package/dist/routes/computer-use/types.d.ts +17 -5
  45. package/dist/routes/shared-world/provisioned.js +2 -2
  46. package/dist/run/bundle-shape.js +2 -2
  47. package/dist/run/bundle.d.ts +5 -2
  48. package/dist/run/bundle.js +4 -1
  49. package/dist/run/detail.d.ts +6 -0
  50. package/dist/run/detail.js +14 -1
  51. package/dist/run/host-suspension.d.ts +2 -2
  52. package/dist/run/host-suspension.js +2 -2
  53. package/dist/run/judge.d.ts +1 -1
  54. package/dist/run/locate.js +3 -1
  55. package/dist/run/projection.d.ts +7 -0
  56. package/dist/run/projection.js +10 -0
  57. package/dist/run/run.js +7 -1
  58. package/dist/run/stored-runs.js +10 -4
  59. package/dist/run/stream-shape.d.ts +11 -1
  60. package/dist/run/stream-shape.js +26 -2
  61. package/dist/run/stream-trace.d.ts +68 -0
  62. package/dist/run/stream-trace.js +260 -0
  63. package/dist/run/streams.d.ts +20 -0
  64. package/dist/study/arrivals.d.ts +52 -5
  65. package/dist/study/arrivals.js +105 -15
  66. package/dist/study/bundle-estimate.d.ts +48 -0
  67. package/dist/study/bundle-estimate.js +159 -0
  68. package/dist/study/doctor.js +4 -0
  69. package/dist/study/keys.d.ts +6 -0
  70. package/dist/study/keys.js +4 -0
  71. package/dist/study/migrate/v2.js +8 -5
  72. package/dist/study/parse/actors.d.ts +1 -1
  73. package/dist/study/parse/actors.js +33 -4
  74. package/dist/study/plan-participants.d.ts +10 -1
  75. package/dist/study/plan-participants.js +10 -1
  76. package/dist/study/plan-types.d.ts +3 -0
  77. package/dist/study/plan.js +22 -2
  78. package/dist/study/preflight.js +19 -1
  79. package/dist/study/types.d.ts +26 -0
  80. package/dist/study/validation.d.ts +3 -2
  81. package/dist/study/validation.js +22 -4
  82. package/dist/tui-app.js +87 -87
  83. package/dist/verify/artifacts.d.ts +17 -10
  84. package/dist/verify/artifacts.js +22 -13
  85. package/dist/verify/scan-limit.d.ts +6 -0
  86. package/dist/verify/scan-limit.js +25 -0
  87. package/dist/verify/verify.d.ts +1 -1
  88. package/dist/verify/verify.js +68 -18
  89. package/docs/architecture/github-feedback-loop.md +1 -1
  90. package/docs/contracts/README.md +1 -1
  91. package/docs/contracts/run-bundle.md +89 -8
  92. package/docs/contracts/schemas.md +38 -11
  93. package/package.json +6 -3
@@ -112,6 +112,8 @@ export interface CuaParticipantPlanEntry {
112
112
  targetDigest?: string;
113
113
  /** The participant's declared start, in ms after the run starts its participants. */
114
114
  startAfterMs?: number;
115
+ /** The participant's declared end, on the same clock. */
116
+ endAfterMs?: number;
115
117
  }
116
118
  /** The pre-flight spend and participant plan (pure; printed to stderr + recorded as a bundle event before
117
119
  * any sandbox or provider call; identical in dry-run, marked $0). */
@@ -138,7 +140,8 @@ export interface CuaParticipantPlan {
138
140
  lanes: CuaParticipantPlanEntry[];
139
141
  }
140
142
  /** One participant's outcome in the result projection. Always present in `result.lanes` (length
141
- * 1 at N=1). A `blocked` participant is one the pipeline-gate / fail-fast skipped before it ran. */
143
+ * 1 at N=1). A `blocked` participant is one the pipeline gate or the study budget skipped before
144
+ * it ran. */
142
145
  export interface CuaParticipantResult {
143
146
  id: string;
144
147
  actorType?: string;
@@ -149,7 +152,7 @@ export interface CuaParticipantResult {
149
152
  device: string;
150
153
  /** Requested E2B/X screen resolution. See the run stream's desktopGeometry for measurements. */
151
154
  resolution: [number, number];
152
- /** Terminal participant status; "blocked" = skipped (gate/fail-fast); "contract_proof_only" = dry-run. */
155
+ /** Terminal participant status; "blocked" = skipped (gate or study budget); "contract_proof_only" = dry-run. */
153
156
  status: ActorStatus | "blocked" | "contract_proof_only";
154
157
  ok: boolean;
155
158
  session?: {
@@ -183,7 +186,7 @@ export interface CuaParticipantSummary {
183
186
  total: number;
184
187
  /** Participants whose own verdict is ok (terminal, engaged, no harness error). */
185
188
  passed: number;
186
- /** Participants skipped by the pipeline gate / fail-fast. */
189
+ /** Participants skipped by the pipeline gate or the study budget. */
187
190
  skipped: number;
188
191
  /** Participants that ended in a harness error. */
189
192
  harnessErrors: number;
@@ -363,6 +366,16 @@ export interface ParticipantModelDeps {
363
366
  env: Record<string, string | undefined>;
364
367
  openaiApiKey: string;
365
368
  timeoutMs: number;
369
+ /**
370
+ * The participant's scheduled end on `now`'s clock, when it declares participants[].endAfterMs
371
+ * (scheduledParticipantDeps). Its session ends there with stop cause `scheduled_end`.
372
+ */
373
+ scheduledEnd?: {
374
+ readonly atMs: number;
375
+ readonly windDownMs: number;
376
+ };
377
+ /** The run's clock. A participant with a scheduled end runs its session on it. */
378
+ now: () => number;
366
379
  participantCount: number;
367
380
  artifactRoot: PreparedOutputRoot;
368
381
  redactScreenshots: boolean;
@@ -441,7 +454,7 @@ export interface ParticipantRunOutcome {
441
454
  * folded into bundle.events at build time. Empty on the in-process route (no provisioning). */
442
455
  phaseRecords: SubjectPhaseEvent[];
443
456
  warnings: string[];
444
- /** Set when the participant was skipped by the pipeline gate / fail-fast (a pinned reason). */
457
+ /** Set when the pipeline gate or the study budget skipped the participant (a pinned reason). */
445
458
  skippedReason?: string;
446
459
  /** When a fan-out participant was due and when its desktop was requested. */
447
460
  arrival?: ParticipantArrival;
@@ -481,7 +494,6 @@ export interface CuaFanoutBundleArgs {
481
494
  source: RunBundle["source"];
482
495
  participantPlan: CuaParticipantPlan;
483
496
  rerun?: RunRerunLineage;
484
- failFastReason?: string;
485
497
  /** The run's host suspensions and the participant failures they likely caused. */
486
498
  hostSuspension?: HostSuspensionReading;
487
499
  publicRepo?: string;
@@ -383,8 +383,8 @@ async function publishInProgress(plane, ctx, live, setup) {
383
383
  startParticipantFlush(ctx, live, inProgressBundle);
384
384
  }
385
385
  // Launch N actor sandboxes independently on the study's schedule (runCuaParticipant +
386
- // runOnSchedule; runCuaParticipants would add a pipeline gate and fail-fast). Each actor's window is
387
- // measured on the one orchestrator clock.
386
+ // runOnSchedule; runCuaParticipants would add a pipeline gate). Each actor's window is measured on
387
+ // the one orchestrator clock.
388
388
  function runParticipants(plane, ctx, live, setup) {
389
389
  const { now } = ctx;
390
390
  const participants = ctx.plan.plane.participants;
@@ -1,6 +1,6 @@
1
1
  import { isCommsReceivingEvidence } from "../comms/receiving-evidence.js";
2
2
  import { isCapturedGitState } from "./git-state.js";
3
- import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, } from "./bundle.js";
3
+ import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, } from "./bundle.js";
4
4
  import { CLEANUP_SCHEMA, } from "./results.js";
5
5
  import { isRunSimulationStatus, isRunStream, isRunStreamKind } from "./stream-shape.js";
6
6
  import { isLocalEvidenceArtifactPath } from "./paths.js";
@@ -10,7 +10,7 @@ import { isSharedWorldEvidence } from "./shared-world-shape.js";
10
10
  import { isNonNegativeSafeInteger, isPositiveSafeInteger, isRecord } from "./type-guards.js";
11
11
  export function isRunBundle(value) {
12
12
  return (isRecord(value) &&
13
- value.schema === RUN_BUNDLE_SCHEMA &&
13
+ (value.schema === RUN_BUNDLE_SCHEMA || value.schema === RUN_BUNDLE_V1_SCHEMA) &&
14
14
  (value.commsReceiving === undefined || isCommsReceivingEvidence(value.commsReceiving)) &&
15
15
  (value.publication === undefined ||
16
16
  (isRecord(value.publication) &&
@@ -5,7 +5,10 @@ import type { SharedWorldEvidence } from "./shared-world-evidence.js";
5
5
  import { type RunStudyProvenance } from "./study-provenance.js";
6
6
  import type { RunInterruptSignal } from "./status.js";
7
7
  import type { RunSimulationStatus, RunStream, RunStreamKind } from "./streams.js";
8
- export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
8
+ /** The run bundle version this release writes: each stream's trace is a file run.json indexes. */
9
+ export declare const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v2";
10
+ /** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
11
+ export declare const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
9
12
  /** The run bundle file, relative to the run directory. */
10
13
  export declare const RUN_BUNDLE_FILE = "run.json";
11
14
  export declare const REVIEW_SCHEMA = "humanish.review.v1";
@@ -228,7 +231,7 @@ export interface RunBundle {
228
231
  restrictions: ["real-communications"];
229
232
  };
230
233
  commsReceiving?: CommsReceivingEvidence;
231
- schema: typeof RUN_BUNDLE_SCHEMA;
234
+ schema: typeof RUN_BUNDLE_SCHEMA | typeof RUN_BUNDLE_V1_SCHEMA;
232
235
  runId: string;
233
236
  mode: "dry-run" | "live";
234
237
  simCount: number;
@@ -1,7 +1,10 @@
1
1
  import path from "node:path";
2
2
  import { captureGitState } from "./git-state.js";
3
3
  import { studyFields } from "./study-provenance.js";
4
- export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v1";
4
+ /** The run bundle version this release writes: each stream's trace is a file run.json indexes. */
5
+ export const RUN_BUNDLE_SCHEMA = "humanish.run-bundle.v2";
6
+ /** The version 0.122.0 and earlier wrote, with every trace inline. It is still read. */
7
+ export const RUN_BUNDLE_V1_SCHEMA = "humanish.run-bundle.v1";
5
8
  /** The run bundle file, relative to the run directory. */
6
9
  export const RUN_BUNDLE_FILE = "run.json";
7
10
  export const REVIEW_SCHEMA = "humanish.review.v1";
@@ -36,6 +36,12 @@ export interface RunParticipant {
36
36
  thoughts?: number;
37
37
  /** `null` when declared absent; absent when never recorded. Never coerced to 0. */
38
38
  estimatedCostUsd?: number | null;
39
+ /**
40
+ * Why this participant's evidence cannot be shown: the loader refused its trace (a file that does
41
+ * not match run.json, or run.json items no trace file binds). The participant then has no
42
+ * thought or thought count.
43
+ */
44
+ evidenceUnavailable?: string;
39
45
  }
40
46
  export interface RunDetail {
41
47
  automaticAnalysis?: AutomaticAnalysisView;
@@ -16,6 +16,7 @@ import { savedCaption } from "./participant-caption.js";
16
16
  import { recordedPersonaId, streamParticipantIdOf } from "./participant-records.js";
17
17
  import { ContainedReadRefusedError, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
18
18
  import { isPathInside, resolvePhysicalCwd } from "./paths.js";
19
+ import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
19
20
  const RUN_DETAIL_SCHEMA = "humanish.run-detail.v1";
20
21
  /**
21
22
  * The latest reasoning a participant has recorded.
@@ -72,6 +73,9 @@ function participantFrom(stream, index, savedPersonaId) {
72
73
  ...(typeof trace.counts?.actions === "number" ? { actions: trace.counts.actions } : {}),
73
74
  ...(thoughts > 0 ? { thoughts } : {}),
74
75
  ...costOf(trace),
76
+ ...(stream.traceUnavailable === undefined
77
+ ? {}
78
+ : { evidenceUnavailable: stream.traceUnavailable }),
75
79
  };
76
80
  }
77
81
  /**
@@ -103,6 +107,15 @@ function costOf(trace) {
103
107
  return {};
104
108
  }
105
109
  }
110
+ /**
111
+ * The run's streams with their traces loaded through the loader and its bundle rules, as a screen
112
+ * shows them: a participant whose trace the loader refused carries the reason and none of the
113
+ * items run.json or a file supplied for it.
114
+ */
115
+ async function displayedStreams(runPaths, streams, bundle) {
116
+ const loaded = await loadStreamTraces(runPaths, { ...bundle, streams });
117
+ return displayedTraces(loaded).streams;
118
+ }
106
119
  /**
107
120
  * Read one run's participants. Returns null when the run has no readable bundle yet, an ordinary
108
121
  * state for a run that has just started, not a failure. Throws when run.json is there and cannot
@@ -138,7 +151,7 @@ export async function readRunDetail(cwdInput, runId) {
138
151
  // Absent or torn: the run has not written one yet, or is writing it now.
139
152
  return null;
140
153
  }
141
- const streams = Array.isArray(bundle.streams) ? bundle.streams : [];
154
+ const streams = await displayedStreams(runPaths, Array.isArray(bundle.streams) ? bundle.streams : [], bundle);
142
155
  const observerAbsolute = path.join(runPaths.absoluteRunRoot, "observer", "index.html");
143
156
  const automaticAnalysis = await readAutomaticAnalysis(cwd, runId);
144
157
  return {
@@ -50,7 +50,7 @@ export interface HostSuspensionReading {
50
50
  * without a suspension. A frozen loop's failures surface at the wake (macOS fires every overdue
51
51
  * timer at once) or up to a turn timeout later (Linux timers do not count suspended time), so the
52
52
  * rule reads who was in flight across the suspension. A skip follows another participant's
53
- * harness error (the pipeline gate, fail-fast), so skips count only when the run had harness
54
- * errors and every one was in flight across a suspension.
53
+ * harness error (the pipeline gate), so skips count only when the run had harness errors and
54
+ * every one was in flight across a suspension.
55
55
  */
56
56
  export declare function readHostSuspensions(suspensions: readonly HostSuspension[], participants: readonly SuspendedParticipant[], runStartMs: number): HostSuspensionReading | undefined;
@@ -79,8 +79,8 @@ function inFlight(participant, suspension) {
79
79
  * without a suspension. A frozen loop's failures surface at the wake (macOS fires every overdue
80
80
  * timer at once) or up to a turn timeout later (Linux timers do not count suspended time), so the
81
81
  * rule reads who was in flight across the suspension. A skip follows another participant's
82
- * harness error (the pipeline gate, fail-fast), so skips count only when the run had harness
83
- * errors and every one was in flight across a suspension.
82
+ * harness error (the pipeline gate), so skips count only when the run had harness errors and
83
+ * every one was in flight across a suspension.
84
84
  */
85
85
  export function readHostSuspensions(suspensions, participants, runStartMs) {
86
86
  if (suspensions.length === 0)
@@ -34,7 +34,7 @@ export interface ParticipantFacts {
34
34
  completionReason?: ActorCompletionReason;
35
35
  /** The harness failed before the session reached a terminal status. */
36
36
  sessionError?: string;
37
- /** The pipeline gate or fail-fast skipped the participant. */
37
+ /** The pipeline gate or the study budget skipped the participant. */
38
38
  skipped: boolean;
39
39
  /** hollowCompletion held for its session. */
40
40
  noEngagement: boolean;
@@ -3,6 +3,7 @@ import { bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveExistingRunDir
3
3
  import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, ContainedReadRefusedError, LATEST_POINTER_MAX_BYTES, readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
4
4
  import { RUN_BUNDLE_FILE } from "./bundle.js";
5
5
  import { isRunBundle, isRunPointer } from "./bundle-shape.js";
6
+ import { loadRunTraces } from "./stream-trace.js";
6
7
  import { readUnchangedFile } from "./evidence-files.js";
7
8
  /** The run input that resolves through latest.json rather than naming a run directory. */
8
9
  export const LATEST_RUN_ALIAS = "latest";
@@ -122,7 +123,8 @@ export async function loadRunBundlePrepared(cwdInput, runPaths) {
122
123
  return null;
123
124
  }
124
125
  return {
125
- bundle,
126
+ // A trace that does not match its index throws StreamTraceRefusedError, naming the participant.
127
+ bundle: await loadRunTraces(runPaths, bundle),
126
128
  bundlePath: path.relative(cwd, bundlePath),
127
129
  runDir: runPaths.absoluteRunRoot,
128
130
  };
@@ -1,3 +1,4 @@
1
+ import type { RunParticipant } from "./detail.js";
1
2
  import type { RunIndexEntry } from "./run-index.js";
2
3
  /** One study as the study list shows it. */
3
4
  export interface StudyRollup {
@@ -72,6 +73,12 @@ export interface NormalizedThought {
72
73
  /** True when the source text did not fit and was cut; shown, never silent. */
73
74
  truncated: boolean;
74
75
  }
76
+ /**
77
+ * What a screen quotes for a participant of a run detail: why its evidence is unavailable, else
78
+ * its latest thought. It lives here, beside normalizeThought, so the TUI bundle does not load the
79
+ * run-detail reader.
80
+ */
81
+ export declare function participantNote(participant: Pick<RunParticipant, "evidenceUnavailable" | "thought">): string | undefined;
75
82
  /**
76
83
  * Prepare a participant's recorded reasoning summary for a fixed-width surface. Provider summaries
77
84
  * arrive with markdown section leads (`**Thinking through setup**`) and hard newlines; a terminal
@@ -162,6 +162,16 @@ export function formatDuration(ms) {
162
162
  const hours = Math.floor(minutes / 60);
163
163
  return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
164
164
  }
165
+ /**
166
+ * What a screen quotes for a participant of a run detail: why its evidence is unavailable, else
167
+ * its latest thought. It lives here, beside normalizeThought, so the TUI bundle does not load the
168
+ * run-detail reader.
169
+ */
170
+ export function participantNote(participant) {
171
+ return participant.evidenceUnavailable === undefined
172
+ ? participant.thought?.text
173
+ : `Evidence unavailable: ${participant.evidenceUnavailable}`;
174
+ }
165
175
  /**
166
176
  * Prepare a participant's recorded reasoning summary for a fixed-width surface. Provider summaries
167
177
  * arrive with markdown section leads (`**Thinking through setup**`) and hard newlines; a terminal
package/dist/run/run.js CHANGED
@@ -17,6 +17,7 @@ import { RunSecrets } from "./secrets.js";
17
17
  import { RUN_STATUS_FILE, beginRunStatus, runStatusOutcome, } from "./status.js";
18
18
  import { readRunJsonIfExists, runJsonValue } from "./locate.js";
19
19
  import { hostSuspensionEvents, systemHostClock, } from "./host-suspension.js";
20
+ import { splitStreamTraces } from "./stream-trace.js";
20
21
  /** An entry point called in a state that does not admit it. */
21
22
  class RunLifecycleError extends Error {
22
23
  name = "RunLifecycleError";
@@ -171,7 +172,12 @@ function runPublisher(args) {
171
172
  cwd: PUBLIC_TARGET_CWD,
172
173
  ...(recorded === undefined ? {} : { outcome: recorded }),
173
174
  });
174
- await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(publicBundle), "utf8");
175
+ // Each trace file first, so run.json never indexes a file that is not yet written. The review
176
+ // and the Observer data below read the whole in-memory bundle.
177
+ const split = splitStreamTraces(publicBundle);
178
+ for (const file of split.files)
179
+ await writeContainedOutputFile(paths, file.path, file.bytes);
180
+ await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(split.bundle), "utf8");
175
181
  await afterBundle(publicBundle);
176
182
  // The run writes its own status.json; one it refuses renders as a missing one does.
177
183
  const status = recorded === undefined
@@ -4,11 +4,12 @@ import { redactText } from "../evidence/redaction.js";
4
4
  import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, } from "./paths.js";
5
5
  import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, refusalText, } from "./contained-output.js";
6
6
  import { RUN_BUNDLE_FILE } from "./bundle.js";
7
- import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
7
+ import { isReviewSummary, isRunBundle, isRunOutcome } from "./bundle-shape.js";
8
8
  import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
9
9
  import { RUN_STATUS_FILE } from "./status.js";
10
10
  import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
11
11
  import { withCuaReviewProvenance } from "./outcomes.js";
12
+ import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
12
13
  import { isNodeError, isRecord } from "./type-guards.js";
13
14
  import { invalidRunStorageVerifyResult, verifyResolvedRun, } from "../verify/verify.js";
14
15
  const RUNS_SCHEMA = "humanish.runs-result.v1";
@@ -174,9 +175,14 @@ export async function readReview(cwdInput, runInput) {
174
175
  const bundle = runPaths
175
176
  ? runJsonValue(await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE))
176
177
  : null;
177
- const projected = isRecord(bundle) && Array.isArray(bundle.streams)
178
- ? withCuaReviewProvenance(review, bundle.streams.filter(isRecord))
179
- : review;
178
+ // The completion provenance reads each participant's items, loaded through the loader; a
179
+ // participant whose trace it refused counts without them.
180
+ const streams = runPaths && isRunBundle(bundle)
181
+ ? displayedTraces(await loadStreamTraces(runPaths, bundle)).streams
182
+ : isRecord(bundle) && Array.isArray(bundle.streams)
183
+ ? bundle.streams.filter(isRecord)
184
+ : undefined;
185
+ const projected = streams ? withCuaReviewProvenance(review, streams) : review;
180
186
  const displayed = displayedBundle(bundle);
181
187
  const { display } = runPaths ? await listedRunDisplay(runPaths, bundle) : {};
182
188
  return {
@@ -1,4 +1,14 @@
1
- import { type RunSimulationStatus, type RunStream, type RunStreamKind } from "./streams.js";
1
+ import { type RunSimulationStatus, type RunStream, type RunStreamKind, type StreamTraceIndex } from "./streams.js";
2
2
  export declare function isRunStream(value: unknown): value is RunStream;
3
+ export declare function isRunStreamArtifact(value: unknown): value is RunStream["artifacts"][number];
4
+ /**
5
+ * The trace file names the run writer produces: actor.json for one participant,
6
+ * actors/<stream>.json for more, actor-<surface>.json on the scripted route. A trace is source
7
+ * evidence, so an index naming any other file (the Observer page, which the public-safety scan
8
+ * reads as a regenerated projection) is refused by the shape check and by the loader.
9
+ */
10
+ export declare function isStreamTracePath(value: string): boolean;
11
+ /** A v2 stream's index of its trace file: a local path, its size and sha256, and what it holds. */
12
+ export declare function isStreamTraceIndex(value: unknown): value is StreamTraceIndex;
3
13
  export declare function isRunSimulationStatus(value: unknown): value is RunSimulationStatus;
4
14
  export declare function isRunStreamKind(value: unknown): value is RunStreamKind;
@@ -2,7 +2,7 @@
2
2
  import { desktopRecordingMetadataSchema, } from "../evidence/desktop-recording-types.js";
3
3
  import { isLocalEvidenceArtifactPath } from "./paths.js";
4
4
  import {} from "./streams.js";
5
- import { isFiniteNumber, isPositiveFiniteNumber, isRecord } from "./type-guards.js";
5
+ import { isFiniteNumber, isNonNegativeSafeInteger, isPositiveFiniteNumber, isRecord, } from "./type-guards.js";
6
6
  export function isRunStream(value) {
7
7
  return (isRecord(value) &&
8
8
  typeof value.id === "string" &&
@@ -21,6 +21,7 @@ export function isRunStream(value) {
21
21
  (value.desktopGeometry === undefined || isRunDesktopGeometry(value.desktopGeometry)) &&
22
22
  (value.recording === undefined || isRunDesktopRecording(value.recording)) &&
23
23
  hasConsistentStreamGeometry(value) &&
24
+ (value.trace === undefined || isStreamTraceIndex(value.trace)) &&
24
25
  Array.isArray(value.artifacts) &&
25
26
  value.artifacts.every(isRunStreamArtifact) &&
26
27
  hasConsistentRecordingArtifact(value.recording, value.artifacts));
@@ -120,7 +121,7 @@ function hasConsistentStreamGeometry(value) {
120
121
  function isMeasuredSize(value) {
121
122
  return (isRecord(value) && isPositiveFiniteNumber(value.width) && isPositiveFiniteNumber(value.height));
122
123
  }
123
- function isRunStreamArtifact(value) {
124
+ export function isRunStreamArtifact(value) {
124
125
  return (isRecord(value) &&
125
126
  typeof value.label === "string" &&
126
127
  typeof value.path === "string" &&
@@ -134,6 +135,29 @@ function isRunStreamArtifact(value) {
134
135
  value.kind === "filesystem" ||
135
136
  value.kind === "recording"));
136
137
  }
138
+ /** A name part of a trace file: a stream id or surface id that is a plain file name. */
139
+ const TRACE_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,127}";
140
+ const TRACE_FILE = new RegExp(`^(?:actor|actor-${TRACE_NAME}|actors/${TRACE_NAME})\\.json$`);
141
+ /**
142
+ * The trace file names the run writer produces: actor.json for one participant,
143
+ * actors/<stream>.json for more, actor-<surface>.json on the scripted route. A trace is source
144
+ * evidence, so an index naming any other file (the Observer page, which the public-safety scan
145
+ * reads as a regenerated projection) is refused by the shape check and by the loader.
146
+ */
147
+ export function isStreamTracePath(value) {
148
+ return TRACE_FILE.test(value);
149
+ }
150
+ /** A v2 stream's index of its trace file: a local path, its size and sha256, and what it holds. */
151
+ export function isStreamTraceIndex(value) {
152
+ return (isRecord(value) &&
153
+ typeof value.path === "string" &&
154
+ isStreamTracePath(value.path) &&
155
+ isNonNegativeSafeInteger(value.bytes) &&
156
+ typeof value.sha256 === "string" &&
157
+ /^[0-9a-f]{64}$/.test(value.sha256) &&
158
+ isNonNegativeSafeInteger(value.items) &&
159
+ isNonNegativeSafeInteger(value.screenshots));
160
+ }
137
161
  export function isRunSimulationStatus(value) {
138
162
  return (value === "queued" ||
139
163
  value === "preparing" ||
@@ -0,0 +1,68 @@
1
+ import { type RunBundle } from "./bundle.js";
2
+ import { type PreparedOutputRoot } from "./contained-output.js";
3
+ import type { RunStream } from "./streams.js";
4
+ /** A trace that does not match its index, or cannot be read. The message names the stream. */
5
+ export declare class StreamTraceRefusedError extends Error {
6
+ readonly streamId: string;
7
+ constructor(streamId: string, detail: string);
8
+ }
9
+ /**
10
+ * The run.json the run writes, and the trace files it writes first. Each stream with a finished
11
+ * actor keeps that actor without `items`, its artifacts other than screenshots, and an index of its
12
+ * trace file; the file is the actor trace with the stream's screenshot entries as `artifacts`. A
13
+ * stream already split (its actor read back from a v2 run.json without its items) keeps its index,
14
+ * since its file is on disk.
15
+ */
16
+ export declare function splitStreamTraces(bundle: RunBundle): {
17
+ bundle: RunBundle;
18
+ files: {
19
+ path: string;
20
+ bytes: Buffer;
21
+ }[];
22
+ };
23
+ /**
24
+ * Where a loader reads trace files from: the bytes at a run-relative path, at most `maxBytes`, or
25
+ * why it could not. Prepared run paths read through the contained reader; the Observer server
26
+ * passes a reader over its pinned run folder.
27
+ */
28
+ export type TraceFileReader = (relativePath: string, maxBytes: number) => Promise<{
29
+ bytes: Buffer;
30
+ } | {
31
+ refused: string;
32
+ }>;
33
+ /**
34
+ * The stream as a v1 run.json held it: its actor with every item and every artifact entry. A
35
+ * stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
36
+ * stream's file must have a trace file name (isStreamTracePath); it is read under `maxBytes` and
37
+ * checked against the index's byte count, sha256, item count and screenshot count, and any
38
+ * difference throws StreamTraceRefusedError. The rules that need the whole bundle (an unindexed v2
39
+ * actor, two participants indexing one file) are loadStreamTraces'.
40
+ */
41
+ export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream: RunStream, maxBytes?: number): Promise<RunStream>;
42
+ /**
43
+ * Every stream of the bundle loaded through loadStreamTrace, one file at a time, and the refusal
44
+ * of each stream that could not be (see bundleRefusal for the rules that need the whole bundle).
45
+ * Each refused stream is kept as run.json holds it; a reader that shows the run passes the result
46
+ * to displayedTraces. `source` is the run's prepared paths, or a TraceFileReader. `onTrace`
47
+ * receives each trace file's path and the bytes the stream was joined from, after they matched
48
+ * the index, so a caller scans exactly what it reads.
49
+ */
50
+ export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileReader, bundle: RunBundle, options?: {
51
+ maxBytes?: number;
52
+ onTrace?: (path: string, bytes: Buffer) => void;
53
+ }): Promise<{
54
+ bundle: RunBundle;
55
+ refused: StreamTraceRefusedError[];
56
+ }>;
57
+ /**
58
+ * A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
59
+ * summary, shows none of the items run.json or a file supplied for it (its actor's items, its
60
+ * in-progress items, its screenshot entries), and carries `traceUnavailable` with the reason; a
61
+ * warn event on its timeline says the same.
62
+ */
63
+ export declare function displayedTraces(loaded: {
64
+ bundle: RunBundle;
65
+ refused: readonly StreamTraceRefusedError[];
66
+ }): RunBundle;
67
+ /** The bundle with every stream loaded; the first refusal throws. */
68
+ export declare function loadRunTraces(source: PreparedOutputRoot | TraceFileReader, bundle: RunBundle, maxBytes?: number): Promise<RunBundle>;