yam-flow 2.6.0 → 2.8.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 +1 -0
- package/CHANGELOG.md +51 -0
- package/COMMANDS.md +72 -5
- package/README.md +49 -9
- package/ROADMAP.md +20 -7
- package/dist/bin/yam.js +616 -68
- package/dist/lib/external-updates.d.ts +9 -0
- package/dist/lib/external-updates.js +232 -4
- package/dist/lib/media-provider-receipt.d.ts +79 -0
- package/dist/lib/media-provider-receipt.js +498 -0
- package/dist/lib/next-step.d.ts +61 -0
- package/dist/lib/next-step.js +340 -0
- package/dist/lib/release-auth-readiness.d.ts +81 -0
- package/dist/lib/release-auth-readiness.js +106 -0
- package/dist/lib/release-registry-status.d.ts +18 -0
- package/dist/lib/release-registry-status.js +37 -0
- package/dist/lib/scout-receipt.d.ts +89 -0
- package/dist/lib/scout-receipt.js +542 -0
- package/dist/lib/skill-installation.d.ts +30 -0
- package/dist/lib/skill-installation.js +738 -110
- package/dist/lib/trust-kernel.d.ts +2 -0
- package/dist/lib/trust-kernel.js +21 -2
- package/dist/lib/verification-closure.d.ts +97 -0
- package/dist/lib/verification-closure.js +728 -0
- package/package.json +23 -3
- package/references/final-report.md +15 -1
- package/references/next-step.md +67 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/study-note.md +4 -2
- package/references/tool-trust-layer.md +2 -0
- package/skills/deep/SKILL.md +2 -0
- package/skills/mission/SKILL.md +2 -0
- package/skills/quick/SKILL.md +1 -0
- package/skills/scout/SKILL.md +19 -9
- package/skills/ueye/SKILL.md +1 -0
- package/src/bin/yam.ts +615 -64
- package/src/lib/external-updates.ts +262 -4
- package/src/lib/media-provider-receipt.ts +548 -0
- package/src/lib/next-step.ts +397 -0
- package/src/lib/release-auth-readiness.ts +148 -0
- package/src/lib/release-registry-status.ts +55 -0
- package/src/lib/scout-receipt.ts +593 -0
- package/src/lib/skill-installation.ts +930 -108
- package/src/lib/trust-kernel.ts +25 -2
- package/src/lib/verification-closure.ts +738 -0
- package/yam.manifest.json +4 -3
package/src/bin/yam.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import {
|
|
51
51
|
inspectSkillInstallation,
|
|
52
52
|
installSkillSetTransactional,
|
|
53
|
+
planSkillSetInstallation,
|
|
53
54
|
uninstallSkillSetSafely
|
|
54
55
|
} from '../lib/skill-installation.js';
|
|
55
56
|
import {
|
|
@@ -66,6 +67,27 @@ import {
|
|
|
66
67
|
buildStrictGateResult,
|
|
67
68
|
verifyStrictGateResult
|
|
68
69
|
} from '../lib/gate-result.js';
|
|
70
|
+
import {
|
|
71
|
+
createVerificationClosureReceipt,
|
|
72
|
+
verifyVerificationClosureReceiptFile
|
|
73
|
+
} from '../lib/verification-closure.js';
|
|
74
|
+
import {
|
|
75
|
+
createMediaProviderReceipt,
|
|
76
|
+
verifyMediaProviderReceipt
|
|
77
|
+
} from '../lib/media-provider-receipt.js';
|
|
78
|
+
import {
|
|
79
|
+
createScoutReceipt,
|
|
80
|
+
verifyScoutReceiptFile
|
|
81
|
+
} from '../lib/scout-receipt.js';
|
|
82
|
+
import {
|
|
83
|
+
buildNextStep,
|
|
84
|
+
verifyNextStep
|
|
85
|
+
} from '../lib/next-step.js';
|
|
86
|
+
import { releaseRegistryStatusFromChecks } from '../lib/release-registry-status.js';
|
|
87
|
+
import {
|
|
88
|
+
buildReleaseAuthReadiness,
|
|
89
|
+
inspectTrustedPublisherWorkflowText
|
|
90
|
+
} from '../lib/release-auth-readiness.js';
|
|
69
91
|
import {
|
|
70
92
|
BoundedInputError,
|
|
71
93
|
GENERAL_STDIN_MAX_BYTES,
|
|
@@ -202,7 +224,9 @@ Usage:
|
|
|
202
224
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
203
225
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
204
226
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
227
|
+
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
205
228
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
229
|
+
yam scout receipt <create|verify> [options]
|
|
206
230
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
207
231
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
208
232
|
yam ueye preflight [dir] [--json]
|
|
@@ -215,18 +239,19 @@ Usage:
|
|
|
215
239
|
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
216
240
|
yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
|
|
217
241
|
yam mission gate [--expected-thread id] [--receipt file] [--json]
|
|
242
|
+
yam verification closure <create|verify> [options]
|
|
218
243
|
yam update check [--json]
|
|
219
244
|
yam update apply --component yam|scrapling|insane-search [--json]
|
|
220
245
|
yam update apply --all [--json]
|
|
221
246
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
222
247
|
yam benchmark report --candidate-digest sha256 --baseline-digest sha256 --sample seed:candidate:baseline --unit score --evidence-source text [--min-samples n] [--min-mean-delta n] [--min-win-rate n] [--json]
|
|
223
|
-
yam release report [--json]
|
|
248
|
+
yam release report [--auth-mode auto|oidc|token] [--json]
|
|
224
249
|
yam safety [text...]
|
|
225
250
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
226
251
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
227
252
|
yam template <project|ueye|mission|proof|tuning>
|
|
228
253
|
yam tune-log [dir]
|
|
229
|
-
yam install [--replace-user-skill name]
|
|
254
|
+
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
230
255
|
yam uninstall
|
|
231
256
|
yam doctor [--json]
|
|
232
257
|
yam examples
|
|
@@ -252,10 +277,20 @@ async function rmrf(target) {
|
|
|
252
277
|
await fsp.rm(target, { recursive: true, force: true });
|
|
253
278
|
}
|
|
254
279
|
|
|
255
|
-
function
|
|
280
|
+
function installOptions(args: string[] = []) {
|
|
256
281
|
const skills: string[] = [];
|
|
282
|
+
let dryRun = false;
|
|
283
|
+
let json = false;
|
|
257
284
|
for (let index = 0; index < args.length; index += 1) {
|
|
258
285
|
const arg = String(args[index] || '');
|
|
286
|
+
if (arg === '--dry-run') {
|
|
287
|
+
dryRun = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (arg === '--json') {
|
|
291
|
+
json = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
259
294
|
if (arg === '--replace-user-skill') {
|
|
260
295
|
const skill = String(args[index + 1] || '');
|
|
261
296
|
if (!skill || skill.startsWith('--')) {
|
|
@@ -273,21 +308,31 @@ function installReplacementSkills(args: string[] = []) {
|
|
|
273
308
|
}
|
|
274
309
|
throw new Error(`unknown yam install argument: ${arg}`);
|
|
275
310
|
}
|
|
276
|
-
|
|
311
|
+
if (json && !dryRun) throw new Error('yam install --json currently requires --dry-run');
|
|
312
|
+
return { replaceSkills: [...new Set(skills)], dryRun, json };
|
|
277
313
|
}
|
|
278
314
|
|
|
279
315
|
async function install(args: string[] = []) {
|
|
280
|
-
const
|
|
281
|
-
const
|
|
316
|
+
const options = installOptions(args);
|
|
317
|
+
const installInput = {
|
|
282
318
|
sourceRoot: ROOT,
|
|
283
319
|
destination: DEST,
|
|
284
320
|
codexMirror: CODEX_MIRROR,
|
|
285
321
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
286
322
|
version: VERSION,
|
|
287
323
|
skills: SKILLS,
|
|
288
|
-
replaceSkills,
|
|
324
|
+
replaceSkills: options.replaceSkills,
|
|
289
325
|
legacySkills: LEGACY_SKILLS,
|
|
290
326
|
retiredSkills: RETIRED_SKILLS
|
|
327
|
+
};
|
|
328
|
+
if (options.dryRun) {
|
|
329
|
+
const plan = await planSkillSetInstallation(installInput);
|
|
330
|
+
printJsonOrHuman(plan, options.json, 'yam install dry run');
|
|
331
|
+
if (!plan.ready) process.exitCode = 1;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const result = await installSkillSetTransactional({
|
|
335
|
+
...installInput
|
|
291
336
|
});
|
|
292
337
|
console.log(`yam installed to ${DEST}`);
|
|
293
338
|
console.log(`install receipt: ${result.receiptPath}`);
|
|
@@ -365,6 +410,92 @@ async function readJson(file): Promise<AnyRecord> {
|
|
|
365
410
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
366
411
|
}
|
|
367
412
|
|
|
413
|
+
async function readBoundedRegularJson(file, label = 'JSON input', maxBytes = GENERAL_STDIN_MAX_BYTES): Promise<AnyRecord> {
|
|
414
|
+
const requested = path.resolve(expandHome(String(file || '')));
|
|
415
|
+
const physicalParent = await fsp.realpath(path.dirname(requested));
|
|
416
|
+
const target = path.join(physicalParent, path.basename(requested));
|
|
417
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
418
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
419
|
+
? fs.constants.O_NOFOLLOW
|
|
420
|
+
: 0;
|
|
421
|
+
const before = await fsp.lstat(target);
|
|
422
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
423
|
+
throw new Error(`${label} must be a regular JSON file no larger than ${maxBytes} bytes`);
|
|
424
|
+
}
|
|
425
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
426
|
+
try {
|
|
427
|
+
const opened = await handle.stat();
|
|
428
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
429
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > maxBytes) {
|
|
430
|
+
throw new Error(`${label} must be a stable regular JSON file no larger than ${maxBytes} bytes`);
|
|
431
|
+
}
|
|
432
|
+
const bytes = await readBoundedFileHandle(handle, maxBytes);
|
|
433
|
+
const afterOpened = await handle.stat();
|
|
434
|
+
const after = await fsp.lstat(target);
|
|
435
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
436
|
+
if (
|
|
437
|
+
bytes.length > maxBytes
|
|
438
|
+
|| after.isSymbolicLink()
|
|
439
|
+
|| !after.isFile()
|
|
440
|
+
|| after.dev !== opened.dev
|
|
441
|
+
|| after.ino !== opened.ino
|
|
442
|
+
|| afterOpened.dev !== opened.dev
|
|
443
|
+
|| afterOpened.ino !== opened.ino
|
|
444
|
+
|| afterOpened.size !== bytes.length
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(`${label} changed identity or size while being read`);
|
|
447
|
+
}
|
|
448
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
449
|
+
} finally {
|
|
450
|
+
await handle.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
type CliPathIdentity = { path: string; dev: number; ino: number };
|
|
455
|
+
|
|
456
|
+
async function captureAbsoluteRegularDirectoryPath(target: string, label: string) {
|
|
457
|
+
const absolute = path.resolve(target);
|
|
458
|
+
const filesystemRoot = path.parse(absolute).root;
|
|
459
|
+
const relative = path.relative(filesystemRoot, absolute);
|
|
460
|
+
const identities: CliPathIdentity[] = [];
|
|
461
|
+
let current = filesystemRoot;
|
|
462
|
+
for (const segment of relative ? ['', ...relative.split(path.sep)] : ['']) {
|
|
463
|
+
if (segment) current = path.join(current, segment);
|
|
464
|
+
const stat = await fsp.lstat(current);
|
|
465
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
466
|
+
throw new Error(`${label} parent must be a regular physical directory: ${current}`);
|
|
467
|
+
}
|
|
468
|
+
identities.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
469
|
+
}
|
|
470
|
+
return identities;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function revalidateAbsoluteRegularDirectoryPath(identities: CliPathIdentity[], label: string) {
|
|
474
|
+
for (const identity of identities) {
|
|
475
|
+
const stat = await fsp.lstat(identity.path);
|
|
476
|
+
if (
|
|
477
|
+
stat.isSymbolicLink()
|
|
478
|
+
|| !stat.isDirectory()
|
|
479
|
+
|| stat.dev !== identity.dev
|
|
480
|
+
|| stat.ino !== identity.ino
|
|
481
|
+
) {
|
|
482
|
+
throw new Error(`${label} parent changed identity while being read: ${identity.path}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function readBoundedFileHandle(handle: fsp.FileHandle, maxBytes: number) {
|
|
488
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
489
|
+
let total = 0;
|
|
490
|
+
while (total < buffer.length) {
|
|
491
|
+
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
|
|
492
|
+
if (bytesRead === 0) break;
|
|
493
|
+
total += bytesRead;
|
|
494
|
+
}
|
|
495
|
+
if (total > maxBytes) throw new Error(`JSON input exceeds ${maxBytes} bytes while being read`);
|
|
496
|
+
return buffer.subarray(0, total);
|
|
497
|
+
}
|
|
498
|
+
|
|
368
499
|
async function readJsonOrDefault(file, fallback: AnyRecord = {}): Promise<AnyRecord> {
|
|
369
500
|
try {
|
|
370
501
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
@@ -836,8 +967,9 @@ Usage:
|
|
|
836
967
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
837
968
|
|
|
838
969
|
Notes:
|
|
839
|
-
Read-only guard. It checks whether changed work has a Study Note
|
|
840
|
-
|
|
970
|
+
Read-only guard. It checks whether changed work has a complete Study Note followed immediately by Next step,
|
|
971
|
+
including relevant UI/CSS/DB hygiene and the ordered fix-first/planned handoff contract.
|
|
972
|
+
It does not generate, edit, or infer either section for you.
|
|
841
973
|
`);
|
|
842
974
|
}
|
|
843
975
|
|
|
@@ -858,6 +990,7 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
858
990
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
859
991
|
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
860
992
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
993
|
+
const studyNoteText = studyNoteSectionText(reportText);
|
|
861
994
|
const checks = [
|
|
862
995
|
studyNoteGuardCheck(
|
|
863
996
|
'changed_files',
|
|
@@ -867,16 +1000,24 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
867
1000
|
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'
|
|
868
1001
|
),
|
|
869
1002
|
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
870
|
-
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(
|
|
871
|
-
studyNoteGuardCheck('execution_point', scopeAwareStatus(/(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(
|
|
872
|
-
studyNoteGuardCheck('before_after_or_change', scopeAwareStatus(/(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(
|
|
873
|
-
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(
|
|
874
|
-
studyNoteGuardCheck('syntax_or_structure', scopeAwareStatus(/(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(
|
|
875
|
-
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(
|
|
876
|
-
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(
|
|
1003
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(studyNoteText)), 'Study Note should explain what the touched code/artifact does'),
|
|
1004
|
+
studyNoteGuardCheck('execution_point', scopeAwareStatus(/(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(studyNoteText)), 'Study Note should explain where or when the touched code/artifact runs or is read'),
|
|
1005
|
+
studyNoteGuardCheck('before_after_or_change', scopeAwareStatus(/(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(studyNoteText)), 'Study Note should explain what changed from before to after'),
|
|
1006
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(studyNoteText)), 'Study Note should describe the expected behavior or result'),
|
|
1007
|
+
studyNoteGuardCheck('syntax_or_structure', scopeAwareStatus(/(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(studyNoteText)), 'Study Note should include one useful syntax or structure insight'),
|
|
1008
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(studyNoteText)), 'Study Note should say what was checked'),
|
|
1009
|
+
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(studyNoteText)), 'Study Note should say what remains uncertain or explicitly state that no meaningful uncertainty remains'),
|
|
1010
|
+
studyNoteGuardCheck('next_step_present', scopeAwareStatus(hasNextStepMarker(reportText)), 'changed-artifact reports should include a Next step section'),
|
|
1011
|
+
studyNoteGuardCheck('single_study_note_next_step_pair', scopeAwareStatus(hasSingleStudyNoteNextStepPair(reportText)), 'changed-artifact reports should contain one unambiguous Study Note followed by one Next step section'),
|
|
1012
|
+
studyNoteGuardCheck('study_note_then_next_step', scopeAwareStatus(hasStudyNoteThenNextStepOrder(reportText)), 'Next step should be the next report section immediately after Study Note'),
|
|
1013
|
+
studyNoteGuardCheck('next_step_whole_process_scan', scopeAwareStatus(hasNextStepWholeProcessScan(reportText)), 'Next step should include current situation, forward outlook, a critical opinion, and an improvement recommendation'),
|
|
1014
|
+
studyNoteGuardCheck('next_step_ordered_actions', scopeAwareStatus(hasNextStepOrderedActions(reportText)), 'Next step should include an ordered action sequence with fix-first work before planned work'),
|
|
1015
|
+
studyNoteGuardCheck('next_step_evidence_and_ownership', scopeAwareStatus(hasNextStepEvidenceAndOwnership(reportText)), 'Next step should name evidence level or stamp plus the owner route or scope'),
|
|
1016
|
+
studyNoteGuardCheck('next_step_safety_context', scopeAwareStatus(hasNextStepSafetyContext(reportText)), 'Next step should state blocker status, safe retry guidance, and side effects'),
|
|
1017
|
+
studyNoteGuardCheck('next_step_truth_status', scopeAwareStatus(hasNextStepTruthStatus(reportText)), 'Next step should state its evidence-bounded truth status')
|
|
877
1018
|
];
|
|
878
1019
|
for (const requirement of hygieneRequirements) {
|
|
879
|
-
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(
|
|
1020
|
+
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
880
1021
|
}
|
|
881
1022
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
882
1023
|
const result = {
|
|
@@ -958,6 +1099,121 @@ function hasStudyNoteMarker(text = '') {
|
|
|
958
1099
|
return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
|
|
959
1100
|
}
|
|
960
1101
|
|
|
1102
|
+
function hasNextStepMarker(text = '') {
|
|
1103
|
+
return /(next step|next-step|다음\s*(?:단계|할\s*일|작업))/i.test(text);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function reportSectionMarkers(text = '') {
|
|
1107
|
+
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1108
|
+
const trimmed = line.trim();
|
|
1109
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1110
|
+
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1111
|
+
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
1112
|
+
if (!rawLabel) return null;
|
|
1113
|
+
const label = rawLabel.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
|
1114
|
+
const kind = /(study note|학습 노트|스터디 노트)/i.test(label)
|
|
1115
|
+
? 'study_note'
|
|
1116
|
+
: /(next step|다음 단계|다음 할 일|다음 작업)/i.test(label)
|
|
1117
|
+
? 'next_step'
|
|
1118
|
+
: 'other';
|
|
1119
|
+
return { index, kind, level: heading?.[0].match(/^#+/)?.[0].length || 0 };
|
|
1120
|
+
}).filter((item): item is { index: number; kind: string; level: number } => Boolean(item));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function hasStudyNoteThenNextStepOrder(text = '') {
|
|
1124
|
+
const markers = reportSectionMarkers(text);
|
|
1125
|
+
const studyIndex = markers.findIndex((item) => item.kind === 'study_note');
|
|
1126
|
+
if (studyIndex < 0) return false;
|
|
1127
|
+
return nextPeerSection(markers, studyIndex)?.kind === 'next_step';
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function hasSingleStudyNoteNextStepPair(text = '') {
|
|
1131
|
+
const markers = reportSectionMarkers(text);
|
|
1132
|
+
return markers.filter((item) => item.kind === 'study_note').length === 1
|
|
1133
|
+
&& markers.filter((item) => item.kind === 'next_step').length === 1;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function hasNextStepWholeProcessScan(text = '') {
|
|
1137
|
+
const value = nextStepSectionText(text);
|
|
1138
|
+
const situation = /(situation|current state|현재\s*(?:상황|상태))/i.test(value);
|
|
1139
|
+
const outlook = /(outlook|forward|앞으로|향후|전망)/i.test(value);
|
|
1140
|
+
const critical = /(critical opinion|critique|비판적|비판\s*의견|tradeoff|약점)/i.test(value);
|
|
1141
|
+
const recommendation = /(recommendation|recommend|개선\s*추천|추천\s*의견|권장)/i.test(value);
|
|
1142
|
+
return situation && outlook && critical && recommendation;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function hasNextStepOrderedActions(text = '') {
|
|
1146
|
+
const value = nextStepSectionText(text);
|
|
1147
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1148
|
+
const actionLines = lines.filter((line) => /^\d+[.)]\s+/.test(line));
|
|
1149
|
+
const labeledLines = lines.filter((line) => /\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i.test(line));
|
|
1150
|
+
if (!actionLines.length || !/^1[.)]\s+/.test(actionLines[0])) return false;
|
|
1151
|
+
if (labeledLines.length !== actionLines.length || labeledLines.some((line) => !/^\d+[.)]\s+/.test(line))) return false;
|
|
1152
|
+
let plannedSeen = false;
|
|
1153
|
+
for (const [index, line] of actionLines.entries()) {
|
|
1154
|
+
const number = Number(line.match(/^(\d+)[.)]/)?.[1] || 0);
|
|
1155
|
+
if (number !== index + 1) return false;
|
|
1156
|
+
const kind = line.match(/\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i)?.[1] || '';
|
|
1157
|
+
if (!kind) return false;
|
|
1158
|
+
const planned = /planned|계획/i.test(kind);
|
|
1159
|
+
if (planned) plannedSeen = true;
|
|
1160
|
+
else if (plannedSeen) return false;
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function hasNextStepEvidenceAndOwnership(text = '') {
|
|
1166
|
+
const value = nextStepSectionText(text);
|
|
1167
|
+
const evidence = /(evidence(?:[_ -](?:level|stamp))?|\bL[0-5]\b|근거|증거|검증\s*(?:수준|스탬프))/i.test(value);
|
|
1168
|
+
const ownership = /(owner(?:[_ -](?:route|scope))?|담당|소유|책임\s*(?:경로|범위|주체))/i.test(value);
|
|
1169
|
+
return evidence && ownership;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function hasNextStepSafetyContext(text = '') {
|
|
1173
|
+
const value = nextStepSectionText(text);
|
|
1174
|
+
const blocker = /(blocker|blocked(?:[_ -]by)?|차단|막힘|방해)/i.test(value);
|
|
1175
|
+
const safeRetry = /(safe[_ -]retry|안전[^\n]{0,20}재시도|재시도[^\n]{0,20}안전)/i.test(value);
|
|
1176
|
+
const sideEffects = /(side[_ -]effects?|부작용|영향)/i.test(value);
|
|
1177
|
+
return blocker && safeRetry && sideEffects;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function hasNextStepTruthStatus(text = '') {
|
|
1181
|
+
return /(truth[_ -]status|진실\s*상태|검증\s*상태)/i.test(nextStepSectionText(text));
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function nextStepSectionText(text = '') {
|
|
1185
|
+
const value = String(text || '');
|
|
1186
|
+
const lines = value.split(/\r?\n/);
|
|
1187
|
+
const markers = reportSectionMarkers(value);
|
|
1188
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1189
|
+
if (study < 0) return '';
|
|
1190
|
+
const adjacent = nextPeerSectionIndex(markers, study);
|
|
1191
|
+
if (adjacent < 0 || markers[adjacent].kind !== 'next_step') return '';
|
|
1192
|
+
const end = nextPeerSection(markers, adjacent);
|
|
1193
|
+
return lines.slice(markers[adjacent].index + 1, end?.index ?? lines.length).join('\n');
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function studyNoteSectionText(text = '') {
|
|
1197
|
+
const value = String(text || '');
|
|
1198
|
+
const lines = value.split(/\r?\n/);
|
|
1199
|
+
const markers = reportSectionMarkers(value);
|
|
1200
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1201
|
+
if (study < 0) return '';
|
|
1202
|
+
const end = nextPeerSection(markers, study);
|
|
1203
|
+
return lines.slice(markers[study].index + 1, end?.index ?? lines.length).join('\n');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function nextPeerSection(markers: Array<{ index: number; kind: string; level: number }>, start: number) {
|
|
1207
|
+
const nextIndex = nextPeerSectionIndex(markers, start);
|
|
1208
|
+
return nextIndex < 0 ? undefined : markers[nextIndex];
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function nextPeerSectionIndex(markers: Array<{ index: number; kind: string; level: number }>, start: number) {
|
|
1212
|
+
const current = markers[start];
|
|
1213
|
+
const relative = markers.slice(start + 1).findIndex((item) => current.level === 0 || item.level === 0 || item.level <= current.level);
|
|
1214
|
+
return relative < 0 ? -1 : start + 1 + relative;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
961
1217
|
function studyNoteGuardCheck(id, status, nextAction) {
|
|
962
1218
|
return {
|
|
963
1219
|
id,
|
|
@@ -1412,6 +1668,8 @@ function buildCapabilityMatrix(readinessRows = [], liteHook: AnyRecord = {}, stu
|
|
|
1412
1668
|
capabilityRow('bounded_design_revisions', 'beta', 'ready', 'maximum two reviewer-finding-backed revision rounds', 'round state does not replace Ueye artifact hash verification'),
|
|
1413
1669
|
capabilityRow('final_gallery_manifest', 'beta', 'ready', 'path-confined hash-verified packaging manifest', 'gallery completeness does not prove visual or license correctness'),
|
|
1414
1670
|
capabilityRow('demand_gated_design_production', 'beta', 'ready', 'pre-recorded operator trigger, Canvas, chronological bounded revisions, gallery, and digest-verifiable phase receipt', 'demand evidence is operator-asserted and overall phase truth remains partial'),
|
|
1671
|
+
capabilityRow('final_scope_verification_closure', 'beta', 'ready', 'explicit Git-scope and selected/executed command receipt with digest verification', 'does not execute checks; unavailable or unplanned Git scope blocks and successful operator evidence remains partial'),
|
|
1672
|
+
capabilityRow('media_provider_execution_receipt', 'beta', 'ready', 'optional demand-gated exact execution boundary plus identity-bound local assets', 'never contacts providers and does not prove visual or implementation correctness'),
|
|
1415
1673
|
capabilityRow('yam_lite_hook', 'beta', String(liteHook.state || readiness.get('yam-lite hook') || 'unknown'), 'optional hook health inspection', 'advisory only; disabled unless explicitly enabled'),
|
|
1416
1674
|
capabilityRow('study_note_hook', 'beta', String(studyNoteHook.state || readiness.get('yam-study-note hook') || 'unknown'), 'optional prompt and bounded Stop check', 'does not generate the Study Note or run verification')
|
|
1417
1675
|
];
|
|
@@ -2171,26 +2429,27 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
2171
2429
|
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2172
2430
|
const changedFiles = changedFileDetection.files;
|
|
2173
2431
|
const lines = [
|
|
2174
|
-
'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.'
|
|
2432
|
+
'yam Study Note guard active: if this turn changes code, config, release metadata, docs, or project artifacts, include a Study Note followed immediately by Next step in the final response.'
|
|
2175
2433
|
];
|
|
2176
2434
|
if (!changedFileDetection.available) {
|
|
2177
2435
|
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2178
|
-
lines.push('Determine manually whether artifacts changed and include
|
|
2436
|
+
lines.push('Determine manually whether artifacts changed and include Study Note followed by Next step when they did; the Stop hook will keep this uncertainty visible.');
|
|
2179
2437
|
return lines.join('\n');
|
|
2180
2438
|
}
|
|
2181
2439
|
if (!changedFiles.length) {
|
|
2182
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add
|
|
2440
|
+
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add Study Note followed immediately by Next step before final.');
|
|
2183
2441
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2184
2442
|
return lines.join('\n');
|
|
2185
2443
|
}
|
|
2186
2444
|
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2187
2445
|
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2446
|
+
lines.push('Next step minimum: quick whole-process scan, current situation, forward outlook, critical opinion, improvement recommendation, ordered fix-first then planned actions, evidence, ownership, blocker status, safe retry, side effects, and truth status.');
|
|
2188
2447
|
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2189
2448
|
if (hygiene.length) {
|
|
2190
2449
|
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2191
2450
|
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.');
|
|
2192
2451
|
}
|
|
2193
|
-
lines.push('This prompt reminder does not generate or edit
|
|
2452
|
+
lines.push('This prompt reminder does not generate or edit either section; the paired Stop hook requests one correction pass if the final response is incomplete or ordered incorrectly.');
|
|
2194
2453
|
return lines.join('\n');
|
|
2195
2454
|
}
|
|
2196
2455
|
|
|
@@ -2268,12 +2527,15 @@ async function safety(args = []) {
|
|
|
2268
2527
|
async function release(args = []) {
|
|
2269
2528
|
const subcommand = args[0] || 'help';
|
|
2270
2529
|
if (subcommand !== 'report') {
|
|
2271
|
-
console.error('usage: yam release report [--json]');
|
|
2530
|
+
console.error('usage: yam release report [--auth-mode auto|oidc|token] [--json]');
|
|
2272
2531
|
process.exitCode = 1;
|
|
2273
2532
|
return;
|
|
2274
2533
|
}
|
|
2275
|
-
const
|
|
2276
|
-
const
|
|
2534
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['auth-mode', 'json']));
|
|
2535
|
+
const authMode = String(flags.auth_mode || 'auto');
|
|
2536
|
+
if (!['auto', 'oidc', 'token'].includes(authMode)) throw new Error('--auth-mode must be auto, oidc, or token');
|
|
2537
|
+
const asJson = Boolean(flags.json);
|
|
2538
|
+
const report = runReleaseReport(authMode);
|
|
2277
2539
|
if (asJson) {
|
|
2278
2540
|
console.log(JSON.stringify(report, null, 2));
|
|
2279
2541
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -2298,7 +2560,7 @@ async function release(args = []) {
|
|
|
2298
2560
|
if (report.publish_readiness) {
|
|
2299
2561
|
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
2300
2562
|
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
2301
|
-
console.log(`-
|
|
2563
|
+
console.log(`- Publish auth: ${report.publish_readiness.auth.mode} (${report.publish_readiness.auth.status})`);
|
|
2302
2564
|
}
|
|
2303
2565
|
if (report.failed.length) {
|
|
2304
2566
|
console.log('- Failed:');
|
|
@@ -2321,7 +2583,7 @@ async function release(args = []) {
|
|
|
2321
2583
|
if (!report.ok) process.exitCode = 1;
|
|
2322
2584
|
}
|
|
2323
2585
|
|
|
2324
|
-
function runReleaseReport() {
|
|
2586
|
+
function runReleaseReport(authMode = 'auto') {
|
|
2325
2587
|
const startedAt = new Date().toISOString();
|
|
2326
2588
|
const checks = [
|
|
2327
2589
|
['typecheck', ['npm', ['run', 'typecheck']]],
|
|
@@ -2336,7 +2598,7 @@ function runReleaseReport() {
|
|
|
2336
2598
|
const tarball = releaseTarballProvenance();
|
|
2337
2599
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
2338
2600
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
2339
|
-
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
2601
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence, authMode);
|
|
2340
2602
|
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
2341
2603
|
const preliminaryOk = failed.length === 0 && publishReadiness.status === 'ready';
|
|
2342
2604
|
const readinessReceipt = releaseReadinessReceipt(provenance, freshness, tarball, publishReadiness, preliminaryOk);
|
|
@@ -2402,9 +2664,14 @@ function releaseReadinessReceipt(provenance: AnyRecord = {}, freshness: AnyRecor
|
|
|
2402
2664
|
truth_status: publishReadiness.registry?.truth_status || 'partial'
|
|
2403
2665
|
},
|
|
2404
2666
|
auth: {
|
|
2405
|
-
|
|
2667
|
+
requested_mode: String(publishReadiness.auth?.requested_mode || 'auto'),
|
|
2668
|
+
mode: String(publishReadiness.auth?.mode || 'manual_token'),
|
|
2669
|
+
command: String(publishReadiness.auth?.command || 'npm whoami'),
|
|
2670
|
+
required_for_selected_mode: Boolean(publishReadiness.auth?.required_for_selected_mode),
|
|
2406
2671
|
status: String(publishReadiness.auth?.status || 'not_verified'),
|
|
2407
|
-
account: publishReadiness.auth?.
|
|
2672
|
+
account: String(publishReadiness.auth?.account || ''),
|
|
2673
|
+
local_token: publishReadiness.auth?.local_token || null,
|
|
2674
|
+
trusted_publisher: publishReadiness.auth?.trusted_publisher || null,
|
|
2408
2675
|
truth_status: publishReadiness.auth?.truth_status || 'partial'
|
|
2409
2676
|
},
|
|
2410
2677
|
git: {
|
|
@@ -2506,14 +2773,36 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
|
|
|
2506
2773
|
};
|
|
2507
2774
|
}
|
|
2508
2775
|
|
|
2509
|
-
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
|
|
2776
|
+
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = [], authMode = 'auto') {
|
|
2510
2777
|
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
2511
2778
|
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
2512
2779
|
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
2513
2780
|
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
2514
|
-
const
|
|
2781
|
+
const trustedPublisherWorkflowPath = '.github/workflows/release.yml';
|
|
2782
|
+
const trustedPublisherWorkflowFile = path.join(ROOT, trustedPublisherWorkflowPath);
|
|
2783
|
+
const trustedPublisherWorkflowExists = fs.existsSync(trustedPublisherWorkflowFile);
|
|
2784
|
+
const trustedPublisherWorkflow = inspectTrustedPublisherWorkflowText(
|
|
2785
|
+
trustedPublisherWorkflowExists ? fs.readFileSync(trustedPublisherWorkflowFile, 'utf8') : '',
|
|
2786
|
+
trustedPublisherWorkflowPath,
|
|
2787
|
+
trustedPublisherWorkflowExists
|
|
2788
|
+
);
|
|
2789
|
+
const authReadiness = buildReleaseAuthReadiness({
|
|
2790
|
+
workflow: trustedPublisherWorkflow,
|
|
2791
|
+
local_token: {
|
|
2792
|
+
command: 'npm whoami',
|
|
2793
|
+
ok: whoamiProbe.ok,
|
|
2794
|
+
note: whoamiProbe.note
|
|
2795
|
+
},
|
|
2796
|
+
requested_mode: authMode as 'auto' | 'oidc' | 'token'
|
|
2797
|
+
});
|
|
2798
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks, {
|
|
2799
|
+
package_name: PACKAGE_JSON.name,
|
|
2800
|
+
version: VERSION
|
|
2801
|
+
});
|
|
2515
2802
|
const latestProbe = registryStatus.checked
|
|
2516
2803
|
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
2804
|
+
: registryStatus.parse_failed
|
|
2805
|
+
? { ok: false, stdout: '', note: `registry check output did not prove ${PACKAGE_JSON.name}@${VERSION} is unpublished: ${registryStatus.note}` }
|
|
2517
2806
|
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
2518
2807
|
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
2519
2808
|
const blockers = [];
|
|
@@ -2524,8 +2813,8 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2524
2813
|
if (!registryProbe.ok) {
|
|
2525
2814
|
blockers.push(readinessBlocker('registry_not_verified', 'npm registry could not be read from this shell', 'run `npm config get registry` and confirm it points to the intended public registry', 'npm config get registry', 'error'));
|
|
2526
2815
|
}
|
|
2527
|
-
|
|
2528
|
-
blockers.push(readinessBlocker(
|
|
2816
|
+
for (const blocker of authReadiness.blockers) {
|
|
2817
|
+
blockers.push(readinessBlocker(blocker.kind, blocker.reason, blocker.safe_next_action, blocker.command, blocker.severity));
|
|
2529
2818
|
}
|
|
2530
2819
|
if (!latestProbe.ok) {
|
|
2531
2820
|
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
@@ -2565,11 +2854,16 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2565
2854
|
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
2566
2855
|
},
|
|
2567
2856
|
auth: {
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2857
|
+
requested_mode: authReadiness.requested_mode,
|
|
2858
|
+
mode: authReadiness.mode,
|
|
2859
|
+
command: authReadiness.local_token.command,
|
|
2860
|
+
required_for_selected_mode: authReadiness.local_token.required_for_selected_mode,
|
|
2861
|
+
status: authReadiness.status,
|
|
2862
|
+
account: authReadiness.local_token.account,
|
|
2863
|
+
note: authReadiness.next_action,
|
|
2864
|
+
local_token: authReadiness.local_token,
|
|
2865
|
+
trusted_publisher: authReadiness.trusted_publisher,
|
|
2866
|
+
truth_status: authReadiness.truth_status
|
|
2573
2867
|
},
|
|
2574
2868
|
blockers: uniqueBlockers,
|
|
2575
2869
|
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
@@ -2577,19 +2871,6 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2577
2871
|
};
|
|
2578
2872
|
}
|
|
2579
2873
|
|
|
2580
|
-
function releaseRegistryStatusFromChecks(checks = []) {
|
|
2581
|
-
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
2582
|
-
const note = String(registryCheck?.note || '');
|
|
2583
|
-
if (registryCheck?.status !== 'passed') return { checked: false, latest_version: '', not_published: false, note };
|
|
2584
|
-
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
2585
|
-
return {
|
|
2586
|
-
checked: true,
|
|
2587
|
-
latest_version: latest,
|
|
2588
|
-
not_published: /not published yet/i.test(note),
|
|
2589
|
-
note
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
2874
|
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
2594
2875
|
return {
|
|
2595
2876
|
kind,
|
|
@@ -2625,13 +2906,13 @@ function releaseStudyNote(publishReadiness: AnyRecord = {}, publishBlockers = []
|
|
|
2625
2906
|
}
|
|
2626
2907
|
return buildStudyNote({
|
|
2627
2908
|
issue_code: 'npm publish readiness',
|
|
2628
|
-
issue_role: 'This part checks registry, version,
|
|
2909
|
+
issue_role: 'This part checks registry, version, the selected OIDC or manual-token authentication path, tarball, and local release checks before any public publish.',
|
|
2629
2910
|
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
2630
2911
|
changed_code: 'yam release report',
|
|
2631
2912
|
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
2632
|
-
change_summary: 'The report
|
|
2633
|
-
why_important: '
|
|
2634
|
-
learning_note: '
|
|
2913
|
+
change_summary: 'The report separates Trusted Publisher OIDC evidence from optional local-token evidence and gives a path-specific safe next action.',
|
|
2914
|
+
why_important: 'npm whoami cannot prove OIDC readiness; treating its E401 as an OIDC blocker would recommend the wrong credential model.',
|
|
2915
|
+
learning_note: 'For Trusted Publisher releases, verify the hosted workflow and npmjs package binding; use npm whoami only for an explicitly selected manual-token path.'
|
|
2635
2916
|
});
|
|
2636
2917
|
}
|
|
2637
2918
|
|
|
@@ -2806,20 +3087,117 @@ async function loop(args = []) {
|
|
|
2806
3087
|
return loopUsage();
|
|
2807
3088
|
}
|
|
2808
3089
|
|
|
3090
|
+
async function scout(args = []) {
|
|
3091
|
+
const subcommand = args[0] || 'help';
|
|
3092
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return scoutUsage();
|
|
3093
|
+
if (subcommand === 'receipt') return scoutReceipt(args.slice(1));
|
|
3094
|
+
console.error(`unknown scout command: ${subcommand}`);
|
|
3095
|
+
process.exitCode = 1;
|
|
3096
|
+
return scoutUsage();
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
function scoutUsage() {
|
|
3100
|
+
console.log(`yam scout
|
|
3101
|
+
|
|
3102
|
+
Immutable, operator-supplied research evidence helpers. These commands do not browse, fetch, or trust external content as instructions.
|
|
3103
|
+
|
|
3104
|
+
Usage:
|
|
3105
|
+
yam scout receipt create [--root dir] --receipt-path .yam/scout/run.json --spec scout-spec.json [--json]
|
|
3106
|
+
yam scout receipt verify [--root dir] --receipt-path .yam/scout/run.json [--json]
|
|
3107
|
+
`);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
async function scoutReceipt(args = []) {
|
|
3111
|
+
const action = args[0] || 'help';
|
|
3112
|
+
if (action === 'help' || action === '--help' || action === '-h') return scoutUsage();
|
|
3113
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['root', 'receipt-path', 'spec', 'json']));
|
|
3114
|
+
const root = path.resolve(expandHome(String(flags.root || process.cwd())));
|
|
3115
|
+
try {
|
|
3116
|
+
let result;
|
|
3117
|
+
if (action === 'create') {
|
|
3118
|
+
if (!flags.receipt_path || !flags.spec) throw new Error('create requires --receipt-path and --spec');
|
|
3119
|
+
result = await createScoutReceipt({
|
|
3120
|
+
root,
|
|
3121
|
+
receipt_path: String(flags.receipt_path),
|
|
3122
|
+
spec: await readBoundedRegularJson(flags.spec, 'Scout spec') as any
|
|
3123
|
+
});
|
|
3124
|
+
} else if (action === 'verify') {
|
|
3125
|
+
if (!flags.receipt_path) throw new Error('verify requires --receipt-path');
|
|
3126
|
+
result = await verifyScoutReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
3127
|
+
} else {
|
|
3128
|
+
throw new Error(`unknown Scout receipt action: ${action}`);
|
|
3129
|
+
}
|
|
3130
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3131
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
3132
|
+
} catch (error) {
|
|
3133
|
+
const result = {
|
|
3134
|
+
schema: 'yam.scout-receipt-error.v1',
|
|
3135
|
+
truth_status: 'blocked',
|
|
3136
|
+
error: errorMessage(error),
|
|
3137
|
+
next_action: 'repair the confined receipt path, canonical entity, version clocks, source digests, claim links, or failure taxonomy before retrying'
|
|
3138
|
+
};
|
|
3139
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3140
|
+
process.exitCode = 1;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
async function nextStepCommand(args = []) {
|
|
3145
|
+
const subcommand = args[0] || 'help';
|
|
3146
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return nextStepUsage();
|
|
3147
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['spec', 'receipt', 'json']));
|
|
3148
|
+
try {
|
|
3149
|
+
let result;
|
|
3150
|
+
if (subcommand === 'report') {
|
|
3151
|
+
if (!flags.spec) throw new Error('report requires --spec');
|
|
3152
|
+
result = buildNextStep(await readBoundedRegularJson(flags.spec, 'Next step spec') as any);
|
|
3153
|
+
} else if (subcommand === 'verify') {
|
|
3154
|
+
if (!flags.receipt) throw new Error('verify requires --receipt');
|
|
3155
|
+
result = verifyNextStep(await readBoundedRegularJson(flags.receipt, 'Next step receipt') as any);
|
|
3156
|
+
} else {
|
|
3157
|
+
throw new Error(`unknown Next step command: ${subcommand}`);
|
|
3158
|
+
}
|
|
3159
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3160
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
3161
|
+
} catch (error) {
|
|
3162
|
+
const result = {
|
|
3163
|
+
schema: 'yam.next-step-error.v1',
|
|
3164
|
+
truth_status: 'blocked',
|
|
3165
|
+
error: errorMessage(error),
|
|
3166
|
+
next_action: 'repair the whole-process scan, ordered steps, evidence level/stamp, owner scope, blockers, or safe retry before retrying'
|
|
3167
|
+
};
|
|
3168
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3169
|
+
process.exitCode = 1;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function nextStepUsage() {
|
|
3174
|
+
console.log(`yam next-step
|
|
3175
|
+
|
|
3176
|
+
Build or verify an evidence-bound ordered handoff after a quick whole-process scan.
|
|
3177
|
+
|
|
3178
|
+
Usage:
|
|
3179
|
+
yam next-step report --spec next-step-spec.json [--json]
|
|
3180
|
+
yam next-step verify --receipt next-step-receipt.json [--json]
|
|
3181
|
+
`);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
2809
3184
|
function loopUsage() {
|
|
2810
3185
|
console.log(`yam loop
|
|
2811
3186
|
|
|
2812
|
-
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers,
|
|
3187
|
+
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers, a short Study Note, and an evidence-bound Next step receipt. It does not run agents, start processes, publish packages, or write files.
|
|
2813
3188
|
|
|
2814
3189
|
Usage:
|
|
2815
|
-
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]
|
|
3190
|
+
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] [--next-step-spec file] [--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]
|
|
2816
3191
|
`);
|
|
2817
3192
|
}
|
|
2818
3193
|
|
|
2819
3194
|
async function loopReport(args = []) {
|
|
2820
3195
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
|
|
2821
|
-
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']));
|
|
3196
|
+
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', 'next-step-spec', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
|
|
2822
3197
|
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
3198
|
+
const nextStepSpec = flags.next_step_spec
|
|
3199
|
+
? await readBoundedRegularJson(flags.next_step_spec, 'Next step spec')
|
|
3200
|
+
: undefined;
|
|
2823
3201
|
const report = buildLoopReport({
|
|
2824
3202
|
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
2825
3203
|
intent: String(flags.intent || ''),
|
|
@@ -2858,6 +3236,7 @@ async function loopReport(args = []) {
|
|
|
2858
3236
|
scope_owner: String(flags.scope_owner || flags.owner || ''),
|
|
2859
3237
|
side_effects: arrayFlag(flags.side_effect),
|
|
2860
3238
|
avoidance_note: String(flags.avoidance_note || ''),
|
|
3239
|
+
next_step: nextStepSpec as any,
|
|
2861
3240
|
issue_code: String(flags.issue_code || ''),
|
|
2862
3241
|
issue_role: String(flags.issue_role || ''),
|
|
2863
3242
|
issue_symptom: String(flags.issue_symptom || ''),
|
|
@@ -3158,15 +3537,60 @@ async function readProductionSpecs(value: unknown, label: string) {
|
|
|
3158
3537
|
return specs;
|
|
3159
3538
|
}
|
|
3160
3539
|
|
|
3540
|
+
async function readOptionalSpecs(value: unknown, label: string) {
|
|
3541
|
+
const files = arrayFlag(value);
|
|
3542
|
+
const specs: AnyRecord[] = [];
|
|
3543
|
+
for (const file of files) specs.push(await readBoundedJsonSpec(path.resolve(expandHome(file)), label));
|
|
3544
|
+
return specs;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3161
3547
|
async function readBoundedJsonSpec(file: string, label: string) {
|
|
3162
|
-
const
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
const
|
|
3166
|
-
if (
|
|
3548
|
+
const physicalParent = await fsp.realpath(path.dirname(file));
|
|
3549
|
+
const target = path.join(physicalParent, path.basename(file));
|
|
3550
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
3551
|
+
const before = await fsp.lstat(target);
|
|
3552
|
+
if (before.isSymbolicLink() || !before.isFile()) throw new Error(`${label} must be a regular non-symlink file: ${target}`);
|
|
3553
|
+
if (before.size > 256 * 1024) throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3554
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
3555
|
+
? fs.constants.O_NOFOLLOW
|
|
3556
|
+
: 0;
|
|
3557
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
3558
|
+
let text = '';
|
|
3559
|
+
try {
|
|
3560
|
+
const opened = await handle.stat();
|
|
3561
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3562
|
+
if (!opened.isFile() || !sameSpecFileIdentity(before, opened)) throw new Error(`${label} identity changed before open: ${target}`);
|
|
3563
|
+
if (opened.size > 256 * 1024) throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3564
|
+
const bytes = Buffer.alloc(opened.size);
|
|
3565
|
+
let offset = 0;
|
|
3566
|
+
while (offset < bytes.length) {
|
|
3567
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
3568
|
+
if (!bytesRead) throw new Error(`${label} became shorter while reading: ${target}`);
|
|
3569
|
+
offset += bytesRead;
|
|
3570
|
+
}
|
|
3571
|
+
const openedAfter = await handle.stat();
|
|
3572
|
+
const after = await fsp.lstat(target);
|
|
3573
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3574
|
+
if (!sameSpecFileIdentity(opened, openedAfter) || openedAfter.size !== opened.size
|
|
3575
|
+
|| after.isSymbolicLink() || !after.isFile() || !sameSpecFileIdentity(opened, after)) {
|
|
3576
|
+
throw new Error(`${label} changed size or identity while reading: ${target}`);
|
|
3577
|
+
}
|
|
3578
|
+
text = bytes.toString('utf8');
|
|
3579
|
+
} finally {
|
|
3580
|
+
await handle.close();
|
|
3581
|
+
}
|
|
3582
|
+
const value = JSON.parse(text);
|
|
3583
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain one JSON object: ${target}`);
|
|
3167
3584
|
return value;
|
|
3168
3585
|
}
|
|
3169
3586
|
|
|
3587
|
+
function sameSpecFileIdentity(
|
|
3588
|
+
left: { dev: number | bigint; ino: number | bigint },
|
|
3589
|
+
right: { dev: number | bigint; ino: number | bigint }
|
|
3590
|
+
) {
|
|
3591
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3170
3594
|
async function existingProductionPath(root: string, value: string, label: string) {
|
|
3171
3595
|
if (!value || path.isAbsolute(value) || value.includes('\0')) throw new Error(`${label} must be a non-empty project-relative path`);
|
|
3172
3596
|
const realRoot = await fsp.realpath(root);
|
|
@@ -3738,10 +4162,77 @@ function normalizeUeyeComparisonResult(value = '') {
|
|
|
3738
4162
|
return 'not-verified';
|
|
3739
4163
|
}
|
|
3740
4164
|
|
|
4165
|
+
async function verification(args = []) {
|
|
4166
|
+
const surface = String(args[0] || 'help');
|
|
4167
|
+
const action = String(args[1] || 'help');
|
|
4168
|
+
if (surface === 'help' || action === 'help' || surface === '--help' || surface === '-h') return verificationUsage();
|
|
4169
|
+
if (surface !== 'closure') {
|
|
4170
|
+
console.error(`unknown verification command: ${surface}`);
|
|
4171
|
+
return verificationUsage();
|
|
4172
|
+
}
|
|
4173
|
+
const flags = parseSimpleFlags(args.slice(2), new Set([
|
|
4174
|
+
'root', 'receipt-path', 'planned-scope', 'changed-file', 'selected-spec',
|
|
4175
|
+
'executed-spec', 'skipped-spec', 'truncated-spec', 'json'
|
|
4176
|
+
]));
|
|
4177
|
+
const root = path.resolve(expandHome(flags.root || process.cwd()));
|
|
4178
|
+
try {
|
|
4179
|
+
let result: AnyRecord;
|
|
4180
|
+
if (action === 'create') {
|
|
4181
|
+
requireProductionFlags(flags, ['receipt_path', 'planned_scope', 'changed_file', 'selected_spec']);
|
|
4182
|
+
result = await createVerificationClosureReceipt({
|
|
4183
|
+
root,
|
|
4184
|
+
receipt_path: String(flags.receipt_path),
|
|
4185
|
+
planned_scope: String(flags.planned_scope) as 'affected' | 'release',
|
|
4186
|
+
changed_files: arrayFlag(flags.changed_file),
|
|
4187
|
+
selected_commands: await readProductionSpecs(flags.selected_spec, 'selected command spec') as any,
|
|
4188
|
+
executed_commands: await readOptionalSpecs(flags.executed_spec, 'executed command spec') as any,
|
|
4189
|
+
skipped: await readOptionalSpecs(flags.skipped_spec, 'skipped command spec') as any,
|
|
4190
|
+
truncated: await readOptionalSpecs(flags.truncated_spec, 'truncated output spec') as any
|
|
4191
|
+
});
|
|
4192
|
+
} else if (action === 'verify') {
|
|
4193
|
+
requireProductionFlags(flags, ['receipt_path']);
|
|
4194
|
+
result = await verifyVerificationClosureReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
4195
|
+
} else {
|
|
4196
|
+
throw new Error(`unknown verification closure command: ${action}`);
|
|
4197
|
+
}
|
|
4198
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Verification closure');
|
|
4199
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
const result = {
|
|
4202
|
+
schema: 'yam.verification-closure-error.v1',
|
|
4203
|
+
action,
|
|
4204
|
+
status: 'blocked',
|
|
4205
|
+
reason: summarizeCheckOutput(errorMessage(error)),
|
|
4206
|
+
next_action: 'repair changed-file scope, selected/executed command evidence, skipped/truncated reasons, or receipt integrity before retrying',
|
|
4207
|
+
truth_status: 'blocked'
|
|
4208
|
+
};
|
|
4209
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Verification closure');
|
|
4210
|
+
process.exitCode = 1;
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
function verificationUsage() {
|
|
4215
|
+
console.log(`yam verification closure
|
|
4216
|
+
|
|
4217
|
+
Explicit, immutable completion receipts. Create records operator-supplied results; it does not execute commands or replace advisory yam detect output.
|
|
4218
|
+
|
|
4219
|
+
Usage:
|
|
4220
|
+
yam verification closure create [--root dir] --receipt-path .yam/verification/closure.json --planned-scope affected|release --changed-file file --selected-spec file [--executed-spec file] [--skipped-spec file] [--truncated-spec file] [--json]
|
|
4221
|
+
yam verification closure verify [--root dir] --receipt-path .yam/verification/closure.json [--json]
|
|
4222
|
+
|
|
4223
|
+
Selected spec: {"id":"typecheck","command":"npm run typecheck","scope":"affected","required":true}
|
|
4224
|
+
Executed spec: {"id":"typecheck","command":"npm run typecheck","exit_code":0,"evidence":"exit 0 observed"}
|
|
4225
|
+
Skipped/truncated spec: {"check_id":"browser","reason":"no UI surface changed"}
|
|
4226
|
+
|
|
4227
|
+
Repeat spec and changed-file flags as needed. Release-sensitive paths promote affected to release and require an executed required release-scope command. Required selected commands cannot be skipped. Digest verification does not upgrade operator-declared execution results beyond partial truth.
|
|
4228
|
+
`);
|
|
4229
|
+
}
|
|
4230
|
+
|
|
3741
4231
|
async function media(args = []) {
|
|
3742
4232
|
const subcommand = args[0] || 'help';
|
|
3743
4233
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
|
|
3744
4234
|
if (subcommand === 'proof') return mediaProof(args.slice(1));
|
|
4235
|
+
if (subcommand === 'provider') return mediaProvider(args.slice(1));
|
|
3745
4236
|
console.error(`unknown media command: ${subcommand}`);
|
|
3746
4237
|
return mediaUsage();
|
|
3747
4238
|
}
|
|
@@ -3753,9 +4244,63 @@ Opt-in media generation proof helpers. Generated media can support visual direct
|
|
|
3753
4244
|
|
|
3754
4245
|
Usage:
|
|
3755
4246
|
yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
|
|
4247
|
+
yam media provider create [--root dir] --receipt-path .yam/media/run.json --demand-kind media_generation|media_transformation --demand-evidence text --provider name --model name --provider-calls n [--provider-execution] [--dry-run] [--submit] --asset-spec file [--json]
|
|
4248
|
+
yam media provider verify [--root dir] --receipt-path .yam/media/run.json [--json]
|
|
4249
|
+
|
|
4250
|
+
Provider receipts are optional and demand-gated. This command hashes local assets and records an operator-asserted boundary; it never contacts a provider. Dry runs require zero calls and no submit. Explicit execution requires at least one call, submit, and a provider-provenance output asset.
|
|
3756
4251
|
`);
|
|
3757
4252
|
}
|
|
3758
4253
|
|
|
4254
|
+
async function mediaProvider(args = []) {
|
|
4255
|
+
const action = String(args[0] || 'help');
|
|
4256
|
+
if (action === 'help' || action === '--help' || action === '-h') return mediaUsage();
|
|
4257
|
+
const flags = parseSimpleFlags(args.slice(1), new Set([
|
|
4258
|
+
'root', 'receipt-path', 'demand-kind', 'demand-evidence', 'provider', 'model',
|
|
4259
|
+
'provider-calls', 'provider-execution', 'dry-run', 'submit', 'asset-spec', 'json'
|
|
4260
|
+
]));
|
|
4261
|
+
const root = path.resolve(expandHome(flags.root || process.cwd()));
|
|
4262
|
+
try {
|
|
4263
|
+
let result: AnyRecord;
|
|
4264
|
+
if (action === 'create') {
|
|
4265
|
+
requireProductionFlags(flags, [
|
|
4266
|
+
'receipt_path', 'demand_kind', 'demand_evidence', 'provider', 'model', 'provider_calls', 'asset_spec'
|
|
4267
|
+
]);
|
|
4268
|
+
result = await createMediaProviderReceipt({
|
|
4269
|
+
root,
|
|
4270
|
+
receipt_path: String(flags.receipt_path),
|
|
4271
|
+
demand_trigger: {
|
|
4272
|
+
kind: String(flags.demand_kind) as 'media_generation' | 'media_transformation',
|
|
4273
|
+
evidence: String(flags.demand_evidence)
|
|
4274
|
+
},
|
|
4275
|
+
provider: { name: String(flags.provider), model: String(flags.model) },
|
|
4276
|
+
provider_calls: Number(flags.provider_calls),
|
|
4277
|
+
provider_execution: Boolean(flags.provider_execution),
|
|
4278
|
+
dry_run: Boolean(flags.dry_run),
|
|
4279
|
+
submit: Boolean(flags.submit),
|
|
4280
|
+
assets: await readProductionSpecs(flags.asset_spec, 'media asset spec') as any
|
|
4281
|
+
});
|
|
4282
|
+
} else if (action === 'verify') {
|
|
4283
|
+
requireProductionFlags(flags, ['receipt_path']);
|
|
4284
|
+
result = await verifyMediaProviderReceipt({ root, receipt_path: String(flags.receipt_path) });
|
|
4285
|
+
} else {
|
|
4286
|
+
throw new Error(`unknown media provider command: ${action}`);
|
|
4287
|
+
}
|
|
4288
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Media provider receipt');
|
|
4289
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
4290
|
+
} catch (error) {
|
|
4291
|
+
const result = {
|
|
4292
|
+
schema: 'yam.media-provider-receipt-error.v1',
|
|
4293
|
+
action,
|
|
4294
|
+
status: 'blocked',
|
|
4295
|
+
reason: summarizeCheckOutput(errorMessage(error)),
|
|
4296
|
+
next_action: 'repair the demand, exact execution fields, confined asset provenance, or receipt integrity before retrying',
|
|
4297
|
+
truth_status: 'blocked'
|
|
4298
|
+
};
|
|
4299
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Media provider receipt');
|
|
4300
|
+
process.exitCode = 1;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
3759
4304
|
async function mediaProof(args = []) {
|
|
3760
4305
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return mediaUsage();
|
|
3761
4306
|
const flags = parseSimpleFlags(args, new Set(['tool', 'requested', 'attempted', 'output', 'wait-loop', 'blocked-reason', 'next-action', 'json']));
|
|
@@ -4257,7 +4802,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
|
4257
4802
|
const key = rawKey.slice(2);
|
|
4258
4803
|
if (allowed.size && !allowed.has(key)) continue;
|
|
4259
4804
|
const normalizedKey = key.replace(/-/g, '_');
|
|
4260
|
-
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'invented-metric', 'placeholder-copy', 'generic-visual', 'operator-provided', 'do-not-replace', 'allowed-for-edit', 'replace'].includes(key)) {
|
|
4805
|
+
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'invented-metric', 'placeholder-copy', 'generic-visual', 'operator-provided', 'do-not-replace', 'allowed-for-edit', 'replace', 'provider-execution', 'dry-run', 'submit'].includes(key)) {
|
|
4261
4806
|
flags[normalizedKey] = true;
|
|
4262
4807
|
continue;
|
|
4263
4808
|
}
|
|
@@ -5807,11 +6352,14 @@ function showRequestedHelp(args: string[]) {
|
|
|
5807
6352
|
context: contextUsage,
|
|
5808
6353
|
cleanup: cleanupUsage,
|
|
5809
6354
|
'study-note': studyNoteUsage,
|
|
6355
|
+
'next-step': nextStepUsage,
|
|
6356
|
+
scout: scoutUsage,
|
|
5810
6357
|
tools: toolsUsage,
|
|
5811
6358
|
hook: hookUsage,
|
|
5812
6359
|
loop: loopUsage,
|
|
5813
6360
|
ueye: ueyeUsage,
|
|
5814
6361
|
media: mediaUsage,
|
|
6362
|
+
verification: verificationUsage,
|
|
5815
6363
|
runtime: runtimeUsage,
|
|
5816
6364
|
mission: missionUsage,
|
|
5817
6365
|
update: updateUsage,
|
|
@@ -5839,9 +6387,12 @@ async function main() {
|
|
|
5839
6387
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
5840
6388
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
5841
6389
|
if (command === 'study-note') return studyNote(process.argv.slice(3));
|
|
6390
|
+
if (command === 'next-step') return nextStepCommand(process.argv.slice(3));
|
|
5842
6391
|
if (command === 'loop') return loop(process.argv.slice(3));
|
|
6392
|
+
if (command === 'scout') return scout(process.argv.slice(3));
|
|
5843
6393
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
5844
6394
|
if (command === 'media') return media(process.argv.slice(3));
|
|
6395
|
+
if (command === 'verification') return verification(process.argv.slice(3));
|
|
5845
6396
|
if (command === 'runtime') return runtime(process.argv.slice(3));
|
|
5846
6397
|
if (command === 'mission') return mission(process.argv.slice(3));
|
|
5847
6398
|
if (command === 'update') return updateCommand(process.argv.slice(3));
|