yam-flow 2.3.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bin/yam.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import fsp from 'node:fs/promises';
5
6
  import { createRequire } from 'node:module';
@@ -34,10 +35,9 @@ import {
34
35
  verifyUeyeRevisionHistory
35
36
  } from '../lib/ueye-artifacts.js';
36
37
  import {
37
- INSTALL_LOCK_NAME,
38
- INSTALL_RECEIPT_NAME,
39
38
  inspectSkillInstallation,
40
- installSkillSetTransactional
39
+ installSkillSetTransactional,
40
+ uninstallSkillSetSafely
41
41
  } from '../lib/skill-installation.js';
42
42
 
43
43
  type AnyRecord = Record<string, any>;
@@ -84,6 +84,13 @@ const VERSION = String(PACKAGE_JSON.version || '0.0.0');
84
84
  const PROJECT_PACK = 'yam.project.md';
85
85
  const LEGACY_PROJECT_PACK = 'timeto.project.md';
86
86
  const PACK_STALE_DAYS = 30;
87
+ const INSTRUCTION_DUPLICATION_LIMITS = {
88
+ maxFiles: 24,
89
+ maxBytesPerFile: 128000,
90
+ maxGroups: 8,
91
+ maxDepth: 6,
92
+ minDirectiveChars: 40
93
+ };
87
94
  const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
88
95
  const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
89
96
  const YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
@@ -151,7 +158,7 @@ Usage:
151
158
  yam list
152
159
  yam status
153
160
  yam verify
154
- yam detect [dir]
161
+ yam detect [dir] [--json]
155
162
  yam pack [dir]
156
163
  yam context pressure [dir] [--json]
157
164
  yam cleanup scan [dir] [--json]
@@ -180,7 +187,7 @@ Usage:
180
187
  yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
181
188
  yam template <project|ueye|mission|proof|tuning>
182
189
  yam tune-log [dir]
183
- yam install
190
+ yam install [--replace-user-skill name]
184
191
  yam uninstall
185
192
  yam doctor [--json]
186
193
  yam examples
@@ -206,7 +213,32 @@ async function rmrf(target) {
206
213
  await fsp.rm(target, { recursive: true, force: true });
207
214
  }
208
215
 
