yam-flow 0.1.7 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to yam-flow are documented here.
4
4
 
5
+ ## v0.1.8 - 2026-06-16
6
+
7
+ ### Added
8
+
9
+ - Added `yam loop report [--json]` as a read-only loop artifact for intent, stages, evidence, blockers, next action, remaining tasks, tool intent, truth status, and study note.
10
+ - Added `yam.study-note.v1` so reports can carry short non-specialist explanations of the failing code, its role, observed symptom, changed code, why it matters, and learning note without guessing missing details.
11
+ - Added `publish_readiness` and `study_note` to `yam release report --json` with read-only npm registry/auth/version probes and redacted account/token details.
12
+ - Added Ueye `design_brief` and `anti_slop_review` fields with `--brief-dimension`, `--constraint`, `--anti-slop`, `--invented-metric`, `--placeholder-copy`, and `--generic-visual`.
13
+
14
+ ### Improved
15
+
16
+ - Ueye anti-slop P0 findings now feed the design completion gate so `done` claims are blocked until fix-first issues are resolved.
17
+ - Release reports now separate publish readiness evidence from the publish action; `npm publish` remains outside the report.
18
+ - CLI smoke coverage now asserts loop report, study note limits, and Ueye design brief/anti-slop blocked paths.
19
+
20
+ ### Verification
21
+
22
+ - `npm run typecheck`
23
+ - `npm run build`
24
+ - `npm run cli-smoke`
25
+ - `npm run verify:self`
26
+ - `npm run forbidden-names:check`
27
+ - `npm run package-boundary:check`
28
+ - `npm run dist:freshness`
29
+ - `node ./dist/bin/yam.js release report --json` (blocked as expected by npm auth readiness and dirty git)
30
+ - `npm whoami --registry https://registry.npmjs.org/` (E401, confirms auth/token blocker)
31
+
5
32
  ## v0.1.7 - 2026-06-12
6
33
 
7
34
  ### Added
package/COMMANDS.md CHANGED
@@ -45,7 +45,7 @@ Opt-in visual evidence helpers:
45
45
  yam ueye preflight /path/to/project --json
46
46
  yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
47
47
  yam ueye compare --reference ./reference.png --actual .yam/screens/home.png
48
- yam ueye report --reference ./reference.png --actual .yam/screens/home.png --preflight-id ueye-preflight-123 --p0-risk "mobile CTA may clip" --quality-gate-note "check CTA contrast before done" --review-session-id pricing-card-v1 --provider-context local --execution-surface in-app-browser --browser-surface in-app-browser --similar "overall hierarchy" --different "button glow intensity" --missing "mobile state" --design-quality needs-polish --json
48
+ yam ueye report --reference ./reference.png --actual .yam/screens/home.png --preflight-id ueye-preflight-123 --p0-risk "mobile CTA may clip" --quality-gate-note "check CTA contrast before done" --brief-dimension "primary CTA clarity" --constraint "mobile first" --review-session-id pricing-card-v1 --provider-context local --execution-surface in-app-browser --browser-surface in-app-browser --similar "overall hierarchy" --different "button glow intensity" --missing "mobile state" --design-quality needs-polish --json
49
49
  yam ueye report --reference ./reference.png --actual .yam/screens/home.png --completion-claim done --design-quality pass --direction-locked --reference-read --states-checked --mobile-checked --contrast-checked --cta-checked --json
50
50
  yam media proof --requested --attempted --output ./generated.png --wait-loop --json
51
51
  yam proof --route ueye --truth partial --visual "browser/local screenshot comparison executed" --require-visual
@@ -148,6 +148,7 @@ yam tools doctor /path/to/project
148
148
  yam tools doctor /path/to/project --json
149
149
  yam context pressure /path/to/project --json
150
150
  yam cleanup scan /path/to/project --json
