yam-flow 0.1.3 → 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
@@ -124,6 +124,9 @@ Usage:
124
124
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
125
125
  yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
126
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]
127
130
  yam release report [--json]
128
131
  yam safety [text...]
129
132
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -420,32 +423,111 @@ async function buildDoctorReport() {
420
423
  if (verifyIssues > 0)
421
424
  issues.push(`verify reported ${verifyIssues} issue(s)`);
422
425
  const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
426
+ const nextActionDetails = doctorNextActionDetails(issues);
423
427
  return {
424
428
  schema: 'yam.doctor.v1',
425
429
  generatedAt: new Date().toISOString(),
426
430
  ok: issues.length === 0,
427
431
  issues,
428
- nextActions: doctorNextActions(issues),
432
+ nextActions: nextActionDetails.map((action) => action.next_action),
433
+ nextActionDetails,
429
434
  yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
430
435
  };
431
436
  }
432
437
  function doctorNextActions(issues = []) {
438
+ return doctorNextActionDetails(issues).map((action) => action.next_action);
439
+ }
440
+ function doctorNextActionDetails(issues = []) {
433
441
  const actions = [];
434
442
  for (const issue of issues) {
435
443
  if (/missing source skill/i.test(issue))
436
- actions.push('run from a complete yam-flow checkout or reinstall the published package');
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'));
437
445
  else if (/missing truth matrix/i.test(issue))
438
- actions.push('restore references/truth-matrix.md before publishing');
446
+ actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
439
447
  else if (/unexpected local hooks/i.test(issue))
440
- actions.push('remove project hooks unless this repo is intentionally dogfooding them');
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'));
441
449
  else if (/unexpected .*automation/i.test(issue))
442
- actions.push('remove stale yam/timeto automations before claiming clean install state');
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'));
443
451
  else if (/legacy|retired/i.test(issue))
444
- actions.push('run `yam install` to replace old skill entries');
452
+ actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
445
453
  else if (/verify reported/i.test(issue))
446
- actions.push('run `npm run verify` and fix the reported package boundary or metadata 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'));
447
455
  }
448
- return [...new Set(actions)];
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);
529
+ }
530
+ return unique;
449
531
  }
450
532
  async function tools(args = []) {
451
533
  const subcommand = args[0] || 'doctor';
@@ -524,6 +606,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
524
606
  ...safety.hits,
525
607
  ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
526
608
  ];
609
+ const nextActionDetails = toolsDoctorNextActionDetails({
610
+ pack,
611
+ rows,
612
+ riskNotes,
613
+ frameworkChecklist,
614
+ commands: detection.packageJson ? detection.commands : {}
615
+ });
527
616
  return {
528
617
  schemaVersion: 1,
529
618
  generatedAt: new Date().toISOString(),
@@ -537,13 +626,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
537
626
  frameworkChecklist,
538
627
  sqlScan,
539
628
  riskNotes,
540
- nextActions: toolsDoctorNextActions({
541
- pack,
542
- rows,
543
- riskNotes,
544
- frameworkChecklist,
545
- commands: detection.packageJson ? detection.commands : {}
546
- }),
629
+ nextActions: nextActionDetails.map((action) => action.next_action),
630
+ nextActionDetails,
547
631
  routeRecommendations: {
548
632
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
549
633
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -710,19 +794,22 @@ async function detectFrameworkChecklist(dir, pkg = null) {
710
794
  function checklistRow(id, label, route) {
711
795
  return { id, label, route };
712
796
  }
713
- function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
797
+ function toolsDoctorNextActions(input) {
798
+ return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
799
+ }
800
+ function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
714
801
  const actions = [];
715
802
  if (!pack)
716
- actions.push('create or refresh yam.project.md so route choices reuse local project context');
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 .'));
717
804
  if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready'))
718
- actions.push('run `yam install` and restart Codex before relying on installed skills');
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'));
719
806
  if (riskNotes.some((note) => note.level === 'danger'))
720
- actions.push('use $deep before claiming destructive database or production write safety');
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>"'));
721
808
  if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck)
722
- actions.push('add or document a small build/typecheck command for implementation verification');
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 .'));
723
810
  if (frameworkChecklist?.uiLike)
724
- actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
725
- return [...new Set(actions)];
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);
726
813
  }
727
814
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
728
815
  const root = path.resolve(rootDir || process.cwd());
@@ -1069,11 +1156,26 @@ async function release(args = []) {
1069
1156
  if (check.note)
1070
1157
  console.log(` ${check.note}`);
1071
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
+ }
1072
1169
  if (report.failed.length) {
1073
1170
  console.log('- Failed:');
1074
1171
  for (const id of report.failed)
1075
1172
  console.log(` - ${id}`);
1076
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
+ }
1077
1179
  if (!report.ok)
1078
1180
  process.exitCode = 1;
1079
1181
  }
@@ -1088,6 +1190,10 @@ function runReleaseReport() {
1088
1190
  ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
1089
1191
  ].map(([id, command]) => runReleaseCheck(id, command));
