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
@@ -2,10 +2,12 @@ import { participantAssignment } from "../../study/participant-assignment.js";
2
2
  import { redactText } from "../../evidence/redaction.js";
3
3
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
4
4
  import {} from "../../run/streams.js";
5
- import { aggregateTaskFunnels, tallyParticipantOutcomes, withCuaReviewProvenance, } from "../../run/outcomes.js";
5
+ import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, sessionEndingInWords, withCuaReviewProvenance, } from "../../run/outcomes.js";
6
+ import { participantCaption } from "../../run/participant-caption.js";
7
+ import { actorEnding } from "../../actors/stop-cause.js";
6
8
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel, subjectProvenanceMessage, } from "./bundle-parts.js";
7
9
  import { participantFeedbackCandidates } from "./participant-feedback.js";
8
- import { participantStatus as participantStatusFor } from "../../run/judge.js";
10
+ import { judgeParticipantRecords } from "../../run/judge.js";
9
11
  import { buildRunCostSummary } from "../../run/cost-summary.js";
10
12
  import { participantEvent, participantIds, participantRecord, participantStream, } from "../../run/participant-records.js";
11
13
  import { plural } from "../../run/text.js";
@@ -43,18 +45,25 @@ function runCost(args) {
43
45
  }
44
46
  /** The participant's status, reason, last frame, geometry and screenshot mode, shared by its records. */
