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/dist/bin/yam.js CHANGED
@@ -8,7 +8,7 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import process from 'node:process';
10
10
  import { fileURLToPath } from 'node:url';
11
- import { TRUTH_STATUSES, buildMissionPatchEnvelope, buildRollbackHint, buildUeyeVisualProvenance, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
11
+ import { TRUTH_STATUSES, buildMediaGenerationProof, buildMissionPatchEnvelope, buildRollbackHint, buildRuntimeBackendEvidence, buildUeyeVisualProvenance, buildUeyeRunReport, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
12
12
  function errorMessage(error) {
13
13
  return error instanceof Error ? error.message : String(error);
14
14
  }
@@ -122,6 +122,11 @@ Usage:
122
122
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
123
123
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
124
124
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
125
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
126
+ yam media proof [--requested] [--attempted] [--output file] [--json]
127
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
128
+ yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
129
+ yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
125
130
  yam release report [--json]
126
131
  yam safety [text...]
127
132
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -130,7 +135,7 @@ Usage:
130
135
  yam tune-log [dir]
131
136
  yam install
132
137
  yam uninstall
133
- yam doctor
138
+ yam doctor [--json]
134
139
  yam examples
135
140
  yam path
136
141
  yam version
@@ -359,7 +364,34 @@ function failOrReport(label, issues, quiet = false) {
359
364
  process.exitCode = 1;
360
365
  return issues.length;
361
366
  }
362
- async function doctor() {
367
+ async function doctor(args = []) {
368
+ const flags = parseSimpleFlags(args, new Set(['json']));
369
+ const report = await buildDoctorReport();
370
+ if (flags.json) {
371
+ console.log(JSON.stringify(report, null, 2));
372
+ if (!report.ok)
373
+ process.exitCode = 1;
374
+ return;
375
+ }
376
+ if (report.ok) {
377
+ console.log('yam doctor: ok');
378
+ console.log('No hooks, automations, or global config are required.');
379
+ console.log('Skill install state is reported by `yam status`.');
380
+ console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
381
+ return;
382
+ }
383
+ console.log('yam doctor: issues');
384
+ for (const issue of report.issues)
385
+ console.log(`- ${issue}`);
386
+ if (report.nextActions.length) {
387
+ console.log('');
388
+ console.log('Next actions:');
389
+ for (const action of report.nextActions)
390
+ console.log(`- ${action}`);
391
+ }
392
+ process.exitCode = 1;
393
+ }
394
+ async function buildDoctorReport() {
363
395
  const issues = [];
364
396
  for (const skill of SKILLS) {
365
397
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md')))
@@ -390,18 +422,112 @@ async function doctor() {
390
422
  const verifyIssues = await verify({ quiet: true });
391
423
  if (verifyIssues > 0)
392
424
  issues.push(`verify reported ${verifyIssues} issue(s)`);
393
- if (!issues.length) {
394
- console.log('yam doctor: ok');
395
- const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
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: ${hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'} (optional)`);
399
- return;
425
+ const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
426
+ const nextActionDetails = doctorNextActionDetails(issues);
427
+ return {
428
+ schema: 'yam.doctor.v1',
429
+ generatedAt: new Date().toISOString(),
430
+ ok: issues.length === 0,
431
+ issues,
432
+ nextActions: nextActionDetails.map((action) => action.next_action),
433
+ nextActionDetails,
434
+ yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
435
+ };
436
+ }
437
+ function doctorNextActions(issues = []) {
438
+ return doctorNextActionDetails(issues).map((action) => action.next_action);
439
+ }
440
+ function doctorNextActionDetails(issues = []) {
441
+ const actions = [];
442
+ for (const issue of issues) {
443
+ if (/missing source skill/i.test(issue))
444
+ 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'));
445
+ else if (/missing truth matrix/i.test(issue))
446
+ actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
447
+ else if (/unexpected local hooks/i.test(issue))
448
+ 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'));
449
+ else if (/unexpected .*automation/i.test(issue))
450
+ 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'));
451
+ else if (/legacy|retired/i.test(issue))
452
+ actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
453
+ else if (/verify reported/i.test(issue))
454
+ actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
455
+ }
456
+ return uniqueNextActionDetails(actions);
457
+ }
458
+ function nextActionDetail(id, severity, reason, nextAction, command = '') {
459
+ const priority = priorityForSeverity(severity);
460
+ const ownerRoute = ownerRouteForAction(id, reason, command);
461
+ return {
462
+ id,
463
+ severity,
464
+ priority,
465
+ owner_route: ownerRoute,
466
+ reason,
467
+ next_action: nextAction,
468
+ command,
469
+ tool_intent: toolIntentForCommand(command),
470
+ fix_first: priority === 'P0' || priority === 'P1',
471
+ blocks_release: priority === 'P0',
472
+ truth_status: 'partial'
473
+ };
474
+ }
475
+ function priorityForSeverity(severity = '') {
476
+ if (/error|danger|block/i.test(String(severity)))
477
+ return 'P0';
478
+ if (/warn/i.test(String(severity)))
479
+ return 'P1';
480
+ if (/info/i.test(String(severity)))
481
+ return 'P2';
482
+ return 'P3';
483
+ }
484
+ function ownerRouteForAction(id = '', reason = '', command = '') {
485
+ const text = `${id} ${reason} ${command}`;
486
+ if (/visual|ueye|screenshot|browser/i.test(text))
487
+ return '$ueye';
488
+ if (/release|publish|package|registry|dist|version/i.test(text))
489
+ return '$deep';
490
+ if (/database|supabase|destructive|production|migration/i.test(text))
491
+ return '$deep';
492
+ if (/mission|subagent|team|lane/i.test(text))
493
+ return '$mission';
494
+ if (/research|docs|current/i.test(text))
495
+ return '$scout';
496
+ return '$quick';
497
+ }
498
+ function toolIntentForCommand(command = '') {
499
+ const text = String(command || '').toLowerCase();
500
+ if (!text)
501
+ return 'read_only';
502
+ if (/(publish|registry|release|npm pack|npm publish)/i.test(text))
503
+ return 'publish';
504
+ if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text))
505
+ return 'destructive';
506
+ if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text))
507
+ return 'runtime';
508
+ if (/(ueye|visual|image|screen)/i.test(text))
509
+ return 'visual';
510
+ if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text))
511
+ return 'read_only';
512
+ return 'write';
513
+ }
514
+ function normalizeToolIntent(value = '') {
515
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
516
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text))
517
+ return text;
518
+ return 'read_only';
519
+ }
520
+ function uniqueNextActionDetails(actions = []) {
521
+ const seen = new Set();
522
+ const unique = [];
523
+ for (const action of actions) {
524
+ const key = `${action.id}:${action.next_action}`;
525
+ if (seen.has(key))
526
+ continue;
527
+ seen.add(key);
528
+ unique.push(action);
400
529
  }
401
- console.log('yam doctor: issues');
402
- for (const issue of issues)
403
- console.log(`- ${issue}`);
404
- process.exitCode = 1;
530
+ return unique;
405
531
  }
