yam-flow 0.1.0 → 0.1.2

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
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import crypto from 'node:crypto';
2
4
  import fs from 'node:fs';
3
5
  import fsp from 'node:fs/promises';
6
+ import { createRequire } from 'node:module';
4
7
  import os from 'node:os';
5
8
  import path from 'node:path';
6
9
  import process from 'node:process';
7
10
  import { fileURLToPath } from 'node:url';
8
11
  import {
9
12
  TRUTH_STATUSES,
13
+ buildMissionPatchEnvelope,
14
+ buildRollbackHint,
15
+ buildUeyeVisualProvenance,
10
16
  buildYamCompletionProof,
11
17
  detectDbSafetyText as detectTrustDbSafetyText,
12
18
  isTruthStatus
@@ -128,6 +134,9 @@ Usage:
128
134
  yam tools doctor [dir]
129
135
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
130
136
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
137
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
138
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
139
+ yam release report [--json]
131
140
  yam safety [text...]
132
141
  yam memory <init|add|list|summary|resolve> [dir] [options]
133
142
  yam hook <status|enable|disable|run> [lite] [--global|--project dir]
@@ -919,6 +928,85 @@ async function safety(args = []) {
919
928
  printSafetyResult(result);
920
929
  }
921
930
 
931
+ async function release(args = []) {
932
+ const subcommand = args[0] || 'help';
933
+ if (subcommand !== 'report') {
934
+ console.error('usage: yam release report [--json]');
935
+ process.exitCode = 1;
936
+ return;
937
+ }
938
+ const asJson = args.includes('--json');
939
+ const report = runReleaseReport();
940
+ if (asJson) {
941
+ console.log(JSON.stringify(report, null, 2));
942
+ if (!report.ok) process.exitCode = 1;
943
+ return;
944
+ }
945
+ console.log('yam release report');
946
+ console.log(`- Package: ${report.packageName}@${report.version}`);
947
+ console.log(`- Truth status: ${report.truth_status}`);
948
+ for (const check of report.checks) {
949
+ console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
950
+ if (check.note) console.log(` ${check.note}`);
951
+ }
952
+ if (report.failed.length) {
953
+ console.log('- Failed:');
954
+ for (const id of report.failed) console.log(` - ${id}`);
955
+ }
956
+ if (!report.ok) process.exitCode = 1;
957
+ }
958
+
959
+ function runReleaseReport() {
960
+ const startedAt = new Date().toISOString();
961
+ const checks = [
962
+ ['typecheck', ['npm', ['run', 'typecheck']]],
963
+ ['forbidden_names', ['npm', ['run', 'forbidden-names:check']]],
964
+ ['package_boundary', ['npm', ['run', 'package-boundary:check']]],
965
+ ['registry_status', ['npm', ['run', 'registry:check']]],
966
+ ['cli_smoke', ['npm', ['run', 'cli-smoke']]],
967
+ ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
968
+ ].map(([id, command]) => runReleaseCheck(id, command));
969
+ const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
970
+ return {
971
+ schema: 'yam.release-report.v1',
972
+ generated_at: startedAt,
973
+ packageName: PACKAGE_JSON.name,
974
+ version: VERSION,
975
+ ok: failed.length === 0,
976
+ truth_status: failed.length === 0 ? 'verified' : 'blocked',
977
+ checks,
978
+ failed
979
+ };
980
+ }
981
+
982
+ function runReleaseCheck(id, commandSpec) {
983
+ const [command, args] = commandSpec;
984
+ const start = Date.now();
985
+ const result = spawnSync(command, args, {
986
+ cwd: ROOT,
987
+ encoding: 'utf8',
988
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
989
+ timeout: 120000
990
+ });
991
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
992
+ const timedOut = result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM';
993
+ return {
994
+ id,
995
+ command: [command, ...args].join(' '),
996
+ status: result.status === 0 ? 'passed' : timedOut ? 'blocked' : 'failed',
997
+ exit_code: typeof result.status === 'number' ? result.status : null,
998
+ duration_ms: Date.now() - start,
999
+ note: summarizeCheckOutput(output || result.error?.message || '')
1000
+ };
1001
+ }
1002
+
1003
+ function summarizeCheckOutput(output = '') {
1004
+ const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1005
+ if (!lines.length) return '';
1006
+ const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
1007
+ return highSignal.join(' | ').slice(0, 600);
1008
+ }
1009
+
922
1010
  async function readStdinTextIfAvailable() {
923
1011
  if (process.stdin.isTTY) return '';
924
1012
  const chunks = [];
@@ -926,6 +1014,261 @@ async function readStdinTextIfAvailable() {
926
1014
  return Buffer.concat(chunks).toString('utf8').trim();
927
1015
  }
928
1016
 
1017
+ async function ueye(args = []) {
1018
+ const subcommand = args[0] || 'help';
1019
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1020
+ if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1021
+ if (subcommand === 'compare') return ueyeCompare(args.slice(1));
1022
+ console.error(`unknown ueye command: ${subcommand}`);
1023
+ return ueyeUsage();
1024
+ }
1025
+
1026
+ function ueyeUsage() {
1027
+ console.log(`yam ueye
1028
+
1029
+ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
1030
+
1031
+ Usage:
1032
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1033
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
1034
+
1035
+ Notes:
1036
+ capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1037
+ compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1038
+ `);
1039
+ }
1040
+
1041
+ async function ueyeCapture(args = []) {
1042
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1043
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1044
+ const url = String(flags.url || '');
1045
+ const out = String(flags.out || '');
1046
+ if (!url || !out) {
1047
+ console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
1048
+ process.exitCode = 1;
1049
+ return;
1050
+ }
1051
+
1052
+ const target = path.resolve(expandHome(out));
1053
+ const viewport = parseViewport(String(flags.viewport || '1440x900'));
1054
+ const timeout = Number(flags.timeout || 30000);
1055
+ const waitUntil = String(flags.wait_until || 'networkidle');
1056
+ let browser;
1057
+
1058
+ try {
1059
+ const playwright = await loadOptionalPlaywright();
1060
+ const chromium = playwright?.chromium;
1061
+ if (!chromium) throw new Error('Playwright chromium launcher was not found');
1062
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1063
+ browser = await chromium.launch({ headless: true });
1064
+ const page = await browser.newPage({ viewport });
1065
+ await page.goto(url, { waitUntil, timeout });
1066
+ await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1067
+ const info = await imageFileInfo(target);
1068
+ const result = {
1069
+ schema: 'yam.ueye-capture.v1',
1070
+ status: 'verified',
1071
+ url,
1072
+ out: target,
1073
+ viewport: `${viewport.width}x${viewport.height}`,
1074
+ full_page: Boolean(flags.full_page),
1075
+ sha256: info.sha256,
1076
+ dimensions: info.dimensions,
1077
+ visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1078
+ };
1079
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1080
+ } catch (error) {
1081
+ const result = {
1082
+ schema: 'yam.ueye-capture.v1',
1083
+ status: 'blocked',
1084
+ url,
1085
+ out: target,
1086
+ reason: errorMessage(error),
1087
+ next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
1088
+ visual_cap: 'blocked'
1089
+ };
1090
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1091
+ process.exitCode = 1;
1092
+ } finally {
1093
+ if (browser) await browser.close().catch(() => {});
1094
+ }
1095
+ }
1096
+
1097
+ async function ueyeCompare(args = []) {
1098
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1099
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1100
+ const reference = String(flags.reference || flags.ref || '');
1101
+ const actual = String(flags.actual || flags.screenshot || '');
1102
+ if (!reference || !actual) {
1103
+ console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
1104
+ process.exitCode = 1;
1105
+ return;
1106
+ }
1107
+
1108
+ const referencePath = path.resolve(expandHome(reference));
1109
+ const actualPath = path.resolve(expandHome(actual));
1110
+ try {
1111
+ const referenceInfo = await imageFileInfo(referencePath);
1112
+ const actualInfo = await imageFileInfo(actualPath);
1113
+ if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
1114
+ throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
1115
+ }
1116
+ const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
1117
+ const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1118
+ const comparisonResult = exactMatch ? 'matched' : 'different';
1119
+ const truthStatus = exactMatch ? 'verified' : 'partial';
1120
+ const provenance = buildUeyeVisualProvenance({
1121
+ source_kind: 'implementation_screenshot',
1122
+ source_path: actualPath,
1123
+ source_hash: actualInfo.sha256,
1124
+ reference_id: String(flags.reference_id || path.basename(referencePath)),
1125
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1126
+ local_only: true,
1127
+ redacted: false,
1128
+ operator_provided: false,
1129
+ comparison_result: comparisonResult,
1130
+ truth_status: truthStatus
1131
+ });
1132
+ const result = {
1133
+ schema: 'yam.ueye-compare.v1',
1134
+ status: truthStatus,
1135
+ comparison_result: comparisonResult,
1136
+ exact_match: exactMatch,
1137
+ same_dimensions: sameDimensions,
1138
+ reference: referenceInfo,
1139
+ actual: actualInfo,
1140
+ visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1141
+ visual_provenance: provenance,
1142
+ proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
1143
+ };
1144
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1145
+ } catch (error) {
1146
+ const result = {
1147
+ schema: 'yam.ueye-compare.v1',
1148
+ status: 'blocked',
1149
+ comparison_result: 'not-verified',
1150
+ reason: errorMessage(error),
1151
+ visual_cap: 'blocked'
1152
+ };
1153
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1154
+ process.exitCode = 1;
1155
+ }
1156
+ }
1157
+
1158
+ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1159
+ const flags: AnyRecord = {};
1160
+ const positionals: string[] = [];
1161
+ for (let index = 0; index < args.length; index += 1) {
1162
+ const arg = String(args[index] || '');
1163
+ if (!arg.startsWith('--')) {
1164
+ positionals.push(arg);
1165
+ continue;
1166
+ }
1167
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1168
+ const key = rawKey.slice(2);
1169
+ if (allowed.size && !allowed.has(key)) continue;
1170
+ const normalizedKey = key.replace(/-/g, '_');
1171
+ if (['json', 'full-page'].includes(key)) {
1172
+ flags[normalizedKey] = true;
1173
+ continue;
1174
+ }
1175
+ const value = inlineValue ?? args[index + 1] ?? '';
1176
+ if (inlineValue === undefined) index += 1;
1177
+ flags[normalizedKey] = value;
1178
+ }
1179
+ flags._ = positionals;
1180
+ return flags;
1181
+ }
1182
+
1183
+ function parseViewport(value = '1440x900') {
1184
+ const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
1185
+ if (!match) return { width: 1440, height: 900 };
1186
+ return { width: Number(match[1]), height: Number(match[2]) };
1187
+ }
1188
+
1189
+ async function loadOptionalPlaywright() {
1190
+ let projectError = '';
1191
+ try {
1192
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
1193
+ return projectRequire('playwright');
1194
+ } catch (error) {
1195
+ projectError = errorMessage(error);
1196
+ // Fall through to the yam package context for local development or bundled installs.
1197
+ }
1198
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<any>;
1199
+ try {
1200
+ return await dynamicImport('playwright');
1201
+ } catch (error) {
1202
+ throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
1203
+ }
1204
+ }
1205
+
1206
+ async function imageFileInfo(file) {
1207
+ const absolute = path.resolve(expandHome(file));
1208
+ const buffer = await fsp.readFile(absolute);
1209
+ const stat = await fsp.stat(absolute);
1210
+ return {
1211
+ path: absolute,
1212
+ sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
1213
+ bytes: stat.size,
1214
+ dimensions: imageDimensions(buffer)
1215
+ };
1216
+ }
1217
+
1218
+ function imageDimensions(buffer) {
1219
+ const png = pngDimensions(buffer);
1220
+ if (png) return png;
1221
+ const jpeg = jpegDimensions(buffer);
1222
+ if (jpeg) return jpeg;
1223
+ const gif = gifDimensions(buffer);
1224
+ if (gif) return gif;
1225
+ return 'unknown';
1226
+ }
1227
+
1228
+ function pngDimensions(buffer) {
1229
+ if (buffer.length < 24) return '';
1230
+ if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a) return '';
1231
+ return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
1232
+ }
1233
+
1234
+ function gifDimensions(buffer) {
1235
+ if (buffer.length < 10) return '';
1236
+ const signature = buffer.toString('ascii', 0, 6);
1237
+ if (signature !== 'GIF87a' && signature !== 'GIF89a') return '';
1238
+ return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
1239
+ }
1240
+
1241
+ function jpegDimensions(buffer) {
1242
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return '';
1243
+ let offset = 2;
1244
+ while (offset < buffer.length) {
1245
+ if (buffer[offset] !== 0xff) return '';
1246
+ const marker = buffer[offset + 1];
1247
+ const length = buffer.readUInt16BE(offset + 2);
1248
+ if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
1249
+ return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
1250
+ }
1251
+ offset += 2 + length;
1252
+ }
1253
+ return '';
1254
+ }
1255
+
1256
+ function printJsonOrHuman(result, json = false, label = 'yam') {
1257
+ if (json) {
1258
+ console.log(JSON.stringify(result, null, 2));
1259
+ return;
1260
+ }
1261
+ console.log(label);
1262
+ for (const [key, value] of Object.entries(result)) {
1263
+ if (key === 'schema') continue;
1264
+ if (typeof value === 'object' && value !== null) {
1265
+ console.log(`- ${key}: ${JSON.stringify(value)}`);
1266
+ } else {
1267
+ console.log(`- ${key}: ${value}`);
1268
+ }
1269
+ }
1270
+ }
1271
+
929
1272
  function detectDbSafetyText(text = '') {
930
1273
  return detectTrustDbSafetyText(text);
931
1274
  }
