humanish 0.122.0 → 0.124.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 (61) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/CONTEXT.md +2 -2
  3. package/README.md +33 -6
  4. package/dist/actors/computer-use/loop/trace.js +6 -1
  5. package/dist/actors/computer-use/openai-conversation.d.ts +0 -6
  6. package/dist/actors/computer-use/openai-conversation.js +0 -8
  7. package/dist/actors/computer-use/openai-provider.js +5 -8
  8. package/dist/analysis/automatic.js +2 -1
  9. package/dist/analysis/evidence.js +41 -21
  10. package/dist/cli/commands/study-run.js +9 -3
  11. package/dist/evidence/image.js +82 -3
  12. package/dist/feedback/derivative-text.d.ts +12 -0
  13. package/dist/feedback/derivative-text.js +130 -0
  14. package/dist/feedback/export-bundle.js +39 -136
  15. package/dist/feedback/export.js +36 -9
  16. package/dist/observer/artifact.js +2 -9
  17. package/dist/observer/data.d.ts +9 -2
  18. package/dist/observer/data.js +17 -2
  19. package/dist/observer/page-data.d.ts +5 -0
  20. package/dist/observer/page-data.js +21 -0
  21. package/dist/observer/participant-items.d.ts +45 -0
  22. package/dist/observer/participant-items.js +85 -0
  23. package/dist/observer/render.js +2 -1
  24. package/dist/observer/run-routes.js +75 -22
  25. package/dist/observer-app.html +4 -4
  26. package/dist/run/bundle-shape.js +2 -2
  27. package/dist/run/bundle.d.ts +5 -2
  28. package/dist/run/bundle.js +4 -1
  29. package/dist/run/detail.d.ts +6 -0
  30. package/dist/run/detail.js +14 -1
  31. package/dist/run/locate.js +3 -1
  32. package/dist/run/projection.d.ts +7 -0
  33. package/dist/run/projection.js +10 -0
  34. package/dist/run/run.js +9 -2
  35. package/dist/run/stored-runs.js +10 -4
  36. package/dist/run/stream-shape.d.ts +11 -1
  37. package/dist/run/stream-shape.js +26 -2
  38. package/dist/run/stream-trace.d.ts +68 -0
  39. package/dist/run/stream-trace.js +260 -0
  40. package/dist/run/streams.d.ts +20 -0
  41. package/dist/study/bundle-estimate.d.ts +48 -0
  42. package/dist/study/bundle-estimate.js +159 -0
  43. package/dist/study/doctor.js +4 -0
  44. package/dist/study/plan-types.d.ts +3 -0
  45. package/dist/study/plan.js +42 -2
  46. package/dist/study/preflight.js +8 -0
  47. package/dist/tui-app.js +87 -87
  48. package/dist/verify/artifacts.d.ts +17 -10
  49. package/dist/verify/artifacts.js +22 -13
  50. package/dist/verify/scan-limit.d.ts +6 -0
  51. package/dist/verify/scan-limit.js +25 -0
  52. package/dist/verify/verify.d.ts +1 -1
  53. package/dist/verify/verify.js +68 -18
  54. package/docs/architecture/github-feedback-loop.md +1 -1
  55. package/docs/architecture/observer.md +24 -5
  56. package/docs/contracts/README.md +1 -1
  57. package/docs/contracts/run-bundle.md +89 -6
  58. package/docs/contracts/schemas.md +14 -7
  59. package/docs/contracts/study-analysis.md +1 -1
  60. package/docs/decisions/0002-observer-is-one-self-contained-file.md +2 -1
  61. package/package.json +7 -4
@@ -2,19 +2,20 @@ import { type PreparedRunArtifactPaths } from "../run/paths.js";
2
2
  import type { RunBundle } from "../run/bundle.js";
3
3
  /** Public-safety and evidence-reference findings stop at this many per list. */
4
4
  export declare const MAX_REPORTED_FINDINGS = 50;
5
- /**
6
- * The most entries, files and directories, the public-safety scan visits in one run. Bundle export
7
- * refuses a run with more, so the scan never passes a run whose files it did not all read.
8
- */
9
- export declare const MAX_RUN_ENTRIES = 10000;
10
5
  /** What the public-safety scan did not read. A run with any of it cannot grade share_ready. */
11
6
  export interface UnscannedArtifacts {
12
7
  /** Run files it could not read as text, by run-relative path. */
13
8
  readonly files: string[];
14
9
  /** Folders it could not list, by run-relative path ("" is the run folder). */
15
10
  readonly unlistedFolders: string[];
16
- /** The folder it was listing when it reached MAX_RUN_ENTRIES. It read nothing listed after. */
17
- stoppedIn?: string;
11
+ /**
12
+ * Where the scan stopped: the folder it was listing when it passed its entry limit
13
+ * (runEntryLimit) and that limit. It read nothing listed after.
14
+ */
15
+ stopped?: {
16
+ readonly folder: string;
17
+ readonly limit: number;
18
+ };
18
19
  }
19
20
  /**
20
21
  * The UNSCANNED_ARTIFACT reason: the files the scan could not read, the folders it could not list
@@ -66,8 +67,14 @@ interface RegisteredStreamMedia {
66
67
  * registered stream media and that the scan cannot read as text (readPlainText), cannot read at
67
68
  * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned.files`, so the caller can
68
69
  * keep the run from grading share_ready. A directory it cannot list goes to
69
- * `unscanned.unlistedFolders`, and the directory it was listing when it passed MAX_RUN_ENTRIES
70
- * entries is `unscanned.stoppedIn`.
70
+ * `unscanned.unlistedFolders`. It visits at most runEntryLimit(`participants`) entries, the
71
+ * participants the run's run.json records, and where it stopped past them is `unscanned.stopped`.
72
+ */
73
+ export declare function scanRunPublicSafetyArtifacts(runPaths: PreparedRunArtifactPaths, derivedFindings: string[], media: RegisteredStreamMedia, unscanned: UnscannedArtifacts, participants: number): Promise<string[]>;
74
+ /**
75
+ * The public-safety scan of one file read as source evidence, an indexed trace, by its bytes and
76
+ * whatever its path: no exemption for a regenerated projection applies. `unscanned` when the bytes
77
+ * are not plain text or hold an encoding the scan cannot read.
71
78
  */
