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/CHANGELOG.md +22 -0
- package/COMMANDS.md +3 -1
- package/README.md +17 -4
- package/ROADMAP.md +2 -2
- package/dist/bin/yam.js +604 -57
- package/dist/lib/skill-installation.d.ts +18 -1
- package/dist/lib/skill-installation.js +433 -38
- package/package.json +3 -2
- package/src/bin/yam.ts +625 -55
- package/src/lib/skill-installation.ts +559 -38
- package/yam.manifest.json +2 -2
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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')))
|
|
454
|
-
|
|
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')))
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
769
|
-
const
|
|
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(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
studyNoteGuardCheck('
|
|
780
|
-
studyNoteGuardCheck('
|
|
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] || (
|
|
796
|
-
|
|
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
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
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
|
|
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
|
|
4776
|
-
|
|
4777
|
-
if (
|
|
4778
|
-
if (command === '
|
|
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
|
|
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));
|