yam-flow 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -4
- package/CHANGELOG.md +57 -0
- package/COMMANDS.md +15 -3
- package/DECISIONS.md +28 -5
- package/README.md +33 -4
- package/ROADMAP.md +2 -2
- package/dist/bin/yam.js +827 -19
- package/dist/lib/trust-kernel.d.ts +50 -0
- package/dist/lib/trust-kernel.js +187 -4
- package/package.json +1 -1
- package/references/cleanup-scan.md +33 -0
- package/references/final-report.md +6 -1
- package/references/risk-escalation.md +4 -0
- package/references/runtime-orchestration.md +7 -0
- package/references/scout.md +9 -3
- package/references/tool-trust-layer.md +14 -2
- package/references/truth-matrix.md +4 -0
- package/skills/scout/SKILL.md +19 -9
- package/src/bin/yam.ts +835 -19
- package/src/lib/trust-kernel.ts +235 -3
- package/templates/yam.project.md +14 -0
- package/yam.manifest.json +3 -2
package/dist/bin/yam.js
CHANGED
|
@@ -8,7 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import process from 'node:process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { TRUTH_STATUSES, buildMediaGenerationProof, buildMissionPatchEnvelope, buildRollbackHint, buildRuntimeBackendEvidence, buildUeyeDesignCompletionGate, buildUeyeVisualProvenance, buildUeyeRunReport, buildUeyeSurfaceContext, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
|
|
11
|
+
import { TRUTH_STATUSES, buildLoopReport, buildMediaGenerationProof, buildMissionPatchEnvelope, buildRollbackHint, buildRuntimeBackendEvidence, buildStudyNote, buildUeyeDesignCompletionGate, buildUeyeVisualProvenance, buildUeyeRunReport, buildUeyeSurfaceContext, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
|
|
12
12
|
function errorMessage(error) {
|
|
13
13
|
return error instanceof Error ? error.message : String(error);
|
|
14
14
|
}
|
|
@@ -115,13 +115,17 @@ 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]
|
|
121
123
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
122
124
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
125
|
+
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
123
126
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
124
127
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
128
|
+
yam ueye preflight [dir] [--json]
|
|
125
129
|
yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
|
|
126
130
|
yam media proof [--requested] [--attempted] [--output file] [--json]
|
|
127
131
|
yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
|
|
@@ -302,6 +306,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
302
306
|
'ui-quality.md',
|
|
303
307
|
'question.md',
|
|
304
308
|
'mission.md',
|
|
309
|
+
'cleanup-scan.md',
|
|
305
310
|
'doctor-scan.md',
|
|
306
311
|
'scout.md',
|
|
307
312
|
'runtime-orchestration.md',
|
|
@@ -499,6 +504,8 @@ function toolIntentForCommand(command = '') {
|
|
|
499
504
|
const text = String(command || '').toLowerCase();
|
|
500
505
|
if (!text)
|
|
501
506
|
return 'read_only';
|
|
507
|
+
if (/(npm\s+(view|whoami|config\s+get|owner\s+ls)|registry:check)/i.test(text))
|
|
508
|
+
return 'read_only';
|
|
502
509
|
if (/(publish|registry|release|npm pack|npm publish)/i.test(text))
|
|
503
510
|
return 'publish';
|
|
504
511
|
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text))
|
|
@@ -529,6 +536,297 @@ function uniqueNextActionDetails(actions = []) {
|
|
|
529
536
|
}
|
|
530
537
|
return unique;
|
|
531
538
|
}
|
|
539
|
+
async function context(args = []) {
|
|
540
|
+
const subcommand = args[0] || 'help';
|
|
541
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
542
|
+
return contextUsage();
|
|
543
|
+
if (subcommand === 'pressure')
|
|
544
|
+
return contextPressure(args.slice(1));
|
|
545
|
+
console.error(`unknown context command: ${subcommand}`);
|
|
546
|
+
return contextUsage();
|
|
547
|
+
}
|
|
548
|
+
function contextUsage() {
|
|
549
|
+
console.log(`yam context
|
|
550
|
+
|
|
551
|
+
Usage:
|
|
552
|
+
yam context pressure [dir] [--json]
|
|
553
|
+
|
|
554
|
+
Notes:
|
|
555
|
+
Read-only context pressure scan. It explains when to summarize, refresh the project pack, narrow scope, or deepen the route.
|
|
556
|
+
`);
|
|
557
|
+
}
|
|
558
|
+
async function contextPressure(args = []) {
|
|
559
|
+
const parsed = parseDirJsonArgs(args);
|
|
560
|
+
const report = await buildContextPressureReport(parsed.dir);
|
|
561
|
+
if (parsed.json) {
|
|
562
|
+
console.log(JSON.stringify(report, null, 2));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
printContextPressureReport(report);
|
|
566
|
+
}
|
|
567
|
+
async function buildContextPressureReport(targetDir = process.cwd()) {
|
|
568
|
+
const dir = path.resolve(expandHome(targetDir || process.cwd()));
|
|
569
|
+
const pack = await findProjectPack(dir);
|
|
570
|
+
const memorySummary = path.join(dir, '.yam', 'memory', 'summary.md');
|
|
571
|
+
const instructionSurfaces = await findInstructionSurfaces(dir);
|
|
572
|
+
const detection = await detectProject(dir, { quiet: true });
|
|
573
|
+
const gitStatus = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
|
|
574
|
+
const dirtyFiles = gitStatus.trim().split(/\r?\n/).filter(Boolean);
|
|
575
|
+
const signals = {
|
|
576
|
+
project_pack: await projectPackPressureSignal(pack),
|
|
577
|
+
memory_summary: {
|
|
578
|
+
present: await exists(memorySummary),
|
|
579
|
+
path: memorySummary
|
|
580
|
+
},
|
|
581
|
+
git: {
|
|
582
|
+
dirty_file_count: dirtyFiles.length,
|
|
583
|
+
dirty_files_sample: dirtyFiles.slice(0, 12)
|
|
584
|
+
},
|
|
585
|
+
package_scripts: detection.packageJson ? detection.commands : {},
|
|
586
|
+
instruction_surfaces: instructionSurfaces
|
|
587
|
+
};
|
|
588
|
+
const reasons = [];
|
|
589
|
+
let score = 0;
|
|
590
|
+
if (!signals.project_pack.present) {
|
|
591
|
+
score += 3;
|
|
592
|
+
reasons.push('project pack is missing, so direction may need rediscovery');
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
if (signals.project_pack.age_days > PACK_STALE_DAYS) {
|
|
596
|
+
score += 2;
|
|
597
|
+
reasons.push(`project pack is ${signals.project_pack.age_days} days old`);
|
|
598
|
+
}
|
|
599
|
+
if (signals.project_pack.words < 80) {
|
|
600
|
+
score += 1;
|
|
601
|
+
reasons.push('project pack is very short, so local direction may be thin');
|
|
602
|
+
}
|
|
603
|
+
if (signals.project_pack.words > 1200) {
|
|
604
|
+
score += 2;
|
|
605
|
+
reasons.push('project pack is long, so reuse may become expensive');
|
|
606
|
+
}
|
|
607
|
+
if (signals.project_pack.missing_sections?.length) {
|
|
608
|
+
score += 2;
|
|
609
|
+
reasons.push(`project pack is missing sections: ${signals.project_pack.missing_sections.join(', ')}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (!signals.memory_summary.present) {
|
|
613
|
+
score += 1;
|
|
614
|
+
reasons.push('project memory summary is missing');
|
|
615
|
+
}
|
|
616
|
+
if (dirtyFiles.length >= 12) {
|
|
617
|
+
score += 3;
|
|
618
|
+
reasons.push(`${dirtyFiles.length} dirty files increase review and reporting pressure`);
|
|
619
|
+
}
|
|
620
|
+
else if (dirtyFiles.length >= 4) {
|
|
621
|
+
score += 2;
|
|
622
|
+
reasons.push(`${dirtyFiles.length} dirty files suggest the scope is widening`);
|
|
623
|
+
}
|
|
624
|
+
else if (dirtyFiles.length > 0) {
|
|
625
|
+
score += 1;
|
|
626
|
+
reasons.push(`${dirtyFiles.length} dirty file(s) should be kept in the final handoff`);
|
|
627
|
+
}
|
|
628
|
+
if (instructionSurfaces.issues.length) {
|
|
629
|
+
score += 3;
|
|
630
|
+
reasons.push('active hook or instruction issue can change route behavior');
|
|
631
|
+
}
|
|
632
|
+
if (instructionSurfaces.warnings.length >= 2) {
|
|
633
|
+
score += 1;
|
|
634
|
+
reasons.push('multiple instruction surfaces can make guidance harder to predict');
|
|
635
|
+
}
|
|
636
|
+
if (detection.packageJson && !detection.commands.build && !detection.commands.typecheck) {
|
|
637
|
+
score += 1;
|
|
638
|
+
reasons.push('no build/typecheck script was detected for quick implementation verification');
|
|
639
|
+
}
|
|
640
|
+
const pressure = score >= 7 ? 'high' : score >= 3 ? 'medium' : 'low';
|
|
641
|
+
return {
|
|
642
|
+
schema: 'yam.context-pressure.v1',
|
|
643
|
+
generated_at: new Date().toISOString(),
|
|
644
|
+
project: dir,
|
|
645
|
+
pressure,
|
|
646
|
+
score,
|
|
647
|
+
why_pressure_is_rising: reasons,
|
|
648
|
+
signals,
|
|
649
|
+
beginner_insight: contextPressureInsight(pressure),
|
|
650
|
+
recommended_next_action: pressure === 'high'
|
|
651
|
+
? 'pause and narrow scope, refresh yam.project.md or memory summary, then choose the route deliberately'
|
|
652
|
+
: pressure === 'medium'
|
|
653
|
+
? 'keep the task scoped and summarize important changes before expanding'
|
|
654
|
+
: 'continue with the current route and the smallest honest verification',
|
|
655
|
+
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',
|
|
656
|
+
truth_status: 'partial'
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
async function projectPackPressureSignal(pack) {
|
|
660
|
+
if (!pack)
|
|
661
|
+
return { present: false, path: '', age_days: null, words: 0, size_bytes: 0, missing_sections: REQUIRED_PACK_SECTIONS };
|
|
662
|
+
const text = await readText(pack).catch(() => '');
|
|
663
|
+
const stat = await fsp.stat(pack).catch(() => null);
|
|
664
|
+
return {
|
|
665
|
+
present: true,
|
|
666
|
+
path: pack,
|
|
667
|
+
age_days: stat ? Math.floor((Date.now() - stat.mtimeMs) / 86400000) : null,
|
|
668
|
+
words: countWords(text),
|
|
669
|
+
size_bytes: stat?.size || 0,
|
|
670
|
+
missing_sections: REQUIRED_PACK_SECTIONS.filter((section) => !hasHeading(text, section))
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function contextPressureInsight(pressure) {
|
|
674
|
+
if (pressure === 'high')
|
|
675
|
+
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.';
|
|
676
|
+
if (pressure === 'medium')
|
|
677
|
+
return 'The task is still manageable, but scope or stale context may start causing repeated reading and weaker final explanations.';
|
|
678
|
+
return 'The current context looks small enough to keep momentum without heavy proof, as long as claims stay modest.';
|
|
679
|
+
}
|
|
680
|
+
function printContextPressureReport(report) {
|
|
681
|
+
console.log('yam context pressure');
|
|
682
|
+
console.log(`Project: ${report.project}`);
|
|
683
|
+
console.log(`Pressure: ${report.pressure} (score ${report.score})`);
|
|
684
|
+
console.log(`Insight: ${report.beginner_insight}`);
|
|
685
|
+
if (report.why_pressure_is_rising.length) {
|
|
686
|
+
console.log('');
|
|
687
|
+
console.log('Why pressure is rising:');
|
|
688
|
+
for (const reason of report.why_pressure_is_rising)
|
|
689
|
+
console.log(`- ${reason}`);
|
|
690
|
+
}
|
|
691
|
+
console.log('');
|
|
692
|
+
console.log(`Next: ${report.recommended_next_action}`);
|
|
693
|
+
console.log(`Route hint: ${report.route_hint}`);
|
|
694
|
+
}
|
|
695
|
+
async function cleanup(args = []) {
|
|
696
|
+
const subcommand = args[0] || 'help';
|
|
697
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
698
|
+
return cleanupUsage();
|
|
699
|
+
if (subcommand === 'scan')
|
|
700
|
+
return cleanupScan(args.slice(1));
|
|
701
|
+
console.error(`unknown cleanup command: ${subcommand}`);
|
|
702
|
+
return cleanupUsage();
|
|
703
|
+
}
|
|
704
|
+
function cleanupUsage() {
|
|
705
|
+
console.log(`yam cleanup
|
|
706
|
+
|
|
707
|
+
Usage:
|
|
708
|
+
yam cleanup scan [dir] [--json]
|
|
709
|
+
|
|
710
|
+
Notes:
|
|
711
|
+
Read-only cleanup scan. It never deletes files; it only reports confusing surfaces and safe next actions.
|
|
712
|
+
`);
|
|
713
|
+
}
|
|
714
|
+
async function cleanupScan(args = []) {
|
|
715
|
+
const parsed = parseDirJsonArgs(args);
|
|
716
|
+
const report = await buildCleanupScanReport(parsed.dir);
|
|
717
|
+
if (parsed.json) {
|
|
718
|
+
console.log(JSON.stringify(report, null, 2));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
printCleanupScanReport(report);
|
|
722
|
+
}
|
|
723
|
+
async function buildCleanupScanReport(targetDir = process.cwd()) {
|
|
724
|
+
const dir = path.resolve(expandHome(targetDir || process.cwd()));
|
|
725
|
+
const findings = [];
|
|
726
|
+
const instructionSurfaces = await findInstructionSurfaces(dir);
|
|
727
|
+
for (const found of instructionSurfaces.found) {
|
|
728
|
+
const issue = instructionSurfaces.issues.find((item) => item.startsWith(`${found}:`));
|
|
729
|
+
const warning = instructionSurfaces.warnings.find((item) => item.startsWith(`${found}:`));
|
|
730
|
+
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'));
|
|
731
|
+
}
|
|
732
|
+
const projectAgents = path.join(dir, '.agents');
|
|
733
|
+
if (await exists(projectAgents)) {
|
|
734
|
+
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'));
|
|
735
|
+
}
|
|
736
|
+
for (const hook of [path.join(dir, '.codex', 'hooks.json'), path.join(os.homedir(), '.codex', 'hooks.json')]) {
|
|
737
|
+
if (await exists(hook))
|
|
738
|
+
findings.push(cleanupFinding('high', hook, 'active hooks can add hidden latency or route pressure', 'disable or edit only after user approval and backup'));
|
|
739
|
+
}
|
|
740
|
+
for (const skill of [...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
|
|
741
|
+
if (await exists(path.join(DEST, skill, 'SKILL.md')))
|
|
742
|
+
findings.push(cleanupFinding('medium', `installed skill: ${skill}`, 'old skill names can confuse route selection', 'run the explicit current installer or remove only with user approval'));
|
|
743
|
+
}
|
|
744
|
+
const legacyPackageHits = await scanLimitedTextFiles(dir, ['package.json', 'README.md', 'COMMANDS.md', 'CHANGELOG.md'], /\b(yam-harness|yam-codex)\b/i);
|
|
745
|
+
for (const hit of legacyPackageHits) {
|
|
746
|
+
findings.push(cleanupFinding('low', hit.file, `legacy package name mention: ${hit.match}`, 'update public text only if it confuses install guidance'));
|
|
747
|
+
}
|
|
748
|
+
for (const artifact of await staleYamArtifacts(dir)) {
|
|
749
|
+
findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
schema: 'yam.cleanup-scan.v1',
|
|
753
|
+
generated_at: new Date().toISOString(),
|
|
754
|
+
project: dir,
|
|
755
|
+
destructive: false,
|
|
756
|
+
findings,
|
|
757
|
+
risk_level: overallCleanupRisk(findings),
|
|
758
|
+
safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
|
|
759
|
+
truth_status: 'partial'
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function cleanupFinding(riskLevel, surface, whyItMatters, safeNextAction) {
|
|
763
|
+
return {
|
|
764
|
+
risk_level: riskLevel,
|
|
765
|
+
surface,
|
|
766
|
+
why_it_matters: whyItMatters,
|
|
767
|
+
safe_next_action: safeNextAction,
|
|
768
|
+
destructive: false,
|
|
769
|
+
truth_status: 'partial'
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function overallCleanupRisk(findings = []) {
|
|
773
|
+
if (findings.some((item) => item.risk_level === 'high'))
|
|
774
|
+
return 'high';
|
|
775
|
+
if (findings.some((item) => item.risk_level === 'medium'))
|
|
776
|
+
return 'medium';
|
|
777
|
+
return 'low';
|
|
778
|
+
}
|
|
779
|
+
async function scanLimitedTextFiles(dir, relativeFiles = [], pattern = /$^/) {
|
|
780
|
+
const hits = [];
|
|
781
|
+
for (const relative of relativeFiles) {
|
|
782
|
+
const file = path.join(dir, relative);
|
|
783
|
+
if (!await exists(file))
|
|
784
|
+
continue;
|
|
785
|
+
const text = await readText(file).catch(() => '');
|
|
786
|
+
const match = text.match(pattern);
|
|
787
|
+
if (match)
|
|
788
|
+
hits.push({ file: relative, match: match[0] });
|
|
789
|
+
}
|
|
790
|
+
return hits;
|
|
791
|
+
}
|
|
792
|
+
async function staleYamArtifacts(dir) {
|
|
793
|
+
const candidates = [
|
|
794
|
+
path.join(dir, '.yam', 'proof.json'),
|
|
795
|
+
path.join(dir, '.yam', 'proof.md'),
|
|
796
|
+
path.join(dir, '.yam', 'runtime-proof.md'),
|
|
797
|
+
path.join(dir, '.yam', 'ueye-report.json')
|
|
798
|
+
];
|
|
799
|
+
const artifacts = [];
|
|
800
|
+
for (const file of candidates) {
|
|
801
|
+
if (!await exists(file))
|
|
802
|
+
continue;
|
|
803
|
+
const stat = await fsp.stat(file).catch(() => null);
|
|
804
|
+
if (stat && Date.now() - stat.mtimeMs > 14 * 86400000) {
|
|
805
|
+
artifacts.push({ file: path.relative(dir, file), reason: 'old proof/runtime artifact may be mistaken for current evidence' });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return artifacts;
|
|
809
|
+
}
|
|
810
|
+
function printCleanupScanReport(report) {
|
|
811
|
+
console.log('yam cleanup scan');
|
|
812
|
+
console.log(`Project: ${report.project}`);
|
|
813
|
+
console.log(`Risk level: ${report.risk_level}`);
|
|
814
|
+
console.log('Destructive: false');
|
|
815
|
+
if (!report.findings.length) {
|
|
816
|
+
console.log('No confusing cleanup surfaces found.');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
for (const finding of report.findings) {
|
|
820
|
+
console.log(`- ${finding.risk_level}: ${finding.surface}`);
|
|
821
|
+
console.log(` why: ${finding.why_it_matters}`);
|
|
822
|
+
console.log(` next: ${finding.safe_next_action}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function parseDirJsonArgs(args = []) {
|
|
826
|
+
const flags = parseSimpleFlags(args, new Set(['json']));
|
|
827
|
+
const dir = flags._?.find(looksLikeDirectoryArg) || process.cwd();
|
|
828
|
+
return { dir, json: Boolean(flags.json) };
|
|
829
|
+
}
|
|
532
830
|
async function tools(args = []) {
|
|
533
831
|
const subcommand = args[0] || 'doctor';
|
|
534
832
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
@@ -588,6 +886,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
588
886
|
const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
|
|
589
887
|
const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
|
|
590
888
|
const sqlScan = await scanSqlFiles(dir);
|
|
889
|
+
const contextPressure = await buildContextPressureReport(dir);
|
|
890
|
+
const realProbe = await buildRealProbeReport(dir, packageInfo.pkg, detection);
|
|
591
891
|
const rows = [
|
|
592
892
|
readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
|
|
593
893
|
readinessRow('Yam skills', await status({ quiet: true }) === 0 ? 'ready' : 'missing', DEST),
|
|
@@ -624,6 +924,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
624
924
|
instructionSurfaces,
|
|
625
925
|
projectSurfaces,
|
|
626
926
|
frameworkChecklist,
|
|
927
|
+
contextPressure,
|
|
928
|
+
realProbe,
|
|
627
929
|
sqlScan,
|
|
628
930
|
riskNotes,
|
|
629
931
|
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
@@ -666,6 +968,16 @@ function printToolsDoctorReport(data) {
|
|
|
666
968
|
console.log(`- ${check.id}: ${check.label} (${check.route})`);
|
|
667
969
|
}
|
|
668
970
|
}
|
|
971
|
+
if (data.contextPressure) {
|
|
972
|
+
console.log('');
|
|
973
|
+
console.log(`Context pressure: ${data.contextPressure.pressure} (${data.contextPressure.recommended_next_action})`);
|
|
974
|
+
}
|
|
975
|
+
if (data.realProbe?.probes?.length) {
|
|
976
|
+
console.log('');
|
|
977
|
+
console.log(`Real probe: ${data.realProbe.readiness_truth_status}`);
|
|
978
|
+
for (const probe of data.realProbe.probes)
|
|
979
|
+
console.log(`- ${probe.id}: ${probe.status} (${probe.truth_status})`);
|
|
980
|
+
}
|
|
669
981
|
if (data.sqlScan.filesScanned > 0) {
|
|
670
982
|
console.log('');
|
|
671
983
|
console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
|
|
@@ -794,6 +1106,62 @@ async function detectFrameworkChecklist(dir, pkg = null) {
|
|
|
794
1106
|
function checklistRow(id, label, route) {
|
|
795
1107
|
return { id, label, route };
|
|
796
1108
|
}
|
|
1109
|
+
async function buildRealProbeReport(dir = process.cwd(), pkg = null, detection = {}) {
|
|
1110
|
+
const probes = [];
|
|
1111
|
+
probes.push(realProbeRow('node_version', 'observed', process.version, 'verified'));
|
|
1112
|
+
const npmVersion = runReadOnlyCommandIn(dir, 'npm', ['--version']).trim();
|
|
1113
|
+
probes.push(realProbeRow('npm_version', npmVersion ? 'observed' : 'not_found', npmVersion || 'npm not found or not runnable', npmVersion ? 'verified' : 'partial'));
|
|
1114
|
+
probes.push(realProbeRow('yam_binary', 'observed', `${process.argv[1] || 'unknown'} @ ${VERSION}`, 'verified'));
|
|
1115
|
+
const scripts = detection.packageJson ? detection.commands : {};
|
|
1116
|
+
probes.push(realProbeRow('package_scripts', Object.values(scripts).some(Boolean) ? 'observed' : 'not_found', scripts, Object.values(scripts).some(Boolean) ? 'verified' : 'partial'));
|
|
1117
|
+
const playwrightPath = await localPackagePath(dir, 'playwright');
|
|
1118
|
+
probes.push(realProbeRow('playwright_package', playwrightPath ? 'observed' : 'not_found', playwrightPath || 'playwright package not found in project or yam package context', playwrightPath ? 'verified' : 'partial'));
|
|
1119
|
+
const browserCache = await pluginCacheHas('openai-bundled/browser');
|
|
1120
|
+
probes.push(realProbeRow('in_app_browser_plugin_cache', browserCache ? 'observed' : 'not_found', browserCache ? 'plugin cache present' : 'plugin cache not found locally', browserCache ? 'partial' : 'partial'));
|
|
1121
|
+
const tmux = await findExecutable('tmux');
|
|
1122
|
+
const zellij = await findExecutable('zellij');
|
|
1123
|
+
probes.push(realProbeRow('tmux', tmux ? 'observed' : 'not_found', tmux || 'not found on PATH', tmux ? 'verified' : 'partial'));
|
|
1124
|
+
probes.push(realProbeRow('zellij', zellij ? 'observed' : 'not_found', zellij || 'not found on PATH', zellij ? 'verified' : 'partial'));
|
|
1125
|
+
const gitInside = runReadOnlyCommandIn(dir, 'git', ['rev-parse', '--is-inside-work-tree']).trim();
|
|
1126
|
+
const gitDirty = runReadOnlyCommandIn(dir, 'git', ['status', '--short']).trim();
|
|
1127
|
+
probes.push(realProbeRow('git_state', gitInside === 'true' ? 'observed' : 'not_found', {
|
|
1128
|
+
inside_work_tree: gitInside === 'true',
|
|
1129
|
+
dirty_file_count: gitDirty ? gitDirty.split(/\r?\n/).length : 0
|
|
1130
|
+
}, gitInside === 'true' ? 'verified' : 'partial'));
|
|
1131
|
+
return {
|
|
1132
|
+
schema: 'yam.real-probe.v1',
|
|
1133
|
+
generated_at: new Date().toISOString(),
|
|
1134
|
+
project: path.resolve(dir),
|
|
1135
|
+
probes,
|
|
1136
|
+
readiness_truth_status: probes.some((probe) => probe.status === 'blocked') ? 'blocked' : 'partial',
|
|
1137
|
+
note: 'read-only local availability probe; no browser, server, database, or process was started'
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function realProbeRow(id, status, evidence, truthStatus = 'partial') {
|
|
1141
|
+
return {
|
|
1142
|
+
id,
|
|
1143
|
+
status,
|
|
1144
|
+
evidence,
|
|
1145
|
+
truth_status: truthStatus
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
async function localPackagePath(dir, packageName) {
|
|
1149
|
+
const candidates = [
|
|
1150
|
+
path.join(dir, 'node_modules', packageName, 'package.json'),
|
|
1151
|
+
path.join(ROOT, 'node_modules', packageName, 'package.json')
|
|
1152
|
+
];
|
|
1153
|
+
for (const candidate of candidates) {
|
|
1154
|
+
if (await exists(candidate))
|
|
1155
|
+
return candidate;
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
const projectRequire = createRequire(path.join(dir, 'package.json'));
|
|
1159
|
+
return projectRequire.resolve(`${packageName}/package.json`);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
return '';
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
797
1165
|
function toolsDoctorNextActions(input) {
|
|
798
1166
|
return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
|
|
799
1167
|
}
|
|
@@ -1166,16 +1534,31 @@ async function release(args = []) {
|
|
|
1166
1534
|
if (report.tarball.file_count !== undefined)
|
|
1167
1535
|
console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1168
1536
|
}
|
|
1537
|
+
if (report.publish_readiness) {
|
|
1538
|
+
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
1539
|
+
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
1540
|
+
console.log(`- npm auth: ${report.publish_readiness.auth.status}`);
|
|
1541
|
+
}
|
|
1169
1542
|
if (report.failed.length) {
|
|
1170
1543
|
console.log('- Failed:');
|
|
1171
1544
|
for (const id of report.failed)
|
|
1172
1545
|
console.log(` - ${id}`);
|
|
1173
1546
|
}
|
|
1547
|
+
if (report.publishBlockerEvidence?.length) {
|
|
1548
|
+
console.log('- Publish blockers:');
|
|
1549
|
+
for (const blocker of report.publishBlockerEvidence) {
|
|
1550
|
+
console.log(` - ${blocker.blocker_kind}: ${blocker.likely_cause}`);
|
|
1551
|
+
console.log(` next: ${blocker.safe_next_action}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1174
1554
|
if (report.next_actions?.length) {
|
|
1175
1555
|
console.log('- Next actions:');
|
|
1176
1556
|
for (const action of report.next_actions)
|
|
1177
1557
|
console.log(` - ${action.next_action}`);
|
|
1178
1558
|
}
|
|
1559
|
+
if (report.study_note?.problem?.summary || report.study_note?.problem?.symptom) {
|
|
1560
|
+
console.log(`- Study note: ${report.study_note.problem.summary || report.study_note.problem.symptom}`);
|
|
1561
|
+
}
|
|
1179
1562
|
if (!report.ok)
|
|
1180
1563
|
process.exitCode = 1;
|
|
1181
1564
|
}
|
|
@@ -1193,19 +1576,25 @@ function runReleaseReport() {
|
|
|
1193
1576
|
const provenance = releaseProvenance(checks);
|
|
1194
1577
|
const tarball = releaseTarballProvenance();
|
|
1195
1578
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1196
|
-
const
|
|
1579
|
+
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
1580
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
1581
|
+
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
1582
|
+
const ok = failed.length === 0 && publishReadiness.status === 'ready';
|
|
1197
1583
|
return {
|
|
1198
1584
|
schema: 'yam.release-report.v1',
|
|
1199
1585
|
generated_at: startedAt,
|
|
1200
1586
|
packageName: PACKAGE_JSON.name,
|
|
1201
1587
|
version: VERSION,
|
|
1202
|
-
ok
|
|
1203
|
-
truth_status:
|
|
1588
|
+
ok,
|
|
1589
|
+
truth_status: ok ? 'verified' : 'blocked',
|
|
1204
1590
|
checks,
|
|
1205
1591
|
failed,
|
|
1206
1592
|
provenance,
|
|
1207
1593
|
tarball,
|
|
1208
1594
|
freshness,
|
|
1595
|
+
publish_readiness: publishReadiness,
|
|
1596
|
+
study_note: releaseStudyNote(publishReadiness, publishBlockerEvidence),
|
|
1597
|
+
publishBlockerEvidence,
|
|
1209
1598
|
next_actions: nextActions
|
|
1210
1599
|
};
|
|
1211
1600
|
}
|
|
@@ -1282,7 +1671,131 @@ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
|
|
|
1282
1671
|
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
1283
1672
|
};
|
|
1284
1673
|
}
|
|
1285
|
-
function
|
|
1674
|
+
function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, publishBlockers = []) {
|
|
1675
|
+
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
1676
|
+
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
1677
|
+
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
1678
|
+
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
1679
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks);
|
|
1680
|
+
const latestProbe = registryStatus.checked
|
|
1681
|
+
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
1682
|
+
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
1683
|
+
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
1684
|
+
const blockers = [];
|
|
1685
|
+
const versionStatus = latestProbe.ok
|
|
1686
|
+
? registryStatus.not_published ? 'package_not_published' : latestVersion === VERSION ? 'already_published' : 'new_version'
|
|
1687
|
+
: 'not_verified';
|
|
1688
|
+
if (!registryProbe.ok) {
|
|
1689
|
+
blockers.push(readinessBlocker('registry_not_verified', 'npm registry could not be read from this shell', 'run `npm config get registry` and confirm it points to the intended public registry', 'npm config get registry', 'error'));
|
|
1690
|
+
}
|
|
1691
|
+
if (!whoamiProbe.ok) {
|
|
1692
|
+
blockers.push(readinessBlocker('auth_not_verified', whoamiProbe.note || 'npm whoami did not confirm an authenticated publisher in this shell', 'refresh npm login/token, then rerun `npm whoami` before publishing', 'npm whoami', 'error'));
|
|
1693
|
+
}
|
|
1694
|
+
if (!latestProbe.ok) {
|
|
1695
|
+
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
1696
|
+
blockers.push(readinessBlocker(blocker?.blocker_kind || 'latest_version_not_verified', blocker?.likely_cause || 'npm latest version could not be read', blocker?.safe_next_action || 'rerun the read-only registry check before publishing', 'npm view', 'error'));
|
|
1697
|
+
}
|
|
1698
|
+
if (versionStatus === 'already_published') {
|
|
1699
|
+
blockers.push(readinessBlocker('version_already_published', `${PACKAGE_JSON.name}@${VERSION} is already present on the registry`, 'bump package.json to the next patch version before publishing', 'npm view', 'error'));
|
|
1700
|
+
}
|
|
1701
|
+
if (failedChecks.length) {
|
|
1702
|
+
blockers.push(readinessBlocker('release_checks_failed', `${failedChecks.length} release check(s) did not pass`, 'fix failing release checks before publishing', 'npm run release:check', 'error'));
|
|
1703
|
+
}
|
|
1704
|
+
if (provenance.git_dirty) {
|
|
1705
|
+
blockers.push(readinessBlocker('git_dirty', 'working tree has uncommitted changes', 'commit intended changes or keep them explicitly local before tagging/publishing', 'git status --short'));
|
|
1706
|
+
}
|
|
1707
|
+
if (provenance.dist_changed_files?.length) {
|
|
1708
|
+
blockers.push(readinessBlocker('dist_not_fresh', 'dist has changed files after freshness check', 'run build and commit dist only if this package publishes dist', 'npm run build'));
|
|
1709
|
+
}
|
|
1710
|
+
if (tarball.status !== 'checked') {
|
|
1711
|
+
blockers.push(readinessBlocker('tarball_not_verified', tarball.reason || 'npm pack dry-run did not produce tarball provenance', 'fix tarball/package-boundary issues before publishing', 'npm pack --dry-run', 'error'));
|
|
1712
|
+
}
|
|
1713
|
+
for (const blocker of publishBlockers) {
|
|
1714
|
+
blockers.push(readinessBlocker(blocker.blocker_kind, blocker.likely_cause, blocker.safe_next_action, 'npm publish', 'error'));
|
|
1715
|
+
}
|
|
1716
|
+
const uniqueBlockers = uniqueReadinessBlockers(blockers);
|
|
1717
|
+
const status = uniqueBlockers.length ? 'blocked' : 'ready';
|
|
1718
|
+
return {
|
|
1719
|
+
schema: 'yam.release-publish-readiness.v1',
|
|
1720
|
+
generated_at: new Date().toISOString(),
|
|
1721
|
+
package_name: PACKAGE_JSON.name,
|
|
1722
|
+
local_version: VERSION,
|
|
1723
|
+
status,
|
|
1724
|
+
registry: {
|
|
1725
|
+
url: registryUrl,
|
|
1726
|
+
latest_version: latestVersion,
|
|
1727
|
+
version_status: versionStatus,
|
|
1728
|
+
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
1729
|
+
},
|
|
1730
|
+
auth: {
|
|
1731
|
+
command: 'npm whoami',
|
|
1732
|
+
status: whoamiProbe.ok ? 'authenticated' : 'not_authenticated',
|
|
1733
|
+
account: whoamiProbe.ok ? 'observed_redacted' : '',
|
|
1734
|
+
note: whoamiProbe.ok ? 'npm account observed; username intentionally redacted' : whoamiProbe.note,
|
|
1735
|
+
truth_status: whoamiProbe.ok ? 'verified' : 'blocked'
|
|
1736
|
+
},
|
|
1737
|
+
blockers: uniqueBlockers,
|
|
1738
|
+
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
1739
|
+
truth_status: status === 'ready' ? 'verified' : 'blocked'
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function releaseRegistryStatusFromChecks(checks = []) {
|
|
1743
|
+
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
1744
|
+
const note = String(registryCheck?.note || '');
|
|
1745
|
+
if (registryCheck?.status !== 'passed')
|
|
1746
|
+
return { checked: false, latest_version: '', not_published: false, note };
|
|
1747
|
+
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
1748
|
+
return {
|
|
1749
|
+
checked: true,
|
|
1750
|
+
latest_version: latest,
|
|
1751
|
+
not_published: /not published yet/i.test(note),
|
|
1752
|
+
note
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
1756
|
+
return {
|
|
1757
|
+
kind,
|
|
1758
|
+
severity,
|
|
1759
|
+
reason: summarizeCheckOutput(redactSensitiveText(reason)),
|
|
1760
|
+
safe_next_action: safeNextAction,
|
|
1761
|
+
command,
|
|
1762
|
+
truth_status: 'partial'
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function uniqueReadinessBlockers(blockers = []) {
|
|
1766
|
+
const seen = new Set();
|
|
1767
|
+
return blockers.filter((blocker) => {
|
|
1768
|
+
const key = `${blocker.kind}:${blocker.safe_next_action}`;
|
|
1769
|
+
if (seen.has(key))
|
|
1770
|
+
return false;
|
|
1771
|
+
seen.add(key);
|
|
1772
|
+
return true;
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
function releaseStudyNote(publishReadiness = {}, publishBlockers = []) {
|
|
1776
|
+
const authBlocked = publishReadiness.blockers?.find((blocker) => /auth|otp|permission|forbidden|not_found/i.test(`${blocker.kind} ${blocker.reason}`));
|
|
1777
|
+
const firstBlocker = authBlocked || publishReadiness.blockers?.[0] || publishBlockers[0];
|
|
1778
|
+
if (!firstBlocker) {
|
|
1779
|
+
return buildStudyNote({
|
|
1780
|
+
changed_code: 'yam release report',
|
|
1781
|
+
changed_role: 'It checks whether the package looks ready to publish without running npm publish.',
|
|
1782
|
+
change_summary: 'Release readiness passed the read-only checks that were available.',
|
|
1783
|
+
why_important: 'A publish command changes public package state, so yam keeps the readiness proof separate from the publish action.',
|
|
1784
|
+
learning_note: 'Readiness means the checks looked good; publishing still requires an explicit user action.'
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
return buildStudyNote({
|
|
1788
|
+
issue_code: 'npm publish readiness',
|
|
1789
|
+
issue_role: 'This part checks registry, version, auth, tarball, and local release checks before any public publish.',
|
|
1790
|
+
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
1791
|
+
changed_code: 'yam release report',
|
|
1792
|
+
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
1793
|
+
change_summary: 'The report adds auth-safe readiness, safe next action, and beginner-readable blocker evidence.',
|
|
1794
|
+
why_important: 'A bad token or wrong npm account can make publish fail or target the wrong package context.',
|
|
1795
|
+
learning_note: 'When release readiness is blocked, fix the first blocker before trying a publish command.'
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishBlockers = [], publishReadiness = {}) {
|
|
1286
1799
|
const actions = [];
|
|
1287
1800
|
for (const check of checks) {
|
|
1288
1801
|
if (check.status === 'passed')
|
|
@@ -1298,8 +1811,73 @@ function releaseNextActions(checks = [], provenance = {}, tarball = {}) {
|
|
|
1298
1811
|
if (tarball.status !== 'checked') {
|
|
1299
1812
|
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
1813
|
}
|
|
1814
|
+
for (const blocker of publishBlockers) {
|
|
1815
|
+
actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
|
|
1816
|
+
}
|
|
1817
|
+
for (const blocker of publishReadiness.blockers || []) {
|
|
1818
|
+
actions.push(nextActionDetail(`publish-readiness-${blocker.kind}`, blocker.severity || 'warning', blocker.reason, blocker.safe_next_action, blocker.command || 'npm publish'));
|
|
1819
|
+
}
|
|
1301
1820
|
return uniqueNextActionDetails(actions);
|
|
1302
1821
|
}
|
|
1822
|
+
function publishBlockerEvidenceFromRelease(checks = [], tarball = {}) {
|
|
1823
|
+
const texts = [
|
|
1824
|
+
...checks.map((check) => `${check.id}\n${check.note || ''}`),
|
|
1825
|
+
tarball.reason || ''
|
|
1826
|
+
].filter(Boolean);
|
|
1827
|
+
const blockers = [];
|
|
1828
|
+
for (const text of texts) {
|
|
1829
|
+
const blocker = classifyPublishBlockerText(text);
|
|
1830
|
+
if (blocker)
|
|
1831
|
+
blockers.push(blocker);
|
|
1832
|
+
}
|
|
1833
|
+
const seen = new Set();
|
|
1834
|
+
return blockers.filter((blocker) => {
|
|
1835
|
+
if (seen.has(blocker.blocker_kind))
|
|
1836
|
+
return false;
|
|
1837
|
+
seen.add(blocker.blocker_kind);
|
|
1838
|
+
return true;
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
function classifyPublishBlockerText(text = '') {
|
|
1842
|
+
const value = String(text || '');
|
|
1843
|
+
if (!value.trim())
|
|
1844
|
+
return null;
|
|
1845
|
+
const excerpt = summarizeCheckOutput(value);
|
|
1846
|
+
if (/\b(ENEEDAUTH|E401|Unauthorized|whoami)\b/i.test(value)) {
|
|
1847
|
+
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`');
|
|
1848
|
+
}
|
|
1849
|
+
if (/\b(OTP|one-time password|two-factor|2FA)\b/i.test(value)) {
|
|
1850
|
+
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');
|
|
1851
|
+
}
|
|
1852
|
+
if (/\bE403\b|403\s+Forbidden|Forbidden\s+-\s+PUT|npm ERR!.*Forbidden/i.test(value)) {
|
|
1853
|
+
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');
|
|
1854
|
+
}
|
|
1855
|
+
if (/\bE404\b|Not Found - PUT/i.test(value)) {
|
|
1856
|
+
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');
|
|
1857
|
+
}
|
|
1858
|
+
if (/previously published|cannot publish over|already exists|You cannot publish over/i.test(value)) {
|
|
1859
|
+
return publishBlocker('version_already_published', excerpt, 'npm versions are immutable once published', 'bump package.json to the next patch version before publishing');
|
|
1860
|
+
}
|
|
1861
|
+
if (/(EACCES|EPERM|permission denied|cache|_cacache)/i.test(value)) {
|
|
1862
|
+
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`');
|
|
1863
|
+
}
|
|
1864
|
+
if (/(ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|registry unreachable|network timeout)/i.test(value)) {
|
|
1865
|
+
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');
|
|
1866
|
+
}
|
|
1867
|
+
if (/(tarball|package boundary|files list|npm pack)/i.test(value)) {
|
|
1868
|
+
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');
|
|
1869
|
+
}
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
function publishBlocker(kind, excerpt, likelyCause, safeNextAction) {
|
|
1873
|
+
return {
|
|
1874
|
+
blocker_kind: kind,
|
|
1875
|
+
native_error_excerpt: excerpt,
|
|
1876
|
+
likely_cause: likelyCause,
|
|
1877
|
+
safe_next_action: safeNextAction,
|
|
1878
|
+
truth_status: 'partial'
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1303
1881
|
function runReadOnlyCommand(command, args = []) {
|
|
1304
1882
|
const result = spawnSync(command, args, {
|
|
1305
1883
|
cwd: ROOT,
|
|
@@ -1310,6 +1888,33 @@ function runReadOnlyCommand(command, args = []) {
|
|
|
1310
1888
|
return '';
|
|
1311
1889
|
return String(result.stdout || '');
|
|
1312
1890
|
}
|
|
1891
|
+
function runReadOnlyCommandResult(command, args = []) {
|
|
1892
|
+
const result = spawnSync(command, args, {
|
|
1893
|
+
cwd: ROOT,
|
|
1894
|
+
encoding: 'utf8',
|
|
1895
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1896
|
+
timeout: 30000
|
|
1897
|
+
});
|
|
1898
|
+
const stdout = redactSensitiveText(String(result.stdout || ''));
|
|
1899
|
+
const stderr = redactSensitiveText(String(result.stderr || result.error?.message || ''));
|
|
1900
|
+
return {
|
|
1901
|
+
ok: result.status === 0,
|
|
1902
|
+
status: typeof result.status === 'number' ? result.status : null,
|
|
1903
|
+
stdout,
|
|
1904
|
+
stderr,
|
|
1905
|
+
note: summarizeCheckOutput([stdout, stderr].filter(Boolean).join('\n'))
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function runReadOnlyCommandIn(cwd, command, args = []) {
|
|
1909
|
+
const result = spawnSync(command, args, {
|
|
1910
|
+
cwd,
|
|
1911
|
+
encoding: 'utf8',
|
|
1912
|
+
timeout: 30000
|
|
1913
|
+
});
|
|
1914
|
+
if (result.status !== 0)
|
|
1915
|
+
return '';
|
|
1916
|
+
return String(result.stdout || '');
|
|
1917
|
+
}
|
|
1313
1918
|
function runReleaseCheck(id, commandSpec) {
|
|
1314
1919
|
const [command, args] = commandSpec;
|
|
1315
1920
|
const start = Date.now();
|
|
@@ -1331,12 +1936,19 @@ function runReleaseCheck(id, commandSpec) {
|
|
|
1331
1936
|
};
|
|
1332
1937
|
}
|
|
1333
1938
|
function summarizeCheckOutput(output = '') {
|
|
1334
|
-
const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1939
|
+
const lines = redactSensitiveText(String(output || '')).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1335
1940
|
if (!lines.length)
|
|
1336
1941
|
return '';
|
|
1337
1942
|
const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
|
|
1338
1943
|
return highSignal.join(' | ').slice(0, 600);
|
|
1339
1944
|
}
|
|
1945
|
+
function redactSensitiveText(text = '') {
|
|
1946
|
+
return String(text || '')
|
|
1947
|
+
.replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
|
|
1948
|
+
.replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
|
|
1949
|
+
.replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
|
|
1950
|
+
.replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
|
|
1951
|
+
}
|
|
1340
1952
|
async function readStdinTextIfAvailable() {
|
|
1341
1953
|
if (process.stdin.isTTY)
|
|
1342
1954
|
return '';
|
|
@@ -1345,6 +1957,54 @@ async function readStdinTextIfAvailable() {
|
|
|
1345
1957
|
chunks.push(Buffer.from(chunk));
|
|
1346
1958
|
return Buffer.concat(chunks).toString('utf8').trim();
|
|
1347
1959
|
}
|
|
1960
|
+
async function loop(args = []) {
|
|
1961
|
+
const subcommand = args[0] || 'help';
|
|
1962
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
1963
|
+
return loopUsage();
|
|
1964
|
+
if (subcommand === 'report')
|
|
1965
|
+
return loopReport(args.slice(1));
|
|
1966
|
+
console.error(`unknown loop command: ${subcommand}`);
|
|
1967
|
+
process.exitCode = 1;
|
|
1968
|
+
return loopUsage();
|
|
1969
|
+
}
|
|
1970
|
+
function loopUsage() {
|
|
1971
|
+
console.log(`yam loop
|
|
1972
|
+
|
|
1973
|
+
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers, next action, and a short study note. It does not run agents, start processes, publish packages, or write files.
|
|
1974
|
+
|
|
1975
|
+
Usage:
|
|
1976
|
+
yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--blocked text] [--remaining-task text] [--truth status] [--intent-label read_only|write|destructive|runtime|visual|publish] [--issue-code text] [--issue-role text] [--issue-symptom text] [--changed-code text] [--changed-role text] [--change-summary text] [--why-important text] [--learning-note text] [--json]
|
|
1977
|
+
`);
|
|
1978
|
+
}
|
|
1979
|
+
async function loopReport(args = []) {
|
|
1980
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1981
|
+
return loopUsage();
|
|
1982
|
+
const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'evidence', 'blocked', 'blocker', 'next-action', 'remaining-task', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
|
|
1983
|
+
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
1984
|
+
const report = buildLoopReport({
|
|
1985
|
+
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
1986
|
+
intent: String(flags.intent || ''),
|
|
1987
|
+
loop_kind: String(flags.loop_kind || flags.route || 'harness'),
|
|
1988
|
+
stages: arrayFlag(flags.stage),
|
|
1989
|
+
evidence: arrayFlag(flags.evidence),
|
|
1990
|
+
blockers,
|
|
1991
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined,
|
|
1992
|
+
intent_label: normalizeToolIntent(flags.intent_label || flags.tool_intent || 'read_only'),
|
|
1993
|
+
next_action: String(flags.next_action || ''),
|
|
1994
|
+
remaining_tasks: arrayFlag(flags.remaining_task),
|
|
1995
|
+
issue_code: String(flags.issue_code || ''),
|
|
1996
|
+
issue_role: String(flags.issue_role || ''),
|
|
1997
|
+
issue_symptom: String(flags.issue_symptom || ''),
|
|
1998
|
+
changed_code: String(flags.changed_code || ''),
|
|
1999
|
+
changed_role: String(flags.changed_role || ''),
|
|
2000
|
+
change_summary: String(flags.change_summary || ''),
|
|
2001
|
+
why_important: String(flags.why_important || ''),
|
|
2002
|
+
learning_note: String(flags.learning_note || '')
|
|
2003
|
+
});
|
|
2004
|
+
printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
|
|
2005
|
+
if (report.truth_status === 'blocked')
|
|
2006
|
+
process.exitCode = 1;
|
|
2007
|
+
}
|
|
1348
2008
|
async function ueye(args = []) {
|
|
1349
2009
|
const subcommand = args[0] || 'help';
|
|
1350
2010
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
@@ -1353,6 +2013,8 @@ async function ueye(args = []) {
|
|
|
1353
2013
|
return ueyeCapture(args.slice(1));
|
|
1354
2014
|
if (subcommand === 'compare')
|
|
1355
2015
|
return ueyeCompare(args.slice(1));
|
|
2016
|
+
if (subcommand === 'preflight')
|
|
2017
|
+
return ueyePreflight(args.slice(1));
|
|
1356
2018
|
if (subcommand === 'report')
|
|
1357
2019
|
return ueyeReport(args.slice(1));
|
|
1358
2020
|
console.error(`unknown ueye command: ${subcommand}`);
|
|
@@ -1366,7 +2028,8 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
|
|
|
1366
2028
|
Usage:
|
|
1367
2029
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1368
2030
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
1369
|
-
yam ueye
|
|
2031
|
+
yam ueye preflight [dir] [--json]
|
|
2032
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--brief-dimension text] [--constraint text] [--anti-slop text] [--invented-metric] [--placeholder-copy] [--generic-visual] [--review-session-id id] [--provider-context local] [--execution-surface in-app-browser] [--app-surface codex-app] [--browser-surface in-app-browser] [--control-mode manual|automated] [--preserved-state] [--preserved-url URL] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
|
|
1370
2033
|
|
|
1371
2034
|
Notes:
|
|
1372
2035
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
@@ -1374,6 +2037,74 @@ Notes:
|
|
|
1374
2037
|
report produces a proof-ready Ueye visual run report, design completion gate, and continuity/comparison record without requiring a new capture.
|
|
1375
2038
|
`);
|
|
1376
2039
|
}
|
|
2040
|
+
async function ueyePreflight(args = []) {
|
|
2041
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
2042
|
+
return ueyeUsage();
|
|
2043
|
+
const parsed = parseDirJsonArgs(args);
|
|
2044
|
+
const report = await buildUeyePreflightReport(parsed.dir);
|
|
2045
|
+
if (parsed.json) {
|
|
2046
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
console.log('Ueye preflight');
|
|
2050
|
+
console.log(`Project: ${report.project}`);
|
|
2051
|
+
console.log(`Preflight id: ${report.preflight_id}`);
|
|
2052
|
+
console.log(`Truth status: ${report.truth_status}`);
|
|
2053
|
+
for (const item of report.checklist)
|
|
2054
|
+
console.log(`- ${item.status}: ${item.item} (${item.next_action})`);
|
|
2055
|
+
}
|
|
2056
|
+
async function buildUeyePreflightReport(targetDir = process.cwd()) {
|
|
2057
|
+
const dir = path.resolve(expandHome(targetDir || process.cwd()));
|
|
2058
|
+
const packageInfo = await projectPackageInfo(dir);
|
|
2059
|
+
const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
|
|
2060
|
+
const pack = await findProjectPack(dir);
|
|
2061
|
+
const playwrightPath = await localPackagePath(dir, 'playwright');
|
|
2062
|
+
const browserCache = await pluginCacheHas('openai-bundled/browser');
|
|
2063
|
+
const uiPaths = await existingRelativePaths(dir, ['app', 'src/app', 'pages', 'src/pages', 'components', 'src/components', 'styles', 'src/styles']);
|
|
2064
|
+
const screenshotAvailability = playwrightPath ? 'automated_capture_possible' : browserCache ? 'in_app_or_user_screenshot_needed' : 'user_screenshot_needed';
|
|
2065
|
+
const p0p1Risks = [];
|
|
2066
|
+
if (!frameworkChecklist.uiLike)
|
|
2067
|
+
p0p1Risks.push('target UI surface is not detected yet');
|
|
2068
|
+
if (!browserCache && !playwrightPath)
|
|
2069
|
+
p0p1Risks.push('no local capture surface was observed');
|
|
2070
|
+
if (!pack)
|
|
2071
|
+
p0p1Risks.push('project direction pack is missing');
|
|
2072
|
+
return {
|
|
2073
|
+
schema: 'yam.ueye-preflight.v1',
|
|
2074
|
+
preflight_id: `ueye-preflight-${timestampId()}`,
|
|
2075
|
+
generated_at: new Date().toISOString(),
|
|
2076
|
+
project: dir,
|
|
2077
|
+
detected_framework: frameworkChecklist.framework,
|
|
2078
|
+
ui_paths: uiPaths,
|
|
2079
|
+
reference_source_needed: true,
|
|
2080
|
+
target_states_needed: ['default', 'loading', 'error', 'empty', 'mobile'],
|
|
2081
|
+
mobile_responsive_check_needed: true,
|
|
2082
|
+
contrast_cta_accessibility_risk: frameworkChecklist.uiLike ? 'check before done claim' : 'unknown until UI target is identified',
|
|
2083
|
+
p0_p1_risk_candidates: p0p1Risks,
|
|
2084
|
+
screenshot_capture_availability: screenshotAvailability,
|
|
2085
|
+
preferred_surface: 'in-app-browser',
|
|
2086
|
+
checklist: [
|
|
2087
|
+
preflightItem('reference', 'reference source selected or user confirms no reference', 'needed', 'record reference path, screenshot, or text target before claiming design fidelity'),
|
|
2088
|
+
preflightItem('states', 'primary states are listed', 'needed', 'prepare default/loading/error/empty/mobile states when the UI supports them'),
|
|
2089
|
+
preflightItem('mobile', 'mobile/responsive pass planned', 'needed', 'capture or inspect a narrow viewport before verified visual claims'),
|
|
2090
|
+
preflightItem('contrast_cta_accessibility', 'CTA, contrast, and accessible labels reviewed', 'needed', 'mark P0/P1 risks before saying done'),
|
|
2091
|
+
preflightItem('capture', 'screenshot/capture surface available', screenshotAvailability === 'automated_capture_possible' ? 'observed' : 'partial', 'use in-app browser or user-provided screenshot if automation is unavailable')
|
|
2092
|
+
],
|
|
2093
|
+
completion_cap: 'preflight cannot prove visual quality without implementation screenshot and comparison evidence',
|
|
2094
|
+
truth_status: 'partial'
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function preflightItem(id, item, status, nextAction) {
|
|
2098
|
+
return { id, item, status, next_action: nextAction, truth_status: status === 'observed' ? 'verified' : 'partial' };
|
|
2099
|
+
}
|
|
2100
|
+
async function existingRelativePaths(dir, relativePaths = []) {
|
|
2101
|
+
const found = [];
|
|
2102
|
+
for (const relative of relativePaths) {
|
|
2103
|
+
if (await exists(path.join(dir, relative)))
|
|
2104
|
+
found.push(relative);
|
|
2105
|
+
}
|
|
2106
|
+
return found;
|
|
2107
|
+
}
|
|
1377
2108
|
async function ueyeCapture(args = []) {
|
|
1378
2109
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1379
2110
|
return ueyeUsage();
|
|
@@ -1539,7 +2270,7 @@ async function ueyeCompare(args = []) {
|
|
|
1539
2270
|
async function ueyeReport(args = []) {
|
|
1540
2271
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1541
2272
|
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']));
|
|
2273
|
+
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'preflight-id', 'p0-risk', 'quality-gate-note', 'brief-dimension', 'constraint', 'anti-slop', 'invented-metric', 'placeholder-copy', 'generic-visual', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'evidence-id', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
|
|
1543
2274
|
const reference = String(flags.reference || flags.ref || '');
|
|
1544
2275
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
1545
2276
|
const referenceSources = [];
|
|
@@ -1548,6 +2279,8 @@ async function ueyeReport(args = []) {
|
|
|
1548
2279
|
let blockedReason = String(flags.blocked_reason || '');
|
|
1549
2280
|
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
1550
2281
|
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
2282
|
+
const designBrief = buildUeyeDesignBrief(flags);
|
|
2283
|
+
const antiSlopReview = buildUeyeAntiSlopReview(flags);
|
|
1551
2284
|
const surfaceContext = buildUeyeSurfaceContext({
|
|
1552
2285
|
provider_context: String(flags.provider_context || 'not-recorded'),
|
|
1553
2286
|
provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
|
|
@@ -1632,7 +2365,7 @@ async function ueyeReport(args = []) {
|
|
|
1632
2365
|
completion_claim: flags.completion_claim || flags.completion_status || 'draft',
|
|
1633
2366
|
design_score: numberOrNull(flags.design_score),
|
|
1634
2367
|
min_design_score: numberOrNull(flags.min_design_score) ?? 8,
|
|
1635
|
-
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0)],
|
|
2368
|
+
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
|
|
1636
2369
|
p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
|
|
1637
2370
|
states_checked: Boolean(flags.states_checked),
|
|
1638
2371
|
mobile_checked: Boolean(flags.mobile_checked),
|
|
@@ -1673,6 +2406,14 @@ async function ueyeReport(args = []) {
|
|
|
1673
2406
|
const result = {
|
|
1674
2407
|
...report,
|
|
1675
2408
|
review_session_id: reviewSessionId,
|
|
2409
|
+
design_brief: designBrief,
|
|
2410
|
+
anti_slop_review: antiSlopReview,
|
|
2411
|
+
preflight: {
|
|
2412
|
+
preflight_id: String(flags.preflight_id || ''),
|
|
2413
|
+
p0_risks: arrayFlag(flags.p0_risk),
|
|
2414
|
+
quality_gate_notes: arrayFlag(flags.quality_gate_note),
|
|
2415
|
+
truth_status: flags.preflight_id || flags.p0_risk || flags.quality_gate_note ? 'partial' : 'assumed'
|
|
2416
|
+
},
|
|
1676
2417
|
tool_intent: 'visual',
|
|
1677
2418
|
capture_backend: String(flags.capture_backend || 'not-recorded'),
|
|
1678
2419
|
compare_backend: String(flags.compare_backend || 'local-file-hash'),
|
|
@@ -1683,6 +2424,48 @@ async function ueyeReport(args = []) {
|
|
|
1683
2424
|
if (report.truth_status === 'blocked')
|
|
1684
2425
|
process.exitCode = 1;
|
|
1685
2426
|
}
|
|
2427
|
+
function buildUeyeDesignBrief(flags = {}) {
|
|
2428
|
+
const dimensions = arrayFlag(flags.brief_dimension);
|
|
2429
|
+
const constraints = arrayFlag(flags.constraint);
|
|
2430
|
+
const hasBrief = dimensions.length || constraints.length;
|
|
2431
|
+
return {
|
|
2432
|
+
schema: 'yam.ueye-design-brief.v1',
|
|
2433
|
+
dimensions,
|
|
2434
|
+
constraints,
|
|
2435
|
+
source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
|
|
2436
|
+
next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
|
|
2437
|
+
truth_status: hasBrief ? 'partial' : 'assumed'
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
function buildUeyeAntiSlopReview(flags = {}) {
|
|
2441
|
+
const explicit = arrayFlag(flags.anti_slop);
|
|
2442
|
+
const checks = [
|
|
2443
|
+
antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
|
|
2444
|
+
antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
|
|
2445
|
+
antiSlopCheck('generic_visual', 'Generic visual treatment does not fit the product context', Boolean(flags.generic_visual), 'tie visuals to the product, state, or reference before claiming design quality')
|
|
2446
|
+
];
|
|
2447
|
+
const blockers = [
|
|
2448
|
+
...explicit,
|
|
2449
|
+
...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
|
|
2450
|
+
];
|
|
2451
|
+
return {
|
|
2452
|
+
schema: 'yam.ueye-anti-slop-review.v1',
|
|
2453
|
+
checks,
|
|
2454
|
+
blockers,
|
|
2455
|
+
next_action: blockers[0] || (checks.some((check) => check.status === 'pass') || explicit.length ? 'anti-slop risks are recorded; keep visual proof separate' : 'record anti-slop risks when they matter before claiming done'),
|
|
2456
|
+
truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function antiSlopCheck(id, label, failed = false, nextAction = '') {
|
|
2460
|
+
return {
|
|
2461
|
+
id,
|
|
2462
|
+
label,
|
|
2463
|
+
status: failed ? 'fail' : 'not_checked',
|
|
2464
|
+
severity: failed ? 'P0' : 'P2',
|
|
2465
|
+
next_action: nextAction,
|
|
2466
|
+
truth_status: failed ? 'blocked' : 'assumed'
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
1686
2469
|
async function readPreviousUeyeReport(file = '') {
|
|
1687
2470
|
const target = String(file || '');
|
|
1688
2471
|
if (!target)
|
|
@@ -1820,7 +2603,7 @@ function runtimeUsage() {
|
|
|
1820
2603
|
console.log(`yam runtime
|
|
1821
2604
|
|
|
1822
2605
|
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]
|
|
2606
|
+
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
2607
|
|
|
1825
2608
|
Notes:
|
|
1826
2609
|
Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
|
|
@@ -1829,13 +2612,21 @@ Notes:
|
|
|
1829
2612
|
async function runtimeEvidence(args = []) {
|
|
1830
2613
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1831
2614
|
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']));
|
|
2615
|
+
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
2616
|
const evidence = buildRuntimeBackendEvidence({
|
|
1834
2617
|
backend: flags.backend,
|
|
1835
2618
|
claim: flags.claim,
|
|
1836
2619
|
evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
|
|
1837
2620
|
command: String(flags.command || ''),
|
|
1838
2621
|
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
2622
|
+
started_at: String(flags.started_at || ''),
|
|
2623
|
+
stopped_at: String(flags.stopped_at || ''),
|
|
2624
|
+
exit_code: numberOrNull(flags.exit_code),
|
|
2625
|
+
pid: numberOrNull(flags.pid),
|
|
2626
|
+
port: numberOrNull(flags.port),
|
|
2627
|
+
cleanup_method: String(flags.cleanup_method || ''),
|
|
2628
|
+
cleanup_observed: Boolean(flags.cleanup_observed),
|
|
2629
|
+
left_running_intentionally: Boolean(flags.left_running_intentionally),
|
|
1839
2630
|
note: String(flags.note || ''),
|
|
1840
2631
|
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
|
|
1841
2632
|
});
|
|
@@ -1862,17 +2653,20 @@ function runtimeEvidenceDetails(flags = {}) {
|
|
|
1862
2653
|
exit_code: numberOrNull(flags.exit_code),
|
|
1863
2654
|
screenshot_id: String(flags.screenshot_id || ''),
|
|
1864
2655
|
started_at: String(flags.started_at || ''),
|
|
1865
|
-
stopped_at: String(flags.stopped_at || '')
|
|
2656
|
+
stopped_at: String(flags.stopped_at || ''),
|
|
2657
|
+
cleanup_method: String(flags.cleanup_method || ''),
|
|
2658
|
+
cleanup_observed: Boolean(flags.cleanup_observed),
|
|
2659
|
+
left_running_intentionally: Boolean(flags.left_running_intentionally)
|
|
1866
2660
|
};
|
|
1867
2661
|
}
|
|
1868
2662
|
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);
|
|
2663
|
+
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
2664
|
}
|
|
1871
2665
|
function runtimeEvidenceNextAction(evidence) {
|
|
1872
2666
|
if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified')
|
|
1873
2667
|
return 'attach this evidence to `yam proof` when making a runtime claim';
|
|
1874
|
-
if (evidence.claim === 'cleanup_verified' && !evidence.
|
|
1875
|
-
return 'rerun with --cleanup-
|
|
2668
|
+
if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_observed && !evidence.left_running_intentionally)
|
|
2669
|
+
return 'rerun with --cleanup-observed plus exit/closure evidence, or record --left-running-intentionally';
|
|
1876
2670
|
if (evidence.backend === 'none' || evidence.backend === 'unknown')
|
|
1877
2671
|
return 'record the actual runtime backend before claiming runtime verification';
|
|
1878
2672
|
return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
|
|
@@ -2053,7 +2847,7 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
|
|
|
2053
2847
|
if (allowed.size && !allowed.has(key))
|
|
2054
2848
|
continue;
|
|
2055
2849
|
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)) {
|
|
2850
|
+
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'invented-metric', 'placeholder-copy', 'generic-visual'].includes(key)) {
|
|
2057
2851
|
flags[normalizedKey] = true;
|
|
2058
2852
|
continue;
|
|
2059
2853
|
}
|
|
@@ -2408,7 +3202,7 @@ function renderProofMarkdownList(values) {
|
|
|
2408
3202
|
function parseProofArgs(args = []) {
|
|
2409
3203
|
const flags = {};
|
|
2410
3204
|
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']);
|
|
3205
|
+
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
3206
|
for (let index = 0; index < args.length; index += 1) {
|
|
2413
3207
|
const arg = args[index];
|
|
2414
3208
|
if (arg.startsWith('--')) {
|
|
@@ -2417,7 +3211,7 @@ function parseProofArgs(args = []) {
|
|
|
2417
3211
|
if (!aliases.has(key))
|
|
2418
3212
|
continue;
|
|
2419
3213
|
const normalizedKey = key.replace(/-/g, '_');
|
|
2420
|
-
if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
|
|
3214
|
+
if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked' || key === 'cleanup-observed' || key === 'left-running-intentionally') {
|
|
2421
3215
|
flags[normalizedKey] = true;
|
|
2422
3216
|
continue;
|
|
2423
3217
|
}
|
|
@@ -2575,13 +3369,21 @@ function structuredEvidenceList(value) {
|
|
|
2575
3369
|
return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
|
|
2576
3370
|
}
|
|
2577
3371
|
function directRuntimeBackendEvidence(flags) {
|
|
2578
|
-
if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked)
|
|
3372
|
+
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
3373
|
return [];
|
|
2580
3374
|
return [JSON.stringify(buildRuntimeBackendEvidence({
|
|
2581
3375
|
backend: flags.runtime_backend,
|
|
2582
3376
|
claim: flags.runtime_claim,
|
|
2583
3377
|
evidence_id: flags.runtime_evidence_id,
|
|
2584
3378
|
command: flags.runtime_command,
|
|
3379
|
+
pid: numberOrNull(flags.runtime_pid),
|
|
3380
|
+
port: numberOrNull(flags.runtime_port),
|
|
3381
|
+
exit_code: numberOrNull(flags.runtime_exit_code),
|
|
3382
|
+
started_at: String(flags.runtime_started_at || ''),
|
|
3383
|
+
stopped_at: String(flags.runtime_stopped_at || ''),
|
|
3384
|
+
cleanup_method: String(flags.runtime_cleanup_method || ''),
|
|
3385
|
+
cleanup_observed: Boolean(flags.cleanup_observed),
|
|
3386
|
+
left_running_intentionally: Boolean(flags.left_running_intentionally),
|
|
2585
3387
|
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
2586
3388
|
note: flags.runtime_note
|
|
2587
3389
|
}))];
|
|
@@ -3316,6 +4118,10 @@ async function main() {
|
|
|
3316
4118
|
return detectProject(process.argv[3]);
|
|
3317
4119
|
if (command === 'pack')
|
|
3318
4120
|
return inspectProjectPack(process.argv[3]);
|
|
4121
|
+
if (command === 'context')
|
|
4122
|
+
return context(process.argv.slice(3));
|
|
4123
|
+
if (command === 'cleanup')
|
|
4124
|
+
return cleanup(process.argv.slice(3));
|
|
3319
4125
|
if (command === 'budget')
|
|
3320
4126
|
return budget(process.argv[3]);
|
|
3321
4127
|
if (command === 'measure')
|
|
@@ -3324,6 +4130,8 @@ async function main() {
|
|
|
3324
4130
|
return tools(process.argv.slice(3));
|
|
3325
4131
|
if (command === 'proof')
|
|
3326
4132
|
return proof(process.argv.slice(3));
|
|
4133
|
+
if (command === 'loop')
|
|
4134
|
+
return loop(process.argv.slice(3));
|
|
3327
4135
|
if (command === 'ueye')
|
|
3328
4136
|
return ueye(process.argv.slice(3));
|
|
3329
4137
|
if (command === 'media')
|