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.
@@ -199,6 +199,70 @@ export function buildRuntimeBackendEvidence(input = {}) {
199
199
  })
200
200
  };
201
201
  }
202
+ export function buildStudyNote(input = {}) {
203
+ const problem = buildStudyNoteSection(input.problem, {
204
+ code: input.issue_code,
205
+ role: input.issue_role,
206
+ symptom: input.issue_symptom
207
+ }, 'problem');
208
+ const change = buildStudyNoteSection(input.change, {
209
+ code: input.changed_code,
210
+ role: input.changed_role,
211
+ summary: input.change_summary
212
+ }, 'change');
213
+ const whyItMatters = shortText(input.why_it_matters || input.why_important);
214
+ const learningNote = shortText(input.learning_note);
215
+ const limits = [
216
+ ...asList(input.limits),
217
+ ...missingStudyNoteLimits(problem, change, whyItMatters, learningNote)
218
+ ];
219
+ const hasProblem = Boolean(problem.code || problem.role || problem.symptom);
220
+ const hasChange = Boolean(change.code || change.role || change.summary);
221
+ const truth = isTruthStatus(input.truth_status) ? input.truth_status
222
+ : hasProblem || hasChange || whyItMatters || learningNote ? 'partial'
223
+ : 'assumed';
224
+ return {
225
+ schema: 'yam.study-note.v1',
226
+ problem,
227
+ change,
228
+ why_it_matters: whyItMatters,
229
+ learning_note: learningNote,
230
+ limits: [...new Set(limits)],
231
+ truth_status: truth
232
+ };
233
+ }
234
+ export function buildLoopReport(input = {}) {
235
+ const stages = Array.isArray(input.stages)
236
+ ? input.stages.map((stage) => buildLoopStage(stage))
237
+ : [];
238
+ const evidence = asList(input.evidence);
239
+ const blockers = asList(input.blockers || input.blocked);
240
+ const hasBlockedStage = stages.some((stage) => stage.status === 'blocked' || stage.status === 'failed');
241
+ const requestedTruth = isTruthStatus(input.truth_status) ? input.truth_status : '';
242
+ const derivedTruth = blockers.length || hasBlockedStage ? 'blocked'
243
+ : evidence.length && stages.some((stage) => stage.status === 'passed') ? 'verified'
244
+ : evidence.length || stages.length ? 'partial'
245
+ : 'assumed';
246
+ const truth = requestedTruth ? weakestTruth(requestedTruth, derivedTruth) : derivedTruth;
247
+ const studyNote = buildStudyNote(input.study_note && typeof input.study_note === 'object' ? input.study_note : input);
248
+ const intentLabel = normalizeToolIntentLabel(input.intent_label || input.tool_intent);
249
+ return {
250
+ schema: 'yam.loop-report.v1',
251
+ generated_at: String(input.generated_at || new Date().toISOString()),
252
+ route: String(input.route || ''),
253
+ intent: shortText(input.intent),
254
+ loop_kind: String(input.loop_kind || 'harness'),
255
+ stages,
256
+ evidence,
257
+ blockers,
258
+ truth_status: truth,
259
+ intent_label: intentLabel,
260
+ tool_intent: intentLabel,
261
+ next_action: shortText(input.next_action || (blockers[0] ? 'resolve the blocker before claiming this loop complete' : 'record the next smallest useful action')),
262
+ remaining_tasks: asList(input.remaining_tasks || input.remaining_task),
263
+ study_note: studyNote
264
+ };
265
+ }
202
266
  export function buildUeyeRunReport(input = {}) {
203
267
  const references = Array.isArray(input.reference_sources)
204
268
  ? input.reference_sources.map((item) => buildUeyeVisualProvenance(item))
@@ -240,6 +304,96 @@ export function buildUeyeRunReport(input = {}) {
240
304
  truth_status: isTruthStatus(input.truth_status) ? input.truth_status : finalTruth
241
305
  };
242
306
  }
