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/dist/bin/yam.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { 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';
@@ -9,7 +10,7 @@ import process from 'node:process';
9
10
  import { fileURLToPath } from 'node:url';
10
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';
11
12
  import { archiveUeyeRevision, inspectImageFile, upsertUeyeAsset, verifyUeyeAssetManifest, verifyUeyeRevisionHistory } from '../lib/ueye-artifacts.js';
12
- import { INSTALL_LOCK_NAME, INSTALL_RECEIPT_NAME, inspectSkillInstallation, installSkillSetTransactional } from '../lib/skill-installation.js';
13
+ import { inspectSkillInstallation, installSkillSetTransactional, uninstallSkillSetSafely } from '../lib/skill-installation.js';
13
14
  function errorMessage(error) {
14
15
  return error instanceof Error ? error.message : String(error);
15
16
  }
@@ -50,6 +51,13 @@ const VERSION = String(PACKAGE_JSON.version || '0.0.0');
50
51
  const PROJECT_PACK = 'yam.project.md';
51
52
  const LEGACY_PROJECT_PACK = 'timeto.project.md';
52
53
  const PACK_STALE_DAYS = 30;
54
+ const INSTRUCTION_DUPLICATION_LIMITS = {
55
+ maxFiles: 24,
56
+ maxBytesPerFile: 128000,
57
+ maxGroups: 8,
58
+ maxDepth: 6,
59
+ minDirectiveChars: 40
60
+ };
53
61
  const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
54
62
  const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
55
63
  const YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
@@ -116,7 +124,7 @@ Usage:
116
124
  yam list
117
125
  yam status
118
126
  yam verify
119
- yam detect [dir]
127
+ yam detect [dir] [--json]
120
128
  yam pack [dir]
121
129
  yam context pressure [dir] [--json]
122
130
  yam cleanup scan [dir] [--json]
@@ -145,7 +153,7 @@ Usage:
145
153
  yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
146
154
  yam template <project|ueye|mission|proof|tuning>
147
155
  yam tune-log [dir]
148
- yam install
156
+ yam install [--replace-user-skill name]
149
157
  yam uninstall
150
158
  yam doctor [--json]
151
159
  yam examples
@@ -169,7 +177,32 @@ async function exists(file) {
169
177
  async function rmrf(target) {
170
178
  await fsp.rm(target, { recursive: true, force: true });
171
179
  }
172
- async function install() {
180
+ function installReplacementSkills(args = []) {
181
+ const skills = [];
182
+ for (let index = 0; index < args.length; index += 1) {
183
+ const arg = String(args[index] || '');
184
+ if (arg === '--replace-user-skill') {
185
+ const skill = String(args[index + 1] || '');
186
+ if (!skill || skill.startsWith('--')) {
187
+ throw new Error('missing skill name after --replace-user-skill');
188
+ }
189
+ skills.push(skill);
190
+ index += 1;
191
+ continue;
192
+ }
193
+ if (arg.startsWith('--replace-user-skill=')) {
194
+ const skill = arg.slice('--replace-user-skill='.length);
195
+ if (!skill)
196
+ throw new Error('missing skill name after --replace-user-skill=');
197
+ skills.push(skill);
198
+ continue;
199
+ }
200
+ throw new Error(`unknown yam install argument: ${arg}`);
201
+ }
202
+ return [...new Set(skills)];
203
+ }
204
+ async function install(args = []) {
205
+ const replaceSkills = installReplacementSkills(args);
173
206
  const result = await installSkillSetTransactional({
174
207
  sourceRoot: ROOT,
175
208
  destination: DEST,
@@ -177,6 +210,7 @@ async function install() {
177
210
  packageName: String(PACKAGE_JSON.name || 'yam-flow'),
178
211
  version: VERSION,
179
212
  skills: SKILLS,
213
+ replaceSkills,
180
214
  legacySkills: LEGACY_SKILLS,
181
215
  retiredSkills: RETIRED_SKILLS
182
216
  });
@@ -187,21 +221,22 @@ async function install() {
187
221
  console.warn(`warning: ${warning}`);
188
222
  console.log('Restart Codex to reload skills.');
189
223
  }
190
- async function uninstall() {
191
- const lockPath = path.join(DEST, INSTALL_LOCK_NAME);
192
- if (await exists(lockPath)) {
193
- throw new Error(`cannot uninstall while an install lock exists: ${lockPath}`);
194
- }
195
- for (const skill of [...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS]) {
196
- await rmrf(path.join(DEST, skill));
197
- if (CODEX_MIRROR !== DEST) {
198
- await rmrf(path.join(CODEX_MIRROR, skill));
199
- }
200
- }
201
- await rmrf(path.join(DEST, INSTALL_RECEIPT_NAME));
224
+ async function uninstall(args = []) {
225
+ if (args.length)
226
+ throw new Error(`unknown yam uninstall argument: ${args[0]}`);
227
+ const result = await uninstallSkillSetSafely({
228
+ destination: DEST,
229
+ codexMirror: CODEX_MIRROR,
230
+ packageName: String(PACKAGE_JSON.name || 'yam-flow'),
231
+ skills: SKILLS
232
+ });
202
233
  console.log(`yam removed from ${DEST}`);
203
- if (CODEX_MIRROR !== DEST)
204
- console.log(`yam mirror entries removed from ${CODEX_MIRROR}`);
234
+ console.log(`removed receipt: ${result.receiptPath}`);
235
+ for (const warning of result.cleanupWarnings)
236
+ console.warn(`warning: ${warning}`);
237
+ if (CODEX_MIRROR !== DEST) {
238
+ console.log(`unowned legacy, retired, and Codex mirror entries were preserved`);
239
+ }
205
240
  console.log('Restart Codex to unload skills.');
206
241
  }
207
242
  async function status({ quiet = false } = {}) {
@@ -397,12 +432,21 @@ async function doctor(args = []) {
397
432
  console.log('yam doctor: ok');
398
433
  console.log('No hooks, automations, or global config are required.');
399
434
  console.log('Skill install state is reported by `yam status`.');
435
+ if (report.preservedUnprovenSkillEntries.length) {
436
+ console.log(`Preserved unproven legacy/retired/mirror entries: ${report.preservedUnprovenSkillEntries.length}`);
437
+ }
400
438
  console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
401
439
  return;
402
440
  }
403
441
  console.log('yam doctor: issues');
404
442
  for (const issue of report.issues)
405
443
  console.log(`- ${issue}`);
444
+ if (report.preservedUnprovenSkillEntries.length) {
445
+ console.log('');
446
+ console.log('Preserved unproven skill entries:');
447
+ for (const entry of report.preservedUnprovenSkillEntries)
448
+ console.log(`- ${entry}`);
449
+ }
406
450
  if (report.nextActions.length) {
407
451
  console.log('');
408
452
  console.log('Next actions:');
@@ -413,6 +457,7 @@ async function doctor(args = []) {
413
457
  }
414
458
  async function buildDoctorReport() {
415
459
  const issues = [];
460
+ const preservedUnprovenSkillEntries = [];
416
461
  for (const skill of SKILLS) {
417
462
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md')))
418
463
  issues.push(`missing source skill: ${skill}`);
@@ -426,17 +471,22 @@ async function buildDoctorReport() {
426
471
  if (await exists(path.join(os.homedir(), '.codex', 'automations', 'timeto')))
427
472
  issues.push('unexpected legacy timeto automation');
428
473
  for (const legacySkill of LEGACY_SKILLS) {
429
- if (await exists(path.join(DEST, legacySkill, 'SKILL.md')))
430
- issues.push(`unexpected legacy installed skill: ${legacySkill}`);
431
- if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, legacySkill, 'SKILL.md'))) {
432
- issues.push(`unexpected legacy mirror skill: ${legacySkill}`);
474
+ if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) {
475
+ preservedUnprovenSkillEntries.push(`legacy-named destination entry: ${legacySkill}`);
433
476
  }
434
477
  }
435
478
  for (const retiredSkill of RETIRED_SKILLS) {
436
- if (await exists(path.join(DEST, retiredSkill, 'SKILL.md')))
437
- issues.push(`unexpected retired installed skill: ${retiredSkill}`);
438
- if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, retiredSkill, 'SKILL.md'))) {
439
- issues.push(`unexpected retired mirror skill: ${retiredSkill}`);
479
+ if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) {
480
+ preservedUnprovenSkillEntries.push(`retired-name destination entry: ${retiredSkill}`);
481
+ }
482
+ }
483
+ if (CODEX_MIRROR !== DEST) {
484
+ const mirrorSkillNames = [...new Set([...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS])];
485
+ for (const skill of mirrorSkillNames) {
486
+ if (!await exists(path.join(CODEX_MIRROR, skill, 'SKILL.md')))
487
+ continue;
488
+ const nameKind = SKILLS.includes(skill) ? 'active-name' : LEGACY_SKILLS.includes(skill) ? 'legacy-named' : 'retired-name';
489
+ preservedUnprovenSkillEntries.push(`${nameKind} Codex mirror entry: ${skill}`);
440
490
  }
441
491
  }
442
492
  const verifyIssues = await verify({ quiet: true });
@@ -449,6 +499,7 @@ async function buildDoctorReport() {
449
499
  generatedAt: new Date().toISOString(),
450
500
  ok: issues.length === 0,
451
501
  issues,
502
+ preservedUnprovenSkillEntries,
452
503
  nextActions: nextActionDetails.map((action) => action.next_action),
453
504
  nextActionDetails,
454
505
  yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
@@ -468,8 +519,6 @@ function doctorNextActionDetails(issues = []) {
468
519
  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'));
469
520
  else if (/unexpected .*automation/i.test(issue))
470
521
  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'));
471
- else if (/legacy|retired/i.test(issue))
472
- actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
473
522
  else if (/verify reported/i.test(issue))
474
523
  actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
475
524
  }
@@ -759,19 +808,24 @@ async function studyNoteCheck(args = []) {
759
808
  process.exitCode = 1;
760
809
  }
761
810
  async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
762
- const changedFiles = await gitChangedFiles(dir);
763
- const needsStudyNote = changedFiles.length > 0;
811
+ const changedFileDetection = gitChangedFileSnapshot(dir);
812
+ const changedFiles = changedFileDetection.files;
813
+ const changeScopeAvailable = changedFileDetection.available;
814
+ const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
815
+ const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
764
816
  const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
765
817
  const checks = [
766
- studyNoteGuardCheck('changed_files', needsStudyNote ? 'pass' : 'skipped', needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected'),
767
- studyNoteGuardCheck('study_note_present', !needsStudyNote || hasStudyNoteMarker(reportText) ? 'pass' : 'fail', 'final report should include a Study Note when project artifacts changed'),
768
- 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'),
769
- 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'),
770
- 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'),
771
- studyNoteGuardCheck('expected_behavior', !needsStudyNote || /(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should describe the expected behavior or result'),
772
- 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'),
773
- studyNoteGuardCheck('verification', !needsStudyNote || /(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText) ? 'pass' : 'fail', 'Study Note should say what was checked'),
774
- 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')
818
+ studyNoteGuardCheck('changed_files', changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial', changeScopeAvailable
819
+ ? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
820
+ : 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'),
821
+ studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
822
+ studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText)), 'Study Note should explain what the touched code/artifact does'),
823
+ 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'),
824
+ 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'),
825
+ studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText)), 'Study Note should describe the expected behavior or result'),
826
+ 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'),
827
+ studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText)), 'Study Note should say what was checked'),
828
+ 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')
775
829
  ];
776
830
  for (const requirement of hygieneRequirements) {
777
831
  checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(reportText, requirement), requirement.note));
