mimetic-cli 0.1.3 → 0.1.5

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 (50) hide show
  1. package/README.md +67 -12
  2. package/dist/env-file.d.ts +14 -0
  3. package/dist/env-file.js +108 -0
  4. package/dist/env-file.js.map +1 -0
  5. package/dist/feedback.d.ts +7 -5
  6. package/dist/feedback.js +61 -4
  7. package/dist/feedback.js.map +1 -1
  8. package/dist/init-templates.js +29 -0
  9. package/dist/init-templates.js.map +1 -1
  10. package/dist/lab-app-runner.d.ts +78 -0
  11. package/dist/lab-app-runner.js +403 -0
  12. package/dist/lab-app-runner.js.map +1 -0
  13. package/dist/labs.d.ts +67 -0
  14. package/dist/labs.js +257 -0
  15. package/dist/labs.js.map +1 -0
  16. package/dist/observer-assets.js +473 -25
  17. package/dist/observer-assets.js.map +1 -1
  18. package/dist/observer.d.ts +6 -0
  19. package/dist/observer.js +49 -8
  20. package/dist/observer.js.map +1 -1
  21. package/dist/oss-lab.d.ts +1 -1
  22. package/dist/oss-lab.js +6 -6
  23. package/dist/oss-lab.js.map +1 -1
  24. package/dist/oss-meta-lab.d.ts +113 -1
  25. package/dist/oss-meta-lab.js +2753 -200
  26. package/dist/oss-meta-lab.js.map +1 -1
  27. package/dist/oss-remote-telemetry.d.ts +77 -0
  28. package/dist/oss-remote-telemetry.js +393 -0
  29. package/dist/oss-remote-telemetry.js.map +1 -0
  30. package/dist/program.d.ts +8 -0
  31. package/dist/program.js +668 -70
  32. package/dist/program.js.map +1 -1
  33. package/dist/run.d.ts +105 -3
  34. package/dist/run.js +684 -22
  35. package/dist/run.js.map +1 -1
  36. package/docs/architecture/local-codex-tui-actor.md +9 -6
  37. package/docs/architecture/oss-lab-poc.md +119 -47
  38. package/docs/architecture/project-layout.md +40 -6
  39. package/docs/assets/mimetic-oss-lab-observer.png +0 -0
  40. package/docs/contracts/feedback.md +15 -12
  41. package/docs/contracts/policy.md +9 -2
  42. package/docs/contracts/run-bundle.md +62 -0
  43. package/docs/contracts/schemas.md +21 -0
  44. package/docs/goals/current.md +50 -17
  45. package/docs/product/open-source-install-experience.md +63 -8
  46. package/docs/ramp/README.md +26 -8
  47. package/docs/roadmap/world-class-open-source-v0.md +41 -20
  48. package/package.json +9 -6
  49. package/skills/mimetic-cli/SKILL.md +89 -4
  50. package/skills/mimetic-cli/agents/openai.yaml +1 -1
package/dist/run.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { promisify } from "node:util";
6
7
  import { buildObserverData } from "./observer-data.js";
7
8
  export const RUN_BUNDLE_SCHEMA = "mimetic.run-bundle.v1";
8
9
  export const REVIEW_SCHEMA = "mimetic.review.v1";
@@ -10,13 +11,18 @@ export const VERIFY_SCHEMA = "mimetic.verify-result.v1";
10
11
  export const RUNS_SCHEMA = "mimetic.runs-result.v1";
11
12
  export const DOCTOR_SCHEMA = "mimetic.doctor-result.v1";
12
13
  const sensitivePatterns = [
13
- /sk-[a-z0-9_-]{12,}/i,
14
- /gho_[a-z0-9_]{12,}/i,
14
+ /sk-[a-z0-9_-]{20,}/i,
15
+ /e2b_[a-z0-9_-]{12,}/i,
16
+ /gh[pousr]_[a-z0-9_]{12,}/i,
17
+ /https?:\/\/[^/\s]*e2b[^)\s]+/i,
15
18
  /BEGIN (RSA|OPENSSH|PRIVATE) KEY/i
16
19
  ];
17
20
  const LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS = 240_000;
18
21
  const LOCAL_CODEX_TUI_MAX_TIMEOUT_MS = 600_000;
19
22
  const LOCAL_ACTOR_TRANSCRIPT_MAX_CHARS = 80_000;
23
+ const LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY = 4;
24
+ const BROWSER_APP_DEFAULT_TIMEOUT_MS = 60_000;
25
+ const execFileAsync = promisify(execFile);
20
26
  const builtinPersona = {
21
27
  id: "builtin-synthetic-new-user",
22
28
  name: "Built-in Synthetic New User",
@@ -55,7 +61,7 @@ export async function runDryRun(options) {
55
61
  }
56
62
  };
57
63
  }
