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
@@ -45,5 +45,5 @@ interface CuaRunBundleState {
45
45
  */
46
46
  export declare function buildCuaRunBundle(base: CuaRunBundleBase, state: CuaRunBundleState): RunBundle;
47
47
  /** The run's review.md: title, run, mode, gate, summary, subject, actor evidence and gaps. */
48
- export declare function renderCuaReviewMarkdown(bundle: RunBundle): string;
48
+ export declare function renderCuaReviewMarkdown(bundle: RunBundle, status?: unknown): string;
49
49
  export {};
@@ -2,8 +2,8 @@
2
2
  // and buildCuaRunBundle sends it to the single-participant or the fan-out builder with that
3
3
  // shape's arguments. renderCuaReviewMarkdown writes review.md. shared-world/bundle.ts holds the
4
4
  // same for that route.
5
- import { judgeOneParticipant, judgeParticipants, verdictText, } from "../../run/judge.js";
6
- import { reviewOutcome } from "../../run/display.js";
5
+ import { judgeOneParticipant, judgeParticipants } from "../../run/judge.js";
6
+ import { renderReviewMarkdown } from "../../run/review-markdown.js";
7
7
  import { desktopSpanToMinutes } from "../../run/cost-summary.js";
8
8
  import { e2bDesktopTemplate } from "../../substrates/e2b/sandbox.js";
9
9
  import { providerResourcesForOutcome } from "./bundle-parts.js";
@@ -92,6 +92,7 @@ function singleParticipantArgs(base, state) {
92
92
  ...(spec.evidenceAssignment === undefined ? {} : { assignment: spec.evidenceAssignment }),
93
93
  persona: spec.persona,
94
94
  resolution: spec.planned.device.resolution,
95
+ deviceName: spec.planned.device.name,
95
96
  desktopRoute: base.plan.runner.desktop !== "in-process",
96
97
  substrate: base.plan.runner.desktop === "in-process"
97
98
  ? "local-filesystem"
@@ -151,17 +152,10 @@ function singleParticipantArgs(base, state) {
151
152
  };
152
153
  }
153
154
  /** The run's review.md: title, run, mode, gate, summary, subject, actor evidence and gaps. */
154
- export function renderCuaReviewMarkdown(bundle) {
155
+ export function renderCuaReviewMarkdown(bundle, status) {
155
156
  const trace = bundle.streams[0]?.actor;
156
157
  const provenance = bundle.events.find((event) => event.type === "cua-lab.subject.provenance");
157
- return [
158
- `# ${bundle.scenario.title}`,
159
- "",
160
- `- run: ${bundle.runId}`,
161
- `- mode: ${bundle.mode}`,
162
- `- verdict: ${verdictText(bundle.review.verdict, bundle.mode)}`,
163
- `- outcome: ${reviewOutcome(bundle)}`,
164
- `- summary: ${bundle.review.summary}`,
158
+ return renderReviewMarkdown(bundle, [
165
159
  ...(provenance ? [`- subject: ${provenance.message}`] : []),
166
160
  ...(trace
167
161
  ? [
@@ -173,9 +167,5 @@ export function renderCuaReviewMarkdown(bundle) {
173
167
  : "screenshot")}`,
174
168
  ]
175
169
  : []),
176
- ...(bundle.review.gaps.length > 0
177
- ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)]
178
- : []),
179
- "",
180
- ].join("\n");
170
+ ], { status });
181
171
  }
@@ -3,11 +3,11 @@ import { receivingPublication } from "../../comms/receiving-runtime.js";
3
3
  import { actorEnding } from "../../actors/stop-cause.js";
4
4
  import { redactText } from "../../evidence/redaction.js";
5
5
  import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
6
- import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, tallyParticipantOutcomes, withCuaReviewProvenance, } from "../../run/outcomes.js";
6
+ import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, withCuaReviewProvenance, } from "../../run/outcomes.js";
7
7
  import { providerResourcesForOutcome, publicSafeAppUrlLabel } from "./bundle-parts.js";
8
8
  import { participantFeedbackCandidates } from "./participant-feedback.js";
9
9
  import { participantFactsOf } from "./participant-facts.js";
10
- import { participantPassed, participantStatus } from "../../run/judge.js";
10
+ import { judgeParticipantRecords } from "../../run/judge.js";
11
11
  import { buildRunCostSummary, desktopSpanToMinutes } from "../../run/cost-summary.js";
12
12
  import { formatParticipantPlanEntry } from "./participant-runs.js";
13
13
  import { fanoutParticipantRecords } from "./fanout-records.js";
@@ -32,29 +32,32 @@ function fanoutPlanEvents(args) {
32
32
  });
33
33
  return events;
34
34
  }
35
- function fanoutReview(args, streams) {
36
- const { specs, outcomes } = args;
35
+ /**
36
+ * The run's summary in plain sentences: who took part, what happened to them with the denominator
37
+ * attached, and that the gaps list the participants who did not pass.
38
+ */
39
+ function fanoutSummary(args, facts) {
40
+ const count = args.specs.length;
41
+ const people = plural(count, "participant");
42
+ const rerun = args.rerun === undefined ? "" : `Rerun from ${args.rerun.sourceRunId}: `;
43
+ if (args.inProgress === true) {
44
+ return `${people} ${count === 1 ? "is" : "are"} each using their own copy of the app. The run is still going, so nothing here is final.`;
45
+ }
46
+ if (args.dryRun) {
47
+ return `${rerun}Dry run: ${people} would each use their own copy of ${publicSafeAppUrlLabel(args.appUrl)}. No desktops were launched and $0 was spent.`;
48
+ }
49
+ const tasks = facts.tasks === undefined ? "" : ` Tasks: ${facts.tasks}.`;
50
+ return `${rerun}${people} took part, each in their own copy of the app: ${facts.outcomes}${gapsListClause(count - facts.passed, count)}.${tasks}`;
51
+ }
52
+ function fanoutReview(args, streams, judgment) {
53
+ const { outcomes } = args;
37
54
  // The judge's verdict: live fan-out must prove every participant (judgeParticipants).
38
55
  const verdict = args.verdict;
39
- const passedParticipants = (outcomes ?? []).filter((outcome) => participantPassed(participantFactsOf(outcome))).length;
56
+ const passedParticipants = judgment.participants.filter((participant) => participant.judgedStatus === "passed").length;
40
57
  // What happened to the participants, with the denominator attached. The verdict above has to
41
58
  // collapse the run to one word; this does not (docs/principles/three-roles.md).
42
59
  const terminalOutcomes = (outcomes ?? []).filter((outcome) => outcome?.session?.status !== undefined);
43
- const participants = terminalOutcomes.length > 0
44
- ? tallyParticipantOutcomes(
45
- // A no-engagement participant is not one who reached the goal. It said "done" having
46
- // taken zero actions and said nothing, and `passedParticipants` above already refuses to
47
- // count it, but `reachedGoal` was reading the trace status directly, so one run could be both
48
- // "not a passed participant" and "1/1 reached the goal". The headline number a researcher reads
49
- // first was the dishonest one. Found by a provider bug that ended a study on turn one.
50
- terminalOutcomes.map((outcome) => participantStatus(outcome.session.status, {
51
- noEngagement: outcome.noEngagement === true,
52
- selfReportedBlocker: outcome.selfReportedBlocker === true,
53
- })),
54
- // A participant who reached the goal and told you the road there was broken is the most
55
- // useful result a study produces; reporting only the outcome would bury it.
56
- terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
57
- : undefined;
60
+ const participants = judgment.tally.total > 0 ? judgment.tally : undefined;
58
61
  // The study funnel: per-task completion rates across every session that measured one. This is
59
62
  // "where did people get stuck" as data, next to who got stuck (participants) above.
60
63
  const participantFunnels = (outcomes ?? [])
@@ -75,25 +78,18 @@ function fanoutReview(args, streams) {
75
78
  ...(runTasks === undefined ? {} : { tasks: runTasks }),
76
79
  // The app URL is named as the participant records name it: one on an E2B host names a
77
80
  // sandbox, so the summary carries its digest, which verify accepts in a shared run.
78
- summary: args.inProgress === true
79
- ? `Live computer-use fan-out is running (${specs.length} participants, one world each); terminal participant evidence has not been written yet.`
80
- : args.dryRun
81
- ? `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Dry-run fan-out: ${specs.length} participants composed for ${args.descriptor.id} against ${publicSafeAppUrlLabel(args.appUrl)}, one world each; no desktops launched, $0 spend.`
82
- : `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Computer-use fan-out (${specs.length} participants, one world each): ${passedParticipants}/${plural(specs.length, "participant")} reached a terminal, engaged verdict${participants ? `: ${formatParticipantOutcomes(participants, participantEndings)}` : ""}${runTasks ? `; tasks: ${formatRunTaskFunnel(runTasks)}` : ""}.`,
81
+ summary: fanoutSummary(args, {
82
+ passed: passedParticipants,
83
+ outcomes: participants === undefined
84
+ ? `0/${args.specs.length} finished a session`
85
+ : formatParticipantOutcomes(participants, participantEndings),
86
+ ...(runTasks === undefined ? {} : { tasks: formatRunTaskFunnel(runTasks) }),
87
+ }),
83
88
  gaps: args.inProgress === true
84
89
  ? ["Live fan-out session is still running."]
85
90
  : args.dryRun
86
91
  ? ["Live fan-out session not yet run (dry run only)."]
87
- : specs
88
- .map((spec, index) => ({ spec, outcome: outcomes?.[index] }))
89
- .filter(({ outcome }) => outcome === undefined ||
90
- outcome.skippedReason !== undefined ||
91
- outcome.sessionError !== undefined ||
92
- outcome.noEngagement ||
93
- outcome.selfReportedBlocker ||
94
- outcome.session === undefined ||
95
- outcome.session.status !== "passed")
96
- .map(({ spec, outcome }) => `${spec.planned.id}: ${outcome?.skippedReason ?? outcome?.sessionError ?? outcome?.session?.reason ?? "did not pass"}`),
92
+ : judgment.participants.flatMap((participant) => participant.gapLine === undefined ? [] : [participant.gapLine]),
97
93
  }, streams);
98
94
  return review;
99
95
  }
@@ -175,6 +171,13 @@ export function buildCuaFanoutBundle(args) {
175
171
  const simulations = [];
176
172
  const streams = [];
177
173
  const events = fanoutPlanEvents(args);
174
+ const judgment = judgeParticipantRecords(specs.map((spec, index) => ({
175
+ ...participantFactsOf(outcomes?.[index]),
176
+ id: spec.planned.id,
177
+ inProgress: args.inProgress === true && outcomes?.[index] === undefined,
178
+ })), {
179
+ runningReason: "Live computer-use participant is running; stream auth URL is available only through the attached Observer server.",
180
+ });
178
181
  let eventSeq = 2;
179
182
  const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
180
183
  if (args.rerun) {
@@ -187,7 +190,7 @@ export function buildCuaFanoutBundle(args) {
187
190
  });
188
191
  }
189
192
  specs.forEach((spec, index) => {
190
- const records = fanoutParticipantRecords({ args, nextEventId }, spec, index);
193
+ const records = fanoutParticipantRecords({ args, nextEventId, participants: judgment.participants }, spec, index);
191
194
  simulations.push(records.simulation);
192
195
  streams.push(records.stream);
193
196
  events.push(...records.events);
@@ -201,7 +204,7 @@ export function buildCuaFanoutBundle(args) {
201
204
  message: `Fan-out fail-fast: ${args.failFastReason}. In-flight participants finished; queued participants were skipped (blocked); completed evidence is retained.`,
202
205
  });
203
206
  }
204
- const review = fanoutReview(args, streams);
207
+ const review = fanoutReview(args, streams, judgment);
205
208
  const anyRaw = (outcomes ?? []).some((outcome) => outcome.session?.trace.redaction.screenshots === "raw");
206
209
  const ranLive = (outcomes ?? []).some((outcome) => outcome.session !== undefined || outcome.sessionError !== undefined);
207
210
  const desktopTemplate = e2bDesktopTemplate(plan.residual);
@@ -1,9 +1,11 @@
1
1
  import type { RunEvent, RunSimulation } from "../../run/bundle.js";
2
2
  import type { RunStream } from "../../run/streams.js";
3
+ import type { judgeParticipantRecords } from "../../run/judge.js";
3
4
  import type { CuaFanoutBundleArgs, DesktopParticipantRun } from "./types.js";
4
5
  /** What every participant's records share. */
5
6
  export interface FanoutParticipantContext {
6
7
  args: CuaFanoutBundleArgs;
8
+ participants: ReturnType<typeof judgeParticipantRecords>["participants"];
7
9
  /** Numbers events in the order they are pushed. */
8
10
  nextEventId: (suffix: string) => string;
9
11
  }
@@ -2,13 +2,13 @@
2
2
  // subject provenance event and the events of its outcome (session, geometry warnings, phase trail).
3
3
  import { participantAssignment } from "../../study/participant-assignment.js";
4
4
  import { participantEvent, participantRecord, participantStream, } from "../../run/participant-records.js";
5
+ import { participantCaption } from "../../run/participant-caption.js";
6
+ import { sessionEndingInWords } from "../../run/outcomes.js";
5
7
  import { declaredScreenForRender } from "../../substrates/e2b/desktop-geometry.js";
6
8
  import { describeSubjectState, phaseEventIdSuffix, publicSafeAppUrlLabel } from "./bundle-parts.js";
7
- import { judgedStatus } from "../../run/judge.js";
8
- import { participantFactsOf } from "./participant-facts.js";
9
9
  import { participantSubjectEnv } from "./types.js";
10
10
  const describeEnvNames = (names) => names.length > 0 ? names.join(", ") : "none";
11
- function fanoutParticipantView(args, spec, index) {
11
+ function fanoutParticipantView(args, spec, index, participant) {
12
12
  const { outcomes, plan } = args;
13
13
  const outcome = outcomes?.[index];
14
14
  const targetUrl = spec.planned.targetUrl ?? args.appUrl;
@@ -27,21 +27,7 @@ function fanoutParticipantView(args, spec, index) {
27
27
  };
28
28
  const screenshots = outcome?.screenshots ?? [];
29
29
  const lastScreenshot = screenshots[screenshots.length - 1];
30
- const status = args.inProgress === true && outcome === undefined
31
- ? "running"
32
- : outcome?.skippedReason !== undefined
33
- ? "blocked"
34
- : session
35
- ? session.status
36
- : outcome?.sessionError !== undefined
37
- ? "failed"
38
- : "contract_proof_only";
39
- const reason = args.inProgress === true && outcome === undefined
40
- ? "Live computer-use participant is running; stream auth URL is available only through the attached Observer server."
41
- : (outcome?.skippedReason ??
42
- session?.reason ??
43
- outcome?.sessionError ??
44
- "Dry run: the evidence shape was written without launching a desktop or spending provider tokens.");
30
+ const { status, reason } = participant;
45
31
  const traceScreenshotMode = session?.trace.redaction.screenshots;
46
32
  const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
47
33
  ? traceScreenshotMode
@@ -50,6 +36,7 @@ function fanoutParticipantView(args, spec, index) {
50
36
  : "raw";
51
37
  return {
52
38
  outcome,
39
+ participant,
53
40
  publicTargetUrl,
54
41
  subject,
55
42
  session,
@@ -64,6 +51,7 @@ function fanoutParticipantView(args, spec, index) {
64
51
  function fanoutParticipantRecord(args, spec, index, view) {
65
52
  const { plan } = args;
66
53
  const { outcome, publicTargetUrl, session, status, reason } = view;
54
+ const name = participantCaption({ id: spec.planned.id, personaId: spec.persona.id });
67
55
  return participantRecord(spec, index + 1, {
68
56
  personaId: spec.persona.id,
69
57
  scenarioId: `cua-${plan.studyId}`,
@@ -73,14 +61,14 @@ function fanoutParticipantRecord(args, spec, index, view) {
73
61
  progress: args.inProgress === true && outcome === undefined ? 20 : 100,
74
62
  currentStep: reason,
75
63
  summary: session
76
- ? `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}): computer-use actor (${args.descriptor.id}) drove the subject app; ${session.completionReason}.`
64
+ ? `${name} used the app and ${sessionEndingInWords(session.trace)}.`
77
65
  : args.inProgress === true && outcome === undefined
78
- ? `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}): computer-use actor (${args.descriptor.id}) is driving the subject app.`
66
+ ? `${name} is using the app.`
79
67
  : outcome?.skippedReason !== undefined
80
- ? `Participant ${spec.planned.id} ${outcome.skippedReason}.`
68
+ ? `${name} was ${outcome.skippedReason}.`
81
69
  : outcome?.sessionError !== undefined
82
- ? `Participant ${spec.planned.id} failed before a terminal session verdict: ${outcome.sessionError}`
83
- : `Participant ${spec.planned.id} (${spec.persona.id}/${spec.planned.device.name}) for ${args.descriptor.id} against ${publicTargetUrl}; no session ran.`,
70
+ ? `${name} did not finish a session: ${outcome.sessionError}`
71
+ : `${name} would use ${publicTargetUrl}; no session ran.`,
84
72
  startedAt: args.run.createdAt,
85
73
  updatedAt: args.run.createdAt,
86
74
  });
@@ -88,7 +76,7 @@ function fanoutParticipantRecord(args, spec, index, view) {
88
76
  function fanoutParticipantStream(args, spec, view) {
89
77
  const { outcome, publicTargetUrl, session, desktopGeometry, screenshots } = view;
90
78
  const { lastScreenshot, status, reason, screenshotMode } = view;
91
- const judged = outcome === undefined ? undefined : judgedStatus(participantFactsOf(outcome));
79
+ const judged = view.participant.judgedStatus;
92
80
  return participantStream(spec, {
93
81
  ...(spec.evidenceAssignment === undefined
94
82
  ? {}
@@ -103,7 +91,11 @@ function fanoutParticipantStream(args, spec, view) {
103
91
  ? {}
104
92
  : { caseGroup: spec.planned.labels.caseGroup }),
105
93
  kind: "browser",
106
- label: `${spec.planned.id} · browser`,
94
+ label: participantCaption({
95
+ id: spec.planned.id,
96
+ personaId: spec.persona.id,
97
+ device: spec.planned.device,
98
+ }),
107
99
  status,
108
100
  ...(judged === undefined ? {} : { judgedStatus: judged }),
109
101
  transport: "snapshot",
@@ -298,7 +290,7 @@ function fanoutOutcomeEvents(ctx, spec, view) {
298
290
  }
299
291
  /** One participant's simulation, stream and events, in the order the bundle records them. */
300
292
  export function fanoutParticipantRecords(ctx, spec, index) {
301
- const view = fanoutParticipantView(ctx.args, spec, index);
293
+ const view = fanoutParticipantView(ctx.args, spec, index, ctx.participants[index]);
302
294
  return {
303
295
  simulation: fanoutParticipantRecord(ctx.args, spec, index, view),
304
296
  stream: fanoutParticipantStream(ctx.args, spec, view),
@@ -3,8 +3,9 @@ import path from "node:path";
3
3
  import { inboxRecipientFor, } from "./participant-desktop.js";
4
4
  import { createLocalFirecrackerDesktop, } from "../../substrates/local/firecracker-desktop.js";
5
5
  import { localBrowserDefaults, localBrowserUnsupportedReason, } from "../../substrates/local/runtime-config.js";
6
- import { prepareLocalRuntime } from "../../substrates/local/runtime.js";
7
- import { dockerCommandLine } from "../../substrates/local/runtime-host.js";
6
+ import { localRuntimeCapacity, prepareLocalRuntime } from "../../substrates/local/runtime.js";
7
+ import { dockerCommandLine, usesLima } from "../../substrates/local/runtime-host.js";
8
+ import { outOfMemoryMessage } from "../../substrates/local/capacity.js";
8
9
  import { checkRestrictedCodexAnalysisReadiness } from "../../analysis/restricted-codex.js";
9
10
  import { createRestrictedCodexParticipant } from "../../actors/codex/restricted-participant.js";
10
11
  import { withCloseReport } from "./participant-model.js";
@@ -110,6 +111,11 @@ function createLocalParticipantDesktop(context, run, warnings, artifactRoot) {
110
111
  warnings.push("Desktop video/audio recording could not be retained. Screenshots and participant evidence remain available.");
111
112
  }
112
113
  }
114
+ // Asked before close, which removes the container; the answer comes from Docker's events.
115
+ if ((await session.killedForMemory?.()) === true) {
116
+ evidence.failureCode = "HUMANISH_COMPUTER_USE_DESKTOP_OUT_OF_MEMORY";
117
+ evidence.desktopFailure = outOfMemoryMessage(usesLima() ? "lima-vm" : "linux-host");
118
+ }
113
119
  try {
114
120
  evidence.released = (await session.close()).status === "released";
115
121
  }
@@ -180,6 +186,7 @@ export function prepareLocalVmRun(options) {
180
186
  options: account ? { ...runOptions, createProvider: accountProvider(state) } : runOptions,
181
187
  localVm: {
182
188
  desktop: (run, warnings, artifactRoot) => createLocalParticipantDesktop(context, run, warnings, artifactRoot),
189
+ capacity: () => localRuntimeCapacity(),
183
190
  analysisRefusal: () => state.cleanupUnconfirmed ? "AUTOMATIC_ANALYSIS_CLEANUP_UNCONFIRMED" : undefined,
184
191
  ...(options.signal === undefined ? {} : { signal: options.signal }),
185
192
  },
@@ -16,6 +16,12 @@ export type ParticipantDesktopEvidence = {
16
16
  /** The substrate confirmed the desktop was released (an E2B kill or a local VM shutdown). The participant
17
17
  * outcome records it as `killed`. */
18
18
  released: boolean;
19
+ /**
20
+ * Why the desktop itself ended the session, in words a person reads, when the substrate knows:
21
+ * a local desktop Docker killed for memory. It replaces the session's error for a participant
22
+ * who did not pass.
23
+ */
24
+ desktopFailure?: string;
19
25
  } & Pick<ParticipantRunOutcome, "sandboxId" | "sandboxRelease" | "desktopDurationMs" | "desktopResources" | "streamUrlPresent" | "subjectCommit" | "desktopBrowser" | "desktopGeometry" | "stateStepRecords" | "phaseRecords" | "failureCode" | "commsArtifactPath" | "recording">;
20
26
  /**
21
27
  * The desktop one participant runs on, owned from preparation through final evidence collection.
@@ -1,3 +1,4 @@
1
+ import { judgeParticipantRecords } from "../../run/judge.js";
1
2
  import { createE2BParticipantDesktop } from "./e2b-desktop/desktop.js";
2
3
  import { createInProcessDesktop } from "./in-process-desktop.js";
3
4
  import path from "node:path";
@@ -6,7 +7,7 @@ import { mapWithConcurrency } from "../../run/concurrency.js";
6
7
  import { assertScreenshotEvidence, stripPngMetadataChunks } from "../../evidence/image.js";
7
8
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
8
9
  import { assertSafeOutputPathSegment, writeContainedOutputFile, } from "../../run/contained-output.js";
9
- import { participantOutcomeOk } from "./participant-facts.js";
10
+ import { participantOutcomeOk, participantFactsOf } from "./participant-facts.js";
10
11
  import { closeParticipantModel, judgeParticipantSession, participantSessionOptions, recordParticipantTrace, startParticipantModel, } from "./participant-model.js";
11
12
  /** Build a participant's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
12
13
  * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
@@ -106,6 +107,15 @@ export async function runCuaParticipant(spec, deps) {
106
107
  });
107
108
  }
108
109
  }
110
+ const { released, desktopFailure, ...desktopEvidence } = desktop.snapshot();
111
+ // A desktop that died under the participant explains the failure better than the transport
112
+ // error the session saw. A session that passed keeps its result.
113
+ if (desktopFailure !== undefined) {
114
+ if (session?.status === "passed")
115
+ warnings.push(desktopFailure);
116
+ else
117
+ sessionError = desktopFailure;
118
+ }
109
119
  if (session)
110
120
  await recordParticipantTrace(spec, deps, session, warnings);
111
121
  const { noEngagement, selfReportedBlocker, reportedFriction } = judgeParticipantSession(session, deps, warnings);
@@ -115,7 +125,6 @@ export async function runCuaParticipant(spec, deps) {
115
125
  providerCleanupError !== undefined ||
116
126
  providerPolicyError !== undefined ||
117
127
  session?.completionReason === "harness_error";
118
- const { released, ...desktopEvidence } = desktop.snapshot();
119
128
  return {
120
129
  spec,
121
130
  ...(session ? { session } : {}),
@@ -302,16 +311,8 @@ export function toParticipantResult(spec, outcome, subject, dryRun) {
302
311
  : {
303
312
  error: {
304
313
  code: outcome.failureCode ?? "HUMANISH_COMPUTER_USE_FAILED",
305
- message: outcome.sessionError ??
306
- (outcome.noEngagement
307
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
308
- : outcome.selfReportedBlocker
309
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
310
- : session?.completionReason === "harness_error"
311
- ? `Computer-use session ended with a harness error: ${session.reason}`
312
- : session?.status !== "passed"
313
- ? `Computer-use session ended with ${session?.status ?? "unknown"}: ${session?.reason ?? "no terminal reason"}`
314
- : "The computer-use run did not produce a terminal session."),
314
+ message: judgeParticipantRecords([participantFactsOf(outcome)]).participants[0]
315
+ .notPassedMessage,
315
316
  },
316
317
  }),
317
318
  };
@@ -1,7 +1,7 @@
1
- import { type ExecutionFailure, type ParticipantFacts } from "../../run/judge.js";
1
+ import { type ExecutionFailure, type ParticipantRecordFacts } from "../../run/judge.js";
2
2
  import type { ParticipantRunOutcome } from "./types.js";
3
3
  /** A participant outcome's facts for the judge. */
4
- export declare function participantFactsOf(outcome: ParticipantRunOutcome): ParticipantFacts;
4
+ export declare function participantFactsOf(outcome: ParticipantRunOutcome | undefined): ParticipantRecordFacts;
5
5
  /** Whether a participant passed. A dry-run participant passes as a contract. */
6
6
  export declare function participantOutcomeOk(outcome: ParticipantRunOutcome | undefined, dryRun: boolean): boolean;
7
7
  /**
@@ -4,10 +4,19 @@
4
4
  import { participantHarnessFailed, participantPassed, sandboxCleanupFailure, } from "../../run/judge.js";
5
5
  /** A participant outcome's facts for the judge. */
6
6
  export function participantFactsOf(outcome) {
7
+ if (outcome === undefined)
8
+ return { skipped: false, noEngagement: false, selfReportedBlocker: false };
7
9
  return {
10
+ id: outcome.spec.planned.id,
11
+ ...(outcome.skippedReason === undefined ? {} : { skippedReason: outcome.skippedReason }),
12
+ reportedFriction: outcome.reportedFriction === true,
8
13
  ...(outcome.session === undefined
9
14
  ? {}
10
- : { status: outcome.session.status, completionReason: outcome.session.completionReason }),
15
+ : {
16
+ status: outcome.session.status,
17
+ completionReason: outcome.session.completionReason,
18
+ reason: outcome.session.reason,
19
+ }),
11
20
  ...(outcome.sessionError === undefined ? {} : { sessionError: outcome.sessionError }),
12
21
  skipped: outcome.skippedReason !== undefined,
13
22
  noEngagement: outcome.noEngagement === true,
@@ -4,42 +4,44 @@ import {} from "../../run/bundle.js";
4
4
  import { loadRunBundle } from "../../run/locate.js";
5
5
  import {} from "../../run/streams.js";
6
6
  import { plural } from "../../run/text.js";
7
+ import { access } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { cli } from "../../cli/invocation.js";
7
10
  export async function resolveCuaRerunSelection(args) {
8
- const source = await loadRunBundle(args.cwd, args.sourceRunId);
11
+ const { studyId, sourceRunId } = args;
12
+ // Every refusal ends with the command that runs the whole study, which is what a user who
13
+ // reached for a rerun too early wants.
14
+ const instead = `To run every participant of ${studyId} instead, leave out --rerun-failed-from: ${cli(`watch ${studyId}`)} or ${cli(`run ${studyId}`)}.`;
15
+ const refuse = (what) => ({ ok: false, message: `${what} ${instead}` });
16
+ const source = await loadRunBundle(args.cwd, sourceRunId);
9
17
  if (!source) {
10
- return { ok: false, message: `source run not found or invalid: ${args.sourceRunId}` };
18
+ const noRuns = sourceRunId === "latest" &&
19
+ !(await access(path.join(args.cwd, ".humanish", "runs", "latest.json")).then(() => true, () => false));
20
+ return refuse(noRuns
21
+ ? "--rerun-failed-from latest found no run: this project has no runs yet, and a rerun repeats the failed participants of an earlier live run."
22
+ : `--rerun-failed-from ${sourceRunId} names no run humanish can read in this project. ${cli("runs")} lists its runs.`);
11
23
  }
12
24
  const bundle = source.bundle;
25
+ const run = `Run ${bundle.runId}${sourceRunId === bundle.runId ? "" : ` (the ${sourceRunId} run)`}`;
26
+ const notFanout = `${run} is not a live computer-use run with more than one participant, so it has no participants to rerun.`;
13
27
  if (bundle.mode !== "live") {
14
- return {
15
- ok: false,
16
- message: `source run ${bundle.runId} is ${bundle.mode}; rerun selection only applies to live CUA fan-out evidence.`,
17
- };
28
+ return refuse(`${run} is a dry run: no participant ran in it, so none can be rerun. A rerun repeats the failed participants of a live run with more than one participant.`);
18
29
  }
19
30
  const fanoutEvent = bundle.events.some((event) => event.type === "cua-lab.fanout.plan");
20
- if (!fanoutEvent || bundle.streams.length < 2) {
21
- return { ok: false, message: `source run ${bundle.runId} is not a CUA fan-out run.` };
22
- }
31
+ if (!fanoutEvent || bundle.streams.length < 2)
32
+ return refuse(notFanout);
23
33
  const prior = bundle.streams
24
34
  .map(snapshotPriorParticipant)
25
35
  .filter((entry) => entry !== null);
26
36
  const priorById = new Map(prior.map((entry) => [entry.participantId, entry]));
27
- if (priorById.size < 2) {
28
- return {
29
- ok: false,
30
- message: `source run ${bundle.runId} does not expose multiple participant ids.`,
31
- };
32
- }
37
+ if (priorById.size < 2)
38
+ return refuse(notFanout);
33
39
  const explicitIds = uniqueIds(args.participantIds ?? []);
34
40
  const selectedIds = explicitIds.length > 0
35
41
  ? explicitIds
36
42
  : prior.filter((entry) => entry.rerunnable).map((entry) => entry.participantId);
37
- if (selectedIds.length === 0) {
38
- return {
39
- ok: false,
40
- message: `source run ${bundle.runId} has no participant to rerun: none failed, was blocked, timed out or ended without engaging.`,
41
- };
42
- }
43
+ if (selectedIds.length === 0)
44
+ return refuse(`${run} has no participant to rerun: none failed, was blocked, timed out or ended without engaging.`);
43
45
  const missingPrior = selectedIds.filter((id) => !priorById.has(id));
44
46
  if (missingPrior.length > 0) {
45
47
  return {
@@ -1,8 +1,8 @@
1
1
  import { adapterScoreFailureMessage, applyBrowserScorer } from "../../study/adapter-extension.js";
2
2
  import { redactText } from "../../evidence/redaction.js";
3
- import { foldScorerFailures, judgeExecution, OUTCOME_POLICIES, resultOk, } from "../../run/judge.js";
3
+ import { foldScorerFailures, judgeExecution, judgeParticipantRecords, OUTCOME_POLICIES, resultOk, } from "../../run/judge.js";
4
4
  import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
5
- import { participantExecutionFailures, participantOutcomeOk } from "./participant-facts.js";
5
+ import { participantExecutionFailures, participantOutcomeOk, participantFactsOf, } from "./participant-facts.js";
6
6
  import { participantCapWarning } from "./participant-model.js";
7
7
  import { summarizeCuaDiagnostics } from "./diagnostics.js";
8
8
  import { toParticipantResult } from "./participant-execution.js";
@@ -90,24 +90,17 @@ function cuaStudyResult(args) {
90
90
  message: outcome?.sessionError ??
91
91
  outcome?.providerCleanupError ??
92
92
  outcome?.providerPolicyError ??
93
- (outcome?.noEngagement
94
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
95
- : // The participant result (toParticipantResult) named this refusal; the N=1 envelope fell through to
96
- // "did not produce a terminal session", which is false: it produced one and refused it.
97
- outcome?.selfReportedBlocker
98
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
99
- : observer.ok
100
- ? outcome?.session?.completionReason === "harness_error"
101
- ? `Computer-use session ended with a harness error: ${outcome.session.reason}`
102
- : outcome?.session?.status !== "passed"
103
- ? `Computer-use session ended with ${outcome?.session?.status ?? "unknown"}: ${outcome?.session?.reason ?? "no terminal reason"}`
104
- : "The computer-use run did not produce a terminal session."
105
- : (observer.error?.message ?? "Observer failed for the computer-use run.")),
93
+ (outcome?.noEngagement || outcome?.selfReportedBlocker || observer.ok
94
+ ? (participantResults[0]?.error?.message ??
95
+ judgeParticipantRecords([participantFactsOf(outcome)]).participants[0]
96
+ .notPassedMessage)
97
+ : (observer.error?.message ?? "Observer failed for the computer-use run.")),
106
98
  };
107
99
  }
108
100
  const failing = (outcomes ?? []).find((outcome) => !participantOk(outcome));
109
- const geometryFailure = (outcomes ?? []).find((outcome) => outcome.failureCode === "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY");
110
- const code = geometryFailure?.failureCode ?? "HUMANISH_COMPUTER_USE_FAILED";
101
+ // A participant whose desktop failed for a known cause gives the run that cause's code.
102
+ const codedFailure = (outcomes ?? []).find((outcome) => outcome.failureCode !== undefined);
103
+ const code = codedFailure?.failureCode ?? "HUMANISH_COMPUTER_USE_FAILED";
111
104
  return {
112
105
  code,
113
106
  message: observer.ok
@@ -200,8 +200,14 @@ export function resolveSelfReportedFriction(session) {
200
200
  // reached the goal and described what was hard on the way has reported friction.
201
201
  if (session.trace.declaredOutcome === "blocked")
202
202
  return session.reason;
203
+ // Impressions are opinions about the product, not reports of what went wrong in the session.
204
+ const impressions = new Set(session.trace.impressions?.status === "collected"
205
+ ? session.trace.impressions.items.map((item) => item.messageId)
206
+ : []);
203
207
  const messages = session.trace.items
204
- .filter((item) => item.kind === "message" && item.id !== session.trace.debrief?.messageId)
208
+ .filter((item) => item.kind === "message" &&
209
+ item.id !== session.trace.debrief?.messageId &&
210
+ !impressions.has(item.id))
205
211
  .map((item) => item.text?.trim() ?? "")
206
212
  .filter((text) => text.length > 0);
207
213
  // A custom session may keep its closing report only in reason even when earlier messages exist.
@@ -22,6 +22,7 @@ import {} from "./bundle.js";
22
22
  import { packRunLocalTree } from "./local-tree-pack.js";
23
23
  import { projectParticipantSubjects, subjectProvenanceArg } from "./subject-projection.js";
24
24
  import { participantSubjectEnv, } from "./types.js";
25
+ import { capacityShortfall } from "../../substrates/local/capacity.js";
25
26
  import { studyPersonaIds } from "../../study/persona-resolve.js";
26
27
  import { refusedResult } from "../../run/study-result.js";
27
28
  /** The physical project, bound before any caller hook runs. */
@@ -145,6 +146,11 @@ export async function admitCuaRun(plan, input, config) {
145
146
  });
146
147
  if (rejection)
147
148
  return refuse(rejection.code, rejection.message, descriptor.id);
149
+ const shortfall = await localCapacityShortfall(input.localVm, participantPlan.concurrency);
150
+ if (shortfall !== undefined && "refusal" in shortfall)
151
+ return refuse("HUMANISH_COMPUTER_USE_LOCAL_CAPACITY_EXCEEDED", shortfall.refusal, descriptor.id);
152
+ if (shortfall !== undefined)
153
+ process.stderr.write(`warning: ${shortfall.warning}\n`);
148
154
  }
149
155
  // Pack the working tree once per run, on the host, before any sandbox or provider call: every
150
156
  // fan-out participant below uploads this same archive, so one archiveSha256 describes every
@@ -192,6 +198,14 @@ export async function admitCuaRun(plan, input, config) {
192
198
  },
193
199
  };
194
200
  }
201
+ /**
202
+ * Whether a local study's desktops at once fit the local runtime. Read before any desktop: a
203
+ * desktop past the VM's memory is killed mid-session, after its participant has spent.
204
+ */
205
+ async function localCapacityShortfall(localVm, desktops) {
206
+ const capacity = await localVm?.capacity?.();
207
+ return capacity === undefined ? undefined : capacityShortfall(capacity, desktops);
208
+ }
195
209
  /** Starts the run and builds what the participants and the finish read: the deps and the bundle base. */
196
210
  export async function startCuaRun(plan, input, admitted, scope) {
197
211
  const { cwd, descriptor, participantRuns, participantPlan, publicRepo } = admitted;
@@ -32,6 +32,8 @@ export declare function buildSingleParticipantBundle(args: {
32
32
  assignment?: RunStream["assignment"];
33
33
  persona: ActorPersonaRef;
34
34
  resolution: [number, number];
35
+ /** The device preset's name; the caption names a tablet. */
36
+ deviceName?: string;
35
37
  /** False only for the custom in-process route, which has no hosted screen/window to claim. */
36
38
  desktopRoute?: boolean;
37
39
  /** The participant's runner; see runnerSubstrate for the default. */