yam-flow 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bin/yam.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  buildRuntimeBackendEvidence,
17
17
  buildUeyeVisualProvenance,
18
18
  buildUeyeRunReport,
19
+ buildUeyeSurfaceContext,
19
20
  buildYamCompletionProof,
20
21
  detectDbSafetyText as detectTrustDbSafetyText,
21
22
  isTruthStatus
@@ -139,8 +140,11 @@ Usage:
139
140
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
140
141
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
141
142
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
142
- yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
143
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
143
144
  yam media proof [--requested] [--attempted] [--output file] [--json]
145
+ yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
146
+ yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
147
+ yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
144
148
  yam release report [--json]
145
149
  yam safety [text...]
146
150
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -430,27 +434,97 @@ async function buildDoctorReport() {
430
434
  const verifyIssues = await verify({ quiet: true });
431
435
  if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
432
436
  const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
437
+ const nextActionDetails = doctorNextActionDetails(issues);
433
438
  return {
434
439
  schema: 'yam.doctor.v1',
435
440
  generatedAt: new Date().toISOString(),
436
441
  ok: issues.length === 0,
437
442
  issues,
438
- nextActions: doctorNextActions(issues),
443
+ nextActions: nextActionDetails.map((action) => action.next_action),
444
+ nextActionDetails,
439
445
  yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
440
446
  };
441
447
  }
442
448
 
443
449
  function doctorNextActions(issues = []) {
450
+ return doctorNextActionDetails(issues).map((action) => action.next_action);
451
+ }
452
+
453
+ function doctorNextActionDetails(issues = []) {
444
454
  const actions = [];
445
455
  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');
456
+ 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'));
457
+ 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'));
458
+ 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'));
459
+ 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'));
460
+ 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'));
461
+ 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'));
452
462
  }
453
- return [...new Set(actions)];
463
+ return uniqueNextActionDetails(actions);
464
+ }
465
+
466
+ function nextActionDetail(id, severity, reason, nextAction, command = '') {
467
+ const priority = priorityForSeverity(severity);
468
+ const ownerRoute = ownerRouteForAction(id, reason, command);
469
+ return {
470
+ id,
471
+ severity,
472
+ priority,
473
+ owner_route: ownerRoute,
474
+ reason,
475
+ next_action: nextAction,
476
+ command,
477
+ tool_intent: toolIntentForCommand(command),
478
+ fix_first: priority === 'P0' || priority === 'P1',
479
+ blocks_release: priority === 'P0',
480
+ truth_status: 'partial'
481
+ };
482
+ }
483
+
484
+ function priorityForSeverity(severity = '') {
485
+ if (/error|danger|block/i.test(String(severity))) return 'P0';
486
+ if (/warn/i.test(String(severity))) return 'P1';
487
+ if (/info/i.test(String(severity))) return 'P2';
488
+ return 'P3';
489
+ }
490
+
491
+ function ownerRouteForAction(id = '', reason = '', command = '') {
492
+ const text = `${id} ${reason} ${command}`;
493
+ if (/visual|ueye|screenshot|browser/i.test(text)) return '$ueye';
494
+ if (/release|publish|package|registry|dist|version/i.test(text)) return '$deep';
495
+ if (/database|supabase|destructive|production|migration/i.test(text)) return '$deep';
496
+ if (/mission|subagent|team|lane/i.test(text)) return '$mission';
497
+ if (/research|docs|current/i.test(text)) return '$scout';
498
+ return '$quick';
499
+ }
500
+
501
+ function toolIntentForCommand(command = '') {
502
+ const text = String(command || '').toLowerCase();
503
+ if (!text) return 'read_only';
504
+ if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
505
+ if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
506
+ if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
507
+ if (/(ueye|visual|image|screen)/i.test(text)) return 'visual';
508
+ if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text)) return 'read_only';
509
+ return 'write';
510
+ }
511
+
512
+ function normalizeToolIntent(value = '') {
513
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
514
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
515
+ return 'read_only';
516
+ }
517
+
518
+ function uniqueNextActionDetails(actions = []) {
519
+ const seen = new Set<string>();
520
+ const unique = [];
521
+ for (const action of actions) {
522
+ const key = `${action.id}:${action.next_action}`;
523
+ if (seen.has(key)) continue;
524
+ seen.add(key);
525
+ unique.push(action);
526
+ }
527
+ return unique;
454
528
  }
455
529
 
456
530
  async function tools(args = []) {
@@ -531,6 +605,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
531
605
  ...safety.hits,
532
606
  ...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
533
607
  ];