45
47
  function participantView(args, publicAppUrl) {
46
- const status = args.inProgress === true
47
- ? "running"
48
- : args.session
49
- ? args.session.status
50
- : args.sessionError !== undefined
51
- ? "failed"
52
- : "contract_proof_only";
53
- const reason = args.inProgress === true
54
- ? "Live computer-use session is running; stream auth URL is available only through the attached Observer server."
55
- : (args.session?.reason ??
56
- args.sessionError ??
57
- "Dry run: the evidence shape was written without launching a desktop or spending provider tokens.");
48
+ const judgment = judgeParticipantRecords([
49
+ {
50
+ ...(args.session === undefined
51
+ ? {}
52
+ : {
53
+ status: args.session.status,
54
+ completionReason: args.session.completionReason,
55
+ reason: args.session.reason,
56
+ }),
57
+ ...(args.sessionError === undefined ? {} : { sessionError: args.sessionError }),
58
+ inProgress: args.inProgress === true,
59
+ skipped: false,
60
+ noEngagement: args.credibility?.noEngagement === true,
61
+ selfReportedBlocker: args.credibility?.selfReportedBlocker === true,
62
+ reportedFriction: args.credibility?.reportedFriction === true,
63
+ },
64
+ ]);
65
+ const participant = judgment.participants[0];
66
+ const { status, reason } = participant;
58
67
  const lastScreenshot = args.screenshots[args.screenshots.length - 1];
59
68
  const desktopGeometry = args.desktopRoute === false
60
69
  ? undefined
@@ -70,6 +79,8 @@ function participantView(args, publicAppUrl) {
70
79
  : (args.captureRedaction ?? "raw");
71
80
  return {
72
81
  publicAppUrl,
82
+ participant,
83
+ tally: judgment.tally,
73
84
  status,
74
85
  reason,
75
86
  lastScreenshot,
@@ -80,6 +91,10 @@ function participantView(args, publicAppUrl) {
80
91
  }
81
92
  function singleSimulation(args, view) {
82
93
  const { publicAppUrl, status, reason } = view;
94
+ const name = participantCaption({
95
+ id: args.participantId ?? "lane-01",
96
+ personaId: args.persona.id,
97
+ });
83
98
  return participantRecord(SINGLE, 1, {
84
99
  personaId: args.persona.id,
85
100
  scenarioId: `cua-${args.studyId}`,
@@ -89,23 +104,19 @@ function singleSimulation(args, view) {
89
104
  progress: args.inProgress === true ? 20 : 100,
90
105
  currentStep: reason,
91
106
  summary: args.session
92
- ? `Computer-use actor (${args.actorId}) drove the subject app ${browserPlace(args)}; ${args.session.completionReason}.`
107
+ ? `${name} used the app ${browserPlace(args)}, and ${sessionEndingInWords(args.session.trace)}.`
93
108
  : args.inProgress === true
94
- ? `Computer-use actor (${args.actorId}) is driving the subject app ${browserPlace(args)}.`
109
+ ? `${name} is using the app ${browserPlace(args)}.`
95
110
  : args.sessionError !== undefined
96
- ? `The computer-use run failed before a terminal session verdict: ${args.sessionError}`
97
- : `Computer-use actor (${args.actorId}) against ${publicAppUrl}; no session ran.`,
111
+ ? `${name} did not finish a session: ${args.sessionError}`
112
+ : `${name} would use ${publicAppUrl}; no session ran.`,
98
113
  startedAt: args.run.createdAt,
99
114
  updatedAt: args.run.createdAt,
100
115
  });
101
116
  }
102
117
  function singleStream(args, view) {
103
118
  const { publicAppUrl, status, reason, lastScreenshot, desktopGeometry, screenshotMode } = view;
104
- // The judge's status for a finished session, as the review counts it, so the Observer shows
105
- // the participant the way the verdict judged it.
106
- const judged = args.session === undefined || args.inProgress === true
107
- ? undefined
108
- : participantStatusFor(args.session.status, args.credibility);
119
+ const judged = args.session === undefined ? undefined : view.participant.judgedStatus;
109
120
  return participantStream(SINGLE, {
110
121
  ...(args.assignment === undefined
111
122
  ? {}
@@ -114,7 +125,13 @@ function singleStream(args, view) {
114
125
  ...(args.surface === undefined ? {} : { surface: args.surface }),
115
126
  ...(args.caseGroup === undefined ? {} : { caseGroup: args.caseGroup }),
116
127
  kind: "browser",
117
- label: `${args.participantId ?? "lane-01"} · browser`,
128
+ label: participantCaption({
129
+ id: args.participantId ?? "lane-01",
130
+ personaId: args.persona.id,
131
+ ...(args.isMobile === undefined
132
+ ? {}
133
+ : { device: { name: args.deviceName ?? "", preset: { isMobile: args.isMobile } } }),
134
+ }),
118
135
  status,
119
136
  ...(judged === undefined ? {} : { judgedStatus: judged }),
120
137
  transport: "snapshot",
@@ -257,37 +274,64 @@ function singleEvents(args, view) {
257
274
  }
258
275
  return events;
259
276
  }
277
+ /**
278
+ * The run's summary in plain sentences. A finished session gives the outcome with its denominator
279
+ * of one, any reason the harness did not count a pass, and how the session ended. The single
280
+ * participant has no gap line, so the summary carries the reason.
281
+ */
282
+ function singleSummary(args, view, facts) {
283
+ if (args.inProgress === true) {
284
+ return "1 participant is using the app. The run is still going, so nothing here is final.";
285
+ }
286
+ if (args.dryRun) {
287
+ return `Dry run: 1 participant would use ${view.publicAppUrl}. Nothing was launched and $0 was spent.`;
288
+ }
289
+ if (args.session === undefined) {
290
+ return args.sessionError === undefined
291
+ ? `1 participant was planned against ${view.publicAppUrl}, but no session ran.`
292
+ : `1 participant was planned, but the run failed before their session finished: ${view.reason}`;
293
+ }
294
+ const ending = actorEnding(args.session.trace);
295
+ const outcomes = formatParticipantOutcomes(view.tally, [
296
+ { status: args.session.status, ...(ending === undefined ? {} : { label: ending.label }) },
297
+ ]);
298
+ return [
299
+ `1 participant took part: ${outcomes}.`,
300
+ facts.tasks === undefined ? undefined : `Tasks: ${formatRunTaskFunnel(facts.tasks)}.`,
301
+ facts.credibilityNote,
302
+ `How it ended: ${view.reason}`,
303
+ ]
304
+ .filter((sentence) => sentence !== undefined)
305
+ .join(" ");
306
+ }
260
307
  function singleReview(args, view, stream) {
261
- const { reason } = view;
262
308
  // A funnel with a denominator of one is still the funnel, and its absence stays recorded: no
263
309
  // declared protocol (or a dry run) means no `tasks` field, never an empty one.
264
310
  const singleRunTasks = args.inProgress !== true && args.session?.trace.taskFunnel !== undefined
265
311
  ? aggregateTaskFunnels([args.session.trace.taskFunnel])
266
312
  : undefined;
267
- // What happened to the participant, as the route judged it: the same rule the fan-out roll-up
268
- // applies (participantStatus in src/run/judge.ts). Reading the actor's own status instead would write up
269
- // a run the route refused as "not a credible pass" as verdict pass, 1/1 reached the goal, and
270
- // every projection of the bundle (Observer tally, `runs`, the status index) would repeat it.
271
- const participantStatus = args.session === undefined
272
- ? undefined
273
- : participantStatusFor(args.session.status, args.credibility);
274
- const credibilityNote = args.session === undefined || participantStatus === args.session.status
313
+ const credibilityNote = args.inProgress === true || args.session?.status !== "passed"
275
314
  ? undefined
276
315
  : args.credibility?.noEngagement === true
277
316
  ? "Not counted as a pass: the participant took no actions and said nothing."
278
- : "Not counted as a pass: the participant's final message described a blocker.";
317
+ : args.credibility?.selfReportedBlocker === true
318
+ ? "Not counted as a pass: the participant's final message described a blocker."
319
+ : undefined;
279
320
  const review = withCuaReviewProvenance({
280
321
  schema: REVIEW_SCHEMA,
281
322
  verdict: args.verdict,
282
323
  // One participant is still a study with a denominator of one, and saying so keeps a
283
324
  // single-participant result from being read as though it generalized.
284
- ...(participantStatus !== undefined && args.inProgress !== true
325
+ ...(args.session !== undefined && args.inProgress !== true
285
326
  ? {
286
- participants: tallyParticipantOutcomes([participantStatus], [args.credibility?.reportedFriction === true]),
327
+ participants: view.tally,
287
328
  }
288
329
  : {}),
289
330
  ...(singleRunTasks === undefined ? {} : { tasks: singleRunTasks }),
290
- summary: credibilityNote === undefined ? reason : `${credibilityNote} ${reason}`,
331
+ summary: singleSummary(args, view, {
332
+ ...(singleRunTasks === undefined ? {} : { tasks: singleRunTasks }),
333
+ ...(credibilityNote === undefined ? {} : { credibilityNote }),
334
+ }),
291
335
  gaps: args.session || args.sessionError !== undefined
292
336
  ? []
293
337
  : args.inProgress === true
@@ -1,4 +1,5 @@
1
1
  import type { Verdict } from "../../run/judge.js";
2
+ import type { LocalCapacity } from "../../substrates/local/capacity.js";
2
3
  import type { RunDesktopRecording } from "../../evidence/desktop-recording-types.js";
3
4
  import type { CuaExecutor, CuaLiveMetadata, CuaLoopResult, CuaProvider } from "../../actors/computer-use/loop.js";
4
5
  import type { CommsReceivingRun } from "../../comms/receiving.js";
@@ -38,6 +39,8 @@ export declare const MIN_DERIVED_SESSION_TIMEOUT_MS: number;
38
39
  */
39
40
  export interface LocalVmInput {
40
41
  readonly desktop: ParticipantDesktopFactory;
42
+ /** How many desktops the local runtime holds, or undefined when it cannot be read. */
43
+ readonly capacity?: () => Promise<LocalCapacity | undefined>;
41
44
  /** The skip reason automatic analysis records instead of running, or undefined to run it. */
42
45
  readonly analysisRefusal: () => AutomaticAnalysisRefusal | undefined;
43
46
  readonly signal?: AbortSignal;
@@ -182,7 +185,7 @@ export interface CuaParticipantSummary {
182
185
  concurrency: number;
183
186
  waves: number;
184
187
  }
185
- export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
188
+ export type CuaActorStudyErrorCode = StudyResultErrorCode | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED" | "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
186
189
  /** Subject provenance projection: what the actor actually drove. */
187
190
  export interface CuaSubjectProjection {
188
191
  source: "app-url" | "clone" | "local-tree";
@@ -36,5 +36,5 @@ interface ScriptedBundleArgs {
36
36
  * the bundle-builder tests.
37
37
  */
38
38
  export declare function buildScriptedStudyBundle(args: ScriptedBundleArgs): RunBundle;
39
- export declare function renderScriptedReviewMarkdown(bundle: RunBundle): string;
39
+ export declare function renderScriptedReviewMarkdown(bundle: RunBundle, status?: unknown): string;
40
40
  export {};
@@ -1,13 +1,13 @@
1
1
  // Assemble the scripted-browser study's run bundle from its sessions, and the review summary and
2
2
  // markdown that ship with it.
3
- import { verdictText } from "../../run/judge.js";
4
- import { reviewOutcome } from "../../run/display.js";
3
+ import { renderReviewMarkdown } from "../../run/review-markdown.js";
5
4
  import { redactText } from "../../evidence/redaction.js";
6
5
  import { buildRunCostSummary, desktopSpanToMinutes, spendFreeCostSummary, } from "../../run/cost-summary.js";
7
6
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
8
7
  import {} from "../../run/streams.js";
9
8
  import { participantEvent, recordIdOf } from "../../run/participant-records.js";
10
9
  import { scriptedSurfaceIds, scriptedSurfaceRecords } from "./surface-records.js";
10
+ import { gapsListClause } from "../../run/outcomes.js";
11
11
  import { plural } from "../../run/text.js";
12
12
  /**
13
13
  * Project the scripted run into a humanish.run-bundle.v1 (no schema change: a new
@@ -146,7 +146,7 @@ function buildScriptedReview(args) {
146
146
  return {
147
147
  schema: REVIEW_SCHEMA,
148
148
  verdict: args.verdict,
149
- summary: `The scripted run failed before a terminal session verdict: ${args.sessionError}`,
149
+ summary: `The run failed before the script finished: ${args.sessionError}`,
150
150
  gaps: [],
151
151
  };
152
152
  }
@@ -154,41 +154,31 @@ function buildScriptedReview(args) {
154
154
  return {
155
155
  schema: REVIEW_SCHEMA,
156
156
  verdict: args.verdict,
157
- summary: `Dry run of scenario ${args.journey.scenarioId} (${args.scenarioSource}, ${args.journey.steps.length} steps) against ${args.appUrl}: composition and scenario checked at $0; no browser ran.`,
157
+ summary: `Dry run: scenario ${args.journey.scenarioId} (${args.scenarioSource}, ${args.journey.steps.length} steps) would replay against ${args.appUrl} on ${plural(args.surfaces.length, "browser surface")}. No browser ran and $0 was spent.`,
158
158
  gaps: ["Live scripted session not yet run (dry run only)."],
159
159
  };
160
160
  }
161
161
  const passed = args.sessionResults.filter((result) => result.status === "passed").length;
162
+ const ran = args.sessionResults.length;
162
163
  return {
163
164
  schema: REVIEW_SCHEMA,
164
165
  verdict: args.verdict,
165
- summary: `Scripted-browser actor replayed ${args.journey.scenarioId} on ${args.sessionResults.length} surface${args.sessionResults.length === 1 ? "" : "s"} against ${args.appUrl}: ${passed}/${args.sessionResults.length} satisfied the scenario predicate.`,
166
+ summary: `Scenario ${args.journey.scenarioId} ran on ${plural(ran, "browser surface")} against ${args.appUrl}: ${passed}/${ran} passed it${gapsListClause(ran - passed, ran)}.`,
166
167
  gaps: args.sessionResults
167
168
  .filter((result) => result.status !== "passed")
168
169
  .map((result) => `${result.capture.surface.id}: ${result.reason}`),
169
170
  };
170
171
  }
171
- export function renderScriptedReviewMarkdown(bundle) {
172
+ export function renderScriptedReviewMarkdown(bundle, status) {
172
173
  const subject = bundle.events.find((event) => event.type === "scripted-lab.subject.declared");
173
174
  const spend = bundle.events.find((event) => event.type === "scripted-lab.spend");
174
175
  const traces = bundle.streams
175
176
  .map((stream) => ({ stream, trace: stream.actor }))
176
177
  .filter((entry) => entry.trace !== undefined);
177
- return [
178
- `# ${bundle.scenario.title}`,
179
- "",
180
- `- run: ${bundle.runId}`,
181
- `- mode: ${bundle.mode}`,
182
- `- verdict: ${verdictText(bundle.review.verdict, bundle.mode)}`,
183
- `- outcome: ${reviewOutcome(bundle)}`,
184
- `- summary: ${bundle.review.summary}`,
178
+ return renderReviewMarkdown(bundle, [
185
179
  `- scenario: ${bundle.scenario.id} @ ${bundle.scenario.sourceDigest} (${bundle.scenario.source})`,
186
180
  ...(subject ? [`- subject: ${subject.message}`] : []),
187
181
  ...(spend ? [`- spend: ${spend.message}`] : []),
188
182
  ...traces.map(({ stream, trace }) => `- ${recordIdOf(stream)}: ${trace.provider} (${trace.lane}/${trace.protocol}) ${trace.status} (${trace.completionReason}); ${plural(trace.counts.actions ?? 0, "step action")}, ${plural(trace.counts.screenshots ?? 0, "raw screenshot")}`),
189
- ...(bundle.review.gaps.length > 0
190
- ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)]
191
- : []),
192
- "",
193
- ].join("\n");
183
+ ], { status });
194
184
  }
@@ -6,7 +6,6 @@ import { type ParticipantIds } from "../../run/participant-records.js";
6
6
  import type { RunStream } from "../../run/streams.js";
7
7
  /** What each surface's records read from the bundle arguments. */
8
8
  export interface ScriptedSurfaceContext {
9
- actorId: string;
10
9
  appUrl: string;
11
10
  createdAt: string;
12
11
  journey: BrowserPersonaJourney;
@@ -1,5 +1,7 @@
1
- // One scripted-browser surface's simulation and stream in the run bundle.
1
+ import { judgeParticipantRecords } from "../../run/judge.js";
2
2
  import { participantIdsOf, participantRecord, participantStream, } from "../../run/participant-records.js";
3
+ import { participantCaption } from "../../run/participant-caption.js";
4
+ import { sessionEndingInWords } from "../../run/outcomes.js";
3
5
  /** A surface's saved ids: `scripted-<surface>` and its `-stream`. */
4
6
  export function scriptedSurfaceIds(surfaceId) {
5
7
  return participantIdsOf(`scripted-${surfaceId}`, `scripted-${surfaceId}-stream`);
@@ -11,14 +13,23 @@ export function scriptedSurfaceIds(surfaceId) {
11
13
  export function scriptedSurfaceRecords(context, surface, index, result, screenshots) {
12
14
  const ids = scriptedSurfaceIds(surface.id);
13
15
  const lastScreenshot = screenshots.at(-1);
14
- const status = result
15
- ? result.status
16
- : context.sessionError !== undefined
17
- ? "failed"
18
- : "contract_proof_only";
19
- const reason = result?.reason ??
20
- context.sessionError ??
21
- "Dry run: the scenario was pinned without launching a browser or touching the subject app.";
16
+ const { status, reason } = judgeParticipantRecords([
17
+ {
18
+ ...(result === undefined
19
+ ? {}
20
+ : {
21
+ status: result.status,
22
+ completionReason: result.completionReason,
23
+ reason: result.reason,
24
+ }),
25
+ ...(context.sessionError === undefined ? {} : { sessionError: context.sessionError }),
26
+ skipped: false,
27
+ noEngagement: false,
28
+ selfReportedBlocker: false,
29
+ },
30
+ ], {
31
+ contractReason: "Dry run: the scenario was pinned without launching a browser or touching the subject app.",
32
+ }).participants[0];
22
33
  const simulation = participantRecord(ids, index + 1, {
23
34
  personaId: context.persona.id,
24
35
  scenarioId: context.journey.scenarioId,
@@ -28,16 +39,19 @@ export function scriptedSurfaceRecords(context, surface, index, result, screensh
28
39
  progress: 100,
29
40
  currentStep: reason,
30
41
  summary: result
31
- ? `Scripted-browser actor (${context.actorId}) replayed ${context.journey.scenarioId} on the ${surface.id} surface; ${result.completionReason}.`
42
+ ? `Scenario ${context.journey.scenarioId} ran in the ${surface.id} browser and ${sessionEndingInWords(result.trace)}.`
32
43
  : context.sessionError !== undefined
33
- ? `The scripted run failed before a terminal session verdict: ${context.sessionError}`
34
- : `Scripted-browser actor (${context.actorId}) against ${context.appUrl}; no session ran.`,
44
+ ? `The run failed before the script finished: ${context.sessionError}`
45
+ : `Scenario ${context.journey.scenarioId} would replay in the ${surface.id} browser against ${context.appUrl}; no browser ran.`,
35
46
  startedAt: context.createdAt,
36
47
  updatedAt: result?.capture.capturedAt ?? context.createdAt,
37
48
  });
38
49
  const stream = participantStream(ids, {
39
50
  kind: "browser",
40
- label: `${surface.label} · ${context.studyId}`,
51
+ label: participantCaption({
52
+ personaId: context.persona.id,
53
+ device: { name: surface.id, preset: { isMobile: surface.viewport.isMobile } },
54
+ }),
41
55
  status,
42
56
  transport: "snapshot",
43
57
  updatedAt: result?.capture.capturedAt ?? context.createdAt,
@@ -1,14 +1,14 @@
1
1
  import type { RunEvent, RunSimulation } from "../../run/bundle.js";
2
+ import type { judgeParticipantRecords } from "../../run/judge.js";
2
3
  import type { RunStream } from "../../run/streams.js";
3
4
  import type { DesktopParticipantRun } from "../computer-use/types.js";
4
5
  import type { ConcurrentBundleArgs } from "./types.js";
5
6
  /** What every participant's records share. */
6
7
  export interface SharedWorldRecordContext {
7
8
  args: ConcurrentBundleArgs;
9
+ participants: ReturnType<typeof judgeParticipantRecords>["participants"];
8
10
  external: boolean;
9
11
  inProgress: boolean;
10
- /** The public-safe plane label: never the raw getHost URL or public origin. */
11
- appUrl: string;
12
12
  /** Numbers events in the order they are pushed. */
13
13
  nextEventId: (suffix: string) => string;
14
14
  }
@@ -3,14 +3,13 @@
3
3
  // running).
4
4
  import { participantAssignment } from "../../study/participant-assignment.js";
5
5
  import { participantEvent, participantRecord, participantStream, } from "../../run/participant-records.js";
6
- import { judgedStatus } from "../../run/judge.js";
7
6
  import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.js";
8
- import { participantFactsOf } from "../computer-use/participant-facts.js";
9
7
  import { publicSafeRouteLabel } from "./provenance.js";
10
- import { participantTaxonomyLabel } from "./participant-specs.js";
8
+ import { participantCaption } from "../../run/participant-caption.js";
9
+ import { sessionEndingInWords } from "../../run/outcomes.js";
11
10
  function participantView(ctx, spec, index) {
12
- const { args, external, inProgress } = ctx;
13
- const taxonomy = participantTaxonomyLabel(spec.planned.labels);
11
+ const { args, external } = ctx;
12
+ const name = participantCaption({ id: spec.planned.id, personaId: spec.persona.id });
14
13
  const result = args.actorResults[index];
15
14
  const outcome = result?.outcome;
16
15
  const session = outcome?.session;
@@ -21,18 +20,8 @@ function participantView(ctx, spec, index) {
21
20
  const route = external
22
21
  ? "[external-public-plane]"
23
22
  : publicSafeRouteLabel(args.plan.plane.participants[index]?.entry);
24
- const status = session
25
- ? session.status
26
- : outcome?.sessionError !== undefined
27
- ? "failed"
28
- : inProgress
29
- ? "running"
30
- : "contract_proof_only";
31
- const reason = session?.reason ??
32
- outcome?.sessionError ??
33
- (inProgress
34
- ? "Actor desktop is running; the attached Observer hydrates the runtime stream URL without persisting it."
35
- : "Dry run: the evidence shape was written without launching a desktop or spending provider tokens.");
23
+ const judgment = ctx.participants[index];
24
+ const { status, reason } = judgment;
36
25
  const traceScreenshotMode = session?.trace.redaction.screenshots;
37
26
  // Include `declared` on the no-outcome fallback too (dry-run, skipped participant): otherwise an
38
27
  // absent `declared` means either "the preset rendered faithfully" or "there was no live
@@ -53,7 +42,8 @@ function participantView(ctx, spec, index) {
53
42
  ? "blurred"
54
43
  : "raw";
55
44
  return {
56
- taxonomy,
45
+ name,
46
+ judgment,
57
47
  outcome,
58
48
  session,
59
49
  screenshots,
@@ -67,7 +57,7 @@ function participantView(ctx, spec, index) {
67
57
  }
68
58
  function sharedWorldSimulation(ctx, spec, index, view) {
69
59
  const { args, inProgress } = ctx;
70
- const { taxonomy, outcome, session } = view;
60
+ const { name, outcome, session } = view;
71
61
  return participantRecord(spec, index + 1, {
72
62
  personaId: spec.persona.id,
73
63
  scenarioId: `concurrent-shared-world-${args.plan.studyId}`,
@@ -77,27 +67,31 @@ function sharedWorldSimulation(ctx, spec, index, view) {
77
67
  progress: inProgress ? 35 : 100,
78
68
  currentStep: view.reason,
79
69
  summary: session
80
- ? `Persona ${spec.planned.id}${taxonomy} (${spec.persona.id}): drove the shared plane concurrently; ${session.completionReason}.`
70
+ ? `${name} used the shared app and ${sessionEndingInWords(session.trace)}.`
81
71
  : outcome?.sessionError !== undefined
82
- ? `Persona ${spec.planned.id}${taxonomy} failed before a terminal session verdict: ${outcome.sessionError}`
72
+ ? `${name} did not finish a session: ${outcome.sessionError}`
83
73
  : inProgress
84
- ? `Persona ${spec.planned.id}${taxonomy} (${spec.persona.id}) is running against the shared plane.`
85
- : `Persona ${spec.planned.id}${taxonomy} (${spec.persona.id}) for ${args.descriptor.id} against the shared plane at ${ctx.appUrl}; no session ran.`,
74
+ ? `${name} is using the shared app.`
75
+ : `${name} would use the shared app; no session ran.`,
86
76
  startedAt: args.run.createdAt,
87
77
  updatedAt: args.run.createdAt,
88
78
  });
89
79
  }
90
80
  function sharedWorldStream(ctx, spec, index, view) {
91
81
  const { args } = ctx;
92
- const { taxonomy, session, screenshots, lastScreenshot, desktopGeometry, screenshotMode } = view;
93
- // The judge's status for the participant, so the Observer shows it the way the verdict judged it.
94
- const judged = view.outcome === undefined ? undefined : judgedStatus(participantFactsOf(view.outcome));
82
+ const { name, session, screenshots, lastScreenshot, desktopGeometry, screenshotMode } = view;
83
+ const judged = view.judgment.judgedStatus;
84
+ const caption = participantCaption({
85
+ id: spec.planned.id,
86
+ personaId: spec.persona.id,
87
+ device: spec.planned.device,
88
+ });
95
89
  return participantStream(spec, {
96
90
  ...(spec.evidenceAssignment === undefined
97
91
  ? {}
98
92
  : { assignment: participantAssignment(spec.evidenceAssignment) }),
99
93
  kind: "browser",
100
- label: `Concurrent persona ${spec.planned.id}${taxonomy} · ${args.plan.studyId}`,
94
+ label: caption,
101
95
  status: view.status,
102
96
  ...(judged === undefined ? {} : { judgedStatus: judged }),
103
97
  transport: "snapshot",
@@ -106,9 +100,9 @@ function sharedWorldStream(ctx, spec, index, view) {
106
100
  ? {
107
101
  kind: "screenshot",
108
102
  url: lastScreenshot,
109
- title: `Shared plane, persona ${spec.planned.id} (${screenshotMode})`,
103
+ title: `${caption} (${screenshotMode})`,
110
104
  }
111
- : { kind: "placeholder", title: `Shared plane, persona ${spec.planned.id}` },
105
+ : { kind: "placeholder", title: caption },
112
106
  ...(desktopGeometry.viewport === undefined
113
107
  ? {}
114
108
  : {
@@ -122,7 +116,7 @@ function sharedWorldStream(ctx, spec, index, view) {
122
116
  desktopGeometry,
123
117
  ui: {
124
118
  route: view.route,
125
- intent: `Watch persona ${spec.planned.id}${taxonomy} (${spec.persona.id}) use the shared app at the same time as the other personas.`,
119
+ intent: `Watch ${name} use the shared app at the same time as the others.`,
126
120
  state: view.reason,
127
121
  ...(session ? { actorStatus: session.status } : {}),
128
122
  ...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {}),
@@ -17,4 +17,4 @@ export declare function judgeSharedWorldRun(args: Omit<ConcurrentBundleArgs, "ju
17
17
  export declare function actorRunPassed(result: ActorRunResult | undefined): boolean;
18
18
  /** Project the concurrent run into a humanish.run-bundle.v1 with the concurrent shared-world block. */
19
19
  export declare function buildConcurrentSharedWorldBundle(args: ConcurrentBundleArgs): RunBundle;
20
- export declare function renderConcurrentReviewMarkdown(bundle: RunBundle): string;
20
+ export declare function renderConcurrentReviewMarkdown(bundle: RunBundle, status?: unknown): string;