307
+ function buildStudyNoteSection(section, fallback, kind) {
308
+ const source = section && typeof section === 'object' ? section : fallback;
309
+ const code = shortText(source.code);
310
+ const role = shortText(source.role);
311
+ const symptom = kind === 'problem' ? shortText(source.symptom) : undefined;
312
+ const summary = kind === 'change' ? shortText(source.summary) : undefined;
313
+ return {
314
+ code,
315
+ role,
316
+ ...(symptom !== undefined ? { symptom } : {}),
317
+ ...(summary !== undefined ? { summary } : {}),
318
+ truth_status: code || role || symptom || summary ? 'partial' : 'assumed'
319
+ };
320
+ }
321
+ function missingStudyNoteLimits(problem, change, whyItMatters = '', learningNote = '') {
322
+ const limits = [];
323
+ if (!problem.code)
324
+ limits.push('issue_code not provided');
325
+ if (!problem.role)
326
+ limits.push('issue_role not provided');
327
+ if (!problem.symptom)
328
+ limits.push('issue_symptom not provided');
329
+ if (!change.code)
330
+ limits.push('changed_code not provided');
331
+ if (!change.role)
332
+ limits.push('changed_role not provided');
333
+ if (!change.summary)
334
+ limits.push('change_summary not provided');
335
+ if (!whyItMatters)
336
+ limits.push('why_it_matters not provided');
337
+ if (!learningNote)
338
+ limits.push('learning_note not provided');
339
+ return limits;
340
+ }
341
+ function buildLoopStage(value = '') {
342
+ if (value && typeof value === 'object') {
343
+ const item = value;
344
+ return loopStage(String(item.id || ''), item.status, String(item.note || ''));
345
+ }
346
+ const [id = '', rawStatus = '', ...noteParts] = String(value || '').split(':');
347
+ return loopStage(id, rawStatus, noteParts.join(':'));
348
+ }
349
+ function loopStage(id = '', rawStatus = '', note = '') {
350
+ const status = normalizeLoopStageStatus(rawStatus);
351
+ return {
352
+ id: shortText(id || 'stage'),
353
+ status,
354
+ note: shortText(note),
355
+ truth_status: stageTruth(status)
356
+ };
357
+ }
358
+ function normalizeLoopStageStatus(value = '') {
359
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
360
+ if (text === 'pass' || text === 'passed' || text === 'ok')
361
+ return 'passed';
362
+ if (text === 'fail' || text === 'failed' || text === 'error')
363
+ return 'failed';
364
+ if (text === 'block' || text === 'blocked')
365
+ return 'blocked';
366
+ if (text === 'skip' || text === 'skipped')
367
+ return 'skipped';
368
+ if (text === 'partial')
369
+ return 'partial';
370
+ if (text === 'pending')
371
+ return 'pending';
372
+ return 'recorded';
373
+ }
374
+ function stageTruth(status) {
375
+ if (status === 'passed')
376
+ return 'verified';
377
+ if (status === 'failed' || status === 'blocked')
378
+ return 'blocked';
379
+ if (status === 'skipped')
380
+ return 'skipped';
381
+ if (status === 'pending')
382
+ return 'assumed';
383
+ return 'partial';
384
+ }
385
+ function normalizeToolIntentLabel(value = '') {
386
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
387
+ if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text))
388
+ return text;
389
+ return 'read_only';
390
+ }
391
+ function shortText(value = '', limit = 240) {
392
+ const text = String(value || '').trim().replace(/\s+/g, ' ');
393
+ if (text.length <= limit)
394
+ return text;
395
+ return `${text.slice(0, limit - 3)}...`;
396
+ }
243
397
  export function buildMediaGenerationProof(input = {}) {
244
398
  const requested = Boolean(input.generation_requested);
245
399
  const attempted = Boolean(input.generation_attempted);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yam-flow",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
5
5
  "type": "module",
6
6
  "author": "0kim0bos",
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 nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence);
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: failed.length === 0,
1563
- truth_status: failed.length === 0 ? 'verified' : 'blocked',
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 releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
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,53 @@ 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, 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] [--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]
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', 'remaining-task', '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
+ remaining_tasks: arrayFlag(flags.remaining_task),
1993
+ issue_code: String(flags.issue_code || ''),
1994
+ issue_role: String(flags.issue_role || ''),
1995
+ issue_symptom: String(flags.issue_symptom || ''),
1996
+ changed_code: String(flags.changed_code || ''),
1997
+ changed_role: String(flags.changed_role || ''),
1998
+ change_summary: String(flags.change_summary || ''),
1999
+ why_important: String(flags.why_important || ''),
2000
+ learning_note: String(flags.learning_note || '')
2001
+ });
2002
+ printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
2003
+ if (report.truth_status === 'blocked') process.exitCode = 1;
2004
+ }
2005
+
1784
2006
  async function ueye(args = []) {
1785
2007
  const subcommand = args[0] || 'help';
1786
2008
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
@@ -1801,7 +2023,7 @@ Usage:
1801
2023
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1802
2024
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1803
2025
  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]
2026
+ 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
2027
 
1806
2028
  Notes:
