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/src/bin/yam.ts CHANGED
@@ -141,6 +141,9 @@ Usage:
141
141
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
142
142
  yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
143
143
  yam media proof [--requested] [--attempted] [--output file] [--json]
144
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
145
+ yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
146
+ yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
144
147
  yam release report [--json]
145
148
  yam safety [text...]
146
149
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -430,27 +433,97 @@ async function buildDoctorReport() {
430
433
  const verifyIssues = await verify({ quiet: true });
431
434
  if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
432
435
  const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
436
+ const nextActionDetails = doctorNextActionDetails(issues);
433
437
  return {
434
438
  schema: 'yam.doctor.v1',
435
439
  generatedAt: new Date().toISOString(),
436
440
  ok: issues.length === 0,
437
441
  issues,
438
- nextActions: doctorNextActions(issues),
442
+ nextActions: nextActionDetails.map((action) => action.next_action),
443
+ nextActionDetails,
439
444
  yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
440
445
  };
441
446
  }
442
447
 
443
448
  function doctorNextActions(issues = []) {
449
+ return doctorNextActionDetails(issues).map((action) => action.next_action);
450
+ }
451
+
452
+ function doctorNextActionDetails(issues = []) {
444
453
  const actions = [];
445
454
  for (const issue of issues) {
446
- if (/missing source skill/i.test(issue)) actions.push('run from a complete yam-flow checkout or reinstall the published package');
447
- else if (/missing truth matrix/i.test(issue)) actions.push('restore references/truth-matrix.md before publishing');
448
- else if (/unexpected local hooks/i.test(issue)) actions.push('remove project hooks unless this repo is intentionally dogfooding them');
449
- else if (/unexpected .*automation/i.test(issue)) actions.push('remove stale yam/timeto automations before claiming clean install state');
450
- else if (/legacy|retired/i.test(issue)) actions.push('run `yam install` to replace old skill entries');
451
- else if (/verify reported/i.test(issue)) actions.push('run `npm run verify` and fix the reported package boundary or metadata issue');
455
+ if (/missing source skill/i.test(issue)) actions.push(nextActionDetail('restore-skill-source', 'warning', issue, 'run from a complete yam-flow checkout or reinstall the published package', 'npm install -g yam-flow@latest && yam install'));
456
+ else if (/missing truth matrix/i.test(issue)) actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
457
+ else if (/unexpected local hooks/i.test(issue)) actions.push(nextActionDetail('remove-project-hook', 'warning', issue, 'remove project hooks unless this repo is intentionally dogfooding them', 'inspect .codex/hooks.json and remove only intentional stale hooks'));
458
+ else if (/unexpected .*automation/i.test(issue)) actions.push(nextActionDetail('remove-stale-automation', 'warning', issue, 'remove stale yam/timeto automations before claiming clean install state', 'inspect ~/.codex/automations and remove stale entries manually'));
459
+ else if (/legacy|retired/i.test(issue)) actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
460
+ else if (/verify reported/i.test(issue)) actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
461
+ }
462
+ return uniqueNextActionDetails(actions);
463
+ }
464
+
465
+ function nextActionDetail(id, severity, reason, nextAction, command = '') {
466
+ const priority = priorityForSeverity(severity);
467
+ const ownerRoute = ownerRouteForAction(id, reason, command);
468
+ return {
469
+ id,
470
+ severity,
471
+ priority,
472
+ owner_route: ownerRoute,
473
+ reason,
474
+ next_action: nextAction,
475
+ command,
476
+ tool_intent: toolIntentForCommand(command),
477
+ fix_first: priority === 'P0' || priority === 'P1',
478
+ blocks_release: priority === 'P0',
479
+ truth_status: 'partial'
480
+ };
481
+ }
482
+
483
+ function priorityForSeverity(severity = '') {
484
+ if (/error|danger|block/i.test(String(severity))) return 'P0';
485
+ if (/warn/i.test(String(severity))) return 'P1';
486
+ if (/info/i.test(String(severity))) return 'P2';
487
+ return 'P3';
488
+ }
489
+
490
+ function ownerRouteForAction(id = '', reason = '', command = '') {
491
+ const text = `${id} ${reason} ${command}`;
492
+ if (/visual|ueye|screenshot|browser/i.test(text)) return '$ueye';
493
+ if (/release|publish|package|registry|dist|version/i.test(text)) return '$deep';
494
+ if (/database|supabase|destructive|production|migration/i.test(text)) return '$deep';
495
+ if (/mission|subagent|team|lane/i.test(text)) return '$mission';
496
+ if (/research|docs|current/i.test(text)) return '$scout';
497
+ return '$quick';
498
+ }
499
+
500
+ function toolIntentForCommand(command = '') {
501
+ const text = String(command || '').toLowerCase();
502
+ if (!text) return 'read_only';
503
+ if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
504
+ if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
505
+ if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
506
+ if (/(ueye|visual|image|screen)/i.test(text)) return 'visual';
507
+ if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text)) return 'read_only';
508
+ return 'write';
509
+ }
510
+
511
+ function normalizeToolIntent(value = '') {
512
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
513
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
514
+ return 'read_only';
515
+ }
516
+
517
+ function uniqueNextActionDetails(actions = []) {
518
+ const seen = new Set<string>();
519
+ const unique = [];
520
+ for (const action of actions) {
521
+ const key = `${action.id}:${action.next_action}`;
522
+ if (seen.has(key)) continue;
523
+ seen.add(key);
524
+ unique.push(action);
452
525
  }
453
- return [...new Set(actions)];
526
+ return unique;
454
527
  }