72
- export declare function scanRunPublicSafetyArtifacts(runPaths: PreparedRunArtifactPaths, derivedFindings: string[], media: RegisteredStreamMedia, unscanned: UnscannedArtifacts): Promise<string[]>;
79
+ export declare function scanSourceEvidence(bytes: Buffer): "clean" | "sensitive" | "unscanned";
73
80
  export {};
@@ -9,6 +9,7 @@ import { isLocalEvidenceArtifactPath, isRiskyPublicArtifactPath, validatePrepare
9
9
  import { openContainedRegularFile, RUN_ARTIFACT_MAX_BYTES } from "../run/contained-output.js";
10
10
  import { holdsKeyedSandboxId } from "../run/sandbox-ids.js";
11
11
  import { SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
12
+ import { runEntryLimit } from "./scan-limit.js";
12
13
  import { readSafeRunArtifact, readSafeRunArtifactBytes, readSafeRunArtifactJson, } from "../run/locate.js";
13
14
  import { isRecord } from "../run/type-guards.js";
14
15
  import { TERMINAL_EVENTS_ARTIFACT } from "../run/terminal-contract.js";
@@ -16,11 +17,6 @@ import { plural } from "../run/text.js";
16
17
  import { isZeroEventTerminalTrace } from "./actor.js";
17
18
  /** Public-safety and evidence-reference findings stop at this many per list. */
18
19
  export const MAX_REPORTED_FINDINGS = 50;
19
- /**
20
- * The most entries, files and directories, the public-safety scan visits in one run. Bundle export
21
- * refuses a run with more, so the scan never passes a run whose files it did not all read.
22
- */
23
- export const MAX_RUN_ENTRIES = 10_000;
24
20
  // The UNSCANNED_ARTIFACT reason names this many paths of each kind and counts the rest.
25
21
  const MAX_LISTED_UNSCANNED = 10;
26
22
  /**
@@ -35,11 +31,11 @@ export function unscannedMessage(unscanned) {
35
31
  sentences.push(`The public-safety scan cannot read ${plural(files.length, "file")} as text that ${files.length === 1 ? "is not a stream screenshot under screenshots/ or a registered recording" : "are not stream screenshots under screenshots/ or registered recordings"}: ${listed(files)}.`);
36
32
  if (folders.length > 0)
37
33
  sentences.push(`The public-safety scan could not list ${plural(folders.length, "folder")}, so it read none of the files in ${folders.length === 1 ? "it" : "them"}: ${listed(folders)}.`);
38
- if (unscanned.stoppedIn !== undefined)
39
- sentences.push(`The public-safety scan stopped at its limit of ${MAX_RUN_ENTRIES} files and folders while listing ${folderName(unscanned.stoppedIn)}, so it read nothing listed after that point.`);
34
+ if (unscanned.stopped !== undefined)
35
+ sentences.push(`The public-safety scan stopped at its limit of ${unscanned.stopped.limit} files and folders while listing ${folderName(unscanned.stopped.folder)}, so it read nothing listed after that point.`);
40
36
  if (sentences.length === 0)
41
37
  return undefined;
42
- const onlyFiles = folders.length === 0 && unscanned.stoppedIn === undefined;
38
+ const onlyFiles = folders.length === 0 && unscanned.stopped === undefined;
43
39
  sentences.push(onlyFiles ? "Review them before sharing." : "Review the run folder before sharing.");
44
40
  return sentences.join(" ");
45
41
  }
@@ -323,14 +319,15 @@ export function rawScreenshotStreamIds(bundle, redactedShapeFrames) {
323
319
  * registered stream media and that the scan cannot read as text (readPlainText), cannot read at
324
320
  * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned.files`, so the caller can
325
321
  * keep the run from grading share_ready. A directory it cannot list goes to
326
- * `unscanned.unlistedFolders`, and the directory it was listing when it passed MAX_RUN_ENTRIES
327
- * entries is `unscanned.stoppedIn`.
322
+ * `unscanned.unlistedFolders`. It visits at most runEntryLimit(`participants`) entries, the
323
+ * participants the run's run.json records, and where it stopped past them is `unscanned.stopped`.
328
324
  */
329
- export async function scanRunPublicSafetyArtifacts(runPaths, derivedFindings, media, unscanned) {
325
+ export async function scanRunPublicSafetyArtifacts(runPaths, derivedFindings, media, unscanned, participants) {
330
326
  const findings = [];
331
327
  await validatePreparedRunArtifactPaths(runPaths);
332
328
  await scanRunPublicSafetyDirectory(runPaths, "", findings, derivedFindings, media, unscanned, {
333
329
  entries: 0,
330
+ limit: runEntryLimit(participants),
334
331
  });
335
332
  await validatePreparedRunArtifactPaths(runPaths);
336
333
  return findings;
@@ -351,9 +348,9 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
351
348
  }
352
349
  for (const entryName of entries) {
353
350
  walk.entries += 1;
354
- if (walk.entries > MAX_RUN_ENTRIES) {
351
+ if (walk.entries > walk.limit) {
355
352
  // Every directory the walk returns through comes here once more; the first one stopped it.
356
- unscanned.stoppedIn ??= relativeDirectory;
353
+ unscanned.stopped ??= { folder: relativeDirectory, limit: walk.limit };
357
354
  return;
358
355
  }
359
356
  const relativePath = relativeDirectory ? `${relativeDirectory}/${entryName}` : entryName;
@@ -424,6 +421,18 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
424
421
  }
425
422
  }
426
423
  }
424
+ /**
425
+ * The public-safety scan of one file read as source evidence, an indexed trace, by its bytes and
426
+ * whatever its path: no exemption for a regenerated projection applies. `unscanned` when the bytes
427
+ * are not plain text or hold an encoding the scan cannot read.
428
+ */
429
+ export function scanSourceEvidence(bytes) {
430
+ const decoded = readPlainText(bytes);
431
+ if (!decoded.ok)
432
+ return "unscanned";
433
+ const scan = scanEncodedTextCached(decoded.text);
434
+ return scan.sensitive ? "sensitive" : scan.opaque ? "unscanned" : "clean";
435
+ }
427
436
  function normalizeLocalEvidenceReference(value) {
428
437
  if (!value || value.includes("://") || path.isAbsolute(value)) {
429
438
  return null;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The most entries the scan visits in a run of `participants` participants. run.json states the
3
+ * count, so it is clamped to the most participants a run's analysis covers, which keeps a forged
4
+ * count from making the scan unbounded.
5
+ */
6
+ export declare function runEntryLimit(participants: number): number;
@@ -0,0 +1,25 @@
1
+ // How many files and folders the public-safety scan visits in one run, and bundle export counts
2
+ // before it copies one. It lives apart from the scan so the planner can check a study's estimated
3
+ // file count against it without loading the scanner.
4
+ import { EVIDENCE_LIMITS } from "../analysis/analysis-limits.js";
5
+ /** The entries any run may hold, whatever its participants. */
6
+ const MAX_RUN_ENTRIES = 10_000;
7
+ /**
8
+ * The entries each participant adds to the limit. Measured on 2026-10-12: a participant writes a
9
+ * screenshot a model turn, live participants that worked 5 minutes or more took a turn every 10
10
+ * s (median of 42 sessions), and its trace file, screenshot folder and first frame add 3 more. A
11
+ * participant working an 8-hour window then holds about 2,900 entries; 5,000 leaves room for a
12
+ * dwell window's frames or a faster pace.
13
+ */
14
+ const ENTRIES_PER_PARTICIPANT = 5_000;
15
+ /**
16
+ * The most entries the scan visits in a run of `participants` participants. run.json states the
17
+ * count, so it is clamped to the most participants a run's analysis covers, which keeps a forged
18
+ * count from making the scan unbounded.
19
+ */
20
+ export function runEntryLimit(participants) {
21
+ const counted = Number.isFinite(participants)
22
+ ? Math.min(Math.max(0, Math.floor(participants)), EVIDENCE_LIMITS.participants)
23
+ : 0;
24
+ return MAX_RUN_ENTRIES + ENTRIES_PER_PARTICIPANT * counted;
25
+ }
@@ -17,7 +17,7 @@ export interface VerifyResult {
17
17
  shareSafety: {
18
18
  status: "share_ready" | "local_only" | "blocked";
19
19
  reasons: Array<{
20
- code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "ANALYSIS_UNVERIFIED" | "RAW_SCREENSHOTS" | "RAW_SANDBOX_ID" | "CONTINUOUS_MEDIA" | "REAL_COMMUNICATIONS" | "UNSCANNED_ARTIFACT";
20
+ code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "ANALYSIS_UNVERIFIED" | "RAW_SCREENSHOTS" | "RAW_SANDBOX_ID" | "CONTINUOUS_MEDIA" | "REAL_COMMUNICATIONS" | "UNSCANNED_ARTIFACT" | "RUN_NOT_FINISHED";
21
21
  message: string;
22
22
  }>;
23
23
  };
@@ -5,15 +5,16 @@ import { analysisSharingProblems } from "../analysis/sharing.js";
5
5
  import { containsSensitive, REDACTED_SANDBOX_ID } from "../evidence/redaction.js";
6
6
  import { keyedSandboxIds, readRunSandboxIds } from "../run/sandbox-ids.js";
7
7
  import { validatePreparedRunArtifactPaths } from "../run/paths.js";
8
- import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA } from "../run/bundle.js";
8
+ import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, } from "../run/bundle.js";
9
9
  import { ContainedReadRefusedError, refusalText } from "../run/contained-output.js";
10
10
  import { isCleanupResult, isRunBundle } from "../run/bundle-shape.js";
11
+ import { loadStreamTraces } from "../run/stream-trace.js";
11
12
  import { readRunJsonIfExists, readRunTextIfExists, resolveRunPath, runJsonValue, } from "../run/locate.js";
12
13
  import { SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
13
14
  import { isRecord } from "../run/type-guards.js";
14
15
  import { runNotFoundMessage } from "../run/run-not-found.js";
15
16
  import { actorVerdictConsistencyFindings, noEngagementActorFindings, validateCodexAppServerEvidence, validateTerminalProductEvidence, } from "./actor.js";
16
- import { invalidRunEvidenceReferences, MAX_REPORTED_FINDINGS, missingLocalEvidenceArtifacts, rawScreenshotPostureWarnings, rawScreenshotStreamIds, redactedShapeFramePaths, scanRunPublicSafetyArtifacts, streamScreenshotPaths, unscannedMessage, } from "./artifacts.js";
17
+ import { invalidRunEvidenceReferences, MAX_REPORTED_FINDINGS, missingLocalEvidenceArtifacts, rawScreenshotPostureWarnings, rawScreenshotStreamIds, redactedShapeFramePaths, scanRunPublicSafetyArtifacts, scanSourceEvidence, streamScreenshotPaths, unscannedMessage, } from "./artifacts.js";
17
18
  import { flatContextWarnings } from "./context-growth.js";
18
19
  import { runNotFinished } from "./liveness.js";
19
20
  import { costAndReceiptFindings } from "./costs.js";
@@ -75,7 +76,10 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
75
76
  }
76
77
  const bundlePath = path.join(runPaths.absoluteRunRoot, RUN_BUNDLE_FILE);
77
78
  const bundleRead = await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE);
78
- const bundle = runJsonValue(bundleRead);
79
+ // Every check below reads the streams with their traces loaded; a trace that does not match its
80
+ // index fails the evidence-artifacts check and its stream is checked without its items.
81
+ const traces = await loadTraces(runPaths, runJsonValue(bundleRead));
82
+ const { bundle, refusedTraces } = traces;
79
83
  const cleanupRead = await readRunJsonIfExists(runPaths, "cleanup.json");
80
84
  const review = {
81
85
  json: await readRunJsonIfExists(runPaths, "review.json"),
@@ -103,7 +107,8 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
103
107
  // A run from before 0.110 with no receipts still names its ids in run.json.
104
108
  sandboxIds: [...new Set([...journaledSandboxIds, ...keyedSandboxIds(bundle)])],
105
109
  sandboxIdFiles,
106
- }, unscannedArtifacts);
110
+ }, unscannedArtifacts, isRunBundle(bundle) ? bundle.simulations.length : 0);
111
+ addTraceScan(traces, publicSafetyFindings, unscannedArtifacts);
107
112
  // JSON escapes can hide a sensitive value from the byte scan while the
