yam-flow 2.0.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/dist/bin/yam.js CHANGED
@@ -50,6 +50,7 @@ const PROJECT_PACK = 'yam.project.md';
50
50
  const LEGACY_PROJECT_PACK = 'timeto.project.md';
51
51
  const PACK_STALE_DAYS = 30;
52
52
  const YAM_LITE_HOOK_COMMAND = `node ${path.join(ROOT, 'dist', 'bin', 'yam.js')} hook run lite`;
53
+ const YAM_STUDY_NOTE_HOOK_COMMAND = `node ${path.join(ROOT, 'dist', 'bin', 'yam.js')} hook run study-note`;
53
54
  const REQUIRED_PACK_SECTIONS = [
54
55
  'Product Direction',
55
56
  'UI Direction',
@@ -122,6 +123,7 @@ Usage:
122
123
  yam tools doctor [dir]
123
124
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
124
125
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
126
+ yam study-note check [dir] [--report file|--text text] [--json]
125
127
  yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
126
128
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
127
129
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
@@ -134,7 +136,7 @@ Usage:
134
136
  yam release report [--json]
135
137
  yam safety [text...]
136
138
  yam memory <init|add|list|summary|resolve> [dir] [options]
137
- yam hook <status|enable|disable|run> [lite] [--global|--project dir]
139
+ yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
138
140
  yam template <project|ueye|mission|proof|tuning>
139
141
  yam tune-log [dir]
140
142
  yam install
@@ -318,6 +320,7 @@ async function verify({ quiet = false } = {}) {
318
320
  'token-economy.md',
319
321
  'context-reuse.md',
320
322
  'markdown-management.md',
323
+ 'study-note.md',
321
324
  'final-report.md',
322
325
  'token-budget-reporter.md',
323
326
  'memory.md'
@@ -530,6 +533,12 @@ function normalizeLoopEvidenceLevelFlag(value = '') {
530
533
  return text;
531
534
  return 'none';
532
535
  }
536
+ function normalizeReadinessStateFlag(value = '') {
537
+ const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
538
+ if (['usable', 'degraded', 'blocked', 'unknown'].includes(text))
539
+ return text;
540
+ return 'unknown';
541
+ }
533
542
  function uniqueNextActionDetails(actions = []) {
534
543
  const seen = new Set();
535
544
  const unique = [];
@@ -707,6 +716,124 @@ async function cleanup(args = []) {
707
716
  console.error(`unknown cleanup command: ${subcommand}`);
708
717
  return cleanupUsage();
709
718
  }
719
+ async function studyNote(args = []) {
720
+ const subcommand = args[0] || 'help';
721
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
722
+ return studyNoteUsage();
723
+ if (subcommand === 'check')
724
+ return studyNoteCheck(args.slice(1));
725
+ console.error(`unknown study-note command: ${subcommand}`);
726
+ return studyNoteUsage();
727
+ }
728
+ function studyNoteUsage() {
729
+ console.log(`yam study-note
730
+
731
+ Usage:
732
+ yam study-note check [dir] [--report file|--text text] [--json]
733
+
734
+ Notes:
735
+ Read-only guard. It checks whether changed work has a Study Note and whether relevant UI/CSS/DB hygiene was reported.
736
+ It does not generate, edit, or infer the Study Note for you.
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)
771
+ process.exitCode = 1;
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))
778
+ index += 1;
779
+ continue;
780
+ }
781
+ if (arg === 'check')
782
+ continue;
783
+ return arg;
784
+ }
785
+ return '';
786
+ }
787
+ async function gitChangedFiles(dir) {
788
+ const output = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
789
+ return output
790
+ .split(/\r?\n/)
791
+ .filter(Boolean)
792
+ .map((line) => line.replace(/^..\s+/, '').replace(/^.* -> /, ''))
793
+ .map((line) => line.trim())
794
+ .filter((file) => file && !file.startsWith('dist/') && !file.endsWith('.tgz'));
795
+ }
796
+ async function readStudyNoteReportText(flags, dir) {
797
+ if (flags.text)
798
+ return String(flags.text || '');
799
+ if (!flags.report)
800
+ return '';
801
+ const file = path.resolve(dir, String(flags.report));
802
+ try {
803
+ return await fsp.readFile(file, 'utf8');
804
+ }
805
+ catch {
806
+ return '';
807
+ }
808
+ }
809
+ function hasStudyNoteMarker(text = '') {
810
+ return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
811
+ }
812
+ function studyNoteGuardCheck(id, status, nextAction) {
813
+ return {
814
+ id,
815
+ status,
816
+ next_action: nextAction,
817
+ truth_status: status === 'pass' ? 'verified' : status === 'fail' ? 'blocked' : status === 'skipped' ? 'skipped' : 'partial'
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
+ function requirementSatisfied(text = '', requirement) {
834
+ const normalized = String(text || '').toLowerCase();
835
+ return requirement.terms.some((term) => normalized.includes(term.toLowerCase())) ? 'pass' : 'fail';
836
+ }
710
837
  function cleanupUsage() {
711
838
  console.log(`yam cleanup
712
839
 
@@ -1270,20 +1397,20 @@ function hookUsage() {
1270
1397
 
1271
1398
  Usage:
1272
1399
  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
1400
+ yam hook enable <lite|study-note> [--global|--project dir]
1401
+ yam hook disable [lite|study-note] [--global|--project dir]
1402
+ yam hook run <lite|study-note>
1276
1403
 
1277
1404
  Notes:
1278
- yam-lite is opt-in and advisory-only. It does not run checks, tmux, subagents, or proof gates.
1405
+ hooks are opt-in and advisory-only. They do not run tmux, subagents, or proof gates.
1279
1406
  `);
1280
1407
  }
1281
1408
  function parseHookArgs(args = []) {
1282
1409
  const result = { mode: 'project', projectDir: process.cwd(), profile: 'lite' };
1283
1410
  for (let index = 0; index < args.length; index += 1) {
1284
1411
  const arg = args[index];
1285
- if (arg === 'lite') {
1286
- result.profile = 'lite';
1412
+ if (arg === 'lite' || arg === 'study-note') {
1413
+ result.profile = arg;
1287
1414
  }
1288
1415
  else if (arg === '--global') {
1289
1416
  result.mode = 'global';
@@ -1308,13 +1435,19 @@ function hookPathFor(parsed) {
1308
1435
  function isYamLiteHook(handler = {}) {
1309
1436
  return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run lite');
1310
1437
  }
1311
- function stripYamLiteHooks(config = {}) {
1438
+ function isYamStudyNoteHook(handler = {}) {
1439
+ return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run study-note');
1440
+ }
1441
+ function isYamHookProfile(handler = {}, profile = 'lite') {
1442
+ return profile === 'study-note' ? isYamStudyNoteHook(handler) : isYamLiteHook(handler);
1443
+ }
1444
+ function stripYamHooks(config = {}, profile = 'lite') {
1312
1445
  const next = { ...config };
1313
1446
  for (const event of Object.keys(next)) {
1314
1447
  const entries = Array.isArray(next[event]) ? next[event] : [];
1315
1448
  const keptEntries = [];
1316
1449
  for (const entry of entries) {
1317
- const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamLiteHook(handler)) : [];
1450
+ const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
1318
1451
  const rest = { ...entry, hooks };
1319
1452
  if (hooks.length > 0)
1320
1453
  keptEntries.push(rest);
@@ -1326,14 +1459,14 @@ function stripYamLiteHooks(config = {}) {
1326
1459
  }
1327
1460
  return next;
1328
1461
  }
1329
- function withYamLiteHook(config = {}) {
1330
- const next = stripYamLiteHooks(config);
1462
+ function withYamHook(config = {}, profile = 'lite') {
1463
+ const next = stripYamHooks(config, profile);
1331
1464
  const event = 'UserPromptSubmit';
1332
1465
  const entry = {
1333
1466
  hooks: [
1334
1467
  {
1335
1468
  type: 'command',
1336
- command: YAM_LITE_HOOK_COMMAND,
1469
+ command: profile === 'study-note' ? YAM_STUDY_NOTE_HOOK_COMMAND : YAM_LITE_HOOK_COMMAND,
1337
1470
  timeout: 5
1338
1471
  }
1339
1472
  ]
@@ -1345,26 +1478,29 @@ async function hookStatus(args = []) {
1345
1478
  const parsed = parseHookArgs(args);
1346
1479
  const target = hookPathFor(parsed);
1347
1480
  const config = await readJsonOrDefault(target, {});
1348
- const enabled = hookConfigHasYamLite(config);
1349
- console.log(`yam-lite hook: ${enabled ? 'enabled' : 'disabled'}`);
1481
+ console.log(`yam-lite hook: ${hookConfigHasProfile(config, 'lite') ? 'enabled' : 'disabled'}`);
1482
+ console.log(`yam-study-note hook: ${hookConfigHasProfile(config, 'study-note') ? 'enabled' : 'disabled'}`);
1350
1483
  console.log(`scope: ${parsed.mode}`);
1351
1484
  console.log(`file: ${target}`);
1352
1485
  }
1353
1486
  function hookConfigHasYamLite(config = {}) {
1487
+ return hookConfigHasProfile(config, 'lite');
1488
+ }
1489
+ function hookConfigHasProfile(config = {}, profile = 'lite') {
1354
1490
  return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
1355
- return Array.isArray(entry?.hooks) && entry.hooks.some(isYamLiteHook);
1491
+ return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
1356
1492
  }));
1357
1493
  }
1358
1494
  async function hookEnable(args = []) {
1359
1495
  const parsed = parseHookArgs(args);
1360
- if (parsed.profile !== 'lite') {
1361
- console.error('Only yam-lite hook is supported: yam hook enable lite');
1496
+ if (!['lite', 'study-note'].includes(parsed.profile)) {
1497
+ console.error('Only lite and study-note hooks are supported: yam hook enable <lite|study-note>');
1362
1498
  process.exitCode = 1;
1363
1499
  return;
1364
1500
  }
1365
1501
  const target = hookPathFor(parsed);
1366
1502
  const current = await readJsonOrDefault(target, {});
1367
- const next = withYamLiteHook(current);
1503
+ const next = withYamHook(current, parsed.profile);
1368
1504
  await fsp.mkdir(path.dirname(target), { recursive: true });
1369
1505
  if (await exists(target)) {
1370
1506
  const backup = `${target}.yam-backup-${timestampId()}`;
@@ -1372,29 +1508,29 @@ async function hookEnable(args = []) {
1372
1508
  console.log(`backup: ${backup}`);
1373
1509
  }
1374
1510
  await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
1375
- console.log(`yam-lite hook enabled (${parsed.mode}): ${target}`);
1511
+ console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
1376
1512
  console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
1377
1513
  }
1378
1514
  async function hookDisable(args = []) {
1379
1515
  const parsed = parseHookArgs(args);
1380
1516
  const target = hookPathFor(parsed);
1381
1517
  const current = await readJsonOrDefault(target, {});
1382
- if (!hookConfigHasYamLite(current)) {
1383
- console.log(`yam-lite hook already disabled (${parsed.mode}): ${target}`);
1518
+ if (!hookConfigHasProfile(current, parsed.profile)) {
1519
+ console.log(`yam-${parsed.profile} hook already disabled (${parsed.mode}): ${target}`);
1384
1520
  return;
1385
1521
  }
1386
- const next = stripYamLiteHooks(current);
1522
+ const next = stripYamHooks(current, parsed.profile);
1387
1523
  if (Object.keys(next).length === 0) {
1388
1524
  await rmrf(target);
1389
1525
  }
1390
1526
  else {
1391
1527
  await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
1392
1528
  }
1393
- console.log(`yam-lite hook disabled (${parsed.mode}): ${target}`);
1529
+ console.log(`yam-${parsed.profile} hook disabled (${parsed.mode}): ${target}`);
1394
1530
  }
1395
1531
  async function hookRun(args = []) {
1396
1532
  const profile = args[0] || 'lite';
1397
- if (profile !== 'lite') {
1533
+ if (!['lite', 'study-note'].includes(profile)) {
1398
1534
  console.log(JSON.stringify({ continue: true }));
1399
1535
  return;
1400
1536
  }
@@ -1402,7 +1538,9 @@ async function hookRun(args = []) {
1402
1538
  const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
1403
1539
  const cwd = String(input?.cwd || process.cwd());
1404
1540
  const prompt = extractPrompt(input);
1405
- const additionalContext = await buildYamLiteContext({ cwd, prompt });
1541
+ const additionalContext = profile === 'study-note'
1542
+ ? await buildStudyNoteHookContext({ cwd })
1543
+ : await buildYamLiteContext({ cwd, prompt });
1406
1544
  const output = {
1407
1545
  continue: true,
1408
1546
  hookSpecificOutput: {
@@ -1460,6 +1598,26 @@ async function buildYamLiteContext({ cwd, prompt }) {
1460
1598
  lines.push(routeHint);
1461
1599
  return lines.join('\n');
1462
1600
  }
1601
+ async function buildStudyNoteHookContext({ cwd }) {
1602
+ const dir = path.resolve(cwd || process.cwd());
1603
+ const changedFiles = await gitChangedFiles(dir);
1604
+ const lines = [
1605
+ '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.'
1606
+ ];
1607
+ if (!changedFiles.length) {
1608
+ lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
1609
+ return lines.join('\n');
1610
+ }
1611
+ lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
1612
+ lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
1613
+ const hygiene = studyNoteHygieneRequirements(changedFiles);
1614
+ if (hygiene.length) {
1615
+ lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
1616
+ 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.');
1617
+ }
1618
+ lines.push('This hook is advisory; it does not generate or edit the Study Note.');
1619
+ return lines.join('\n');
1620
+ }
1463
1621
  function yamLiteRouteHint(prompt = '') {
1464
1622
  const text = String(prompt || '').toLowerCase();
1465
1623
  if (!text)
@@ -2022,13 +2180,13 @@ function loopUsage() {
2022
2180
  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
2181
 
2024
2182
  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]
2183
+ 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
2184
  `);
2027
2185
  }
2028
2186
  async function loopReport(args = []) {
2029
2187
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
2030
2188
  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']));
2189
+ 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
2190
  const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
2033
2191
  const report = buildLoopReport({
2034
2192
  route: normalizeRoute(flags.route) || String(flags.route || ''),
@@ -2040,6 +2198,13 @@ async function loopReport(args = []) {
2040
2198
  evidence_level: normalizeLoopEvidenceLevelFlag(flags.evidence_level),
2041
2199
  evidence_stamp: String(flags.evidence_stamp || ''),
2042
2200
  source_digest: String(flags.source_digest || ''),
2201
+ touched_files: arrayFlag(flags.touched_file),
2202
+ read_files: arrayFlag(flags.read_file),
2203
+ verified_files: arrayFlag(flags.verified_file),
2204
+ skipped_checks: arrayFlag(flags.skipped_check),
2205
+ stop_condition: String(flags.stop_condition || ''),
2206
+ resume_hint: String(flags.resume_hint || ''),
2207
+ readiness_state: normalizeReadinessStateFlag(flags.readiness_state),
2043
2208
  covered_requirements: arrayFlag(flags.covered_requirement),
2044
2209
  uncovered_requirements: arrayFlag(flags.uncovered_requirement),
2045
2210
  blockers,
@@ -2098,7 +2263,7 @@ Usage:
2098
2263
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
2099
2264
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
2100
2265
  yam ueye preflight [dir] [--json]
2101
- 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]
2266
+ 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]
2102
2267
 
2103
2268
  Notes:
2104
2269
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -2339,7 +2504,7 @@ async function ueyeCompare(args = []) {
2339
2504
  async function ueyeReport(args = []) {
2340
2505
  if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
2341
2506
  return ueyeUsage();
2342
- 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']));
2507
+ 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']));
2343
2508
  const reference = String(flags.reference || flags.ref || '');
2344
2509
  const actual = String(flags.actual || flags.screenshot || '');
2345
2510
  const referenceSources = [];
@@ -2445,6 +2610,20 @@ async function ueyeReport(args = []) {
2445
2610
  direction_locked: Boolean(flags.direction_locked),
2446
2611
  reference_read: Boolean(flags.reference_read)
2447
2612
  },
2613
+ deep_visual_review: {
2614
+ acceptance_criteria: arrayFlag(flags.acceptance_criterion),
2615
+ touched_files: arrayFlag(flags.touched_file),
2616
+ read_files: arrayFlag(flags.read_file),
2617
+ verified_files: arrayFlag(flags.verified_file),
2618
+ skipped_checks: arrayFlag(flags.skipped_check),
2619
+ residual_risks: arrayFlag(flags.residual_risk),
2620
+ stop_condition: String(flags.stop_condition || ''),
2621
+ resume_hint: String(flags.resume_hint || ''),
2622
+ deep_visual_checks: arrayFlag(flags.deep_visual_check),
2623
+ design_system_evidence: arrayFlag(flags.design_system_evidence),
2624
+ implementation_evidence: arrayFlag(flags.implementation_evidence),
2625
+ state_check: arrayFlag(flags.state_check)
2626
+ },
2448
2627
  blocked_reason: blockedReason,
2449
2628
  next_action: String(flags.next_visual_action || flags.next_action || '')
2450
2629
  });
@@ -3782,7 +3961,7 @@ function printDetection(result) {
3782
3961
  }
3783
3962
  console.log('');
3784
3963
  console.log('Smallest useful checks:');
3785
- console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
3964
+ console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
3786
3965
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
3787
3966
  console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
3788
3967
  if (result.frameworkChecklist?.detected) {
@@ -4199,6 +4378,8 @@ async function main() {
4199
4378
  return tools(process.argv.slice(3));
4200
4379
  if (command === 'proof')
4201
4380
  return proof(process.argv.slice(3));
4381
+ if (command === 'study-note')
4382
+ return studyNote(process.argv.slice(3));
4202
4383
  if (command === 'loop')
4203
4384
  return loop(process.argv.slice(3));
4204
4385
  if (command === 'ueye')
@@ -8,6 +8,7 @@ export type RuntimeClaim = 'not_started' | 'started' | 'observed' | 'stopped' |
8
8
  export type LoopStageStatus = 'passed' | 'failed' | 'blocked' | 'skipped' | 'partial' | 'pending' | 'recorded';
9
9
  export type LoopEvidenceLevel = 'none' | 'fixture' | 'smoke' | 'local' | 'real';
10
10
  export type ToolIntent = 'read_only' | 'write' | 'destructive' | 'runtime' | 'visual' | 'publish';
11
+ export type ReadinessState = 'usable' | 'degraded' | 'blocked' | 'unknown';
11
12
  export interface SafetyHit {
12
13
  level: SafetyLevel;
13
14
  reason: string;
@@ -161,6 +162,13 @@ export interface LoopReport {
161
162
  evidence_level: LoopEvidenceLevel;
162
163
  evidence_stamp: string;
163
164
  source_digest: string;
165
+ touched_files: string[];
166
+ read_files: string[];
167
+ verified_files: string[];
168
+ skipped_checks: string[];
169
+ stop_condition: string;
170
+ resume_hint: string;
171
+ readiness_state: ReadinessState;
164
172
  covered_requirements: string[];
165
173
  uncovered_requirements: string[];
166
174
  blockers: string[];
@@ -191,14 +199,35 @@ export interface UeyeRunReport {
191
199
  implementation_sources: UeyeVisualProvenance[];
192
200
  surface_context: UeyeSurfaceContext;
193
201
  design_completion_gate: UeyeDesignCompletionGate;
202
+ deep_visual_review: UeyeDeepVisualReview;
194
203
  comparison_result: string;
195
204
  design_quality: string;
196
205
  blocked_reason: string;
197
206
  next_action: string;
198
207
  truth_status: TruthStatus;
199
208
  }
200
- export interface UeyeRunReportInput extends Partial<Omit<UeyeRunReport, 'design_completion_gate'>> {
209
+ export interface UeyeRunReportInput extends Partial<Omit<UeyeRunReport, 'design_completion_gate' | 'deep_visual_review'>> {
201
210
  design_completion_gate?: Partial<UeyeDesignCompletionGate> & Record<string, unknown>;
211
+ deep_visual_review?: Partial<UeyeDeepVisualReview> & Record<string, unknown>;
212
+ }
213
+ export interface UeyeDeepVisualReview {
214
+ schema: 'yam.ueye-deep-visual-review.v1';
215
+ acceptance_criteria: string[];
216
+ touched_files: string[];
217
+ read_files: string[];
218
+ verified_files: string[];
219
+ skipped_checks: string[];
220
+ residual_risks: string[];
221
+ stop_condition: string;
222
+ resume_hint: string;
223
+ deep_visual_checks: string[];
224
+ design_system_evidence: string[];
225
+ state_matrix: Record<string, string>;
226
+ implementation_evidence: string[];
227
+ blockers: string[];
228
+ warnings: string[];
229
+ next_action: string;
230
+ truth_status: TruthStatus;
202
231
  }
203
232
  export interface MediaGenerationProof {
204
233
  schema: 'yam.media-generation-proof.v1';
@@ -294,6 +323,7 @@ export declare function buildRuntimeBackendEvidence(input?: Partial<RuntimeBacke
294
323
  export declare function buildStudyNote(input?: Partial<StudyNote> & Record<string, unknown>): StudyNote;
295
324
  export declare function buildLoopReport(input?: Partial<LoopReport> & Record<string, unknown>): LoopReport;
296
325
  export declare function buildUeyeRunReport(input?: UeyeRunReportInput): UeyeRunReport;
326
+ export declare function buildUeyeDeepVisualReview(input?: Partial<UeyeDeepVisualReview> & Record<string, unknown>): UeyeDeepVisualReview;
297
327
  export declare function buildMediaGenerationProof(input?: Partial<MediaGenerationProof>): MediaGenerationProof;
298
328
  export declare function isTruthStatus(value?: unknown): value is TruthStatus;
299
329
  export declare function weakestTruth(...statuses: unknown[]): TruthStatus;