`;
}
+
+ // 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 = {
`;
+
+ // 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)
+ Graduate → Autonomous
+
`;
+ }
+ 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 ``;
+ 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
+ ? `
Graduate `
+ : '';
+ return `
+ ${this._escHtml(cls)}
+ ${s.approved}✓ ${s.partial}~ ${s.rejected}✗
+ ${(s.rate * 100).toFixed(0)}%
+ ${badge}
+ `;
+ }).join('');
+
+ return `
+ Graduation Stats (${classes.length} classes)
+
+ Class Outcomes Rate Status
+ ${rows}
+
+ `;
+ } 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; }
+