608
+ const nextActionDetails = toolsDoctorNextActionDetails({
609
+ pack,
610
+ rows,
611
+ riskNotes,
612
+ frameworkChecklist,
613
+ commands: detection.packageJson ? detection.commands : {}
614
+ });
534
615
  return {
535
616
  schemaVersion: 1,
536
617
  generatedAt: new Date().toISOString(),
@@ -544,13 +625,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
544
625
  frameworkChecklist,
545
626
  sqlScan,
546
627
  riskNotes,
547
- nextActions: toolsDoctorNextActions({
548
- pack,
549
- rows,
550
- riskNotes,
551
- frameworkChecklist,
552
- commands: detection.packageJson ? detection.commands : {}
553
- }),
628
+ nextActions: nextActionDetails.map((action) => action.next_action),
629
+ nextActionDetails,
554
630
  routeRecommendations: {
555
631
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
556
632
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -723,14 +799,18 @@ function checklistRow(id, label, route) {
723
799
  return { id, label, route };
724
800
  }
725
801
 
726
- function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
802
+ function toolsDoctorNextActions(input) {
803
+ return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
804
+ }
805
+
806
+ function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
727
807
  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)];
808
+ 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 .'));
809
+ 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'));
810
+ 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>"'));
811
+ 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 .'));
812
+ if (frameworkChecklist?.uiLike) actions.push(nextActionDetail('ueye-evidence', 'info', 'UI-like project detected', 'for Ueye verified claims, record reference, actual screenshot, comparison result, and surface context', 'yam ueye report --reference ref.png --actual shot.png --provider-context local --execution-surface in-app-browser --json'));
813
+ return uniqueNextActionDetails(actions);
734
814
  }
735
815
 
736
816
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
@@ -1062,10 +1142,23 @@ async function release(args = []) {
1062
1142
  console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
1063
1143
  if (check.note) console.log(` ${check.note}`);
1064
1144
  }
1145
+ if (report.provenance) {
1146
+ console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
1147
+ console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
1148
+ console.log(`- Dist freshness: ${report.freshness.dist}`);
1149
+ }
1150
+ if (report.tarball) {
1151
+ console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
1152
+ if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
1153
+ }
1065
1154
  if (report.failed.length) {
1066
1155
  console.log('- Failed:');
1067
1156
  for (const id of report.failed) console.log(` - ${id}`);
1068
1157
  }
1158
+ if (report.next_actions?.length) {
1159
+ console.log('- Next actions:');
1160
+ for (const action of report.next_actions) console.log(` - ${action.next_action}`);
1161
+ }
1069
1162
  if (!report.ok) process.exitCode = 1;
1070
1163
  }
1071
1164
 
@@ -1080,6 +1173,10 @@ function runReleaseReport() {
1080
1173
  ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
1081
1174
  ].map(([id, command]) => runReleaseCheck(id, command));
1082
1175
  const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
1176
+ const provenance = releaseProvenance(checks);
1177
+ const tarball = releaseTarballProvenance();
1178
+ const freshness = releaseFreshness(checks, provenance, tarball);
1179
+ const nextActions = releaseNextActions(checks, provenance, tarball);
1083
1180
  return {
1084
1181
  schema: 'yam.release-report.v1',
1085
1182
  generated_at: startedAt,
@@ -1088,10 +1185,117 @@ function runReleaseReport() {
1088
1185
  ok: failed.length === 0,
1089
1186
  truth_status: failed.length === 0 ? 'verified' : 'blocked',
1090
1187
  checks,
1091
- failed
1188
+ failed,
1189
+ provenance,
1190
+ tarball,
1191
+ freshness,
1192
+ next_actions: nextActions
1092
1193
  };
1093
1194
  }
1094
1195
 
