humanish 0.111.4 → 0.113.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 (158) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +11 -0
  3. package/TELEMETRY.md +21 -0
  4. package/dist/actors/codex/restricted-participant-policy.d.ts +21 -1
  5. package/dist/actors/codex/restricted-participant-policy.js +20 -2
  6. package/dist/actors/codex/restricted-participant.d.ts +1 -1
  7. package/dist/actors/codex/restricted-participant.js +2 -1
  8. package/dist/actors/computer-use/loop/debrief.d.ts +12 -4
  9. package/dist/actors/computer-use/loop/debrief.js +57 -20
  10. package/dist/actors/computer-use/loop/ending.d.ts +5 -2
  11. package/dist/actors/computer-use/loop/impressions.d.ts +14 -0
  12. package/dist/actors/computer-use/loop/impressions.js +40 -0
  13. package/dist/actors/computer-use/loop/session.d.ts +3 -1
  14. package/dist/actors/computer-use/loop/session.js +2 -0
  15. package/dist/actors/computer-use/loop/trace.js +2 -0
  16. package/dist/actors/computer-use/loop/types.d.ts +8 -1
  17. package/dist/actors/computer-use/loop.d.ts +2 -1
  18. package/dist/actors/computer-use/loop.js +15 -3
  19. package/dist/actors/computer-use/openai-conversation.d.ts +96 -0
  20. package/dist/actors/computer-use/openai-conversation.js +238 -0
  21. package/dist/actors/computer-use/openai-provider.js +38 -48
  22. package/dist/actors/contract.d.ts +45 -3
  23. package/dist/actors/contract.js +13 -0
  24. package/dist/analysis/diagnostics.js +7 -12
  25. package/dist/analysis/evidence-sources.d.ts +3 -1
  26. package/dist/analysis/evidence-sources.js +10 -1
  27. package/dist/analysis/evidence.js +2 -2
  28. package/dist/analysis/execute.d.ts +1 -1
  29. package/dist/analysis/execute.js +37 -5
  30. package/dist/analysis/service.js +7 -1
  31. package/dist/analysis/types.d.ts +23 -0
  32. package/dist/analysis/validation.d.ts +80 -35
  33. package/dist/analysis/validation.js +58 -11
  34. package/dist/cli/commands/runs.js +37 -20
  35. package/dist/cli/commands/setup.js +95 -6
  36. package/dist/cli/commands/study-format.js +7 -4
  37. package/dist/cli/commands/study.js +1 -1
  38. package/dist/cli/commands/tui.js +1 -1
  39. package/dist/cli/doctor.d.ts +2 -0
  40. package/dist/cli/doctor.js +18 -0
  41. package/dist/cli/findings.d.ts +34 -3
  42. package/dist/cli/findings.js +136 -18
  43. package/dist/cli/io.d.ts +4 -1
  44. package/dist/cli/io.js +3 -2
  45. package/dist/cli/program.js +35 -1
  46. package/dist/cli/secret-prompt.d.ts +5 -1
  47. package/dist/cli/secret-prompt.js +6 -2
  48. package/dist/cli/update-check-worker.d.ts +1 -0
  49. package/dist/cli/update-check-worker.js +5 -0
  50. package/dist/cli/update-check.d.ts +46 -0
  51. package/dist/cli/update-check.js +203 -0
  52. package/dist/feedback/draft.js +32 -4
  53. package/dist/keys/key-resolution.js +2 -0
  54. package/dist/keys/key-status.d.ts +37 -0
  55. package/dist/keys/key-status.js +68 -0
  56. package/dist/observer/run-routes.js +8 -4
  57. package/dist/observer-app.html +4 -4
  58. package/dist/routes/computer-use/bundle.d.ts +1 -1
  59. package/dist/routes/computer-use/bundle.js +6 -16
  60. package/dist/routes/computer-use/fanout-bundle.js +40 -37
  61. package/dist/routes/computer-use/fanout-records.d.ts +2 -0
  62. package/dist/routes/computer-use/fanout-records.js +18 -26
  63. package/dist/routes/computer-use/local-vm.js +9 -2
  64. package/dist/routes/computer-use/participant-desktop.d.ts +6 -0
  65. package/dist/routes/computer-use/participant-execution.js +13 -12
  66. package/dist/routes/computer-use/participant-facts.d.ts +2 -2
  67. package/dist/routes/computer-use/participant-facts.js +10 -1
  68. package/dist/routes/computer-use/rerun-selection.js +23 -21
  69. package/dist/routes/computer-use/result.js +10 -17
  70. package/dist/routes/computer-use/self-report.js +7 -1
  71. package/dist/routes/computer-use/setup.js +14 -0
  72. package/dist/routes/computer-use/single-bundle.d.ts +2 -0
  73. package/dist/routes/computer-use/single-bundle.js +81 -37
  74. package/dist/routes/computer-use/types.d.ts +4 -1
  75. package/dist/routes/scripted/bundle.d.ts +1 -1
  76. package/dist/routes/scripted/bundle.js +9 -19
  77. package/dist/routes/scripted/surface-records.d.ts +0 -1
  78. package/dist/routes/scripted/surface-records.js +27 -13
  79. package/dist/routes/shared-world/bundle-records.d.ts +2 -2
  80. package/dist/routes/shared-world/bundle-records.js +24 -30
  81. package/dist/routes/shared-world/bundle.d.ts +1 -1
  82. package/dist/routes/shared-world/bundle.js +71 -79
  83. package/dist/routes/shared-world/participant-specs.d.ts +1 -2
  84. package/dist/routes/shared-world/participant-specs.js +0 -8
  85. package/dist/routes/shared-world/result.js +8 -12
  86. package/dist/routes/terminal/bundle.d.ts +1 -1
  87. package/dist/routes/terminal/bundle.js +10 -19
  88. package/dist/routes/terminal/dry-run.js +1 -0
  89. package/dist/routes/terminal/live-sandbox.js +9 -2
  90. package/dist/routes/terminal/plan.js +1 -0
  91. package/dist/routes/terminal/runtime.d.ts +1 -0
  92. package/dist/routes/terminal/runtime.js +1 -0
  93. package/dist/routes/terminal/session.js +1 -0
  94. package/dist/run/costs.js +9 -20
  95. package/dist/run/detail.d.ts +1 -1
  96. package/dist/run/detail.js +13 -4
  97. package/dist/run/display.d.ts +2 -2
  98. package/dist/run/display.js +15 -18
  99. package/dist/run/dry-run.js +10 -25
  100. package/dist/run/judge.d.ts +24 -0
  101. package/dist/run/judge.js +54 -0
  102. package/dist/run/liveness.d.ts +18 -0
  103. package/dist/run/liveness.js +24 -0
  104. package/dist/run/outcomes.d.ts +8 -1
  105. package/dist/run/outcomes.js +58 -12
  106. package/dist/run/participant-caption.d.ts +31 -0
  107. package/dist/run/participant-caption.js +60 -0
  108. package/dist/run/participant-records.d.ts +4 -2
  109. package/dist/run/participant-records.js +6 -0
  110. package/dist/run/review-markdown.d.ts +11 -0
  111. package/dist/run/review-markdown.js +22 -0
  112. package/dist/run/run-cost.d.ts +7 -0
  113. package/dist/run/run-cost.js +25 -5
  114. package/dist/run/run-index.d.ts +4 -50
  115. package/dist/run/run-index.js +57 -87
  116. package/dist/run/run.d.ts +2 -2
  117. package/dist/run/run.js +5 -3
  118. package/dist/run/stats.js +2 -12
  119. package/dist/run/transient-comms-secrets.d.ts +8 -3
  120. package/dist/run/transient-comms-secrets.js +21 -1
  121. package/dist/run-study.js +17 -1
  122. package/dist/study/discover.js +6 -1
  123. package/dist/study/doctor.d.ts +2 -0
  124. package/dist/study/doctor.js +45 -7
  125. package/dist/study/init-templates.js +10 -5
  126. package/dist/study/keys.d.ts +1 -0
  127. package/dist/study/keys.js +1 -1
  128. package/dist/study/parse/execution.js +7 -1
  129. package/dist/study/plan-types.d.ts +1 -0
  130. package/dist/study/plan.js +3 -2
  131. package/dist/study/preflight-result.d.ts +1 -0
  132. package/dist/study/preflight-result.js +1 -1
  133. package/dist/study/preflight.d.ts +2 -0
  134. package/dist/study/preflight.js +54 -14
  135. package/dist/study/types.d.ts +4 -1
  136. package/dist/study/warnings.d.ts +7 -1
  137. package/dist/study/warnings.js +82 -4
  138. package/dist/substrates/local/capacity.d.ts +75 -0
  139. package/dist/substrates/local/capacity.js +143 -0
  140. package/dist/substrates/local/firecracker-desktop.d.ts +8 -0
  141. package/dist/substrates/local/firecracker-desktop.js +34 -3
  142. package/dist/substrates/local/runtime-host.d.ts +16 -3
  143. package/dist/substrates/local/runtime-host.js +49 -4
  144. package/dist/substrates/local/runtime.d.ts +10 -0
  145. package/dist/substrates/local/runtime.js +44 -5
  146. package/dist/tui-app.js +75 -75
  147. package/dist/verify/liveness.d.ts +2 -2
  148. package/dist/verify/liveness.js +7 -4
  149. package/docs/architecture/actor-contract.md +20 -6
  150. package/docs/architecture/terminal-product-route.md +14 -0
  151. package/docs/contracts/feedback.md +8 -1
  152. package/docs/contracts/run-bundle.md +10 -0
  153. package/docs/contracts/schemas.md +65 -34
  154. package/docs/contracts/study-analysis.md +86 -22
  155. package/package.json +2 -1
  156. package/skills/humanish/SKILL.md +21 -10
  157. package/dist/actors/computer-use/openai-requests.d.ts +0 -62
  158. package/dist/actors/computer-use/openai-requests.js +0 -144
