@nerviq/cli 1.8.6 → 1.8.8
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/README.md +349 -362
- package/bin/cli.js +131 -34
- package/package.json +1 -1
- package/src/aider/activity.js +226 -226
- package/src/audit.js +1443 -1443
- package/src/benchmark.js +346 -346
- package/src/codex/activity.js +324 -324
- package/src/context.js +27 -1
- package/src/convert.js +6 -2
- package/src/copilot/patch.js +238 -238
- package/src/cursor/patch.js +243 -243
- package/src/gemini/activity.js +402 -402
- package/src/gemini/patch.js +229 -229
- package/src/governance.js +583 -583
- package/src/harmony/audit.js +306 -306
- package/src/insights.js +119 -119
- package/src/{claudex-sync.json → nerviq-sync.json} +1 -1
- package/src/opencode/activity.js +286 -286
- package/src/setup.js +47 -9
- package/src/state-paths.js +85 -85
- package/src/techniques.js +5498 -5494
- package/src/windsurf/patch.js +231 -231
package/src/opencode/activity.js
CHANGED
|
@@ -1,286 +1,286 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenCode Repeat-Usage Surfaces — 6 repeat-usage surfaces
|
|
3
|
-
*
|
|
4
|
-
* Adapts the shared activity/snapshot backend for OpenCode platform.
|
|
5
|
-
* Provides: history, compare, trend, watch, feedback, insights.
|
|
6
|
-
*
|
|
7
|
-
* OpenCode snapshots are stored alongside Claude snapshots in
|
|
8
|
-
* .nerviq/snapshots/ (legacy: .claude/
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const {
|
|
13
|
-
readSnapshotIndex,
|
|
14
|
-
writeSnapshotArtifact,
|
|
15
|
-
exportTrendReport: sharedExportTrendReport,
|
|
16
|
-
recordRecommendationOutcome,
|
|
17
|
-
readOutcomeIndex,
|
|
18
|
-
summarizeOutcomeEntries,
|
|
19
|
-
} = require('../activity');
|
|
20
|
-
const { version } = require('../../package.json');
|
|
21
|
-
|
|
22
|
-
// --- History ---
|
|
23
|
-
|
|
24
|
-
function getOpenCodeHistory(dir, limit = 20) {
|
|
25
|
-
const entries = readSnapshotIndex(dir);
|
|
26
|
-
return entries
|
|
27
|
-
.filter(e => e.snapshotKind === 'audit' && (e.platform === 'opencode' || e.summary?.platform === 'opencode'))
|
|
28
|
-
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
29
|
-
.slice(0, limit);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatOpenCodeHistory(dir) {
|
|
33
|
-
const history = getOpenCodeHistory(dir, 10);
|
|
34
|
-
if (history.length === 0) {
|
|
35
|
-
return 'No OpenCode snapshots found. Run `npx nerviq --platform opencode --snapshot` to save one.';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const lines = ['OpenCode Score History (most recent first):', ''];
|
|
39
|
-
for (const entry of history) {
|
|
40
|
-
const date = entry.createdAt?.split('T')[0] || 'unknown';
|
|
41
|
-
const score = entry.summary?.score ?? '?';
|
|
42
|
-
const passed = entry.summary?.passed ?? '?';
|
|
43
|
-
const total = entry.summary?.checkCount ?? '?';
|
|
44
|
-
lines.push(` ${date} ${score}/100 (${passed}/${total} passing)`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const comparison = compareOpenCodeLatest(dir);
|
|
48
|
-
if (comparison) {
|
|
49
|
-
lines.push('');
|
|
50
|
-
const sign = comparison.delta.score >= 0 ? '+' : '';
|
|
51
|
-
lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
|
|
52
|
-
if (comparison.improvements.length > 0) lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
|
|
53
|
-
if (comparison.regressions.length > 0) lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return lines.join('\n');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- Compare ---
|
|
60
|
-
|
|
61
|
-
function compareOpenCodeLatest(dir) {
|
|
62
|
-
const audits = getOpenCodeHistory(dir, 2);
|
|
63
|
-
if (audits.length < 2) return null;
|
|
64
|
-
|
|
65
|
-
const current = audits[0];
|
|
66
|
-
const previous = audits[1];
|
|
67
|
-
|
|
68
|
-
const delta = {
|
|
69
|
-
score: (current.summary?.score || 0) - (previous.summary?.score || 0),
|
|
70
|
-
organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
|
|
71
|
-
passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const regressions = [];
|
|
75
|
-
const improvements = [];
|
|
76
|
-
const prevKeys = new Set(previous.summary?.topActionKeys || []);
|
|
77
|
-
const currKeys = new Set(current.summary?.topActionKeys || []);
|
|
78
|
-
for (const key of currKeys) { if (!prevKeys.has(key)) regressions.push(key); }
|
|
79
|
-
for (const key of prevKeys) { if (!currKeys.has(key)) improvements.push(key); }
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
platform: 'opencode',
|
|
83
|
-
current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
|
|
84
|
-
previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
|
|
85
|
-
delta,
|
|
86
|
-
regressions,
|
|
87
|
-
improvements,
|
|
88
|
-
trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// --- Trend ---
|
|
93
|
-
|
|
94
|
-
function exportOpenCodeTrendReport(dir) {
|
|
95
|
-
const history = getOpenCodeHistory(dir, 50);
|
|
96
|
-
if (history.length === 0) return null;
|
|
97
|
-
|
|
98
|
-
const comparison = compareOpenCodeLatest(dir);
|
|
99
|
-
const lines = [
|
|
100
|
-
'# OpenCode Setup Trend Report',
|
|
101
|
-
'',
|
|
102
|
-
`**Project:** ${path.basename(dir)}`,
|
|
103
|
-
'**Platform:** OpenCode',
|
|
104
|
-
`**Generated:** ${new Date().toISOString().split('T')[0]}`,
|
|
105
|
-
`**Snapshots:** ${history.length}`,
|
|
106
|
-
'',
|
|
107
|
-
'## Score History',
|
|
108
|
-
'',
|
|
109
|
-
'| Date | Score | Passed | Checks |',
|
|
110
|
-
'|------|-------|--------|--------|',
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
for (const entry of history) {
|
|
114
|
-
const date = entry.createdAt?.split('T')[0] || '?';
|
|
115
|
-
lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (comparison) {
|
|
119
|
-
lines.push('');
|
|
120
|
-
lines.push('## Latest Comparison');
|
|
121
|
-
lines.push('');
|
|
122
|
-
lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
|
|
123
|
-
lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
|
|
124
|
-
lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
|
|
125
|
-
lines.push(`- **Trend:** ${comparison.trend}`);
|
|
126
|
-
if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
|
|
127
|
-
if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (history.length >= 3) {
|
|
131
|
-
lines.push('');
|
|
132
|
-
lines.push('## Trend Chart');
|
|
133
|
-
lines.push('');
|
|
134
|
-
lines.push('```');
|
|
135
|
-
const scores = history.slice().reverse().map(e => e.summary?.score ?? 0);
|
|
136
|
-
const max = Math.max(...scores, 100);
|
|
137
|
-
const chartHeight = 10;
|
|
138
|
-
for (let row = chartHeight; row >= 0; row--) {
|
|
139
|
-
const threshold = (row / chartHeight) * max;
|
|
140
|
-
const rowLabel = String(Math.round(threshold)).padStart(3);
|
|
141
|
-
const bar = scores.map(s => s >= threshold ? '#' : ' ').join('');
|
|
142
|
-
lines.push(`${rowLabel} |${bar}`);
|
|
143
|
-
}
|
|
144
|
-
lines.push(` +${'─'.repeat(scores.length)}`);
|
|
145
|
-
lines.push('```');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
lines.push('');
|
|
149
|
-
lines.push('---');
|
|
150
|
-
lines.push(`*Generated by nerviq v${version} for OpenCode*`);
|
|
151
|
-
return lines.join('\n');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// --- Feedback ---
|
|
155
|
-
|
|
156
|
-
function recordOpenCodeFeedback(dir, payload) {
|
|
157
|
-
return recordRecommendationOutcome(dir, {
|
|
158
|
-
...payload,
|
|
159
|
-
source: payload.source || 'opencode-cli',
|
|
160
|
-
platform: 'opencode',
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function getOpenCodeFeedbackSummary(dir) {
|
|
165
|
-
const entries = readOutcomeIndex(dir)
|
|
166
|
-
.filter(e => e.source === 'opencode-cli' || e.platform === 'opencode');
|
|
167
|
-
return summarizeOutcomeEntries(entries);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function formatOpenCodeFeedback(dir) {
|
|
171
|
-
const summary = getOpenCodeFeedbackSummary(dir);
|
|
172
|
-
if (!summary || Object.keys(summary).length === 0) {
|
|
173
|
-
return 'No OpenCode feedback recorded yet.';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const lines = ['OpenCode Recommendation Feedback:', ''];
|
|
177
|
-
const entries = Array.isArray(summary) ? summary : Object.values(summary);
|
|
178
|
-
for (const entry of entries) {
|
|
179
|
-
const key = entry.key || 'unknown';
|
|
180
|
-
const accepted = entry.accepted || 0;
|
|
181
|
-
const rejected = entry.rejected || 0;
|
|
182
|
-
const total = entry.total || 0;
|
|
183
|
-
lines.push(` ${key}: ${accepted} accepted, ${rejected} rejected (${total} total)`);
|
|
184
|
-
}
|
|
185
|
-
return lines.join('\n');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// --- Insights ---
|
|
189
|
-
|
|
190
|
-
function generateOpenCodeInsights(dir) {
|
|
191
|
-
const history = getOpenCodeHistory(dir, 50);
|
|
192
|
-
const feedback = getOpenCodeFeedbackSummary(dir);
|
|
193
|
-
const insights = [];
|
|
194
|
-
|
|
195
|
-
if (history.length >= 3) {
|
|
196
|
-
const recentFailKeys = new Map();
|
|
197
|
-
for (const entry of history.slice(0, 5)) {
|
|
198
|
-
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
199
|
-
recentFailKeys.set(key, (recentFailKeys.get(key) || 0) + 1);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const [key, count] of recentFailKeys) {
|
|
203
|
-
if (count >= 3) {
|
|
204
|
-
insights.push({
|
|
205
|
-
type: 'persistent-failure',
|
|
206
|
-
severity: 'high',
|
|
207
|
-
key,
|
|
208
|
-
message: `Check ${key} has failed in ${count} of the last ${Math.min(history.length, 5)} audits.`,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (history.length >= 2) {
|
|
215
|
-
const scores = history.map(e => e.summary?.score ?? 0);
|
|
216
|
-
if (scores[0] < scores[1]) {
|
|
217
|
-
insights.push({
|
|
218
|
-
type: 'regression-pattern',
|
|
219
|
-
severity: 'medium',
|
|
220
|
-
message: `Score dropped from ${scores[1]} to ${scores[0]} in the most recent audit.`,
|
|
221
|
-
delta: scores[0] - scores[1],
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (history.length >= 5) {
|
|
227
|
-
const recentScores = history.slice(0, 5).map(e => e.summary?.score ?? 0);
|
|
228
|
-
const range = Math.max(...recentScores) - Math.min(...recentScores);
|
|
229
|
-
if (range <= 2) {
|
|
230
|
-
insights.push({
|
|
231
|
-
type: 'velocity-stall',
|
|
232
|
-
severity: 'low',
|
|
233
|
-
message: `Score has been flat (range: ${range} points) over the last 5 audits.`,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const feedbackEntries = Array.isArray(feedback) ? feedback : Object.values(feedback || {});
|
|
239
|
-
for (const entry of feedbackEntries) {
|
|
240
|
-
if (entry.rejected > entry.accepted && entry.total >= 2) {
|
|
241
|
-
insights.push({
|
|
242
|
-
type: 'feedback-signal',
|
|
243
|
-
severity: 'medium',
|
|
244
|
-
key: entry.key,
|
|
245
|
-
message: `Recommendation ${entry.key} has been rejected more than accepted (${entry.rejected}/${entry.total}).`,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
platform: 'opencode',
|
|
252
|
-
generatedAt: new Date().toISOString(),
|
|
253
|
-
snapshotCount: history.length,
|
|
254
|
-
feedbackCount: feedbackEntries.length,
|
|
255
|
-
insights,
|
|
256
|
-
summary: insights.length === 0
|
|
257
|
-
? 'No actionable insights detected. Keep running audits to build pattern data.'
|
|
258
|
-
: `${insights.length} insight(s) detected across ${history.length} snapshots.`,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function formatOpenCodeInsights(dir) {
|
|
263
|
-
const result = generateOpenCodeInsights(dir);
|
|
264
|
-
if (result.insights.length === 0) return result.summary;
|
|
265
|
-
|
|
266
|
-
const lines = ['OpenCode Insights:', ''];
|
|
267
|
-
for (const insight of result.insights) {
|
|
268
|
-
const severity = insight.severity.toUpperCase();
|
|
269
|
-
lines.push(` [${severity}] ${insight.message}`);
|
|
270
|
-
}
|
|
271
|
-
lines.push('');
|
|
272
|
-
lines.push(result.summary);
|
|
273
|
-
return lines.join('\n');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
module.exports = {
|
|
277
|
-
getOpenCodeHistory,
|
|
278
|
-
formatOpenCodeHistory,
|
|
279
|
-
compareOpenCodeLatest,
|
|
280
|
-
exportOpenCodeTrendReport,
|
|
281
|
-
recordOpenCodeFeedback,
|
|
282
|
-
getOpenCodeFeedbackSummary,
|
|
283
|
-
formatOpenCodeFeedback,
|
|
284
|
-
generateOpenCodeInsights,
|
|
285
|
-
formatOpenCodeInsights,
|
|
286
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Repeat-Usage Surfaces — 6 repeat-usage surfaces
|
|
3
|
+
*
|
|
4
|
+
* Adapts the shared activity/snapshot backend for OpenCode platform.
|
|
5
|
+
* Provides: history, compare, trend, watch, feedback, insights.
|
|
6
|
+
*
|
|
7
|
+
* OpenCode snapshots are stored alongside Claude snapshots in
|
|
8
|
+
* .nerviq/snapshots/ (legacy: .claude/nerviq-cli/snapshots/) but filtered by platform='opencode'.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const {
|
|
13
|
+
readSnapshotIndex,
|
|
14
|
+
writeSnapshotArtifact,
|
|
15
|
+
exportTrendReport: sharedExportTrendReport,
|
|
16
|
+
recordRecommendationOutcome,
|
|
17
|
+
readOutcomeIndex,
|
|
18
|
+
summarizeOutcomeEntries,
|
|
19
|
+
} = require('../activity');
|
|
20
|
+
const { version } = require('../../package.json');
|
|
21
|
+
|
|
22
|
+
// --- History ---
|
|
23
|
+
|
|
24
|
+
function getOpenCodeHistory(dir, limit = 20) {
|
|
25
|
+
const entries = readSnapshotIndex(dir);
|
|
26
|
+
return entries
|
|
27
|
+
.filter(e => e.snapshotKind === 'audit' && (e.platform === 'opencode' || e.summary?.platform === 'opencode'))
|
|
28
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatOpenCodeHistory(dir) {
|
|
33
|
+
const history = getOpenCodeHistory(dir, 10);
|
|
34
|
+
if (history.length === 0) {
|
|
35
|
+
return 'No OpenCode snapshots found. Run `npx nerviq --platform opencode --snapshot` to save one.';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines = ['OpenCode Score History (most recent first):', ''];
|
|
39
|
+
for (const entry of history) {
|
|
40
|
+
const date = entry.createdAt?.split('T')[0] || 'unknown';
|
|
41
|
+
const score = entry.summary?.score ?? '?';
|
|
42
|
+
const passed = entry.summary?.passed ?? '?';
|
|
43
|
+
const total = entry.summary?.checkCount ?? '?';
|
|
44
|
+
lines.push(` ${date} ${score}/100 (${passed}/${total} passing)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const comparison = compareOpenCodeLatest(dir);
|
|
48
|
+
if (comparison) {
|
|
49
|
+
lines.push('');
|
|
50
|
+
const sign = comparison.delta.score >= 0 ? '+' : '';
|
|
51
|
+
lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
|
|
52
|
+
if (comparison.improvements.length > 0) lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
|
|
53
|
+
if (comparison.regressions.length > 0) lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Compare ---
|
|
60
|
+
|
|
61
|
+
function compareOpenCodeLatest(dir) {
|
|
62
|
+
const audits = getOpenCodeHistory(dir, 2);
|
|
63
|
+
if (audits.length < 2) return null;
|
|
64
|
+
|
|
65
|
+
const current = audits[0];
|
|
66
|
+
const previous = audits[1];
|
|
67
|
+
|
|
68
|
+
const delta = {
|
|
69
|
+
score: (current.summary?.score || 0) - (previous.summary?.score || 0),
|
|
70
|
+
organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
|
|
71
|
+
passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const regressions = [];
|
|
75
|
+
const improvements = [];
|
|
76
|
+
const prevKeys = new Set(previous.summary?.topActionKeys || []);
|
|
77
|
+
const currKeys = new Set(current.summary?.topActionKeys || []);
|
|
78
|
+
for (const key of currKeys) { if (!prevKeys.has(key)) regressions.push(key); }
|
|
79
|
+
for (const key of prevKeys) { if (!currKeys.has(key)) improvements.push(key); }
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
platform: 'opencode',
|
|
83
|
+
current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
|
|
84
|
+
previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
|
|
85
|
+
delta,
|
|
86
|
+
regressions,
|
|
87
|
+
improvements,
|
|
88
|
+
trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Trend ---
|
|
93
|
+
|
|
94
|
+
function exportOpenCodeTrendReport(dir) {
|
|
95
|
+
const history = getOpenCodeHistory(dir, 50);
|
|
96
|
+
if (history.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
const comparison = compareOpenCodeLatest(dir);
|
|
99
|
+
const lines = [
|
|
100
|
+
'# OpenCode Setup Trend Report',
|
|
101
|
+
'',
|
|
102
|
+
`**Project:** ${path.basename(dir)}`,
|
|
103
|
+
'**Platform:** OpenCode',
|
|
104
|
+
`**Generated:** ${new Date().toISOString().split('T')[0]}`,
|
|
105
|
+
`**Snapshots:** ${history.length}`,
|
|
106
|
+
'',
|
|
107
|
+
'## Score History',
|
|
108
|
+
'',
|
|
109
|
+
'| Date | Score | Passed | Checks |',
|
|
110
|
+
'|------|-------|--------|--------|',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const entry of history) {
|
|
114
|
+
const date = entry.createdAt?.split('T')[0] || '?';
|
|
115
|
+
lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (comparison) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('## Latest Comparison');
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
|
|
123
|
+
lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
|
|
124
|
+
lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
|
|
125
|
+
lines.push(`- **Trend:** ${comparison.trend}`);
|
|
126
|
+
if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
|
|
127
|
+
if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (history.length >= 3) {
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push('## Trend Chart');
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push('```');
|
|
135
|
+
const scores = history.slice().reverse().map(e => e.summary?.score ?? 0);
|
|
136
|
+
const max = Math.max(...scores, 100);
|
|
137
|
+
const chartHeight = 10;
|
|
138
|
+
for (let row = chartHeight; row >= 0; row--) {
|
|
139
|
+
const threshold = (row / chartHeight) * max;
|
|
140
|
+
const rowLabel = String(Math.round(threshold)).padStart(3);
|
|
141
|
+
const bar = scores.map(s => s >= threshold ? '#' : ' ').join('');
|
|
142
|
+
lines.push(`${rowLabel} |${bar}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(` +${'─'.repeat(scores.length)}`);
|
|
145
|
+
lines.push('```');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('---');
|
|
150
|
+
lines.push(`*Generated by nerviq v${version} for OpenCode*`);
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Feedback ---
|
|
155
|
+
|
|
156
|
+
function recordOpenCodeFeedback(dir, payload) {
|
|
157
|
+
return recordRecommendationOutcome(dir, {
|
|
158
|
+
...payload,
|
|
159
|
+
source: payload.source || 'opencode-cli',
|
|
160
|
+
platform: 'opencode',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getOpenCodeFeedbackSummary(dir) {
|
|
165
|
+
const entries = readOutcomeIndex(dir)
|
|
166
|
+
.filter(e => e.source === 'opencode-cli' || e.platform === 'opencode');
|
|
167
|
+
return summarizeOutcomeEntries(entries);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatOpenCodeFeedback(dir) {
|
|
171
|
+
const summary = getOpenCodeFeedbackSummary(dir);
|
|
172
|
+
if (!summary || Object.keys(summary).length === 0) {
|
|
173
|
+
return 'No OpenCode feedback recorded yet.';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lines = ['OpenCode Recommendation Feedback:', ''];
|
|
177
|
+
const entries = Array.isArray(summary) ? summary : Object.values(summary);
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const key = entry.key || 'unknown';
|
|
180
|
+
const accepted = entry.accepted || 0;
|
|
181
|
+
const rejected = entry.rejected || 0;
|
|
182
|
+
const total = entry.total || 0;
|
|
183
|
+
lines.push(` ${key}: ${accepted} accepted, ${rejected} rejected (${total} total)`);
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Insights ---
|
|
189
|
+
|
|
190
|
+
function generateOpenCodeInsights(dir) {
|
|
191
|
+
const history = getOpenCodeHistory(dir, 50);
|
|
192
|
+
const feedback = getOpenCodeFeedbackSummary(dir);
|
|
193
|
+
const insights = [];
|
|
194
|
+
|
|
195
|
+
if (history.length >= 3) {
|
|
196
|
+
const recentFailKeys = new Map();
|
|
197
|
+
for (const entry of history.slice(0, 5)) {
|
|
198
|
+
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
199
|
+
recentFailKeys.set(key, (recentFailKeys.get(key) || 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const [key, count] of recentFailKeys) {
|
|
203
|
+
if (count >= 3) {
|
|
204
|
+
insights.push({
|
|
205
|
+
type: 'persistent-failure',
|
|
206
|
+
severity: 'high',
|
|
207
|
+
key,
|
|
208
|
+
message: `Check ${key} has failed in ${count} of the last ${Math.min(history.length, 5)} audits.`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (history.length >= 2) {
|
|
215
|
+
const scores = history.map(e => e.summary?.score ?? 0);
|
|
216
|
+
if (scores[0] < scores[1]) {
|
|
217
|
+
insights.push({
|
|
218
|
+
type: 'regression-pattern',
|
|
219
|
+
severity: 'medium',
|
|
220
|
+
message: `Score dropped from ${scores[1]} to ${scores[0]} in the most recent audit.`,
|
|
221
|
+
delta: scores[0] - scores[1],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (history.length >= 5) {
|
|
227
|
+
const recentScores = history.slice(0, 5).map(e => e.summary?.score ?? 0);
|
|
228
|
+
const range = Math.max(...recentScores) - Math.min(...recentScores);
|
|
229
|
+
if (range <= 2) {
|
|
230
|
+
insights.push({
|
|
231
|
+
type: 'velocity-stall',
|
|
232
|
+
severity: 'low',
|
|
233
|
+
message: `Score has been flat (range: ${range} points) over the last 5 audits.`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const feedbackEntries = Array.isArray(feedback) ? feedback : Object.values(feedback || {});
|
|
239
|
+
for (const entry of feedbackEntries) {
|
|
240
|
+
if (entry.rejected > entry.accepted && entry.total >= 2) {
|
|
241
|
+
insights.push({
|
|
242
|
+
type: 'feedback-signal',
|
|
243
|
+
severity: 'medium',
|
|
244
|
+
key: entry.key,
|
|
245
|
+
message: `Recommendation ${entry.key} has been rejected more than accepted (${entry.rejected}/${entry.total}).`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
platform: 'opencode',
|
|
252
|
+
generatedAt: new Date().toISOString(),
|
|
253
|
+
snapshotCount: history.length,
|
|
254
|
+
feedbackCount: feedbackEntries.length,
|
|
255
|
+
insights,
|
|
256
|
+
summary: insights.length === 0
|
|
257
|
+
? 'No actionable insights detected. Keep running audits to build pattern data.'
|
|
258
|
+
: `${insights.length} insight(s) detected across ${history.length} snapshots.`,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatOpenCodeInsights(dir) {
|
|
263
|
+
const result = generateOpenCodeInsights(dir);
|
|
264
|
+
if (result.insights.length === 0) return result.summary;
|
|
265
|
+
|
|
266
|
+
const lines = ['OpenCode Insights:', ''];
|
|
267
|
+
for (const insight of result.insights) {
|
|
268
|
+
const severity = insight.severity.toUpperCase();
|
|
269
|
+
lines.push(` [${severity}] ${insight.message}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push(result.summary);
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
getOpenCodeHistory,
|
|
278
|
+
formatOpenCodeHistory,
|
|
279
|
+
compareOpenCodeLatest,
|
|
280
|
+
exportOpenCodeTrendReport,
|
|
281
|
+
recordOpenCodeFeedback,
|
|
282
|
+
getOpenCodeFeedbackSummary,
|
|
283
|
+
formatOpenCodeFeedback,
|
|
284
|
+
generateOpenCodeInsights,
|
|
285
|
+
formatOpenCodeInsights,
|
|
286
|
+
};
|
package/src/setup.js
CHANGED
|
@@ -810,7 +810,7 @@ process.stdin.on('end', () => {
|
|
|
810
810
|
console.log(JSON.stringify({ decision: 'allow' }));
|
|
811
811
|
}
|
|
812
812
|
} catch (e) {
|
|
813
|
-
console.log(JSON.stringify({ decision: '
|
|
813
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Hook error - blocking for safety' }));
|
|
814
814
|
}
|
|
815
815
|
});
|
|
816
816
|
`,
|
|
@@ -1062,7 +1062,14 @@ Prepare a release candidate for: $ARGUMENTS
|
|
|
1062
1062
|
- Mock external dependencies, not internal logic
|
|
1063
1063
|
- Include both happy path and edge case tests
|
|
1064
1064
|
`;
|
|
1065
|
-
rules['repository.md'] =
|
|
1065
|
+
rules['repository.md'] = hasPython
|
|
1066
|
+
? `When changing release, packaging, or workflow files:
|
|
1067
|
+
- Keep pyproject.toml (or requirements.txt), CHANGELOG.md, README.md, and docs in sync
|
|
1068
|
+
- Prefer tagged release references over floating branch references in public docs
|
|
1069
|
+
- Preserve backward compatibility in CLI flags where practical
|
|
1070
|
+
- Any automation that writes files must document rollback expectations
|
|
1071
|
+
`
|
|
1072
|
+
: `When changing release, packaging, or workflow files:
|
|
1066
1073
|
- Keep package.json, CHANGELOG.md, README.md, and docs in sync
|
|
1067
1074
|
- Prefer tagged release references over floating branch references in public docs
|
|
1068
1075
|
- Preserve backward compatibility in CLI flags where practical
|
|
@@ -1115,6 +1122,18 @@ async function setup(options) {
|
|
|
1115
1122
|
if (options.platform === 'codex') {
|
|
1116
1123
|
return setupCodex(options);
|
|
1117
1124
|
}
|
|
1125
|
+
if (options.platform === 'windsurf') {
|
|
1126
|
+
const { setupWindsurf } = require('./windsurf/setup');
|
|
1127
|
+
return setupWindsurf(options);
|
|
1128
|
+
}
|
|
1129
|
+
if (options.platform === 'aider') {
|
|
1130
|
+
const { setupAider } = require('./aider/setup');
|
|
1131
|
+
return setupAider(options);
|
|
1132
|
+
}
|
|
1133
|
+
if (options.platform === 'cursor') {
|
|
1134
|
+
const { setupCursor } = require('./cursor/setup');
|
|
1135
|
+
return setupCursor(options);
|
|
1136
|
+
}
|
|
1118
1137
|
|
|
1119
1138
|
const ctx = new ProjectContext(options.dir);
|
|
1120
1139
|
const stacks = ctx.detectStacks(STACKS);
|
|
@@ -1218,21 +1237,40 @@ async function setup(options) {
|
|
|
1218
1237
|
}
|
|
1219
1238
|
}
|
|
1220
1239
|
|
|
1221
|
-
// Auto-register hooks in settings
|
|
1240
|
+
// Auto-register hooks in settings — always merge hooks into settings.json
|
|
1222
1241
|
const hooksDir = path.join(options.dir, '.claude/hooks');
|
|
1223
1242
|
const settingsPath = path.join(options.dir, '.claude/settings.json');
|
|
1224
|
-
if (fs.existsSync(hooksDir)
|
|
1243
|
+
if (fs.existsSync(hooksDir)) {
|
|
1225
1244
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
|
|
1226
1245
|
if (hookFiles.length > 0) {
|
|
1227
|
-
const
|
|
1246
|
+
const newSettings = buildSettingsForProfile({
|
|
1228
1247
|
profileKey: options.profile || 'safe-write',
|
|
1229
1248
|
hookFiles,
|
|
1230
1249
|
mcpPackKeys: options.mcpPacks || [],
|
|
1231
1250
|
});
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1251
|
+
// Merge new settings into existing settings.json, preserving all fields
|
|
1252
|
+
let existingSettings = {};
|
|
1253
|
+
if (fs.existsSync(settingsPath)) {
|
|
1254
|
+
try {
|
|
1255
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1256
|
+
} catch (_) {
|
|
1257
|
+
// If settings.json is malformed, start fresh
|
|
1258
|
+
existingSettings = {};
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// Merge all fields from newSettings into existing, preserving existing values
|
|
1262
|
+
if (newSettings.hooks) existingSettings.hooks = newSettings.hooks;
|
|
1263
|
+
if (newSettings.permissions) existingSettings.permissions = { ...existingSettings.permissions, ...newSettings.permissions };
|
|
1264
|
+
if (newSettings.mcpServers) existingSettings.mcpServers = { ...existingSettings.mcpServers, ...newSettings.mcpServers };
|
|
1265
|
+
if (newSettings.nerviqSetup) existingSettings.nerviqSetup = { ...existingSettings.nerviqSetup, ...newSettings.nerviqSetup };
|
|
1266
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
|
|
1267
|
+
if (!writtenFiles.includes('.claude/settings.json') && !preservedFiles.includes('.claude/settings.json')) {
|
|
1268
|
+
writtenFiles.push('.claude/settings.json');
|
|
1269
|
+
log(` \x1b[32m✅\x1b[0m Updated .claude/settings.json (hooks registered)`);
|
|
1270
|
+
created++;
|
|
1271
|
+
} else {
|
|
1272
|
+
log(` \x1b[32m✅\x1b[0m Merged hooks into existing .claude/settings.json`);
|
|
1273
|
+
}
|
|
1236
1274
|
}
|
|
1237
1275
|
}
|
|
1238
1276
|
|