@nerviq/cli 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -23
- package/README.md +2 -2
- package/bin/cli.js +130 -130
- package/package.json +1 -1
- package/src/activity.js +1039 -1039
- package/src/adoption-advisor.js +299 -299
- package/src/aider/config-parser.js +166 -166
- package/src/aider/context.js +158 -158
- package/src/aider/deep-review.js +316 -316
- package/src/aider/domain-packs.js +303 -303
- package/src/aider/freshness.js +93 -93
- package/src/aider/governance.js +253 -253
- package/src/aider/interactive.js +334 -334
- package/src/aider/mcp-packs.js +329 -329
- package/src/aider/patch.js +214 -214
- package/src/aider/plans.js +186 -186
- package/src/aider/premium.js +360 -360
- package/src/aider/setup.js +404 -404
- package/src/aider/techniques.js +16 -16
- package/src/analyze.js +951 -951
- package/src/anti-patterns.js +485 -485
- package/src/audit/instruction-files.js +180 -180
- package/src/audit/recommendations.js +577 -577
- package/src/auto-suggest.js +154 -154
- package/src/badge.js +13 -13
- package/src/behavioral-drift.js +801 -801
- package/src/benchmark.js +67 -67
- package/src/catalog.js +103 -103
- package/src/certification.js +128 -128
- package/src/codex/config-parser.js +183 -183
- package/src/codex/context.js +223 -223
- package/src/codex/deep-review.js +493 -493
- package/src/codex/domain-packs.js +394 -394
- package/src/codex/freshness.js +84 -84
- package/src/codex/governance.js +192 -192
- package/src/codex/interactive.js +618 -618
- package/src/codex/mcp-packs.js +914 -914
- package/src/codex/patch.js +209 -209
- package/src/codex/plans.js +251 -251
- package/src/codex/premium.js +614 -614
- package/src/codex/setup.js +591 -591
- package/src/context.js +320 -320
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/deep-review.js +346 -346
- package/src/copilot/domain-packs.js +372 -372
- package/src/copilot/freshness.js +57 -57
- package/src/copilot/governance.js +222 -222
- package/src/copilot/interactive.js +406 -406
- package/src/copilot/mcp-packs.js +826 -826
- package/src/copilot/plans.js +253 -253
- package/src/copilot/premium.js +451 -451
- package/src/copilot/setup.js +488 -488
- package/src/cost-tracking.js +61 -61
- package/src/cursor/activity.js +301 -301
- package/src/cursor/config-parser.js +265 -265
- package/src/cursor/context.js +256 -256
- package/src/cursor/deep-review.js +334 -334
- package/src/cursor/domain-packs.js +368 -368
- package/src/cursor/freshness.js +65 -65
- package/src/cursor/governance.js +229 -229
- package/src/cursor/interactive.js +391 -391
- package/src/cursor/mcp-packs.js +828 -828
- package/src/cursor/plans.js +254 -254
- package/src/cursor/premium.js +469 -469
- package/src/cursor/setup.js +488 -488
- package/src/dashboard.js +493 -493
- package/src/deep-review.js +428 -428
- package/src/deprecation.js +98 -98
- package/src/diff-only.js +280 -280
- package/src/doctor.js +119 -119
- package/src/domain-pack-expansion.js +1033 -1033
- package/src/domain-packs.js +387 -387
- package/src/feedback.js +178 -178
- package/src/fix-engine.js +783 -783
- package/src/fix-prompts.js +122 -122
- package/src/formatters/sarif.js +115 -115
- package/src/freshness.js +74 -74
- package/src/gemini/config-parser.js +275 -275
- package/src/gemini/context.js +221 -221
- package/src/gemini/deep-review.js +559 -559
- package/src/gemini/domain-packs.js +393 -393
- package/src/gemini/freshness.js +66 -66
- package/src/gemini/governance.js +201 -201
- package/src/gemini/interactive.js +860 -860
- package/src/gemini/mcp-packs.js +915 -915
- package/src/gemini/plans.js +269 -269
- package/src/gemini/premium.js +760 -760
- package/src/gemini/setup.js +692 -692
- package/src/gemini/techniques.js +14 -14
- package/src/governance.js +72 -72
- package/src/harmony/add.js +68 -68
- package/src/harmony/advisor.js +333 -333
- package/src/harmony/canon.js +565 -565
- package/src/harmony/cli.js +591 -591
- package/src/harmony/drift.js +401 -401
- package/src/harmony/governance.js +313 -313
- package/src/harmony/memory.js +239 -239
- package/src/harmony/sync.js +475 -475
- package/src/harmony/watch.js +370 -370
- package/src/hook-validation.js +342 -342
- package/src/index.js +271 -271
- package/src/init.js +184 -184
- package/src/instruction-surfaces.js +185 -185
- package/src/integrations.js +144 -144
- package/src/interactive.js +118 -118
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/mcp-packs.js +830 -830
- package/src/mcp-server.js +726 -726
- package/src/mcp-validation.js +337 -337
- package/src/nerviq-sync.json +7 -7
- package/src/opencode/config-parser.js +109 -109
- package/src/opencode/context.js +247 -247
- package/src/opencode/deep-review.js +313 -313
- package/src/opencode/domain-packs.js +262 -262
- package/src/opencode/freshness.js +66 -66
- package/src/opencode/governance.js +159 -159
- package/src/opencode/interactive.js +392 -392
- package/src/opencode/mcp-packs.js +705 -705
- package/src/opencode/patch.js +184 -184
- package/src/opencode/plans.js +231 -231
- package/src/opencode/premium.js +413 -413
- package/src/opencode/setup.js +449 -449
- package/src/opencode/techniques.js +27 -27
- package/src/operating-profile.js +574 -574
- package/src/org.js +152 -152
- package/src/permission-rules.js +218 -218
- package/src/plans.js +839 -839
- package/src/platform-change-manifest.js +86 -86
- package/src/plugins.js +110 -110
- package/src/policy-layers.js +210 -210
- package/src/profiles.js +124 -124
- package/src/prompt-injection.js +74 -74
- package/src/public-api.js +173 -173
- package/src/recommendation-rules.js +84 -84
- package/src/repo-archetype.js +386 -386
- package/src/secret-patterns.js +39 -39
- package/src/server.js +527 -527
- package/src/setup/analysis.js +607 -607
- package/src/setup/runtime.js +172 -172
- package/src/setup.js +677 -677
- package/src/shared/capabilities.js +194 -194
- package/src/source-urls.js +132 -132
- package/src/stack-checks.js +565 -565
- package/src/supplemental-checks.js +13 -13
- package/src/synergy/adaptive.js +261 -261
- package/src/synergy/compensation.js +137 -137
- package/src/synergy/evidence.js +193 -193
- package/src/synergy/learning.js +199 -199
- package/src/synergy/patterns.js +227 -227
- package/src/synergy/ranking.js +83 -83
- package/src/synergy/report.js +165 -165
- package/src/synergy/routing.js +146 -146
- package/src/techniques/api.js +407 -407
- package/src/techniques/automation.js +316 -316
- package/src/techniques/compliance.js +257 -257
- package/src/techniques/hygiene.js +294 -294
- package/src/techniques/instructions.js +243 -243
- package/src/techniques/observability.js +226 -226
- package/src/techniques/optimization.js +142 -142
- package/src/techniques/quality.js +318 -318
- package/src/techniques/security.js +237 -237
- package/src/techniques/shared.js +443 -443
- package/src/techniques/stacks.js +2294 -2294
- package/src/techniques/tools.js +106 -106
- package/src/techniques/workflow.js +413 -413
- package/src/techniques.js +81 -81
- package/src/terminology.js +73 -73
- package/src/token-estimate.js +35 -35
- package/src/usage-patterns.js +99 -99
- package/src/verification-metadata.js +145 -145
- package/src/watch.js +247 -247
- package/src/windsurf/activity.js +302 -302
- package/src/windsurf/config-parser.js +267 -267
- package/src/windsurf/context.js +249 -249
- package/src/windsurf/deep-review.js +337 -337
- package/src/windsurf/domain-packs.js +370 -370
- package/src/windsurf/freshness.js +36 -36
- package/src/windsurf/governance.js +231 -231
- package/src/windsurf/interactive.js +388 -388
- package/src/windsurf/mcp-packs.js +792 -792
- package/src/windsurf/plans.js +247 -247
- package/src/windsurf/premium.js +468 -468
- package/src/windsurf/setup.js +471 -471
- package/src/windsurf/techniques.js +17 -17
- package/src/workspace.js +375 -375
package/src/harmony/watch.js
CHANGED
|
@@ -1,370 +1,370 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* H6. Harmony Watch - Continuous Adaptation Loop
|
|
3
|
-
*
|
|
4
|
-
* Monitors repo for changes across ALL platform config files and triggers
|
|
5
|
-
* cross-platform drift detection when changes occur.
|
|
6
|
-
*
|
|
7
|
-
* Extends the watch pattern from src/watch.js to cover all supported platforms.
|
|
8
|
-
* Zero external dependencies.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
const COLORS = {
|
|
15
|
-
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
16
|
-
green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m', red: '\x1b[31m',
|
|
17
|
-
};
|
|
18
|
-
const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
19
|
-
|
|
20
|
-
// ─── Platform config file paths to watch ──────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
const PLATFORM_WATCH_FILES = [
|
|
23
|
-
// Claude
|
|
24
|
-
'CLAUDE.md',
|
|
25
|
-
'.claude/settings.json',
|
|
26
|
-
'.claude/settings.local.json',
|
|
27
|
-
// Codex
|
|
28
|
-
'AGENTS.md',
|
|
29
|
-
'codex.toml',
|
|
30
|
-
'.codex/config.toml',
|
|
31
|
-
// Gemini
|
|
32
|
-
'GEMINI.md',
|
|
33
|
-
'.gemini/settings.json',
|
|
34
|
-
// Copilot
|
|
35
|
-
'.github/copilot-instructions.md',
|
|
36
|
-
'.github/copilot-review-instructions.md',
|
|
37
|
-
// Cursor
|
|
38
|
-
'.cursorrules',
|
|
39
|
-
// Windsurf
|
|
40
|
-
'.windsurfrules',
|
|
41
|
-
// Aider
|
|
42
|
-
'.aider.conf.yml',
|
|
43
|
-
'.aiderignore',
|
|
44
|
-
// OpenCode
|
|
45
|
-
'opencode.json',
|
|
46
|
-
// Shared
|
|
47
|
-
'.gitignore',
|
|
48
|
-
'package.json',
|
|
49
|
-
'tsconfig.json',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const PLATFORM_WATCH_DIRS = [
|
|
53
|
-
'.claude',
|
|
54
|
-
'.claude/commands',
|
|
55
|
-
'.claude/rules',
|
|
56
|
-
'.claude/agents',
|
|
57
|
-
'.claude/skills',
|
|
58
|
-
'.codex',
|
|
59
|
-
'.gemini',
|
|
60
|
-
'.github',
|
|
61
|
-
'.cursor',
|
|
62
|
-
'.cursor/rules',
|
|
63
|
-
'.windsurf',
|
|
64
|
-
'.windsurf/rules',
|
|
65
|
-
'.opencode',
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// ─── fs.watch helpers (mirror pattern from src/watch.js) ──────────────────────
|
|
69
|
-
|
|
70
|
-
function supportsNativeRecursiveWatch(platform) {
|
|
71
|
-
return (platform || process.platform) === 'win32' || (platform || process.platform) === 'darwin';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function statIfExists(fullPath) {
|
|
75
|
-
try {
|
|
76
|
-
return fs.statSync(fullPath);
|
|
77
|
-
} catch (_e) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function listRecursiveDirectories(dir) {
|
|
83
|
-
const directories = [dir];
|
|
84
|
-
let entries = [];
|
|
85
|
-
try {
|
|
86
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
-
} catch (_e) {
|
|
88
|
-
return directories;
|
|
89
|
-
}
|
|
90
|
-
for (const entry of entries) {
|
|
91
|
-
if (entry.isDirectory()) {
|
|
92
|
-
directories.push(...listRecursiveDirectories(path.join(dir, entry.name)));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return directories;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Build a watch plan that covers all platform config files and directories.
|
|
100
|
-
*/
|
|
101
|
-
function buildHarmonyWatchPlan(rootDir, platform) {
|
|
102
|
-
const plan = [];
|
|
103
|
-
const seen = new Set();
|
|
104
|
-
const recursiveSupported = supportsNativeRecursiveWatch(platform);
|
|
105
|
-
|
|
106
|
-
const addTarget = (fullPath, recursive, source) => {
|
|
107
|
-
const resolved = path.resolve(fullPath);
|
|
108
|
-
const key = `${resolved}|${recursive}`;
|
|
109
|
-
if (seen.has(key)) return;
|
|
110
|
-
seen.add(key);
|
|
111
|
-
plan.push({ path: resolved, recursive, source });
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// Watch repo root for top-level file creations
|
|
115
|
-
addTarget(rootDir, false, 'repo-root');
|
|
116
|
-
|
|
117
|
-
// Watch individual platform config files
|
|
118
|
-
for (const watchPath of PLATFORM_WATCH_FILES) {
|
|
119
|
-
const fullPath = path.join(rootDir, watchPath);
|
|
120
|
-
const stat = statIfExists(fullPath);
|
|
121
|
-
if (stat && stat.isFile()) {
|
|
122
|
-
addTarget(fullPath, false, watchPath);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Watch platform config directories
|
|
127
|
-
for (const watchPath of PLATFORM_WATCH_DIRS) {
|
|
128
|
-
const fullPath = path.join(rootDir, watchPath);
|
|
129
|
-
const stat = statIfExists(fullPath);
|
|
130
|
-
if (!stat || !stat.isDirectory()) continue;
|
|
131
|
-
|
|
132
|
-
if (recursiveSupported) {
|
|
133
|
-
addTarget(fullPath, true, watchPath);
|
|
134
|
-
} else {
|
|
135
|
-
for (const dir of listRecursiveDirectories(fullPath)) {
|
|
136
|
-
addTarget(dir, false, watchPath);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return plan;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ─── Watcher registration ─────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
function registerWatchers(rootDir, watchers, onChange, platform) {
|
|
147
|
-
const plan = buildHarmonyWatchPlan(rootDir, platform);
|
|
148
|
-
|
|
149
|
-
for (const item of plan) {
|
|
150
|
-
const key = `${item.path}|${item.recursive}`;
|
|
151
|
-
if (watchers.has(key)) continue;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const watcher = fs.watch(item.path, { recursive: item.recursive }, (eventType, filename) => {
|
|
155
|
-
onChange(item, eventType, filename);
|
|
156
|
-
});
|
|
157
|
-
watchers.set(key, watcher);
|
|
158
|
-
} catch (_e) {
|
|
159
|
-
// Ignore unsupported or transient watch registration failures
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return watchers.size;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function closeWatchers(watchers) {
|
|
167
|
-
for (const watcher of watchers.values()) {
|
|
168
|
-
try {
|
|
169
|
-
watcher.close();
|
|
170
|
-
} catch (_e) {
|
|
171
|
-
// Ignore close errors during shutdown
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
watchers.clear();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Identify which platform a changed file belongs to.
|
|
179
|
-
*/
|
|
180
|
-
function identifyPlatform(filePath) {
|
|
181
|
-
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
182
|
-
if (normalized.includes('.claude') || normalized.includes('claude.md')) return 'claude';
|
|
183
|
-
if (normalized.includes('.codex') || normalized.includes('agents.md') || normalized.includes('codex.toml')) return 'codex';
|
|
184
|
-
if (normalized.includes('.gemini') || normalized.includes('gemini.md')) return 'gemini';
|
|
185
|
-
if (normalized.includes('copilot') || normalized.includes('.github')) return 'copilot';
|
|
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';
|
|
190
|
-
return 'unknown';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ─── Main watch loop ──────────────────────────────────────────────────────────
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Start the harmony watch loop.
|
|
197
|
-
*
|
|
198
|
-
* @param {Object} options
|
|
199
|
-
* @param {string} options.dir - Root directory to watch
|
|
200
|
-
* @param {Function} [options.onDriftDetected] - Callback when drift increases: (platform, details) => void
|
|
201
|
-
* @param {Function} [options.onPlatformChange] - Callback on any platform config change: (platform, file) => void
|
|
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
|
|
204
|
-
* @param {number} [options.debounceMs=800] - Debounce interval in ms
|
|
205
|
-
*/
|
|
206
|
-
async function startHarmonyWatch(options) {
|
|
207
|
-
const {
|
|
208
|
-
dir,
|
|
209
|
-
onDriftDetected,
|
|
210
|
-
onPlatformChange,
|
|
211
|
-
runAudit,
|
|
212
|
-
autoSync = false,
|
|
213
|
-
debounceMs = 800,
|
|
214
|
-
} = options;
|
|
215
|
-
|
|
216
|
-
const recursiveSupported = supportsNativeRecursiveWatch();
|
|
217
|
-
|
|
218
|
-
console.log('');
|
|
219
|
-
console.log(c(' nerviq harmony watch', 'bold'));
|
|
220
|
-
console.log(c(' ═══════════════════════════════════════', 'dim'));
|
|
221
|
-
console.log(c(` Watching: ${dir}`, '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
|
-
}
|
|
226
|
-
console.log(c(` Mode: ${recursiveSupported ? 'native recursive' : 'expanded directory fallback'}`, 'dim'));
|
|
227
|
-
console.log(c(' Press Ctrl+C to stop', 'dim'));
|
|
228
|
-
console.log('');
|
|
229
|
-
|
|
230
|
-
// Initial audit if provided
|
|
231
|
-
let lastScores = {};
|
|
232
|
-
if (runAudit) {
|
|
233
|
-
try {
|
|
234
|
-
const results = await runAudit(dir);
|
|
235
|
-
if (results && typeof results === 'object') {
|
|
236
|
-
for (const [platform, result] of Object.entries(results)) {
|
|
237
|
-
lastScores[platform] = result.score || 0;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
console.log(c(' Initial scores:', 'bold'));
|
|
241
|
-
for (const [platform, score] of Object.entries(lastScores)) {
|
|
242
|
-
console.log(` ${platform}: ${scoreColor(score)}`);
|
|
243
|
-
}
|
|
244
|
-
console.log('');
|
|
245
|
-
} catch (e) {
|
|
246
|
-
console.log(c(` Initial audit skipped: ${e.message}`, 'dim'));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const watchers = new Map();
|
|
251
|
-
let debounceTimer = null;
|
|
252
|
-
let shuttingDown = false;
|
|
253
|
-
|
|
254
|
-
const cleanupAndExit = () => {
|
|
255
|
-
if (shuttingDown) return;
|
|
256
|
-
shuttingDown = true;
|
|
257
|
-
clearTimeout(debounceTimer);
|
|
258
|
-
closeWatchers(watchers);
|
|
259
|
-
console.log('');
|
|
260
|
-
console.log(c(' Harmony watch stopped.', 'dim'));
|
|
261
|
-
process.exit(0);
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const handleChange = (item, _eventType, filename) => {
|
|
265
|
-
clearTimeout(debounceTimer);
|
|
266
|
-
debounceTimer = setTimeout(async () => {
|
|
267
|
-
const changedLabel = filename
|
|
268
|
-
? String(filename)
|
|
269
|
-
: path.relative(dir, item.path) || path.basename(item.path);
|
|
270
|
-
|
|
271
|
-
const platform = identifyPlatform(changedLabel);
|
|
272
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
273
|
-
|
|
274
|
-
console.log(c(` [${timestamp}] Change: ${changedLabel} (${platform})`, 'dim'));
|
|
275
|
-
|
|
276
|
-
// Notify callback
|
|
277
|
-
if (onPlatformChange) {
|
|
278
|
-
try {
|
|
279
|
-
onPlatformChange(platform, changedLabel);
|
|
280
|
-
} catch (_e) {
|
|
281
|
-
// Ignore callback errors
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Re-register to pick up new dirs/files
|
|
286
|
-
registerWatchers(dir, watchers, handleChange);
|
|
287
|
-
|
|
288
|
-
// Re-audit if possible
|
|
289
|
-
if (runAudit) {
|
|
290
|
-
try {
|
|
291
|
-
const results = await runAudit(dir);
|
|
292
|
-
if (results && typeof results === 'object') {
|
|
293
|
-
for (const [p, result] of Object.entries(results)) {
|
|
294
|
-
const newScore = result.score || 0;
|
|
295
|
-
const oldScore = lastScores[p] || 0;
|
|
296
|
-
const delta = newScore - oldScore;
|
|
297
|
-
|
|
298
|
-
if (delta !== 0) {
|
|
299
|
-
const arrow = delta > 0
|
|
300
|
-
? c(`+${delta}`, 'green')
|
|
301
|
-
: c(String(delta), 'yellow');
|
|
302
|
-
console.log(` ${p}: ${scoreColor(newScore)} ${arrow}`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Drift detection
|
|
306
|
-
if (delta < 0 && onDriftDetected) {
|
|
307
|
-
try {
|
|
308
|
-
onDriftDetected(p, { oldScore, newScore, delta, changedFile: changedLabel });
|
|
309
|
-
} catch (_e) {
|
|
310
|
-
// Ignore callback errors
|
|
311
|
-
}
|
|
312
|
-
}
|
|
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
|
-
|
|
330
|
-
lastScores[p] = newScore;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
} catch (_e) {
|
|
334
|
-
// Ignore transient errors during file saves
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
console.log('');
|
|
339
|
-
}, debounceMs);
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
registerWatchers(dir, watchers, handleChange);
|
|
343
|
-
|
|
344
|
-
if (watchers.size === 0) {
|
|
345
|
-
console.log(c(' Could not register any filesystem watchers.', 'yellow'));
|
|
346
|
-
return { watchers, close: () => {} };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
process.once('SIGINT', cleanupAndExit);
|
|
350
|
-
process.once('SIGTERM', cleanupAndExit);
|
|
351
|
-
|
|
352
|
-
console.log(c(` Watching ${watchers.size} targets for changes...`, 'dim'));
|
|
353
|
-
console.log('');
|
|
354
|
-
|
|
355
|
-
// Keep alive
|
|
356
|
-
await new Promise(() => {});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function scoreColor(score) {
|
|
360
|
-
const color = score >= 70 ? 'green' : score >= 40 ? 'yellow' : 'dim';
|
|
361
|
-
return c(`${score}/100`, color);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
module.exports = {
|
|
365
|
-
startHarmonyWatch,
|
|
366
|
-
buildHarmonyWatchPlan,
|
|
367
|
-
PLATFORM_WATCH_FILES,
|
|
368
|
-
PLATFORM_WATCH_DIRS,
|
|
369
|
-
identifyPlatform,
|
|
370
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* H6. Harmony Watch - Continuous Adaptation Loop
|
|
3
|
+
*
|
|
4
|
+
* Monitors repo for changes across ALL platform config files and triggers
|
|
5
|
+
* cross-platform drift detection when changes occur.
|
|
6
|
+
*
|
|
7
|
+
* Extends the watch pattern from src/watch.js to cover all supported platforms.
|
|
8
|
+
* Zero external dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
16
|
+
green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m', red: '\x1b[31m',
|
|
17
|
+
};
|
|
18
|
+
const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
19
|
+
|
|
20
|
+
// ─── Platform config file paths to watch ──────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const PLATFORM_WATCH_FILES = [
|
|
23
|
+
// Claude
|
|
24
|
+
'CLAUDE.md',
|
|
25
|
+
'.claude/settings.json',
|
|
26
|
+
'.claude/settings.local.json',
|
|
27
|
+
// Codex
|
|
28
|
+
'AGENTS.md',
|
|
29
|
+
'codex.toml',
|
|
30
|
+
'.codex/config.toml',
|
|
31
|
+
// Gemini
|
|
32
|
+
'GEMINI.md',
|
|
33
|
+
'.gemini/settings.json',
|
|
34
|
+
// Copilot
|
|
35
|
+
'.github/copilot-instructions.md',
|
|
36
|
+
'.github/copilot-review-instructions.md',
|
|
37
|
+
// Cursor
|
|
38
|
+
'.cursorrules',
|
|
39
|
+
// Windsurf
|
|
40
|
+
'.windsurfrules',
|
|
41
|
+
// Aider
|
|
42
|
+
'.aider.conf.yml',
|
|
43
|
+
'.aiderignore',
|
|
44
|
+
// OpenCode
|
|
45
|
+
'opencode.json',
|
|
46
|
+
// Shared
|
|
47
|
+
'.gitignore',
|
|
48
|
+
'package.json',
|
|
49
|
+
'tsconfig.json',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const PLATFORM_WATCH_DIRS = [
|
|
53
|
+
'.claude',
|
|
54
|
+
'.claude/commands',
|
|
55
|
+
'.claude/rules',
|
|
56
|
+
'.claude/agents',
|
|
57
|
+
'.claude/skills',
|
|
58
|
+
'.codex',
|
|
59
|
+
'.gemini',
|
|
60
|
+
'.github',
|
|
61
|
+
'.cursor',
|
|
62
|
+
'.cursor/rules',
|
|
63
|
+
'.windsurf',
|
|
64
|
+
'.windsurf/rules',
|
|
65
|
+
'.opencode',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ─── fs.watch helpers (mirror pattern from src/watch.js) ──────────────────────
|
|
69
|
+
|
|
70
|
+
function supportsNativeRecursiveWatch(platform) {
|
|
71
|
+
return (platform || process.platform) === 'win32' || (platform || process.platform) === 'darwin';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function statIfExists(fullPath) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.statSync(fullPath);
|
|
77
|
+
} catch (_e) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function listRecursiveDirectories(dir) {
|
|
83
|
+
const directories = [dir];
|
|
84
|
+
let entries = [];
|
|
85
|
+
try {
|
|
86
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
} catch (_e) {
|
|
88
|
+
return directories;
|
|
89
|
+
}
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
directories.push(...listRecursiveDirectories(path.join(dir, entry.name)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return directories;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a watch plan that covers all platform config files and directories.
|
|
100
|
+
*/
|
|
101
|
+
function buildHarmonyWatchPlan(rootDir, platform) {
|
|
102
|
+
const plan = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const recursiveSupported = supportsNativeRecursiveWatch(platform);
|
|
105
|
+
|
|
106
|
+
const addTarget = (fullPath, recursive, source) => {
|
|
107
|
+
const resolved = path.resolve(fullPath);
|
|
108
|
+
const key = `${resolved}|${recursive}`;
|
|
109
|
+
if (seen.has(key)) return;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
plan.push({ path: resolved, recursive, source });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Watch repo root for top-level file creations
|
|
115
|
+
addTarget(rootDir, false, 'repo-root');
|
|
116
|
+
|
|
117
|
+
// Watch individual platform config files
|
|
118
|
+
for (const watchPath of PLATFORM_WATCH_FILES) {
|
|
119
|
+
const fullPath = path.join(rootDir, watchPath);
|
|
120
|
+
const stat = statIfExists(fullPath);
|
|
121
|
+
if (stat && stat.isFile()) {
|
|
122
|
+
addTarget(fullPath, false, watchPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Watch platform config directories
|
|
127
|
+
for (const watchPath of PLATFORM_WATCH_DIRS) {
|
|
128
|
+
const fullPath = path.join(rootDir, watchPath);
|
|
129
|
+
const stat = statIfExists(fullPath);
|
|
130
|
+
if (!stat || !stat.isDirectory()) continue;
|
|
131
|
+
|
|
132
|
+
if (recursiveSupported) {
|
|
133
|
+
addTarget(fullPath, true, watchPath);
|
|
134
|
+
} else {
|
|
135
|
+
for (const dir of listRecursiveDirectories(fullPath)) {
|
|
136
|
+
addTarget(dir, false, watchPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return plan;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Watcher registration ─────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function registerWatchers(rootDir, watchers, onChange, platform) {
|
|
147
|
+
const plan = buildHarmonyWatchPlan(rootDir, platform);
|
|
148
|
+
|
|
149
|
+
for (const item of plan) {
|
|
150
|
+
const key = `${item.path}|${item.recursive}`;
|
|
151
|
+
if (watchers.has(key)) continue;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const watcher = fs.watch(item.path, { recursive: item.recursive }, (eventType, filename) => {
|
|
155
|
+
onChange(item, eventType, filename);
|
|
156
|
+
});
|
|
157
|
+
watchers.set(key, watcher);
|
|
158
|
+
} catch (_e) {
|
|
159
|
+
// Ignore unsupported or transient watch registration failures
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return watchers.size;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function closeWatchers(watchers) {
|
|
167
|
+
for (const watcher of watchers.values()) {
|
|
168
|
+
try {
|
|
169
|
+
watcher.close();
|
|
170
|
+
} catch (_e) {
|
|
171
|
+
// Ignore close errors during shutdown
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
watchers.clear();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Identify which platform a changed file belongs to.
|
|
179
|
+
*/
|
|
180
|
+
function identifyPlatform(filePath) {
|
|
181
|
+
const normalized = filePath.replace(/\\/g, '/').toLowerCase();
|
|
182
|
+
if (normalized.includes('.claude') || normalized.includes('claude.md')) return 'claude';
|
|
183
|
+
if (normalized.includes('.codex') || normalized.includes('agents.md') || normalized.includes('codex.toml')) return 'codex';
|
|
184
|
+
if (normalized.includes('.gemini') || normalized.includes('gemini.md')) return 'gemini';
|
|
185
|
+
if (normalized.includes('copilot') || normalized.includes('.github')) return 'copilot';
|
|
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';
|
|
190
|
+
return 'unknown';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Main watch loop ──────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Start the harmony watch loop.
|
|
197
|
+
*
|
|
198
|
+
* @param {Object} options
|
|
199
|
+
* @param {string} options.dir - Root directory to watch
|
|
200
|
+
* @param {Function} [options.onDriftDetected] - Callback when drift increases: (platform, details) => void
|
|
201
|
+
* @param {Function} [options.onPlatformChange] - Callback on any platform config change: (platform, file) => void
|
|
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
|
|
204
|
+
* @param {number} [options.debounceMs=800] - Debounce interval in ms
|
|
205
|
+
*/
|
|
206
|
+
async function startHarmonyWatch(options) {
|
|
207
|
+
const {
|
|
208
|
+
dir,
|
|
209
|
+
onDriftDetected,
|
|
210
|
+
onPlatformChange,
|
|
211
|
+
runAudit,
|
|
212
|
+
autoSync = false,
|
|
213
|
+
debounceMs = 800,
|
|
214
|
+
} = options;
|
|
215
|
+
|
|
216
|
+
const recursiveSupported = supportsNativeRecursiveWatch();
|
|
217
|
+
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log(c(' nerviq harmony watch', 'bold'));
|
|
220
|
+
console.log(c(' ═══════════════════════════════════════', 'dim'));
|
|
221
|
+
console.log(c(` Watching: ${dir}`, '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
|
+
}
|
|
226
|
+
console.log(c(` Mode: ${recursiveSupported ? 'native recursive' : 'expanded directory fallback'}`, 'dim'));
|
|
227
|
+
console.log(c(' Press Ctrl+C to stop', 'dim'));
|
|
228
|
+
console.log('');
|
|
229
|
+
|
|
230
|
+
// Initial audit if provided
|
|
231
|
+
let lastScores = {};
|
|
232
|
+
if (runAudit) {
|
|
233
|
+
try {
|
|
234
|
+
const results = await runAudit(dir);
|
|
235
|
+
if (results && typeof results === 'object') {
|
|
236
|
+
for (const [platform, result] of Object.entries(results)) {
|
|
237
|
+
lastScores[platform] = result.score || 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log(c(' Initial scores:', 'bold'));
|
|
241
|
+
for (const [platform, score] of Object.entries(lastScores)) {
|
|
242
|
+
console.log(` ${platform}: ${scoreColor(score)}`);
|
|
243
|
+
}
|
|
244
|
+
console.log('');
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.log(c(` Initial audit skipped: ${e.message}`, 'dim'));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const watchers = new Map();
|
|
251
|
+
let debounceTimer = null;
|
|
252
|
+
let shuttingDown = false;
|
|
253
|
+
|
|
254
|
+
const cleanupAndExit = () => {
|
|
255
|
+
if (shuttingDown) return;
|
|
256
|
+
shuttingDown = true;
|
|
257
|
+
clearTimeout(debounceTimer);
|
|
258
|
+
closeWatchers(watchers);
|
|
259
|
+
console.log('');
|
|
260
|
+
console.log(c(' Harmony watch stopped.', 'dim'));
|
|
261
|
+
process.exit(0);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const handleChange = (item, _eventType, filename) => {
|
|
265
|
+
clearTimeout(debounceTimer);
|
|
266
|
+
debounceTimer = setTimeout(async () => {
|
|
267
|
+
const changedLabel = filename
|
|
268
|
+
? String(filename)
|
|
269
|
+
: path.relative(dir, item.path) || path.basename(item.path);
|
|
270
|
+
|
|
271
|
+
const platform = identifyPlatform(changedLabel);
|
|
272
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
273
|
+
|
|
274
|
+
console.log(c(` [${timestamp}] Change: ${changedLabel} (${platform})`, 'dim'));
|
|
275
|
+
|
|
276
|
+
// Notify callback
|
|
277
|
+
if (onPlatformChange) {
|
|
278
|
+
try {
|
|
279
|
+
onPlatformChange(platform, changedLabel);
|
|
280
|
+
} catch (_e) {
|
|
281
|
+
// Ignore callback errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Re-register to pick up new dirs/files
|
|
286
|
+
registerWatchers(dir, watchers, handleChange);
|
|
287
|
+
|
|
288
|
+
// Re-audit if possible
|
|
289
|
+
if (runAudit) {
|
|
290
|
+
try {
|
|
291
|
+
const results = await runAudit(dir);
|
|
292
|
+
if (results && typeof results === 'object') {
|
|
293
|
+
for (const [p, result] of Object.entries(results)) {
|
|
294
|
+
const newScore = result.score || 0;
|
|
295
|
+
const oldScore = lastScores[p] || 0;
|
|
296
|
+
const delta = newScore - oldScore;
|
|
297
|
+
|
|
298
|
+
if (delta !== 0) {
|
|
299
|
+
const arrow = delta > 0
|
|
300
|
+
? c(`+${delta}`, 'green')
|
|
301
|
+
: c(String(delta), 'yellow');
|
|
302
|
+
console.log(` ${p}: ${scoreColor(newScore)} ${arrow}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Drift detection
|
|
306
|
+
if (delta < 0 && onDriftDetected) {
|
|
307
|
+
try {
|
|
308
|
+
onDriftDetected(p, { oldScore, newScore, delta, changedFile: changedLabel });
|
|
309
|
+
} catch (_e) {
|
|
310
|
+
// Ignore callback errors
|
|
311
|
+
}
|
|
312
|
+
}
|
|
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
|
+
|
|
330
|
+
lastScores[p] = newScore;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch (_e) {
|
|
334
|
+
// Ignore transient errors during file saves
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log('');
|
|
339
|
+
}, debounceMs);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
registerWatchers(dir, watchers, handleChange);
|
|
343
|
+
|
|
344
|
+
if (watchers.size === 0) {
|
|
345
|
+
console.log(c(' Could not register any filesystem watchers.', 'yellow'));
|
|
346
|
+
return { watchers, close: () => {} };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
process.once('SIGINT', cleanupAndExit);
|
|
350
|
+
process.once('SIGTERM', cleanupAndExit);
|
|
351
|
+
|
|
352
|
+
console.log(c(` Watching ${watchers.size} targets for changes...`, 'dim'));
|
|
353
|
+
console.log('');
|
|
354
|
+
|
|
355
|
+
// Keep alive
|
|
356
|
+
await new Promise(() => {});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function scoreColor(score) {
|
|
360
|
+
const color = score >= 70 ? 'green' : score >= 40 ? 'yellow' : 'dim';
|
|
361
|
+
return c(`${score}/100`, color);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
startHarmonyWatch,
|
|
366
|
+
buildHarmonyWatchPlan,
|
|
367
|
+
PLATFORM_WATCH_FILES,
|
|
368
|
+
PLATFORM_WATCH_DIRS,
|
|
369
|
+
identifyPlatform,
|
|
370
|
+
};
|