humanish 0.111.2 → 0.111.4

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 (92) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/actors/scripted-browser/actor.d.ts +5 -0
  4. package/dist/actors/scripted-browser/actor.js +40 -19
  5. package/dist/actors/scripted-browser/journey.d.ts +2 -0
  6. package/dist/actors/scripted-browser/journey.js +2 -1
  7. package/dist/actors/scripted-browser/steps.d.ts +2 -0
  8. package/dist/actors/scripted-browser/steps.js +17 -0
  9. package/dist/cli/commands/study-route-computer-use.js +3 -8
  10. package/dist/cli/commands/study-route-preview.js +4 -6
  11. package/dist/cli/commands/study-route-shared-world.js +6 -7
  12. package/dist/cli/findings.d.ts +15 -1
  13. package/dist/cli/findings.js +43 -5
  14. package/dist/evidence/redaction.d.ts +2 -2
  15. package/dist/evidence/redaction.js +3 -3
  16. package/dist/evidence/secret-scrub.d.ts +7 -0
  17. package/dist/evidence/secret-scrub.js +21 -12
  18. package/dist/routes/computer-use/bundle-parts.d.ts +1 -32
  19. package/dist/routes/computer-use/bundle-parts.js +5 -92
  20. package/dist/routes/computer-use/e2b-desktop/prepare.js +11 -4
  21. package/dist/routes/computer-use/fanout-bundle.js +3 -2
  22. package/dist/routes/computer-use/live-phase.js +3 -7
  23. package/dist/routes/computer-use/participant-deps.d.ts +46 -0
  24. package/dist/routes/computer-use/participant-deps.js +48 -0
  25. package/dist/routes/computer-use/participant-facts.d.ts +5 -4
  26. package/dist/routes/computer-use/participant-facts.js +37 -2
  27. package/dist/routes/computer-use/participant-feedback.d.ts +33 -0
  28. package/dist/routes/computer-use/participant-feedback.js +92 -0
  29. package/dist/routes/computer-use/participant-model.d.ts +11 -0
  30. package/dist/routes/computer-use/participant-model.js +15 -0
  31. package/dist/routes/computer-use/preflight.js +33 -65
  32. package/dist/routes/computer-use/result.js +13 -52
  33. package/dist/routes/computer-use/route.js +6 -7
  34. package/dist/routes/computer-use/setup.d.ts +2 -6
  35. package/dist/routes/computer-use/setup.js +47 -93
  36. package/dist/routes/computer-use/single-bundle.js +3 -2
  37. package/dist/routes/computer-use/types.d.ts +2 -2
  38. package/dist/routes/preview.js +2 -8
  39. package/dist/routes/scripted/bundle.d.ts +2 -6
  40. package/dist/routes/scripted/result.d.ts +2 -1
  41. package/dist/routes/scripted/result.js +4 -4
  42. package/dist/routes/scripted/route.js +14 -38
  43. package/dist/routes/scripted/setup.d.ts +2 -1
  44. package/dist/routes/scripted/setup.js +71 -35
  45. package/dist/routes/scripted/subject.d.ts +3 -18
  46. package/dist/routes/scripted/subject.js +22 -78
  47. package/dist/routes/scripted/surface-sessions.d.ts +1 -1
  48. package/dist/routes/scripted/surface-sessions.js +2 -0
  49. package/dist/routes/scripted/types.d.ts +2 -2
  50. package/dist/routes/shared-world/bundle.js +36 -1
  51. package/dist/routes/shared-world/comms.d.ts +2 -1
  52. package/dist/routes/shared-world/comms.js +3 -7
  53. package/dist/routes/shared-world/handoff.d.ts +2 -2
  54. package/dist/routes/shared-world/handoff.js +1 -1
  55. package/dist/routes/shared-world/participant-specs.d.ts +4 -5
  56. package/dist/routes/shared-world/participant-specs.js +19 -25
  57. package/dist/routes/shared-world/plan.js +11 -7
  58. package/dist/routes/shared-world/provisioned.d.ts +2 -1
  59. package/dist/routes/shared-world/provisioned.js +37 -80
  60. package/dist/routes/shared-world/result.js +17 -35
  61. package/dist/routes/shared-world/route.js +33 -36
  62. package/dist/routes/shared-world/setup.d.ts +2 -3
  63. package/dist/routes/shared-world/setup.js +11 -29
  64. package/dist/routes/shared-world/types.d.ts +6 -15
  65. package/dist/routes/terminal/credentials.js +14 -1
  66. package/dist/routes/terminal/dry-run.js +14 -24
  67. package/dist/routes/terminal/live-finish.d.ts +0 -1
  68. package/dist/routes/terminal/live-finish.js +8 -5
  69. package/dist/routes/terminal/route.js +9 -15
  70. package/dist/routes/terminal/session.d.ts +2 -3
  71. package/dist/routes/terminal/session.js +28 -40
  72. package/dist/routes/terminal/trace.d.ts +4 -2
  73. package/dist/routes/terminal/trace.js +30 -35
  74. package/dist/routes/terminal/types.d.ts +2 -2
  75. package/dist/run/run.d.ts +55 -1
  76. package/dist/run/run.js +38 -2
  77. package/dist/run/secrets.d.ts +35 -0
  78. package/dist/run/secrets.js +158 -0
  79. package/dist/run/study-result.d.ts +34 -0
  80. package/dist/run/study-result.js +16 -0
  81. package/dist/study/requirements.d.ts +60 -5
  82. package/dist/study/requirements.js +89 -2
  83. package/dist/study/run-study-events.d.ts +4 -3
  84. package/dist/study/run-study-events.js +9 -14
  85. package/dist/study/run-study-options.d.ts +4 -1
  86. package/dist/study/run-study-options.js +22 -32
  87. package/dist/substrates/e2b/subject-sandbox.d.ts +58 -0
  88. package/dist/substrates/e2b/subject-sandbox.js +94 -0
  89. package/docs/contracts/core.md +5 -4
  90. package/docs/contracts/feedback.md +12 -5
  91. package/docs/contracts/study-analysis.md +6 -2
  92. package/package.json +1 -1