455
528
 
456
529
  async function tools(args = []) {
@@ -531,6 +604,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
531
604
  ...safety.hits,
532
605
  ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
533
606
  ];
607
+ const nextActionDetails = toolsDoctorNextActionDetails({
608
+ pack,
609
+ rows,
610
+ riskNotes,
611
+ frameworkChecklist,
612
+ commands: detection.packageJson ? detection.commands : {}
613
+ });
534
614
  return {
535
615
  schemaVersion: 1,
536
616
  generatedAt: new Date().toISOString(),
@@ -544,13 +624,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
544
624
  frameworkChecklist,
545
625
  sqlScan,
546
626
  riskNotes,
547
- nextActions: toolsDoctorNextActions({
548
- pack,
549
- rows,
550
- riskNotes,
551
- frameworkChecklist,
552
- commands: detection.packageJson ? detection.commands : {}
553
- }),
627
+ nextActions: nextActionDetails.map((action) => action.next_action),
628
+ nextActionDetails,
554
629
  routeRecommendations: {
555
630
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
556
631
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -723,14 +798,18 @@ function checklistRow(id, label, route) {
723
798
  return { id, label, route };
724
799
  }
725
800
 
726
- function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
801
+ function toolsDoctorNextActions(input) {
802
+ return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
803
+ }
804
+
805
+ function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
727
806
  const actions = [];
728
- if (!pack) actions.push('create or refresh yam.project.md so route choices reuse local project context');
729
- if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push('run `yam install` and restart Codex before relying on installed skills');
730
- if (riskNotes.some((note) => note.level === 'danger')) actions.push('use $deep before claiming destructive database or production write safety');
731
- if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push('add or document a small build/typecheck command for implementation verification');
732
- if (frameworkChecklist?.uiLike) actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
733
- return [...new Set(actions)];
807
+ if (!pack) actions.push(nextActionDetail('project-pack', 'info', 'project pack missing', 'create or refresh yam.project.md so route choices reuse local project context', 'yam init-project .'));
808
+ if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push(nextActionDetail('install-skills', 'warning', 'skill install state is incomplete', 'run `yam install` and restart Codex before relying on installed skills', 'yam install'));
809
+ if (riskNotes.some((note) => note.level === 'danger')) actions.push(nextActionDetail('deep-safety', 'error', 'destructive database or production write signal detected', 'use $deep before claiming destructive database or production write safety', 'yam safety "<command or SQL>"'));
810
+ if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push(nextActionDetail('verification-command', 'warning', 'build/typecheck command not detected', 'add or document a small build/typecheck command for implementation verification', 'yam pack .'));
811
+ if (frameworkChecklist?.uiLike) actions.push(nextActionDetail('ueye-evidence', 'info', 'UI-like project detected', 'for Ueye verified claims, record reference, actual screenshot, and comparison result', 'yam ueye report --reference ref.png --actual shot.png --json'));
812
+ return uniqueNextActionDetails(actions);
734
813
  }
735
814
 
736
815
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
@@ -1062,10 +1141,23 @@ async function release(args = []) {
1062
1141
  console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
1063
1142
  if (check.note) console.log(` ${check.note}`);
1064
1143
  }
1144
+ if (report.provenance) {
1145
+ console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
1146
+ console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
1147
+ console.log(`- Dist freshness: ${report.freshness.dist}`);
1148
+ }
1149
+ if (report.tarball) {
1150
+ console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
1151
+ if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
1152
+ }
1065
1153
  if (report.failed.length) {
1066
1154
  console.log('- Failed:');
1067
1155
  for (const id of report.failed) console.log(` - ${id}`);
1068
1156
  }
1157
+ if (report.next_actions?.length) {
1158
+ console.log('- Next actions:');
1159
+ for (const action of report.next_actions) console.log(` - ${action.next_action}`);
1160
+ }
1069
1161
  if (!report.ok) process.exitCode = 1;
1070
1162
  }
