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/dist/bin/yam.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import fsp from 'node:fs/promises';
|
|
5
6
|
import { createRequire } from 'node:module';
|
|
@@ -9,7 +10,8 @@ import process from 'node:process';
|
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import { TRUTH_STATUSES, buildLoopReport, buildMediaGenerationProof, buildMissionCompletionGate, buildMissionPatchEnvelope, buildMissionSubagentReceipt, buildRollbackHint, buildRuntimeBackendEvidence, buildStudyNote, buildUeyeDesignCompletionGate, buildUeyeVisualProvenance, buildUeyeRunReport, buildUeyeSurfaceContext, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
|
|
11
12
|
import { archiveUeyeRevision, inspectImageFile, upsertUeyeAsset, verifyUeyeAssetManifest, verifyUeyeRevisionHistory } from '../lib/ueye-artifacts.js';
|
|
12
|
-
import {
|
|
13
|
+
import { inspectSkillInstallation, installSkillSetTransactional, uninstallSkillSetSafely } from '../lib/skill-installation.js';
|
|
14
|
+
import { EXTERNAL_UPDATE_COMPONENTS, applyExternalUpdates, checkExternalUpdates } from '../lib/external-updates.js';
|
|
13
15
|
function errorMessage(error) {
|
|
14
16
|
return error instanceof Error ? error.message : String(error);
|
|
15
17
|
}
|
|
@@ -50,6 +52,13 @@ const VERSION = String(PACKAGE_JSON.version || '0.0.0');
|
|
|
50
52
|
const PROJECT_PACK = 'yam.project.md';
|
|
51
53
|
const LEGACY_PROJECT_PACK = 'timeto.project.md';
|
|
52
54
|
const PACK_STALE_DAYS = 30;
|
|
55
|
+
const INSTRUCTION_DUPLICATION_LIMITS = {
|
|
56
|
+
maxFiles: 24,
|
|
57
|
+
maxBytesPerFile: 128000,
|
|
58
|
+
maxGroups: 8,
|
|
59
|
+
maxDepth: 6,
|
|
60
|
+
minDirectiveChars: 40
|
|
61
|
+
};
|
|
53
62
|
const YAM_HOOK_ENTRYPOINT = path.join(ROOT, 'dist', 'bin', 'yam.js');
|
|
54
63
|
const YAM_LITE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run lite`;
|
|
55
64
|
const YAM_STUDY_NOTE_HOOK_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(YAM_HOOK_ENTRYPOINT)} hook run study-note`;
|
|
@@ -116,7 +125,7 @@ Usage:
|
|
|
116
125
|
yam list
|
|
117
126
|
yam status
|
|
118
127
|
yam verify
|
|
119
|
-
yam detect [dir]
|
|
128
|
+
yam detect [dir] [--json]
|
|
120
129
|
yam pack [dir]
|
|
121
130
|
yam context pressure [dir] [--json]
|
|
122
131
|
yam cleanup scan [dir] [--json]
|
|
@@ -138,6 +147,9 @@ Usage:
|
|
|
138
147
|
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
139
148
|
yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
|
|
140
149
|
yam mission gate [--expected-thread id] [--receipt file] [--json]
|
|
150
|
+
yam update check [--json]
|
|
151
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
152
|
+
yam update apply --all [--json]
|
|
141
153
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
142
154
|
yam release report [--json]
|
|
143
155
|
yam safety [text...]
|
|
@@ -145,7 +157,7 @@ Usage:
|
|
|
145
157
|
yam hook <status|enable|disable|run> [lite|study-note] [--global|--project dir]
|
|
146
158
|
yam template <project|ueye|mission|proof|tuning>
|
|
147
159
|
yam tune-log [dir]
|
|
148
|
-
yam install
|
|
160
|
+
yam install [--replace-user-skill name]
|
|
149
161
|
yam uninstall
|
|
150
162
|
yam doctor [--json]
|
|
151
163
|
yam examples
|
|
@@ -169,7 +181,32 @@ async function exists(file) {
|
|
|
169
181
|
async function rmrf(target) {
|
|
170
182
|
await fsp.rm(target, { recursive: true, force: true });
|
|
171
183
|
}
|
|
172
|
-
|
|
184
|
+
function installReplacementSkills(args = []) {
|
|
185
|
+
const skills = [];
|
|
186
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
187
|
+
const arg = String(args[index] || '');
|
|
188
|
+
if (arg === '--replace-user-skill') {
|
|
189
|
+
const skill = String(args[index + 1] || '');
|
|
190
|
+
if (!skill || skill.startsWith('--')) {
|
|
191
|
+
throw new Error('missing skill name after --replace-user-skill');
|
|
192
|
+
}
|
|
193
|
+
skills.push(skill);
|
|
194
|
+
index += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (arg.startsWith('--replace-user-skill=')) {
|
|
198
|
+
const skill = arg.slice('--replace-user-skill='.length);
|
|
199
|
+
if (!skill)
|
|
200
|
+
throw new Error('missing skill name after --replace-user-skill=');
|
|
201
|
+
skills.push(skill);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`unknown yam install argument: ${arg}`);
|
|
205
|
+
}
|
|
206
|
+
return [...new Set(skills)];
|
|
207
|
+
}
|
|
208
|
+
async function install(args = []) {
|
|
209
|
+
const replaceSkills = installReplacementSkills(args);
|
|
173
210
|
const result = await installSkillSetTransactional({
|
|
174
211
|
sourceRoot: ROOT,
|
|
175
212
|
destination: DEST,
|
|
@@ -177,6 +214,7 @@ async function install() {
|
|
|
177
214
|
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
178
215
|
version: VERSION,
|
|
179
216
|
skills: SKILLS,
|
|
217
|
+
replaceSkills,
|
|
180
218
|
legacySkills: LEGACY_SKILLS,
|
|
181
219
|
retiredSkills: RETIRED_SKILLS
|
|
182
220
|
});
|
|
@@ -187,21 +225,22 @@ async function install() {
|
|
|
187
225
|
console.warn(`warning: ${warning}`);
|
|
188
226
|
console.log('Restart Codex to reload skills.');
|
|
189
227
|
}
|
|
190
|
-
async function uninstall() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
await rmrf(path.join(DEST, INSTALL_RECEIPT_NAME));
|
|
228
|
+
async function uninstall(args = []) {
|
|
229
|
+
if (args.length)
|
|
230
|
+
throw new Error(`unknown yam uninstall argument: ${args[0]}`);
|
|
231
|
+
const result = await uninstallSkillSetSafely({
|
|
232
|
+
destination: DEST,
|
|
233
|
+
codexMirror: CODEX_MIRROR,
|
|
234
|
+
packageName: String(PACKAGE_JSON.name || 'yam-flow'),
|
|
235
|
+
skills: SKILLS
|
|
236
|
+
});
|
|
202
237
|
console.log(`yam removed from ${DEST}`);
|
|
203
|
-
|
|
204
|
-
|
|
238
|
+
console.log(`removed receipt: ${result.receiptPath}`);
|
|
239
|
+
for (const warning of result.cleanupWarnings)
|
|
240
|
+
console.warn(`warning: ${warning}`);
|
|
241
|
+
if (CODEX_MIRROR !== DEST) {
|
|
242
|
+
console.log(`unowned legacy, retired, and Codex mirror entries were preserved`);
|
|
243
|
+
}
|
|
205
244
|
console.log('Restart Codex to unload skills.');
|
|
206
245
|
}
|
|
207
246
|
async function status({ quiet = false } = {}) {
|
|
@@ -397,12 +436,21 @@ async function doctor(args = []) {
|
|
|
397
436
|
console.log('yam doctor: ok');
|
|
398
437
|
console.log('No hooks, automations, or global config are required.');
|
|
399
438
|
console.log('Skill install state is reported by `yam status`.');
|
|
439
|
+
if (report.preservedUnprovenSkillEntries.length) {
|
|
440
|
+
console.log(`Preserved unproven legacy/retired/mirror entries: ${report.preservedUnprovenSkillEntries.length}`);
|
|
441
|
+
}
|
|
400
442
|
console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
|
|
401
443
|
return;
|
|
402
444
|
}
|
|
403
445
|
console.log('yam doctor: issues');
|
|
404
446
|
for (const issue of report.issues)
|
|
405
447
|
console.log(`- ${issue}`);
|
|
448
|
+
if (report.preservedUnprovenSkillEntries.length) {
|
|
449
|
+
console.log('');
|
|
450
|
+
console.log('Preserved unproven skill entries:');
|
|
451
|
+
for (const entry of report.preservedUnprovenSkillEntries)
|
|
452
|
+
console.log(`- ${entry}`);
|
|
453
|
+
}
|
|
406
454
|
if (report.nextActions.length) {
|
|
407
455
|
console.log('');
|
|
408
456
|
console.log('Next actions:');
|
|
@@ -413,6 +461,7 @@ async function doctor(args = []) {
|
|
|
413
461
|
}
|
|
414
462
|
async function buildDoctorReport() {
|
|
415
463
|
const issues = [];
|
|
464
|
+
const preservedUnprovenSkillEntries = [];
|
|
416
465
|
for (const skill of SKILLS) {
|
|
417
466
|
if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md')))
|
|
418
467
|
issues.push(`missing source skill: ${skill}`);
|
|
@@ -426,17 +475,22 @@ async function buildDoctorReport() {
|
|
|
426
475
|
if (await exists(path.join(os.homedir(), '.codex', 'automations', 'timeto')))
|
|
427
476
|
issues.push('unexpected legacy timeto automation');
|
|
428
477
|
for (const legacySkill of LEGACY_SKILLS) {
|
|
429
|
-
if (await exists(path.join(DEST, legacySkill, 'SKILL.md')))
|
|
430
|
-
|
|
431
|
-
if (CODEX_MIRROR !== DEST && await exists(path.join(CODEX_MIRROR, legacySkill, 'SKILL.md'))) {
|
|
432
|
-
issues.push(`unexpected legacy mirror skill: ${legacySkill}`);
|
|
478
|
+
if (await exists(path.join(DEST, legacySkill, 'SKILL.md'))) {
|
|
479
|
+
preservedUnprovenSkillEntries.push(`legacy-named destination entry: ${legacySkill}`);
|
|
433
480
|
}
|
|
434
481
|
}
|
|
435
482
|
for (const retiredSkill of RETIRED_SKILLS) {
|
|
436
|
-
if (await exists(path.join(DEST, retiredSkill, 'SKILL.md')))
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
483
|
+
if (await exists(path.join(DEST, retiredSkill, 'SKILL.md'))) {
|
|
484
|
+
preservedUnprovenSkillEntries.push(`retired-name destination entry: ${retiredSkill}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (CODEX_MIRROR !== DEST) {
|
|
488
|
+
const mirrorSkillNames = [...new Set([...SKILLS, ...LEGACY_SKILLS, ...RETIRED_SKILLS])];
|
|
489
|
+
for (const skill of mirrorSkillNames) {
|
|
490
|
+
if (!await exists(path.join(CODEX_MIRROR, skill, 'SKILL.md')))
|
|
491
|
+
continue;
|
|
492
|
+
const nameKind = SKILLS.includes(skill) ? 'active-name' : LEGACY_SKILLS.includes(skill) ? 'legacy-named' : 'retired-name';
|
|
493
|
+
preservedUnprovenSkillEntries.push(`${nameKind} Codex mirror entry: ${skill}`);
|
|
440
494
|
}
|
|
441
495
|
}
|
|
442
496
|
const verifyIssues = await verify({ quiet: true });
|
|
@@ -449,6 +503,7 @@ async function buildDoctorReport() {
|
|
|
449
503
|
generatedAt: new Date().toISOString(),
|
|
450
504
|
ok: issues.length === 0,
|
|
451
505
|
issues,
|
|
506
|
+
preservedUnprovenSkillEntries,
|
|
452
507
|
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
453
508
|
nextActionDetails,
|
|
454
509
|
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
@@ -468,8 +523,6 @@ function doctorNextActionDetails(issues = []) {
|
|
|
468
523
|
actions.push(nextActionDetail('remove-project-hook', 'warning', issue, 'remove project hooks unless this repo is intentionally dogfooding them', 'inspect .codex/hooks.json and remove only intentional stale hooks'));
|
|
469
524
|
else if (/unexpected .*automation/i.test(issue))
|
|
470
525
|
actions.push(nextActionDetail('remove-stale-automation', 'warning', issue, 'remove stale yam/timeto automations before claiming clean install state', 'inspect ~/.codex/automations and remove stale entries manually'));
|
|
471
|
-
else if (/legacy|retired/i.test(issue))
|
|
472
|
-
actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
|
|
473
526
|
else if (/verify reported/i.test(issue))
|
|
474
527
|
actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
|
|
475
528
|
}
|
|
@@ -759,19 +812,24 @@ async function studyNoteCheck(args = []) {
|
|
|
759
812
|
process.exitCode = 1;
|
|
760
813
|
}
|
|
761
814
|
async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'not_provided') {
|
|
762
|
-
const
|
|
763
|
-
const
|
|
815
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
816
|
+
const changedFiles = changedFileDetection.files;
|
|
817
|
+
const changeScopeAvailable = changedFileDetection.available;
|
|
818
|
+
const needsStudyNote = changeScopeAvailable && changedFiles.length > 0;
|
|
819
|
+
const scopeAwareStatus = (passes) => changeScopeAvailable ? (!needsStudyNote || passes ? 'pass' : 'fail') : 'partial';
|
|
764
820
|
const hygieneRequirements = studyNoteHygieneRequirements(changedFiles);
|
|
765
821
|
const checks = [
|
|
766
|
-
studyNoteGuardCheck('changed_files', needsStudyNote ? 'pass' : 'skipped'
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
studyNoteGuardCheck('
|
|
770
|
-
studyNoteGuardCheck('
|
|
771
|
-
studyNoteGuardCheck('
|
|
772
|
-
studyNoteGuardCheck('
|
|
773
|
-
studyNoteGuardCheck('
|
|
774
|
-
studyNoteGuardCheck('
|
|
822
|
+
studyNoteGuardCheck('changed_files', changeScopeAvailable ? (needsStudyNote ? 'pass' : 'skipped') : 'partial', changeScopeAvailable
|
|
823
|
+
? (needsStudyNote ? 'changed files detected; Study Note is required before final completion' : 'no changed files detected')
|
|
824
|
+
: 'Git changed-file scope is unavailable; do not treat this project as clean and determine Study Note need manually'),
|
|
825
|
+
studyNoteGuardCheck('study_note_present', scopeAwareStatus(hasStudyNoteMarker(reportText)), 'final report should include a Study Note when project artifacts changed'),
|
|
826
|
+
studyNoteGuardCheck('role_or_responsibility', scopeAwareStatus(/(\brole\b|\bresponsib|\bdoes\b|역할|기능)/i.test(reportText)), 'Study Note should explain what the touched code/artifact does'),
|
|
827
|
+
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'),
|
|
828
|
+
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'),
|
|
829
|
+
studyNoteGuardCheck('expected_behavior', scopeAwareStatus(/(expected|should|will|result|behavior|예상|기대|결과|동작|되어야|하게 됨)/i.test(reportText)), 'Study Note should describe the expected behavior or result'),
|
|
830
|
+
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'),
|
|
831
|
+
studyNoteGuardCheck('verification', scopeAwareStatus(/(verification|verified|checked|tested|검증|확인|테스트)/i.test(reportText)), 'Study Note should say what was checked'),
|
|
832
|
+
studyNoteGuardCheck('limits_or_uncertainty', scopeAwareStatus(/(limits?|uncertain|unknown|not checked|remaining|한계|불확실|모르는|미확인|남은)/i.test(reportText)), 'Study Note should say what remains uncertain or explicitly state that no meaningful uncertainty remains')
|
|
775
833
|
];
|
|
776
834
|
for (const requirement of hygieneRequirements) {
|
|
777
835
|
checks.push(studyNoteGuardCheck(requirement.id, requirementSatisfied(reportText, requirement), requirement.note));
|
|
@@ -781,13 +839,18 @@ async function buildStudyNoteGuardResult(dir, reportText = '', reportSource = 'n
|
|
|
781
839
|
schema: 'yam.study-note-guard.v1',
|
|
782
840
|
generated_at: new Date().toISOString(),
|
|
783
841
|
project: dir,
|
|
842
|
+
changed_file_detection: changedFileDetection,
|
|
784
843
|
changed_files: changedFiles,
|
|
785
844
|
changed_file_count: changedFiles.length,
|
|
786
845
|
report_source: reportSource,
|
|
787
846
|
checks,
|
|
788
847
|
blockers,
|
|
789
|
-
next_action: blockers[0] || (
|
|
790
|
-
|
|
848
|
+
next_action: blockers[0] || (!changeScopeAvailable
|
|
849
|
+
? 'Git change scope is unavailable; inspect changed artifacts manually and include a Study Note when anything changed'
|
|
850
|
+
: needsStudyNote
|
|
851
|
+
? 'Study Note guard passed for the supplied report text'
|
|
852
|
+
: 'no Study Note required because no changed files were detected'),
|
|
853
|
+
truth_status: blockers.length ? 'blocked' : !changeScopeAvailable ? 'partial' : needsStudyNote ? 'verified' : 'skipped'
|
|
791
854
|
};
|
|
792
855
|
return result;
|
|
793
856
|
}
|
|
@@ -805,14 +868,38 @@ function firstPositional(args = []) {
|
|
|
805
868
|
}
|
|
806
869
|
return '';
|
|
807
870
|
}
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
871
|
+
function gitChangedFileSnapshot(dir) {
|
|
872
|
+
const result = spawnSync('git', ['status', '--short', '-z', '--untracked-files=all'], {
|
|
873
|
+
cwd: dir,
|
|
874
|
+
encoding: 'utf8',
|
|
875
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
876
|
+
timeout: 30000
|
|
877
|
+
});
|
|
878
|
+
if (result.status !== 0) {
|
|
879
|
+
return {
|
|
880
|
+
available: false,
|
|
881
|
+
source: 'git status --short -z --untracked-files=all',
|
|
882
|
+
files: []
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const records = String(result.stdout || '').split('\0');
|
|
886
|
+
const files = [];
|
|
887
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
888
|
+
const record = records[index];
|
|
889
|
+
if (!record || record.length < 4)
|
|
890
|
+
continue;
|
|
891
|
+
const status = record.slice(0, 2);
|
|
892
|
+
const file = record.slice(3);
|
|
893
|
+
if (/[RC]/.test(status))
|
|
894
|
+
index += 1;
|
|
895
|
+
if (file && !file.startsWith('dist/') && !file.endsWith('.tgz'))
|
|
896
|
+
files.push(file);
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
available: true,
|
|
900
|
+
source: 'git status --short -z --untracked-files=all',
|
|
901
|
+
files
|
|
902
|
+
};
|
|
816
903
|
}
|
|
817
904
|
async function readStudyNoteReportText(flags, dir) {
|
|
818
905
|
if (flags.text)
|
|
@@ -902,12 +989,17 @@ async function buildCleanupScanReport(targetDir = process.cwd()) {
|
|
|
902
989
|
for (const artifact of await staleYamArtifacts(dir)) {
|
|
903
990
|
findings.push(cleanupFinding('low', artifact.file, artifact.reason, 'archive or refresh only if it misleads the current work'));
|
|
904
991
|
}
|
|
992
|
+
const instructionDuplicationBudget = await buildInstructionDuplicationBudget(dir);
|
|
993
|
+
if (instructionDuplicationBudget.duplicate_group_count > 0) {
|
|
994
|
+
findings.push(cleanupFinding('low', 'AGENTS.md/SKILL.md directive duplication budget', `${instructionDuplicationBudget.duplicate_group_count} normalized directive group(s) repeat across separate instruction surfaces; exact normalized matches are advisory and may be intentional`, 'review the reported source files and consolidate only repetition that causes drift or conflicting maintenance'));
|
|
995
|
+
}
|
|
905
996
|
return {
|
|
906
997
|
schema: 'yam.cleanup-scan.v1',
|
|
907
998
|
generated_at: new Date().toISOString(),
|
|
908
999
|
project: dir,
|
|
909
1000
|
destructive: false,
|
|
910
1001
|
findings,
|
|
1002
|
+
instruction_duplication_budget: instructionDuplicationBudget,
|
|
911
1003
|
risk_level: overallCleanupRisk(findings),
|
|
912
1004
|
safe_next_action: findings.length ? 'review findings and make explicit user-approved cleanup decisions' : 'no cleanup action needed',
|
|
913
1005
|
truth_status: 'partial'
|
|
@@ -961,6 +1053,162 @@ async function staleYamArtifacts(dir) {
|
|
|
961
1053
|
}
|
|
962
1054
|
return artifacts;
|
|
963
1055
|
}
|
|
1056
|
+
async function buildInstructionDuplicationBudget(rootDir = process.cwd(), limits = INSTRUCTION_DUPLICATION_LIMITS) {
|
|
1057
|
+
const root = path.resolve(rootDir || process.cwd());
|
|
1058
|
+
const inventory = await collectInstructionFiles(root, limits);
|
|
1059
|
+
const directives = new Map();
|
|
1060
|
+
let candidateDirectiveCount = 0;
|
|
1061
|
+
for (const relativeFile of inventory.files) {
|
|
1062
|
+
const absolute = path.join(root, relativeFile);
|
|
1063
|
+
const text = await readBoundedUtf8File(absolute, limits.maxBytesPerFile);
|
|
1064
|
+
for (const directive of instructionDirectives(text, relativeFile, limits.minDirectiveChars)) {
|
|
1065
|
+
candidateDirectiveCount += 1;
|
|
1066
|
+
directives.set(directive.normalized, [...(directives.get(directive.normalized) || []), directive]);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const allGroups = [...directives.entries()]
|
|
1070
|
+
.map(([normalized, occurrences]) => {
|
|
1071
|
+
const surfaces = [...new Set(occurrences.map((item) => item.file))];
|
|
1072
|
+
if (surfaces.length < 2)
|
|
1073
|
+
return null;
|
|
1074
|
+
return {
|
|
1075
|
+
directive_digest: createHash('sha256').update(normalized).digest('hex').slice(0, 16),
|
|
1076
|
+
directive_preview: redactSensitiveText(String(occurrences[0]?.text || '')).slice(0, 180),
|
|
1077
|
+
occurrence_count: occurrences.length,
|
|
1078
|
+
surface_count: surfaces.length,
|
|
1079
|
+
surfaces,
|
|
1080
|
+
occurrences: occurrences.slice(0, 8).map((item) => ({
|
|
1081
|
+
file: item.file,
|
|
1082
|
+
line: item.line
|
|
1083
|
+
}))
|
|
1084
|
+
};
|
|
1085
|
+
})
|
|
1086
|
+
.filter(Boolean)
|
|
1087
|
+
.sort((left, right) => (right.surface_count - left.surface_count
|
|
1088
|
+
|| right.occurrence_count - left.occurrence_count
|
|
1089
|
+
|| left.directive_digest.localeCompare(right.directive_digest)));
|
|
1090
|
+
const duplicateGroups = allGroups.slice(0, limits.maxGroups);
|
|
1091
|
+
return {
|
|
1092
|
+
schema: 'yam.instruction-duplication-budget.v1',
|
|
1093
|
+
scope: 'project AGENTS.md and SKILL.md files only',
|
|
1094
|
+
advisory_only: true,
|
|
1095
|
+
hard_gate: false,
|
|
1096
|
+
destructive: false,
|
|
1097
|
+
normalization: 'exact normalized markdown directive text; no semantic similarity inference',
|
|
1098
|
+
limits: {
|
|
1099
|
+
max_files: limits.maxFiles,
|
|
1100
|
+
max_bytes_per_file: limits.maxBytesPerFile,
|
|
1101
|
+
max_groups: limits.maxGroups,
|
|
1102
|
+
max_depth: limits.maxDepth,
|
|
1103
|
+
min_directive_chars: limits.minDirectiveChars
|
|
1104
|
+
},
|
|
1105
|
+
files_scanned: inventory.files.length,
|
|
1106
|
+
scanned_files: inventory.files,
|
|
1107
|
+
candidate_directive_count: candidateDirectiveCount,
|
|
1108
|
+
duplicate_group_count: allGroups.length,
|
|
1109
|
+
duplicate_groups: duplicateGroups,
|
|
1110
|
+
file_limit_reached: inventory.fileLimitReached,
|
|
1111
|
+
group_limit_reached: allGroups.length > duplicateGroups.length,
|
|
1112
|
+
truth_status: 'partial',
|
|
1113
|
+
note: allGroups.length
|
|
1114
|
+
? 'repetition may be intentional; review before consolidating'
|
|
1115
|
+
: 'no cross-surface exact normalized directive repetition found within the bounded scan'
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async function collectInstructionFiles(root, limits) {
|
|
1119
|
+
const files = [];
|
|
1120
|
+
let fileLimitReached = false;
|
|
1121
|
+
async function walk(current, depth) {
|
|
1122
|
+
if (depth > limits.maxDepth || fileLimitReached)
|
|
1123
|
+
return;
|
|
1124
|
+
let entries = [];
|
|
1125
|
+
try {
|
|
1126
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1132
|
+
for (const entry of entries) {
|
|
1133
|
+
const absolute = path.join(current, entry.name);
|
|
1134
|
+
const relative = path.relative(root, absolute);
|
|
1135
|
+
if (shouldSkipScanPath(relative, entry.name) || entry.isSymbolicLink())
|
|
1136
|
+
continue;
|
|
1137
|
+
if (entry.isDirectory()) {
|
|
1138
|
+
await walk(absolute, depth + 1);
|
|
1139
|
+
if (fileLimitReached)
|
|
1140
|
+
return;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (!entry.isFile() || !['AGENTS.md', 'SKILL.md'].includes(entry.name))
|
|
1144
|
+
continue;
|
|
1145
|
+
if (files.length >= limits.maxFiles) {
|
|
1146
|
+
fileLimitReached = true;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
files.push(relative);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
await walk(root, 0);
|
|
1153
|
+
return { files, fileLimitReached };
|
|
1154
|
+
}
|
|
1155
|
+
async function readBoundedUtf8File(file, maxBytes) {
|
|
1156
|
+
const handle = await fsp.open(file, 'r');
|
|
1157
|
+
try {
|
|
1158
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1159
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
1160
|
+
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
1161
|
+
}
|
|
1162
|
+
finally {
|
|
1163
|
+
await handle.close();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function instructionDirectives(text = '', relativeFile = '', minChars = 40) {
|
|
1167
|
+
const directives = [];
|
|
1168
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
1169
|
+
let inFence = false;
|
|
1170
|
+
let inFrontmatter = lines[0]?.trim() === '---';
|
|
1171
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1172
|
+
const raw = lines[index];
|
|
1173
|
+
const trimmed = raw.trim();
|
|
1174
|
+
if (index === 0 && inFrontmatter)
|
|
1175
|
+
continue;
|
|
1176
|
+
if (inFrontmatter) {
|
|
1177
|
+
if (trimmed === '---')
|
|
1178
|
+
inFrontmatter = false;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (/^(```|~~~)/.test(trimmed)) {
|
|
1182
|
+
inFence = !inFence;
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (inFence || !trimmed || /^#{1,6}\s/.test(trimmed))
|
|
1186
|
+
continue;
|
|
1187
|
+
const match = trimmed.match(/^(?:[-*+]|\d+[.)])\s+(.+)$/);
|
|
1188
|
+
if (!match)
|
|
1189
|
+
continue;
|
|
1190
|
+
const normalized = normalizeInstructionDirective(match[1]);
|
|
1191
|
+
if (normalized.length < minChars)
|
|
1192
|
+
continue;
|
|
1193
|
+
directives.push({
|
|
1194
|
+
file: relativeFile,
|
|
1195
|
+
line: index + 1,
|
|
1196
|
+
text: match[1].trim().slice(0, 240),
|
|
1197
|
+
normalized
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
return directives;
|
|
1201
|
+
}
|
|
1202
|
+
function normalizeInstructionDirective(value = '') {
|
|
1203
|
+
return String(value || '')
|
|
1204
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
1205
|
+
.replace(/[`*_~]/g, '')
|
|
1206
|
+
.replace(/<[^>]+>/g, ' ')
|
|
1207
|
+
.replace(/\s+/g, ' ')
|
|
1208
|
+
.replace(/[.;:,!?。,、;:!?]+$/g, '')
|
|
1209
|
+
.trim()
|
|
1210
|
+
.toLowerCase();
|
|
1211
|
+
}
|
|
964
1212
|
function printCleanupScanReport(report) {
|
|
965
1213
|
console.log('yam cleanup scan');
|
|
966
1214
|
console.log(`Project: ${report.project}`);
|
|
@@ -1830,10 +2078,16 @@ async function buildYamLiteContext({ cwd, prompt }) {
|
|
|
1830
2078
|
}
|
|
1831
2079
|
async function buildStudyNoteHookContext({ cwd }) {
|
|
1832
2080
|
const dir = path.resolve(cwd || process.cwd());
|
|
1833
|
-
const
|
|
2081
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
2082
|
+
const changedFiles = changedFileDetection.files;
|
|
1834
2083
|
const lines = [
|
|
1835
2084
|
'yam Study Note guard active: if this turn changes code, config, release metadata, docs, or project artifacts, include a Study Note in the final response.'
|
|
1836
2085
|
];
|
|
2086
|
+
if (!changedFileDetection.available) {
|
|
2087
|
+
lines.push('Git changed-file scope is unavailable at prompt time; do not treat this as a clean project.');
|
|
2088
|
+
lines.push('Determine manually whether artifacts changed and include the Study Note when they did; the Stop hook will keep this uncertainty visible.');
|
|
2089
|
+
return lines.join('\n');
|
|
2090
|
+
}
|
|
1837
2091
|
if (!changedFiles.length) {
|
|
1838
2092
|
lines.push('No changed files were detected at prompt time; if you change artifacts during this turn, add the Study Note before final.');
|
|
1839
2093
|
lines.push('The Study Note Stop hook will check the final response if artifacts are changed later in the turn.');
|
|
@@ -1852,6 +2106,12 @@ async function buildStudyNoteHookContext({ cwd }) {
|
|
|
1852
2106
|
async function buildStudyNoteStopOutput(input = {}, cwd = process.cwd()) {
|
|
1853
2107
|
const lastAssistantMessage = String(input?.last_assistant_message || input?.lastAssistantMessage || '');
|
|
1854
2108
|
const result = await buildStudyNoteGuardResult(path.resolve(cwd), lastAssistantMessage, 'stop_hook_last_assistant_message');
|
|
2109
|
+
if (result.truth_status === 'partial' && !result.changed_file_detection.available) {
|
|
2110
|
+
return {
|
|
2111
|
+
continue: true,
|
|
2112
|
+
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.'
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
1855
2115
|
if (!result.blockers.length)
|
|
1856
2116
|
return { continue: true };
|
|
1857
2117
|
const summary = result.blockers.slice(0, 4).join('; ');
|
|
@@ -3300,6 +3560,86 @@ async function mission(args = []) {
|
|
|
3300
3560
|
console.error(`unknown mission command: ${subcommand}`);
|
|
3301
3561
|
return missionUsage();
|
|
3302
3562
|
}
|
|
3563
|
+
async function updateCommand(args = []) {
|
|
3564
|
+
const subcommand = args[0] || 'help';
|
|
3565
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
3566
|
+
return updateUsage();
|
|
3567
|
+
if (subcommand === 'check') {
|
|
3568
|
+
const rest = args.slice(1);
|
|
3569
|
+
const invalid = rest.filter((arg) => arg !== '--json');
|
|
3570
|
+
if (invalid.length || rest.filter((arg) => arg === '--json').length > 1) {
|
|
3571
|
+
throw new Error(`invalid update check option: ${invalid[0] || '--json repeated'}`);
|
|
3572
|
+
}
|
|
3573
|
+
const report = await checkExternalUpdates(VERSION);
|
|
3574
|
+
printJsonOrHuman(report, rest.includes('--json'), 'External update check');
|
|
3575
|
+
if (!report.success)
|
|
3576
|
+
process.exitCode = 1;
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
if (subcommand !== 'apply') {
|
|
3580
|
+
throw new Error(`unknown update command: ${subcommand}`);
|
|
3581
|
+
}
|
|
3582
|
+
const parsed = parseUpdateApplyArgs(args.slice(1));
|
|
3583
|
+
const report = await applyExternalUpdates(VERSION, {
|
|
3584
|
+
component: parsed.component,
|
|
3585
|
+
all: parsed.all
|
|
3586
|
+
});
|
|
3587
|
+
printJsonOrHuman(report, parsed.json, 'External update apply');
|
|
3588
|
+
if (!report.success)
|
|
3589
|
+
process.exitCode = 1;
|
|
3590
|
+
}
|
|
3591
|
+
function parseUpdateApplyArgs(args) {
|
|
3592
|
+
let component;
|
|
3593
|
+
let all = false;
|
|
3594
|
+
let json = false;
|
|
3595
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3596
|
+
const arg = String(args[index] || '');
|
|
3597
|
+
if (arg === '--json') {
|
|
3598
|
+
if (json)
|
|
3599
|
+
throw new Error('--json may be supplied only once');
|
|
3600
|
+
json = true;
|
|
3601
|
+
continue;
|
|
3602
|
+
}
|
|
3603
|
+
if (arg === '--all') {
|
|
3604
|
+
if (all)
|
|
3605
|
+
throw new Error('--all may be supplied only once');
|
|
3606
|
+
all = true;
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
if (arg === '--component' || arg.startsWith('--component=')) {
|
|
3610
|
+
if (component)
|
|
3611
|
+
throw new Error('--component may be supplied only once');
|
|
3612
|
+
const value = arg === '--component' ? String(args[index + 1] || '') : arg.slice('--component='.length);
|
|
3613
|
+
if (arg === '--component')
|
|
3614
|
+
index += 1;
|
|
3615
|
+
if (!EXTERNAL_UPDATE_COMPONENTS.includes(value)) {
|
|
3616
|
+
throw new Error('component must be one of: yam, scrapling, insane-search');
|
|
3617
|
+
}
|
|
3618
|
+
component = value;
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
throw new Error(`invalid update apply option: ${arg}`);
|
|
3622
|
+
}
|
|
3623
|
+
if ((all && component) || (!all && !component)) {
|
|
3624
|
+
throw new Error('choose exactly one of --component yam|scrapling|insane-search or --all');
|
|
3625
|
+
}
|
|
3626
|
+
return { component, all, json };
|
|
3627
|
+
}
|
|
3628
|
+
function updateUsage() {
|
|
3629
|
+
console.log(`yam update
|
|
3630
|
+
|
|
3631
|
+
Usage:
|
|
3632
|
+
yam update check [--json]
|
|
3633
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
3634
|
+
yam update apply --all [--json]
|
|
3635
|
+
|
|
3636
|
+
Safety:
|
|
3637
|
+
check is read-only. apply requires an explicit component or --all.
|
|
3638
|
+
--all updates Scrapling and Insane Search before yam, then stops at the first failed/manual component.
|
|
3639
|
+
Insane Search is updated only through the official Codex CLI; plugin caches are never edited directly.
|
|
3640
|
+
Every apply attempt records component checks, outcome, side effects, and rollback guidance.
|
|
3641
|
+
`);
|
|
3642
|
+
}
|
|
3303
3643
|
function missionUsage() {
|
|
3304
3644
|
console.log(`yam mission
|
|
3305
3645
|
|
|
@@ -4341,7 +4681,10 @@ function pickScript(scripts = {}, groups = []) {
|
|
|
4341
4681
|
async function detectProject(targetDir = process.cwd(), { quiet = false } = {}) {
|
|
4342
4682
|
const dir = path.resolve(targetDir || process.cwd());
|
|
4343
4683
|
const packageJson = path.join(dir, 'package.json');
|
|
4684
|
+
const changedFileDetection = gitChangedFileSnapshot(dir);
|
|
4685
|
+
const changedFiles = changedFileDetection.files;
|
|
4344
4686
|
const result = {
|
|
4687
|
+
schema: 'yam.project-detection.v1',
|
|
4345
4688
|
dir,
|
|
4346
4689
|
packageJson: await exists(packageJson),
|
|
4347
4690
|
packageManager: 'npm',
|
|
@@ -4352,12 +4695,26 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
4352
4695
|
test: null,
|
|
4353
4696
|
build: null
|
|
4354
4697
|
},
|
|
4355
|
-
frameworkChecklist: null
|
|
4698
|
+
frameworkChecklist: null,
|
|
4699
|
+
changedFileDetection,
|
|
4700
|
+
changedFiles,
|
|
4701
|
+
verificationRecommendations: [],
|
|
4702
|
+
verificationCommandPlan: []
|
|
4356
4703
|
};
|
|
4357
4704
|
if (!result.packageJson) {
|
|
4705
|
+
result.verificationRecommendations = buildChangedFileVerificationRecommendations({
|
|
4706
|
+
packageManager: result.packageManager,
|
|
4707
|
+
scripts: {},
|
|
4708
|
+
commands: result.commands,
|
|
4709
|
+
changedFiles,
|
|
4710
|
+
changedFilesAvailable: changedFileDetection.available,
|
|
4711
|
+
packageJson: false
|
|
4712
|
+
});
|
|
4713
|
+
result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
|
|
4358
4714
|
if (!quiet) {
|
|
4359
4715
|
console.log(`No package.json found in ${dir}`);
|
|
4360
4716
|
console.log('Suggested verification: use project pack or local framework conventions.');
|
|
4717
|
+
printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
|
|
4361
4718
|
}
|
|
4362
4719
|
return result;
|
|
4363
4720
|
}
|
|
@@ -4370,10 +4727,238 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
4370
4727
|
result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
|
|
4371
4728
|
result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
|
|
4372
4729
|
result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
|
|
4730
|
+
result.verificationRecommendations = buildChangedFileVerificationRecommendations({
|
|
4731
|
+
packageManager: result.packageManager,
|
|
4732
|
+
scripts,
|
|
4733
|
+
commands: result.commands,
|
|
4734
|
+
changedFiles,
|
|
4735
|
+
changedFilesAvailable: changedFileDetection.available,
|
|
4736
|
+
packageJson: true
|
|
4737
|
+
});
|
|
4738
|
+
result.verificationCommandPlan = buildVerificationCommandPlan(result.verificationRecommendations);
|
|
4373
4739
|
if (!quiet)
|
|
4374
4740
|
printDetection(result);
|
|
4375
4741
|
return result;
|
|
4376
4742
|
}
|
|
4743
|
+
async function detectCommand(args = []) {
|
|
4744
|
+
const flags = parseSimpleFlags(args, new Set(['json']));
|
|
4745
|
+
const dir = path.resolve(firstPositional(args) || process.cwd());
|
|
4746
|
+
const result = await detectProject(dir, { quiet: true });
|
|
4747
|
+
if (flags.json) {
|
|
4748
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
printDetection(result);
|
|
4752
|
+
}
|
|
4753
|
+
function buildChangedFileVerificationRecommendations({ packageManager = 'npm', scripts = {}, commands = {}, changedFiles = [], changedFilesAvailable = true, packageJson = false }) {
|
|
4754
|
+
const safeFallbackCommands = uniqueStrings([
|
|
4755
|
+
commands.typecheck,
|
|
4756
|
+
commands.lint,
|
|
4757
|
+
commands.test,
|
|
4758
|
+
commands.build
|
|
4759
|
+
]);
|
|
4760
|
+
if (!changedFilesAvailable) {
|
|
4761
|
+
return [{
|
|
4762
|
+
matched_rule: 'git-status-unavailable',
|
|
4763
|
+
confidence: 'low',
|
|
4764
|
+
reason: 'Git change status was unavailable, so yam cannot prove that the project has no changed files',
|
|
4765
|
+
fallback: packageJson
|
|
4766
|
+
? 'run all available non-publish typecheck, lint, test, and build checks or use project-pack conventions'
|
|
4767
|
+
: 'use yam.project.md or local framework conventions; no Git change scope or executable package scripts were detected',
|
|
4768
|
+
files: [],
|
|
4769
|
+
suggested_commands: safeFallbackCommands
|
|
4770
|
+
}];
|
|
4771
|
+
}
|
|
4772
|
+
if (!changedFiles.length) {
|
|
4773
|
+
return [{
|
|
4774
|
+
matched_rule: 'no-changes',
|
|
4775
|
+
confidence: 'high',
|
|
4776
|
+
reason: 'git reports no dirty, staged, or untracked project files',
|
|
4777
|
+
fallback: 'rerun `yam detect --json` after files change; use project-pack conventions if Git is unavailable',
|
|
4778
|
+
files: [],
|
|
4779
|
+
suggested_commands: []
|
|
4780
|
+
}];
|
|
4781
|
+
}
|
|
4782
|
+
const rules = [
|
|
4783
|
+
{
|
|
4784
|
+
id: 'package-release-metadata',
|
|
4785
|
+
confidence: 'high',
|
|
4786
|
+
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),
|
|
4787
|
+
reason: 'package or release metadata changed, so package contents and release readiness can drift',
|
|
4788
|
+
commandNames: ['package-boundary:check', 'release:check'],
|
|
4789
|
+
fallback: 'run the available typecheck/build/package checks and perform the public release workflow separately'
|
|
4790
|
+
},
|
|
4791
|
+
{
|
|
4792
|
+
id: 'src-lib',
|
|
4793
|
+
confidence: 'high',
|
|
4794
|
+
matches: (file) => /^src\/lib\//i.test(file),
|
|
4795
|
+
reason: 'shared library code changed, so compile safety and a focused behavior smoke are the narrowest honest checks',
|
|
4796
|
+
commandNames: ['typecheck'],
|
|
4797
|
+
fallback: 'run typecheck plus the closest available test/smoke; use the full safe fallback when the library crosses package boundaries'
|
|
4798
|
+
},
|
|
4799
|
+
{
|
|
4800
|
+
id: 'scripts',
|
|
4801
|
+
confidence: 'medium',
|
|
4802
|
+
matches: (file) => /^scripts\//i.test(file),
|
|
4803
|
+
reason: 'an executable verification or maintenance script changed, so its direct npm wrapper or closest smoke should run',
|
|
4804
|
+
commandNames: [],
|
|
4805
|
+
fallback: 'run the changed script in an isolated fixture, then use the full safe fallback if no direct wrapper exists'
|
|
4806
|
+
},
|
|
4807
|
+
{
|
|
4808
|
+
id: 'unknown-safe-fallback',
|
|
4809
|
+
confidence: packageJson ? 'medium' : 'low',
|
|
4810
|
+
matches: () => true,
|
|
4811
|
+
reason: 'no focused changed-file rule matched, so skipping verification would be weaker than the available project-wide safe checks',
|
|
4812
|
+
commandNames: [],
|
|
4813
|
+
fallback: packageJson
|
|
4814
|
+
? 'run all available non-publish typecheck, lint, test, and build checks'
|
|
4815
|
+
: 'use yam.project.md or local framework conventions; no executable package scripts were detected'
|
|
4816
|
+
}
|
|
4817
|
+
];
|
|
4818
|
+
const grouped = new Map();
|
|
4819
|
+
for (const rawFile of changedFiles) {
|
|
4820
|
+
const file = String(rawFile || '').replaceAll('\\', '/');
|
|
4821
|
+
const rule = rules.find((candidate) => candidate.matches(file)) || rules[rules.length - 1];
|
|
4822
|
+
grouped.set(rule.id, [...(grouped.get(rule.id) || []), file]);
|
|
4823
|
+
}
|
|
4824
|
+
const recommendations = [];
|
|
4825
|
+
for (const rule of rules) {
|
|
4826
|
+
const files = grouped.get(rule.id) || [];
|
|
4827
|
+
if (!files.length)
|
|
4828
|
+
continue;
|
|
4829
|
+
let commandEvidence = rule.commandNames.map((name) => ({
|
|
4830
|
+
name,
|
|
4831
|
+
files,
|
|
4832
|
+
basis: 'rule-wide'
|
|
4833
|
+
}));
|
|
4834
|
+
if (rule.id === 'src-lib') {
|
|
4835
|
+
commandEvidence = [
|
|
4836
|
+
{ name: 'typecheck', files, basis: 'rule-wide' },
|
|
4837
|
+
...focusedSmokeScriptEvidenceForFiles(scripts, files)
|
|
4838
|
+
];
|
|
4839
|
+
}
|
|
4840
|
+
else if (rule.id === 'scripts') {
|
|
4841
|
+
const directEvidence = files.flatMap((file) => (directScriptNamesForFile(scripts, file).map((name) => ({
|
|
4842
|
+
name,
|
|
4843
|
+
files: [file],
|
|
4844
|
+
basis: 'direct-script-wrapper'
|
|
4845
|
+
}))));
|
|
4846
|
+
commandEvidence = directEvidence.length
|
|
4847
|
+
? directEvidence
|
|
4848
|
+
: focusedSmokeScriptNames(scripts).slice(0, 1).map((name) => ({
|
|
4849
|
+
name,
|
|
4850
|
+
files,
|
|
4851
|
+
basis: 'group-fallback'
|
|
4852
|
+
}));
|
|
4853
|
+
}
|
|
4854
|
+
else if (rule.id === 'unknown-safe-fallback') {
|
|
4855
|
+
commandEvidence = safeFallbackCommands.map((command) => ({
|
|
4856
|
+
command,
|
|
4857
|
+
files,
|
|
4858
|
+
basis: 'safe-fallback'
|
|
4859
|
+
}));
|
|
4860
|
+
}
|
|
4861
|
+
const commandSources = commandEvidence
|
|
4862
|
+
.map((evidence) => ({
|
|
4863
|
+
command: evidence.command || scriptCommandIfPresent(packageManager, scripts, evidence.name),
|
|
4864
|
+
files: [...new Set(evidence.files || [])].sort(),
|
|
4865
|
+
basis: evidence.basis
|
|
4866
|
+
}))
|
|
4867
|
+
.filter((evidence) => evidence.command);
|
|
4868
|
+
const suggestedCommands = uniqueStrings(commandSources.map((evidence) => evidence.command));
|
|
4869
|
+
recommendations.push({
|
|
4870
|
+
matched_rule: rule.id,
|
|
4871
|
+
confidence: rule.confidence,
|
|
4872
|
+
reason: rule.reason,
|
|
4873
|
+
fallback: rule.fallback,
|
|
4874
|
+
files: [...new Set(files)].sort(),
|
|
4875
|
+
suggested_commands: suggestedCommands,
|
|
4876
|
+
command_sources: commandSources
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
return recommendations;
|
|
4880
|
+
}
|
|
4881
|
+
function focusedSmokeScriptNames(scripts = {}) {
|
|
4882
|
+
return Object.keys(scripts)
|
|
4883
|
+
.filter((name) => /(?:^|[-:])(?:smoke|test)(?:$|[-:])/i.test(name) || /smoke/i.test(String(scripts[name] || '')))
|
|
4884
|
+
.sort();
|
|
4885
|
+
}
|
|
4886
|
+
function focusedSmokeScriptEvidenceForFiles(scripts = {}, files = []) {
|
|
4887
|
+
const candidates = focusedSmokeScriptNames(scripts);
|
|
4888
|
+
const selected = [];
|
|
4889
|
+
for (const file of files) {
|
|
4890
|
+
const stem = path.posix.basename(String(file || '').replaceAll('\\', '/')).replace(/\.[^.]+$/, '');
|
|
4891
|
+
const direct = candidates.find((name) => {
|
|
4892
|
+
const command = String(scripts[name] || '');
|
|
4893
|
+
return name.includes(stem)
|
|
4894
|
+
|| command.includes(stem)
|
|
4895
|
+
|| (stem === 'skill-installation' && /install.*smoke|smoke.*install/i.test(`${name} ${command}`))
|
|
4896
|
+
|| (['trust-kernel', 'ueye-artifacts'].includes(stem) && /cli.*smoke|smoke.*cli/i.test(`${name} ${command}`));
|
|
4897
|
+
});
|
|
4898
|
+
if (direct)
|
|
4899
|
+
selected.push({
|
|
4900
|
+
name: direct,
|
|
4901
|
+
files: [file],
|
|
4902
|
+
basis: 'focused-file-match'
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
if (!selected.length && candidates.length) {
|
|
4906
|
+
selected.push({
|
|
4907
|
+
name: candidates[0],
|
|
4908
|
+
files,
|
|
4909
|
+
basis: 'group-fallback'
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
return selected;
|
|
4913
|
+
}
|
|
4914
|
+
function directScriptNamesForFile(scripts = {}, changedFile = '') {
|
|
4915
|
+
const normalized = String(changedFile || '').replaceAll('\\', '/');
|
|
4916
|
+
const basename = path.posix.basename(normalized);
|
|
4917
|
+
return Object.entries(scripts)
|
|
4918
|
+
.filter(([, command]) => {
|
|
4919
|
+
const text = String(command || '').replaceAll('\\', '/');
|
|
4920
|
+
return text.includes(normalized) || text.includes(`/${basename}`) || text.includes(` ${basename}`);
|
|
4921
|
+
})
|
|
4922
|
+
.map(([name]) => name)
|
|
4923
|
+
.sort();
|
|
4924
|
+
}
|
|
4925
|
+
function scriptCommandIfPresent(packageManager, scripts = {}, name = '') {
|
|
4926
|
+
return name && Object.prototype.hasOwnProperty.call(scripts, name)
|
|
4927
|
+
? runCommand(packageManager, name)
|
|
4928
|
+
: null;
|
|
4929
|
+
}
|
|
4930
|
+
function uniqueStrings(values = []) {
|
|
4931
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
|
|
4932
|
+
}
|
|
4933
|
+
function buildVerificationCommandPlan(recommendations = []) {
|
|
4934
|
+
const commands = new Map();
|
|
4935
|
+
for (const recommendation of recommendations) {
|
|
4936
|
+
const sources = recommendation.command_sources?.length
|
|
4937
|
+
? recommendation.command_sources
|
|
4938
|
+
: (recommendation.suggested_commands || []).map((command) => ({
|
|
4939
|
+
command,
|
|
4940
|
+
files: recommendation.files || [],
|
|
4941
|
+
basis: 'recommendation-fallback'
|
|
4942
|
+
}));
|
|
4943
|
+
for (const source of sources) {
|
|
4944
|
+
const current = commands.get(source.command) || {
|
|
4945
|
+
command: source.command,
|
|
4946
|
+
matched_rules: [],
|
|
4947
|
+
files: [],
|
|
4948
|
+
provenance: []
|
|
4949
|
+
};
|
|
4950
|
+
current.matched_rules = uniqueStrings([...current.matched_rules, recommendation.matched_rule]);
|
|
4951
|
+
current.files = uniqueStrings([...current.files, ...(source.files || [])]).sort();
|
|
4952
|
+
current.provenance.push({
|
|
4953
|
+
matched_rule: recommendation.matched_rule,
|
|
4954
|
+
files: [...new Set(source.files || [])].sort(),
|
|
4955
|
+
basis: source.basis
|
|
4956
|
+
});
|
|
4957
|
+
commands.set(source.command, current);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
return [...commands.values()];
|
|
4961
|
+
}
|
|
4377
4962
|
function printDetection(result) {
|
|
4378
4963
|
console.log(`Project: ${result.dir}`);
|
|
4379
4964
|
console.log(`Package manager: ${result.packageManager}`);
|
|
@@ -4387,6 +4972,7 @@ function printDetection(result) {
|
|
|
4387
4972
|
console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'L1 inspected; no command detected'}`);
|
|
4388
4973
|
console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
|
|
4389
4974
|
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}`}`);
|
|
4975
|
+
printChangedFileVerificationRecommendations(result.verificationRecommendations, result.verificationCommandPlan);
|
|
4390
4976
|
if (result.frameworkChecklist?.detected) {
|
|
4391
4977
|
console.log('');
|
|
4392
4978
|
console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
|
|
@@ -4395,6 +4981,28 @@ function printDetection(result) {
|
|
|
4395
4981
|
}
|
|
4396
4982
|
}
|
|
4397
4983
|
}
|
|
4984
|
+
function printChangedFileVerificationRecommendations(recommendations = [], commandPlan = []) {
|
|
4985
|
+
console.log('');
|
|
4986
|
+
console.log('Changed-file verification recommendations (advisory; commands were not run):');
|
|
4987
|
+
for (const recommendation of recommendations) {
|
|
4988
|
+
console.log(`- ${recommendation.matched_rule} [confidence: ${recommendation.confidence}]`);
|
|
4989
|
+
console.log(` files: ${recommendation.files.length ? recommendation.files.join(', ') : '(none)'}`);
|
|
4990
|
+
console.log(` reason: ${recommendation.reason}`);
|
|
4991
|
+
console.log(` commands: ${recommendation.suggested_commands.length ? recommendation.suggested_commands.join(' && ') : '(none detected)'}`);
|
|
4992
|
+
console.log(` fallback: ${recommendation.fallback}`);
|
|
4993
|
+
}
|
|
4994
|
+
console.log('');
|
|
4995
|
+
console.log('Verification command plan (deduplicated; advisory; commands were not run):');
|
|
4996
|
+
if (!commandPlan.length) {
|
|
4997
|
+
console.log('- (none detected)');
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
for (const item of commandPlan) {
|
|
5001
|
+
console.log(`- ${item.command}`);
|
|
5002
|
+
console.log(` rules: ${item.matched_rules.join(', ')}`);
|
|
5003
|
+
console.log(` files: ${item.files.join(', ') || '(none)'}`);
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
4398
5006
|
function budget(routeArg = '') {
|
|
4399
5007
|
const normalized = normalizeRoute(routeArg);
|
|
4400
5008
|
const entries = normalized ? [[normalized, ROUTE_BUDGETS[normalized]]] : Object.entries(ROUTE_BUDGETS);
|
|
@@ -4773,20 +5381,44 @@ function findSensitivePattern(text = '') {
|
|
|
4773
5381
|
function showPath() {
|
|
4774
5382
|
console.log(`root: ${ROOT}`);
|
|
4775
5383
|
console.log(`skills: ${DEST}`);
|
|
4776
|
-
console.log(`codex mirror
|
|
5384
|
+
console.log(`codex mirror (preserved without an ownership receipt): ${CODEX_MIRROR}`);
|
|
5385
|
+
}
|
|
5386
|
+
function showRequestedHelp(args) {
|
|
5387
|
+
if (!args.some((arg) => arg === '--help' || arg === '-h'))
|
|
5388
|
+
return false;
|
|
5389
|
+
const commandUsage = {
|
|
5390
|
+
context: contextUsage,
|
|
5391
|
+
cleanup: cleanupUsage,
|
|
5392
|
+
'study-note': studyNoteUsage,
|
|
5393
|
+
tools: toolsUsage,
|
|
5394
|
+
hook: hookUsage,
|
|
5395
|
+
loop: loopUsage,
|
|
5396
|
+
ueye: ueyeUsage,
|
|
5397
|
+
media: mediaUsage,
|
|
5398
|
+
runtime: runtimeUsage,
|
|
5399
|
+
mission: missionUsage,
|
|
5400
|
+
update: updateUsage,
|
|
5401
|
+
benchmark: benchmarkUsage,
|
|
5402
|
+
memory: memoryUsage
|
|
5403
|
+
};
|
|
5404
|
+
(commandUsage[args[0]] || usage)();
|
|
5405
|
+
return true;
|
|
4777
5406
|
}
|
|
4778
5407
|
async function main() {
|
|
4779
|
-
const
|
|
4780
|
-
|
|
5408
|
+
const args = process.argv.slice(2);
|
|
5409
|
+
const command = args[0] || 'help';
|
|
5410
|
+
if (showRequestedHelp(args))
|
|
5411
|
+
return;
|
|
5412
|
+
if (command === 'help')
|
|
4781
5413
|
return usage();
|
|
4782
5414
|
if (command === 'install')
|
|
4783
|
-
return install();
|
|
5415
|
+
return install(args.slice(1));
|
|
4784
5416
|
if (command === 'uninstall')
|
|
4785
|
-
return uninstall();
|
|
5417
|
+
return uninstall(args.slice(1));
|
|
4786
5418
|
if (command === 'version')
|
|
4787
5419
|
return console.log(VERSION);
|
|
4788
5420
|
if (command === 'detect')
|
|
4789
|
-
return
|
|
5421
|
+
return detectCommand(args.slice(1));
|
|
4790
5422
|
if (command === 'pack')
|
|
4791
5423
|
return inspectProjectPack(process.argv[3]);
|
|
4792
5424
|
if (command === 'context')
|
|
@@ -4813,6 +5445,8 @@ async function main() {
|
|
|
4813
5445
|
return runtime(process.argv.slice(3));
|
|
4814
5446
|
if (command === 'mission')
|
|
4815
5447
|
return mission(process.argv.slice(3));
|
|
5448
|
+
if (command === 'update')
|
|
5449
|
+
return updateCommand(process.argv.slice(3));
|
|
4816
5450
|
if (command === 'benchmark')
|
|
4817
5451
|
return benchmark(process.argv.slice(3));
|
|
4818
5452
|
if (command === 'release')
|