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/AGENTS.md +7 -5
- package/CHANGELOG.md +26 -0
- package/COMMANDS.md +10 -4
- package/DECISIONS.md +3 -0
- package/README.md +17 -9
- package/dist/bin/yam.js +211 -30
- package/dist/lib/trust-kernel.d.ts +31 -1
- package/dist/lib/trust-kernel.js +128 -6
- package/package.json +2 -1
- package/references/final-report.md +17 -11
- package/references/honest-completion.md +13 -0
- package/references/study-note.md +69 -0
- package/references/tool-trust-layer.md +6 -1
- package/references/ueye-proof.md +42 -1
- package/references/ueye.md +29 -3
- package/references/verification-levels.md +130 -25
- package/skills/deep/SKILL.md +10 -2
- package/skills/mission/SKILL.md +10 -1
- package/skills/quick/SKILL.md +6 -2
- package/skills/ueye/SKILL.md +5 -1
- package/src/bin/yam.ts +217 -31
- package/src/lib/trust-kernel.ts +155 -7
- package/templates/yam.project.md +5 -1
- package/yam.manifest.json +6 -3
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
|
|
@@ -346,6 +348,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
346
348
|
'token-economy.md',
|
|
347
349
|
'context-reuse.md',
|
|
348
350
|
'markdown-management.md',
|
|
351
|
+
'study-note.md',
|
|
349
352
|
'final-report.md',
|
|
350
353
|
'token-budget-reporter.md',
|
|
351
354
|
'memory.md'
|
|
@@ -531,6 +534,12 @@ function normalizeLoopEvidenceLevelFlag(value = ''): LoopEvidenceLevel {
|
|
|
531
534
|
return 'none';
|
|
532
535
|
}
|
|
533
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
|
+
|
|
534
543
|
function uniqueNextActionDetails(actions = []) {
|
|
535
544
|
const seen = new Set<string>();
|
|
536
545
|
const unique = [];
|
|
@@ -707,6 +716,126 @@ async function cleanup(args = []) {
|
|
|
707
716
|
return cleanupUsage();
|
|
708
717
|
}
|
|
709
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
|
+
|
|
710
839
|
function cleanupUsage() {
|
|
711
840
|
console.log(`yam cleanup
|
|
712
841
|
|
|
@@ -1269,12 +1398,12 @@ function hookUsage() {
|
|
|
1269
1398
|
|
|
1270
1399
|
Usage:
|
|
1271
1400
|
yam hook status [--global|--project dir]
|
|
1272
|
-
yam hook enable lite [--global|--project dir]
|
|
1273
|
-
yam hook disable [lite] [--global|--project dir]
|
|
1274
|
-
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>
|
|
1275
1404
|
|
|
1276
1405
|
Notes:
|
|
1277
|
-
|
|
1406
|
+
hooks are opt-in and advisory-only. They do not run tmux, subagents, or proof gates.
|
|
1278
1407
|
`);
|
|
1279
1408
|
}
|
|
1280
1409
|
|
|
@@ -1282,8 +1411,8 @@ function parseHookArgs(args = []) {
|
|
|
1282
1411
|
const result = { mode: 'project', projectDir: process.cwd(), profile: 'lite' };
|
|
1283
1412
|
for (let index = 0; index < args.length; index += 1) {
|
|
1284
1413
|
const arg = args[index];
|
|
1285
|
-
if (arg === 'lite') {
|
|
1286
|
-
result.profile =
|
|
1414
|
+
if (arg === 'lite' || arg === 'study-note') {
|
|
1415
|
+
result.profile = arg;
|
|
1287
1416
|
} else if (arg === '--global') {
|
|
1288
1417
|
result.mode = 'global';
|
|
1289
1418
|
} else if (arg === '--project') {
|
|
@@ -1307,13 +1436,21 @@ function isYamLiteHook(handler: AnyRecord = {}) {
|
|
|
1307
1436
|
return handler?.type === 'command' && String(handler.command || '').includes('yam.js hook run lite');
|
|
1308
1437
|
}
|
|
1309
1438
|
|
|
1310
|
-
function
|
|
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') {
|
|
1311
1448
|
const next = { ...config };
|
|
1312
1449
|
for (const event of Object.keys(next)) {
|
|
1313
1450
|
const entries = Array.isArray(next[event]) ? next[event] : [];
|
|
1314
1451
|
const keptEntries = [];
|
|
1315
1452
|
for (const entry of entries) {
|
|
1316
|
-
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !
|
|
1453
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks.filter((handler) => !isYamHookProfile(handler, profile)) : [];
|
|
1317
1454
|
const rest = { ...entry, hooks };
|
|
1318
1455
|
if (hooks.length > 0) keptEntries.push(rest);
|
|
1319
1456
|
}
|
|
@@ -1323,14 +1460,14 @@ function stripYamLiteHooks(config: AnyRecord = {}) {
|
|
|
1323
1460
|
return next;
|
|
1324
1461
|
}
|
|
1325
1462
|
|
|
1326
|
-
function
|
|
1327
|
-
const next =
|
|
1463
|
+
function withYamHook(config: AnyRecord = {}, profile = 'lite') {
|
|
1464
|
+
const next = stripYamHooks(config, profile);
|
|
1328
1465
|
const event = 'UserPromptSubmit';
|
|
1329
1466
|
const entry = {
|
|
1330
1467
|
hooks: [
|
|
1331
1468
|
{
|
|
1332
1469
|
type: 'command',
|
|
1333
|
-
command: YAM_LITE_HOOK_COMMAND,
|
|
1470
|
+
command: profile === 'study-note' ? YAM_STUDY_NOTE_HOOK_COMMAND : YAM_LITE_HOOK_COMMAND,
|
|
1334
1471
|
timeout: 5
|
|
1335
1472
|
}
|
|
1336
1473
|
]
|
|
@@ -1343,28 +1480,32 @@ async function hookStatus(args = []) {
|
|
|
1343
1480
|
const parsed = parseHookArgs(args);
|
|
1344
1481
|
const target = hookPathFor(parsed);
|
|
1345
1482
|
const config = await readJsonOrDefault(target, {});
|
|
1346
|
-
|
|
1347
|
-
console.log(`yam-
|
|
1483
|
+
console.log(`yam-lite hook: ${hookConfigHasProfile(config, 'lite') ? 'enabled' : 'disabled'}`);
|
|
1484
|
+
console.log(`yam-study-note hook: ${hookConfigHasProfile(config, 'study-note') ? 'enabled' : 'disabled'}`);
|
|
1348
1485
|
console.log(`scope: ${parsed.mode}`);
|
|
1349
1486
|
console.log(`file: ${target}`);
|
|
1350
1487
|
}
|
|
1351
1488
|
|
|
1352
1489
|
function hookConfigHasYamLite(config = {}) {
|
|
1490
|
+
return hookConfigHasProfile(config, 'lite');
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function hookConfigHasProfile(config = {}, profile = 'lite') {
|
|
1353
1494
|
return Object.values(config).some((entries) => Array.isArray(entries) && entries.some((entry) => {
|
|
1354
|
-
return Array.isArray(entry?.hooks) && entry.hooks.some(
|
|
1495
|
+
return Array.isArray(entry?.hooks) && entry.hooks.some((handler) => isYamHookProfile(handler, profile));
|
|
1355
1496
|
}));
|
|
1356
1497
|
}
|
|
1357
1498
|
|
|
1358
1499
|
async function hookEnable(args = []) {
|
|
1359
1500
|
const parsed = parseHookArgs(args);
|
|
1360
|
-
if (parsed.profile
|
|
1361
|
-
console.error('Only
|
|
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>');
|
|
1362
1503
|
process.exitCode = 1;
|
|
1363
1504
|
return;
|
|
1364
1505
|
}
|
|
1365
1506
|
const target = hookPathFor(parsed);
|
|
1366
1507
|
const current = await readJsonOrDefault(target, {});
|
|
1367
|
-
const next =
|
|
1508
|
+
const next = withYamHook(current, parsed.profile);
|
|
1368
1509
|
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
1369
1510
|
if (await exists(target)) {
|
|
1370
1511
|
const backup = `${target}.yam-backup-${timestampId()}`;
|
|
@@ -1372,7 +1513,7 @@ async function hookEnable(args = []) {
|
|
|
1372
1513
|
console.log(`backup: ${backup}`);
|
|
1373
1514
|
}
|
|
1374
1515
|
await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
|
|
1375
|
-
console.log(`yam
|
|
1516
|
+
console.log(`yam-${parsed.profile} hook enabled (${parsed.mode}): ${target}`);
|
|
1376
1517
|
console.log('Restart Codex or start a new thread if the app does not pick up hook changes immediately.');
|
|
1377
1518
|
}
|
|
1378
1519
|
|
|
@@ -1380,22 +1521,22 @@ async function hookDisable(args = []) {
|
|
|
1380
1521
|
const parsed = parseHookArgs(args);
|
|
1381
1522
|
const target = hookPathFor(parsed);
|
|
1382
1523
|
const current = await readJsonOrDefault(target, {});
|
|
1383
|
-
if (!
|
|
1384
|
-
console.log(`yam
|
|
1524
|
+
if (!hookConfigHasProfile(current, parsed.profile)) {
|
|
1525
|
+
console.log(`yam-${parsed.profile} hook already disabled (${parsed.mode}): ${target}`);
|
|
1385
1526
|
return;
|
|
1386
1527
|
}
|
|
1387
|
-
const next =
|
|
1528
|
+
const next = stripYamHooks(current, parsed.profile);
|
|
1388
1529
|
if (Object.keys(next).length === 0) {
|
|
1389
1530
|
await rmrf(target);
|
|
1390
1531
|
} else {
|
|
1391
1532
|
await fsp.writeFile(target, `${JSON.stringify(next, null, 2)}\n`);
|
|
1392
1533
|
}
|
|
1393
|
-
console.log(`yam
|
|
1534
|
+
console.log(`yam-${parsed.profile} hook disabled (${parsed.mode}): ${target}`);
|
|
1394
1535
|
}
|
|
1395
1536
|
|
|
1396
1537
|
async function hookRun(args = []) {
|
|
1397
1538
|
const profile = args[0] || 'lite';
|
|
1398
|
-
if (
|
|
1539
|
+
if (!['lite', 'study-note'].includes(profile)) {
|
|
1399
1540
|
console.log(JSON.stringify({ continue: true }));
|
|
1400
1541
|
return;
|
|
1401
1542
|
}
|
|
@@ -1403,7 +1544,9 @@ async function hookRun(args = []) {
|
|
|
1403
1544
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
1404
1545
|
const cwd = String(input?.cwd || process.cwd());
|
|
1405
1546
|
const prompt = extractPrompt(input);
|
|
1406
|
-
const additionalContext =
|
|
1547
|
+
const additionalContext = profile === 'study-note'
|
|
1548
|
+
? await buildStudyNoteHookContext({ cwd })
|
|
1549
|
+
: await buildYamLiteContext({ cwd, prompt });
|
|
1407
1550
|
const output = {
|
|
1408
1551
|
continue: true,
|
|
1409
1552
|
hookSpecificOutput: {
|
|
@@ -1457,6 +1600,27 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
1457
1600
|
return lines.join('\n');
|
|
1458
1601
|
}
|
|
1459
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
|
+
|
|
1460
1624
|
function yamLiteRouteHint(prompt = '') {
|
|
1461
1625
|
const text = String(prompt || '').toLowerCase();
|
|
1462
1626
|
if (!text) return '';
|
|
@@ -2021,13 +2185,13 @@ function loopUsage() {
|
|
|
2021
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.
|
|
2022
2186
|
|
|
2023
2187
|
Usage:
|
|
2024
|
-
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]
|
|
2025
2189
|
`);
|
|
2026
2190
|
}
|
|
2027
2191
|
|
|
2028
2192
|
async function loopReport(args = []) {
|
|
2029
2193
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
|
|
2030
|
-
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']));
|
|
2031
2195
|
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
2032
2196
|
const report = buildLoopReport({
|
|
2033
2197
|
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
@@ -2039,6 +2203,13 @@ async function loopReport(args = []) {
|
|
|
2039
2203
|
evidence_level: normalizeLoopEvidenceLevelFlag(flags.evidence_level),
|
|
2040
2204
|
evidence_stamp: String(flags.evidence_stamp || ''),
|
|
2041
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),
|
|
2042
2213
|
covered_requirements: arrayFlag(flags.covered_requirement),
|
|
2043
2214
|
uncovered_requirements: arrayFlag(flags.uncovered_requirement),
|
|
2044
2215
|
blockers,
|
|
@@ -2093,7 +2264,7 @@ Usage:
|
|
|
2093
2264
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
2094
2265
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
2095
2266
|
yam ueye preflight [dir] [--json]
|
|
2096
|
-
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]
|
|
2097
2268
|
|
|
2098
2269
|
Notes:
|
|
2099
2270
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
@@ -2330,7 +2501,7 @@ async function ueyeCompare(args = []) {
|
|
|
2330
2501
|
|
|
2331
2502
|
async function ueyeReport(args = []) {
|
|
2332
2503
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
2333
|
-
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']));
|
|
2334
2505
|
const reference = String(flags.reference || flags.ref || '');
|
|
2335
2506
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
2336
2507
|
const referenceSources = [];
|
|
@@ -2435,6 +2606,20 @@ async function ueyeReport(args = []) {
|
|
|
2435
2606
|
direction_locked: Boolean(flags.direction_locked),
|
|
2436
2607
|
reference_read: Boolean(flags.reference_read)
|
|
2437
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
|
+
},
|
|
2438
2623
|
blocked_reason: blockedReason,
|
|
2439
2624
|
next_action: String(flags.next_visual_action || flags.next_action || '')
|
|
2440
2625
|
});
|
|
@@ -3776,7 +3961,7 @@ function printDetection(result) {
|
|
|
3776
3961
|
}
|
|
3777
3962
|
console.log('');
|
|
3778
3963
|
console.log('Smallest useful checks:');
|
|
3779
|
-
console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || '
|
|
3964
|
+
console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
|
|
3780
3965
|
console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
|
|
3781
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}`}`);
|
|
3782
3967
|
if (result.frameworkChecklist?.detected) {
|
|
@@ -4192,6 +4377,7 @@ async function main() {
|
|
|
4192
4377
|
if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
|
|
4193
4378
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
4194
4379
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
4380
|
+
if (command === 'study-note') return studyNote(process.argv.slice(3));
|
|
4195
4381
|
if (command === 'loop') return loop(process.argv.slice(3));
|
|
4196
4382
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
4197
4383
|
if (command === 'media') return media(process.argv.slice(3));
|