151
+ yam loop report --route quick --intent "fix release readiness" --stage "inspect:passed:read release report" --evidence "typecheck passed" --issue-code "src/bin/yam.ts release report" --issue-role "summarizes release readiness without publishing" --issue-symptom "npm auth failure needs clearer next action" --changed-code "yam loop report" --changed-role "records loop evidence and learning note" --change-summary "added a read-only loop artifact" --why-important "it helps users learn what changed without overclaiming verification" --learning-note "fix blockers before claiming done" --json
151
152
  yam proof /path/to/project
152
153
  yam proof --route deep --truth verified --command "npm run build: pass"
153
154
  yam proof --route ueye --truth verified --visual "reference image only"
@@ -187,15 +188,19 @@ yam release report --json
187
188
  yam runtime evidence --backend terminal --claim cleanup-verified --cleanup-observed --exit-code 0 --cleanup-method "exit observed" --json
188
189
  yam mission queue --agent-id reviewer --scope "changed files review" --changed src/bin/yam.ts --verification-hint "npm run typecheck" --json
189
190
  yam benchmark report --label "bundle size" --baseline 120 --current 118 --unit kB --target lower --json
191
+ yam loop report --route deep --intent "verify release readiness" --blocked "npm auth not verified" --next-action "refresh npm token, then rerun npm whoami" --json
190
192
  ```
191
193
 
192
194
  Supported report shapes:
193
195
 
194
196
  - Runtime evidence mini: command/process, observation, cleanup, truth status, next action.
195
197
  - Patch queue lite: mission lane status, changed files, verification hint, rollback hint, truth status.
198
+ - Loop report: intent, stage outcomes, evidence, blockers, next action, remaining tasks, and study note.
199
+ - Study note: short non-specialist explanation of the failing code, its role, observed symptom, changed code, and why it matters.
196
200
  - Benchmark optimization loop lite: baseline, one change, rerun, keep/revert decision.
197
201
  - Structured diagnostic next action: severity, evidence, owner route, next action, truth status.
198
202
  - Ueye continuity/comparison: previous report, current report, comparison delta, design quality, next action.
203
+ - Ueye design brief/anti-slop: brief dimensions, constraints, invented metrics, placeholder copy, generic visuals, and custom P0 blockers.
199
204
  - Context pressure: project pack age/size, memory summary, dirty scope, scripts, instruction surfaces, and route hint.
200
205
  - Cleanup scan: advisory-only list of confusing surfaces with destructive=false.
201
206
 
package/README.md CHANGED
@@ -142,6 +142,7 @@ yam ueye report --reference ./reference.png --actual .yam/screens/home.png --pre
142
142
  yam proof --route ueye --truth verified --visual "implementation screenshot evidence recorded" --design-completion '{"completion_claim":"done","has_implementation_screenshot":true,"design_quality":"pass","states_checked":true,"mobile_checked":true,"contrast_checked":true,"cta_checked":true,"direction_locked":true,"truth_status":"verified"}'
143
143
  yam media proof --requested --attempted --output ./generated.png --wait-loop --json
144
144
  yam proof --route mission --mission-envelope '{"agent_id":"implementer","assigned_scope":"target component","changed_files":["src/file.ts"],"verification_hint":"npm run typecheck","truth_status":"partial"}'
145
+ yam loop report --route quick --intent "fix release readiness" --stage "inspect:passed:read release report" --evidence "typecheck passed" --issue-code "src/bin/yam.ts release report" --issue-role "summarizes release readiness without publishing" --issue-symptom "npm auth failure needs clearer next action" --changed-code "yam loop report" --changed-role "records loop evidence and learning note" --change-summary "added a read-only loop artifact" --why-important "it helps users learn what changed without overclaiming verification" --learning-note "fix blockers before claiming done" --json
145
146
  yam release report --json
146
147
  yam safety "supabase db reset"
147
148
  yam detect /path/to/project
@@ -194,10 +195,13 @@ Publishing still requires confirming the final npm package name and account acce
194
195
  - runtime evidence mini summaries
195
196
  - mission patch queue lite records
196
197
  - release report JSON
198
+ - loop report JSON
199
+ - study note JSON
197
200
  - structured diagnostic next actions
198
201
  - context pressure summaries
199
202
  - advisory cleanup scans
200
203
  - publish blocker evidence
204
+ - publish readiness evidence
201
205
  - change insight reporting
202
206
  - reference source boundary notes
203
207
  - media generation proof caps
@@ -212,7 +216,10 @@ These shapes keep reports machine-readable without turning normal work into a he
212
216
  - Runtime evidence mini: compact route, command/process, observation, cleanup, truth status, and next action fields for `$deep` or `$mission` runtime claims.
213
217
  - Patch queue lite: `$mission` lane records with pending/applied/verified/reverted/blocked state, changed files, verification hint, rollback hint, truth status, and next action.
214
218
  - Release report JSON: `yam release report --json` summarizes typecheck, forbidden-name scan, package boundary, registry status, CLI smoke, dist freshness, diagnostics, and final truth status.
219
+ - Loop report JSON: `yam loop report --json` records a read-only loop artifact: intent, stages, evidence, blockers, next action, remaining tasks, tool intent, truth status, and study note.
220
+ - Study note: `yam.study-note.v1` keeps short non-specialist explanations for what code was failing, what role it plays, how the symptom showed up, what changed, and why it matters. Missing details stay in `limits` instead of being guessed.
215
221
  - Publish blocker evidence: release reports classify common npm auth, permission, OTP, immutable-version, cache, registry, and tarball failures into safe next actions.
222
+ - Publish readiness evidence: release reports run read-only registry/auth/version probes, redact account/token details, and keep npm publish outside the report.
216
223
  - Context pressure: `yam context pressure` explains when project context is getting stale, broad, or confusing enough to summarize, refresh, narrow scope, or deepen route.
217
224
  - Cleanup scan: `yam cleanup scan` is read-only and advisory; it reports confusing hooks, local skill folders, stale traces, or old proof artifacts without deleting anything.
218
225
  - Benchmark optimization loop lite: use `yam measure <route>` and `yam template tuning` to record baseline, one route wording change, rerun result, keep/revert decision, and stop condition.
@@ -220,6 +227,7 @@ These shapes keep reports machine-readable without turning normal work into a he
220
227
  - Ueye review continuity/comparison report: `$ueye` run reports can link a previous review, current evidence, comparison delta, design quality result, and next action.
221
228
  - Ueye surface context: `$ueye` run reports can record provider context, provider badge, execution surface, app surface, browser surface, control mode, preserved URL/state, and evidence id.
222
229
  - Ueye design completion gate: `$ueye` can stay fast for draft work, but a `done` claim is capped until implementation evidence, comparison/design-quality status, P0/P1 status, key states, mobile/responsive behavior, contrast/accessibility visuals, CTA affordance, and direction/reference-read proof are recorded when relevant.
230
+ - Ueye design brief and anti-slop review: `$ueye` reports can record operator-provided brief dimensions, constraints, invented metrics, placeholder copy, generic visuals, and custom anti-slop blockers. P0 anti-slop findings block `done` claims.
223
231
 
224
232
  ## Ueye Capture And Compare
225
233
 
@@ -229,7 +237,7 @@ These shapes keep reports machine-readable without turning normal work into a he
229
237
  yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
230
238
  yam ueye compare --reference ./reference.png --actual .yam/screens/home.png --json
231
239
  yam ueye preflight . --json
232
- yam ueye report --reference ./reference.png --actual .yam/screens/home.png --completion-claim done --design-quality pass --direction-locked --reference-read --states-checked --mobile-checked --contrast-checked --cta-checked --provider-context local --execution-surface in-app-browser --browser-surface in-app-browser --preserved-state --json
240
+ yam ueye report --reference ./reference.png --actual .yam/screens/home.png --brief-dimension "primary CTA clarity" --constraint "mobile first" --anti-slop "placeholder copy remains" --completion-claim done --design-quality pass --direction-locked --reference-read --states-checked --mobile-checked --contrast-checked --cta-checked --provider-context local --execution-surface in-app-browser --browser-surface in-app-browser --preserved-state --json
233
241
  ```