@@ -9,13 +9,14 @@ import { commandDigestOf } from "../../subject/state.js";
9
9
  import { buildRunCostSummary, desktopSpanToMinutes } from "../../run/cost-summary.js";
10
10
  import { participantFactsOf } from "../computer-use/participant-facts.js";
11
11
  import { participantFeedbackCandidates } from "../computer-use/participant-feedback.js";
12
- import { judgeSharedWorld, participantPassed, sharedWorldShortfall, verdictText, } from "../../run/judge.js";
13
- import { reviewOutcome } from "../../run/display.js";
12
+ import { judgeSharedWorld, judgeParticipantRecords, participantPassed, sharedWorldShortfall, } from "../../run/judge.js";
13
+ import { renderReviewMarkdown } from "../../run/review-markdown.js";
14
14
  import { combineCheckpointDigest } from "./checkpoints.js";
15
15
  import { hostOriginDigest } from "./provenance.js";
16
16
  import { participantEvent, participantIds } from "../../run/participant-records.js";
17
17
  import { sharedWorldParticipantRecords } from "./bundle-records.js";
18
18
  import { CONCURRENT_ATTRIBUTION_LIMITS, EXTERNAL_PUBLIC_ATTRIBUTION_LIMITS, } from "./types.js";
19
+ import { gapsListClause } from "../../run/outcomes.js";
19
20
  import { plural } from "../../run/text.js";
20
21
  /** Max windows live at the same instant (sweep over start/end points). The simultaneity
21
22
  * count: the participant count says how many existed; this says how many ever ran at once. */
@@ -63,33 +64,39 @@ export function actorRunPassed(result) {
63
64
  return result !== undefined && participantPassed(participantFactsOf(result.outcome));
64
65
  }
65
66
  /**
66
- * Keep three different claims separate in the stakeholder roll-up:
67
- *
68
- * - `outcome.ok` says the actor session passed the harness's credibility checks;
69
- * - `completionReason` says how the participant session ended;
70
- * - shared-world convergence is reported by the plane-specific summary alongside this line.
71
- *
72
- * None of those is adopter-scored proof that the mission text was completed. In particular, a
73
- * productive `budget_reached` session can coexist with lobby convergence without becoming a
74
- * `goal_satisfied` result.
67
+ * The run's summary in plain sentences: who took part, how many reached the goal, and what the
68
+ * world showed. A participant reached the goal when its session passed the judge's checks and
69
+ * ended at the goal, so a productive `budget_reached` session does not count even when the lobby
70
+ * converged. Each participant's ending stays in `sharedWorld.outcomes`, and the plane, topology
71
+ * and attribution limits stay in the bundle's events and `sharedWorld` block.
75
72
  */
