@nerviq/cli 1.2.5 → 1.2.6
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 +105 -1
- package/package.json +1 -1
- package/src/activity.js +5 -1
- package/src/audit.js +19 -1
- package/src/harmony/advisor.js +10 -0
- package/src/harmony/canon.js +23 -0
- package/src/harmony/sync.js +18 -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', '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,9 @@ 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
|
|
315
318
|
nerviq synergy-report Multi-agent amplification opportunities
|
|
316
319
|
nerviq convert --from X --to Y Convert configs between platforms
|
|
317
320
|
nerviq migrate --platform X Platform version migration helper
|
|
@@ -408,6 +411,7 @@ async function main() {
|
|
|
408
411
|
lite: flags.includes('--lite'),
|
|
409
412
|
snapshot: flags.includes('--snapshot'),
|
|
410
413
|
feedback: flags.includes('--feedback'),
|
|
414
|
+
fix: flags.includes('--fix'),
|
|
411
415
|
dryRun: flags.includes('--dry-run'),
|
|
412
416
|
threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
|
|
413
417
|
out: parsed.out,
|
|
@@ -862,6 +866,106 @@ async function main() {
|
|
|
862
866
|
process.on('SIGINT', closeServer);
|
|
863
867
|
process.on('SIGTERM', closeServer);
|
|
864
868
|
return;
|
|
869
|
+
} else if (normalizedCommand === 'harmony-audit') {
|
|
870
|
+
const { runHarmonyAudit } = require('../src/harmony/cli');
|
|
871
|
+
await runHarmonyAudit(options);
|
|
872
|
+
process.exit(0);
|
|
873
|
+
} else if (normalizedCommand === 'harmony-sync') {
|
|
874
|
+
const { previewHarmonySync, applyHarmonySync } = require('../src/harmony/sync');
|
|
875
|
+
const dir = options.dir || process.cwd();
|
|
876
|
+
|
|
877
|
+
if (options.fix) {
|
|
878
|
+
// Apply mode: write files
|
|
879
|
+
const result = applyHarmonySync(dir);
|
|
880
|
+
if (options.json) {
|
|
881
|
+
console.log(JSON.stringify(result, null, 2));
|
|
882
|
+
} else {
|
|
883
|
+
console.log('');
|
|
884
|
+
console.log('\x1b[1m Harmony Sync — Apply\x1b[0m');
|
|
885
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
886
|
+
console.log('');
|
|
887
|
+
if (result.applied.length === 0 && result.skipped.length === 0) {
|
|
888
|
+
console.log(' \x1b[32mAll platforms are already in sync. Nothing to apply.\x1b[0m');
|
|
889
|
+
} else {
|
|
890
|
+
for (const item of result.applied) {
|
|
891
|
+
console.log(` \x1b[32m✓\x1b[0m ${item.action.padEnd(8)} ${item.platform.padEnd(12)} ${item.path}`);
|
|
892
|
+
}
|
|
893
|
+
for (const item of result.skipped) {
|
|
894
|
+
const reason = typeof item === 'string' ? item : (item.reason || item.path);
|
|
895
|
+
console.log(` \x1b[33m⚠\x1b[0m skipped ${reason}`);
|
|
896
|
+
}
|
|
897
|
+
console.log('');
|
|
898
|
+
if (result.summary) {
|
|
899
|
+
console.log(` Files: ${result.summary.totalFiles} (${result.summary.creates} created, ${result.summary.patches} patched)`);
|
|
900
|
+
console.log(` Platforms: ${result.summary.platforms.join(', ')}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
904
|
+
console.log('');
|
|
905
|
+
for (const w of result.warnings) {
|
|
906
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
console.log('');
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
// Preview mode (dry run)
|
|
913
|
+
const plan = previewHarmonySync(dir);
|
|
914
|
+
if (options.json) {
|
|
915
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
916
|
+
} else {
|
|
917
|
+
console.log('');
|
|
918
|
+
console.log('\x1b[1m Harmony Sync — Preview\x1b[0m');
|
|
919
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
920
|
+
console.log('');
|
|
921
|
+
if (plan.files.length === 0) {
|
|
922
|
+
console.log(' \x1b[32mAll platforms are already in sync. No changes needed.\x1b[0m');
|
|
923
|
+
} else {
|
|
924
|
+
for (const file of plan.files) {
|
|
925
|
+
const actionColor = file.action === 'create' ? '\x1b[32m' : '\x1b[36m';
|
|
926
|
+
console.log(` ${actionColor}${file.action.padEnd(8)}\x1b[0m ${file.platform.padEnd(12)} ${file.path}`);
|
|
927
|
+
if (file.preview) {
|
|
928
|
+
console.log(` \x1b[2m${file.preview}\x1b[0m`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
console.log('');
|
|
932
|
+
console.log(` Total: ${plan.summary.totalFiles} file(s) — ${plan.summary.creates} create, ${plan.summary.patches} patch`);
|
|
933
|
+
console.log(` Platforms: ${plan.summary.platforms.join(', ')}`);
|
|
934
|
+
if (plan.summary.recommendedTrust) {
|
|
935
|
+
console.log(` Recommended trust: ${plan.summary.recommendedTrust}`);
|
|
936
|
+
}
|
|
937
|
+
console.log('');
|
|
938
|
+
console.log(' Run \x1b[1mnerviq harmony-sync --fix\x1b[0m to apply these changes.');
|
|
939
|
+
}
|
|
940
|
+
if (plan.warnings && plan.warnings.length > 0) {
|
|
941
|
+
console.log('');
|
|
942
|
+
for (const w of plan.warnings) {
|
|
943
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
console.log('');
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
process.exit(0);
|
|
950
|
+
} else if (normalizedCommand === 'harmony-drift') {
|
|
951
|
+
const { runHarmonyDrift } = require('../src/harmony/cli');
|
|
952
|
+
await runHarmonyDrift(options);
|
|
953
|
+
process.exit(0);
|
|
954
|
+
} else if (normalizedCommand === 'harmony-advise') {
|
|
955
|
+
const { runHarmonyAdvise } = require('../src/harmony/cli');
|
|
956
|
+
await runHarmonyAdvise(options);
|
|
957
|
+
process.exit(0);
|
|
958
|
+
} else if (normalizedCommand === 'harmony-watch') {
|
|
959
|
+
const { runHarmonyWatch } = require('../src/harmony/cli');
|
|
960
|
+
await runHarmonyWatch(options);
|
|
961
|
+
} else if (normalizedCommand === 'harmony-governance') {
|
|
962
|
+
const { runHarmonyGovernance } = require('../src/harmony/cli');
|
|
963
|
+
await runHarmonyGovernance(options);
|
|
964
|
+
process.exit(0);
|
|
965
|
+
} else if (normalizedCommand === 'synergy-report') {
|
|
966
|
+
// Placeholder — synergy report is referenced but may not be implemented yet
|
|
967
|
+
console.log('\n Synergy report: coming soon.\n');
|
|
968
|
+
process.exit(0);
|
|
865
969
|
} else if (normalizedCommand === 'doctor') {
|
|
866
970
|
const { runDoctor } = require('../src/doctor');
|
|
867
971
|
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
|
|
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/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/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
|
|