yam-flow 0.1.6 → 0.1.7

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
@@ -134,6 +134,8 @@ Usage:
134
134
  yam verify
135
135
  yam detect [dir]
136
136
  yam pack [dir]
137
+ yam context pressure [dir] [--json]
138
+ yam cleanup scan [dir] [--json]
137
139
  yam budget [route]
138
140
  yam measure <route> [--files n] [--commands n] [--report-lines n] [--seconds n]
139
141
  yam tools doctor [dir]
@@ -141,6 +143,7 @@ Usage:
141
143
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
142
144
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
143
145
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
146
+ yam ueye preflight [dir] [--json]
144
147
  yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
145
148
  yam media proof [--requested] [--attempted] [--output file] [--json]
146
149
  yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
@@ -327,6 +330,7 @@ async function verify({ quiet = false } = {}) {
327
330
  'ui-quality.md',
328
331
  'question.md',
329
332
  'mission.md',
333
+ 'cleanup-scan.md',
330
334
  'doctor-scan.md',
331
335
  'scout.md',
332
336
  'runtime-orchestration.md',
@@ -528,6 +532,298 @@ function uniqueNextActionDetails(actions = []) {
528
532
  return unique;
529
533
  }
530
534
 
535
+ async function context(args = []) {
536
+ const subcommand = args[0] || 'help';
537
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return contextUsage();
538
+ if (subcommand === 'pressure') return contextPressure(args.slice(1));
539
+ console.error(`unknown context command: ${subcommand}`);
540
+ return contextUsage();
541
+ }
542
+
543
+ function contextUsage() {
544
+ console.log(`yam context
545
+
546
+ Usage:
547
+ yam context pressure [dir] [--json]
548
+
549
+ Notes:
550
+ Read-only context pressure scan. It explains when to summarize, refresh the project pack, narrow scope, or deepen the route.
551
+ `);
552
+ }
553
+
554
+ async function contextPressure(args = []) {
555
+ const parsed = parseDirJsonArgs(args);
556
+ const report = await buildContextPressureReport(parsed.dir);
557
+ if (parsed.json) {
558
+ console.log(JSON.stringify(report, null, 2));
559
+ return;
560
+ }
561
+ printContextPressureReport(report);
562
+ }
563
+
564
+ async function buildContextPressureReport(targetDir = process.cwd()) {
565
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
566
+ const pack = await findProjectPack(dir);
567
+ const memorySummary = path.join(dir, '.yam', 'memory', 'summary.md');
568
+ const instructionSurfaces = await findInstructionSurfaces(dir);
569
+ const detection = await detectProject(dir, { quiet: true });
570
+ const gitStatus = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
571
+ const dirtyFiles = gitStatus.trim().split(/\r?\n/).filter(Boolean);
572
+ const signals: AnyRecord = {
573
+ project_pack: await projectPackPressureSignal(pack),
574
+ memory_summary: {
575
+ present: await exists(memorySummary),
576
+ path: memorySummary
577
+ },
578
+ git: {
579
+ dirty_file_count: dirtyFiles.length,
580
+ dirty_files_sample: dirtyFiles.slice(0, 12)
581
+ },
582
+ package_scripts: detection.packageJson ? detection.commands : {},
583
+ instruction_surfaces: instructionSurfaces
584
+ };
585
+ const reasons = [];
586
+ let score = 0;
587
+
588
+ if (!signals.project_pack.present) {
589
+ score += 3;
590
+ reasons.push('project pack is missing, so direction may need rediscovery');
591
+ } else {
592
+ if (signals.project_pack.age_days > PACK_STALE_DAYS) {
593
+ score += 2;
594
+ reasons.push(`project pack is ${signals.project_pack.age_days} days old`);
595
+ }
596
+ if (signals.project_pack.words < 80) {
597
+ score += 1;
598
+ reasons.push('project pack is very short, so local direction may be thin');
599
+ }
600
+ if (signals.project_pack.words > 1200) {
601
+ score += 2;
602
+ reasons.push('project pack is long, so reuse may become expensive');
603
+ }
604
+ if (signals.project_pack.missing_sections?.length) {
605
+ score += 2;
606
+ reasons.push(`project pack is missing sections: ${signals.project_pack.missing_sections.join(', ')}`);
607
+ }
608
+ }
609
+ if (!signals.memory_summary.present) {
610
+ score += 1;
611
+ reasons.push('project memory summary is missing');
612
+ }
613
+ if (dirtyFiles.length >= 12) {
614
+ score += 3;
615
+ reasons.push(`${dirtyFiles.length} dirty files increase review and reporting pressure`);
616
+ } else if (dirtyFiles.length >= 4) {
617
+ score += 2;
618
+ reasons.push(`${dirtyFiles.length} dirty files suggest the scope is widening`);
619
+ } else if (dirtyFiles.length > 0) {
620
+ score += 1;
621
+ reasons.push(`${dirtyFiles.length} dirty file(s) should be kept in the final handoff`);
622
+ }
623
+ if (instructionSurfaces.issues.length) {
624
+ score += 3;
625
+ reasons.push('active hook or instruction issue can change route behavior');
626
+ }
627
+ if (instructionSurfaces.warnings.length >= 2) {
628
+ score += 1;
629
+ reasons.push('multiple instruction surfaces can make guidance harder to predict');
630
+ }
631
+ if (detection.packageJson && !detection.commands.build && !detection.commands.typecheck) {
632
+ score += 1;
633
+ reasons.push('no build/typecheck script was detected for quick implementation verification');
634
+ }
635
+
636
+ const pressure = score >= 7 ? 'high' : score >= 3 ? 'medium' : 'low';
637
+ return {
638
+ schema: 'yam.context-pressure.v1',
639
+ generated_at: new Date().toISOString(),
640
+ project: dir,
641
+ pressure,
642
+ score,
643
+ why_pressure_is_rising: reasons,
644
+ signals,
645
+ beginner_insight: contextPressureInsight(pressure),
646
+ recommended_next_action: pressure === 'high'
647
+ ? 'pause and narrow scope, refresh yam.project.md or memory summary, then choose the route deliberately'
648
+ : pressure === 'medium'
649
+ ? 'keep the task scoped and summarize important changes before expanding'
650
+ : 'continue with the current route and the smallest honest verification',
651
+ 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',
652
+ truth_status: 'partial'
653
+ };
654
+ }
655
+
656
+ async function projectPackPressureSignal(pack) {
657
+ if (!pack) return { present: false, path: '', age_days: null, words: 0, size_bytes: 0, missing_sections: REQUIRED_PACK_SECTIONS };
658
+ const text = await readText(pack).catch(() => '');
659
+ const stat = await fsp.stat(pack).catch(() => null);
660
+ return {
661
+ present: true,
662
+ path: pack,
663
+ age_days: stat ? Math.floor((Date.now() - stat.mtimeMs) / 86400000) : null,
664
+ words: countWords(text),
665
+ size_bytes: stat?.size || 0,
666
+ missing_sections: REQUIRED_PACK_SECTIONS.filter((section) => !hasHeading(text, section))
667
+ };
668
+ }
669
+
670
+ function contextPressureInsight(pressure) {
671
+ 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.';
672
+ if (pressure === 'medium') return 'The task is still manageable, but scope or stale context may start causing repeated reading and weaker final explanations.';
673
+ return 'The current context looks small enough to keep momentum without heavy proof, as long as claims stay modest.';
674
+ }
675
+
676
+ function printContextPressureReport(report) {
677
+ console.log('yam context pressure');
678
+ console.log(`Project: ${report.project}`);
679
+ console.log(`Pressure: ${report.pressure} (score ${report.score})`);
680
+ console.log(`Insight: ${report.beginner_insight}`);
681
+ if (report.why_pressure_is_rising.length) {
682
+ console.log('');
683
+ console.log('Why pressure is rising:');
684
+ for (const reason of report.why_pressure_is_rising) console.log(`- ${reason}`);
685
+ }
686
+ console.log('');
687
+ console.log(`Next: ${report.recommended_next_action}`);
688
+ console.log(`Route hint: ${report.route_hint}`);
689
+ }
690
+
691
+ async function cleanup(args = []) {
692
+ const subcommand = args[0] || 'help';
693
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return cleanupUsage();
694
+ if (subcommand === 'scan') return cleanupScan(args.slice(1));
695
+ console.error(`unknown cleanup command: ${subcommand}`);
696
+ return cleanupUsage();
697
+ }
698
+
699
+ function cleanupUsage() {
700
+ console.log(`yam cleanup
701
+
702
+ Usage:
703
+ yam cleanup scan [dir] [--json]
704
+
705
+ Notes:
706
+ Read-only cleanup scan. It never deletes files; it only reports confusing surfaces and safe next actions.
707
+ `);
708
+ }
709
+
710
+ async function cleanupScan(args = []) {
711
+ const parsed = parseDirJsonArgs(args);
712
+ const report = await buildCleanupScanReport(parsed.dir);
713
+ if (parsed.json) {
714
+ console.log(JSON.stringify(report, null, 2));
715
+ return;
716
+ }
717
+ printCleanupScanReport(report);
718
+ }
719
+
720
+ async function buildCleanupScanReport(targetDir = process.cwd()) {
721
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
722
+ const findings = [];
723
+ const instructionSurfaces = await findInstructionSurfaces(dir);
724
+ for (const found of instructionSurfaces.found) {
725
+ const issue = instructionSurfaces.issues.find((item) => item.startsWith(`${found}:`));
726
+ const warning = instructionSurfaces.warnings.find((item) => item.startsWith(`${found}:`));
727
+ 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'));
728
+ }
729
+ const projectAgents = path.join(dir, '.agents');
730
+ if (await exists(projectAgents)) {
731
+ 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'));
732
+ }
733
+ for (const hook of [path.join(dir, '.codex', 'hooks.json'), path.join(os.homedir(), '.codex', 'hooks.json')]) {
734
+ 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'));
735
+ }
736
+ for (const skill of [...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
737
+ 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'));
738
+ }
739
+ const legacyPackageHits = await scanLimitedTextFiles(dir, ['package.json', 'README.md', 'COMMANDS.md', 'CHANGELOG.md'], /\b(yam-harness|yam-codex)\b/i);
740
+ for (const hit of legacyPackageHits) {
741
+ findings.push(cleanupFinding('low', hit.file, `legacy package name mention: ${hit.match}`, 'update public text only if it confuses install guidance'));
742
+ }
743
+ for (const artifact of await staleYamArtifacts(dir)) {
744
+ findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
745
+ }
746
+ return {
747
+ schema: 'yam.cleanup-scan.v1',
748
+ generated_at: new Date().toISOString(),
749
+ project: dir,
750
+ destructive: false,
751
+ findings,
752
+ risk_level: overallCleanupRisk(findings),
753
+ safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
754
+ truth_status: 'partial'
755
+ };
756
+ }
757
+
758
+ function cleanupFinding(riskLevel, surface, whyItMatters, safeNextAction) {
759
+ return {
760
+ risk_level: riskLevel,
761
+ surface,
762
+ why_it_matters: whyItMatters,
763
+ safe_next_action: safeNextAction,
764
+ destructive: false,
765
+ truth_status: 'partial'
766
+ };
767
+ }
768
+
769
+ function overallCleanupRisk(findings = []) {
770
+ if (findings.some((item) => item.risk_level === 'high')) return 'high';
771
+ if (findings.some((item) => item.risk_level === 'medium')) return 'medium';
772
+ return 'low';
773
+ }
774
+
775
+ async function scanLimitedTextFiles(dir, relativeFiles = [], pattern = /$^/) {
776
+ const hits = [];
777
+ for (const relative of relativeFiles) {
778
+ const file = path.join(dir, relative);
779
+ if (!await exists(file)) continue;
780
+ const text = await readText(file).catch(() => '');
781
+ const match = text.match(pattern);
782
+ if (match) hits.push({ file: relative, match: match[0] });
783
+ }
784
+ return hits;
785
+ }
786
+
787
+ async function staleYamArtifacts(dir) {
788
+ const candidates = [
789
+ path.join(dir, '.yam', 'proof.json'),
790
+ path.join(dir, '.yam', 'proof.md'),
791
+ path.join(dir, '.yam', 'runtime-proof.md'),
792
+ path.join(dir, '.yam', 'ueye-report.json')
793
+ ];
794
+ const artifacts = [];
795
+ for (const file of candidates) {
796
+ if (!await exists(file)) continue;
797
+ const stat = await fsp.stat(file).catch(() => null);
798
+ if (stat && Date.now() - stat.mtimeMs > 14 * 86400000) {
799
+ artifacts.push({ file: path.relative(dir, file), reason: 'old proof/runtime artifact may be mistaken for current evidence' });
800
+ }
801
+ }
802
+ return artifacts;
803
+ }
804
+
805
+ function printCleanupScanReport(report) {
806
+ console.log('yam cleanup scan');
807
+ console.log(`Project: ${report.project}`);
808
+ console.log(`Risk level: ${report.risk_level}`);
809
+ console.log('Destructive: false');
810
+ if (!report.findings.length) {
811
+ console.log('No confusing cleanup surfaces found.');
812
+ return;
813
+ }
814
+ for (const finding of report.findings) {
815
+ console.log(`- ${finding.risk_level}: ${finding.surface}`);
816
+ console.log(` why: ${finding.why_it_matters}`);
817
+ console.log(` next: ${finding.safe_next_action}`);
818
+ }
819
+ }
820
+
821
+ function parseDirJsonArgs(args = []) {
822
+ const flags = parseSimpleFlags(args, new Set(['json']));
823
+ const dir = flags._?.find(looksLikeDirectoryArg) || process.cwd();
824
+ return { dir, json: Boolean(flags.json) };
825
+ }
826
+
531
827
  async function tools(args = []) {
532
828
  const subcommand = args[0] || 'doctor';
533
829
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return toolsUsage();
@@ -588,6 +884,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
588
884
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
589
885
  const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
590
886
  const sqlScan = await scanSqlFiles(dir);
887
+ const contextPressure = await buildContextPressureReport(dir);
888
+ const realProbe = await buildRealProbeReport(dir, packageInfo.pkg, detection);
591
889
  const rows = [
592
890
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
593
891
  readinessRow('Yam skills', await status({ quiet: true }) === 0 ? 'ready' : 'missing', DEST),
@@ -624,6 +922,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
624
922
  instructionSurfaces,
625
923
  projectSurfaces,
626
924
  frameworkChecklist,
925
+ contextPressure,
926
+ realProbe,
627
927
  sqlScan,
628
928
  riskNotes,
629
929
  nextActions: nextActionDetails.map((action) => action.next_action),
@@ -668,6 +968,17 @@ function printToolsDoctorReport(data) {
668
968
  }
669
969
  }
670
970
 
971
+ if (data.contextPressure) {
972
+ console.log('');
973
+ console.log(`Context pressure: ${data.contextPressure.pressure} (${data.contextPressure.recommended_next_action})`);
974
+ }
975
+
976
+ if (data.realProbe?.probes?.length) {
977
+ console.log('');
978
+ console.log(`Real probe: ${data.realProbe.readiness_truth_status}`);
979
+ for (const probe of data.realProbe.probes) console.log(`- ${probe.id}: ${probe.status} (${probe.truth_status})`);
980
+ }
981
+
671
982
  if (data.sqlScan.filesScanned > 0) {
672
983
  console.log('');
673
984
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -800,6 +1111,63 @@ function checklistRow(id, label, route) {
800
1111
  return { id, label, route };
801
1112
  }
802
1113
 
1114
+ async function buildRealProbeReport(dir = process.cwd(), pkg: AnyRecord | null = null, detection: AnyRecord = {}) {
1115
+ const probes = [];
1116
+ probes.push(realProbeRow('node_version', 'observed', process.version, 'verified'));
1117
+ const npmVersion = runReadOnlyCommandIn(dir, 'npm', ['--version']).trim();
1118
+ probes.push(realProbeRow('npm_version', npmVersion ? 'observed' : 'not_found', npmVersion || 'npm not found or not runnable', npmVersion ? 'verified' : 'partial'));
1119
+ probes.push(realProbeRow('yam_binary', 'observed', `${process.argv[1] || 'unknown'} @ ${VERSION}`, 'verified'));
1120
+ const scripts = detection.packageJson ? detection.commands : {};
1121
+ probes.push(realProbeRow('package_scripts', Object.values(scripts).some(Boolean) ? 'observed' : 'not_found', scripts, Object.values(scripts).some(Boolean) ? 'verified' : 'partial'));
1122
+ const playwrightPath = await localPackagePath(dir, 'playwright');
1123
+ probes.push(realProbeRow('playwright_package', playwrightPath ? 'observed' : 'not_found', playwrightPath || 'playwright package not found in project or yam package context', playwrightPath ? 'verified' : 'partial'));
1124
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
1125
+ probes.push(realProbeRow('in_app_browser_plugin_cache', browserCache ? 'observed' : 'not_found', browserCache ? 'plugin cache present' : 'plugin cache not found locally', browserCache ? 'partial' : 'partial'));
1126
+ const tmux = await findExecutable('tmux');
1127
+ const zellij = await findExecutable('zellij');
1128
+ probes.push(realProbeRow('tmux', tmux ? 'observed' : 'not_found', tmux || 'not found on PATH', tmux ? 'verified' : 'partial'));
1129
+ probes.push(realProbeRow('zellij', zellij ? 'observed' : 'not_found', zellij || 'not found on PATH', zellij ? 'verified' : 'partial'));
1130
+ const gitInside = runReadOnlyCommandIn(dir, 'git', ['rev-parse', '--is-inside-work-tree']).trim();
1131
+ const gitDirty = runReadOnlyCommandIn(dir, 'git', ['status', '--short']).trim();
1132
+ probes.push(realProbeRow('git_state', gitInside === 'true' ? 'observed' : 'not_found', {
1133
+ inside_work_tree: gitInside === 'true',
1134
+ dirty_file_count: gitDirty ? gitDirty.split(/\r?\n/).length : 0
1135
+ }, gitInside === 'true' ? 'verified' : 'partial'));
1136
+ return {
1137
+ schema: 'yam.real-probe.v1',
1138
+ generated_at: new Date().toISOString(),
1139
+ project: path.resolve(dir),
1140
+ probes,
1141
+ readiness_truth_status: probes.some((probe) => probe.status === 'blocked') ? 'blocked' : 'partial',
1142
+ note: 'read-only local availability probe; no browser, server, database, or process was started'
1143
+ };
1144
+ }
1145
+
1146
+ function realProbeRow(id, status, evidence, truthStatus = 'partial') {
1147
+ return {
1148
+ id,
1149
+ status,
1150
+ evidence,
1151
+ truth_status: truthStatus
1152
+ };
1153
+ }
1154
+
1155
+ async function localPackagePath(dir, packageName) {
1156
+ const candidates = [
1157
+ path.join(dir, 'node_modules', packageName, 'package.json'),
1158
+ path.join(ROOT, 'node_modules', packageName, 'package.json')
1159
+ ];
1160
+ for (const candidate of candidates) {
1161
+ if (await exists(candidate)) return candidate;
1162
+ }
1163
+ try {
1164
+ const projectRequire = createRequire(path.join(dir, 'package.json'));
1165
+ return projectRequire.resolve(`${packageName}/package.json`);
1166
+ } catch {
1167
+ return '';
1168
+ }
1169
+ }
1170
+
803
1171
  function toolsDoctorNextActions(input) {
804
1172
  return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
805
1173
  }
@@ -1156,6 +1524,13 @@ async function release(args = []) {
1156
1524
  console.log('- Failed:');
1157
1525
  for (const id of report.failed) console.log(` - ${id}`);
1158
1526
  }
1527
+ if (report.publishBlockerEvidence?.length) {
1528
+ console.log('- Publish blockers:');
1529
+ for (const blocker of report.publishBlockerEvidence) {
1530
+ console.log(` - ${blocker.blocker_kind}: ${blocker.likely_cause}`);
1531
+ console.log(` next: ${blocker.safe_next_action}`);
1532
+ }
1533
+ }
1159
1534
  if (report.next_actions?.length) {
1160
1535
  console.log('- Next actions:');
1161
1536
  for (const action of report.next_actions) console.log(` - ${action.next_action}`);
@@ -1177,7 +1552,8 @@ function runReleaseReport() {
1177
1552
  const provenance = releaseProvenance(checks);
1178
1553
  const tarball = releaseTarballProvenance();
1179
1554
  const freshness = releaseFreshness(checks, provenance, tarball);
1180
- const nextActions = releaseNextActions(checks, provenance, tarball);
1555
+ const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
1556
+ const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence);
1181
1557
  return {
1182
1558
  schema: 'yam.release-report.v1',
1183
1559
  generated_at: startedAt,
@@ -1190,6 +1566,7 @@ function runReleaseReport() {
1190
1566
  provenance,
1191
1567
  tarball,
1192
1568
  freshness,
1569
+ publishBlockerEvidence,
1193
1570
  next_actions: nextActions
1194
1571
  };
1195
1572
  }
@@ -1269,7 +1646,7 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
1269
1646
  };
1270
1647
  }
1271
1648
 
1272
- function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
1649
+ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
1273
1650
  const actions = [];
1274
1651
  for (const check of checks) {
1275
1652
  if (check.status === 'passed') continue;
@@ -1284,9 +1661,71 @@ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: An
1284
1661
  if (tarball.status !== 'checked') {
1285
1662
  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
1663
  }
1664
+ for (const blocker of publishBlockers) {
1665
+ actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
1666
+ }
1287
1667
  return uniqueNextActionDetails(actions);
1288
1668
  }
1289
1669
 
1670
+ function publishBlockerEvidenceFromRelease(checks = [], tarball: AnyRecord = {}) {
1671
+ const texts = [
1672
+ ...checks.map((check) => `${check.id}\n${check.note || ''}`),
1673
+ tarball.reason || ''
1674
+ ].filter(Boolean);
1675
+ const blockers = [];
1676
+ for (const text of texts) {
1677
+ const blocker = classifyPublishBlockerText(text);
1678
+ if (blocker) blockers.push(blocker);
1679
+ }
1680
+ const seen = new Set<string>();
1681
+ return blockers.filter((blocker) => {
1682
+ if (seen.has(blocker.blocker_kind)) return false;
1683
+ seen.add(blocker.blocker_kind);
1684
+ return true;
1685
+ });
1686
+ }
1687
+
1688
+ function classifyPublishBlockerText(text = '') {
1689
+ const value = String(text || '');
1690
+ if (!value.trim()) return null;
1691
+ const excerpt = summarizeCheckOutput(value);
1692
+ if (/\b(ENEEDAUTH|E401|Unauthorized|whoami)\b/i.test(value)) {
1693
+ 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`');
1694
+ }
1695
+ if (/\b(OTP|one-time password|two-factor|2FA)\b/i.test(value)) {
1696
+ 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');
1697
+ }
1698
+ if (/\bE403\b|403\s+Forbidden|Forbidden\s+-\s+PUT|npm ERR!.*Forbidden/i.test(value)) {
1699
+ 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');
1700
+ }
1701
+ if (/\bE404\b|Not Found - PUT/i.test(value)) {
1702
+ 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');
1703
+ }
1704
+ if (/previously published|cannot publish over|already exists|You cannot publish over/i.test(value)) {
1705
+ return publishBlocker('version_already_published', excerpt, 'npm versions are immutable once published', 'bump package.json to the next patch version before publishing');
1706
+ }
1707
+ if (/(EACCES|EPERM|permission denied|cache|_cacache)/i.test(value)) {
1708
+ 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`');
1709
+ }
1710
+ if (/(ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|registry unreachable|network timeout)/i.test(value)) {
1711
+ 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');
1712
+ }
1713
+ if (/(tarball|package boundary|files list|npm pack)/i.test(value)) {
1714
+ 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');
1715
+ }
1716
+ return null;
1717
+ }
1718
+
1719
+ function publishBlocker(kind, excerpt, likelyCause, safeNextAction) {
1720
+ return {
1721
+ blocker_kind: kind,
1722
+ native_error_excerpt: excerpt,
1723
+ likely_cause: likelyCause,
1724
+ safe_next_action: safeNextAction,
1725
+ truth_status: 'partial'
1726
+ };
1727
+ }
1728
+
1290
1729
  function runReadOnlyCommand(command, args = []) {
1291
1730
  const result = spawnSync(command, args, {
1292
1731
  cwd: ROOT,
@@ -1297,6 +1736,16 @@ function runReadOnlyCommand(command, args = []) {
1297
1736
  return String(result.stdout || '');
1298
1737
  }
1299
1738
 
1739
+ function runReadOnlyCommandIn(cwd, command, args = []) {
1740
+ const result = spawnSync(command, args, {
1741
+ cwd,
1742
+ encoding: 'utf8',
1743
+ timeout: 30000
1744
+ });
1745
+ if (result.status !== 0) return '';
1746
+ return String(result.stdout || '');
1747
+ }
1748
+
1300
1749
  function runReleaseCheck(id, commandSpec) {
1301
1750
  const [command, args] = commandSpec;
1302
1751
  const start = Date.now();
@@ -1337,6 +1786,7 @@ async function ueye(args = []) {
1337
1786
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1338
1787
  if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1339
1788
  if (subcommand === 'compare') return ueyeCompare(args.slice(1));
1789
+ if (subcommand === 'preflight') return ueyePreflight(args.slice(1));
1340
1790
  if (subcommand === 'report') return ueyeReport(args.slice(1));
1341
1791
  console.error(`unknown ueye command: ${subcommand}`);
1342
1792
  return ueyeUsage();
@@ -1350,7 +1800,8 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1350
1800
  Usage:
1351
1801
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1352
1802
  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]
1803
+ yam ueye preflight [dir] [--json]
1804
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--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
1805
 
1355
1806
  Notes:
1356
1807
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -1359,6 +1810,72 @@ Notes:
1359
1810
  `);
1360
1811
  }
1361
1812
 
1813
+ async function ueyePreflight(args = []) {
1814
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1815
+ const parsed = parseDirJsonArgs(args);
1816
+ const report = await buildUeyePreflightReport(parsed.dir);
1817
+ if (parsed.json) {
1818
+ console.log(JSON.stringify(report, null, 2));
1819
+ return;
1820
+ }
1821
+ console.log('Ueye preflight');
1822
+ console.log(`Project: ${report.project}`);
1823
+ console.log(`Preflight id: ${report.preflight_id}`);
1824
+ console.log(`Truth status: ${report.truth_status}`);
1825
+ for (const item of report.checklist) console.log(`- ${item.status}: ${item.item} (${item.next_action})`);
1826
+ }
1827
+
1828
+ async function buildUeyePreflightReport(targetDir = process.cwd()) {
1829
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
1830
+ const packageInfo = await projectPackageInfo(dir);
1831
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
1832
+ const pack = await findProjectPack(dir);
1833
+ const playwrightPath = await localPackagePath(dir, 'playwright');
1834
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
1835
+ const uiPaths = await existingRelativePaths(dir, ['app', 'src/app', 'pages', 'src/pages', 'components', 'src/components', 'styles', 'src/styles']);
1836
+ const screenshotAvailability = playwrightPath ? 'automated_capture_possible' : browserCache ? 'in_app_or_user_screenshot_needed' : 'user_screenshot_needed';
1837
+ const p0p1Risks = [];
1838
+ if (!frameworkChecklist.uiLike) p0p1Risks.push('target UI surface is not detected yet');
1839
+ if (!browserCache && !playwrightPath) p0p1Risks.push('no local capture surface was observed');
1840
+ if (!pack) p0p1Risks.push('project direction pack is missing');
1841
+ return {
1842
+ schema: 'yam.ueye-preflight.v1',
1843
+ preflight_id: `ueye-preflight-${timestampId()}`,
1844
+ generated_at: new Date().toISOString(),
1845
+ project: dir,
1846
+ detected_framework: frameworkChecklist.framework,
1847
+ ui_paths: uiPaths,
1848
+ reference_source_needed: true,
1849
+ target_states_needed: ['default', 'loading', 'error', 'empty', 'mobile'],
1850
+ mobile_responsive_check_needed: true,
1851
+ contrast_cta_accessibility_risk: frameworkChecklist.uiLike ? 'check before done claim' : 'unknown until UI target is identified',
1852
+ p0_p1_risk_candidates: p0p1Risks,
1853
+ screenshot_capture_availability: screenshotAvailability,
1854
+ preferred_surface: 'in-app-browser',
1855
+ checklist: [
1856
+ preflightItem('reference', 'reference source selected or user confirms no reference', 'needed', 'record reference path, screenshot, or text target before claiming design fidelity'),
1857
+ preflightItem('states', 'primary states are listed', 'needed', 'prepare default/loading/error/empty/mobile states when the UI supports them'),
1858
+ preflightItem('mobile', 'mobile/responsive pass planned', 'needed', 'capture or inspect a narrow viewport before verified visual claims'),
1859
+ preflightItem('contrast_cta_accessibility', 'CTA, contrast, and accessible labels reviewed', 'needed', 'mark P0/P1 risks before saying done'),
1860
+ preflightItem('capture', 'screenshot/capture surface available', screenshotAvailability === 'automated_capture_possible' ? 'observed' : 'partial', 'use in-app browser or user-provided screenshot if automation is unavailable')
1861
+ ],
1862
+ completion_cap: 'preflight cannot prove visual quality without implementation screenshot and comparison evidence',
1863
+ truth_status: 'partial'
1864
+ };
1865
+ }
1866
+
1867
+ function preflightItem(id, item, status, nextAction) {
1868
+ return { id, item, status, next_action: nextAction, truth_status: status === 'observed' ? 'verified' : 'partial' };
1869
+ }
1870
+
1871
+ async function existingRelativePaths(dir, relativePaths = []) {
1872
+ const found = [];
1873
+ for (const relative of relativePaths) {
1874
+ if (await exists(path.join(dir, relative))) found.push(relative);
1875
+ }
1876
+ return found;
1877
+ }
1878
+
1362
1879
  async function ueyeCapture(args = []) {
1363
1880
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1364
1881
  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 +2038,7 @@ async function ueyeCompare(args = []) {
1521
2038
 
1522
2039
  async function ueyeReport(args = []) {
1523
2040
  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']));
2041
+ 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', '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
2042
  const reference = String(flags.reference || flags.ref || '');
1526
2043
  const actual = String(flags.actual || flags.screenshot || '');
1527
2044
  const referenceSources = [];
@@ -1613,7 +2130,7 @@ async function ueyeReport(args = []) {
1613
2130
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
1614
2131
  design_score: numberOrNull(flags.design_score),
1615
2132
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
1616
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0)],
2133
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
1617
2134
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
1618
2135
  states_checked: Boolean(flags.states_checked),
1619
2136
  mobile_checked: Boolean(flags.mobile_checked),
@@ -1654,6 +2171,12 @@ async function ueyeReport(args = []) {
1654
2171
  const result = {
1655
2172
  ...report,
1656
2173
  review_session_id: reviewSessionId,
2174
+ preflight: {
2175
+ preflight_id: String(flags.preflight_id || ''),
2176
+ p0_risks: arrayFlag(flags.p0_risk),
2177
+ quality_gate_notes: arrayFlag(flags.quality_gate_note),
2178
+ truth_status: flags.preflight_id || flags.p0_risk || flags.quality_gate_note ? 'partial' : 'assumed'
2179
+ },
1657
2180
  tool_intent: 'visual',
1658
2181
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1659
2182
  compare_backend: String(flags.compare_backend || 'local-file-hash'),
@@ -1821,7 +2344,7 @@ function runtimeUsage() {
1821
2344
  console.log(`yam runtime
1822
2345
 
1823
2346
  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]
2347
+ 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
2348
 
1826
2349
  Notes:
1827
2350
  Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
@@ -1830,13 +2353,21 @@ Notes:
1830
2353
 
1831
2354
  async function runtimeEvidence(args = []) {
1832
2355
  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']));
2356
+ 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
2357
  const evidence = buildRuntimeBackendEvidence({
1835
2358
  backend: flags.backend,
1836
2359
  claim: flags.claim,
1837
2360
  evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1838
2361
  command: String(flags.command || ''),
1839
2362
  cleanup_checked: Boolean(flags.cleanup_checked),
2363
+ started_at: String(flags.started_at || ''),
2364
+ stopped_at: String(flags.stopped_at || ''),
2365
+ exit_code: numberOrNull(flags.exit_code),
2366
+ pid: numberOrNull(flags.pid),
2367
+ port: numberOrNull(flags.port),
2368
+ cleanup_method: String(flags.cleanup_method || ''),
2369
+ cleanup_observed: Boolean(flags.cleanup_observed),
2370
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
1840
2371
  note: String(flags.note || ''),
1841
2372
  truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1842
2373
  });
@@ -1863,17 +2394,20 @@ function runtimeEvidenceDetails(flags: AnyRecord = {}) {
1863
2394
  exit_code: numberOrNull(flags.exit_code),
1864
2395
  screenshot_id: String(flags.screenshot_id || ''),
1865
2396
  started_at: String(flags.started_at || ''),
1866
- stopped_at: String(flags.stopped_at || '')
2397
+ stopped_at: String(flags.stopped_at || ''),
2398
+ cleanup_method: String(flags.cleanup_method || ''),
2399
+ cleanup_observed: Boolean(flags.cleanup_observed),
2400
+ left_running_intentionally: Boolean(flags.left_running_intentionally)
1867
2401
  };
1868
2402
  }
1869
2403
 
1870
2404
  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);
2405
+ 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
2406
  }
1873
2407
 
1874
2408
  function runtimeEvidenceNextAction(evidence) {
1875
2409
  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';
2410
+ 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
2411
  if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
1878
2412
  return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
1879
2413
  }
@@ -2043,7 +2577,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
2043
2577
  const key = rawKey.slice(2);
2044
2578
  if (allowed.size && !allowed.has(key)) continue;
2045
2579
  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)) {
2580
+ 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'].includes(key)) {
2047
2581
  flags[normalizedKey] = true;
2048
2582
  continue;
2049
2583
  }
@@ -2393,7 +2927,7 @@ function renderProofMarkdownList(values) {
2393
2927
  function parseProofArgs(args = []) {
2394
2928
  const flags: AnyRecord = {};
2395
2929
  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']);
2930
+ 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
2931
  for (let index = 0; index < args.length; index += 1) {
2398
2932
  const arg = args[index];
2399
2933
  if (arg.startsWith('--')) {
@@ -2401,7 +2935,7 @@ function parseProofArgs(args = []) {
2401
2935
  const key = rawKey.slice(2);
2402
2936
  if (!aliases.has(key)) continue;
2403
2937
  const normalizedKey = key.replace(/-/g, '_');
2404
- if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
2938
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked' || key === 'cleanup-observed' || key === 'left-running-intentionally') {
2405
2939
  flags[normalizedKey] = true;
2406
2940
  continue;
2407
2941
  }
@@ -2561,12 +3095,20 @@ function structuredEvidenceList(value) {
2561
3095
  }
2562
3096
 
2563
3097
  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 [];
3098
+ 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
3099
  return [JSON.stringify(buildRuntimeBackendEvidence({
2566
3100
  backend: flags.runtime_backend,
2567
3101
  claim: flags.runtime_claim,
2568
3102
  evidence_id: flags.runtime_evidence_id,
2569
3103
  command: flags.runtime_command,
3104
+ pid: numberOrNull(flags.runtime_pid),
3105
+ port: numberOrNull(flags.runtime_port),
3106
+ exit_code: numberOrNull(flags.runtime_exit_code),
3107
+ started_at: String(flags.runtime_started_at || ''),
3108
+ stopped_at: String(flags.runtime_stopped_at || ''),
3109
+ cleanup_method: String(flags.runtime_cleanup_method || ''),
3110
+ cleanup_observed: Boolean(flags.cleanup_observed),
3111
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
2570
3112
  cleanup_checked: Boolean(flags.cleanup_checked),
2571
3113
  note: flags.runtime_note
2572
3114
  }))];
@@ -3303,6 +3845,8 @@ async function main() {
3303
3845
  if (command === 'version') return console.log(VERSION);
3304
3846
  if (command === 'detect') return detectProject(process.argv[3]);
3305
3847
  if (command === 'pack') return inspectProjectPack(process.argv[3]);
3848
+ if (command === 'context') return context(process.argv.slice(3));
3849
+ if (command === 'cleanup') return cleanup(process.argv.slice(3));
3306
3850
  if (command === 'budget') return budget(process.argv[3]);
3307
3851
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
3308
3852
  if (command === 'tools') return tools(process.argv.slice(3));