yam-flow 0.1.2 → 0.1.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.
package/src/bin/yam.ts CHANGED
@@ -10,9 +10,12 @@ import process from 'node:process';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import {
12
12
  TRUTH_STATUSES,
13
+ buildMediaGenerationProof,
13
14
  buildMissionPatchEnvelope,
14
15
  buildRollbackHint,
16
+ buildRuntimeBackendEvidence,
15
17
  buildUeyeVisualProvenance,
18
+ buildUeyeRunReport,
16
19
  buildYamCompletionProof,
17
20
  detectDbSafetyText as detectTrustDbSafetyText,
18
21
  isTruthStatus
@@ -136,6 +139,11 @@ Usage:
136
139
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
137
140
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
138
141
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
142
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
143
+ yam media proof [--requested] [--attempted] [--output file] [--json]
144
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
145
+ yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
146
+ yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
139
147
  yam release report [--json]
140
148
  yam safety [text...]
141
149
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -144,7 +152,7 @@ Usage:
144
152
  yam tune-log [dir]
145
153
  yam install
146
154
  yam uninstall
147
- yam doctor
155
+ yam doctor [--json]
148
156
  yam examples
149
157
  yam path
150
158
  yam version
@@ -375,7 +383,33 @@ function failOrReport(label, issues, quiet = false) {
375
383
  return issues.length;
376
384
  }
377
385
 
378
- async function doctor() {
386
+ async function doctor(args = []) {
387
+ const flags = parseSimpleFlags(args, new Set(['json']));
388
+ const report = await buildDoctorReport();
389
+ if (flags.json) {
390
+ console.log(JSON.stringify(report, null, 2));
391
+ if (!report.ok) process.exitCode = 1;
392
+ return;
393
+ }
394
+ if (report.ok) {
395
+ console.log('yam doctor: ok');
396
+ console.log('No hooks, automations, or global config are required.');
397
+ console.log('Skill install state is reported by `yam status`.');
398
+ console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
399
+ return;
400
+ }
401
+
402
+ console.log('yam doctor: issues');
403
+ for (const issue of report.issues) console.log(`- ${issue}`);
404
+ if (report.nextActions.length) {
405
+ console.log('');
406
+ console.log('Next actions:');
407
+ for (const action of report.nextActions) console.log(`- ${action}`);
408
+ }
409
+ process.exitCode = 1;
410
+ }
411
+
412
+ async function buildDoctorReport() {
379
413
  const issues = [];
380
414
  for (const skill of SKILLS) {
381
415
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
@@ -398,19 +432,98 @@ async function doctor() {
398
432
  }
399
433
  const verifyIssues = await verify({ quiet: true });
400
434
  if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
435
+ const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
436
+ const nextActionDetails = doctorNextActionDetails(issues);
437
+ return {
438
+ schema: 'yam.doctor.v1',
439
+ generatedAt: new Date().toISOString(),
440
+ ok: issues.length === 0,
441
+ issues,
442
+ nextActions: nextActionDetails.map((action) => action.next_action),
443
+ nextActionDetails,
444
+ yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
445
+ };
446
+ }
401
447
 
402
- if (!issues.length) {
403
- console.log('yam doctor: ok');
404
- const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
405
- console.log('No hooks, automations, or global config are required.');
406
- console.log('Skill install state is reported by `yam status`.');
407
- console.log(`yam-lite hook: ${hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'} (optional)`);
408
- return;
448
+ function doctorNextActions(issues = []) {
449
+ return doctorNextActionDetails(issues).map((action) => action.next_action);
450
+ }
451
+
452
+ function doctorNextActionDetails(issues = []) {
453
+ const actions = [];
454
+ for (const issue of issues) {
455
+ if (/missing source skill/i.test(issue)) actions.push(nextActionDetail('restore-skill-source', 'warning', issue, 'run from a complete yam-flow checkout or reinstall the published package', 'npm install -g yam-flow@latest && yam install'));
456
+ else if (/missing truth matrix/i.test(issue)) actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
457
+ else if (/unexpected local hooks/i.test(issue)) actions.push(nextActionDetail('remove-project-hook', 'warning', issue, 'remove project hooks unless this repo is intentionally dogfooding them', 'inspect .codex/hooks.json and remove only intentional stale hooks'));
458
+ else if (/unexpected .*automation/i.test(issue)) actions.push(nextActionDetail('remove-stale-automation', 'warning', issue, 'remove stale yam/timeto automations before claiming clean install state', 'inspect ~/.codex/automations and remove stale entries manually'));
459
+ else if (/legacy|retired/i.test(issue)) actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
460
+ else if (/verify reported/i.test(issue)) actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
409
461
  }
462
+ return uniqueNextActionDetails(actions);
463
+ }
410
464
 
411
- console.log('yam doctor: issues');
412
- for (const issue of issues) console.log(`- ${issue}`);
413
- process.exitCode = 1;
465
+ function nextActionDetail(id, severity, reason, nextAction, command = '') {
466
+ const priority = priorityForSeverity(severity);
467
+ const ownerRoute = ownerRouteForAction(id, reason, command);
468
+ return {
469
+ id,
470
+ severity,
471
+ priority,
472
+ owner_route: ownerRoute,
473
+ reason,
474
+ next_action: nextAction,
475
+ command,
476
+ tool_intent: toolIntentForCommand(command),
477
+ fix_first: priority === 'P0' || priority === 'P1',
478
+ blocks_release: priority === 'P0',
479
+ truth_status: 'partial'
480
+ };
481
+ }
482
+
483
+ function priorityForSeverity(severity = '') {
484
+ if (/error|danger|block/i.test(String(severity))) return 'P0';
485
+ if (/warn/i.test(String(severity))) return 'P1';
486
+ if (/info/i.test(String(severity))) return 'P2';
487
+ return 'P3';
488
+ }
489
+
490
+ function ownerRouteForAction(id = '', reason = '', command = '') {
491
+ const text = `${id} ${reason} ${command}`;
492
+ if (/visual|ueye|screenshot|browser/i.test(text)) return '$ueye';
493
+ if (/release|publish|package|registry|dist|version/i.test(text)) return '$deep';
494
+ if (/database|supabase|destructive|production|migration/i.test(text)) return '$deep';
495
+ if (/mission|subagent|team|lane/i.test(text)) return '$mission';
496
+ if (/research|docs|current/i.test(text)) return '$scout';
497
+ return '$quick';
498
+ }
499
+
500
+ function toolIntentForCommand(command = '') {
501
+ const text = String(command || '').toLowerCase();
502
+ if (!text) return 'read_only';
503
+ if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
504
+ if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
505
+ if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
506
+ if (/(ueye|visual|image|screen)/i.test(text)) return 'visual';
507
+ if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text)) return 'read_only';
508
+ return 'write';
509
+ }
510
+
511
+ function normalizeToolIntent(value = '') {
512
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
513
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
514
+ return 'read_only';
515
+ }
516
+
517
+ function uniqueNextActionDetails(actions = []) {
518
+ const seen = new Set<string>();
519
+ const unique = [];
520
+ for (const action of actions) {
521
+ const key = `${action.id}:${action.next_action}`;
522
+ if (seen.has(key)) continue;
523
+ seen.add(key);
524
+ unique.push(action);
525
+ }
526
+ return unique;
414
527
  }
415
528
 
416
529
  async function tools(args = []) {
@@ -471,6 +584,7 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
471
584
  packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
472
585
  ].join('\n'));
473
586
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
587
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
474
588
  const sqlScan = await scanSqlFiles(dir);
475
589
  const rows = [
476
590
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
@@ -490,6 +604,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
490
604
  ...safety.hits,
491
605
  ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
492
606
  ];
607
+ const nextActionDetails = toolsDoctorNextActionDetails({
608
+ pack,
609
+ rows,
610
+ riskNotes,
611
+ frameworkChecklist,
612
+ commands: detection.packageJson ? detection.commands : {}
613
+ });
493
614
  return {
494
615
  schemaVersion: 1,
495
616
  generatedAt: new Date().toISOString(),
@@ -500,8 +621,11 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
500
621
  packageManager: detection.packageJson ? detection.packageManager : null,
501
622
  instructionSurfaces,
502
623
  projectSurfaces,
624
+ frameworkChecklist,
503
625
  sqlScan,
504
626
  riskNotes,
627
+ nextActions: nextActionDetails.map((action) => action.next_action),
628
+ nextActionDetails,
505
629
  routeRecommendations: {
506
630
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
507
631
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -534,6 +658,14 @@ function printToolsDoctorReport(data) {
534
658
  for (const surface of data.projectSurfaces) console.log(`- ${surface}`);
535
659
  }
536
660
 
661
+ if (data.frameworkChecklist?.detected) {
662
+ console.log('');
663
+ console.log(`Framework checklist: ${data.frameworkChecklist.framework}`);
664
+ for (const check of data.frameworkChecklist.checks) {
665
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
666
+ }
667
+ }
668
+
537
669
  if (data.sqlScan.filesScanned > 0) {
538
670
  console.log('');
539
671
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -548,6 +680,12 @@ function printToolsDoctorReport(data) {
548
680
  for (const note of data.riskNotes) console.log(`- ${note.level}: ${note.reason}`);
549
681
  }
550
682
 
683
+ if (data.nextActions?.length) {
684
+ console.log('');
685
+ console.log('Next actions:');
686
+ for (const action of data.nextActions) console.log(`- ${action}`);
687
+ }
688
+
551
689
  console.log('');
552
690
  console.log('Route recommendations:');
553
691
  console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
@@ -620,6 +758,60 @@ async function detectProjectToolSurfaces(dir, pkg = null) {
620
758
  return surfaces;
621
759
  }
622
760
 
761
+ async function detectFrameworkChecklist(dir, pkg = null) {
762
+ const deps = {
763
+ ...pkg?.dependencies,
764
+ ...pkg?.devDependencies
765
+ };
766
+ const depNames = Object.keys(deps || {});
767
+ const hasReact = depNames.includes('react');
768
+ const hasNext = depNames.includes('next') || await exists(path.join(dir, 'next.config.js')) || await exists(path.join(dir, 'next.config.mjs')) || await exists(path.join(dir, 'next.config.ts'));
769
+ const hasVite = depNames.includes('vite') || await exists(path.join(dir, 'vite.config.js')) || await exists(path.join(dir, 'vite.config.ts'));
770
+ const framework = hasNext ? 'next-app' : hasReact && hasVite ? 'react-vite' : hasReact ? 'react' : pkg ? 'node-package' : 'unknown';
771
+ const uiLike = hasReact || hasNext || hasVite || await exists(path.join(dir, 'app')) || await exists(path.join(dir, 'src', 'app')) || await exists(path.join(dir, 'pages'));
772
+ const checks = [];
773
+ if (uiLike) {
774
+ checks.push(
775
+ checklistRow('states', 'default/loading/error/empty states are reachable before claiming complete', '$ueye'),
776
+ checklistRow('responsive', 'mobile and desktop layouts avoid overlap and clipped text', '$ueye'),
777
+ checklistRow('accessibility', 'interactive controls have accessible labels and keyboard path', '$ueye'),
778
+ checklistRow('runtime', 'browser or in-app runtime evidence is recorded when claiming visual verified', '$ueye')
779
+ );
780
+ }
781
+ if (hasNext) {
782
+ checks.push(checklistRow('boundaries', 'server/client boundaries and async data states match the framework pattern', '$deep when risky'));
783
+ }
784
+ if (pkg) {
785
+ checks.push(checklistRow('package', 'run the smallest honest type/build/package check before publishing', '$quick or $deep'));
786
+ }
787
+ return {
788
+ schema: 'yam.framework-checklist.v1',
789
+ detected: checks.length > 0,
790
+ uiLike,
791
+ framework,
792
+ routeHint: uiLike ? '$ueye for visual claims; $deep when runtime/data risk appears' : '$quick by default; deepen on risk',
793
+ checks
794
+ };
795
+ }
796
+
797
+ function checklistRow(id, label, route) {
798
+ return { id, label, route };
799
+ }
800
+
801
+ function toolsDoctorNextActions(input) {
802
+ return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
803
+ }
804
+
805
+ function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
806
+ const actions = [];
807
+ if (!pack) actions.push(nextActionDetail('project-pack', 'info', 'project pack missing', 'create or refresh yam.project.md so route choices reuse local project context', 'yam init-project .'));
808
+ if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push(nextActionDetail('install-skills', 'warning', 'skill install state is incomplete', 'run `yam install` and restart Codex before relying on installed skills', 'yam install'));
809
+ if (riskNotes.some((note) => note.level === 'danger')) actions.push(nextActionDetail('deep-safety', 'error', 'destructive database or production write signal detected', 'use $deep before claiming destructive database or production write safety', 'yam safety "<command or SQL>"'));
810
+ if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push(nextActionDetail('verification-command', 'warning', 'build/typecheck command not detected', 'add or document a small build/typecheck command for implementation verification', 'yam pack .'));
811
+ if (frameworkChecklist?.uiLike) actions.push(nextActionDetail('ueye-evidence', 'info', 'UI-like project detected', 'for Ueye verified claims, record reference, actual screenshot, and comparison result', 'yam ueye report --reference ref.png --actual shot.png --json'));
812
+ return uniqueNextActionDetails(actions);
813
+ }
814
+
623
815
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
624
816
  const root = path.resolve(rootDir || process.cwd());
625
817
  const files = [];
@@ -949,10 +1141,23 @@ async function release(args = []) {
949
1141
  console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
950
1142
  if (check.note) console.log(` ${check.note}`);
951
1143
  }
1144
+ if (report.provenance) {
1145
+ console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
1146
+ console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
1147
+ console.log(`- Dist freshness: ${report.freshness.dist}`);
1148
+ }
1149
+ if (report.tarball) {
1150
+ console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
1151
+ if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
1152
+ }
952
1153
  if (report.failed.length) {
953
1154
  console.log('- Failed:');
954
1155
  for (const id of report.failed) console.log(` - ${id}`);
955
1156
  }
1157
+ if (report.next_actions?.length) {
1158
+ console.log('- Next actions:');
1159
+ for (const action of report.next_actions) console.log(` - ${action.next_action}`);
1160
+ }
956
1161
  if (!report.ok) process.exitCode = 1;
957
1162
  }
958
1163
 
@@ -967,6 +1172,10 @@ function runReleaseReport() {
967
1172
  ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
968
1173
  ].map(([id, command]) => runReleaseCheck(id, command));
969
1174
  const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
1175
+ const provenance = releaseProvenance(checks);
1176
+ const tarball = releaseTarballProvenance();
1177
+ const freshness = releaseFreshness(checks, provenance, tarball);
1178
+ const nextActions = releaseNextActions(checks, provenance, tarball);
970
1179
  return {
971
1180
  schema: 'yam.release-report.v1',
972
1181
  generated_at: startedAt,
@@ -975,10 +1184,117 @@ function runReleaseReport() {
975
1184
  ok: failed.length === 0,
976
1185
  truth_status: failed.length === 0 ? 'verified' : 'blocked',
977
1186
  checks,
978
- failed
1187
+ failed,
1188
+ provenance,
1189
+ tarball,
1190
+ freshness,
1191
+ next_actions: nextActions
1192
+ };
1193
+ }
1194
+
1195
+ function releaseProvenance(checks = []) {
1196
+ const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
1197
+ const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
1198
+ const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
1199
+ const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
1200
+ return {
1201
+ schema: 'yam.release-provenance.v1',
1202
+ package_name: PACKAGE_JSON.name,
1203
+ version: VERSION,
1204
+ git_commit: gitCommit.trim(),
1205
+ git_dirty: Boolean(gitStatus.trim()),
1206
+ git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1207
+ dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1208
+ package_files: packageFiles,
1209
+ generated_at: new Date().toISOString(),
1210
+ truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
1211
+ };
1212
+ }
1213
+
1214
+ function releaseTarballProvenance() {
1215
+ const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
1216
+ cwd: ROOT,
1217
+ encoding: 'utf8',
1218
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
1219
+ timeout: 120000
1220
+ });
1221
+ if (result.status !== 0) {
1222
+ return {
1223
+ schema: 'yam.release-tarball-provenance.v1',
1224
+ status: 'blocked',
1225
+ reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
1226
+ truth_status: 'blocked'
1227
+ };
1228
+ }
1229
+ try {
1230
+ const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
1231
+ return {
1232
+ schema: 'yam.release-tarball-provenance.v1',
1233
+ status: 'checked',
1234
+ tarball_name: String(data.filename || ''),
1235
+ package_size: Number(data.size || data.packageSize || 0),
1236
+ unpacked_size: Number(data.unpackedSize || 0),
1237
+ file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
1238
+ integrity: String(data.integrity || ''),
1239
+ shasum: String(data.shasum || ''),
1240
+ truth_status: data.filename ? 'verified' : 'partial'
1241
+ };
1242
+ } catch (error) {
1243
+ return {
1244
+ schema: 'yam.release-tarball-provenance.v1',
1245
+ status: 'blocked',
1246
+ reason: `could not parse npm pack output: ${errorMessage(error)}`,
1247
+ truth_status: 'blocked'
1248
+ };
1249
+ }
1250
+ }
1251
+
1252
+ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1253
+ const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
1254
+ const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
1255
+ const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
1256
+ const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
1257
+ const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
1258
+ const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
1259
+ return {
1260
+ schema: 'yam.release-freshness.v1',
1261
+ dist,
1262
+ registry,
1263
+ package_boundary: boundary,
1264
+ cli_smoke: smoke,
1265
+ tarball: tarballStatus,
1266
+ truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
979
1267
  };
