yam-flow 0.1.6 → 0.1.8

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
@@ -10,10 +10,12 @@ import process from 'node:process';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import {
12
12
  TRUTH_STATUSES,
13
+ buildLoopReport,
13
14
  buildMediaGenerationProof,
14
15
  buildMissionPatchEnvelope,
15
16
  buildRollbackHint,
16
17
  buildRuntimeBackendEvidence,
18
+ buildStudyNote,
17
19
  buildUeyeDesignCompletionGate,
18
20
  buildUeyeVisualProvenance,
19
21
  buildUeyeRunReport,
@@ -22,6 +24,7 @@ import {
22
24
  detectDbSafetyText as detectTrustDbSafetyText,
23
25
  isTruthStatus
24
26
  } from '../lib/trust-kernel.js';
27
+ import type { ToolIntent } from '../lib/trust-kernel.js';
25
28
 
26
29
  type AnyRecord = Record<string, any>;
27
30
 
@@ -134,13 +137,17 @@ Usage:
134
137
  yam verify
135
138
  yam detect [dir]
136
139
  yam pack [dir]
140
+ yam context pressure [dir] [--json]
141
+ yam cleanup scan [dir] [--json]
137
142
  yam budget [route]
138
143
  yam measure <route> [--files n] [--commands n] [--report-lines n] [--seconds n]
139
144
  yam tools doctor [dir]
140
145
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
141
146
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
147
+ yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
142
148
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
143
149
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
150
+ yam ueye preflight [dir] [--json]
144
151
  yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
145
152
  yam media proof [--requested] [--attempted] [--output file] [--json]
146
153
  yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
@@ -327,6 +334,7 @@ async function verify({ quiet = false } = {}) {
327
334
  'ui-quality.md',
328
335
  'question.md',
329
336
  'mission.md',
337
+ 'cleanup-scan.md',
330
338
  'doctor-scan.md',
331
339
  'scout.md',
332
340
  'runtime-orchestration.md',
@@ -502,6 +510,7 @@ function ownerRouteForAction(id = '', reason = '', command = '') {
502
510
  function toolIntentForCommand(command = '') {
503
511
  const text = String(command || '').toLowerCase();
504
512
  if (!text) return 'read_only';
513
+ if (/(npm\s+(view|whoami|config\s+get|owner\s+ls)|registry:check)/i.test(text)) return 'read_only';
505
514
  if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
506
515
  if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
507
516
  if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
@@ -510,9 +519,9 @@ function toolIntentForCommand(command = '') {
510
519
  return 'write';
511
520
  }
512
521
 
513
- function normalizeToolIntent(value = '') {
522
+ function normalizeToolIntent(value = ''): ToolIntent {
514
523
  const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
515
- if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
524
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text as ToolIntent;
516
525
  return 'read_only';
517
526
  }
518
527
 
@@ -528,6 +537,298 @@ function uniqueNextActionDetails(actions = []) {
528
537
  return unique;
529
538
  }
530
539
 
540
+ async function context(args = []) {
541
+ const subcommand = args[0] || 'help';
542
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return contextUsage();
543
+ if (subcommand === 'pressure') return contextPressure(args.slice(1));
544
+ console.error(`unknown context command: ${subcommand}`);
545
+ return contextUsage();
546
+ }
547
+
548
+ function contextUsage() {
549
+ console.log(`yam context
550
+
551
+ Usage:
552
+ yam context pressure [dir] [--json]
553
+
554
+ Notes:
555
+ Read-only context pressure scan. It explains when to summarize, refresh the project pack, narrow scope, or deepen the route.
556
+ `);
557
+ }
558
+
559
+ async function contextPressure(args = []) {
560
+ const parsed = parseDirJsonArgs(args);
561
+ const report = await buildContextPressureReport(parsed.dir);
562
+ if (parsed.json) {
563
+ console.log(JSON.stringify(report, null, 2));
564
+ return;
565
+ }
566
+ printContextPressureReport(report);
567
+ }
568
+
569
+ async function buildContextPressureReport(targetDir = process.cwd()) {
570
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
571
+ const pack = await findProjectPack(dir);
572
+ const memorySummary = path.join(dir, '.yam', 'memory', 'summary.md');
573
+ const instructionSurfaces = await findInstructionSurfaces(dir);
574
+ const detection = await detectProject(dir, { quiet: true });
575
+ const gitStatus = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
576
+ const dirtyFiles = gitStatus.trim().split(/\r?\n/).filter(Boolean);
577
+ const signals: AnyRecord = {
578
+ project_pack: await projectPackPressureSignal(pack),
579
+ memory_summary: {
580
+ present: await exists(memorySummary),
581
+ path: memorySummary
582
+ },
583
+ git: {
584
+ dirty_file_count: dirtyFiles.length,
585
+ dirty_files_sample: dirtyFiles.slice(0, 12)
586
+ },
587
+ package_scripts: detection.packageJson ? detection.commands : {},
588
+ instruction_surfaces: instructionSurfaces
589
+ };
590
+ const reasons = [];
591
+ let score = 0;
592
+
593
+ if (!signals.project_pack.present) {
594
+ score += 3;
595
+ reasons.push('project pack is missing, so direction may need rediscovery');
596
+ } else {
597
+ if (signals.project_pack.age_days > PACK_STALE_DAYS) {
598
+ score += 2;
599
+ reasons.push(`project pack is ${signals.project_pack.age_days} days old`);
600
+ }
601
+ if (signals.project_pack.words < 80) {
602
+ score += 1;
603
+ reasons.push('project pack is very short, so local direction may be thin');
604
+ }
605
+ if (signals.project_pack.words > 1200) {
606
+ score += 2;
607
+ reasons.push('project pack is long, so reuse may become expensive');
608
+ }
609
+ if (signals.project_pack.missing_sections?.length) {
610
+ score += 2;
611
+ reasons.push(`project pack is missing sections: ${signals.project_pack.missing_sections.join(', ')}`);
612
+ }
613
+ }
614
+ if (!signals.memory_summary.present) {
615
+ score += 1;
616
+ reasons.push('project memory summary is missing');
617
+ }
618
+ if (dirtyFiles.length >= 12) {
619
+ score += 3;
620
+ reasons.push(`${dirtyFiles.length} dirty files increase review and reporting pressure`);
621
+ } else if (dirtyFiles.length >= 4) {
622
+ score += 2;
623
+ reasons.push(`${dirtyFiles.length} dirty files suggest the scope is widening`);
624
+ } else if (dirtyFiles.length > 0) {
625
+ score += 1;
626
+ reasons.push(`${dirtyFiles.length} dirty file(s) should be kept in the final handoff`);
627
+ }
628
+ if (instructionSurfaces.issues.length) {
629
+ score += 3;
630
+ reasons.push('active hook or instruction issue can change route behavior');
631
+ }
632
+ if (instructionSurfaces.warnings.length >= 2) {
633
+ score += 1;
634
+ reasons.push('multiple instruction surfaces can make guidance harder to predict');
635
+ }
636
+ if (detection.packageJson && !detection.commands.build && !detection.commands.typecheck) {
637
+ score += 1;
638
+ reasons.push('no build/typecheck script was detected for quick implementation verification');
639
+ }
640
+
641
+ const pressure = score >= 7 ? 'high' : score >= 3 ? 'medium' : 'low';
642
+ return {
643
+ schema: 'yam.context-pressure.v1',
644
+ generated_at: new Date().toISOString(),
645
+ project: dir,
646
+ pressure,
647
+ score,
648
+ why_pressure_is_rising: reasons,
649
+ signals,
650
+ beginner_insight: contextPressureInsight(pressure),
651
+ recommended_next_action: pressure === 'high'
652
+ ? 'pause and narrow scope, refresh yam.project.md or memory summary, then choose the route deliberately'
653
+ : pressure === 'medium'
654
+ ? 'keep the task scoped and summarize important changes before expanding'
655
+ : 'continue with the current route and the smallest honest verification',
656
+ route_hint: pressure === 'high' ? '$deep or $mission when risk is real; otherwise split the task' : pressure === 'medium' ? '$quick with explicit boundaries, or $deep if verification claims grow' : '$quick or current route',
657
+ truth_status: 'partial'
658
+ };
659
+ }
660
+
661
+ async function projectPackPressureSignal(pack) {
662
+ if (!pack) return { present: false, path: '', age_days: null, words: 0, size_bytes: 0, missing_sections: REQUIRED_PACK_SECTIONS };
663
+ const text = await readText(pack).catch(() => '');
664
+ const stat = await fsp.stat(pack).catch(() => null);
665
+ return {
666
+ present: true,
667
+ path: pack,
668
+ age_days: stat ? Math.floor((Date.now() - stat.mtimeMs) / 86400000) : null,
669
+ words: countWords(text),
670
+ size_bytes: stat?.size || 0,
671
+ missing_sections: REQUIRED_PACK_SECTIONS.filter((section) => !hasHeading(text, section))
672
+ };
673
+ }
674
+
675
+ function contextPressureInsight(pressure) {
676
+ if (pressure === 'high') return 'The project has enough moving pieces that a non-specialist may lose track of what changed, what was checked, or which instruction surface is steering the agent.';
677
+ if (pressure === 'medium') return 'The task is still manageable, but scope or stale context may start causing repeated reading and weaker final explanations.';
678
+ return 'The current context looks small enough to keep momentum without heavy proof, as long as claims stay modest.';
679
+ }
680
+
681
+ function printContextPressureReport(report) {
682
+ console.log('yam context pressure');
683
+ console.log(`Project: ${report.project}`);
684
+ console.log(`Pressure: ${report.pressure} (score ${report.score})`);
685
+ console.log(`Insight: ${report.beginner_insight}`);
686
+ if (report.why_pressure_is_rising.length) {
687
+ console.log('');
688
+ console.log('Why pressure is rising:');
689
+ for (const reason of report.why_pressure_is_rising) console.log(`- ${reason}`);
690
+ }
691
+ console.log('');
692
+ console.log(`Next: ${report.recommended_next_action}`);
693
+ console.log(`Route hint: ${report.route_hint}`);
694
+ }
695
+
696
+ async function cleanup(args = []) {
697
+ const subcommand = args[0] || 'help';
698
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return cleanupUsage();
699
+ if (subcommand === 'scan') return cleanupScan(args.slice(1));
700
+ console.error(`unknown cleanup command: ${subcommand}`);
701
+ return cleanupUsage();
702
+ }
703
+
704
+ function cleanupUsage() {
705
+ console.log(`yam cleanup
706
+
707
+ Usage:
708
+ yam cleanup scan [dir] [--json]
709
+
710
+ Notes:
711
+ Read-only cleanup scan. It never deletes files; it only reports confusing surfaces and safe next actions.
712
+ `);
713
+ }
714
+
715
+ async function cleanupScan(args = []) {
716
+ const parsed = parseDirJsonArgs(args);
717
+ const report = await buildCleanupScanReport(parsed.dir);
718
+ if (parsed.json) {
719
+ console.log(JSON.stringify(report, null, 2));
720
+ return;
721
+ }
722
+ printCleanupScanReport(report);
723
+ }
724
+
725
+ async function buildCleanupScanReport(targetDir = process.cwd()) {
726
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
727
+ const findings = [];
728
+ const instructionSurfaces = await findInstructionSurfaces(dir);
729
+ for (const found of instructionSurfaces.found) {
730
+ const issue = instructionSurfaces.issues.find((item) => item.startsWith(`${found}:`));
731
+ const warning = instructionSurfaces.warnings.find((item) => item.startsWith(`${found}:`));
732
+ findings.push(cleanupFinding(issue ? 'high' : 'medium', found, issue || warning || 'instruction surface detected', 'inspect the file and keep only the instruction surface you intend to use'));
733
+ }
734
+ const projectAgents = path.join(dir, '.agents');
735
+ if (await exists(projectAgents)) {
736
+ findings.push(cleanupFinding('medium', '.agents', 'project-local skills can override or duplicate user-level skills', 'review project-local skills before assuming global yam-flow behavior'));
737
+ }
738
+ for (const hook of [path.join(dir, '.codex', 'hooks.json'), path.join(os.homedir(), '.codex', 'hooks.json')]) {
739
+ if (await exists(hook)) findings.push(cleanupFinding('high', hook, 'active hooks can add hidden latency or route pressure', 'disable or edit only after user approval and backup'));
740
+ }
741
+ for (const skill of [...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
742
+ if (await exists(path.join(DEST, skill, 'SKILL.md'))) findings.push(cleanupFinding('medium', `installed skill: ${skill}`, 'old skill names can confuse route selection', 'run the explicit current installer or remove only with user approval'));
743
+ }
744
+ const legacyPackageHits = await scanLimitedTextFiles(dir, ['package.json', 'README.md', 'COMMANDS.md', 'CHANGELOG.md'], /\b(yam-harness|yam-codex)\b/i);
745
+ for (const hit of legacyPackageHits) {
746
+ findings.push(cleanupFinding('low', hit.file, `legacy package name mention: ${hit.match}`, 'update public text only if it confuses install guidance'));
747
+ }
748
+ for (const artifact of await staleYamArtifacts(dir)) {
749
+ findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
750
+ }
751
+ return {
752
+ schema: 'yam.cleanup-scan.v1',
753
+ generated_at: new Date().toISOString(),
754
+ project: dir,
755
+ destructive: false,
756
+ findings,
757
+ risk_level: overallCleanupRisk(findings),
758
+ safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
759
+ truth_status: 'partial'
760
+ };
761
+ }
762
+
763
+ function cleanupFinding(riskLevel, surface, whyItMatters, safeNextAction) {
764
+ return {
765
+ risk_level: riskLevel,
766
+ surface,
767
+ why_it_matters: whyItMatters,
768
+ safe_next_action: safeNextAction,
769
+ destructive: false,
770
+ truth_status: 'partial'
771
+ };
772
+ }
773
+
774
+ function overallCleanupRisk(findings = []) {
775
+ if (findings.some((item) => item.risk_level === 'high')) return 'high';
776
+ if (findings.some((item) => item.risk_level === 'medium')) return 'medium';
777
+ return 'low';
778
+ }
779
+
780
+ async function scanLimitedTextFiles(dir, relativeFiles = [], pattern = /$^/) {
781
+ const hits = [];
782
+ for (const relative of relativeFiles) {
783
+ const file = path.join(dir, relative);
784
+ if (!await exists(file)) continue;
785
+ const text = await readText(file).catch(() => '');
786
+ const match = text.match(pattern);
787
+ if (match) hits.push({ file: relative, match: match[0] });
788
+ }
789
+ return hits;
790
+ }
791
+
792
+ async function staleYamArtifacts(dir) {
793
+ const candidates = [
794
+ path.join(dir, '.yam', 'proof.json'),
795
+ path.join(dir, '.yam', 'proof.md'),
796
+ path.join(dir, '.yam', 'runtime-proof.md'),
797
+ path.join(dir, '.yam', 'ueye-report.json')
798
+ ];
799
+ const artifacts = [];
800
+ for (const file of candidates) {
801
+ if (!await exists(file)) continue;
802
+ const stat = await fsp.stat(file).catch(() => null);
803
+ if (stat && Date.now() - stat.mtimeMs > 14 * 86400000) {
804
+ artifacts.push({ file: path.relative(dir, file), reason: 'old proof/runtime artifact may be mistaken for current evidence' });
805
+ }
806
+ }
807
+ return artifacts;
808
+ }
809
+
810
+ function printCleanupScanReport(report) {
811
+ console.log('yam cleanup scan');
812
+ console.log(`Project: ${report.project}`);
813
+ console.log(`Risk level: ${report.risk_level}`);
814
+ console.log('Destructive: false');
815
+ if (!report.findings.length) {
816
+ console.log('No confusing cleanup surfaces found.');
817
+ return;
818
+ }
819
+ for (const finding of report.findings) {
820
+ console.log(`- ${finding.risk_level}: ${finding.surface}`);
821
+ console.log(` why: ${finding.why_it_matters}`);
822
+ console.log(` next: ${finding.safe_next_action}`);
823
+ }
824
+ }
825
+
826
+ function parseDirJsonArgs(args = []) {
827
+ const flags = parseSimpleFlags(args, new Set(['json']));
828
+ const dir = flags._?.find(looksLikeDirectoryArg) || process.cwd();
829
+ return { dir, json: Boolean(flags.json) };
830
+ }
831
+
531
832
  async function tools(args = []) {
532
833
  const subcommand = args[0] || 'doctor';
533
834
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return toolsUsage();
@@ -588,6 +889,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
588
889
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
589
890
  const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
590
891
  const sqlScan = await scanSqlFiles(dir);
892
+ const contextPressure = await buildContextPressureReport(dir);
893
+ const realProbe = await buildRealProbeReport(dir, packageInfo.pkg, detection);
591
894
  const rows = [
592
895
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
593
896
  readinessRow('Yam skills', await status({ quiet: true }) === 0 ? 'ready' : 'missing', DEST),
@@ -624,6 +927,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
624
927
  instructionSurfaces,
625
928
  projectSurfaces,
626
929
  frameworkChecklist,
930
+ contextPressure,
931
+ realProbe,
627
932
  sqlScan,
628
933
  riskNotes,
629
934
  nextActions: nextActionDetails.map((action) => action.next_action),
@@ -668,6 +973,17 @@ function printToolsDoctorReport(data) {
668
973
  }
669
974
  }
670
975
 
976
+ if (data.contextPressure) {
977
+ console.log('');
978
+ console.log(`Context pressure: ${data.contextPressure.pressure} (${data.contextPressure.recommended_next_action})`);
979
+ }
980
+
981
+ if (data.realProbe?.probes?.length) {
982
+ console.log('');
983
+ console.log(`Real probe: ${data.realProbe.readiness_truth_status}`);
984
+ for (const probe of data.realProbe.probes) console.log(`- ${probe.id}: ${probe.status} (${probe.truth_status})`);
985
+ }
986
+
671
987
  if (data.sqlScan.filesScanned > 0) {
672
988
  console.log('');
673
989
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -800,6 +1116,63 @@ function checklistRow(id, label, route) {
800
1116
  return { id, label, route };
801
1117
  }
802
1118
 
1119
+ async function buildRealProbeReport(dir = process.cwd(), pkg: AnyRecord | null = null, detection: AnyRecord = {}) {
1120
+ const probes = [];
1121
+ probes.push(realProbeRow('node_version', 'observed', process.version, 'verified'));
1122
+ const npmVersion = runReadOnlyCommandIn(dir, 'npm', ['--version']).trim();
1123
+ probes.push(realProbeRow('npm_version', npmVersion ? 'observed' : 'not_found', npmVersion || 'npm not found or not runnable', npmVersion ? 'verified' : 'partial'));
1124
+ probes.push(realProbeRow('yam_binary', 'observed', `${process.argv[1] || 'unknown'} @ ${VERSION}`, 'verified'));
1125
+ const scripts = detection.packageJson ? detection.commands : {};
1126
+ probes.push(realProbeRow('package_scripts', Object.values(scripts).some(Boolean) ? 'observed' : 'not_found', scripts, Object.values(scripts).some(Boolean) ? 'verified' : 'partial'));
1127
+ const playwrightPath = await localPackagePath(dir, 'playwright');
1128
+ probes.push(realProbeRow('playwright_package', playwrightPath ? 'observed' : 'not_found', playwrightPath || 'playwright package not found in project or yam package context', playwrightPath ? 'verified' : 'partial'));
1129
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
1130
+ probes.push(realProbeRow('in_app_browser_plugin_cache', browserCache ? 'observed' : 'not_found', browserCache ? 'plugin cache present' : 'plugin cache not found locally', browserCache ? 'partial' : 'partial'));
1131
+ const tmux = await findExecutable('tmux');
1132
+ const zellij = await findExecutable('zellij');
1133
+ probes.push(realProbeRow('tmux', tmux ? 'observed' : 'not_found', tmux || 'not found on PATH', tmux ? 'verified' : 'partial'));
1134
+ probes.push(realProbeRow('zellij', zellij ? 'observed' : 'not_found', zellij || 'not found on PATH', zellij ? 'verified' : 'partial'));
1135
+ const gitInside = runReadOnlyCommandIn(dir, 'git', ['rev-parse', '--is-inside-work-tree']).trim();
1136
+ const gitDirty = runReadOnlyCommandIn(dir, 'git', ['status', '--short']).trim();
1137
+ probes.push(realProbeRow('git_state', gitInside === 'true' ? 'observed' : 'not_found', {
1138
+ inside_work_tree: gitInside === 'true',
1139
+ dirty_file_count: gitDirty ? gitDirty.split(/\r?\n/).length : 0
1140
+ }, gitInside === 'true' ? 'verified' : 'partial'));
1141
+ return {
1142
+ schema: 'yam.real-probe.v1',
1143
+ generated_at: new Date().toISOString(),
1144
+ project: path.resolve(dir),
1145
+ probes,
1146
+ readiness_truth_status: probes.some((probe) => probe.status === 'blocked') ? 'blocked' : 'partial',
1147
+ note: 'read-only local availability probe; no browser, server, database, or process was started'
1148
+ };
1149
+ }
1150
+
1151
+ function realProbeRow(id, status, evidence, truthStatus = 'partial') {
1152
+ return {
1153
+ id,
1154
+ status,
1155
+ evidence,
1156
+ truth_status: truthStatus
1157
+ };
1158
+ }
1159
+
1160
+ async function localPackagePath(dir, packageName) {
1161
+ const candidates = [
1162
+ path.join(dir, 'node_modules', packageName, 'package.json'),
1163
+ path.join(ROOT, 'node_modules', packageName, 'package.json')
1164
+ ];
1165
+ for (const candidate of candidates) {
1166
+ if (await exists(candidate)) return candidate;
1167
+ }
1168
+ try {
1169
+ const projectRequire = createRequire(path.join(dir, 'package.json'));
1170
+ return projectRequire.resolve(`${packageName}/package.json`);
1171
+ } catch {
1172
+ return '';
1173
+ }
1174
+ }
1175
+
803
1176
  function toolsDoctorNextActions(input) {
804
1177
  return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
805
1178
  }
@@ -1152,14 +1525,29 @@ async function release(args = []) {
1152
1525
  console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
1153
1526
  if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
1154
1527
  }
1528
+ if (report.publish_readiness) {
1529
+ console.log(`- Publish readiness: ${report.publish_readiness.status}`);
1530
+ console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
1531
+ console.log(`- npm auth: ${report.publish_readiness.auth.status}`);
1532
+ }
1155
1533
  if (report.failed.length) {
1156
1534
  console.log('- Failed:');
1157
1535
  for (const id of report.failed) console.log(` - ${id}`);
1158
1536
  }
1537
+ if (report.publishBlockerEvidence?.length) {
1538
+ console.log('- Publish blockers:');
1539
+ for (const blocker of report.publishBlockerEvidence) {
1540
+ console.log(` - ${blocker.blocker_kind}: ${blocker.likely_cause}`);
1541
+ console.log(` next: ${blocker.safe_next_action}`);
1542
+ }
1543
+ }
1159
1544
  if (report.next_actions?.length) {
1160
1545
  console.log('- Next actions:');
1161
1546
  for (const action of report.next_actions) console.log(` - ${action.next_action}`);
1162
1547
  }
1548
+ if (report.study_note?.problem?.summary || report.study_note?.problem?.symptom) {
1549
+ console.log(`- Study note: ${report.study_note.problem.summary || report.study_note.problem.symptom}`);
1550
+ }
1163
1551
  if (!report.ok) process.exitCode = 1;
1164
1552
  }
1165
1553
 
@@ -1177,19 +1565,25 @@ function runReleaseReport() {
1177
1565
  const provenance = releaseProvenance(checks);
1178
1566
  const tarball = releaseTarballProvenance();
1179
1567
  const freshness = releaseFreshness(checks, provenance, tarball);
1180
- const nextActions = releaseNextActions(checks, provenance, tarball);
1568
+ const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
1569
+ const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
1570
+ const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
1571
+ const ok = failed.length === 0 && publishReadiness.status === 'ready';
1181
1572
  return {
1182
1573
  schema: 'yam.release-report.v1',
1183
1574
  generated_at: startedAt,
1184
1575
  packageName: PACKAGE_JSON.name,
1185
1576
  version: VERSION,
1186
- ok: failed.length === 0,
1187
- truth_status: failed.length === 0 ? 'verified' : 'blocked',
1577
+ ok,
1578
+ truth_status: ok ? 'verified' : 'blocked',
1188
1579
  checks,
1189
1580
  failed,
1190
1581
  provenance,
1191
1582
  tarball,
1192
1583
  freshness,
1584
+ publish_readiness: publishReadiness,
1585
+ study_note: releaseStudyNote(publishReadiness, publishBlockerEvidence),
1586
+ publishBlockerEvidence,
1193
1587
  next_actions: nextActions
1194
1588
  };
1195
1589
  }
@@ -1269,7 +1663,136 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
1269
1663
  };
1270
1664
  }
1271
1665
 
1272
- function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1666
+ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
1667
+ const failedChecks = checks.filter((check) => check.status !== 'passed');
1668
+ const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
1669
+ const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
1670
+ const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
1671
+ const registryStatus = releaseRegistryStatusFromChecks(checks);
1672
+ const latestProbe = registryStatus.checked
1673
+ ? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
1674
+ : runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
1675
+ const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
1676
+ const blockers = [];
1677
+ const versionStatus = latestProbe.ok
1678
+ ? registryStatus.not_published ? 'package_not_published' : latestVersion === VERSION ? 'already_published' : 'new_version'
1679
+ : 'not_verified';
1680
+
1681
+ if (!registryProbe.ok) {
1682
+ blockers.push(readinessBlocker('registry_not_verified', 'npm registry could not be read from this shell', 'run `npm config get registry` and confirm it points to the intended public registry', 'npm config get registry', 'error'));
1683
+ }
1684
+ if (!whoamiProbe.ok) {
1685
+ blockers.push(readinessBlocker('auth_not_verified', whoamiProbe.note || 'npm whoami did not confirm an authenticated publisher in this shell', 'refresh npm login/token, then rerun `npm whoami` before publishing', 'npm whoami', 'error'));
1686
+ }
1687
+ if (!latestProbe.ok) {
1688
+ const blocker = classifyPublishBlockerText(latestProbe.note);
1689
+ blockers.push(readinessBlocker(blocker?.blocker_kind || 'latest_version_not_verified', blocker?.likely_cause || 'npm latest version could not be read', blocker?.safe_next_action || 'rerun the read-only registry check before publishing', 'npm view', 'error'));
1690
+ }
1691
+ if (versionStatus === 'already_published') {
1692
+ blockers.push(readinessBlocker('version_already_published', `${PACKAGE_JSON.name}@${VERSION} is already present on the registry`, 'bump package.json to the next patch version before publishing', 'npm view', 'error'));
1693
+ }
1694
+ if (failedChecks.length) {
1695
+ blockers.push(readinessBlocker('release_checks_failed', `${failedChecks.length} release check(s) did not pass`, 'fix failing release checks before publishing', 'npm run release:check', 'error'));
1696
+ }
1697
+ if (provenance.git_dirty) {
1698
+ blockers.push(readinessBlocker('git_dirty', 'working tree has uncommitted changes', 'commit intended changes or keep them explicitly local before tagging/publishing', 'git status --short'));
1699
+ }
1700
+ if (provenance.dist_changed_files?.length) {
1701
+ blockers.push(readinessBlocker('dist_not_fresh', 'dist has changed files after freshness check', 'run build and commit dist only if this package publishes dist', 'npm run build'));
1702
+ }
1703
+ if (tarball.status !== 'checked') {
1704
+ blockers.push(readinessBlocker('tarball_not_verified', tarball.reason || 'npm pack dry-run did not produce tarball provenance', 'fix tarball/package-boundary issues before publishing', 'npm pack --dry-run', 'error'));
1705
+ }
1706
+ for (const blocker of publishBlockers) {
1707
+ blockers.push(readinessBlocker(blocker.blocker_kind, blocker.likely_cause, blocker.safe_next_action, 'npm publish', 'error'));
1708
+ }
1709
+
1710
+ const uniqueBlockers = uniqueReadinessBlockers(blockers);
1711
+ const status = uniqueBlockers.length ? 'blocked' : 'ready';
1712
+ return {
1713
+ schema: 'yam.release-publish-readiness.v1',
1714
+ generated_at: new Date().toISOString(),
1715
+ package_name: PACKAGE_JSON.name,
1716
+ local_version: VERSION,
1717
+ status,
1718
+ registry: {
1719
+ url: registryUrl,
1720
+ latest_version: latestVersion,
1721
+ version_status: versionStatus,
1722
+ truth_status: latestProbe.ok ? 'verified' : 'blocked'
1723
+ },
1724
+ auth: {
1725
+ command: 'npm whoami',
1726
+ status: whoamiProbe.ok ? 'authenticated' : 'not_authenticated',
1727
+ account: whoamiProbe.ok ? 'observed_redacted' : '',
1728
+ note: whoamiProbe.ok ? 'npm account observed; username intentionally redacted' : whoamiProbe.note,
1729
+ truth_status: whoamiProbe.ok ? 'verified' : 'blocked'
1730
+ },
1731
+ blockers: uniqueBlockers,
1732
+ next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
1733
+ truth_status: status === 'ready' ? 'verified' : 'blocked'
1734
+ };
1735
+ }
1736
+
1737
+ function releaseRegistryStatusFromChecks(checks = []) {
1738
+ const registryCheck = checks.find((check) => check.id === 'registry_status');
1739
+ const note = String(registryCheck?.note || '');
1740
+ if (registryCheck?.status !== 'passed') return { checked: false, latest_version: '', not_published: false, note };
1741
+ const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
1742
+ return {
1743
+ checked: true,
1744
+ latest_version: latest,
1745
+ not_published: /not published yet/i.test(note),
1746
+ note
1747
+ };
1748
+ }
1749
+
1750
+ function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
1751
+ return {
1752
+ kind,
1753
+ severity,
1754
+ reason: summarizeCheckOutput(redactSensitiveText(reason)),
1755
+ safe_next_action: safeNextAction,
1756
+ command,
1757
+ truth_status: 'partial'
1758
+ };
1759
+ }
1760
+
1761
+ function uniqueReadinessBlockers(blockers = []) {
1762
+ const seen = new Set<string>();
1763
+ return blockers.filter((blocker) => {
1764
+ const key = `${blocker.kind}:${blocker.safe_next_action}`;
1765
+ if (seen.has(key)) return false;
1766
+ seen.add(key);
1767
+ return true;
1768
+ });
1769
+ }
1770
+
1771
+ function releaseStudyNote(publishReadiness: AnyRecord = {}, publishBlockers = []) {
1772
+ const authBlocked = publishReadiness.blockers?.find((blocker) => /auth|otp|permission|forbidden|not_found/i.test(`${blocker.kind} ${blocker.reason}`));
1773
+ const firstBlocker = authBlocked || publishReadiness.blockers?.[0] || publishBlockers[0];
1774
+ if (!firstBlocker) {
1775
+ return buildStudyNote({
1776
+ changed_code: 'yam release report',
1777
+ changed_role: 'It checks whether the package looks ready to publish without running npm publish.',
1778
+ change_summary: 'Release readiness passed the read-only checks that were available.',
1779
+ why_important: 'A publish command changes public package state, so yam keeps the readiness proof separate from the publish action.',
1780
+ learning_note: 'Readiness means the checks looked good; publishing still requires an explicit user action.'
1781
+ });
1782
+ }
1783
+ return buildStudyNote({
1784
+ issue_code: 'npm publish readiness',
1785
+ issue_role: 'This part checks registry, version, auth, tarball, and local release checks before any public publish.',
1786
+ issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
1787
+ changed_code: 'yam release report',
1788
+ changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
1789
+ change_summary: 'The report adds auth-safe readiness, safe next action, and beginner-readable blocker evidence.',
1790
+ why_important: 'A bad token or wrong npm account can make publish fail or target the wrong package context.',
1791
+ learning_note: 'When release readiness is blocked, fix the first blocker before trying a publish command.'
1792
+ });
1793
+ }
1794
+
1795
+ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = [], publishReadiness: AnyRecord = {}) {
1273
1796
  const actions = [];
1274
1797
  for (const check of checks) {
1275
1798
  if (check.status === 'passed') continue;
@@ -1284,9 +1807,74 @@ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: An
1284
1807
  if (tarball.status !== 'checked') {
1285
1808
  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'));
1286
1809
  }
1810
+ for (const blocker of publishBlockers) {
1811
+ actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
1812
+ }
1813
+ for (const blocker of publishReadiness.blockers || []) {
1814
+ actions.push(nextActionDetail(`publish-readiness-${blocker.kind}`, blocker.severity || 'warning', blocker.reason, blocker.safe_next_action, blocker.command || 'npm publish'));
1815
+ }
1287
1816
  return uniqueNextActionDetails(actions);
1288
1817
  }
1289
1818
 
1819
+ function publishBlockerEvidenceFromRelease(checks = [], tarball: AnyRecord = {}) {
1820
+ const texts = [
1821
+ ...checks.map((check) => `${check.id}\n${check.note || ''}`),
1822
+ tarball.reason || ''
1823
+ ].filter(Boolean);
1824
+ const blockers = [];
1825
+ for (const text of texts) {
1826
+ const blocker = classifyPublishBlockerText(text);
1827
+ if (blocker) blockers.push(blocker);
1828
+ }
1829
+ const seen = new Set<string>();
1830
+ return blockers.filter((blocker) => {
1831
+ if (seen.has(blocker.blocker_kind)) return false;
1832
+ seen.add(blocker.blocker_kind);
1833
+ return true;
1834
+ });
1835
+ }
1836
+
1837
+ function classifyPublishBlockerText(text = '') {
1838
+ const value = String(text || '');
1839
+ if (!value.trim()) return null;
1840
+ const excerpt = summarizeCheckOutput(value);
1841
+ if (/\b(ENEEDAUTH|E401|Unauthorized|whoami)\b/i.test(value)) {
1842
+ return publishBlocker('auth_required', excerpt, 'npm is not logged in for this shell or token', 'run `npm login` or refresh the npm token, then rerun `npm whoami`');
1843
+ }
1844
+ if (/\b(OTP|one-time password|two-factor|2FA)\b/i.test(value)) {
1845
+ return publishBlocker('otp_required', excerpt, 'npm publish requires a current two-factor authentication code', 'rerun publish with `npm publish --otp=123456` using the current authenticator code');
1846
+ }
1847
+ if (/\bE403\b|403\s+Forbidden|Forbidden\s+-\s+PUT|npm ERR!.*Forbidden/i.test(value)) {
1848
+ return publishBlocker('permission_forbidden', excerpt, 'the npm account lacks publish permission or the package policy rejected the request', 'confirm package ownership with `npm owner ls <package>` and publish with the owning account');
1849
+ }
1850
+ if (/\bE404\b|Not Found - PUT/i.test(value)) {
1851
+ return publishBlocker('package_or_permission_not_found', excerpt, 'npm could not publish this package name from the current account or registry context', 'check `npm config get registry`, `npm owner ls <package>`, and package name access');
1852
+ }
1853
+ if (/previously published|cannot publish over|already exists|You cannot publish over/i.test(value)) {
1854
+ return publishBlocker('version_already_published', excerpt, 'npm versions are immutable once published', 'bump package.json to the next patch version before publishing');
1855
+ }
1856
+ if (/(EACCES|EPERM|permission denied|cache|_cacache)/i.test(value)) {
1857
+ return publishBlocker('npm_cache_permission', excerpt, 'npm cache or filesystem permissions are blocking the command', 'retry with a user-owned cache such as `npm_config_cache=/private/tmp/yam-npm-cache npm publish`');
1858
+ }
1859
+ if (/(ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|registry unreachable|network timeout)/i.test(value)) {
1860
+ return publishBlocker('registry_unreachable', excerpt, 'the npm registry or network path was unreachable', 'wait, check network/VPN, then retry the read-only registry check first');
1861
+ }
1862
+ if (/(tarball|package boundary|files list|npm pack)/i.test(value)) {
1863
+ return publishBlocker('tarball_or_boundary_failure', excerpt, 'the package contents or dry-run tarball check failed', 'run `npm run package-boundary:check` and `npm pack --dry-run` before publishing');
1864
+ }
1865
+ return null;
1866
+ }
1867
+
1868
+ function publishBlocker(kind, excerpt, likelyCause, safeNextAction) {
1869
+ return {
1870
+ blocker_kind: kind,
1871
+ native_error_excerpt: excerpt,
1872
+ likely_cause: likelyCause,
1873
+ safe_next_action: safeNextAction,
1874
+ truth_status: 'partial'
1875
+ };
1876
+ }
1877
+
1290
1878
  function runReadOnlyCommand(command, args = []) {
1291
1879
  const result = spawnSync(command, args, {
1292
1880
  cwd: ROOT,
@@ -1297,6 +1885,34 @@ function runReadOnlyCommand(command, args = []) {
1297
1885
  return String(result.stdout || '');
1298
1886
  }
1299
1887
 
1888
+ function runReadOnlyCommandResult(command, args = []) {
1889
+ const result = spawnSync(command, args, {
1890
+ cwd: ROOT,
1891
+ encoding: 'utf8',
1892
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
1893
+ timeout: 30000
1894
+ });
1895
+ const stdout = redactSensitiveText(String(result.stdout || ''));
1896
+ const stderr = redactSensitiveText(String(result.stderr || result.error?.message || ''));
1897
+ return {
1898
+ ok: result.status === 0,
1899
+ status: typeof result.status === 'number' ? result.status : null,
1900
+ stdout,
1901
+ stderr,
1902
+ note: summarizeCheckOutput([stdout, stderr].filter(Boolean).join('\n'))
1903
+ };
1904
+ }
1905
+
1906
+ function runReadOnlyCommandIn(cwd, command, args = []) {
1907
+ const result = spawnSync(command, args, {
1908
+ cwd,
1909
+ encoding: 'utf8',
1910
+ timeout: 30000
1911
+ });
1912
+ if (result.status !== 0) return '';
1913
+ return String(result.stdout || '');
1914
+ }
1915
+
1300
1916
  function runReleaseCheck(id, commandSpec) {
1301
1917
  const [command, args] = commandSpec;
1302
1918
  const start = Date.now();
@@ -1319,12 +1935,20 @@ function runReleaseCheck(id, commandSpec) {
1319
1935
  }
1320
1936
 
1321
1937
  function summarizeCheckOutput(output = '') {
1322
- const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1938
+ const lines = redactSensitiveText(String(output || '')).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1323
1939
  if (!lines.length) return '';
1324
1940
  const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
1325
1941
  return highSignal.join(' | ').slice(0, 600);
1326
1942
  }
1327
1943
 
1944
+ function redactSensitiveText(text = '') {
1945
+ return String(text || '')
1946
+ .replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
1947
+ .replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
1948
+ .replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
1949
+ .replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
1950
+ }
1951
+
1328
1952
  async function readStdinTextIfAvailable() {
1329
1953
  if (process.stdin.isTTY) return '';
1330
1954
  const chunks = [];
@@ -1332,11 +1956,59 @@ async function readStdinTextIfAvailable() {
1332
1956
  return Buffer.concat(chunks).toString('utf8').trim();
1333
1957
  }
1334
1958
 
1959
+ async function loop(args = []) {
1960
+ const subcommand = args[0] || 'help';
1961
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return loopUsage();
1962
+ if (subcommand === 'report') return loopReport(args.slice(1));
1963
+ console.error(`unknown loop command: ${subcommand}`);
1964
+ process.exitCode = 1;
1965
+ return loopUsage();
1966
+ }
1967
+
1968
+ function loopUsage() {
1969
+ console.log(`yam loop
1970
+
1971
+ Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers, next action, and a short study note. It does not run agents, start processes, publish packages, or write files.
1972
+
1973
+ Usage:
1974
+ yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--blocked text] [--remaining-task text] [--truth status] [--intent-label read_only|write|destructive|runtime|visual|publish] [--issue-code text] [--issue-role text] [--issue-symptom text] [--changed-code text] [--changed-role text] [--change-summary text] [--why-important text] [--learning-note text] [--json]
1975
+ `);
1976
+ }
1977
+
1978
+ async function loopReport(args = []) {
1979
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
1980
+ const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'evidence', 'blocked', 'blocker', 'next-action', 'remaining-task', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
1981
+ const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
1982
+ const report = buildLoopReport({
1983
+ route: normalizeRoute(flags.route) || String(flags.route || ''),
1984
+ intent: String(flags.intent || ''),
1985
+ loop_kind: String(flags.loop_kind || flags.route || 'harness'),
1986
+ stages: arrayFlag(flags.stage),
1987
+ evidence: arrayFlag(flags.evidence),
1988
+ blockers,
1989
+ truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined,
1990
+ intent_label: normalizeToolIntent(flags.intent_label || flags.tool_intent || 'read_only'),
1991
+ next_action: String(flags.next_action || ''),
1992
+ remaining_tasks: arrayFlag(flags.remaining_task),
1993
+ issue_code: String(flags.issue_code || ''),
1994
+ issue_role: String(flags.issue_role || ''),
1995
+ issue_symptom: String(flags.issue_symptom || ''),
1996
+ changed_code: String(flags.changed_code || ''),
1997
+ changed_role: String(flags.changed_role || ''),
1998
+ change_summary: String(flags.change_summary || ''),
1999
+ why_important: String(flags.why_important || ''),
2000
+ learning_note: String(flags.learning_note || '')
2001
+ });
2002
+ printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
2003
+ if (report.truth_status === 'blocked') process.exitCode = 1;
2004
+ }
2005
+
1335
2006
  async function ueye(args = []) {
1336
2007
  const subcommand = args[0] || 'help';
1337
2008
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1338
2009
  if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1339
2010
  if (subcommand === 'compare') return ueyeCompare(args.slice(1));
2011
+ if (subcommand === 'preflight') return ueyePreflight(args.slice(1));
1340
2012
  if (subcommand === 'report') return ueyeReport(args.slice(1));
1341
2013
  console.error(`unknown ueye command: ${subcommand}`);
1342
2014
  return ueyeUsage();
@@ -1350,7 +2022,8 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1350
2022
  Usage:
1351
2023
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1352
2024
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1353
- 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] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--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]
2025
+ yam ueye preflight [dir] [--json]
2026
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--brief-dimension text] [--constraint text] [--anti-slop text] [--invented-metric] [--placeholder-copy] [--generic-visual] [--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] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--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]
1354
2027
 
1355
2028
  Notes:
1356
2029
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -1359,6 +2032,72 @@ Notes:
1359
2032
  `);
1360
2033
  }
1361
2034
 
2035
+ async function ueyePreflight(args = []) {
2036
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
2037
+ const parsed = parseDirJsonArgs(args);
2038
+ const report = await buildUeyePreflightReport(parsed.dir);
2039
+ if (parsed.json) {
2040
+ console.log(JSON.stringify(report, null, 2));
2041
+ return;
2042
+ }
2043
+ console.log('Ueye preflight');
2044
+ console.log(`Project: ${report.project}`);
2045
+ console.log(`Preflight id: ${report.preflight_id}`);
2046
+ console.log(`Truth status: ${report.truth_status}`);
2047
+ for (const item of report.checklist) console.log(`- ${item.status}: ${item.item} (${item.next_action})`);
2048
+ }
2049
+
2050
+ async function buildUeyePreflightReport(targetDir = process.cwd()) {
2051
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
2052
+ const packageInfo = await projectPackageInfo(dir);
2053
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
2054
+ const pack = await findProjectPack(dir);
2055
+ const playwrightPath = await localPackagePath(dir, 'playwright');
2056
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
2057
+ const uiPaths = await existingRelativePaths(dir, ['app', 'src/app', 'pages', 'src/pages', 'components', 'src/components', 'styles', 'src/styles']);
2058
+ const screenshotAvailability = playwrightPath ? 'automated_capture_possible' : browserCache ? 'in_app_or_user_screenshot_needed' : 'user_screenshot_needed';
2059
+ const p0p1Risks = [];
2060
+ if (!frameworkChecklist.uiLike) p0p1Risks.push('target UI surface is not detected yet');
2061
+ if (!browserCache && !playwrightPath) p0p1Risks.push('no local capture surface was observed');
2062
+ if (!pack) p0p1Risks.push('project direction pack is missing');
2063
+ return {
2064
+ schema: 'yam.ueye-preflight.v1',
2065
+ preflight_id: `ueye-preflight-${timestampId()}`,
2066
+ generated_at: new Date().toISOString(),
2067
+ project: dir,
2068
+ detected_framework: frameworkChecklist.framework,
2069
+ ui_paths: uiPaths,
2070
+ reference_source_needed: true,
2071
+ target_states_needed: ['default', 'loading', 'error', 'empty', 'mobile'],
2072
+ mobile_responsive_check_needed: true,
2073
+ contrast_cta_accessibility_risk: frameworkChecklist.uiLike ? 'check before done claim' : 'unknown until UI target is identified',
2074
+ p0_p1_risk_candidates: p0p1Risks,
2075
+ screenshot_capture_availability: screenshotAvailability,
2076
+ preferred_surface: 'in-app-browser',
2077
+ checklist: [
2078
+ preflightItem('reference', 'reference source selected or user confirms no reference', 'needed', 'record reference path, screenshot, or text target before claiming design fidelity'),
2079
+ preflightItem('states', 'primary states are listed', 'needed', 'prepare default/loading/error/empty/mobile states when the UI supports them'),
2080
+ preflightItem('mobile', 'mobile/responsive pass planned', 'needed', 'capture or inspect a narrow viewport before verified visual claims'),
2081
+ preflightItem('contrast_cta_accessibility', 'CTA, contrast, and accessible labels reviewed', 'needed', 'mark P0/P1 risks before saying done'),
2082
+ preflightItem('capture', 'screenshot/capture surface available', screenshotAvailability === 'automated_capture_possible' ? 'observed' : 'partial', 'use in-app browser or user-provided screenshot if automation is unavailable')
2083
+ ],
2084
+ completion_cap: 'preflight cannot prove visual quality without implementation screenshot and comparison evidence',
2085
+ truth_status: 'partial'
2086
+ };
2087
+ }
2088
+
2089
+ function preflightItem(id, item, status, nextAction) {
2090
+ return { id, item, status, next_action: nextAction, truth_status: status === 'observed' ? 'verified' : 'partial' };
2091
+ }
2092
+
2093
+ async function existingRelativePaths(dir, relativePaths = []) {
2094
+ const found = [];
2095
+ for (const relative of relativePaths) {
2096
+ if (await exists(path.join(dir, relative))) found.push(relative);
2097
+ }
2098
+ return found;
2099
+ }
2100
+
1362
2101
  async function ueyeCapture(args = []) {
1363
2102
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1364
2103
  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']));
@@ -1521,7 +2260,7 @@ async function ueyeCompare(args = []) {
1521
2260
 
1522
2261
  async function ueyeReport(args = []) {
1523
2262
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1524
- 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', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
2263
+ 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', 'preflight-id', 'p0-risk', 'quality-gate-note', 'brief-dimension', 'constraint', 'anti-slop', 'invented-metric', 'placeholder-copy', 'generic-visual', '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', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
1525
2264
  const reference = String(flags.reference || flags.ref || '');
1526
2265
  const actual = String(flags.actual || flags.screenshot || '');
1527
2266
  const referenceSources = [];
@@ -1530,6 +2269,8 @@ async function ueyeReport(args = []) {
1530
2269
  let blockedReason = String(flags.blocked_reason || '');
1531
2270
  const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
1532
2271
  const previousReport = await readPreviousUeyeReport(flags.previous_report);
2272
+ const designBrief = buildUeyeDesignBrief(flags);
2273
+ const antiSlopReview = buildUeyeAntiSlopReview(flags);
1533
2274
  const surfaceContext = buildUeyeSurfaceContext({
1534
2275
  provider_context: String(flags.provider_context || 'not-recorded'),
1535
2276
  provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
@@ -1613,7 +2354,7 @@ async function ueyeReport(args = []) {
1613
2354
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
1614
2355
  design_score: numberOrNull(flags.design_score),
1615
2356
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
1616
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0)],
2357
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
1617
2358
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
1618
2359
  states_checked: Boolean(flags.states_checked),
1619
2360
  mobile_checked: Boolean(flags.mobile_checked),
@@ -1654,6 +2395,14 @@ async function ueyeReport(args = []) {
1654
2395
  const result = {
1655
2396
  ...report,
1656
2397
  review_session_id: reviewSessionId,
2398
+ design_brief: designBrief,
2399
+ anti_slop_review: antiSlopReview,
2400
+ preflight: {
2401
+ preflight_id: String(flags.preflight_id || ''),
2402
+ p0_risks: arrayFlag(flags.p0_risk),
2403
+ quality_gate_notes: arrayFlag(flags.quality_gate_note),
2404
+ truth_status: flags.preflight_id || flags.p0_risk || flags.quality_gate_note ? 'partial' : 'assumed'
2405
+ },
1657
2406
  tool_intent: 'visual',
1658
2407
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1659
2408
  compare_backend: String(flags.compare_backend || 'local-file-hash'),
@@ -1664,6 +2413,51 @@ async function ueyeReport(args = []) {
1664
2413
  if (report.truth_status === 'blocked') process.exitCode = 1;
1665
2414
  }
1666
2415
 
2416
+ function buildUeyeDesignBrief(flags: AnyRecord = {}) {
2417
+ const dimensions = arrayFlag(flags.brief_dimension);
2418
+ const constraints = arrayFlag(flags.constraint);
2419
+ const hasBrief = dimensions.length || constraints.length;
2420
+ return {
2421
+ schema: 'yam.ueye-design-brief.v1',
2422
+ dimensions,
2423
+ constraints,
2424
+ source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
2425
+ next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
2426
+ truth_status: hasBrief ? 'partial' : 'assumed'
2427
+ };
2428
+ }
2429
+
2430
+ function buildUeyeAntiSlopReview(flags: AnyRecord = {}) {
2431
+ const explicit = arrayFlag(flags.anti_slop);
2432
+ const checks = [
2433
+ antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
2434
+ antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
2435
+ antiSlopCheck('generic_visual', 'Generic visual treatment does not fit the product context', Boolean(flags.generic_visual), 'tie visuals to the product, state, or reference before claiming design quality')
2436
+ ];
2437
+ const blockers = [
2438
+ ...explicit,
2439
+ ...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
2440
+ ];
2441
+ return {
2442
+ schema: 'yam.ueye-anti-slop-review.v1',
2443
+ checks,
2444
+ blockers,
2445
+ next_action: blockers[0] || (checks.some((check) => check.status === 'pass') || explicit.length ? 'anti-slop risks are recorded; keep visual proof separate' : 'record anti-slop risks when they matter before claiming done'),
2446
+ truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
2447
+ };
2448
+ }
2449
+
2450
+ function antiSlopCheck(id, label, failed = false, nextAction = '') {
2451
+ return {
2452
+ id,
2453
+ label,
2454
+ status: failed ? 'fail' : 'not_checked',
2455
+ severity: failed ? 'P0' : 'P2',
2456
+ next_action: nextAction,
2457
+ truth_status: failed ? 'blocked' : 'assumed'
2458
+ };
2459
+ }
2460
+
1667
2461
  async function readPreviousUeyeReport(file = '') {
1668
2462
  const target = String(file || '');
1669
2463
  if (!target) return null;
@@ -1821,7 +2615,7 @@ function runtimeUsage() {
1821
2615
  console.log(`yam runtime
1822
2616
 
1823
2617
  Usage:
1824
- 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]
2618
+ 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] [--started-at time] [--stopped-at time] [--cleanup-method text] [--cleanup-observed] [--left-running-intentionally] [--cleanup-checked] [--note text] [--json]
1825
2619
 
1826
2620
  Notes:
1827
2621
  Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
@@ -1830,13 +2624,21 @@ Notes:
1830
2624
 
1831
2625
  async function runtimeEvidence(args = []) {
1832
2626
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
1833
- 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']));
2627
+ const flags = parseSimpleFlags(args, new Set(['backend', 'claim', 'evidence-id', 'command', 'pid', 'port', 'url', 'exit-code', 'screenshot-id', 'started-at', 'stopped-at', 'cleanup-method', 'cleanup-observed', 'left-running-intentionally', 'cleanup-checked', 'note', 'truth', 'intent', 'json']));
1834
2628
  const evidence = buildRuntimeBackendEvidence({
1835
2629
  backend: flags.backend,
1836
2630
  claim: flags.claim,
1837
2631
  evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1838
2632
  command: String(flags.command || ''),
1839
2633
  cleanup_checked: Boolean(flags.cleanup_checked),
2634
+ started_at: String(flags.started_at || ''),
2635
+ stopped_at: String(flags.stopped_at || ''),
2636
+ exit_code: numberOrNull(flags.exit_code),
2637
+ pid: numberOrNull(flags.pid),
2638
+ port: numberOrNull(flags.port),
2639
+ cleanup_method: String(flags.cleanup_method || ''),
2640
+ cleanup_observed: Boolean(flags.cleanup_observed),
2641
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
1840
2642
  note: String(flags.note || ''),
1841
2643
  truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1842
2644
  });
@@ -1863,17 +2665,20 @@ function runtimeEvidenceDetails(flags: AnyRecord = {}) {
1863
2665
  exit_code: numberOrNull(flags.exit_code),
1864
2666
  screenshot_id: String(flags.screenshot_id || ''),
1865
2667
  started_at: String(flags.started_at || ''),
1866
- stopped_at: String(flags.stopped_at || '')
2668
+ stopped_at: String(flags.stopped_at || ''),
2669
+ cleanup_method: String(flags.cleanup_method || ''),
2670
+ cleanup_observed: Boolean(flags.cleanup_observed),
2671
+ left_running_intentionally: Boolean(flags.left_running_intentionally)
1867
2672
  };
1868
2673
  }
1869
2674
 
1870
2675
  function runtimeDetailsHasVerification(details: AnyRecord = {}) {
1871
- return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
2676
+ return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at || details.cleanup_method || details.cleanup_observed || details.left_running_intentionally);
1872
2677
  }
1873
2678
 
1874
2679
  function runtimeEvidenceNextAction(evidence) {
1875
2680
  if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
1876
- if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
2681
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_observed && !evidence.left_running_intentionally) return 'rerun with --cleanup-observed plus exit/closure evidence, or record --left-running-intentionally';
1877
2682
  if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
1878
2683
  return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1879
2684
  }
@@ -2043,7 +2848,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
2043
2848
  const key = rawKey.slice(2);
2044
2849
  if (allowed.size && !allowed.has(key)) continue;
2045
2850
  const normalizedKey = key.replace(/-/g, '_');
2046
- if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read'].includes(key)) {
2851
+ if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'invented-metric', 'placeholder-copy', 'generic-visual'].includes(key)) {
2047
2852
  flags[normalizedKey] = true;
2048
2853
  continue;
2049
2854
  }