209
- async function install() {
216
+ function installReplacementSkills(args: string[] = []) {
217
+ const skills: string[] = [];
218
+ for (let index = 0; index < args.length; index += 1) {
219
+ const arg = String(args[index] || '');
220
+ if (arg === '--replace-user-skill') {
221
+ const skill = String(args[index + 1] || '');
222
+ if (!skill || skill.startsWith('--')) {
223
+ throw new Error('missing skill name after --replace-user-skill');
224
+ }
225
+ skills.push(skill);
226
+ index += 1;
227
+ continue;
228
+ }
229
+ if (arg.startsWith('--replace-user-skill=')) {
230
+ const skill = arg.slice('--replace-user-skill='.length);
231
+ if (!skill) throw new Error('missing skill name after --replace-user-skill=');
232
+ skills.push(skill);
233
+ continue;
234
+ }
235
+ throw new Error(`unknown yam install argument: ${arg}`);
236
+ }
237
+ return [...new Set(skills)];
238
+ }
239
+
240
+ async function install(args: string[] = []) {
241
+ const replaceSkills = installReplacementSkills(args);
210
242
  const result = await installSkillSetTransactional({
211
243
  sourceRoot: ROOT,
212
244
  destination: DEST,
@@ -214,6 +246,7 @@ async function install() {
214
246
  packageName: String(PACKAGE_JSON.name || 'yam-flow'),
215
247
  version: VERSION,
216
248
  skills: SKILLS,
249
+ replaceSkills,
217
250
  legacySkills: LEGACY_SKILLS,
218
251
  retiredSkills: RETIRED_SKILLS
219
252
  });
@@ -224,20 +257,20 @@ async function install() {
224
257
  console.log('Restart Codex to reload skills.');
225
258
  }
226
259
 
227
- async function uninstall() {
228
- const lockPath = path.join(DEST, INSTALL_LOCK_NAME);
229
- if (await exists(lockPath)) {
230
- throw new Error(`cannot uninstall while an install lock exists: ${lockPath}`);
231
- }
232
- for (const skill of [...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
233
- await rmrf(path.join(DEST, skill));
234
- if (CODEX_MIRROR !== DEST) {
235
- await rmrf(path.join(CODEX_MIRROR, skill));
236
- }
237
- }
238
- await rmrf(path.join(DEST, INSTALL_RECEIPT_NAME));
260
+ async function uninstall(args: string[] = []) {
261
+ if (args.length) throw new Error(`unknown yam uninstall argument: ${args[0]}`);
262
+ const result = await uninstallSkillSetSafely({
263
+ destination: DEST,
264
+ codexMirror: CODEX_MIRROR,
265
+ packageName: String(PACKAGE_JSON.name || 'yam-flow'),
266
+ skills: SKILLS
267
+ });
239
268
  console.log(`yam removed from ${DEST}`);
240
- if (CODEX_MIRROR !== DEST) console.log(`yam mirror entries removed from ${CODEX_MIRROR}`);
269
+ console.log(`removed receipt: ${result.receiptPath}`);
270
+ for (const warning of result.cleanupWarnings) console.warn(`warning: ${warning}`);
271
+ if (CODEX_MIRROR !== DEST) {
272
+ console.log(`unowned legacy, retired, and Codex mirror entries were preserved`);
273
+ }
241
274
  console.log('Restart Codex to unload skills.');
242
275
  }
243
276
 
@@ -426,12 +459,20 @@ async function doctor(args = []) {
426
459
  console.log('yam doctor: ok');
427
460
  console.log('No hooks, automations, or global config are required.');
428
461
  console.log('Skill install state is reported by `yam status`.');
462
+ if (report.preservedUnprovenSkillEntries.length) {
463
+ console.log(`Preserved unproven legacy/retired/mirror entries: ${report.preservedUnprovenSkillEntries.length}`);
464
+ }
429
465
  console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
430
466
  return;
431
467
  }
432
468
 
433
469
  console.log('yam doctor: issues');
434
470
  for (const issue of report.issues) console.log(`- ${issue}`);
471
+ if (report.preservedUnprovenSkillEntries.length) {
472
+ console.log('');
473
+ console.log('Preserved unproven skill entries:');
474
+ for (const entry of report.preservedUnprovenSkillEntries) console.log(`- ${entry}`);
475
+ }
435
476
  if (report.nextActions.length) {
436
477
  console.log('');
437
478
  console.log('Next actions:');
@@ -442,6 +483,7 @@ async function doctor(args = []) {
442
483
 
443
484
  async function buildDoctorReport() {
444
485
  const issues = [];
486
+ const preservedUnprovenSkillEntries = [];
445
487
  for (const skill of SKILLS) {
446
488
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
447
489
  }
@@ -450,15 +492,21 @@ async function buildDoctorReport() {
450
492
  if (await exists(path.join(os.homedir(), '.codex', 'automations', 'yam'))) issues.push('unexpected yam automation');
451
493
  if (await exists(path.join(os.homedir(), '.codex', 'automations', 'timeto'))) issues.push('unexpected legacy timeto automation');
452
494
  for (const legacySkill of LEGACY_SKILLS) {
453
- if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) issues.push(`unexpected legacy installed skill: ${legacySkill}`);
454
- if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, legacySkill, 'SKILL.md'))) {
455
- issues.push(`unexpected legacy mirror skill: ${legacySkill}`);
495
+ if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) {
496
+ preservedUnprovenSkillEntries.push(`legacy-named destination entry: ${legacySkill}`);
456
497
  }
457
498
  }
458
499
  for (const retiredSkill of RETIRED_SKILLS) {
459
- if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) issues.push(`unexpected retired installed skill: ${retiredSkill}`);
460
- if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, retiredSkill, 'SKILL.md'))) {
461
- issues.push(`unexpected retired mirror skill: ${retiredSkill}`);
500
+ if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) {
501
+ preservedUnprovenSkillEntries.push(`retired-name destination entry: ${retiredSkill}`);
502
+ }
503
+ }
504
+ if (CODEX_MIRROR !== DEST) {
505
+ const mirrorSkillNames = [...new Set([...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS])];
506
+ for (const skill of mirrorSkillNames) {
507
+ if (!await exists(path.join(CODEX_MIRROR, skill, 'SKILL.md'))) continue;
508
+ const nameKind = SKILLS.includes(skill) ? 'active-name' : LEGACY_SKILLS.includes(skill) ? 'legacy-named' : 'retired-name';
509
+ preservedUnprovenSkillEntries.push(`${nameKind} Codex mirror entry: ${skill}`);
462
510
  }
463
511
  }
464
512
  const verifyIssues = await verify({ quiet: true });
@@ -470,6 +518,7 @@ async function buildDoctorReport() {
470
518
  generatedAt: new Date().toISOString(),
471
519
  ok: issues.length === 0,
472
520
  issues,
521
+ preservedUnprovenSkillEntries,
473
522
  nextActions: nextActionDetails.map((action) => action.next_action),
474
523
  nextActionDetails,
475
524
  yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
@@ -487,7 +536,6 @@ function doctorNextActionDetails(issues = []) {
487
536
  else if (/missing truth matrix/i.test(issue)) actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
488
537
  else if (/unexpected local hooks/i.test(issue)) actions.push(nextActionDetail('remove-project-hook', 'warning', issue, 'remove project hooks unless this repo is intentionally dogfooding them', 'inspect .codex/hooks.json and remove only intentional stale hooks'));
489
538
  else if (/unexpected .*automation/i.test(issue)) actions.push(nextActionDetail('remove-stale-automation', 'warning', issue, 'remove stale yam/timeto automations before claiming clean install state', 'inspect ~/.codex/automations and remove stale entries manually'));
490
- else if (/legacy|retired/i.test(issue)) actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
491
539
  else if (/verify reported/i.test(issue)) actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
492
540
  }
493
541
  return uniqueNextActionDetails(actions);
@@ -765,19 +813,28 @@ async function studyNoteCheck(args = []) {
765
813
  }
766
814
 
767
815
  async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
768
- const changedFiles = await gitChangedFiles(dir);
769
- const needsStudyNote = changedFiles.length > 0;
816
+ const changedFileDetection = gitChangedFileSnapshot(dir);
817
+ const changedFiles = changedFileDetection.files;
818
+ const changeScopeAvailable = changedFileDetection.available;
819
+ const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
820
+ const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
770
821
  const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
771
822
  const checks = [
772
- studyNoteGuardCheck('changed_files', needsStudyNote ? 'pass' : 'skipped', needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected'),
773
- studyNoteGuardCheck('study_note_present', !needsStudyNote || hasStudyNoteMarker(reportText) ? 'pass' : 'fail', 'final report should include a Study Note when project artifacts changed'),
774
- studyNoteGuardCheck('role_or_responsibility', !needsStudyNote || /(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should explain what the touched code/artifact does'),
775
- studyNoteGuardCheck('execution_point', !needsStudyNote || /(execution|\bruns?\b|\bloads?\b|\brenders?\b|validates?|builds?|publishes?|read by|실행|로드|렌더|검사|검증|빌드|게시|배포|읽)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should explain where or when the touched code/artifact runs or is read'),
776
- studyNoteGuardCheck('before_after_or_change', !needsStudyNote || /(\bbefore\b|\bafter\b|before\/after|changed|change meaning|바뀌|변경|수정)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should explain what changed from before to after'),
777
- studyNoteGuardCheck('expected_behavior', !needsStudyNote || /(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should describe the expected behavior or result'),
778
- studyNoteGuardCheck('syntax_or_structure', !needsStudyNote || /(syntax|structure|schema|\bapi\b|function|array|field|type|condition|문법|구조|스키마|함수|배열|필드|타입|조건)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should include one useful syntax or structure insight'),
779
- studyNoteGuardCheck('verification', !needsStudyNote || /(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should say what was checked'),
780
- studyNoteGuardCheck('limits_or_uncertainty', !needsStudyNote || /(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should say what remains uncertain or explicitly state that no meaningful uncertainty remains')
823
+ studyNoteGuardCheck(
824
+ 'changed_files',
825
+ changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial',
826
+ changeScopeAvailable
827
+ ? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
828
+ : 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'
829
+ ),
830
+ studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
831
+ studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText)), 'Study Note should explain what the touched code/artifact does'),
832
+ 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'),
833
+ 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'),
834
+ studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText)), 'Study Note should describe the expected behavior or result'),
835
+ 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'),
836
+ studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText)), 'Study Note should say what was checked'),
837
+ 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')
781
838
  ];
782
839
  for (const requirement of hygieneRequirements) {
783
840
  checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(reportText, requirement), requirement.note));
@@ -787,13 +844,18 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
787
844
  schema: 'yam.study-note-guard.v1',
788
845
  generated_at: new Date().toISOString(),
789
846
  project: dir,
847
+ changed_file_detection: changedFileDetection,
790
848
  changed_files: changedFiles,
791
849
  changed_file_count: changedFiles.length,
792
850
  report_source: reportSource,
793
851
  checks,
794
852
  blockers,
795
- next_action: blockers[0] || (needsStudyNote ? 'Study Note guard passed for the supplied report text' : 'no Study Note required because no changed files were detected'),
796
- truth_status: blockers.length ? 'blocked' : needsStudyNote ? 'verified' : 'skipped'
853
+ next_action: blockers[0] || (!changeScopeAvailable
854
+ ? 'Git change scope is unavailable; inspect changed artifacts manually and include a Study Note when anything changed'
855
+ : needsStudyNote
856
+ ? 'Study Note guard passed for the supplied report text'
857
+ : 'no Study Note required because no changed files were detected'),
858
+ truth_status: blockers.length ? 'blocked' : !changeScopeAvailable ? 'partial' : needsStudyNote ? 'verified' : 'skipped'
797
859
  };
798
860
  return result;
799
861
  }
@@ -811,14 +873,35 @@ function firstPositional(args = []) {
811
873
  return '';
812
874
  }
813
875
 
814
- async function gitChangedFiles(dir) {
815
- const output = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
816
- return output
817
- .split(/\r?\n/)
818
- .filter(Boolean)
819
- .map((line) => line.replace(/^..\s+/, '').replace(/^.* -> /, ''))
820
- .map((line) => line.trim())
821
- .filter((file) => file && !file.startsWith('dist/') && !file.endsWith('.tgz'));
876
+ function gitChangedFileSnapshot(dir) {
877
+ const result = spawnSync('git', ['status', '--short', '-z', '--untracked-files=all'], {
878
+ cwd: dir,
879
+ encoding: 'utf8',
880
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
881
+ timeout: 30000
882
+ });
883
+ if (result.status !== 0) {
884
+ return {
885
+ available: false,
886
+ source: 'git status --short -z --untracked-files=all',
887
+ files: []
888
+ };
889
+ }
890
+ const records = String(result.stdout || '').split('\0');
891
+ const files = [];
892
+ for (let index = 0; index < records.length; index += 1) {
893
+ const record = records[index];
894
+ if (!record || record.length < 4) continue;
895
+ const status = record.slice(0, 2);
896
+ const file = record.slice(3);
897
+ if (/[RC]/.test(status)) index += 1;
898
+ if (file && !file.startsWith('dist/') && !file.endsWith('.tgz')) files.push(file);
899
+ }
900
+ return {
901
+ available: true,
902
+ source: 'git status --short -z --untracked-files=all',
903
+ files
904
+ };
822
905
  }
823
906
 
824
907
  async function readStudyNoteReportText(flags, dir) {
@@ -911,12 +994,22 @@ async function buildCleanupScanReport(targetDir = process.cwd()) {
911
994
  for (const artifact of await staleYamArtifacts(dir)) {
912
995
  findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
913
996
  }
997
+ const instructionDuplicationBudget = await buildInstructionDuplicationBudget(dir);
998
+ if (instructionDuplicationBudget.duplicate_group_count > 0) {
999
+ findings.push(cleanupFinding(
1000
+ 'low',
1001
+ 'AGENTS.md/SKILL.md directive duplication budget',
1002
+ `${instructionDuplicationBudget.duplicate_group_count} normalized directive group(s) repeat across separate instruction surfaces; exact normalized matches are advisory and may be intentional`,
1003
+ 'review the reported source files and consolidate only repetition that causes drift or conflicting maintenance'
1004
+ ));
1005
+ }
914
1006
  return {
915
1007
  schema: 'yam.cleanup-scan.v1',
916
1008
  generated_at: new Date().toISOString(),
917
1009
  project: dir,
918
1010
  destructive: false,
919
1011
  findings,
1012
+ instruction_duplication_budget: instructionDuplicationBudget,
920
1013
  risk_level: overallCleanupRisk(findings),
921
1014
  safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
922
1015
  truth_status: 'partial'
@@ -970,6 +1063,163 @@ async function staleYamArtifacts(dir) {
970
1063
  return artifacts;
971
1064
  }
972
1065
 
1066
+ async function buildInstructionDuplicationBudget(
1067
+ rootDir = process.cwd(),
1068
+ limits = INSTRUCTION_DUPLICATION_LIMITS
1069
+ ) {
1070
+ const root = path.resolve(rootDir || process.cwd());
1071
+ const inventory = await collectInstructionFiles(root, limits);
1072
+ const directives = new Map<string, AnyRecord[]>();
1073
+ let candidateDirectiveCount = 0;
1074
+ for (const relativeFile of inventory.files) {
1075
+ const absolute = path.join(root, relativeFile);
1076
+ const text = await readBoundedUtf8File(absolute, limits.maxBytesPerFile);
1077
+ for (const directive of instructionDirectives(text, relativeFile, limits.minDirectiveChars)) {
1078
+ candidateDirectiveCount += 1;
1079
+ directives.set(directive.normalized, [...(directives.get(directive.normalized) || []), directive]);
1080
+ }
1081
+ }
1082
+
1083
+ const allGroups = [...directives.entries()]
1084
+ .map(([normalized, occurrences]) => {
1085
+ const surfaces = [...new Set(occurrences.map((item) => item.file))];
1086
+ if (surfaces.length < 2) return null;
1087
+ return {
1088
+ directive_digest: createHash('sha256').update(normalized).digest('hex').slice(0, 16),
1089
+ directive_preview: redactSensitiveText(String(occurrences[0]?.text || '')).slice(0, 180),
1090
+ occurrence_count: occurrences.length,
1091
+ surface_count: surfaces.length,
1092
+ surfaces,
1093
+ occurrences: occurrences.slice(0, 8).map((item) => ({
1094
+ file: item.file,
1095
+ line: item.line
1096
+ }))
1097
+ };
1098
+ })
1099
+ .filter(Boolean)
1100
+ .sort((left, right) => (
1101
+ right.surface_count - left.surface_count
1102
+ || right.occurrence_count - left.occurrence_count
1103
+ || left.directive_digest.localeCompare(right.directive_digest)
1104
+ ));
1105
+ const duplicateGroups = allGroups.slice(0, limits.maxGroups);
1106
+ return {
1107
+ schema: 'yam.instruction-duplication-budget.v1',
1108
+ scope: 'project AGENTS.md and SKILL.md files only',
1109
+ advisory_only: true,
1110
+ hard_gate: false,
1111
+ destructive: false,
1112
+ normalization: 'exact normalized markdown directive text; no semantic similarity inference',
1113
+ limits: {
1114
+ max_files: limits.maxFiles,
1115
+ max_bytes_per_file: limits.maxBytesPerFile,
1116
+ max_groups: limits.maxGroups,
1117
+ max_depth: limits.maxDepth,
1118
+ min_directive_chars: limits.minDirectiveChars
1119
+ },
1120
+ files_scanned: inventory.files.length,
1121
+ scanned_files: inventory.files,
1122
+ candidate_directive_count: candidateDirectiveCount,
1123
+ duplicate_group_count: allGroups.length,
1124
+ duplicate_groups: duplicateGroups,
1125
+ file_limit_reached: inventory.fileLimitReached,
1126
+ group_limit_reached: allGroups.length > duplicateGroups.length,
1127
+ truth_status: 'partial',
1128
+ note: allGroups.length
1129
+ ? 'repetition may be intentional; review before consolidating'
1130
+ : 'no cross-surface exact normalized directive repetition found within the bounded scan'
1131
+ };
1132
+ }
1133
+
1134
+ async function collectInstructionFiles(root, limits) {
1135
+ const files: string[] = [];
1136
+ let fileLimitReached = false;
1137
+
1138
+ async function walk(current, depth) {
1139
+ if (depth > limits.maxDepth || fileLimitReached) return;
1140
+ let entries = [];
1141
+ try {
1142
+ entries = await fsp.readdir(current, { withFileTypes: true });
1143
+ } catch {
1144
+ return;
1145
+ }
1146
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1147
+ for (const entry of entries) {
1148
+ const absolute = path.join(current, entry.name);
1149
+ const relative = path.relative(root, absolute);
1150
+ if (shouldSkipScanPath(relative, entry.name) || entry.isSymbolicLink()) continue;
1151
+ if (entry.isDirectory()) {
1152
+ await walk(absolute, depth + 1);
1153
+ if (fileLimitReached) return;
1154
+ continue;
1155
+ }
1156
+ if (!entry.isFile() || !['AGENTS.md', 'SKILL.md'].includes(entry.name)) continue;
1157
+ if (files.length >= limits.maxFiles) {
1158
+ fileLimitReached = true;
1159
+ return;
1160
+ }
1161
+ files.push(relative);
1162
+ }
1163
+ }
1164
+
1165
+ await walk(root, 0);
1166
+ return { files, fileLimitReached };
1167
+ }
1168
+
1169
+ async function readBoundedUtf8File(file, maxBytes) {
1170
+ const handle = await fsp.open(file, 'r');
1171
+ try {
1172
+ const buffer = Buffer.alloc(maxBytes);
1173
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1174
+ return buffer.subarray(0, bytesRead).toString('utf8');
1175
+ } finally {
1176
+ await handle.close();
1177
+ }
1178
+ }
1179
+
1180
+ function instructionDirectives(text = '', relativeFile = '', minChars = 40) {
1181
+ const directives = [];
1182
+ const lines = String(text || '').split(/\r?\n/);
1183
+ let inFence = false;
1184
+ let inFrontmatter = lines[0]?.trim() === '---';
1185
+ for (let index = 0; index < lines.length; index += 1) {
1186
+ const raw = lines[index];
1187
+ const trimmed = raw.trim();
1188
+ if (index === 0 && inFrontmatter) continue;
1189
+ if (inFrontmatter) {
1190
+ if (trimmed === '---') inFrontmatter = false;
1191
+ continue;
1192
+ }
1193
+ if (/^(```|~~~)/.test(trimmed)) {
1194
+ inFence = !inFence;
1195
+ continue;
1196
+ }
1197
+ if (inFence || !trimmed || /^#{1,6}\s/.test(trimmed)) continue;
1198
+ const match = trimmed.match(/^(?:[-*+]|\d+[.)])\s+(.+)$/);
1199
+ if (!match) continue;
1200
+ const normalized = normalizeInstructionDirective(match[1]);
1201
+ if (normalized.length < minChars) continue;
1202
+ directives.push({
1203
+ file: relativeFile,
1204
+ line: index + 1,
1205
+ text: match[1].trim().slice(0, 240),
1206
+ normalized
1207
+ });
1208
+ }
1209
+ return directives;
1210
+ }
1211
+
1212
+ function normalizeInstructionDirective(value = '') {
1213
+ return String(value || '')
1214
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
1215
+ .replace(/[`*_~]/g, '')
1216
+ .replace(/<[^>]+>/g, ' ')
1217
+ .replace(/\s+/g, ' ')
1218
+ .replace(/[.;:,!?。,、;:!?]+$/g, '')
1219
+ .trim()
1220
+ .toLowerCase();
1221
+ }
1222
+
973
1223
  function printCleanupScanReport(report) {
974
1224
  console.log('yam cleanup scan');
975
1225
  console.log(`Project: ${report.project}`);
@@ -1829,10 +2079,16 @@ async function buildYamLiteContext({ cwd, prompt }) {
1829
2079
 
1830
2080
  async function buildStudyNoteHookContext({ cwd }) {
1831
2081
  const dir = path.resolve(cwd || process.cwd());
1832
- const changedFiles = await gitChangedFiles(dir);
2082
+ const changedFileDetection = gitChangedFileSnapshot(dir);
2083
+ const changedFiles = changedFileDetection.files;
1833
2084
  const lines = [
1834
2085
  '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.'
1835
2086
  ];
2087
+ if (!changedFileDetection.available) {
2088
+ lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
2089
+ lines.push('Determine manually whether artifacts changed and include the Study Note when they did; the Stop hook will keep this uncertainty visible.');
2090
+ return lines.join('\n');
2091
+ }
1836
2092
  if (!changedFiles.length) {
1837
2093
  lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
1838
2094
  lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
@@ -1852,6 +2108,12 @@ async function buildStudyNoteHookContext({ cwd }) {
1852
2108
  async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd()) {
1853
2109
  const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
1854
2110
  const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message');
2111
+ if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
2112
+ return {
2113
+ continue: true,
2114
+ systemMessage: 'yam Study Note guard could not read Git changed-file scope; do not treat this project as clean. Confirm manually whether artifacts changed and include a complete Study Note when they did.'
2115
+ };
2116
+ }
1855
2117
  if (!result.blockers.length) return { continue: true };
1856
2118
  const summary = result.blockers.slice(0, 4).join('; ');
1857
2119
  if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
@@ -4324,7 +4586,10 @@ function pickScript(scripts = {}, groups = []) {
4324
4586
  async function detectProject(targetDir = process.cwd(), { quiet = false } = {}) {
4325
4587
  const dir = path.resolve(targetDir || process.cwd());
4326
4588
  const packageJson = path.join(dir, 'package.json');
4589
+ const changedFileDetection = gitChangedFileSnapshot(dir);
4590
+ const changedFiles = changedFileDetection.files;
4327
4591
  const result = {
4592
+ schema: 'yam.project-detection.v1',
4328
4593
  dir,
4329
4594
  packageJson: await exists(packageJson),
4330
4595
  packageManager: 'npm',
@@ -4335,12 +4600,26 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
4335
4600
  test: null,
4336
4601
  build: null
4337
4602
  },
4338
- frameworkChecklist: null
4603
+ frameworkChecklist: null,
4604
+ changedFileDetection,
4605
+ changedFiles,
4606
+ verificationRecommendations: [],
4607
+ verificationCommandPlan: []
4339
4608
  };
4340
4609
  if (!result.packageJson) {
4610
+ result.verificationRecommendations = buildChangedFileVerificationRecommendations({
4611
+ packageManager: result.packageManager,
4612
+ scripts: {},
4613
+ commands: result.commands,
4614
+ changedFiles,
4615
+ changedFilesAvailable: changedFileDetection.available,
4616
+ packageJson: false
4617
+ });
4618
+ result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
4341
4619
  if (!quiet) {
4342
4620
  console.log(`No package.json found in ${dir}`);
4343
4621
  console.log('Suggested verification: use project pack or local framework conventions.');
4622
+ printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
4344
4623
  }
4345
4624
  return result;
4346
4625
  }
@@ -4354,11 +4633,255 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
4354
4633
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
4355
4634
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
4356
4635
  result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
4636
+ result.verificationRecommendations = buildChangedFileVerificationRecommendations({
4637
+ packageManager: result.packageManager,
4638
+ scripts,
4639
+ commands: result.commands,
4640
+ changedFiles,
4641
+ changedFilesAvailable: changedFileDetection.available,
4642
+ packageJson: true
4643
+ });
4644
+ result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
4357
4645
 
4358
4646
  if (!quiet) printDetection(result);
4359
4647
  return result;
4360
4648
  }
4361
4649
 
4650
+ async function detectCommand(args = []) {
4651
+ const flags = parseSimpleFlags(args, new Set(['json']));
4652
+ const dir = path.resolve(firstPositional(args) || process.cwd());
4653
+ const result = await detectProject(dir, { quiet: true });
4654
+ if (flags.json) {
4655
+ console.log(JSON.stringify(result, null, 2));
4656
+ return;
4657
+ }
4658
+ printDetection(result);
4659
+ }
4660
+
4661
+ function buildChangedFileVerificationRecommendations({
4662
+ packageManager = 'npm',
4663
+ scripts = {},
4664
+ commands = {},
4665
+ changedFiles = [],
4666
+ changedFilesAvailable = true,
4667
+ packageJson = false
4668
+ }: AnyRecord) {
4669
+ const safeFallbackCommands = uniqueStrings([
4670
+ commands.typecheck,
4671
+ commands.lint,
4672
+ commands.test,
4673
+ commands.build
4674
+ ]);
4675
+ if (!changedFilesAvailable) {
4676
+ return [{
4677
+ matched_rule: 'git-status-unavailable',
4678
+ confidence: 'low',
4679
+ reason: 'Git change status was unavailable, so yam cannot prove that the project has no changed files',
4680
+ fallback: packageJson
4681
+ ? 'run all available non-publish typecheck, lint, test, and build checks or use project-pack conventions'
4682
+ : 'use yam.project.md or local framework conventions; no Git change scope or executable package scripts were detected',
4683
+ files: [],
4684
+ suggested_commands: safeFallbackCommands
4685
+ }];
4686
+ }
4687
+ if (!changedFiles.length) {
4688
+ return [{
4689
+ matched_rule: 'no-changes',
4690
+ confidence: 'high',
4691
+ reason: 'git reports no dirty, staged, or untracked project files',
4692
+ fallback: 'rerun `yam detect --json` after files change; use project-pack conventions if Git is unavailable',
4693
+ files: [],
4694
+ suggested_commands: []
4695
+ }];
4696
+ }
4697
+
4698
+ const rules = [
4699
+ {
4700
+ id: 'package-release-metadata',
4701
+ confidence: 'high',
4702
+ matches: (file) => /^(?:package(?:-lock)?\.json|npm-shrinkwrap\.json|yam\.manifest\.json|CHANGELOG\.md|scripts\/check-(?:package-boundary|registry-version|dist-freshness)\.mjs)$/i.test(file),
4703
+ reason: 'package or release metadata changed, so package contents and release readiness can drift',
4704
+ commandNames: ['package-boundary:check', 'release:check'],
4705
+ fallback: 'run the available typecheck/build/package checks and perform the public release workflow separately'
4706
+ },
4707
+ {
4708
+ id: 'src-lib',
4709
+ confidence: 'high',
4710
+ matches: (file) => /^src\/lib\//i.test(file),
4711
+ reason: 'shared library code changed, so compile safety and a focused behavior smoke are the narrowest honest checks',
4712
+ commandNames: ['typecheck'],
4713
+ fallback: 'run typecheck plus the closest available test/smoke; use the full safe fallback when the library crosses package boundaries'
4714
+ },
4715
+ {
4716
+ id: 'scripts',
4717
+ confidence: 'medium',
4718
+ matches: (file) => /^scripts\//i.test(file),
4719
+ reason: 'an executable verification or maintenance script changed, so its direct npm wrapper or closest smoke should run',
4720
+ commandNames: [],
4721
+ fallback: 'run the changed script in an isolated fixture, then use the full safe fallback if no direct wrapper exists'
4722
+ },
4723
+ {
4724
+ id: 'unknown-safe-fallback',
4725
+ confidence: packageJson ? 'medium' : 'low',
4726
+ matches: () => true,
4727
+ reason: 'no focused changed-file rule matched, so skipping verification would be weaker than the available project-wide safe checks',
4728
+ commandNames: [],
4729
+ fallback: packageJson
4730
+ ? 'run all available non-publish typecheck, lint, test, and build checks'
4731
+ : 'use yam.project.md or local framework conventions; no executable package scripts were detected'
4732
+ }
4733
+ ];
4734
+
4735
+ const grouped = new Map<string, string[]>();
4736
+ for (const rawFile of changedFiles) {
4737
+ const file = String(rawFile || '').replaceAll('\\', '/');
4738
+ const rule = rules.find((candidate) => candidate.matches(file)) || rules[rules.length - 1];
4739
+ grouped.set(rule.id, [...(grouped.get(rule.id) || []), file]);
4740
+ }
4741
+
4742
+ const recommendations = [];
4743
+ for (const rule of rules) {
4744
+ const files = grouped.get(rule.id) || [];
4745
+ if (!files.length) continue;
4746
+ let commandEvidence: AnyRecord[] = rule.commandNames.map((name) => ({
4747
+ name,
4748
+ files,
4749
+ basis: 'rule-wide'
4750
+ }));
4751
+ if (rule.id === 'src-lib') {
4752
+ commandEvidence = [
4753
+ { name: 'typecheck', files, basis: 'rule-wide' },
4754
+ ...focusedSmokeScriptEvidenceForFiles(scripts, files)
4755
+ ];
4756
+ } else if (rule.id === 'scripts') {
4757
+ const directEvidence = files.flatMap((file) => (
4758
+ directScriptNamesForFile(scripts, file).map((name) => ({
4759
+ name,
4760
+ files: [file],
4761
+ basis: 'direct-script-wrapper'
4762
+ }))
4763
+ ));
4764
+ commandEvidence = directEvidence.length
4765
+ ? directEvidence
4766
+ : focusedSmokeScriptNames(scripts).slice(0, 1).map((name) => ({
4767
+ name,
4768
+ files,
4769
+ basis: 'group-fallback'
4770
+ }));
4771
+ } else if (rule.id === 'unknown-safe-fallback') {
4772
+ commandEvidence = safeFallbackCommands.map((command) => ({
4773
+ command,
4774
+ files,
4775
+ basis: 'safe-fallback'
4776
+ }));
4777
+ }
4778
+ const commandSources = commandEvidence
4779
+ .map((evidence) => ({
4780
+ command: evidence.command || scriptCommandIfPresent(packageManager, scripts, evidence.name),
4781
+ files: [...new Set(evidence.files || [])].sort(),
4782
+ basis: evidence.basis
4783
+ }))
4784
+ .filter((evidence) => evidence.command);
4785
+ const suggestedCommands = uniqueStrings(commandSources.map((evidence) => evidence.command));
4786
+ recommendations.push({
4787
+ matched_rule: rule.id,
4788
+ confidence: rule.confidence,
4789
+ reason: rule.reason,
4790
+ fallback: rule.fallback,
4791
+ files: [...new Set(files)].sort(),
4792
+ suggested_commands: suggestedCommands,
4793
+ command_sources: commandSources
4794
+ });
4795
+ }
4796
+ return recommendations;
4797
+ }
4798
+
4799
+ function focusedSmokeScriptNames(scripts = {}) {
4800
+ return Object.keys(scripts)
4801
+ .filter((name) => /(?:^|[-:])(?:smoke|test)(?:$|[-:])/i.test(name) || /smoke/i.test(String(scripts[name] || '')))
4802
+ .sort();
4803
+ }
4804
+
4805
+ function focusedSmokeScriptEvidenceForFiles(scripts = {}, files = []) {
4806
+ const candidates = focusedSmokeScriptNames(scripts);
4807
+ const selected = [];
4808
+ for (const file of files) {
4809
+ const stem = path.posix.basename(String(file || '').replaceAll('\\', '/')).replace(/\.[^.]+$/, '');
4810
+ const direct = candidates.find((name) => {
4811
+ const command = String(scripts[name] || '');
4812
+ return name.includes(stem)
4813
+ || command.includes(stem)
4814
+ || (stem === 'skill-installation' && /install.*smoke|smoke.*install/i.test(`${name} ${command}`))
4815
+ || (['trust-kernel', 'ueye-artifacts'].includes(stem) && /cli.*smoke|smoke.*cli/i.test(`${name} ${command}`));
4816
+ });
4817
+ if (direct) selected.push({
4818
+ name: direct,
4819
+ files: [file],
4820
+ basis: 'focused-file-match'
4821
+ });
4822
+ }
4823
+ if (!selected.length && candidates.length) {
4824
+ selected.push({
4825
+ name: candidates[0],
4826
+ files,
4827
+ basis: 'group-fallback'
4828
+ });
4829
+ }
4830
+ return selected;
4831
+ }
4832
+
4833
+ function directScriptNamesForFile(scripts = {}, changedFile = '') {
4834
+ const normalized = String(changedFile || '').replaceAll('\\', '/');
4835
+ const basename = path.posix.basename(normalized);
4836
+ return Object.entries(scripts)
4837
+ .filter(([, command]) => {
4838
+ const text = String(command || '').replaceAll('\\', '/');
4839
+ return text.includes(normalized) || text.includes(`/${basename}`) || text.includes(` ${basename}`);
4840
+ })
4841
+ .map(([name]) => name)
4842
+ .sort();
4843
+ }
4844
+
4845
+ function scriptCommandIfPresent(packageManager, scripts = {}, name = '') {
4846
+ return name && Object.prototype.hasOwnProperty.call(scripts, name)
4847
+ ? runCommand(packageManager, name)
4848
+ : null;
4849
+ }
4850
+
4851
+ function uniqueStrings(values = []) {
4852
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
4853
+ }
4854
+
4855
+ function buildVerificationCommandPlan(recommendations = []) {
4856
+ const commands = new Map<string, AnyRecord>();
4857
+ for (const recommendation of recommendations) {
4858
+ const sources = recommendation.command_sources?.length
4859
+ ? recommendation.command_sources
4860
+ : (recommendation.suggested_commands || []).map((command) => ({
4861
+ command,
4862
+ files: recommendation.files || [],
4863
+ basis: 'recommendation-fallback'
4864
+ }));
4865
+ for (const source of sources) {
4866
+ const current = commands.get(source.command) || {
4867
+ command: source.command,
4868
+ matched_rules: [],
4869
+ files: [],
4870
+ provenance: []
4871
+ };
4872
+ current.matched_rules = uniqueStrings([...current.matched_rules, recommendation.matched_rule]);
4873
+ current.files = uniqueStrings([...current.files, ...(source.files || [])]).sort();
4874
+ current.provenance.push({
4875
+ matched_rule: recommendation.matched_rule,
4876
+ files: [...new Set(source.files || [])].sort(),
4877
+ basis: source.basis
4878
+ });
4879
+ commands.set(source.command, current);
4880
+ }
4881
+ }
4882
+ return [...commands.values()];
4883
+ }
4884
+
4362
4885
  function printDetection(result) {
4363
4886
  console.log(`Project: ${result.dir}`);
4364
4887
  console.log(`Package manager: ${result.packageManager}`);
@@ -4372,6 +4895,7 @@ function printDetection(result) {
4372
4895
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
4373
4896
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
4374
4897
  console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
4898
+ printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
4375
4899
  if (result.frameworkChecklist?.detected) {
4376
4900
  console.log('');
4377
4901
  console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
@@ -4381,6 +4905,29 @@ function printDetection(result) {
4381
4905
  }
4382
4906
  }
4383
4907
 
4908
+ function printChangedFileVerificationRecommendations(recommendations = [], commandPlan = []) {
4909
+ console.log('');
4910
+ console.log('Changed-file verification recommendations (advisory; commands were not run):');
4911
+ for (const recommendation of recommendations) {
4912
+ console.log(`- ${recommendation.matched_rule} [confidence: ${recommendation.confidence}]`);
4913
+ console.log(` files: ${recommendation.files.length ? recommendation.files.join(', ') : '(none)'}`);
4914
+ console.log(` reason: ${recommendation.reason}`);
4915
+ console.log(` commands: ${recommendation.suggested_commands.length ? recommendation.suggested_commands.join(' && ') : '(none detected)'}`);
4916
+ console.log(` fallback: ${recommendation.fallback}`);
4917
+ }
4918
+ console.log('');
4919
+ console.log('Verification command plan (deduplicated; advisory; commands were not run):');
4920
+ if (!commandPlan.length) {
4921
+ console.log('- (none detected)');
4922
+ return;
4923
+ }
4924
+ for (const item of commandPlan) {
4925
+ console.log(`- ${item.command}`);
4926
+ console.log(` rules: ${item.matched_rules.join(', ')}`);
4927
+ console.log(` files: ${item.files.join(', ') || '(none)'}`);
4928
+ }
4929
+ }
4930
+
4384
4931
  function budget(routeArg = '') {
4385
4932
  const normalized = normalizeRoute(routeArg);
4386
4933
  const entries = normalized ? [[normalized, ROUTE_BUDGETS[normalized]]] : Object.entries(ROUTE_BUDGETS);
@@ -4768,16 +5315,39 @@ function findSensitivePattern(text = '') {
4768
5315
  function showPath() {
4769
5316
  console.log(`root: ${ROOT}`);
4770
5317
  console.log(`skills: ${DEST}`);
4771
- console.log(`codex mirror cleanup: ${CODEX_MIRROR}`);
5318
+ console.log(`codex mirror (preserved without an ownership receipt): ${CODEX_MIRROR}`);
5319
+ }
5320
+
5321
+ function showRequestedHelp(args: string[]) {
5322
+ if (!args.some((arg) => arg === '--help' || arg === '-h')) return false;
5323
+
5324
+ const commandUsage: Record<string, () => void> = {
5325
+ context: contextUsage,
5326
+ cleanup: cleanupUsage,
5327
+ 'study-note': studyNoteUsage,
5328
+ tools: toolsUsage,
5329
+ hook: hookUsage,
5330
+ loop: loopUsage,
5331
+ ueye: ueyeUsage,
5332
+ media: mediaUsage,
5333
+ runtime: runtimeUsage,
5334
+ mission: missionUsage,
5335
+ benchmark: benchmarkUsage,
5336
+ memory: memoryUsage
5337
+ };
5338
+ (commandUsage[args[0]] || usage)();
5339
+ return true;
4772
5340
  }
4773
5341
 
4774
5342
  async function main() {
4775
- const command = process.argv[2] || 'help';
4776
- if (command === 'help' || command === '--help' || command === '-h') return usage();
4777
- if (command === 'install') return install();
4778
- if (command === 'uninstall') return uninstall();
5343
+ const args = process.argv.slice(2);
5344
+ const command = args[0] || 'help';
5345
+ if (showRequestedHelp(args)) return;
5346
+ if (command === 'help') return usage();
5347
+ if (command === 'install') return install(args.slice(1));
5348
+ if (command === 'uninstall') return uninstall(args.slice(1));
4779
5349
  if (command === 'version') return console.log(VERSION);
4780
- if (command === 'detect') return detectProject(process.argv[3]);
5350
+ if (command === 'detect') return detectCommand(args.slice(1));
4781
5351
  if (command === 'pack') return inspectProjectPack(process.argv[3]);
4782
5352
  if (command === 'context') return context(process.argv.slice(3));
4783
5353
  if (command === 'cleanup') return cleanup(process.argv.slice(3));