108
113
  // decoded recording exposes it to Observer, feedback, or analysis input.
109
114
  if (publicSafetyFindings.length < MAX_REPORTED_FINDINGS &&
@@ -125,17 +130,14 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
125
130
  ? await redactedShapeFramePaths(runPaths, bundle)
126
131
  : new Set();
127
132
  const invalidEvidenceReferences = isRunBundle(bundle) ? invalidRunEvidenceReferences(bundle) : [];
128
- checks.push({
129
- name: "local evidence artifacts exist",
130
- ok: missingEvidenceArtifacts.length === 0 && invalidEvidenceReferences.length === 0,
131
- message: missingEvidenceArtifacts.length === 0 && invalidEvidenceReferences.length === 0
132
- ? isRunBundle(bundle)
133
- ? "every referenced screenshot, trace, log and filesystem artifact is present"
134
- : SHAPE_UNCHECKED
135
- : invalidEvidenceReferences.length > 0
136
- ? `invalid evidence artifact references: ${invalidEvidenceReferences.join(", ")}`
137
- : `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`,
138
- });
133
+ checks.push(evidenceArtifactsCheck(isRunBundle(bundle), [
134
+ ...refusedTraces,
135
+ ...(invalidEvidenceReferences.length > 0
136
+ ? [`invalid evidence artifact references: ${invalidEvidenceReferences.join(", ")}`]
137
+ : missingEvidenceArtifacts.length > 0
138
+ ? [`missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`]
139
+ : []),
140
+ ]));
139
141
  checks.push(...(await evidenceChecks(runPaths, bundle, cleanupRead)));
140
142
  const recordingOk = checks.every((check) => check.ok);
141
143
  if (derivedPublicSafetyFindings.length > 0)
@@ -208,6 +210,46 @@ export async function verifyRunPrepared(cwdInput, runInput, runPaths) {
208
210
  }
209
211
  return verifyResolvedRun(cwd, runInput, runPaths);
210
212
  }