@@ -2393,7 +3198,7 @@ function renderProofMarkdownList(values) {
2393
3198
  function parseProofArgs(args = []) {
2394
3199
  const flags: AnyRecord = {};
2395
3200
  const positionals: string[] = [];
2396
- const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'runtime-backend', 'runtime-claim', 'runtime-evidence-id', 'runtime-command', 'cleanup-checked', 'runtime-note', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'design-completion', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
3201
+ const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'runtime-backend', 'runtime-claim', 'runtime-evidence-id', 'runtime-command', 'runtime-pid', 'runtime-port', 'runtime-exit-code', 'runtime-started-at', 'runtime-stopped-at', 'runtime-cleanup-method', 'cleanup-observed', 'left-running-intentionally', 'cleanup-checked', 'runtime-note', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'design-completion', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
2397
3202
  for (let index = 0; index < args.length; index += 1) {
2398
3203
  const arg = args[index];
2399
3204
  if (arg.startsWith('--')) {
@@ -2401,7 +3206,7 @@ function parseProofArgs(args = []) {
2401
3206
  const key = rawKey.slice(2);
2402
3207
  if (!aliases.has(key)) continue;
2403
3208
  const normalizedKey = key.replace(/-/g, '_');
2404
- if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
3209
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked' || key === 'cleanup-observed' || key === 'left-running-intentionally') {
2405
3210
  flags[normalizedKey] = true;
2406
3211
  continue;
2407
3212
  }
@@ -2561,12 +3366,20 @@ function structuredEvidenceList(value) {
2561
3366
  }
2562
3367
 
2563
3368
  function directRuntimeBackendEvidence(flags) {
2564
- if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked) return [];
3369
+ if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked && !flags.cleanup_observed && !flags.left_running_intentionally) return [];
2565
3370
  return [JSON.stringify(buildRuntimeBackendEvidence({
2566
3371
  backend: flags.runtime_backend,
2567
3372
  claim: flags.runtime_claim,
2568
3373
  evidence_id: flags.runtime_evidence_id,
2569
3374
  command: flags.runtime_command,
3375
+ pid: numberOrNull(flags.runtime_pid),
3376
+ port: numberOrNull(flags.runtime_port),
3377
+ exit_code: numberOrNull(flags.runtime_exit_code),
3378
+ started_at: String(flags.runtime_started_at || ''),
3379
+ stopped_at: String(flags.runtime_stopped_at || ''),
3380
+ cleanup_method: String(flags.runtime_cleanup_method || ''),
3381
+ cleanup_observed: Boolean(flags.cleanup_observed),
3382
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
2570
3383
  cleanup_checked: Boolean(flags.cleanup_checked),
2571
3384
  note: flags.runtime_note
2572
3385
  }))];
@@ -3303,10 +4116,13 @@ async function main() {
3303
4116
  if (command === 'version') return console.log(VERSION);
3304
4117
  if (command === 'detect') return detectProject(process.argv[3]);
3305
4118
  if (command === 'pack') return inspectProjectPack(process.argv[3]);
4119
+ if (command === 'context') return context(process.argv.slice(3));
4120
+ if (command === 'cleanup') return cleanup(process.argv.slice(3));
3306
4121
  if (command === 'budget') return budget(process.argv[3]);
3307
4122
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
3308
4123
  if (command === 'tools') return tools(process.argv.slice(3));
3309
4124
  if (command === 'proof') return proof(process.argv.slice(3));
4125
+ if (command === 'loop') return loop(process.argv.slice(3));
3310
4126
  if (command === 'ueye') return ueye(process.argv.slice(3));
3311
4127
  if (command === 'media') return media(process.argv.slice(3));
3312
4128
  if (command === 'runtime') return runtime(process.argv.slice(3));