980
1268
  }
981
1269
 
1270
+ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1271
+ const actions = [];
1272
+ for (const check of checks) {
1273
+ if (check.status === 'passed') continue;
1274
+ actions.push(nextActionDetail(`release-${check.id}`, check.status === 'blocked' ? 'error' : 'warning', check.note || `${check.id} did not pass`, `fix ${check.id} before publishing`, check.command));
1275
+ }
1276
+ if (provenance.git_dirty) {
1277
+ actions.push(nextActionDetail('release-git-dirty', 'warning', 'working tree has uncommitted changes', 'commit or intentionally keep local-only changes before tagging a release', 'git status --short'));
1278
+ }
1279
+ if (provenance.dist_changed_files?.length) {
1280
+ actions.push(nextActionDetail('release-dist-drift', 'warning', 'dist has unstaged or uncommitted differences', 'run build and commit generated dist only if this package publishes dist', 'npm run build'));
1281
+ }
1282
+ if (tarball.status !== 'checked') {
1283
+ actions.push(nextActionDetail('release-tarball-provenance', 'error', tarball.reason || 'tarball provenance was not produced', 'run npm pack dry-run before publishing', 'npm pack --dry-run'));
1284
+ }
1285
+ return uniqueNextActionDetails(actions);
1286
+ }
1287
+
1288
+ function runReadOnlyCommand(command, args = []) {
1289
+ const result = spawnSync(command, args, {
1290
+ cwd: ROOT,
1291
+ encoding: 'utf8',
1292
+ timeout: 30000
1293
+ });
1294
+ if (result.status !== 0) return '';
1295
+ return String(result.stdout || '');
1296
+ }
1297
+
982
1298
  function runReleaseCheck(id, commandSpec) {
983
1299
  const [command, args] = commandSpec;
984
1300
  const start = Date.now();
@@ -1019,6 +1335,7 @@ async function ueye(args = []) {
1019
1335
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1020
1336
  if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1021
1337
  if (subcommand === 'compare') return ueyeCompare(args.slice(1));
1338
+ if (subcommand === 'report') return ueyeReport(args.slice(1));
1022
1339
  console.error(`unknown ueye command: ${subcommand}`);
1023
1340
  return ueyeUsage();
1024
1341
  }
@@ -1031,10 +1348,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1031
1348
  Usage:
1032
1349
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1033
1350
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1351
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--review-session-id id] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
1034
1352
 