234
242
 
235
243
  `capture` uses a Playwright install from the current project when present, then falls back to the package context. It does not install browsers or add runtime dependencies by itself. If capture is unavailable, the visual claim stays `partial` or `blocked` until a real screenshot is supplied.
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
  }
@@ -122,6 +122,7 @@ Usage:
122
122
  yam tools doctor [dir]
123
123
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
124
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]
125
126
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
126
127
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
127
128
  yam ueye preflight [dir] [--json]
@@ -503,6 +504,8 @@ function toolIntentForCommand(command = '') {
503
504
  const text = String(command || '').toLowerCase();
504
505
  if (!text)
505
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';
506
509
  if (/(publish|registry|release|npm pack|npm publish)/i.test(text))
507
510
  return 'publish';
508
511
  if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text))
@@ -1531,6 +1534,11 @@ async function release(args = []) {
1531
1534
  if (report.tarball.file_count !== undefined)
1532
1535
  console.log(`- Tarball files: ${report.tarball.file_count}`);
1533
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
+ }
1534
1542
  if (report.failed.length) {
1535
1543
  console.log('- Failed:');
1536
1544
  for (const id of report.failed)
@@ -1548,6 +1556,9 @@ async function release(args = []) {
1548
1556
  for (const action of report.next_actions)
1549
1557
  console.log(` - ${action.next_action}`);
1550
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
+ }
1551
1562
  if (!report.ok)
1552
1563
  process.exitCode = 1;
1553
1564
  }