@@ -966,6 +1309,9 @@ async function proof(args = []) {
966
1309
  printProofList('Evidence', summary.evidence);
967
1310
  printProofList('Visual evidence', summary.visual);
968
1311
  printProofList('Runtime evidence', summary.runtime);
1312
+ printProofList('Visual provenance', summary.visualProvenance || []);
1313
+ printProofList('Mission patch envelope', summary.missionEnvelope || []);
1314
+ printProofList('Rollback hint', summary.rollbackHint || []);
969
1315
  printProofList('Changed surfaces', summary.changed);
970
1316
  printProofList('Skipped', summary.skipped);
971
1317
  printProofList('Blocked', summary.blocked);
@@ -1040,6 +1386,18 @@ function buildProofSummary(parsed, artifact) {
1040
1386
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1041
1387
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1042
1388
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
1389
+ visualProvenance: [
1390
+ ...structuredEvidenceList(artifact.visualProvenance),
1391
+ ...structuredEvidenceList(parsed.flags.visual_provenance)
1392
+ ],
1393
+ missionEnvelope: [
1394
+ ...structuredEvidenceList(artifact.missionEnvelope),
1395
+ ...structuredEvidenceList(parsed.flags.mission_envelope)
1396
+ ],
1397
+ rollbackHint: [
1398
+ ...structuredEvidenceList(artifact.rollbackHint),
1399
+ ...structuredEvidenceList(parsed.flags.rollback_hint)
1400
+ ],
1043
1401
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1044
1402
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1045
1403
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1090,6 +1448,15 @@ function renderProofMarkdown(summary) {
1090
1448
  '## Runtime evidence',
1091
1449
  ...renderProofMarkdownList(summary.runtime),
1092
1450
  '',
1451
+ '## Visual provenance',
1452
+ ...renderProofMarkdownList(summary.visualProvenance || []),
1453
+ '',
1454
+ '## Mission patch envelope',
1455
+ ...renderProofMarkdownList(summary.missionEnvelope || []),
1456
+ '',
1457
+ '## Rollback hint',
1458
+ ...renderProofMarkdownList(summary.rollbackHint || []),
1459
+ '',
1093
1460
  '## Changed surfaces',
1094
1461
  ...renderProofMarkdownList(summary.changed),
1095
1462
  '',
@@ -1122,7 +1489,7 @@ function renderProofMarkdownList(values) {
1122
1489
  function parseProofArgs(args = []) {
1123
1490
  const flags: AnyRecord = {};
1124
1491
  const positionals: string[] = [];
1125
- const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1492
+ const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1126
1493
  for (let index = 0; index < args.length; index += 1) {
1127
1494
  const arg = args[index];
1128
1495
  if (arg.startsWith('--')) {
@@ -1136,7 +1503,7 @@ function parseProofArgs(args = []) {
1136
1503
  }
1137
1504
  const value = inlineValue ?? args[index + 1] ?? '';
1138
1505
  if (inlineValue === undefined) index += 1;
1139
- if (['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1506
+ if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1140
1507
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1141
1508
  } else {
1142
1509
  flags[normalizedKey] = value;
@@ -1174,6 +1541,9 @@ function emptyProofArtifact() {
1174
1541
  evidence: [],
1175
1542
  visual: [],
1176
1543
  runtime: [],
1544
+ visualProvenance: [],
1545
+ missionEnvelope: [],
1546
+ rollbackHint: [],
1177
1547
  cleanup: '',
1178
1548
  changed: [],
1179
1549
  skipped: [],
@@ -1196,6 +1566,9 @@ async function parseProofJson(file, empty) {
1196
1566
  evidence: arrayFlag(data.evidence),
1197
1567
  visual: arrayFlag(data.visual || data.visualEvidence),
1198
1568
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
1569
+ visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1570
+ missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1571
+ rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
1199
1572
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1200
1573
  changed: arrayFlag(data.changed || data.files),
1201
1574
  skipped: arrayFlag(data.skipped),
@@ -1221,6 +1594,9 @@ async function parseProofMarkdown(file, empty) {
1221
1594
  evidence: readProofList(text, 'Evidence'),
1222
1595
  visual: readProofList(text, 'Visual evidence'),
1223
1596
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
1597
+ visualProvenance: readProofList(text, 'Visual provenance'),
1598
+ missionEnvelope: readProofList(text, 'Mission patch envelope'),
1599
+ rollbackHint: readProofList(text, 'Rollback hint'),
1224
1600
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1225
1601
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1226
1602
  skipped: readProofList(text, 'Skipped'),
@@ -1267,9 +1643,36 @@ function arrayFlag(value) {
1267
1643
  return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean);
1268
1644
  }
1269
1645
 
1646
+ function structuredEvidenceList(value) {
1647
+ return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1648
+ }
1649
+
1650
+ function formatStructuredEvidence(value) {
1651
+ if (!value) return '';
1652
+ if (typeof value === 'object') return JSON.stringify(value);
1653
+ const text = String(value).trim();
1654
+ if (!text) return '';
1655
+ if (!text.startsWith('{')) return text;
1656
+ try {
1657
+ const data = JSON.parse(text);
1658
+ if (data.schema === 'yam.ueye-visual-provenance.v1' || data.source_kind || data.reference_id || data.screenshot_id) {
1659
+ return JSON.stringify(buildUeyeVisualProvenance(data));
1660
+ }
1661
+ if (data.schema === 'yam.mission-patch-envelope.v1' || data.agent_id || data.assigned_scope) {
1662
+ return JSON.stringify(buildMissionPatchEnvelope(data));
1663
+ }
1664
+ if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1665
+ return JSON.stringify(buildRollbackHint(data));
1666
+ }
1667
+ return JSON.stringify(data);
1668
+ } catch {
1669
+ return text;
1670
+ }
1671
+ }
1672
+
1270
1673
  function hasProofEvidence(parsed) {
1271
1674
  const flags = parsed.flags || {};
1272
- return ['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1675
+ return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1273
1676
  }
1274
1677
 
1275
1678
  function printProofList(label, values) {
@@ -1961,6 +2364,8 @@ async function main() {
1961
2364
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
1962
2365
  if (command === 'tools') return tools(process.argv.slice(3));
1963
2366
  if (command === 'proof') return proof(process.argv.slice(3));
2367
+ if (command === 'ueye') return ueye(process.argv.slice(3));
2368
+ if (command === 'release') return release(process.argv.slice(3));
1964
2369
  if (command === 'safety') return safety(process.argv.slice(3));
1965
2370
  if (command === 'memory') return memory(process.argv.slice(3));
1966
2371
  if (command === 'hook') return hook(process.argv.slice(3));
@@ -47,6 +47,38 @@ export interface RuntimeTruthMatrix {
47
47
  blockers: string[];
48
48
  }
49
49
 
50
+ export interface UeyeVisualProvenance {
51
+ schema: 'yam.ueye-visual-provenance.v1';
52
+ source_kind: string;
53
+ source_path: string;
54
+ source_hash: string;
55
+ reference_id: string;
56
+ screenshot_id: string;
57
+ local_only: boolean;
58
+ redacted: boolean;
59
+ operator_provided: boolean;
60
+ comparison_result: string;
61
+ truth_status: TruthStatus;
62
+ }
63
+
64
+ export interface RollbackHint {
65
+ schema: 'yam.rollback-hint.v1';
66
+ touched_files: string[];
67
+ generated_files: string[];
68
+ before_check: string;
69
+ safe_revert_note: string;
70
+ }
71
+
72
+ export interface MissionPatchEnvelope {
73
+ schema: 'yam.mission-patch-envelope.v1';
74
+ agent_id: string;
75
+ assigned_scope: string;
76
+ changed_files: string[];
77
+ verification_hint: string;
78
+ rollback_hint: RollbackHint;
79
+ truth_status: TruthStatus;
80
+ }
81
+
50
82
  export interface FakeRealPolicyResult {
51
83
  schema: 'yam.fake-real-policy.v1';
52
84
  ok: boolean;
@@ -71,6 +103,9 @@ export interface ProofSummary {
71
103
  blocked?: unknown;
72
104
  assumptions?: unknown;
73
105
  unverified?: unknown;
106
+ visualProvenance?: unknown;
107
+ missionEnvelope?: unknown;
108
+ rollbackHint?: unknown;
74
109
  }
75
110
 
76
111
  export interface ProofOptions {
@@ -101,6 +136,9 @@ export interface YamCompletionProof {
101
136
  evidence: string[];
102
137
  visual: string[];
103
138
  runtime: string[];
139
+ visualProvenance: string[];
140
+ missionEnvelope: string[];
141
+ rollbackHint: string[];
104
142
  cleanup: unknown;
105
143
  changed: string[];
106
144
  skipped: string[];
@@ -113,6 +151,48 @@ export interface YamCompletionProof {
113
151
  runtimeTruth: RuntimeTruthMatrix;
114
152
  }
115
153
 
154
+ export function buildUeyeVisualProvenance(input: Partial<UeyeVisualProvenance> = {}): UeyeVisualProvenance {
155
+ const truth = isTruthStatus(input.truth_status) ? input.truth_status : 'partial';
156
+ return {
157
+ schema: 'yam.ueye-visual-provenance.v1',
158
+ source_kind: String(input.source_kind || ''),
159
+ source_path: String(input.source_path || ''),
160
+ source_hash: String(input.source_hash || 'unknown'),
161
+ reference_id: String(input.reference_id || ''),
162
+ screenshot_id: String(input.screenshot_id || ''),
163
+ local_only: Boolean(input.local_only),
164
+ redacted: Boolean(input.redacted),
165
+ operator_provided: Boolean(input.operator_provided),
166
+ comparison_result: String(input.comparison_result || 'not-verified'),
167
+ truth_status: truth
168
+ };
169
+ }
170
+
171
+ export function buildRollbackHint(input: Partial<RollbackHint> = {}): RollbackHint {
172
+ return {
173
+ schema: 'yam.rollback-hint.v1',
174
+ touched_files: asList(input.touched_files),
175
+ generated_files: asList(input.generated_files),
176
+ before_check: String(input.before_check || ''),
177
+ safe_revert_note: String(input.safe_revert_note || '')
178
+ };
179
+ }
180
+
181
+ export function buildMissionPatchEnvelope(input: Partial<MissionPatchEnvelope> = {}): MissionPatchEnvelope {
182
+ const rollback = input.rollback_hint && typeof input.rollback_hint === 'object'
183
+ ? buildRollbackHint(input.rollback_hint)
184
+ : buildRollbackHint();
185
+ return {
186
+ schema: 'yam.mission-patch-envelope.v1',
187
+ agent_id: String(input.agent_id || ''),
188
+ assigned_scope: String(input.assigned_scope || ''),
189
+ changed_files: asList(input.changed_files),
190
+ verification_hint: String(input.verification_hint || ''),
191
+ rollback_hint: rollback,
192
+ truth_status: isTruthStatus(input.truth_status) ? input.truth_status : 'partial'
193
+ };
194
+ }
195
+
116
196
  const TRUTH_RANK: Readonly<Record<TruthStatus, number>> = Object.freeze({
117
197
  blocked: 0,
118
198
  real_required_missing: 0,
@@ -184,6 +264,15 @@ export function classifyEvidenceTruth(summary: ProofSummary = {}, options: Proof
184
264
  for (const visual of asList(summary.visual)) {
185
265
  rows.push(evidenceRow('visual', visual, classifyVisualEvidence(visual)));
186
266
  }
267
+ for (const provenance of asList(summary.visualProvenance)) {
268
+ rows.push(evidenceRow('visual', provenance, metadataTruth(provenance, 'partial')));
269
+ }
270
+ for (const envelope of asList(summary.missionEnvelope)) {
271
+ rows.push(evidenceRow('evidence', envelope, metadataTruth(envelope, 'partial')));
272
+ }
273
+ for (const rollback of asList(summary.rollbackHint)) {
274
+ rows.push(evidenceRow('evidence', rollback, metadataTruth(rollback, 'partial')));
275
+ }
187
276
  for (const runtime of asList(summary.runtime)) {
188
277
  rows.push(evidenceRow('runtime', runtime, classifyRuntimeEvidence(runtime, options)));
189
278
  }
@@ -267,6 +356,9 @@ export function buildYamCompletionProof(summary: ProofSummary = {}, options: Pro
267
356
  evidence: asList(summary.evidence),
268
357
  visual: asList(summary.visual),
269
358
  runtime: asList(summary.runtime),
359
+ visualProvenance: asList(summary.visualProvenance),
360
+ missionEnvelope: asList(summary.missionEnvelope),
361
+ rollbackHint: asList(summary.rollbackHint),
270
362
  cleanup: summary.cleanup || '',
271
363
  changed: asList(summary.changed),
272
364
  skipped: asList(summary.skipped),
@@ -317,6 +409,25 @@ function commandPassed(value: unknown = ''): boolean {
317
409
  return /pass|passed|ok|success|exit\s*0|green/i.test(String(value || '')) && !/fail|failed|error|blocked/i.test(String(value || ''));
318
410
  }
319
411
 
412
+ function metadataTruth(value: unknown = '', fallback: TruthStatus = 'partial'): TruthStatus {
413
+ return weakestTruth(readStructuredTruth(value, fallback), 'partial');
414
+ }
415
+
416
+ function readStructuredTruth(value: unknown = '', fallback: TruthStatus = 'partial'): TruthStatus {
417
+ if (typeof value === 'object' && value) {
418
+ const candidate = (value as { truth_status?: unknown; truth?: unknown }).truth_status || (value as { truth?: unknown }).truth;
419
+ return isTruthStatus(candidate) ? candidate : fallback;
420
+ }
421
+ const text = String(value || '');
422
+ try {
423
+ const data = JSON.parse(text);
424
+ const candidate = data.truth_status || data.truth;
425
+ return isTruthStatus(candidate) ? candidate : fallback;
426
+ } catch {
427
+ return fallback;
428
+ }
429
+ }
430
+
320
431
  function strongestSupportedTruth(rows: EvidenceRow[] = []): TruthStatus {
321
432
  if (rows.some((row) => row.truth === 'blocked')) return 'blocked';
322
433
  if (rows.some((row) => row.truth === 'real_required_missing')) return 'real_required_missing';
@@ -29,6 +29,19 @@ Subagent 판단:
29
29
  - 이유:
30
30
  - subagent가 불가능하거나 불필요하면 기본적으로 $deep으로 전환:
31
31
 
32
+ Patch envelope:
33
+ - agent_id:
34
+ - assigned_scope:
35
+ - changed_files:
36
+ - verification_hint:
37
+ - truth_status: verified / partial / skipped / blocked / assumed
38
+
39
+ Rollback hint:
40
+ - touched_files:
41
+ - generated_files:
42
+ - before_check:
43
+ - safe_revert_note:
44
+
32
45
  검증:
33
46
  - 필요한 가장 작은 검증 명령을 우선 실행
34
47
  - 필요하면 tmux/dev server/browser QA/process cleanup proof 사용