@nerviq/cli 1.2.5 → 1.2.7
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/bin/cli.js +131 -2
- package/package.json +1 -1
- package/src/activity.js +5 -1
- package/src/audit.js +19 -1
- package/src/harmony/add.js +68 -0
- package/src/harmony/advisor.js +10 -0
- package/src/harmony/canon.js +23 -0
- package/src/harmony/cli.js +1 -0
- package/src/harmony/sync.js +18 -1
- package/src/harmony/watch.js +35 -1
- package/src/techniques.js +259 -9
package/bin/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ const COMMAND_ALIASES = {
|
|
|
24
24
|
gov: 'governance',
|
|
25
25
|
outcome: 'feedback',
|
|
26
26
|
};
|
|
27
|
-
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'help', 'version'];
|
|
27
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'help', 'version'];
|
|
28
28
|
|
|
29
29
|
function levenshtein(a, b) {
|
|
30
30
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -312,6 +312,10 @@ const HELP = `
|
|
|
312
312
|
|
|
313
313
|
CROSS-PLATFORM
|
|
314
314
|
nerviq harmony-audit Drift detection across all active platforms
|
|
315
|
+
nerviq harmony-sync Preview cross-platform sync (dry run)
|
|
316
|
+
nerviq harmony-sync --fix Apply cross-platform sync (write files)
|
|
317
|
+
nerviq harmony-sync --json JSON output for CI/automation
|
|
318
|
+
nerviq harmony-add <platform> Add a new platform to the project
|
|
315
319
|
nerviq synergy-report Multi-agent amplification opportunities
|
|
316
320
|
nerviq convert --from X --to Y Convert configs between platforms
|
|
317
321
|
nerviq migrate --platform X Platform version migration helper
|
|
@@ -408,6 +412,8 @@ async function main() {
|
|
|
408
412
|
lite: flags.includes('--lite'),
|
|
409
413
|
snapshot: flags.includes('--snapshot'),
|
|
410
414
|
feedback: flags.includes('--feedback'),
|
|
415
|
+
fix: flags.includes('--fix'),
|
|
416
|
+
autoSync: flags.includes('--auto-sync'),
|
|
411
417
|
dryRun: flags.includes('--dry-run'),
|
|
412
418
|
threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
|
|
413
419
|
out: parsed.out,
|
|
@@ -501,7 +507,7 @@ async function main() {
|
|
|
501
507
|
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
|
|
502
508
|
// Harmony + Synergy (cross-platform)
|
|
503
509
|
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
504
|
-
'harmony-watch', 'harmony-governance', 'synergy-report',
|
|
510
|
+
'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report',
|
|
505
511
|
]);
|
|
506
512
|
|
|
507
513
|
if (options.platform === 'codex') {
|
|
@@ -862,6 +868,129 @@ async function main() {
|
|
|
862
868
|
process.on('SIGINT', closeServer);
|
|
863
869
|
process.on('SIGTERM', closeServer);
|
|
864
870
|
return;
|
|
871
|
+
} else if (normalizedCommand === 'harmony-audit') {
|
|
872
|
+
const { runHarmonyAudit } = require('../src/harmony/cli');
|
|
873
|
+
await runHarmonyAudit(options);
|
|
874
|
+
process.exit(0);
|
|
875
|
+
} else if (normalizedCommand === 'harmony-sync') {
|
|
876
|
+
const { previewHarmonySync, applyHarmonySync } = require('../src/harmony/sync');
|
|
877
|
+
const dir = options.dir || process.cwd();
|
|
878
|
+
|
|
879
|
+
if (options.fix) {
|
|
880
|
+
// Apply mode: write files
|
|
881
|
+
const result = applyHarmonySync(dir);
|
|
882
|
+
if (options.json) {
|
|
883
|
+
console.log(JSON.stringify(result, null, 2));
|
|
884
|
+
} else {
|
|
885
|
+
console.log('');
|
|
886
|
+
console.log('\x1b[1m Harmony Sync — Apply\x1b[0m');
|
|
887
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
888
|
+
console.log('');
|
|
889
|
+
if (result.applied.length === 0 && result.skipped.length === 0) {
|
|
890
|
+
console.log(' \x1b[32mAll platforms are already in sync. Nothing to apply.\x1b[0m');
|
|
891
|
+
} else {
|
|
892
|
+
for (const item of result.applied) {
|
|
893
|
+
console.log(` \x1b[32m✓\x1b[0m ${item.action.padEnd(8)} ${item.platform.padEnd(12)} ${item.path}`);
|
|
894
|
+
}
|
|
895
|
+
for (const item of result.skipped) {
|
|
896
|
+
const reason = typeof item === 'string' ? item : (item.reason || item.path);
|
|
897
|
+
console.log(` \x1b[33m⚠\x1b[0m skipped ${reason}`);
|
|
898
|
+
}
|
|
899
|
+
console.log('');
|
|
900
|
+
if (result.summary) {
|
|
901
|
+
console.log(` Files: ${result.summary.totalFiles} (${result.summary.creates} created, ${result.summary.patches} patched)`);
|
|
902
|
+
console.log(` Platforms: ${result.summary.platforms.join(', ')}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
906
|
+
console.log('');
|
|
907
|
+
for (const w of result.warnings) {
|
|
908
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
console.log('');
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
// Preview mode (dry run)
|
|
915
|
+
const plan = previewHarmonySync(dir);
|
|
916
|
+
if (options.json) {
|
|
917
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
918
|
+
} else {
|
|
919
|
+
console.log('');
|
|
920
|
+
console.log('\x1b[1m Harmony Sync — Preview\x1b[0m');
|
|
921
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
922
|
+
console.log('');
|
|
923
|
+
if (plan.files.length === 0) {
|
|
924
|
+
console.log(' \x1b[32mAll platforms are already in sync. No changes needed.\x1b[0m');
|
|
925
|
+
} else {
|
|
926
|
+
for (const file of plan.files) {
|
|
927
|
+
const actionColor = file.action === 'create' ? '\x1b[32m' : '\x1b[36m';
|
|
928
|
+
console.log(` ${actionColor}${file.action.padEnd(8)}\x1b[0m ${file.platform.padEnd(12)} ${file.path}`);
|
|
929
|
+
if (file.preview) {
|
|
930
|
+
console.log(` \x1b[2m${file.preview}\x1b[0m`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
console.log('');
|
|
934
|
+
console.log(` Total: ${plan.summary.totalFiles} file(s) — ${plan.summary.creates} create, ${plan.summary.patches} patch`);
|
|
935
|
+
console.log(` Platforms: ${plan.summary.platforms.join(', ')}`);
|
|
936
|
+
if (plan.summary.recommendedTrust) {
|
|
937
|
+
console.log(` Recommended trust: ${plan.summary.recommendedTrust}`);
|
|
938
|
+
}
|
|
939
|
+
console.log('');
|
|
940
|
+
console.log(' Run \x1b[1mnerviq harmony-sync --fix\x1b[0m to apply these changes.');
|
|
941
|
+
}
|
|
942
|
+
if (plan.warnings && plan.warnings.length > 0) {
|
|
943
|
+
console.log('');
|
|
944
|
+
for (const w of plan.warnings) {
|
|
945
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
console.log('');
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
process.exit(0);
|
|
952
|
+
} else if (normalizedCommand === 'harmony-drift') {
|
|
953
|
+
const { runHarmonyDrift } = require('../src/harmony/cli');
|
|
954
|
+
await runHarmonyDrift(options);
|
|
955
|
+
process.exit(0);
|
|
956
|
+
} else if (normalizedCommand === 'harmony-advise') {
|
|
957
|
+
const { runHarmonyAdvise } = require('../src/harmony/cli');
|
|
958
|
+
await runHarmonyAdvise(options);
|
|
959
|
+
process.exit(0);
|
|
960
|
+
} else if (normalizedCommand === 'harmony-watch') {
|
|
961
|
+
const { runHarmonyWatch } = require('../src/harmony/cli');
|
|
962
|
+
await runHarmonyWatch(options);
|
|
963
|
+
} else if (normalizedCommand === 'harmony-governance') {
|
|
964
|
+
const { runHarmonyGovernance } = require('../src/harmony/cli');
|
|
965
|
+
await runHarmonyGovernance(options);
|
|
966
|
+
process.exit(0);
|
|
967
|
+
} else if (normalizedCommand === 'harmony-add') {
|
|
968
|
+
const { addPlatform } = require('../src/harmony/add');
|
|
969
|
+
const platformArg = parsed.extraArgs[0];
|
|
970
|
+
if (!platformArg) {
|
|
971
|
+
console.log('\n Usage: nerviq harmony-add <platform>');
|
|
972
|
+
console.log(' Available: claude, codex, gemini, copilot, cursor, windsurf, aider, opencode\n');
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
const dir = options.dir || process.cwd();
|
|
976
|
+
const result = addPlatform(dir, platformArg.toLowerCase());
|
|
977
|
+
if (options.json) {
|
|
978
|
+
console.log(JSON.stringify(result, null, 2));
|
|
979
|
+
} else if (result.success) {
|
|
980
|
+
console.log(`\n \x1b[32m\u2713\x1b[0m Added ${result.platform} to project`);
|
|
981
|
+
result.created.forEach(f => console.log(` Created: ${f}`));
|
|
982
|
+
console.log(` Platforms: ${result.beforeCount} \u2192 ${result.afterCount}`);
|
|
983
|
+
if (result.syncApplied > 0) console.log(` Harmony sync: ${result.syncApplied} file(s) updated`);
|
|
984
|
+
console.log('');
|
|
985
|
+
} else {
|
|
986
|
+
console.log(`\n \x1b[31m\u2717\x1b[0m ${result.error}\n`);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
process.exit(0);
|
|
990
|
+
} else if (normalizedCommand === 'synergy-report') {
|
|
991
|
+
// Placeholder — synergy report is referenced but may not be implemented yet
|
|
992
|
+
console.log('\n Synergy report: coming soon.\n');
|
|
993
|
+
process.exit(0);
|
|
865
994
|
} else if (normalizedCommand === 'doctor') {
|
|
866
995
|
const { runDoctor } = require('../src/doctor');
|
|
867
996
|
const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
|
package/package.json
CHANGED
package/src/activity.js
CHANGED
|
@@ -228,7 +228,11 @@ function getHistory(dir, limit = 20) {
|
|
|
228
228
|
const entries = readSnapshotIndex(dir);
|
|
229
229
|
return entries
|
|
230
230
|
.filter(e => e.snapshotKind === 'audit')
|
|
231
|
-
.sort((a, b) =>
|
|
231
|
+
.sort((a, b) => {
|
|
232
|
+
const dateDiff = new Date(b.createdAt) - new Date(a.createdAt);
|
|
233
|
+
if (dateDiff !== 0) return dateDiff;
|
|
234
|
+
return (b.id || '').localeCompare(a.id || '');
|
|
235
|
+
})
|
|
232
236
|
.slice(0, limit);
|
|
233
237
|
}
|
|
234
238
|
|
package/src/audit.js
CHANGED
|
@@ -1279,7 +1279,25 @@ async function audit(options) {
|
|
|
1279
1279
|
console.log('');
|
|
1280
1280
|
}
|
|
1281
1281
|
|
|
1282
|
-
|
|
1282
|
+
// Cross-platform synergy hint
|
|
1283
|
+
try {
|
|
1284
|
+
const { detectActivePlatforms } = require('./harmony/canon');
|
|
1285
|
+
const { analyzeCompensation } = require('./synergy/compensation');
|
|
1286
|
+
const { calculateSynergyScore } = require('./synergy/ranking');
|
|
1287
|
+
const detected = detectActivePlatforms(options.dir);
|
|
1288
|
+
const activePlatforms = (detected || []).filter(p => p.detected).map(p => p.platform);
|
|
1289
|
+
if (activePlatforms.length >= 2) {
|
|
1290
|
+
const comp = analyzeCompensation(activePlatforms);
|
|
1291
|
+
const synergyScore = calculateSynergyScore(activePlatforms);
|
|
1292
|
+
console.log(colorize(` Cross-platform synergy: ${activePlatforms.length} platforms detected`, 'blue'));
|
|
1293
|
+
console.log(colorize(` Platforms: ${activePlatforms.join(', ')}`, 'dim'));
|
|
1294
|
+
console.log(colorize(` Compensations: ${comp.compensations.length} | Gaps: ${comp.uncoveredGaps.length}`, 'dim'));
|
|
1295
|
+
console.log(colorize(` Run: npx nerviq harmony-audit for full cross-platform analysis`, 'dim'));
|
|
1296
|
+
console.log('');
|
|
1297
|
+
}
|
|
1298
|
+
} catch { /* synergy display is optional */ }
|
|
1299
|
+
|
|
1300
|
+
console.log(colorize(` Backed by NERVIQ research and evidence for ${spec.platformLabel}`, 'dim'));
|
|
1283
1301
|
console.log(colorize(' https://github.com/nerviq/nerviq', 'dim'));
|
|
1284
1302
|
console.log('');
|
|
1285
1303
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Addition Wizard
|
|
3
|
+
* Helps users add a new platform config to their project.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { detectActivePlatforms, PLATFORM_SIGNATURES } = require('./canon');
|
|
9
|
+
const { applyHarmonySync } = require('./sync');
|
|
10
|
+
|
|
11
|
+
const PLATFORM_BOOTSTRAPS = {
|
|
12
|
+
claude: { files: [{ path: 'CLAUDE.md', content: '# Project Instructions\n\nAdd your Claude Code instructions here.\n' }] },
|
|
13
|
+
codex: { files: [{ path: 'AGENTS.md', content: '# Agents Instructions\n\nAdd your Codex instructions here.\n' }] },
|
|
14
|
+
gemini: { files: [{ path: 'GEMINI.md', content: '# Gemini Instructions\n\nAdd your Gemini CLI instructions here.\n' }] },
|
|
15
|
+
copilot: { files: [{ path: '.github/copilot-instructions.md', content: '# Copilot Instructions\n\nAdd your GitHub Copilot instructions here.\n' }] },
|
|
16
|
+
cursor: { files: [{ path: '.cursorrules', content: '# Cursor Rules\n\nAdd your Cursor rules here.\n' }] },
|
|
17
|
+
windsurf: { files: [{ path: '.windsurfrules', content: '# Windsurf Rules\n\nAdd your Windsurf rules here.\n' }] },
|
|
18
|
+
aider: { files: [{ path: '.aider.conf.yml', content: '# Aider Configuration\n# See: https://aider.chat/docs/config/aider_conf.html\n' }] },
|
|
19
|
+
opencode: { files: [{ path: 'opencode.json', content: '{\n "instructions": "Add your OpenCode instructions here."\n}\n' }] },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function addPlatform(dir, platformKey) {
|
|
23
|
+
// Validate platform
|
|
24
|
+
if (!PLATFORM_SIGNATURES[platformKey]) {
|
|
25
|
+
return { success: false, error: `Unknown platform: ${platformKey}. Available: ${Object.keys(PLATFORM_SIGNATURES).join(', ')}` };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if already active
|
|
29
|
+
const active = detectActivePlatforms(dir);
|
|
30
|
+
const alreadyActive = active.find(p => p.platform === platformKey);
|
|
31
|
+
if (alreadyActive) {
|
|
32
|
+
return { success: false, error: `${platformKey} is already active in this project.` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const beforeCount = active.length;
|
|
36
|
+
const bootstrap = PLATFORM_BOOTSTRAPS[platformKey];
|
|
37
|
+
const created = [];
|
|
38
|
+
|
|
39
|
+
// Create bootstrap files
|
|
40
|
+
for (const file of bootstrap.files) {
|
|
41
|
+
const fullPath = path.join(dir, file.path);
|
|
42
|
+
if (fs.existsSync(fullPath)) continue;
|
|
43
|
+
const dirName = path.dirname(fullPath);
|
|
44
|
+
fs.mkdirSync(dirName, { recursive: true });
|
|
45
|
+
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
46
|
+
created.push(file.path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run harmony sync to populate managed blocks
|
|
50
|
+
let syncResult = null;
|
|
51
|
+
try {
|
|
52
|
+
syncResult = applyHarmonySync(dir);
|
|
53
|
+
} catch { /* sync is optional */ }
|
|
54
|
+
|
|
55
|
+
const afterActive = detectActivePlatforms(dir);
|
|
56
|
+
const afterCount = afterActive.length;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
platform: platformKey,
|
|
61
|
+
created,
|
|
62
|
+
beforeCount,
|
|
63
|
+
afterCount,
|
|
64
|
+
syncApplied: syncResult ? syncResult.applied.length : 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { addPlatform, PLATFORM_BOOTSTRAPS };
|
package/src/harmony/advisor.js
CHANGED
|
@@ -55,6 +55,16 @@ const TASK_TYPE_PROFILES = {
|
|
|
55
55
|
requiredStrengths: { reasoning: 0.3, automation: 0.3, ide: 0.2, refactoring: 0.2 },
|
|
56
56
|
description: 'Starting new projects or major new features from scratch.',
|
|
57
57
|
},
|
|
58
|
+
'harness-optimization': {
|
|
59
|
+
label: 'Config Optimization',
|
|
60
|
+
requiredStrengths: { governance: 0.4, automation: 0.3, reasoning: 0.3 },
|
|
61
|
+
description: 'Optimizing AI coding agent configuration and harness settings.',
|
|
62
|
+
},
|
|
63
|
+
'phased-migration': {
|
|
64
|
+
label: 'Phased Migration',
|
|
65
|
+
requiredStrengths: { reasoning: 0.4, refactoring: 0.3, architecture: 0.3 },
|
|
66
|
+
description: 'Multi-phase migrations requiring sequential execution with validation gates.',
|
|
67
|
+
},
|
|
58
68
|
};
|
|
59
69
|
|
|
60
70
|
// ─── Scoring ──────────────────────────────────────────────────────────────────
|
package/src/harmony/canon.js
CHANGED
|
@@ -513,10 +513,33 @@ function buildCanonicalModel(dir) {
|
|
|
513
513
|
governanceSummary[key] = platformDetails[key].governance;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
// SD2: Adaptive project signals — infrastructure & tooling detection
|
|
517
|
+
const projectSignals = {};
|
|
518
|
+
const signalChecks = [
|
|
519
|
+
{ key: 'docker', label: 'Docker', files: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', '.dockerignore'] },
|
|
520
|
+
{ key: 'terraform', label: 'Terraform', files: ['main.tf', 'terraform.tf', '.terraform.lock.hcl'] },
|
|
521
|
+
{ key: 'kubernetes', label: 'Kubernetes', files: ['k8s/', 'kubernetes/', 'helm/', 'Chart.yaml'] },
|
|
522
|
+
{ key: 'ci-github', label: 'GitHub Actions', files: ['.github/workflows/'] },
|
|
523
|
+
{ key: 'ci-gitlab', label: 'GitLab CI', files: ['.gitlab-ci.yml'] },
|
|
524
|
+
{ key: 'pytest', label: 'pytest', files: ['pytest.ini', 'conftest.py', 'pyproject.toml'] },
|
|
525
|
+
{ key: 'jest', label: 'Jest', files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'] },
|
|
526
|
+
{ key: 'migrations', label: 'DB Migrations', files: ['migrations/', 'alembic/', 'prisma/migrations/', 'db/migrate/'] },
|
|
527
|
+
{ key: 'monorepo', label: 'Monorepo', files: ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json'] },
|
|
528
|
+
{ key: 'openapi', label: 'OpenAPI', files: ['openapi.yaml', 'openapi.json', 'swagger.yaml', 'swagger.json'] },
|
|
529
|
+
];
|
|
530
|
+
for (const signal of signalChecks) {
|
|
531
|
+
const detected = signal.files.some(f => {
|
|
532
|
+
const full = path.join(dir, f);
|
|
533
|
+
try { return fs.existsSync(full); } catch { return false; }
|
|
534
|
+
});
|
|
535
|
+
if (detected) projectSignals[signal.key] = signal.label;
|
|
536
|
+
}
|
|
537
|
+
|
|
516
538
|
return {
|
|
517
539
|
projectName,
|
|
518
540
|
dir,
|
|
519
541
|
stacks: stacks.map(s => s.key),
|
|
542
|
+
projectSignals,
|
|
520
543
|
activePlatforms: platformKeys.map(key => ({
|
|
521
544
|
platform: key,
|
|
522
545
|
label: platformDetails[key].label,
|
package/src/harmony/cli.js
CHANGED
|
@@ -300,6 +300,7 @@ async function runHarmonyWatch(options) {
|
|
|
300
300
|
|
|
301
301
|
await startHarmonyWatch({
|
|
302
302
|
dir,
|
|
303
|
+
autoSync: !!options.autoSync,
|
|
303
304
|
debounceMs: options.debounce || 800,
|
|
304
305
|
onDriftDetected: (platform, details) => {
|
|
305
306
|
console.log(c(` DRIFT ALERT: ${platform} score dropped by ${Math.abs(details.delta)}`, 'red'));
|
package/src/harmony/sync.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates aligned configs for ALL active platforms from a shared canonical
|
|
5
5
|
* understanding. Ensures instructions, MCP servers, and trust posture are
|
|
6
|
-
* consistent across Claude, Codex, Gemini, Copilot,
|
|
6
|
+
* consistent across Claude, Codex, Gemini, Copilot, Cursor, Windsurf,
|
|
7
|
+
* Aider, and OpenCode.
|
|
7
8
|
*
|
|
8
9
|
* Uses managed blocks from each platform's patch module so hand-authored
|
|
9
10
|
* content is always preserved.
|
|
@@ -37,6 +38,12 @@ const MANAGED_MARKERS = {
|
|
|
37
38
|
start: '<!-- nerviq:managed:start -->',
|
|
38
39
|
end: '<!-- nerviq:managed:end -->',
|
|
39
40
|
},
|
|
41
|
+
windsurf: {
|
|
42
|
+
start: '<!-- nerviq:managed:start -->',
|
|
43
|
+
end: '<!-- nerviq:managed:end -->',
|
|
44
|
+
},
|
|
45
|
+
// aider: uses .aider.conf.yml (YAML) — managed HTML-comment blocks not supported
|
|
46
|
+
// opencode: uses opencode.json (JSON) — managed HTML-comment blocks not supported
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
@@ -144,6 +151,9 @@ function getInstructionPath(platform) {
|
|
|
144
151
|
case 'gemini': return 'GEMINI.md';
|
|
145
152
|
case 'copilot': return '.github/copilot-instructions.md';
|
|
146
153
|
case 'cursor': return '.cursorrules';
|
|
154
|
+
case 'windsurf': return '.windsurfrules';
|
|
155
|
+
// aider: .aider.conf.yml is YAML config — no managed block support, skip instruction sync
|
|
156
|
+
// opencode: opencode.json is JSON config — no managed block support, skip instruction sync
|
|
147
157
|
default: return null;
|
|
148
158
|
}
|
|
149
159
|
}
|
|
@@ -178,6 +188,11 @@ function buildMcpConfig(platform, mcpServers) {
|
|
|
178
188
|
return { mcpServers: servers };
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
if (platform === 'windsurf') {
|
|
192
|
+
// Windsurf mcp.json format
|
|
193
|
+
return { mcpServers: servers };
|
|
194
|
+
}
|
|
195
|
+
|
|
181
196
|
return null;
|
|
182
197
|
}
|
|
183
198
|
|
|
@@ -190,6 +205,8 @@ function getMcpConfigPath(platform) {
|
|
|
190
205
|
case 'gemini': return '.gemini/settings.json';
|
|
191
206
|
case 'copilot': return '.vscode/mcp.json';
|
|
192
207
|
case 'cursor': return '.cursor/mcp.json';
|
|
208
|
+
case 'windsurf': return '.windsurf/mcp.json';
|
|
209
|
+
// aider & opencode: no MCP config support
|
|
193
210
|
default: return null;
|
|
194
211
|
}
|
|
195
212
|
}
|
package/src/harmony/watch.js
CHANGED
|
@@ -36,6 +36,13 @@ const PLATFORM_WATCH_FILES = [
|
|
|
36
36
|
'.github/copilot-review-instructions.md',
|
|
37
37
|
// Cursor
|
|
38
38
|
'.cursorrules',
|
|
39
|
+
// Windsurf
|
|
40
|
+
'.windsurfrules',
|
|
41
|
+
// Aider
|
|
42
|
+
'.aider.conf.yml',
|
|
43
|
+
'.aiderignore',
|
|
44
|
+
// OpenCode
|
|
45
|
+
'opencode.json',
|
|
39
46
|
// Shared
|
|
40
47
|
'.gitignore',
|
|
41
48
|
'package.json',
|
|
@@ -53,6 +60,9 @@ const PLATFORM_WATCH_DIRS = [
|
|
|
53
60
|
'.github',
|
|
54
61
|
'.cursor',
|
|
55
62
|
'.cursor/rules',
|
|
63
|
+
'.windsurf',
|
|
64
|
+
'.windsurf/rules',
|
|
65
|
+
'.opencode',
|
|
56
66
|
];
|
|
57
67
|
|
|
58
68
|
// ─── fs.watch helpers (mirror pattern from src/watch.js) ──────────────────────
|
|
@@ -174,6 +184,9 @@ function identifyPlatform(filePath) {
|
|
|
174
184
|
if (normalized.includes('.gemini') || normalized.includes('gemini.md')) return 'gemini';
|
|
175
185
|
if (normalized.includes('copilot') || normalized.includes('.github')) return 'copilot';
|
|
176
186
|
if (normalized.includes('.cursor') || normalized.includes('cursorrules')) return 'cursor';
|
|
187
|
+
if (normalized.includes('.windsurf') || normalized.includes('windsurfrules')) return 'windsurf';
|
|
188
|
+
if (normalized.includes('.aider') || normalized.includes('aiderignore')) return 'aider';
|
|
189
|
+
if (normalized.includes('.opencode') || normalized.includes('opencode.json')) return 'opencode';
|
|
177
190
|
return 'unknown';
|
|
178
191
|
}
|
|
179
192
|
|
|
@@ -187,6 +200,7 @@ function identifyPlatform(filePath) {
|
|
|
187
200
|
* @param {Function} [options.onDriftDetected] - Callback when drift increases: (platform, details) => void
|
|
188
201
|
* @param {Function} [options.onPlatformChange] - Callback on any platform config change: (platform, file) => void
|
|
189
202
|
* @param {Function} [options.runAudit] - Optional audit function to re-run on changes
|
|
203
|
+
* @param {boolean} [options.autoSync=false] - Auto-apply harmony sync when drift is detected
|
|
190
204
|
* @param {number} [options.debounceMs=800] - Debounce interval in ms
|
|
191
205
|
*/
|
|
192
206
|
async function startHarmonyWatch(options) {
|
|
@@ -195,6 +209,7 @@ async function startHarmonyWatch(options) {
|
|
|
195
209
|
onDriftDetected,
|
|
196
210
|
onPlatformChange,
|
|
197
211
|
runAudit,
|
|
212
|
+
autoSync = false,
|
|
198
213
|
debounceMs = 800,
|
|
199
214
|
} = options;
|
|
200
215
|
|
|
@@ -204,7 +219,10 @@ async function startHarmonyWatch(options) {
|
|
|
204
219
|
console.log(c(' nerviq harmony watch', 'bold'));
|
|
205
220
|
console.log(c(' ═══════════════════════════════════════', 'dim'));
|
|
206
221
|
console.log(c(` Watching: ${dir}`, 'dim'));
|
|
207
|
-
console.log(c(` Platforms: Claude, Codex, Gemini, Copilot, Cursor`, 'dim'));
|
|
222
|
+
console.log(c(` Platforms: Claude, Codex, Gemini, Copilot, Cursor, Windsurf, Aider, OpenCode`, 'dim'));
|
|
223
|
+
if (autoSync) {
|
|
224
|
+
console.log(c(` Auto-sync: ON — drift will be auto-corrected`, 'green'));
|
|
225
|
+
}
|
|
208
226
|
console.log(c(` Mode: ${recursiveSupported ? 'native recursive' : 'expanded directory fallback'}`, 'dim'));
|
|
209
227
|
console.log(c(' Press Ctrl+C to stop', 'dim'));
|
|
210
228
|
console.log('');
|
|
@@ -293,6 +311,22 @@ async function startHarmonyWatch(options) {
|
|
|
293
311
|
}
|
|
294
312
|
}
|
|
295
313
|
|
|
314
|
+
// Auto-sync on drift
|
|
315
|
+
if (delta < 0 && autoSync) {
|
|
316
|
+
try {
|
|
317
|
+
const { applyHarmonySync } = require('./sync');
|
|
318
|
+
const syncResult = applyHarmonySync(dir);
|
|
319
|
+
if (syncResult.applied.length > 0) {
|
|
320
|
+
console.log(c(` Auto-sync: applied ${syncResult.applied.length} fix(es)`, 'green'));
|
|
321
|
+
for (const item of syncResult.applied) {
|
|
322
|
+
console.log(c(` ✓ ${item.action} ${item.path}`, 'dim'));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch (_e) {
|
|
326
|
+
console.log(c(` Auto-sync failed: ${_e.message}`, 'yellow'));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
296
330
|
lastScores[p] = newScore;
|
|
297
331
|
}
|
|
298
332
|
}
|
package/src/techniques.js
CHANGED
|
@@ -4,12 +4,203 @@
|
|
|
4
4
|
* Each technique includes: what to check, how to fix, impact level.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
7
10
|
function hasFrontendSignals(ctx) {
|
|
8
11
|
const pkg = ctx.fileContent('package.json') || '';
|
|
9
12
|
return /react|vue|angular|next|svelte|tailwind|vite|astro/i.test(pkg) ||
|
|
10
13
|
ctx.files.some(f => /tailwind\.config|vite\.config|next\.config|svelte\.config|nuxt\.config|pages\/|components\/|app\//i.test(f));
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
function getClaudeHookContents(ctx) {
|
|
17
|
+
const hookFiles = ctx.dirFiles('.claude/hooks').filter(f => /\.(js|cjs|mjs|ts|sh|py)$/.test(f));
|
|
18
|
+
return hookFiles.map(f => ctx.fileContent(`.claude/hooks/${f}`) || '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesPattern(value, pattern) {
|
|
22
|
+
if (pattern instanceof RegExp) {
|
|
23
|
+
pattern.lastIndex = 0;
|
|
24
|
+
return pattern.test(value);
|
|
25
|
+
}
|
|
26
|
+
return value === pattern;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getProjectEntries(ctx) {
|
|
30
|
+
if (ctx.__nerviqProjectEntries) return ctx.__nerviqProjectEntries;
|
|
31
|
+
|
|
32
|
+
const entries = [];
|
|
33
|
+
const skippedDirs = new Set([
|
|
34
|
+
'.git',
|
|
35
|
+
'.hg',
|
|
36
|
+
'.svn',
|
|
37
|
+
'node_modules',
|
|
38
|
+
'__pycache__',
|
|
39
|
+
'.pytest_cache',
|
|
40
|
+
'.mypy_cache',
|
|
41
|
+
'.ruff_cache',
|
|
42
|
+
'.venv',
|
|
43
|
+
'venv',
|
|
44
|
+
'env',
|
|
45
|
+
'.tox',
|
|
46
|
+
'.nox',
|
|
47
|
+
'vendor',
|
|
48
|
+
'dist',
|
|
49
|
+
'build',
|
|
50
|
+
'coverage',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const walk = (relPath = '') => {
|
|
54
|
+
const fullPath = relPath
|
|
55
|
+
? path.join(ctx.dir, ...relPath.split('/'))
|
|
56
|
+
: ctx.dir;
|
|
57
|
+
|
|
58
|
+
let dirents = [];
|
|
59
|
+
try {
|
|
60
|
+
dirents = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const dirent of dirents) {
|
|
66
|
+
if (dirent.name === '.DS_Store') continue;
|
|
67
|
+
|
|
68
|
+
const entryPath = relPath ? `${relPath}/${dirent.name}` : dirent.name;
|
|
69
|
+
if (dirent.isDirectory()) {
|
|
70
|
+
if (skippedDirs.has(dirent.name)) continue;
|
|
71
|
+
entries.push(`${entryPath}/`);
|
|
72
|
+
walk(entryPath);
|
|
73
|
+
} else {
|
|
74
|
+
entries.push(entryPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
walk();
|
|
80
|
+
ctx.__nerviqProjectEntries = entries;
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getProjectFiles(ctx) {
|
|
85
|
+
if (ctx.__nerviqProjectFiles) return ctx.__nerviqProjectFiles;
|
|
86
|
+
ctx.__nerviqProjectFiles = getProjectEntries(ctx).filter(entry => !entry.endsWith('/'));
|
|
87
|
+
return ctx.__nerviqProjectFiles;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findProjectFiles(ctx, pattern) {
|
|
91
|
+
return getProjectFiles(ctx).filter(file => matchesPattern(file, pattern));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasProjectFile(ctx, pattern) {
|
|
95
|
+
return findProjectFiles(ctx, pattern).length > 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readProjectFiles(ctx, pattern, limit = 60) {
|
|
99
|
+
return findProjectFiles(ctx, pattern)
|
|
100
|
+
.slice(0, limit)
|
|
101
|
+
.map(file => ctx.fileContent(file) || '')
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isPythonProject(ctx) {
|
|
107
|
+
if (ctx.__nerviqIsPython !== undefined) return ctx.__nerviqIsPython;
|
|
108
|
+
ctx.__nerviqIsPython =
|
|
109
|
+
hasProjectFile(ctx, /(^|\/)(pyproject\.toml|requirements[^/]*\.txt|setup\.py)$/i) ||
|
|
110
|
+
hasProjectFile(ctx, /\.py$/i);
|
|
111
|
+
return ctx.__nerviqIsPython;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isGoProject(ctx) {
|
|
115
|
+
if (ctx.__nerviqIsGo !== undefined) return ctx.__nerviqIsGo;
|
|
116
|
+
ctx.__nerviqIsGo = hasProjectFile(ctx, /(^|\/)go\.mod$/i);
|
|
117
|
+
return ctx.__nerviqIsGo;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getPythonFiles(ctx) {
|
|
121
|
+
if (ctx.__nerviqPythonFiles) return ctx.__nerviqPythonFiles;
|
|
122
|
+
ctx.__nerviqPythonFiles = findProjectFiles(ctx, /\.py$/i);
|
|
123
|
+
return ctx.__nerviqPythonFiles;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getMainPythonFiles(ctx) {
|
|
127
|
+
if (ctx.__nerviqMainPythonFiles) return ctx.__nerviqMainPythonFiles;
|
|
128
|
+
ctx.__nerviqMainPythonFiles = getPythonFiles(ctx)
|
|
129
|
+
.filter(file => !/(^|\/)(tests?|__pycache__|migrations)\//i.test(file))
|
|
130
|
+
.filter(file => !/(^|\/)(test_[^/]+|conftest)\.py$/i.test(file))
|
|
131
|
+
.slice(0, 50);
|
|
132
|
+
return ctx.__nerviqMainPythonFiles;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getPythonProjectText(ctx) {
|
|
136
|
+
if (ctx.__nerviqPythonProjectText) return ctx.__nerviqPythonProjectText;
|
|
137
|
+
ctx.__nerviqPythonProjectText = [
|
|
138
|
+
readProjectFiles(ctx, /(^|\/)pyproject\.toml$/i),
|
|
139
|
+
readProjectFiles(ctx, /(^|\/)requirements[^/]*\.txt$/i),
|
|
140
|
+
readProjectFiles(ctx, /(^|\/)setup\.py$/i),
|
|
141
|
+
readProjectFiles(ctx, /(^|\/)setup\.cfg$/i),
|
|
142
|
+
readProjectFiles(ctx, /(^|\/)Pipfile$/i),
|
|
143
|
+
].filter(Boolean).join('\n');
|
|
144
|
+
return ctx.__nerviqPythonProjectText;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getGoFiles(ctx) {
|
|
148
|
+
if (ctx.__nerviqGoFiles) return ctx.__nerviqGoFiles;
|
|
149
|
+
ctx.__nerviqGoFiles = findProjectFiles(ctx, /\.go$/i);
|
|
150
|
+
return ctx.__nerviqGoFiles;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getMainGoFiles(ctx) {
|
|
154
|
+
if (ctx.__nerviqMainGoFiles) return ctx.__nerviqMainGoFiles;
|
|
155
|
+
ctx.__nerviqMainGoFiles = getGoFiles(ctx).filter(file => !/_test\.go$/i.test(file)).slice(0, 60);
|
|
156
|
+
return ctx.__nerviqMainGoFiles;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getWorkflowContent(ctx) {
|
|
160
|
+
if (ctx.__nerviqWorkflowContent !== undefined) return ctx.__nerviqWorkflowContent;
|
|
161
|
+
ctx.__nerviqWorkflowContent = readProjectFiles(ctx, /^\.github\/workflows\/.*\.ya?ml$/i);
|
|
162
|
+
return ctx.__nerviqWorkflowContent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getPreCommitContent(ctx) {
|
|
166
|
+
if (ctx.__nerviqPreCommitContent !== undefined) return ctx.__nerviqPreCommitContent;
|
|
167
|
+
ctx.__nerviqPreCommitContent = readProjectFiles(ctx, /(^|\/)\.pre-commit-config\.ya?ml$/i);
|
|
168
|
+
return ctx.__nerviqPreCommitContent;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getGoProjectText(ctx) {
|
|
172
|
+
if (ctx.__nerviqGoProjectText) return ctx.__nerviqGoProjectText;
|
|
173
|
+
ctx.__nerviqGoProjectText = [
|
|
174
|
+
readProjectFiles(ctx, /(^|\/)go\.mod$/i),
|
|
175
|
+
getWorkflowContent(ctx),
|
|
176
|
+
readProjectFiles(ctx, /(^|\/)Makefile$/),
|
|
177
|
+
getPreCommitContent(ctx),
|
|
178
|
+
getMainGoFiles(ctx).slice(0, 25).map(file => ctx.fileContent(file) || '').filter(Boolean).join('\n'),
|
|
179
|
+
].filter(Boolean).join('\n');
|
|
180
|
+
return ctx.__nerviqGoProjectText;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getGoInterfaceBlocks(ctx) {
|
|
184
|
+
if (ctx.__nerviqGoInterfaces) return ctx.__nerviqGoInterfaces;
|
|
185
|
+
const blocks = [];
|
|
186
|
+
for (const file of getMainGoFiles(ctx)) {
|
|
187
|
+
const content = ctx.fileContent(file) || '';
|
|
188
|
+
for (const match of content.matchAll(/type\s+\w+\s+interface\s*\{([\s\S]*?)\}/g)) {
|
|
189
|
+
blocks.push(match[1]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
ctx.__nerviqGoInterfaces = blocks;
|
|
193
|
+
return ctx.__nerviqGoInterfaces;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function countGoInterfaceMethods(block) {
|
|
197
|
+
return block
|
|
198
|
+
.split(/\r?\n/)
|
|
199
|
+
.map(line => line.trim())
|
|
200
|
+
.filter(line => line && !line.startsWith('//') && !line.startsWith('/*'))
|
|
201
|
+
.length;
|
|
202
|
+
}
|
|
203
|
+
|
|
13
204
|
const { containsEmbeddedSecret } = require('./secret-patterns');
|
|
14
205
|
const { attachSourceUrls } = require('./source-urls');
|
|
15
206
|
const { buildSupplementalChecks } = require('./supplemental-checks');
|
|
@@ -2914,15 +3105,9 @@ const TECHNIQUES = {
|
|
|
2914
3105
|
id: 110003,
|
|
2915
3106
|
name: 'Hook scripts handle exit codes correctly',
|
|
2916
3107
|
check: (ctx) => {
|
|
2917
|
-
const
|
|
2918
|
-
if (
|
|
2919
|
-
|
|
2920
|
-
if (hookFiles.length === 0) return null;
|
|
2921
|
-
const hasExitHandling = hookFiles.some(f => {
|
|
2922
|
-
const content = ctx.fileContent('.claude/hooks/' + f) || '';
|
|
2923
|
-
return /process\.exit|exit\s+[012]|sys\.exit|return\s+[012]/i.test(content);
|
|
2924
|
-
});
|
|
2925
|
-
return hasExitHandling;
|
|
3108
|
+
const hookContents = getClaudeHookContents(ctx);
|
|
3109
|
+
if (hookContents.length === 0) return null;
|
|
3110
|
+
return hookContents.some(content => /process\.exit|exit\s+[012]|sys\.exit|return\s+[012]/i.test(content));
|
|
2926
3111
|
},
|
|
2927
3112
|
impact: 'low', rating: 3, category: 'governance',
|
|
2928
3113
|
fix: 'Hooks should use explicit exit codes: 0=success, 1=warning, 2=block. See Claude Code docs.',
|
|
@@ -2930,6 +3115,71 @@ const TECHNIQUES = {
|
|
|
2930
3115
|
confidence: 0.7,
|
|
2931
3116
|
},
|
|
2932
3117
|
|
|
3118
|
+
loopSafetyBoundaries: {
|
|
3119
|
+
id: 110004,
|
|
3120
|
+
name: 'Loop safety boundaries configured',
|
|
3121
|
+
check: (ctx) => {
|
|
3122
|
+
const md = ctx.claudeMdContent() || '';
|
|
3123
|
+
const settings = ctx.fileContent('.claude/settings.json') || '';
|
|
3124
|
+
const hookContents = getClaudeHookContents(ctx).join('\n');
|
|
3125
|
+
const loopSafetyConfig = [md, settings, hookContents].filter(Boolean).join('\n');
|
|
3126
|
+
|
|
3127
|
+
return /max[-_ ]?turns|maxTurns|max[-_ ]?tokens|maxTokens|loop(?:[-_ ]?(?:limit|limits|safety|guard|budget|boundary|boundaries))|iteration(?:[-_ ]?(?:limit|limits|guard|budget|cap|caps|count|max(?:imum)?))/i.test(loopSafetyConfig);
|
|
3128
|
+
},
|
|
3129
|
+
impact: 'medium', rating: 4, category: 'governance',
|
|
3130
|
+
fix: 'Document loop safety limits such as maxTurns, maxTokens, or iteration caps in CLAUDE.md, settings, or hook guards.',
|
|
3131
|
+
template: null,
|
|
3132
|
+
confidence: 0.8,
|
|
3133
|
+
},
|
|
3134
|
+
|
|
3135
|
+
consistencyPassAtK: {
|
|
3136
|
+
id: 110005,
|
|
3137
|
+
name: 'Consistency/pass@k evaluation mentioned',
|
|
3138
|
+
check: (ctx) => {
|
|
3139
|
+
const md = ctx.claudeMdContent() || '';
|
|
3140
|
+
const configPaths = [
|
|
3141
|
+
'package.json',
|
|
3142
|
+
'jest.config.js',
|
|
3143
|
+
'jest.config.cjs',
|
|
3144
|
+
'jest.config.mjs',
|
|
3145
|
+
'vitest.config.js',
|
|
3146
|
+
'vitest.config.ts',
|
|
3147
|
+
'playwright.config.js',
|
|
3148
|
+
'playwright.config.ts',
|
|
3149
|
+
'pytest.ini',
|
|
3150
|
+
'pyproject.toml',
|
|
3151
|
+
'tox.ini',
|
|
3152
|
+
'.github/workflows/ci.yml',
|
|
3153
|
+
'.github/workflows/ci.yaml',
|
|
3154
|
+
'.github/workflows/test.yml',
|
|
3155
|
+
'.github/workflows/test.yaml',
|
|
3156
|
+
];
|
|
3157
|
+
const configContent = configPaths
|
|
3158
|
+
.map(file => ctx.fileContent(file) || '')
|
|
3159
|
+
.filter(Boolean)
|
|
3160
|
+
.join('\n');
|
|
3161
|
+
|
|
3162
|
+
return /pass@k|consistency|multiple runs?|reproducib/i.test(`${md}\n${configContent}`);
|
|
3163
|
+
},
|
|
3164
|
+
impact: 'low', rating: 3, category: 'quality',
|
|
3165
|
+
fix: 'Mention pass@k or consistency testing in CLAUDE.md or test configuration so repeated-run quality evaluation is explicit.',
|
|
3166
|
+
template: null,
|
|
3167
|
+
confidence: 0.7,
|
|
3168
|
+
},
|
|
3169
|
+
|
|
3170
|
+
instinctToSkillProgression: {
|
|
3171
|
+
id: 110006,
|
|
3172
|
+
name: 'Instinct-to-skill progression documented',
|
|
3173
|
+
check: (ctx) => {
|
|
3174
|
+
const md = ctx.claudeMdContent() || '';
|
|
3175
|
+
return /progressive learning|instinct[- ]to[- ]skill|instinct.{0,40}skill|skill.{0,40}instinct|graduated|phased approach/i.test(md);
|
|
3176
|
+
},
|
|
3177
|
+
impact: 'low', rating: 3, category: 'features',
|
|
3178
|
+
fix: 'Document a progressive learning path that turns repeated instincts into reusable skills or phased practices.',
|
|
3179
|
+
template: null,
|
|
3180
|
+
confidence: 0.7,
|
|
3181
|
+
},
|
|
3182
|
+
|
|
2933
3183
|
|
|
2934
3184
|
};
|
|
2935
3185
|
|