package/ARCHITECTURE.md CHANGED
@@ -129,7 +129,7 @@ comments name an invariant by its rule.
129
129
  | The run bundle is the source of truth, and the Observer derives from it | `verifyRun` grades only `run.json` and its artifacts (`src/verify/verify.ts`). It reads `status.json` and the sandbox and reclaim receipts only for the `RUN_NOT_FINISHED` warning (`runNotFinished` in `src/verify/liveness.ts`), which changes no check and no grade. `buildObserverData` takes the bundle (`src/observer/data.ts`) | `tests/observer/data-contract.test.ts`; `tests/verify/run-not-finished.test.ts` checks that the grades are the same when `status.json` contradicts the bundle or is gone; `docs/decisions/0001-run-bundle-is-the-source-of-truth.md` |
130
130
  | Unsupported execution is refused before side effects | `parseStudy` (`src/study/config.ts`), `taskProtocolValidationReason` (`src/study/validation.ts`), `planComputerUseStudy` (`src/routes/computer-use/plan.ts`) | `tests/admission/library.test.ts` checks each library refusal for a run directory, a desktop module load, a caller executor or provider, and a subprocess; `tests/admission/cli.test.ts` checks each CLI refusal for a run directory, a started process and a scorer's host code; `tests/study/task-route-preflight.test.ts`; `pnpm cli:preflight:test` runs `scripts/task-route-preflight-proof.mjs` with `tests/fixtures/task-route-preflight/deny-side-effects.mjs` preloaded |
131
131
  | A sandbox id is recorded before any work runs in it | `acquireE2BDesktopSandbox` and `acquireE2BShellSandbox` (`src/substrates/e2b/sandbox.ts`) tag the sandbox with its run's owner tags, register the create with `beginSandboxCreate` (`src/run/sandbox-creates.ts`) and write the receipt before they return the handle, and every route that creates a sandbox calls them; `reclaimRunSandboxes` (`src/run/reclaim.ts`) switches on the receipt's provider, kills by exact id and by the run's exact tags | `tests/substrates/e2b/sandbox.test.ts`, which also checks that `Sandbox.create` has one call site in `src/`, `tests/routes/terminal/acquisition-boundary.test.ts`, `tests/run/reclaim.test.ts`; `tests/routes/scripted/route.test.ts` checks create, receipt, then first work |
132
- | Bundles carry no secrets | `redactText` and `scrubLiterals` (`src/evidence/redaction.ts`); `scanRunPublicSafetyArtifacts` (`src/verify/artifacts.ts`); `buildShareSafety` (`src/verify/verify.ts`) grades `share_ready`, `local_only` or `blocked` | `tests/evidence/redaction-hooks.test.ts`, `tests/run/transient-comms-secrets.test.ts`, `tests/verify/evidence-refs.test.ts` |
132
+ | Bundles carry no secrets | `redactText` and `scrubLiterals` (`src/evidence/redaction.ts`); `RunSecrets` (`src/run/secrets.ts`) holds the literal values each run scrubs; `scanRunPublicSafetyArtifacts` (`src/verify/artifacts.ts`); `buildShareSafety` (`src/verify/verify.ts`) grades `share_ready`, `local_only` or `blocked` | `tests/evidence/redaction-hooks.test.ts`, `tests/routes/known-secrets.test.ts`, `tests/run/transient-comms-secrets.test.ts`, `tests/verify/evidence-refs.test.ts` |
133
133
  | Goldens pin route output | `runDirSnapshot` (`tests/helpers/run-golden.ts`) snapshots a whole run folder | `tests/golden/routes/`, `tests/golden/observer-data/`, `tests/golden/labs/`; `tests/study/plan-lab.test.ts` checks that every `StudyRoute` has a dry-run golden |
134
134
  | A run is closed on every exit, only a published run is analyzed, and `latest.json` moves only after its run is published | `runScope` and `FinishedRun` (`src/run/run.ts`); `completeAutomaticAnalysis` requires an issued `FinishedRun` for the result's run; `Run.finish` writes `latest.json` last through `writePreparedRunLatestPointer` (`src/run/contained-output.ts`) | `tests/run/run-lifecycle.test.ts`, which also checks that `src/run/run.ts` is the pointer's one writer, `tests/analysis/automatic-analysis.test.ts` |