406
532
  async function tools(args = []) {
407
533
  const subcommand = args[0] || 'doctor';
@@ -460,6 +586,7 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
460
586
  packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
461
587
  ].join('\n'));
462
588
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
589
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
463
590
  const sqlScan = await scanSqlFiles(dir);
464
591
  const rows = [
465
592
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
@@ -479,6 +606,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
479
606
  ...safety.hits,
480
607
  ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
481
608
  ];
609
+ const nextActionDetails = toolsDoctorNextActionDetails({
610
+ pack,
611
+ rows,
612
+ riskNotes,
613
+ frameworkChecklist,
614
+ commands: detection.packageJson ? detection.commands : {}
615
+ });
482
616
  return {
483
617
  schemaVersion: 1,
484
618
  generatedAt: new Date().toISOString(),
@@ -489,8 +623,11 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
489
623
  packageManager: detection.packageJson ? detection.packageManager : null,
490
624
  instructionSurfaces,
491
625
  projectSurfaces,
626
+ frameworkChecklist,
492
627
  sqlScan,
493
628
  riskNotes,
629
+ nextActions: nextActionDetails.map((action) => action.next_action),
630
+ nextActionDetails,
494
631
  routeRecommendations: {
495
632
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
496
633
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -522,6 +659,13 @@ function printToolsDoctorReport(data) {
522
659
  for (const surface of data.projectSurfaces)
523
660
  console.log(`- ${surface}`);
524
661
  }
662
+ if (data.frameworkChecklist?.detected) {
663
+ console.log('');
664
+ console.log(`Framework checklist: ${data.frameworkChecklist.framework}`);
665
+ for (const check of data.frameworkChecklist.checks) {
666
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
667
+ }
668
+ }
525
669
  if (data.sqlScan.filesScanned > 0) {
526
670
  console.log('');
527
671
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -535,6 +679,12 @@ function printToolsDoctorReport(data) {
535
679
  for (const note of data.riskNotes)
536
680
  console.log(`- ${note.level}: ${note.reason}`);
537
681
  }
682
+ if (data.nextActions?.length) {
683
+ console.log('');
684
+ console.log('Next actions:');
685
+ for (const action of data.nextActions)
686
+ console.log(`- ${action}`);
687
+ }
538
688
  console.log('');
539
689
  console.log('Route recommendations:');
540
690
  console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
@@ -611,6 +761,56 @@ async function detectProjectToolSurfaces(dir, pkg = null) {
611
761
  surfaces.push('.env file detected; values were not read');
612
762
  return surfaces;
613
763
  }
764
+ async function detectFrameworkChecklist(dir, pkg = null) {
765
+ const deps = {
766
+ ...pkg?.dependencies,
767
+ ...pkg?.devDependencies
768
+ };
769
+ const depNames = Object.keys(deps || {});
770
+ const hasReact = depNames.includes('react');
771
+ 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'));
772
+ const hasVite = depNames.includes('vite') || await exists(path.join(dir, 'vite.config.js')) || await exists(path.join(dir, 'vite.config.ts'));
773
+ const framework = hasNext ? 'next-app' : hasReact && hasVite ? 'react-vite' : hasReact ? 'react' : pkg ? 'node-package' : 'unknown';
774
+ const uiLike = hasReact || hasNext || hasVite || await exists(path.join(dir, 'app')) || await exists(path.join(dir, 'src', 'app')) || await exists(path.join(dir, 'pages'));
775
+ const checks = [];
776
+ if (uiLike) {
777
+ checks.push(checklistRow('states', 'default/loading/error/empty states are reachable before claiming complete', '$ueye'), checklistRow('responsive', 'mobile and desktop layouts avoid overlap and clipped text', '$ueye'), checklistRow('accessibility', 'interactive controls have accessible labels and keyboard path', '$ueye'), checklistRow('runtime', 'browser or in-app runtime evidence is recorded when claiming visual verified', '$ueye'));
778
+ }
779
+ if (hasNext) {
780
+ checks.push(checklistRow('boundaries', 'server/client boundaries and async data states match the framework pattern', '$deep when risky'));
781
+ }
782
+ if (pkg) {
783
+ checks.push(checklistRow('package', 'run the smallest honest type/build/package check before publishing', '$quick or $deep'));
784
+ }
785
+ return {
786
+ schema: 'yam.framework-checklist.v1',
787
+ detected: checks.length > 0,
788
+ uiLike,
789
+ framework,
790
+ routeHint: uiLike ? '$ueye for visual claims; $deep when runtime/data risk appears' : '$quick by default; deepen on risk',
791
+ checks
792
+ };
793
+ }
794
+ function checklistRow(id, label, route) {
795
+ return { id, label, route };
796
+ }
797
+ function toolsDoctorNextActions(input) {
798
+ return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
799
+ }
800
+ function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
801
+ const actions = [];
802
+ if (!pack)
803
+ 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 .'));
804
+ if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready'))
805
+ actions.push(nextActionDetail('install-skills', 'warning', 'skill install state is incomplete', 'run `yam install` and restart Codex before relying on installed skills', 'yam install'));
806
+ if (riskNotes.some((note) => note.level === 'danger'))
807
+ 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>"'));
808
+ if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck)
809
+ actions.push(nextActionDetail('verification-command', 'warning', 'build/typecheck command not detected', 'add or document a small build/typecheck command for implementation verification', 'yam pack .'));
810
+ if (frameworkChecklist?.uiLike)
811
+ 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
+ }
614
814
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
615
815
  const root = path.resolve(rootDir || process.cwd());
616
816
  const files = [];
@@ -956,11 +1156,26 @@ async function release(args = []) {
956
1156
  if (check.note)
957
1157
  console.log(` ${check.note}`);
958
1158
  }
1159
+ if (report.provenance) {
1160
+ console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
1161
+ console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
1162
+ console.log(`- Dist freshness: ${report.freshness.dist}`);
1163
+ }
1164
+ if (report.tarball) {
1165
+ console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
1166
+ if (report.tarball.file_count !== undefined)
1167
+ console.log(`- Tarball files: ${report.tarball.file_count}`);
1168
+ }
959
1169
  if (report.failed.length) {
960
1170
  console.log('- Failed:');
961
1171
  for (const id of report.failed)
962
1172
  console.log(` - ${id}`);
963
1173
  }
1174
+ if (report.next_actions?.length) {
1175
+ console.log('- Next actions:');
1176
+ for (const action of report.next_actions)
1177
+ console.log(` - ${action.next_action}`);
1178
+ }
964
1179
  if (!report.ok)
965
1180
  process.exitCode = 1;
966
1181
  }
