From 98f1c7eb42fe54800e39331581756558fece0a76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:16:47 +0000 Subject: [PATCH 1/2] Initial plan From dc26417d498e9f2b200fd57c65fdb6e4e3fbe11f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:36:32 +0000 Subject: [PATCH 2/2] feat: HITL approval workflow with queue persistence, code diffs, graduation, and notifications Co-authored-by: michaeloboyle <61171+michaeloboyle@users.noreply.github.com> --- .gitignore | 7 + js/fix-review.js | 246 ++++++++- package-lock.json | 1255 +++++++++++++++++++++++++++++++++++++++++++++ serve.py | 469 ++++++++++++++++- style.css | 222 ++++++++ 5 files changed, 2174 insertions(+), 25 deletions(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index e451b87..b2c93ec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ __pycache__/ *.pyc analyses/ fix-snapshots/ +fix-queue.json +fix-approval-history.json +fix-webhook.json +test-results/ +test-results.json +junit.xml +playwright-report/ diff --git a/js/fix-review.js b/js/fix-review.js index bfc92ba..e7b6a55 100644 --- a/js/fix-review.js +++ b/js/fix-review.js @@ -1,6 +1,7 @@ const fixReview = { pollInterval: null, currentFixId: null, + _notifiedIds: new Set(), toggle() { const panel = document.getElementById('fix-review-panel'); @@ -19,31 +20,83 @@ const fixReview = { if (!fixes.length) { body.innerHTML = '
No fixes to review
'; document.getElementById('fix-review-badge').classList.remove('has-fixes'); + document.getElementById('fix-review-badge').textContent = 'Fix Review'; return; } document.getElementById('fix-review-badge').classList.add('has-fixes'); document.getElementById('fix-review-badge').textContent = `Fix Review (${fixes.length})`; + // Send desktop notifications for newly ready fixes + fixes.forEach(fix => { + if (fix.status === 'ready' && !this._notifiedIds.has(fix.fixId)) { + this._notifiedIds.add(fix.fixId); + this._notify(fix); + } + }); + + // Group fixes by issue class for batch actions + const byClass = {}; + fixes.forEach(fix => { + const cls = fix.issueClass || '__unknown__'; + (byClass[cls] = byClass[cls] || []).push(fix); + }); + let html = ''; + // Batch action bar when multiple fixes share a class + for (const [cls, group] of Object.entries(byClass)) { + if (group.length > 1 && cls !== '__unknown__') { + html += `
+ ${this._escHtml(cls)} + ${group.length} fixes + + +
`; + } + } + for (const fix of fixes) { - html += `
`; - html += `
${fix.description}
`; + html += `
`; + html += `
${this._escHtml(fix.description)}
`; + + // Issue class + confidence badge + if (fix.issueClass || fix.confidence !== undefined) { + html += `
`; + if (fix.issueClass) { + html += `${this._escHtml(fix.issueClass)}`; + } + const conf = typeof fix.confidence === 'number' ? fix.confidence : 0; + const confClass = conf >= 0.7 ? 'confidence-high' : 'confidence-low'; + const confLabel = conf >= 0.7 ? 'High' : 'Low'; + html += `${confLabel} ${(conf * 100).toFixed(0)}%`; + html += `
`; + } if (fix.status === 'running') { html += `
Session running... after screenshot pending
`; } else if (fix.hasBeforeAfter) { html += await this.renderDiff(fix.fixId); } else if (fix.status === 'ready') { - html += `
Screenshots captured (partial)
`; + html += `
Screenshots captured
`; } + // Code diff section (lazy-loaded) + html += `
+ + +
`; + html += `
`; html += ``; html += ``; html += ``; + html += ``; html += `
`; } + + // Graduation stats section + html += await this.renderGraduationStats(); + body.innerHTML = html; } catch (e) { console.warn('[fix-review] refresh error:', e); @@ -59,7 +112,7 @@ const fixReview = { const beforeFile = data.beforePath.split('/').pop(); const afterFile = data.afterPath.split('/').pop(); - return `
+ let html = `
Before @@ -69,15 +122,181 @@ const fixReview = { After
`; + + // Show graduation button if class qualifies + if (data.issueClass && data.canGraduate) { + html += `
+ 🎓 Class ${this._escHtml(data.issueClass)} is ready to graduate + (${(data.graduationRate * 100).toFixed(0)}% approval over ${data.graduationTotal} fixes) + +
`; + } + return html; } catch { return ''; } }, - resolve(fixId, outcome) { + async loadDiff(fixId) { + const wrapper = document.getElementById(`diff-${fixId}`); + const content = document.getElementById(`diff-content-${fixId}`); + if (!wrapper || !content) return; + + try { + const resp = await fetch(`/api/fix/diff?id=${fixId}`); + const data = await resp.json(); + const diffs = data.diffs || []; + if (!diffs.length) { + content.innerHTML = '
No code edits found in session
'; + } else { + content.innerHTML = diffs.map(d => ` +
+
${this._escHtml(d.file_path)} ${d.tool}
+
${this._renderUnifiedDiff(d.unified_diff)}
+
`).join(''); + } + content.classList.remove('hidden'); + wrapper.querySelector('.btn-show-diff').textContent = 'Hide Code Diff'; + wrapper.querySelector('.btn-show-diff').onclick = () => { + content.classList.toggle('hidden'); + wrapper.querySelector('.btn-show-diff').textContent = + content.classList.contains('hidden') ? 'Show Code Diff' : 'Hide Code Diff'; + }; + } catch (e) { + content.innerHTML = `
Error loading diff: ${e.message}
`; + content.classList.remove('hidden'); + } + }, + + _renderUnifiedDiff(diff) { + if (!diff) return ''; + return diff.split('\n').map(line => { + const esc = this._escHtml(line); + if (line.startsWith('+++') || line.startsWith('---')) return `${esc}`; + if (line.startsWith('@@')) return `${esc}`; + if (line.startsWith('+')) return `${esc}`; + if (line.startsWith('-')) return `${esc}`; + return `${esc}`; + }).join('\n'); + }, + + async renderGraduationStats() { + try { + const resp = await fetch('/api/fix/graduation-stats'); + const stats = await resp.json(); + const classes = Object.entries(stats); + if (!classes.length) return ''; + + let rows = classes.map(([cls, s]) => { + const badge = s.graduated + ? 'AUTO' + : s.canGraduate + ? `` + : ''; + return ` + ${this._escHtml(cls)} + ${s.approved}✓ ${s.partial}~ ${s.rejected}✗ + ${(s.rate * 100).toFixed(0)}% + ${badge} + `; + }).join(''); + + return `
+ Graduation Stats (${classes.length} classes) + + + ${rows} +
ClassOutcomesRateStatus
+
`; + } catch { return ''; } + }, + + async resolve(fixId, outcome) { + try { + await fetch('/api/fix/resolve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fixId, outcome }), + }); + } catch (e) { + console.warn('[fix-review] resolve error:', e); + } + // Show revert button on reject + if (outcome === 'failure') { + const btn = document.getElementById(`revert-${fixId}`); + if (btn) btn.classList.remove('hidden'); + } if (window.__claude && window.__claude.resolve) { window.__claude.resolve(fixId, outcome); } - // Remove from UI - this.refresh(); + // Delay refresh so the server has time to persist the resolved status + // before the poll reads it back (avoids the fix flickering back into view) + setTimeout(() => this.refresh(), 800); + }, + + async batchResolve(fixIds, outcome) { + await Promise.all(fixIds.map(id => this.resolve(id, outcome))); + }, + + async revert(fixId) { + try { + const resp = await fetch('/api/fix/revert', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fixId }), + }); + const data = await resp.json(); + const btn = document.getElementById(`revert-${fixId}`); + if (btn) { + btn.textContent = data.reverted?.length + ? `Reverted ${data.reverted.length} file(s)` + : 'Nothing to revert'; + btn.disabled = true; + } + } catch (e) { + console.warn('[fix-review] revert error:', e); + } + }, + + async graduate(issueClass) { + try { + const resp = await fetch('/api/fix/graduate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ issueClass }), + }); + const data = await resp.json(); + if (data.ok) { + this.refresh(); + } else { + alert(`Cannot graduate: ${data.error}`); + } + } catch (e) { + console.warn('[fix-review] graduate error:', e); + } + }, + + _notify(fix) { + if (!('Notification' in window)) return; + const show = () => { + new Notification('Fix ready for review', { + body: fix.description || 'A fix is ready for your approval.', + icon: '/favicon.ico', + tag: fix.fixId, + }); + }; + if (Notification.permission === 'granted') { + show(); + } else if (Notification.permission !== 'denied') { + Notification.requestPermission().then(p => { if (p === 'granted') show(); }); + } + }, + + _escHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); }, startPolling() { @@ -87,17 +306,26 @@ const fixReview = { .then(r => r.json()) .then(fixes => { const badge = document.getElementById('fix-review-badge'); - if (fixes.length > 0) { + const pending = fixes.filter(f => !['approved', 'rejected', 'reverted'].includes(f.status)); + if (pending.length > 0) { badge.classList.add('has-fixes'); - badge.textContent = `Fix Review (${fixes.length})`; + badge.textContent = `Fix Review (${pending.length})`; } else { badge.classList.remove('has-fixes'); + badge.textContent = 'Fix Review'; } // Auto-refresh if panel is open const panel = document.getElementById('fix-review-panel'); if (panel.classList.contains('visible')) { this.refresh(); } + // Notify for newly ready fixes + pending.forEach(fix => { + if (fix.status === 'ready' && !this._notifiedIds.has(fix.fixId)) { + this._notifiedIds.add(fix.fixId); + this._notify(fix); + } + }); }) .catch(() => {}); }, 5000); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c3c054b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1255 @@ +{ + "name": "tribbles", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tribbles", + "version": "1.0.0", + "devDependencies": { + "@cucumber/cucumber": "^9.5.1", + "@playwright/test": "^1.40.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/ci-environment": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-9.2.0.tgz", + "integrity": "sha512-jLzRtVwdtNt+uAmTwvXwW9iGYLEOJFpDSmnx/dgoMGKXUWRx1UHT86Q696CLdgXO8kyTwsgJY0c6n5SW9VitAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/cucumber": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-9.6.0.tgz", + "integrity": "sha512-bCw2uJdGHHLg4B3RoZpLzx0RXyXURmPe+swtdK1cGoA8rs+vv+/6osifcNwvFM2sv0nQ91+gDACSrXK7AHCylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/ci-environment": "9.2.0", + "@cucumber/cucumber-expressions": "16.1.2", + "@cucumber/gherkin": "26.2.0", + "@cucumber/gherkin-streams": "5.0.1", + "@cucumber/gherkin-utils": "8.0.2", + "@cucumber/html-formatter": "20.4.0", + "@cucumber/message-streams": "4.0.1", + "@cucumber/messages": "22.0.0", + "@cucumber/tag-expressions": "5.0.1", + "assertion-error-formatter": "^3.0.0", + "capital-case": "^1.0.4", + "chalk": "^4.1.2", + "cli-table3": "0.6.3", + "commander": "^10.0.0", + "debug": "^4.3.4", + "error-stack-parser": "^2.1.4", + "figures": "^3.2.0", + "glob": "^7.1.6", + "has-ansi": "^4.0.1", + "indent-string": "^4.0.0", + "is-installed-globally": "^0.4.0", + "is-stream": "^2.0.0", + "knuth-shuffle-seeded": "^1.0.6", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", + "luxon": "3.2.1", + "mkdirp": "^2.1.5", + "mz": "^2.7.0", + "progress": "^2.0.3", + "resolve-pkg": "^2.0.0", + "semver": "7.5.3", + "string-argv": "^0.3.1", + "strip-ansi": "6.0.1", + "supports-color": "^8.1.1", + "tmp": "^0.2.1", + "util-arity": "^1.1.0", + "verror": "^1.10.0", + "xmlbuilder": "^15.1.1", + "yaml": "^2.2.2", + "yup": "1.2.0" + }, + "bin": { + "cucumber-js": "bin/cucumber.js" + }, + "engines": { + "node": "14 || 16 || >=18" + } + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-16.1.2.tgz", + "integrity": "sha512-CfHEbxJ5FqBwF6mJyLLz4B353gyHkoi6cCL4J0lfDZ+GorpcWw4n2OUAdxJmP7ZlREANWoTFlp4FhmkLKrCfUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-26.2.0.tgz", + "integrity": "sha512-iRSiK8YAIHAmLrn/mUfpAx7OXZ7LyNlh1zT89RoziSVCbqSVDxJS6ckEzW8loxs+EEXl0dKPQOXiDmbHV+C/fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <=22" + } + }, + "node_modules/@cucumber/gherkin-streams": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", + "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "9.1.0", + "source-map-support": "0.5.21" + }, + "bin": { + "gherkin-javascript": "bin/gherkin" + }, + "peerDependencies": { + "@cucumber/gherkin": ">=22.0.0", + "@cucumber/message-streams": ">=4.0.0", + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/gherkin-streams/node_modules/commander": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", + "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-8.0.2.tgz", + "integrity": "sha512-aQlziN3r3cTwprEDbLEcFoMRQajb9DTOu2OZZp5xkuNz6bjSTowSY90lHUD2pWT7jhEEckZRIREnk7MAwC2d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^25.0.0", + "@cucumber/messages": "^19.1.4", + "@teppeis/multimaps": "2.0.0", + "commander": "9.4.1", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-25.0.2.tgz", + "integrity": "sha512-EdsrR33Y5GjuOoe2Kq5Y9DYwgNRtUD32H4y2hCrT6+AWo7ibUQu7H+oiWTgfVhwbkHsZmksxHSxXz/AwqqyCRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": "^19.1.4" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/messages": { + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-19.1.4.tgz", + "integrity": "sha512-Pksl0pnDz2l1+L5Ug85NlG6LWrrklN9qkMxN5Mv+1XZ3T6u580dnE6mVaxjJRdcOq4tR17Pc0RqIDZMyVY1FlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "8.3.4", + "class-transformer": "0.5.1", + "reflect-metadata": "0.1.13", + "uuid": "9.0.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@cucumber/html-formatter": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-20.4.0.tgz", + "integrity": "sha512-TnLSXC5eJd8AXHENo69f5z+SixEVtQIf7Q2dZuTpT/Y8AOkilGpGl1MQR1Vp59JIw+fF3EQSUKdf+DAThCxUNg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/message-streams": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", + "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/messages": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz", + "integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "9.0.1", + "class-transformer": "0.5.1", + "reflect-metadata": "0.1.13", + "uuid": "9.0.0" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-5.0.1.tgz", + "integrity": "sha512-N43uWud8ZXuVjza423T9ZCIJsaZhFekmakt7S9bvogTxqdVGbRobjR663s0+uW0Rz9e+Pa8I6jUuWtoBLQD2Mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@teppeis/multimaps": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-2.0.0.tgz", + "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.17" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error-formatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", + "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff": "^4.0.1", + "pad-right": "^0.2.2", + "repeat-string": "^1.6.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-ansi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-4.0.1.tgz", + "integrity": "sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/knuth-shuffle-seeded": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", + "integrity": "sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "seed-random": "~2.2.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/luxon": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", + "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg/-/resolve-pkg-2.0.0.tgz", + "integrity": "sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/util-arity": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", + "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yup": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", + "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + } + } +} diff --git a/serve.py b/serve.py index 54308e6..9f102ba 100755 --- a/serve.py +++ b/serve.py @@ -30,6 +30,9 @@ LIVE_MODE = "--live" in sys.argv SCRIPT_DIR = Path(__file__).parent SNAPSHOTS_DIR = SCRIPT_DIR / "fix-snapshots" +QUEUE_FILE = SCRIPT_DIR / "fix-queue.json" +APPROVAL_HISTORY_FILE = SCRIPT_DIR / "fix-approval-history.json" +WEBHOOK_CONFIG_FILE = SCRIPT_DIR / "fix-webhook.json" ANALYSES_DIR = SCRIPT_DIR / "analyses" # Resolve the claude binary — prefer ~/.claude/local/claude over PATH @@ -681,7 +684,196 @@ def trigger_analysis_generation(): # ── Fix Snapshot Tracking ───────────────────────────────────── # Tracks active fix sessions so we can capture before/after screenshots -active_fixes = {} # fix_id -> { session_id, description, before_path, after_path, status } +# fix_id -> { session_id, description, issue_class, confidence, before_path, +# after_path, status, created_at, resolved_at, outcome, +# changed_files, diffs, auto_applied } +_fixes_lock = threading.Lock() + + +def _load_queue(): + """Load the persistent fix queue from disk.""" + if QUEUE_FILE.exists(): + try: + with open(QUEUE_FILE) as f: + return json.load(f) + except Exception: + pass + return {} + + +def _save_queue(): + """Persist the current fix queue to disk (call while holding _fixes_lock).""" + try: + with open(QUEUE_FILE, "w") as f: + json.dump(active_fixes, f, indent=2, default=str) + except Exception as e: + print(f"[queue] save error: {e}") + + +def _load_approval_history(): + """Load per-class approval history from disk.""" + if APPROVAL_HISTORY_FILE.exists(): + try: + with open(APPROVAL_HISTORY_FILE) as f: + return json.load(f) + except Exception: + pass + return {} + + +def _save_approval_history(): + """Persist approval history to disk (call while holding _fixes_lock).""" + try: + with open(APPROVAL_HISTORY_FILE, "w") as f: + json.dump(approval_history, f, indent=2, default=str) + except Exception as e: + print(f"[approval] save error: {e}") + + +def _load_webhook_config(): + """Load webhook configuration from disk.""" + if WEBHOOK_CONFIG_FILE.exists(): + try: + with open(WEBHOOK_CONFIG_FILE) as f: + return json.load(f) + except Exception: + pass + return {"url": None, "sound": False} + + +active_fixes = _load_queue() +approval_history = _load_approval_history() +webhook_config = _load_webhook_config() + +# Confidence threshold above which a fix can be auto-applied (configurable) +CONFIDENCE_AUTO_APPLY_THRESHOLD = 0.9 +# Minimum number of fixes needed before graduation is considered +GRADUATION_MIN_FIXES = 10 +# Approval rate required for graduation +GRADUATION_MIN_RATE = 0.95 + + +def compute_confidence(issue_class): + """Compute a confidence score (0–1) for a fix based on class history. + + - Classes with many approvals and high approval rate → high confidence + - Novel or frequently-rejected classes → low confidence + """ + if not issue_class: + return 0.3 # novel/unknown class — low confidence + stats = approval_history.get(issue_class) + if not stats or stats.get("total", 0) < 3: + return 0.4 # too few samples + total = stats["total"] + approved = stats.get("approved", 0) + partial = stats.get("partial", 0) + # Weight partial approvals at 0.5 + rate = (approved + 0.5 * partial) / total + # Scale: 0.4 at 0% approval → 1.0 at 100% approval + return round(0.4 + 0.6 * rate, 3) + + +def check_graduation(issue_class): + """Check if a class should be graduated to autonomous. + + Returns (should_graduate, current_rate, total) tuple. + """ + if not issue_class: + return False, 0.0, 0 + stats = approval_history.get(issue_class, {}) + total = stats.get("total", 0) + if total < GRADUATION_MIN_FIXES: + return False, 0.0, total + approved = stats.get("approved", 0) + partial = stats.get("partial", 0) + rate = (approved + 0.5 * partial) / total + return rate >= GRADUATION_MIN_RATE, round(rate, 3), total + + +def parse_session_diffs(session_id): + """Parse a Claude session JSONL for Edit and Write tool calls. + + Returns a list of diff entries: + [{ "tool": "Edit"|"Write", "file_path": str, "old_string": str, + "new_string": str, "unified_diff": str }, ...] + """ + import difflib + pattern = str(CLAUDE_DIR / "*" / f"{session_id}.jsonl") + matches = globmod.glob(pattern) + if not matches: + return [] + + diffs = [] + try: + with open(matches[0]) as f: + for raw in f: + raw = raw.strip() + if not raw: + continue + try: + entry = json.loads(raw) + except json.JSONDecodeError: + continue + if entry.get("type") != "assistant": + continue + for block in entry.get("message", {}).get("content", []): + if block.get("type") != "tool_use": + continue + name = block.get("name", "") + inp = block.get("input", {}) + if name == "Edit" and inp.get("file_path"): + old = inp.get("old_string", "") + new = inp.get("new_string", "") + udiff = "".join(difflib.unified_diff( + old.splitlines(keepends=True), + new.splitlines(keepends=True), + fromfile=f"a/{inp['file_path']}", + tofile=f"b/{inp['file_path']}", + )) + diffs.append({ + "tool": "Edit", + "file_path": inp["file_path"], + "old_string": old, + "new_string": new, + "unified_diff": udiff, + }) + elif name == "Write" and inp.get("file_path"): + content = inp.get("content", "") + diffs.append({ + "tool": "Write", + "file_path": inp["file_path"], + "new_string": content, + "unified_diff": f"--- /dev/null\n+++ b/{inp['file_path']}\n" + + "".join(f"+{l}" for l in content.splitlines(keepends=True)), + }) + except Exception as e: + print(f"[diff] parse error: {e}") + return diffs + + +def send_webhook_notification(fix_id, fix): + """POST a fix-ready notification to the configured webhook URL.""" + url = webhook_config.get("url") + if not url: + return + try: + import urllib.request + payload = json.dumps({ + "text": f"Fix ready for review: {fix.get('description', '')[:80]}", + "fixId": fix_id, + "issueClass": fix.get("issue_class"), + "confidence": fix.get("confidence"), + "status": fix.get("status"), + }).encode("utf-8") + req = urllib.request.Request( + url, data=payload, + headers={"Content-Type": "application/json"}, + method="POST", + ) + urllib.request.urlopen(req, timeout=5) + print(f"[webhook] notified for fix {fix_id}") + except Exception as e: + print(f"[webhook] notification error: {e}") def capture_screenshot(fix_id, label="before"): @@ -725,13 +917,12 @@ def capture_after_screenshot(fix_id): """Capture the 'after' screenshot for a completed fix. Called in a background thread that polls for session completion. + Also parses the session JSONL for code diffs and sends a webhook notification. """ fix = active_fixes.get(fix_id) if not fix: return - session_id = fix["session_id"] - # Poll for session completion (max 5 minutes) for _ in range(300): time.sleep(1) @@ -742,12 +933,30 @@ def capture_after_screenshot(fix_id): time.sleep(2) after_path = capture_screenshot(fix_id, "after") + with _fixes_lock: + fix = active_fixes.get(fix_id) + if not fix: + return + if after_path: + fix["after_path"] = after_path + fix["status"] = "ready" + print(f"[snapshot] After screenshot captured: {after_path}") + else: + fix["status"] = "after_failed" + # Parse code diffs from the session JSONL + session_id = fix.get("session_id", "") + if session_id: + fix["diffs"] = parse_session_diffs(session_id) + fix["changed_files"] = list({d["file_path"] for d in fix["diffs"]}) + _save_queue() + + # Send webhook notification now that the fix is ready if after_path: - fix["after_path"] = after_path - fix["status"] = "ready" - print(f"[snapshot] After screenshot captured: {after_path}") - else: - fix["status"] = "after_failed" + threading.Thread( + target=send_webhook_notification, + args=(fix_id, fix), + daemon=True, + ).start() def build_devtools_prompt(action, description, context): @@ -1091,27 +1300,82 @@ def do_GET(self): if not fix: self.send_json({"error": "Fix not found", "fixId": fix_id}) return + issue_class = fix.get("issue_class") + _, grad_rate, grad_total = check_graduation(issue_class) + stats = approval_history.get(issue_class, {}) if issue_class else {} self.send_json({ "fixId": fix_id, "status": fix["status"], "description": fix["description"], + "issueClass": issue_class, + "confidence": fix.get("confidence", 0.0), "beforePath": fix.get("before_path"), "afterPath": fix.get("after_path"), "hasBeforeAfter": bool(fix.get("before_path") and fix.get("after_path")), + "changedFiles": fix.get("changed_files", []), + "outcome": fix.get("outcome"), + "graduationRate": grad_rate, + "graduationTotal": grad_total, + "isGraduated": stats.get("graduated", False), }) elif parsed.path == "/api/fix/snapshots": # List all tracked fixes with snapshot status result = [] - for fid, fix in active_fixes.items(): + with _fixes_lock: + snapshot = dict(active_fixes) + for fid, fix in snapshot.items(): + if fix.get("status") in ("approved", "rejected", "reverted"): + continue # hide resolved fixes from the queue + issue_class = fix.get("issue_class") result.append({ "fixId": fid, "status": fix["status"], "description": fix["description"][:100], + "issueClass": issue_class, + "confidence": fix.get("confidence", 0.0), "hasBeforeAfter": bool(fix.get("before_path") and fix.get("after_path")), }) self.send_json(result) + elif parsed.path == "/api/fix/diff": + # Return parsed code diffs for a fix + params = parse_qs(parsed.query) + fix_id = params.get("id", [None])[0] + if not fix_id: + self.send_error(400, "Missing id parameter") + return + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + diffs = fix.get("diffs") + if diffs is None: + session_id = fix.get("session_id", "") + diffs = parse_session_diffs(session_id) if session_id else [] + with _fixes_lock: + active_fixes.get(fix_id, {})["diffs"] = diffs + self.send_json({"fixId": fix_id, "diffs": diffs}) + + elif parsed.path == "/api/fix/graduation-stats": + # Return per-class approval rates and graduation status + result = {} + with _fixes_lock: + hist = dict(approval_history) + for cls, stats in hist.items(): + should_grad, rate, total = check_graduation(cls) + result[cls] = { + "approved": stats.get("approved", 0), + "partial": stats.get("partial", 0), + "rejected": stats.get("rejected", 0), + "total": total, + "rate": rate, + "graduated": stats.get("graduated", False), + "autoApply": stats.get("auto_apply", False), + "canGraduate": should_grad and not stats.get("graduated", False), + } + self.send_json(result) + elif parsed.path == "/api/themes": themes = list_vsc_themes() # Strip internal path from response @@ -1215,11 +1479,19 @@ def do_POST(self): description = data.get("description", "").strip() context = data.get("context", {}) fix_id = data.get("fixId") # from bridge telemetry + issue_class = data.get("issueClass", "").strip() or None if not description: self.send_error(400, "Missing description") return + # Compute confidence for this fix + confidence = compute_confidence(issue_class) + + # If class is graduated and confidence is high enough, skip HITL queue + cls_stats = approval_history.get(issue_class, {}) if issue_class else {} + auto_apply = cls_stats.get("auto_apply", False) and confidence >= CONFIDENCE_AUTO_APPLY_THRESHOLD + # Capture "before" screenshot (non-blocking, quick) before_path = None if action == "fix" and fix_id: @@ -1242,13 +1514,23 @@ def do_POST(self): # Track fix for after-capture if fix_id: - active_fixes[fix_id] = { - "session_id": result_id, - "description": description, - "before_path": before_path, - "after_path": None, - "status": "running", - } + with _fixes_lock: + active_fixes[fix_id] = { + "session_id": result_id, + "description": description, + "issue_class": issue_class, + "confidence": confidence, + "before_path": before_path, + "after_path": None, + "status": "running", + "created_at": datetime.now(timezone.utc).isoformat(), + "resolved_at": None, + "outcome": None, + "changed_files": [], + "diffs": None, + "auto_applied": auto_apply, + } + _save_queue() # Start background thread to capture "after" screenshot threading.Thread( target=capture_after_screenshot, @@ -1263,9 +1545,164 @@ def do_POST(self): "action": action, "description": description, "fixId": fix_id, + "issueClass": issue_class, + "confidence": confidence, + "autoApplied": auto_apply, "beforeScreenshot": before_path is not None, }) + elif parsed.path == "/api/fix/resolve": + # Approve / Partial / Reject a fix and update approval history + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + fix_id = data.get("fixId") + outcome = data.get("outcome") # "success" | "partial" | "failure" + if not fix_id or outcome not in ("success", "partial", "failure"): + self.send_error(400, "Missing fixId or invalid outcome") + return + with _fixes_lock: + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + fix["outcome"] = outcome + fix["resolved_at"] = datetime.now(timezone.utc).isoformat() + fix["status"] = "approved" if outcome == "success" else ( + "partial" if outcome == "partial" else "rejected" + ) + # Update per-class approval history + issue_class = fix.get("issue_class") + if issue_class: + if issue_class not in approval_history: + approval_history[issue_class] = { + "approved": 0, "partial": 0, "rejected": 0, + "total": 0, "graduated": False, "auto_apply": False, + } + cls = approval_history[issue_class] + cls["total"] += 1 + if outcome == "success": + cls["approved"] += 1 + elif outcome == "partial": + cls["partial"] += 1 + else: + cls["rejected"] += 1 + # Regression detection: revoke auto-apply if a graduated fix fails + if outcome == "failure" and cls.get("auto_apply"): + cls["auto_apply"] = False + cls["graduated"] = False + print(f"[graduation] Revoked auto-apply for class '{issue_class}' due to failure") + _save_approval_history() + _save_queue() + should_grad, rate, total = check_graduation(issue_class) if issue_class else (False, 0, 0) + self.send_json({ + "ok": True, + "fixId": fix_id, + "outcome": outcome, + "issueClass": issue_class, + "canGraduate": should_grad, + "graduationRate": rate, + "graduationTotal": total, + }) + + elif parsed.path == "/api/fix/revert": + # Revert changed files for a rejected fix via git checkout + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + fix_id = data.get("fixId") + if not fix_id: + self.send_error(400, "Missing fixId") + return + fix = active_fixes.get(fix_id) + if not fix: + self.send_json({"error": "Fix not found", "fixId": fix_id}) + return + changed_files = fix.get("changed_files", []) + if not changed_files: + self.send_json({"ok": True, "reverted": [], "note": "no changed files tracked"}) + return + reverted = [] + errors = [] + for fp in changed_files: + try: + result = subprocess.run( + ["git", "checkout", "--", fp], + capture_output=True, text=True, timeout=10, + cwd=str(SCRIPT_DIR), + ) + if result.returncode == 0: + reverted.append(fp) + else: + errors.append({"file": fp, "error": result.stderr.strip()}) + except Exception as e: + errors.append({"file": fp, "error": str(e)}) + with _fixes_lock: + if fix_id in active_fixes: + active_fixes[fix_id]["status"] = "reverted" + _save_queue() + self.send_json({"ok": True, "reverted": reverted, "errors": errors}) + + elif parsed.path == "/api/fix/graduate": + # Graduate a class from HITL review to autonomous operation + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + issue_class = data.get("issueClass", "").strip() + if not issue_class: + self.send_error(400, "Missing issueClass") + return + with _fixes_lock: + if issue_class not in approval_history: + self.send_json({"error": "No history for class", "issueClass": issue_class}) + return + should_grad, rate, total = check_graduation(issue_class) + if not should_grad: + self.send_json({ + "error": "Class does not meet graduation criteria", + "issueClass": issue_class, + "rate": rate, + "total": total, + "required_rate": GRADUATION_MIN_RATE, + "required_total": GRADUATION_MIN_FIXES, + }) + return + approval_history[issue_class]["graduated"] = True + approval_history[issue_class]["auto_apply"] = True + _save_approval_history() + print(f"[graduation] Class '{issue_class}' graduated (rate={rate}, total={total})") + self.send_json({ + "ok": True, + "issueClass": issue_class, + "rate": rate, + "total": total, + "graduated": True, + }) + + elif parsed.path == "/api/fix/webhook": + # Configure webhook URL for fix notifications + try: + data = json.loads(body) if body else {} + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + url = data.get("url", "").strip() or None + sound = bool(data.get("sound", False)) + webhook_config["url"] = url + webhook_config["sound"] = sound + try: + with open(WEBHOOK_CONFIG_FILE, "w") as f: + json.dump(webhook_config, f, indent=2) + except Exception as e: + print(f"[webhook] config save error: {e}") + self.send_json({"ok": True, "url": url, "sound": sound}) + else: self.send_error(404, "Not found") diff --git a/style.css b/style.css index 4c1951e..4bd8417 100644 --- a/style.css +++ b/style.css @@ -1530,3 +1530,225 @@ body:has(#landing.sidebar) #sidebar-resize:hover, #zoom-group { opacity: 0.6; } } #fix-review-badge.has-fixes { display: block; } + +/* ── HITL: Fix Review Panel Enhancements ── */ +.fix-review-item { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.fix-review-meta { + display: flex; + align-items: center; + gap: 6px; + margin: 4px 0 8px; + flex-wrap: wrap; +} +.fix-class-badge { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + color: var(--text-dim); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.fix-confidence-badge { + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.3px; +} +.confidence-high { + background: #1a3a1a; + color: #4ade80; + border: 1px solid #2a5a2a; +} +.confidence-low { + background: #3a2a1a; + color: #fb923c; + border: 1px solid #5a3a1a; +} + +/* Batch action bar */ +.fix-batch-bar { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px 10px; + margin-bottom: 12px; + flex-wrap: wrap; +} +.fix-batch-class { + font-weight: 700; + color: var(--text-bright); + font-size: 11px; + text-transform: uppercase; +} +.fix-batch-count { + color: var(--text-dim); + font-size: 11px; +} +.btn-batch { + padding: 4px 10px !important; + font-size: 11px !important; + flex: 0 !important; +} + +/* Revert button */ +.btn-revert { + background: #2a1a3a; + color: #c084fc; + border: 1px solid #3a2a5a !important; + flex: 1; + padding: 8px; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-size: 12px; + font-weight: 600; + transition: all 0.15s; +} +.btn-revert:hover { background: #3a2a5a; } +.btn-revert:disabled { opacity: 0.5; cursor: default; } + +/* Code diff section */ +.fix-code-diff-wrapper { + margin: 8px 0; +} +.btn-show-diff { + background: none; + border: 1px solid var(--border); + color: var(--text-dim); + border-radius: 4px; + padding: 4px 10px; + font-size: 11px; + font-family: inherit; + cursor: pointer; + transition: all 0.15s; +} +.btn-show-diff:hover { background: var(--bg-card); color: var(--text); } +.fix-code-diff { + margin-top: 6px; + max-height: 300px; + overflow: auto; + border: 1px solid var(--border); + border-radius: 4px; + background: #0d1117; +} +.fix-code-diff.hidden { display: none; } +.fix-diff-file { + border-bottom: 1px solid var(--border); +} +.fix-diff-file:last-child { border-bottom: none; } +.fix-diff-filename { + padding: 4px 8px; + background: var(--bg-card); + color: var(--text-dim); + font-size: 10px; + font-family: 'Menlo', 'Consolas', monospace; + border-bottom: 1px solid var(--border); + display: flex; + gap: 6px; + align-items: center; +} +.fix-diff-tool { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: 3px; + padding: 0 4px; + font-size: 9px; + color: var(--text-dim); +} +.fix-diff-pre { + margin: 0; + padding: 6px 8px; + font-family: 'Menlo', 'Consolas', monospace; + font-size: 11px; + line-height: 1.5; + white-space: pre; + overflow-x: auto; +} +.diff-add { color: #4ade80; display: block; } +.diff-del { color: #f87171; display: block; } +.diff-hunk { color: #60a5fa; display: block; } +.diff-header { color: var(--text-dim); display: block; } +.diff-ctx { color: var(--text-dim); display: block; } + +/* Graduation bar and stats */ +.fix-graduation-bar { + display: flex; + align-items: center; + gap: 10px; + background: #1a2a1a; + border: 1px solid #2a5a2a; + border-radius: 6px; + padding: 8px 10px; + margin: 8px 0; + font-size: 11px; + color: #4ade80; + flex-wrap: wrap; +} +.fix-graduation-bar span { flex: 1; } +.btn-graduate { + background: #1a4a1a; + color: #4ade80; + border: 1px solid #2a7a2a !important; + border-radius: 4px; + padding: 6px 12px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + font-family: inherit; + transition: all 0.15s; + white-space: nowrap; +} +.btn-graduate:hover { background: #2a6a2a; } +.btn-graduate-small { + padding: 2px 8px !important; + font-size: 10px !important; +} +.fix-grad-badge { + border-radius: 10px; + padding: 2px 8px; + font-size: 10px; + font-weight: 700; +} +.grad-auto { + background: #1a3a1a; + color: #4ade80; + border: 1px solid #2a5a2a; +} +.fix-graduation-stats { + margin-top: 16px; + border-top: 1px solid var(--border); + padding-top: 10px; +} +.fix-graduation-stats summary { + cursor: pointer; + color: var(--text-dim); + font-size: 11px; + padding: 4px 0; + user-select: none; +} +.fix-graduation-stats summary:hover { color: var(--text); } +.fix-stats-table { + width: 100%; + border-collapse: collapse; + margin-top: 8px; + font-size: 11px; +} +.fix-stats-table th, .fix-stats-table td { + padding: 4px 6px; + text-align: left; + border-bottom: 1px solid var(--border); + color: var(--text-dim); +} +.fix-stats-table th { color: var(--text); font-weight: 600; } +