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/dist/bin/yam.js CHANGED
@@ -115,6 +115,8 @@ Usage:
115
115
  yam verify
116
116
  yam detect [dir]
117
117
  yam pack [dir]
118
+ yam context pressure [dir] [--json]
119
+ yam cleanup scan [dir] [--json]
118
120
  yam budget [route]
119
121
  yam measure <route> [--files n] [--commands n] [--report-lines n] [--seconds n]
120
122
  yam tools doctor [dir]
@@ -122,6 +124,7 @@ Usage:
122
124
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
123
125
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
124
126
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
127
+ yam ueye preflight [dir] [--json]
125
128
  yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
126
129
  yam media proof [--requested] [--attempted] [--output file] [--json]
127
130
  yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
@@ -302,6 +305,7 @@ async function verify({ quiet = false } = {}) {
302
305
  'ui-quality.md',
303
306
  'question.md',
304
307
  'mission.md',
308
+ 'cleanup-scan.md',
305
309
  'doctor-scan.md',
306
310
  'scout.md',
307
311
  'runtime-orchestration.md',
@@ -529,6 +533,297 @@ function uniqueNextActionDetails(actions = []) {
529
533
  }
530
534
  return unique;
531
535
  }
536
+ async function context(args = []) {
537
+ const subcommand = args[0] || 'help';
538
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
539
+ return contextUsage();
540
+ if (subcommand === 'pressure')
541
+ return contextPressure(args.slice(1));
542
+ console.error(`unknown context command: ${subcommand}`);
543
+ return contextUsage();
544
+ }
545
+ function contextUsage() {
546
+ console.log(`yam context
547
+
548
+ Usage:
549
+ yam context pressure [dir] [--json]
550
+
551
+ Notes:
552
+ Read-only context pressure scan. It explains when to summarize, refresh the project pack, narrow scope, or deepen the route.
553
+ `);
554
+ }
555
+ async function contextPressure(args = []) {
556
+ const parsed = parseDirJsonArgs(args);
557
+ const report = await buildContextPressureReport(parsed.dir);
558
+ if (parsed.json) {
559
+ console.log(JSON.stringify(report, null, 2));
560
+ return;
561
+ }
562
+ printContextPressureReport(report);
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 = {
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
+ if (!signals.project_pack.present) {
588
+ score += 3;
589
+ reasons.push('project pack is missing, so direction may need rediscovery');
590
+ }
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
+ }
617
+ else if (dirtyFiles.length >= 4) {
618
+ score += 2;
619
+ reasons.push(`${dirtyFiles.length} dirty files suggest the scope is widening`);
620
+ }
621
+ else if (dirtyFiles.length > 0) {
622
+ score += 1;
623
+ reasons.push(`${dirtyFiles.length} dirty file(s) should be kept in the final handoff`);
624
+ }
625
+ if (instructionSurfaces.issues.length) {
626
+ score += 3;
627
+ reasons.push('active hook or instruction issue can change route behavior');
628
+ }
629
+ if (instructionSurfaces.warnings.length >= 2) {
630
+ score += 1;
631
+ reasons.push('multiple instruction surfaces can make guidance harder to predict');
632
+ }
633
+ if (detection.packageJson && !detection.commands.build && !detection.commands.typecheck) {
634
+ score += 1;
635
+ reasons.push('no build/typecheck script was detected for quick implementation verification');
636
+ }
637
+ const pressure = score >= 7 ? 'high' : score >= 3 ? 'medium' : 'low';
638
+ return {
639
+ schema: 'yam.context-pressure.v1',
640
+ generated_at: new Date().toISOString(),
641
+ project: dir,
642
+ pressure,
643
+ score,
644
+ why_pressure_is_rising: reasons,
645
+ signals,
646
+ beginner_insight: contextPressureInsight(pressure),
647
+ recommended_next_action: pressure === 'high'
648
+ ? 'pause and narrow scope, refresh yam.project.md or memory summary, then choose the route deliberately'
649
+ : pressure === 'medium'
650
+ ? 'keep the task scoped and summarize important changes before expanding'
651
+ : 'continue with the current route and the smallest honest verification',
652
+ 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',
653
+ truth_status: 'partial'
654
+ };
655
+ }
656
+ async function projectPackPressureSignal(pack) {
657
+ if (!pack)
658
+ return { present: false, path: '', age_days: null, words: 0, size_bytes: 0, missing_sections: REQUIRED_PACK_SECTIONS };
659
+ const text = await readText(pack).catch(() => '');
660
+ const stat = await fsp.stat(pack).catch(() => null);
661
+ return {
662
+ present: true,
663
+ path: pack,
664
+ age_days: stat ? Math.floor((Date.now() - stat.mtimeMs) / 86400000) : null,
665
+ words: countWords(text),
666
+ size_bytes: stat?.size || 0,
667
+ missing_sections: REQUIRED_PACK_SECTIONS.filter((section) => !hasHeading(text, section))
668
+ };
669
+ }
670
+ function contextPressureInsight(pressure) {
671
+ if (pressure === 'high')
672
+ 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.';
673
+ if (pressure === 'medium')
674
+ return 'The task is still manageable, but scope or stale context may start causing repeated reading and weaker final explanations.';
675
+ return 'The current context looks small enough to keep momentum without heavy proof, as long as claims stay modest.';
676
+ }
677
+ function printContextPressureReport(report) {
678
+ console.log('yam context pressure');
679
+ console.log(`Project: ${report.project}`);
680
+ console.log(`Pressure: ${report.pressure} (score ${report.score})`);
681
+ console.log(`Insight: ${report.beginner_insight}`);
682
+ if (report.why_pressure_is_rising.length) {
683
+ console.log('');
684
+ console.log('Why pressure is rising:');
685
+ for (const reason of report.why_pressure_is_rising)
686
+ console.log(`- ${reason}`);
687
+ }
688
+ console.log('');
689
+ console.log(`Next: ${report.recommended_next_action}`);
690
+ console.log(`Route hint: ${report.route_hint}`);
691
+ }
692
+ async function cleanup(args = []) {
693
+ const subcommand = args[0] || 'help';
694
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
695
+ return cleanupUsage();
696
+ if (subcommand === 'scan')
697
+ return cleanupScan(args.slice(1));
698
+ console.error(`unknown cleanup command: ${subcommand}`);
699
+ return cleanupUsage();
700
+ }
701
+ function cleanupUsage() {
702
+ console.log(`yam cleanup
703
+
704
+ Usage:
705
+ yam cleanup scan [dir] [--json]
706
+
707
+ Notes:
708
+ Read-only cleanup scan. It never deletes files; it only reports confusing surfaces and safe next actions.
709
+ `);
710
+ }
711
+ async function cleanupScan(args = []) {
712
+ const parsed = parseDirJsonArgs(args);
713
+ const report = await buildCleanupScanReport(parsed.dir);
714
+ if (parsed.json) {
715
+ console.log(JSON.stringify(report, null, 2));
716
+ return;
717
+ }
718
+ printCleanupScanReport(report);
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))
735
+ findings.push(cleanupFinding('high', hook, 'active hooks can add hidden latency or route pressure', 'disable or edit only after user approval and backup'));
736
+ }
737
+ for (const skill of [...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
738
+ if (await exists(path.join(DEST, skill, 'SKILL.md')))
739
+ 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'));
740
+ }
741
+ const legacyPackageHits = await scanLimitedTextFiles(dir, ['package.json', 'README.md', 'COMMANDS.md', 'CHANGELOG.md'], /\b(yam-harness|yam-codex)\b/i);
742
+ for (const hit of legacyPackageHits) {
743
+ findings.push(cleanupFinding('low', hit.file, `legacy package name mention: ${hit.match}`, 'update public text only if it confuses install guidance'));
744
+ }
745
+ for (const artifact of await staleYamArtifacts(dir)) {
746
+ findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
747
+ }
748
+ return {
749
+ schema: 'yam.cleanup-scan.v1',
750
+ generated_at: new Date().toISOString(),
751
+ project: dir,
752
+ destructive: false,
753
+ findings,
754
+ risk_level: overallCleanupRisk(findings),
755
+ safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
756
+ truth_status: 'partial'
757
+ };
758
+ }
759
+ function cleanupFinding(riskLevel, surface, whyItMatters, safeNextAction) {
760
+ return {
761
+ risk_level: riskLevel,
762
+ surface,
763
+ why_it_matters: whyItMatters,
764
+ safe_next_action: safeNextAction,
765
+ destructive: false,
766
+ truth_status: 'partial'
767
+ };
768
+ }
769
+ function overallCleanupRisk(findings = []) {
770
+ if (findings.some((item) => item.risk_level === 'high'))
771
+ return 'high';
772
+ if (findings.some((item) => item.risk_level === 'medium'))
773
+ return 'medium';
774
+ return 'low';
775
+ }
776
+ async function scanLimitedTextFiles(dir, relativeFiles = [], pattern = /$^/) {
777
+ const hits = [];
778
+ for (const relative of relativeFiles) {
779
+ const file = path.join(dir, relative);
780
+ if (!await exists(file))
781
+ continue;
782
+ const text = await readText(file).catch(() => '');
783
+ const match = text.match(pattern);
784
+ if (match)
785
+ hits.push({ file: relative, match: match[0] });
786
+ }
787
+ return hits;
788
+ }
789
+ async function staleYamArtifacts(dir) {
790
+ const candidates = [
791
+ path.join(dir, '.yam', 'proof.json'),
792
+ path.join(dir, '.yam', 'proof.md'),
793
+ path.join(dir, '.yam', 'runtime-proof.md'),
794
+ path.join(dir, '.yam', 'ueye-report.json')
795
+ ];
796
+ const artifacts = [];
797
+ for (const file of candidates) {
798
+ if (!await exists(file))
799
+ continue;
800
+ const stat = await fsp.stat(file).catch(() => null);
801
+ if (stat && Date.now() - stat.mtimeMs > 14 * 86400000) {
802
+ artifacts.push({ file: path.relative(dir, file), reason: 'old proof/runtime artifact may be mistaken for current evidence' });
803
+ }
804
+ }
805
+ return artifacts;
806
+ }
807
+ function printCleanupScanReport(report) {
808
+ console.log('yam cleanup scan');
809
+ console.log(`Project: ${report.project}`);
810
+ console.log(`Risk level: ${report.risk_level}`);
811
+ console.log('Destructive: false');
812
+ if (!report.findings.length) {
813
+ console.log('No confusing cleanup surfaces found.');
814
+ return;
815
+ }
816
+ for (const finding of report.findings) {
817
+ console.log(`- ${finding.risk_level}: ${finding.surface}`);
818
+ console.log(` why: ${finding.why_it_matters}`);
819
+ console.log(` next: ${finding.safe_next_action}`);
820
+ }
821
+ }
822
+ function parseDirJsonArgs(args = []) {
823
+ const flags = parseSimpleFlags(args, new Set(['json']));
824
+ const dir = flags._?.find(looksLikeDirectoryArg) || process.cwd();
825
+ return { dir, json: Boolean(flags.json) };
826
+ }
532
827
  async function tools(args = []) {
533
828
  const subcommand = args[0] || 'doctor';
534
829
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
@@ -588,6 +883,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
588
883
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
589
884
  const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
590
885
  const sqlScan = await scanSqlFiles(dir);
886
+ const contextPressure = await buildContextPressureReport(dir);
887
+ const realProbe = await buildRealProbeReport(dir, packageInfo.pkg, detection);
591
888
  const rows = [
592
889
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
593
890
  readinessRow('Yam skills', await status({ quiet: true }) === 0 ? 'ready' : 'missing', DEST),
@@ -624,6 +921,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
624
921
  instructionSurfaces,
625
922
  projectSurfaces,
626
923
  frameworkChecklist,
924
+ contextPressure,
925
+ realProbe,
627
926
  sqlScan,
628
927
  riskNotes,
629
928
  nextActions: nextActionDetails.map((action) => action.next_action),
@@ -666,6 +965,16 @@ function printToolsDoctorReport(data) {
666
965
  console.log(`- ${check.id}: ${check.label} (${check.route})`);
667
966
  }
668
967
  }
968
+ if (data.contextPressure) {
969
+ console.log('');
970
+ console.log(`Context pressure: ${data.contextPressure.pressure} (${data.contextPressure.recommended_next_action})`);
971
+ }
972
+ if (data.realProbe?.probes?.length) {
973
+ console.log('');
974
+ console.log(`Real probe: ${data.realProbe.readiness_truth_status}`);
975
+ for (const probe of data.realProbe.probes)
976
+ console.log(`- ${probe.id}: ${probe.status} (${probe.truth_status})`);
977
+ }
669
978
  if (data.sqlScan.filesScanned > 0) {
670
979
  console.log('');
671
980
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -794,6 +1103,62 @@ async function detectFrameworkChecklist(dir, pkg = null) {
794
1103
  function checklistRow(id, label, route) {
795
1104
  return { id, label, route };
796
1105
  }
1106
+ async function buildRealProbeReport(dir = process.cwd(), pkg = null, detection = {}) {
1107
+ const probes = [];
1108
+ probes.push(realProbeRow('node_version', 'observed', process.version, 'verified'));
1109
+ const npmVersion = runReadOnlyCommandIn(dir, 'npm', ['--version']).trim();
1110
+ probes.push(realProbeRow('npm_version', npmVersion ? 'observed' : 'not_found', npmVersion || 'npm not found or not runnable', npmVersion ? 'verified' : 'partial'));
1111
+ probes.push(realProbeRow('yam_binary', 'observed', `${process.argv[1] || 'unknown'} @ ${VERSION}`, 'verified'));
1112
+ const scripts = detection.packageJson ? detection.commands : {};
1113
+ probes.push(realProbeRow('package_scripts', Object.values(scripts).some(Boolean) ? 'observed' : 'not_found', scripts, Object.values(scripts).some(Boolean) ? 'verified' : 'partial'));
1114
+ const playwrightPath = await localPackagePath(dir, 'playwright');
1115
+ probes.push(realProbeRow('playwright_package', playwrightPath ? 'observed' : 'not_found', playwrightPath || 'playwright package not found in project or yam package context', playwrightPath ? 'verified' : 'partial'));
1116
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
1117
+ probes.push(realProbeRow('in_app_browser_plugin_cache', browserCache ? 'observed' : 'not_found', browserCache ? 'plugin cache present' : 'plugin cache not found locally', browserCache ? 'partial' : 'partial'));
1118
+ const tmux = await findExecutable('tmux');
1119
+ const zellij = await findExecutable('zellij');
1120
+ probes.push(realProbeRow('tmux', tmux ? 'observed' : 'not_found', tmux || 'not found on PATH', tmux ? 'verified' : 'partial'));
1121
+ probes.push(realProbeRow('zellij', zellij ? 'observed' : 'not_found', zellij || 'not found on PATH', zellij ? 'verified' : 'partial'));
1122
+ const gitInside = runReadOnlyCommandIn(dir, 'git', ['rev-parse', '--is-inside-work-tree']).trim();
1123
+ const gitDirty = runReadOnlyCommandIn(dir, 'git', ['status', '--short']).trim();
1124
+ probes.push(realProbeRow('git_state', gitInside === 'true' ? 'observed' : 'not_found', {
1125
+ inside_work_tree: gitInside === 'true',
1126
+ dirty_file_count: gitDirty ? gitDirty.split(/\r?\n/).length : 0
1127
+ }, gitInside === 'true' ? 'verified' : 'partial'));
1128
+ return {
1129
+ schema: 'yam.real-probe.v1',
1130
+ generated_at: new Date().toISOString(),
1131
+ project: path.resolve(dir),
1132
+ probes,
1133
+ readiness_truth_status: probes.some((probe) => probe.status === 'blocked') ? 'blocked' : 'partial',
1134
+ note: 'read-only local availability probe; no browser, server, database, or process was started'
1135
+ };
1136
+ }
1137
+ function realProbeRow(id, status, evidence, truthStatus = 'partial') {
1138
+ return {
1139
+ id,
1140
+ status,
1141
+ evidence,
1142
+ truth_status: truthStatus
1143
+ };
1144
+ }
1145
+ async function localPackagePath(dir, packageName) {
1146
+ const candidates = [
1147
+ path.join(dir, 'node_modules', packageName, 'package.json'),
1148
+ path.join(ROOT, 'node_modules', packageName, 'package.json')
1149
+ ];
1150
+ for (const candidate of candidates) {
1151
+ if (await exists(candidate))
1152
+ return candidate;
1153
+ }
1154
+ try {
1155
+ const projectRequire = createRequire(path.join(dir, 'package.json'));
1156
+ return projectRequire.resolve(`${packageName}/package.json`);
1157
+ }
1158
+ catch {
1159
+ return '';
1160
+ }
1161
+ }
797
1162
  function toolsDoctorNextActions(input) {
798
1163
  return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
799
1164
  }
@@ -1171,6 +1536,13 @@ async function release(args = []) {
1171
1536
  for (const id of report.failed)
1172
1537
  console.log(` - ${id}`);
1173
1538
  }
1539
+ if (report.publishBlockerEvidence?.length) {
1540
+ console.log('- Publish blockers:');
1541
+ for (const blocker of report.publishBlockerEvidence) {
1542
+ console.log(` - ${blocker.blocker_kind}: ${blocker.likely_cause}`);
1543
+ console.log(` next: ${blocker.safe_next_action}`);
1544
+ }
1545
+ }
1174
1546
  if (report.next_actions?.length) {
1175
1547
  console.log('- Next actions:');
1176
1548
  for (const action of report.next_actions)
@@ -1193,7 +1565,8 @@ function runReleaseReport() {
1193
1565
  const provenance = releaseProvenance(checks);
1194
1566
  const tarball = releaseTarballProvenance();
1195
1567
  const freshness = releaseFreshness(checks, provenance, tarball);
1196
- const nextActions = releaseNextActions(checks, provenance, tarball);
1568
+ const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
1569
+ const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence);
1197
1570
  return {
1198
1571
  schema: 'yam.release-report.v1',
1199
1572
  generated_at: startedAt,
@@ -1206,6 +1579,7 @@ function runReleaseReport() {
1206
1579
  provenance,
1207
1580
  tarball,
1208
1581
  freshness,
1582
+ publishBlockerEvidence,
1209
1583
  next_actions: nextActions
1210
1584
  };
1211
1585
  }
@@ -1282,7 +1656,7 @@ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
1282
1656
  truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
1283
1657
  };
1284
1658
  }
1285
- function releaseNextActions(checks = [], provenance = {}, tarball = {}) {
1659
+ function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishBlockers = []) {
1286
1660
  const actions = [];
1287
1661
  for (const check of checks) {
1288
1662
  if (check.status === 'passed')
@@ -1298,8 +1672,70 @@ function releaseNextActions(checks = [], provenance = {}, tarball = {}) {
1298
1672
  if (tarball.status !== 'checked') {
1299
1673
  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'));
1300
1674
  }
1675
+ for (const blocker of publishBlockers) {
1676
+ actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
1677
+ }
1301
1678
  return uniqueNextActionDetails(actions);
1302
1679
  }
1680
+ function publishBlockerEvidenceFromRelease(checks = [], tarball = {}) {
1681
+ const texts = [
1682
+ ...checks.map((check) => `${check.id}\n${check.note || ''}`),
1683
+ tarball.reason || ''
1684
+ ].filter(Boolean);
1685
+ const blockers = [];
1686
+ for (const text of texts) {
1687
+ const blocker = classifyPublishBlockerText(text);
1688
+ if (blocker)
1689
+ blockers.push(blocker);
1690
+ }
1691
+ const seen = new Set();
1692
+ return blockers.filter((blocker) => {
1693
+ if (seen.has(blocker.blocker_kind))
1694
+ return false;
1695
+ seen.add(blocker.blocker_kind);
1696
+ return true;
1697
+ });
1698
+ }
1699
+ function classifyPublishBlockerText(text = '') {
1700
+ const value = String(text || '');
1701
+ if (!value.trim())
1702
+ return null;
1703
+ const excerpt = summarizeCheckOutput(value);
1704
+ if (/\b(ENEEDAUTH|E401|Unauthorized|whoami)\b/i.test(value)) {
1705
+ 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`');
1706
+ }
1707
+ if (/\b(OTP|one-time password|two-factor|2FA)\b/i.test(value)) {
1708
+ 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');
1709
+ }
1710
+ if (/\bE403\b|403\s+Forbidden|Forbidden\s+-\s+PUT|npm ERR!.*Forbidden/i.test(value)) {
1711
+ 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');
1712
+ }
1713
+ if (/\bE404\b|Not Found - PUT/i.test(value)) {
1714
+ 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');
1715
+ }
1716
+ if (/previously published|cannot publish over|already exists|You cannot publish over/i.test(value)) {
1717
+ return publishBlocker('version_already_published', excerpt, 'npm versions are immutable once published', 'bump package.json to the next patch version before publishing');
1718
+ }
1719
+ if (/(EACCES|EPERM|permission denied|cache|_cacache)/i.test(value)) {
1720
+ 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`');
1721
+ }
1722
+ if (/(ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|registry unreachable|network timeout)/i.test(value)) {
1723
+ 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');
1724
+ }
1725
+ if (/(tarball|package boundary|files list|npm pack)/i.test(value)) {
1726
+ 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');
1727
+ }
1728
+ return null;
1729
+ }
1730
+ function publishBlocker(kind, excerpt, likelyCause, safeNextAction) {
1731
+ return {
1732
+ blocker_kind: kind,
1733
+ native_error_excerpt: excerpt,
1734
+ likely_cause: likelyCause,
1735
+ safe_next_action: safeNextAction,
1736
+ truth_status: 'partial'
1737
+ };
1738
+ }
1303
1739
  function runReadOnlyCommand(command, args = []) {
1304
1740
  const result = spawnSync(command, args, {
1305
1741
  cwd: ROOT,
@@ -1310,6 +1746,16 @@ function runReadOnlyCommand(command, args = []) {
1310
1746
  return '';
1311
1747
  return String(result.stdout || '');
1312
1748
  }
1749
+ function runReadOnlyCommandIn(cwd, command, args = []) {
1750
+ const result = spawnSync(command, args, {
1751
+ cwd,
1752
+ encoding: 'utf8',
1753
+ timeout: 30000
1754
+ });
1755
+ if (result.status !== 0)
1756
+ return '';
1757
+ return String(result.stdout || '');
1758
+ }
1313
1759
  function runReleaseCheck(id, commandSpec) {
1314
1760
  const [command, args] = commandSpec;
1315
1761
  const start = Date.now();
@@ -1353,6 +1799,8 @@ async function ueye(args = []) {
1353
1799
  return ueyeCapture(args.slice(1));
1354
1800
  if (subcommand === 'compare')
1355
1801
  return ueyeCompare(args.slice(1));
1802
+ if (subcommand === 'preflight')
1803
+ return ueyePreflight(args.slice(1));
1356
1804
  if (subcommand === 'report')
1357
1805
  return ueyeReport(args.slice(1));
1358
1806
  console.error(`unknown ueye command: ${subcommand}`);
@@ -1366,7 +1814,8 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
1366
1814
  Usage:
1367
1815
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1368
1816
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1369
- 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]
1817
+ yam ueye preflight [dir] [--json]
1818
+ 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]
1370
1819
 
1371
1820
  Notes:
1372
1821
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -1374,6 +1823,74 @@ Notes:
1374
1823
  report produces a proof-ready Ueye visual run report, design completion gate, and continuity/comparison record without requiring a new capture.
1375
1824
  `);
1376
1825
  }
1826
+ async function ueyePreflight(args = []) {
1827
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1828
+ return ueyeUsage();
1829
+ const parsed = parseDirJsonArgs(args);
1830
+ const report = await buildUeyePreflightReport(parsed.dir);
1831
+ if (parsed.json) {
1832
+ console.log(JSON.stringify(report, null, 2));
1833
+ return;
1834
+ }
1835
+ console.log('Ueye preflight');
1836
+ console.log(`Project: ${report.project}`);
1837
+ console.log(`Preflight id: ${report.preflight_id}`);
1838
+ console.log(`Truth status: ${report.truth_status}`);
1839
+ for (const item of report.checklist)
1840
+ console.log(`- ${item.status}: ${item.item} (${item.next_action})`);
1841
+ }
1842
+ async function buildUeyePreflightReport(targetDir = process.cwd()) {
1843
+ const dir = path.resolve(expandHome(targetDir || process.cwd()));
1844
+ const packageInfo = await projectPackageInfo(dir);
1845
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
1846
+ const pack = await findProjectPack(dir);
1847
+ const playwrightPath = await localPackagePath(dir, 'playwright');
1848
+ const browserCache = await pluginCacheHas('openai-bundled/browser');
1849
+ const uiPaths = await existingRelativePaths(dir, ['app', 'src/app', 'pages', 'src/pages', 'components', 'src/components', 'styles', 'src/styles']);
1850
+ const screenshotAvailability = playwrightPath ? 'automated_capture_possible' : browserCache ? 'in_app_or_user_screenshot_needed' : 'user_screenshot_needed';
1851
+ const p0p1Risks = [];
1852
+ if (!frameworkChecklist.uiLike)
1853
+ p0p1Risks.push('target UI surface is not detected yet');
1854
+ if (!browserCache && !playwrightPath)
1855
+ p0p1Risks.push('no local capture surface was observed');
1856
+ if (!pack)
1857
+ p0p1Risks.push('project direction pack is missing');
1858
+ return {
1859
+ schema: 'yam.ueye-preflight.v1',
1860
+ preflight_id: `ueye-preflight-${timestampId()}`,
1861
+ generated_at: new Date().toISOString(),
1862
+ project: dir,
1863
+ detected_framework: frameworkChecklist.framework,
1864
+ ui_paths: uiPaths,
1865
+ reference_source_needed: true,
1866
+ target_states_needed: ['default', 'loading', 'error', 'empty', 'mobile'],
1867
+ mobile_responsive_check_needed: true,
1868
+ contrast_cta_accessibility_risk: frameworkChecklist.uiLike ? 'check before done claim' : 'unknown until UI target is identified',
1869
+ p0_p1_risk_candidates: p0p1Risks,
1870
+ screenshot_capture_availability: screenshotAvailability,
1871
+ preferred_surface: 'in-app-browser',
1872
+ checklist: [
1873
+ preflightItem('reference', 'reference source selected or user confirms no reference', 'needed', 'record reference path, screenshot, or text target before claiming design fidelity'),
1874
+ preflightItem('states', 'primary states are listed', 'needed', 'prepare default/loading/error/empty/mobile states when the UI supports them'),
1875
+ preflightItem('mobile', 'mobile/responsive pass planned', 'needed', 'capture or inspect a narrow viewport before verified visual claims'),
1876
+ preflightItem('contrast_cta_accessibility', 'CTA, contrast, and accessible labels reviewed', 'needed', 'mark P0/P1 risks before saying done'),
1877
+ preflightItem('capture', 'screenshot/capture surface available', screenshotAvailability === 'automated_capture_possible' ? 'observed' : 'partial', 'use in-app browser or user-provided screenshot if automation is unavailable')
1878
+ ],
1879
+ completion_cap: 'preflight cannot prove visual quality without implementation screenshot and comparison evidence',
1880
+ truth_status: 'partial'
1881
+ };
1882
+ }
1883
+ function preflightItem(id, item, status, nextAction) {
1884
+ return { id, item, status, next_action: nextAction, truth_status: status === 'observed' ? 'verified' : 'partial' };
1885
+ }
1886
+ async function existingRelativePaths(dir, relativePaths = []) {
1887
+ const found = [];
1888
+ for (const relative of relativePaths) {
1889
+ if (await exists(path.join(dir, relative)))
1890
+ found.push(relative);
1891
+ }
1892
+ return found;
1893
+ }
1377
1894
  async function ueyeCapture(args = []) {
1378
1895
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1379
1896
  return ueyeUsage();
@@ -1539,7 +2056,7 @@ async function ueyeCompare(args = []) {
1539
2056
  async function ueyeReport(args = []) {
1540
2057
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1541
2058
  return ueyeUsage();
1542
- 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']));
2059
+ 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']));
1543
2060
  const reference = String(flags.reference || flags.ref || '');
1544
2061
  const actual = String(flags.actual || flags.screenshot || '');
1545
2062
  const referenceSources = [];
@@ -1632,7 +2149,7 @@ async function ueyeReport(args = []) {
1632
2149
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
1633
2150
  design_score: numberOrNull(flags.design_score),
1634
2151
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
1635
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0)],
2152
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
1636
2153
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
1637
2154
  states_checked: Boolean(flags.states_checked),
1638
2155
  mobile_checked: Boolean(flags.mobile_checked),
@@ -1673,6 +2190,12 @@ async function ueyeReport(args = []) {
1673
2190
  const result = {
1674
2191
  ...report,
1675
2192
  review_session_id: reviewSessionId,
2193
+ preflight: {
2194
+ preflight_id: String(flags.preflight_id || ''),
2195
+ p0_risks: arrayFlag(flags.p0_risk),
2196
+ quality_gate_notes: arrayFlag(flags.quality_gate_note),
2197
+ truth_status: flags.preflight_id || flags.p0_risk || flags.quality_gate_note ? 'partial' : 'assumed'
2198
+ },
1676
2199
  tool_intent: 'visual',
1677
2200
  capture_backend: String(flags.capture_backend || 'not-recorded'),
1678
2201
  compare_backend: String(flags.compare_backend || 'local-file-hash'),
@@ -1820,7 +2343,7 @@ function runtimeUsage() {
1820
2343
  console.log(`yam runtime
1821
2344
 
1822
2345
  Usage:
1823
- 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]
2346
+ 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]
1824
2347
 
1825
2348
  Notes:
1826
2349
  Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
@@ -1829,13 +2352,21 @@ Notes:
1829
2352
  async function runtimeEvidence(args = []) {
1830
2353
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1831
2354
  return runtimeUsage();
1832
- 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']));
2355
+ 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']));
1833
2356
  const evidence = buildRuntimeBackendEvidence({
1834
2357
  backend: flags.backend,
1835
2358
  claim: flags.claim,
1836
2359
  evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
1837
2360
  command: String(flags.command || ''),
1838
2361
  cleanup_checked: Boolean(flags.cleanup_checked),
2362
+ started_at: String(flags.started_at || ''),
2363
+ stopped_at: String(flags.stopped_at || ''),
2364
+ exit_code: numberOrNull(flags.exit_code),
2365
+ pid: numberOrNull(flags.pid),
2366
+ port: numberOrNull(flags.port),
2367
+ cleanup_method: String(flags.cleanup_method || ''),
2368
+ cleanup_observed: Boolean(flags.cleanup_observed),
2369
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
1839
2370
  note: String(flags.note || ''),
1840
2371
  truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
1841
2372
  });
@@ -1862,17 +2393,20 @@ function runtimeEvidenceDetails(flags = {}) {
1862
2393
  exit_code: numberOrNull(flags.exit_code),
1863
2394
  screenshot_id: String(flags.screenshot_id || ''),
1864
2395
  started_at: String(flags.started_at || ''),
1865
- stopped_at: String(flags.stopped_at || '')
2396
+ stopped_at: String(flags.stopped_at || ''),
2397
+ cleanup_method: String(flags.cleanup_method || ''),
2398
+ cleanup_observed: Boolean(flags.cleanup_observed),
2399
+ left_running_intentionally: Boolean(flags.left_running_intentionally)
1866
2400
  };
1867
2401
  }
1868
2402
  function runtimeDetailsHasVerification(details = {}) {
1869
- return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
2403
+ 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);
1870
2404
  }
1871
2405
  function runtimeEvidenceNextAction(evidence) {
1872
2406
  if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified')
1873
2407
  return 'attach this evidence to `yam proof` when making a runtime claim';
1874
- if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked)
1875
- return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
2408
+ if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_observed && !evidence.left_running_intentionally)
2409
+ return 'rerun with --cleanup-observed plus exit/closure evidence, or record --left-running-intentionally';
1876
2410
  if (evidence.backend === 'none' || evidence.backend === 'unknown')
1877
2411
  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';
@@ -2053,7 +2587,7 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
2053
2587
  if (allowed.size && !allowed.has(key))
2054
2588
  continue;
2055
2589
  const normalizedKey = key.replace(/-/g, '_');
2056
- 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)) {
2590
+ 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)) {
2057
2591
  flags[normalizedKey] = true;
2058
2592
  continue;
2059
2593
  }
