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
@@ -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
  }
@@ -121,6 +121,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
121
121
  persona: setup.persona,
122
122
  timeoutMs: setup.timeoutMs,
123
123
  artifactRoot,
124
+ scrubKnownValues: run.secrets.scrub,
124
125
  }, {
125
126
  surfaces: setup.surfaces,
126
127
  deps: setup.deps,
@@ -5,6 +5,7 @@ import { realpath } from "node:fs/promises";
5
5
  import { firstLiveRefusal, keysCheck, subjectEnvCheck } from "../../study/requirements.js";
6
6
  import path from "node:path";
7
7
  import { resolveBrowserCommand } from "../../actors/scripted-browser/browser-command.js";
8
+ import { publicSafeToken } from "../../actors/scripted-browser/journey.js";
8
9
  import { prepareSelectedOutputDirectory } from "../../run/contained-output.js";
9
10
  import { RunSecrets } from "../../run/secrets.js";
10
11
  import { evidenceAppUrlOf } from "./plan.js";
@@ -26,7 +27,16 @@ export async function prepareScriptedRun(plan, input) {
26
27
  const deps = input.deps ?? {};
27
28
  const warnings = [];
28
29
  const clone = plan.subject.kind === "clone" ? plan.subject : undefined;
29
- const evidenceAppUrl = evidenceAppUrlOf(plan.subject);
30
+ const subjectEnvNames = [...(clone?.env ?? [])];
31
+ const env = input.env ?? process.env;
32
+ const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
33
+ const secrets = new RunSecrets([
34
+ e2bApiKey,
35
+ ...(clone ? [clone.repo] : []),
36
+ ...subjectEnvNames.map((name) => env[name] ?? ""),
37
+ ]);
38
+ // An app-url run records the URL it was given, in the bundle and every summary.
39
+ const evidenceAppUrl = secrets.scrub(evidenceAppUrlOf(plan.subject));
30
40
  const failed = (code, message) => refusedResult("scripted", { studyId: plan.studyId, cwd, warnings, error: { code, message } }, {
31
41
  actor: plan.actor,
32
42
  appUrl: evidenceAppUrl,
@@ -37,16 +47,8 @@ export async function prepareScriptedRun(plan, input) {
37
47
  const urlPolicy = clone
38
48
  ? { kind: "provisioned-subject", evidenceOrigin: evidenceAppUrl }
39
49
  : { kind: "loopback" };
40
- const subjectEnvNames = [...(clone?.env ?? [])];
41
- const env = input.env ?? process.env;
42
- const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
43
50
  const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
44
51
  const redactRepoLabel = plan.residual.policies?.redactRepos ?? hasGithubToken;
45
- const secrets = new RunSecrets([
46
- e2bApiKey,
47
- ...(clone ? [clone.repo] : []),
48
- ...subjectEnvNames.map((name) => env[name] ?? ""),
49
- ]);
50
52
  // A clone's URL is replaced by its getHost URL once it is served.
51
53
  let appUrl = plan.subject.kind === "clone" ? plan.subject.serve.url : plan.subject.appUrl;
52
54
  const scenario = await resolveScriptedScenario(projectRoot, plan.scenarioRef);
@@ -56,7 +58,17 @@ export async function prepareScriptedRun(plan, input) {
56
58
  result: failed("HUMANISH_SCRIPTED_SCENARIO_INVALID", scenario.message),
57
59
  };
58
60
  }
59
- const journey = scenario.journey;
61
+ // The goal, title and step labels are recorded; a step's path, selector, value and expected text
62
+ // drive the browser and stay as written.
63
+ const journey = {
64
+ ...scenario.journey,
65
+ scenarioId: idHoldsKnownValue(scenario.journey.scenarioId, secrets)
66
+ ? "scenario"
67
+ : scenario.journey.scenarioId,
68
+ goal: secrets.scrub(scenario.journey.goal),
69
+ scenarioTitle: secrets.scrub(scenario.journey.scenarioTitle),
70
+ steps: recordedStepIds(scenario.journey.steps, secrets),
71
+ };
60
72
  // The plan lists E2B_API_KEY and the subject env only for a live clone.
61
73
  const { requirements } = plan;
62
74
  const refusal = await firstLiveRefusal([
@@ -118,3 +130,34 @@ export async function prepareScriptedRun(plan, input) {
118
130
  },
119
131
  };
120
132
  }
133
+ /**
134
+ * Whether an id holds a known value as written or as the parser's id token of it, lower-cased with
135
+ * other characters as `-`, which the scrub would not find.
136
+ */
137
+ function idHoldsKnownValue(id, secrets) {
138
+ return (secrets.scrub(id) !== id ||
139
+ secrets.values().some((value) => {
140
+ const token = publicSafeToken(value, "");
141
+ return token.length > 0 && id.includes(token);
142
+ }));
143
+ }
144
+ /**
145
+ * The steps with their labels scrubbed and their ids safe to record. A step id names the step's
146
+ * screenshot file and is recorded beside it, and the parser derives an omitted id from the label,
147
+ * lower-cased and cut to 80 characters, so the scrub may not find a value in it. A step whose
148
+ * label or id holds a known value is renamed by its position instead.
149
+ */
150
+ function recordedStepIds(steps, secrets) {
151
+ const taken = new Set(steps.map((step) => step.id));
152
+ return steps.map((step, index) => {
153
+ const label = secrets.scrub(step.label);
154
+ if (label === step.label && !idHoldsKnownValue(step.id, secrets))
155
+ return { ...step, label };
156
+ const position = `step-${String(index + 1).padStart(2, "0")}`;
157
+ let id = position;
158
+ for (let suffix = 2; taken.has(id); suffix += 1)
159
+ id = `${position}-${suffix}`;
160
+ taken.add(id);
161
+ return { ...step, id, label };
162
+ });
163
+ }
@@ -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,
@@ -3,7 +3,7 @@ import type { ScriptedPlan } from "../../study/plan-types.js";
3
3
  import { type PreparedRunArtifactPaths } from "../../run/paths.js";
4
4
  import type { StudyDeps } from "../../study/study-deps.js";
5
5
  /** One session per surface, in parallel. */
6
- export declare function runScriptedSessions(session: Required<Pick<ScriptedBrowserSessionOptions, "appUrl" | "evidenceAppUrl" | "urlPolicy" | "journey" | "persona" | "timeoutMs" | "artifactRoot">>, run: {
6
+ export declare function runScriptedSessions(session: Required<Pick<ScriptedBrowserSessionOptions, "appUrl" | "evidenceAppUrl" | "urlPolicy" | "journey" | "persona" | "timeoutMs" | "artifactRoot" | "scrubKnownValues">>, run: {
7
7
  surfaces: ScriptedPlan["surfaces"];
8
8
  deps: StudyDeps;
9
9
  browserCommand: string | undefined;
@@ -7,6 +7,7 @@ import { validateScriptedSessionResult } from "./session-result.js";
7
7
  /** One session per surface, in parallel. */
8
8
  export function runScriptedSessions(session, run) {
9
9
  const { appUrl, evidenceAppUrl, urlPolicy, journey, persona, timeoutMs, artifactRoot } = session;
10
+ const { scrubKnownValues } = session;
10
11
  const { surfaces, deps, browserCommand, runPaths, onSessionStart } = run;
11
12
  const runSession = deps.runScriptedSession;
12
13
  return Promise.all(surfaces.map((surface) => {
@@ -20,6 +21,7 @@ export function runScriptedSessions(session, run) {
20
21
  persona,
21
22
  timeoutMs,
22
23
  artifactRoot,
24
+ scrubKnownValues,
23
25
  ...(browserCommand === undefined ? {} : { browserCommand }),
24
26
  ...(deps.launchBrowser === undefined ? {} : { launchBrowser: deps.launchBrowser }),
25
27
  ...(deps.now === undefined ? {} : { now: deps.now }),
@@ -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,30 @@ 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;
95
84
  return participantStream(spec, {
96
85
  ...(spec.evidenceAssignment === undefined
97
86
  ? {}
98
87
  : { assignment: participantAssignment(spec.evidenceAssignment) }),
99
88
  kind: "browser",
100
- label: `Concurrent persona ${spec.planned.id}${taxonomy} · ${args.plan.studyId}`,
89
+ label: participantCaption({
90
+ id: spec.planned.id,
91
+ personaId: spec.persona.id,
92
+ device: spec.planned.device,
93
+ }),
101
94
  status: view.status,
102
95
  ...(judged === undefined ? {} : { judgedStatus: judged }),
103
96
  transport: "snapshot",
@@ -122,7 +115,7 @@ function sharedWorldStream(ctx, spec, index, view) {
122
115
  desktopGeometry,
123
116
  ui: {
124
117
  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.`,
118
+ intent: `Watch ${name} use the shared app at the same time as the others.`,
126
119
  state: view.reason,
127
120
  ...(session ? { actorStatus: session.status } : {}),
128
121
  ...(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;