humanish 0.116.0 → 0.118.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 (120) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
  3. package/dist/actors/codex/restricted-participant-policy.js +4 -32
  4. package/dist/actors/codex/restricted-participant.d.ts +5 -0
  5. package/dist/actors/codex/restricted-participant.js +3 -2
  6. package/dist/actors/computer-use/actor.d.ts +4 -0
  7. package/dist/actors/computer-use/actor.js +2 -0
  8. package/dist/actors/computer-use/loop/actions.d.ts +4 -4
  9. package/dist/actors/computer-use/loop/actions.js +25 -18
  10. package/dist/actors/computer-use/loop/session.d.ts +3 -0
  11. package/dist/actors/computer-use/loop/session.js +9 -1
  12. package/dist/actors/computer-use/loop/trace.js +1 -0
  13. package/dist/actors/computer-use/loop/types.d.ts +16 -14
  14. package/dist/actors/computer-use/loop.d.ts +1 -1
  15. package/dist/actors/computer-use/loop.js +1 -1
  16. package/dist/actors/computer-use/wait.d.ts +74 -0
  17. package/dist/actors/computer-use/wait.js +100 -0
  18. package/dist/actors/contract.d.ts +14 -0
  19. package/dist/analysis/admission.d.ts +7 -2
  20. package/dist/analysis/admission.js +12 -3
  21. package/dist/analysis/automatic-config.d.ts +6 -1
  22. package/dist/analysis/automatic-config.js +12 -11
  23. package/dist/analysis/execute.d.ts +19 -7
  24. package/dist/analysis/execute.js +55 -17
  25. package/dist/browser-control/protocol.d.ts +86 -6
  26. package/dist/browser-control/protocol.js +13 -6
  27. package/dist/cli/commands/analyze.js +2 -1
  28. package/dist/cli/commands/study-format.js +8 -11
  29. package/dist/cli/commands/study-route-computer-use.js +2 -2
  30. package/dist/cli/commands/study-route-scripted.js +2 -2
  31. package/dist/cli/commands/study-route-shared-world.js +5 -3
  32. package/dist/cli/commands/study-route-terminal.js +2 -2
  33. package/dist/cli/commands/study-run.js +3 -3
  34. package/dist/cli/commands/study.js +2 -2
  35. package/dist/cli/doctor.js +11 -1
  36. package/dist/cli/io.js +3 -2
  37. package/dist/comms/lease-store.d.ts +1 -1
  38. package/dist/comms/lease-store.js +3 -2
  39. package/dist/comms/receiving-types.d.ts +2 -0
  40. package/dist/comms/receiving-types.js +2 -0
  41. package/dist/guest/desktop-executor.js +3 -1
  42. package/dist/observer-app.html +7 -7
  43. package/dist/routes/computer-use/e2b-desktop/prepare.js +40 -31
  44. package/dist/routes/computer-use/fanout-bundle.js +2 -1
  45. package/dist/routes/computer-use/fanout-records.js +2 -1
  46. package/dist/routes/computer-use/local-vm.js +5 -2
  47. package/dist/routes/computer-use/participant-model.js +9 -0
  48. package/dist/routes/computer-use/participant-runs.js +3 -9
  49. package/dist/routes/computer-use/plan.d.ts +3 -1
  50. package/dist/routes/computer-use/plan.js +29 -9
  51. package/dist/routes/computer-use/single-bundle.js +7 -4
  52. package/dist/routes/computer-use/subject-projection.js +2 -0
  53. package/dist/routes/computer-use/types.d.ts +1 -1
  54. package/dist/routes/scripted/plan.d.ts +4 -1
  55. package/dist/routes/scripted/plan.js +25 -2
  56. package/dist/routes/scripted/setup.js +1 -4
  57. package/dist/routes/scripted/types.d.ts +1 -1
  58. package/dist/routes/shared-world/participant-specs.js +2 -2
  59. package/dist/routes/shared-world/plan.d.ts +3 -1
  60. package/dist/routes/shared-world/plan.js +39 -7
  61. package/dist/routes/shared-world/route.js +2 -2
  62. package/dist/routes/terminal/live-sandbox.d.ts +1 -1
  63. package/dist/routes/terminal/live-sandbox.js +14 -11
  64. package/dist/routes/terminal/sandbox.d.ts +4 -3
  65. package/dist/routes/terminal/sandbox.js +5 -5
  66. package/dist/run/bundle-shape.js +2 -1
  67. package/dist/run/bundle.d.ts +5 -3
  68. package/dist/run/projection.js +7 -2
  69. package/dist/run/route-shell.js +10 -5
  70. package/dist/run/subject-name.d.ts +11 -0
  71. package/dist/run/subject-name.js +11 -0
  72. package/dist/study/automatic-analysis-plan.d.ts +28 -0
  73. package/dist/study/automatic-analysis-plan.js +57 -0
  74. package/dist/study/composition-rules.js +1 -1
  75. package/dist/study/config.js +10 -21
  76. package/dist/study/doctor.js +17 -3
  77. package/dist/study/keys.d.ts +2 -0
  78. package/dist/study/keys.js +2 -0
  79. package/dist/study/migrate/v2.js +2 -1
  80. package/dist/study/parse/actors.d.ts +1 -1
  81. package/dist/study/parse/actors.js +9 -1
  82. package/dist/study/parse/front.d.ts +1 -1
  83. package/dist/study/parse/front.js +7 -1
  84. package/dist/study/plan-base.d.ts +2 -0
  85. package/dist/study/plan-base.js +3 -0
  86. package/dist/study/plan-participants.d.ts +4 -0
  87. package/dist/study/plan-participants.js +6 -5
  88. package/dist/study/plan-types.d.ts +8 -3
  89. package/dist/study/plan.d.ts +4 -4
  90. package/dist/study/plan.js +21 -12
  91. package/dist/study/preflight-result.js +2 -2
  92. package/dist/study/preflight.d.ts +2 -1
  93. package/dist/study/preflight.js +12 -3
  94. package/dist/study/routing.d.ts +5 -4
  95. package/dist/study/routing.js +16 -7
  96. package/dist/study/summary.d.ts +3 -3
  97. package/dist/study/summary.js +3 -5
  98. package/dist/study/types.d.ts +14 -0
  99. package/dist/study/validation.d.ts +7 -2
  100. package/dist/study/validation.js +30 -14
  101. package/dist/study/warnings.js +12 -0
  102. package/dist/subject/serve.d.ts +3 -0
  103. package/dist/subject/serve.js +4 -1
  104. package/dist/substrates/detached.js +14 -7
  105. package/dist/substrates/e2b/desktop-executor.d.ts +0 -2
  106. package/dist/substrates/e2b/desktop-executor.js +3 -3
  107. package/dist/substrates/e2b/lifetime.d.ts +75 -4
  108. package/dist/substrates/e2b/lifetime.js +134 -10
  109. package/dist/substrates/e2b/subject-sandbox.js +2 -6
  110. package/dist/tui-app.js +119 -118
  111. package/dist/verify/artifacts.d.ts +19 -4
  112. package/dist/verify/artifacts.js +39 -8
  113. package/dist/verify/verify.js +6 -15
  114. package/docs/architecture/browser-control.md +10 -4
  115. package/docs/contracts/run-bundle.md +7 -6
  116. package/docs/contracts/schemas.md +33 -19
  117. package/docs/contracts/study-analysis.md +2 -2
  118. package/docs/principles/invariants-and-defaults.md +1 -1
  119. package/docs/product/automatic-analysis.md +4 -3
  120. package/package.json +2 -2