213
+ /** The evidence-artifacts check: refused traces, then invalid references or missing files. */
214
+ function evidenceArtifactsCheck(shaped, problems) {
215
+ return {
216
+ name: "local evidence artifacts exist",
217
+ ok: problems.length === 0,
218
+ message: problems.length > 0
219
+ ? problems.join(" ")
220
+ : shaped
221
+ ? "every referenced screenshot, trace, log and filesystem artifact is present"
222
+ : SHAPE_UNCHECKED,
223
+ };
224
+ }
225
+ /**
226
+ * run.json with each stream's trace loaded, one file at a time, and the refusal of each trace that
227
+ * does not match its index. A run.json that fails the shape guard is returned as it is.
228
+ */
229
+ async function loadTraces(runPaths, stored) {
230
+ const scanned = { sensitive: [], opaque: [] };
231
+ if (!isRunBundle(stored))
232
+ return { bundle: stored, refusedTraces: [], ...scanned };
233
+ // Each trace is source evidence: scanned by the bytes it was joined from, apart from the
234
+ // directory walk and any path exemption the walk makes.
235
+ const { bundle, refused } = await loadStreamTraces(runPaths, stored, {
236
+ onTrace: (file, bytes) => {
237
+ const scan = scanSourceEvidence(bytes);
238
+ if (scan !== "clean")
239
+ scanned[scan === "sensitive" ? "sensitive" : "opaque"].push(file);
240
+ },
241
+ });
242
+ return { bundle, refusedTraces: refused.map((error) => error.message), ...scanned };
243
+ }
244
+ /** The trace scan's results, beside the walk's: each file named once. */
245
+ function addTraceScan(traces, findings, unscanned) {
246
+ for (const file of traces.sensitive)
247
+ if (!findings.includes(`sensitive text ${file}`))
248
+ findings.push(`sensitive text ${file}`);
249
+ for (const file of traces.opaque)
250
+ if (!unscanned.files.includes(file))
251
+ unscanned.files.push(file);
252
+ }
211
253
  /** The top-level run.json sections the shape guard requires. */