1196
+ function releaseProvenance(checks = []) {
1197
+ const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
1198
+ const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
1199
+ const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
1200
+ const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
1201
+ return {
1202
+ schema: 'yam.release-provenance.v1',
1203
+ package_name: PACKAGE_JSON.name,
1204
+ version: VERSION,
1205
+ git_commit: gitCommit.trim(),
1206
+ git_dirty: Boolean(gitStatus.trim()),
1207
+ git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1208
+ dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
1209
+ package_files: packageFiles,
1210
+ generated_at: new Date().toISOString(),
1211
+ truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
1212
+ };
1213
+ }
1214
+
1215
+ function releaseTarballProvenance() {
1216
+ const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
1217
+ cwd: ROOT,
1218
+ encoding: 'utf8',
1219
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
1220
+ timeout: 120000
1221
+ });
1222
+ if (result.status !== 0) {
1223
+ return {
1224
+ schema: 'yam.release-tarball-provenance.v1',
1225
+ status: 'blocked',
1226
+ reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
1227
+ truth_status: 'blocked'
1228
+ };
1229
+ }
1230
+ try {
1231
+ const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
1232
+ return {
1233
+ schema: 'yam.release-tarball-provenance.v1',
1234
+ status: 'checked',
1235
+ tarball_name: String(data.filename || ''),
1236
+ package_size: Number(data.size || data.packageSize || 0),
1237
+ unpacked_size: Number(data.unpackedSize || 0),
1238
+ file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
1239
+ integrity: String(data.integrity || ''),
1240
+ shasum: String(data.shasum || ''),
1241
+ truth_status: data.filename ? 'verified' : 'partial'
1242
+ };
1243
+ } catch (error) {
1244
+ return {
1245
+ schema: 'yam.release-tarball-provenance.v1',
1246
+ status: 'blocked',
1247
+ reason: `could not parse npm pack output: ${errorMessage(error)}`,
1248
+ truth_status: 'blocked'
1249
+ };
1250
+ }
1251
+ }
1252
+
1253
+ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1254
+ const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
1255
+ const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
1256
+ const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
1257
+ const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
1258
+ const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
1259
+ const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
1260
+ return {
1261
+ schema: 'yam.release-freshness.v1',
1262
+ dist,
1263
+ registry,
1264
+ package_boundary: boundary,
1265
+ cli_smoke: smoke,
1266
+ tarball: tarballStatus,
1267
+ truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
1268
+ };
1269
+ }
1270
+
1271
+ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1272
+ const actions = [];
1273
+ for (const check of checks) {
1274
+ if (check.status === 'passed') continue;
1275
+ 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));
1276
+ }
1277
+ if (provenance.git_dirty) {
1278
+ 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'));
1279
+ }
1280
+ if (provenance.dist_changed_files?.length) {
1281
+ 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'));
1282
+ }
1283
+ if (tarball.status !== 'checked') {
1284
+ 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'));
1285
+ }
1286
+ return uniqueNextActionDetails(actions);
1287
+ }
1288
+
1289
+ function runReadOnlyCommand(command, args = []) {
1290
+ const result = spawnSync(command, args, {
1291
+ cwd: ROOT,
1292
+ encoding: 'utf8',
1293
+ timeout: 30000
1294
+ });
1295
+ if (result.status !== 0) return '';
1296
+ return String(result.stdout || '');
1297
+ }
1298
+
1095
1299
  function runReleaseCheck(id, commandSpec) {
1096
1300
  const [command, args] = commandSpec;
1097
1301
  const start = Date.now();
@@ -1145,18 +1349,18 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1145
1349
  Usage:
1146
1350
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1147
1351
  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]