1090
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);
1091
1197
  return {
1092
1198
  schema: 'yam.release-report.v1',
1093
1199
  generated_at: startedAt,
@@ -1096,9 +1202,114 @@ function runReleaseReport() {
1096
1202
  ok: failed.length === 0,
1097
1203
  truth_status: failed.length === 0 ? 'verified' : 'blocked',
1098
1204
  checks,
1099
- 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'
1100
1283
  };
1101
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
+ }
1102
1313
  function runReleaseCheck(id, commandSpec) {
1103
1314
  const [command, args] = commandSpec;
1104
1315
  const start = Date.now();
@@ -1155,12 +1366,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1155
1366
  Usage:
1156
1367
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1157
1368
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1158
- yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--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]
1159
1370
 
1160
1371
  Notes:
1161
1372
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1162
1373
  compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1163
- report produces a proof-ready Ueye visual run report without requiring a new capture.
1374
+ report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
1164
1375
  `);
1165
1376
  }
1166
1377
  async function ueyeCapture(args = []) {
@@ -1285,13 +1496,15 @@ async function ueyeCompare(args = []) {
1285
1496
  async function ueyeReport(args = []) {
1286
1497
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1287
1498
  return ueyeUsage();
1288
- const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
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']));
1289
1500
  const reference = String(flags.reference || flags.ref || '');
1290
1501
  const actual = String(flags.actual || flags.screenshot || '');
1291
1502
  const referenceSources = [];
1292
1503
  const implementationSources = [];
1293
1504
  let comparisonResult = 'not-verified';
1294
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);
1295
1508
  try {
1296
1509
  if (reference) {
1297
1510
  const info = await imageFileInfo(reference);
@@ -1299,7 +1512,7 @@ async function ueyeReport(args = []) {
1299
1512
  source_kind: 'reference_image',
1300
1513
  source_path: info.path,
1301
1514
  source_hash: info.sha256,
1302
- reference_id: path.basename(info.path),
1515
+ reference_id: String(flags.reference_id || path.basename(info.path)),
1303
1516
  local_only: true,
1304
1517
  operator_provided: true,
1305
1518
  comparison_result: 'reference-recorded',
@@ -1312,7 +1525,8 @@ async function ueyeReport(args = []) {
1312
1525
  source_kind: 'implementation_screenshot',
1313
1526
  source_path: info.path,
1314
1527
  source_hash: info.sha256,
1315
- screenshot_id: path.basename(info.path),
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)),
1316
1530
  local_only: true,
1317
1531
  operator_provided: false,
1318
1532
  comparison_result: 'implementation-recorded',
@@ -1335,17 +1549,108 @@ async function ueyeReport(args = []) {
1335
1549
  comparison_result: comparisonResult,
1336
1550
  design_quality: String(flags.design_quality || 'not-checked'),
1337
1551
  blocked_reason: blockedReason,
1338
- next_action: String(flags.next_action || '')
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
1339
1575
  });
1340
1576
  const result = {
1341
1577
  ...report,
1578
+ review_session_id: reviewSessionId,
1579
+ tool_intent: 'visual',
1342
1580
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1343
- compare_backend: String(flags.compare_backend || 'local-file-hash')
1581
+ compare_backend: String(flags.compare_backend || 'local-file-hash'),
1582
+ continuity: comparisonReport.continuity,
1583
+ comparison_report: comparisonReport
1344
1584
  };
1345
1585
  printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1346
1586
  if (report.truth_status === 'blocked')
1347
1587
  process.exitCode = 1;
1348
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
+ }
1349
1654
  async function media(args = []) {
1350
1655
  const subcommand = args[0] || 'help';
1351
1656
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
@@ -1397,6 +1702,238 @@ async function mediaProof(args = []) {
1397
1702
  if (proof.truth_status === 'blocked')
1398
1703
  process.exitCode = 1;
1399
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
+ }
1400
1937
  function parseSimpleFlags(args = [], allowed = new Set()) {
1401
1938
  const flags = {};
1402
1939
  const positionals = [];
@@ -1418,7 +1955,12 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
1418
1955
  const value = inlineValue ?? args[index + 1] ?? '';
1419
1956
  if (inlineValue === undefined)
1420
1957
  index += 1;
1421
- flags[normalizedKey] = value;
1958
+ if (flags[normalizedKey] !== undefined) {
1959
+ flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1960
+ }
1961
+ else {
1962
+ flags[normalizedKey] = value;
1963
+ }
1422
1964
  }
1423
1965
  flags._ = positionals;
1424
1966
  return flags;
@@ -2667,6 +3209,12 @@ async function main() {
2667
3209
  return ueye(process.argv.slice(3));
2668
3210
  if (command === 'media')
2669
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));
2670
3218
  if (command === 'release')
2671
3219
  return release(process.argv.slice(3));
2672
3220
  if (command === 'safety')