yam-flow 2.3.2 → 2.5.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/AGENTS.md +6 -4
- package/CHANGELOG.md +50 -0
- package/COMMANDS.md +16 -1
- package/README.md +65 -4
- package/ROADMAP.md +3 -2
- package/dist/bin/yam.js +691 -57
- package/dist/lib/external-updates.d.ts +103 -0
- package/dist/lib/external-updates.js +989 -0
- package/dist/lib/skill-installation.d.ts +18 -1
- package/dist/lib/skill-installation.js +433 -38
- package/package.json +5 -3
- package/src/bin/yam.ts +713 -55
- package/src/lib/external-updates.ts +1150 -0
- 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,11 +35,16 @@ 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
|
+
import {
|
|
43
|
+
EXTERNAL_UPDATE_COMPONENTS,
|
|
44
|
+
applyExternalUpdates,
|
|
45
|
+
checkExternalUpdates
|
|
46
|
+
} from '../lib/external-updates.js';
|
|
47
|
+
import type { ExternalUpdateComponent } from '../lib/external-updates.js';
|
|
42
48
|
|
|
43
49
|
type AnyRecord = Record<string, any>;
|
|
44
50
|
|
|
@@ -84,6 +90,13 @@ const VERSION = String(PACKAGE_JSON.version || '0.0.0');
|
|
|
84
90
|
const PROJECT_PACK = 'yam.project.md';
|
|
85
91
|
const LEGACY_PROJECT_PACK = 'timeto.project.md';
|
|
86
92
|
const PACK_STALE_DAYS = 30;
|
|
93
|
+
const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
94
|
+
maxFiles: 24,
|
|
95
|
+
maxBytesPerFile: 128000,
|
|
96
|
+
maxGroups: 8,
|
|
97
|
+
maxDepth: 6,
|
|
98
|
+
minDirectiveChars: 40
|
|
99
|
+
};
|
|
87
100
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
88
101
|
const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
|
|
89
102
|
const YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
|
|
@@ -151,7 +164,7 @@ Usage:
|
|
|
151
164
|
yam list
|
|
152
165
|
yam status
|
|
153
166
|
yam verify
|
|
154
|
-
yam detect [dir]
|
|
167
|
+
yam detect [dir] [--json]
|
|
155
168
|
yam pack [dir]
|
|
156
169
|
yam context pressure [dir] [--json]
|
|
157
170
|
yam cleanup scan [dir] [--json]
|
|
@@ -173,6 +186,9 @@ Usage:
|
|
|
173
186
|
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
174
187
|
yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
|
|
175
188
|
yam mission gate [--expected-thread id] [--receipt file] [--json]
|
|
189
|
+
yam update check [--json]
|
|
190
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
191
|
+
yam update apply --all [--json]
|
|
176
192
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
177
193
|
yam release report [--json]
|
|
178
194
|
yam safety [text...]
|
|
@@ -180,7 +196,7 @@ Usage:
|
|
|
180
196
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
181
197
|
yam template <project|ueye|mission|proof|tuning>
|
|
182
198
|
yam tune-log [dir]
|
|
183
|
-
yam install
|
|
199
|
+
yam install [--replace-user-skill name]
|
|
184
200
|
yam uninstall
|
|
185
201
|
yam doctor [--json]
|
|
186
202
|
yam examples
|
|
@@ -206,7 +222,32 @@ async function rmrf(target) {
|
|
|
206
222
|
await fsp.rm(target, { recursive: true, force: true });
|
|
207
223
|
}
|
|
208
224
|
|
|
209
|
-
|
|
225
|
+
function installReplacementSkills(args: string[] = []) {
|
|
226
|
+
const skills: string[] = [];
|
|
227
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
228
|
+
const arg = String(args[index] || '');
|
|
229
|
+
if (arg === '--replace-user-skill') {
|
|
230
|
+
const skill = String(args[index + 1] || '');
|
|
231
|
+
if (!skill || skill.startsWith('--')) {
|
|
232
|
+
throw new Error('missing skill name after --replace-user-skill');
|
|
233
|
+
}
|
|
234
|
+
skills.push(skill);
|
|
235
|
+
index += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (arg.startsWith('--replace-user-skill=')) {
|
|
239
|
+
const skill = arg.slice('--replace-user-skill='.length);
|
|
240
|
+
if (!skill) throw new Error('missing skill name after --replace-user-skill=');
|
|
241
|
+
skills.push(skill);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`unknown yam install argument: ${arg}`);
|
|
245
|
+
}
|
|
246
|
+
return [...new Set(skills)];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function install(args: string[] = []) {
|
|
250
|
+
const replaceSkills = installReplacementSkills(args);
|
|
210
251
|
const result = await installSkillSetTransactional({
|
|
211
252
|
sourceRoot: ROOT,
|
|
212
253
|
destination: DEST,
|
|
@@ -214,6 +255,7 @@ async function install() {
|
|
|
214
255
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
215
256
|
version: VERSION,
|
|
216
257
|
skills: SKILLS,
|
|
258
|
+
replaceSkills,
|
|
217
259
|
legacySkills: LEGACY_SKILLS,
|
|
218
260
|
retiredSkills: RETIRED_SKILLS
|
|
219
261
|
});
|
|
@@ -224,20 +266,20 @@ async function install() {
|
|
|
224
266
|
console.log('Restart Codex to reload skills.');
|
|
225
267
|
}
|
|
226
268
|
|
|
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));
|
|
269
|
+
async function uninstall(args: string[] = []) {
|
|
270
|
+
if (args.length) throw new Error(`unknown yam uninstall argument: ${args[0]}`);
|
|
271
|
+
const result = await uninstallSkillSetSafely({
|
|
272
|
+
destination: DEST,
|
|
273
|
+
codexMirror: CODEX_MIRROR,
|
|
274
|
+
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
275
|
+
skills: SKILLS
|
|
276
|
+
});
|
|
239
277
|
console.log(`yam removed from ${DEST}`);
|
|
240
|
-
|
|
278
|
+
console.log(`removed receipt: ${result.receiptPath}`);
|
|
279
|
+
for (const warning of result.cleanupWarnings) console.warn(`warning: ${warning}`);
|
|
280
|
+
if (CODEX_MIRROR !== DEST) {
|
|
281
|
+
console.log(`unowned legacy, retired, and Codex mirror entries were preserved`);
|
|
282
|
+
}
|
|
241
283
|
console.log('Restart Codex to unload skills.');
|
|
242
284
|
}
|
|
243
285
|
|
|
@@ -426,12 +468,20 @@ async function doctor(args = []) {
|
|
|
426
468
|
console.log('yam doctor: ok');
|
|
427
469
|
console.log('No hooks, automations, or global config are required.');
|
|
428
470
|
console.log('Skill install state is reported by `yam status`.');
|
|
471
|
+
if (report.preservedUnprovenSkillEntries.length) {
|
|
472
|
+
console.log(`Preserved unproven legacy/retired/mirror entries: ${report.preservedUnprovenSkillEntries.length}`);
|
|
473
|
+
}
|
|
429
474
|
console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
|
|
430
475
|
return;
|
|
431
476
|
}
|
|
432
477
|
|
|
433
478
|
console.log('yam doctor: issues');
|
|
434
479
|
for (const issue of report.issues) console.log(`- ${issue}`);
|
|
480
|
+
if (report.preservedUnprovenSkillEntries.length) {
|
|
481
|
+
console.log('');
|
|
482
|
+
console.log('Preserved unproven skill entries:');
|
|
483
|
+
for (const entry of report.preservedUnprovenSkillEntries) console.log(`- ${entry}`);
|
|
484
|
+
}
|
|
435
485
|
if (report.nextActions.length) {
|
|
436
486
|
console.log('');
|
|
437
487
|
console.log('Next actions:');
|
|
@@ -442,6 +492,7 @@ async function doctor(args = []) {
|
|
|
442
492
|
|
|
443
493
|
async function buildDoctorReport() {
|
|
444
494
|
const issues = [];
|
|
495
|
+
const preservedUnprovenSkillEntries = [];
|
|
445
496
|
for (const skill of SKILLS) {
|
|
446
497
|
if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
|
|
447
498
|
}
|
|
@@ -450,15 +501,21 @@ async function buildDoctorReport() {
|
|
|
450
501
|
if (await exists(path.join(os.homedir(), '.codex', 'automations', 'yam'))) issues.push('unexpected yam automation');
|
|
451
502
|
if (await exists(path.join(os.homedir(), '.codex', 'automations', 'timeto'))) issues.push('unexpected legacy timeto automation');
|
|
452
503
|
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}`);
|
|
504
|
+
if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) {
|
|
505
|
+
preservedUnprovenSkillEntries.push(`legacy-named destination entry: ${legacySkill}`);
|
|
456
506
|
}
|
|
457
507
|
}
|
|
458
508
|
for (const retiredSkill of RETIRED_SKILLS) {
|
|
459
|
-
if (await exists(path.join(DEST, retiredSkill, 'SKILL.md')))
|
|
460
|
-
|
|
461
|
-
|
|
509
|
+
if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) {
|
|
510
|
+
preservedUnprovenSkillEntries.push(`retired-name destination entry: ${retiredSkill}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (CODEX_MIRROR !== DEST) {
|
|
514
|
+
const mirrorSkillNames = [...new Set([...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS])];
|
|
515
|
+
for (const skill of mirrorSkillNames) {
|
|
516
|
+
if (!await exists(path.join(CODEX_MIRROR, skill, 'SKILL.md'))) continue;
|
|
517
|
+
const nameKind = SKILLS.includes(skill) ? 'active-name' : LEGACY_SKILLS.includes(skill) ? 'legacy-named' : 'retired-name';
|
|
518
|
+
preservedUnprovenSkillEntries.push(`${nameKind} Codex mirror entry: ${skill}`);
|
|
462
519
|
}
|
|
463
520
|
}
|
|
464
521
|
const verifyIssues = await verify({ quiet: true });
|
|
@@ -470,6 +527,7 @@ async function buildDoctorReport() {
|
|
|
470
527
|
generatedAt: new Date().toISOString(),
|
|
471
528
|
ok: issues.length === 0,
|
|
472
529
|
issues,
|
|
530
|
+
preservedUnprovenSkillEntries,
|
|
473
531
|
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
474
532
|
nextActionDetails,
|
|
475
533
|
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
@@ -487,7 +545,6 @@ function doctorNextActionDetails(issues = []) {
|
|
|
487
545
|
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
546
|
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
547
|
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
548
|
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
549
|
}
|
|
493
550
|
return uniqueNextActionDetails(actions);
|
|
@@ -765,19 +822,28 @@ async function studyNoteCheck(args = []) {
|
|
|
765
822
|
}
|
|
766
823
|
|
|
767
824
|
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
768
|
-
const
|
|
769
|
-
const
|
|
825
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
826
|
+
const changedFiles = changedFileDetection.files;
|
|
827
|
+
const changeScopeAvailable = changedFileDetection.available;
|
|
828
|
+
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
829
|
+
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
770
830
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
771
831
|
const checks = [
|
|
772
|
-
studyNoteGuardCheck(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
studyNoteGuardCheck('
|
|
780
|
-
studyNoteGuardCheck('
|
|
832
|
+
studyNoteGuardCheck(
|
|
833
|
+
'changed_files',
|
|
834
|
+
changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial',
|
|
835
|
+
changeScopeAvailable
|
|
836
|
+
? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
|
|
837
|
+
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'
|
|
838
|
+
),
|
|
839
|
+
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
840
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText)), 'Study Note should explain what the touched code/artifact does'),
|
|
841
|
+
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'),
|
|
842
|
+
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'),
|
|
843
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText)), 'Study Note should describe the expected behavior or result'),
|
|
844
|
+
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'),
|
|
845
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText)), 'Study Note should say what was checked'),
|
|
846
|
+
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
847
|
];
|
|
782
848
|
for (const requirement of hygieneRequirements) {
|
|
783
849
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(reportText, requirement), requirement.note));
|
|
@@ -787,13 +853,18 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
787
853
|
schema: 'yam.study-note-guard.v1',
|
|
788
854
|
generated_at: new Date().toISOString(),
|
|
789
855
|
project: dir,
|
|
856
|
+
changed_file_detection: changedFileDetection,
|
|
790
857
|
changed_files: changedFiles,
|
|
791
858
|
changed_file_count: changedFiles.length,
|
|
792
859
|
report_source: reportSource,
|
|
793
860
|
checks,
|
|
794
861
|
blockers,
|
|
795
|
-
next_action: blockers[0] || (
|
|
796
|
-
|
|
862
|
+
next_action: blockers[0] || (!changeScopeAvailable
|
|
863
|
+
? 'Git change scope is unavailable; inspect changed artifacts manually and include a Study Note when anything changed'
|
|
864
|
+
: needsStudyNote
|
|
865
|
+
? 'Study Note guard passed for the supplied report text'
|
|
866
|
+
: 'no Study Note required because no changed files were detected'),
|
|
867
|
+
truth_status: blockers.length ? 'blocked' : !changeScopeAvailable ? 'partial' : needsStudyNote ? 'verified' : 'skipped'
|
|
797
868
|
};
|
|
798
869
|
return result;
|
|
799
870
|
}
|
|
@@ -811,14 +882,35 @@ function firstPositional(args = []) {
|
|
|
811
882
|
return '';
|
|
812
883
|
}
|
|
813
884
|
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
885
|
+
function gitChangedFileSnapshot(dir) {
|
|
886
|
+
const result = spawnSync('git', ['status', '--short', '-z', '--untracked-files=all'], {
|
|
887
|
+
cwd: dir,
|
|
888
|
+
encoding: 'utf8',
|
|
889
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
890
|
+
timeout: 30000
|
|
891
|
+
});
|
|
892
|
+
if (result.status !== 0) {
|
|
893
|
+
return {
|
|
894
|
+
available: false,
|
|
895
|
+
source: 'git status --short -z --untracked-files=all',
|
|
896
|
+
files: []
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const records = String(result.stdout || '').split('\0');
|
|
900
|
+
const files = [];
|
|
901
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
902
|
+
const record = records[index];
|
|
903
|
+
if (!record || record.length < 4) continue;
|
|
904
|
+
const status = record.slice(0, 2);
|
|
905
|
+
const file = record.slice(3);
|
|
906
|
+
if (/[RC]/.test(status)) index += 1;
|
|
907
|
+
if (file && !file.startsWith('dist/') && !file.endsWith('.tgz')) files.push(file);
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
available: true,
|
|
911
|
+
source: 'git status --short -z --untracked-files=all',
|
|
912
|
+
files
|
|
913
|
+
};
|
|
822
914
|
}
|
|
823
915
|
|
|
824
916
|
async function readStudyNoteReportText(flags, dir) {
|
|
@@ -911,12 +1003,22 @@ async function buildCleanupScanReport(targetDir = process.cwd()) {
|
|
|
911
1003
|
for (const artifact of await staleYamArtifacts(dir)) {
|
|
912
1004
|
findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
|
|
913
1005
|
}
|
|
1006
|
+
const instructionDuplicationBudget = await buildInstructionDuplicationBudget(dir);
|
|
1007
|
+
if (instructionDuplicationBudget.duplicate_group_count > 0) {
|
|
1008
|
+
findings.push(cleanupFinding(
|
|
1009
|
+
'low',
|
|
1010
|
+
'AGENTS.md/SKILL.md directive duplication budget',
|
|
1011
|
+
`${instructionDuplicationBudget.duplicate_group_count} normalized directive group(s) repeat across separate instruction surfaces; exact normalized matches are advisory and may be intentional`,
|
|
1012
|
+
'review the reported source files and consolidate only repetition that causes drift or conflicting maintenance'
|
|
1013
|
+
));
|
|
1014
|
+
}
|
|
914
1015
|
return {
|
|
915
1016
|
schema: 'yam.cleanup-scan.v1',
|
|
916
1017
|
generated_at: new Date().toISOString(),
|
|
917
1018
|
project: dir,
|
|
918
1019
|
destructive: false,
|
|
919
1020
|
findings,
|
|
1021
|
+
instruction_duplication_budget: instructionDuplicationBudget,
|
|
920
1022
|
risk_level: overallCleanupRisk(findings),
|
|
921
1023
|
safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
|
|
922
1024
|
truth_status: 'partial'
|
|
@@ -970,6 +1072,163 @@ async function staleYamArtifacts(dir) {
|
|
|
970
1072
|
return artifacts;
|
|
971
1073
|
}
|
|
972
1074
|
|
|
1075
|
+
async function buildInstructionDuplicationBudget(
|
|
1076
|
+
rootDir = process.cwd(),
|
|
1077
|
+
limits = INSTRUCTION_DUPLICATION_LIMITS
|
|
1078
|
+
) {
|
|
1079
|
+
const root = path.resolve(rootDir || process.cwd());
|
|
1080
|
+
const inventory = await collectInstructionFiles(root, limits);
|
|
1081
|
+
const directives = new Map<string, AnyRecord[]>();
|
|
1082
|
+
let candidateDirectiveCount = 0;
|
|
1083
|
+
for (const relativeFile of inventory.files) {
|
|
1084
|
+
const absolute = path.join(root, relativeFile);
|
|
1085
|
+
const text = await readBoundedUtf8File(absolute, limits.maxBytesPerFile);
|
|
1086
|
+
for (const directive of instructionDirectives(text, relativeFile, limits.minDirectiveChars)) {
|
|
1087
|
+
candidateDirectiveCount += 1;
|
|
1088
|
+
directives.set(directive.normalized, [...(directives.get(directive.normalized) || []), directive]);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const allGroups = [...directives.entries()]
|
|
1093
|
+
.map(([normalized, occurrences]) => {
|
|
1094
|
+
const surfaces = [...new Set(occurrences.map((item) => item.file))];
|
|
1095
|
+
if (surfaces.length < 2) return null;
|
|
1096
|
+
return {
|
|
1097
|
+
directive_digest: createHash('sha256').update(normalized).digest('hex').slice(0, 16),
|
|
1098
|
+
directive_preview: redactSensitiveText(String(occurrences[0]?.text || '')).slice(0, 180),
|
|
1099
|
+
occurrence_count: occurrences.length,
|
|
1100
|
+
surface_count: surfaces.length,
|
|
1101
|
+
surfaces,
|
|
1102
|
+
occurrences: occurrences.slice(0, 8).map((item) => ({
|
|
1103
|
+
file: item.file,
|
|
1104
|
+
line: item.line
|
|
1105
|
+
}))
|
|
1106
|
+
};
|
|
1107
|
+
})
|
|
1108
|
+
.filter(Boolean)
|
|
1109
|
+
.sort((left, right) => (
|
|
1110
|
+
right.surface_count - left.surface_count
|
|
1111
|
+
|| right.occurrence_count - left.occurrence_count
|
|
1112
|
+
|| left.directive_digest.localeCompare(right.directive_digest)
|
|
1113
|
+
));
|
|
1114
|
+
const duplicateGroups = allGroups.slice(0, limits.maxGroups);
|
|
1115
|
+
return {
|
|
1116
|
+
schema: 'yam.instruction-duplication-budget.v1',
|
|
1117
|
+
scope: 'project AGENTS.md and SKILL.md files only',
|
|
1118
|
+
advisory_only: true,
|
|
1119
|
+
hard_gate: false,
|
|
1120
|
+
destructive: false,
|
|
1121
|
+
normalization: 'exact normalized markdown directive text; no semantic similarity inference',
|
|
1122
|
+
limits: {
|
|
1123
|
+
max_files: limits.maxFiles,
|
|
1124
|
+
max_bytes_per_file: limits.maxBytesPerFile,
|
|
1125
|
+
max_groups: limits.maxGroups,
|
|
1126
|
+
max_depth: limits.maxDepth,
|
|
1127
|
+
min_directive_chars: limits.minDirectiveChars
|
|
1128
|
+
},
|
|
1129
|
+
files_scanned: inventory.files.length,
|
|
1130
|
+
scanned_files: inventory.files,
|
|
1131
|
+
candidate_directive_count: candidateDirectiveCount,
|
|
1132
|
+
duplicate_group_count: allGroups.length,
|
|
1133
|
+
duplicate_groups: duplicateGroups,
|
|
1134
|
+
file_limit_reached: inventory.fileLimitReached,
|
|
1135
|
+
group_limit_reached: allGroups.length > duplicateGroups.length,
|
|
1136
|
+
truth_status: 'partial',
|
|
1137
|
+
note: allGroups.length
|
|
1138
|
+
? 'repetition may be intentional; review before consolidating'
|
|
1139
|
+
: 'no cross-surface exact normalized directive repetition found within the bounded scan'
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function collectInstructionFiles(root, limits) {
|
|
1144
|
+
const files: string[] = [];
|
|
1145
|
+
let fileLimitReached = false;
|
|
1146
|
+
|
|
1147
|
+
async function walk(current, depth) {
|
|
1148
|
+
if (depth > limits.maxDepth || fileLimitReached) return;
|
|
1149
|
+
let entries = [];
|
|
1150
|
+
try {
|
|
1151
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
1152
|
+
} catch {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1156
|
+
for (const entry of entries) {
|
|
1157
|
+
const absolute = path.join(current, entry.name);
|
|
1158
|
+
const relative = path.relative(root, absolute);
|
|
1159
|
+
if (shouldSkipScanPath(relative, entry.name) || entry.isSymbolicLink()) continue;
|
|
1160
|
+
if (entry.isDirectory()) {
|
|
1161
|
+
await walk(absolute, depth + 1);
|
|
1162
|
+
if (fileLimitReached) return;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (!entry.isFile() || !['AGENTS.md', 'SKILL.md'].includes(entry.name)) continue;
|
|
1166
|
+
if (files.length >= limits.maxFiles) {
|
|
1167
|
+
fileLimitReached = true;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
files.push(relative);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
await walk(root, 0);
|
|
1175
|
+
return { files, fileLimitReached };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
async function readBoundedUtf8File(file, maxBytes) {
|
|
1179
|
+
const handle = await fsp.open(file, 'r');
|
|
1180
|
+
try {
|
|
1181
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1182
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
1183
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
1184
|
+
} finally {
|
|
1185
|
+
await handle.close();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function instructionDirectives(text = '', relativeFile = '', minChars = 40) {
|
|
1190
|
+
const directives = [];
|
|
1191
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
1192
|
+
let inFence = false;
|
|
1193
|
+
let inFrontmatter = lines[0]?.trim() === '---';
|
|
1194
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1195
|
+
const raw = lines[index];
|
|
1196
|
+
const trimmed = raw.trim();
|
|
1197
|
+
if (index === 0 && inFrontmatter) continue;
|
|
1198
|
+
if (inFrontmatter) {
|
|
1199
|
+
if (trimmed === '---') inFrontmatter = false;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (/^(```|~~~)/.test(trimmed)) {
|
|
1203
|
+
inFence = !inFence;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (inFence || !trimmed || /^#{1,6}\s/.test(trimmed)) continue;
|
|
1207
|
+
const match = trimmed.match(/^(?:[-*+]|\d+[.)])\s+(.+)$/);
|
|
1208
|
+
if (!match) continue;
|
|
1209
|
+
const normalized = normalizeInstructionDirective(match[1]);
|
|
1210
|
+
if (normalized.length < minChars) continue;
|
|
1211
|
+
directives.push({
|
|
1212
|
+
file: relativeFile,
|
|
1213
|
+
line: index + 1,
|
|
1214
|
+
text: match[1].trim().slice(0, 240),
|
|
1215
|
+
normalized
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return directives;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function normalizeInstructionDirective(value = '') {
|
|
1222
|
+
return String(value || '')
|
|
1223
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
1224
|
+
.replace(/[`*_~]/g, '')
|
|
1225
|
+
.replace(/<[^>]+>/g, ' ')
|
|
1226
|
+
.replace(/\s+/g, ' ')
|
|
1227
|
+
.replace(/[.;:,!?。,、;:!?]+$/g, '')
|
|
1228
|
+
.trim()
|
|
1229
|
+
.toLowerCase();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
973
1232
|
function printCleanupScanReport(report) {
|
|
974
1233
|
console.log('yam cleanup scan');
|
|
975
1234
|
console.log(`Project: ${report.project}`);
|
|
@@ -1829,10 +2088,16 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
1829
2088
|
|
|
1830
2089
|
async function buildStudyNoteHookContext({ cwd }) {
|
|
1831
2090
|
const dir = path.resolve(cwd || process.cwd());
|
|
1832
|
-
const
|
|
2091
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2092
|
+
const changedFiles = changedFileDetection.files;
|
|
1833
2093
|
const lines = [
|
|
1834
2094
|
'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
2095
|
];
|
|
2096
|
+
if (!changedFileDetection.available) {
|
|
2097
|
+
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2098
|
+
lines.push('Determine manually whether artifacts changed and include the Study Note when they did; the Stop hook will keep this uncertainty visible.');
|
|
2099
|
+
return lines.join('\n');
|
|
2100
|
+
}
|
|
1836
2101
|
if (!changedFiles.length) {
|
|
1837
2102
|
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
|
|
1838
2103
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
@@ -1852,6 +2117,12 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
1852
2117
|
async function buildStudyNoteStopOutput(input: AnyRecord = {}, cwd = process.cwd()) {
|
|
1853
2118
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
1854
2119
|
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message');
|
|
2120
|
+
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2121
|
+
return {
|
|
2122
|
+
continue: true,
|
|
2123
|
+
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.'
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
1855
2126
|
if (!result.blockers.length) return { continue: true };
|
|
1856
2127
|
const summary = result.blockers.slice(0, 4).join('; ');
|
|
1857
2128
|
if (Boolean(input?.stop_hook_active || input?.stopHookActive)) {
|
|
@@ -3302,6 +3573,83 @@ async function mission(args = []) {
|
|
|
3302
3573
|
return missionUsage();
|
|
3303
3574
|
}
|
|
3304
3575
|
|
|
3576
|
+
async function updateCommand(args: string[] = []) {
|
|
3577
|
+
const subcommand = args[0] || 'help';
|
|
3578
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return updateUsage();
|
|
3579
|
+
if (subcommand === 'check') {
|
|
3580
|
+
const rest = args.slice(1);
|
|
3581
|
+
const invalid = rest.filter((arg) => arg !== '--json');
|
|
3582
|
+
if (invalid.length || rest.filter((arg) => arg === '--json').length > 1) {
|
|
3583
|
+
throw new Error(`invalid update check option: ${invalid[0] || '--json repeated'}`);
|
|
3584
|
+
}
|
|
3585
|
+
const report = await checkExternalUpdates(VERSION);
|
|
3586
|
+
printJsonOrHuman(report, rest.includes('--json'), 'External update check');
|
|
3587
|
+
if (!report.success) process.exitCode = 1;
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
if (subcommand !== 'apply') {
|
|
3591
|
+
throw new Error(`unknown update command: ${subcommand}`);
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
const parsed = parseUpdateApplyArgs(args.slice(1));
|
|
3595
|
+
const report = await applyExternalUpdates(VERSION, {
|
|
3596
|
+
component: parsed.component,
|
|
3597
|
+
all: parsed.all
|
|
3598
|
+
});
|
|
3599
|
+
printJsonOrHuman(report, parsed.json, 'External update apply');
|
|
3600
|
+
if (!report.success) process.exitCode = 1;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function parseUpdateApplyArgs(args: string[]) {
|
|
3604
|
+
let component: ExternalUpdateComponent | undefined;
|
|
3605
|
+
let all = false;
|
|
3606
|
+
let json = false;
|
|
3607
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3608
|
+
const arg = String(args[index] || '');
|
|
3609
|
+
if (arg === '--json') {
|
|
3610
|
+
if (json) throw new Error('--json may be supplied only once');
|
|
3611
|
+
json = true;
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
if (arg === '--all') {
|
|
3615
|
+
if (all) throw new Error('--all may be supplied only once');
|
|
3616
|
+
all = true;
|
|
3617
|
+
continue;
|
|
3618
|
+
}
|
|
3619
|
+
if (arg === '--component' || arg.startsWith('--component=')) {
|
|
3620
|
+
if (component) throw new Error('--component may be supplied only once');
|
|
3621
|
+
const value = arg === '--component' ? String(args[index + 1] || '') : arg.slice('--component='.length);
|
|
3622
|
+
if (arg === '--component') index += 1;
|
|
3623
|
+
if (!EXTERNAL_UPDATE_COMPONENTS.includes(value as ExternalUpdateComponent)) {
|
|
3624
|
+
throw new Error('component must be one of: yam, scrapling, insane-search');
|
|
3625
|
+
}
|
|
3626
|
+
component = value as ExternalUpdateComponent;
|
|
3627
|
+
continue;
|
|
3628
|
+
}
|
|
3629
|
+
throw new Error(`invalid update apply option: ${arg}`);
|
|
3630
|
+
}
|
|
3631
|
+
if ((all && component) || (!all && !component)) {
|
|
3632
|
+
throw new Error('choose exactly one of --component yam|scrapling|insane-search or --all');
|
|
3633
|
+
}
|
|
3634
|
+
return { component, all, json };
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
function updateUsage() {
|
|
3638
|
+
console.log(`yam update
|
|
3639
|
+
|
|
3640
|
+
Usage:
|
|
3641
|
+
yam update check [--json]
|
|
3642
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
3643
|
+
yam update apply --all [--json]
|
|
3644
|
+
|
|
3645
|
+
Safety:
|
|
3646
|
+
check is read-only. apply requires an explicit component or --all.
|
|
3647
|
+
--all updates Scrapling and Insane Search before yam, then stops at the first failed/manual component.
|
|
3648
|
+
Insane Search is updated only through the official Codex CLI; plugin caches are never edited directly.
|
|
3649
|
+
Every apply attempt records component checks, outcome, side effects, and rollback guidance.
|
|
3650
|
+
`);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3305
3653
|
function missionUsage() {
|
|
3306
3654
|
console.log(`yam mission
|
|
3307
3655
|
|
|
@@ -4324,7 +4672,10 @@ function pickScript(scripts = {}, groups = []) {
|
|
|
4324
4672
|
async function detectProject(targetDir = process.cwd(), { quiet = false } = {}) {
|
|
4325
4673
|
const dir = path.resolve(targetDir || process.cwd());
|
|
4326
4674
|
const packageJson = path.join(dir, 'package.json');
|
|
4675
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
4676
|
+
const changedFiles = changedFileDetection.files;
|
|
4327
4677
|
const result = {
|
|
4678
|
+
schema: 'yam.project-detection.v1',
|
|
4328
4679
|
dir,
|
|
4329
4680
|
packageJson: await exists(packageJson),
|
|
4330
4681
|
packageManager: 'npm',
|
|
@@ -4335,12 +4686,26 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
4335
4686
|
test: null,
|
|
4336
4687
|
build: null
|
|
4337
4688
|
},
|
|
4338
|
-
frameworkChecklist: null
|
|
4689
|
+
frameworkChecklist: null,
|
|
4690
|
+
changedFileDetection,
|
|
4691
|
+
changedFiles,
|
|
4692
|
+
verificationRecommendations: [],
|
|
4693
|
+
verificationCommandPlan: []
|
|
4339
4694
|
};
|
|
4340
4695
|
if (!result.packageJson) {
|
|
4696
|
+
result.verificationRecommendations = buildChangedFileVerificationRecommendations({
|
|
4697
|
+
packageManager: result.packageManager,
|
|
4698
|
+
scripts: {},
|
|
4699
|
+
commands: result.commands,
|
|
4700
|
+
changedFiles,
|
|
4701
|
+
changedFilesAvailable: changedFileDetection.available,
|
|
4702
|
+
packageJson: false
|
|
4703
|
+
});
|
|
4704
|
+
result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
|
|
4341
4705
|
if (!quiet) {
|
|
4342
4706
|
console.log(`No package.json found in ${dir}`);
|
|
4343
4707
|
console.log('Suggested verification: use project pack or local framework conventions.');
|
|
4708
|
+
printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
|
|
4344
4709
|
}
|
|
4345
4710
|
return result;
|
|
4346
4711
|
}
|
|
@@ -4354,11 +4719,255 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
4354
4719
|
result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
|
|
4355
4720
|
result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
|
|
4356
4721
|
result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
|
|
4722
|
+
result.verificationRecommendations = buildChangedFileVerificationRecommendations({
|
|
4723
|
+
packageManager: result.packageManager,
|
|
4724
|
+
scripts,
|
|
4725
|
+
commands: result.commands,
|
|
4726
|
+
changedFiles,
|
|
4727
|
+
changedFilesAvailable: changedFileDetection.available,
|
|
4728
|
+
packageJson: true
|
|
4729
|
+
});
|
|
4730
|
+
result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
|
|
4357
4731
|
|
|
4358
4732
|
if (!quiet) printDetection(result);
|
|
4359
4733
|
return result;
|
|
4360
4734
|
}
|
|
4361
4735
|
|
|
4736
|
+
async function detectCommand(args = []) {
|
|
4737
|
+
const flags = parseSimpleFlags(args, new Set(['json']));
|
|
4738
|
+
const dir = path.resolve(firstPositional(args) || process.cwd());
|
|
4739
|
+
const result = await detectProject(dir, { quiet: true });
|
|
4740
|
+
if (flags.json) {
|
|
4741
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
printDetection(result);
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
function buildChangedFileVerificationRecommendations({
|
|
4748
|
+
packageManager = 'npm',
|
|
4749
|
+
scripts = {},
|
|
4750
|
+
commands = {},
|
|
4751
|
+
changedFiles = [],
|
|
4752
|
+
changedFilesAvailable = true,
|
|
4753
|
+
packageJson = false
|
|
4754
|
+
}: AnyRecord) {
|
|
4755
|
+
const safeFallbackCommands = uniqueStrings([
|
|
4756
|
+
commands.typecheck,
|
|
4757
|
+
commands.lint,
|
|
4758
|
+
commands.test,
|
|
4759
|
+
commands.build
|
|
4760
|
+
]);
|
|
4761
|
+
if (!changedFilesAvailable) {
|
|
4762
|
+
return [{
|
|
4763
|
+
matched_rule: 'git-status-unavailable',
|
|
4764
|
+
confidence: 'low',
|
|
4765
|
+
reason: 'Git change status was unavailable, so yam cannot prove that the project has no changed files',
|
|
4766
|
+
fallback: packageJson
|
|
4767
|
+
? 'run all available non-publish typecheck, lint, test, and build checks or use project-pack conventions'
|
|
4768
|
+
: 'use yam.project.md or local framework conventions; no Git change scope or executable package scripts were detected',
|
|
4769
|
+
files: [],
|
|
4770
|
+
suggested_commands: safeFallbackCommands
|
|
4771
|
+
}];
|
|
4772
|
+
}
|
|
4773
|
+
if (!changedFiles.length) {
|
|
4774
|
+
return [{
|
|
4775
|
+
matched_rule: 'no-changes',
|
|
4776
|
+
confidence: 'high',
|
|
4777
|
+
reason: 'git reports no dirty, staged, or untracked project files',
|
|
4778
|
+
fallback: 'rerun `yam detect --json` after files change; use project-pack conventions if Git is unavailable',
|
|
4779
|
+
files: [],
|
|
4780
|
+
suggested_commands: []
|
|
4781
|
+
}];
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
const rules = [
|
|
4785
|
+
{
|
|
4786
|
+
id: 'package-release-metadata',
|
|
4787
|
+
confidence: 'high',
|
|
4788
|
+
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),
|
|
4789
|
+
reason: 'package or release metadata changed, so package contents and release readiness can drift',
|
|
4790
|
+
commandNames: ['package-boundary:check', 'release:check'],
|
|
4791
|
+
fallback: 'run the available typecheck/build/package checks and perform the public release workflow separately'
|
|
4792
|
+
},
|
|
4793
|
+
{
|
|
4794
|
+
id: 'src-lib',
|
|
4795
|
+
confidence: 'high',
|
|
4796
|
+
matches: (file) => /^src\/lib\//i.test(file),
|
|
4797
|
+
reason: 'shared library code changed, so compile safety and a focused behavior smoke are the narrowest honest checks',
|
|
4798
|
+
commandNames: ['typecheck'],
|
|
4799
|
+
fallback: 'run typecheck plus the closest available test/smoke; use the full safe fallback when the library crosses package boundaries'
|
|
4800
|
+
},
|
|
4801
|
+
{
|
|
4802
|
+
id: 'scripts',
|
|
4803
|
+
confidence: 'medium',
|
|
4804
|
+
matches: (file) => /^scripts\//i.test(file),
|
|
4805
|
+
reason: 'an executable verification or maintenance script changed, so its direct npm wrapper or closest smoke should run',
|
|
4806
|
+
commandNames: [],
|
|
4807
|
+
fallback: 'run the changed script in an isolated fixture, then use the full safe fallback if no direct wrapper exists'
|
|
4808
|
+
},
|
|
4809
|
+
{
|
|
4810
|
+
id: 'unknown-safe-fallback',
|
|
4811
|
+
confidence: packageJson ? 'medium' : 'low',
|
|
4812
|
+
matches: () => true,
|
|
4813
|
+
reason: 'no focused changed-file rule matched, so skipping verification would be weaker than the available project-wide safe checks',
|
|
4814
|
+
commandNames: [],
|
|
4815
|
+
fallback: packageJson
|
|
4816
|
+
? 'run all available non-publish typecheck, lint, test, and build checks'
|
|
4817
|
+
: 'use yam.project.md or local framework conventions; no executable package scripts were detected'
|
|
4818
|
+
}
|
|
4819
|
+
];
|
|
4820
|
+
|
|
4821
|
+
const grouped = new Map<string, string[]>();
|
|
4822
|
+
for (const rawFile of changedFiles) {
|
|
4823
|
+
const file = String(rawFile || '').replaceAll('\\', '/');
|
|
4824
|
+
const rule = rules.find((candidate) => candidate.matches(file)) || rules[rules.length - 1];
|
|
4825
|
+
grouped.set(rule.id, [...(grouped.get(rule.id) || []), file]);
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
const recommendations = [];
|
|
4829
|
+
for (const rule of rules) {
|
|
4830
|
+
const files = grouped.get(rule.id) || [];
|
|
4831
|
+
if (!files.length) continue;
|
|
4832
|
+
let commandEvidence: AnyRecord[] = rule.commandNames.map((name) => ({
|
|
4833
|
+
name,
|
|
4834
|
+
files,
|
|
4835
|
+
basis: 'rule-wide'
|
|
4836
|
+
}));
|
|
4837
|
+
if (rule.id === 'src-lib') {
|
|
4838
|
+
commandEvidence = [
|
|
4839
|
+
{ name: 'typecheck', files, basis: 'rule-wide' },
|
|
4840
|
+
...focusedSmokeScriptEvidenceForFiles(scripts, files)
|
|
4841
|
+
];
|
|
4842
|
+
} else if (rule.id === 'scripts') {
|
|
4843
|
+
const directEvidence = files.flatMap((file) => (
|
|
4844
|
+
directScriptNamesForFile(scripts, file).map((name) => ({
|
|
4845
|
+
name,
|
|
4846
|
+
files: [file],
|
|
4847
|
+
basis: 'direct-script-wrapper'
|
|
4848
|
+
}))
|
|
4849
|
+
));
|
|
4850
|
+
commandEvidence = directEvidence.length
|
|
4851
|
+
? directEvidence
|
|
4852
|
+
: focusedSmokeScriptNames(scripts).slice(0, 1).map((name) => ({
|
|
4853
|
+
name,
|
|
4854
|
+
files,
|
|
4855
|
+
basis: 'group-fallback'
|
|
4856
|
+
}));
|
|
4857
|
+
} else if (rule.id === 'unknown-safe-fallback') {
|
|
4858
|
+
commandEvidence = safeFallbackCommands.map((command) => ({
|
|
4859
|
+
command,
|
|
4860
|
+
files,
|
|
4861
|
+
basis: 'safe-fallback'
|
|
4862
|
+
}));
|
|
4863
|
+
}
|
|
4864
|
+
const commandSources = commandEvidence
|
|
4865
|
+
.map((evidence) => ({
|
|
4866
|
+
command: evidence.command || scriptCommandIfPresent(packageManager, scripts, evidence.name),
|
|
4867
|
+
files: [...new Set(evidence.files || [])].sort(),
|
|
4868
|
+
basis: evidence.basis
|
|
4869
|
+
}))
|
|
4870
|
+
.filter((evidence) => evidence.command);
|
|
4871
|
+
const suggestedCommands = uniqueStrings(commandSources.map((evidence) => evidence.command));
|
|
4872
|
+
recommendations.push({
|
|
4873
|
+
matched_rule: rule.id,
|
|
4874
|
+
confidence: rule.confidence,
|
|
4875
|
+
reason: rule.reason,
|
|
4876
|
+
fallback: rule.fallback,
|
|
4877
|
+
files: [...new Set(files)].sort(),
|
|
4878
|
+
suggested_commands: suggestedCommands,
|
|
4879
|
+
command_sources: commandSources
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
return recommendations;
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
function focusedSmokeScriptNames(scripts = {}) {
|
|
4886
|
+
return Object.keys(scripts)
|
|
4887
|
+
.filter((name) => /(?:^|[-:])(?:smoke|test)(?:$|[-:])/i.test(name) || /smoke/i.test(String(scripts[name] || '')))
|
|
4888
|
+
.sort();
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4891
|
+
function focusedSmokeScriptEvidenceForFiles(scripts = {}, files = []) {
|
|
4892
|
+
const candidates = focusedSmokeScriptNames(scripts);
|
|
4893
|
+
const selected = [];
|
|
4894
|
+
for (const file of files) {
|
|
4895
|
+
const stem = path.posix.basename(String(file || '').replaceAll('\\', '/')).replace(/\.[^.]+$/, '');
|
|
4896
|
+
const direct = candidates.find((name) => {
|
|
4897
|
+
const command = String(scripts[name] || '');
|
|
4898
|
+
return name.includes(stem)
|
|
4899
|
+
|| command.includes(stem)
|
|
4900
|
+
|| (stem === 'skill-installation' && /install.*smoke|smoke.*install/i.test(`${name} ${command}`))
|
|
4901
|
+
|| (['trust-kernel', 'ueye-artifacts'].includes(stem) && /cli.*smoke|smoke.*cli/i.test(`${name} ${command}`));
|
|
4902
|
+
});
|
|
4903
|
+
if (direct) selected.push({
|
|
4904
|
+
name: direct,
|
|
4905
|
+
files: [file],
|
|
4906
|
+
basis: 'focused-file-match'
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
if (!selected.length && candidates.length) {
|
|
4910
|
+
selected.push({
|
|
4911
|
+
name: candidates[0],
|
|
4912
|
+
files,
|
|
4913
|
+
basis: 'group-fallback'
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
return selected;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
function directScriptNamesForFile(scripts = {}, changedFile = '') {
|
|
4920
|
+
const normalized = String(changedFile || '').replaceAll('\\', '/');
|
|
4921
|
+
const basename = path.posix.basename(normalized);
|
|
4922
|
+
return Object.entries(scripts)
|
|
4923
|
+
.filter(([, command]) => {
|
|
4924
|
+
const text = String(command || '').replaceAll('\\', '/');
|
|
4925
|
+
return text.includes(normalized) || text.includes(`/${basename}`) || text.includes(` ${basename}`);
|
|
4926
|
+
})
|
|
4927
|
+
.map(([name]) => name)
|
|
4928
|
+
.sort();
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
function scriptCommandIfPresent(packageManager, scripts = {}, name = '') {
|
|
4932
|
+
return name && Object.prototype.hasOwnProperty.call(scripts, name)
|
|
4933
|
+
? runCommand(packageManager, name)
|
|
4934
|
+
: null;
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
function uniqueStrings(values = []) {
|
|
4938
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
|
|
4939
|
+
}
|
|
4940
|
+
|
|
4941
|
+
function buildVerificationCommandPlan(recommendations = []) {
|
|
4942
|
+
const commands = new Map<string, AnyRecord>();
|
|
4943
|
+
for (const recommendation of recommendations) {
|
|
4944
|
+
const sources = recommendation.command_sources?.length
|
|
4945
|
+
? recommendation.command_sources
|
|
4946
|
+
: (recommendation.suggested_commands || []).map((command) => ({
|
|
4947
|
+
command,
|
|
4948
|
+
files: recommendation.files || [],
|
|
4949
|
+
basis: 'recommendation-fallback'
|
|
4950
|
+
}));
|
|
4951
|
+
for (const source of sources) {
|
|
4952
|
+
const current = commands.get(source.command) || {
|
|
4953
|
+
command: source.command,
|
|
4954
|
+
matched_rules: [],
|
|
4955
|
+
files: [],
|
|
4956
|
+
provenance: []
|
|
4957
|
+
};
|
|
4958
|
+
current.matched_rules = uniqueStrings([...current.matched_rules, recommendation.matched_rule]);
|
|
4959
|
+
current.files = uniqueStrings([...current.files, ...(source.files || [])]).sort();
|
|
4960
|
+
current.provenance.push({
|
|
4961
|
+
matched_rule: recommendation.matched_rule,
|
|
4962
|
+
files: [...new Set(source.files || [])].sort(),
|
|
4963
|
+
basis: source.basis
|
|
4964
|
+
});
|
|
4965
|
+
commands.set(source.command, current);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
return [...commands.values()];
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4362
4971
|
function printDetection(result) {
|
|
4363
4972
|
console.log(`Project: ${result.dir}`);
|
|
4364
4973
|
console.log(`Package manager: ${result.packageManager}`);
|
|
@@ -4372,6 +4981,7 @@ function printDetection(result) {
|
|
|
4372
4981
|
console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
|
|
4373
4982
|
console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
|
|
4374
4983
|
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}`}`);
|
|
4984
|
+
printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
|
|
4375
4985
|
if (result.frameworkChecklist?.detected) {
|
|
4376
4986
|
console.log('');
|
|
4377
4987
|
console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
|
|
@@ -4381,6 +4991,29 @@ function printDetection(result) {
|
|
|
4381
4991
|
}
|
|
4382
4992
|
}
|
|
4383
4993
|
|
|
4994
|
+
function printChangedFileVerificationRecommendations(recommendations = [], commandPlan = []) {
|
|
4995
|
+
console.log('');
|
|
4996
|
+
console.log('Changed-file verification recommendations (advisory; commands were not run):');
|
|
4997
|
+
for (const recommendation of recommendations) {
|
|
4998
|
+
console.log(`- ${recommendation.matched_rule} [confidence: ${recommendation.confidence}]`);
|
|
4999
|
+
console.log(` files: ${recommendation.files.length ? recommendation.files.join(', ') : '(none)'}`);
|
|
5000
|
+
console.log(` reason: ${recommendation.reason}`);
|
|
5001
|
+
console.log(` commands: ${recommendation.suggested_commands.length ? recommendation.suggested_commands.join(' && ') : '(none detected)'}`);
|
|
5002
|
+
console.log(` fallback: ${recommendation.fallback}`);
|
|
5003
|
+
}
|
|
5004
|
+
console.log('');
|
|
5005
|
+
console.log('Verification command plan (deduplicated; advisory; commands were not run):');
|
|
5006
|
+
if (!commandPlan.length) {
|
|
5007
|
+
console.log('- (none detected)');
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
5010
|
+
for (const item of commandPlan) {
|
|
5011
|
+
console.log(`- ${item.command}`);
|
|
5012
|
+
console.log(` rules: ${item.matched_rules.join(', ')}`);
|
|
5013
|
+
console.log(` files: ${item.files.join(', ') || '(none)'}`);
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
|
|
4384
5017
|
function budget(routeArg = '') {
|
|
4385
5018
|
const normalized = normalizeRoute(routeArg);
|
|
4386
5019
|
const entries = normalized ? [[normalized, ROUTE_BUDGETS[normalized]]] : Object.entries(ROUTE_BUDGETS);
|
|
@@ -4768,16 +5401,40 @@ function findSensitivePattern(text = '') {
|
|
|
4768
5401
|
function showPath() {
|
|
4769
5402
|
console.log(`root: ${ROOT}`);
|
|
4770
5403
|
console.log(`skills: ${DEST}`);
|
|
4771
|
-
console.log(`codex mirror
|
|
5404
|
+
console.log(`codex mirror (preserved without an ownership receipt): ${CODEX_MIRROR}`);
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
function showRequestedHelp(args: string[]) {
|
|
5408
|
+
if (!args.some((arg) => arg === '--help' || arg === '-h')) return false;
|
|
5409
|
+
|
|
5410
|
+
const commandUsage: Record<string, () => void> = {
|
|
5411
|
+
context: contextUsage,
|
|
5412
|
+
cleanup: cleanupUsage,
|
|
5413
|
+
'study-note': studyNoteUsage,
|
|
5414
|
+
tools: toolsUsage,
|
|
5415
|
+
hook: hookUsage,
|
|
5416
|
+
loop: loopUsage,
|
|
5417
|
+
ueye: ueyeUsage,
|
|
5418
|
+
media: mediaUsage,
|
|
5419
|
+
runtime: runtimeUsage,
|
|
5420
|
+
mission: missionUsage,
|
|
5421
|
+
update: updateUsage,
|
|
5422
|
+
benchmark: benchmarkUsage,
|
|
5423
|
+
memory: memoryUsage
|
|
5424
|
+
};
|
|
5425
|
+
(commandUsage[args[0]] || usage)();
|
|
5426
|
+
return true;
|
|
4772
5427
|
}
|
|
4773
5428
|
|
|
4774
5429
|
async function main() {
|
|
4775
|
-
const
|
|
4776
|
-
|
|
4777
|
-
if (
|
|
4778
|
-
if (command === '
|
|
5430
|
+
const args = process.argv.slice(2);
|
|
5431
|
+
const command = args[0] || 'help';
|
|
5432
|
+
if (showRequestedHelp(args)) return;
|
|
5433
|
+
if (command === 'help') return usage();
|
|
5434
|
+
if (command === 'install') return install(args.slice(1));
|
|
5435
|
+
if (command === 'uninstall') return uninstall(args.slice(1));
|
|
4779
5436
|
if (command === 'version') return console.log(VERSION);
|
|
4780
|
-
if (command === 'detect') return
|
|
5437
|
+
if (command === 'detect') return detectCommand(args.slice(1));
|
|
4781
5438
|
if (command === 'pack') return inspectProjectPack(process.argv[3]);
|
|
4782
5439
|
if (command === 'context') return context(process.argv.slice(3));
|
|
4783
5440
|
if (command === 'cleanup') return cleanup(process.argv.slice(3));
|
|
@@ -4791,6 +5448,7 @@ async function main() {
|
|
|
4791
5448
|
if (command === 'media') return media(process.argv.slice(3));
|
|
4792
5449
|
if (command === 'runtime') return runtime(process.argv.slice(3));
|
|
4793
5450
|
if (command === 'mission') return mission(process.argv.slice(3));
|
|
5451
|
+
if (command === 'update') return updateCommand(process.argv.slice(3));
|
|
4794
5452
|
if (command === 'benchmark') return benchmark(process.argv.slice(3));
|
|
4795
5453
|
if (command === 'release') return release(process.argv.slice(3));
|
|
4796
5454
|
if (command === 'safety') return safety(process.argv.slice(3));
|