1352
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--review-session-id id] [--provider-context local] [--execution-surface in-app-browser] [--app-surface codex-app] [--browser-surface in-app-browser] [--control-mode manual|automated] [--preserved-state] [--preserved-url URL] [--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
1353
 
1150
1354
  Notes:
1151
1355
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1152
1356
  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.
1357
+ report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
1154
1358
  `);
1155
1359
  }
1156
1360
 
1157
1361
  async function ueyeCapture(args = []) {
1158
1362
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1159
- const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1363
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'evidence-id', 'screenshot-id', 'json']));
1160
1364
  const url = String(flags.url || '');
1161
1365
  const out = String(flags.out || '');
1162
1366
  if (!url || !out) {
@@ -1181,6 +1385,24 @@ async function ueyeCapture(args = []) {
1181
1385
  await page.goto(url, { waitUntil, timeout });
1182
1386
  await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1183
1387
  const info = await imageFileInfo(target);
1388
+ const surfaceContext = buildUeyeSurfaceContext({
1389
+ provider_context: String(flags.provider_context || 'local-playwright'),
1390
+ provider_badge: String(flags.provider_badge || flags.provider_context || 'local-playwright'),
1391
+ execution_surface: String(flags.execution_surface || 'browser-capture'),
1392
+ app_surface: String(flags.app_surface || 'local-project'),
1393
+ browser_surface: String(flags.browser_surface || 'playwright'),
1394
+ control_mode: String(flags.control_mode || 'automated-capture'),
1395
+ route: 'ueye',
1396
+ mode: 'capture',
1397
+ url,
1398
+ viewport: `${viewport.width}x${viewport.height}`,
1399
+ screenshot_id: String(flags.screenshot_id || path.basename(target)),
1400
+ evidence_id: String(flags.evidence_id || `ueye-capture-${timestampId()}`),
1401
+ preserved_state: Boolean(flags.preserved_state),
1402
+ preserved_url: String(flags.preserved_url || url),
1403
+ local_only: true,
1404
+ truth_status: 'verified'
1405
+ });
1184
1406
  const result = {
1185
1407
  schema: 'yam.ueye-capture.v1',
1186
1408
  status: 'verified',
@@ -1190,6 +1412,7 @@ async function ueyeCapture(args = []) {
1190
1412
  full_page: Boolean(flags.full_page),
1191
1413
  sha256: info.sha256,
1192
1414
  dimensions: info.dimensions,
1415
+ surface_context: surfaceContext,
1193
1416
  visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1194
1417
  };
1195
1418
  printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
@@ -1212,7 +1435,7 @@ async function ueyeCapture(args = []) {
1212
1435
 
1213
1436
  async function ueyeCompare(args = []) {
1214
1437
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1215
- const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1438
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'viewport', 'evidence-id']));
1216
1439
  const reference = String(flags.reference || flags.ref || '');
1217
1440
  const actual = String(flags.actual || flags.screenshot || '');
1218
1441
  if (!reference || !actual) {
@@ -1233,12 +1456,35 @@ async function ueyeCompare(args = []) {
1233
1456
  const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1234
1457
  const comparisonResult = exactMatch ? 'matched' : 'different';
1235
1458
  const truthStatus = exactMatch ? 'verified' : 'partial';
1459
+ const surfaceContext = buildUeyeSurfaceContext({
1460
+ provider_context: String(flags.provider_context || 'local-file'),
1461
+ provider_badge: String(flags.provider_badge || flags.provider_context || 'local-file'),
1462
+ execution_surface: String(flags.execution_surface || 'local-file-compare'),
1463
+ app_surface: String(flags.app_surface || 'local-project'),
1464
+ browser_surface: String(flags.browser_surface || 'not-used'),
1465
+ control_mode: String(flags.control_mode || 'local-compare'),
1466
+ route: 'ueye',
1467
+ mode: 'compare',
1468
+ url: String(flags.url || ''),
1469
+ viewport: String(flags.viewport || ''),
1470
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1471
+ evidence_id: String(flags.evidence_id || `ueye-compare-${timestampId()}`),
1472
+ preserved_state: Boolean(flags.preserved_state),
1473
+ preserved_url: String(flags.preserved_url || flags.url || ''),
1474
+ local_only: true,
1475
+ truth_status: truthStatus
1476
+ });
1236
1477
  const provenance = buildUeyeVisualProvenance({
1237
1478
  source_kind: 'implementation_screenshot',
1238
1479
  source_path: actualPath,
1239
1480
  source_hash: actualInfo.sha256,
1240
1481
  reference_id: String(flags.reference_id || path.basename(referencePath)),
1241
1482
  screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1483
+ provider_context: surfaceContext.provider_context,
1484
+ provider_badge: surfaceContext.provider_badge,
1485
+ execution_surface: surfaceContext.execution_surface,
1486
+ app_surface: surfaceContext.app_surface,
1487
+ browser_surface: surfaceContext.browser_surface,
1242
1488
  local_only: true,
1243
1489
  redacted: false,
1244
1490
  operator_provided: false,
@@ -1253,6 +1499,7 @@ async function ueyeCompare(args = []) {
1253
1499
  same_dimensions: sameDimensions,
1254
1500
  reference: referenceInfo,
1255
1501
  actual: actualInfo,
1502
+ surface_context: surfaceContext,
1256
1503
  visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1257
1504
  visual_provenance: provenance,
1258
1505
  proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
@@ -1273,13 +1520,33 @@ async function ueyeCompare(args = []) {
1273
1520
 
1274
1521
  async function ueyeReport(args = []) {
1275
1522
  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']));
1523
+ 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', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'evidence-id', 'json']));
1277
1524
  const reference = String(flags.reference || flags.ref || '');
1278
1525
  const actual = String(flags.actual || flags.screenshot || '');
1279
1526
  const referenceSources = [];
1280
1527
  const implementationSources = [];
1281
1528
  let comparisonResult = 'not-verified';
1282
1529
  let blockedReason = String(flags.blocked_reason || '');
1530
+ const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
1531
+ const previousReport = await readPreviousUeyeReport(flags.previous_report);
1532
+ const surfaceContext = buildUeyeSurfaceContext({
1533
+ provider_context: String(flags.provider_context || 'not-recorded'),
1534
+ provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
1535
+ execution_surface: String(flags.execution_surface || flags.capture_backend || 'not-recorded'),
1536
+ app_surface: String(flags.app_surface || 'not-recorded'),
1537
+ browser_surface: String(flags.browser_surface || flags.capture_backend || 'not-recorded'),
1538
+ control_mode: String(flags.control_mode || 'not-recorded'),
1539
+ route: 'ueye',
1540
+ mode: 'report',
1541
+ url: String(flags.url || flags.preserved_url || ''),
1542
+ viewport: String(flags.viewport || ''),
1543
+ screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || ''),
1544
+ evidence_id: String(flags.evidence_id || reviewSessionId),
1545
+ preserved_state: Boolean(flags.preserved_state),
1546
+ preserved_url: String(flags.preserved_url || flags.url || ''),
1547
+ local_only: true,
1548
+ truth_status: actual ? 'partial' : 'assumed'
1549
+ });
1283
1550
 
1284
1551
  try {
1285
1552
  if (reference) {
@@ -1288,7 +1555,12 @@ async function ueyeReport(args = []) {
1288
1555
  source_kind: 'reference_image',
1289
1556
  source_path: info.path,
1290
1557
  source_hash: info.sha256,
1291
- reference_id: path.basename(info.path),
1558
+ reference_id: String(flags.reference_id || path.basename(info.path)),
1559
+ provider_context: surfaceContext.provider_context,
1560
+ provider_badge: surfaceContext.provider_badge,
1561
+ execution_surface: surfaceContext.execution_surface,
1562
+ app_surface: surfaceContext.app_surface,
1563
+ browser_surface: surfaceContext.browser_surface,
1292
1564
  local_only: true,
1293
1565
  operator_provided: true,
1294
1566
  comparison_result: 'reference-recorded',
@@ -1301,7 +1573,13 @@ async function ueyeReport(args = []) {
1301
1573
  source_kind: 'implementation_screenshot',
1302
1574
  source_path: info.path,
1303
1575
  source_hash: info.sha256,
1304
- screenshot_id: path.basename(info.path),
1576
+ reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
1577
+ screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
1578
+ provider_context: surfaceContext.provider_context,
1579
+ provider_badge: surfaceContext.provider_badge,
1580
+ execution_surface: surfaceContext.execution_surface,
1581
+ app_surface: surfaceContext.app_surface,
1582
+ browser_surface: surfaceContext.browser_surface,
1305
1583
  local_only: true,
1306
1584
  operator_provided: false,
1307
1585
  comparison_result: 'implementation-recorded',
@@ -1320,20 +1598,140 @@ async function ueyeReport(args = []) {
1320
1598
  const report = buildUeyeRunReport({
1321
1599
  reference_sources: referenceSources,
1322
1600
  implementation_sources: implementationSources,
1601
+ surface_context: {
1602
+ ...surfaceContext,
1603
+ screenshot_id: surfaceContext.screenshot_id || String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
1604
+ truth_status: blockedReason ? 'blocked' : implementationSources.length ? 'partial' : 'assumed'
1605
+ },
1323
1606
  comparison_result: comparisonResult,
1324
1607
  design_quality: String(flags.design_quality || 'not-checked'),
1325
1608
  blocked_reason: blockedReason,
1326
- next_action: String(flags.next_action || '')
1609
+ next_action: String(flags.next_visual_action || flags.next_action || '')
1610
+ });
1611
+ const comparisonReport = buildUeyeComparisonReport({
1612
+ reviewSessionId,
1613
+ previousReport,
1614
+ referenceSources,
1615
+ implementationSources,
1616
+ surfaceContext: report.surface_context,
1617
+ comparisonResult,
1618
+ designQuality: String(flags.design_quality || 'not-checked'),
1619
+ similar: arrayFlag(flags.similar),
1620
+ different: arrayFlag(flags.different),
1621
+ missing: arrayFlag(flags.missing),
1622
+ notes: arrayFlag(flags.comparison_notes),
1623
+ resolved: arrayFlag(flags.resolved),
1624
+ newFindings: arrayFlag(flags.new_finding),
1625
+ stillOpen: arrayFlag(flags.still_open),
1626
+ regressions: arrayFlag(flags.regression),
1627
+ previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
1628
+ currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
1629
+ viewport: String(flags.viewport || ''),
1630
+ state: String(flags.state || 'unknown'),
1631
+ nextAction: report.next_action,
1632
+ truthStatus: report.truth_status
1327
1633
  });
1328
1634
  const result = {
1329
1635
  ...report,
1636
+ review_session_id: reviewSessionId,
1637
+ tool_intent: 'visual',
1330
1638
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1331
- compare_backend: String(flags.compare_backend || 'local-file-hash')
1639
+ compare_backend: String(flags.compare_backend || 'local-file-hash'),
1640
+ continuity: comparisonReport.continuity,
1641
+ comparison_report: comparisonReport
1332
1642
  };
1333
1643
  printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1334
1644
  if (report.truth_status === 'blocked') process.exitCode = 1;
1335
1645
  }
1336
1646
 
1647
+ async function readPreviousUeyeReport(file = '') {
1648
+ const target = String(file || '');
1649
+ if (!target) return null;
1650
+ try {
1651
+ const data = await readJson(path.resolve(expandHome(target)));
1652
+ return {
1653
+ path: path.resolve(expandHome(target)),
1654
+ review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
1655
+ screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
1656
+ truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
1657
+ comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
1658
+ };
1659
+ } catch (error) {
1660
+ return {
1661
+ path: path.resolve(expandHome(target)),
1662
+ blocked_reason: errorMessage(error),
1663
+ truth_status: 'blocked',
1664
+ comparison_result: 'not-verified'
1665
+ };
1666
+ }
1667
+ }
1668
+
1669
+ function buildUeyeComparisonReport({
1670
+ reviewSessionId,
1671
+ previousReport,
1672
+ referenceSources,
1673
+ implementationSources,
1674
+ surfaceContext,
1675
+ comparisonResult,
1676
+ designQuality,
1677
+ similar,
1678
+ different,
1679
+ missing,
1680
+ notes,
1681
+ resolved,
1682
+ newFindings,
1683
+ stillOpen,
1684
+ regressions,
1685
+ previousScreenshotId,
1686
+ currentScreenshotId,
1687
+ viewport,
1688
+ state,
1689
+ nextAction,
1690
+ truthStatus
1691
+ }) {
1692
+ const hasReference = referenceSources.length > 0;
1693
+ const hasImplementation = implementationSources.length > 0;
1694
+ const continuityTruth = previousReport?.blocked_reason ? 'blocked' : previousReport ? 'partial' : 'assumed';
1695
+ return {
1696
+ schema: 'yam.ueye-comparison-report.v1',
1697
+ review_session_id: reviewSessionId,
1698
+ previous_report: previousReport,
1699
+ continuity: {
1700
+ schema: 'yam.ueye-review-continuity.v1',
1701
+ current_review_session_id: reviewSessionId,
1702
+ previous_review_session_id: previousReport?.review_session_id || '',
1703
+ previous_report_path: previousReport?.path || '',
1704
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1705
+ current_screenshot_id: currentScreenshotId || '',
1706
+ carries_forward: Boolean(previousReport && !previousReport.blocked_reason),
1707
+ truth_status: continuityTruth
1708
+ },
1709
+ evidence_summary: {
1710
+ has_reference: hasReference,
1711
+ has_implementation_screenshot: hasImplementation,
1712
+ reference_count: referenceSources.length,
1713
+ implementation_count: implementationSources.length
1714
+ },
1715
+ surface_context: surfaceContext || buildUeyeSurfaceContext(),
1716
+ comparison_result: comparisonResult,
1717
+ design_quality: designQuality,
1718
+ similar,
1719
+ different,
1720
+ missing,
1721
+ resolved,
1722
+ new_findings: newFindings,
1723
+ still_open: stillOpen,
1724
+ regressions,
1725
+ previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
1726
+ current_screenshot_id: currentScreenshotId || '',
1727
+ viewport,
1728
+ state,
1729
+ notes,
1730
+ next_action: nextAction || (truthStatus === 'verified' ? 'no action required' : 'capture or provide implementation screenshot, then compare again'),
1731
+ truth_status: truthStatus
1732
+ };
1733
+ }
1734
+
1337
1735
  async function media(args = []) {
1338
1736
  const subcommand = args[0] || 'help';
1339
1737
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
@@ -1383,6 +1781,227 @@ async function mediaProof(args = []) {
1383
1781
  if (proof.truth_status === 'blocked') process.exitCode = 1;
1384
1782
  }
1385
1783
 
1784
+ async function runtime(args = []) {
1785
+ const subcommand = args[0] || 'help';
1786
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return runtimeUsage();
1787
+ if (subcommand === 'evidence') return runtimeEvidence(args.slice(1));
1788
+ console.error(`unknown runtime command: ${subcommand}`);
1789
+ return runtimeUsage();
1790
+ }
1791
+
1792
+ function runtimeUsage() {
1793
+ console.log(`yam runtime
1794
+
1795
+ Usage:
1796
+ 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]
1797
+
1798
+ Notes:
1799
+ Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
1800
+ `);
1801
+ }
1802
+
1803
+ async function runtimeEvidence(args = []) {
1804
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
1805
+ 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']));
1806
+ const evidence = buildRuntimeBackendEvidence({
1807
+ backend: flags.backend,
1808
+ claim: flags.claim,
1809
+ evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1810
+ command: String(flags.command || ''),
1811
+ cleanup_checked: Boolean(flags.cleanup_checked),
1812
+ note: String(flags.note || ''),
1813
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1814
+ });
1815
+ const runtimeDetails = runtimeEvidenceDetails(flags);
1816
+ if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
1817
+ evidence.truth_status = 'partial';
1818
+ }
1819
+ const result = {
1820
+ ...evidence,
1821
+ runtime_details: runtimeDetails,
1822
+ tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
1823
+ next_action: runtimeEvidenceNextAction(evidence)
1824
+ };
1825
+ printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
1826
+ if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing') process.exitCode = 1;
1827
+ }
1828
+
1829
+ function runtimeEvidenceDetails(flags: AnyRecord = {}) {
1830
+ return {
1831
+ schema: 'yam.runtime-details.v1',
1832
+ pid: numberOrNull(flags.pid),
1833
+ port: numberOrNull(flags.port),
1834
+ url: String(flags.url || ''),
1835
+ exit_code: numberOrNull(flags.exit_code),
1836
+ screenshot_id: String(flags.screenshot_id || ''),
1837
+ started_at: String(flags.started_at || ''),
1838
+ stopped_at: String(flags.stopped_at || '')
1839
+ };
1840
+ }
1841
+
1842
+ function runtimeDetailsHasVerification(details: AnyRecord = {}) {
1843
+ return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
1844
+ }
1845
+
1846
+ function runtimeEvidenceNextAction(evidence) {
1847
+ if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
1848
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
1849
+ if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
1850
+ return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1851
+ }
1852
+
1853
+ async function mission(args = []) {
1854
+ const subcommand = args[0] || 'help';
1855
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return missionUsage();
1856
+ if (subcommand === 'queue' || subcommand === 'patch') return missionQueue(args.slice(1));
1857
+ console.error(`unknown mission command: ${subcommand}`);
1858
+ return missionUsage();
1859
+ }
1860
+
1861
+ function missionUsage() {
1862
+ console.log(`yam mission
1863
+
1864
+ Usage:
1865
+ 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]
1866
+
1867
+ Notes:
1868
+ Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
1869
+ `);
1870
+ }
1871
+
1872
+ async function missionQueue(args = []) {
1873
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return missionUsage();
1874
+ 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']));
1875
+ const changedFiles = arrayFlag(flags.changed);
1876
+ const generatedFiles = arrayFlag(flags.generated);
1877
+ const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
1878
+ const status = normalizeMissionQueueStatus(flags.status || 'pending');
1879
+ const rollback = buildRollbackHint({
1880
+ touched_files: changedFiles,
1881
+ generated_files: generatedFiles,
1882
+ before_check: String(flags.before_check || ''),
1883
+ safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
1884
+ });
1885
+ const envelope = buildMissionPatchEnvelope({
1886
+ agent_id: String(flags.agent_id || `agent-${timestampId()}`),
1887
+ assigned_scope: String(flags.scope || ''),
1888
+ changed_files: changedFiles,
1889
+ verification_hint: String(flags.verification_hint || ''),
1890
+ rollback_hint: rollback,
1891
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
1892
+ });
1893
+ const item = {
1894
+ schema: 'yam.mission-patch-queue-item.v1',
1895
+ lane_id: laneId,
1896
+ status,
1897
+ depends_on: arrayFlag(flags.depends_on),
1898
+ tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
1899
+ patch_envelope: envelope,
1900
+ next_action: missionQueueNextAction(envelope, status)
1901
+ };
1902
+ const result = {
1903
+ schema: 'yam.mission-patch-queue-lite.v1',
1904
+ generated_at: new Date().toISOString(),
1905
+ persistence: flags.out ? 'written' : 'not-persisted',
1906
+ out: flags.out ? path.resolve(expandHome(flags.out)) : '',
1907
+ items: [item],
1908
+ queue_depth: 1,
1909
+ next_action: item.next_action,
1910
+ truth_status: envelope.truth_status
1911
+ };
1912
+ if (flags.out) {
1913
+ const target = path.resolve(expandHome(flags.out));
1914
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1915
+ await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
1916
+ }
1917
+ printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
1918
+ }
1919
+
1920
+ function normalizeMissionQueueStatus(value = '') {
1921
+ const text = String(value || '').toLowerCase();
1922
+ if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text)) return text;
1923
+ return 'pending';
1924
+ }
1925
+
1926
+ function missionQueueNextAction(envelope, status = 'pending') {
1927
+ if (status === 'blocked') return 'resolve or document the blocker before applying dependent mission lanes';
1928
+ if (status === 'reverted') return 'rerun the before-check or verification hint before continuing dependent work';
1929
+ if (!envelope.assigned_scope) return 'add --scope so the patch can be reviewed against an assigned boundary';
1930
+ if (!envelope.changed_files.length) return 'add at least one --changed file when this represents code-changing work';
1931
+ if (!envelope.verification_hint) return 'add --verification-hint so reviewers know the smallest honest check';
1932
+ 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';
1933
+ return 'attach this envelope to the mission proof summary and cross-check changed files';
1934
+ }
1935
+
1936
+ async function benchmark(args = []) {
1937
+ const subcommand = args[0] || 'help';
1938
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return benchmarkUsage();
1939
+ if (subcommand === 'report') return benchmarkReport(args.slice(1));
1940
+ console.error(`unknown benchmark command: ${subcommand}`);
1941
+ return benchmarkUsage();
1942
+ }
1943
+
1944
+ function benchmarkUsage() {
1945
+ console.log(`yam benchmark
1946
+
1947
+ Usage:
1948
+ yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
1949
+
1950
+ Notes:
1951
+ Records a small optimization loop result. It does not run benchmarks by itself.
1952
+ `);
1953
+ }
1954
+
1955
+ async function benchmarkReport(args = []) {
1956
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return benchmarkUsage();
1957
+ const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
1958
+ const baseline = numberOrNull(flags.baseline);
1959
+ const current = numberOrNull(flags.current);
1960
+ const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
1961
+ const hasNumbers = baseline !== null && current !== null;
1962
+ const delta = hasNumbers ? current - baseline : null;
1963
+ const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
1964
+ const status = benchmarkStatus({ baseline, current, target });
1965
+ const result = {
1966
+ schema: 'yam.benchmark-report.v1',
1967
+ generated_at: new Date().toISOString(),
1968
+ label: String(flags.label || 'unnamed'),
1969
+ baseline,
1970
+ current,
1971
+ unit: String(flags.unit || ''),
1972
+ target,
1973
+ delta,
1974
+ percent,
1975
+ status,
1976
+ tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
1977
+ 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')),
1978
+ next_action: String(flags.next_action || benchmarkNextAction(status)),
1979
+ truth_status: hasNumbers ? 'verified' : 'partial'
1980
+ };
1981
+ printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
1982
+ if (status === 'regressed') process.exitCode = 1;
1983
+ }
1984
+
1985
+ function numberOrNull(value) {
1986
+ if (value === undefined || value === null || value === '') return null;
1987
+ const number = Number(value);
1988
+ return Number.isFinite(number) ? number : null;
1989
+ }
1990
+
1991
+ function benchmarkStatus({ baseline, current, target }) {
1992
+ if (baseline === null || current === null) return 'needs-baseline';
1993
+ if (current === baseline) return 'unchanged';
1994
+ if (target === 'higher') return current > baseline ? 'improved' : 'regressed';
1995
+ return current < baseline ? 'improved' : 'regressed';
1996
+ }
1997
+
1998
+ function benchmarkNextAction(status) {
1999
+ if (status === 'improved') return 'record the measurement source and keep the smallest relevant regression check';
2000
+ if (status === 'regressed') return 'treat this as a fix-first item before continuing planned optimization';
2001
+ if (status === 'unchanged') return 'keep the change only if it improves clarity or enables the next measured step';
2002
+ return 'capture baseline and current values before claiming optimization impact';
2003
+ }
2004
+
1386
2005
  function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1387
2006
  const flags: AnyRecord = {};
1388
2007
  const positionals: string[] = [];
@@ -1402,7 +2021,11 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1402
2021
  }
1403
2022
  const value = inlineValue ?? args[index + 1] ?? '';
1404
2023
  if (inlineValue === undefined) index += 1;
1405
- flags[normalizedKey] = value;
2024
+ if (flags[normalizedKey] !== undefined) {
2025
+ flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
2026
+ } else {
2027
+ flags[normalizedKey] = value;
2028
+ }
1406
2029
  }
1407
2030
  flags._ = positionals;
1408
2031
  return flags;
@@ -2644,6 +3267,9 @@ async function main() {
2644
3267
  if (command === 'proof') return proof(process.argv.slice(3));
2645
3268
  if (command === 'ueye') return ueye(process.argv.slice(3));
2646
3269
  if (command === 'media') return media(process.argv.slice(3));
3270
+ if (command === 'runtime') return runtime(process.argv.slice(3));
3271
+ if (command === 'mission') return mission(process.argv.slice(3));
3272
+ if (command === 'benchmark') return benchmark(process.argv.slice(3));
2647
3273
  if (command === 'release') return release(process.argv.slice(3));
2648
3274
  if (command === 'safety') return safety(process.argv.slice(3));
2649
3275
  if (command === 'memory') return memory(process.argv.slice(3));