@mohantn/gate-keeper 2.2.0 → 2.2.2

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.
Files changed (82) hide show
  1. package/README.md +82 -46
  2. package/package.json +2 -2
  3. package/dist/cli/query-repl.d.ts +0 -37
  4. package/dist/cli/query-repl.d.ts.map +0 -1
  5. package/dist/cli/query-repl.js +0 -298
  6. package/dist/cli/query-repl.js.map +0 -1
  7. package/dist/cli/repl-algorithms.d.ts +0 -49
  8. package/dist/cli/repl-algorithms.d.ts.map +0 -1
  9. package/dist/cli/repl-algorithms.js +0 -147
  10. package/dist/cli/repl-algorithms.js.map +0 -1
  11. package/dist/cli/setup-core.d.ts +0 -38
  12. package/dist/cli/setup-core.d.ts.map +0 -1
  13. package/dist/cli/setup-core.js +0 -427
  14. package/dist/cli/setup-core.js.map +0 -1
  15. package/dist/cli/setup.d.ts +0 -25
  16. package/dist/cli/setup.d.ts.map +0 -1
  17. package/dist/cli/setup.js +0 -159
  18. package/dist/cli/setup.js.map +0 -1
  19. package/dist/github/app.d.ts +0 -34
  20. package/dist/github/app.d.ts.map +0 -1
  21. package/dist/github/app.js +0 -261
  22. package/dist/github/app.js.map +0 -1
  23. package/dist/github/commenter.d.ts +0 -67
  24. package/dist/github/commenter.d.ts.map +0 -1
  25. package/dist/github/commenter.js +0 -155
  26. package/dist/github/commenter.js.map +0 -1
  27. package/dist/hooks/git-hooks.d.ts +0 -30
  28. package/dist/hooks/git-hooks.d.ts.map +0 -1
  29. package/dist/hooks/git-hooks.js +0 -179
  30. package/dist/hooks/git-hooks.js.map +0 -1
  31. package/dist/mcp/cache-preload.d.ts +0 -29
  32. package/dist/mcp/cache-preload.d.ts.map +0 -1
  33. package/dist/mcp/cache-preload.js +0 -103
  34. package/dist/mcp/cache-preload.js.map +0 -1
  35. package/dist/mcp/handlers/context.d.ts +0 -25
  36. package/dist/mcp/handlers/context.d.ts.map +0 -1
  37. package/dist/mcp/handlers/context.js +0 -382
  38. package/dist/mcp/handlers/context.js.map +0 -1
  39. package/dist/mcp/handlers/graph-intelligence.d.ts +0 -26
  40. package/dist/mcp/handlers/graph-intelligence.d.ts.map +0 -1
  41. package/dist/mcp/handlers/graph-intelligence.js +0 -371
  42. package/dist/mcp/handlers/graph-intelligence.js.map +0 -1
  43. package/dist/mcp/handlers/graph-query.d.ts +0 -25
  44. package/dist/mcp/handlers/graph-query.d.ts.map +0 -1
  45. package/dist/mcp/handlers/graph-query.js +0 -410
  46. package/dist/mcp/handlers/graph-query.js.map +0 -1
  47. package/dist/mcp/handlers/impact.d.ts +0 -4
  48. package/dist/mcp/handlers/impact.d.ts.map +0 -1
  49. package/dist/mcp/handlers/impact.js +0 -139
  50. package/dist/mcp/handlers/impact.js.map +0 -1
  51. package/dist/mcp/handlers/improvement.d.ts +0 -4
  52. package/dist/mcp/handlers/improvement.d.ts.map +0 -1
  53. package/dist/mcp/handlers/improvement.js +0 -136
  54. package/dist/mcp/handlers/improvement.js.map +0 -1
  55. package/dist/mcp/handlers/platform-installer.d.ts +0 -10
  56. package/dist/mcp/handlers/platform-installer.d.ts.map +0 -1
  57. package/dist/mcp/handlers/platform-installer.js +0 -168
  58. package/dist/mcp/handlers/platform-installer.js.map +0 -1
  59. package/dist/mcp/handlers/pr-review.d.ts +0 -33
  60. package/dist/mcp/handlers/pr-review.d.ts.map +0 -1
  61. package/dist/mcp/handlers/pr-review.js +0 -170
  62. package/dist/mcp/handlers/pr-review.js.map +0 -1
  63. package/dist/mcp/token-tracker.d.ts +0 -47
  64. package/dist/mcp/token-tracker.d.ts.map +0 -1
  65. package/dist/mcp/token-tracker.js +0 -93
  66. package/dist/mcp/token-tracker.js.map +0 -1
  67. package/dist/quality-loop/file-lock.d.ts +0 -12
  68. package/dist/quality-loop/file-lock.d.ts.map +0 -1
  69. package/dist/quality-loop/file-lock.js +0 -38
  70. package/dist/quality-loop/file-lock.js.map +0 -1
  71. package/dist/quality-loop/fix-worker.d.ts +0 -44
  72. package/dist/quality-loop/fix-worker.d.ts.map +0 -1
  73. package/dist/quality-loop/fix-worker.js +0 -414
  74. package/dist/quality-loop/fix-worker.js.map +0 -1
  75. package/dist/quality-loop/orchestrator.d.ts +0 -137
  76. package/dist/quality-loop/orchestrator.d.ts.map +0 -1
  77. package/dist/quality-loop/orchestrator.js +0 -894
  78. package/dist/quality-loop/orchestrator.js.map +0 -1
  79. package/dist/quality-loop/queue-manager.d.ts +0 -45
  80. package/dist/quality-loop/queue-manager.d.ts.map +0 -1
  81. package/dist/quality-loop/queue-manager.js +0 -173
  82. package/dist/quality-loop/queue-manager.js.map +0 -1
