@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.
@@ -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/claudex-setup/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
- };
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: 'allow' }));
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'] = `When changing release, packaging, or workflow files:
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 if hooks were created but no settings exist
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) && !fs.existsSync(settingsPath)) {
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 settings = buildSettingsForProfile({
1246
+ const newSettings = buildSettingsForProfile({
1228
1247
  profileKey: options.profile || 'safe-write',
1229
1248
  hookFiles,
1230
1249
  mcpPackKeys: options.mcpPacks || [],
1231
1250
  });
1232
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
1233
- writtenFiles.push('.claude/settings.json');
1234
- log(` \x1b[32m✅\x1b[0m Created .claude/settings.json (hooks registered)`);
1235
- created++;
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