58
- const simCount = normalizeSimCount(options.simCount);
64
+ const simCount = normalizeSimCount(options.appUrl ? options.simCount ?? 2 : options.simCount);
59
65
  if (simCount === null) {
60
66
  return {
61
67
  schema: "mimetic.run-result.v1",
@@ -64,10 +70,37 @@ export async function runDryRun(options) {
64
70
  warnings,
65
71
  error: {
66
72
  code: "MIMETIC_INVALID_SIM_COUNT",
67
- message: "--sims must be an integer between 1 and 64."
73
+ message: "--sims must be a positive integer."
68
74
  }
69
75
  };
70
76
  }
77
+ if (options.appUrl !== undefined) {
78
+ if (options.dryRun) {
79
+ return {
80
+ schema: "mimetic.run-result.v1",
81
+ ok: false,
82
+ cwd,
83
+ warnings,
84
+ error: {
85
+ code: "MIMETIC_APP_URL_OPTION_CONFLICT",
86
+ message: "Use --app-url for a live browser app proof; remove --dry-run."
87
+ }
88
+ };
89
+ }
90
+ if (simCount > 2) {
91
+ return {
92
+ schema: "mimetic.run-result.v1",
93
+ ok: false,
94
+ cwd,
95
+ warnings,
96
+ error: {
97
+ code: "MIMETIC_INVALID_SIM_COUNT",
98
+ message: "--sims must be 1 or 2 when --app-url is used."
99
+ }
100
+ };
101
+ }
102
+ return runBrowserAppProof({ ...options, appUrl: options.appUrl, cwd, simCount });
103
+ }
71
104
  if (!options.dryRun) {
72
105
  const actor = resolveRequestedLocalCodexActor(options.actor);
73
106
  if (actor === "codex-tui") {
@@ -187,6 +220,498 @@ export async function runDryRun(options) {
187
220
  warnings
188
221
  };
189
222
  }
223
+ const browserSurfaces = [
224
+ {
225
+ id: "desktop",
226
+ label: "Desktop browser surface",
227
+ viewport: {
228
+ width: 1440,
229
+ height: 960,
230
+ deviceScaleFactor: 1,
231
+ isMobile: false
232
+ }
233
+ },
234
+ {
235
+ id: "mobile",
236
+ label: "Mobile browser surface",
237
+ viewport: {
238
+ width: 390,
239
+ height: 844,
240
+ deviceScaleFactor: 2,
241
+ isMobile: true
242
+ }
243
+ }
244
+ ];
245
+ async function runBrowserAppProof(options) {
246
+ const warnings = [];
247
+ const appUrl = normalizeLocalAppUrl(options.appUrl);
248
+ if (!appUrl) {
249
+ return {
250
+ schema: "mimetic.run-result.v1",
251
+ ok: false,
252
+ cwd: options.cwd,
253
+ warnings,
254
+ error: {
255
+ code: "MIMETIC_INVALID_APP_URL",
256
+ message: "--app-url must be an http(s) loopback URL such as http://127.0.0.1:5173."
257
+ }
258
+ };
259
+ }
260
+ const browserCommand = await resolveBrowserCommand();
261
+ if (!browserCommand) {
262
+ return {
263
+ schema: "mimetic.run-result.v1",
264
+ ok: false,
265
+ cwd: options.cwd,
266
+ warnings,
267
+ error: {
268
+ code: "MIMETIC_BROWSER_APP_CAPTURE_FAILED",
269
+ message: "No Chrome/Chromium browser command was found. Set MIMETIC_BROWSER_COMMAND to a browser binary that supports --headless and --screenshot."
270
+ }
271
+ };
272
+ }
273
+ const now = new Date();
274
+ const createdAt = now.toISOString();
275
+ const runId = options.runId ?? `browser-${createdAt.replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
276
+ const artifactRoot = path.join(".mimetic", "runs", runId);
277
+ const absoluteArtifactRoot = path.join(options.cwd, artifactRoot);
278
+ const packageName = await readPackageName(options.cwd);
279
+ const mimeticSource = await directoryExists(path.join(options.cwd, "mimetic")) ? "present" : "missing";
280
+ const selection = await loadDryRunSelection(options.cwd, mimeticSource);
281
+ if (mimeticSource === "missing") {
282
+ warnings.push("Committed mimetic/ source was not found; using built-in synthetic browser-app defaults.");
283
+ }
284
+ warnings.push(...selection.warnings);
285
+ await mkdir(path.join(absoluteArtifactRoot, "screenshots"), { recursive: true });
286
+ await mkdir(path.join(absoluteArtifactRoot, "traces"), { recursive: true });
287
+ const surfaces = browserSurfaces.slice(0, options.simCount);
288
+ const captures = await Promise.all(surfaces.map((surface) => captureBrowserSurface({
289
+ absoluteArtifactRoot,
290
+ appUrl,
291
+ browserCommand,
292
+ surface,
293
+ timeoutMs: options.timeoutMs ?? BROWSER_APP_DEFAULT_TIMEOUT_MS
294
+ })));
295
+ const completedAt = new Date().toISOString();
296
+ const events = buildBrowserAppEvents({ appUrl, captures, createdAt });
297
+ const allPassed = captures.every((capture) => capture.ok);
298
+ const review = createBrowserAppReviewSummary({ appUrl, captures });
299
+ const bundle = {
300
+ schema: RUN_BUNDLE_SCHEMA,
301
+ runId,
302
+ mode: "live",
303
+ simCount: captures.length,
304
+ createdAt,
305
+ cwd: options.cwd,
306
+ artifactRoot,
307
+ source: {
308
+ packageName,
309
+ mimeticSource,
310
+ git: {
311
+ status: "not_captured",
312
+ note: "Browser app proof records local app evidence only; host git state capture is a later primitive."
313
+ }
314
+ },
315
+ persona: {
316
+ id: selection.persona.id,
317
+ name: selection.persona.name,
318
+ source: selection.persona.source,
319
+ sourceDigest: selection.persona.sourceDigest
320
+ },
321
+ scenario: {
322
+ id: "browser-app-surface-smoke",
323
+ title: "Browser App Surface Smoke",
324
+ goal: "Capture desktop and mobile browser evidence against a running local app URL.",
325
+ source: "builtin:browser-app-surface-smoke",
326
+ sourceDigest: digestText(appUrl)
327
+ },
328
+ lifecycle: [
329
+ {
330
+ at: createdAt,
331
+ event: "run.created",
332
+ message: `Live browser app proof created for ${appUrl}.`
333
+ },
334
+ {
335
+ at: createdAt,
336
+ event: "app.url.accepted",
337
+ message: "Accepted public-safe loopback app URL for browser surface capture."
338
+ },
339
+ {
340
+ at: completedAt,
341
+ event: "review.created",
342
+ message: allPassed
343
+ ? "Created review from desktop/mobile browser screenshot evidence."
344
+ : "Created review with missing or blocked browser screenshot evidence."
345
+ }
346
+ ],
347
+ simulations: captures.map((capture, index) => {
348
+ const simId = `browser-${capture.surface.id}`;
349
+ const streamId = `${simId}-stream`;
350
+ return {
351
+ id: simId,
352
+ index: index + 1,
353
+ personaId: selection.persona.id,
354
+ scenarioId: "browser-app-surface-smoke",
355
+ status: capture.ok ? "passed" : "blocked",
356
+ streamKind: "browser",
357
+ mode: "browser-sim",
358
+ progress: 100,
359
+ currentStep: capture.ok
360
+ ? `${capture.surface.label} captured`
361
+ : `${capture.surface.label} blocked`,
362
+ summary: capture.reason,
363
+ streamIds: [streamId],
364
+ startedAt: createdAt,
365
+ updatedAt: capture.capturedAt
366
+ };
367
+ }),
368
+ streams: captures.map((capture) => {
369
+ const simId = `browser-${capture.surface.id}`;
370
+ const streamId = `${simId}-stream`;
371
+ const screenshotUrl = `../${capture.screenshotPath}`;
372
+ return {
373
+ id: streamId,
374
+ simId,
375
+ kind: "browser",
376
+ label: capture.surface.label,
377
+ status: capture.ok ? "passed" : "blocked",
378
+ transport: "snapshot",
379
+ updatedAt: capture.capturedAt,
380
+ embed: {
381
+ kind: "screenshot",
382
+ url: screenshotUrl,
383
+ title: capture.surface.label
384
+ },
385
+ viewport: capture.surface.viewport,
386
+ ui: {
387
+ appStatus: capture.ok ? "running" : "blocked",
388
+ appUrl,
389
+ route: appUrl,
390
+ intent: "Verify that the running app renders in a real browser surface for this viewport.",
391
+ screenshotUrl,
392
+ state: capture.reason,
393
+ visualStatus: capture.ok ? "visible" : "blocked"
394
+ },
395
+ completion: {
396
+ checkedAt: capture.capturedAt,
397
+ exitCode: capture.ok ? 0 : 1,
398
+ reason: capture.reason,
399
+ status: capture.ok ? "passed" : "blocked"
400
+ },
401
+ artifacts: [
402
+ { label: "run bundle", path: "run.json", kind: "bundle" },
403
+ { label: "review", path: "review.md", kind: "review" },
404
+ { label: "event log", path: "events.ndjson", kind: "events" },
405
+ { label: `${capture.surface.id} screenshot`, path: capture.screenshotPath, kind: "screenshot" },
406
+ { label: `${capture.surface.id} browser trace`, path: capture.tracePath, kind: "trace" }
407
+ ]
408
+ };
409
+ }),
410
+ events,
411
+ redaction: {
412
+ status: "passed",
413
+ notes: "Browser app proof stores loopback app URLs, screenshots, and generated traces only; secret-like text is rejected by verify."
414
+ },
415
+ artifacts: {
416
+ run: "run.json",
417
+ reviewJson: "review.json",
418
+ reviewMarkdown: "review.md",
419
+ observerData: "observer/observer-data.json",
420
+ events: "events.ndjson"
421
+ },
422
+ review,
423
+ feedbackCandidates: []
424
+ };
425
+ await writeRunBundleArtifacts(absoluteArtifactRoot, bundle);
426
+ await writeJson(path.join(options.cwd, ".mimetic", "runs", "latest.json"), {
427
+ schema: "mimetic.latest-run.v1",
428
+ runId,
429
+ path: artifactRoot,
430
+ updatedAt: completedAt
431
+ });
432
+ return {
433
+ schema: "mimetic.run-result.v1",
434
+ ok: allPassed,
435
+ runId,
436
+ mode: "live",
437
+ simCount: captures.length,
438
+ cwd: options.cwd,
439
+ artifactRoot,
440
+ bundlePath: path.join(artifactRoot, "run.json"),
441
+ reviewPath: path.join(artifactRoot, "review.md"),
442
+ latestPath: path.join(".mimetic", "runs", "latest.json"),
443
+ warnings,
444
+ ...(allPassed
445
+ ? {}
446
+ : {
447
+ error: {
448
+ code: "MIMETIC_BROWSER_APP_CAPTURE_FAILED",
449
+ message: review.summary
450
+ }
451
+ })
452
+ };
453
+ }
454
+ async function captureBrowserSurface(args) {
455
+ const started = Date.now();
456
+ const screenshotPath = path.join("screenshots", `${args.surface.id}.png`);
457
+ const tracePath = path.join("traces", `${args.surface.id}.json`);
458
+ const absoluteScreenshotPath = path.join(args.absoluteArtifactRoot, screenshotPath);
459
+ const absoluteTracePath = path.join(args.absoluteArtifactRoot, tracePath);
460
+ const httpProbe = await probeAppUrl(args.appUrl, Math.min(args.timeoutMs, 15_000));
461
+ const capturedAt = new Date().toISOString();
462
+ const profileDir = await mkdtemp(path.join(os.tmpdir(), "mimetic-browser-profile-"));
463
+ try {
464
+ await captureScreenshotWithBrowser({
465
+ args: [
466
+ "--headless=new",
467
+ "--disable-background-networking",
468
+ "--disable-default-apps",
469
+ "--disable-extensions",
470
+ "--disable-gpu",
471
+ "--disable-dev-shm-usage",
472
+ "--no-first-run",
473
+ "--no-default-browser-check",
474
+ "--hide-scrollbars",
475
+ `--user-data-dir=${profileDir}`,
476
+ `--window-size=${args.surface.viewport.width},${args.surface.viewport.height}`,
477
+ `--force-device-scale-factor=${args.surface.viewport.deviceScaleFactor}`,
478
+ `--screenshot=${absoluteScreenshotPath}`,
479
+ args.appUrl
480
+ ],
481
+ browserCommand: args.browserCommand,
482
+ screenshotPath: absoluteScreenshotPath,
483
+ timeoutMs: args.timeoutMs
484
+ });
485
+ }
486
+ catch (error) {
487
+ const reason = `Browser screenshot command failed for ${args.surface.id}: ${compactBrowserError(error)}`;
488
+ await writeJson(absoluteTracePath, buildBrowserTrace({
489
+ appUrl: args.appUrl,
490
+ browserCommand: path.basename(args.browserCommand),
491
+ capturedAt,
492
+ durationMs: Date.now() - started,
493
+ ...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
494
+ ok: false,
495
+ reason,
496
+ screenshotPath,
497
+ surface: args.surface
498
+ }));
499
+ return {
500
+ capturedAt,
501
+ durationMs: Date.now() - started,
502
+ ...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
503
+ ok: false,
504
+ reason,
505
+ screenshotPath,
506
+ surface: args.surface,
507
+ tracePath
508
+ };
509
+ }
510
+ finally {
511
+ await rm(profileDir, { force: true, recursive: true }).catch(() => undefined);
512
+ }
513
+ const screenshotStats = await stat(absoluteScreenshotPath).catch(() => null);
514
+ const ok = Boolean(screenshotStats?.isFile() && screenshotStats.size > 0 && httpProbe.ok);
515
+ const reason = ok
516
+ ? `${args.surface.label} screenshot captured from ${args.appUrl}${httpProbe.status === undefined ? "" : ` with HTTP ${httpProbe.status}`}.`
517
+ : screenshotStats?.isFile() && screenshotStats.size > 0
518
+ ? `${args.surface.label} screenshot exists, but app HTTP readiness was not proven: ${httpProbe.reason}.`
519
+ : `${args.surface.label} screenshot artifact was missing or empty.`;
520
+ const completedAt = new Date().toISOString();
521
+ const durationMs = Date.now() - started;
522
+ await writeJson(absoluteTracePath, buildBrowserTrace({
523
+ appUrl: args.appUrl,
524
+ browserCommand: path.basename(args.browserCommand),
525
+ capturedAt: completedAt,
526
+ durationMs,
527
+ ...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
528
+ ok,
529
+ reason,
530
+ screenshotPath,
531
+ surface: args.surface
532
+ }));
533
+ return {
534
+ capturedAt: completedAt,
535
+ durationMs,
536
+ ...(httpProbe.status === undefined ? {} : { httpStatus: httpProbe.status }),
537
+ ok,
538
+ reason,
539
+ screenshotPath,
540
+ surface: args.surface,
541
+ tracePath
542
+ };
543
+ }
544
+ function buildBrowserTrace(args) {
545
+ return {
546
+ schema: "mimetic.browser-surface-trace.v1",
547
+ capturedAt: args.capturedAt,
548
+ appUrl: args.appUrl,
549
+ browserCommand: args.browserCommand,
550
+ durationMs: args.durationMs,
551
+ ...(args.httpStatus === undefined ? {} : { httpStatus: args.httpStatus }),
552
+ ok: args.ok,
553
+ reason: args.reason,
554
+ screenshotPath: args.screenshotPath,
555
+ surface: args.surface,
556
+ redaction: "passed"
557
+ };
558
+ }
559
+ async function captureScreenshotWithBrowser(args) {
560
+ const child = spawn(args.browserCommand, args.args, {
561
+ detached: true,
562
+ stdio: "ignore"
563
+ });
564
+ let exitCode = null;
565
+ let signal = null;
566
+ child.once("exit", (code, childSignal) => {
567
+ exitCode = code;
568
+ signal = childSignal;
569
+ });
570
+ child.unref();
571
+ const deadline = Date.now() + args.timeoutMs;
572
+ while (Date.now() <= deadline) {
573
+ const stats = await stat(args.screenshotPath).catch(() => null);
574
+ if (stats?.isFile() && stats.size > 0) {
575
+ terminateProcessGroup(child.pid);
576
+ return;
577
+ }
578
+ if (exitCode !== null || signal !== null) {
579
+ break;
580
+ }
581
+ await wait(250);
582
+ }
583
+ terminateProcessGroup(child.pid, true);
584
+ const stats = await stat(args.screenshotPath).catch(() => null);
585
+ if (stats?.isFile() && stats.size > 0) {
586
+ return;
587
+ }
588
+ throw new Error(exitCode !== null || signal !== null
589
+ ? `browser exited before screenshot was written (exit=${exitCode ?? "null"} signal=${signal ?? "null"})`
590
+ : `timed out after ${args.timeoutMs}ms waiting for screenshot`);
591
+ }
592
+ function terminateProcessGroup(pid, force = false) {
593
+ if (!pid) {
594
+ return;
595
+ }
596
+ try {
597
+ process.kill(-pid, force ? "SIGKILL" : "SIGTERM");
598
+ return;
599
+ }
600
+ catch { }
601
+ try {
602
+ process.kill(pid, force ? "SIGKILL" : "SIGTERM");
603
+ }
604
+ catch { }
605
+ }
606
+ async function wait(ms) {
607
+ await new Promise((resolve) => setTimeout(resolve, ms));
608
+ }
609
+ function buildBrowserAppEvents(args) {
610
+ const events = [
611
+ {
612
+ id: "event-001",
613
+ at: args.createdAt,
614
+ level: "info",
615
+ type: "browser-app.run.created",
616
+ message: "Created live browser app proof run against a loopback URL."
617
+ }
618
+ ];
619
+ args.captures.forEach((capture) => {
620
+ events.push({
621
+ id: `event-${String(events.length + 1).padStart(3, "0")}`,
622
+ at: capture.capturedAt,
623
+ level: capture.ok ? "info" : "warn",
624
+ type: capture.ok ? "browser-app.screenshot.captured" : "browser-app.screenshot.blocked",
625
+ message: `${capture.surface.id}: ${capture.reason}`,
626
+ simId: `browser-${capture.surface.id}`,
627
+ streamId: `browser-${capture.surface.id}-stream`
628
+ });
629
+ });
630
+ return events;
631
+ }
632
+ function createBrowserAppReviewSummary(args) {
633
+ const passed = args.captures.filter((capture) => capture.ok).length;
634
+ const allPassed = passed === args.captures.length;
635
+ return {
636
+ schema: REVIEW_SCHEMA,
637
+ verdict: allPassed ? "pass" : "blocked",
638
+ summary: allPassed
639
+ ? `Captured ${passed}/${args.captures.length} live browser app surface${args.captures.length === 1 ? "" : "s"} from ${args.appUrl}.`
640
+ : `Captured ${passed}/${args.captures.length} live browser app surfaces from ${args.appUrl}; at least one required surface was blocked.`,
641
+ gaps: [
642
+ "This browser app proof captures render evidence and HTTP readiness; it does not yet prove autonomous persona navigation through multi-step product flows.",
643
+ "Only loopback app URLs are accepted so generated bundles do not preserve private external targets.",
644
+ ...args.captures
645
+ .filter((capture) => !capture.ok)
646
+ .map((capture) => `${capture.surface.id}: ${capture.reason}`)
647
+ ]
648
+ };
649
+ }
650
+ async function probeAppUrl(appUrl, timeoutMs) {
651
+ const controller = new AbortController();
652
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
653
+ try {
654
+ const response = await fetch(appUrl, {
655
+ signal: controller.signal
656
+ });
657
+ return {
658
+ ok: response.status < 500,
659
+ reason: `HTTP ${response.status}`,
660
+ status: response.status
661
+ };
662
+ }
663
+ catch (error) {
664
+ return {
665
+ ok: false,
666
+ reason: compactBrowserError(error)
667
+ };
668
+ }
669
+ finally {
670
+ clearTimeout(timer);
671
+ }
672
+ }
673
+ async function resolveBrowserCommand() {
674
+ const candidates = [
675
+ process.env.MIMETIC_BROWSER_COMMAND,
676
+ "google-chrome",
677
+ "chromium",
678
+ "chromium-browser",
679
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
680
+ ].filter((candidate) => Boolean(candidate?.trim()));
681
+ for (const candidate of candidates) {
682
+ try {
683
+ await execFileAsync(candidate, ["--version"], {
684
+ timeout: 5_000,
685
+ maxBuffer: 256 * 1024
686
+ });
687
+ return candidate;
688
+ }
689
+ catch { }
690
+ }
691
+ return null;
692
+ }
693
+ function normalizeLocalAppUrl(value) {
694
+ try {
695
+ const url = new URL(value);
696
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
697
+ return null;
698
+ }
699
+ if (!["127.0.0.1", "localhost", "::1"].includes(url.hostname)) {
700
+ return null;
701
+ }
702
+ url.hash = "";
703
+ return url.toString();
704
+ }
705
+ catch {
706
+ return null;
707
+ }
708
+ }
709
+ function compactBrowserError(error) {
710
+ if (error instanceof Error) {
711
+ return redactSensitiveText(error.message).replace(/\s+/g, " ").slice(0, 240);
712
+ }
713
+ return redactSensitiveText(String(error)).replace(/\s+/g, " ").slice(0, 240);
714
+ }
190
715
  function isLocalCodexActor(value) {
191
716
  return value === "codex-tui" || value === "codex-exec";
192
717
  }
@@ -212,7 +737,7 @@ async function runLocalCodexTui(options) {
212
737
  warnings,
213
738
  error: {
214
739
  code: "MIMETIC_ACTOR_FANOUT_UNIMPLEMENTED",
215
- message: "Local Codex TUI actor support is intentionally limited to --sims 1 in this slice. Split 4x fanout after the 1x lifecycle is deterministic."
740
+ message: "Local Codex TUI actor support is currently single-lane because it owns one PTY/UI session. Use codex-exec for bounded-concurrency fanout."
216
741
  }
217
742
  };
218
743
  }
@@ -638,28 +1163,29 @@ function buildLocalCodexExecBundle(args) {
638
1163
  }
639
1164
  async function runLocalCodexExec(options) {
640
1165
  const warnings = [];
641
- if (options.simCount > 4) {
1166
+ const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("MIMETIC_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
1167
+ if (timeoutMs === null) {
642
1168
  return {
643
1169
  schema: "mimetic.run-result.v1",
644
1170
  ok: false,
645
1171
  cwd: options.cwd,
646
1172
  warnings,
647
1173
  error: {
648
- code: "MIMETIC_ACTOR_FANOUT_UNIMPLEMENTED",
649
- message: "Local Codex exec actor fanout is intentionally limited to --sims 4 in this slice."
1174
+ code: "MIMETIC_INVALID_TIMEOUT",
1175
+ message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
650
1176
  }
651
1177
  };
652
1178
  }
653
- const timeoutMs = normalizeActorTimeout(options.timeoutMs ?? readEnvInteger("MIMETIC_CODEX_ACTOR_TIMEOUT_MS") ?? LOCAL_CODEX_TUI_DEFAULT_TIMEOUT_MS);
654
- if (timeoutMs === null) {
1179
+ const maxConcurrency = normalizePositiveInteger(readEnvInteger("MIMETIC_LOCAL_CODEX_EXEC_MAX_CONCURRENCY") ?? LOCAL_CODEX_EXEC_DEFAULT_MAX_CONCURRENCY);
1180
+ if (maxConcurrency === null) {
655
1181
  return {
656
1182
  schema: "mimetic.run-result.v1",
657
1183
  ok: false,
658
1184
  cwd: options.cwd,
659
1185
  warnings,
660
1186
  error: {
661
- code: "MIMETIC_INVALID_TIMEOUT",
662
- message: `--timeout-ms must be an integer between 1 and ${LOCAL_CODEX_TUI_MAX_TIMEOUT_MS}.`
1187
+ code: "MIMETIC_INVALID_ACTOR_CONCURRENCY",
1188
+ message: "MIMETIC_LOCAL_CODEX_EXEC_MAX_CONCURRENCY must be a positive integer."
663
1189
  }
664
1190
  };
665
1191
  }
@@ -720,7 +1246,7 @@ async function runLocalCodexExec(options) {
720
1246
  {
721
1247
  at: createdAt,
722
1248
  event: "run.created",
723
- message: `Live local Codex exec run created with ${options.simCount} explicit opt-in actor${options.simCount === 1 ? "" : "s"}.`
1249
+ message: `Live local Codex exec run created with ${options.simCount} explicit opt-in actor${options.simCount === 1 ? "" : "s"} and max concurrency ${maxConcurrency}.`
724
1250
  },
725
1251
  {
726
1252
  at: createdAt,
@@ -744,7 +1270,7 @@ async function runLocalCodexExec(options) {
744
1270
  {
745
1271
  at: runningAt,
746
1272
  event: "actor.running",
747
- message: "Local Codex exec actor lanes are running; Observer data will refresh as sanitized evidence arrives."
1273
+ message: `Local Codex exec actor lanes are running with max concurrency ${maxConcurrency}; Observer data will refresh as sanitized evidence arrives.`
748
1274
  }
749
1275
  ],
750
1276
  lanes: lanes.map((lane) => ({
@@ -767,7 +1293,7 @@ async function runLocalCodexExec(options) {
767
1293
  review: createLocalActorRunningReviewSummary(options.simCount === 1 ? "Codex exec" : "Codex exec fanout")
768
1294
  });
769
1295
  await writeRunBundleArtifacts(absoluteArtifactRoot, runningBundle);
770
- const laneResults = await Promise.all(lanes.map(async (lane) => {
1296
+ const laneResults = await mapWithConcurrency(lanes, maxConcurrency, async (lane) => {
771
1297
  const actor = await executeLocalActorCommand(lane.command, {
772
1298
  cwd: options.cwd,
773
1299
  timeoutMs
@@ -807,7 +1333,7 @@ async function runLocalCodexExec(options) {
807
1333
  tracePath,
808
1334
  transcriptPath
809
1335
  };
810
- }));
1336
+ });
811
1337
  const completedAt = new Date().toISOString();
812
1338
  const laneStatuses = laneResults.map((result) => result.actor.status);
813
1339
  const status = aggregateActorStatus(laneStatuses);
@@ -1336,7 +1862,13 @@ function isSamePath(candidatePath, targetPath) {
1336
1862
  return candidate === target;
1337
1863
  }
1338
1864
  function normalizeActorTimeout(value) {
1339
- if (!Number.isInteger(value) || value === undefined || value < 1 || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
1865
+ if (normalizePositiveInteger(value) === null || value === undefined || value > LOCAL_CODEX_TUI_MAX_TIMEOUT_MS) {
1866
+ return null;
1867
+ }
1868
+ return value;
1869
+ }
1870
+ function normalizePositiveInteger(value) {
1871
+ if (!Number.isInteger(value) || value === undefined || value < 1) {
1340
1872
  return null;
1341
1873
  }
1342
1874
  return value;
@@ -1348,6 +1880,26 @@ function readEnvInteger(name) {
1348
1880
  }
1349
1881
  return /^\d+$/.test(value) ? Number.parseInt(value, 10) : Number.NaN;
1350
1882
  }
1883
+ async function mapWithConcurrency(items, concurrency, mapper) {
1884
+ const results = new Array(items.length);
1885
+ let nextIndex = 0;
1886
+ const workerCount = Math.min(concurrency, items.length);
1887
+ await Promise.all(Array.from({ length: workerCount }, async () => {
1888
+ while (true) {
1889
+ const index = nextIndex;
1890
+ nextIndex += 1;
1891
+ if (index >= items.length) {
1892
+ return;
1893
+ }
1894
+ const item = items[index];
1895
+ if (item === undefined) {
1896
+ return;
1897
+ }
1898
+ results[index] = await mapper(item, index);
1899
+ }
1900
+ }));
1901
+ return results;
1902
+ }
1351
1903
  function buildLocalCodexTuiPrompt(selection, verdictNonce) {
1352
1904
  return [
1353
1905
  "You are a Mimetic local Codex TUI dogfood actor.",
@@ -1511,7 +2063,7 @@ function normalizeSimCount(value) {
1511
2063
  if (value === undefined) {
1512
2064
  return 1;
1513
2065
  }
1514
- if (!Number.isInteger(value) || value < 1 || value > 64) {
2066
+ if (!Number.isSafeInteger(value) || value < 1) {
1515
2067
  return null;
1516
2068
  }
1517
2069
  return value;
@@ -1557,10 +2109,23 @@ export async function verifyRun(cwdInput, runInput) {
1557
2109
  ok: reviewJson !== null && reviewMarkdown !== null,
1558
2110
  message: "review.json and review.md must exist"
1559
2111
  });
2112
+ const publicSafetyFindings = await scanRunPublicSafetyArtifacts(resolved);
1560
2113
  checks.push({
1561
2114
  name: "public-safety scan",
1562
- ok: !containsSensitivePattern(JSON.stringify(bundle ?? {}) + (reviewMarkdown ?? "")),
1563
- message: "bundle and review text must not match known secret patterns"
2115
+ ok: publicSafetyFindings.length === 0,
2116
+ message: publicSafetyFindings.length === 0
2117
+ ? "run text artifacts and public-proof paths must not match known secret or browser-profile patterns"
2118
+ : `public-safety findings: ${publicSafetyFindings.slice(0, 5).join(", ")}`
2119
+ });
2120
+ const missingEvidenceArtifacts = isRunBundle(bundle)
2121
+ ? await missingLocalEvidenceArtifacts(resolved, bundle)
2122
+ : [];
2123
+ checks.push({
2124
+ name: "local evidence artifacts exist",
2125
+ ok: missingEvidenceArtifacts.length === 0,
2126
+ message: missingEvidenceArtifacts.length === 0
2127
+ ? "referenced local screenshot/trace/log/filesystem artifacts are present"
2128
+ : `missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`
1564
2129
  });
1565
2130
  const ok = checks.every((check) => check.ok);
1566
2131
  return {
@@ -1848,9 +2413,106 @@ async function writeRunBundleArtifacts(absoluteArtifactRoot, bundle) {
1848
2413
  await writeJson(path.join(absoluteArtifactRoot, "run.json"), bundle);
1849
2414
  await writeJson(path.join(absoluteArtifactRoot, "review.json"), bundle.review);
1850
2415
  await writeFile(path.join(absoluteArtifactRoot, "review.md"), renderReviewMarkdown(bundle), "utf8");
2416
+ await writeFile(path.join(absoluteArtifactRoot, "events.ndjson"), `${bundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
1851
2417
  await mkdir(path.join(absoluteArtifactRoot, "observer"), { recursive: true });
1852
2418
  await writeJson(path.join(absoluteArtifactRoot, "observer", "observer-data.json"), buildObserverData(bundle));
1853
2419
  }
2420
+ async function missingLocalEvidenceArtifacts(runRoot, bundle) {
2421
+ const requiredPaths = new Set();
2422
+ for (const stream of bundle.streams) {
2423
+ for (const artifact of stream.artifacts) {
2424
+ if ((artifact.kind === "screenshot" || artifact.kind === "trace" || artifact.kind === "log" || artifact.kind === "filesystem")
2425
+ && isLocalEvidenceArtifactPath(artifact.path)) {
2426
+ requiredPaths.add(artifact.path);
2427
+ }
2428
+ }
2429
+ const embedPath = normalizeLocalEvidenceReference(stream.embed?.kind === "screenshot" ? stream.embed.url : undefined);
2430
+ if (embedPath) {
2431
+ requiredPaths.add(embedPath);
2432
+ }
2433
+ const uiScreenshotPath = normalizeLocalEvidenceReference(stream.ui?.screenshotUrl);
2434
+ if (uiScreenshotPath) {
2435
+ requiredPaths.add(uiScreenshotPath);
2436
+ }
2437
+ }
2438
+ const missing = [];
2439
+ for (const artifactPath of requiredPaths) {
2440
+ const absolutePath = path.join(runRoot, artifactPath);
2441
+ const stats = await stat(absolutePath).catch(() => null);
2442
+ if (!stats?.isFile() || stats.size <= 0) {
2443
+ missing.push(artifactPath);
2444
+ }
2445
+ }
2446
+ return missing;
2447
+ }
2448
+ const riskyPublicArtifactPathSegments = new Set([
2449
+ ".git",
2450
+ "Cookies",
2451
+ "Login Data",
2452
+ "Local Storage",
2453
+ "Preferences",
2454
+ "Secure Preferences",
2455
+ "profiles"
2456
+ ]);
2457
+ async function scanRunPublicSafetyArtifacts(runRoot) {
2458
+ const findings = [];
2459
+ await scanRunPublicSafetyDirectory(runRoot, runRoot, findings);
2460
+ return findings;
2461
+ }
2462
+ async function scanRunPublicSafetyDirectory(root, current, findings) {
2463
+ if (findings.length >= 50) {
2464
+ return;
2465
+ }
2466
+ const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
2467
+ for (const entry of entries) {
2468
+ const absolutePath = path.join(current, entry.name);
2469
+ const relativePath = path.relative(root, absolutePath).replace(/\\/g, "/");
2470
+ if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
2471
+ findings.push(`risky artifact path ${relativePath}`);
2472
+ if (findings.length >= 50)
2473
+ return;
2474
+ }
2475
+ if (entry.isDirectory()) {
2476
+ await scanRunPublicSafetyDirectory(root, absolutePath, findings);
2477
+ if (findings.length >= 50)
2478
+ return;
2479
+ continue;
2480
+ }
2481
+ if (!entry.isFile() || !shouldScanTextArtifact(relativePath)) {
2482
+ continue;
2483
+ }
2484
+ const text = await readFile(absolutePath, "utf8").catch(() => null);
2485
+ if (text !== null && containsSensitivePattern(text)) {
2486
+ findings.push(`sensitive text ${relativePath}`);
2487
+ if (findings.length >= 50)
2488
+ return;
2489
+ }
2490
+ }
2491
+ }
2492
+ function isRiskyPublicArtifactPath(relativePath) {
2493
+ return relativePath.split(/[\\/]/).some((segment) => riskyPublicArtifactPathSegments.has(segment));
2494
+ }
2495
+ function shouldScanTextArtifact(relativePath) {
2496
+ const extension = path.extname(relativePath).toLowerCase();
2497
+ return ![".png", ".jpg", ".jpeg", ".webp", ".gif", ".tgz", ".gz", ".zip"].includes(extension);
2498
+ }
2499
+ function isLocalEvidenceArtifactPath(value) {
2500
+ return value.length > 0
2501
+ && !path.isAbsolute(value)
2502
+ && !value.includes("://")
2503
+ && !value.startsWith("..")
2504
+ && !value.split(/[\\/]/).includes("..");
2505
+ }
2506
+ function normalizeLocalEvidenceReference(value) {
2507
+ if (!value || value.includes("://") || path.isAbsolute(value)) {
2508
+ return null;
2509
+ }
2510
+ const normalized = value.replace(/\\/g, "/");
2511
+ if (normalized.startsWith("../")) {
2512
+ return isLocalEvidenceArtifactPath(normalized.slice(3)) ? normalized.slice(3) : null;
2513
+ }
2514
+ return isLocalEvidenceArtifactPath(normalized) ? normalized : null;
2515
+ }
1854
2516
  async function validateCwd(cwd) {
1855
2517
  try {
1856
2518
  const stats = await stat(cwd);