@@ -1566,19 +1577,23 @@ function runReleaseReport() {
1566
1577
  const tarball = releaseTarballProvenance();
1567
1578
  const freshness = releaseFreshness(checks, provenance, tarball);
1568
1579
  const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
1569
- const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence);
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';
1570
1583
  return {
1571
1584
  schema: 'yam.release-report.v1',
1572
1585
  generated_at: startedAt,
1573
1586
  packageName: PACKAGE_JSON.name,
1574
1587
  version: VERSION,
1575
- ok: failed.length === 0,
1576
- truth_status: failed.length === 0 ? 'verified' : 'blocked',
1588
+ ok,
1589
+ truth_status: ok ? 'verified' : 'blocked',
1577
1590
  checks,
1578
1591
  failed,
1579
1592
  provenance,
1580
1593
  tarball,
1581
1594
  freshness,
1595
+ publish_readiness: publishReadiness,
1596
+ study_note: releaseStudyNote(publishReadiness, publishBlockerEvidence),
1582
1597
  publishBlockerEvidence,
1583
1598
  next_actions: nextActions
1584
1599
  };
@@ -1656,7 +1671,131 @@ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
1656
1671
  truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
1657
1672
  };
1658
1673
  }
1659
- function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishBlockers = []) {
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 = {}) {
1660
1799
  const actions = [];
1661
1800
  for (const check of checks) {
1662
1801
  if (check.status === 'passed')
@@ -1675,6 +1814,9 @@ function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishB
1675
1814
  for (const blocker of publishBlockers) {
1676
1815
  actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
1677
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
+ }
1678
1820
  return uniqueNextActionDetails(actions);
1679
1821
  }
1680
1822
  function publishBlockerEvidenceFromRelease(checks = [], tarball = {}) {
@@ -1746,6 +1888,23 @@ function runReadOnlyCommand(command, args = []) {
1746
1888
  return '';
1747
1889
  return String(result.stdout || '');
1748
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
+ }
1749
1908
  function runReadOnlyCommandIn(cwd, command, args = []) {
1750
1909
  const result = spawnSync(command, args, {
1751
1910
  cwd,
@@ -1777,12 +1936,19 @@ function runReleaseCheck(id, commandSpec) {
1777
1936
  };
1778
1937
  }
1779
1938
  function summarizeCheckOutput(output = '') {
1780
- 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);
1781
1940
  if (!lines.length)
1782
1941
  return '';
1783
1942
  const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
1784
1943
  return highSignal.join(' | ').slice(0, 600);
1785
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
+ }
1786
1952
  async function readStdinTextIfAvailable() {
1787
1953
  if (process.stdin.isTTY)
1788
1954
  return '';
@@ -1791,6 +1957,54 @@ async function readStdinTextIfAvailable() {
1791
1957
  chunks.push(Buffer.from(chunk));
1792
1958
  return Buffer.concat(chunks).toString('utf8').trim();
1793
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
+ }
1794
2008
  async function ueye(args = []) {
1795
2009
  const subcommand = args[0] || 'help';
1796
2010
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
@@ -1815,7 +2029,7 @@ Usage:
1815
2029
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1816
2030
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1817
2031
  yam ueye preflight [dir] [--json]
1818
- yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--review-session-id id] [--provider-context local] [--execution-surface in-app-browser] [--app-surface codex-app] [--browser-surface in-app-browser] [--control-mode manual|automated] [--preserved-state] [--preserved-url URL] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
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]
1819
2033
 
1820
2034
  Notes:
1821
2035
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -2056,7 +2270,7 @@ async function ueyeCompare(args = []) {
2056
2270
  async function ueyeReport(args = []) {
2057
2271
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
2058
2272
  return ueyeUsage();
2059
- const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'preflight-id', 'p0-risk', 'quality-gate-note', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'evidence-id', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
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']));
2060
2274
  const reference = String(flags.reference || flags.ref || '');
2061
2275
  const actual = String(flags.actual || flags.screenshot || '');
2062
2276
  const referenceSources = [];
@@ -2065,6 +2279,8 @@ async function ueyeReport(args = []) {
2065
2279
  let blockedReason = String(flags.blocked_reason || '');
2066
2280
  const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
2067
2281
  const previousReport = await readPreviousUeyeReport(flags.previous_report);
2282
+ const designBrief = buildUeyeDesignBrief(flags);
2283
+ const antiSlopReview = buildUeyeAntiSlopReview(flags);
2068
2284
  const surfaceContext = buildUeyeSurfaceContext({
2069
2285
  provider_context: String(flags.provider_context || 'not-recorded'),
2070
2286
  provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
@@ -2149,7 +2365,7 @@ async function ueyeReport(args = []) {
2149
2365
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
2150
2366
  design_score: numberOrNull(flags.design_score),
2151
2367
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
2152
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
2368
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
2153
2369
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
2154
2370
  states_checked: Boolean(flags.states_checked),
2155
2371
  mobile_checked: Boolean(flags.mobile_checked),
@@ -2190,6 +2406,8 @@ async function ueyeReport(args = []) {
2190
2406
  const result = {
2191
2407
  ...report,
2192
2408
  review_session_id: reviewSessionId,
2409
+ design_brief: designBrief,
2410
+ anti_slop_review: antiSlopReview,
2193
2411
  preflight: {
2194
2412
  preflight_id: String(flags.preflight_id || ''),
2195
2413
  p0_risks: arrayFlag(flags.p0_risk),
@@ -2206,6 +2424,48 @@ async function ueyeReport(args = []) {
2206
2424
  if (report.truth_status === 'blocked')
2207
2425
  process.exitCode = 1;
2208
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
+ }
2209
2469
  async function readPreviousUeyeReport(file = '') {
2210
2470
  const target = String(file || '');
2211
2471
  if (!target)
@@ -2587,7 +2847,7 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
2587
2847
  if (allowed.size && !allowed.has(key))
2588
2848
  continue;
2589
2849
  const normalizedKey = key.replace(/-/g, '_');
2590
- if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read'].includes(key)) {
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)) {
2591
2851
  flags[normalizedKey] = true;
2592
2852
  continue;
2593
2853
  }
@@ -3870,6 +4130,8 @@ async function main() {
3870
4130
  return tools(process.argv.slice(3));
3871
4131
  if (command === 'proof')
3872
4132
  return proof(process.argv.slice(3));
4133
+ if (command === 'loop')
4134
+ return loop(process.argv.slice(3));
3873
4135
  if (command === 'ueye')
3874
4136
  return ueye(process.argv.slice(3));
3875
4137
  if (command === 'media')
@@ -5,6 +5,8 @@ export type EvidenceKind = 'command' | 'evidence' | 'visual' | 'runtime' | 'skip
5
5
  export type RuntimeSubsystem = 'real_runtime' | 'tmux_physical' | 'process_cleanup' | 'browser_visual' | 'db_safety';
6
6
  export type RuntimeBackend = 'none' | 'in_app_browser' | 'playwright' | 'terminal' | 'tmux' | 'zellij' | 'unknown';
7
7
  export type RuntimeClaim = 'not_started' | 'started' | 'observed' | 'stopped' | 'cleanup_verified' | 'blocked' | 'unknown';
8
+ export type LoopStageStatus = 'passed' | 'failed' | 'blocked' | 'skipped' | 'partial' | 'pending' | 'recorded';
9
+ export type ToolIntent = 'read_only' | 'write' | 'destructive' | 'runtime' | 'visual' | 'publish';
8
10
  export interface SafetyHit {
9
11
  level: SafetyLevel;
10
12
  reason: string;
@@ -124,6 +126,44 @@ export interface RuntimeBackendEvidence {
124
126
  note: string;
125
127
  truth_status: TruthStatus;
126
128
  }
129
+ export interface StudyNoteSection {
130
+ code: string;
131
+ role: string;
132
+ symptom?: string;
133
+ summary?: string;
134
+ truth_status: TruthStatus;
135
+ }
136
+ export interface StudyNote {
137
+ schema: 'yam.study-note.v1';
138
+ problem: StudyNoteSection;
139
+ change: StudyNoteSection;
140
+ why_it_matters: string;
141
+ learning_note: string;
142
+ limits: string[];
143
+ truth_status: TruthStatus;
144
+ }
145
+ export interface LoopStage {
146
+ id: string;
147
+ status: LoopStageStatus;
148
+ note: string;
149
+ truth_status: TruthStatus;
150
+ }
151
+ export interface LoopReport {
152
+ schema: 'yam.loop-report.v1';
153
+ generated_at: string;
154
+ route: string;
155
+ intent: string;
156
+ loop_kind: string;
157
+ stages: LoopStage[];
158
+ evidence: string[];
159
+ blockers: string[];
160
+ truth_status: TruthStatus;
161
+ intent_label: ToolIntent;
162
+ tool_intent: ToolIntent;
163
+ next_action: string;
164
+ remaining_tasks: string[];
165
+ study_note: StudyNote;
166
+ }
127
167
  export interface UeyeRunReport {
128
168
  schema: 'yam.ueye-run-report.v1';
129
169
  reference_sources: UeyeVisualProvenance[];
@@ -230,6 +270,8 @@ export declare function buildUeyeDesignCompletionGate(input?: Partial<UeyeDesign
230
270
  export declare function buildRollbackHint(input?: Partial<RollbackHint>): RollbackHint;
231
271
  export declare function buildMissionPatchEnvelope(input?: Partial<MissionPatchEnvelope>): MissionPatchEnvelope;
232
272
  export declare function buildRuntimeBackendEvidence(input?: Partial<RuntimeBackendEvidence>): RuntimeBackendEvidence;
273
+ export declare function buildStudyNote(input?: Partial<StudyNote> & Record<string, unknown>): StudyNote;
274
+ export declare function buildLoopReport(input?: Partial<LoopReport> & Record<string, unknown>): LoopReport;
233
275
  export declare function buildUeyeRunReport(input?: UeyeRunReportInput): UeyeRunReport;
234
276
  export declare function buildMediaGenerationProof(input?: Partial<MediaGenerationProof>): MediaGenerationProof;
235
277
  export declare function isTruthStatus(value?: unknown): value is TruthStatus;