@@ -781,13 +835,18 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
781
835
  schema: 'yam.study-note-guard.v1',
782
836
  generated_at: new Date().toISOString(),
783
837
  project: dir,
838
+ changed_file_detection: changedFileDetection,
784
839
  changed_files: changedFiles,
785
840
  changed_file_count: changedFiles.length,
786
841
  report_source: reportSource,
787
842
  checks,
788
843
  blockers,
789
- next_action: blockers[0] || (needsStudyNote ? 'Study Note guard passed for the supplied report text' : 'no Study Note required because no changed files were detected'),
790
- truth_status: blockers.length ? 'blocked' : needsStudyNote ? 'verified' : 'skipped'
844
+ next_action: blockers[0] || (!changeScopeAvailable
845
+ ? 'Git change scope is unavailable; inspect changed artifacts manually and include a Study Note when anything changed'
846
+ : needsStudyNote
847
+ ? 'Study Note guard passed for the supplied report text'
848
+ : 'no Study Note required because no changed files were detected'),
849
+ truth_status: blockers.length ? 'blocked' : !changeScopeAvailable ? 'partial' : needsStudyNote ? 'verified' : 'skipped'
791
850
  };
792
851
  return result;
793
852
  }
@@ -805,14 +864,38 @@ function firstPositional(args = []) {
805
864
  }