76
- function formatSharedWorldActorOutcomes(outcomes, expectedCount) {
77
- const passedSessions = outcomes.filter((outcome) => outcome.ok).length;
78
- const goalSatisfiedSessions = outcomes.filter((outcome) => outcome.ok && outcome.completionReason === "goal_satisfied").length;
79
- const completionReasonCounts = new Map();
80
- for (const outcome of outcomes) {
81
- const reason = outcome.completionReason ?? "not_recorded";
82
- completionReasonCounts.set(reason, (completionReasonCounts.get(reason) ?? 0) + 1);
73
+ function sharedWorldSummary(args, facts) {
74
+ const count = args.actorSpecs.length;
75
+ const people = plural(count, "participant");
76
+ const app = facts.external ? "one public app" : "one shared app";
77
+ if (args.dryRun) {
78
+ const served = facts.external ? ", as it is deployed" : ", served from a sandbox";
79
+ return `Dry run: ${people} would use ${app} at the same time${served}. No sandboxes were launched and $0 was spent.`;
83
80
  }
84
- for (let missing = outcomes.length; missing < expectedCount; missing += 1) {
85
- completionReasonCounts.set("not_recorded", (completionReasonCounts.get("not_recorded") ?? 0) + 1);
81
+ if (facts.inProgress) {
82
+ return `${people} ${count === 1 ? "is" : "are"} using ${app} at the same time. The run is still going, so nothing here is final.`;
86
83
  }
87
- const completionReasons = [...completionReasonCounts.entries()]
88
- // ASCII contract tokens: compare directly so bundle text is byte-stable across host locales.
89
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
90
- .map(([reason, count]) => `${reason} ${count}/${expectedCount}`)
91
- .join(", ");
92
- return `${passedSessions}/${plural(expectedCount, "actor session")} passed credibility checks; mission endpoint: ${goalSatisfiedSessions}/${expectedCount} ended goal_satisfied; completion reasons: ${completionReasons}`;
84
+ const passed = facts.outcomes.filter((outcome) => outcome.ok);
85
+ const reached = passed.filter((outcome) => outcome.completionReason === "goal_satisfied").length;
86
+ const together = facts.overlaps ? " at the same time" : ", but never at the same time";
87
+ // A participant who did not pass has a gap line; one who passed but stopped short has none.
88
+ const rest = [
89
+ passed.length > reached ? `; ${passed.length - reached} ended without reaching it` : "",
90
+ gapsListClause(count - passed.length, count),
91
+ ].join("");
92
+ const world = facts.external
93
+ ? args.lobbyConvergenceDigest
94
+ ? `All ${count} ended up in the same lobby.`
95
+ : "humanish did not see them all end up in the same lobby."
96
+ : facts.deltas === 0
97
+ ? "The app's shared state did not change during the run."
98
+ : `The app's shared state changed ${plural(facts.deltas, "time")} during the run.`;
99
+ return `${people} used ${app}${together}. ${reached} of ${count} reached the goal${rest}. ${world}`;
93
100
  }
94
101
  /** The run's first two events: its creation and the shared plane's provenance. */
95
102
  function planeEvents(args, external) {
@@ -234,24 +241,18 @@ function sharedWorldEvidence(args, external, inProgress, planeCommit) {
234
241
  * A finished run's gaps: each participant that did not pass, or, when every participant passed, the
235
242
  * world shortfall that failed the run.
236
243
  */
237
- function finishedGaps(actorResults, shortfall) {
238
- const participantGaps = actorResults
239
- .filter((result) => result.outcome.sessionError !== undefined ||
240
- result.outcome.noEngagement ||
241
- result.outcome.selfReportedBlocker ||
242
- result.outcome.session === undefined ||
243
- result.outcome.session.status !== "passed")
244
- .map((result) => `${result.spec.planned.id}: ${result.outcome.sessionError ?? result.outcome.session?.reason ?? "did not pass"}`);
244
+ function finishedGaps(participants, shortfall) {
245
+ const participantGaps = participants.flatMap((participant) => participant.gapLine === undefined ? [] : [participant.gapLine]);
245
246
  return participantGaps.length === 0 && shortfall !== undefined ? [shortfall] : participantGaps;
246
247
  }
247
248
  /** Records the concurrency event and returns the review. */
248
- function concurrencyReview(args, external, inProgress, evidence, events, nextEventId) {
249
- const { plan, descriptor, run: { createdAt }, dryRun, actorSpecs, actorResults, } = args;
249
+ function concurrencyReview(args, external, inProgress, evidence, events, nextEventId, participants) {
250
+ const { plan, run: { createdAt }, dryRun, } = args;
250
251
  const { sharedWorld, windows, stateSeries, outcomes } = evidence;
251
252
  const overlaps = args.judgment.world.overlap;
252
253
  const deltas = (stateSeries ?? []).filter((snapshot, i) => i > 0 && snapshot.digest !== (stateSeries ?? [])[i - 1].digest).length;
253
254
  const stateSeriesLabel = external
254
- ? "stateSeries omitted (no authoritative shared-state proof on the external-public plane)"
255
+ ? "Shared app state was not recorded because the app is hosted outside humanish"
255
256
  : `stateSeries ${plural((stateSeries ?? []).length, "snapshot")}, ${plural(deltas, "delta")}`;
256
257
  const convergenceLabel = external
257
258
  ? `; lobby convergence ${args.lobbyConvergenceDigest ? "shown (all participants reached one /lobby/CODE)" : "not observed"}`
@@ -268,25 +269,12 @@ function concurrencyReview(args, external, inProgress, evidence, events, nextEve
268
269
  message: `Concurrency: ${plural(windows.length, "participant")}${dryRun ? " (dry run; $0)" : `, up to ${maxLive} live at once (cap ${capForReport}), overlap ${overlaps ? "shown" : "not observed"}`}; ${stateSeriesLabel}${convergenceLabel}. Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}. ${dryRun ? "This dry run proves nothing about live concurrency, scale, or adoption." : "This run reports only its own observed overlap and state changes; it does not prove scale, repeatability, or adopter-harness replacement."}`,
269
270
  });
270
271
  // The judge's verdict (judgeSharedWorld): every participant produced a terminal, engaged, passed
271
- // session. Mission endpoint and completion reasons are reported separately below; `outcomes[].ok`
272
- // is not renamed into mission success.
273
- const verdict = args.judgment.verdict;
274
- const actorOutcomeSummary = formatSharedWorldActorOutcomes(outcomes, actorSpecs.length);
272
+ // session and the world showed the concurrency verify requires.
275
273
  const review = {
276
274
  schema: REVIEW_SCHEMA,
277
- verdict,
278
- // Plane-class-aware: the external-public plane has no getHost/clone/seed and carries no
279
- // authoritative state series, so its summary must not claim a getHost-exposed plane (dry-run) nor
280
- // report "state delta(s) under load" (live). It reports lobby convergence instead.
281
- summary: dryRun
282
- ? external
283
- ? `Concurrent shared-world dry run: ${plural(actorSpecs.length, "persona")} declared against one public deployment, used as it is (no sandbox, clone or seed); no sandboxes launched, $0 spent.`
284
- : `Concurrent shared-world dry run: ${plural(actorSpecs.length, "persona")} declared against one app served at a public sandbox URL (${descriptor.id}); no sandboxes launched, $0 spent.`
285
- : inProgress
286
- ? `In-progress concurrent shared-world Observer snapshot: ${plural(actorSpecs.length, "persona")} running against one shared app; final verification is pending.`
287
- : external
288
- ? `Concurrent shared-world (one public deployment, ${actorSpecs.length} personas at once): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${actorOutcomeSummary}; overlap ${overlaps ? "proven" : "not observed"}; ${args.lobbyConvergenceDigest ? `${actorSpecs.length} participants converged on one lobby` : "lobby convergence not observed"}.`
289
- : `Concurrent shared-world (one shared app, ${actorSpecs.length} personas at once): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${actorOutcomeSummary}; overlap ${overlaps ? "proven" : "not observed"}; ${plural(deltas, "state delta")} under load.`,
275
+ verdict: args.judgment.verdict,
276
+ // The external-public app has no state series, so its summary reports the lobby instead.
277
+ summary: sharedWorldSummary(args, { external, inProgress, overlaps, deltas, outcomes }),
290
278
  gaps: dryRun
291
279
  ? [
292
280
  "This dry run launched no concurrent shared-world session; it checks the evidence shape only, not live behavior, scale, or adopter-harness replacement.",
@@ -295,7 +283,7 @@ function concurrencyReview(args, external, inProgress, evidence, events, nextEve
295
283
  ? [
296
284
  "Final actor traces, screenshots, state deltas, and verification are pending; this Observer is for live watch only.",
297
285
  ]
298
- : finishedGaps(actorResults, sharedWorldShortfall(args.judgment.world)),
286
+ : finishedGaps(participants, sharedWorldShortfall(args.judgment.world)),
299
287
  };
300
288
  return review;
301
289
  }
@@ -306,15 +294,26 @@ export function buildConcurrentSharedWorldBundle(args) {
306
294
  const external = (args.planeClass ?? "provisioned-getHost") === "external-public";
307
295
  const simulations = [];
308
296
  const streams = [];
309
- // Public-safe label only: neither the raw getHost URL (provisioned) nor the raw public origin
310
- // (external-public) lands in the bundle. The plane identity is a digest (plane.hostDigest on
311
- // getHost; plane.publicOriginDigest on external-public).
312
- const appUrl = external ? "[external-public-plane]" : "[provisioned-subject]";
313
297
  const planeCommit = external ? undefined : dryRun ? undefined : args.subjectCommit;
314
298
  const events = planeEvents(args, external);
315
299
  let eventSeq = 2;
316
300
  const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
317
- const recordContext = { args, external, inProgress, appUrl, nextEventId };
301
+ const judgment = judgeParticipantRecords(actorSpecs.map((spec, index) => ({
302
+ ...participantFactsOf(actorResults[index]?.outcome),
303
+ id: spec.planned.id,
304
+ inProgress: inProgress && actorResults[index] === undefined,
305
+ })), {
306
+ runningReason: "Actor desktop is running; the attached Observer hydrates the runtime stream URL without persisting it.",
307
+ sessionLabel: "Participant",
308
+ missingSessionMessage: "Actor did not produce a terminal session.",
309
+ });
310
+ const recordContext = {
311
+ args,
312
+ external,
313
+ inProgress,
314
+ nextEventId,
315
+ participants: judgment.participants,
316
+ };
318
317
  actorSpecs.forEach((spec, index) => {
319
318
  const records = sharedWorldParticipantRecords(recordContext, spec, index);
320
319
  simulations.push(records.simulation);
@@ -323,7 +322,7 @@ export function buildConcurrentSharedWorldBundle(args) {
323
322
  });
324
323
  const evidence = sharedWorldEvidence(args, external, inProgress, planeCommit);
325
324
  const { sharedWorld } = evidence;
326
- const review = concurrencyReview(args, external, inProgress, evidence, events, nextEventId);
325
+ const review = concurrencyReview(args, external, inProgress, evidence, events, nextEventId, judgment.participants.filter((_, index) => actorResults[index] !== undefined));
327
326
  const anyRaw = actorResults.some((result) => result.outcome.session?.trace.redaction.screenshots === "raw");
328
327
  const ranLive = actorResults.some((result) => result.outcome.session !== undefined || result.outcome.sessionError !== undefined);
329
328
  const cost = concurrentCostSummary(args, inProgress);
@@ -335,7 +334,7 @@ export function buildConcurrentSharedWorldBundle(args) {
335
334
  }),
336
335
  persona: {
337
336
  id: actorSpecs[0]?.persona.id ?? "concurrent-persona",
338
- name: `Concurrent shared-world swarm (${actorSpecs.length} personas)`,
337
+ name: `${plural(actorSpecs.length, "participant")} in one ${external ? "public" : "shared"} app`,
339
338
  source: `study:${plan.studyId}`,
340
339
  sourceDigest: actorSpecs[0]?.persona.promptDigest ?? args.seedDigest,
341
340
  },
@@ -456,27 +455,20 @@ function declaredStateDigest(state) {
456
455
  const probes = state?.checkpoint ?? [];
457
456
  return combineCheckpointDigest(probes.map((probe) => `${probe.name}=${commandDigestOf(probe.command)}`));
458
457
  }
459
- export function renderConcurrentReviewMarkdown(bundle) {
458
+ export function renderConcurrentReviewMarkdown(bundle, status) {
460
459
  const plane = bundle.events.find((event) => event.type === "concurrent-shared-world.plane.provenance");
461
460
  const concurrency = bundle.events.find((event) => event.type === "concurrent-shared-world.concurrency");
462
461
  const sw = bundle.sharedWorld;
463
- return [
464
- `# ${bundle.scenario.title}`,
465
- "",
466
- `- run: ${bundle.runId}`,
467
- `- mode: ${bundle.mode}`,
468
- `- attribution class: ${bundle.attributionClass ?? "isolated"}`,
469
- `- topology: ${sw?.topology ?? "(none)"} / ${sw?.topologyMode ?? "(none)"}`,
470
- `- personas: ${sw?.roleCount ?? 0}`,
471
- `- verdict: ${verdictText(bundle.review.verdict, bundle.mode)}`,
472
- `- outcome: ${reviewOutcome(bundle)}`,
473
- `- summary: ${bundle.review.summary}`,
462
+ return renderReviewMarkdown(bundle, [
474
463
  ...(plane ? [`- plane: ${plane.message}`] : []),
475
464
  ...(concurrency ? [`- concurrency: ${concurrency.message}`] : []),
476
465
  ...(sw ? [`- attribution limits: ${sw.attributionLimits.join(", ")}`] : []),
477
- ...(bundle.review.gaps.length > 0
478
- ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)]
479
- : []),
480
- "",
481
- ].join("\n");
466
+ ], {
467
+ status,
468
+ beforeVerdict: [
469
+ `- attribution class: ${bundle.attributionClass ?? "isolated"}`,
470
+ `- topology: ${sw?.topology ?? "(none)"} / ${sw?.topologyMode ?? "(none)"}`,
471
+ `- personas: ${sw?.roleCount ?? 0}`,
472
+ ],
473
+ });
482
474
  }
@@ -1,5 +1,5 @@
1
1
  import type { SharedWorldPlan } from "../../study/plan-types.js";
2
- import type { Participant, SharedWorldParticipant } from "../../study/plan-participants.js";
2
+ import type { SharedWorldParticipant } from "../../study/plan-participants.js";
3
3
  import type { RunBundle } from "../../run/bundle.js";
4
4
  import type { DesktopParticipantRun, ParticipantRunOutcome } from "../computer-use/types.js";
5
5
  import { type DesktopParticipantDeps } from "../computer-use/participant-deps.js";
@@ -8,7 +8,6 @@ export declare function defaultSessionTimeoutMs(plan: SharedWorldPlan): number;
8
8
  /** Resolve an actor's participant URL against the harness-minted getHost base (entry is a
9
9
  * same-origin relative path, validated at parse against serve.url). */
10
10
  export declare function resolveActorEntryUrl(baseUrl: string, entry: string | undefined): string;
11
- export declare function participantTaxonomyLabel(labels: Participant["labels"]): string;
12
11
  /** Thread the host-yielded lobby code into a follower's mission at runtime (external-public route).
13
12
  * The code flows into the follower's join instruction; it is persisted only as the composed prompt
14
13
  * the model reads (never a raw bundle field), and humanish scrubs the code from all narration. The
@@ -45,14 +45,6 @@ export function resolveActorEntryUrl(baseUrl, entry) {
45
45
  return baseUrl;
46
46
  }
47
47
  }
48
- export function participantTaxonomyLabel(labels) {
49
- const parts = [
50
- labels.actorType ? `type:${labels.actorType}` : undefined,
51
- labels.surface ? `surface:${labels.surface}` : undefined,
52
- labels.caseGroup ? `case:${labels.caseGroup}` : undefined,
53
- ].filter((part) => part !== undefined);
54
- return parts.length > 0 ? ` (${parts.join(" / ")})` : "";
55
- }
56
48
  /** Build one participant's DesktopParticipantRun from its plan (each participant has its own
57
49
  * desktop, so its own device). */
58
50
  function buildActorSpec(participant, personas) {
@@ -3,9 +3,9 @@
3
3
  import { redactText } from "../../evidence/redaction.js";
4
4
  import { adapterScoreFailureMessage, applyBrowserScorer } from "../../study/adapter-extension.js";
5
5
  import { attachObserverRuntimeStreamUrls } from "../../observer/render.js";
6
- import { foldScorerFailures, judgeExecution, sandboxCleanupFailure, OUTCOME_POLICIES, resultOk, sharedWorldShortfall, } from "../../run/judge.js";
6
+ import { foldScorerFailures, judgeParticipantRecords, judgeExecution, sandboxCleanupFailure, OUTCOME_POLICIES, resultOk, sharedWorldShortfall, } from "../../run/judge.js";
7
7
  import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
8
- import { participantExecutionFailures } from "../computer-use/participant-facts.js";
8
+ import { participantExecutionFailures, participantFactsOf, } from "../computer-use/participant-facts.js";
9
9
  import { participantCapWarning } from "../computer-use/participant-model.js";
10
10
  import { resolveSubjectState } from "../computer-use/subject-projection.js";
11
11
  import { actorRunPassed, buildConcurrentSharedWorldBundle, judgeSharedWorldRun, maxSimultaneousWindows, } from "./bundle.js";
@@ -70,14 +70,10 @@ function concurrentParticipantResults(actorSpecs, actorResults, dryRun) {
70
70
  : {
71
71
  error: {
72
72
  code: "HUMANISH_SHARED_WORLD_FAILED",
73
- message: result.outcome.sessionError ??
74
- (result.outcome.noEngagement
75
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
76
- : result.outcome.selfReportedBlocker
77
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
78
- : session?.completionReason === "harness_error"
79
- ? `Participant ended with a harness error: ${session.reason}`
80
- : "Actor did not produce a terminal session."),
73
+ message: judgeParticipantRecords([participantFactsOf(result.outcome)], {
74
+ sessionLabel: "Participant",
75
+ missingSessionMessage: "Actor did not produce a terminal session.",
76
+ }).participants[0].notPassedMessage,
81
77
  },
82
78
  }),
83
79
  };
@@ -126,7 +122,7 @@ function concurrentStudyError(args) {
126
122
  if (passed === participantCount && args.shortfall !== undefined) {
127
123
  return {
128
124
  code: "HUMANISH_SHARED_WORLD_FAILED",
129
- message: `Concurrent shared-world run did not run coherently. ${args.shortfall}`,
125
+ message: `The shared-world run failed although every participant passed. ${args.shortfall}`,
130
126
  };
131
127
  }
132
128
  if (passed === participantCount && args.executionFailure !== undefined) {
@@ -137,7 +133,7 @@ function concurrentStudyError(args) {
137
133
  }
138
134
  return {
139
135
  code: "HUMANISH_SHARED_WORLD_FAILED",
140
- message: `Concurrent shared-world run did not run coherently: ${passed}/${plural(participantCount, "actor")} reached a terminal, engaged passed session.`,
136
+ message: `The shared-world run failed: ${passed} of ${plural(participantCount, "participant")} passed. The run's review.md lists what happened to the others.`,
141
137
  };
142
138
  }
143
139
  /**
@@ -71,4 +71,4 @@ export declare function buildLiveTerminalProductBundle(args: {
71
71
  * the review names, beside the agent's own verdict. */
72
72
  capFailure?: string;
73
73
  }): RunBundle;
74
- export declare function renderTerminalReviewMarkdown(bundle: RunBundle): string;
74
+ export declare function renderTerminalReviewMarkdown(bundle: RunBundle, status?: unknown): string;
@@ -1,12 +1,14 @@
1
1
  import { redactText } from "../../evidence/redaction.js";
2
2
  import { participantAssignment } from "../../study/participant-assignment.js";
3
3
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
4
- import { verdictText } from "../../run/judge.js";
5
- import { reviewOutcome } from "../../run/display.js";
4
+ import {} from "../../run/judge.js";
5
+ import { renderReviewMarkdown } from "../../run/review-markdown.js";
6
6
  import {} from "../../run/streams.js";
7
7
  import { participantEvent, participantIds, participantRecord, participantStream, } from "../../run/participant-records.js";
8
8
  import { TERMINAL_EVENTS_ARTIFACT, TERMINAL_LEDGERS_ARTIFACT, TERMINAL_TRANSCRIPT_ARTIFACT, } from "../../run/terminal-contract.js";
9
9
  import { DEFAULT_RUNTIME_AUTH } from "./runtime-auth.js";
10
+ import { participantCaption } from "../../run/participant-caption.js";
11
+ import { formatParticipantOutcomes, tallyParticipantOutcomes } from "../../run/outcomes.js";
10
12
  import { describeMeasuredSpend, noSpendLineMeasured, noSpendNotEstablished } from "./ledger.js";
11
13
  /**
12
14
  * Project the terminal-product run into a humanish.run-bundle.v1 (no schema change: a new
@@ -80,7 +82,7 @@ export function buildTerminalProductBundle(args) {
80
82
  const review = {
81
83
  schema: REVIEW_SCHEMA,
82
84
  verdict: args.verdict,
83
- summary: reason,
85
+ summary: `Dry run: 1 participant would try ${args.productName} from a terminal. No sandbox was created, no key was used and $0 was spent.`,
84
86
  gaps: [
85
87
  "This dry run did not execute the live in-sandbox agent route; it checks the evidence shape only, not live behavior, scale, or adoption.",
86
88
  "No exec-stream, transcript, substrate, cost, or cleanup artifacts were produced because no live session ran; live verification requires those artifacts.",
@@ -164,7 +166,7 @@ export function buildLiveTerminalProductBundle(args) {
164
166
  const review = {
165
167
  schema: REVIEW_SCHEMA,
166
168
  verdict: args.verdict,
167
- summary: args.sessionReason,
169
+ summary: `1 participant took part: ${formatParticipantOutcomes(tallyParticipantOutcomes([args.trace.status]))}. How it ended: ${args.sessionReason}`,
168
170
  gaps: [
169
171
  ...(args.trace.status === "passed"
170
172
  ? []
@@ -212,7 +214,7 @@ function terminalParticipant(args, session) {
212
214
  const stream = participantStream(TERMINAL_IDS, {
213
215
  assignment: participantAssignment({ mission: args.mission }),
214
216
  kind: "terminal",
215
- label: `Terminal agent · ${args.studyId}`,
217
+ label: participantCaption({ personaId: args.persona.id }),
216
218
  status: session.status,
217
219
  transport: "snapshot",
218
220
  updatedAt: session.updatedAt,
@@ -275,25 +277,14 @@ function describeCaps(caps) {
275
277
  parts.push(`maxMinutes=${caps.maxMinutes}`);
276
278
  return parts.length > 0 ? parts.join(", ") : "empty";
277
279
  }
278
- export function renderTerminalReviewMarkdown(bundle) {
280
+ export function renderTerminalReviewMarkdown(bundle, status) {
279
281
  const subject = bundle.events.find((event) => event.type === "terminal-lab.subject.declared");
280
282
  const credentials = bundle.events.find((event) => event.type === "terminal-lab.credentials.declared");
281
283
  const caps = bundle.events.find((event) => event.type === "terminal-lab.caps.declared");
282
- return [
283
- `# ${bundle.scenario.title}`,
284
- "",
285
- `- run: ${bundle.runId}`,
286
- `- mode: ${bundle.mode}`,
287
- `- verdict: ${verdictText(bundle.review.verdict, bundle.mode)}`,
288
- `- outcome: ${reviewOutcome(bundle)}`,
289
- `- summary: ${bundle.review.summary}`,
284
+ return renderReviewMarkdown(bundle, [
290
285
  `- mission: ${bundle.scenario.goal}`,
291
286
  ...(subject ? [`- subject: ${subject.message}`] : []),
292
287
  ...(credentials ? [`- credentials: ${credentials.message}`] : []),
293
288
  ...(caps ? [`- caps: ${caps.message}`] : []),
294
- ...(bundle.review.gaps.length > 0
295
- ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)]
296
- : []),
297
- "",
298
- ].join("\n");
289
+ ], { status });
299
290
  }
@@ -124,6 +124,7 @@ function runtimeDeclaredEvent(runtime, createdAt) {
124
124
  level: "info",
125
125
  type: "terminal-lab.runtime.declared",
126
126
  message: redactText(JSON.stringify(declaredRuntimeProvenance({
127
+ doNotTrack: runtime.doNotTrack,
127
128
  ...(version === undefined ? {} : { version }),
128
129
  model,
129
130
  modelSource,
@@ -261,7 +261,10 @@ export class LiveTerminalSandbox {
261
261
  // The install step may run the product itself (release:dogfood's does: `humanish init
262
262
  // --yes`); on the 0.67.0 dogfood that one command arrived unmarked while the participant's
263
263
  // nine others carried the marker.
264
- envs: { HUMANISH_STUDY_PARTICIPANT: "1" },
264
+ envs: {
265
+ HUMANISH_STUDY_PARTICIPANT: "1",
266
+ ...(plan.runtime.doNotTrack ? { DO_NOT_TRACK: "1" } : {}),
267
+ },
265
268
  requestTimeoutMs,
266
269
  timeoutMs: PRODUCT_SETUP_TIMEOUT_MS,
267
270
  });
@@ -314,7 +317,11 @@ export class LiveTerminalSandbox {
314
317
  // The selected command env (raw key or inert placeholder). The participant
315
318
  // marker rides the same command: humanish telemetry from inside a study reads as a new
316
319
  // adopter otherwise.
317
- envs: { ...runtimeEnv.envs, HUMANISH_STUDY_PARTICIPANT: "1" },
320
+ envs: {
321
+ ...runtimeEnv.envs,
322
+ HUMANISH_STUDY_PARTICIPANT: "1",
323
+ ...(plan.runtime.doNotTrack ? { DO_NOT_TRACK: "1" } : {}),
324
+ },
318
325
  requestTimeoutMs,
319
326
  timeoutMs: wallClockMs,
320
327
  onStdout: (data) => recordStreamedTerminalChunk("stdout", data),
@@ -69,6 +69,7 @@ export function planTerminalStudy(config, input) {
69
69
  ...(actor?.persona === undefined ? {} : { personaId: actor.persona }),
70
70
  ...(actor?.mission === undefined ? {} : { mission: actor.mission }),
71
71
  runtime: {
72
+ doNotTrack: config.execution?.terminal?.doNotTrack !== false,
72
73
  ...(runtimeVersion === undefined ? {} : { version: runtimeVersion }),
73
74
  // Codex's own default can change with any release and its JSON stream does not name it, so
74
75
  // the route always passes a model: the declared one, else humanish's participant default.
@@ -8,6 +8,7 @@ export declare function parseTerminalRuntimeVersion(stdout: string): string | un
8
8
  export declare function buildRuntimeVersionCommand(requestedVersion?: string): string;
9
9
  export declare function buildRuntimeExecPrefix(version: string, model: string, reasoningEffort?: ReasoningEffort): string;
10
10
  export declare function declaredRuntimeProvenance(args: {
11
+ doNotTrack: boolean;
11
12
  version?: string;
12
13
  model: string;
13
14
  modelSource: "declared" | "humanish_default";
@@ -35,6 +35,7 @@ export function declaredRuntimeProvenance(args) {
35
35
  return {
36
36
  schema: "humanish.actor-runtime.v1",
37
37
  package: TERMINAL_RUNTIME_PACKAGE,
38
+ doNotTrack: args.doNotTrack,
38
39
  requestedVersion: args.version ?? "latest",
39
40
  versionStatus: "unobserved",
40
41
  requestedModel: args.model,
@@ -75,6 +75,7 @@ export async function runLiveTerminalSession(args) {
75
75
  });
76
76
  const { version, model, modelSource, reasoningEffort } = plan.runtime;
77
77
  const runtime = declaredRuntimeProvenance({
78
+ doNotTrack: plan.runtime.doNotTrack,
78
79
  ...(version === undefined ? {} : { version }),
79
80
  model: sanitize(model),
80
81
  modelSource,
package/dist/run/costs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { round6 } from "./pricing.js";
1
+ import { runEstimateUsd, sumEstimatedUsd } from "./run-cost.js";
2
2
  import { bindExistingRunArtifactPaths } from "./paths.js";
3
3
  import { contradictsAccountBilling } from "../verify/costs.js";
4
4
  import { EVIDENCE_LIMITS } from "../analysis/evidence.js";
@@ -20,13 +20,9 @@ export function emptyCostTotals() {
20
20
  analysisHistoryUncertainRuns: 0,
21
21
  };
22
22
  }
23
- const isKnownUsd = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
24
- function sumKnown(a, b) {
25
- return a === null && b === null ? null : round6((a ?? 0) + (b ?? 0));
26
- }
27
23
  export function addCostTotals(into, next) {
28
24
  for (const key of ["estimatedTotalUsd", "runEstimatedUsd", "analysisEstimatedUsd"]) {
29
- into[key] = sumKnown(into[key], next[key]);
25
+ into[key] = sumEstimatedUsd(into[key], next[key]);
30
26
  }
31
27
  for (const key of [
32
28
  "incompleteRunEstimates",
@@ -49,11 +45,7 @@ export async function readCostTotals(cwd, entry) {
49
45
  const analysisWarnings = [];
50
46
  // A dry run makes no model request and creates no desktop, so with no figure it costs $0. A
51
47
  // recorded unknown (null) figure stays unknown.
52
- costs.runEstimatedUsd = isKnownUsd(entry.estimatedCostUsd)
53
- ? entry.estimatedCostUsd
54
- : entry.estimatedCostUsd === undefined && entry.mode === "dry-run"
55
- ? 0
56
- : null;
48
+ costs.runEstimatedUsd = runEstimateUsd(entry);
57
49
  try {
58
50
  const prepared = await bindExistingRunArtifactPaths(cwd, entry.runId);
59
51
  const bytes = await readBoundedFile(prepared, RUN_BUNDLE_FILE, EVIDENCE_LIMITS.sourceBytes);
@@ -70,9 +62,7 @@ export async function readCostTotals(cwd, entry) {
70
62
  runWarnings.push("RUN_COST_ID_MISMATCH");
71
63
  }
72
64
  if (bundle?.cost !== undefined) {
73
- costs.runEstimatedUsd = isKnownUsd(bundle.cost?.estimatedTotalUsd)
74
- ? bundle.cost.estimatedTotalUsd
75
- : null;
65
+ costs.runEstimatedUsd = runEstimateUsd({ estimatedCostUsd: bundle.cost?.estimatedTotalUsd });
76
66
  if (bundle.cost?.fullyEstimated !== true || costs.runEstimatedUsd === null) {
77
67
  costs.incompleteRunEstimates = 1;
78
68
  runWarnings.push("RUN_COST_PARTIAL_OR_UNKNOWN");
@@ -102,7 +92,7 @@ export async function readCostTotals(cwd, entry) {
102
92
  costs.analysisHistoryUncertainRuns = 1;
103
93
  analysisWarnings.push("STUDY_COST_ACCOUNTING_UNAVAILABLE");
104
94
  }
105
- costs.estimatedTotalUsd = sumKnown(costs.runEstimatedUsd, costs.analysisEstimatedUsd);
95
+ costs.estimatedTotalUsd = sumEstimatedUsd(costs.runEstimatedUsd, costs.analysisEstimatedUsd);
106
96
  return {
107
97
  runId: entry.runId,
108
98
  costs,
@@ -153,16 +143,15 @@ export async function readAnalysisAccounting(prepared) {
153
143
  }
154
144
  if (!usage.dispatched) {
155
145
  accounting.notDispatched += 1;
156
- accounting.estimatedUsd = sumKnown(accounting.estimatedUsd, 0);
146
+ accounting.estimatedUsd = sumEstimatedUsd(accounting.estimatedUsd, 0);
157
147
  continue;
158
148
  }
159
149
  accounting.dispatched += 1;
160
150
  if (record.receipt)
161
151
  providers.add(record.receipt.provider);
162
- if (isKnownUsd(usage.estimatedCostUsd)) {
163
- accounting.estimatedUsd = sumKnown(accounting.estimatedUsd, usage.estimatedCostUsd);
164
- }
165
- if (!isKnownUsd(usage.estimatedCostUsd) || !usage.usageComplete)
152
+ const estimatedUsd = runEstimateUsd({ estimatedCostUsd: usage.estimatedCostUsd });
153
+ accounting.estimatedUsd = sumEstimatedUsd(accounting.estimatedUsd, estimatedUsd);
154
+ if (estimatedUsd === null || !usage.usageComplete)
166
155
  accounting.unpriced += 1;
167
156
  }
168
157
  // A skipped or queued automatic job says nothing about earlier manual requests. Only final
@@ -11,7 +11,7 @@ interface RunThought {
11
11
  export interface RunParticipant {
12
12
  /** Stream id: stable within a run, and what distinguishes participants of one study. */
13
13
  id: string;
14
- /** What to call them on screen: the participant's own label, else the persona id. */
14
+ /** What to call them on screen: the caption the Observer shows, else the persona id. */
15
15
  label: string;
16
16
  /** The persona id, when the trace recorded one. */
17
17
  personaId?: string;
@@ -12,6 +12,8 @@ import path from "node:path";
12
12
  import { estimateActorCostForExecution } from "./pricing.js";
13
13
  import { resolveRunPath } from "./locate.js";
14
14
  import { RUN_BUNDLE_FILE } from "./bundle.js";
15
+ import { savedCaption } from "./participant-caption.js";
16
+ import { recordedPersonaId, streamParticipantIdOf } from "./participant-records.js";
15
17
  import { readContainedRegularFile } from "./contained-output.js";
16
18
  import { isPathInside, resolvePhysicalCwd } from "./paths.js";
17
19
  const RUN_DETAIL_SCHEMA = "humanish.run-detail.v1";
@@ -41,19 +43,26 @@ function latestThought(trace) {
41
43
  }
42
44
  return undefined;
43
45
  }
44
- function participantFrom(stream, index) {
46
+ function participantFrom(stream, index, savedPersonaId) {
45
47
  // A live run's flush wins over the finished trace: while both exist, the live one is the newer
46
48
  // account of what is happening.
47
49
  const trace = stream.liveActor ?? stream.actor ?? {};
48
50
  const id = stream.id ?? `stream-${index + 1}`;
49
- const personaId = trace.persona?.id;
51
+ const personaId = trace.persona?.id ?? savedPersonaId;
52
+ const participantId = streamParticipantIdOf(stream);
50
53
  // Computed once: calling it twice to test-then-use reads as though the two could differ.
51
54
  const thought = latestThought(trace);
52
55
  const status = trace.status ?? stream.status;
53
56
  const thoughts = (trace.items ?? []).filter((item) => item?.kind === "reasoning").length;
54
57
  return {
55
58
  id,
56
- label: stream.label ?? personaId ?? id,
59
+ label: stream.label === undefined
60
+ ? (personaId ?? id)
61
+ : savedCaption({
62
+ label: stream.label,
63
+ ...(participantId === undefined ? {} : { participantId }),
64
+ ...(personaId === undefined ? {} : { personaId }),
65
+ }),
57
66
  ...(personaId === undefined ? {} : { personaId }),
58
67
  traits: Array.isArray(trace.persona?.traitsApplied) ? trace.persona.traitsApplied : [],
59
68
  ...(status === undefined ? {} : { status }),
@@ -131,7 +140,7 @@ export async function readRunDetail(cwdInput, runId) {
131
140
  ...(automaticAnalysis === undefined ? {} : { automaticAnalysis }),
132
141
  schema: RUN_DETAIL_SCHEMA,
133
142
  runId: bundle.runId ?? runId,
134
- participants: streams.map(participantFrom),
143
+ participants: streams.map((stream, index) => participantFrom(stream, index, recordedPersonaId(bundle, stream))),
135
144
  ...(isPathInside(cwd, observerAbsolute)
136
145
  ? { observerPath: path.relative(cwd, observerAbsolute) }
137
146
  : { observerPath: observerAbsolute }),
@@ -49,14 +49,14 @@ export interface DisplayedBundle {
49
49
  * The facts for a run from its bundle. run.json's `outcome` decides when it has one. A bundle
50
50
  * without one is in progress, or was recorded before the field existed: its status record, when
51
51
  * given and naming the run, then says whether the run is alive and, for the older run, holds its ok.
52
- * With no record, a bundle whose simulations are still running is live.
52
+ * With no record, a bundle whose simulations are still running is interrupted.
53
53
  */
54
54
  export declare function bundleDisplayFacts(bundle: DisplayedBundle, status?: unknown, nowMs?: number): RunDisplayFacts;
55
55
  /**
56
56
  * How review.md says the run ended: the display of the bundle it is rendered from, so a run whose
57
57
  * execution failed never reads as a pass beside its participants' verdict.
58
58
  */
59
- export declare function reviewOutcome(bundle: DisplayedBundle): string;
59
+ export declare function reviewOutcome(bundle: DisplayedBundle, status?: unknown): string;
60
60
  /**
61
61
  * The summary and gaps a review shows beside its run's display. A run stopped while a participant
62
62
  * was still running keeps the review its last live flush wrote, which describes that session as