@@ -1,894 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.QualityOrchestrator = void 0;
37
- exports.loadQualityConfig = loadQualityConfig;
38
- exports.saveQualityConfig = saveQualityConfig;
39
- const fs = __importStar(require("fs"));
40
- const path = __importStar(require("path"));
41
- const os = __importStar(require("os"));
42
- const child_process_1 = require("child_process");
43
- const queue_manager_1 = require("./queue-manager");
44
- const file_lock_1 = require("./file-lock");
45
- const fix_worker_1 = require("./fix-worker");
46
- const QUALITY_CONFIG_PATH = path.join(process.env.HOME ?? '/tmp', '.gate-keeper', 'quality-config.json');
47
- class QualityOrchestrator {
48
- config;
49
- cache;
50
- queue;
51
- locks;
52
- callbacks;
53
- /** Tracks auto-scheduled workers (orchestrator-driven) */
54
- activeWorkers = new Map();
55
- /** Tracks manual executions triggered from the dashboard */
56
- manualExecutions = new Map();
57
- paused = false;
58
- stopped = false;
59
- checkpointTimer = null;
60
- heartbeatTimer = null;
61
- loopPromise = null;
62
- filesFixed = 0;
63
- workerCounter = 0;
64
- constructor(config, cache, callbacks) {
65
- this.config = config;
66
- this.cache = cache;
67
- this.queue = new queue_manager_1.QueueManager(cache);
68
- this.locks = new file_lock_1.FileLockManager(cache);
69
- this.callbacks = callbacks;
70
- }
71
- get isRunning() {
72
- return this.loopPromise !== null && !this.stopped;
73
- }
74
- get isPaused() {
75
- return this.paused;
76
- }
77
- get stats() {
78
- return this.queue.getStats();
79
- }
80
- start() {
81
- if (this.loopPromise) {
82
- console.error('[quality-loop] Already running');
83
- return;
84
- }
85
- this.stopped = false;
86
- this.paused = false;
87
- this.loopPromise = this.run();
88
- this.loopPromise.catch(err => {
89
- console.error('[quality-loop] Fatal loop error:', err);
90
- this.stop();
91
- });
92
- console.error('[quality-loop] Orchestrator started');
93
- }
94
- stop() {
95
- this.stopped = true;
96
- console.error('[quality-loop] Stopping...');
97
- }
98
- pause() {
99
- this.paused = true;
100
- console.error('[quality-loop] Paused');
101
- }
102
- resume() {
103
- this.paused = false;
104
- console.error('[quality-loop] Resumed');
105
- }
106
- enqueueRepos() {
107
- return this.buildQueue();
108
- }
109
- resetFailed() {
110
- return this.queue.resetFailed();
111
- }
112
- getQueueItems() {
113
- return this.queue.getAllItems();
114
- }
115
- getAttempts(queueId) {
116
- return this.cache.quality.getAttemptLogWithOutput(queueId);
117
- }
118
- getTrends() {
119
- const rows = this.cache.quality.getTrends();
120
- return rows.map(r => ({
121
- id: r.id,
122
- repo: r.repo,
123
- overallRating: r.overall_rating,
124
- filesTotal: r.files_total,
125
- filesPassed: r.files_passed,
126
- filesFailed: r.files_failed,
127
- filesPending: r.files_pending,
128
- recordedAt: r.recorded_at,
129
- }));
130
- }
131
- updateConfig(partial) {
132
- if (partial.threshold != null)
133
- this.config.threshold = partial.threshold;
134
- if (partial.maxWorkers != null)
135
- this.config.maxWorkers = partial.maxWorkers;
136
- if (partial.maxAttemptsPerFile != null)
137
- this.config.maxAttemptsPerFile = partial.maxAttemptsPerFile;
138
- if (partial.workerMode != null)
139
- this.config.workerMode = partial.workerMode;
140
- if (partial.repos != null)
141
- this.config.repos = partial.repos;
142
- if (partial.checkpointIntervalSec != null)
143
- this.config.checkpointIntervalSec = partial.checkpointIntervalSec;
144
- if (partial.heartbeatIntervalSec != null)
145
- this.config.heartbeatIntervalSec = partial.heartbeatIntervalSec;
146
- // Persist to disk
147
- try {
148
- fs.writeFileSync(QUALITY_CONFIG_PATH, JSON.stringify(this.config, null, 2), 'utf8');
149
- }
150
- catch { /* non-fatal */ }
151
- this.log(`Config updated: threshold=${this.config.threshold} workers=${this.config.maxWorkers}`);
152
- }
153
- getConfig() {
154
- return { ...this.config };
155
- }
156
- // ── Manual execution (dashboard-triggered) ──────────────────────────────────
157
- /**
158
- * Look up repo metadata to determine session type.
159
- * Returns 'claude', 'github-copilot', or 'unknown'.
160
- */
161
- getRepoSessionType(repo) {
162
- try {
163
- const meta = this.cache.getRepositoryByPath(repo);
164
- return meta?.sessionType ?? 'unknown';
165
- }
166
- catch {
167
- return 'unknown';
168
- }
169
- }
170
- /**
171
- * Generate a human-readable command string for a queue item.
172
- */
173
- getCmdForItem(itemId) {
174
- const item = this.queue.getItem(itemId);
175
- if (!item)
176
- return null;
177
- const sessionType = this.getRepoSessionType(item.repo);
178
- const shortFile = path.basename(item.filePath);
179
- if (sessionType === 'github-copilot') {
180
- return { cmd: `gh copilot suggest "fix violations in ${shortFile}"`, sessionType };
181
- }
182
- return {
183
- cmd: `claude --dangerously-skip-permissions "fix all violations in @${shortFile}"`,
184
- sessionType,
185
- };
186
- }
187
- /**
188
- * Execute a single queue item manually from the dashboard.
189
- * Opens a real terminal window and polls a status file for completion.
190
- */
191
- async executeItem(itemId) {
192
- const item = this.queue.getItem(itemId);
193
- if (!item)
194
- return { ok: false, workerId: null, error: 'Queue item not found' };
195
- if (item.status === 'in_progress')
196
- return { ok: false, workerId: null, error: 'Item already in progress' };
197
- // Mark as in_progress
198
- const workerId = `manual-${itemId}-${Date.now()}`;
199
- this.queue.markInProgress(itemId, workerId);
200
- this.broadcastItem(itemId);
201
- // Generate the fix prompt
202
- const prompt = await this.generateFixPrompt(item.filePath, item.repo);
203
- if (!prompt) {
204
- this.queue.markFailed(itemId, 'Could not generate fix prompt');
205
- this.broadcastItem(itemId);
206
- return { ok: false, workerId: null, error: 'Could not fetch analysis for file' };
207
- }
208
- const sessionType = this.getRepoSessionType(item.repo);
209
- const claudePath = this.resolveClaudePath();
210
- // Write prompt + bash script to temp files
211
- const id = `gk-manual-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
212
- const tmp = os.tmpdir();
213
- const promptFile = path.join(tmp, `${id}.prompt`);
214
- const statusFile = path.join(tmp, `${id}.status`);
215
- const logFile = path.join(tmp, `${id}.log`);
216
- const scriptFile = path.join(tmp, `${id}.sh`);
217
- try {
218
- fs.writeFileSync(promptFile, prompt, 'utf8');
219
- this.writeTerminalScript(scriptFile, { promptFile, statusFile, logFile, claudePath, repo: item.repo, sessionType });
220
- }
221
- catch (err) {
222
- this.queue.markFailed(itemId, 'Could not write script files');
223
- this.broadcastItem(itemId);
224
- return { ok: false, workerId: null, error: 'Could not write script files' };
225
- }
226
- this.log(`Manual execute: ${claudePath} for ${path.basename(item.filePath)} (${sessionType})`);
227
- // Open a real terminal window
228
- const opened = this.openTerminal(scriptFile);
229
- if (!opened) {
230
- this.queue.markFailed(itemId, 'Could not open terminal');
231
- this.broadcastItem(itemId);
232
- this.cleanupFiles(promptFile, statusFile, logFile, scriptFile);
233
- return { ok: false, workerId: null, error: 'Could not open terminal' };
234
- }
235
- const execution = {
236
- workerId,
237
- queueId: itemId,
238
- filePath: item.filePath,
239
- repo: item.repo,
240
- outputFile: logFile,
241
- statusFile,
242
- scriptFile,
243
- promptFile,
244
- startTime: Date.now(),
245
- running: true,
246
- exitCode: null,
247
- };
248
- this.manualExecutions.set(workerId, execution);
249
- // Poll for completion in the background
250
- this.pollManualCompletion(workerId);
251
- return { ok: true, workerId };
252
- }
253
- /**
254
- * Cancel a running manual execution by writing an abort marker to the status file.
255
- * The polling loop detects it and cleans up.
256
- */
257
- cancelExecution(workerId) {
258
- const exec = this.manualExecutions.get(workerId);
259
- if (!exec || !exec.running)
260
- return false;
261
- exec.running = false;
262
- exec.exitCode = -1;
263
- if (exec.pollTimer)
264
- clearInterval(exec.pollTimer);
265
- // Write cancelled marker so polling loop won't treat it as a real exit
266
- try {
267
- fs.writeFileSync(exec.statusFile, JSON.stringify({ exitCode: -1, timestamp: Math.floor(Date.now() / 1000) }), 'utf8');
268
- }
269
- catch { /* ignore */ }
270
- this.queue.markFailed(exec.queueId, 'Cancelled by user');
271
- this.broadcastItem(exec.queueId);
272
- this.cleanupFiles(exec.outputFile, exec.statusFile, exec.scriptFile, exec.promptFile);
273
- this.manualExecutions.delete(workerId);
274
- this.log(`Manual execution cancelled: ${workerId}`);
275
- return true;
276
- }
277
- /**
278
- * Delete a queue item (remove from database).
279
- */
280
- deleteQueueItem(itemId) {
281
- for (const [wid, exec] of this.manualExecutions) {
282
- if (exec.queueId === itemId && exec.running) {
283
- exec.running = false;
284
- exec.exitCode = -1;
285
- if (exec.pollTimer)
286
- clearInterval(exec.pollTimer);
287
- try {
288
- fs.writeFileSync(exec.statusFile, JSON.stringify({ exitCode: -1, timestamp: Math.floor(Date.now() / 1000) }), 'utf8');
289
- }
290
- catch { /* ignore */ }
291
- this.manualExecutions.delete(wid);
292
- }
293
- }
294
- try {
295
- this.cache.quality.updateQueueItem(itemId, { status: 'skipped' });
296
- this.broadcast({ type: 'queue_update', queueItem: this.queue.getItem(itemId) ?? undefined });
297
- return true;
298
- }
299
- catch {
300
- return false;
301
- }
302
- }
303
- /**
304
- * Get the current output of a running manual execution.
305
- */
306
- getExecutionOutput(workerId) {
307
- const exec = this.manualExecutions.get(workerId);
308
- if (!exec)
309
- return null;
310
- return {
311
- output: this.readOutputFile(exec.outputFile),
312
- running: exec.running,
313
- exitCode: exec.exitCode,
314
- startTime: exec.startTime,
315
- };
316
- }
317
- // ── Manual execution helpers ─────────────────────────────────────────────────
318
- async generateFixPrompt(filePath, repo) {
319
- try {
320
- const url = `http://127.0.0.1:5378/api/file-detail?file=${encodeURIComponent(filePath)}&repo=${encodeURIComponent(repo)}`;
321
- const res = await fetch(url);
322
- if (!res.ok)
323
- return null;
324
- const data = await res.json();
325
- if (!data.analysis)
326
- return null;
327
- const { rating, violations } = data.analysis;
328
- const numbered = violations.map((v, i) => {
329
- const fixStr = typeof v.fix === 'string' ? v.fix : '';
330
- return `${i + 1}. [${v.severity.toUpperCase()}]${v.line ? ` (line ${v.line})` : ''}: ${v.message}${fixStr ? `\n Fix: ${fixStr}` : ''}`;
331
- }).join('\n');
332
- return `fix all violations in @${filePath}\nFile: ${filePath}\nRating: ${rating}/10\nViolations: ${violations.length}\n\n${numbered}`;
333
- }
334
- catch {
335
- return null;
336
- }
337
- }
338
- resolveClaudePath() {
339
- const candidates = [
340
- 'claude',
341
- '/home/mohantn/.local/bin/claude',
342
- '/usr/local/bin/claude',
343
- `${os.homedir()}/.npm-global/bin/claude`,
344
- `${os.homedir()}/.npm-packages/bin/claude`,
345
- ];
346
- for (const c of candidates) {
347
- try {
348
- if (fs.existsSync(c))
349
- return c;
350
- }
351
- catch { /* ignore */ }
352
- }
353
- try {
354
- const resolved = (0, child_process_1.execSync)('which claude 2>/dev/null', { encoding: 'utf8', timeout: 2000 }).trim();
355
- if (resolved)
356
- return resolved;
357
- }
358
- catch { /* ignore */ }
359
- return 'claude';
360
- }
361
- async reanalyzeFile(filePath, repo) {
362
- try {
363
- const res = await fetch(`http://127.0.0.1:5379/analyze`, {
364
- method: 'POST',
365
- headers: { 'Content-Type': 'application/json' },
366
- body: JSON.stringify({ filePath, repoRoot: repo }),
367
- });
368
- if (!res.ok)
369
- return null;
370
- const data = await res.json();
371
- return data.analysis ?? null;
372
- }
373
- catch {
374
- return null;
375
- }
376
- }
377
- readOutputFile(filePath) {
378
- try {
379
- return fs.readFileSync(filePath, 'utf8');
380
- }
381
- catch {
382
- return '';
383
- }
384
- }
385
- cleanupFiles(...files) {
386
- for (const f of files) {
387
- try {
388
- fs.unlinkSync(f);
389
- }
390
- catch { /* ignore */ }
391
- }
392
- }
393
- // ── Terminal helpers (reuse the same pattern as FixWorker) ────────────────────
394
- /** Write the bash script that will run inside the terminal window. */
395
- writeTerminalScript(scriptPath, opts) {
396
- const cmd = opts.sessionType === 'github-copilot'
397
- ? `gh copilot suggest "fix violations in $(cat '${opts.promptFile}')"`
398
- : `"${opts.claudePath}" --dangerously-skip-permissions "$(cat '${opts.promptFile}')"`;
399
- fs.writeFileSync(scriptPath, `#!/usr/bin/env bash
400
- # Note: this script is invoked via "bash -i" so ~/.bashrc is sourced automatically.
401
- set -uo pipefail
402
-
403
- STATUS_FILE='${opts.statusFile}'
404
- LOG_FILE='${opts.logFile}'
405
- REPO='${opts.repo}'
406
-
407
- EXIT_CODE=0
408
-
409
- on_exit() {
410
- local last=$?
411
- [ "$last" -ne 0 ] && [ "$EXIT_CODE" -eq 0 ] && EXIT_CODE=$last
412
- printf '{"exitCode":%d,"timestamp":%d}' "$EXIT_CODE" "$(date +%s)" > "$STATUS_FILE" 2>/dev/null || true
413
- }
414
- trap on_exit EXIT
415
-
416
- cd "$REPO"
417
-
418
- ${cmd} 2>&1 | tee "$LOG_FILE" || EXIT_CODE=$?
419
-
420
- echo ""
421
- echo "────────────────────────────────────────────"
422
- if [ "$EXIT_CODE" -eq 0 ]; then
423
- echo " Claude fix finished — press Enter to close"
424
- else
425
- echo " Claude exited with code $EXIT_CODE — press Enter to close"
426
- fi
427
- echo "────────────────────────────────────────────"
428
- read -r || true
429
- `, 'utf8');
430
- fs.chmodSync(scriptPath, 0o755);
431
- }
432
- /** Open a real terminal window running the given script. */
433
- openTerminal(scriptPath) {
434
- if (this.isWSL()) {
435
- // Strategy 1: cmd.exe /c start — opens a new CMD window (most reliable on WSL)
436
- // Use cmd /k to keep the window open after the script exits
437
- try {
438
- (0, child_process_1.spawn)('cmd.exe', ['/c', 'start', '', 'cmd', '/k', 'wsl.exe', 'bash', '-i', scriptPath], { detached: true, stdio: 'ignore' }).unref();
439
- return true;
440
- }
441
- catch { /* try next */ }
442
- // Strategy 2: Windows Terminal (wt.exe)
443
- try {
444
- (0, child_process_1.execSync)('command -v wt.exe 2>/dev/null', { timeout: 1000 });
445
- (0, child_process_1.spawn)('wt.exe', ['-w', '0', 'nt', '--', 'wsl.exe', 'bash', '-i', scriptPath], { detached: true, stdio: 'ignore' }).unref();
446
- return true;
447
- }
448
- catch { /* try next */ }
449
- // Strategy 3: powershell Start-Process
450
- try {
451
- (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-Command',
452
- `Start-Process cmd -ArgumentList '/k wsl.exe bash -i \\"${scriptPath}\\"'`], { detached: true, stdio: 'ignore' }).unref();
453
- return true;
454
- }
455
- catch {
456
- return false;
457
- }
458
- }
459
- const terminals = [
460
- ['gnome-terminal', ['--', 'bash', scriptPath]],
461
- ['konsole', ['--hold', '-e', 'bash', scriptPath]],
462
- ['xterm', ['-e', `bash "${scriptPath}"`]],
463
- ['x-terminal-emulator', ['-e', `bash "${scriptPath}"`]],
464
- ];
465
- for (const [bin, args] of terminals) {
466
- try {
467
- (0, child_process_1.execSync)(`command -v ${bin} 2>/dev/null`, { timeout: 1000 });
468
- (0, child_process_1.spawn)(bin, args, { detached: true, stdio: 'ignore' }).unref();
469
- return true;
470
- }
471
- catch { /* not found */ }
472
- }
473
- return false;
474
- }
475
- isWSL() {
476
- if (process.env['WSL_DISTRO_NAME'])
477
- return true;
478
- try {
479
- const v = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
480
- return v.includes('microsoft') || v.includes('wsl');
481
- }
482
- catch {
483
- return false;
484
- }
485
- }
486
- /** Poll the status file every 3 seconds until the terminal script writes a result. */
487
- pollManualCompletion(workerId) {
488
- const exec = this.manualExecutions.get(workerId);
489
- if (!exec)
490
- return;
491
- const startTime = Date.now();
492
- const POLL_MS = 3000;
493
- const MAX_WAIT = 600_000; // 10 minutes for manual execution
494
- const poll = async () => {
495
- const e = this.manualExecutions.get(workerId);
496
- if (!e || !e.running)
497
- return;
498
- // Timeout check
499
- if (Date.now() - startTime > MAX_WAIT) {
500
- e.running = false;
501
- e.exitCode = null;
502
- this.queue.markFailed(e.queueId, 'Manual execution timed out');
503
- this.broadcastItem(e.queueId);
504
- this.cleanupFiles(e.outputFile, e.statusFile, e.scriptFile, e.promptFile);
505
- this.manualExecutions.delete(workerId);
506
- return;
507
- }
508
- // Check status file
509
- let status = null;
510
- try {
511
- if (fs.existsSync(e.statusFile)) {
512
- status = JSON.parse(fs.readFileSync(e.statusFile, 'utf8'));
513
- }
514
- }
515
- catch { /* not written yet */ }
516
- if (status !== null) {
517
- clearInterval(e.pollTimer);
518
- e.running = false;
519
- e.exitCode = status.exitCode;
520
- const fullOutput = this.readOutputFile(e.outputFile);
521
- const analysis = await this.reanalyzeFile(e.filePath, e.repo);
522
- const newRating = analysis?.rating ?? 0;
523
- const violationsRemaining = analysis?.violations.length ?? 0;
524
- this.cache.quality.logAttempt({
525
- queueId: e.queueId,
526
- attempt: 1, // approximate
527
- ratingBefore: 0,
528
- ratingAfter: newRating,
529
- violationsFixed: 0,
530
- violationsRemaining,
531
- fixSummary: status.exitCode === 0 ? `Manual fix completed (exit ${status.exitCode})` : `Manual fix failed (exit ${status.exitCode})`,
532
- errorMessage: status.exitCode !== 0 ? `Process exited with code ${status.exitCode}` : undefined,
533
- durationMs: Date.now() - e.startTime,
534
- workerOutput: fullOutput,
535
- });
536
- if (status.exitCode === 0 && newRating >= this.config.threshold) {
537
- this.queue.markCompleted(e.queueId, newRating);
538
- }
539
- else if (status.exitCode !== 0) {
540
- this.queue.markFailed(e.queueId, `Manual fix exited with code ${status.exitCode}`);
541
- }
542
- else {
543
- this.queue.markFailed(e.queueId, `Rating ${newRating} below threshold ${this.config.threshold}`);
544
- }
545
- this.broadcastItem(e.queueId);
546
- this.cleanupFiles(e.outputFile, e.statusFile, e.scriptFile, e.promptFile);
547
- this.manualExecutions.delete(workerId);
548
- }
549
- };
550
- exec.pollTimer = setInterval(poll, POLL_MS);
551
- }
552
- async run() {
553
- this.log('Quality loop started');
554
- // 1. Try restore from checkpoint
555
- const restored = this.queue.restoreFromCheckpoint();
556
- if (restored.length > 0) {
557
- this.log(`Restored ${restored.length} items from checkpoint`);
558
- }
559
- // 2. Build queue from repos
560
- await this.buildQueue();
561
- // 3. Start timers
562
- this.startHeartbeat();
563
- this.startCheckpoints();
564
- // 4. Main loop
565
- while (!this.stopped) {
566
- if (this.paused) {
567
- await this.sleep(2000);
568
- continue;
569
- }
570
- // Clean stale locks
571
- this.locks.clearStale();
572
- // Get stats
573
- const stats = this.queue.getStats();
574
- this.broadcastProgress(stats);
575
- // Check completion
576
- if (stats.pending === 0 && stats.inProgress === 0) {
577
- if (stats.failed > 0 || stats.skipped > 0) {
578
- this.log(`Loop complete: ${stats.completed} passed, ${stats.failed} failed, ${stats.skipped} skipped`);
579
- }
580
- else {
581
- this.log(`ALL ${stats.completed} files pass threshold!`);
582
- }
583
- await this.saveFinalCheckpoint('complete');
584
- break;
585
- }
586
- // Calculate available slots
587
- const slots = this.config.maxWorkers - this.activeWorkers.size;
588
- if (slots <= 0) {
589
- await this.waitForAnyWorker();
590
- continue;
591
- }
592
- // Pick next files
593
- const candidates = this.queue.pickNext(slots, this.locks.getLockedPaths());
594
- if (candidates.length === 0) {
595
- // All pending files are locked — wait
596
- await this.sleep(1000);
597
- continue;
598
- }
599
- // Spawn workers
600
- for (const item of candidates) {
601
- if (this.stopped)
602
- break;
603
- const workerId = `w${++this.workerCounter}-${item.id}`;
604
- if (!this.locks.acquire(item.filePath, workerId))
605
- continue;
606
- this.queue.markInProgress(item.id, workerId);
607
- this.broadcastItem(item.id);
608
- const workerStartTime = Date.now();
609
- const promise = this.runWorker(item.id, item.filePath, workerId, workerStartTime);
610
- const timeout = setTimeout(() => {
611
- this.log(`Worker ${workerId} timed out for ${path.basename(item.filePath)}`);
612
- this.handleWorkerTimeout(workerId);
613
- }, 300_000); // 5 min hard limit
614
- this.activeWorkers.set(workerId, {
615
- queueId: item.id,
616
- filePath: item.filePath,
617
- promise,
618
- startTime: workerStartTime,
619
- timeout,
620
- });
621
- this.broadcast({
622
- type: 'worker_activity',
623
- workerAction: 'start',
624
- workerFilePath: item.filePath,
625
- workerId,
626
- });
627
- // Handle completion
628
- promise.finally(() => {
629
- clearTimeout(timeout);
630
- this.activeWorkers.delete(workerId);
631
- });
632
- }
633
- // Wait for a worker slot if all full
634
- if (this.activeWorkers.size >= this.config.maxWorkers) {
635
- await this.waitForAnyWorker();
636
- }
637
- else {
638
- await this.sleep(500);
639
- }
640
- }
641
- // 5. Shutdown
642
- this.cleanup();
643
- }
644
- async runWorker(queueId, filePath, workerId, startTime) {
645
- let result;
646
- try {
647
- const worker = new fix_worker_1.FixWorker({
648
- filePath,
649
- repo: this.resolveRepo(filePath),
650
- threshold: this.config.threshold,
651
- });
652
- result = await worker.fix();
653
- }
654
- catch (err) {
655
- const message = err instanceof Error ? err.message : String(err);
656
- result = {
657
- success: false,
658
- newRating: 0,
659
- ratingBefore: 0,
660
- violationsRemaining: 0,
661
- violationsFixed: 0,
662
- durationMs: Date.now() - startTime,
663
- attemptNumber: 0,
664
- fixSummary: '',
665
- error: message,
666
- shouldRetry: false,
667
- };
668
- }
669
- // Release file lock
670
- this.locks.release(filePath, workerId);
671
- // Log attempt
672
- this.cache.quality.logAttempt({
673
- queueId,
674
- attempt: result.attemptNumber,
675
- ratingBefore: result.ratingBefore,
676
- ratingAfter: result.newRating,
677
- violationsFixed: result.violationsFixed,
678
- violationsRemaining: result.violationsRemaining,
679
- fixSummary: result.fixSummary,
680
- errorMessage: result.error,
681
- durationMs: result.durationMs,
682
- workerOutput: result.workerOutput,
683
- });
684
- // Update queue item
685
- if (result.success) {
686
- this.queue.markCompleted(queueId, result.newRating);
687
- this.filesFixed++;
688
- }
689
- else {
690
- this.queue.markFailed(queueId, result.error || 'Unknown error');
691
- }
692
- // Record trend
693
- const stats = this.queue.getStats();
694
- this.queue.recordTrend(this.config.repos.join(','), this.getOverallRating(), this.getTotalFiles(), stats);
695
- // Broadcast
696
- this.broadcast({
697
- type: 'worker_activity',
698
- workerAction: result.success ? 'complete' : 'error',
699
- workerFilePath: filePath,
700
- workerId,
701
- workerRating: result.newRating,
702
- workerSuccess: result.success,
703
- workerError: result.error,
704
- });
705
- this.broadcastItem(queueId);
706
- const relPath = path.basename(filePath);
707
- if (result.success) {
708
- this.log(`[OK] ${relPath}: ${result.ratingBefore} → ${result.newRating} (${result.durationMs}ms, ${result.attemptNumber} cycle(s))`);
709
- }
710
- else {
711
- this.log(`[FAIL] ${relPath}: ${result.ratingBefore} → ${result.newRating} — ${result.error ?? 'Max attempts'} (${result.durationMs}ms)`);
712
- }
713
- }
714
- resolveRepo(filePath) {
715
- // Find the best matching repo from config
716
- for (const repo of this.config.repos) {
717
- if (filePath.startsWith(repo))
718
- return repo;
719
- }
720
- return this.config.repos[0] ?? path.dirname(filePath);
721
- }
722
- handleWorkerTimeout(workerId) {
723
- const worker = this.activeWorkers.get(workerId);
724
- if (!worker)
725
- return;
726
- this.locks.release(worker.filePath, workerId);
727
- this.activeWorkers.delete(workerId);
728
- }
729
- async buildQueue() {
730
- const fileRatings = new Map();
731
- const violationCounts = new Map();
732
- for (const repo of this.config.repos) {
733
- const analyses = this.callbacks.getAnalyzedFiles(repo);
734
- for (const a of analyses) {
735
- if (a.rating < this.config.threshold) {
736
- fileRatings.set(a.path, { rating: a.rating, repo: repo });
737
- }
738
- }
739
- }
740
- this.log(`Found ${fileRatings.size} files below threshold ${this.config.threshold}`);
741
- if (fileRatings.size === 0)
742
- return 0;
743
- return this.queue.buildQueue({
744
- repos: this.config.repos,
745
- threshold: this.config.threshold,
746
- fileRatings,
747
- violationCounts,
748
- });
749
- }
750
- startHeartbeat() {
751
- const interval = (this.config.heartbeatIntervalSec ?? 10) * 1000;
752
- this.heartbeatTimer = setInterval(() => {
753
- if (this.stopped)
754
- return;
755
- const stats = this.queue.getStats();
756
- this.broadcastProgress(stats);
757
- }, interval);
758
- }
759
- startCheckpoints() {
760
- const interval = (this.config.checkpointIntervalSec ?? 30) * 1000;
761
- this.checkpointTimer = setInterval(() => {
762
- if (this.stopped)
763
- return;
764
- this.queue.saveCheckpoint('heartbeat', this.filesFixed, this.getOverallRating());
765
- }, interval);
766
- }
767
- async saveFinalCheckpoint(reason) {
768
- this.queue.saveCheckpoint(reason, this.filesFixed, this.getOverallRating());
769
- const stats = this.queue.getStats();
770
- this.broadcast({
771
- type: 'queue_progress',
772
- queueStats: {
773
- total: stats.total,
774
- pending: stats.pending,
775
- inProgress: stats.inProgress,
776
- completed: stats.completed,
777
- failed: stats.failed,
778
- skipped: stats.skipped,
779
- },
780
- queueOverallRating: this.getOverallRating(),
781
- queueDone: true,
782
- });
783
- }
784
- broadcastProgress(stats) {
785
- this.broadcast({
786
- type: 'queue_progress',
787
- queueStats: {
788
- total: stats.total,
789
- pending: stats.pending,
790
- inProgress: stats.inProgress,
791
- completed: stats.completed,
792
- failed: stats.failed,
793
- skipped: stats.skipped,
794
- },
795
- queueOverallRating: this.getOverallRating(),
796
- queueDone: stats.pending === 0 && stats.inProgress === 0,
797
- });
798
- }
799
- broadcastItem(itemId) {
800
- const item = this.queue.getItem(itemId);
801
- if (!item)
802
- return;
803
- this.broadcast({ type: 'queue_update', queueItem: item });
804
- }
805
- broadcast(msg) {
806
- try {
807
- this.callbacks.broadcast(msg);
808
- }
809
- catch {
810
- // Broadcast failures are non-fatal
811
- }
812
- }
813
- async waitForAnyWorker() {
814
- if (this.activeWorkers.size === 0)
815
- return;
816
- // Wait for first worker to complete
817
- const workers = Array.from(this.activeWorkers.values());
818
- await Promise.race(workers.map(w => w.promise));
819
- }
820
- getOverallRating() {
821
- try {
822
- const all = this.cache.getAll();
823
- if (all.length === 0)
824
- return 10;
825
- const sum = all.reduce((a, f) => a + f.rating, 0);
826
- return Math.round((sum / all.length) * 10) / 10;
827
- }
828
- catch {
829
- return 10;
830
- }
831
- }
832
- getTotalFiles() {
833
- try {
834
- const all = this.cache.getAll();
835
- return all.length;
836
- }
837
- catch {
838
- return 0;
839
- }
840
- }
841
- cleanup() {
842
- if (this.checkpointTimer)
843
- clearInterval(this.checkpointTimer);
844
- if (this.heartbeatTimer)
845
- clearInterval(this.heartbeatTimer);
846
- this.checkpointTimer = null;
847
- this.heartbeatTimer = null;
848
- // Kill any remaining workers (just timeouts)
849
- for (const [id, w] of this.activeWorkers) {
850
- clearTimeout(w.timeout);
851
- this.locks.release(w.filePath, id);
852
- }
853
- this.activeWorkers.clear();
854
- this.locks.clearStale();
855
- this.log('Shutdown complete');
856
- this.loopPromise = null;
857
- }
858
- log(msg) {
859
- console.error(`[quality-loop] ${msg}`);
860
- }
861
- sleep(ms) {
862
- return new Promise(r => setTimeout(r, ms));
863
- }
864
- }
865
- exports.QualityOrchestrator = QualityOrchestrator;
866
- // Load config from disk
867
- function loadQualityConfig() {
868
- try {
869
- if (fs.existsSync(QUALITY_CONFIG_PATH)) {
870
- const raw = fs.readFileSync(QUALITY_CONFIG_PATH, 'utf8');
871
- return JSON.parse(raw);
872
- }
873
- }
874
- catch (err) {
875
- console.error(`[quality-loop] Failed to load config: ${err}`);
876
- }
877
- return {
878
- threshold: 7.0,
879
- maxWorkers: 2,
880
- maxAttemptsPerFile: 3,
881
- workerMode: 'auto',
882
- repos: [],
883
- excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
884
- checkpointIntervalSec: 30,
885
- heartbeatIntervalSec: 10,
886
- };
887
- }
888
- // Save config to disk
889
- function saveQualityConfig(config) {
890
- const dir = path.dirname(QUALITY_CONFIG_PATH);
891
- fs.mkdirSync(dir, { recursive: true });
892
- fs.writeFileSync(QUALITY_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
893
- }
894
- //# sourceMappingURL=orchestrator.js.map