1035
1353
  Notes:
1036
1354
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1037
1355
  compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1356
+ report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
1038
1357
  `);
1039
1358
  }
1040
1359
 
@@ -1155,6 +1474,454 @@ async function ueyeCompare(args = []) {
1155
1474
  }
1156
1475
  }
1157
1476
 
1477
+ async function ueyeReport(args = []) {
1478
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1479
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'json']));
1480
+ const reference = String(flags.reference || flags.ref || '');
1481
+ const actual = String(flags.actual || flags.screenshot || '');
1482
+ const referenceSources = [];
1483
+ const implementationSources = [];
1484
+ let comparisonResult = 'not-verified';
1485
+ let blockedReason = String(flags.blocked_reason || '');
1486
+ const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
1487
+ const previousReport = await readPreviousUeyeReport(flags.previous_report);
1488
+
1489
+ try {
1490
+ if (reference) {
1491
+ const info = await imageFileInfo(reference);
1492
+ referenceSources.push(buildUeyeVisualProvenance({
1493
+ source_kind: 'reference_image',
1494
+ source_path: info.path,
1495
+ source_hash: info.sha256,
1496
+ reference_id: String(flags.reference_id || path.basename(info.path)),
1497
+ local_only: true,
1498
+ operator_provided: true,
1499
+ comparison_result: 'reference-recorded',
1500
+ truth_status: 'partial'
1501
+ }));
1502
+ }
1503
+ if (actual) {
1504
+ const info = await imageFileInfo(actual);
1505
+ implementationSources.push(buildUeyeVisualProvenance({
1506
+ source_kind: 'implementation_screenshot',
1507
+ source_path: info.path,
1508
+ source_hash: info.sha256,
1509
+ reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
1510
+ screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
1511
+ local_only: true,
1512
+ operator_provided: false,
1513
+ comparison_result: 'implementation-recorded',
1514
+ truth_status: 'partial'
1515
+ }));
1516
+ }
1517
+ if (reference && actual && referenceSources[0]?.source_hash === implementationSources[0]?.source_hash) {
1518
+ comparisonResult = 'matched';
1519
+ } else if (reference && actual) {
1520
+ comparisonResult = 'different';
1521
+ }
1522
+ } catch (error) {
1523
+ blockedReason = errorMessage(error);
1524
+ }
1525
+
1526
+ const report = buildUeyeRunReport({
1527
+ reference_sources: referenceSources,
1528
+ implementation_sources: implementationSources,
1529
+ comparison_result: comparisonResult,
1530
+ design_quality: String(flags.design_quality || 'not-checked'),
1531
+ blocked_reason: blockedReason,
1532
+ next_action: String(flags.next_visual_action || flags.next_action || '')
1533
+ });
1534
+ const comparisonReport = buildUeyeComparisonReport({
1535
+ reviewSessionId,
1536
+ previousReport,
1537
+ referenceSources,
1538
+ implementationSources,
1539
+ comparisonResult,
1540
+ designQuality: String(flags.design_quality || 'not-checked'),
1541
+ similar: arrayFlag(flags.similar),
1542
+ different: arrayFlag(flags.different),
1543
+ missing: arrayFlag(flags.missing),
1544
+ notes: arrayFlag(flags.comparison_notes),
1545
+ resolved: arrayFlag(flags.resolved),
1546
+ newFindings: arrayFlag(flags.new_finding),
1547
+ stillOpen: arrayFlag(flags.still_open),
1548
+ regressions: arrayFlag(flags.regression),
1549
+ previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
1550
+ currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
1551
+ viewport: String(flags.viewport || ''),
1552
+ state: String(flags.state || 'unknown'),
1553
+ nextAction: report.next_action,
1554
+ truthStatus: report.truth_status
1555
+ });
1556
+ const result = {
1557
+ ...report,
1558
+ review_session_id: reviewSessionId,
1559
+ tool_intent: 'visual',
1560
+ capture_backend: String(flags.capture_backend || 'not-recorded'),
1561
+ compare_backend: String(flags.compare_backend || 'local-file-hash'),
1562
+ continuity: comparisonReport.continuity,
1563
+ comparison_report: comparisonReport
1564
+ };
1565
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1566
+ if (report.truth_status === 'blocked') process.exitCode = 1;
1567
+ }
1568
+
1569
+ async function readPreviousUeyeReport(file = '') {
1570
+ const target = String(file || '');
1571
+ if (!target) return null;
1572
+ try {
1573
+ const data = await readJson(path.resolve(expandHome(target)));
1574
+ return {
1575
+ path: path.resolve(expandHome(target)),
1576
+ review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
1577
+ screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
1578
+ truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
1579
+ comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
1580
+ };
1581
+ } catch (error) {
1582
+ return {
1583
+ path: path.resolve(expandHome(target)),
1584
+ blocked_reason: errorMessage(error),
1585
+ truth_status: 'blocked',
1586
+ comparison_result: 'not-verified'
1587
+ };
1588
+ }
1589
+ }
1590
+
1591
+ function buildUeyeComparisonReport({
1592
+ reviewSessionId,
1593
+ previousReport,
1594
+ referenceSources,
1595
+ implementationSources,
1596
+ comparisonResult,
1597
+ designQuality,
1598
+ similar,
1599
+ different,
1600
+ missing,
1601
+ notes,
1602
+ resolved,
1603
+ newFindings,
1604
+ stillOpen,
1605
+ regressions,
1606
+ previousScreenshotId,
1607
+ currentScreenshotId,
1608
+ viewport,
1609
+ state,
1610
+ nextAction,
1611
+ truthStatus
1612
+ }) {
1613
+ const hasReference = referenceSources.length > 0;
1614
+ const hasImplementation = implementationSources.length > 0;
1615
+ const continuityTruth = previousReport?.blocked_reason ? 'blocked' : previousReport ? 'partial' : 'assumed';
1616
+ return {
1617
+ schema: 'yam.ueye-comparison-report.v1',
1618
+ review_session_id: reviewSessionId,
1619
+ previous_report: previousReport,
1620
+ continuity: {
1621
+ schema: 'yam.ueye-review-continuity.v1',
1622
+ current_review_session_id: reviewSessionId,
1623
+ previous_review_session_id: previousReport?.review_session_id || '',
1624
+ previous_report_path: previousReport?.path || '',
1625
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1626
+ current_screenshot_id: currentScreenshotId || '',
1627
+ carries_forward: Boolean(previousReport && !previousReport.blocked_reason),
1628
+ truth_status: continuityTruth
1629
+ },
1630
+ evidence_summary: {
1631
+ has_reference: hasReference,
1632
+ has_implementation_screenshot: hasImplementation,
1633
+ reference_count: referenceSources.length,
1634
+ implementation_count: implementationSources.length
1635
+ },
1636
+ comparison_result: comparisonResult,
1637
+ design_quality: designQuality,
1638
+ similar,
1639
+ different,
1640
+ missing,
1641
+ resolved,
1642
+ new_findings: newFindings,
1643
+ still_open: stillOpen,
1644
+ regressions,
1645
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1646
+ current_screenshot_id: currentScreenshotId || '',
1647
+ viewport,
1648
+ state,
1649
+ notes,
1650
+ next_action: nextAction || (truthStatus === 'verified' ? 'no action required' : 'capture or provide implementation screenshot, then compare again'),
1651
+ truth_status: truthStatus
1652
+ };
1653
+ }
1654
+
1655
+ async function media(args = []) {
1656
+ const subcommand = args[0] || 'help';
1657
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
1658
+ if (subcommand === 'proof') return mediaProof(args.slice(1));
1659
+ console.error(`unknown media command: ${subcommand}`);
1660
+ return mediaUsage();
1661
+ }
1662
+
1663
+ function mediaUsage() {
1664
+ console.log(`yam media
1665
+
1666
+ Opt-in media generation proof helpers. Generated media can support visual direction, but it cannot verify implemented UI without implementation screenshot evidence.
1667
+
1668
+ Usage:
1669
+ yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
1670
+ `);
1671
+ }
1672
+
1673
+ async function mediaProof(args = []) {
1674
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return mediaUsage();
1675
+ const flags = parseSimpleFlags(args, new Set(['tool', 'requested', 'attempted', 'output', 'wait-loop', 'blocked-reason', 'next-action', 'json']));
1676
+ const output = String(flags.output || '');
1677
+ let outputHash = 'unknown';
1678
+ let outputPath = '';
1679
+ let blockedReason = String(flags.blocked_reason || '');
1680
+ if (output) {
1681
+ try {
1682
+ const info = await imageFileInfo(output);
1683
+ outputHash = info.sha256;
1684
+ outputPath = info.path;
1685
+ } catch (error) {
1686
+ blockedReason = blockedReason || errorMessage(error);
1687
+ outputPath = path.resolve(expandHome(output));
1688
+ }
1689
+ }
1690
+ const proof = buildMediaGenerationProof({
1691
+ tool_name: String(flags.tool || ''),
1692
+ generation_requested: Boolean(flags.requested),
1693
+ generation_attempted: Boolean(flags.attempted || output),
1694
+ output_path: outputPath,
1695
+ output_hash: outputHash,
1696
+ wait_loop_checked: Boolean(flags.wait_loop),
1697
+ blocked_reason: blockedReason,
1698
+ next_action: String(flags.next_action || '')
1699
+ });
1700
+ printJsonOrHuman(proof, Boolean(flags.json), 'Media proof');
1701
+ if (proof.truth_status === 'blocked') process.exitCode = 1;
1702
+ }
1703
+
1704
+ async function runtime(args = []) {
1705
+ const subcommand = args[0] || 'help';
1706
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return runtimeUsage();
1707
+ if (subcommand === 'evidence') return runtimeEvidence(args.slice(1));
1708
+ console.error(`unknown runtime command: ${subcommand}`);
1709
+ return runtimeUsage();
1710
+ }
1711
+
1712
+ function runtimeUsage() {
1713
+ console.log(`yam runtime
1714
+
1715
+ Usage:
1716
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--evidence-id id] [--command text] [--pid n] [--port n] [--url URL] [--exit-code n] [--screenshot-id id] [--cleanup-checked] [--note text] [--json]
1717
+
1718
+ Notes:
1719
+ Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
1720
+ `);
1721
+ }
1722
+
1723
+ async function runtimeEvidence(args = []) {
1724
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
1725
+ const flags = parseSimpleFlags(args, new Set(['backend', 'claim', 'evidence-id', 'command', 'pid', 'port', 'url', 'exit-code', 'screenshot-id', 'started-at', 'stopped-at', 'cleanup-checked', 'note', 'truth', 'intent', 'json']));
1726
+ const evidence = buildRuntimeBackendEvidence({
1727
+ backend: flags.backend,
1728
+ claim: flags.claim,
1729
+ evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1730
+ command: String(flags.command || ''),
1731
+ cleanup_checked: Boolean(flags.cleanup_checked),
1732
+ note: String(flags.note || ''),
1733
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1734
+ });
1735
+ const runtimeDetails = runtimeEvidenceDetails(flags);
1736
+ if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
1737
+ evidence.truth_status = 'partial';
1738
+ }
1739
+ const result = {
1740
+ ...evidence,
1741
+ runtime_details: runtimeDetails,
1742
+ tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
1743
+ next_action: runtimeEvidenceNextAction(evidence)
1744
+ };
1745
+ printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
1746
+ if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing') process.exitCode = 1;
1747
+ }
1748
+
1749
+ function runtimeEvidenceDetails(flags: AnyRecord = {}) {
1750
+ return {
1751
+ schema: 'yam.runtime-details.v1',
1752
+ pid: numberOrNull(flags.pid),
1753
+ port: numberOrNull(flags.port),
1754
+ url: String(flags.url || ''),
1755
+ exit_code: numberOrNull(flags.exit_code),
1756
+ screenshot_id: String(flags.screenshot_id || ''),
1757
+ started_at: String(flags.started_at || ''),
1758
+ stopped_at: String(flags.stopped_at || '')
1759
+ };
1760
+ }
1761
+
1762
+ function runtimeDetailsHasVerification(details: AnyRecord = {}) {
1763
+ return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
1764
+ }
1765
+
1766
+ function runtimeEvidenceNextAction(evidence) {
1767
+ if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
1768
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
1769
+ if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
1770
+ return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1771
+ }
1772
+
1773
+ async function mission(args = []) {
1774
+ const subcommand = args[0] || 'help';
1775
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return missionUsage();
1776
+ if (subcommand === 'queue' || subcommand === 'patch') return missionQueue(args.slice(1));
1777
+ console.error(`unknown mission command: ${subcommand}`);
1778
+ return missionUsage();
1779
+ }
1780
+
1781
+ function missionUsage() {
1782
+ console.log(`yam mission
1783
+
1784
+ Usage:
1785
+ yam mission queue [--lane-id id] [--agent-id id] [--status pending|applied|verified|blocked|reverted] [--scope text] [--changed file] [--depends-on lane] [--generated file] [--verification-hint text] [--rollback-hint text] [--before-check text] [--out file] [--truth status] [--json]
1786
+
1787
+ Notes:
1788
+ Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
1789
+ `);
1790
+ }
1791
+
1792
+ async function missionQueue(args = []) {
1793
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return missionUsage();
1794
+ const flags = parseSimpleFlags(args, new Set(['lane-id', 'agent-id', 'status', 'scope', 'changed', 'generated', 'depends-on', 'verification-hint', 'rollback-hint', 'before-check', 'safe-revert-note', 'truth', 'intent', 'out', 'json']));
1795
+ const changedFiles = arrayFlag(flags.changed);
1796
+ const generatedFiles = arrayFlag(flags.generated);
1797
+ const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
1798
+ const status = normalizeMissionQueueStatus(flags.status || 'pending');
1799
+ const rollback = buildRollbackHint({
1800
+ touched_files: changedFiles,
1801
+ generated_files: generatedFiles,
1802
+ before_check: String(flags.before_check || ''),
1803
+ safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
1804
+ });
1805
+ const envelope = buildMissionPatchEnvelope({
1806
+ agent_id: String(flags.agent_id || `agent-${timestampId()}`),
1807
+ assigned_scope: String(flags.scope || ''),
1808
+ changed_files: changedFiles,
1809
+ verification_hint: String(flags.verification_hint || ''),
1810
+ rollback_hint: rollback,
1811
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
1812
+ });
1813
+ const item = {
1814
+ schema: 'yam.mission-patch-queue-item.v1',
1815
+ lane_id: laneId,
1816
+ status,
1817
+ depends_on: arrayFlag(flags.depends_on),
1818
+ tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
1819
+ patch_envelope: envelope,
1820
+ next_action: missionQueueNextAction(envelope, status)
1821
+ };
1822
+ const result = {
1823
+ schema: 'yam.mission-patch-queue-lite.v1',
1824
+ generated_at: new Date().toISOString(),
1825
+ persistence: flags.out ? 'written' : 'not-persisted',
1826
+ out: flags.out ? path.resolve(expandHome(flags.out)) : '',
1827
+ items: [item],
1828
+ queue_depth: 1,
1829
+ next_action: item.next_action,
1830
+ truth_status: envelope.truth_status
1831
+ };
1832
+ if (flags.out) {
1833
+ const target = path.resolve(expandHome(flags.out));
1834
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1835
+ await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
1836
+ }
1837
+ printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
1838
+ }
1839
+
1840
+ function normalizeMissionQueueStatus(value = '') {
1841
+ const text = String(value || '').toLowerCase();
1842
+ if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text)) return text;
1843
+ return 'pending';
1844
+ }
1845
+
1846
+ function missionQueueNextAction(envelope, status = 'pending') {
1847
+ if (status === 'blocked') return 'resolve or document the blocker before applying dependent mission lanes';
1848
+ if (status === 'reverted') return 'rerun the before-check or verification hint before continuing dependent work';
1849
+ if (!envelope.assigned_scope) return 'add --scope so the patch can be reviewed against an assigned boundary';
1850
+ if (!envelope.changed_files.length) return 'add at least one --changed file when this represents code-changing work';
1851
+ if (!envelope.verification_hint) return 'add --verification-hint so reviewers know the smallest honest check';
1852
+ if (!envelope.rollback_hint.safe_revert_note && !envelope.rollback_hint.before_check) return 'add rollback or before-check detail before using this for risky work';
1853
+ return 'attach this envelope to the mission proof summary and cross-check changed files';
1854
+ }
1855
+
1856
+ async function benchmark(args = []) {
1857
+ const subcommand = args[0] || 'help';
1858
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return benchmarkUsage();
1859
+ if (subcommand === 'report') return benchmarkReport(args.slice(1));
1860
+ console.error(`unknown benchmark command: ${subcommand}`);
1861
+ return benchmarkUsage();
1862
+ }
1863
+
1864
+ function benchmarkUsage() {
1865
+ console.log(`yam benchmark
1866
+
1867
+ Usage:
1868
+ yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
1869
+
1870
+ Notes:
1871
+ Records a small optimization loop result. It does not run benchmarks by itself.
1872
+ `);
1873
+ }
1874
+
1875
+ async function benchmarkReport(args = []) {
1876
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return benchmarkUsage();
1877
+ const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
1878
+ const baseline = numberOrNull(flags.baseline);
1879
+ const current = numberOrNull(flags.current);
1880
+ const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
1881
+ const hasNumbers = baseline !== null && current !== null;
1882
+ const delta = hasNumbers ? current - baseline : null;
1883
+ const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
1884
+ const status = benchmarkStatus({ baseline, current, target });
1885
+ const result = {
1886
+ schema: 'yam.benchmark-report.v1',
1887
+ generated_at: new Date().toISOString(),
1888
+ label: String(flags.label || 'unnamed'),
1889
+ baseline,
1890
+ current,
1891
+ unit: String(flags.unit || ''),
1892
+ target,
1893
+ delta,
1894
+ percent,
1895
+ status,
1896
+ tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
1897
+ rollback_hint: String(flags.rollback_hint || (status === 'regressed' ? 'inspect the optimization patch and revert the touched surface if the regression is confirmed' : 'no rollback action recorded')),
1898
+ next_action: String(flags.next_action || benchmarkNextAction(status)),
1899
+ truth_status: hasNumbers ? 'verified' : 'partial'
1900
+ };
1901
+ printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
1902
+ if (status === 'regressed') process.exitCode = 1;
1903
+ }
1904
+
1905
+ function numberOrNull(value) {
1906
+ if (value === undefined || value === null || value === '') return null;
1907
+ const number = Number(value);
1908
+ return Number.isFinite(number) ? number : null;
1909
+ }
1910
+
1911
+ function benchmarkStatus({ baseline, current, target }) {
1912
+ if (baseline === null || current === null) return 'needs-baseline';
1913
+ if (current === baseline) return 'unchanged';
1914
+ if (target === 'higher') return current > baseline ? 'improved' : 'regressed';
1915
+ return current < baseline ? 'improved' : 'regressed';
1916
+ }
1917
+
1918
+ function benchmarkNextAction(status) {
1919
+ if (status === 'improved') return 'record the measurement source and keep the smallest relevant regression check';
1920
+ if (status === 'regressed') return 'treat this as a fix-first item before continuing planned optimization';
1921
+ if (status === 'unchanged') return 'keep the change only if it improves clarity or enables the next measured step';
1922
+ return 'capture baseline and current values before claiming optimization impact';
1923
+ }
1924
+
1158
1925
  function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1159
1926
  const flags: AnyRecord = {};
1160
1927
  const positionals: string[] = [];
@@ -1168,13 +1935,17 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1168
1935
  const key = rawKey.slice(2);
1169
1936
  if (allowed.size && !allowed.has(key)) continue;
1170
1937
  const normalizedKey = key.replace(/-/g, '_');
1171
- if (['json', 'full-page'].includes(key)) {
1938
+ if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked'].includes(key)) {
1172
1939
  flags[normalizedKey] = true;
1173
1940
  continue;
1174
1941
  }
1175
1942
  const value = inlineValue ?? args[index + 1] ?? '';
1176
1943
  if (inlineValue === undefined) index += 1;
1177
- flags[normalizedKey] = value;
1944
+ if (flags[normalizedKey] !== undefined) {
1945
+ flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1946
+ } else {
1947
+ flags[normalizedKey] = value;
1948
+ }
1178
1949
  }
1179
1950
  flags._ = positionals;
1180
1951
  return flags;
@@ -1309,9 +2080,11 @@ async function proof(args = []) {
1309
2080
  printProofList('Evidence', summary.evidence);
1310
2081
  printProofList('Visual evidence', summary.visual);
1311
2082
  printProofList('Runtime evidence', summary.runtime);
2083
+ printProofList('Runtime backend evidence', summary.runtimeBackendEvidence || []);
1312
2084
  printProofList('Visual provenance', summary.visualProvenance || []);
1313
2085
  printProofList('Mission patch envelope', summary.missionEnvelope || []);
1314
2086
  printProofList('Rollback hint', summary.rollbackHint || []);
2087
+ printProofList('Media proof', summary.mediaProof || []);
1315
2088
  printProofList('Changed surfaces', summary.changed);
1316
2089
  printProofList('Skipped', summary.skipped);
1317
2090
  printProofList('Blocked', summary.blocked);
@@ -1386,6 +2159,11 @@ function buildProofSummary(parsed, artifact) {
1386
2159
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1387
2160
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1388
2161
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
2162
+ runtimeBackendEvidence: [
2163
+ ...structuredEvidenceList(artifact.runtimeBackendEvidence),
2164
+ ...structuredEvidenceList(parsed.flags.runtime_backend_evidence),
2165
+ ...directRuntimeBackendEvidence(parsed.flags)
2166
+ ],
1389
2167
  visualProvenance: [
1390
2168
  ...structuredEvidenceList(artifact.visualProvenance),
1391
2169
  ...structuredEvidenceList(parsed.flags.visual_provenance)
@@ -1398,6 +2176,10 @@ function buildProofSummary(parsed, artifact) {
1398
2176
  ...structuredEvidenceList(artifact.rollbackHint),
1399
2177
  ...structuredEvidenceList(parsed.flags.rollback_hint)
1400
2178
  ],
2179
+ mediaProof: [
2180
+ ...structuredEvidenceList(artifact.mediaProof),
2181
+ ...structuredEvidenceList(parsed.flags.media_proof)
2182
+ ],
1401
2183
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1402
2184
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1403
2185
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1448,6 +2230,9 @@ function renderProofMarkdown(summary) {
1448
2230
  '## Runtime evidence',
1449
2231
  ...renderProofMarkdownList(summary.runtime),
1450
2232
  '',
2233
+ '## Runtime backend evidence',
2234
+ ...renderProofMarkdownList(summary.runtimeBackendEvidence || []),
2235
+ '',
1451
2236
  '## Visual provenance',
1452
2237
  ...renderProofMarkdownList(summary.visualProvenance || []),
1453
2238
  '',
@@ -1457,6 +2242,9 @@ function renderProofMarkdown(summary) {
1457
2242
  '## Rollback hint',
1458
2243
  ...renderProofMarkdownList(summary.rollbackHint || []),
1459
2244
  '',
2245
+ '## Media proof',
2246
+ ...renderProofMarkdownList(summary.mediaProof || []),
2247
+ '',
1460
2248
  '## Changed surfaces',
1461
2249
  ...renderProofMarkdownList(summary.changed),
1462
2250
  '',
@@ -1489,7 +2277,7 @@ function renderProofMarkdownList(values) {
1489
2277
  function parseProofArgs(args = []) {
1490
2278
  const flags: AnyRecord = {};
1491
2279
  const positionals: string[] = [];
1492
- const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
2280
+ const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'runtime-backend', 'runtime-claim', 'runtime-evidence-id', 'runtime-command', 'cleanup-checked', 'runtime-note', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1493
2281
  for (let index = 0; index < args.length; index += 1) {
1494
2282
  const arg = args[index];
1495
2283
  if (arg.startsWith('--')) {
@@ -1497,13 +2285,13 @@ function parseProofArgs(args = []) {
1497
2285
  const key = rawKey.slice(2);
1498
2286
  if (!aliases.has(key)) continue;
1499
2287
  const normalizedKey = key.replace(/-/g, '_');
1500
- if (key === 'json' || key.startsWith('require-')) {
2288
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
1501
2289
  flags[normalizedKey] = true;
1502
2290
  continue;
1503
2291
  }
1504
2292
  const value = inlineValue ?? args[index + 1] ?? '';
1505
2293
  if (inlineValue === undefined) index += 1;
1506
- if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
2294
+ if (['command', 'evidence', 'visual', 'runtime', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1507
2295
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1508
2296
  } else {
1509
2297
  flags[normalizedKey] = value;
@@ -1541,9 +2329,11 @@ function emptyProofArtifact() {
1541
2329
  evidence: [],
1542
2330
  visual: [],
1543
2331
  runtime: [],
2332
+ runtimeBackendEvidence: [],
1544
2333
  visualProvenance: [],
1545
2334
  missionEnvelope: [],
1546
2335
  rollbackHint: [],
2336
+ mediaProof: [],
1547
2337
  cleanup: '',
1548
2338
  changed: [],
1549
2339
  skipped: [],
@@ -1566,9 +2356,11 @@ async function parseProofJson(file, empty) {
1566
2356
  evidence: arrayFlag(data.evidence),
1567
2357
  visual: arrayFlag(data.visual || data.visualEvidence),
1568
2358
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
2359
+ runtimeBackendEvidence: structuredEvidenceList(data.runtimeBackendEvidence || data.runtime_backend_evidence),
1569
2360
  visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1570
2361
  missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1571
2362
  rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
2363
+ mediaProof: structuredEvidenceList(data.mediaProof || data.media_proof),
1572
2364
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1573
2365
  changed: arrayFlag(data.changed || data.files),
1574
2366
  skipped: arrayFlag(data.skipped),
@@ -1594,9 +2386,11 @@ async function parseProofMarkdown(file, empty) {
1594
2386
  evidence: readProofList(text, 'Evidence'),
1595
2387
  visual: readProofList(text, 'Visual evidence'),
1596
2388
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
2389
+ runtimeBackendEvidence: readProofList(text, 'Runtime backend evidence'),
1597
2390
  visualProvenance: readProofList(text, 'Visual provenance'),
1598
2391
  missionEnvelope: readProofList(text, 'Mission patch envelope'),
1599
2392
  rollbackHint: readProofList(text, 'Rollback hint'),
2393
+ mediaProof: readProofList(text, 'Media proof'),
1600
2394
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1601
2395
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1602
2396
  skipped: readProofList(text, 'Skipped'),
@@ -1647,6 +2441,18 @@ function structuredEvidenceList(value) {
1647
2441
  return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1648
2442
  }
1649
2443
 
2444
+ function directRuntimeBackendEvidence(flags) {
2445
+ if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked) return [];
2446
+ return [JSON.stringify(buildRuntimeBackendEvidence({
2447
+ backend: flags.runtime_backend,
2448
+ claim: flags.runtime_claim,
2449
+ evidence_id: flags.runtime_evidence_id,
2450
+ command: flags.runtime_command,
2451
+ cleanup_checked: Boolean(flags.cleanup_checked),
2452
+ note: flags.runtime_note
2453
+ }))];
2454
+ }
2455
+
1650
2456
  function formatStructuredEvidence(value) {
1651
2457
  if (!value) return '';
1652
2458
  if (typeof value === 'object') return JSON.stringify(value);
@@ -1664,6 +2470,12 @@ function formatStructuredEvidence(value) {
1664
2470
  if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1665
2471
  return JSON.stringify(buildRollbackHint(data));
1666
2472
  }
2473
+ if (data.schema === 'yam.runtime-backend-evidence.v1' || data.backend || data.evidence_id) {
2474
+ return JSON.stringify(buildRuntimeBackendEvidence(data));
2475
+ }
2476
+ if (data.schema === 'yam.media-generation-proof.v1' || data.generation_requested || data.output_hash) {
2477
+ return JSON.stringify(buildMediaGenerationProof(data));
2478
+ }
1667
2479
  return JSON.stringify(data);
1668
2480
  } catch {
1669
2481
  return text;
@@ -1672,7 +2484,7 @@ function formatStructuredEvidence(value) {
1672
2484
 
1673
2485
  function hasProofEvidence(parsed) {
1674
2486
  const flags = parsed.flags || {};
1675
- return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
2487
+ return ['command', 'evidence', 'visual', 'runtime', 'runtime_backend_evidence', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'media_proof', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup || flags.runtime_backend || flags.runtime_claim);
1676
2488
  }
1677
2489
 
1678
2490
  function printProofList(label, values) {
@@ -1924,7 +2736,8 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1924
2736
  lint: null,
1925
2737
  test: null,
1926
2738
  build: null
1927
- }
2739
+ },
2740
+ frameworkChecklist: null
1928
2741
  };
1929
2742
  if (!result.packageJson) {
1930
2743
  if (!quiet) {
@@ -1942,6 +2755,7 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1942
2755
  result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
1943
2756
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
1944
2757
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
2758
+ result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
1945
2759
 
1946
2760
  if (!quiet) printDetection(result);
1947
2761
  return result;
@@ -1960,6 +2774,13 @@ function printDetection(result) {
1960
2774
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
1961
2775
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
1962
2776
  console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
2777
+ if (result.frameworkChecklist?.detected) {
2778
+ console.log('');
2779
+ console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
2780
+ for (const check of result.frameworkChecklist.checks) {
2781
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
2782
+ }
2783
+ }
1963
2784
  }
1964
2785
 
1965
2786
  function budget(routeArg = '') {
@@ -2365,6 +3186,10 @@ async function main() {
2365
3186
  if (command === 'tools') return tools(process.argv.slice(3));
2366
3187
  if (command === 'proof') return proof(process.argv.slice(3));
2367
3188
  if (command === 'ueye') return ueye(process.argv.slice(3));
3189
+ if (command === 'media') return media(process.argv.slice(3));
3190
+ if (command === 'runtime') return runtime(process.argv.slice(3));
3191
+ if (command === 'mission') return mission(process.argv.slice(3));
3192
+ if (command === 'benchmark') return benchmark(process.argv.slice(3));
2368
3193
  if (command === 'release') return release(process.argv.slice(3));
2369
3194
  if (command === 'safety') return safety(process.argv.slice(3));
2370
3195
  if (command === 'memory') return memory(process.argv.slice(3));
@@ -2378,7 +3203,7 @@ async function main() {
2378
3203
  }
2379
3204
  if (command === 'list') return list();
2380
3205
  if (command === 'verify') return verify();
2381
- if (command === 'doctor') return doctor();
3206
+ if (command === 'doctor') return doctor(process.argv.slice(3));
2382
3207
  if (command === 'examples') return examples();
2383
3208
  if (command === 'path') return showPath();
2384
3209
  if (command === 'init-project') return initProject(process.argv[3]);