@@ -7,6 +7,20 @@ export declare const MAX_REPORTED_FINDINGS = 50;
7
7
  * refuses a run with more, so the scan never passes a run whose files it did not all read.
8
8
  */
9
9
  export declare const MAX_RUN_ENTRIES = 10000;
10
+ /** What the public-safety scan did not read. A run with any of it cannot grade share_ready. */
11
+ export interface UnscannedArtifacts {
12
+ /** Run files it could not read as text, by run-relative path. */
13
+ readonly files: string[];
14
+ /** Folders it could not list, by run-relative path ("" is the run folder). */
15
+ readonly unlistedFolders: string[];
16
+ /** The folder it was listing when it reached MAX_RUN_ENTRIES. It read nothing listed after. */
17
+ stoppedIn?: string;
18
+ }
19
+ /**
20
+ * The UNSCANNED_ARTIFACT reason: the files the scan could not read, the folders it could not list
21
+ * and where it stopped, each in its own sentence. Undefined when the scan read everything.
22
+ */
23
+ export declare function unscannedMessage(unscanned: UnscannedArtifacts): string | undefined;
10
24
  export declare function missingLocalEvidenceArtifacts(runPaths: PreparedRunArtifactPaths, bundle: RunBundle): Promise<string[]>;
11
25
  /**
12
26
  * The PNG frames that actor traces register as stream screenshots, relative to the run root.
@@ -50,9 +64,10 @@ interface RegisteredStreamMedia {
50
64
  /**
51
65
  * Scans every run file for secret and path patterns and returns the findings. A file that is not
52
66
  * registered stream media and that the scan cannot read as text (readPlainText), cannot read at
53
- * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned`, so the caller can keep
54
- * the run from grading share_ready. So does a directory it cannot list, and the rest of the run
55
- * after MAX_RUN_ENTRIES entries.
67
+ * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned.files`, so the caller can
68
+ * keep the run from grading share_ready. A directory it cannot list goes to
69
+ * `unscanned.unlistedFolders`, and the directory it was listing when it passed MAX_RUN_ENTRIES
70
+ * entries is `unscanned.stoppedIn`.
56
71
  */
