yam-flow 0.1.7 → 0.1.9
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/CHANGELOG.md +50 -0
- package/COMMANDS.md +6 -1
- package/README.md +9 -1
- package/dist/bin/yam.js +278 -10
- package/dist/lib/trust-kernel.d.ts +48 -0
- package/dist/lib/trust-kernel.js +166 -0
- package/package.json +1 -1
- package/references/final-report.md +26 -0
- package/skills/deep/SKILL.md +2 -2
- package/skills/mission/SKILL.md +2 -2
- package/skills/quick/SKILL.md +2 -1
- package/skills/ueye/SKILL.md +2 -1
- package/src/bin/yam.ts +289 -11
- package/src/lib/trust-kernel.ts +206 -0
- package/yam.manifest.json +4 -2
package/src/bin/yam.ts
CHANGED
|
@@ -10,10 +10,12 @@ import process from 'node:process';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import {
|
|
12
12
|
TRUTH_STATUSES,
|
|
13
|
+
buildLoopReport,
|
|
13
14
|
buildMediaGenerationProof,
|
|
14
15
|
buildMissionPatchEnvelope,
|
|
15
16
|
buildRollbackHint,
|
|
16
17
|
buildRuntimeBackendEvidence,
|
|
18
|
+
buildStudyNote,
|
|
17
19
|
buildUeyeDesignCompletionGate,
|
|
18
20
|
buildUeyeVisualProvenance,
|
|
19
21
|
buildUeyeRunReport,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
detectDbSafetyText as detectTrustDbSafetyText,
|
|
23
25
|
isTruthStatus
|
|
24
26
|
} from '../lib/trust-kernel.js';
|
|
27
|
+
import type { ToolIntent } from '../lib/trust-kernel.js';
|
|
25
28
|
|
|
26
29
|
type AnyRecord = Record<string, any>;
|
|
27
30
|
|
|
@@ -141,6 +144,7 @@ Usage:
|
|
|
141
144
|
yam tools doctor [dir]
|
|
142
145
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
143
146
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
147
|
+
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
144
148
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
145
149
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
146
150
|
yam ueye preflight [dir] [--json]
|
|
@@ -506,6 +510,7 @@ function ownerRouteForAction(id = '', reason = '', command = '') {
|
|
|
506
510
|
function toolIntentForCommand(command = '') {
|
|
507
511
|
const text = String(command || '').toLowerCase();
|
|
508
512
|
if (!text) return 'read_only';
|
|
513
|
+
if (/(npm\s+(view|whoami|config\s+get|owner\s+ls)|registry:check)/i.test(text)) return 'read_only';
|
|
509
514
|
if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
|
|
510
515
|
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
|
|
511
516
|
if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
|
|
@@ -514,9 +519,9 @@ function toolIntentForCommand(command = '') {
|
|
|
514
519
|
return 'write';
|
|
515
520
|
}
|
|
516
521
|
|
|
517
|
-
function normalizeToolIntent(value = '') {
|
|
522
|
+
function normalizeToolIntent(value = ''): ToolIntent {
|
|
518
523
|
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
519
|
-
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
|
|
524
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text as ToolIntent;
|
|
520
525
|
return 'read_only';
|
|
521
526
|
}
|
|
522
527
|
|
|
@@ -1520,6 +1525,11 @@ async function release(args = []) {
|
|
|
1520
1525
|
console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
|
|
1521
1526
|
if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1522
1527
|
}
|
|
1528
|
+
if (report.publish_readiness) {
|
|
1529
|
+
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
1530
|
+
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
1531
|
+
console.log(`- npm auth: ${report.publish_readiness.auth.status}`);
|
|
1532
|
+
}
|
|
1523
1533
|
if (report.failed.length) {
|
|
1524
1534
|
console.log('- Failed:');
|
|
1525
1535
|
for (const id of report.failed) console.log(` - ${id}`);
|
|
@@ -1535,6 +1545,9 @@ async function release(args = []) {
|
|
|
1535
1545
|
console.log('- Next actions:');
|
|
1536
1546
|
for (const action of report.next_actions) console.log(` - ${action.next_action}`);
|
|
1537
1547
|
}
|
|
1548
|
+
if (report.study_note?.problem?.summary || report.study_note?.problem?.symptom) {
|
|
1549
|
+
console.log(`- Study note: ${report.study_note.problem.summary || report.study_note.problem.symptom}`);
|
|
1550
|
+
}
|
|
1538
1551
|
if (!report.ok) process.exitCode = 1;
|
|
1539
1552
|
}
|
|
1540
1553
|
|
|
@@ -1553,19 +1566,23 @@ function runReleaseReport() {
|
|
|
1553
1566
|
const tarball = releaseTarballProvenance();
|
|
1554
1567
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1555
1568
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
1556
|
-
const
|
|
1569
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
1570
|
+
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
1571
|
+
const ok = failed.length === 0 && publishReadiness.status === 'ready';
|
|
1557
1572
|
return {
|
|
1558
1573
|
schema: 'yam.release-report.v1',
|
|
1559
1574
|
generated_at: startedAt,
|
|
1560
1575
|
packageName: PACKAGE_JSON.name,
|
|
1561
1576
|
version: VERSION,
|
|
1562
|
-
ok
|
|
1563
|
-
truth_status:
|
|
1577
|
+
ok,
|
|
1578
|
+
truth_status: ok ? 'verified' : 'blocked',
|
|
1564
1579
|
checks,
|
|
1565
1580
|
failed,
|
|
1566
1581
|
provenance,
|
|
1567
1582
|
tarball,
|
|
1568
1583
|
freshness,
|
|
1584
|
+
publish_readiness: publishReadiness,
|
|
1585
|
+
study_note: releaseStudyNote(publishReadiness, publishBlockerEvidence),
|
|
1569
1586
|
publishBlockerEvidence,
|
|
1570
1587
|
next_actions: nextActions
|
|
1571
1588
|
};
|
|
@@ -1646,7 +1663,136 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
|
|
|
1646
1663
|
};
|
|
1647
1664
|
}
|
|
1648
1665
|
|
|
1649
|
-
function
|
|
1666
|
+
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
|
|
1667
|
+
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
1668
|
+
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
1669
|
+
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
1670
|
+
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
1671
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks);
|
|
1672
|
+
const latestProbe = registryStatus.checked
|
|
1673
|
+
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
1674
|
+
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
1675
|
+
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
1676
|
+
const blockers = [];
|
|
1677
|
+
const versionStatus = latestProbe.ok
|
|
1678
|
+
? registryStatus.not_published ? 'package_not_published' : latestVersion === VERSION ? 'already_published' : 'new_version'
|
|
1679
|
+
: 'not_verified';
|
|
1680
|
+
|
|
1681
|
+
if (!registryProbe.ok) {
|
|
1682
|
+
blockers.push(readinessBlocker('registry_not_verified', 'npm registry could not be read from this shell', 'run `npm config get registry` and confirm it points to the intended public registry', 'npm config get registry', 'error'));
|
|
1683
|
+
}
|
|
1684
|
+
if (!whoamiProbe.ok) {
|
|
1685
|
+
blockers.push(readinessBlocker('auth_not_verified', whoamiProbe.note || 'npm whoami did not confirm an authenticated publisher in this shell', 'refresh npm login/token, then rerun `npm whoami` before publishing', 'npm whoami', 'error'));
|
|
1686
|
+
}
|
|
1687
|
+
if (!latestProbe.ok) {
|
|
1688
|
+
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
1689
|
+
blockers.push(readinessBlocker(blocker?.blocker_kind || 'latest_version_not_verified', blocker?.likely_cause || 'npm latest version could not be read', blocker?.safe_next_action || 'rerun the read-only registry check before publishing', 'npm view', 'error'));
|
|
1690
|
+
}
|
|
1691
|
+
if (versionStatus === 'already_published') {
|
|
1692
|
+
blockers.push(readinessBlocker('version_already_published', `${PACKAGE_JSON.name}@${VERSION} is already present on the registry`, 'bump package.json to the next patch version before publishing', 'npm view', 'error'));
|
|
1693
|
+
}
|
|
1694
|
+
if (failedChecks.length) {
|
|
1695
|
+
blockers.push(readinessBlocker('release_checks_failed', `${failedChecks.length} release check(s) did not pass`, 'fix failing release checks before publishing', 'npm run release:check', 'error'));
|
|
1696
|
+
}
|
|
1697
|
+
if (provenance.git_dirty) {
|
|
1698
|
+
blockers.push(readinessBlocker('git_dirty', 'working tree has uncommitted changes', 'commit intended changes or keep them explicitly local before tagging/publishing', 'git status --short'));
|
|
1699
|
+
}
|
|
1700
|
+
if (provenance.dist_changed_files?.length) {
|
|
1701
|
+
blockers.push(readinessBlocker('dist_not_fresh', 'dist has changed files after freshness check', 'run build and commit dist only if this package publishes dist', 'npm run build'));
|
|
1702
|
+
}
|
|
1703
|
+
if (tarball.status !== 'checked') {
|
|
1704
|
+
blockers.push(readinessBlocker('tarball_not_verified', tarball.reason || 'npm pack dry-run did not produce tarball provenance', 'fix tarball/package-boundary issues before publishing', 'npm pack --dry-run', 'error'));
|
|
1705
|
+
}
|
|
1706
|
+
for (const blocker of publishBlockers) {
|
|
1707
|
+
blockers.push(readinessBlocker(blocker.blocker_kind, blocker.likely_cause, blocker.safe_next_action, 'npm publish', 'error'));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const uniqueBlockers = uniqueReadinessBlockers(blockers);
|
|
1711
|
+
const status = uniqueBlockers.length ? 'blocked' : 'ready';
|
|
1712
|
+
return {
|
|
1713
|
+
schema: 'yam.release-publish-readiness.v1',
|
|
1714
|
+
generated_at: new Date().toISOString(),
|
|
1715
|
+
package_name: PACKAGE_JSON.name,
|
|
1716
|
+
local_version: VERSION,
|
|
1717
|
+
status,
|
|
1718
|
+
registry: {
|
|
1719
|
+
url: registryUrl,
|
|
1720
|
+
latest_version: latestVersion,
|
|
1721
|
+
version_status: versionStatus,
|
|
1722
|
+
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
1723
|
+
},
|
|
1724
|
+
auth: {
|
|
1725
|
+
command: 'npm whoami',
|
|
1726
|
+
status: whoamiProbe.ok ? 'authenticated' : 'not_authenticated',
|
|
1727
|
+
account: whoamiProbe.ok ? 'observed_redacted' : '',
|
|
1728
|
+
note: whoamiProbe.ok ? 'npm account observed; username intentionally redacted' : whoamiProbe.note,
|
|
1729
|
+
truth_status: whoamiProbe.ok ? 'verified' : 'blocked'
|
|
1730
|
+
},
|
|
1731
|
+
blockers: uniqueBlockers,
|
|
1732
|
+
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
1733
|
+
truth_status: status === 'ready' ? 'verified' : 'blocked'
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function releaseRegistryStatusFromChecks(checks = []) {
|
|
1738
|
+
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
1739
|
+
const note = String(registryCheck?.note || '');
|
|
1740
|
+
if (registryCheck?.status !== 'passed') return { checked: false, latest_version: '', not_published: false, note };
|
|
1741
|
+
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
1742
|
+
return {
|
|
1743
|
+
checked: true,
|
|
1744
|
+
latest_version: latest,
|
|
1745
|
+
not_published: /not published yet/i.test(note),
|
|
1746
|
+
note
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
1751
|
+
return {
|
|
1752
|
+
kind,
|
|
1753
|
+
severity,
|
|
1754
|
+
reason: summarizeCheckOutput(redactSensitiveText(reason)),
|
|
1755
|
+
safe_next_action: safeNextAction,
|
|
1756
|
+
command,
|
|
1757
|
+
truth_status: 'partial'
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function uniqueReadinessBlockers(blockers = []) {
|
|
1762
|
+
const seen = new Set<string>();
|
|
1763
|
+
return blockers.filter((blocker) => {
|
|
1764
|
+
const key = `${blocker.kind}:${blocker.safe_next_action}`;
|
|
1765
|
+
if (seen.has(key)) return false;
|
|
1766
|
+
seen.add(key);
|
|
1767
|
+
return true;
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function releaseStudyNote(publishReadiness: AnyRecord = {}, publishBlockers = []) {
|
|
1772
|
+
const authBlocked = publishReadiness.blockers?.find((blocker) => /auth|otp|permission|forbidden|not_found/i.test(`${blocker.kind} ${blocker.reason}`));
|
|
1773
|
+
const firstBlocker = authBlocked || publishReadiness.blockers?.[0] || publishBlockers[0];
|
|
1774
|
+
if (!firstBlocker) {
|
|
1775
|
+
return buildStudyNote({
|
|
1776
|
+
changed_code: 'yam release report',
|
|
1777
|
+
changed_role: 'It checks whether the package looks ready to publish without running npm publish.',
|
|
1778
|
+
change_summary: 'Release readiness passed the read-only checks that were available.',
|
|
1779
|
+
why_important: 'A publish command changes public package state, so yam keeps the readiness proof separate from the publish action.',
|
|
1780
|
+
learning_note: 'Readiness means the checks looked good; publishing still requires an explicit user action.'
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return buildStudyNote({
|
|
1784
|
+
issue_code: 'npm publish readiness',
|
|
1785
|
+
issue_role: 'This part checks registry, version, auth, tarball, and local release checks before any public publish.',
|
|
1786
|
+
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
1787
|
+
changed_code: 'yam release report',
|
|
1788
|
+
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
1789
|
+
change_summary: 'The report adds auth-safe readiness, safe next action, and beginner-readable blocker evidence.',
|
|
1790
|
+
why_important: 'A bad token or wrong npm account can make publish fail or target the wrong package context.',
|
|
1791
|
+
learning_note: 'When release readiness is blocked, fix the first blocker before trying a publish command.'
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = [], publishReadiness: AnyRecord = {}) {
|
|
1650
1796
|
const actions = [];
|
|
1651
1797
|
for (const check of checks) {
|
|
1652
1798
|
if (check.status === 'passed') continue;
|
|
@@ -1664,6 +1810,9 @@ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: An
|
|
|
1664
1810
|
for (const blocker of publishBlockers) {
|
|
1665
1811
|
actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
|
|
1666
1812
|
}
|
|
1813
|
+
for (const blocker of publishReadiness.blockers || []) {
|
|
1814
|
+
actions.push(nextActionDetail(`publish-readiness-${blocker.kind}`, blocker.severity || 'warning', blocker.reason, blocker.safe_next_action, blocker.command || 'npm publish'));
|
|
1815
|
+
}
|
|
1667
1816
|
return uniqueNextActionDetails(actions);
|
|
1668
1817
|
}
|
|
1669
1818
|
|
|
@@ -1736,6 +1885,24 @@ function runReadOnlyCommand(command, args = []) {
|
|
|
1736
1885
|
return String(result.stdout || '');
|
|
1737
1886
|
}
|
|
1738
1887
|
|
|
1888
|
+
function runReadOnlyCommandResult(command, args = []) {
|
|
1889
|
+
const result = spawnSync(command, args, {
|
|
1890
|
+
cwd: ROOT,
|
|
1891
|
+
encoding: 'utf8',
|
|
1892
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1893
|
+
timeout: 30000
|
|
1894
|
+
});
|
|
1895
|
+
const stdout = redactSensitiveText(String(result.stdout || ''));
|
|
1896
|
+
const stderr = redactSensitiveText(String(result.stderr || result.error?.message || ''));
|
|
1897
|
+
return {
|
|
1898
|
+
ok: result.status === 0,
|
|
1899
|
+
status: typeof result.status === 'number' ? result.status : null,
|
|
1900
|
+
stdout,
|
|
1901
|
+
stderr,
|
|
1902
|
+
note: summarizeCheckOutput([stdout, stderr].filter(Boolean).join('\n'))
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1739
1906
|
function runReadOnlyCommandIn(cwd, command, args = []) {
|
|
1740
1907
|
const result = spawnSync(command, args, {
|
|
1741
1908
|
cwd,
|
|
@@ -1768,12 +1935,20 @@ function runReleaseCheck(id, commandSpec) {
|
|
|
1768
1935
|
}
|
|
1769
1936
|
|
|
1770
1937
|
function summarizeCheckOutput(output = '') {
|
|
1771
|
-
const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1938
|
+
const lines = redactSensitiveText(String(output || '')).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1772
1939
|
if (!lines.length) return '';
|
|
1773
1940
|
const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
|
|
1774
1941
|
return highSignal.join(' | ').slice(0, 600);
|
|
1775
1942
|
}
|
|
1776
1943
|
|
|
1944
|
+
function redactSensitiveText(text = '') {
|
|
1945
|
+
return String(text || '')
|
|
1946
|
+
.replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
|
|
1947
|
+
.replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
|
|
1948
|
+
.replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
|
|
1949
|
+
.replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1777
1952
|
async function readStdinTextIfAvailable() {
|
|
1778
1953
|
if (process.stdin.isTTY) return '';
|
|
1779
1954
|
const chunks = [];
|
|
@@ -1781,6 +1956,59 @@ async function readStdinTextIfAvailable() {
|
|
|
1781
1956
|
return Buffer.concat(chunks).toString('utf8').trim();
|
|
1782
1957
|
}
|
|
1783
1958
|
|
|
1959
|
+
async function loop(args = []) {
|
|
1960
|
+
const subcommand = args[0] || 'help';
|
|
1961
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return loopUsage();
|
|
1962
|
+
if (subcommand === 'report') return loopReport(args.slice(1));
|
|
1963
|
+
console.error(`unknown loop command: ${subcommand}`);
|
|
1964
|
+
process.exitCode = 1;
|
|
1965
|
+
return loopUsage();
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function loopUsage() {
|
|
1969
|
+
console.log(`yam loop
|
|
1970
|
+
|
|
1971
|
+
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers, next action, handoff fields, and a short study note. It does not run agents, start processes, publish packages, or write files.
|
|
1972
|
+
|
|
1973
|
+
Usage:
|
|
1974
|
+
yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--blocked text] [--fix-first-item text] [--remaining-task text] [--recommended-direction text] [--implementation-note text] [--why-this-next text] [--blocked-by text] [--owner-route route] [--truth status] [--intent-label read_only|write|destructive|runtime|visual|publish] [--issue-code text] [--issue-role text] [--issue-symptom text] [--changed-code text] [--changed-role text] [--change-summary text] [--why-important text] [--learning-note text] [--json]
|
|
1975
|
+
`);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
async function loopReport(args = []) {
|
|
1979
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
|
|
1980
|
+
const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'evidence', 'blocked', 'blocker', 'next-action', 'fix-first-item', 'remaining-task', 'recommended-direction', 'direction', 'implementation-note', 'why-this-next', 'blocked-by', 'owner-route', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
|
|
1981
|
+
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
1982
|
+
const report = buildLoopReport({
|
|
1983
|
+
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
1984
|
+
intent: String(flags.intent || ''),
|
|
1985
|
+
loop_kind: String(flags.loop_kind || flags.route || 'harness'),
|
|
1986
|
+
stages: arrayFlag(flags.stage),
|
|
1987
|
+
evidence: arrayFlag(flags.evidence),
|
|
1988
|
+
blockers,
|
|
1989
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined,
|
|
1990
|
+
intent_label: normalizeToolIntent(flags.intent_label || flags.tool_intent || 'read_only'),
|
|
1991
|
+
next_action: String(flags.next_action || ''),
|
|
1992
|
+
fix_first_items: arrayFlag(flags.fix_first_item),
|
|
1993
|
+
remaining_tasks: arrayFlag(flags.remaining_task),
|
|
1994
|
+
recommended_direction: String(flags.recommended_direction || flags.direction || ''),
|
|
1995
|
+
implementation_notes: arrayFlag(flags.implementation_note),
|
|
1996
|
+
why_this_next: String(flags.why_this_next || ''),
|
|
1997
|
+
blocked_by: arrayFlag(flags.blocked_by),
|
|
1998
|
+
owner_route: String(flags.owner_route || flags.route || ''),
|
|
1999
|
+
issue_code: String(flags.issue_code || ''),
|
|
2000
|
+
issue_role: String(flags.issue_role || ''),
|
|
2001
|
+
issue_symptom: String(flags.issue_symptom || ''),
|
|
2002
|
+
changed_code: String(flags.changed_code || ''),
|
|
2003
|
+
changed_role: String(flags.changed_role || ''),
|
|
2004
|
+
change_summary: String(flags.change_summary || ''),
|
|
2005
|
+
why_important: String(flags.why_important || ''),
|
|
2006
|
+
learning_note: String(flags.learning_note || '')
|
|
2007
|
+
});
|
|
2008
|
+
printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
|
|
2009
|
+
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1784
2012
|
async function ueye(args = []) {
|
|
1785
2013
|
const subcommand = args[0] || 'help';
|
|
1786
2014
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
|
|
@@ -1801,7 +2029,7 @@ Usage:
|
|
|
1801
2029
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1802
2030
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
1803
2031
|
yam ueye preflight [dir] [--json]
|
|
1804
|
-
yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--review-session-id id] [--provider-context local] [--execution-surface in-app-browser] [--app-surface codex-app] [--browser-surface in-app-browser] [--control-mode manual|automated] [--preserved-state] [--preserved-url URL] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
|
|
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]
|
|
1805
2033
|
|
|
1806
2034
|
Notes:
|
|
1807
2035
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
@@ -2038,7 +2266,7 @@ async function ueyeCompare(args = []) {
|
|
|
2038
2266
|
|
|
2039
2267
|
async function ueyeReport(args = []) {
|
|
2040
2268
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
2041
|
-
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'preflight-id', 'p0-risk', 'quality-gate-note', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'evidence-id', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
|
|
2269
|
+
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']));
|
|
2042
2270
|
const reference = String(flags.reference || flags.ref || '');
|
|
2043
2271
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
2044
2272
|
const referenceSources = [];
|
|
@@ -2047,6 +2275,8 @@ async function ueyeReport(args = []) {
|
|
|
2047
2275
|
let blockedReason = String(flags.blocked_reason || '');
|
|
2048
2276
|
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
2049
2277
|
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
2278
|
+
const designBrief = buildUeyeDesignBrief(flags);
|
|
2279
|
+
const antiSlopReview = buildUeyeAntiSlopReview(flags);
|
|
2050
2280
|
const surfaceContext = buildUeyeSurfaceContext({
|
|
2051
2281
|
provider_context: String(flags.provider_context || 'not-recorded'),
|
|
2052
2282
|
provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
|
|
@@ -2130,7 +2360,7 @@ async function ueyeReport(args = []) {
|
|
|
2130
2360
|
completion_claim: flags.completion_claim || flags.completion_status || 'draft',
|
|
2131
2361
|
design_score: numberOrNull(flags.design_score),
|
|
2132
2362
|
min_design_score: numberOrNull(flags.min_design_score) ?? 8,
|
|
2133
|
-
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
|
|
2363
|
+
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
|
|
2134
2364
|
p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
|
|
2135
2365
|
states_checked: Boolean(flags.states_checked),
|
|
2136
2366
|
mobile_checked: Boolean(flags.mobile_checked),
|
|
@@ -2171,6 +2401,8 @@ async function ueyeReport(args = []) {
|
|
|
2171
2401
|
const result = {
|
|
2172
2402
|
...report,
|
|
2173
2403
|
review_session_id: reviewSessionId,
|
|
2404
|
+
design_brief: designBrief,
|
|
2405
|
+
anti_slop_review: antiSlopReview,
|
|
2174
2406
|
preflight: {
|
|
2175
2407
|
preflight_id: String(flags.preflight_id || ''),
|
|
2176
2408
|
p0_risks: arrayFlag(flags.p0_risk),
|
|
@@ -2187,6 +2419,51 @@ async function ueyeReport(args = []) {
|
|
|
2187
2419
|
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
2188
2420
|
}
|
|
2189
2421
|
|
|
2422
|
+
function buildUeyeDesignBrief(flags: AnyRecord = {}) {
|
|
2423
|
+
const dimensions = arrayFlag(flags.brief_dimension);
|
|
2424
|
+
const constraints = arrayFlag(flags.constraint);
|
|
2425
|
+
const hasBrief = dimensions.length || constraints.length;
|
|
2426
|
+
return {
|
|
2427
|
+
schema: 'yam.ueye-design-brief.v1',
|
|
2428
|
+
dimensions,
|
|
2429
|
+
constraints,
|
|
2430
|
+
source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
|
|
2431
|
+
next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
|
|
2432
|
+
truth_status: hasBrief ? 'partial' : 'assumed'
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function buildUeyeAntiSlopReview(flags: AnyRecord = {}) {
|
|
2437
|
+
const explicit = arrayFlag(flags.anti_slop);
|
|
2438
|
+
const checks = [
|
|
2439
|
+
antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
|
|
2440
|
+
antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
|
|
2441
|
+
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')
|
|
2442
|
+
];
|
|
2443
|
+
const blockers = [
|
|
2444
|
+
...explicit,
|
|
2445
|
+
...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
|
|
2446
|
+
];
|
|
2447
|
+
return {
|
|
2448
|
+
schema: 'yam.ueye-anti-slop-review.v1',
|
|
2449
|
+
checks,
|
|
2450
|
+
blockers,
|
|
2451
|
+
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'),
|
|
2452
|
+
truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function antiSlopCheck(id, label, failed = false, nextAction = '') {
|
|
2457
|
+
return {
|
|
2458
|
+
id,
|
|
2459
|
+
label,
|
|
2460
|
+
status: failed ? 'fail' : 'not_checked',
|
|
2461
|
+
severity: failed ? 'P0' : 'P2',
|
|
2462
|
+
next_action: nextAction,
|
|
2463
|
+
truth_status: failed ? 'blocked' : 'assumed'
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2190
2467
|
async function readPreviousUeyeReport(file = '') {
|
|
2191
2468
|
const target = String(file || '');
|
|
2192
2469
|
if (!target) return null;
|
|
@@ -2577,7 +2854,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
|
2577
2854
|
const key = rawKey.slice(2);
|
|
2578
2855
|
if (allowed.size && !allowed.has(key)) continue;
|
|
2579
2856
|
const normalizedKey = key.replace(/-/g, '_');
|
|
2580
|
-
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read'].includes(key)) {
|
|
2857
|
+
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)) {
|
|
2581
2858
|
flags[normalizedKey] = true;
|
|
2582
2859
|
continue;
|
|
2583
2860
|
}
|
|
@@ -3851,6 +4128,7 @@ async function main() {
|
|
|
3851
4128
|
if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
|
|
3852
4129
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
3853
4130
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
4131
|
+
if (command === 'loop') return loop(process.argv.slice(3));
|
|
3854
4132
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
3855
4133
|
if (command === 'media') return media(process.argv.slice(3));
|
|
3856
4134
|
if (command === 'runtime') return runtime(process.argv.slice(3));
|