humanish 0.111.3 → 0.112.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 (112) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/actors/computer-use/openai-conversation.d.ts +91 -0
  3. package/dist/actors/computer-use/openai-conversation.js +209 -0
  4. package/dist/actors/computer-use/openai-provider.js +25 -41
  5. package/dist/actors/contract.d.ts +2 -0
  6. package/dist/actors/scripted-browser/actor.d.ts +5 -0
  7. package/dist/actors/scripted-browser/actor.js +40 -19
  8. package/dist/actors/scripted-browser/journey.d.ts +2 -0
  9. package/dist/actors/scripted-browser/journey.js +2 -1
  10. package/dist/actors/scripted-browser/steps.d.ts +2 -0
  11. package/dist/actors/scripted-browser/steps.js +17 -0
  12. package/dist/analysis/diagnostics.js +7 -12
  13. package/dist/analysis/execute.d.ts +1 -1
  14. package/dist/analysis/execute.js +35 -5
  15. package/dist/analysis/service.js +7 -1
  16. package/dist/analysis/types.d.ts +23 -0
  17. package/dist/analysis/validation.d.ts +80 -35
  18. package/dist/analysis/validation.js +58 -11
  19. package/dist/cli/commands/runs.js +4 -10
  20. package/dist/cli/findings.d.ts +49 -4
  21. package/dist/cli/findings.js +179 -23
  22. package/dist/cli/io.d.ts +2 -1
  23. package/dist/cli/io.js +3 -2
  24. package/dist/evidence/secret-scrub.d.ts +7 -0
  25. package/dist/evidence/secret-scrub.js +21 -12
  26. package/dist/feedback/draft.js +14 -4
  27. package/dist/observer/run-routes.js +8 -4
  28. package/dist/observer-app.html +4 -4
  29. package/dist/routes/computer-use/bundle.d.ts +1 -1
  30. package/dist/routes/computer-use/bundle.js +6 -16
  31. package/dist/routes/computer-use/fanout-bundle.js +40 -37
  32. package/dist/routes/computer-use/fanout-records.d.ts +2 -0
  33. package/dist/routes/computer-use/fanout-records.js +18 -26
  34. package/dist/routes/computer-use/participant-execution.js +4 -11
  35. package/dist/routes/computer-use/participant-facts.d.ts +2 -2
  36. package/dist/routes/computer-use/participant-facts.js +10 -1
  37. package/dist/routes/computer-use/result.js +7 -15
  38. package/dist/routes/computer-use/single-bundle.d.ts +2 -0
  39. package/dist/routes/computer-use/single-bundle.js +81 -37
  40. package/dist/routes/scripted/bundle.d.ts +1 -1
  41. package/dist/routes/scripted/bundle.js +9 -19
  42. package/dist/routes/scripted/route.js +1 -0
  43. package/dist/routes/scripted/setup.js +53 -10
  44. package/dist/routes/scripted/surface-records.d.ts +0 -1
  45. package/dist/routes/scripted/surface-records.js +27 -13
  46. package/dist/routes/scripted/surface-sessions.d.ts +1 -1
  47. package/dist/routes/scripted/surface-sessions.js +2 -0
  48. package/dist/routes/shared-world/bundle-records.d.ts +2 -2
  49. package/dist/routes/shared-world/bundle-records.js +21 -28
  50. package/dist/routes/shared-world/bundle.d.ts +1 -1
  51. package/dist/routes/shared-world/bundle.js +70 -78
  52. package/dist/routes/shared-world/participant-specs.d.ts +1 -2
  53. package/dist/routes/shared-world/participant-specs.js +0 -8
  54. package/dist/routes/shared-world/result.js +8 -12
  55. package/dist/routes/terminal/bundle.d.ts +1 -1
  56. package/dist/routes/terminal/bundle.js +10 -19
  57. package/dist/routes/terminal/credentials.js +14 -1
  58. package/dist/routes/terminal/dry-run.js +1 -0
  59. package/dist/routes/terminal/live-finish.js +7 -4
  60. package/dist/routes/terminal/live-sandbox.js +9 -2
  61. package/dist/routes/terminal/plan.js +1 -0
  62. package/dist/routes/terminal/runtime.d.ts +1 -0
  63. package/dist/routes/terminal/runtime.js +1 -0
  64. package/dist/routes/terminal/session.js +2 -1
  65. package/dist/routes/terminal/trace.d.ts +4 -2
  66. package/dist/routes/terminal/trace.js +30 -35
  67. package/dist/run/costs.js +9 -20
  68. package/dist/run/detail.d.ts +1 -1
  69. package/dist/run/detail.js +10 -1
  70. package/dist/run/display.d.ts +2 -2
  71. package/dist/run/display.js +15 -18
  72. package/dist/run/dry-run.js +10 -25
  73. package/dist/run/judge.d.ts +24 -0
  74. package/dist/run/judge.js +54 -0
  75. package/dist/run/liveness.d.ts +18 -0
  76. package/dist/run/liveness.js +24 -0
  77. package/dist/run/outcomes.d.ts +8 -1
  78. package/dist/run/outcomes.js +58 -12
  79. package/dist/run/participant-caption.d.ts +30 -0
  80. package/dist/run/participant-caption.js +50 -0
  81. package/dist/run/participant-records.d.ts +1 -1
  82. package/dist/run/review-markdown.d.ts +11 -0
  83. package/dist/run/review-markdown.js +22 -0
  84. package/dist/run/run-cost.d.ts +7 -0
  85. package/dist/run/run-cost.js +25 -5
  86. package/dist/run/run-index.d.ts +4 -50
  87. package/dist/run/run-index.js +57 -87
  88. package/dist/run/run.d.ts +2 -2
  89. package/dist/run/run.js +5 -3
  90. package/dist/run/secrets.d.ts +15 -2
  91. package/dist/run/secrets.js +134 -9
  92. package/dist/run/stats.js +2 -12
  93. package/dist/run/transient-comms-secrets.d.ts +8 -3
  94. package/dist/run/transient-comms-secrets.js +21 -1
  95. package/dist/study/keys.d.ts +1 -0
  96. package/dist/study/keys.js +1 -1
  97. package/dist/study/parse/execution.js +7 -1
  98. package/dist/study/plan-types.d.ts +1 -0
  99. package/dist/study/requirements.js +2 -1
  100. package/dist/study/types.d.ts +4 -1
  101. package/dist/study/warnings.d.ts +2 -1
  102. package/dist/study/warnings.js +3 -2
  103. package/dist/tui-app.js +73 -73
  104. package/dist/verify/liveness.d.ts +2 -2
  105. package/dist/verify/liveness.js +7 -4
  106. package/docs/architecture/terminal-product-route.md +14 -0
  107. package/docs/contracts/run-bundle.md +10 -0
  108. package/docs/contracts/schemas.md +9 -1
  109. package/docs/contracts/study-analysis.md +76 -23
  110. package/package.json +2 -1
  111. package/dist/actors/computer-use/openai-requests.d.ts +0 -62
  112. 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,19 +241,13 @@ 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;