@@ -975,6 +1190,10 @@ function runReleaseReport() {
975
1190
  ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
976
1191
  ].map(([id, command]) => runReleaseCheck(id, command));
977
1192
  const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
1193
+ const provenance = releaseProvenance(checks);
1194
+ const tarball = releaseTarballProvenance();
1195
+ const freshness = releaseFreshness(checks, provenance, tarball);
1196
+ const nextActions = releaseNextActions(checks, provenance, tarball);
978
1197
  return {
979
1198
  schema: 'yam.release-report.v1',
980
1199
  generated_at: startedAt,
@@ -983,9 +1202,114 @@ function runReleaseReport() {
983
1202
  ok: failed.length === 0,
984
1203
  truth_status: failed.length === 0 ? 'verified' : 'blocked',
985
1204
  checks,
986
- failed
1205
+ failed,
1206
+ provenance,
1207
+ tarball,
1208
+ freshness,
1209
+ next_actions: nextActions
1210
+ };
1211
+ }
1212
+ function releaseProvenance(checks = []) {
1213
+ const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
1214
+ const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
1215
+ const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
1216
+ const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
1217
+ return {
1218
+ schema: 'yam.release-provenance.v1',
1219
+ package_name: PACKAGE_JSON.name,
1220
+ version: VERSION,
1221
+ git_commit: gitCommit.trim(),
1222
+ git_dirty: Boolean(gitStatus.trim()),
1223
+ git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1224
+ dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1225
+ package_files: packageFiles,
1226
+ generated_at: new Date().toISOString(),
1227
+ truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
1228
+ };
1229
+ }
1230
+ function releaseTarballProvenance() {
1231
+ const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
1232
+ cwd: ROOT,
1233
+ encoding: 'utf8',
1234
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
1235
+ timeout: 120000
1236
+ });
1237
+ if (result.status !== 0) {
1238
+ return {
1239
+ schema: 'yam.release-tarball-provenance.v1',
1240
+ status: 'blocked',
1241
+ reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
1242
+ truth_status: 'blocked'
1243
+ };
1244
+ }
1245
+ try {
1246
+ const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
1247
+ return {
1248
+ schema: 'yam.release-tarball-provenance.v1',
1249
+ status: 'checked',
1250
+ tarball_name: String(data.filename || ''),
1251
+ package_size: Number(data.size || data.packageSize || 0),
1252
+ unpacked_size: Number(data.unpackedSize || 0),
1253
+ file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
1254
+ integrity: String(data.integrity || ''),
1255
+ shasum: String(data.shasum || ''),
1256
+ truth_status: data.filename ? 'verified' : 'partial'
1257
+ };
1258
+ }
1259
+ catch (error) {
1260
+ return {
1261
+ schema: 'yam.release-tarball-provenance.v1',
1262
+ status: 'blocked',
1263
+ reason: `could not parse npm pack output: ${errorMessage(error)}`,
1264
+ truth_status: 'blocked'
1265
+ };
1266
+ }
1267
+ }
1268
+ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
1269
+ const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
1270
+ const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
1271
+ const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
1272
+ const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
1273
+ const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
1274
+ const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
1275
+ return {
1276
+ schema: 'yam.release-freshness.v1',
1277
+ dist,
1278
+ registry,
1279
+ package_boundary: boundary,
1280
+ cli_smoke: smoke,
1281
+ tarball: tarballStatus,
1282
+ truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
987
1283
  };
988
1284
  }