1071
1163
 
@@ -1080,6 +1172,10 @@ function runReleaseReport() {
1080
1172
  ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
1081
1173
  ].map(([id, command]) => runReleaseCheck(id, command));
1082
1174
  const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
1175
+ const provenance = releaseProvenance(checks);
1176
+ const tarball = releaseTarballProvenance();
1177
+ const freshness = releaseFreshness(checks, provenance, tarball);
1178
+ const nextActions = releaseNextActions(checks, provenance, tarball);
1083
1179
  return {
1084
1180
  schema: 'yam.release-report.v1',
1085
1181
  generated_at: startedAt,
@@ -1088,10 +1184,117 @@ function runReleaseReport() {
1088
1184
  ok: failed.length === 0,
1089
1185
  truth_status: failed.length === 0 ? 'verified' : 'blocked',
1090
1186
  checks,
1091
- failed
1187
+ failed,
1188
+ provenance,
1189
+ tarball,
1190
+ freshness,
1191
+ next_actions: nextActions
1192
+ };
1193
+ }
1194
+
1195
+ function releaseProvenance(checks = []) {
1196
+ const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
1197
+ const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
1198
+ const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
1199
+ const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
1200
+ return {
1201
+ schema: 'yam.release-provenance.v1',
1202
+ package_name: PACKAGE_JSON.name,
1203
+ version: VERSION,
1204
+ git_commit: gitCommit.trim(),
1205
+ git_dirty: Boolean(gitStatus.trim()),
1206
+ git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1207
+ dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1208
+ package_files: packageFiles,
1209
+ generated_at: new Date().toISOString(),
1210
+ truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
1211
+ };
1212
+ }
1213
+
1214
+ function releaseTarballProvenance() {
1215
+ const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
1216
+ cwd: ROOT,
1217
+ encoding: 'utf8',
1218
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
1219
+ timeout: 120000
1220
+ });
1221
+ if (result.status !== 0) {
1222
+ return {
1223
+ schema: 'yam.release-tarball-provenance.v1',
1224
+ status: 'blocked',
1225
+ reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
1226
+ truth_status: 'blocked'
1227
+ };
1228
+ }
1229
+ try {
1230
+ const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
1231
+ return {
1232
+ schema: 'yam.release-tarball-provenance.v1',
1233
+ status: 'checked',
1234
+ tarball_name: String(data.filename || ''),
1235
+ package_size: Number(data.size || data.packageSize || 0),
1236
+ unpacked_size: Number(data.unpackedSize || 0),
1237
+ file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
1238
+ integrity: String(data.integrity || ''),
1239
+ shasum: String(data.shasum || ''),
1240
+ truth_status: data.filename ? 'verified' : 'partial'
1241
+ };
1242
+ } catch (error) {
1243
+ return {
1244
+ schema: 'yam.release-tarball-provenance.v1',
1245
+ status: 'blocked',
1246
+ reason: `could not parse npm pack output: ${errorMessage(error)}`,
1247
+ truth_status: 'blocked'
1248
+ };
1249
+ }
1250
+ }
1251
+
1252
+ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1253
+ const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
1254
+ const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
1255
+ const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
1256
+ const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
1257
+ const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
1258
+ const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
1259
+ return {
1260
+ schema: 'yam.release-freshness.v1',
1261
+ dist,
1262
+ registry,
1263
+ package_boundary: boundary,
1264
+ cli_smoke: smoke,
1265
+ tarball: tarballStatus,
1266
+ truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
1092
1267
  };