1807
2029
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -2038,7 +2260,7 @@ async function ueyeCompare(args = []) {
2038
2260
 
2039
2261
  async function ueyeReport(args = []) {
2040
2262
  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']));
2263
+ 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
2264
  const reference = String(flags.reference || flags.ref || '');
2043
2265
  const actual = String(flags.actual || flags.screenshot || '');
2044
2266
  const referenceSources = [];
@@ -2047,6 +2269,8 @@ async function ueyeReport(args = []) {
2047
2269
  let blockedReason = String(flags.blocked_reason || '');
2048
2270
  const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
2049
2271
  const previousReport = await readPreviousUeyeReport(flags.previous_report);
2272
+ const designBrief = buildUeyeDesignBrief(flags);
2273
+ const antiSlopReview = buildUeyeAntiSlopReview(flags);
2050
2274
  const surfaceContext = buildUeyeSurfaceContext({
2051
2275
  provider_context: String(flags.provider_context || 'not-recorded'),
2052
2276
  provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
@@ -2130,7 +2354,7 @@ async function ueyeReport(args = []) {
2130
2354
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
2131
2355
  design_score: numberOrNull(flags.design_score),
2132
2356
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
2133
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
2357
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
2134
2358
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
2135
2359
  states_checked: Boolean(flags.states_checked),
2136
2360
  mobile_checked: Boolean(flags.mobile_checked),
@@ -2171,6 +2395,8 @@ async function ueyeReport(args = []) {
2171
2395
  const result = {
2172
2396
  ...report,
2173
2397
  review_session_id: reviewSessionId,
2398
+ design_brief: designBrief,
2399
+ anti_slop_review: antiSlopReview,
2174
2400
  preflight: {
2175
2401
  preflight_id: String(flags.preflight_id || ''),
2176
2402
  p0_risks: arrayFlag(flags.p0_risk),
@@ -2187,6 +2413,51 @@ async function ueyeReport(args = []) {
2187
2413
  if (report.truth_status === 'blocked') process.exitCode = 1;
2188
2414
  }
2189
2415
 
2416
+ function buildUeyeDesignBrief(flags: AnyRecord = {}) {
2417
+ const dimensions = arrayFlag(flags.brief_dimension);
2418
+ const constraints = arrayFlag(flags.constraint);
2419
+ const hasBrief = dimensions.length || constraints.length;
2420
+ return {
2421
+ schema: 'yam.ueye-design-brief.v1',
2422
+ dimensions,
2423
+ constraints,
2424
+ source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
2425
+ next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
2426
+ truth_status: hasBrief ? 'partial' : 'assumed'
2427
+ };
2428
+ }
2429
+
2430
+ function buildUeyeAntiSlopReview(flags: AnyRecord = {}) {
2431
+ const explicit = arrayFlag(flags.anti_slop);
2432
+ const checks = [
2433
+ antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
2434
+ antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
2435
+ 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')
2436
+ ];
2437
+ const blockers = [
2438
+ ...explicit,
2439
+ ...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
2440
+ ];
2441
+ return {
2442
+ schema: 'yam.ueye-anti-slop-review.v1',
2443
+ checks,
2444
+ blockers,
2445
+ 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'),
2446
+ truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
2447
+ };
2448
+ }
2449
+
2450
+ function antiSlopCheck(id, label, failed = false, nextAction = '') {
2451
+ return {
2452
+ id,
2453
+ label,
2454
+ status: failed ? 'fail' : 'not_checked',
2455
+ severity: failed ? 'P0' : 'P2',
2456
+ next_action: nextAction,
2457
+ truth_status: failed ? 'blocked' : 'assumed'
2458
+ };
2459
+ }
2460
+
2190
2461
  async function readPreviousUeyeReport(file = '') {
2191
2462
  const target = String(file || '');
2192
2463
  if (!target) return null;
@@ -2577,7 +2848,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
2577
2848
  const key = rawKey.slice(2);
2578
2849
  if (allowed.size && !allowed.has(key)) continue;
2579
2850
  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)) {
2851
+ 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
2852
  flags[normalizedKey] = true;
2582
2853
  continue;
2583
2854
  }
@@ -3851,6 +4122,7 @@ async function main() {
3851
4122
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
3852
4123
  if (command === 'tools') return tools(process.argv.slice(3));
3853
4124
  if (command === 'proof') return proof(process.argv.slice(3));
4125
+ if (command === 'loop') return loop(process.argv.slice(3));
3854
4126
  if (command === 'ueye') return ueye(process.argv.slice(3));
3855
4127
  if (command === 'media') return media(process.argv.slice(3));
3856
4128
  if (command === 'runtime') return runtime(process.argv.slice(3));