57
- export declare function scanRunPublicSafetyArtifacts(runPaths: PreparedRunArtifactPaths, derivedFindings: string[], media: RegisteredStreamMedia, unscanned: string[]): Promise<string[]>;
72
+ export declare function scanRunPublicSafetyArtifacts(runPaths: PreparedRunArtifactPaths, derivedFindings: string[], media: RegisteredStreamMedia, unscanned: UnscannedArtifacts): Promise<string[]>;
58
73
  export {};
@@ -12,6 +12,7 @@ import { SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
12
12
  import { readSafeRunArtifact, readSafeRunArtifactBytes, readSafeRunArtifactJson, } from "../run/locate.js";
13
13
  import { isRecord } from "../run/type-guards.js";
14
14
  import { TERMINAL_EVENTS_ARTIFACT } from "../run/terminal-contract.js";
15
+ import { plural } from "../run/text.js";
15
16
  import { isZeroEventTerminalTrace } from "./actor.js";
16
17
  /** Public-safety and evidence-reference findings stop at this many per list. */
17
18
  export const MAX_REPORTED_FINDINGS = 50;
@@ -20,6 +21,35 @@ export const MAX_REPORTED_FINDINGS = 50;
20
21
  * refuses a run with more, so the scan never passes a run whose files it did not all read.
21
22
  */
22
23
  export const MAX_RUN_ENTRIES = 10_000;
24
+ // The UNSCANNED_ARTIFACT reason names this many paths of each kind and counts the rest.
25
+ const MAX_LISTED_UNSCANNED = 10;
26
+ /**
27
+ * The UNSCANNED_ARTIFACT reason: the files the scan could not read, the folders it could not list
28
+ * and where it stopped, each in its own sentence. Undefined when the scan read everything.
29
+ */
30
+ export function unscannedMessage(unscanned) {
31
+ const files = [...new Set(unscanned.files)].sort();
32
+ const folders = [...new Set(unscanned.unlistedFolders)].sort().map(folderName);
33
+ const sentences = [];
34
+ if (files.length > 0)
35
+ sentences.push(`The public-safety scan cannot read ${plural(files.length, "file")} as text that ${files.length === 1 ? "is not a stream screenshot under screenshots/ or a registered recording" : "are not stream screenshots under screenshots/ or registered recordings"}: ${listed(files)}.`);
36
+ if (folders.length > 0)
37
+ sentences.push(`The public-safety scan could not list ${plural(folders.length, "folder")}, so it read none of the files in ${folders.length === 1 ? "it" : "them"}: ${listed(folders)}.`);
38
+ if (unscanned.stoppedIn !== undefined)
39
+ sentences.push(`The public-safety scan stopped at its limit of ${MAX_RUN_ENTRIES} files and folders while listing ${folderName(unscanned.stoppedIn)}, so it read nothing listed after that point.`);
40
+ if (sentences.length === 0)
41
+ return undefined;
42
+ const onlyFiles = folders.length === 0 && unscanned.stoppedIn === undefined;
43
+ sentences.push(onlyFiles ? "Review them before sharing." : "Review the run folder before sharing.");
44
+ return sentences.join(" ");
45
+ }
46
+ function folderName(relativeDirectory) {
47
+ return relativeDirectory === "" ? "the run folder" : `${relativeDirectory}/`;
48
+ }
49
+ function listed(paths) {
50
+ const more = paths.length - MAX_LISTED_UNSCANNED;
51
+ return `${paths.slice(0, MAX_LISTED_UNSCANNED).join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
52
+ }
23
53
  export async function missingLocalEvidenceArtifacts(runPaths, bundle) {
24
54
  const recordings = new Map(bundle.streams.flatMap((stream) => stream.recording ? [[stream.recording.path, stream.recording]] : []));
25
55
  const requiredPaths = new Map();
@@ -291,9 +321,10 @@ export function rawScreenshotStreamIds(bundle, redactedShapeFrames) {
291
321
  /**
292
322
  * Scans every run file for secret and path patterns and returns the findings. A file that is not
293
323
  * registered stream media and that the scan cannot read as text (readPlainText), cannot read at
294
- * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned`, so the caller can keep
295
- * the run from grading share_ready. So does a directory it cannot list, and the rest of the run
296
- * after MAX_RUN_ENTRIES entries.
324
+ * all, or that holds more than RUN_ARTIFACT_MAX_BYTES goes to `unscanned.files`, so the caller can
325
+ * keep the run from grading share_ready. A directory it cannot list goes to
326
+ * `unscanned.unlistedFolders`, and the directory it was listing when it passed MAX_RUN_ENTRIES
327
+ * entries is `unscanned.stoppedIn`.
297
328
  */
298
329
  export async function scanRunPublicSafetyArtifacts(runPaths, derivedFindings, media, unscanned) {
299
330
  const findings = [];
@@ -315,14 +346,14 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
315
346
  : runPaths.physicalRunRoot;
316
347
  const entries = await readdir(current).catch(() => null);
317
348
  if (entries === null) {
318
- unscanned.push(`${relativeDirectory || "."}/ (could not be listed)`);
349
+ unscanned.unlistedFolders.push(relativeDirectory);
319
350
  return;
320
351
  }
321
352
  for (const entryName of entries) {
322
353
  walk.entries += 1;
323
354
  if (walk.entries > MAX_RUN_ENTRIES) {
324
- if (walk.entries === MAX_RUN_ENTRIES + 1)
325
- unscanned.push(`${relativeDirectory || "."}/ (the scan stops after ${MAX_RUN_ENTRIES} entries)`);
355
+ // Every directory the walk returns through comes here once more; the first one stopped it.
356
+ unscanned.stoppedIn ??= relativeDirectory;
326
357
  return;
327
358
  }
328
359
  const relativePath = relativeDirectory ? `${relativeDirectory}/${entryName}` : entryName;
@@ -371,7 +402,7 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
371
402
  const bytes = await readSafeRunArtifactBytes(runPaths, relativePath).catch(() => null);
372
403
  const decoded = bytes === null ? undefined : readPlainText(bytes);
373
404
  if (decoded === undefined || !decoded.ok) {
374
- unscanned.push(relativePath);
405
+ unscanned.files.push(relativePath);
375
406
  continue;
376
407
  }
377
408
  // A receipt's id in any other file, or a raw id at a sandbox-id key, which an older run or
@@ -389,7 +420,7 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
389
420
  selectedFindings.push(`sensitive text ${relativePath}`);
390
421
  }
391
422
  else if (scan.opaque) {
392
- unscanned.push(relativePath);
423
+ unscanned.files.push(relativePath);
393
424
  }
394
425
  }
395
426
  }
@@ -13,18 +13,15 @@ import { SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
13
13
  import { isRecord } from "../run/type-guards.js";
14
14
  import { runNotFoundMessage } from "../run/run-not-found.js";
15
15
  import { actorVerdictConsistencyFindings, noEngagementActorFindings, validateCodexAppServerEvidence, validateTerminalProductEvidence, } from "./actor.js";
16
- import { invalidRunEvidenceReferences, MAX_REPORTED_FINDINGS, missingLocalEvidenceArtifacts, rawScreenshotPostureWarnings, rawScreenshotStreamIds, redactedShapeFramePaths, scanRunPublicSafetyArtifacts, streamScreenshotPaths, } from "./artifacts.js";
16
+ import { invalidRunEvidenceReferences, MAX_REPORTED_FINDINGS, missingLocalEvidenceArtifacts, rawScreenshotPostureWarnings, rawScreenshotStreamIds, redactedShapeFramePaths, scanRunPublicSafetyArtifacts, streamScreenshotPaths, unscannedMessage, } from "./artifacts.js";
17
17
  import { flatContextWarnings } from "./context-growth.js";
18
18
  import { runNotFinished } from "./liveness.js";
19
19
  import { costAndReceiptFindings } from "./costs.js";
20
20
  import { rerunLineageFindings } from "./rerun.js";
21
21
  import { sharedWorldEvidenceFindings } from "./shared-world.js";
22
22
  import { subjectStateFindings, undeclaredSubjectStateWarnings } from "./subject.js";
23
- import { plural } from "../run/text.js";
24
23
  import { cli } from "../cli/invocation.js";
25
24
  export const VERIFY_SCHEMA = "humanish.verify-result.v1";
26
- // The UNSCANNED_ARTIFACT reason names this many paths and counts the rest.
27
- const MAX_LISTED_UNSCANNED = 10;
28
25
  export async function verifyRun(cwdInput, runInput) {
29
26
  const cwd = path.resolve(cwdInput);
30
27
  let runPaths;
@@ -86,7 +83,7 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
86
83
  };
87
84
  checks.push(...bundlePresenceChecks(bundleRead, review));
88
85
  const derivedPublicSafetyFindings = [];
89
- const unscannedArtifacts = [];
86
+ const unscannedArtifacts = { files: [], unlistedFolders: [] };
90
87
  const sandboxIdFiles = [];
91
88
  let journaledSandboxIds = [];
92
89
  try {
@@ -96,7 +93,7 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
96
93
  if (!(error instanceof ContainedReadRefusedError))
97
94
  throw error;
98
95
  // Without the journal's ids, verify cannot clear any file of naming one.
99
- unscannedArtifacts.push(SANDBOX_RECEIPTS_ARTIFACT);
96
+ unscannedArtifacts.files.push(SANDBOX_RECEIPTS_ARTIFACT);
100
97
  }
101
98
  const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths, derivedPublicSafetyFindings, {
102
99
  recordingPaths: new Set(isRunBundle(bundle)
@@ -397,15 +394,9 @@ function buildShareSafety(args) {
397
394
  message: "Continuous screen/audio recordings are retained for local review. Screenshot redaction does not redact this media.",
398
395
  });
399
396
  }
400
- if (args.unscannedArtifacts.length > 0) {
401
- const paths = [...new Set(args.unscannedArtifacts)].sort();
402
- const shown = paths.slice(0, MAX_LISTED_UNSCANNED).join(", ");
403
- const more = paths.length - MAX_LISTED_UNSCANNED;
404
- reasons.push({
405
- code: "UNSCANNED_ARTIFACT",
406
- message: `The public-safety scan cannot read ${plural(paths.length, "file")} as text that ${paths.length === 1 ? "is not a stream screenshot under screenshots/ or a registered recording" : "are not stream screenshots under screenshots/ or registered recordings"}: ${shown}${more > 0 ? ` and ${more} more` : ""}. Review them before sharing.`,
407
- });
408
- }
397
+ const unscanned = unscannedMessage(args.unscannedArtifacts);
398
+ if (unscanned !== undefined)
399
+ reasons.push({ code: "UNSCANNED_ARTIFACT", message: unscanned });
409
400
  // Runs from 0.110 keep raw sandbox ids only in sandbox-receipts.ndjson. An earlier run records
410
401
  // them in run.json, and any file that names one is not share-ready as it is.
411
402
  const rawResources = (args.bundle.providerResources ?? []).some((resource) => resource.id !== REDACTED_SANDBOX_ID);
@@ -103,10 +103,16 @@ Observation requires a PNG and state signature. It may include bounded URL,
103
103
  title, text, fractional scroll position and heard speech; those remain
104
104
  runtime-only under the existing loop contract. Arbitrary `appState` is refused
105
105
  because v1 has no closed schema for it. The protocol does not truncate strings,
106
- round coordinates, or silently drop unsupported state. A Codex participant's
107
- `humanish_ui` call is checked before it reaches the protocol: a wait longer than 30 seconds is
108
- shortened to 30 seconds there, and the trace records a `wait shortened` notice with both
109
- durations.
106
+ round coordinates, or silently drop unsupported state. A participant may ask for a
107
+ longer wait than one request carries. The computer-use loop shortens it to the study's
108
+ `actor.maxWaitMs` (by default two minutes, or 30 seconds on a desktop with speech, whose heard
109
+ speech reaches the participant only with a screenshot), records a `wait shortened` notice with both
110
+ durations when it does, and sends the rest as consecutive waits of at most 30 seconds. The Codex
111
+ participant's `humanish_ui` schema leaves a wait's length open for that reason; the wire still
112
+ refuses a single wait over 30 seconds. A wait that names no duration, as every OpenAI computer-use
113
+ wait does, reaches the desktop with a length too: the study's `actor.idleWaitMs` (5 seconds by
114
+ default) when the participant's turn only waits or takes screenshots, and 500 ms after an action in
115
+ the same turn.
110
116
 
111
117
  Click, double-click, move, scroll and drag may carry `heldKeys`, which the
112
118
  OpenAI provider maps from its computer tool's `keys`. The Codex participant's
@@ -305,15 +305,16 @@ exactly; this hosted-desktop rule does not change that contract.
305
305
  `subject` is an optional, additive top-level field: structured provenance for
306
306
  what the computer-use, shared-world or scripted route actually drove (code pin plus state story). It
307
307
  is absent on pre-existing bundles and on bundles from routes that have not
308
- adopted it. The field shape, its four sources (`clone`, `app-url`,
309
- `local-tree`, `desktop-cli`), and the `humanish verify` checks that guard it are the schema doc's
310
- job, not this one: see the `subject` entry under
308
+ adopted it. The field shape, its five sources (`clone`, `app-url`,
309
+ `local-tree`, `desktop-cli`, `local-app`), and the `humanish verify` checks that guard it are the
310
+ schema doc's job, not this one: see the `subject` entry under
311
311
  [`schemas.md`](schemas.md#run-bundle). In short, `clone` carries a
312
312
  `repo`/`commit` pin, `local-tree` carries an `archiveSha256`/`dirty` pin
313
313
  instead (a dirty working tree cannot be commit-pinned), `desktop-cli` carries the
314
- declared `product` name, and `app-url` carries no code pin at all. A computer-use run
315
- of a desktop-cli study saved before `desktop-cli` was recorded says `app-url`, and
316
- readers accept it. No path, basename, or other host-machine string ever
314
+ declared `product` name, and `app-url` and `local-app` (a running app the caller drives in
315
+ process) carry no code pin at all. A computer-use run of a desktop-cli study saved before
316
+ `desktop-cli` was recorded, or of a local-app study saved before `local-app` was recorded, says
317
+ `app-url`, and readers accept it. No path, basename, or other host-machine string ever
317
318
  enters this field; identity is digests, a sha, a boolean, counts and the declared product name.
318
319
 
319
320
  ## Cost Estimate (advisory)
@@ -76,17 +76,17 @@ map each one to its v3 key.
76
76
  A study declares its route and composes code primitives; it is not a hardcoded kind. The
77
77
  top-level keys:
78
78
 
79
- | Key | Meaning |
80
- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
81
- | `schema`, `id`, `title`, `description` | the document's identity |
82
- | `route` | required: `preview`, `computer-use`, `shared-world`, `terminal` or `scripted` |
83
- | `mode` | `dry-run` (the default) or `live` |
84
- | `actor` | who drives it, one object: `type`, `model`, `localAgent`, `persona`, `mission`, `tasks`, `maxOutputTokens`, `reasoningEffort`, `stopWhen`, `dwell` |
85
- | `participants` | a count, a homogeneous `{ count, instruction }`, or a list of entries (below) |
86
- | `surfaces` | scripted only: `[desktop]` or `[desktop, mobile]` |
87
- | `caps` | the route's budget (below) |
88
- | `scenario` | scripted only: a committed scenario id or path |
89
- | `subject`, `execution`, `policies`, `review`, `defaults`, `comms` | as below |
79
+ | Key | Meaning |
80
+ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
+ | `schema`, `id`, `title`, `description` | the document's identity |
82
+ | `route` | required: `preview`, `computer-use`, `shared-world`, `terminal` or `scripted` |
83
+ | `mode` | `dry-run` (the default) or `live` |
84
+ | `actor` | who drives it, one object: `type`, `model`, `localAgent`, `persona`, `mission`, `tasks`, `maxOutputTokens`, `maxWaitMs`, `idleWaitMs`, `reasoningEffort`, `stopWhen`, `dwell` |
85
+ | `participants` | a count, a homogeneous `{ count, instruction }`, or a list of entries (below) |
86
+ | `surfaces` | scripted only: `[desktop]` or `[desktop, mobile]` |
87
+ | `caps` | the route's budget (below) |
88
+ | `scenario` | scripted only: a committed scenario id or path |
89
+ | `subject`, `execution`, `policies`, `review`, `defaults`, `comms` | as below |
90
90
 
91
91
  The parser reads only the declared route's keys. A key that route never reads is an error that
92
92
  names the route, such as "route: computer-use does not read scenario. Remove it.", and an
@@ -183,7 +183,7 @@ The rest of this section names keys by their v3 spelling:
183
183
  owns the live sandbox, auth, cap, evidence, and cleanup lifecycle.
184
184
  A `participants` count is the preview route's participant count (simCount),
185
185
  and on the computer-use **E2B** route the homogeneous fan-out count (N
186
- identical participants, each its own E2B desktop: separate worlds, cap 16).
186
+ identical participants, each its own E2B desktop: separate worlds, at most 100).
187
187
  The scripted route's `surfaces` is its surface list (`[desktop]`, the default,
188
188
  or `[desktop, mobile]`). The in-process/local-app computer-use route stays