135
135
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,38 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.111.4: Encoded known values scrubbed, scripted traces scrubbed, a live command after a dry run (2026-10-06)
12
+
13
+ humanish 0.111.4 scrubs a run's known values from more of its evidence and fixes three messages.
14
+ Every route now also finds a known value, such as a provider key or a `subject.env` value, in its
15
+ encoded forms: percent-encoded, JSON-escaped, base64, base64url and hex, and where a terminal color
16
+ code or an output chunk boundary splits it. A scripted run scrubs its known values from the step
17
+ trace in `traces/<surface>.json` and `actor-<surface>.json`, and from the app URL, scenario goal,
18
+ title and step labels it records. Two places keep a known value: a scenario file name, and the
19
+ path of a computer-use `subject.appUrl`. `humanish review` on a dry run names the command that
20
+ starts a live run, so `next` in `review --json` holds a string where it held null. A terminal
21
+ refusal for a missing runtime key no longer reads `runtimeAuth "undefined"`, and a refusal that
22
+ names two signed-in coding agents reads "report".
23
+
24
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.111.4)
25
+
26
+ ## 0.111.3: Shared-world feedback candidates, scripted E2B key scrubbed, subject sandbox warnings (2026-10-06)
27
+
28
+ humanish 0.111.3 gives shared-world runs feedback candidates and repairs seven places where one
29
+ route's warnings, refusals, secret scrubbing or run directory checks differed from the other
30
+ routes'. A shared-world participant who reports friction or gives up becomes a candidate that
31
+ `humanish feedback` drafts, and each candidate from a run with several participants shows that
32
+ participant's own instructions as `expected`. A scripted run scrubs the value of `E2B_API_KEY` from
33
+ the errors and subject setup output it records, and writes `[REDACTED_SECRET]` where it wrote
34
+ `[redacted]`. A shared-world study with `caps.maxUsd` and no `caps.maxTotalUsd` warns that the cap
35
+ applies to each participant. The subject sandbox of a scripted clone study or a provisioned
36
+ shared-world study warns when its create was retried or E2B reports no size. A scripted clone study
37
+ missing a `subject.env` value, and a provisioned shared-world study missing `OPENAI_API_KEY`,
38
+ refuse with the wording computer use gives. Computer-use and shared-world runs check the run
39
+ directory again after a `prepareDesktop` hook and an Observer render.
40
+
41
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.111.3)
42
+
11
43
  ## 0.111.2: A taller Observer frame, even grid cards, a notice when the server stops (2026-10-06)
12
44
 
13
45
  humanish 0.111.2 changes the Observer and nothing else. The focus view's heading line holds the
@@ -24,6 +24,11 @@ export interface ScriptedBrowserSessionOptions {
24
24
  /** DI seam; production default is launchPlaywrightChromium. */
25
25
  launchBrowser?: (args: ScriptedBrowserLaunchArgs) => Promise<ScriptedBrowserLike>;
26
26
  now?: () => number;
27
+ /**
28
+ * The run's scrub of its known secret values. Each URL and text the session records passes
29
+ * through it before the trace is written or the capture returned.
30
+ */
31
+ scrubKnownValues?: (text: string) => string;
27
32
  }
28
33
  export interface ScriptedBrowserSessionResult extends ActorSessionResult {
29
34
  /** Native evidence incl. tracePath (humanish.browser-persona-trace.v1, written to disk). */
@@ -21,7 +21,7 @@ import { CHROMIUM_EVIDENCE_HYGIENE_FLAGS } from "../../evidence/browser-hygiene.
21
21
  import { redactText } from "../../evidence/redaction.js";
22
22
  import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareContainedOutputFile, prepareSelectedOutputDirectory, writeContainedOutputFile, } from "../../run/contained-output.js";
23
23
  import { ACTOR_TRACE_SCHEMA, SCRIPTED_BROWSER_CAPABILITIES, } from "../contract.js";
24
- import { buildBlockedBrowserPersonaSteps, buildBrowserTrace, captureBlockedStepScreenshot, compactBrowserError, executeBrowserPersonaStep, probeAppUrl, sanitizeBrowserEvidenceUrl, screenshotPathForBrowserStep, surfaceScreenshotPath, tracePathForBrowserSurface, } from "./steps.js";
24
+ import { buildBlockedBrowserPersonaSteps, buildBrowserTrace, captureBlockedStepScreenshot, compactBrowserError, executeBrowserPersonaStep, probeAppUrl, sanitizeBrowserEvidenceUrl, screenshotPathForBrowserStep, scrubRecordedStep, surfaceScreenshotPath, tracePathForBrowserSurface, } from "./steps.js";
25
25
  import { LOOPBACK_EVIDENCE_URL_POLICY, } from "./types.js";
26
26
  import { chromeLaunchEnv, chromeLaunchError } from "./launch-env.js";
