yam-flow 2.1.0 → 2.2.0

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/src/bin/yam.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  detectDbSafetyText as detectTrustDbSafetyText,
25
25
  isTruthStatus
26
26
  } from '../lib/trust-kernel.js';
27
- import type { LoopEvidenceLevel, ToolIntent } from '../lib/trust-kernel.js';
27
+ import type { LoopEvidenceLevel, ReadinessState, ToolIntent } from '../lib/trust-kernel.js';
28
28
 
29
29
  type AnyRecord = Record<string, any>;
30
30
 
@@ -71,6 +71,7 @@ const PROJECT_PACK = 'yam.project.md';
71
71
  const LEGACY_PROJECT_PACK = 'timeto.project.md';
72
72
  const PACK_STALE_DAYS = 30;
73
73
  const YAM_LITE_HOOK_COMMAND = `node ${path.join(ROOT, 'dist', 'bin', 'yam.js')} hook run lite`;
74
+ const YAM_STUDY_NOTE_HOOK_COMMAND = `node ${path.join(ROOT, 'dist', 'bin', 'yam.js')} hook run study-note`;
74
75
  const REQUIRED_PACK_SECTIONS = [
75
76
  'Product Direction',
76
77
  'UI Direction',
@@ -144,6 +145,7 @@ Usage:
144
145
  yam tools doctor [dir]
145
146
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
146
147
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
148
+ yam study-note check [dir] [--report file|--text text] [--json]
147
149
  yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
148
150
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
149
151
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
@@ -156,7 +158,7 @@ Usage:
156
158
  yam release report [--json]
157
159
  yam safety [text...]
158
160
  yam memory <init|add|list|summary|resolve> [dir] [options]
159
- yam hook <status|enable|disable|run> [lite] [--global|--project dir]
161
+ yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
160
162
  yam template <project|ueye|mission|proof|tuning>
161
163
  yam tune-log [dir]
162
164
  yam install
@@ -532,6 +534,12 @@ function normalizeLoopEvidenceLevelFlag(value = ''): LoopEvidenceLevel {
532
534
  return 'none';
533
535
  }
534
536
 
537
+ function normalizeReadinessStateFlag(value = ''): ReadinessState {
538
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
539
+ if (['usable', 'degraded', 'blocked', 'unknown'].includes(text)) return text as ReadinessState;
540
+ return 'unknown';
541
+ }
542
+
535
543
  function uniqueNextActionDetails(actions = []) {
536
544
  const seen = new Set<string>();
537
545
  const unique = [];
@@ -708,6 +716,126 @@ async function cleanup(args = []) {
708
716
  return cleanupUsage();
709
717
  }
710
718
 
719
+ async function studyNote(args = []) {
720
+ const subcommand = args[0] || 'help';
721
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return studyNoteUsage();
722
+ if (subcommand === 'check') return studyNoteCheck(args.slice(1));
723
+ console.error(`unknown study-note command: ${subcommand}`);
724
+ return studyNoteUsage();
725
+ }
726
+
727
+ function studyNoteUsage() {
728
+ console.log(`yam study-note
729
+
730
+ Usage:
731
+ yam study-note check [dir] [--report file|--text text] [--json]
732
+
733
+ Notes:
734
+ Read-only guard. It checks whether changed work has a Study Note and whether relevant UI/CSS/DB hygiene was reported.
735
+ It does not generate, edit, or infer the Study Note for you.
736
+ `);
737
+ }
738
+
739
+ async function studyNoteCheck(args = []) {
740
+ const flags = parseSimpleFlags(args, new Set(['report', 'text', 'json']));
741
+ const dir = path.resolve(firstPositional(args) || process.cwd());
742
+ const changedFiles = await gitChangedFiles(dir);
743
+ const reportText = await readStudyNoteReportText(flags, dir);
744
+ const needsStudyNote = changedFiles.length > 0;
745
+ const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
746
+ const checks = [
747
+ studyNoteGuardCheck('changed_files', needsStudyNote ? 'pass' : 'skipped', needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected'),
748
+ studyNoteGuardCheck('study_note_present', !needsStudyNote || hasStudyNoteMarker(reportText) ? 'pass' : 'fail', 'final report should include a Study Note when project artifacts changed'),
749
+ studyNoteGuardCheck('role_or_responsibility', !needsStudyNote || /(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should explain what the touched code/artifact does'),
750
+ studyNoteGuardCheck('before_after_or_change', !needsStudyNote || /(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should explain what changed from before to after'),
751
+ studyNoteGuardCheck('verification_or_limits', !needsStudyNote || /(verification|verified|checked|limits|uncertain|검증|확인|한계|모르는)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should say what was checked and what remains uncertain')
752
+ ];
753
+ for (const requirement of hygieneRequirements) {
754
+ checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(reportText, requirement), requirement.note));
755
+ }
756
+ const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
757
+ const result = {
758
+ schema: 'yam.study-note-guard.v1',
759
+ generated_at: new Date().toISOString(),
760
+ project: dir,
761
+ changed_files: changedFiles,
762
+ changed_file_count: changedFiles.length,
763
+ report_source: flags.report ? path.resolve(dir, String(flags.report)) : flags.text ? 'inline_text' : 'not_provided',
764
+ checks,
765
+ blockers,
766
+ next_action: blockers[0] || (needsStudyNote ? 'Study Note guard passed for the supplied report text' : 'no Study Note required because no changed files were detected'),
767
+ truth_status: blockers.length ? 'blocked' : needsStudyNote ? 'verified' : 'skipped'
768
+ };
769
+ printJsonOrHuman(result, Boolean(flags.json), 'Study Note guard');
770
+ if (blockers.length) process.exitCode = 1;
771
+ }
772
+
773
+ function firstPositional(args = []) {
774
+ for (let index = 0; index < args.length; index += 1) {
775
+ const arg = String(args[index] || '');
776
+ if (arg.startsWith('--')) {
777
+ if (!arg.includes('=') && !['--json'].includes(arg)) index += 1;
778
+ continue;
779
+ }
780
+ if (arg === 'check') continue;
781
+ return arg;
782
+ }
783
+ return '';
784
+ }
785
+
786
+ async function gitChangedFiles(dir) {
787
+ const output = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
788
+ return output
789
+ .split(/\r?\n/)
790
+ .filter(Boolean)
791
+ .map((line) => line.replace(/^..\s+/, '').replace(/^.* -> /, ''))
792
+ .map((line) => line.trim())
793
+ .filter((file) => file && !file.startsWith('dist/') && !file.endsWith('.tgz'));
794
+ }
795
+
796
+ async function readStudyNoteReportText(flags, dir) {
797
+ if (flags.text) return String(flags.text || '');
798
+ if (!flags.report) return '';
799
+ const file = path.resolve(dir, String(flags.report));
800
+ try {
801
+ return await fsp.readFile(file, 'utf8');
802
+ } catch {
803
+ return '';
804
+ }
805
+ }
806
+
807
+ function hasStudyNoteMarker(text = '') {
808
+ return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
809
+ }
810
+
811
+ function studyNoteGuardCheck(id, status, nextAction) {
812
+ return {
813
+ id,
814
+ status,
815
+ next_action: nextAction,
816
+ truth_status: status === 'pass' ? 'verified' : status === 'fail' ? 'blocked' : status === 'skipped' ? 'skipped' : 'partial'
817
+ };
818
+ }
819
+
820
+ function studyNoteHygieneRequirements(files = []) {
821
+ const requirements = [];
822
+ if (files.some((file) => /(^|\/)page\.tsx$/i.test(file))) {
823
+ requirements.push({ id: 'page_tsx_hygiene', terms: ['page.tsx', 'component', 'hook', 'helper', 'server action', 'route'], note: 'Study Note should say whether page.tsx stayed focused or needs component/hook/helper/server-action separation' });
824
+ }
825
+ if (files.some((file) => /(^|\/)global\.css$/i.test(file))) {
826
+ requirements.push({ id: 'global_css_hygiene', terms: ['global.css', 'token', 'component', 'scoped', 'module', 'utility'], note: 'Study Note should say whether CSS belongs globally or should move to tokens/scoped/component styles' });
827
+ }
828
+ if (files.some((file) => /(migration|schema|db|database|sql|prisma|drizzle|supabase)/i.test(file))) {
829
+ requirements.push({ id: 'jsonb_hygiene', terms: ['jsonb', 'column', 'table', 'constraint', 'index', 'schema', 'relation'], note: 'Study Note should say whether structured product data belongs in typed columns/tables/constraints/indexes instead of broad DB jsonb' });
830
+ }
831
+ return requirements;
832
+ }
833
+
834
+ function requirementSatisfied(text = '', requirement) {
835
+ const normalized = String(text || '').toLowerCase();
836
+ return requirement.terms.some((term) => normalized.includes(term.toLowerCase())) ? 'pass' : 'fail';
837
+ }
838
+
711
839
  function cleanupUsage() {
712
840
  console.log(`yam cleanup
713
841
 
@@ -1270,12 +1398,12 @@ function hookUsage() {
1270
1398
 
1271
1399
  Usage:
1272
1400
  yam hook status [--global|--project dir]
1273
- yam hook enable lite [--global|--project dir]
1274
- yam hook disable [lite] [--global|--project dir]
1275
- yam hook run lite
1401
+ yam hook enable <lite|study-note> [--global|--project dir]
1402
+ yam hook disable [lite|study-note] [--global|--project dir]
1403
+ yam hook run <lite|study-note>
1276
1404
 
1277
1405
  Notes:
1278
- yam-lite is opt-in and advisory-only. It does not run checks, tmux, subagents, or proof gates.
1406
+ hooks are opt-in and advisory-only. They do not run tmux, subagents, or proof gates.
1279
1407
  `);
1280
1408
  }
1281
1409
 
@@ -1283,8 +1411,8 @@ function parseHookArgs(args = []) {
1283
1411
  const result = { mode: 'project', projectDir: process.cwd(), profile: 'lite' };
1284
1412
  for (let index = 0; index < args.length; index += 1) {
1285
1413
  const arg = args[index];
1286
- if (arg === 'lite') {
1287
- result.profile = 'lite';
1414
+ if (arg === 'lite' || arg === 'study-note') {
1415
+ result.profile = arg;
1288
1416
  } else if (arg === '--global') {
1289
1417
  result.mode = 'global';
1290
1418
  } else if (arg === '--project') {
@@ -1308,13 +1436,21 @@ function isYamLiteHook(handler: AnyRecord = {}) {
1308
1436
  return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run lite');
1309
1437
  }
1310
1438
 
1311
- function stripYamLiteHooks(config: AnyRecord = {}) {
1439
+ function isYamStudyNoteHook(handler: AnyRecord = {}) {
1440
+ return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run study-note');
1441
+ }
1442
+
1443
+ function isYamHookProfile(handler: AnyRecord = {}, profile = 'lite') {
1444
+ return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
1445
+ }
1446
+
1447
+ function stripYamHooks(config: AnyRecord = {}, profile = 'lite') {
1312
1448
  const next = { ...config };
1313
1449
  for (const event of Object.keys(next)) {
1314
1450
  const entries = Array.isArray(next[event]) ? next[event] : [];
1315
1451
  const keptEntries = [];
1316
1452
  for (const entry of entries) {
1317
- const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamLiteHook(handler)) : [];
1453
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
1318
1454
  const rest = { ...entry, hooks };
1319
1455
  if (hooks.length > 0) keptEntries.push(rest);
1320
1456
  }
@@ -1324,14 +1460,14 @@ function stripYamLiteHooks(config: AnyRecord = {}) {
1324
1460
  return next;
1325
1461
  }
1326
1462
 
1327
- function withYamLiteHook(config: AnyRecord = {}) {
1328
- const next = stripYamLiteHooks(config);
1463
+ function withYamHook(config: AnyRecord = {}, profile = 'lite') {
1464
+ const next = stripYamHooks(config, profile);
1329
1465
  const event = 'UserPromptSubmit';
1330
1466
  const entry = {
1331
1467
  hooks: [
1332
1468
  {
1333
1469
  type: 'command',
1334
- command: YAM_LITE_HOOK_COMMAND,
1470
+ command: profile === 'study-note' ? YAM_STUDY_NOTE_HOOK_COMMAND : YAM_LITE_HOOK_COMMAND,
1335
1471
  timeout: 5
1336
1472
  }
1337
1473
  ]
@@ -1344,28 +1480,32 @@ async function hookStatus(args = []) {
1344
1480
  const parsed = parseHookArgs(args);
1345
1481
  const target = hookPathFor(parsed);
1346
1482
  const config = await readJsonOrDefault(target, {});
1347
- const enabled = hookConfigHasYamLite(config);
1348
- console.log(`yam-lite hook: ${enabled ? 'enabled' : 'disabled'}`);
1483
+ console.log(`yam-lite hook: ${hookConfigHasProfile(config, 'lite') ? 'enabled' : 'disabled'}`);
1484
+ console.log(`yam-study-note hook: ${hookConfigHasProfile(config, 'study-note') ? 'enabled' : 'disabled'}`);
1349
1485
  console.log(`scope: ${parsed.mode}`);
1350
1486
  console.log(`file: ${target}`);
1351
1487
  }
1352
1488
 
1353
1489
  function hookConfigHasYamLite(config = {}) {
1490
+ return hookConfigHasProfile(config, 'lite');
1491
+ }
1492
+
1493
+ function hookConfigHasProfile(config = {}, profile = 'lite') {
1354
1494
  return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
1355
- return Array.isArray(entry?.hooks) && entry.hooks.some(isYamLiteHook);
1495
+ return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
1356
1496
  }));
1357
1497
  }
1358
1498
 
1359
1499
  async function hookEnable(args = []) {
1360
1500
  const parsed = parseHookArgs(args);
1361
- if (parsed.profile !== 'lite') {
1362
- console.error('Only yam-lite hook is supported: yam hook enable lite');
1501
+ if (!['lite', 'study-note'].includes(parsed.profile)) {
1502
+ console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
1363
1503
  process.exitCode = 1;
1364
1504
  return;
1365
1505
  }
1366
1506
  const target = hookPathFor(parsed);
1367
1507
  const current = await readJsonOrDefault(target, {});
1368
- const next = withYamLiteHook(current);
1508
+ const next = withYamHook(current, parsed.profile);
1369
1509
  await fsp.mkdir(path.dirname(target), { recursive: true });
1370
1510
  if (await exists(target)) {
1371
1511
  const backup = `${target}.yam-backup-${timestampId()}`;
@@ -1373,7 +1513,7 @@ async function hookEnable(args = []) {
1373
1513
  console.log(`backup: ${backup}`);
1374
1514
  }
1375
1515
  await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
1376
- console.log(`yam-lite hook enabled (${parsed.mode}): ${target}`);
1516
+ console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
1377
1517
  console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
1378
1518
  }
1379
1519
 
@@ -1381,22 +1521,22 @@ async function hookDisable(args = []) {
1381
1521
  const parsed = parseHookArgs(args);
1382
1522
  const target = hookPathFor(parsed);
1383
1523
  const current = await readJsonOrDefault(target, {});
1384
- if (!hookConfigHasYamLite(current)) {
1385
- console.log(`yam-lite hook already disabled (${parsed.mode}): ${target}`);
1524
+ if (!hookConfigHasProfile(current, parsed.profile)) {
1525
+ console.log(`yam-${parsed.profile} hook already disabled (${parsed.mode}): ${target}`);
1386
1526
  return;
1387
1527
  }
1388
- const next = stripYamLiteHooks(current);
1528
+ const next = stripYamHooks(current, parsed.profile);
1389
1529
  if (Object.keys(next).length === 0) {
1390
1530
  await rmrf(target);
1391
1531
  } else {
1392
1532
  await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
1393
1533
  }
1394
- console.log(`yam-lite hook disabled (${parsed.mode}): ${target}`);
1534
+ console.log(`yam-${parsed.profile} hook disabled (${parsed.mode}): ${target}`);
1395
1535
  }
1396
1536
 
1397
1537
  async function hookRun(args = []) {
1398
1538
  const profile = args[0] || 'lite';
1399
- if (profile !== 'lite') {
1539
+ if (!['lite', 'study-note'].includes(profile)) {
1400
1540
  console.log(JSON.stringify({ continue: true }));
1401
1541
  return;
1402
1542
  }
@@ -1404,7 +1544,9 @@ async function hookRun(args = []) {
1404
1544
  const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
1405
1545
  const cwd = String(input?.cwd || process.cwd());
1406
1546
  const prompt = extractPrompt(input);
1407
- const additionalContext = await buildYamLiteContext({ cwd, prompt });
1547
+ const additionalContext = profile === 'study-note'
1548
+ ? await buildStudyNoteHookContext({ cwd })
1549
+ : await buildYamLiteContext({ cwd, prompt });
1408
1550
  const output = {
1409
1551
  continue: true,
1410
1552
  hookSpecificOutput: {
@@ -1458,6 +1600,27 @@ async function buildYamLiteContext({ cwd, prompt }) {
1458
1600
  return lines.join('\n');
1459
1601
  }
1460
1602
 
1603
+ async function buildStudyNoteHookContext({ cwd }) {
1604
+ const dir = path.resolve(cwd || process.cwd());
1605
+ const changedFiles = await gitChangedFiles(dir);
1606
+ const lines = [
1607
+ 'yam Study Note guard active: if this turn changes code, config, release metadata, docs, or project artifacts, include a Study Note in the final response.'
1608
+ ];
1609
+ if (!changedFiles.length) {
1610
+ lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
1611
+ return lines.join('\n');
1612
+ }
1613
+ lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
1614
+ lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
1615
+ const hygiene = studyNoteHygieneRequirements(changedFiles);
1616
+ if (hygiene.length) {
1617
+ lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
1618
+ lines.push('Report whether the change avoided dumping unrelated logic into page.tsx, one-off CSS into global.css, or structured product data into broad DB jsonb.');
1619
+ }
1620
+ lines.push('This hook is advisory; it does not generate or edit the Study Note.');
1621
+ return lines.join('\n');
1622
+ }
1623
+
1461
1624
  function yamLiteRouteHint(prompt = '') {
1462
1625
  const text = String(prompt || '').toLowerCase();
1463
1626
  if (!text) return '';
@@ -2022,13 +2185,13 @@ function loopUsage() {
2022
2185
  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.
2023
2186
 
2024
2187
  Usage:
2025
- yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--evidence-level none|fixture|smoke|local|real] [--evidence-stamp text] [--source-digest text] [--covered-requirement text] [--uncovered-requirement text] [--blocked text] [--blocked-kind text] [--failure-cause text] [--safe-retry text] [--recovery-hint text] [--fix-first-item text] [--remaining-task text] [--recommended-direction text] [--implementation-note text] [--why-this-next text] [--blocked-by text] [--owner-route route] [--owner-scope text] [--scope-owner text] [--side-effect text] [--avoidance-note 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]
2188
+ yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--evidence-level none|fixture|smoke|local|real] [--evidence-stamp text] [--source-digest text] [--touched-file file] [--read-file file] [--verified-file file] [--skipped-check text] [--stop-condition text] [--resume-hint text] [--readiness-state usable|degraded|blocked|unknown] [--covered-requirement text] [--uncovered-requirement text] [--blocked text] [--blocked-kind text] [--failure-cause text] [--safe-retry text] [--recovery-hint text] [--fix-first-item text] [--remaining-task text] [--recommended-direction text] [--implementation-note text] [--why-this-next text] [--blocked-by text] [--owner-route route] [--owner-scope text] [--scope-owner text] [--side-effect text] [--avoidance-note 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]
2026
2189
  `);
2027
2190
  }
2028
2191
 
2029
2192
  async function loopReport(args = []) {
2030
2193
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
2031
- const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'stage-convention', 'evidence', 'evidence-level', 'evidence-stamp', 'source-digest', 'covered-requirement', 'uncovered-requirement', 'blocked', 'blocker', 'blocked-kind', 'failure-cause', 'next-action', 'safe-retry', 'recovery-hint', 'fix-first-item', 'remaining-task', 'recommended-direction', 'direction', 'implementation-note', 'why-this-next', 'blocked-by', 'owner-route', 'owner-scope', 'scope-owner', 'owner', 'scope', 'side-effect', 'avoidance-note', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
2194
+ const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'stage-convention', 'evidence', 'evidence-level', 'evidence-stamp', 'source-digest', 'touched-file', 'read-file', 'verified-file', 'skipped-check', 'stop-condition', 'resume-hint', 'readiness-state', 'covered-requirement', 'uncovered-requirement', 'blocked', 'blocker', 'blocked-kind', 'failure-cause', 'next-action', 'safe-retry', 'recovery-hint', 'fix-first-item', 'remaining-task', 'recommended-direction', 'direction', 'implementation-note', 'why-this-next', 'blocked-by', 'owner-route', 'owner-scope', 'scope-owner', 'owner', 'scope', 'side-effect', 'avoidance-note', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
2032
2195
  const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
2033
2196
  const report = buildLoopReport({
2034
2197
  route: normalizeRoute(flags.route) || String(flags.route || ''),
@@ -2040,6 +2203,13 @@ async function loopReport(args = []) {
2040
2203
  evidence_level: normalizeLoopEvidenceLevelFlag(flags.evidence_level),
2041
2204
  evidence_stamp: String(flags.evidence_stamp || ''),
2042
2205
  source_digest: String(flags.source_digest || ''),
2206
+ touched_files: arrayFlag(flags.touched_file),
2207
+ read_files: arrayFlag(flags.read_file),
2208
+ verified_files: arrayFlag(flags.verified_file),
2209
+ skipped_checks: arrayFlag(flags.skipped_check),
2210
+ stop_condition: String(flags.stop_condition || ''),
2211
+ resume_hint: String(flags.resume_hint || ''),
2212
+ readiness_state: normalizeReadinessStateFlag(flags.readiness_state),
2043
2213
  covered_requirements: arrayFlag(flags.covered_requirement),
2044
2214
  uncovered_requirements: arrayFlag(flags.uncovered_requirement),
2045
2215
  blockers,
@@ -2094,7 +2264,7 @@ Usage:
2094
2264
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
2095
2265
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
2096
2266
  yam ueye preflight [dir] [--json]
2097
- 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]
2267
+ 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] [--acceptance-criterion text] [--touched-file file] [--read-file file] [--verified-file file] [--skipped-check text] [--residual-risk text] [--stop-condition text] [--resume-hint text] [--deep-visual-check text] [--design-system-evidence text] [--implementation-evidence text] [--state-check default:pass] [--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]
2098
2268
 
2099
2269
  Notes:
2100
2270
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -2331,7 +2501,7 @@ async function ueyeCompare(args = []) {
2331
2501
 
2332
2502
  async function ueyeReport(args = []) {
2333
2503
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
2334
- 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']));
2504
+ 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', 'acceptance-criterion', 'touched-file', 'read-file', 'verified-file', 'skipped-check', 'residual-risk', 'stop-condition', 'resume-hint', 'deep-visual-check', 'design-system-evidence', 'implementation-evidence', 'state-check', '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']));
2335
2505
  const reference = String(flags.reference || flags.ref || '');
2336
2506
  const actual = String(flags.actual || flags.screenshot || '');
2337
2507
  const referenceSources = [];
@@ -2436,6 +2606,20 @@ async function ueyeReport(args = []) {
2436
2606
  direction_locked: Boolean(flags.direction_locked),
2437
2607
  reference_read: Boolean(flags.reference_read)
2438
2608
  },
2609
+ deep_visual_review: {
2610
+ acceptance_criteria: arrayFlag(flags.acceptance_criterion),
2611
+ touched_files: arrayFlag(flags.touched_file),
2612
+ read_files: arrayFlag(flags.read_file),
2613
+ verified_files: arrayFlag(flags.verified_file),
2614
+ skipped_checks: arrayFlag(flags.skipped_check),
2615
+ residual_risks: arrayFlag(flags.residual_risk),
2616
+ stop_condition: String(flags.stop_condition || ''),
2617
+ resume_hint: String(flags.resume_hint || ''),
2618
+ deep_visual_checks: arrayFlag(flags.deep_visual_check),
2619
+ design_system_evidence: arrayFlag(flags.design_system_evidence),
2620
+ implementation_evidence: arrayFlag(flags.implementation_evidence),
2621
+ state_check: arrayFlag(flags.state_check)
2622
+ },
2439
2623
  blocked_reason: blockedReason,
2440
2624
  next_action: String(flags.next_visual_action || flags.next_action || '')
2441
2625
  });
@@ -4193,6 +4377,7 @@ async function main() {
4193
4377
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
4194
4378
  if (command === 'tools') return tools(process.argv.slice(3));
4195
4379
  if (command === 'proof') return proof(process.argv.slice(3));
4380
+ if (command === 'study-note') return studyNote(process.argv.slice(3));
4196
4381
  if (command === 'loop') return loop(process.argv.slice(3));
4197
4382
  if (command === 'ueye') return ueye(process.argv.slice(3));
4198
4383
  if (command === 'media') return media(process.argv.slice(3));