@@ -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
  }
@@ -1,5 +1,18 @@
1
1
  import { DEFAULT_RUNTIME_AUTH, E2B_SYSTEM_CA_BUNDLE, OPENAI_EGRESS_PLACEHOLDER, } from "./runtime-auth.js";
2
2
  import { TERMINAL_PRODUCT_STUDY_PROVIDER_METADATA } from "./types.js";
3
+ /**
4
+ * Where the study's runtime auth puts the key, as a sentence for the missing-key refusal. Typed
5
+ * `unknown` because a config that skipped the planner may hold a mode the type does not list.
6
+ */
7
+ function keyPlacement(runtimeAuth) {
8
+ if (runtimeAuth === undefined)
9
+ return ` \`execution.runtimeAuth\` is unset, so the default ${DEFAULT_RUNTIME_AUTH} keeps the key outside the sandbox in an E2B header transform.`;
10
+ if (runtimeAuth === "openai-egress")
11
+ return " `execution.runtimeAuth: openai-egress` keeps the key outside the sandbox in an E2B header transform.";
12
+ if (runtimeAuth === "openai-env")
13
+ return " `execution.runtimeAuth: openai-env` passes the key to the codex command only.";
14
+ return "";
15
+ }
3
16
  /**
4
17
  * Resolve the runtime key on the host. openai-egress, the default, returns an inert command env
5
18
  * while retaining the actual value for the external transform and literal redaction; openai-env
@@ -38,7 +51,7 @@ export function buildRuntimeAuth(args) {
38
51
  return {
39
52
  ok: false,
40
53
  code: "HUMANISH_TERMINAL_RUNTIME_AUTH_MISSING",
41
- message: `Live terminal-product studies declare runtimeAuth "${String(args.runtimeAuth)}" and need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment (pass via --dotenv; the selected auth mode places the value in command-scoped env or an external E2B header transform; the value is never persisted).`,
54
+ message: `Live terminal-product studies need ${ALLOWED_RUNTIME_KEY_NAMES.join(" or ")} in the environment, and neither is set.${keyPlacement(args.runtimeAuth)} Pass one with --dotenv; the value is never persisted.`,
42
55
  };
43
56
  }
44
57
  const keyValue = args.env[keyName];
@@ -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,
@@ -19,14 +19,17 @@ function buildLiveTrace(inputs) {
19
19
  const { session } = inputs;
20
20
  const { createdAt } = inputs.run;
21
21
  const { terminalEvents, commandLog, discardedPrefixes } = inputs.recorder;
22
- // Prefix reconciliation may cut through a known key. Scrub literal values across the retained
23
- // chunks before any transcript/trace/event artifact is persisted. Check both each stream and
22
+ // Prefix reconciliation may cut through a known key. Scrub the known values, as written and in
23
+ // each encoded form, across the retained chunks before any transcript/trace/event artifact is
24
+ // persisted. Check both each stream and
24
25
  // the combined event order that the transcript uses; either view can assemble a split value.
25
- scrubSplitKnownValues(terminalEvents, inputs.run.secrets.values(), discardedPrefixes);
26
+ scrubSplitKnownValues(terminalEvents, (text) => inputs.run.secrets.spans(text), discardedPrefixes);
26
27
  // Build the actor trace first (the cost ledger reads its tokenUsage). A session past the cap
27
28
  // ends its transcript with what was not stored, and its trace says the same.
28
29
  const cut = inputs.recorder.transcriptCut();
29
- const normalizedTranscript = `${normalizeLocalActorTranscript(terminalEvents.map((e) => e.chunk).join(""))}${cut ? `\n[humanish] ${cut.notice}` : ""}`;
30
+ // Normalizing removes terminal escape sequences, which rejoins a value one of them split, so the
31
+ // transcript is sanitized again after it.
32
+ const normalizedTranscript = `${sanitize(normalizeLocalActorTranscript(terminalEvents.map((e) => e.chunk).join("")))}${cut ? `\n[humanish] ${cut.notice}` : ""}`;
30
33
  // Parsed from the full stream: usage records arrive once per turn, and the tail
31
34
  // would drop all but the last. Records past the cap were kept as they arrived.
32
35
  const terminalTokenUsage = parseTerminalTokenUsage([normalizedTranscript, ...(cut?.usage ?? [])].join("\n"));
@@ -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,
@@ -70,11 +70,12 @@ export async function runLiveTerminalSession(args) {
70
70
  const recorder = createTerminalRecorder({
71
71
  nowIso,
72
72
  sanitize,
73
- knownSecretValues: run.secrets.values(),
73
+ knownSecretValues: run.secrets.forms(),
74
74
  verdictNonce,
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,
@@ -5,9 +5,11 @@ import { type CommandLogRecord, type TerminalEventRecord } from "./types.js";
5
5
  /**
6
6
  * Per-chunk sanitization cannot recognize a value split across deliveries. Redact those complete
7
7
  * known values before persistence without collapsing events or changing stdout/stderr ordering.
8
- * Work backwards through matches so edits to later text leave earlier offsets valid.
8
+ * `spansOf` finds them in the joined text: RunSecrets.spans, which also finds a value in an
9
+ * encoded form or split by a terminal escape sequence. Work backwards through the spans so edits
10
+ * to later text leave earlier offsets valid.
9
11
  */
10
- export declare function scrubSplitKnownValues(events: TerminalEventRecord[], knownValues: readonly string[], discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
12
+ export declare function scrubSplitKnownValues(events: TerminalEventRecord[], spansOf: (text: string) => ReadonlyArray<readonly [number, number]>, discardedPrefixes: Record<"stdout" | "stderr" | "combined", string>): void;
11
13
  /** Redacted, ellipsis-prefixed tail of a captured stream/log for a message field. */
12
14
  export declare function tailOf(text: string): string;
13
15
  /**