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