806
865
  return '';
807
866
  }
808
- async function gitChangedFiles(dir) {
809
- const output = runReadOnlyCommandIn(dir, 'git', ['status', '--short']);
810
- return output
811
- .split(/\r?\n/)
812
- .filter(Boolean)
813
- .map((line) => line.replace(/^..\s+/, '').replace(/^.* -> /, ''))
814
- .map((line) => line.trim())
815
- .filter((file) => file && !file.startsWith('dist/') && !file.endsWith('.tgz'));
867
+ function gitChangedFileSnapshot(dir) {
868
+ const result = spawnSync('git', ['status', '--short', '-z', '--untracked-files=all'], {
869
+ cwd: dir,
870
+ encoding: 'utf8',
871
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
872
+ timeout: 30000
873
+ });
874
+ if (result.status !== 0) {
875
+ return {
876
+ available: false,
877
+ source: 'git status --short -z --untracked-files=all',
878
+ files: []
879
+ };
880
+ }
881
+ const records = String(result.stdout || '').split('\0');
882
+ const files = [];
883
+ for (let index = 0; index < records.length; index += 1) {
884
+ const record = records[index];
885
+ if (!record || record.length < 4)
886
+ continue;
887
+ const status = record.slice(0, 2);
888
+ const file = record.slice(3);
889
+ if (/[RC]/.test(status))
890
+ index += 1;
891
+ if (file && !file.startsWith('dist/') && !file.endsWith('.tgz'))
892
+ files.push(file);
893
+ }
894
+ return {
895
+ available: true,
896
+ source: 'git status --short -z --untracked-files=all',
897
+ files
898
+ };
816
899
  }
817
900
  async function readStudyNoteReportText(flags, dir) {
818
901
  if (flags.text)
@@ -902,12 +985,17 @@ async function buildCleanupScanReport(targetDir = process.cwd()) {
902
985
  for (const artifact of await staleYamArtifacts(dir)) {
903
986
  findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
904
987
  }
988
+ const instructionDuplicationBudget = await buildInstructionDuplicationBudget(dir);
989
+ if (instructionDuplicationBudget.duplicate_group_count > 0) {
990
+ findings.push(cleanupFinding('low', 'AGENTS.md/SKILL.md directive duplication budget', `${instructionDuplicationBudget.duplicate_group_count} normalized directive group(s) repeat across separate instruction surfaces; exact normalized matches are advisory and may be intentional`, 'review the reported source files and consolidate only repetition that causes drift or conflicting maintenance'));
991
+ }
905
992
  return {
906
993
  schema: 'yam.cleanup-scan.v1',
907
994
  generated_at: new Date().toISOString(),
908
995
  project: dir,
909
996
  destructive: false,
910
997
  findings,
998
+ instruction_duplication_budget: instructionDuplicationBudget,
911
999
  risk_level: overallCleanupRisk(findings),
912
1000
  safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
913
1001
  truth_status: 'partial'
@@ -961,6 +1049,162 @@ async function staleYamArtifacts(dir) {
961
1049
  }
962
1050
  return artifacts;
963
1051
  }
1052
+ async function buildInstructionDuplicationBudget(rootDir = process.cwd(), limits = INSTRUCTION_DUPLICATION_LIMITS) {
1053
+ const root = path.resolve(rootDir || process.cwd());
1054
+ const inventory = await collectInstructionFiles(root, limits);
1055
+ const directives = new Map();
1056
+ let candidateDirectiveCount = 0;
1057
+ for (const relativeFile of inventory.files) {
1058
+ const absolute = path.join(root, relativeFile);
1059
+ const text = await readBoundedUtf8File(absolute, limits.maxBytesPerFile);
1060
+ for (const directive of instructionDirectives(text, relativeFile, limits.minDirectiveChars)) {
1061
+ candidateDirectiveCount += 1;
1062
+ directives.set(directive.normalized, [...(directives.get(directive.normalized) || []), directive]);
1063
+ }
1064
+ }
1065
+ const allGroups = [...directives.entries()]
1066
+ .map(([normalized, occurrences]) => {
1067
+ const surfaces = [...new Set(occurrences.map((item) => item.file))];
1068
+ if (surfaces.length < 2)
1069
+ return null;
1070
+ return {
1071
+ directive_digest: createHash('sha256').update(normalized).digest('hex').slice(0, 16),
1072
+ directive_preview: redactSensitiveText(String(occurrences[0]?.text || '')).slice(0, 180),
1073
+ occurrence_count: occurrences.length,
1074
+ surface_count: surfaces.length,
1075
+ surfaces,
1076
+ occurrences: occurrences.slice(0, 8).map((item) => ({
1077
+ file: item.file,
1078
+ line: item.line
1079
+ }))
1080
+ };
1081
+ })
1082
+ .filter(Boolean)
1083
+ .sort((left, right) => (right.surface_count - left.surface_count
1084
+ || right.occurrence_count - left.occurrence_count
1085
+ || left.directive_digest.localeCompare(right.directive_digest)));
1086
+ const duplicateGroups = allGroups.slice(0, limits.maxGroups);
1087
+ return {
1088
+ schema: 'yam.instruction-duplication-budget.v1',
1089
+ scope: 'project AGENTS.md and SKILL.md files only',
1090
+ advisory_only: true,
1091
+ hard_gate: false,
1092
+ destructive: false,
1093
+ normalization: 'exact normalized markdown directive text; no semantic similarity inference',
1094
+ limits: {
1095
+ max_files: limits.maxFiles,
1096
+ max_bytes_per_file: limits.maxBytesPerFile,
1097
+ max_groups: limits.maxGroups,
1098
+ max_depth: limits.maxDepth,
1099
+ min_directive_chars: limits.minDirectiveChars
1100
+ },
1101
+ files_scanned: inventory.files.length,
1102
+ scanned_files: inventory.files,
1103
+ candidate_directive_count: candidateDirectiveCount,
1104
+ duplicate_group_count: allGroups.length,
1105
+ duplicate_groups: duplicateGroups,
1106
+ file_limit_reached: inventory.fileLimitReached,
1107
+ group_limit_reached: allGroups.length > duplicateGroups.length,
1108
+ truth_status: 'partial',
1109
+ note: allGroups.length
1110
+ ? 'repetition may be intentional; review before consolidating'
1111
+ : 'no cross-surface exact normalized directive repetition found within the bounded scan'
1112
+ };
1113
+ }
1114
+ async function collectInstructionFiles(root, limits) {
1115
+ const files = [];
1116
+ let fileLimitReached = false;
1117
+ async function walk(current, depth) {
1118
+ if (depth > limits.maxDepth || fileLimitReached)
1119
+ return;
1120
+ let entries = [];
1121
+ try {
1122
+ entries = await fsp.readdir(current, { withFileTypes: true });
1123
+ }
1124
+ catch {
1125
+ return;
1126
+ }
1127
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1128
+ for (const entry of entries) {
1129
+ const absolute = path.join(current, entry.name);
1130
+ const relative = path.relative(root, absolute);
1131
+ if (shouldSkipScanPath(relative, entry.name) || entry.isSymbolicLink())
1132
+ continue;
1133
+ if (entry.isDirectory()) {
1134
+ await walk(absolute, depth + 1);
1135
+ if (fileLimitReached)
1136
+ return;
1137
+ continue;
1138
+ }
1139
+ if (!entry.isFile() || !['AGENTS.md', 'SKILL.md'].includes(entry.name))
1140
+ continue;
1141
+ if (files.length >= limits.maxFiles) {
1142
+ fileLimitReached = true;
1143
+ return;
1144
+ }
1145
+ files.push(relative);
1146
+ }
1147
+ }
1148
+ await walk(root, 0);
1149
+ return { files, fileLimitReached };
1150
+ }
1151
+ async function readBoundedUtf8File(file, maxBytes) {
1152
+ const handle = await fsp.open(file, 'r');
1153
+ try {
1154
+ const buffer = Buffer.alloc(maxBytes);
1155
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1156
+ return buffer.subarray(0, bytesRead).toString('utf8');
1157
+ }
1158
+ finally {
1159
+ await handle.close();
1160
+ }
1161
+ }
1162
+ function instructionDirectives(text = '', relativeFile = '', minChars = 40) {
1163
+ const directives = [];
1164
+ const lines = String(text || '').split(/\r?\n/);
1165
+ let inFence = false;
1166
+ let inFrontmatter = lines[0]?.trim() === '---';
1167
+ for (let index = 0; index < lines.length; index += 1) {
1168
+ const raw = lines[index];
1169
+ const trimmed = raw.trim();
1170
+ if (index === 0 && inFrontmatter)
1171
+ continue;
1172
+ if (inFrontmatter) {
1173
+ if (trimmed === '---')
1174
+ inFrontmatter = false;
1175
+ continue;
1176
+ }
1177
+ if (/^(```|~~~)/.test(trimmed)) {
1178
+ inFence = !inFence;
1179
+ continue;
1180
+ }
1181
+ if (inFence || !trimmed || /^#{1,6}\s/.test(trimmed))
1182
+ continue;
1183
+ const match = trimmed.match(/^(?:[-*+]|\d+[.)])\s+(.+)$/);
1184
+ if (!match)
1185
+ continue;
1186
+ const normalized = normalizeInstructionDirective(match[1]);
1187
+ if (normalized.length < minChars)
1188
+ continue;
1189
+ directives.push({
1190
+ file: relativeFile,
1191
+ line: index + 1,
1192
+ text: match[1].trim().slice(0, 240),
1193
+ normalized
1194
+ });
1195
+ }
1196
+ return directives;
1197
+ }
1198
+ function normalizeInstructionDirective(value = '') {
1199
+ return String(value || '')
1200
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
1201
+ .replace(/[`*_~]/g, '')
1202
+ .replace(/<[^>]+>/g, ' ')
1203
+ .replace(/\s+/g, ' ')
1204
+ .replace(/[.;:,!?。,、;:!?]+$/g, '')
1205
+ .trim()
1206
+ .toLowerCase();
1207
+ }
964
1208
  function printCleanupScanReport(report) {
965
1209
  console.log('yam cleanup scan');
966
1210
  console.log(`Project: ${report.project}`);
@@ -1830,10 +2074,16 @@ async function buildYamLiteContext({ cwd, prompt }) {
1830
2074
  }
1831
2075
  async function buildStudyNoteHookContext({ cwd }) {
1832
2076
  const dir = path.resolve(cwd || process.cwd());
1833
- const changedFiles = await gitChangedFiles(dir);
2077
+ const changedFileDetection = gitChangedFileSnapshot(dir);
2078
+ const changedFiles = changedFileDetection.files;
1834
2079
  const lines = [
1835
2080
  '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.'
1836
2081
  ];
2082
+ if (!changedFileDetection.available) {
2083
+ lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
2084
+ lines.push('Determine manually whether artifacts changed and include the Study Note when they did; the Stop hook will keep this uncertainty visible.');
2085
+ return lines.join('\n');
2086
+ }
1837
2087
  if (!changedFiles.length) {
1838
2088
  lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
1839
2089
  lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
@@ -1852,6 +2102,12 @@ async function buildStudyNoteHookContext({ cwd }) {
1852
2102
  async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
1853
2103
  const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
1854
2104
  const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message');
2105
+ if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
2106
+ return {
2107
+ continue: true,
2108
+ 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.'
2109
+ };
2110
+ }
1855
2111
  if (!result.blockers.length)
1856
2112
  return { continue: true };
1857
2113
  const summary = result.blockers.slice(0, 4).join('; ');
@@ -4341,7 +4597,10 @@ function pickScript(scripts = {}, groups = []) {
4341
4597
  async function detectProject(targetDir = process.cwd(), { quiet = false } = {}) {
4342
4598
  const dir = path.resolve(targetDir || process.cwd());
4343
4599
  const packageJson = path.join(dir, 'package.json');
4600
+ const changedFileDetection = gitChangedFileSnapshot(dir);
4601
+ const changedFiles = changedFileDetection.files;
4344
4602
  const result = {
4603
+ schema: 'yam.project-detection.v1',
4345
4604
  dir,
4346
4605
  packageJson: await exists(packageJson),
4347
4606
  packageManager: 'npm',
@@ -4352,12 +4611,26 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
4352
4611
  test: null,
4353
4612
  build: null
4354
4613
  },
4355
- frameworkChecklist: null
4614
+ frameworkChecklist: null,
4615
+ changedFileDetection,
4616
+ changedFiles,
4617
+ verificationRecommendations: [],
4618
+ verificationCommandPlan: []
4356
4619
  };
4357
4620
  if (!result.packageJson) {
4621
+ result.verificationRecommendations = buildChangedFileVerificationRecommendations({
4622
+ packageManager: result.packageManager,
4623
+ scripts: {},
4624
+ commands: result.commands,
4625
+ changedFiles,
4626
+ changedFilesAvailable: changedFileDetection.available,
4627
+ packageJson: false
4628
+ });
4629
+ result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
4358
4630
  if (!quiet) {
4359
4631
  console.log(`No package.json found in ${dir}`);
4360
4632
  console.log('Suggested verification: use project pack or local framework conventions.');
4633
+ printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
4361
4634
  }
4362
4635
  return result;
4363
4636
  }
@@ -4370,10 +4643,238 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
4370
4643
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
4371
4644
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
4372
4645
  result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
4646
+ result.verificationRecommendations = buildChangedFileVerificationRecommendations({
4647
+ packageManager: result.packageManager,
4648
+ scripts,
4649
+ commands: result.commands,
4650
+ changedFiles,
4651
+ changedFilesAvailable: changedFileDetection.available,
4652
+ packageJson: true
4653
+ });
4654
+ result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
4373
4655
  if (!quiet)
4374
4656
  printDetection(result);
4375
4657
  return result;
4376
4658
  }
4659
+ async function detectCommand(args = []) {
4660
+ const flags = parseSimpleFlags(args, new Set(['json']));
4661
+ const dir = path.resolve(firstPositional(args) || process.cwd());
4662
+ const result = await detectProject(dir, { quiet: true });
4663
+ if (flags.json) {
4664
+ console.log(JSON.stringify(result, null, 2));
4665
+ return;
4666
+ }
4667
+ printDetection(result);
4668
+ }
4669
+ function buildChangedFileVerificationRecommendations({ packageManager = 'npm', scripts = {}, commands = {}, changedFiles = [], changedFilesAvailable = true, packageJson = false }) {
4670
+ const safeFallbackCommands = uniqueStrings([
4671
+ commands.typecheck,
4672
+ commands.lint,
4673
+ commands.test,
4674
+ commands.build
4675
+ ]);
4676
+ if (!changedFilesAvailable) {
4677
+ return [{
4678
+ matched_rule: 'git-status-unavailable',
4679
+ confidence: 'low',
4680
+ reason: 'Git change status was unavailable, so yam cannot prove that the project has no changed files',
4681
+ fallback: packageJson
4682
+ ? 'run all available non-publish typecheck, lint, test, and build checks or use project-pack conventions'
4683
+ : 'use yam.project.md or local framework conventions; no Git change scope or executable package scripts were detected',
4684
+ files: [],
4685
+ suggested_commands: safeFallbackCommands
4686
+ }];
4687
+ }
4688
+ if (!changedFiles.length) {
4689
+ return [{
4690
+ matched_rule: 'no-changes',
4691
+ confidence: 'high',
4692
+ reason: 'git reports no dirty, staged, or untracked project files',
4693
+ fallback: 'rerun `yam detect --json` after files change; use project-pack conventions if Git is unavailable',
4694
+ files: [],
4695
+ suggested_commands: []
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
+ const grouped = new Map();
4735
+ for (const rawFile of changedFiles) {
4736
+ const file = String(rawFile || '').replaceAll('\\', '/');
4737
+ const rule = rules.find((candidate) => candidate.matches(file)) || rules[rules.length - 1];
4738
+ grouped.set(rule.id, [...(grouped.get(rule.id) || []), file]);
4739
+ }
4740
+ const recommendations = [];
4741
+ for (const rule of rules) {
4742
+ const files = grouped.get(rule.id) || [];
4743
+ if (!files.length)
4744
+ continue;
4745
+ let commandEvidence = rule.commandNames.map((name) => ({
4746
+ name,
4747
+ files,
4748
+ basis: 'rule-wide'
4749
+ }));
4750
+ if (rule.id === 'src-lib') {
4751
+ commandEvidence = [
4752
+ { name: 'typecheck', files, basis: 'rule-wide' },
4753
+ ...focusedSmokeScriptEvidenceForFiles(scripts, files)
4754
+ ];
4755
+ }
4756
+ else if (rule.id === 'scripts') {
4757
+ const directEvidence = files.flatMap((file) => (directScriptNamesForFile(scripts, file).map((name) => ({
4758
+ name,
4759
+ files: [file],
4760
+ basis: 'direct-script-wrapper'
4761
+ }))));
4762
+ commandEvidence = directEvidence.length
4763
+ ? directEvidence
4764
+ : focusedSmokeScriptNames(scripts).slice(0, 1).map((name) => ({
4765
+ name,
4766
+ files,
4767
+ basis: 'group-fallback'
4768
+ }));
4769
+ }
4770
+ else if (rule.id === 'unknown-safe-fallback') {
4771
+ commandEvidence = safeFallbackCommands.map((command) => ({
4772
+ command,
4773
+ files,
4774
+ basis: 'safe-fallback'
4775
+ }));
4776
+ }
4777
+ const commandSources = commandEvidence
4778
+ .map((evidence) => ({
4779
+ command: evidence.command || scriptCommandIfPresent(packageManager, scripts, evidence.name),
4780
+ files: [...new Set(evidence.files || [])].sort(),
4781
+ basis: evidence.basis
4782
+ }))
4783
+ .filter((evidence) => evidence.command);
4784
+ const suggestedCommands = uniqueStrings(commandSources.map((evidence) => evidence.command));
4785
+ recommendations.push({
4786
+ matched_rule: rule.id,
4787
+ confidence: rule.confidence,
4788
+ reason: rule.reason,
4789
+ fallback: rule.fallback,
4790
+ files: [...new Set(files)].sort(),
4791
+ suggested_commands: suggestedCommands,
4792
+ command_sources: commandSources
4793
+ });
4794
+ }
4795
+ return recommendations;
4796
+ }
4797
+ function focusedSmokeScriptNames(scripts = {}) {
4798
+ return Object.keys(scripts)
4799
+ .filter((name) => /(?:^|[-:])(?:smoke|test)(?:$|[-:])/i.test(name) || /smoke/i.test(String(scripts[name] || '')))
4800
+ .sort();
4801
+ }
4802
+ function focusedSmokeScriptEvidenceForFiles(scripts = {}, files = []) {
4803
+ const candidates = focusedSmokeScriptNames(scripts);
4804
+ const selected = [];
4805
+ for (const file of files) {
4806
+ const stem = path.posix.basename(String(file || '').replaceAll('\\', '/')).replace(/\.[^.]+$/, '');
4807
+ const direct = candidates.find((name) => {
4808
+ const command = String(scripts[name] || '');
4809
+ return name.includes(stem)
4810
+ || command.includes(stem)
4811
+ || (stem === 'skill-installation' && /install.*smoke|smoke.*install/i.test(`${name} ${command}`))
4812
+ || (['trust-kernel', 'ueye-artifacts'].includes(stem) && /cli.*smoke|smoke.*cli/i.test(`${name} ${command}`));
4813
+ });
4814
+ if (direct)
4815
+ selected.push({
4816
+ name: direct,
4817
+ files: [file],
4818
+ basis: 'focused-file-match'
4819
+ });
4820
+ }
4821
+ if (!selected.length && candidates.length) {
4822
+ selected.push({
4823
+ name: candidates[0],
4824
+ files,
4825
+ basis: 'group-fallback'
4826
+ });
4827
+ }
4828
+ return selected;
4829
+ }
4830
+ function directScriptNamesForFile(scripts = {}, changedFile = '') {
4831
+ const normalized = String(changedFile || '').replaceAll('\\', '/');
4832
+ const basename = path.posix.basename(normalized);
4833
+ return Object.entries(scripts)
4834
+ .filter(([, command]) => {
4835
+ const text = String(command || '').replaceAll('\\', '/');
4836
+ return text.includes(normalized) || text.includes(`/${basename}`) || text.includes(` ${basename}`);
4837
+ })
4838
+ .map(([name]) => name)
4839
+ .sort();
4840
+ }
4841
+ function scriptCommandIfPresent(packageManager, scripts = {}, name = '') {
4842
+ return name && Object.prototype.hasOwnProperty.call(scripts, name)
4843
+ ? runCommand(packageManager, name)
4844
+ : null;
4845
+ }
4846
+ function uniqueStrings(values = []) {
4847
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
4848
+ }
4849
+ function buildVerificationCommandPlan(recommendations = []) {
4850
+ const commands = new Map();
4851
+ for (const recommendation of recommendations) {
4852
+ const sources = recommendation.command_sources?.length
4853
+ ? recommendation.command_sources
4854
+ : (recommendation.suggested_commands || []).map((command) => ({
4855
+ command,
4856
+ files: recommendation.files || [],
4857
+ basis: 'recommendation-fallback'
4858
+ }));
4859
+ for (const source of sources) {
4860
+ const current = commands.get(source.command) || {
4861
+ command: source.command,
4862
+ matched_rules: [],
4863
+ files: [],
4864
+ provenance: []
4865
+ };
4866
+ current.matched_rules = uniqueStrings([...current.matched_rules, recommendation.matched_rule]);
4867
+ current.files = uniqueStrings([...current.files, ...(source.files || [])]).sort();
4868
+ current.provenance.push({
4869
+ matched_rule: recommendation.matched_rule,
4870
+ files: [...new Set(source.files || [])].sort(),
4871
+ basis: source.basis
4872
+ });
4873
+ commands.set(source.command, current);
4874
+ }
4875
+ }
4876
+ return [...commands.values()];
4877
+ }
4377
4878
  function printDetection(result) {
4378
4879
  console.log(`Project: ${result.dir}`);
4379
4880
  console.log(`Package manager: ${result.packageManager}`);
@@ -4387,6 +4888,7 @@ function printDetection(result) {
4387
4888
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
4388
4889
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
4389
4890
  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}`}`);
4891
+ printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
4390
4892
  if (result.frameworkChecklist?.detected) {
4391
4893
  console.log('');
4392
4894
  console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
@@ -4395,6 +4897,28 @@ function printDetection(result) {
4395
4897
  }
4396
4898
  }
4397
4899
  }
4900
+ function printChangedFileVerificationRecommendations(recommendations = [], commandPlan = []) {
4901
+ console.log('');
4902
+ console.log('Changed-file verification recommendations (advisory; commands were not run):');
4903
+ for (const recommendation of recommendations) {
4904
+ console.log(`- ${recommendation.matched_rule} [confidence: ${recommendation.confidence}]`);
4905
+ console.log(` files: ${recommendation.files.length ? recommendation.files.join(', ') : '(none)'}`);
4906
+ console.log(` reason: ${recommendation.reason}`);
4907
+ console.log(` commands: ${recommendation.suggested_commands.length ? recommendation.suggested_commands.join(' && ') : '(none detected)'}`);
4908
+ console.log(` fallback: ${recommendation.fallback}`);
4909
+ }
4910
+ console.log('');
4911
+ console.log('Verification command plan (deduplicated; advisory; commands were not run):');
4912
+ if (!commandPlan.length) {
4913
+ console.log('- (none detected)');
4914
+ return;
4915
+ }
4916
+ for (const item of commandPlan) {
4917
+ console.log(`- ${item.command}`);
4918
+ console.log(` rules: ${item.matched_rules.join(', ')}`);
4919
+ console.log(` files: ${item.files.join(', ') || '(none)'}`);
4920
+ }
4921
+ }
4398
4922
  function budget(routeArg = '') {
4399
4923
  const normalized = normalizeRoute(routeArg);
4400
4924
  const entries = normalized ? [[normalized, ROUTE_BUDGETS[normalized]]] : Object.entries(ROUTE_BUDGETS);
@@ -4773,20 +5297,43 @@ function findSensitivePattern(text = '') {
4773
5297
  function showPath() {
4774
5298
  console.log(`root: ${ROOT}`);
4775
5299
  console.log(`skills: ${DEST}`);
4776
- console.log(`codex mirror cleanup: ${CODEX_MIRROR}`);
5300
+ console.log(`codex mirror (preserved without an ownership receipt): ${CODEX_MIRROR}`);
5301
+ }
5302
+ function showRequestedHelp(args) {
5303
+ if (!args.some((arg) => arg === '--help' || arg === '-h'))
5304
+ return false;
5305
+ const commandUsage = {
5306
+ context: contextUsage,
5307
+ cleanup: cleanupUsage,
5308
+ 'study-note': studyNoteUsage,
5309
+ tools: toolsUsage,
5310
+ hook: hookUsage,
5311
+ loop: loopUsage,
5312
+ ueye: ueyeUsage,
5313
+ media: mediaUsage,
5314
+ runtime: runtimeUsage,
5315
+ mission: missionUsage,
5316
+ benchmark: benchmarkUsage,
5317
+ memory: memoryUsage
5318
+ };
5319
+ (commandUsage[args[0]] || usage)();
5320
+ return true;
4777
5321
  }
4778
5322
  async function main() {
4779
- const command = process.argv[2] || 'help';
4780
- if (command === 'help' || command === '--help' || command === '-h')
5323
+ const args = process.argv.slice(2);
5324
+ const command = args[0] || 'help';
5325
+ if (showRequestedHelp(args))
5326
+ return;
5327
+ if (command === 'help')
4781
5328
  return usage();
4782
5329
  if (command === 'install')
4783
- return install();
5330
+ return install(args.slice(1));
4784
5331
  if (command === 'uninstall')
4785
- return uninstall();
5332
+ return uninstall(args.slice(1));
4786
5333
  if (command === 'version')
4787
5334
  return console.log(VERSION);
4788
5335
  if (command === 'detect')
4789
- return detectProject(process.argv[3]);
5336
+ return detectCommand(args.slice(1));
4790
5337
  if (command === 'pack')
4791
5338
  return inspectProjectPack(process.argv[3]);
4792
5339
  if (command === 'context')