27
27
  /** Production default: lazy playwright-core import + chromium.launch, exactly as the driver
@@ -93,8 +93,10 @@ export async function runScriptedBrowserSessionInPreparedRoot(options, preparedA
93
93
  const browserCommand = options.browserCommand ?? (options.launchBrowser ? "injected-browser" : "");
94
94
  const evidenceAppUrl = options.evidenceAppUrl ?? options.appUrl;
95
95
  const urlPolicy = options.urlPolicy ?? LOOPBACK_EVIDENCE_URL_POLICY;
96
+ const scrub = options.scrubKnownValues ?? ((text) => text);
96
97
  const finish = async (args) => {
97
98
  const completedAtMs = now();
99
+ const reason = scrub(args.reason);
98
100
  const trace = await projectScriptedActorTrace({
99
101
  artifactRoot: preparedArtifactRoot,
100
102
  capture: args.capture,
@@ -104,14 +106,14 @@ export async function runScriptedBrowserSessionInPreparedRoot(options, preparedA
104
106
  executedSteps: args.executedSteps,
105
107
  journey: options.journey,
106
108
  persona: options.persona,
107
- reason: args.reason,
109
+ reason,
108
110
  startedAt,
109
111
  status: args.status,
110
112
  });
111
113
  return {
112
114
  status: args.status,
113
115
  completionReason: args.completionReason,
114
- reason: args.reason,
116
+ reason,
115
117
  capture: args.capture,
116
118
  trace,
117
119
  };
@@ -130,6 +132,7 @@ export async function runScriptedBrowserSessionInPreparedRoot(options, preparedA
130
132
  evidenceAppUrl,
131
133
  journey: options.journey,
132
134
  reason,
135
+ scrub,
133
136
  surface: options.surface,
134
137
  urlPolicy,
135
138
  });
@@ -155,6 +158,7 @@ export async function runScriptedBrowserSessionInPreparedRoot(options, preparedA
155
158
  browserCommand,
156
159
  evidenceAppUrl,
157
160
  journey: options.journey,
161
+ scrub,
158
162
  surface: options.surface,
159
163
  timeoutMs: options.timeoutMs,
160
164
  urlPolicy,
@@ -258,17 +262,18 @@ async function runScriptedJourney(args) {
258
262
  finally {
259
263
  await args.browser.close().catch(() => undefined);
260
264
  }
265
+ const recordedSteps = steps.map((step) => scrubRecordedStep(step, args.scrub));
261
266
  const completedAt = new Date().toISOString();
262
267
  const durationMs = Date.now() - started;
263
268
  const ok = !timedOut &&
264
269
  httpProbe.ok &&
265
- steps.length === args.journey.steps.length &&
266
- steps.every((step) => step.status === "passed");
267
- const reason = ok
268
- ? `${args.surface.label} completed ${steps.length}/${steps.length} scripted browser steps from ${args.evidenceAppUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
269
- : `${args.surface.label} scripted browser journey blocked: ${steps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`;
270
- const scriptedScreenshotPath = surfaceScreenshotPath(steps);
271
- await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
270
+ recordedSteps.length === args.journey.steps.length &&
271
+ recordedSteps.every((step) => step.status === "passed");
272
+ const reason = args.scrub(ok
273
+ ? `${args.surface.label} completed ${recordedSteps.length}/${recordedSteps.length} scripted browser steps from ${args.evidenceAppUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
274
+ : `${args.surface.label} scripted browser journey blocked: ${recordedSteps.find((step) => step.status !== "passed")?.reason ?? httpProbe.reason}`);
275
+ const scriptedScreenshotPath = surfaceScreenshotPath(recordedSteps);
276
+ await writeBrowserTrace(args.artifactRoot, tracePath, {
272
277
  appUrl: args.evidenceAppUrl,
273
278
  browserCommand: path.basename(args.browserCommand || "injected-browser"),
274
279
  browserJourney: args.journey,
@@ -278,9 +283,9 @@ async function runScriptedJourney(args) {
278
283
  ok,
279
284
  reason,
280
285
  ...(scriptedScreenshotPath === undefined ? {} : { screenshotPath: scriptedScreenshotPath }),
281
- steps,
286
+ steps: recordedSteps,
282
287
  surface: args.surface,
283
- }), null, 2)}\n`, "utf8");
288
+ }, args.scrub);
284
289
  return {
285
290
  capture: {
286
291
  capturedAt: completedAt,
@@ -289,7 +294,7 @@ async function runScriptedJourney(args) {
289
294
  ok,
290
295
  reason,
291
296
  ...(scriptedScreenshotPath === undefined ? {} : { screenshotPath: scriptedScreenshotPath }),
292
- steps,
297
+ steps: recordedSteps,
293
298
  surface: args.surface,
294
299
  tracePath,
295
300
  },
@@ -327,40 +332,56 @@ async function withJourneyDeadline(promise, deadline, timeoutMs) {
327
332
  async function persistScriptedFailureCapture(args) {
328
333
  const capturedAt = new Date().toISOString();
329
334
  const tracePath = tracePathForBrowserSurface(args.surface);
335
+ const reason = args.scrub(args.reason);
330
336
  const blockedSteps = buildBlockedBrowserPersonaSteps({
331
337
  browserJourney: args.journey,
332
338
  currentUrl: args.appUrl,
333
- reason: args.reason,
339
+ reason,
334
340
  surface: args.surface,
335
341
  timestamp: capturedAt,
336
342
  urlPolicy: args.urlPolicy,
337
- });
343
+ }).map((step) => scrubRecordedStep(step, args.scrub));
338
344
  // Pre-actuation failure: no screenshots were written, so the surface omits the
339
345
  // screenshot reference and the failure itself stands as the evidence.
340
346
  const screenshotPath = surfaceScreenshotPath(blockedSteps);
341
- await writeContainedOutputFile(args.artifactRoot, tracePath, `${JSON.stringify(buildBrowserTrace({
347
+ await writeBrowserTrace(args.artifactRoot, tracePath, {
342
348
  appUrl: args.evidenceAppUrl,
343
349
  browserCommand: path.basename(args.browserCommand || "injected-browser"),
344
350
  browserJourney: args.journey,
345
351
  capturedAt,
346
352
  durationMs: 0,
347
353
  ok: false,
348
- reason: args.reason,
354
+ reason,
349
355
  ...(screenshotPath === undefined ? {} : { screenshotPath }),
350
356
  steps: blockedSteps,
351
357
  surface: args.surface,
352
- }), null, 2)}\n`, "utf8");
358
+ }, args.scrub);
353
359
  return {
354
360
  capturedAt,
355
361
  durationMs: 0,
356
362
  ok: false,
357
- reason: args.reason,
363
+ reason,
358
364
  ...(screenshotPath === undefined ? {} : { screenshotPath }),
359
365
  steps: blockedSteps,
360
366
  surface: args.surface,
361
367
  tracePath,
362
368
  };
363
369
  }
370
+ /**
371
+ * Writes one surface's native trace. Its steps and reason arrive scrubbed; the app URL and the
372
+ * scenario title pass through the same scrub here.
373
+ */
374
+ async function writeBrowserTrace(artifactRoot, tracePath, trace, scrub) {
375
+ const recorded = buildBrowserTrace({
376
+ ...trace,
377
+ appUrl: scrub(trace.appUrl),
378
+ browserJourney: {
379
+ ...trace.browserJourney,
380
+ scenarioTitle: scrub(trace.browserJourney.scenarioTitle),
381
+ },
382
+ });
383
+ await writeContainedOutputFile(artifactRoot, tracePath, `${JSON.stringify(recorded, null, 2)}\n`, "utf8");
384
+ }
364
385
  /** Project one surface capture into humanish.actor-trace.v1. screenshotRefs are attached only
365
386
  * for frames that actually exist on disk (so counts match disk; blocked-not-executed steps name a
366
387
  * path that was never written). */