@@ -2408,7 +2942,7 @@ function renderProofMarkdownList(values) {
2408
2942
  function parseProofArgs(args = []) {
2409
2943
  const flags = {};
2410
2944
  const positionals = [];
2411
- 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']);
2945
+ 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']);
2412
2946
  for (let index = 0; index < args.length; index += 1) {
2413
2947
  const arg = args[index];
2414
2948
  if (arg.startsWith('--')) {
@@ -2417,7 +2951,7 @@ function parseProofArgs(args = []) {
2417
2951
  if (!aliases.has(key))
2418
2952
  continue;
2419
2953
  const normalizedKey = key.replace(/-/g, '_');
2420
- if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
2954
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked' || key === 'cleanup-observed' || key === 'left-running-intentionally') {
2421
2955
  flags[normalizedKey] = true;
2422
2956
  continue;
2423
2957
  }
@@ -2575,13 +3109,21 @@ function structuredEvidenceList(value) {
2575
3109
  return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
2576
3110
  }
2577
3111
  function directRuntimeBackendEvidence(flags) {
2578
- if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked)
3112
+ 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)
2579
3113
  return [];
2580
3114
  return [JSON.stringify(buildRuntimeBackendEvidence({
2581
3115
  backend: flags.runtime_backend,
2582
3116
  claim: flags.runtime_claim,
2583
3117
  evidence_id: flags.runtime_evidence_id,
2584
3118
  command: flags.runtime_command,
3119
+ pid: numberOrNull(flags.runtime_pid),
3120
+ port: numberOrNull(flags.runtime_port),
3121
+ exit_code: numberOrNull(flags.runtime_exit_code),
3122
+ started_at: String(flags.runtime_started_at || ''),
3123
+ stopped_at: String(flags.runtime_stopped_at || ''),
3124
+ cleanup_method: String(flags.runtime_cleanup_method || ''),
3125
+ cleanup_observed: Boolean(flags.cleanup_observed),
3126
+ left_running_intentionally: Boolean(flags.left_running_intentionally),
2585
3127
  cleanup_checked: Boolean(flags.cleanup_checked),
2586
3128
  note: flags.runtime_note
2587
3129
  }))];
@@ -3316,6 +3858,10 @@ async function main() {
3316
3858
  return detectProject(process.argv[3]);
3317
3859
  if (command === 'pack')
3318
3860
  return inspectProjectPack(process.argv[3]);
3861
+ if (command === 'context')
3862
+ return context(process.argv.slice(3));
3863
+ if (command === 'cleanup')
3864
+ return cleanup(process.argv.slice(3));
3319
3865
  if (command === 'budget')
3320
3866
  return budget(process.argv[3]);
3321
3867
  if (command === 'measure')