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/dist/bin/yam.js
CHANGED
|
@@ -11,10 +11,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { TRUTH_STATUSES, buildLoopReport, buildMediaGenerationProof, buildMissionCompletionGate, buildMissionPatchEnvelope, buildMissionSubagentReceipt, buildRollbackHint, buildRuntimeBackendEvidence, buildStudyNote, buildUeyeDesignCompletionGate, buildUeyeVisualProvenance, buildUeyeRunReport, buildUeyeSurfaceContext, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
|
|
12
12
|
import { archiveUeyeRevision, inspectImageFile, upsertUeyeAsset, verifyUeyeAssetManifest, verifyUeyeRevisionHistory } from '../lib/ueye-artifacts.js';
|
|
13
13
|
import { addPlanReviewComment, closePlanReviewCanvas, createDesignRevisionState, createPlanReviewCanvas, finalizeDesignProductionPhase, readCanonicalDesignRevisionState, readPlanReviewSession, recordDesignRevisionRound, verifyFinalGalleryManifest, verifyDesignProductionPhaseReceipt, writeFinalGalleryManifest } from '../lib/design-production.js';
|
|
14
|
-
import { inspectSkillInstallation, installSkillSetTransactional, uninstallSkillSetSafely } from '../lib/skill-installation.js';
|
|
14
|
+
import { inspectSkillInstallation, installSkillSetTransactional, planSkillSetInstallation, uninstallSkillSetSafely } from '../lib/skill-installation.js';
|
|
15
15
|
import { EXTERNAL_UPDATE_COMPONENTS, applyExternalUpdates, checkExternalUpdates } from '../lib/external-updates.js';
|
|
16
16
|
import { buildBoundedPromotionReport, parsePromotionSample } from '../lib/benchmark-promotion.js';
|
|
17
17
|
import { buildStrictGateResult, verifyStrictGateResult } from '../lib/gate-result.js';
|
|
18
|
+
import { createVerificationClosureReceipt, verifyVerificationClosureReceiptFile } from '../lib/verification-closure.js';
|
|
19
|
+
import { createMediaProviderReceipt, verifyMediaProviderReceipt } from '../lib/media-provider-receipt.js';
|
|
20
|
+
import { createScoutReceipt, verifyScoutReceiptFile } from '../lib/scout-receipt.js';
|
|
21
|
+
import { buildNextStep, verifyNextStep } from '../lib/next-step.js';
|
|
22
|
+
import { releaseRegistryStatusFromChecks } from '../lib/release-registry-status.js';
|
|
23
|
+
import { buildReleaseAuthReadiness, inspectTrustedPublisherWorkflowText } from '../lib/release-auth-readiness.js';
|
|
18
24
|
import { BoundedInputError, GENERAL_STDIN_MAX_BYTES, HOOK_STDIN_MAX_BYTES, readBoundedStdinJson, readBoundedStdinText } from '../lib/bounded-input.js';
|
|
19
25
|
function errorMessage(error) {
|
|
20
26
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -139,7 +145,9 @@ Usage:
|
|
|
139
145
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
140
146
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
141
147
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
148
|
+
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
142
149
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
150
|
+
yam scout receipt <create|verify> [options]
|
|
143
151
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
144
152
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
145
153
|
yam ueye preflight [dir] [--json]
|
|
@@ -152,18 +160,19 @@ Usage:
|
|
|
152
160
|
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
153
161
|
yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
|
|
154
162
|
yam mission gate [--expected-thread id] [--receipt file] [--json]
|
|
163
|
+
yam verification closure <create|verify> [options]
|
|
155
164
|
yam update check [--json]
|
|
156
165
|
yam update apply --component yam|scrapling|insane-search [--json]
|
|
157
166
|
yam update apply --all [--json]
|
|
158
167
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
159
168
|
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]
|
|
160
|
-
yam release report [--json]
|
|
169
|
+
yam release report [--auth-mode auto|oidc|token] [--json]
|
|
161
170
|
yam safety [text...]
|
|
162
171
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
163
172
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
164
173
|
yam template <project|ueye|mission|proof|tuning>
|
|
165
174
|
yam tune-log [dir]
|
|
166
|
-
yam install [--replace-user-skill name]
|
|
175
|
+
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
167
176
|
yam uninstall
|
|
168
177
|
yam doctor [--json]
|
|
169
178
|
yam examples
|
|
@@ -187,10 +196,20 @@ async function exists(file) {
|
|
|
187
196
|
async function rmrf(target) {
|
|
188
197
|
await fsp.rm(target, { recursive: true, force: true });
|
|
189
198
|
}
|
|
190
|
-
function
|
|
199
|
+
function installOptions(args = []) {
|
|
191
200
|
const skills = [];
|
|
201
|
+
let dryRun = false;
|
|
202
|
+
let json = false;
|
|
192
203
|
for (let index = 0; index < args.length; index += 1) {
|
|
193
204
|
const arg = String(args[index] || '');
|
|
205
|
+
if (arg === '--dry-run') {
|
|
206
|
+
dryRun = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (arg === '--json') {
|
|
210
|
+
json = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
194
213
|
if (arg === '--replace-user-skill') {
|
|
195
214
|
const skill = String(args[index + 1] || '');
|
|
196
215
|
if (!skill || skill.startsWith('--')) {
|
|
@@ -209,20 +228,32 @@ function installReplacementSkills(args = []) {
|
|
|
209
228
|
}
|
|
210
229
|
throw new Error(`unknown yam install argument: ${arg}`);
|
|
211
230
|
}
|
|
212
|
-
|
|
231
|
+
if (json && !dryRun)
|
|
232
|
+
throw new Error('yam install --json currently requires --dry-run');
|
|
233
|
+
return { replaceSkills: [...new Set(skills)], dryRun, json };
|
|
213
234
|
}
|
|
214
235
|
async function install(args = []) {
|
|
215
|
-
const
|
|
216
|
-
const
|
|
236
|
+
const options = installOptions(args);
|
|
237
|
+
const installInput = {
|
|
217
238
|
sourceRoot: ROOT,
|
|
218
239
|
destination: DEST,
|
|
219
240
|
codexMirror: CODEX_MIRROR,
|
|
220
241
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
221
242
|
version: VERSION,
|
|
222
243
|
skills: SKILLS,
|
|
223
|
-
replaceSkills,
|
|
244
|
+
replaceSkills: options.replaceSkills,
|
|
224
245
|
legacySkills: LEGACY_SKILLS,
|
|
225
246
|
retiredSkills: RETIRED_SKILLS
|
|
247
|
+
};
|
|
248
|
+
if (options.dryRun) {
|
|
249
|
+
const plan = await planSkillSetInstallation(installInput);
|
|
250
|
+
printJsonOrHuman(plan, options.json, 'yam install dry run');
|
|
251
|
+
if (!plan.ready)
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const result = await installSkillSetTransactional({
|
|
256
|
+
...installInput
|
|
226
257
|
});
|
|
227
258
|
console.log(`yam installed to ${DEST}`);
|
|
228
259
|
console.log(`install receipt: ${result.receiptPath}`);
|
|
@@ -302,6 +333,86 @@ async function loadManifest() {
|
|
|
302
333
|
async function readJson(file) {
|
|
303
334
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
304
335
|
}
|
|
336
|
+
async function readBoundedRegularJson(file, label = 'JSON input', maxBytes = GENERAL_STDIN_MAX_BYTES) {
|
|
337
|
+
const requested = path.resolve(expandHome(String(file || '')));
|
|
338
|
+
const physicalParent = await fsp.realpath(path.dirname(requested));
|
|
339
|
+
const target = path.join(physicalParent, path.basename(requested));
|
|
340
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
341
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
342
|
+
? fs.constants.O_NOFOLLOW
|
|
343
|
+
: 0;
|
|
344
|
+
const before = await fsp.lstat(target);
|
|
345
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
346
|
+
throw new Error(`${label} must be a regular JSON file no larger than ${maxBytes} bytes`);
|
|
347
|
+
}
|
|
348
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
349
|
+
try {
|
|
350
|
+
const opened = await handle.stat();
|
|
351
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
352
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > maxBytes) {
|
|
353
|
+
throw new Error(`${label} must be a stable regular JSON file no larger than ${maxBytes} bytes`);
|
|
354
|
+
}
|
|
355
|
+
const bytes = await readBoundedFileHandle(handle, maxBytes);
|
|
356
|
+
const afterOpened = await handle.stat();
|
|
357
|
+
const after = await fsp.lstat(target);
|
|
358
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
359
|
+
if (bytes.length > maxBytes
|
|
360
|
+
|| after.isSymbolicLink()
|
|
361
|
+
|| !after.isFile()
|
|
362
|
+
|| after.dev !== opened.dev
|
|
363
|
+
|| after.ino !== opened.ino
|
|
364
|
+
|| afterOpened.dev !== opened.dev
|
|
365
|
+
|| afterOpened.ino !== opened.ino
|
|
366
|
+
|| afterOpened.size !== bytes.length) {
|
|
367
|
+
throw new Error(`${label} changed identity or size while being read`);
|
|
368
|
+
}
|
|
369
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
await handle.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function captureAbsoluteRegularDirectoryPath(target, label) {
|
|
376
|
+
const absolute = path.resolve(target);
|
|
377
|
+
const filesystemRoot = path.parse(absolute).root;
|
|
378
|
+
const relative = path.relative(filesystemRoot, absolute);
|
|
379
|
+
const identities = [];
|
|
380
|
+
let current = filesystemRoot;
|
|
381
|
+
for (const segment of relative ? ['', ...relative.split(path.sep)] : ['']) {
|
|
382
|
+
if (segment)
|
|
383
|
+
current = path.join(current, segment);
|
|
384
|
+
const stat = await fsp.lstat(current);
|
|
385
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
386
|
+
throw new Error(`${label} parent must be a regular physical directory: ${current}`);
|
|
387
|
+
}
|
|
388
|
+
identities.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
389
|
+
}
|
|
390
|
+
return identities;
|
|
391
|
+
}
|
|
392
|
+
async function revalidateAbsoluteRegularDirectoryPath(identities, label) {
|
|
393
|
+
for (const identity of identities) {
|
|
394
|
+
const stat = await fsp.lstat(identity.path);
|
|
395
|
+
if (stat.isSymbolicLink()
|
|
396
|
+
|| !stat.isDirectory()
|
|
397
|
+
|| stat.dev !== identity.dev
|
|
398
|
+
|| stat.ino !== identity.ino) {
|
|
399
|
+
throw new Error(`${label} parent changed identity while being read: ${identity.path}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function readBoundedFileHandle(handle, maxBytes) {
|
|
404
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
405
|
+
let total = 0;
|
|
406
|
+
while (total < buffer.length) {
|
|
407
|
+
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
|
|
408
|
+
if (bytesRead === 0)
|
|
409
|
+
break;
|
|
410
|
+
total += bytesRead;
|
|
411
|
+
}
|
|
412
|
+
if (total > maxBytes)
|
|
413
|
+
throw new Error(`JSON input exceeds ${maxBytes} bytes while being read`);
|
|
414
|
+
return buffer.subarray(0, total);
|
|
415
|
+
}
|
|
305
416
|
async function readJsonOrDefault(file, fallback = {}) {
|
|
306
417
|
try {
|
|
307
418
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
@@ -803,8 +914,9 @@ Usage:
|
|
|
803
914
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
804
915
|
|
|
805
916
|
Notes:
|
|
806
|
-
Read-only guard. It checks whether changed work has a Study Note
|
|
807
|
-
|
|
917
|
+
Read-only guard. It checks whether changed work has a complete Study Note followed immediately by Next step,
|
|
918
|
+
including relevant UI/CSS/DB hygiene and the ordered fix-first/planned handoff contract.
|
|
919
|
+
It does not generate, edit, or infer either section for you.
|
|
808
920
|
`);
|
|
809
921
|
}
|
|
810
922
|
async function studyNoteCheck(args = []) {
|
|
@@ -824,21 +936,30 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
824
936
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
825
937
|
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
826
938
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
939
|
+
const studyNoteText = studyNoteSectionText(reportText);
|
|
827
940
|
const checks = [
|
|
828
941
|
studyNoteGuardCheck('changed_files', changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial', changeScopeAvailable
|
|
829
942
|
? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
|
|
830
943
|
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'),
|
|
831
944
|
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
832
|
-
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(
|
|
833
|
-
studyNoteGuardCheck('execution_point', scopeAwareStatus(/(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(
|
|
834
|
-
studyNoteGuardCheck('before_after_or_change', scopeAwareStatus(/(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(
|
|
835
|
-
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(
|
|
836
|
-
studyNoteGuardCheck('syntax_or_structure', scopeAwareStatus(/(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(
|
|
837
|
-
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(
|
|
838
|
-
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(
|
|
945
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(studyNoteText)), 'Study Note should explain what the touched code/artifact does'),
|
|
946
|
+
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'),
|
|
947
|
+
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'),
|
|
948
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(studyNoteText)), 'Study Note should describe the expected behavior or result'),
|
|
949
|
+
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'),
|
|
950
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(studyNoteText)), 'Study Note should say what was checked'),
|
|
951
|
+
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'),
|
|
952
|
+
studyNoteGuardCheck('next_step_present', scopeAwareStatus(hasNextStepMarker(reportText)), 'changed-artifact reports should include a Next step section'),
|
|
953
|
+
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'),
|
|
954
|
+
studyNoteGuardCheck('study_note_then_next_step', scopeAwareStatus(hasStudyNoteThenNextStepOrder(reportText)), 'Next step should be the next report section immediately after Study Note'),
|
|
955
|
+
studyNoteGuardCheck('next_step_whole_process_scan', scopeAwareStatus(hasNextStepWholeProcessScan(reportText)), 'Next step should include current situation, forward outlook, a critical opinion, and an improvement recommendation'),
|
|
956
|
+
studyNoteGuardCheck('next_step_ordered_actions', scopeAwareStatus(hasNextStepOrderedActions(reportText)), 'Next step should include an ordered action sequence with fix-first work before planned work'),
|
|
957
|
+
studyNoteGuardCheck('next_step_evidence_and_ownership', scopeAwareStatus(hasNextStepEvidenceAndOwnership(reportText)), 'Next step should name evidence level or stamp plus the owner route or scope'),
|
|
958
|
+
studyNoteGuardCheck('next_step_safety_context', scopeAwareStatus(hasNextStepSafetyContext(reportText)), 'Next step should state blocker status, safe retry guidance, and side effects'),
|
|
959
|
+
studyNoteGuardCheck('next_step_truth_status', scopeAwareStatus(hasNextStepTruthStatus(reportText)), 'Next step should state its evidence-bounded truth status')
|
|
839
960
|
];
|
|
840
961
|
for (const requirement of hygieneRequirements) {
|
|
841
|
-
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(
|
|
962
|
+
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
842
963
|
}
|
|
843
964
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
844
965
|
const result = {
|
|
@@ -923,6 +1044,119 @@ async function readStudyNoteReportText(flags, dir) {
|
|
|
923
1044
|
function hasStudyNoteMarker(text = '') {
|
|
924
1045
|
return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
|
|
925
1046
|
}
|
|
1047
|
+
function hasNextStepMarker(text = '') {
|
|
1048
|
+
return /(next step|next-step|다음\s*(?:단계|할\s*일|작업))/i.test(text);
|
|
1049
|
+
}
|
|
1050
|
+
function reportSectionMarkers(text = '') {
|
|
1051
|
+
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1052
|
+
const trimmed = line.trim();
|
|
1053
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1054
|
+
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1055
|
+
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
1056
|
+
if (!rawLabel)
|
|
1057
|
+
return null;
|
|
1058
|
+
const label = rawLabel.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
|
1059
|
+
const kind = /(study note|학습 노트|스터디 노트)/i.test(label)
|
|
1060
|
+
? 'study_note'
|
|
1061
|
+
: /(next step|다음 단계|다음 할 일|다음 작업)/i.test(label)
|
|
1062
|
+
? 'next_step'
|
|
1063
|
+
: 'other';
|
|
1064
|
+
return { index, kind, level: heading?.[0].match(/^#+/)?.[0].length || 0 };
|
|
1065
|
+
}).filter((item) => Boolean(item));
|
|
1066
|
+
}
|
|
1067
|
+
function hasStudyNoteThenNextStepOrder(text = '') {
|
|
1068
|
+
const markers = reportSectionMarkers(text);
|
|
1069
|
+
const studyIndex = markers.findIndex((item) => item.kind === 'study_note');
|
|
1070
|
+
if (studyIndex < 0)
|
|
1071
|
+
return false;
|
|
1072
|
+
return nextPeerSection(markers, studyIndex)?.kind === 'next_step';
|
|
1073
|
+
}
|
|
1074
|
+
function hasSingleStudyNoteNextStepPair(text = '') {
|
|
1075
|
+
const markers = reportSectionMarkers(text);
|
|
1076
|
+
return markers.filter((item) => item.kind === 'study_note').length === 1
|
|
1077
|
+
&& markers.filter((item) => item.kind === 'next_step').length === 1;
|
|
1078
|
+
}
|
|
1079
|
+
function hasNextStepWholeProcessScan(text = '') {
|
|
1080
|
+
const value = nextStepSectionText(text);
|
|
1081
|
+
const situation = /(situation|current state|현재\s*(?:상황|상태))/i.test(value);
|
|
1082
|
+
const outlook = /(outlook|forward|앞으로|향후|전망)/i.test(value);
|
|
1083
|
+
const critical = /(critical opinion|critique|비판적|비판\s*의견|tradeoff|약점)/i.test(value);
|
|
1084
|
+
const recommendation = /(recommendation|recommend|개선\s*추천|추천\s*의견|권장)/i.test(value);
|
|
1085
|
+
return situation && outlook && critical && recommendation;
|
|
1086
|
+
}
|
|
1087
|
+
function hasNextStepOrderedActions(text = '') {
|
|
1088
|
+
const value = nextStepSectionText(text);
|
|
1089
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1090
|
+
const actionLines = lines.filter((line) => /^\d+[.)]\s+/.test(line));
|
|
1091
|
+
const labeledLines = lines.filter((line) => /\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i.test(line));
|
|
1092
|
+
if (!actionLines.length || !/^1[.)]\s+/.test(actionLines[0]))
|
|
1093
|
+
return false;
|
|
1094
|
+
if (labeledLines.length !== actionLines.length || labeledLines.some((line) => !/^\d+[.)]\s+/.test(line)))
|
|
1095
|
+
return false;
|
|
1096
|
+
let plannedSeen = false;
|
|
1097
|
+
for (const [index, line] of actionLines.entries()) {
|
|
1098
|
+
const number = Number(line.match(/^(\d+)[.)]/)?.[1] || 0);
|
|
1099
|
+
if (number !== index + 1)
|
|
1100
|
+
return false;
|
|
1101
|
+
const kind = line.match(/\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i)?.[1] || '';
|
|
1102
|
+
if (!kind)
|
|
1103
|
+
return false;
|
|
1104
|
+
const planned = /planned|계획/i.test(kind);
|
|
1105
|
+
if (planned)
|
|
1106
|
+
plannedSeen = true;
|
|
1107
|
+
else if (plannedSeen)
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
return true;
|
|
1111
|
+
}
|
|
1112
|
+
function hasNextStepEvidenceAndOwnership(text = '') {
|
|
1113
|
+
const value = nextStepSectionText(text);
|
|
1114
|
+
const evidence = /(evidence(?:[_ -](?:level|stamp))?|\bL[0-5]\b|근거|증거|검증\s*(?:수준|스탬프))/i.test(value);
|
|
1115
|
+
const ownership = /(owner(?:[_ -](?:route|scope))?|담당|소유|책임\s*(?:경로|범위|주체))/i.test(value);
|
|
1116
|
+
return evidence && ownership;
|
|
1117
|
+
}
|
|
1118
|
+
function hasNextStepSafetyContext(text = '') {
|
|
1119
|
+
const value = nextStepSectionText(text);
|
|
1120
|
+
const blocker = /(blocker|blocked(?:[_ -]by)?|차단|막힘|방해)/i.test(value);
|
|
1121
|
+
const safeRetry = /(safe[_ -]retry|안전[^\n]{0,20}재시도|재시도[^\n]{0,20}안전)/i.test(value);
|
|
1122
|
+
const sideEffects = /(side[_ -]effects?|부작용|영향)/i.test(value);
|
|
1123
|
+
return blocker && safeRetry && sideEffects;
|
|
1124
|
+
}
|
|
1125
|
+
function hasNextStepTruthStatus(text = '') {
|
|
1126
|
+
return /(truth[_ -]status|진실\s*상태|검증\s*상태)/i.test(nextStepSectionText(text));
|
|
1127
|
+
}
|
|
1128
|
+
function nextStepSectionText(text = '') {
|
|
1129
|
+
const value = String(text || '');
|
|
1130
|
+
const lines = value.split(/\r?\n/);
|
|
1131
|
+
const markers = reportSectionMarkers(value);
|
|
1132
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1133
|
+
if (study < 0)
|
|
1134
|
+
return '';
|
|
1135
|
+
const adjacent = nextPeerSectionIndex(markers, study);
|
|
1136
|
+
if (adjacent < 0 || markers[adjacent].kind !== 'next_step')
|
|
1137
|
+
return '';
|
|
1138
|
+
const end = nextPeerSection(markers, adjacent);
|
|
1139
|
+
return lines.slice(markers[adjacent].index + 1, end?.index ?? lines.length).join('\n');
|
|
1140
|
+
}
|
|
1141
|
+
function studyNoteSectionText(text = '') {
|
|
1142
|
+
const value = String(text || '');
|
|
1143
|
+
const lines = value.split(/\r?\n/);
|
|
1144
|
+
const markers = reportSectionMarkers(value);
|
|
1145
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1146
|
+
if (study < 0)
|
|
1147
|
+
return '';
|
|
1148
|
+
const end = nextPeerSection(markers, study);
|
|
1149
|
+
return lines.slice(markers[study].index + 1, end?.index ?? lines.length).join('\n');
|
|
1150
|
+
}
|
|
1151
|
+
function nextPeerSection(markers, start) {
|
|
1152
|
+
const nextIndex = nextPeerSectionIndex(markers, start);
|
|
1153
|
+
return nextIndex < 0 ? undefined : markers[nextIndex];
|
|
1154
|
+
}
|
|
1155
|
+
function nextPeerSectionIndex(markers, start) {
|
|
1156
|
+
const current = markers[start];
|
|
1157
|
+
const relative = markers.slice(start + 1).findIndex((item) => current.level === 0 || item.level === 0 || item.level <= current.level);
|
|
1158
|
+
return relative < 0 ? -1 : start + 1 + relative;
|
|
1159
|
+
}
|
|
926
1160
|
function studyNoteGuardCheck(id, status, nextAction) {
|
|
927
1161
|
return {
|
|
928
1162
|
id,
|
|
@@ -1364,6 +1598,8 @@ function buildCapabilityMatrix(readinessRows = [], liteHook = {}, studyNoteHook
|
|
|
1364
1598
|
capabilityRow('bounded_design_revisions', 'beta', 'ready', 'maximum two reviewer-finding-backed revision rounds', 'round state does not replace Ueye artifact hash verification'),
|
|
1365
1599
|
capabilityRow('final_gallery_manifest', 'beta', 'ready', 'path-confined hash-verified packaging manifest', 'gallery completeness does not prove visual or license correctness'),
|
|
1366
1600
|
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'),
|
|
1601
|
+
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'),
|
|
1602
|
+
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'),
|
|
1367
1603
|
capabilityRow('yam_lite_hook', 'beta', String(liteHook.state || readiness.get('yam-lite hook') || 'unknown'), 'optional hook health inspection', 'advisory only; disabled unless explicitly enabled'),
|
|
1368
1604
|
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')
|
|
1369
1605
|
];
|
|
@@ -2133,26 +2369,27 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
2133
2369
|
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2134
2370
|
const changedFiles = changedFileDetection.files;
|
|
2135
2371
|
const lines = [
|
|
2136
|
-
'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.'
|
|
2372
|
+
'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.'
|
|
2137
2373
|
];
|
|
2138
2374
|
if (!changedFileDetection.available) {
|
|
2139
2375
|
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2140
|
-
lines.push('Determine manually whether artifacts changed and include
|
|
2376
|
+
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.');
|
|
2141
2377
|
return lines.join('\n');
|
|
2142
2378
|
}
|
|
2143
2379
|
if (!changedFiles.length) {
|
|
2144
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add
|
|
2380
|
+
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.');
|
|
2145
2381
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2146
2382
|
return lines.join('\n');
|
|
2147
2383
|
}
|
|
2148
2384
|
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2149
2385
|
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2386
|
+
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.');
|
|
2150
2387
|
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2151
2388
|
if (hygiene.length) {
|
|
2152
2389
|
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2153
2390
|
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.');
|
|
2154
2391
|
}
|
|
2155
|
-
lines.push('This prompt reminder does not generate or edit
|
|
2392
|
+
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.');
|
|
2156
2393
|
return lines.join('\n');
|
|
2157
2394
|
}
|
|
2158
2395
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
@@ -2232,12 +2469,16 @@ async function safety(args = []) {
|
|
|
2232
2469
|
async function release(args = []) {
|
|
2233
2470
|
const subcommand = args[0] || 'help';
|
|
2234
2471
|
if (subcommand !== 'report') {
|
|
2235
|
-
console.error('usage: yam release report [--json]');
|
|
2472
|
+
console.error('usage: yam release report [--auth-mode auto|oidc|token] [--json]');
|
|
2236
2473
|
process.exitCode = 1;
|
|
2237
2474
|
return;
|
|
2238
2475
|
}
|
|
2239
|
-
const
|
|
2240
|
-
const
|
|
2476
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['auth-mode', 'json']));
|
|
2477
|
+
const authMode = String(flags.auth_mode || 'auto');
|
|
2478
|
+
if (!['auto', 'oidc', 'token'].includes(authMode))
|
|
2479
|
+
throw new Error('--auth-mode must be auto, oidc, or token');
|
|
2480
|
+
const asJson = Boolean(flags.json);
|
|
2481
|
+
const report = runReleaseReport(authMode);
|
|
2241
2482
|
if (asJson) {
|
|
2242
2483
|
console.log(JSON.stringify(report, null, 2));
|
|
2243
2484
|
if (!report.ok)
|
|
@@ -2265,7 +2506,7 @@ async function release(args = []) {
|
|
|
2265
2506
|
if (report.publish_readiness) {
|
|
2266
2507
|
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
2267
2508
|
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
2268
|
-
console.log(`-
|
|
2509
|
+
console.log(`- Publish auth: ${report.publish_readiness.auth.mode} (${report.publish_readiness.auth.status})`);
|
|
2269
2510
|
}
|
|
2270
2511
|
if (report.failed.length) {
|
|
2271
2512
|
console.log('- Failed:');
|
|
@@ -2290,7 +2531,7 @@ async function release(args = []) {
|
|
|
2290
2531
|
if (!report.ok)
|
|
2291
2532
|
process.exitCode = 1;
|
|
2292
2533
|
}
|
|
2293
|
-
function runReleaseReport() {
|
|
2534
|
+
function runReleaseReport(authMode = 'auto') {
|
|
2294
2535
|
const startedAt = new Date().toISOString();
|
|
2295
2536
|
const checks = [
|
|
2296
2537
|
['typecheck', ['npm', ['run', 'typecheck']]],
|
|
@@ -2305,7 +2546,7 @@ function runReleaseReport() {
|
|
|
2305
2546
|
const tarball = releaseTarballProvenance();
|
|
2306
2547
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
2307
2548
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
2308
|
-
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
2549
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence, authMode);
|
|
2309
2550
|
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
2310
2551
|
const preliminaryOk = failed.length === 0 && publishReadiness.status === 'ready';
|
|
2311
2552
|
const readinessReceipt = releaseReadinessReceipt(provenance, freshness, tarball, publishReadiness, preliminaryOk);
|
|
@@ -2370,9 +2611,14 @@ function releaseReadinessReceipt(provenance = {}, freshness = {}, tarball = {},
|
|
|
2370
2611
|
truth_status: publishReadiness.registry?.truth_status || 'partial'
|
|
2371
2612
|
},
|
|
2372
2613
|
auth: {
|
|
2373
|
-
|
|
2614
|
+
requested_mode: String(publishReadiness.auth?.requested_mode || 'auto'),
|
|
2615
|
+
mode: String(publishReadiness.auth?.mode || 'manual_token'),
|
|
2616
|
+
command: String(publishReadiness.auth?.command || 'npm whoami'),
|
|
2617
|
+
required_for_selected_mode: Boolean(publishReadiness.auth?.required_for_selected_mode),
|
|
2374
2618
|
status: String(publishReadiness.auth?.status || 'not_verified'),
|
|
2375
|
-
account: publishReadiness.auth?.
|
|
2619
|
+
account: String(publishReadiness.auth?.account || ''),
|
|
2620
|
+
local_token: publishReadiness.auth?.local_token || null,
|
|
2621
|
+
trusted_publisher: publishReadiness.auth?.trusted_publisher || null,
|
|
2376
2622
|
truth_status: publishReadiness.auth?.truth_status || 'partial'
|
|
2377
2623
|
},
|
|
2378
2624
|
git: {
|
|
@@ -2471,15 +2717,33 @@ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
|
|
|
2471
2717
|
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
2472
2718
|
};
|
|
2473
2719
|
}
|
|
2474
|
-
function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, publishBlockers = []) {
|
|
2720
|
+
function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, publishBlockers = [], authMode = 'auto') {
|
|
2475
2721
|
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
2476
2722
|
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
2477
2723
|
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
2478
2724
|
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
2479
|
-
const
|
|
2725
|
+
const trustedPublisherWorkflowPath = '.github/workflows/release.yml';
|
|
2726
|
+
const trustedPublisherWorkflowFile = path.join(ROOT, trustedPublisherWorkflowPath);
|
|
2727
|
+
const trustedPublisherWorkflowExists = fs.existsSync(trustedPublisherWorkflowFile);
|
|
2728
|
+
const trustedPublisherWorkflow = inspectTrustedPublisherWorkflowText(trustedPublisherWorkflowExists ? fs.readFileSync(trustedPublisherWorkflowFile, 'utf8') : '', trustedPublisherWorkflowPath, trustedPublisherWorkflowExists);
|
|
2729
|
+
const authReadiness = buildReleaseAuthReadiness({
|
|
2730
|
+
workflow: trustedPublisherWorkflow,
|
|
2731
|
+
local_token: {
|
|
2732
|
+
command: 'npm whoami',
|
|
2733
|
+
ok: whoamiProbe.ok,
|
|
2734
|
+
note: whoamiProbe.note
|
|
2735
|
+
},
|
|
2736
|
+
requested_mode: authMode
|
|
2737
|
+
});
|
|
2738
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks, {
|
|
2739
|
+
package_name: PACKAGE_JSON.name,
|
|
2740
|
+
version: VERSION
|
|
2741
|
+
});
|
|
2480
2742
|
const latestProbe = registryStatus.checked
|
|
2481
2743
|
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
2482
|
-
:
|
|
2744
|
+
: registryStatus.parse_failed
|
|
2745
|
+
? { ok: false, stdout: '', note: `registry check output did not prove ${PACKAGE_JSON.name}@${VERSION} is unpublished: ${registryStatus.note}` }
|
|
2746
|
+
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
2483
2747
|
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
2484
2748
|
const blockers = [];
|
|
2485
2749
|
const versionStatus = latestProbe.ok
|
|
@@ -2488,8 +2752,8 @@ function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, pub
|
|
|
2488
2752
|
if (!registryProbe.ok) {
|
|
2489
2753
|
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'));
|
|
2490
2754
|
}
|
|
2491
|
-
|
|
2492
|
-
blockers.push(readinessBlocker(
|
|
2755
|
+
for (const blocker of authReadiness.blockers) {
|
|
2756
|
+
blockers.push(readinessBlocker(blocker.kind, blocker.reason, blocker.safe_next_action, blocker.command, blocker.severity));
|
|
2493
2757
|
}
|
|
2494
2758
|
if (!latestProbe.ok) {
|
|
2495
2759
|
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
@@ -2528,30 +2792,22 @@ function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, pub
|
|
|
2528
2792
|
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
2529
2793
|
},
|
|
2530
2794
|
auth: {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2795
|
+
requested_mode: authReadiness.requested_mode,
|
|
2796
|
+
mode: authReadiness.mode,
|
|
2797
|
+
command: authReadiness.local_token.command,
|
|
2798
|
+
required_for_selected_mode: authReadiness.local_token.required_for_selected_mode,
|
|
2799
|
+
status: authReadiness.status,
|
|
2800
|
+
account: authReadiness.local_token.account,
|
|
2801
|
+
note: authReadiness.next_action,
|
|
2802
|
+
local_token: authReadiness.local_token,
|
|
2803
|
+
trusted_publisher: authReadiness.trusted_publisher,
|
|
2804
|
+
truth_status: authReadiness.truth_status
|
|
2536
2805
|
},
|
|
2537
2806
|
blockers: uniqueBlockers,
|
|
2538
2807
|
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
2539
2808
|
truth_status: status === 'ready' ? 'verified' : 'blocked'
|
|
2540
2809
|
};
|
|
2541
2810
|
}
|
|
2542
|
-
function releaseRegistryStatusFromChecks(checks = []) {
|
|
2543
|
-
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
2544
|
-
const note = String(registryCheck?.note || '');
|
|
2545
|
-
if (registryCheck?.status !== 'passed')
|
|
2546
|
-
return { checked: false, latest_version: '', not_published: false, note };
|
|
2547
|
-
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
2548
|
-
return {
|
|
2549
|
-
checked: true,
|
|
2550
|
-
latest_version: latest,
|
|
2551
|
-
not_published: /not published yet/i.test(note),
|
|
2552
|
-
note
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
2811
|
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
2556
2812
|
return {
|
|
2557
2813
|
kind,
|
|
@@ -2586,13 +2842,13 @@ function releaseStudyNote(publishReadiness = {}, publishBlockers = []) {
|
|
|
2586
2842
|
}
|
|
2587
2843
|
return buildStudyNote({
|
|
2588
2844
|
issue_code: 'npm publish readiness',
|
|
2589
|
-
issue_role: 'This part checks registry, version,
|
|
2845
|
+
issue_role: 'This part checks registry, version, the selected OIDC or manual-token authentication path, tarball, and local release checks before any public publish.',
|
|
2590
2846
|
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
2591
2847
|
changed_code: 'yam release report',
|
|
2592
2848
|
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
2593
|
-
change_summary: 'The report
|
|
2594
|
-
why_important: '
|
|
2595
|
-
learning_note: '
|
|
2849
|
+
change_summary: 'The report separates Trusted Publisher OIDC evidence from optional local-token evidence and gives a path-specific safe next action.',
|
|
2850
|
+
why_important: 'npm whoami cannot prove OIDC readiness; treating its E401 as an OIDC blocker would recommend the wrong credential model.',
|
|
2851
|
+
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.'
|
|
2596
2852
|
});
|
|
2597
2853
|
}
|
|
2598
2854
|
function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishBlockers = [], publishReadiness = {}) {
|
|
@@ -2764,20 +3020,128 @@ async function loop(args = []) {
|
|
|
2764
3020
|
process.exitCode = 1;
|
|
2765
3021
|
return loopUsage();
|
|
2766
3022
|
}
|
|
3023
|
+
async function scout(args = []) {
|
|
3024
|
+
const subcommand = args[0] || 'help';
|
|
3025
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3026
|
+
return scoutUsage();
|
|
3027
|
+
if (subcommand === 'receipt')
|
|
3028
|
+
return scoutReceipt(args.slice(1));
|
|
3029
|
+
console.error(`unknown scout command: ${subcommand}`);
|
|
3030
|
+
process.exitCode = 1;
|
|
3031
|
+
return scoutUsage();
|
|
3032
|
+
}
|
|
3033
|
+
function scoutUsage() {
|
|
3034
|
+
console.log(`yam scout
|
|
3035
|
+
|
|
3036
|
+
Immutable, operator-supplied research evidence helpers. These commands do not browse, fetch, or trust external content as instructions.
|
|
3037
|
+
|
|
3038
|
+
Usage:
|
|
3039
|
+
yam scout receipt create [--root dir] --receipt-path .yam/scout/run.json --spec scout-spec.json [--json]
|
|
3040
|
+
yam scout receipt verify [--root dir] --receipt-path .yam/scout/run.json [--json]
|
|
3041
|
+
`);
|
|
3042
|
+
}
|
|
3043
|
+
async function scoutReceipt(args = []) {
|
|
3044
|
+
const action = args[0] || 'help';
|
|
3045
|
+
if (action === 'help' || action === '--help' || action === '-h')
|
|
3046
|
+
return scoutUsage();
|
|
3047
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['root', 'receipt-path', 'spec', 'json']));
|
|
3048
|
+
const root = path.resolve(expandHome(String(flags.root || process.cwd())));
|
|
3049
|
+
try {
|
|
3050
|
+
let result;
|
|
3051
|
+
if (action === 'create') {
|
|
3052
|
+
if (!flags.receipt_path || !flags.spec)
|
|
3053
|
+
throw new Error('create requires --receipt-path and --spec');
|
|
3054
|
+
result = await createScoutReceipt({
|
|
3055
|
+
root,
|
|
3056
|
+
receipt_path: String(flags.receipt_path),
|
|
3057
|
+
spec: await readBoundedRegularJson(flags.spec, 'Scout spec')
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
else if (action === 'verify') {
|
|
3061
|
+
if (!flags.receipt_path)
|
|
3062
|
+
throw new Error('verify requires --receipt-path');
|
|
3063
|
+
result = await verifyScoutReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
throw new Error(`unknown Scout receipt action: ${action}`);
|
|
3067
|
+
}
|
|
3068
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3069
|
+
if (result.truth_status === 'blocked')
|
|
3070
|
+
process.exitCode = 1;
|
|
3071
|
+
}
|
|
3072
|
+
catch (error) {
|
|
3073
|
+
const result = {
|
|
3074
|
+
schema: 'yam.scout-receipt-error.v1',
|
|
3075
|
+
truth_status: 'blocked',
|
|
3076
|
+
error: errorMessage(error),
|
|
3077
|
+
next_action: 'repair the confined receipt path, canonical entity, version clocks, source digests, claim links, or failure taxonomy before retrying'
|
|
3078
|
+
};
|
|
3079
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3080
|
+
process.exitCode = 1;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function nextStepCommand(args = []) {
|
|
3084
|
+
const subcommand = args[0] || 'help';
|
|
3085
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3086
|
+
return nextStepUsage();
|
|
3087
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['spec', 'receipt', 'json']));
|
|
3088
|
+
try {
|
|
3089
|
+
let result;
|
|
3090
|
+
if (subcommand === 'report') {
|
|
3091
|
+
if (!flags.spec)
|
|
3092
|
+
throw new Error('report requires --spec');
|
|
3093
|
+
result = buildNextStep(await readBoundedRegularJson(flags.spec, 'Next step spec'));
|
|
3094
|
+
}
|
|
3095
|
+
else if (subcommand === 'verify') {
|
|
3096
|
+
if (!flags.receipt)
|
|
3097
|
+
throw new Error('verify requires --receipt');
|
|
3098
|
+
result = verifyNextStep(await readBoundedRegularJson(flags.receipt, 'Next step receipt'));
|
|
3099
|
+
}
|
|
3100
|
+
else {
|
|
3101
|
+
throw new Error(`unknown Next step command: ${subcommand}`);
|
|
3102
|
+
}
|
|
3103
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3104
|
+
if (result.truth_status === 'blocked')
|
|
3105
|
+
process.exitCode = 1;
|
|
3106
|
+
}
|
|
3107
|
+
catch (error) {
|
|
3108
|
+
const result = {
|
|
3109
|
+
schema: 'yam.next-step-error.v1',
|
|
3110
|
+
truth_status: 'blocked',
|
|
3111
|
+
error: errorMessage(error),
|
|
3112
|
+
next_action: 'repair the whole-process scan, ordered steps, evidence level/stamp, owner scope, blockers, or safe retry before retrying'
|
|
3113
|
+
};
|
|
3114
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3115
|
+
process.exitCode = 1;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
function nextStepUsage() {
|
|
3119
|
+
console.log(`yam next-step
|
|
3120
|
+
|
|
3121
|
+
Build or verify an evidence-bound ordered handoff after a quick whole-process scan.
|
|
3122
|
+
|
|
3123
|
+
Usage:
|
|
3124
|
+
yam next-step report --spec next-step-spec.json [--json]
|
|
3125
|
+
yam next-step verify --receipt next-step-receipt.json [--json]
|
|
3126
|
+
`);
|
|
3127
|
+
}
|
|
2767
3128
|
function loopUsage() {
|
|
2768
3129
|
console.log(`yam loop
|
|
2769
3130
|
|
|
2770
|
-
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers,
|
|
3131
|
+
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.
|
|
2771
3132
|
|
|
2772
3133
|
Usage:
|
|
2773
|
-
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]
|
|
3134
|
+
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]
|
|
2774
3135
|
`);
|
|
2775
3136
|
}
|
|
2776
3137
|
async function loopReport(args = []) {
|
|
2777
3138
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
2778
3139
|
return loopUsage();
|
|
2779
|
-
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']));
|
|
3140
|
+
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']));
|
|
2780
3141
|
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
3142
|
+
const nextStepSpec = flags.next_step_spec
|
|
3143
|
+
? await readBoundedRegularJson(flags.next_step_spec, 'Next step spec')
|
|
3144
|
+
: undefined;
|
|
2781
3145
|
const report = buildLoopReport({
|
|
2782
3146
|
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
2783
3147
|
intent: String(flags.intent || ''),
|
|
@@ -2816,6 +3180,7 @@ async function loopReport(args = []) {
|
|
|
2816
3180
|
scope_owner: String(flags.scope_owner || flags.owner || ''),
|
|
2817
3181
|
side_effects: arrayFlag(flags.side_effect),
|
|
2818
3182
|
avoidance_note: String(flags.avoidance_note || ''),
|
|
3183
|
+
next_step: nextStepSpec,
|
|
2819
3184
|
issue_code: String(flags.issue_code || ''),
|
|
2820
3185
|
issue_role: String(flags.issue_role || ''),
|
|
2821
3186
|
issue_symptom: String(flags.issue_symptom || ''),
|
|
@@ -3141,17 +3506,62 @@ async function readProductionSpecs(value, label) {
|
|
|
3141
3506
|
specs.push(await readBoundedJsonSpec(path.resolve(expandHome(file)), label));
|
|
3142
3507
|
return specs;
|
|
3143
3508
|
}
|
|
3509
|
+
async function readOptionalSpecs(value, label) {
|
|
3510
|
+
const files = arrayFlag(value);
|
|
3511
|
+
const specs = [];
|
|
3512
|
+
for (const file of files)
|
|
3513
|
+
specs.push(await readBoundedJsonSpec(path.resolve(expandHome(file)), label));
|
|
3514
|
+
return specs;
|
|
3515
|
+
}
|
|
3144
3516
|
async function readBoundedJsonSpec(file, label) {
|
|
3145
|
-
const
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3517
|
+
const physicalParent = await fsp.realpath(path.dirname(file));
|
|
3518
|
+
const target = path.join(physicalParent, path.basename(file));
|
|
3519
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
3520
|
+
const before = await fsp.lstat(target);
|
|
3521
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
3522
|
+
throw new Error(`${label} must be a regular non-symlink file: ${target}`);
|
|
3523
|
+
if (before.size > 256 * 1024)
|
|
3524
|
+
throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3525
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
3526
|
+
? fs.constants.O_NOFOLLOW
|
|
3527
|
+
: 0;
|
|
3528
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
3529
|
+
let text = '';
|
|
3530
|
+
try {
|
|
3531
|
+
const opened = await handle.stat();
|
|
3532
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3533
|
+
if (!opened.isFile() || !sameSpecFileIdentity(before, opened))
|
|
3534
|
+
throw new Error(`${label} identity changed before open: ${target}`);
|
|
3535
|
+
if (opened.size > 256 * 1024)
|
|
3536
|
+
throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3537
|
+
const bytes = Buffer.alloc(opened.size);
|
|
3538
|
+
let offset = 0;
|
|
3539
|
+
while (offset < bytes.length) {
|
|
3540
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
3541
|
+
if (!bytesRead)
|
|
3542
|
+
throw new Error(`${label} became shorter while reading: ${target}`);
|
|
3543
|
+
offset += bytesRead;
|
|
3544
|
+
}
|
|
3545
|
+
const openedAfter = await handle.stat();
|
|
3546
|
+
const after = await fsp.lstat(target);
|
|
3547
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3548
|
+
if (!sameSpecFileIdentity(opened, openedAfter) || openedAfter.size !== opened.size
|
|
3549
|
+
|| after.isSymbolicLink() || !after.isFile() || !sameSpecFileIdentity(opened, after)) {
|
|
3550
|
+
throw new Error(`${label} changed size or identity while reading: ${target}`);
|
|
3551
|
+
}
|
|
3552
|
+
text = bytes.toString('utf8');
|
|
3553
|
+
}
|
|
3554
|
+
finally {
|
|
3555
|
+
await handle.close();
|
|
3556
|
+
}
|
|
3557
|
+
const value = JSON.parse(text);
|
|
3151
3558
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3152
|
-
throw new Error(`${label} must contain one JSON object: ${
|
|
3559
|
+
throw new Error(`${label} must contain one JSON object: ${target}`);
|
|
3153
3560
|
return value;
|
|
3154
3561
|
}
|
|
3562
|
+
function sameSpecFileIdentity(left, right) {
|
|
3563
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
3564
|
+
}
|
|
3155
3565
|
async function existingProductionPath(root, value, label) {
|
|
3156
3566
|
if (!value || path.isAbsolute(value) || value.includes('\0'))
|
|
3157
3567
|
throw new Error(`${label} must be a non-empty project-relative path`);
|
|
@@ -3707,12 +4117,83 @@ function normalizeUeyeComparisonResult(value = '') {
|
|
|
3707
4117
|
return text;
|
|
3708
4118
|
return 'not-verified';
|
|
3709
4119
|
}
|
|
4120
|
+
async function verification(args = []) {
|
|
4121
|
+
const surface = String(args[0] || 'help');
|
|
4122
|
+
const action = String(args[1] || 'help');
|
|
4123
|
+
if (surface === 'help' || action === 'help' || surface === '--help' || surface === '-h')
|
|
4124
|
+
return verificationUsage();
|
|
4125
|
+
if (surface !== 'closure') {
|
|
4126
|
+
console.error(`unknown verification command: ${surface}`);
|
|
4127
|
+
return verificationUsage();
|
|
4128
|
+
}
|
|
4129
|
+
const flags = parseSimpleFlags(args.slice(2), new Set([
|
|
4130
|
+
'root', 'receipt-path', 'planned-scope', 'changed-file', 'selected-spec',
|
|
4131
|
+
'executed-spec', 'skipped-spec', 'truncated-spec', 'json'
|
|
4132
|
+
]));
|
|
4133
|
+
const root = path.resolve(expandHome(flags.root || process.cwd()));
|
|
4134
|
+
try {
|
|
4135
|
+
let result;
|
|
4136
|
+
if (action === 'create') {
|
|
4137
|
+
requireProductionFlags(flags, ['receipt_path', 'planned_scope', 'changed_file', 'selected_spec']);
|
|
4138
|
+
result = await createVerificationClosureReceipt({
|
|
4139
|
+
root,
|
|
4140
|
+
receipt_path: String(flags.receipt_path),
|
|
4141
|
+
planned_scope: String(flags.planned_scope),
|
|
4142
|
+
changed_files: arrayFlag(flags.changed_file),
|
|
4143
|
+
selected_commands: await readProductionSpecs(flags.selected_spec, 'selected command spec'),
|
|
4144
|
+
executed_commands: await readOptionalSpecs(flags.executed_spec, 'executed command spec'),
|
|
4145
|
+
skipped: await readOptionalSpecs(flags.skipped_spec, 'skipped command spec'),
|
|
4146
|
+
truncated: await readOptionalSpecs(flags.truncated_spec, 'truncated output spec')
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
else if (action === 'verify') {
|
|
4150
|
+
requireProductionFlags(flags, ['receipt_path']);
|
|
4151
|
+
result = await verifyVerificationClosureReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
4152
|
+
}
|
|
4153
|
+
else {
|
|
4154
|
+
throw new Error(`unknown verification closure command: ${action}`);
|
|
4155
|
+
}
|
|
4156
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Verification closure');
|
|
4157
|
+
if (result.truth_status === 'blocked')
|
|
4158
|
+
process.exitCode = 1;
|
|
4159
|
+
}
|
|
4160
|
+
catch (error) {
|
|
4161
|
+
const result = {
|
|
4162
|
+
schema: 'yam.verification-closure-error.v1',
|
|
4163
|
+
action,
|
|
4164
|
+
status: 'blocked',
|
|
4165
|
+
reason: summarizeCheckOutput(errorMessage(error)),
|
|
4166
|
+
next_action: 'repair changed-file scope, selected/executed command evidence, skipped/truncated reasons, or receipt integrity before retrying',
|
|
4167
|
+
truth_status: 'blocked'
|
|
4168
|
+
};
|
|
4169
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Verification closure');
|
|
4170
|
+
process.exitCode = 1;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
function verificationUsage() {
|
|
4174
|
+
console.log(`yam verification closure
|
|
4175
|
+
|
|
4176
|
+
Explicit, immutable completion receipts. Create records operator-supplied results; it does not execute commands or replace advisory yam detect output.
|
|
4177
|
+
|
|
4178
|
+
Usage:
|
|
4179
|
+
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]
|
|
4180
|
+
yam verification closure verify [--root dir] --receipt-path .yam/verification/closure.json [--json]
|
|
4181
|
+
|
|
4182
|
+
Selected spec: {"id":"typecheck","command":"npm run typecheck","scope":"affected","required":true}
|
|
4183
|
+
Executed spec: {"id":"typecheck","command":"npm run typecheck","exit_code":0,"evidence":"exit 0 observed"}
|
|
4184
|
+
Skipped/truncated spec: {"check_id":"browser","reason":"no UI surface changed"}
|
|
4185
|
+
|
|
4186
|
+
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.
|
|
4187
|
+
`);
|
|
4188
|
+
}
|
|
3710
4189
|
async function media(args = []) {
|
|
3711
4190
|
const subcommand = args[0] || 'help';
|
|
3712
4191
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3713
4192
|
return mediaUsage();
|
|
3714
4193
|
if (subcommand === 'proof')
|
|
3715
4194
|
return mediaProof(args.slice(1));
|
|
4195
|
+
if (subcommand === 'provider')
|
|
4196
|
+
return mediaProvider(args.slice(1));
|
|
3716
4197
|
console.error(`unknown media command: ${subcommand}`);
|
|
3717
4198
|
return mediaUsage();
|
|
3718
4199
|
}
|
|
@@ -3723,8 +4204,66 @@ Opt-in media generation proof helpers. Generated media can support visual direct
|
|
|
3723
4204
|
|
|
3724
4205
|
Usage:
|
|
3725
4206
|
yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
|
|
4207
|
+
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]
|
|
4208
|
+
yam media provider verify [--root dir] --receipt-path .yam/media/run.json [--json]
|
|
4209
|
+
|
|
4210
|
+
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.
|
|
3726
4211
|
`);
|
|
3727
4212
|
}
|
|
4213
|
+
async function mediaProvider(args = []) {
|
|
4214
|
+
const action = String(args[0] || 'help');
|
|
4215
|
+
if (action === 'help' || action === '--help' || action === '-h')
|
|
4216
|
+
return mediaUsage();
|
|
4217
|
+
const flags = parseSimpleFlags(args.slice(1), new Set([
|
|
4218
|
+
'root', 'receipt-path', 'demand-kind', 'demand-evidence', 'provider', 'model',
|
|
4219
|
+
'provider-calls', 'provider-execution', 'dry-run', 'submit', 'asset-spec', 'json'
|
|
4220
|
+
]));
|
|
4221
|
+
const root = path.resolve(expandHome(flags.root || process.cwd()));
|
|
4222
|
+
try {
|
|
4223
|
+
let result;
|
|
4224
|
+
if (action === 'create') {
|
|
4225
|
+
requireProductionFlags(flags, [
|
|
4226
|
+
'receipt_path', 'demand_kind', 'demand_evidence', 'provider', 'model', 'provider_calls', 'asset_spec'
|
|
4227
|
+
]);
|
|
4228
|
+
result = await createMediaProviderReceipt({
|
|
4229
|
+
root,
|
|
4230
|
+
receipt_path: String(flags.receipt_path),
|
|
4231
|
+
demand_trigger: {
|
|
4232
|
+
kind: String(flags.demand_kind),
|
|
4233
|
+
evidence: String(flags.demand_evidence)
|
|
4234
|
+
},
|
|
4235
|
+
provider: { name: String(flags.provider), model: String(flags.model) },
|
|
4236
|
+
provider_calls: Number(flags.provider_calls),
|
|
4237
|
+
provider_execution: Boolean(flags.provider_execution),
|
|
4238
|
+
dry_run: Boolean(flags.dry_run),
|
|
4239
|
+
submit: Boolean(flags.submit),
|
|
4240
|
+
assets: await readProductionSpecs(flags.asset_spec, 'media asset spec')
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
else if (action === 'verify') {
|
|
4244
|
+
requireProductionFlags(flags, ['receipt_path']);
|
|
4245
|
+
result = await verifyMediaProviderReceipt({ root, receipt_path: String(flags.receipt_path) });
|
|
4246
|
+
}
|
|
4247
|
+
else {
|
|
4248
|
+
throw new Error(`unknown media provider command: ${action}`);
|
|
4249
|
+
}
|
|
4250
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Media provider receipt');
|
|
4251
|
+
if (result.truth_status === 'blocked')
|
|
4252
|
+
process.exitCode = 1;
|
|
4253
|
+
}
|
|
4254
|
+
catch (error) {
|
|
4255
|
+
const result = {
|
|
4256
|
+
schema: 'yam.media-provider-receipt-error.v1',
|
|
4257
|
+
action,
|
|
4258
|
+
status: 'blocked',
|
|
4259
|
+
reason: summarizeCheckOutput(errorMessage(error)),
|
|
4260
|
+
next_action: 'repair the demand, exact execution fields, confined asset provenance, or receipt integrity before retrying',
|
|
4261
|
+
truth_status: 'blocked'
|
|
4262
|
+
};
|
|
4263
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Media provider receipt');
|
|
4264
|
+
process.exitCode = 1;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
3728
4267
|
async function mediaProof(args = []) {
|
|
3729
4268
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
3730
4269
|
return mediaUsage();
|
|
@@ -4250,7 +4789,7 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
|
|
|
4250
4789
|
if (allowed.size && !allowed.has(key))
|
|
4251
4790
|
continue;
|
|
4252
4791
|
const normalizedKey = key.replace(/-/g, '_');
|
|
4253
|
-
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)) {
|
|
4792
|
+
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)) {
|
|
4254
4793
|
flags[normalizedKey] = true;
|
|
4255
4794
|
continue;
|
|
4256
4795
|
}
|
|
@@ -5773,11 +6312,14 @@ function showRequestedHelp(args) {
|
|
|
5773
6312
|
context: contextUsage,
|
|
5774
6313
|
cleanup: cleanupUsage,
|
|
5775
6314
|
'study-note': studyNoteUsage,
|
|
6315
|
+
'next-step': nextStepUsage,
|
|
6316
|
+
scout: scoutUsage,
|
|
5776
6317
|
tools: toolsUsage,
|
|
5777
6318
|
hook: hookUsage,
|
|
5778
6319
|
loop: loopUsage,
|
|
5779
6320
|
ueye: ueyeUsage,
|
|
5780
6321
|
media: mediaUsage,
|
|
6322
|
+
verification: verificationUsage,
|
|
5781
6323
|
runtime: runtimeUsage,
|
|
5782
6324
|
mission: missionUsage,
|
|
5783
6325
|
update: updateUsage,
|
|
@@ -5818,12 +6360,18 @@ async function main() {
|
|
|
5818
6360
|
return proof(process.argv.slice(3));
|
|
5819
6361
|
if (command === 'study-note')
|
|
5820
6362
|
return studyNote(process.argv.slice(3));
|
|
6363
|
+
if (command === 'next-step')
|
|
6364
|
+
return nextStepCommand(process.argv.slice(3));
|
|
5821
6365
|
if (command === 'loop')
|
|
5822
6366
|
return loop(process.argv.slice(3));
|
|
6367
|
+
if (command === 'scout')
|
|
6368
|
+
return scout(process.argv.slice(3));
|
|
5823
6369
|
if (command === 'ueye')
|
|
5824
6370
|
return ueye(process.argv.slice(3));
|
|
5825
6371
|
if (command === 'media')
|
|
5826
6372
|
return media(process.argv.slice(3));
|
|
6373
|
+
if (command === 'verification')
|
|
6374
|
+
return verification(process.argv.slice(3));
|
|
5827
6375
|
if (command === 'runtime')
|
|
5828
6376
|
return runtime(process.argv.slice(3));
|
|
5829
6377
|
if (command === 'mission')
|