@@ -7,3 +7,5 @@ export declare function parseBrowserPersonaJourneyFromScenario(args: {
7
7
  failure?: string;
8
8
  journey?: BrowserPersonaJourney;
9
9
  };
10
+ /** `value` lower-cased, with each run of other characters a `-`, cut to 80; else `fallback`. */
11
+ export declare function publicSafeToken(value: string | undefined, fallback: string): string;
@@ -172,7 +172,8 @@ function stringValue(value) {
172
172
  function booleanValue(value) {
173
173
  return typeof value === "boolean" ? value : undefined;
174
174
  }
175
- function publicSafeToken(value, fallback) {
175
+ /** `value` lower-cased, with each run of other characters a `-`, cut to 80; else `fallback`. */
176
+ export function publicSafeToken(value, fallback) {
176
177
  const candidate = (value ?? fallback)
177
178
  .toLowerCase()
178
179
  .replace(/[^a-z0-9_.-]+/g, "-")
@@ -18,6 +18,8 @@ export declare function buildBlockedBrowserPersonaSteps(args: {
18
18
  timestamp: string;
19
19
  urlPolicy?: ScriptedBrowserEvidenceUrlPolicy;
20
20
  }): BrowserPersonaStepCapture[];
21
+ /** A step as the trace records it: its label, reason, URL and assertion reasons scrubbed. */
22
+ export declare function scrubRecordedStep(step: BrowserPersonaStepCapture, scrub: (text: string) => string): BrowserPersonaStepCapture;
21
23
  export declare function screenshotPathForBrowserStep(surface: BrowserSurface, step: BrowserPersonaStepManifest | undefined): string;
22
24
  export declare function tracePathForBrowserSurface(surface: BrowserSurface): string;
23
25
  /**
@@ -191,6 +191,23 @@ export function buildBlockedBrowserPersonaSteps(args) {
191
191
  url: sanitizeBrowserEvidenceUrl(args.currentUrl, args.urlPolicy),
192
192
  }));
193
193
  }
194
+ /** A step as the trace records it: its label, reason, URL and assertion reasons scrubbed. */
195
+ export function scrubRecordedStep(step, scrub) {
196
+ return {
197
+ ...step,
198
+ ...(step.assertions === undefined
199
+ ? {}
200
+ : {
201
+ assertions: step.assertions.map((assertion) => ({
202
+ ...assertion,
203
+ reason: scrub(assertion.reason),
204
+ })),
205
+ }),
206
+ label: scrub(step.label),
207
+ reason: scrub(step.reason),
208
+ url: scrub(step.url),
209
+ };
210
+ }
194
211
  export function screenshotPathForBrowserStep(surface, step) {
195
212
  assertSafeOutputPathSegment(surface.id, "Browser surface id");
196
213
  if (step) {
@@ -12,7 +12,7 @@ import { exposureRequestFromOptions, followDecision, followObserver, planObserve
12
12
  import { writeRunFindings } from "../findings.js";
13
13
  import { formatCuaStudyHuman } from "./study-format.js";
14
14
  import { resolveRouteShouldOpen } from "./study-route-open.js";
15
- import { studyResultIdentity } from "../../run/study-result.js";
15
+ import { refusedResult } from "../../run/study-result.js";
16
16
  import { declaredParticipantCount } from "../../study/study-fields.js";
17
17
  /**
18
18
  * The computer-use route's CLI setup: its settings, the watch and exposure plan, the runStudyWith options
@@ -51,17 +51,12 @@ export function computerUseRouteRun(args) {
51
51
  };
52
52
  }
53
53
  function refuseCua(args, dryRun, code, message) {
54
- const result = {
55
- ...studyResultIdentity("computer-use", args.config.id),
56
- ok: false,
57
- cwd: args.options.cwd,
54
+ const result = refusedResult("computer-use", { studyId: args.config.id, cwd: args.options.cwd, error: { code, message } }, {
58
55
  actor: args.config.actor?.type ?? "",
59
56
  appUrl: "",
60
57
  dryRun,
61
58
  runId: args.options.runId ?? "not-created",
62
- warnings: [],
63
- error: { code, message },
64
- };
59
+ });
65
60
  writeResult(args.command, args.io, result, formatCuaStudyHuman);
66
61
  args.io.setExitCode(2);
67
62
  }
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
- import { studyResultIdentity } from "../../run/study-result.js";
2
+ import { refusedResult } from "../../run/study-result.js";
3
3
  import { formatRunHuman, parseStudyCount, writeResult, } from "../io.js";
4
4
  import { planObserver, showObserver, staticObserverOpen } from "../observer-follow.js";
5
5
  import { declaredParticipantCount } from "../../study/study-fields.js";
@@ -10,16 +10,14 @@ import { declaredParticipantCount } from "../../study/study-fields.js";
10
10
  export function previewRouteRun(args) {
11
11
  const participantCount = parseStudyCount(args.options.count, declaredParticipantCount(args.config) ?? 4);
12
12
  if (participantCount === null) {
13
- const result = {
14
- ...studyResultIdentity("preview", args.config.id),
15
- ok: false,
13
+ const result = refusedResult("preview", {
14
+ studyId: args.config.id,
16
15
  cwd: args.options.cwd,
17
- warnings: [],
18
16
  error: {
19
17
  code: "HUMANISH_INVALID_PARTICIPANT_COUNT",
20
18
  message: "--count must be a positive integer.",
21
19
  },
22
- };
20
+ }, {});
23
21
  writeResult(args.command, args.io, result, formatRunHuman);
24
22
  args.io.setExitCode(2);
25
23
  return undefined;
@@ -9,7 +9,7 @@ import { followDecision, followObserver, showObserver, staticObserverOpen, withO
9
9
  import { writeRunFindings } from "../findings.js";
10
10
  import { formatConcurrentSharedWorldStudyHuman } from "./study-format.js";
11
11
  import { resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
12
- import { studyResultIdentity } from "../../run/study-result.js";
12
+ import { refusedResult } from "../../run/study-result.js";
13
13
  import { participantList } from "../../study/study-fields.js";
14
14
  /**
15
15
  * The shared-world route's CLI setup: dry run, open and follow semantics, the live Observer server
@@ -30,10 +30,11 @@ export function sharedWorldRouteRun(args) {
30
30
  followDecision({ wantsMachine, detach: args.options.detach, serve: args.options.serve }).follow;
31
31
  const port = parseObserverPort(args.options.port ?? "0");
32
32
  const failConcurrent = (message, runId) => {
33
- const result = {
34
- ...studyResultIdentity("shared-world", args.config.id),
35
- ok: false,
33
+ const result = refusedResult("shared-world", {
34
+ studyId: args.config.id,
36
35
  cwd: args.options.cwd,
36
+ error: { code: "HUMANISH_SHARED_WORLD_FAILED", message },
37
+ }, {
37
38
  actor: args.config.actor?.type ?? "",
38
39
  topology: "shared-world",
39
40
  topologyMode: "concurrent",
@@ -42,9 +43,7 @@ export function sharedWorldRouteRun(args) {
42
43
  dryRun,
43
44
  runId: runId ?? args.options.runId ?? "not-created",
44
45
  roles: [],
45
- warnings: [],
46
- error: { code: "HUMANISH_SHARED_WORLD_FAILED", message },
47
- };
46
+ });
48
47
  writeResult(args.command, args.io, result, formatConcurrentSharedWorldStudyHuman);
49
48
  args.io.setExitCode(2);
50
49
  };
@@ -48,7 +48,10 @@ export interface AnalysisFindings {
48
48
  /** The stable code behind a state other than ready, when one is recorded. */
49
49
  reason: string | null;
50
50
  message: string;
51
- /** The command that gets findings for this run, or null when none can. */
51
+ /**
52
+ * The command that gets findings for this run, or for a dry run the command that starts a live
53
+ * one. Null when none can.
54
+ */
52
55
  next: string | null;
53
56
  analysisId: string | null;
54
57
  status: AnalysisArtifact["status"] | null;
@@ -80,6 +83,17 @@ interface FindingsSource {
80
83
  * false`, read when the view is built. Undefined when it does not, or cannot be read.
81
84
  */
82
85
  analysisOffIn?: string;
86
+ /** For a dry run, the study a live run can start from, read when the view is built. */
87
+ liveStudy?: LiveStudy;
88
+ }
89
+ /** A study `run` can start live after a dry run. */
90
+ interface LiveStudy {
91
+ /** The study argument for `run`: its id, or its path when the run named it by path. */
92
+ run: string;
93
+ /** Its id, for the message. */
94
+ id: string;
95
+ /** The study file to set `mode: live` in first, when the file still starts as a dry run. */
96
+ setLiveIn?: string;
83
97
  }
84
98
  /** The findings view of a run's selected analysis, or why it has none. */
85
99
  export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
@@ -9,6 +9,7 @@ import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
9
9
  import { resolvePhysicalCwd, runIdOf } from "../run/paths.js";
10
10
  import { studyProvenanceOf } from "../run/study-provenance.js";
11
11
  import { resolveStudyManifest } from "../study/discover.js";
12
+ import { routeOf } from "../study/routing.js";
12
13
  import { plural } from "../run/text.js";
13
14
  import { shellQuote } from "../substrates/shell.js";
14
15
  import { analysisOutcomeText, wantsJson } from "./io.js";
@@ -102,10 +103,21 @@ function withoutFindings(source, state, reason, message, next) {
102
103
  warnings: source.loaded.warnings,
103
104
  };
104
105
  }
106
+ /** A dry run's view: nothing to analyze, and the command that starts a live run. */
107
+ function dryRunFindings(source) {
108
+ const opening = "This is a dry run: no participant used the product, so there is nothing to analyze.";
109
+ const live = source.liveStudy;
110
+ if (live === undefined)
111
+ return withoutFindings(source, "dry_run", null, `${opening} Findings come from live runs. The next command lists this project's studies; a study with mode: live runs participants.`, cli(`study list${source.cwdFlag}`));
112
+ const howLive = live.setLiveIn === undefined
113
+ ? `the next command runs ${live.id} live.`
114
+ : `set mode: live in ${live.setLiveIn}, then run the next command.`;
115
+ return withoutFindings(source, "dry_run", null, `${opening} Findings come from live runs: ${howLive}`, cli(`run ${live.run}${source.cwdFlag}`));
116
+ }
105
117
  /** The findings view of a run's selected analysis, or why it has none. */
106
118
  export function analysisFindings(source) {
107
119
  if (source.mode !== undefined && source.mode !== "live")
108
- return withoutFindings(source, "dry_run", null, "This is a dry run: no participant used the product, so there is nothing to analyze. Findings come from live runs.", null);
120
+ return dryRunFindings(source);
109
121
  const loaded = projectShareCheckedAnalysis(source.loaded);
110
122
  const analysis = loaded.analysis;
111
123
  if (analysis?.result && (loaded.state === "ready" || loaded.state === "stale")) {
@@ -170,6 +182,30 @@ async function studyAnalysisOff(cwd, study) {
170
182
  return undefined;
171
183
  return resolved.config.review?.analysis === false ? study.path : undefined;
172
184
  }
185
+ /**
186
+ * The study a live run can start from after a dry run: the run's own study when its file runs
187
+ * live, else the starter study try-live when this project has one that runs live, else the run's
188
+ * own study once its file is set live. A preview study only dry-runs. Undefined when none applies.
189
+ */
190
+ async function liveStudyAfterDryRun(cwd, study) {
191
+ const resolve = async (ref) => {
192
+ const resolved = await resolveStudyManifest(cwd, ref).catch(() => null);
193
+ return resolved?.ok ? resolved.config : undefined;
194
+ };
195
+ const ownConfig = study === undefined
196
+ ? undefined
197
+ : await resolve(study.path ?? study.id).then((config) => config?.id === study.id ? config : undefined);
198
+ const own = study === undefined
199
+ ? undefined
200
+ : { run: study.origin === "explicit" ? (study.path ?? study.id) : study.id, id: study.id };
201
+ if (own !== undefined && ownConfig?.mode === "live")
202
+ return own;
203
+ if ((await resolve("try-live"))?.mode === "live")
204
+ return { run: "try-live", id: "try-live" };
205
+ if (own !== undefined && ownConfig !== undefined && study?.path !== undefined)
206
+ return routeOf(ownConfig) === "preview" ? undefined : { ...own, setLiveIn: study.path };
207
+ return undefined;
208
+ }
173
209
  /**
174
210
  * The run's bundle mode and its selected analysis: `analysisId`, or the version `analyze show`
175
211
  * defaults to. Null when the run is missing or its storage is unsafe.
@@ -180,16 +216,18 @@ export async function readRunAnalysis(cwd, run, analysisId) {
180
216
  if (!prepared)
181
217
  return null;
182
218
  const bundle = await loadRunBundlePrepared(physical, prepared).catch(() => null);
183
- const analysisOffIn = bundle?.bundle.mode === "live"
184
- ? await studyAnalysisOff(physical, studyProvenanceOf(bundle.bundle))
185
- : undefined;
219
+ const mode = bundle?.bundle.mode;
220
+ const study = bundle === null ? undefined : studyProvenanceOf(bundle.bundle);
221
+ const analysisOffIn = mode === "live" ? await studyAnalysisOff(physical, study) : undefined;
222
+ const liveStudy = mode !== undefined && mode !== "live" ? await liveStudyAfterDryRun(physical, study) : undefined;
186
223
  return {
187
224
  runId: runIdOf(prepared),
188
- mode: bundle?.bundle.mode,
225
+ mode,
189
226
  runRoot: prepared.relativeRunRoot,
190
227
  loaded: await loadAnalysis(prepared, analysisId),
191
228
  cwdFlag: cwdFlag(cwd),
192
229
  ...(analysisOffIn === undefined ? {} : { analysisOffIn }),
230
+ ...(liveStudy === undefined ? {} : { liveStudy }),
193
231
  };
194
232
  }
195
233
  /** The view of a run that could not be found. */
@@ -112,11 +112,11 @@ export interface RedactionHooks {
112
112
  /** The default hooks wired into every actor: redaction.ts is the source of truth. */
113
113
  export declare const defaultRedactionHooks: RedactionHooks;
114
114
  /**
115
- * Replace each literal value with [REDACTED_SECRET]. For values that have no secret shape to
115
+ * Replace each literal value with `marker`. For values that have no secret shape to
116
116
  * pattern-match, such as provisioned keys and subject env values. `values` is read on each call,
117
117
  * so a value registered later is scrubbed too.
118
118
  */
119
- export declare function scrubLiterals(values: readonly string[]): (text: string) => string;
119
+ export declare function scrubLiterals(values: readonly string[], marker?: string): (text: string) => string;
120
120
  /**
121
121
  * Coerce an unknown thrown value to its message string. Prefer this over the
122
122
  * inline `error instanceof Error ? error.message : String(error)` so error
@@ -522,12 +522,12 @@ function blurPass(src, width, height, radius, horizontal) {
522
522
  return out;
523
523
  }
524
524
  /**
525
- * Replace each literal value with [REDACTED_SECRET]. For values that have no secret shape to
525
+ * Replace each literal value with `marker`. For values that have no secret shape to
526
526
  * pattern-match, such as provisioned keys and subject env values. `values` is read on each call,
527
527
  * so a value registered later is scrubbed too.
528
528
  */
529
- export function scrubLiterals(values) {
530
- return (text) => values.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
529
+ export function scrubLiterals(values, marker = "[REDACTED_SECRET]") {
530
+ return (text) => values.reduce((current, value) => current.split(value).join(marker), text);
531
531
  }
532
532
  /**
533
533
  * Coerce an unknown thrown value to its message string. Prefer this over the
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A value as written, and percent-encoded, JSON-escaped once (also with non-ASCII characters as
3
+ * `\u` escapes) and twice, base64 at each byte offset, base64url and hex. Twice, because a JSON event can carry a command's JSON output as a string. An
4
+ * escaped form is searched for at any length, as the value is: it holds a backslash or a `%`, so it
5
+ * is not ordinary text.
6
+ */
7
+ export declare function encodedForms(value: string): string[];
1
8
  /**
2
9
  * A scrub that replaces each non-empty value and its encoded forms with `[REDACTED_SECRET]`. Every
3
10
  * occurrence of every form is found on its own, and overlapping finds are merged into one span, so
@@ -25,25 +25,34 @@ function base64Middles(bytes, encoding) {
25
25
  return encoded.slice(start, end);
26
26
  });
27
27
  }
28
- /** A value as written, and percent-encoded, JSON-escaped, base64 at each byte offset, base64url and hex. */
29
- function encodedForms(value) {
28
+ /**
29
+ * A value as written, and percent-encoded, JSON-escaped once (also with non-ASCII characters as
30
+ * `\u` escapes) and twice, base64 at each byte offset, base64url and hex. Twice, because a JSON event can carry a command's JSON output as a string. An
31
+ * escaped form is searched for at any length, as the value is: it holds a backslash or a `%`, so it
32
+ * is not ordinary text.
33
+ */
34
+ export function encodedForms(value) {
30
35
  const bytes = Buffer.from(value, "utf8");
31
- const encoded = [
32
- JSON.stringify(value).slice(1, -1),
36
+ const json = JSON.stringify(value).slice(1, -1);
37
+ // A serializer that writes ASCII only, such as Python's json.dumps, spells é as \u00e9.
38
+ const asciiJson = json.replace(/[^\x00-\x7f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
39
+ const escaped = [json, asciiJson, JSON.stringify(json).slice(1, -1)];
40
+ // The encoders throw on a lone surrogate. Such a value is still found as written. encodeURI
41
+ // keeps a `/` or a `:` as written, as a URL path does.
42
+ try {
43
+ escaped.push(encodeURIComponent(value), encodeURI(value));
44
+ }
45
+ catch {
46
+ // no percent-encoded form
47
+ }
48
+ const binary = [
33
49
  bytes.toString("base64"),
34
50
  bytes.toString("base64url"),
35
51
  bytes.toString("hex"),
36
52
  ...base64Middles(bytes, "base64"),
37
53
  ...base64Middles(bytes, "base64url"),
38
54
  ];
39
- // encodeURIComponent throws on a lone surrogate. Such a value is still found as written.
40
- try {
41
- encoded.push(encodeURIComponent(value));
42
- }
43
- catch {
44
- // no percent-encoded form
45
- }
46
- return [value, ...encoded.filter((form) => form.length >= MIN_ENCODED_FORM)];
55
+ return [value, ...escaped, ...binary.filter((form) => form.length >= MIN_ENCODED_FORM)];
47
56
  }
48
57
  /** Every occurrence of every form, outside the markers, merged where they overlap or touch. */
49
58
  function secretSpans(text, forms) {
@@ -1,39 +1,8 @@
1
- import type { CuaLoopResult } from "../../actors/computer-use/loop.js";
2
- import { type RunFeedbackCandidate, type RunProviderResource, type RunSubjectProvenance } from "../../run/bundle.js";
1
+ import { type RunProviderResource, type RunSubjectProvenance } from "../../run/bundle.js";
3
2
  import { type ParticipantIds } from "../../run/participant-records.js";
4
3
  import { type CuaSubjectProvenanceArg, type ParticipantRunOutcome } from "./types.js";
5
4
  /** The human-readable state story appended to the provenance event (and review.md via it). */
6
5
  export declare function describeSubjectState(state: RunSubjectProvenance["state"], dryRun: boolean): string;
7
- /**
8
- * Feedback candidates derived from what live participants actually reported.
9
- *
10
- * A live run's feedback draft comes from these candidates, never from a dry-run template. The
11
- * candidate worth filing is the one the study produced: a participant
12
- * who reported friction on the way (the most valuable thing a run captures), or one who stopped
13
- * trying. A clean pass files nothing here: feedback exists to carry findings, and a run without
14
- * any falls back to a live summary in the draft layer instead of a template.
15
- *
16
- * Everything quoted is already scrub+redacted (participant messages and `session.reason` pass
17
- * through redactNarration in the loop) and passes redactText again here as defense-in-depth.
18
- */
19
- export declare function participantFeedbackCandidates(args: {
20
- runId: string;
21
- scenarioId: string;
22
- adapterId: string;
23
- /** The already-redacted study goal (what bundle.scenario.goal carries). */
24
- goal: string;
25
- substrate: RunFeedbackCandidate["substrate"];
26
- participants: Array<{
27
- /** The participant's plan id; it names the candidate and its idempotency key. */
28
- participantId: string;
29
- streamId: string;
30
- personaId: string;
31
- session?: CuaLoopResult;
32
- traceArtifactPath?: string;
33
- screenshots: string[];
34
- commsArtifactPath?: string;
35
- }>;
36
- }): RunFeedbackCandidate[];
37
6
  /** Human-readable provenance line for the single-participant subject.provenance event:
38
7
  * claims "cloned/packed and served" only when it actually happened. */
39
8
  export declare function subjectProvenanceMessage(provenance: CuaSubjectProvenanceArg, publicAppUrl: string, dryRun: boolean, hasSession: boolean): string;