1093
1268
  }
1094
1269
 
1270
+ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1271
+ const actions = [];
1272
+ for (const check of checks) {
1273
+ if (check.status === 'passed') continue;
1274
+ actions.push(nextActionDetail(`release-${check.id}`, check.status === 'blocked' ? 'error' : 'warning', check.note || `${check.id} did not pass`, `fix ${check.id} before publishing`, check.command));
1275
+ }
1276
+ if (provenance.git_dirty) {
1277
+ actions.push(nextActionDetail('release-git-dirty', 'warning', 'working tree has uncommitted changes', 'commit or intentionally keep local-only changes before tagging a release', 'git status --short'));
1278
+ }
1279
+ if (provenance.dist_changed_files?.length) {
1280
+ actions.push(nextActionDetail('release-dist-drift', 'warning', 'dist has unstaged or uncommitted differences', 'run build and commit generated dist only if this package publishes dist', 'npm run build'));
1281
+ }
1282
+ if (tarball.status !== 'checked') {
1283
+ actions.push(nextActionDetail('release-tarball-provenance', 'error', tarball.reason || 'tarball provenance was not produced', 'run npm pack dry-run before publishing', 'npm pack --dry-run'));
1284
+ }
1285
+ return uniqueNextActionDetails(actions);
1286
+ }
1287
+
1288
+ function runReadOnlyCommand(command, args = []) {
1289
+ const result = spawnSync(command, args, {
1290
+ cwd: ROOT,
1291
+ encoding: 'utf8',
1292
+ timeout: 30000
1293
+ });
1294
+ if (result.status !== 0) return '';
1295
+ return String(result.stdout || '');
1296
+ }
1297
+
1095
1298
  function runReleaseCheck(id, commandSpec) {
1096
1299
  const [command, args] = commandSpec;
1097
1300
  const start = Date.now();
@@ -1145,12 +1348,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1145
1348
  Usage:
1146
1349
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1147
1350
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1148
- yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--json]
1351
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--review-session-id id] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
1149
1352
 
1150
1353
  Notes:
1151
1354
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1152
1355
  compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1153
