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/dist/bin/yam.js
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';
|
|
@@ -11,12 +12,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
12
|
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
13
|
import { archiveUeyeRevision, inspectImageFile, upsertUeyeAsset, verifyUeyeAssetManifest, verifyUeyeRevisionHistory } from '../lib/ueye-artifacts.js';
|
|
13
14
|
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';
|
|
15
|
+
import { inspectSkillInstallation, installSkillSetTransactional, planSkillSetInstallation, uninstallSkillSetSafely } from '../lib/skill-installation.js';
|
|
15
16
|
import { EXTERNAL_UPDATE_COMPONENTS, applyExternalUpdates, checkExternalUpdates } from '../lib/external-updates.js';
|
|
16
17
|
import { buildBoundedPromotionReport, parsePromotionSample } from '../lib/benchmark-promotion.js';
|
|
17
18
|
import { buildStrictGateResult, verifyStrictGateResult } from '../lib/gate-result.js';
|
|
18
19
|
import { createVerificationClosureReceipt, verifyVerificationClosureReceiptFile } from '../lib/verification-closure.js';
|
|
19
20
|
import { createMediaProviderReceipt, verifyMediaProviderReceipt } from '../lib/media-provider-receipt.js';
|
|
21
|
+
import { createScoutReceipt, verifyScoutReceiptFile } from '../lib/scout-receipt.js';
|
|
22
|
+
import { buildNextStep, verifyNextStep } from '../lib/next-step.js';
|
|
23
|
+
import { releaseRegistryStatusFromChecks } from '../lib/release-registry-status.js';
|
|
24
|
+
import { buildReleaseAuthReadiness, inspectTrustedPublisherWorkflowText } from '../lib/release-auth-readiness.js';
|
|
20
25
|
import { BoundedInputError, GENERAL_STDIN_MAX_BYTES, HOOK_STDIN_MAX_BYTES, readBoundedStdinJson, readBoundedStdinText } from '../lib/bounded-input.js';
|
|
21
26
|
function errorMessage(error) {
|
|
22
27
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -141,7 +146,10 @@ Usage:
|
|
|
141
146
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
142
147
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
143
148
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
149
|
+
yam security <init|check|hooks|status> [--stage commit|push|ci|deploy] [--json]
|
|
150
|
+
yam next-step <report|verify> [--spec file|--receipt file] [--json]
|
|
144
151
|
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
152
|
+
yam scout receipt <create|verify> [options]
|
|
145
153
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
146
154
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
147
155
|
yam ueye preflight [dir] [--json]
|
|
@@ -160,13 +168,13 @@ Usage:
|
|
|
160
168
|
yam update apply --all [--json]
|
|
161
169
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
162
170
|
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]
|
|
163
|
-
yam release report [--json]
|
|
171
|
+
yam release report [--auth-mode auto|oidc|token] [--json]
|
|
164
172
|
yam safety [text...]
|
|
165
173
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
166
174
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
167
|
-
yam template <project|ueye|mission|proof|tuning>
|
|
175
|
+
yam template <project|ueye|mission|proof|tuning|security|security-ci>
|
|
168
176
|
yam tune-log [dir]
|
|
169
|
-
yam install [--replace-user-skill name]
|
|
177
|
+
yam install [--dry-run] [--json] [--replace-user-skill name]
|
|
170
178
|
yam uninstall
|
|
171
179
|
yam doctor [--json]
|
|
172
180
|
yam examples
|
|
@@ -190,10 +198,20 @@ async function exists(file) {
|
|
|
190
198
|
async function rmrf(target) {
|
|
191
199
|
await fsp.rm(target, { recursive: true, force: true });
|
|
192
200
|
}
|
|
193
|
-
function
|
|
201
|
+
function installOptions(args = []) {
|
|
194
202
|
const skills = [];
|
|
203
|
+
let dryRun = false;
|
|
204
|
+
let json = false;
|
|
195
205
|
for (let index = 0; index < args.length; index += 1) {
|
|
196
206
|
const arg = String(args[index] || '');
|
|
207
|
+
if (arg === '--dry-run') {
|
|
208
|
+
dryRun = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (arg === '--json') {
|
|
212
|
+
json = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
197
215
|
if (arg === '--replace-user-skill') {
|
|
198
216
|
const skill = String(args[index + 1] || '');
|
|
199
217
|
if (!skill || skill.startsWith('--')) {
|
|
@@ -212,20 +230,32 @@ function installReplacementSkills(args = []) {
|
|
|
212
230
|
}
|
|
213
231
|
throw new Error(`unknown yam install argument: ${arg}`);
|
|
214
232
|
}
|
|
215
|
-
|
|
233
|
+
if (json && !dryRun)
|
|
234
|
+
throw new Error('yam install --json currently requires --dry-run');
|
|
235
|
+
return { replaceSkills: [...new Set(skills)], dryRun, json };
|
|
216
236
|
}
|
|
217
237
|
async function install(args = []) {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
238
|
+
const options = installOptions(args);
|
|
239
|
+
const installInput = {
|
|
220
240
|
sourceRoot: ROOT,
|
|
221
241
|
destination: DEST,
|
|
222
242
|
codexMirror: CODEX_MIRROR,
|
|
223
243
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
224
244
|
version: VERSION,
|
|
225
245
|
skills: SKILLS,
|
|
226
|
-
replaceSkills,
|
|
246
|
+
replaceSkills: options.replaceSkills,
|
|
227
247
|
legacySkills: LEGACY_SKILLS,
|
|
228
248
|
retiredSkills: RETIRED_SKILLS
|
|
249
|
+
};
|
|
250
|
+
if (options.dryRun) {
|
|
251
|
+
const plan = await planSkillSetInstallation(installInput);
|
|
252
|
+
printJsonOrHuman(plan, options.json, 'yam install dry run');
|
|
253
|
+
if (!plan.ready)
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const result = await installSkillSetTransactional({
|
|
258
|
+
...installInput
|
|
229
259
|
});
|
|
230
260
|
console.log(`yam installed to ${DEST}`);
|
|
231
261
|
console.log(`install receipt: ${result.receiptPath}`);
|
|
@@ -305,6 +335,86 @@ async function loadManifest() {
|
|
|
305
335
|
async function readJson(file) {
|
|
306
336
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
307
337
|
}
|
|
338
|
+
async function readBoundedRegularJson(file, label = 'JSON input', maxBytes = GENERAL_STDIN_MAX_BYTES) {
|
|
339
|
+
const requested = path.resolve(expandHome(String(file || '')));
|
|
340
|
+
const physicalParent = await fsp.realpath(path.dirname(requested));
|
|
341
|
+
const target = path.join(physicalParent, path.basename(requested));
|
|
342
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
343
|
+
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
344
|
+
? fs.constants.O_NOFOLLOW
|
|
345
|
+
: 0;
|
|
346
|
+
const before = await fsp.lstat(target);
|
|
347
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
348
|
+
throw new Error(`${label} must be a regular JSON file no larger than ${maxBytes} bytes`);
|
|
349
|
+
}
|
|
350
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
351
|
+
try {
|
|
352
|
+
const opened = await handle.stat();
|
|
353
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
354
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > maxBytes) {
|
|
355
|
+
throw new Error(`${label} must be a stable regular JSON file no larger than ${maxBytes} bytes`);
|
|
356
|
+
}
|
|
357
|
+
const bytes = await readBoundedFileHandle(handle, maxBytes);
|
|
358
|
+
const afterOpened = await handle.stat();
|
|
359
|
+
const after = await fsp.lstat(target);
|
|
360
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
361
|
+
if (bytes.length > maxBytes
|
|
362
|
+
|| after.isSymbolicLink()
|
|
363
|
+
|| !after.isFile()
|
|
364
|
+
|| after.dev !== opened.dev
|
|
365
|
+
|| after.ino !== opened.ino
|
|
366
|
+
|| afterOpened.dev !== opened.dev
|
|
367
|
+
|| afterOpened.ino !== opened.ino
|
|
368
|
+
|| afterOpened.size !== bytes.length) {
|
|
369
|
+
throw new Error(`${label} changed identity or size while being read`);
|
|
370
|
+
}
|
|
371
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
await handle.close();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function captureAbsoluteRegularDirectoryPath(target, label) {
|
|
378
|
+
const absolute = path.resolve(target);
|
|
379
|
+
const filesystemRoot = path.parse(absolute).root;
|
|
380
|
+
const relative = path.relative(filesystemRoot, absolute);
|
|
381
|
+
const identities = [];
|
|
382
|
+
let current = filesystemRoot;
|
|
383
|
+
for (const segment of relative ? ['', ...relative.split(path.sep)] : ['']) {
|
|
384
|
+
if (segment)
|
|
385
|
+
current = path.join(current, segment);
|
|
386
|
+
const stat = await fsp.lstat(current);
|
|
387
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
388
|
+
throw new Error(`${label} parent must be a regular physical directory: ${current}`);
|
|
389
|
+
}
|
|
390
|
+
identities.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
391
|
+
}
|
|
392
|
+
return identities;
|
|
393
|
+
}
|
|
394
|
+
async function revalidateAbsoluteRegularDirectoryPath(identities, label) {
|
|
395
|
+
for (const identity of identities) {
|
|
396
|
+
const stat = await fsp.lstat(identity.path);
|
|
397
|
+
if (stat.isSymbolicLink()
|
|
398
|
+
|| !stat.isDirectory()
|
|
399
|
+
|| stat.dev !== identity.dev
|
|
400
|
+
|| stat.ino !== identity.ino) {
|
|
401
|
+
throw new Error(`${label} parent changed identity while being read: ${identity.path}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function readBoundedFileHandle(handle, maxBytes) {
|
|
406
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
407
|
+
let total = 0;
|
|
408
|
+
while (total < buffer.length) {
|
|
409
|
+
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
|
|
410
|
+
if (bytesRead === 0)
|
|
411
|
+
break;
|
|
412
|
+
total += bytesRead;
|
|
413
|
+
}
|
|
414
|
+
if (total > maxBytes)
|
|
415
|
+
throw new Error(`JSON input exceeds ${maxBytes} bytes while being read`);
|
|
416
|
+
return buffer.subarray(0, total);
|
|
417
|
+
}
|
|
308
418
|
async function readJsonOrDefault(file, fallback = {}) {
|
|
309
419
|
try {
|
|
310
420
|
return JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
@@ -381,6 +491,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
381
491
|
'context-reuse.md',
|
|
382
492
|
'markdown-management.md',
|
|
383
493
|
'study-note.md',
|
|
494
|
+
'security-check.md',
|
|
384
495
|
'final-report.md',
|
|
385
496
|
'token-budget-reporter.md',
|
|
386
497
|
'memory.md'
|
|
@@ -399,7 +510,7 @@ async function verify({ quiet = false } = {}) {
|
|
|
399
510
|
issues.push(`project template missing section: ${section}`);
|
|
400
511
|
}
|
|
401
512
|
}
|
|
402
|
-
for (const template of ['ueye-review.md', 'ueye-comparison.md', 'mission-plan.md', 'runtime-proof.md', 'tuning-log.md']) {
|
|
513
|
+
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']) {
|
|
403
514
|
if (!await exists(path.join(ROOT, 'templates', template)))
|
|
404
515
|
issues.push(`missing template: ${template}`);
|
|
405
516
|
}
|
|
@@ -806,8 +917,9 @@ Usage:
|
|
|
806
917
|
yam study-note check [dir] [--report file|--text text] [--json]
|
|
807
918
|
|
|
808
919
|
Notes:
|
|
809
|
-
Read-only guard. It checks whether changed work has a Study Note
|
|
810
|
-
|
|
920
|
+
Read-only guard. It checks whether changed work has a complete Study Note followed immediately by Next step,
|
|
921
|
+
including relevant UI/CSS/DB hygiene and the ordered fix-first/planned handoff contract.
|
|
922
|
+
It does not generate, edit, or infer either section for you.
|
|
811
923
|
`);
|
|
812
924
|
}
|
|
813
925
|
async function studyNoteCheck(args = []) {
|
|
@@ -827,21 +939,30 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
827
939
|
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
828
940
|
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
829
941
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
942
|
+
const studyNoteText = studyNoteSectionText(reportText);
|
|
830
943
|
const checks = [
|
|
831
944
|
studyNoteGuardCheck('changed_files', changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial', changeScopeAvailable
|
|
832
945
|
? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
|
|
833
946
|
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'),
|
|
834
947
|
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
835
|
-
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(
|
|
836
|
-
studyNoteGuardCheck('execution_point', scopeAwareStatus(/(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(
|
|
837
|
-
studyNoteGuardCheck('before_after_or_change', scopeAwareStatus(/(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(
|
|
838
|
-
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(
|
|
839
|
-
studyNoteGuardCheck('syntax_or_structure', scopeAwareStatus(/(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(
|
|
840
|
-
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(
|
|
841
|
-
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(
|
|
948
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(studyNoteText)), 'Study Note should explain what the touched code/artifact does'),
|
|
949
|
+
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'),
|
|
950
|
+
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'),
|
|
951
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(studyNoteText)), 'Study Note should describe the expected behavior or result'),
|
|
952
|
+
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'),
|
|
953
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(studyNoteText)), 'Study Note should say what was checked'),
|
|
954
|
+
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'),
|
|
955
|
+
studyNoteGuardCheck('next_step_present', scopeAwareStatus(hasNextStepMarker(reportText)), 'changed-artifact reports should include a Next step section'),
|
|
956
|
+
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'),
|
|
957
|
+
studyNoteGuardCheck('study_note_then_next_step', scopeAwareStatus(hasStudyNoteThenNextStepOrder(reportText)), 'Next step should be the next report section immediately after Study Note'),
|
|
958
|
+
studyNoteGuardCheck('next_step_whole_process_scan', scopeAwareStatus(hasNextStepWholeProcessScan(reportText)), 'Next step should include current situation, forward outlook, a critical opinion, and an improvement recommendation'),
|
|
959
|
+
studyNoteGuardCheck('next_step_ordered_actions', scopeAwareStatus(hasNextStepOrderedActions(reportText)), 'Next step should include an ordered action sequence with fix-first work before planned work'),
|
|
960
|
+
studyNoteGuardCheck('next_step_evidence_and_ownership', scopeAwareStatus(hasNextStepEvidenceAndOwnership(reportText)), 'Next step should name evidence level or stamp plus the owner route or scope'),
|
|
961
|
+
studyNoteGuardCheck('next_step_safety_context', scopeAwareStatus(hasNextStepSafetyContext(reportText)), 'Next step should state blocker status, safe retry guidance, and side effects'),
|
|
962
|
+
studyNoteGuardCheck('next_step_truth_status', scopeAwareStatus(hasNextStepTruthStatus(reportText)), 'Next step should state its evidence-bounded truth status')
|
|
842
963
|
];
|
|
843
964
|
for (const requirement of hygieneRequirements) {
|
|
844
|
-
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(
|
|
965
|
+
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(studyNoteText, requirement), requirement.note));
|
|
845
966
|
}
|
|
846
967
|
const blockers = checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`);
|
|
847
968
|
const result = {
|
|
@@ -926,6 +1047,119 @@ async function readStudyNoteReportText(flags, dir) {
|
|
|
926
1047
|
function hasStudyNoteMarker(text = '') {
|
|
927
1048
|
return /(study note|study-note|학습\s*노트|스터디\s*노트)/i.test(text);
|
|
928
1049
|
}
|
|
1050
|
+
function hasNextStepMarker(text = '') {
|
|
1051
|
+
return /(next step|next-step|다음\s*(?:단계|할\s*일|작업))/i.test(text);
|
|
1052
|
+
}
|
|
1053
|
+
function reportSectionMarkers(text = '') {
|
|
1054
|
+
return String(text || '').split(/\r?\n/).map((line, index) => {
|
|
1055
|
+
const trimmed = line.trim();
|
|
1056
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*\s*$/);
|
|
1057
|
+
const named = trimmed.match(/^(study note|study-note|학습\s*노트|스터디\s*노트|next step|next-step|다음\s*(?:단계|할\s*일|작업))\s*:?[ \t]*$/i);
|
|
1058
|
+
const rawLabel = heading?.[1] || named?.[1] || '';
|
|
1059
|
+
if (!rawLabel)
|
|
1060
|
+
return null;
|
|
1061
|
+
const label = rawLabel.toLowerCase().replace(/[-\s]+/g, ' ').trim();
|
|
1062
|
+
const kind = /(study note|학습 노트|스터디 노트)/i.test(label)
|
|
1063
|
+
? 'study_note'
|
|
1064
|
+
: /(next step|다음 단계|다음 할 일|다음 작업)/i.test(label)
|
|
1065
|
+
? 'next_step'
|
|
1066
|
+
: 'other';
|
|
1067
|
+
return { index, kind, level: heading?.[0].match(/^#+/)?.[0].length || 0 };
|
|
1068
|
+
}).filter((item) => Boolean(item));
|
|
1069
|
+
}
|
|
1070
|
+
function hasStudyNoteThenNextStepOrder(text = '') {
|
|
1071
|
+
const markers = reportSectionMarkers(text);
|
|
1072
|
+
const studyIndex = markers.findIndex((item) => item.kind === 'study_note');
|
|
1073
|
+
if (studyIndex < 0)
|
|
1074
|
+
return false;
|
|
1075
|
+
return nextPeerSection(markers, studyIndex)?.kind === 'next_step';
|
|
1076
|
+
}
|
|
1077
|
+
function hasSingleStudyNoteNextStepPair(text = '') {
|
|
1078
|
+
const markers = reportSectionMarkers(text);
|
|
1079
|
+
return markers.filter((item) => item.kind === 'study_note').length === 1
|
|
1080
|
+
&& markers.filter((item) => item.kind === 'next_step').length === 1;
|
|
1081
|
+
}
|
|
1082
|
+
function hasNextStepWholeProcessScan(text = '') {
|
|
1083
|
+
const value = nextStepSectionText(text);
|
|
1084
|
+
const situation = /(situation|current state|현재\s*(?:상황|상태))/i.test(value);
|
|
1085
|
+
const outlook = /(outlook|forward|앞으로|향후|전망)/i.test(value);
|
|
1086
|
+
const critical = /(critical opinion|critique|비판적|비판\s*의견|tradeoff|약점)/i.test(value);
|
|
1087
|
+
const recommendation = /(recommendation|recommend|개선\s*추천|추천\s*의견|권장)/i.test(value);
|
|
1088
|
+
return situation && outlook && critical && recommendation;
|
|
1089
|
+
}
|
|
1090
|
+
function hasNextStepOrderedActions(text = '') {
|
|
1091
|
+
const value = nextStepSectionText(text);
|
|
1092
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1093
|
+
const actionLines = lines.filter((line) => /^\d+[.)]\s+/.test(line));
|
|
1094
|
+
const labeledLines = lines.filter((line) => /\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i.test(line));
|
|
1095
|
+
if (!actionLines.length || !/^1[.)]\s+/.test(actionLines[0]))
|
|
1096
|
+
return false;
|
|
1097
|
+
if (labeledLines.length !== actionLines.length || labeledLines.some((line) => !/^\d+[.)]\s+/.test(line)))
|
|
1098
|
+
return false;
|
|
1099
|
+
let plannedSeen = false;
|
|
1100
|
+
for (const [index, line] of actionLines.entries()) {
|
|
1101
|
+
const number = Number(line.match(/^(\d+)[.)]/)?.[1] || 0);
|
|
1102
|
+
if (number !== index + 1)
|
|
1103
|
+
return false;
|
|
1104
|
+
const kind = line.match(/\[(fix[- ]first|우선\s*(?:수정|해결|처리)|planned|계획(?:된|한)?\s*(?:작업|단계)?)\]/i)?.[1] || '';
|
|
1105
|
+
if (!kind)
|
|
1106
|
+
return false;
|
|
1107
|
+
const planned = /planned|계획/i.test(kind);
|
|
1108
|
+
if (planned)
|
|
1109
|
+
plannedSeen = true;
|
|
1110
|
+
else if (plannedSeen)
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
function hasNextStepEvidenceAndOwnership(text = '') {
|
|
1116
|
+
const value = nextStepSectionText(text);
|
|
1117
|
+
const evidence = /(evidence(?:[_ -](?:level|stamp))?|\bL[0-5]\b|근거|증거|검증\s*(?:수준|스탬프))/i.test(value);
|
|
1118
|
+
const ownership = /(owner(?:[_ -](?:route|scope))?|담당|소유|책임\s*(?:경로|범위|주체))/i.test(value);
|
|
1119
|
+
return evidence && ownership;
|
|
1120
|
+
}
|
|
1121
|
+
function hasNextStepSafetyContext(text = '') {
|
|
1122
|
+
const value = nextStepSectionText(text);
|
|
1123
|
+
const blocker = /(blocker|blocked(?:[_ -]by)?|차단|막힘|방해)/i.test(value);
|
|
1124
|
+
const safeRetry = /(safe[_ -]retry|안전[^\n]{0,20}재시도|재시도[^\n]{0,20}안전)/i.test(value);
|
|
1125
|
+
const sideEffects = /(side[_ -]effects?|부작용|영향)/i.test(value);
|
|
1126
|
+
return blocker && safeRetry && sideEffects;
|
|
1127
|
+
}
|
|
1128
|
+
function hasNextStepTruthStatus(text = '') {
|
|
1129
|
+
return /(truth[_ -]status|진실\s*상태|검증\s*상태)/i.test(nextStepSectionText(text));
|
|
1130
|
+
}
|
|
1131
|
+
function nextStepSectionText(text = '') {
|
|
1132
|
+
const value = String(text || '');
|
|
1133
|
+
const lines = value.split(/\r?\n/);
|
|
1134
|
+
const markers = reportSectionMarkers(value);
|
|
1135
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1136
|
+
if (study < 0)
|
|
1137
|
+
return '';
|
|
1138
|
+
const adjacent = nextPeerSectionIndex(markers, study);
|
|
1139
|
+
if (adjacent < 0 || markers[adjacent].kind !== 'next_step')
|
|
1140
|
+
return '';
|
|
1141
|
+
const end = nextPeerSection(markers, adjacent);
|
|
1142
|
+
return lines.slice(markers[adjacent].index + 1, end?.index ?? lines.length).join('\n');
|
|
1143
|
+
}
|
|
1144
|
+
function studyNoteSectionText(text = '') {
|
|
1145
|
+
const value = String(text || '');
|
|
1146
|
+
const lines = value.split(/\r?\n/);
|
|
1147
|
+
const markers = reportSectionMarkers(value);
|
|
1148
|
+
const study = markers.findIndex((item) => item.kind === 'study_note');
|
|
1149
|
+
if (study < 0)
|
|
1150
|
+
return '';
|
|
1151
|
+
const end = nextPeerSection(markers, study);
|
|
1152
|
+
return lines.slice(markers[study].index + 1, end?.index ?? lines.length).join('\n');
|
|
1153
|
+
}
|
|
1154
|
+
function nextPeerSection(markers, start) {
|
|
1155
|
+
const nextIndex = nextPeerSectionIndex(markers, start);
|
|
1156
|
+
return nextIndex < 0 ? undefined : markers[nextIndex];
|
|
1157
|
+
}
|
|
1158
|
+
function nextPeerSectionIndex(markers, start) {
|
|
1159
|
+
const current = markers[start];
|
|
1160
|
+
const relative = markers.slice(start + 1).findIndex((item) => current.level === 0 || item.level === 0 || item.level <= current.level);
|
|
1161
|
+
return relative < 0 ? -1 : start + 1 + relative;
|
|
1162
|
+
}
|
|
929
1163
|
function studyNoteGuardCheck(id, status, nextAction) {
|
|
930
1164
|
return {
|
|
931
1165
|
id,
|
|
@@ -2138,26 +2372,27 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
2138
2372
|
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2139
2373
|
const changedFiles = changedFileDetection.files;
|
|
2140
2374
|
const lines = [
|
|
2141
|
-
'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.'
|
|
2375
|
+
'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.'
|
|
2142
2376
|
];
|
|
2143
2377
|
if (!changedFileDetection.available) {
|
|
2144
2378
|
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2145
|
-
lines.push('Determine manually whether artifacts changed and include
|
|
2379
|
+
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.');
|
|
2146
2380
|
return lines.join('\n');
|
|
2147
2381
|
}
|
|
2148
2382
|
if (!changedFiles.length) {
|
|
2149
|
-
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add
|
|
2383
|
+
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.');
|
|
2150
2384
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
2151
2385
|
return lines.join('\n');
|
|
2152
2386
|
}
|
|
2153
2387
|
lines.push(`Changed files detected (${Math.min(changedFiles.length, 8)} shown): ${changedFiles.slice(0, 8).join(', ')}`);
|
|
2154
2388
|
lines.push('Study Note minimum: touched code/artifact, role, execution point, before/after change, expected behavior, one syntax/structure insight, verification, and limits.');
|
|
2389
|
+
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.');
|
|
2155
2390
|
const hygiene = studyNoteHygieneRequirements(changedFiles);
|
|
2156
2391
|
if (hygiene.length) {
|
|
2157
2392
|
lines.push(`Architecture hygiene required: ${hygiene.map((item) => item.id).join(', ')}.`);
|
|
2158
2393
|
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.');
|
|
2159
2394
|
}
|
|
2160
|
-
lines.push('This prompt reminder does not generate or edit
|
|
2395
|
+
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.');
|
|
2161
2396
|
return lines.join('\n');
|
|
2162
2397
|
}
|
|
2163
2398
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
@@ -2237,12 +2472,16 @@ async function safety(args = []) {
|
|
|
2237
2472
|
async function release(args = []) {
|
|
2238
2473
|
const subcommand = args[0] || 'help';
|
|
2239
2474
|
if (subcommand !== 'report') {
|
|
2240
|
-
console.error('usage: yam release report [--json]');
|
|
2475
|
+
console.error('usage: yam release report [--auth-mode auto|oidc|token] [--json]');
|
|
2241
2476
|
process.exitCode = 1;
|
|
2242
2477
|
return;
|
|
2243
2478
|
}
|
|
2244
|
-
const
|
|
2245
|
-
const
|
|
2479
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['auth-mode', 'json']));
|
|
2480
|
+
const authMode = String(flags.auth_mode || 'auto');
|
|
2481
|
+
if (!['auto', 'oidc', 'token'].includes(authMode))
|
|
2482
|
+
throw new Error('--auth-mode must be auto, oidc, or token');
|
|
2483
|
+
const asJson = Boolean(flags.json);
|
|
2484
|
+
const report = runReleaseReport(authMode);
|
|
2246
2485
|
if (asJson) {
|
|
2247
2486
|
console.log(JSON.stringify(report, null, 2));
|
|
2248
2487
|
if (!report.ok)
|
|
@@ -2270,7 +2509,7 @@ async function release(args = []) {
|
|
|
2270
2509
|
if (report.publish_readiness) {
|
|
2271
2510
|
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
2272
2511
|
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
2273
|
-
console.log(`-
|
|
2512
|
+
console.log(`- Publish auth: ${report.publish_readiness.auth.mode} (${report.publish_readiness.auth.status})`);
|
|
2274
2513
|
}
|
|
2275
2514
|
if (report.failed.length) {
|
|
2276
2515
|
console.log('- Failed:');
|
|
@@ -2295,7 +2534,7 @@ async function release(args = []) {
|
|
|
2295
2534
|
if (!report.ok)
|
|
2296
2535
|
process.exitCode = 1;
|
|
2297
2536
|
}
|
|
2298
|
-
function runReleaseReport() {
|
|
2537
|
+
function runReleaseReport(authMode = 'auto') {
|
|
2299
2538
|
const startedAt = new Date().toISOString();
|
|
2300
2539
|
const checks = [
|
|
2301
2540
|
['typecheck', ['npm', ['run', 'typecheck']]],
|
|
@@ -2310,7 +2549,7 @@ function runReleaseReport() {
|
|
|
2310
2549
|
const tarball = releaseTarballProvenance();
|
|
2311
2550
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
2312
2551
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
2313
|
-
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
2552
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence, authMode);
|
|
2314
2553
|
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
2315
2554
|
const preliminaryOk = failed.length === 0 && publishReadiness.status === 'ready';
|
|
2316
2555
|
const readinessReceipt = releaseReadinessReceipt(provenance, freshness, tarball, publishReadiness, preliminaryOk);
|
|
@@ -2375,9 +2614,14 @@ function releaseReadinessReceipt(provenance = {}, freshness = {}, tarball = {},
|
|
|
2375
2614
|
truth_status: publishReadiness.registry?.truth_status || 'partial'
|
|
2376
2615
|
},
|
|
2377
2616
|
auth: {
|
|
2378
|
-
|
|
2617
|
+
requested_mode: String(publishReadiness.auth?.requested_mode || 'auto'),
|
|
2618
|
+
mode: String(publishReadiness.auth?.mode || 'manual_token'),
|
|
2619
|
+
command: String(publishReadiness.auth?.command || 'npm whoami'),
|
|
2620
|
+
required_for_selected_mode: Boolean(publishReadiness.auth?.required_for_selected_mode),
|
|
2379
2621
|
status: String(publishReadiness.auth?.status || 'not_verified'),
|
|
2380
|
-
account: publishReadiness.auth?.
|
|
2622
|
+
account: String(publishReadiness.auth?.account || ''),
|
|
2623
|
+
local_token: publishReadiness.auth?.local_token || null,
|
|
2624
|
+
trusted_publisher: publishReadiness.auth?.trusted_publisher || null,
|
|
2381
2625
|
truth_status: publishReadiness.auth?.truth_status || 'partial'
|
|
2382
2626
|
},
|
|
2383
2627
|
git: {
|
|
@@ -2476,15 +2720,33 @@ function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
|
|
|
2476
2720
|
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
2477
2721
|
};
|
|
2478
2722
|
}
|
|
2479
|
-
function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, publishBlockers = []) {
|
|
2723
|
+
function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, publishBlockers = [], authMode = 'auto') {
|
|
2480
2724
|
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
2481
2725
|
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
2482
2726
|
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
2483
2727
|
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
2484
|
-
const
|
|
2728
|
+
const trustedPublisherWorkflowPath = '.github/workflows/release.yml';
|
|
2729
|
+
const trustedPublisherWorkflowFile = path.join(ROOT, trustedPublisherWorkflowPath);
|
|
2730
|
+
const trustedPublisherWorkflowExists = fs.existsSync(trustedPublisherWorkflowFile);
|
|
2731
|
+
const trustedPublisherWorkflow = inspectTrustedPublisherWorkflowText(trustedPublisherWorkflowExists ? fs.readFileSync(trustedPublisherWorkflowFile, 'utf8') : '', trustedPublisherWorkflowPath, trustedPublisherWorkflowExists);
|
|
2732
|
+
const authReadiness = buildReleaseAuthReadiness({
|
|
2733
|
+
workflow: trustedPublisherWorkflow,
|
|
2734
|
+
local_token: {
|
|
2735
|
+
command: 'npm whoami',
|
|
2736
|
+
ok: whoamiProbe.ok,
|
|
2737
|
+
note: whoamiProbe.note
|
|
2738
|
+
},
|
|
2739
|
+
requested_mode: authMode
|
|
2740
|
+
});
|
|
2741
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks, {
|
|
2742
|
+
package_name: PACKAGE_JSON.name,
|
|
2743
|
+
version: VERSION
|
|
2744
|
+
});
|
|
2485
2745
|
const latestProbe = registryStatus.checked
|
|
2486
2746
|
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
2487
|
-
:
|
|
2747
|
+
: registryStatus.parse_failed
|
|
2748
|
+
? { ok: false, stdout: '', note: `registry check output did not prove ${PACKAGE_JSON.name}@${VERSION} is unpublished: ${registryStatus.note}` }
|
|
2749
|
+
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
2488
2750
|
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
2489
2751
|
const blockers = [];
|
|
2490
2752
|
const versionStatus = latestProbe.ok
|
|
@@ -2493,8 +2755,8 @@ function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, pub
|
|
|
2493
2755
|
if (!registryProbe.ok) {
|
|
2494
2756
|
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'));
|
|
2495
2757
|
}
|
|
2496
|
-
|
|
2497
|
-
blockers.push(readinessBlocker(
|
|
2758
|
+
for (const blocker of authReadiness.blockers) {
|
|
2759
|
+
blockers.push(readinessBlocker(blocker.kind, blocker.reason, blocker.safe_next_action, blocker.command, blocker.severity));
|
|
2498
2760
|
}
|
|
2499
2761
|
if (!latestProbe.ok) {
|
|
2500
2762
|
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
@@ -2533,30 +2795,22 @@ function releasePublishReadiness(checks = [], provenance = {}, tarball = {}, pub
|
|
|
2533
2795
|
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
2534
2796
|
},
|
|
2535
2797
|
auth: {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2798
|
+
requested_mode: authReadiness.requested_mode,
|
|
2799
|
+
mode: authReadiness.mode,
|
|
2800
|
+
command: authReadiness.local_token.command,
|
|
2801
|
+
required_for_selected_mode: authReadiness.local_token.required_for_selected_mode,
|
|
2802
|
+
status: authReadiness.status,
|
|
2803
|
+
account: authReadiness.local_token.account,
|
|
2804
|
+
note: authReadiness.next_action,
|
|
2805
|
+
local_token: authReadiness.local_token,
|
|
2806
|
+
trusted_publisher: authReadiness.trusted_publisher,
|
|
2807
|
+
truth_status: authReadiness.truth_status
|
|
2541
2808
|
},
|
|
2542
2809
|
blockers: uniqueBlockers,
|
|
2543
2810
|
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
2544
2811
|
truth_status: status === 'ready' ? 'verified' : 'blocked'
|
|
2545
2812
|
};
|
|
2546
2813
|
}
|
|
2547
|
-
function releaseRegistryStatusFromChecks(checks = []) {
|
|
2548
|
-
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
2549
|
-
const note = String(registryCheck?.note || '');
|
|
2550
|
-
if (registryCheck?.status !== 'passed')
|
|
2551
|
-
return { checked: false, latest_version: '', not_published: false, note };
|
|
2552
|
-
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
2553
|
-
return {
|
|
2554
|
-
checked: true,
|
|
2555
|
-
latest_version: latest,
|
|
2556
|
-
not_published: /not published yet/i.test(note),
|
|
2557
|
-
note
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
2814
|
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
2561
2815
|
return {
|
|
2562
2816
|
kind,
|
|
@@ -2591,13 +2845,13 @@ function releaseStudyNote(publishReadiness = {}, publishBlockers = []) {
|
|
|
2591
2845
|
}
|
|
2592
2846
|
return buildStudyNote({
|
|
2593
2847
|
issue_code: 'npm publish readiness',
|
|
2594
|
-
issue_role: 'This part checks registry, version,
|
|
2848
|
+
issue_role: 'This part checks registry, version, the selected OIDC or manual-token authentication path, tarball, and local release checks before any public publish.',
|
|
2595
2849
|
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
2596
2850
|
changed_code: 'yam release report',
|
|
2597
2851
|
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
2598
|
-
change_summary: 'The report
|
|
2599
|
-
why_important: '
|
|
2600
|
-
learning_note: '
|
|
2852
|
+
change_summary: 'The report separates Trusted Publisher OIDC evidence from optional local-token evidence and gives a path-specific safe next action.',
|
|
2853
|
+
why_important: 'npm whoami cannot prove OIDC readiness; treating its E401 as an OIDC blocker would recommend the wrong credential model.',
|
|
2854
|
+
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.'
|
|
2601
2855
|
});
|
|
2602
2856
|
}
|
|
2603
2857
|
function releaseNextActions(checks = [], provenance = {}, tarball = {}, publishBlockers = [], publishReadiness = {}) {
|
|
@@ -2769,20 +3023,128 @@ async function loop(args = []) {
|
|
|
2769
3023
|
process.exitCode = 1;
|
|
2770
3024
|
return loopUsage();
|
|
2771
3025
|
}
|
|
3026
|
+
async function scout(args = []) {
|
|
3027
|
+
const subcommand = args[0] || 'help';
|
|
3028
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3029
|
+
return scoutUsage();
|
|
3030
|
+
if (subcommand === 'receipt')
|
|
3031
|
+
return scoutReceipt(args.slice(1));
|
|
3032
|
+
console.error(`unknown scout command: ${subcommand}`);
|
|
3033
|
+
process.exitCode = 1;
|
|
3034
|
+
return scoutUsage();
|
|
3035
|
+
}
|
|
3036
|
+
function scoutUsage() {
|
|
3037
|
+
console.log(`yam scout
|
|
3038
|
+
|
|
3039
|
+
Immutable, operator-supplied research evidence helpers. These commands do not browse, fetch, or trust external content as instructions.
|
|
3040
|
+
|
|
3041
|
+
Usage:
|
|
3042
|
+
yam scout receipt create [--root dir] --receipt-path .yam/scout/run.json --spec scout-spec.json [--json]
|
|
3043
|
+
yam scout receipt verify [--root dir] --receipt-path .yam/scout/run.json [--json]
|
|
3044
|
+
`);
|
|
3045
|
+
}
|
|
3046
|
+
async function scoutReceipt(args = []) {
|
|
3047
|
+
const action = args[0] || 'help';
|
|
3048
|
+
if (action === 'help' || action === '--help' || action === '-h')
|
|
3049
|
+
return scoutUsage();
|
|
3050
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['root', 'receipt-path', 'spec', 'json']));
|
|
3051
|
+
const root = path.resolve(expandHome(String(flags.root || process.cwd())));
|
|
3052
|
+
try {
|
|
3053
|
+
let result;
|
|
3054
|
+
if (action === 'create') {
|
|
3055
|
+
if (!flags.receipt_path || !flags.spec)
|
|
3056
|
+
throw new Error('create requires --receipt-path and --spec');
|
|
3057
|
+
result = await createScoutReceipt({
|
|
3058
|
+
root,
|
|
3059
|
+
receipt_path: String(flags.receipt_path),
|
|
3060
|
+
spec: await readBoundedRegularJson(flags.spec, 'Scout spec')
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
else if (action === 'verify') {
|
|
3064
|
+
if (!flags.receipt_path)
|
|
3065
|
+
throw new Error('verify requires --receipt-path');
|
|
3066
|
+
result = await verifyScoutReceiptFile({ root, receipt_path: String(flags.receipt_path) });
|
|
3067
|
+
}
|
|
3068
|
+
else {
|
|
3069
|
+
throw new Error(`unknown Scout receipt action: ${action}`);
|
|
3070
|
+
}
|
|
3071
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3072
|
+
if (result.truth_status === 'blocked')
|
|
3073
|
+
process.exitCode = 1;
|
|
3074
|
+
}
|
|
3075
|
+
catch (error) {
|
|
3076
|
+
const result = {
|
|
3077
|
+
schema: 'yam.scout-receipt-error.v1',
|
|
3078
|
+
truth_status: 'blocked',
|
|
3079
|
+
error: errorMessage(error),
|
|
3080
|
+
next_action: 'repair the confined receipt path, canonical entity, version clocks, source digests, claim links, or failure taxonomy before retrying'
|
|
3081
|
+
};
|
|
3082
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Scout receipt');
|
|
3083
|
+
process.exitCode = 1;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function nextStepCommand(args = []) {
|
|
3087
|
+
const subcommand = args[0] || 'help';
|
|
3088
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3089
|
+
return nextStepUsage();
|
|
3090
|
+
const flags = parseSimpleFlags(args.slice(1), new Set(['spec', 'receipt', 'json']));
|
|
3091
|
+
try {
|
|
3092
|
+
let result;
|
|
3093
|
+
if (subcommand === 'report') {
|
|
3094
|
+
if (!flags.spec)
|
|
3095
|
+
throw new Error('report requires --spec');
|
|
3096
|
+
result = buildNextStep(await readBoundedRegularJson(flags.spec, 'Next step spec'));
|
|
3097
|
+
}
|
|
3098
|
+
else if (subcommand === 'verify') {
|
|
3099
|
+
if (!flags.receipt)
|
|
3100
|
+
throw new Error('verify requires --receipt');
|
|
3101
|
+
result = verifyNextStep(await readBoundedRegularJson(flags.receipt, 'Next step receipt'));
|
|
3102
|
+
}
|
|
3103
|
+
else {
|
|
3104
|
+
throw new Error(`unknown Next step command: ${subcommand}`);
|
|
3105
|
+
}
|
|
3106
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3107
|
+
if (result.truth_status === 'blocked')
|
|
3108
|
+
process.exitCode = 1;
|
|
3109
|
+
}
|
|
3110
|
+
catch (error) {
|
|
3111
|
+
const result = {
|
|
3112
|
+
schema: 'yam.next-step-error.v1',
|
|
3113
|
+
truth_status: 'blocked',
|
|
3114
|
+
error: errorMessage(error),
|
|
3115
|
+
next_action: 'repair the whole-process scan, ordered steps, evidence level/stamp, owner scope, blockers, or safe retry before retrying'
|
|
3116
|
+
};
|
|
3117
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Next step');
|
|
3118
|
+
process.exitCode = 1;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
function nextStepUsage() {
|
|
3122
|
+
console.log(`yam next-step
|
|
3123
|
+
|
|
3124
|
+
Build or verify an evidence-bound ordered handoff after a quick whole-process scan.
|
|
3125
|
+
|
|
3126
|
+
Usage:
|
|
3127
|
+
yam next-step report --spec next-step-spec.json [--json]
|
|
3128
|
+
yam next-step verify --receipt next-step-receipt.json [--json]
|
|
3129
|
+
`);
|
|
3130
|
+
}
|
|
2772
3131
|
function loopUsage() {
|
|
2773
3132
|
console.log(`yam loop
|
|
2774
3133
|
|
|
2775
|
-
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers,
|
|
3134
|
+
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.
|
|
2776
3135
|
|
|
2777
3136
|
Usage:
|
|
2778
|
-
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]
|
|
3137
|
+
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]
|
|
2779
3138
|
`);
|
|
2780
3139
|
}
|
|
2781
3140
|
async function loopReport(args = []) {
|
|
2782
3141
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
2783
3142
|
return loopUsage();
|
|
2784
|
-
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']));
|
|
3143
|
+
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']));
|
|
2785
3144
|
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
3145
|
+
const nextStepSpec = flags.next_step_spec
|
|
3146
|
+
? await readBoundedRegularJson(flags.next_step_spec, 'Next step spec')
|
|
3147
|
+
: undefined;
|
|
2786
3148
|
const report = buildLoopReport({
|
|
2787
3149
|
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
2788
3150
|
intent: String(flags.intent || ''),
|
|
@@ -2821,6 +3183,7 @@ async function loopReport(args = []) {
|
|
|
2821
3183
|
scope_owner: String(flags.scope_owner || flags.owner || ''),
|
|
2822
3184
|
side_effects: arrayFlag(flags.side_effect),
|
|
2823
3185
|
avoidance_note: String(flags.avoidance_note || ''),
|
|
3186
|
+
next_step: nextStepSpec,
|
|
2824
3187
|
issue_code: String(flags.issue_code || ''),
|
|
2825
3188
|
issue_role: String(flags.issue_role || ''),
|
|
2826
3189
|
issue_symptom: String(flags.issue_symptom || ''),
|
|
@@ -3154,35 +3517,40 @@ async function readOptionalSpecs(value, label) {
|
|
|
3154
3517
|
return specs;
|
|
3155
3518
|
}
|
|
3156
3519
|
async function readBoundedJsonSpec(file, label) {
|
|
3157
|
-
const
|
|
3520
|
+
const physicalParent = await fsp.realpath(path.dirname(file));
|
|
3521
|
+
const target = path.join(physicalParent, path.basename(file));
|
|
3522
|
+
const parents = await captureAbsoluteRegularDirectoryPath(physicalParent, label);
|
|
3523
|
+
const before = await fsp.lstat(target);
|
|
3158
3524
|
if (before.isSymbolicLink() || !before.isFile())
|
|
3159
|
-
throw new Error(`${label} must be a regular non-symlink file: ${
|
|
3525
|
+
throw new Error(`${label} must be a regular non-symlink file: ${target}`);
|
|
3160
3526
|
if (before.size > 256 * 1024)
|
|
3161
|
-
throw new Error(`${label} exceeds the 256 KiB limit: ${
|
|
3527
|
+
throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3162
3528
|
const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
|
|
3163
3529
|
? fs.constants.O_NOFOLLOW
|
|
3164
3530
|
: 0;
|
|
3165
|
-
const handle = await fsp.open(
|
|
3531
|
+
const handle = await fsp.open(target, fs.constants.O_RDONLY | noFollow);
|
|
3166
3532
|
let text = '';
|
|
3167
3533
|
try {
|
|
3168
3534
|
const opened = await handle.stat();
|
|
3535
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3169
3536
|
if (!opened.isFile() || !sameSpecFileIdentity(before, opened))
|
|
3170
|
-
throw new Error(`${label} identity changed before open: ${
|
|
3537
|
+
throw new Error(`${label} identity changed before open: ${target}`);
|
|
3171
3538
|
if (opened.size > 256 * 1024)
|
|
3172
|
-
throw new Error(`${label} exceeds the 256 KiB limit: ${
|
|
3539
|
+
throw new Error(`${label} exceeds the 256 KiB limit: ${target}`);
|
|
3173
3540
|
const bytes = Buffer.alloc(opened.size);
|
|
3174
3541
|
let offset = 0;
|
|
3175
3542
|
while (offset < bytes.length) {
|
|
3176
3543
|
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
3177
3544
|
if (!bytesRead)
|
|
3178
|
-
throw new Error(`${label} became shorter while reading: ${
|
|
3545
|
+
throw new Error(`${label} became shorter while reading: ${target}`);
|
|
3179
3546
|
offset += bytesRead;
|
|
3180
3547
|
}
|
|
3181
3548
|
const openedAfter = await handle.stat();
|
|
3182
|
-
const after = await fsp.lstat(
|
|
3549
|
+
const after = await fsp.lstat(target);
|
|
3550
|
+
await revalidateAbsoluteRegularDirectoryPath(parents, label);
|
|
3183
3551
|
if (!sameSpecFileIdentity(opened, openedAfter) || openedAfter.size !== opened.size
|
|
3184
3552
|
|| after.isSymbolicLink() || !after.isFile() || !sameSpecFileIdentity(opened, after)) {
|
|
3185
|
-
throw new Error(`${label} changed size or identity while reading: ${
|
|
3553
|
+
throw new Error(`${label} changed size or identity while reading: ${target}`);
|
|
3186
3554
|
}
|
|
3187
3555
|
text = bytes.toString('utf8');
|
|
3188
3556
|
}
|
|
@@ -3191,7 +3559,7 @@ async function readBoundedJsonSpec(file, label) {
|
|
|
3191
3559
|
}
|
|
3192
3560
|
const value = JSON.parse(text);
|
|
3193
3561
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3194
|
-
throw new Error(`${label} must contain one JSON object: ${
|
|
3562
|
+
throw new Error(`${label} must contain one JSON object: ${target}`);
|
|
3195
3563
|
return value;
|
|
3196
3564
|
}
|
|
3197
3565
|
function sameSpecFileIdentity(left, right) {
|
|
@@ -5673,11 +6041,13 @@ async function printTemplate(name = '') {
|
|
|
5673
6041
|
mission: 'mission-plan.md',
|
|
5674
6042
|
proof: 'runtime-proof.md',
|
|
5675
6043
|
runtime: 'runtime-proof.md',
|
|
5676
|
-
tuning: 'tuning-log.md'
|
|
6044
|
+
tuning: 'tuning-log.md',
|
|
6045
|
+
security: 'security-check-note.md',
|
|
6046
|
+
'security-ci': 'security-gate.yml'
|
|
5677
6047
|
};
|
|
5678
6048
|
const file = map[key];
|
|
5679
6049
|
if (!file) {
|
|
5680
|
-
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning>');
|
|
6050
|
+
console.error('usage: yam template <project|ueye|ueye-comparison|mission|proof|tuning|security|security-ci>');
|
|
5681
6051
|
process.exitCode = 1;
|
|
5682
6052
|
return;
|
|
5683
6053
|
}
|
|
@@ -5944,9 +6314,12 @@ function showRequestedHelp(args) {
|
|
|
5944
6314
|
return true;
|
|
5945
6315
|
}
|
|
5946
6316
|
const commandUsage = {
|
|
6317
|
+
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.'),
|
|
5947
6318
|
context: contextUsage,
|
|
5948
6319
|
cleanup: cleanupUsage,
|
|
5949
6320
|
'study-note': studyNoteUsage,
|
|
6321
|
+
'next-step': nextStepUsage,
|
|
6322
|
+
scout: scoutUsage,
|
|
5950
6323
|
tools: toolsUsage,
|
|
5951
6324
|
hook: hookUsage,
|
|
5952
6325
|
loop: loopUsage,
|
|
@@ -5969,6 +6342,17 @@ async function main() {
|
|
|
5969
6342
|
return;
|
|
5970
6343
|
if (command === 'help')
|
|
5971
6344
|
return usage();
|
|
6345
|
+
if (command === 'security') {
|
|
6346
|
+
if (args.length === 1 || ['help', '--help', '-h'].includes(args[1])) {
|
|
6347
|
+
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.');
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
6350
|
+
const result = await runSecurityCommand(args.slice(1), { cwd: process.cwd(), cliPath: fileURLToPath(import.meta.url) });
|
|
6351
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6352
|
+
if (!result.ok)
|
|
6353
|
+
process.exitCode = 1;
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
5972
6356
|
if (command === 'install')
|
|
5973
6357
|
return install(args.slice(1));
|
|
5974
6358
|
if (command === 'uninstall')
|
|
@@ -5993,8 +6377,12 @@ async function main() {
|
|
|
5993
6377
|
return proof(process.argv.slice(3));
|
|
5994
6378
|
if (command === 'study-note')
|
|
5995
6379
|
return studyNote(process.argv.slice(3));
|
|
6380
|
+
if (command === 'next-step')
|
|
6381
|
+
return nextStepCommand(process.argv.slice(3));
|
|
5996
6382
|
if (command === 'loop')
|
|
5997
6383
|
return loop(process.argv.slice(3));
|
|
6384
|
+
if (command === 'scout')
|
|
6385
|
+
return scout(process.argv.slice(3));
|
|
5998
6386
|
if (command === 'ueye')
|
|
5999
6387
|
return ueye(process.argv.slice(3));
|
|
6000
6388
|
if (command === 'media')
|