1285
+ function releaseNextActions(checks = [], provenance = {}, tarball = {}) {
1286
+ const actions = [];
1287
+ for (const check of checks) {
1288
+ if (check.status === 'passed')
1289
+ continue;
1290
+ 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));
1291
+ }
1292
+ if (provenance.git_dirty) {
1293
+ 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'));
1294
+ }
1295
+ if (provenance.dist_changed_files?.length) {
1296
+ 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'));
1297
+ }
1298
+ if (tarball.status !== 'checked') {
1299
+ 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'));
1300
+ }
1301
+ return uniqueNextActionDetails(actions);
1302
+ }
1303
+ function runReadOnlyCommand(command, args = []) {
1304
+ const result = spawnSync(command, args, {
1305
+ cwd: ROOT,
1306
+ encoding: 'utf8',
1307
+ timeout: 30000
1308
+ });
1309
+ if (result.status !== 0)
1310
+ return '';
1311
+ return String(result.stdout || '');
1312
+ }
989
1313
  function runReleaseCheck(id, commandSpec) {
990
1314
  const [command, args] = commandSpec;
991
1315
  const start = Date.now();
@@ -1029,6 +1353,8 @@ async function ueye(args = []) {
1029
1353
  return ueyeCapture(args.slice(1));
1030
1354
  if (subcommand === 'compare')
1031
1355
  return ueyeCompare(args.slice(1));
1356
+ if (subcommand === 'report')
1357
+ return ueyeReport(args.slice(1));
1032
1358
  console.error(`unknown ueye command: ${subcommand}`);
1033
1359
  return ueyeUsage();
1034
1360
  }
@@ -1040,10 +1366,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1040
1366
  Usage:
1041
1367
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1042
1368
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1369
+ 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]
1043
1370
 
1044
1371
  Notes:
1045
1372
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1046
1373
  compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1374