189
189
  single participant (no E2B to fan out);
@@ -197,7 +197,8 @@ The rest of this section names keys by their v3 spelling:
197
197
  so `--count` does not apply to it, and each entry's `instruction` is the steer;
198
198
  an entry's `device` is XOR with a raw `execution.desktop.resolution`. Participant ids
199
199
  default `lane-01`..`lane-NN` (`role-01`..`role-NN` for shared-world participants), must be unique, and name per-participant evidence paths
200
- (`actors/<streamId>.json`, `screenshots/<laneId>/`). Cap 16 participants. The other
200
+ (`actors/<streamId>.json`, `screenshots/<laneId>/`). At most 100 participants, on the
201
+ shared-world route too. The other
201
202
  routes refuse a list (table above). `subject.clone.fanout` is rejected on the
202
203
  computer-use, shared-world and scripted routes (declare fan-out with
203
204
  `participants`). No current route reads `clone.fanout`;
@@ -216,9 +217,12 @@ The rest of this section names keys by their v3 spelling:
216
217
  shape. Ids are checked for collisions after expansion;
217
218
  - `execution.concurrency` (computer-use E2B routes, including shared-world): a
218
219
  cap on participants in flight at once. When omitted, every participant runs
219
- simultaneously: independent participants resolve it from the final participant count,
220
- after any `--count` override, and the parser fills `concurrency = participantCount`
221
- for multi-participant shared-world studies. Total sessions and spend are identical either way; only wall-clock
220
+ simultaneously: both routes resolve it when they plan, from the final participant count
221
+ after any `--count` override. On hosted desktops the plan runs at most
222
+ `HUMANISH_E2B_MAX_CONCURRENT_SANDBOXES` sandboxes at once (20 when unset; a provisioned
223
+ shared world's app sandbox counts against it), so a larger roster runs in waves and the plan
224
+ records a warning naming the setting. A declared value above that limit is refused at plan
225
+ time. Total sessions and spend are identical either way; only wall-clock
222
226
  and simultaneity differ. Declaring a value below the participant count runs participants in
223
227
  waves and emits a warning saying so, because a green waved run is otherwise
224
228
  indistinguishable from the all-live run the author meant. Shared-world studies
@@ -529,7 +533,7 @@ Core-owned fields:
529
533
  an older run, else `inferLegacyStudyId`, which reads the `persona.source` or `scenario.source`
530
534
  convention `study:<id>`, or `lab:<id>` from runs written before 0.108, and nothing else.
531
535
  - `subject` (optional, additive): structured subject provenance,
532
- `{ source: clone | app-url | local-tree | desktop-cli, repo?, product?, commit?,
536
+ `{ source: clone | app-url | local-tree | desktop-cli | local-app, repo?, product?, commit?,
533
537
  archiveSha256?, dirty?, envNames?, state }` where `state` is `{ provenance: seeded |
534
538
  unpinned | declared-not-run | undeclared | external-public, seed?: [{ name, when,
535
539
  commandDigest, ok?, exitCode?, timedOut?, durationMs? }], externalEnvNames?
@@ -541,7 +545,9 @@ Core-owned fields:
541
545
  the archive content digest stands in for it. `product` is the desktop-cli field: the
542
546
  `subject.product.name` the study declares, with state `undeclared`. A computer-use
543
547
  run of a desktop-cli study saved before this field existed records `app-url` with no
544
- `product`. `commandDigest` is the
548
+ `product`. `local-app` is an in-process run of a local-app study, the caller's running app
549
+ with no code pin and state `undeclared`; one saved before that value existed records
550
+ `app-url`. `commandDigest` is the
545
551
  sha256-16 of the exact seed command; command text and env values never
546
552
  appear. `humanish verify` fails closed when a live `local-tree` bundle carries
547
553
  no well-formed `archiveSha256`, in addition to the existing `subject state
@@ -985,6 +991,14 @@ Core-owned fields:
985
991
  “provider reply cut off by the output limit; asking again” notice. A second
986
992
  cut-off reply to the same request ends the session with
987
993
  `stopCause: provider_output_limit`.
994
+ - optional `waitSettings`: the wait lengths a computer-use participant's session
995
+ applied, in milliseconds, defaults included. `maxWaitMs` is the longest one
996
+ wait action lasts (`actor.maxWaitMs`); a longer wait is shortened to it.
997
+ `idleWaitMs` is how long a wait that names no duration lasts in a turn that
998
+ only waits or takes screenshots (`actor.idleWaitMs`), and `settleWaitMs` how
999
+ long such a wait lasts after an action in the same turn. Each `ui_action` for
1000
+ a wait is titled with the length it lasted. Absent on other routes and on
1001
+ earlier bundles
988
1002
  - optional `conversation`: how the first-party OpenAI computer-use provider
989
1003
  carried the conversation between requests. `mode` is `threaded` (the server
990
1004
  kept it through `previous_response_id`) or `explicit_context` (the server kept
@@ -1668,7 +1682,7 @@ of it fails.
1668
1682
  | `cuaLaneCount` | the `plan` `StudyEvent`, which lists every participant |
1669
1683
  | `resolveSeatUrl` | `parseStudy`, which refuses a participant entry that is not same-origin loopback; build the URL with `new URL(entry, serveUrl)` |
1670
1684
  | `cuaLaneValidationReason`, `sharedWorldValidationReason`, `concurrentSharedWorldValidationReason`, `externalPublicSharedWorldValidationReason` | `parseStudy`, which returns the same reason |
1671
- | `MAX_CUA_LANES` | none; `parseStudy` refuses a roster of more than 16 participants |
1685
+ | `MAX_CUA_LANES` | none; `parseStudy` refuses a roster of more than 100 participants |
1672
1686
 
1673
1687
  ## Product-Adapter Extension Seam
1674
1688
 
@@ -27,8 +27,8 @@ The default provider is `openai`; its default model is `gpt-6-astra`, with high
27
27
  captures to OpenAI, without tools, redirects, provider-side response storage, or
28
28
  automatic retries. `--question` adds a reviewer question; it never changes the
29
29
  participant assignment. For OpenAI API analysis, `--max-cost` is required, including for dry-run
30
- admission. Admission compares the expected cost plus a 10% margin with it; the
31
- provider bill is not limited by it.
30
+ admission. Admission refuses the request when both its worst case and its expected cost plus a
31
+ 10% margin are over it; the provider bill is not limited by it.
32
32
  `--timeout-ms` and `--max-output-tokens` bound the request. A bill above the
33
33
  worst case or the cap retains valid findings and usage but returns a partial
34
34
  result and a nonzero command exit, including when that version is reused.
@@ -96,7 +96,7 @@ silently drifting from one is not.
96
96
  | Loopback entry URLs | Public-safety: never drive third-party sites unbidden | `policies.allowPublicTargets` for an owner-declared deployment/preview (a Vercel preview of your own app). Multi-participant public/preview fan-out needs explicit `participants[].target` for every participant, so the adapter-owned topology is declared rather than inferred. Provisioned clone subjects always serve in-sandbox on loopback |
97
97
  | Full-fidelity screenshots, local | The common case is watching a run of your own app locally; blur destroys the deliverable. Raw frames live in gitignored `.humanish/` (this repo's CI adds a binary-asset commit scan; downstream projects rely on the scaffolded `.gitignore` and their own review) | `policies.redactScreenshots: true` blurs at capture; `export --format bundle --redact-screenshots` produces a separate verified copy of a completed run |
98
98
  | Synthetic, seeded state | Pinned provenance; no real user data in evidence paths | Declared external state, recorded as unpinned in provenance |
99
- | Single participant | Cost + evidence simplicity | Declared fan-out where the route supports it: `participants: N` (homogeneous), an explicit `participants` list (differentiated persona/device/instruction), or list entries with a `count` that expand into one entry per participant on the computer-use E2B route (separate worlds, cap 16; `execution.concurrency` bounds concurrent paid participants) |
99
+ | Single participant | Cost + evidence simplicity | Declared fan-out where the route supports it: `participants: N` (homogeneous), an explicit `participants` list (differentiated persona/device/instruction), or list entries with a `count` that expand into one entry per participant on the computer-use E2B route (separate worlds, at most 100; `execution.concurrency` and the E2B plan bound concurrent paid participants) |
100
100
  | Stock `desktop` template | The stock E2B desktop image is right for most subjects; absent `execution.desktop.template` keeps `Sandbox.create(opts)` byte-stable | `execution.desktop.template` names a custom E2B desktop image (any name/id, no allowlist) for a subject needing baked-in runtimes the stock image lacks (e.g. node/bun/a local Postgres); it is threaded to `Sandbox.create(template, opts)` on every desktop-creating route and recorded in the bundle as `desktopTemplate` (public-safe) |
101
101
  | Desktop default URL opener | Preserve the route/image's historical browser/default opener behavior when unset | `execution.desktop.browser` set to `chrome`, `chromium` or `firefox` makes the hosted CUA/shared-world browser choice explicit, fail-closed, and recorded as `desktopBrowser` in the run bundle |
102
102
  | Loopback-only observer serving | Run bundles are local by default (public-safety plank 3); the serve/watch surfaces bind `127.0.0.1` unconditionally, and exposing them is publishing-adjacent, so exposure is fail-closed and carries declared friction | `humanish observe --all --expose` behind tunnel-edge auth (ngrok `--oauth google` with `--allow-email`/`--allow-domain` allow rules, or an operator `--public-url` you secure), or `--expose --safe` (open, gated on verify-`share_ready` runs only). `watch --expose` streams a live run behind edge auth (a live run is never `share_ready`, so `--safe` alone is refused). humanish carries no in-process auth; both print computed warnings naming exactly what became reachable |
@@ -23,12 +23,13 @@ review:
23
23
  Omitting `review.analysis` uses these defaults. Set `review.analysis: false` to
24
24
  run participants without the additional analysis request. An explicit analysis
25
25
  mapping using the default OpenAI API provider requires `maxCostUsd`. Admission
26
- compares the expected cost plus a 10% margin with it; it does not limit the
27
- provider's final bill, and it is separate from participant spending limits. The
26
+ refuses the request when both its worst case and its expected cost plus a 10% margin are over
27
+ it; it does not limit the provider's final bill, and it is separate from participant spending limits. The
28
28
  estimate gives two numbers: the expected cost, and the worst case if the analyst
29
29
  spends its whole output allowance. `humanish study check` gives the expected cost
30
30
  range for the study's participant count, from a run that keeps no evidence to one
31
- at the evidence limits. Use `analyze --dry-run --max-cost <usd>` on retained
31
+ that keeps the most evidence admission admits under the cap, at most the evidence limits. When
32
+ the cap admits no analysis at all, it says so instead. Use `analyze --dry-run --max-cost <usd>` on retained
32
33
  evidence to inspect the expected cost, the worst case and the selected token
33
34
  allowance before deliberately choosing a larger budget. Explicit
34
35
  `maxOutputTokens` and `--max-output-tokens` limits are honored exactly. The output allowance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.116.0",
3
+ "version": "0.118.0",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",
@@ -91,7 +91,7 @@
91
91
  "comms:agentmail:contract": "node --import tsx scripts/agentmail-contract-check.mjs",
92
92
  "format": "oxfmt",
93
93
  "format:check": "oxfmt --check",
94
- "lint": "node scripts/check-lint-cap.mjs --max-warnings=436",
94
+ "lint": "node scripts/check-lint-cap.mjs --max-warnings=426",
95
95
  "knip": "knip",
96
96
  "prose:check": "node scripts/check-code-prose.mjs",
97
97
  "vocabulary:check": "tsx scripts/check-retired-words.ts",