- report produces a proof-ready Ueye visual run report without requiring a new capture.
1356
+ report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
1154
1357
  `);
1155
1358
  }
1156
1359
 
@@ -1273,13 +1476,15 @@ async function ueyeCompare(args = []) {
1273
1476
 
1274
1477
  async function ueyeReport(args = []) {
1275
1478
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1276
- const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
1479
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'json']));
1277
1480
  const reference = String(flags.reference || flags.ref || '');
1278
1481
  const actual = String(flags.actual || flags.screenshot || '');
1279
1482
  const referenceSources = [];
1280
1483
  const implementationSources = [];
1281
1484
  let comparisonResult = 'not-verified';
1282
1485
  let blockedReason = String(flags.blocked_reason || '');
1486
+ const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
1487
+ const previousReport = await readPreviousUeyeReport(flags.previous_report);
1283
1488
 
1284
1489
  try {
1285
1490
  if (reference) {
@@ -1288,7 +1493,7 @@ async function ueyeReport(args = []) {
1288
1493
  source_kind: 'reference_image',
1289
1494
  source_path: info.path,
1290
1495
  source_hash: info.sha256,
1291
- reference_id: path.basename(info.path),
1496
+ reference_id: String(flags.reference_id || path.basename(info.path)),
1292
1497
  local_only: true,
1293
1498
  operator_provided: true,
1294
1499
  comparison_result: 'reference-recorded',
@@ -1301,7 +1506,8 @@ async function ueyeReport(args = []) {
1301
1506
  source_kind: 'implementation_screenshot',
1302
1507
  source_path: info.path,
1303
1508
  source_hash: info.sha256,
1304
- screenshot_id: path.basename(info.path),
1509
+ reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
1510
+ screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
1305
1511
  local_only: true,
1306
1512
  operator_provided: false,
1307
1513
  comparison_result: 'implementation-recorded',
@@ -1323,17 +1529,129 @@ async function ueyeReport(args = []) {
1323
1529
  comparison_result: comparisonResult,
1324
1530
  design_quality: String(flags.design_quality || 'not-checked'),
1325
1531
  blocked_reason: blockedReason,
1326
- next_action: String(flags.next_action || '')
1532
+ next_action: String(flags.next_visual_action || flags.next_action || '')
1533
+ });
1534
+ const comparisonReport = buildUeyeComparisonReport({
1535
+ reviewSessionId,
1536
+ previousReport,
1537
+ referenceSources,
1538
+ implementationSources,
1539
+ comparisonResult,
1540
+ designQuality: String(flags.design_quality || 'not-checked'),
1541
+ similar: arrayFlag(flags.similar),
1542
+ different: arrayFlag(flags.different),
1543
+ missing: arrayFlag(flags.missing),
1544
+ notes: arrayFlag(flags.comparison_notes),
1545
+ resolved: arrayFlag(flags.resolved),
1546
+ newFindings: arrayFlag(flags.new_finding),
1547
+ stillOpen: arrayFlag(flags.still_open),
1548
+ regressions: arrayFlag(flags.regression),
1549
+ previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
1550
+ currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
1551
+ viewport: String(flags.viewport || ''),
1552
+ state: String(flags.state || 'unknown'),
1553
+ nextAction: report.next_action,
1554
+ truthStatus: report.truth_status
1327
1555
  });
1328
1556
  const result = {
1329
1557
  ...report,
1558
+ review_session_id: reviewSessionId,
1559
+ tool_intent: 'visual',
1330
1560
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1331
- compare_backend: String(flags.compare_backend || 'local-file-hash')
1561
+ compare_backend: String(flags.compare_backend || 'local-file-hash'),
1562
+ continuity: comparisonReport.continuity,
1563
+ comparison_report: comparisonReport
1332
1564
  };
1333
1565
  printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1334
1566
  if (report.truth_status === 'blocked') process.exitCode = 1;
1335
1567
  }
1336
1568
 
1569
+ async function readPreviousUeyeReport(file = '') {
1570
+ const target = String(file || '');
1571
+ if (!target) return null;
1572
+ try {
1573
+ const data = await readJson(path.resolve(expandHome(target)));
1574
+ return {
1575
+ path: path.resolve(expandHome(target)),
1576
+ review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
1577
+ screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
1578
+ truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
1579
+ comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
1580
+ };
1581
+ } catch (error) {
1582
+ return {
1583
+ path: path.resolve(expandHome(target)),
1584
+ blocked_reason: errorMessage(error),
1585
+ truth_status: 'blocked',
1586
+ comparison_result: 'not-verified'
1587
+ };
1588
+ }
1589
+ }
1590
+
1591
+ function buildUeyeComparisonReport({
1592
+ reviewSessionId,
1593
+ previousReport,
1594
+ referenceSources,
1595
+ implementationSources,
1596
+ comparisonResult,
1597
+ designQuality,
1598
+ similar,
1599
+ different,
1600
+ missing,
1601
+ notes,
1602
+ resolved,
1603
+ newFindings,
1604
+ stillOpen,
1605
+ regressions,
1606
+ previousScreenshotId,
1607
+ currentScreenshotId,
1608
+ viewport,
1609
+ state,
1610
+ nextAction,
1611
+ truthStatus
1612
+ }) {
1613
+ const hasReference = referenceSources.length > 0;
1614
+ const hasImplementation = implementationSources.length > 0;
1615
+ const continuityTruth = previousReport?.blocked_reason ? 'blocked' : previousReport ? 'partial' : 'assumed';
1616
+ return {
1617
+ schema: 'yam.ueye-comparison-report.v1',
1618
+ review_session_id: reviewSessionId,
1619
+ previous_report: previousReport,
1620
+ continuity: {
1621
+ schema: 'yam.ueye-review-continuity.v1',
1622
+ current_review_session_id: reviewSessionId,
1623
+ previous_review_session_id: previousReport?.review_session_id || '',
1624
+ previous_report_path: previousReport?.path || '',
1625
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1626
+ current_screenshot_id: currentScreenshotId || '',
1627
+ carries_forward: Boolean(previousReport && !previousReport.blocked_reason),
1628
+ truth_status: continuityTruth
1629
+ },
1630
+ evidence_summary: {
1631
+ has_reference: hasReference,
1632
+ has_implementation_screenshot: hasImplementation,
1633
+ reference_count: referenceSources.length,
1634
+ implementation_count: implementationSources.length
1635
+ },
1636
+ comparison_result: comparisonResult,
1637
+ design_quality: designQuality,
1638
+ similar,
1639
+ different,
1640
+ missing,
1641
+ resolved,
1642
+ new_findings: newFindings,
1643
+ still_open: stillOpen,
1644
+ regressions,
1645
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1646
+ current_screenshot_id: currentScreenshotId || '',
1647
+ viewport,
1648
+ state,
1649
+ notes,
1650
+ next_action: nextAction || (truthStatus === 'verified' ? 'no action required' : 'capture or provide implementation screenshot, then compare again'),
1651
+ truth_status: truthStatus
1652
+ };
1653
+ }
1654
+
1337
1655
  async function media(args = []) {
1338
1656
  const subcommand = args[0] || 'help';
1339
1657
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
@@ -1383,6 +1701,227 @@ async function mediaProof(args = []) {
1383
1701
  if (proof.truth_status === 'blocked') process.exitCode = 1;
1384
1702
  }
1385
1703
 
1704
+ async function runtime(args = []) {
1705
+ const subcommand = args[0] || 'help';
1706
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return runtimeUsage();
1707
+ if (subcommand === 'evidence') return runtimeEvidence(args.slice(1));
1708
+ console.error(`unknown runtime command: ${subcommand}`);
1709
+ return runtimeUsage();
1710
+ }
1711
+
1712
+ function runtimeUsage() {
1713
+ console.log(`yam runtime
1714
+
1715
+ Usage:
1716
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--evidence-id id] [--command text] [--pid n] [--port n] [--url URL] [--exit-code n] [--screenshot-id id] [--cleanup-checked] [--note text] [--json]
1717
+
1718
+ Notes:
1719
+ Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
1720
+ `);
1721
+ }
1722
+
1723
+ async function runtimeEvidence(args = []) {
1724
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
1725
+ const flags = parseSimpleFlags(args, new Set(['backend', 'claim', 'evidence-id', 'command', 'pid', 'port', 'url', 'exit-code', 'screenshot-id', 'started-at', 'stopped-at', 'cleanup-checked', 'note', 'truth', 'intent', 'json']));
1726
+ const evidence = buildRuntimeBackendEvidence({
1727
+ backend: flags.backend,
1728
+ claim: flags.claim,
1729
+ evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1730
+ command: String(flags.command || ''),
1731
+ cleanup_checked: Boolean(flags.cleanup_checked),
1732
+ note: String(flags.note || ''),
1733
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1734
+ });
1735
+ const runtimeDetails = runtimeEvidenceDetails(flags);
1736
+ if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
1737
+ evidence.truth_status = 'partial';
1738
+ }
1739
+ const result = {
1740
+ ...evidence,
1741
+ runtime_details: runtimeDetails,
1742
+ tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
1743
+ next_action: runtimeEvidenceNextAction(evidence)
1744
+ };
1745
+ printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
1746
+ if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing') process.exitCode = 1;
1747
+ }
1748
+
1749
+ function runtimeEvidenceDetails(flags: AnyRecord = {}) {
1750
+ return {
1751
+ schema: 'yam.runtime-details.v1',
1752
+ pid: numberOrNull(flags.pid),
1753
+ port: numberOrNull(flags.port),
1754
+ url: String(flags.url || ''),
1755
+ exit_code: numberOrNull(flags.exit_code),
1756
+ screenshot_id: String(flags.screenshot_id || ''),
1757
+ started_at: String(flags.started_at || ''),
1758
+ stopped_at: String(flags.stopped_at || '')
1759
+ };
1760
+ }
1761
+
1762
+ function runtimeDetailsHasVerification(details: AnyRecord = {}) {
1763
+ return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
1764
+ }
1765
+
1766
+ function runtimeEvidenceNextAction(evidence) {
1767
+ if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
1768
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
1769
+ if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
1770
+ return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1771
+ }
1772
+
1773
+ async function mission(args = []) {
1774
+ const subcommand = args[0] || 'help';
1775
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return missionUsage();
1776
+ if (subcommand === 'queue' || subcommand === 'patch') return missionQueue(args.slice(1));
1777
+ console.error(`unknown mission command: ${subcommand}`);
1778
+ return missionUsage();
1779
+ }
1780
+
1781
+ function missionUsage() {
1782
+ console.log(`yam mission
1783
+
1784
+ Usage:
1785
+ yam mission queue [--lane-id id] [--agent-id id] [--status pending|applied|verified|blocked|reverted] [--scope text] [--changed file] [--depends-on lane] [--generated file] [--verification-hint text] [--rollback-hint text] [--before-check text] [--out file] [--truth status] [--json]
1786
+
1787
+ Notes:
1788
+ Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
1789
+ `);
1790
+ }
1791
+
1792
+ async function missionQueue(args = []) {
1793
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return missionUsage();
1794
+ const flags = parseSimpleFlags(args, new Set(['lane-id', 'agent-id', 'status', 'scope', 'changed', 'generated', 'depends-on', 'verification-hint', 'rollback-hint', 'before-check', 'safe-revert-note', 'truth', 'intent', 'out', 'json']));
1795
+ const changedFiles = arrayFlag(flags.changed);
1796
+ const generatedFiles = arrayFlag(flags.generated);
1797
+ const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
1798
+ const status = normalizeMissionQueueStatus(flags.status || 'pending');
1799
+ const rollback = buildRollbackHint({
1800
+ touched_files: changedFiles,
1801
+ generated_files: generatedFiles,
1802
+ before_check: String(flags.before_check || ''),
1803
+ safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
1804
+ });
1805
+ const envelope = buildMissionPatchEnvelope({
1806
+ agent_id: String(flags.agent_id || `agent-${timestampId()}`),
1807
+ assigned_scope: String(flags.scope || ''),
1808
+ changed_files: changedFiles,
1809
+ verification_hint: String(flags.verification_hint || ''),
1810
+ rollback_hint: rollback,
1811
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
1812
+ });
1813
+ const item = {
1814
+ schema: 'yam.mission-patch-queue-item.v1',
1815
+ lane_id: laneId,
1816
+ status,
1817
+ depends_on: arrayFlag(flags.depends_on),
1818
+ tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
1819
+ patch_envelope: envelope,
1820
+ next_action: missionQueueNextAction(envelope, status)
1821
+ };
1822
+ const result = {
1823
+ schema: 'yam.mission-patch-queue-lite.v1',
1824
+ generated_at: new Date().toISOString(),
1825
+ persistence: flags.out ? 'written' : 'not-persisted',
1826
+ out: flags.out ? path.resolve(expandHome(flags.out)) : '',
1827
+ items: [item],
1828
+ queue_depth: 1,
1829
+ next_action: item.next_action,
1830
+ truth_status: envelope.truth_status
1831
+ };
1832
+ if (flags.out) {
1833
+ const target = path.resolve(expandHome(flags.out));
1834
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1835
+ await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
1836
+ }
1837
+ printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
1838
+ }
1839
+
1840
+ function normalizeMissionQueueStatus(value = '') {
1841
+ const text = String(value || '').toLowerCase();
1842
+ if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text)) return text;
1843
+ return 'pending';
1844
+ }
1845
+
1846
+ function missionQueueNextAction(envelope, status = 'pending') {
1847
+ if (status === 'blocked') return 'resolve or document the blocker before applying dependent mission lanes';
1848
+ if (status === 'reverted') return 'rerun the before-check or verification hint before continuing dependent work';
1849
+ if (!envelope.assigned_scope) return 'add --scope so the patch can be reviewed against an assigned boundary';
1850
+ if (!envelope.changed_files.length) return 'add at least one --changed file when this represents code-changing work';
1851
+ if (!envelope.verification_hint) return 'add --verification-hint so reviewers know the smallest honest check';
1852
+ if (!envelope.rollback_hint.safe_revert_note && !envelope.rollback_hint.before_check) return 'add rollback or before-check detail before using this for risky work';
1853
+ return 'attach this envelope to the mission proof summary and cross-check changed files';
1854
+ }
1855
+
1856
+ async function benchmark(args = []) {
1857
+ const subcommand = args[0] || 'help';
1858
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return benchmarkUsage();
1859
+ if (subcommand === 'report') return benchmarkReport(args.slice(1));
1860
+ console.error(`unknown benchmark command: ${subcommand}`);
1861
+ return benchmarkUsage();
1862
+ }
1863
+
1864
+ function benchmarkUsage() {
1865
+ console.log(`yam benchmark
1866
+
1867
+ Usage:
1868
+ yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
1869
+
1870
+ Notes:
1871
+ Records a small optimization loop result. It does not run benchmarks by itself.
1872
+ `);
1873
+ }
1874
+
1875
+ async function benchmarkReport(args = []) {
1876
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return benchmarkUsage();
1877
+ const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
1878
+ const baseline = numberOrNull(flags.baseline);
1879
+ const current = numberOrNull(flags.current);
1880
+ const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
1881
+ const hasNumbers = baseline !== null && current !== null;
1882
+ const delta = hasNumbers ? current - baseline : null;
1883
+ const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
1884
+ const status = benchmarkStatus({ baseline, current, target });
1885
+ const result = {
1886
+ schema: 'yam.benchmark-report.v1',
1887
+ generated_at: new Date().toISOString(),
1888
+ label: String(flags.label || 'unnamed'),
1889
+ baseline,
1890
+ current,
1891
+ unit: String(flags.unit || ''),
1892
+ target,
1893
+ delta,
1894
+ percent,
1895
+ status,
1896
+ tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
1897
+ rollback_hint: String(flags.rollback_hint || (status === 'regressed' ? 'inspect the optimization patch and revert the touched surface if the regression is confirmed' : 'no rollback action recorded')),
1898
+ next_action: String(flags.next_action || benchmarkNextAction(status)),
1899
+ truth_status: hasNumbers ? 'verified' : 'partial'
1900
+ };
1901
+ printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
1902
+ if (status === 'regressed') process.exitCode = 1;
1903
+ }
1904
+
1905
+ function numberOrNull(value) {
1906
+ if (value === undefined || value === null || value === '') return null;
1907
+ const number = Number(value);
1908
+ return Number.isFinite(number) ? number : null;
1909
+ }
1910
+
1911
+ function benchmarkStatus({ baseline, current, target }) {
1912
+ if (baseline === null || current === null) return 'needs-baseline';
1913
+ if (current === baseline) return 'unchanged';
1914
+ if (target === 'higher') return current > baseline ? 'improved' : 'regressed';
1915
+ return current < baseline ? 'improved' : 'regressed';
1916
+ }
1917
+
1918
+ function benchmarkNextAction(status) {
1919
+ if (status === 'improved') return 'record the measurement source and keep the smallest relevant regression check';
1920
+ if (status === 'regressed') return 'treat this as a fix-first item before continuing planned optimization';
1921
+ if (status === 'unchanged') return 'keep the change only if it improves clarity or enables the next measured step';
1922
+ return 'capture baseline and current values before claiming optimization impact';
1923
+ }
1924
+
1386
1925
  function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1387
1926
  const flags: AnyRecord = {};
1388
1927
  const positionals: string[] = [];
@@ -1402,7 +1941,11 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1402
1941
  }
1403
1942
  const value = inlineValue ?? args[index + 1] ?? '';
1404
1943
  if (inlineValue === undefined) index += 1;
1405
- flags[normalizedKey] = value;
1944
+ if (flags[normalizedKey] !== undefined) {
1945
+ flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1946
+ } else {
1947
+ flags[normalizedKey] = value;
1948
+ }
1406
1949
  }
1407
1950
  flags._ = positionals;
1408
1951
  return flags;
@@ -2644,6 +3187,9 @@ async function main() {
2644
3187
  if (command === 'proof') return proof(process.argv.slice(3));
2645
3188
  if (command === 'ueye') return ueye(process.argv.slice(3));
2646
3189
  if (command === 'media') return media(process.argv.slice(3));
3190
+ if (command === 'runtime') return runtime(process.argv.slice(3));
3191
+ if (command === 'mission') return mission(process.argv.slice(3));
3192
+ if (command === 'benchmark') return benchmark(process.argv.slice(3));
2647
3193
  if (command === 'release') return release(process.argv.slice(3));
2648
3194
  if (command === 'safety') return safety(process.argv.slice(3));
2649
3195
  if (command === 'memory') return memory(process.argv.slice(3));