+ report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
1047
1375
  `);
1048
1376
  }
1049
1377
  async function ueyeCapture(args = []) {
@@ -1165,6 +1493,447 @@ async function ueyeCompare(args = []) {
1165
1493
  process.exitCode = 1;
1166
1494
  }
1167
1495
  }
1496
+ async function ueyeReport(args = []) {
1497
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1498
+ return ueyeUsage();
1499
+ 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']));
1500
+ const reference = String(flags.reference || flags.ref || '');
1501
+ const actual = String(flags.actual || flags.screenshot || '');
1502
+ const referenceSources = [];
1503
+ const implementationSources = [];
1504
+ let comparisonResult = 'not-verified';
1505
+ let blockedReason = String(flags.blocked_reason || '');
1506
+ const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
1507
+ const previousReport = await readPreviousUeyeReport(flags.previous_report);
1508
+ try {
1509
+ if (reference) {
1510
+ const info = await imageFileInfo(reference);
1511
+ referenceSources.push(buildUeyeVisualProvenance({
1512
+ source_kind: 'reference_image',
1513
+ source_path: info.path,
1514
+ source_hash: info.sha256,
1515
+ reference_id: String(flags.reference_id || path.basename(info.path)),
1516
+ local_only: true,
1517
+ operator_provided: true,
1518
+ comparison_result: 'reference-recorded',
1519
+ truth_status: 'partial'
1520
+ }));
1521
+ }
1522
+ if (actual) {
1523
+ const info = await imageFileInfo(actual);
1524
+ implementationSources.push(buildUeyeVisualProvenance({
1525
+ source_kind: 'implementation_screenshot',
1526
+ source_path: info.path,
1527
+ source_hash: info.sha256,
1528
+ reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
1529
+ screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
1530
+ local_only: true,
1531
+ operator_provided: false,
1532
+ comparison_result: 'implementation-recorded',
1533
+ truth_status: 'partial'
1534
+ }));
1535
+ }
1536
+ if (reference && actual && referenceSources[0]?.source_hash === implementationSources[0]?.source_hash) {
1537
+ comparisonResult = 'matched';
1538
+ }
1539
+ else if (reference && actual) {
1540
+ comparisonResult = 'different';
1541
+ }
1542
+ }
1543
+ catch (error) {
1544
+ blockedReason = errorMessage(error);
1545
+ }
1546
+ const report = buildUeyeRunReport({
1547
+ reference_sources: referenceSources,
1548
+ implementation_sources: implementationSources,
1549
+ comparison_result: comparisonResult,
1550
+ design_quality: String(flags.design_quality || 'not-checked'),
1551
+ blocked_reason: blockedReason,
1552
+ next_action: String(flags.next_visual_action || flags.next_action || '')
1553
+ });
1554
+ const comparisonReport = buildUeyeComparisonReport({
1555
+ reviewSessionId,
1556
+ previousReport,
1557
+ referenceSources,
1558
+ implementationSources,
1559
+ comparisonResult,
1560
+ designQuality: String(flags.design_quality || 'not-checked'),
1561
+ similar: arrayFlag(flags.similar),
1562
+ different: arrayFlag(flags.different),
1563
+ missing: arrayFlag(flags.missing),
1564
+ notes: arrayFlag(flags.comparison_notes),
1565
+ resolved: arrayFlag(flags.resolved),
1566
+ newFindings: arrayFlag(flags.new_finding),
1567
+ stillOpen: arrayFlag(flags.still_open),
1568
+ regressions: arrayFlag(flags.regression),
1569
+ previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
1570
+ currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
1571
+ viewport: String(flags.viewport || ''),
1572
+ state: String(flags.state || 'unknown'),
1573
+ nextAction: report.next_action,
1574
+ truthStatus: report.truth_status
1575
+ });
1576
+ const result = {
1577
+ ...report,
1578
+ review_session_id: reviewSessionId,
1579
+ tool_intent: 'visual',
1580
+ capture_backend: String(flags.capture_backend || 'not-recorded'),
1581
+ compare_backend: String(flags.compare_backend || 'local-file-hash'),
1582
+ continuity: comparisonReport.continuity,
1583
+ comparison_report: comparisonReport
1584
+ };
1585
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1586
+ if (report.truth_status === 'blocked')
1587
+ process.exitCode = 1;
1588
+ }
1589
+ async function readPreviousUeyeReport(file = '') {
1590
+ const target = String(file || '');
1591
+ if (!target)
1592
+ return null;
1593
+ try {
1594
+ const data = await readJson(path.resolve(expandHome(target)));
1595
+ return {
1596
+ path: path.resolve(expandHome(target)),
1597
+ review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
1598
+ screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
1599
+ truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
1600
+ comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
1601
+ };
1602
+ }
1603
+ catch (error) {
1604
+ return {
1605
+ path: path.resolve(expandHome(target)),
1606
+ blocked_reason: errorMessage(error),
1607
+ truth_status: 'blocked',
1608
+ comparison_result: 'not-verified'
1609
+ };
1610
+ }
1611
+ }
1612
+ function buildUeyeComparisonReport({ reviewSessionId, previousReport, referenceSources, implementationSources, comparisonResult, designQuality, similar, different, missing, notes, resolved, newFindings, stillOpen, regressions, previousScreenshotId, currentScreenshotId, viewport, state, nextAction, truthStatus }) {
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
+ async function media(args = []) {
1655
+ const subcommand = args[0] || 'help';
1656
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1657
+ return mediaUsage();
1658
+ if (subcommand === 'proof')
1659
+ return mediaProof(args.slice(1));
1660
+ console.error(`unknown media command: ${subcommand}`);
1661
+ return mediaUsage();
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
+ async function mediaProof(args = []) {
1673
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1674
+ 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
+ }
1686
+ catch (error) {
1687
+ blockedReason = blockedReason || errorMessage(error);
1688
+ outputPath = path.resolve(expandHome(output));
1689
+ }
1690
+ }
1691
+ const proof = buildMediaGenerationProof({
1692
+ tool_name: String(flags.tool || ''),
1693
+ generation_requested: Boolean(flags.requested),
1694
+ generation_attempted: Boolean(flags.attempted || output),
1695
+ output_path: outputPath,
1696
+ output_hash: outputHash,
1697
+ wait_loop_checked: Boolean(flags.wait_loop),
1698
+ blocked_reason: blockedReason,
1699
+ next_action: String(flags.next_action || '')
1700
+ });
1701
+ printJsonOrHuman(proof, Boolean(flags.json), 'Media proof');
1702
+ if (proof.truth_status === 'blocked')
1703
+ process.exitCode = 1;
1704
+ }
1705
+ async function runtime(args = []) {
1706
+ const subcommand = args[0] || 'help';
1707
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1708
+ return runtimeUsage();
1709
+ if (subcommand === 'evidence')
1710
+ return runtimeEvidence(args.slice(1));
1711
+ console.error(`unknown runtime command: ${subcommand}`);
1712
+ return runtimeUsage();
1713
+ }
1714
+ function runtimeUsage() {
1715
+ console.log(`yam runtime
1716
+
1717
+ Usage:
1718
+ 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]
1719
+
1720
+ Notes:
1721
+ Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
1722
+ `);
1723
+ }
1724
+ async function runtimeEvidence(args = []) {
1725
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1726
+ return runtimeUsage();
1727
+ 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']));
1728
+ const evidence = buildRuntimeBackendEvidence({
1729
+ backend: flags.backend,
1730
+ claim: flags.claim,
1731
+ evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1732
+ command: String(flags.command || ''),
1733
+ cleanup_checked: Boolean(flags.cleanup_checked),
1734
+ note: String(flags.note || ''),
1735
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1736
+ });
1737
+ const runtimeDetails = runtimeEvidenceDetails(flags);
1738
+ if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
1739
+ evidence.truth_status = 'partial';
1740
+ }
1741
+ const result = {
1742
+ ...evidence,
1743
+ runtime_details: runtimeDetails,
1744
+ tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
1745
+ next_action: runtimeEvidenceNextAction(evidence)
1746
+ };
1747
+ printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
1748
+ if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing')
1749
+ process.exitCode = 1;
1750
+ }
1751
+ function runtimeEvidenceDetails(flags = {}) {
1752
+ return {
1753
+ schema: 'yam.runtime-details.v1',
1754
+ pid: numberOrNull(flags.pid),
1755
+ port: numberOrNull(flags.port),
1756
+ url: String(flags.url || ''),
1757
+ exit_code: numberOrNull(flags.exit_code),
1758
+ screenshot_id: String(flags.screenshot_id || ''),
1759
+ started_at: String(flags.started_at || ''),
1760
+ stopped_at: String(flags.stopped_at || '')
1761
+ };
1762
+ }
1763
+ function runtimeDetailsHasVerification(details = {}) {
1764
+ return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
1765
+ }
1766
+ function runtimeEvidenceNextAction(evidence) {
1767
+ if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified')
1768
+ return 'attach this evidence to `yam proof` when making a runtime claim';
1769
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked)
1770
+ return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
1771
+ if (evidence.backend === 'none' || evidence.backend === 'unknown')
1772
+ return 'record the actual runtime backend before claiming runtime verification';
1773
+ return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1774
+ }
1775
+ async function mission(args = []) {
1776
+ const subcommand = args[0] || 'help';
1777
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1778
+ return missionUsage();
1779
+ if (subcommand === 'queue' || subcommand === 'patch')
1780
+ return missionQueue(args.slice(1));
1781
+ console.error(`unknown mission command: ${subcommand}`);
1782
+ return missionUsage();
1783
+ }
1784
+ function missionUsage() {
1785
+ console.log(`yam mission
1786
+
1787
+ Usage:
1788
+ 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]
1789
+
1790
+ Notes:
1791
+ Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
1792
+ `);
1793
+ }
1794
+ async function missionQueue(args = []) {
1795
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1796
+ return missionUsage();
1797
+ 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']));
1798
+ const changedFiles = arrayFlag(flags.changed);
1799
+ const generatedFiles = arrayFlag(flags.generated);
1800
+ const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
1801
+ const status = normalizeMissionQueueStatus(flags.status || 'pending');
1802
+ const rollback = buildRollbackHint({
1803
+ touched_files: changedFiles,
1804
+ generated_files: generatedFiles,
1805
+ before_check: String(flags.before_check || ''),
1806
+ safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
1807
+ });
1808
+ const envelope = buildMissionPatchEnvelope({
1809
+ agent_id: String(flags.agent_id || `agent-${timestampId()}`),
1810
+ assigned_scope: String(flags.scope || ''),
1811
+ changed_files: changedFiles,
1812
+ verification_hint: String(flags.verification_hint || ''),
1813
+ rollback_hint: rollback,
1814
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
1815
+ });
1816
+ const item = {
1817
+ schema: 'yam.mission-patch-queue-item.v1',
1818
+ lane_id: laneId,
1819
+ status,
1820
+ depends_on: arrayFlag(flags.depends_on),
1821
+ tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
1822
+ patch_envelope: envelope,
1823
+ next_action: missionQueueNextAction(envelope, status)
1824
+ };
1825
+ const result = {
1826
+ schema: 'yam.mission-patch-queue-lite.v1',
1827
+ generated_at: new Date().toISOString(),
1828
+ persistence: flags.out ? 'written' : 'not-persisted',
1829
+ out: flags.out ? path.resolve(expandHome(flags.out)) : '',
1830
+ items: [item],
1831
+ queue_depth: 1,
1832
+ next_action: item.next_action,
1833
+ truth_status: envelope.truth_status
1834
+ };
1835
+ if (flags.out) {
1836
+ const target = path.resolve(expandHome(flags.out));
1837
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1838
+ await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
1839
+ }
1840
+ printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
1841
+ }
1842
+ function normalizeMissionQueueStatus(value = '') {
1843
+ const text = String(value || '').toLowerCase();
1844
+ if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text))
1845
+ return text;
1846
+ return 'pending';
1847
+ }
1848
+ function missionQueueNextAction(envelope, status = 'pending') {
1849
+ if (status === 'blocked')
1850
+ return 'resolve or document the blocker before applying dependent mission lanes';
1851
+ if (status === 'reverted')
1852
+ return 'rerun the before-check or verification hint before continuing dependent work';
1853
+ if (!envelope.assigned_scope)
1854
+ return 'add --scope so the patch can be reviewed against an assigned boundary';
1855
+ if (!envelope.changed_files.length)
1856
+ return 'add at least one --changed file when this represents code-changing work';
1857
+ if (!envelope.verification_hint)
1858
+ return 'add --verification-hint so reviewers know the smallest honest check';
1859
+ if (!envelope.rollback_hint.safe_revert_note && !envelope.rollback_hint.before_check)
1860
+ return 'add rollback or before-check detail before using this for risky work';
1861
+ return 'attach this envelope to the mission proof summary and cross-check changed files';
1862
+ }
1863
+ async function benchmark(args = []) {
1864
+ const subcommand = args[0] || 'help';
1865
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1866
+ return benchmarkUsage();
1867
+ if (subcommand === 'report')
1868
+ return benchmarkReport(args.slice(1));
1869
+ console.error(`unknown benchmark command: ${subcommand}`);
1870
+ return benchmarkUsage();
1871
+ }
1872
+ function benchmarkUsage() {
1873
+ console.log(`yam benchmark
1874
+
1875
+ Usage:
1876
+ yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
1877
+
1878
+ Notes:
1879
+ Records a small optimization loop result. It does not run benchmarks by itself.
1880
+ `);
1881
+ }
1882
+ async function benchmarkReport(args = []) {
1883
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1884
+ return benchmarkUsage();
1885
+ const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
1886
+ const baseline = numberOrNull(flags.baseline);
1887
+ const current = numberOrNull(flags.current);
1888
+ const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
1889
+ const hasNumbers = baseline !== null && current !== null;
1890
+ const delta = hasNumbers ? current - baseline : null;
1891
+ const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
1892
+ const status = benchmarkStatus({ baseline, current, target });
1893
+ const result = {
1894
+ schema: 'yam.benchmark-report.v1',
1895
+ generated_at: new Date().toISOString(),
1896
+ label: String(flags.label || 'unnamed'),
1897
+ baseline,
1898
+ current,
1899
+ unit: String(flags.unit || ''),
1900
+ target,
1901
+ delta,
1902
+ percent,
1903
+ status,
1904
+ tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
1905
+ 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')),
1906
+ next_action: String(flags.next_action || benchmarkNextAction(status)),
1907
+ truth_status: hasNumbers ? 'verified' : 'partial'
1908
+ };
1909
+ printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
1910
+ if (status === 'regressed')
1911
+ process.exitCode = 1;
1912
+ }
1913
+ function numberOrNull(value) {
1914
+ if (value === undefined || value === null || value === '')
1915
+ return null;
1916
+ const number = Number(value);
1917
+ return Number.isFinite(number) ? number : null;
1918
+ }
1919
+ function benchmarkStatus({ baseline, current, target }) {
1920
+ if (baseline === null || current === null)
1921
+ return 'needs-baseline';
1922
+ if (current === baseline)
1923
+ return 'unchanged';
1924
+ if (target === 'higher')
1925
+ return current > baseline ? 'improved' : 'regressed';
1926
+ return current < baseline ? 'improved' : 'regressed';
1927
+ }
1928
+ function benchmarkNextAction(status) {
1929
+ if (status === 'improved')
1930
+ return 'record the measurement source and keep the smallest relevant regression check';
1931
+ if (status === 'regressed')
1932
+ return 'treat this as a fix-first item before continuing planned optimization';
1933
+ if (status === 'unchanged')
1934
+ return 'keep the change only if it improves clarity or enables the next measured step';
1935
+ return 'capture baseline and current values before claiming optimization impact';
1936
+ }
1168
1937
  function parseSimpleFlags(args = [], allowed = new Set()) {
1169
1938
  const flags = {};
1170
1939
  const positionals = [];
@@ -1179,14 +1948,19 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
1179
1948
  if (allowed.size && !allowed.has(key))
1180
1949
  continue;
1181
1950
  const normalizedKey = key.replace(/-/g, '_');
1182
- if (['json', 'full-page'].includes(key)) {
1951
+ if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked'].includes(key)) {
1183
1952
  flags[normalizedKey] = true;
1184
1953
  continue;
1185
1954
  }
1186
1955
  const value = inlineValue ?? args[index + 1] ?? '';
1187
1956
  if (inlineValue === undefined)
1188
1957
  index += 1;
1189
- flags[normalizedKey] = value;
1958
+ if (flags[normalizedKey] !== undefined) {
1959
+ flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1960
+ }
1961
+ else {
1962
+ flags[normalizedKey] = value;
1963
+ }
1190
1964
  }
1191
1965
  flags._ = positionals;
1192
1966
  return flags;
@@ -1326,9 +2100,11 @@ async function proof(args = []) {
1326
2100
  printProofList('Evidence', summary.evidence);
1327
2101
  printProofList('Visual evidence', summary.visual);
1328
2102
  printProofList('Runtime evidence', summary.runtime);
2103
+ printProofList('Runtime backend evidence', summary.runtimeBackendEvidence || []);
1329
2104
  printProofList('Visual provenance', summary.visualProvenance || []);
1330
2105
  printProofList('Mission patch envelope', summary.missionEnvelope || []);
1331
2106
  printProofList('Rollback hint', summary.rollbackHint || []);
2107
+ printProofList('Media proof', summary.mediaProof || []);
1332
2108
  printProofList('Changed surfaces', summary.changed);
1333
2109
  printProofList('Skipped', summary.skipped);
1334
2110
  printProofList('Blocked', summary.blocked);
@@ -1404,6 +2180,11 @@ function buildProofSummary(parsed, artifact) {
1404
2180
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1405
2181
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1406
2182
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
2183
+ runtimeBackendEvidence: [
2184
+ ...structuredEvidenceList(artifact.runtimeBackendEvidence),
2185
+ ...structuredEvidenceList(parsed.flags.runtime_backend_evidence),
2186
+ ...directRuntimeBackendEvidence(parsed.flags)
2187
+ ],
1407
2188
  visualProvenance: [
1408
2189
  ...structuredEvidenceList(artifact.visualProvenance),
1409
2190
  ...structuredEvidenceList(parsed.flags.visual_provenance)
@@ -1416,6 +2197,10 @@ function buildProofSummary(parsed, artifact) {
1416
2197
  ...structuredEvidenceList(artifact.rollbackHint),
1417
2198
  ...structuredEvidenceList(parsed.flags.rollback_hint)
1418
2199
  ],
2200
+ mediaProof: [
2201
+ ...structuredEvidenceList(artifact.mediaProof),
2202
+ ...structuredEvidenceList(parsed.flags.media_proof)
2203
+ ],
1419
2204
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1420
2205
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1421
2206
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1465,6 +2250,9 @@ function renderProofMarkdown(summary) {
1465
2250
  '## Runtime evidence',
1466
2251
  ...renderProofMarkdownList(summary.runtime),
1467
2252
  '',
2253
+ '## Runtime backend evidence',
2254
+ ...renderProofMarkdownList(summary.runtimeBackendEvidence || []),
2255
+ '',
1468
2256
  '## Visual provenance',
1469
2257
  ...renderProofMarkdownList(summary.visualProvenance || []),
1470
2258
  '',
@@ -1474,6 +2262,9 @@ function renderProofMarkdown(summary) {
1474
2262
  '## Rollback hint',
1475
2263
  ...renderProofMarkdownList(summary.rollbackHint || []),
1476
2264
  '',
2265
+ '## Media proof',
2266
+ ...renderProofMarkdownList(summary.mediaProof || []),
2267
+ '',
1477
2268
  '## Changed surfaces',
1478
2269
  ...renderProofMarkdownList(summary.changed),
1479
2270
  '',
@@ -1504,7 +2295,7 @@ function renderProofMarkdownList(values) {
1504
2295
  function parseProofArgs(args = []) {
1505
2296
  const flags = {};
1506
2297
  const positionals = [];
1507
- 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']);
2298
+ 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']);
1508
2299
  for (let index = 0; index < args.length; index += 1) {
1509
2300
  const arg = args[index];
1510
2301
  if (arg.startsWith('--')) {
@@ -1513,14 +2304,14 @@ function parseProofArgs(args = []) {
1513
2304
  if (!aliases.has(key))
1514
2305
  continue;
1515
2306
  const normalizedKey = key.replace(/-/g, '_');
1516
- if (key === 'json' || key.startsWith('require-')) {
2307
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
1517
2308
  flags[normalizedKey] = true;
1518
2309
  continue;
1519
2310
  }
1520
2311
  const value = inlineValue ?? args[index + 1] ?? '';
1521
2312
  if (inlineValue === undefined)
1522
2313
  index += 1;
1523
- if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
2314
+ if (['command', 'evidence', 'visual', 'runtime', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1524
2315
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1525
2316
  }
1526
2317
  else {
@@ -1559,9 +2350,11 @@ function emptyProofArtifact() {
1559
2350
  evidence: [],
1560
2351
  visual: [],
1561
2352
  runtime: [],
2353
+ runtimeBackendEvidence: [],
1562
2354
  visualProvenance: [],
1563
2355
  missionEnvelope: [],
1564
2356
  rollbackHint: [],
2357
+ mediaProof: [],
1565
2358
  cleanup: '',
1566
2359
  changed: [],
1567
2360
  skipped: [],
@@ -1583,9 +2376,11 @@ async function parseProofJson(file, empty) {
1583
2376
  evidence: arrayFlag(data.evidence),
1584
2377
  visual: arrayFlag(data.visual || data.visualEvidence),
1585
2378
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
2379
+ runtimeBackendEvidence: structuredEvidenceList(data.runtimeBackendEvidence || data.runtime_backend_evidence),
1586
2380
  visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1587
2381
  missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1588
2382
  rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
2383
+ mediaProof: structuredEvidenceList(data.mediaProof || data.media_proof),
1589
2384
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1590
2385
  changed: arrayFlag(data.changed || data.files),
1591
2386
  skipped: arrayFlag(data.skipped),
@@ -1611,9 +2406,11 @@ async function parseProofMarkdown(file, empty) {
1611
2406
  evidence: readProofList(text, 'Evidence'),
1612
2407
  visual: readProofList(text, 'Visual evidence'),
1613
2408
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
2409
+ runtimeBackendEvidence: readProofList(text, 'Runtime backend evidence'),
1614
2410
  visualProvenance: readProofList(text, 'Visual provenance'),
1615
2411
  missionEnvelope: readProofList(text, 'Mission patch envelope'),
1616
2412
  rollbackHint: readProofList(text, 'Rollback hint'),
2413
+ mediaProof: readProofList(text, 'Media proof'),
1617
2414
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1618
2415
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1619
2416
  skipped: readProofList(text, 'Skipped'),
@@ -1661,6 +2458,18 @@ function arrayFlag(value) {
1661
2458
  function structuredEvidenceList(value) {
1662
2459
  return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1663
2460
  }
2461
+ function directRuntimeBackendEvidence(flags) {
2462
+ if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked)
2463
+ return [];
2464
+ return [JSON.stringify(buildRuntimeBackendEvidence({
2465
+ backend: flags.runtime_backend,
2466
+ claim: flags.runtime_claim,
2467
+ evidence_id: flags.runtime_evidence_id,
2468
+ command: flags.runtime_command,
2469
+ cleanup_checked: Boolean(flags.cleanup_checked),
2470
+ note: flags.runtime_note
2471
+ }))];
2472
+ }
1664
2473
  function formatStructuredEvidence(value) {
1665
2474
  if (!value)
1666
2475
  return '';
@@ -1682,6 +2491,12 @@ function formatStructuredEvidence(value) {
1682
2491
  if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1683
2492
  return JSON.stringify(buildRollbackHint(data));
1684
2493
  }
2494
+ if (data.schema === 'yam.runtime-backend-evidence.v1' || data.backend || data.evidence_id) {
2495
+ return JSON.stringify(buildRuntimeBackendEvidence(data));
2496
+ }
2497
+ if (data.schema === 'yam.media-generation-proof.v1' || data.generation_requested || data.output_hash) {
2498
+ return JSON.stringify(buildMediaGenerationProof(data));
2499
+ }
1685
2500
  return JSON.stringify(data);
1686
2501
  }
1687
2502
  catch {
@@ -1690,7 +2505,7 @@ function formatStructuredEvidence(value) {
1690
2505
  }
1691
2506
  function hasProofEvidence(parsed) {
1692
2507
  const flags = parsed.flags || {};
1693
- return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
2508
+ 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);
1694
2509
  }
1695
2510
  function printProofList(label, values) {
1696
2511
  console.log(`- ${label}:`);
@@ -1944,7 +2759,8 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1944
2759
  lint: null,
1945
2760
  test: null,
1946
2761
  build: null
1947
- }
2762
+ },
2763
+ frameworkChecklist: null
1948
2764
  };
1949
2765
  if (!result.packageJson) {
1950
2766
  if (!quiet) {
@@ -1961,6 +2777,7 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1961
2777
  result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
1962
2778
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
1963
2779
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
2780
+ result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
1964
2781
  if (!quiet)
1965
2782
  printDetection(result);
1966
2783
  return result;
@@ -1978,6 +2795,13 @@ function printDetection(result) {
1978
2795
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
1979
2796
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
1980
2797
  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}`}`);
2798
+ if (result.frameworkChecklist?.detected) {
2799
+ console.log('');
2800
+ console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
2801
+ for (const check of result.frameworkChecklist.checks) {
2802
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
2803
+ }
2804
+ }
1981
2805
  }
1982
2806
  function budget(routeArg = '') {
1983
2807
  const normalized = normalizeRoute(routeArg);
@@ -2383,6 +3207,14 @@ async function main() {
2383
3207
  return proof(process.argv.slice(3));
2384
3208
  if (command === 'ueye')
2385
3209
  return ueye(process.argv.slice(3));
3210
+ if (command === 'media')
3211
+ return media(process.argv.slice(3));
3212
+ if (command === 'runtime')
3213
+ return runtime(process.argv.slice(3));
3214
+ if (command === 'mission')
3215
+ return mission(process.argv.slice(3));
3216
+ if (command === 'benchmark')
3217
+ return benchmark(process.argv.slice(3));
2386
3218
  if (command === 'release')
2387
3219
  return release(process.argv.slice(3));
2388
3220
  if (command === 'safety')
@@ -2406,7 +3238,7 @@ async function main() {
2406
3238
  if (command === 'verify')
2407
3239
  return verify();
2408
3240
  if (command === 'doctor')
2409
- return doctor();
3241
+ return doctor(process.argv.slice(3));
2410
3242
  if (command === 'examples')
2411
3243
  return examples();
2412
3244
  if (command === 'path')