212
254
  const REQUIRED_SECTIONS = [
213
255
  "source",
@@ -230,7 +272,7 @@ function bundlePresenceChecks(bundleRead, review) {
230
272
  const bundle = runJsonValue(bundleRead);
231
273
  const record = isRecord(bundle) ? bundle : undefined;
232
274
  const schema = record?.schema;
233
- const schemaOk = schema === RUN_BUNDLE_SCHEMA;
275
+ const schemaOk = schema === RUN_BUNDLE_SCHEMA || schema === RUN_BUNDLE_V1_SCHEMA;
234
276
  const redactionRecord = record?.redaction;
235
277
  const redaction = isRecord(redactionRecord) ? redactionRecord.status : undefined;
236
278
  const missingSections = REQUIRED_SECTIONS.filter((key) => record?.[key] === undefined);
@@ -260,10 +302,10 @@ function bundlePresenceChecks(bundleRead, review) {
260
302
  name: "run schema",
261
303
  ok: schemaOk,
262
304
  message: schemaOk
263
- ? `run.json declares ${RUN_BUNDLE_SCHEMA}`
305
+ ? `run.json declares ${String(schema)}`
264
306
  : record === undefined
265
307
  ? "no readable run.json, so no schema"
266
- : `run.json declares ${typeof schema === "string" ? schema : "no schema"}; verify reads ${RUN_BUNDLE_SCHEMA}`,
308
+ : `run.json declares ${typeof schema === "string" ? schema : "no schema"}; verify reads ${RUN_BUNDLE_SCHEMA} and ${RUN_BUNDLE_V1_SCHEMA}`,
267
309
  },
268
310
  {
269
311
  name: "run bundle shape",
@@ -397,6 +439,14 @@ function buildShareSafety(args) {
397
439
  const unscanned = unscannedMessage(args.unscannedArtifacts);
398
440
  if (unscanned !== undefined)
399
441
  reasons.push({ code: "UNSCANNED_ARTIFACT", message: unscanned });
442
+ // A v2 run.json binds each finished participant's items by its trace index. Items written while
443
+ // a participant was still working (liveActor) have no index, so such a run stays local.
444
+ const live = args.bundle.streams.filter((stream) => stream.liveActor !== undefined);
445
+ if (args.bundle.schema === RUN_BUNDLE_SCHEMA && live.length > 0)
446
+ reasons.push({
447
+ code: "RUN_NOT_FINISHED",
448
+ message: `The run did not finish: run.json holds in-progress items for ${live.map((stream) => stream.id).join(", ")}, which no trace file binds. Review it locally; rerun the study for evidence you can share.`,
449
+ });
400
450
  // Runs from 0.110 keep raw sandbox ids only in sandbox-receipts.ndjson. An earlier run records
401
451
  // them in run.json, and any file that names one is not share-ready as it is.
402
452
  const rawResources = (args.bundle.providerResources ?? []).some((resource) => resource.id !== REDACTED_SANDBOX_ID);
@@ -129,7 +129,7 @@ humanish_swarm:
129
129
  - .env*
130
130
  - .github/workflows/**
131
131
  - infra/**
132
- artifact_schema_version: humanish.run-bundle.v1
132
+ artifact_schema_version: humanish.run-bundle.v2
133
133
  credential_manifest: []
134
134
  network_policy: no_network
135
135
  spend_policy: no_spend
@@ -24,9 +24,26 @@ Observer commands may add derived artifacts:
24
24
  observer-data.json
25
25
  ```
26
26
 
27
- `run.json` remains the source bundle. `observer/observer-data.json` is the
28
- normalized view model consumed by the Observer. `events.ndjson` is the appendable
29
- event stream contract that live adapters update while a run is active.
27
+ `run.json` remains the source bundle. `observer/observer-data.json`
28
+ (`humanish.observer-data.v2`) is the normalized view model consumed by the Observer,
29
+ with every participant except their trace items and the screenshot entries of their
30
+ artifact list. A participant working for hours records thousands of items and a
31
+ screenshot each turn, so these are most of a long run's bytes; apart, the saved file
32
+ stays small enough for `verify` to read. The items stay in the run's traces
33
+ (`actors/<stream>.json`). `events.ndjson` is the appendable event stream
34
+ contract that live adapters update while a run is active.
35
+
36
+ `observer/index.html` carries both parts: the data slot holds observer-data.json's
37
+ content, and `<script id="observer-items" type="application/octet-stream"
38
+ data-encoding="gzip">` holds the items and full artifact lists (`humanish.observer-items.v1`, by stream id)
39
+ gzip-compressed in base64, which a `file://` page decodes without a fetch. Real
40
+ trace items compress about eight times. The page joins the two before it renders
41
+ (`joinObserverData` in `src/observer/participant-items.ts`), so every view reads the
42
+ same data as before. A stream whose screenshot entries moved records how many in
43
+ `screenshotArtifacts`, and the page renders nothing from items that are another
44
+ run's or miss a participant's items or screenshot entries. The server answers a
45
+ saved v2 observer-data.json that leaves items out as unavailable, since it has no
46
+ items to join.
30
47
 
31
48
  ## Stream Model
32
49
 
@@ -55,7 +72,9 @@ public-safe metadata.
55
72
  4. opens the served Observer URL;
56
73
  5. keeps the shell attached until Ctrl-C.
57
74
 
58
- The browser polls `observer-data.json` with `no-store` caching. Static
75
+ The browser polls `observer-feed.json` with `no-store` caching: the server's
76
+ projection of the run as `{ data, items }`, observer-data.json's content and the
77
+ items, from one read of the run per poll. Static
59
78
  `file://` opening still works for immutable review, but follow mode is the
60
79
  operator path. Agents and CI should use `humanish watch --json --no-open` for
61
80
  the same fresh evidence without browser open or a long-running process.
@@ -253,7 +272,7 @@ These remain adapter work:
253
272
  ## The rendering layer
254
273
 
255
274
  The renderer is the `observer/` workspace: a Vite single-file build on the
256
- `@humanish` registry tokens, frozen against `humanish.observer-data.v1`
275
+ `@humanish` registry tokens, frozen against `humanish.observer-data.v2`
257
276
  (`tests/observer/data-contract.test.ts`). The root build copies the workspace
258
277
  artifact to `dist/observer-app.html`; in a repo checkout a missing or stale
259
278
  artifact auto-builds, and an unconditional preflight at CLI startup makes a
@@ -8,7 +8,7 @@ disagree.
8
8
  (`RunStudyOptions`) and the exports removed in 0.107.0.
9
9
  - [core.md](core.md): the records a run writes under `.humanish/runs/<id>/` and how each route
10
10
  starts a run.
11
- - [run-bundle.md](run-bundle.md): `humanish.run-bundle.v1`, the evidence contract that `verify`
11
+ - [run-bundle.md](run-bundle.md): `humanish.run-bundle.v2`, the evidence contract that `verify`
12
12
  checks.
13
13
  - [policy.md](policy.md): credential, network, spend, redaction, sharing and assisted-run
14
14
  boundaries.
@@ -1,6 +1,6 @@
1
1
  # Run Bundle Contract
2
2
 
3
- `humanish.run-bundle.v1` is the evidence contract. The TypeScript shape in `src/run/bundle.ts`
3
+ `humanish.run-bundle.v2` is the evidence contract. The TypeScript shape in `src/run/bundle.ts`
4
4
  (with `streams[]` in `src/run/streams.ts` and `sharedWorld` as `SharedWorldEvidence` in
5
5
  `src/run/shared-world-evidence.ts`) and the fail-closed checks in `src/verify/verify.ts` are the
6
6
  source of truth. This page explains the stable public fields and the extension rules; it does not
@@ -13,6 +13,79 @@ and `terminal-ledgers.json`. `true` means participant and product setup commands
13
13
  in the JSON message of the `terminal-lab.runtime.declared` event. Older bundles omit it.
14
14
  The setting does not attest that the studied CLI honors the variable.
15
15
 
16
+ ## Participant traces
17
+
18
+ From v2, run.json holds each participant's summary and the trace file holds its items. The run
19
+ writer (`src/run/run.ts`, through `splitStreamTraces` in `src/run/stream-trace.ts`) writes, for
20
+ each stream with an `actor`:
21
+
22
+ - the trace file: the actor trace (`humanish.actor-trace.v1`) with every item, plus `artifacts`,
23
+ the stream's screenshot entries. Its path is the stream's actor trace artifact (`actor.json` for
24
+ one participant, `actors/<stream>.json` for more, `actor-<surface>.json` on the scripted route),
25
+ or `actors/<n>.json`, `n` its position in `streams[]` (from 1), for a stream with none or whose
26
+ trace artifact an earlier stream claimed. Names of that form are never reused as a stream's own
27
+ artifact, so two streams never get one file;
28
+ - in run.json, `streams[].actor` without `items`, `streams[].artifacts` without the screenshot
29
+ entries, and the index:
30
+
31
+ ```yaml
32
+ trace:
33
+ path: actors/stream-001.json
34
+ bytes: 5480211 # the file's size
35
+ sha256: "<hex digest of the file>"
36
+ items: 23104 # entries in the file's items
37
+ screenshots: 2881 # entries in the file's artifacts
38
+ ```
39
+
40
+ run.json keeps every field readers list runs by: each stream's status, judged status,
41
+ assignment and geometry, and the actor's status, completion reason, counts, persona, debrief,
42
+ impressions, token usage and estimated cost. Each trace is written once.
43
+
44
+ `trace.path` must be one of those trace file names (`actor.json`, `actors/<name>.json`,
45
+ `actor-<name>.json`, with `<name>` a plain file name); `isStreamTracePath` in
46
+ `src/run/stream-shape.ts` holds the rule, and the writer, the shape check and the loader all use
47
+ it. Any other path, such as `observer/index.html`, fails the shape check.
48
+
49
+ Readers load a stream through `loadStreamTrace(runPaths, stream)`. A v1 stream returns as it is.
50
+ An indexed stream's file is read under the 32 MiB per-file limit (analysis uses its 16 MiB
51
+ source limit for each trace) and refused unless its size, sha256, item count and screenshot count
52
+ match the index. A trace file belongs to one participant: every stream whose index names a file
53
+ another stream's index names, compared without case as a case-insensitive file system compares
54
+ names, is refused before any file is read. The writer claims names the same way. In a v2 run.json every `actor`
55
+ must have an index: an actor with no index, with
56
+ or without inline `items`, is refused, and so is one with both an index and inline `items`.
57
+ verify reports a refused trace in the `local evidence artifacts exist` check, so the run is
58
+ `blocked`. verify also scans the bytes of each trace it loaded with the public-safety text scan,
59
+ apart from its walk of the run folder, so no path exemption the walk makes applies to a trace.
60
+ run.json's digest, which an analysis record binds, covers each index and so each trace.
61
+
62
+ Mid-run flushes: the writer indexes every finished participant on every write, flushes included,
63
+ so a v2 run.json never holds `actor.items`. A participant still working has its items so far in
64
+ `streams[].liveActor.items` (`humanish.live-actor.v1`), which no index binds. A v2 run.json may
65
+ carry `liveActor` only while its run has not finished: one whose `outcome.state` is `finished`
66
+ and still holds `liveActor` is refused. An unfinished v2 run.json with `liveActor` (a flush, or
67
+ the last write of an interrupted run) verifies as `local_only` with the share-safety reason
68
+ `RUN_NOT_FINISHED`, never `share_ready`. v1 bundles keep their earlier behavior.
69
+
70
+ Readers that show a run (the Observer server, run detail and the TUI, the review command's
71
+ provenance) load traces through the same loader and rules. A participant whose trace the loader
72
+ refused shows none of the items run.json or a file supplied for it: its stream carries
73
+ `traceUnavailable` with the reason, its Observer timeline a `trace.unavailable` warning, and its
74
+ run-detail entry `evidenceUnavailable`. The Observer server reads the saved observer-data.json
75
+ only while a run has no run.json at all; a run.json that is there and cannot be read, parsed or
76
+ used as a bundle answers 404 with the reason, never the saved projection. In safe serving
77
+ (`observe --all --safe`) every read is checked against the files admitted with the run's
78
+ share_ready verification, so a run.json changed after admission is refused and the admitted
79
+ projection is served.
80
+ observer-data.json is built from the loaded bundle. From 0.124.0 it is
81
+ `humanish.observer-data.v2`: the same data without each participant's trace items, which stay in
82
+ the traces (docs/architecture/observer.md).
83
+
84
+ Compatibility: humanish 0.122.0 and earlier read only v1 and refuse a v2 run.json by its schema
85
+ (`run schema` fails in verify, analysis refuses the source). This release reads v1 bundles as
86
+ they are, with no migration. An exported copy (`humanish export --format bundle`) is written as
87
+ v2 whatever the source version.
88
+
16
89
  ## Purpose
17
90
 
18
91
  A run bundle is the durable evidence packet for one harness run. It should be
@@ -22,7 +95,7 @@ feedback drafts and future public issues.
22
95
  ## Minimum Bundle Shape
23
96
 
24
97
  ```yaml
25
- schema: humanish.run-bundle.v1
98
+ schema: humanish.run-bundle.v2 # v1 bundles are still read
26
99
  runId: "<core run id>"
27
100
  mode: "dry-run|live"
28
101
  simCount: 1
@@ -522,9 +595,13 @@ For run id `example-2026-06-02t10-00-00-000z-proof`, the core layout is:
522
595
  .humanish/runs/example-2026-06-02t10-00-00-000z-proof/review.md
523
596
  .humanish/runs/example-2026-06-02t10-00-00-000z-proof/observer/observer-data.json
524
597
  .humanish/runs/example-2026-06-02t10-00-00-000z-proof/events.ndjson
598
+ .humanish/runs/example-2026-06-02t10-00-00-000z-proof/actors/stream-001.json
525
599
  .humanish/runs/latest.json
526
600
  ```
527
601
 
602
+ `actors/<stream>.json` (or `actor.json` for one participant) is a participant's trace file; see
603
+ [Participant traces](#participant-traces).
604
+
528
605
  Absolute paths, traversal segments, remotes, hosted logs, and private artifact
529
606
  URLs are not part of the core layout.
530
607
 
@@ -691,7 +768,9 @@ bundle is safe to promote into a public issue. Public promotion should branch on
691
768
  `ok: true` also does not mean the run finished. A run killed mid-way leaves an
692
769
  in-progress bundle that can pass every check. When the run is not finished,
693
770
  `warnings[]` carries one entry starting with the stable code `RUN_NOT_FINISHED`.
694
- `ok` and `shareSafety` do not change. Verify decides "not finished" by the run
771
+ `ok` does not change. `shareSafety` changes only for a v2 bundle that holds a participant's
772
+ in-progress items (`liveActor`): it is `local_only` with the reason `RUN_NOT_FINISHED`, since no
773
+ trace index binds those items. Verify decides "not finished" by the run
695
774
  index's rule, which the TUI and `humanish stats` use: the run's `status.json` when
696
775
  it is well formed and names the run, else a `simulations[]` entry still `running` in the
697
776
  bundle. A `running` record updated within the stale window reads as still
@@ -715,7 +794,8 @@ receipt records a sandbox not gone or misses a journaled one, and `unknown` othe
715
794
  including a receipt from humanish 0.110.0 or earlier, which did not search by tag.
716
795
 
717
796
  The local-evidence check includes screenshots declared only by
718
- `streams[].actor.items[].screenshotRef` or `streams[].liveActor.items[].screenshotRef`,
797
+ `streams[].actor.items[].screenshotRef` (in the trace file for a v2 bundle) or
798
+ `streams[].liveActor.items[].screenshotRef`,
719
799
  as well as every feedback candidate's evidence. Actor frame paths are relative
720
800
  to the run root; missing, malformed or nonlocal references fail verification.
721
801
  Screenshots use the existing bounded PNG decoder. Nonimage candidate evidence
@@ -749,8 +829,9 @@ writes every frame, and the recordings `streams[].recording` registers. Every
749
829
  other file the scan cannot read as text, cannot read at all, or that is larger
750
830
  than 32 MiB (verify reads at most that much of one file) contributes
751
831
  `UNSCANNED_ARTIFACT`. So does a folder the scan cannot list, and the rest of a
752
- run past its first 10,000 files and folders, the most `export --format bundle`
753
- takes. The reason's message lists the paths, and it keeps otherwise valid
832
+ run past its first 10,000 files and folders plus 5,000 for each participant
833
+ `run.json` records, counting at most 128. `export --format bundle` takes at most
834
+ 10,000. The reason's message lists the paths, and it keeps otherwise valid
754
835
  evidence `local_only`. This includes images that only feedback candidates,
755
836
  adapter artifacts or stream artifact entries cite, and a PNG an actor trace
756
837
  references outside `screenshots/`. An adapter artifact larger than 32 MiB grades
@@ -848,6 +929,8 @@ does not inherit resource cleanup authority. Unsupported images, binary formats,
848
929
  inline image payloads, invalid evidence and references to omitted files are refused.
849
930
  The original and derivative are independently verified. Text still requires human
850
931
  review before sharing; screenshot blur does not certify natural-language privacy.
932
+ Export counts the run's files and folders against the public-safety scan's limit
933
+ (10,000 plus 5,000 for each participant `run.json` records) and refuses a run past it.
851
934
 
852
935
  ## Git Provenance
853
936
 
@@ -26,7 +26,7 @@ workflow without leaking private upstream truth into core.
26
26
 
27
27
  | Contract | Schema | Public-safe fixture |
28
28
  | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
29
- | Run bundle | `humanish.run-bundle.v1` | `synthetic-run-bundle` |
29
+ | Run bundle | `humanish.run-bundle.v2` (each participant's trace in its own file, indexed by `streams[].trace`; v1 bundles, with traces inline, are still read) | `synthetic-run-bundle` |
30
30
  | Adapter | `humanish.adapter.v1` | `synthetic-cli-adapter` |
31
31
  | Study file | `humanish.study.v3` | `first-run` |
32
32
  | Persona | `humanish.persona.v1` | `synthetic-maintainer` |
@@ -471,7 +471,7 @@ A study's result has one schema on every route, `humanish.study-result.v1`
471
471
  (`src/run/study-result.ts`), with `route` and `studyId`. 0.109.0 removed `labId`, which 0.108.0
472
472
  wrote with the same value. Route-specific fields stay on their route's result. `humanish run`
473
473
  with no study, `observe`, and a refusal made before a route is picked keep
474
- `humanish.run-result.v1`. The evidence record stays `humanish.run-bundle.v1` in every case.
474
+ `humanish.run-result.v1`. The evidence record stays `humanish.run-bundle.v2` in every case.
475
475
  The computer-use result carries `plan` (the pre-flight participant table: concurrency, waves,
476
476
  per-participant session budget, worst-case sandbox-minutes), `lanes[]` (always present,
477
477
  length 1 at N=1; per-participant status/session/sandbox/subject), and `laneSummary`
@@ -578,7 +578,7 @@ Core-owned fields:
578
578
  }`. Emitted by the computer-use and shared-world routes and the clone
579
579
  scripted-browser route; absent on pre-existing and other bundles. `repo`/`commit` are clone-route fields; `archiveSha256`
580
580
  (64-hex sha256, the local-tree provenance pin) and `dirty` (host git
581
- porcelain status at pack time) are local-tree-route fields, additive under
581
+ porcelain status at pack time) are local-tree-route fields, additive since
582
582
  `humanish.run-bundle.v1`: a dirty working tree cannot be commit-pinned, so
583
583
  the archive content digest stands in for it. `product` is the desktop-cli field: the
584
584
  `subject.product.name` the study declares, with state `undeclared`. A computer-use
@@ -616,7 +616,7 @@ Adapter-owned fields:
616
616
  Abbreviated synthetic fixture (omits `cwd`, `simulations`, `streams` and `events`):
617
617
 
618
618
  ```yaml
619
- schema: humanish.run-bundle.v1
619
+ schema: humanish.run-bundle.v2
620
620
  runId: synthetic-run-bundle-2026-06-02t10-00-00-000z-proof
621
621
  mode: dry-run
622
622
  simCount: 1
@@ -686,8 +686,8 @@ sequential shared-world route, which 0.106.0 removed. Only bundles written befor
686
686
  (`tests/run/legacy-sequential-shared-world.test.ts`). The sequential rules below apply
687
687
  to those bundles.
688
688
 
689
- A shared-world bundle adds two additive, optional fields to `humanish.run-bundle.v1`
690
- (absent on every other bundle, so they stay byte-stable):
689
+ A shared-world bundle adds two additive, optional fields, added under `humanish.run-bundle.v1`
690
+ (absent on every other bundle):
691
691
 
692
692
  - `attributionClass: isolated | shared-world`: a separate attribution axis
693
693
  ("how well did the run attribute interaction?"), distinct from the persona-sampling
@@ -967,6 +967,13 @@ output all map onto one `ActorTrace`. Registered actors live in
967
967
  `pi-agent-core` or `claude-agent-sdk` fails to parse. There is no
968
968
  `humanish.actor.v1`; that name never shipped.
969
969
 
970
+ Where the trace is stored depends on the run bundle version. In `humanish.run-bundle.v2`,
971
+ `streams[].actor` in run.json is the trace without `items`, and the full trace, with the stream's
972
+ screenshot entries as `artifacts`, is the file `streams[].trace.path` names (`actor.json`,
973
+ `actors/<stream>.json` or `actor-<surface>.json`). A v1 run.json holds the full trace inline.
974
+ `loadStreamTrace` in `src/run/stream-trace.ts` reads either; see
975
+ [run-bundle.md](run-bundle.md#participant-traces).
976
+
970
977
  Core-owned fields:
971
978
 
972
979
  - `schema`
@@ -1822,7 +1829,7 @@ including existence for referenced adapter artifacts.
1822
1829
 
1823
1830
  Reserved: `humanish.evidence-stream.v1` has never shipped as a standalone
1824
1831
  schema, and streams are not standalone artifacts. They are the `streams` array
1825
- inside `humanish.run-bundle.v1`, normalizing UI, browser, terminal, TUI,
1832
+ inside the run bundle (`humanish.run-bundle.v2`), normalizing UI, browser, terminal, TUI,
1826
1833
  code-agent UI, artifact, and summary streams, each with transport, terminal
1827
1834
  tail, completion, meaningful-use verdicts, and artifact pointers. See
1828
1835
  [`run-bundle.md`](run-bundle.md#completion-and-meaningful-use-verdicts) for
@@ -367,7 +367,7 @@ and an impression alone supports only a finding about what the participant said.
367
367
 
368
368
  ## Durable records
369
369
 
370
- The frozen `humanish.observer-data.v1` schema is unchanged. A companion
370
+ The `humanish.observer-data.v2` schema is unchanged by analysis. A companion
371
371
  `humanish.study-analysis.v1` artifact records source/config/input hashes,
372
372
  participant context, evidence manifest, coverage, provider/model/prompt version,
373
373
  status, usage and validated findings. The Observer's projection of it,