yam-flow 2.7.0 → 2.8.1
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 +5 -0
- package/CHANGELOG.md +41 -0
- package/COMMANDS.md +52 -5
- package/README.md +40 -8
- package/ROADMAP.md +17 -6
- package/dist/bin/yam.js +462 -74
- package/dist/lib/external-updates.d.ts +9 -0
- package/dist/lib/external-updates.js +232 -4
- 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/security-check.d.ts +98 -0
- package/dist/lib/security-check.js +262 -0
- package/dist/lib/security-integration.d.ts +8 -0
- package/dist/lib/security-integration.js +428 -0
- package/dist/lib/skill-installation.d.ts +30 -0
- package/dist/lib/skill-installation.js +470 -82
- package/dist/lib/trust-kernel.d.ts +2 -0
- package/dist/lib/trust-kernel.js +21 -2
- package/package.json +26 -4
- package/references/final-report.md +17 -1
- package/references/next-step.md +67 -0
- package/references/scout-wide-scan.md +16 -0
- package/references/security-check.md +131 -0
- package/references/study-note.md +10 -2
- package/references/tool-trust-layer.md +2 -0
- package/skills/deep/SKILL.md +6 -0
- package/skills/mission/SKILL.md +6 -0
- package/skills/quick/SKILL.md +5 -0
- package/skills/scout/SKILL.md +19 -9
- package/skills/ueye/SKILL.md +5 -0
- package/src/bin/yam.ts +465 -72
- package/src/lib/external-updates.ts +262 -4
- 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/security-check.ts +184 -0
- package/src/lib/security-integration.ts +299 -0
- package/src/lib/skill-installation.ts +542 -87
- package/src/lib/trust-kernel.ts +25 -2
- package/templates/security-check-note.md +42 -0
- package/templates/security-gate.yml +44 -0
- package/yam.manifest.json +4 -3
package/src/bin/yam.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { runSecurityCommand } from '../lib/security-integration.js';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import fsp from 'node:fs/promises';
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
import {
|
|
51
52
|
inspectSkillInstallation,
|
|
52
53
|
installSkillSetTransactional,
|
|
54
|
+
planSkillSetInstallation,
|
|
53
55
|
uninstallSkillSetSafely
|
|
54
56
|
} from '../lib/skill-installation.js';
|
|
55
57
|
import {
|
|
@@ -74,6 +76,19 @@ import {
|
|
|
74
76
|
createMediaProviderReceipt,
|
|
75
77
|
verifyMediaProviderReceipt
|
|
76
78
|
} from '../lib/media-provider-receipt.js';
|
|
79
|
+
import {
|
|
80
|
+
createScoutReceipt,
|
|
81
|
+
verifyScoutReceiptFile
|
|
82
|
+
} from '../lib/scout-receipt.js';
|
|
83
|
+
import {
|
|
84
|
+
buildNextStep,
|
|
85
|
+
verifyNextStep
|
|
86
|
+
} from '../lib/next-step.js';
|
|
87
|
+
import { releaseRegistryStatusFromChecks } from '../lib/release-registry-status.js';
|
|
88
|
+
import {
|
|
89
|
+
buildReleaseAuthReadiness,
|
|
90
|
+
inspectTrustedPublisherWorkflowText
|
|
91
|
+
} from '../lib/release-auth-readiness.js';
|
|
77
92
|
import {
|
|
78
93
|
BoundedInputError,
|
|
79
94
|
GENERAL_STDIN_MAX_BYTES,
|
|
@@ -210,7 +225,10 @@ Usage:
|
|
|
210
225
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
211
226
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
212
227
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
228
|
+
yam security <init|check|hooks|status> [--stage commit|push|ci|deploy] [--json]
|
|
229
|
+
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
213
230
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
231
|
+
yam scout receipt <create|verify> [options]
|
|
214
232
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
215
233
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
216
234
|
yam ueye preflight [dir] [--json]
|
|
@@ -229,13 +247,13 @@ Usage:
|
|
|
229
247
|
yam update apply --all [--json]
|
|
230
248
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
231
249
|
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]
|
|
232
|
-
yam release report [--json]
|
|
250
|
+
yam release report [--auth-mode auto|oidc|token] [--json]
|
|
233
251
|
yam safety [text...]
|
|
234
252
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
235
253
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
236
|
-
yam template <project|ueye|mission|proof|tuning>
|
|
254
|
+
yam template <project|ueye|mission|proof|tuning|security|security-ci>
|
|
237
255
|
yam tune-log [dir]
|
|
238
|
-
yam install [--replace-user-skill name]
|
|
256
|
+
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
239
257
|
yam uninstall
|
|
240
258
|
yam doctor [--json]
|
|
241
259
|
yam examples
|
|
@@ -261,10 +279,20 @@ async function rmrf(target) {
|
|
|
261
279
|
await fsp.rm(target, { recursive: true, force: true });
|
|
262
280
|
}
|
|
263
281
|
|
|
264
|
-
function
|
|
282
|
+
function installOptions(args: string[] = []) {
|
|
265
283
|
const skills: string[] = [];
|
|
284
|
+
let dryRun = false;
|
|
285
|
+
let json = false;
|
|
266
286
|
for (let index = 0; index < args.length; index += 1) {
|
|
267
287
|
const arg = String(args[index] || '');
|
|
288
|
+
if (arg === '--dry-run') {
|
|
289
|
+
dryRun = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (arg === '--json') {
|
|
293
|
+
json = true;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
268
296
|
if (arg === '--replace-user-skill') {
|
|
269
297
|
const skill = String(args[index + 1] || '');
|
|
270
298
|
if (!skill || skill.startsWith('--')) {
|
|
@@ -282,21 +310,31 @@ function installReplacementSkills(args: string[] = []) {
|
|
|
282
310
|
}
|
|
283
311
|
throw new Error(`unknown yam install argument: ${arg}`);
|
|
284
312
|
}
|
|
285
|
-
|
|
313
|
+
if (json && !dryRun) throw new Error('yam install --json currently requires --dry-run');
|
|
314
|
+
return { replaceSkills: [...new Set(skills)], dryRun, json };
|
|
286
315
|
}
|
|
287
316
|
|
|
288
317
|
async function install(args: string[] = []) {
|
|
289
|
-
const
|
|
290
|
-
const
|
|
318
|
+
const options = installOptions(args);
|
|
319
|
+
const installInput = {
|
|
291
320
|
sourceRoot: ROOT,
|
|
292
321
|
destination: DEST,
|
|
293
322
|
codexMirror: CODEX_MIRROR,
|
|
294
323
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
295
324
|
version: VERSION,
|
|
296
325
|
skills: SKILLS,
|
|
297
|
-
replaceSkills,
|
|
326
|
+
replaceSkills: options.replaceSkills,
|
|
298
327
|
legacySkills: LEGACY_SKILLS,
|
|
299
328
|
retiredSkills: RETIRED_SKILLS
|
|
329
|
+
};
|
|
330
|
+
if (options.dryRun) {
|
|
331
|
+
const plan = await planSkillSetInstallation(installInput);
|
|
332
|
+
printJsonOrHuman(plan, options.json, 'yam install dry run');
|
|
333
|
+
if (!plan.ready) process.exitCode = 1;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const result = await installSkillSetTransactional({
|
|
337
|
+
...installInput
|
|
300
338
|
});
|
|
301
339
|
console.log(`yam installed to ${DEST}`);
|
|
302
340
|
console.log(`install receipt: ${result.receiptPath}`);
|
|
@@ -374,6 +412,92 @@ async function readJson(file): Promise<AnyRecord> {
|
|
|
374
412
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
375
413
|
}
|
|
376
414
|
|
|
415
|
+
async function readBoundedRegularJson(file, label = 'JSON input', maxBytes = GENERAL_STDIN_MAX_BYTES): Promise<AnyRecord> {
|
|
416
|
+
const requested = path.resolve(expandHome(String(file || '')));
|
|
417
|
+
const physicalParent = await fsp.realpath(path.dirname(requested));
|
|
418
|
+
const target = path.join(physicalParent, path.basename(requested));
|
|
419
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
420
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
421
|
+
? fs.constants.O_NOFOLLOW
|
|
422
|
+
: 0;
|
|
423
|
+
const before = await fsp.lstat(target);
|
|
424
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
425
|
+
throw new Error(`${label} must be a regular JSON file no larger than ${maxBytes} bytes`);
|
|
426
|
+
}
|
|
427
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
428
|
+
try {
|
|
429
|
+
const opened = await handle.stat();
|
|
430
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
431
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > maxBytes) {
|
|
432
|
+
throw new Error(`${label} must be a stable regular JSON file no larger than ${maxBytes} bytes`);
|
|
433
|
+
}
|
|
434
|
+
const bytes = await readBoundedFileHandle(handle, maxBytes);
|
|
435
|
+
const afterOpened = await handle.stat();
|
|
436
|
+
const after = await fsp.lstat(target);
|
|
437
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
438
|
+
if (
|
|
439
|
+
bytes.length > maxBytes
|
|
440
|
+
|| after.isSymbolicLink()
|
|
441
|
+
|| !after.isFile()
|
|
442
|
+
|| after.dev !== opened.dev
|
|
443
|
+
|| after.ino !== opened.ino
|
|
444
|
+
|| afterOpened.dev !== opened.dev
|
|
445
|
+
|| afterOpened.ino !== opened.ino
|
|
446
|
+
|| afterOpened.size !== bytes.length
|
|
447
|
+
) {
|
|
448
|
+
throw new Error(`${label} changed identity or size while being read`);
|
|
449
|
+
}
|
|
450
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
451
|
+
} finally {
|
|
452
|
+
await handle.close();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
type CliPathIdentity = { path: string; dev: number; ino: number };
|
|
457
|
+
|
|
458
|
+
async function captureAbsoluteRegularDirectoryPath(target: string, label: string) {
|
|
459
|
+
const absolute = path.resolve(target);
|
|
460
|
+
const filesystemRoot = path.parse(absolute).root;
|
|
461
|
+
const relative = path.relative(filesystemRoot, absolute);
|
|
462
|
+
const identities: CliPathIdentity[] = [];
|
|
463
|
+
let current = filesystemRoot;
|
|
464
|
+
for (const segment of relative ? ['', ...relative.split(path.sep)] : ['']) {
|
|
465
|
+
if (segment) current = path.join(current, segment);
|
|
466
|
+
const stat = await fsp.lstat(current);
|
|
467
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
468
|
+
throw new Error(`${label} parent must be a regular physical directory: ${current}`);
|
|
469
|
+
}
|
|
470
|
+
identities.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
471
|
+
}
|
|
472
|
+
return identities;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function revalidateAbsoluteRegularDirectoryPath(identities: CliPathIdentity[], label: string) {
|
|
476
|
+
for (const identity of identities) {
|
|
477
|
+
const stat = await fsp.lstat(identity.path);
|
|
478
|
+
if (
|
|
479
|
+
stat.isSymbolicLink()
|
|
480
|
+
|| !stat.isDirectory()
|
|
481
|
+
|| stat.dev !== identity.dev
|
|
482
|
+
|| stat.ino !== identity.ino
|
|
483
|
+
) {
|
|
484
|
+
throw new Error(`${label} parent changed identity while being read: ${identity.path}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function readBoundedFileHandle(handle: fsp.FileHandle, maxBytes: number) {
|
|
490
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
491
|
+
let total = 0;
|
|
492
|
+
while (total < buffer.length) {
|
|
493
|
+
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
|
|
494
|
+
if (bytesRead === 0) break;
|
|
495
|
+
total += bytesRead;
|
|
496
|
+
}
|
|
497
|
+
if (total > maxBytes) throw new Error(`JSON input exceeds ${maxBytes} bytes while being read`);
|
|
498
|
+
return buffer.subarray(0, total);
|
|
499
|
+
}
|
|
500
|
+
|
|
377
501
|
async function readJsonOrDefault(file, fallback: AnyRecord = {}): Promise<AnyRecord> {
|
|
378
502
|
try {
|
|
379
503
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
@@ -448,6 +572,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
448
572
|
'context-reuse.md',
|
|
449
573
|
'markdown-management.md',
|
|
450
574
|
'study-note.md',
|
|
575
|
+
'security-check.md',
|
|
451
576
|
'final-report.md',
|
|
452
577
|
'token-budget-reporter.md',
|
|
453
578
|
'memory.md'
|
|
@@ -463,7 +588,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
463
588
|
if (!hasHeading(projectTemplate, section)) issues.push(`project template missing section: ${section}`);
|
|
464
589
|
}
|
|
465
590
|
}
|
|
466
|
-
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md']) {
|
|
591
|
+
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md', 'security-check-note.md', 'security-gate.yml']) {
|
|
467
592
|
if (!await exists(path.join(ROOT, 'templates', template))) issues.push(`missing template: ${template}`);
|
|
468
593
|
}
|
|
469
594
|
for (const module of ['trust-kernel.js', 'ueye-artifacts.js']) {
|
|
@@ -845,8 +970,9 @@ Usage:
|
|
|
845
970
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
846
971
|
|
|
847
972
|
Notes:
|
|
848
|
-
Read-only guard. It checks whether changed work has a Study Note
|
|
849
|
-
|
|
973
|
+
Read-only guard. It checks whether changed work has a complete Study Note followed immediately by Next step,
|
|
974
|
+
including relevant UI/CSS/DB hygiene and the ordered fix-first/planned handoff contract.
|
|
975
|
+
It does not generate, edit, or infer either section for you.
|
|
850
976
|
`);
|
|
851
977
|
}
|
|
852
978
|
|
|
@@ -867,6 +993,7 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
867
993
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
868
994
|
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
869
995
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
996
|
+
const studyNoteText = studyNoteSectionText(reportText);
|
|
870
997
|
const checks = [
|
|
871
998
|
studyNoteGuardCheck(
|
|
872
999
|
'changed_files',
|
|
@@ -876,16 +1003,24 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
876
1003
|
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'
|
|
877
1004
|
),
|
|
878
1005
|
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
879
|
-
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(
|
|
880
|
-
studyNoteGuardCheck('execution_point', scopeAwareStatus(/(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(
|
|
881
|
-
studyNoteGuardCheck('before_after_or_change', scopeAwareStatus(/(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(
|
|
882
|
-
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(
|
|
883
|
-
studyNoteGuardCheck('syntax_or_structure', scopeAwareStatus(/(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(
|
|
884
|
-
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(
|
|
885
|
-
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(
|
|
1006
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(studyNoteText)), 'Study Note should explain what the touched code/artifact does'),
|
|
1007
|
+
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'),
|
|
1008
|
+
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'),
|
|
1009
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(studyNoteText)), 'Study Note should describe the expected behavior or result'),
|
|
1010
|
+
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'),
|
|
1011
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(studyNoteText)), 'Study Note should say what was checked'),
|
|
1012
|
+
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'),
|
|
1013
|
+
studyNoteGuardCheck('next_step_present', scopeAwareStatus(hasNextStepMarker(reportText)), 'changed-artifact reports should include a Next step section'),
|
|
1014
|
+
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'),
|
|
1015
|
+
studyNoteGuardCheck('study_note_then_next_step', scopeAwareStatus(hasStudyNoteThenNextStepOrder(reportText)), 'Next step should be the next report section immediately after Study Note'),
|
|
1016
|
+
studyNoteGuardCheck('next_step_whole_process_scan', scopeAwareStatus(hasNextStepWholeProcessScan(reportText)), 'Next step should include current situation, forward outlook, a critical opinion, and an improvement recommendation'),
|
|
1017
|
+
studyNoteGuardCheck('next_step_ordered_actions', scopeAwareStatus(hasNextStepOrderedActions(reportText)), 'Next step should include an ordered action sequence with fix-first work before planned work'),
|
|
1018
|
+
studyNoteGuardCheck('next_step_evidence_and_ownership', scopeAwareStatus(hasNextStepEvidenceAndOwnership(reportText)), 'Next step should name evidence level or stamp plus the owner route or scope'),
|
|
1019
|
+
studyNoteGuardCheck('next_step_safety_context', scopeAwareStatus(hasNextStepSafetyContext(reportText)), 'Next step should state blocker status, safe retry guidance, and side effects'),
|
|
1020
|
+
studyNoteGuardCheck('next_step_truth_status', scopeAwareStatus(hasNextStepTruthStatus(reportText)), 'Next step should state its evidence-bounded truth status')
|
|
886
1021
|
];
|
|
887
1022
|
for (const requirement of hygieneRequirements) {
|
|
888
|
-
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(
|
|
1023
|
+
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
889
1024
|
}
|
|
890
1025
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
891
1026
|
const result = {
|
|
@@ -967,6 +1102,121 @@ function hasStudyNoteMarker(text = '') {
|
|
|
967
1102
|
return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
|
|
968
1103
|
}
|
|
969
1104
|
|
|
1105
|
+
function hasNextStepMarker(text = '') {
|
|
1106
|
+
return /(next step|next-step|다음\s*(?:단계|할\s*일|작업))/i.test(text);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function reportSectionMarkers(text = '') {
|
|
1110
|
+
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1111
|
+
const trimmed = line.trim();
|
|
1112
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1113
|
+
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1114
|
+
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
1115
|
+
if (!rawLabel) return null;
|
|
1116
|
+
const label = rawLabel.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
|
1117
|
+
const kind = /(study note|학습 노트|스터디 노트)/i.test(label)
|
|
1118
|
+
? 'study_note'
|
|
1119
|
+
: /(next step|다음 단계|다음 할 일|다음 작업)/i.test(label)
|
|
1120
|
+
? 'next_step'
|
|
1121
|
+
: 'other';
|
|
1122
|
+
return { index, kind, level: heading?.[0].match(/^#+/)?.[0].length || 0 };
|
|
1123
|
+
}).filter((item): item is { index: number; kind: string; level: number } => Boolean(item));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function hasStudyNoteThenNextStepOrder(text = '') {
|
|
1127
|
+
const markers = reportSectionMarkers(text);
|
|
1128
|
+
const studyIndex = markers.findIndex((item) => item.kind === 'study_note');
|
|
1129
|
+
if (studyIndex < 0) return false;
|
|
1130
|
+
return nextPeerSection(markers, studyIndex)?.kind === 'next_step';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function hasSingleStudyNoteNextStepPair(text = '') {
|
|
1134
|
+
const markers = reportSectionMarkers(text);
|
|
1135
|
+
return markers.filter((item) => item.kind === 'study_note').length === 1
|
|
1136
|
+
&& markers.filter((item) => item.kind === 'next_step').length === 1;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function hasNextStepWholeProcessScan(text = '') {
|
|
1140
|
+
const value = nextStepSectionText(text);
|
|
1141
|
+
const situation = /(situation|current state|현재\s*(?:상황|상태))/i.test(value);
|
|
1142
|
+
const outlook = /(outlook|forward|앞으로|향후|전망)/i.test(value);
|
|
1143
|
+
const critical = /(critical opinion|critique|비판적|비판\s*의견|tradeoff|약점)/i.test(value);
|
|
1144
|
+
const recommendation = /(recommendation|recommend|개선\s*추천|추천\s*의견|권장)/i.test(value);
|
|
1145
|
+
return situation && outlook && critical && recommendation;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function hasNextStepOrderedActions(text = '') {
|
|
1149
|
+
const value = nextStepSectionText(text);
|
|
1150
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1151
|
+
const actionLines = lines.filter((line) => /^\d+[.)]\s+/.test(line));
|
|
1152
|
+
const labeledLines = lines.filter((line) => /\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i.test(line));
|
|
1153
|
+
if (!actionLines.length || !/^1[.)]\s+/.test(actionLines[0])) return false;
|
|
1154
|
+
if (labeledLines.length !== actionLines.length || labeledLines.some((line) => !/^\d+[.)]\s+/.test(line))) return false;
|
|
1155
|
+
let plannedSeen = false;
|
|
1156
|
+
for (const [index, line] of actionLines.entries()) {
|
|
1157
|
+
const number = Number(line.match(/^(\d+)[.)]/)?.[1] || 0);
|
|
1158
|
+
if (number !== index + 1) return false;
|
|
1159
|
+
const kind = line.match(/\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i)?.[1] || '';
|
|
1160
|
+
if (!kind) return false;
|
|
1161
|
+
const planned = /planned|계획/i.test(kind);
|
|
1162
|
+
if (planned) plannedSeen = true;
|
|
1163
|
+
else if (plannedSeen) return false;
|
|
1164
|
+
}
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function hasNextStepEvidenceAndOwnership(text = '') {
|
|
1169
|
+
const value = nextStepSectionText(text);
|
|
1170
|
+
const evidence = /(evidence(?:[_ -](?:level|stamp))?|\bL[0-5]\b|근거|증거|검증\s*(?:수준|스탬프))/i.test(value);
|
|
1171
|
+
const ownership = /(owner(?:[_ -](?:route|scope))?|담당|소유|책임\s*(?:경로|범위|주체))/i.test(value);
|
|
1172
|
+
return evidence && ownership;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function hasNextStepSafetyContext(text = '') {
|
|
1176
|
+
const value = nextStepSectionText(text);
|
|
1177
|
+
const blocker = /(blocker|blocked(?:[_ -]by)?|차단|막힘|방해)/i.test(value);
|
|
1178
|
+
const safeRetry = /(safe[_ -]retry|안전[^\n]{0,20}재시도|재시도[^\n]{0,20}안전)/i.test(value);
|
|
1179
|
+
const sideEffects = /(side[_ -]effects?|부작용|영향)/i.test(value);
|
|
1180
|
+
return blocker && safeRetry && sideEffects;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function hasNextStepTruthStatus(text = '') {
|
|
1184
|
+
return /(truth[_ -]status|진실\s*상태|검증\s*상태)/i.test(nextStepSectionText(text));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function nextStepSectionText(text = '') {
|
|
1188
|
+
const value = String(text || '');
|
|
1189
|
+
const lines = value.split(/\r?\n/);
|
|
1190
|
+
const markers = reportSectionMarkers(value);
|
|
1191
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1192
|
+
if (study < 0) return '';
|
|
1193
|
+
const adjacent = nextPeerSectionIndex(markers, study);
|
|
1194
|
+
if (adjacent < 0 || markers[adjacent].kind !== 'next_step') return '';
|
|
1195
|
+
const end = nextPeerSection(markers, adjacent);
|
|
1196
|
+
return lines.slice(markers[adjacent].index + 1, end?.index ?? lines.length).join('\n');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function studyNoteSectionText(text = '') {
|
|
1200
|
+
const value = String(text || '');
|
|
1201
|
+
const lines = value.split(/\r?\n/);
|
|
1202
|
+
const markers = reportSectionMarkers(value);
|
|
1203
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1204
|
+
if (study < 0) return '';
|
|
1205
|
+
const end = nextPeerSection(markers, study);
|
|
1206
|
+
return lines.slice(markers[study].index + 1, end?.index ?? lines.length).join('\n');
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function nextPeerSection(markers: Array<{ index: number; kind: string; level: number }>, start: number) {
|
|
1210
|
+
const nextIndex = nextPeerSectionIndex(markers, start);
|
|
1211
|
+
return nextIndex < 0 ? undefined : markers[nextIndex];
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function nextPeerSectionIndex(markers: Array<{ index: number; kind: string; level: number }>, start: number) {
|
|
1215
|
+
const current = markers[start];
|
|
1216
|
+
const relative = markers.slice(start + 1).findIndex((item) => current.level === 0 || item.level === 0 || item.level <= current.level);
|
|
1217
|
+
return relative < 0 ? -1 : start + 1 + relative;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
970
1220
|
function studyNoteGuardCheck(id, status, nextAction) {
|
|
971
1221
|
return {
|
|
972
1222
|
id,
|
|
@@ -2182,26 +2432,27 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
2182
2432
|
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2183
2433
|
const changedFiles = changedFileDetection.files;
|
|
2184
2434
|
const lines = [
|
|
2185
|
-
'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.'
|
|
2435
|
+
'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.'
|
|
2186
2436
|
];
|
|
2187
2437
|
if (!changedFileDetection.available) {
|
|
2188
2438
|
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2189
|
-
lines.push('Determine manually whether artifacts changed and include
|
|
2439
|
+
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.');
|
|
2190
2440
|
return lines.join('\n');
|
|
2191
2441
|
}
|
|
2192
2442
|
if (!changedFiles.length) {
|
|
2193
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add
|
|
2443
|
+
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.');
|
|
2194
2444
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2195
2445
|
return lines.join('\n');
|
|
2196
2446
|
}
|
|
2197
2447
|
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2198
2448
|
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2449
|
+
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.');
|
|
2199
2450
|
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2200
2451
|
if (hygiene.length) {
|
|
2201
2452
|
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2202
2453
|
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.');
|
|
2203
2454
|
}
|
|
2204
|
-
lines.push('This prompt reminder does not generate or edit
|
|
2455
|
+
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.');
|
|
2205
2456
|
return lines.join('\n');
|
|
2206
2457
|
}
|
|
2207
2458
|
|
|
@@ -2279,12 +2530,15 @@ async function safety(args = []) {
|
|
|
2279
2530
|
async function release(args = []) {
|
|
2280
2531
|
const subcommand = args[0] || 'help';
|
|
2281
2532
|
if (subcommand !== 'report') {
|
|
2282
|
-
console.error('usage: yam release report [--json]');
|
|
2533
|
+
console.error('usage: yam release report [--auth-mode auto|oidc|token] [--json]');
|
|
2283
2534
|
process.exitCode = 1;
|
|
2284
2535
|
return;
|
|
2285
2536
|
}
|
|
2286
|
-
const
|
|
2287
|
-
const
|
|
2537
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['auth-mode', 'json']));
|
|
2538
|
+
const authMode = String(flags.auth_mode || 'auto');
|
|
2539
|
+
if (!['auto', 'oidc', 'token'].includes(authMode)) throw new Error('--auth-mode must be auto, oidc, or token');
|
|
2540
|
+
const asJson = Boolean(flags.json);
|
|
2541
|
+
const report = runReleaseReport(authMode);
|
|
2288
2542
|
if (asJson) {
|
|
2289
2543
|
console.log(JSON.stringify(report, null, 2));
|
|
2290
2544
|
if (!report.ok) process.exitCode = 1;
|
|
@@ -2309,7 +2563,7 @@ async function release(args = []) {
|
|
|
2309
2563
|
if (report.publish_readiness) {
|
|
2310
2564
|
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
2311
2565
|
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
2312
|
-
console.log(`-
|
|
2566
|
+
console.log(`- Publish auth: ${report.publish_readiness.auth.mode} (${report.publish_readiness.auth.status})`);
|
|
2313
2567
|
}
|
|
2314
2568
|
if (report.failed.length) {
|
|
2315
2569
|
console.log('- Failed:');
|
|
@@ -2332,7 +2586,7 @@ async function release(args = []) {
|
|
|
2332
2586
|
if (!report.ok) process.exitCode = 1;
|
|
2333
2587
|
}
|
|
2334
2588
|
|
|
2335
|
-
function runReleaseReport() {
|
|
2589
|
+
function runReleaseReport(authMode = 'auto') {
|
|
2336
2590
|
const startedAt = new Date().toISOString();
|
|
2337
2591
|
const checks = [
|
|
2338
2592
|
['typecheck', ['npm', ['run', 'typecheck']]],
|
|
@@ -2347,7 +2601,7 @@ function runReleaseReport() {
|
|
|
2347
2601
|
const tarball = releaseTarballProvenance();
|
|
2348
2602
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
2349
2603
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
2350
|
-
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
2604
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence, authMode);
|
|
2351
2605
|
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
2352
2606
|
const preliminaryOk = failed.length === 0 && publishReadiness.status === 'ready';
|
|
2353
2607
|
const readinessReceipt = releaseReadinessReceipt(provenance, freshness, tarball, publishReadiness, preliminaryOk);
|
|
@@ -2413,9 +2667,14 @@ function releaseReadinessReceipt(provenance: AnyRecord = {}, freshness: AnyRecor
|
|
|
2413
2667
|
truth_status: publishReadiness.registry?.truth_status || 'partial'
|
|
2414
2668
|
},
|
|
2415
2669
|
auth: {
|
|
2416
|
-
|
|
2670
|
+
requested_mode: String(publishReadiness.auth?.requested_mode || 'auto'),
|
|
2671
|
+
mode: String(publishReadiness.auth?.mode || 'manual_token'),
|
|
2672
|
+
command: String(publishReadiness.auth?.command || 'npm whoami'),
|
|
2673
|
+
required_for_selected_mode: Boolean(publishReadiness.auth?.required_for_selected_mode),
|
|
2417
2674
|
status: String(publishReadiness.auth?.status || 'not_verified'),
|
|
2418
|
-
account: publishReadiness.auth?.
|
|
2675
|
+
account: String(publishReadiness.auth?.account || ''),
|
|
2676
|
+
local_token: publishReadiness.auth?.local_token || null,
|
|
2677
|
+
trusted_publisher: publishReadiness.auth?.trusted_publisher || null,
|
|
2419
2678
|
truth_status: publishReadiness.auth?.truth_status || 'partial'
|
|
2420
2679
|
},
|
|
2421
2680
|
git: {
|
|
@@ -2517,14 +2776,36 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
|
|
|
2517
2776
|
};
|
|
2518
2777
|
}
|
|
2519
2778
|
|
|
2520
|
-
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
|
|
2779
|
+
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = [], authMode = 'auto') {
|
|
2521
2780
|
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
2522
2781
|
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
2523
2782
|
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
2524
2783
|
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
2525
|
-
const
|
|
2784
|
+
const trustedPublisherWorkflowPath = '.github/workflows/release.yml';
|
|
2785
|
+
const trustedPublisherWorkflowFile = path.join(ROOT, trustedPublisherWorkflowPath);
|
|
2786
|
+
const trustedPublisherWorkflowExists = fs.existsSync(trustedPublisherWorkflowFile);
|
|
2787
|
+
const trustedPublisherWorkflow = inspectTrustedPublisherWorkflowText(
|
|
2788
|
+
trustedPublisherWorkflowExists ? fs.readFileSync(trustedPublisherWorkflowFile, 'utf8') : '',
|
|
2789
|
+
trustedPublisherWorkflowPath,
|
|
2790
|
+
trustedPublisherWorkflowExists
|
|
2791
|
+
);
|
|
2792
|
+
const authReadiness = buildReleaseAuthReadiness({
|
|
2793
|
+
workflow: trustedPublisherWorkflow,
|
|
2794
|
+
local_token: {
|
|
2795
|
+
command: 'npm whoami',
|
|
2796
|
+
ok: whoamiProbe.ok,
|
|
2797
|
+
note: whoamiProbe.note
|
|
2798
|
+
},
|
|
2799
|
+
requested_mode: authMode as 'auto' | 'oidc' | 'token'
|
|
2800
|
+
});
|
|
2801
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks, {
|
|
2802
|
+
package_name: PACKAGE_JSON.name,
|
|
2803
|
+
version: VERSION
|
|
2804
|
+
});
|
|
2526
2805
|
const latestProbe = registryStatus.checked
|
|
2527
2806
|
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
2807
|
+
: registryStatus.parse_failed
|
|
2808
|
+
? { ok: false, stdout: '', note: `registry check output did not prove ${PACKAGE_JSON.name}@${VERSION} is unpublished: ${registryStatus.note}` }
|
|
2528
2809
|
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
2529
2810
|
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
2530
2811
|
const blockers = [];
|
|
@@ -2535,8 +2816,8 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2535
2816
|
if (!registryProbe.ok) {
|
|
2536
2817
|
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'));
|
|
2537
2818
|
}
|
|
2538
|
-
|
|
2539
|
-
blockers.push(readinessBlocker(
|
|
2819
|
+
for (const blocker of authReadiness.blockers) {
|
|
2820
|
+
blockers.push(readinessBlocker(blocker.kind, blocker.reason, blocker.safe_next_action, blocker.command, blocker.severity));
|
|
2540
2821
|
}
|
|
2541
2822
|
if (!latestProbe.ok) {
|
|
2542
2823
|
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
@@ -2576,11 +2857,16 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2576
2857
|
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
2577
2858
|
},
|
|
2578
2859
|
auth: {
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2860
|
+
requested_mode: authReadiness.requested_mode,
|
|
2861
|
+
mode: authReadiness.mode,
|
|
2862
|
+
command: authReadiness.local_token.command,
|
|
2863
|
+
required_for_selected_mode: authReadiness.local_token.required_for_selected_mode,
|
|
2864
|
+
status: authReadiness.status,
|
|
2865
|
+
account: authReadiness.local_token.account,
|
|
2866
|
+
note: authReadiness.next_action,
|
|
2867
|
+
local_token: authReadiness.local_token,
|
|
2868
|
+
trusted_publisher: authReadiness.trusted_publisher,
|
|
2869
|
+
truth_status: authReadiness.truth_status
|
|
2584
2870
|
},
|
|
2585
2871
|
blockers: uniqueBlockers,
|
|
2586
2872
|
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
@@ -2588,19 +2874,6 @@ function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarbal
|
|
|
2588
2874
|
};
|
|
2589
2875
|
}
|
|
2590
2876
|
|
|
2591
|
-
function releaseRegistryStatusFromChecks(checks = []) {
|
|
2592
|
-
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
2593
|
-
const note = String(registryCheck?.note || '');
|
|
2594
|
-
if (registryCheck?.status !== 'passed') return { checked: false, latest_version: '', not_published: false, note };
|
|
2595
|
-
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
2596
|
-
return {
|
|
2597
|
-
checked: true,
|
|
2598
|
-
latest_version: latest,
|
|
2599
|
-
not_published: /not published yet/i.test(note),
|
|
2600
|
-
note
|
|
2601
|
-
};
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
2877
|
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
2605
2878
|
return {
|
|
2606
2879
|
kind,
|
|
@@ -2636,13 +2909,13 @@ function releaseStudyNote(publishReadiness: AnyRecord = {}, publishBlockers = []
|
|
|
2636
2909
|
}
|
|
2637
2910
|
return buildStudyNote({
|
|
2638
2911
|
issue_code: 'npm publish readiness',
|
|
2639
|
-
issue_role: 'This part checks registry, version,
|
|
2912
|
+
issue_role: 'This part checks registry, version, the selected OIDC or manual-token authentication path, tarball, and local release checks before any public publish.',
|
|
2640
2913
|
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
2641
2914
|
changed_code: 'yam release report',
|
|
2642
2915
|
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
2643
|
-
change_summary: 'The report
|
|
2644
|
-
why_important: '
|
|
2645
|
-
learning_note: '
|
|
2916
|
+
change_summary: 'The report separates Trusted Publisher OIDC evidence from optional local-token evidence and gives a path-specific safe next action.',
|
|
2917
|
+
why_important: 'npm whoami cannot prove OIDC readiness; treating its E401 as an OIDC blocker would recommend the wrong credential model.',
|
|
2918
|
+
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.'
|
|
2646
2919
|
});
|
|
2647
2920
|
}
|
|
2648
2921
|
|
|
@@ -2817,20 +3090,117 @@ async function loop(args = []) {
|
|
|
2817
3090
|
return loopUsage();
|
|
2818
3091
|
}
|
|
2819
3092
|
|
|
3093
|
+
async function scout(args = []) {
|
|
3094
|
+
const subcommand = args[0] || 'help';
|
|
3095
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return scoutUsage();
|
|
3096
|
+
if (subcommand === 'receipt') return scoutReceipt(args.slice(1));
|
|
3097
|
+
console.error(`unknown scout command: ${subcommand}`);
|
|
3098
|
+
process.exitCode = 1;
|
|
3099
|
+
return scoutUsage();
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function scoutUsage() {
|
|
3103
|
+
console.log(`yam scout
|
|
3104
|
+
|
|
3105
|
+
Immutable, operator-supplied research evidence helpers. These commands do not browse, fetch, or trust external content as instructions.
|
|
3106
|
+
|
|
3107
|
+
Usage:
|
|
3108
|
+
yam scout receipt create [--root dir] --receipt-path .yam/scout/run.json --spec scout-spec.json [--json]
|
|
3109
|
+
yam scout receipt verify [--root dir] --receipt-path .yam/scout/run.json [--json]
|
|
3110
|
+
`);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
async function scoutReceipt(args = []) {
|
|
3114
|
+
const action = args[0] || 'help';
|
|
3115
|
+
if (action === 'help' || action === '--help' || action === '-h') return scoutUsage();
|
|
3116
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['root', 'receipt-path', 'spec', 'json']));
|
|
3117
|
+
const root = path.resolve(expandHome(String(flags.root || process.cwd())));
|
|
3118
|
+
try {
|
|
3119
|
+
let result;
|
|
3120
|
+
if (action === 'create') {
|
|
3121
|
+
if (!flags.receipt_path || !flags.spec) throw new Error('create requires --receipt-path and --spec');
|
|
3122
|
+
result = await createScoutReceipt({
|
|
3123
|
+
root,
|
|
3124
|
+
receipt_path: String(flags.receipt_path),
|
|
3125
|
+
spec: await readBoundedRegularJson(flags.spec, 'Scout spec') as any
|
|
3126
|
+
});
|
|
3127
|
+
} else if (action === 'verify') {
|
|
3128
|
+
if (!flags.receipt_path) throw new Error('verify requires --receipt-path');
|
|
3129
|
+
result = await verifyScoutReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
3130
|
+
} else {
|
|
3131
|
+
throw new Error(`unknown Scout receipt action: ${action}`);
|
|
3132
|
+
}
|
|
3133
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3134
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
3135
|
+
} catch (error) {
|
|
3136
|
+
const result = {
|
|
3137
|
+
schema: 'yam.scout-receipt-error.v1',
|
|
3138
|
+
truth_status: 'blocked',
|
|
3139
|
+
error: errorMessage(error),
|
|
3140
|
+
next_action: 'repair the confined receipt path, canonical entity, version clocks, source digests, claim links, or failure taxonomy before retrying'
|
|
3141
|
+
};
|
|
3142
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3143
|
+
process.exitCode = 1;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
async function nextStepCommand(args = []) {
|
|
3148
|
+
const subcommand = args[0] || 'help';
|
|
3149
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return nextStepUsage();
|
|
3150
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['spec', 'receipt', 'json']));
|
|
3151
|
+
try {
|
|
3152
|
+
let result;
|
|
3153
|
+
if (subcommand === 'report') {
|
|
3154
|
+
if (!flags.spec) throw new Error('report requires --spec');
|
|
3155
|
+
result = buildNextStep(await readBoundedRegularJson(flags.spec, 'Next step spec') as any);
|
|
3156
|
+
} else if (subcommand === 'verify') {
|
|
3157
|
+
if (!flags.receipt) throw new Error('verify requires --receipt');
|
|
3158
|
+
result = verifyNextStep(await readBoundedRegularJson(flags.receipt, 'Next step receipt') as any);
|
|
3159
|
+
} else {
|
|
3160
|
+
throw new Error(`unknown Next step command: ${subcommand}`);
|
|
3161
|
+
}
|
|
3162
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3163
|
+
if (result.truth_status === 'blocked') process.exitCode = 1;
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
const result = {
|
|
3166
|
+
schema: 'yam.next-step-error.v1',
|
|
3167
|
+
truth_status: 'blocked',
|
|
3168
|
+
error: errorMessage(error),
|
|
3169
|
+
next_action: 'repair the whole-process scan, ordered steps, evidence level/stamp, owner scope, blockers, or safe retry before retrying'
|
|
3170
|
+
};
|
|
3171
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3172
|
+
process.exitCode = 1;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
function nextStepUsage() {
|
|
3177
|
+
console.log(`yam next-step
|
|
3178
|
+
|
|
3179
|
+
Build or verify an evidence-bound ordered handoff after a quick whole-process scan.
|
|
3180
|
+
|
|
3181
|
+
Usage:
|
|
3182
|
+
yam next-step report --spec next-step-spec.json [--json]
|
|
3183
|
+
yam next-step verify --receipt next-step-receipt.json [--json]
|
|
3184
|
+
`);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
2820
3187
|
function loopUsage() {
|
|
2821
3188
|
console.log(`yam loop
|
|
2822
3189
|
|
|
2823
|
-
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers,
|
|
3190
|
+
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.
|
|
2824
3191
|
|
|
2825
3192
|
Usage:
|
|
2826
|
-
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]
|
|
3193
|
+
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]
|
|
2827
3194
|
`);
|
|
2828
3195
|
}
|
|
2829
3196
|
|
|
2830
3197
|
async function loopReport(args = []) {
|
|
2831
3198
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
|
|
2832
|
-
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']));
|
|
3199
|
+
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']));
|
|
2833
3200
|
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
3201
|
+
const nextStepSpec = flags.next_step_spec
|
|
3202
|
+
? await readBoundedRegularJson(flags.next_step_spec, 'Next step spec')
|
|
3203
|
+
: undefined;
|
|
2834
3204
|
const report = buildLoopReport({
|
|
2835
3205
|
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
2836
3206
|
intent: String(flags.intent || ''),
|
|
@@ -2869,6 +3239,7 @@ async function loopReport(args = []) {
|
|
|
2869
3239
|
scope_owner: String(flags.scope_owner || flags.owner || ''),
|
|
2870
3240
|
side_effects: arrayFlag(flags.side_effect),
|
|
2871
3241
|
avoidance_note: String(flags.avoidance_note || ''),
|
|
3242
|
+
next_step: nextStepSpec as any,
|
|
2872
3243
|
issue_code: String(flags.issue_code || ''),
|
|
2873
3244
|
issue_role: String(flags.issue_role || ''),
|
|
2874
3245
|
issue_symptom: String(flags.issue_symptom || ''),
|
|
@@ -3177,37 +3548,42 @@ async function readOptionalSpecs(value: unknown, label: string) {
|
|
|
3177
3548
|
}
|
|
3178
3549
|
|
|
3179
3550
|
async function readBoundedJsonSpec(file: string, label: string) {
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3182
|
-
|
|
3551
|
+
const physicalParent = await fsp.realpath(path.dirname(file));
|
|
3552
|
+
const target = path.join(physicalParent, path.basename(file));
|
|
3553
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
3554
|
+
const before = await fsp.lstat(target);
|
|
3555
|
+
if (before.isSymbolicLink() || !before.isFile()) throw new Error(`${label} must be a regular non-symlink file: ${target}`);
|
|
3556
|
+
if (before.size > 256 * 1024) throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3183
3557
|
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
3184
3558
|
? fs.constants.O_NOFOLLOW
|
|
3185
3559
|
: 0;
|
|
3186
|
-
const handle = await fsp.open(
|
|
3560
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
3187
3561
|
let text = '';
|
|
3188
3562
|
try {
|
|
3189
3563
|
const opened = await handle.stat();
|
|
3190
|
-
|
|
3191
|
-
if (opened.
|
|
3564
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3565
|
+
if (!opened.isFile() || !sameSpecFileIdentity(before, opened)) throw new Error(`${label} identity changed before open: ${target}`);
|
|
3566
|
+
if (opened.size > 256 * 1024) throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3192
3567
|
const bytes = Buffer.alloc(opened.size);
|
|
3193
3568
|
let offset = 0;
|
|
3194
3569
|
while (offset < bytes.length) {
|
|
3195
3570
|
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
3196
|
-
if (!bytesRead) throw new Error(`${label} became shorter while reading: ${
|
|
3571
|
+
if (!bytesRead) throw new Error(`${label} became shorter while reading: ${target}`);
|
|
3197
3572
|
offset += bytesRead;
|
|
3198
3573
|
}
|
|
3199
3574
|
const openedAfter = await handle.stat();
|
|
3200
|
-
const after = await fsp.lstat(
|
|
3575
|
+
const after = await fsp.lstat(target);
|
|
3576
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3201
3577
|
if (!sameSpecFileIdentity(opened, openedAfter) || openedAfter.size !== opened.size
|
|
3202
3578
|
|| after.isSymbolicLink() || !after.isFile() || !sameSpecFileIdentity(opened, after)) {
|
|
3203
|
-
throw new Error(`${label} changed size or identity while reading: ${
|
|
3579
|
+
throw new Error(`${label} changed size or identity while reading: ${target}`);
|
|
3204
3580
|
}
|
|
3205
3581
|
text = bytes.toString('utf8');
|
|
3206
3582
|
} finally {
|
|
3207
3583
|
await handle.close();
|
|
3208
3584
|
}
|
|
3209
3585
|
const value = JSON.parse(text);
|
|
3210
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain one JSON object: ${
|
|
3586
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain one JSON object: ${target}`);
|
|
3211
3587
|
return value;
|
|
3212
3588
|
}
|
|
3213
3589
|
|
|
@@ -5696,11 +6072,13 @@ async function printTemplate(name = '') {
|
|
|
5696
6072
|
mission: 'mission-plan.md',
|
|
5697
6073
|
proof: 'runtime-proof.md',
|
|
5698
6074
|
runtime: 'runtime-proof.md',
|
|
5699
|
-
tuning: 'tuning-log.md'
|
|
6075
|
+
tuning: 'tuning-log.md',
|
|
6076
|
+
security: 'security-check-note.md',
|
|
6077
|
+
'security-ci': 'security-gate.yml'
|
|
5700
6078
|
};
|
|
5701
6079
|
const file = map[key];
|
|
5702
6080
|
if (!file) {
|
|
5703
|
-
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning>');
|
|
6081
|
+
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci>');
|
|
5704
6082
|
process.exitCode = 1;
|
|
5705
6083
|
return;
|
|
5706
6084
|
}
|
|
@@ -5976,9 +6354,12 @@ function showRequestedHelp(args: string[]) {
|
|
|
5976
6354
|
}
|
|
5977
6355
|
|
|
5978
6356
|
const commandUsage: Record<string, () => void> = {
|
|
6357
|
+
security: () => console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.'),
|
|
5979
6358
|
context: contextUsage,
|
|
5980
6359
|
cleanup: cleanupUsage,
|
|
5981
6360
|
'study-note': studyNoteUsage,
|
|
6361
|
+
'next-step': nextStepUsage,
|
|
6362
|
+
scout: scoutUsage,
|
|
5982
6363
|
tools: toolsUsage,
|
|
5983
6364
|
hook: hookUsage,
|
|
5984
6365
|
loop: loopUsage,
|
|
@@ -6000,6 +6381,16 @@ async function main() {
|
|
|
6000
6381
|
const command = args[0] || 'help';
|
|
6001
6382
|
if (showRequestedHelp(args)) return;
|
|
6002
6383
|
if (command === 'help') return usage();
|
|
6384
|
+
if (command === 'security') {
|
|
6385
|
+
if (args.length === 1 || ['help', '--help', '-h'].includes(args[1])) {
|
|
6386
|
+
console.log('yam security <init|check|hooks enable|status> [--stage commit|push|ci|deploy] [--policy file --policy-digest sha256 --evidence file --note file]\nSee references/security-check.md for exact bindings and CI/deploy trust configuration.');
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6389
|
+
const result = await runSecurityCommand(args.slice(1), { cwd: process.cwd(), cliPath: fileURLToPath(import.meta.url) });
|
|
6390
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6391
|
+
if (!result.ok) process.exitCode = 1;
|
|
6392
|
+
return;
|
|
6393
|
+
}
|
|
6003
6394
|
if (command === 'install') return install(args.slice(1));
|
|
6004
6395
|
if (command === 'uninstall') return uninstall(args.slice(1));
|
|
6005
6396
|
if (command === 'version') return console.log(VERSION);
|
|
@@ -6012,7 +6403,9 @@ async function main() {
|
|
|
6012
6403
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
6013
6404
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
6014
6405
|
if (command === 'study-note') return studyNote(process.argv.slice(3));
|
|
6406
|
+
if (command === 'next-step') return nextStepCommand(process.argv.slice(3));
|
|
6015
6407
|
if (command === 'loop') return loop(process.argv.slice(3));
|
|
6408
|
+
if (command === 'scout') return scout(process.argv.slice(3));
|
|
6016
6409
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
6017
6410
|
if (command === 'media') return media(process.argv.slice(3));
|
|
6018
6411
|
if (command === 'verification') return verification(process.argv.slice(3));
|