yam-flow 2.7.0 → 2.8.0

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