@boyingliu01/xp-gate 0.5.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/xp-gate.js +134 -4
- package/hooks/pre-push +11 -2
- package/lib/__tests__/detect-deps.test.js +34 -0
- package/lib/__tests__/gate-audit.test.ts +199 -0
- package/lib/__tests__/install-skill.test.js +45 -0
- package/lib/detect-deps.js +19 -6
- package/lib/gate-audit.ts +279 -0
- package/lib/install-skill.js +26 -15
- package/lib/ui-detector.ts +5 -5
- package/lib/ui-review.ts +22 -22
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/qoder/AGENTS.md +93 -0
- package/plugins/qoder/README.md +87 -0
- package/plugins/qoder/widgets/quality-report.html +163 -0
- package/plugins/qoder/widgets/sprint-dashboard.html +172 -0
- package/skills/delphi-review/SKILL.md +48 -0
- package/skills/delphi-review/references/qoder-multi-model.md +191 -0
- package/skills/ralph-loop/SKILL.md +40 -0
- package/skills/sprint-flow/SKILL.md +68 -0
- package/skills/sprint-flow/references/qoder-adaptation.md +173 -0
- package/skills/test-specification-alignment/SKILL.md +23 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
:root {
|
|
3
|
+
--qr-bg: var(--vscode-editor-background, #1e1e1e);
|
|
4
|
+
--qr-fg: var(--vscode-editor-foreground, #d4d4d4);
|
|
5
|
+
--qr-border: var(--vscode-panel-border, #333);
|
|
6
|
+
--qr-pass: var(--vscode-testing-iconPassed, #73c991);
|
|
7
|
+
--qr-fail: var(--vscode-testing-iconFailed, #f14c4c);
|
|
8
|
+
--qr-skip: var(--vscode-descriptionForeground, #888);
|
|
9
|
+
--qr-accent: var(--vscode-focusBorder, #007acc);
|
|
10
|
+
--qr-card-bg: var(--vscode-editor-widget-background, #252526);
|
|
11
|
+
}
|
|
12
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
13
|
+
body { font-family: var(--vscode-font-family, 'Segoe UI', sans-serif); color: var(--qr-fg); background: var(--qr-bg); padding: 16px; }
|
|
14
|
+
.qr-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
15
|
+
.qr-header h2 { font-size: 16px; font-weight: 600; }
|
|
16
|
+
.qr-verdict { padding: 4px 12px; border-radius: 4px; font-size: 13px; font-weight: 600; }
|
|
17
|
+
.qr-verdict.pass { background: color-mix(in srgb, var(--qr-pass) 20%, transparent); color: var(--qr-pass); border: 1px solid var(--qr-pass); }
|
|
18
|
+
.qr-verdict.fail { background: color-mix(in srgb, var(--qr-fail) 20%, transparent); color: var(--qr-fail); border: 1px solid var(--qr-fail); }
|
|
19
|
+
.qr-meta { font-size: 12px; color: var(--qr-skip); margin-bottom: 16px; display: flex; gap: 16px; }
|
|
20
|
+
.qr-score-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 20px; }
|
|
21
|
+
.qr-score-bar .bar-track { flex: 1; height: 8px; background: var(--qr-border); border-radius: 4px; overflow: hidden; }
|
|
22
|
+
.qr-score-bar .bar-fill { height: 100%; border-radius: 4px; transition: width 0.4s ease; }
|
|
23
|
+
.qr-score-bar .bar-fill.pass { background: var(--qr-pass); }
|
|
24
|
+
.qr-score-bar .bar-fill.fail { background: var(--qr-fail); }
|
|
25
|
+
.qr-score-bar .score-label { font-size: 14px; font-weight: 600; min-width: 48px; text-align: right; }
|
|
26
|
+
.qr-gates { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-bottom: 20px; }
|
|
27
|
+
.qr-gate-card { background: var(--qr-card-bg); border: 1px solid var(--qr-border); border-radius: 6px; padding: 12px; display: flex; align-items: flex-start; gap: 10px; }
|
|
28
|
+
.qr-gate-icon { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
|
|
29
|
+
.qr-gate-icon.pass { background: color-mix(in srgb, var(--qr-pass) 20%, transparent); color: var(--qr-pass); }
|
|
30
|
+
.qr-gate-icon.fail { background: color-mix(in srgb, var(--qr-fail) 20%, transparent); color: var(--qr-fail); }
|
|
31
|
+
.qr-gate-icon.skip { background: color-mix(in srgb, var(--qr-skip) 20%, transparent); color: var(--qr-skip); }
|
|
32
|
+
.qr-gate-info { flex: 1; min-width: 0; }
|
|
33
|
+
.qr-gate-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
34
|
+
.qr-gate-detail { font-size: 11px; color: var(--qr-skip); margin-top: 2px; }
|
|
35
|
+
.qr-section { margin-bottom: 16px; }
|
|
36
|
+
.qr-section h3 { font-size: 14px; font-weight: 600; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--qr-border); }
|
|
37
|
+
.qr-delphi-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 13px; }
|
|
38
|
+
.qr-delphi-row .expert-badge { padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: 600; background: var(--qr-card-bg); border: 1px solid var(--qr-border); }
|
|
39
|
+
.qr-delphi-row .verdict-tag { margin-left: auto; font-weight: 500; }
|
|
40
|
+
.qr-delphi-row .verdict-tag.approved { color: var(--qr-pass); }
|
|
41
|
+
.qr-delphi-row .verdict-tag.rejected { color: var(--qr-fail); }
|
|
42
|
+
.qr-metrics { display: flex; gap: 12px; flex-wrap: wrap; }
|
|
43
|
+
.qr-metric { background: var(--qr-card-bg); border: 1px solid var(--qr-border); border-radius: 6px; padding: 12px 16px; text-align: center; min-width: 120px; }
|
|
44
|
+
.qr-metric .value { font-size: 24px; font-weight: 700; }
|
|
45
|
+
.qr-metric .label { font-size: 11px; color: var(--qr-skip); margin-top: 4px; }
|
|
46
|
+
.qr-metric .value.pass { color: var(--qr-pass); }
|
|
47
|
+
.qr-metric .value.fail { color: var(--qr-fail); }
|
|
48
|
+
.qr-empty { text-align: center; padding: 32px; color: var(--qr-skip); font-size: 13px; }
|
|
49
|
+
</style>
|
|
50
|
+
|
|
51
|
+
<div id="qr-root"></div>
|
|
52
|
+
|
|
53
|
+
<script>
|
|
54
|
+
(function() {
|
|
55
|
+
const data = window.__WIDGET_DATA__ || {};
|
|
56
|
+
const root = document.getElementById('qr-root');
|
|
57
|
+
|
|
58
|
+
if (!data || (!data.overall && !data.gates)) {
|
|
59
|
+
root.innerHTML = '<div class="qr-empty">No quality report data available.<br>Run the quality gates pipeline to generate a report.</div>';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const overall = data.overall || {};
|
|
64
|
+
const gates = data.gates || {};
|
|
65
|
+
const delphi = data.delphi || null;
|
|
66
|
+
const testAlign = data.testAlignment || null;
|
|
67
|
+
const isPass = overall.verdict === 'PASS';
|
|
68
|
+
|
|
69
|
+
let html = '';
|
|
70
|
+
|
|
71
|
+
// Header
|
|
72
|
+
html += '<div class="qr-header">';
|
|
73
|
+
html += '<h2>Quality Report' + (data.project ? ' — ' + esc(data.project) : '') + '</h2>';
|
|
74
|
+
html += '<span class="qr-verdict ' + (isPass ? 'pass' : 'fail') + '">' + esc(overall.verdict || 'N/A') + '</span>';
|
|
75
|
+
html += '</div>';
|
|
76
|
+
|
|
77
|
+
// Meta
|
|
78
|
+
if (data.generatedAt || data.commit) {
|
|
79
|
+
html += '<div class="qr-meta">';
|
|
80
|
+
if (data.generatedAt) html += '<span>Generated: ' + esc(formatDate(data.generatedAt)) + '</span>';
|
|
81
|
+
if (data.commit) html += '<span>Commit: ' + esc(data.commit.substring(0, 8)) + '</span>';
|
|
82
|
+
if (data.language) html += '<span>Language: ' + esc(data.language) + '</span>';
|
|
83
|
+
html += '</div>';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Score bar
|
|
87
|
+
const passed = overall.gatesPassed || 0;
|
|
88
|
+
const total = overall.gatesTotal || 0;
|
|
89
|
+
const score = overall.score || 0;
|
|
90
|
+
const pct = total > 0 ? Math.round((passed / total) * 100) : 0;
|
|
91
|
+
html += '<div class="qr-score-bar">';
|
|
92
|
+
html += '<div class="bar-track"><div class="bar-fill ' + (isPass ? 'pass' : 'fail') + '" style="width:' + pct + '%"></div></div>';
|
|
93
|
+
html += '<span class="score-label">' + passed + '/' + total + '</span>';
|
|
94
|
+
html += '</div>';
|
|
95
|
+
|
|
96
|
+
// Gate cards
|
|
97
|
+
html += '<div class="qr-gates">';
|
|
98
|
+
const gateKeys = Object.keys(gates).sort();
|
|
99
|
+
for (const key of gateKeys) {
|
|
100
|
+
const g = gates[key];
|
|
101
|
+
const status = (g.status || 'SKIP').toUpperCase();
|
|
102
|
+
const icon = status === 'PASS' ? '✓' : status === 'FAIL' ? '✗' : '—';
|
|
103
|
+
const cls = status === 'PASS' ? 'pass' : status === 'FAIL' ? 'fail' : 'skip';
|
|
104
|
+
html += '<div class="qr-gate-card">';
|
|
105
|
+
html += '<div class="qr-gate-icon ' + cls + '">' + icon + '</div>';
|
|
106
|
+
html += '<div class="qr-gate-info">';
|
|
107
|
+
html += '<div class="qr-gate-name">' + esc(g.name || key) + '</div>';
|
|
108
|
+
let detail = '';
|
|
109
|
+
if (g.tool) detail += 'Tool: ' + esc(g.tool);
|
|
110
|
+
if (g.metric) detail += (detail ? ' · ' : '') + esc(g.metric);
|
|
111
|
+
if (g.threshold) detail += (detail ? ' · ' : '') + 'Threshold: ' + esc(g.threshold);
|
|
112
|
+
if (g.coverage) detail += (detail ? ' · ' : '') + 'Coverage: ' + esc(g.coverage);
|
|
113
|
+
if (g.warnings !== undefined) detail += (detail ? ' · ' : '') + 'Warnings: ' + g.warnings;
|
|
114
|
+
if (!detail) detail = status;
|
|
115
|
+
html += '<div class="qr-gate-detail">' + detail + '</div>';
|
|
116
|
+
html += '</div></div>';
|
|
117
|
+
}
|
|
118
|
+
html += '</div>';
|
|
119
|
+
|
|
120
|
+
// Delphi Review section
|
|
121
|
+
if (delphi) {
|
|
122
|
+
html += '<div class="qr-section"><h3>Delphi Review</h3>';
|
|
123
|
+
if (delphi.experts) {
|
|
124
|
+
for (const exp of delphi.experts) {
|
|
125
|
+
const vCls = (exp.verdict || '').toUpperCase() === 'APPROVED' ? 'approved' : 'rejected';
|
|
126
|
+
html += '<div class="qr-delphi-row">';
|
|
127
|
+
html += '<span class="expert-badge">' + esc(exp.id || '?') + '</span>';
|
|
128
|
+
html += '<span>' + esc(exp.role || '') + '</span>';
|
|
129
|
+
html += '<span class="verdict-tag ' + vCls + '">' + esc(exp.verdict || 'N/A') + '</span>';
|
|
130
|
+
html += '</div>';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (delphi.consensus !== undefined) {
|
|
134
|
+
html += '<div class="qr-delphi-row"><span>Consensus</span><span class="verdict-tag ' + (delphi.consensus >= 0.95 ? 'approved' : 'rejected') + '">' + Math.round(delphi.consensus * 100) + '%</span></div>';
|
|
135
|
+
}
|
|
136
|
+
html += '</div>';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Metrics section (test alignment + coverage)
|
|
140
|
+
const hasMetrics = testAlign || (overall.coverage_pct !== undefined);
|
|
141
|
+
if (hasMetrics) {
|
|
142
|
+
html += '<div class="qr-section"><h3>Metrics</h3><div class="qr-metrics">';
|
|
143
|
+
if (testAlign) {
|
|
144
|
+
const alignScore = testAlign.score || 0;
|
|
145
|
+
const alignCls = alignScore >= 80 ? 'pass' : 'fail';
|
|
146
|
+
html += '<div class="qr-metric"><div class="value ' + alignCls + '">' + alignScore + '%</div><div class="label">Test-Spec Alignment</div></div>';
|
|
147
|
+
}
|
|
148
|
+
if (overall.coverage_pct !== undefined) {
|
|
149
|
+
const covCls = overall.coverage_pct >= 80 ? 'pass' : 'fail';
|
|
150
|
+
html += '<div class="qr-metric"><div class="value ' + covCls + '">' + overall.coverage_pct + '%</div><div class="label">Coverage</div></div>';
|
|
151
|
+
}
|
|
152
|
+
if (overall.score !== undefined) {
|
|
153
|
+
html += '<div class="qr-metric"><div class="value">' + overall.score + '</div><div class="label">Overall Score</div></div>';
|
|
154
|
+
}
|
|
155
|
+
html += '</div></div>';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
root.innerHTML = html;
|
|
159
|
+
|
|
160
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
|
|
161
|
+
function formatDate(iso) { try { return new Date(iso).toLocaleString(); } catch { return iso; } }
|
|
162
|
+
})();
|
|
163
|
+
</script>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
:root {
|
|
3
|
+
--sd-bg: var(--vscode-editor-background, #1e1e1e);
|
|
4
|
+
--sd-fg: var(--vscode-editor-foreground, #d4d4d4);
|
|
5
|
+
--sd-border: var(--vscode-panel-border, #333);
|
|
6
|
+
--sd-pass: var(--vscode-testing-iconPassed, #73c991);
|
|
7
|
+
--sd-fail: var(--vscode-testing-iconFailed, #f14c4c);
|
|
8
|
+
--sd-skip: var(--vscode-descriptionForeground, #888);
|
|
9
|
+
--sd-accent: var(--vscode-focusBorder, #007acc);
|
|
10
|
+
--sd-card-bg: var(--vscode-editor-widget-background, #252526);
|
|
11
|
+
--sd-active: var(--vscode-charts-yellow, #e5c07b);
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
body { font-family: var(--vscode-font-family, 'Segoe UI', sans-serif); color: var(--sd-fg); background: var(--sd-bg); padding: 16px; }
|
|
15
|
+
.sd-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
16
|
+
.sd-header h2 { font-size: 16px; font-weight: 600; }
|
|
17
|
+
.sd-status-badge { padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
|
18
|
+
.sd-status-badge.running { background: color-mix(in srgb, var(--sd-accent) 20%, transparent); color: var(--sd-accent); border: 1px solid var(--sd-accent); }
|
|
19
|
+
.sd-status-badge.paused { background: color-mix(in srgb, var(--sd-active) 20%, transparent); color: var(--sd-active); border: 1px solid var(--sd-active); }
|
|
20
|
+
.sd-status-badge.completed, .sd-status-badge.merged { background: color-mix(in srgb, var(--sd-pass) 20%, transparent); color: var(--sd-pass); border: 1px solid var(--sd-pass); }
|
|
21
|
+
.sd-meta { font-size: 12px; color: var(--sd-skip); margin-bottom: 16px; }
|
|
22
|
+
.sd-phase-track { display: flex; align-items: center; gap: 4px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 4px; }
|
|
23
|
+
.sd-phase { display: flex; flex-direction: column; align-items: center; min-width: 72px; }
|
|
24
|
+
.sd-phase-dot { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; border: 2px solid var(--sd-border); color: var(--sd-skip); background: var(--sd-bg); }
|
|
25
|
+
.sd-phase-dot.done { background: color-mix(in srgb, var(--sd-pass) 20%, transparent); border-color: var(--sd-pass); color: var(--sd-pass); }
|
|
26
|
+
.sd-phase-dot.active { background: color-mix(in srgb, var(--sd-accent) 25%, transparent); border-color: var(--sd-accent); color: var(--sd-accent); animation: pulse 1.5s infinite; }
|
|
27
|
+
.sd-phase-dot.future { border-color: var(--sd-border); color: var(--sd-skip); }
|
|
28
|
+
.sd-phase-label { font-size: 10px; margin-top: 4px; color: var(--sd-skip); text-align: center; white-space: nowrap; }
|
|
29
|
+
.sd-phase-label.active { color: var(--sd-accent); font-weight: 600; }
|
|
30
|
+
.sd-phase-label.done { color: var(--sd-pass); }
|
|
31
|
+
.sd-phase-line { width: 16px; height: 2px; background: var(--sd-border); margin-bottom: 20px; }
|
|
32
|
+
.sd-phase-line.done { background: var(--sd-pass); }
|
|
33
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
34
|
+
.sd-section { margin-bottom: 16px; }
|
|
35
|
+
.sd-section h3 { font-size: 14px; font-weight: 600; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--sd-border); }
|
|
36
|
+
.sd-progress-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
37
|
+
.sd-progress-row .label { font-size: 13px; min-width: 100px; }
|
|
38
|
+
.sd-progress-row .bar-track { flex: 1; height: 10px; background: var(--sd-border); border-radius: 5px; overflow: hidden; }
|
|
39
|
+
.sd-progress-row .bar-fill { height: 100%; border-radius: 5px; background: var(--sd-accent); transition: width 0.4s ease; }
|
|
40
|
+
.sd-progress-row .count { font-size: 13px; font-weight: 600; min-width: 60px; text-align: right; }
|
|
41
|
+
.sd-outputs { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }
|
|
42
|
+
.sd-output-item { background: var(--sd-card-bg); border: 1px solid var(--sd-border); border-radius: 4px; padding: 8px 10px; font-size: 12px; }
|
|
43
|
+
.sd-output-item .key { color: var(--sd-skip); font-size: 11px; }
|
|
44
|
+
.sd-output-item .val { margin-top: 2px; font-weight: 500; word-break: break-all; }
|
|
45
|
+
.sd-metrics { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
46
|
+
.sd-metric-card { background: var(--sd-card-bg); border: 1px solid var(--sd-border); border-radius: 6px; padding: 10px 14px; text-align: center; min-width: 100px; }
|
|
47
|
+
.sd-metric-card .value { font-size: 22px; font-weight: 700; color: var(--sd-accent); }
|
|
48
|
+
.sd-metric-card .label { font-size: 11px; color: var(--sd-skip); margin-top: 2px; }
|
|
49
|
+
.sd-learnings { list-style: none; }
|
|
50
|
+
.sd-learnings li { padding: 6px 0; font-size: 12px; border-bottom: 1px solid var(--sd-border); }
|
|
51
|
+
.sd-learnings li:last-child { border-bottom: none; }
|
|
52
|
+
.sd-learnings .cat { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; background: var(--sd-card-bg); border: 1px solid var(--sd-border); margin-right: 6px; }
|
|
53
|
+
.sd-empty { text-align: center; padding: 32px; color: var(--sd-skip); font-size: 13px; }
|
|
54
|
+
</style>
|
|
55
|
+
|
|
56
|
+
<div id="sd-root"></div>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
(function() {
|
|
60
|
+
const data = window.__WIDGET_DATA__ || {};
|
|
61
|
+
const root = document.getElementById('sd-root');
|
|
62
|
+
|
|
63
|
+
if (!data || (!data.phase && !data.id && !data.status)) {
|
|
64
|
+
root.innerHTML = '<div class="sd-empty">No sprint state data available.<br>Start a sprint with /sprint-flow to see the dashboard.</div>';
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PHASES = [
|
|
69
|
+
{ num: -1, name: 'ISOLATE' },
|
|
70
|
+
{ num: 0, name: 'THINK' },
|
|
71
|
+
{ num: 1, name: 'PLAN' },
|
|
72
|
+
{ num: 2, name: 'BUILD' },
|
|
73
|
+
{ num: 3, name: 'REVIEW' },
|
|
74
|
+
{ num: 4, name: 'UAT' },
|
|
75
|
+
{ num: 5, name: 'FEEDBACK' },
|
|
76
|
+
{ num: 6, name: 'SHIP' },
|
|
77
|
+
{ num: 7, name: 'LAND' },
|
|
78
|
+
{ num: 8, name: 'CLEANUP' }
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const currentPhase = data.phase !== undefined ? data.phase : -2;
|
|
82
|
+
const status = data.status || 'unknown';
|
|
83
|
+
const outputs = data.outputs || {};
|
|
84
|
+
const metrics = data.metrics || {};
|
|
85
|
+
const learnings = data.learnings || [];
|
|
86
|
+
|
|
87
|
+
let html = '';
|
|
88
|
+
|
|
89
|
+
// Header
|
|
90
|
+
html += '<div class="sd-header">';
|
|
91
|
+
html += '<h2>Sprint' + (data.id ? ' — ' + esc(data.id) : '') + '</h2>';
|
|
92
|
+
const statusCls = ['running', 'paused', 'completed', 'merged'].includes(status) ? status : 'running';
|
|
93
|
+
html += '<span class="sd-status-badge ' + statusCls + '">' + esc(status) + '</span>';
|
|
94
|
+
html += '</div>';
|
|
95
|
+
|
|
96
|
+
// Phase progress track
|
|
97
|
+
html += '<div class="sd-phase-track">';
|
|
98
|
+
for (let i = 0; i < PHASES.length; i++) {
|
|
99
|
+
const p = PHASES[i];
|
|
100
|
+
if (i > 0) {
|
|
101
|
+
const lineCls = p.num <= currentPhase ? 'done' : '';
|
|
102
|
+
html += '<div class="sd-phase-line ' + lineCls + '"></div>';
|
|
103
|
+
}
|
|
104
|
+
let dotCls = 'future';
|
|
105
|
+
if (p.num < currentPhase) dotCls = 'done';
|
|
106
|
+
else if (p.num === currentPhase) dotCls = 'active';
|
|
107
|
+
const lblCls = p.num < currentPhase ? 'done' : p.num === currentPhase ? 'active' : '';
|
|
108
|
+
const icon = p.num < currentPhase ? '✓' : p.num === currentPhase ? '▶' : String(p.num);
|
|
109
|
+
html += '<div class="sd-phase">';
|
|
110
|
+
html += '<div class="sd-phase-dot ' + dotCls + '">' + icon + '</div>';
|
|
111
|
+
html += '<div class="sd-phase-label ' + lblCls + '">' + esc(p.name) + '</div>';
|
|
112
|
+
html += '</div>';
|
|
113
|
+
}
|
|
114
|
+
html += '</div>';
|
|
115
|
+
|
|
116
|
+
// Metrics cards
|
|
117
|
+
if (metrics.tests_passed !== undefined || metrics.coverage_pct !== undefined || metrics.tests_failed !== undefined) {
|
|
118
|
+
html += '<div class="sd-metrics">';
|
|
119
|
+
if (metrics.tests_passed !== undefined) {
|
|
120
|
+
html += '<div class="sd-metric-card"><div class="value">' + metrics.tests_passed + '</div><div class="label">Tests Passed</div></div>';
|
|
121
|
+
}
|
|
122
|
+
if (metrics.tests_failed !== undefined) {
|
|
123
|
+
html += '<div class="sd-metric-card"><div class="value" style="color:' + (metrics.tests_failed > 0 ? 'var(--sd-fail)' : 'var(--sd-pass)') + '">' + metrics.tests_failed + '</div><div class="label">Tests Failed</div></div>';
|
|
124
|
+
}
|
|
125
|
+
if (metrics.coverage_pct !== undefined) {
|
|
126
|
+
html += '<div class="sd-metric-card"><div class="value" style="color:' + (metrics.coverage_pct >= 80 ? 'var(--sd-pass)' : 'var(--sd-fail)') + '">' + metrics.coverage_pct + '%</div><div class="label">Coverage</div></div>';
|
|
127
|
+
}
|
|
128
|
+
html += '</div>';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// REQ progress
|
|
132
|
+
if (data.req_progress || data.reqs) {
|
|
133
|
+
const rp = data.req_progress || {};
|
|
134
|
+
const done = rp.done || 0;
|
|
135
|
+
const totalReqs = rp.total || (data.reqs ? data.reqs.length : 0);
|
|
136
|
+
const pct = totalReqs > 0 ? Math.round((done / totalReqs) * 100) : 0;
|
|
137
|
+
html += '<div class="sd-section"><h3>REQ Progress</h3>';
|
|
138
|
+
html += '<div class="sd-progress-row">';
|
|
139
|
+
html += '<span class="label">Completed</span>';
|
|
140
|
+
html += '<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>';
|
|
141
|
+
html += '<span class="count">' + done + '/' + totalReqs + '</span>';
|
|
142
|
+
html += '</div></div>';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Phase outputs
|
|
146
|
+
const outputKeys = Object.keys(outputs);
|
|
147
|
+
if (outputKeys.length > 0) {
|
|
148
|
+
html += '<div class="sd-section"><h3>Phase Outputs</h3>';
|
|
149
|
+
html += '<div class="sd-outputs">';
|
|
150
|
+
for (const key of outputKeys) {
|
|
151
|
+
html += '<div class="sd-output-item"><div class="key">' + esc(key) + '</div><div class="val">' + esc(outputs[key]) + '</div></div>';
|
|
152
|
+
}
|
|
153
|
+
html += '</div></div>';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Learnings
|
|
157
|
+
if (learnings.length > 0) {
|
|
158
|
+
html += '<div class="sd-section"><h3>Recent Learnings</h3>';
|
|
159
|
+
html += '<ul class="sd-learnings">';
|
|
160
|
+
const recent = learnings.slice(-5).reverse();
|
|
161
|
+
for (const l of recent) {
|
|
162
|
+
const cat = l.category || 'general';
|
|
163
|
+
html += '<li><span class="cat">' + esc(cat) + '</span>' + esc(l.summary || l.text || JSON.stringify(l)) + '</li>';
|
|
164
|
+
}
|
|
165
|
+
html += '</ul></div>';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
root.innerHTML = html;
|
|
169
|
+
|
|
170
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
|
|
171
|
+
})();
|
|
172
|
+
</script>
|
|
@@ -316,6 +316,54 @@ Every review round output MUST follow this exact JSON structure:
|
|
|
316
316
|
|
|
317
317
|
---
|
|
318
318
|
|
|
319
|
+
## Qoder 平台适配
|
|
320
|
+
|
|
321
|
+
在 Qoder IDE 中,delphi-review 的多专家评审通过以下方式实现:
|
|
322
|
+
|
|
323
|
+
### 多模型评审(替代 OpenCode 的多 agent 配置)
|
|
324
|
+
|
|
325
|
+
利用 Qoder 的多模型访问能力(类似 coding plan),通过 Agent subagent 派发不同模型完成 Expert A/B/C 评审:
|
|
326
|
+
|
|
327
|
+
| 专家 | 角色 | Qoder Subagent 映射 | 说明 |
|
|
328
|
+
|------|------|---------------------|------|
|
|
329
|
+
| Expert A (架构) | 架构审查 | **plan-agent** subagent | 适合架构级分析 |
|
|
330
|
+
| Expert B (技术) | 实现审查 | **CodeReview** subagent | 代码审查天然适合 |
|
|
331
|
+
| Expert C (可行性) | 可行性仲裁 | **plan-agent** subagent(不同 prompt) | 与 Expert A 使用不同的评审 prompt |
|
|
332
|
+
|
|
333
|
+
**执行流程**:
|
|
334
|
+
1. orchestrator 准备评审材料(设计文档/代码变更)
|
|
335
|
+
2. 分别派发 2-3 个 Agent subagent,每个 subagent 携带对应专家的 system prompt
|
|
336
|
+
3. 收集各专家评审结果 → 合并为共识报告
|
|
337
|
+
4. 如未达成共识 → 迭代(携带其他专家意见重新评审)
|
|
338
|
+
|
|
339
|
+
### 专家配置
|
|
340
|
+
|
|
341
|
+
专家→模型映射通过 `.qoder/delphi-config.json` 配置:
|
|
342
|
+
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"experts": {
|
|
346
|
+
"A": { "role": "architecture", "model_hint": "deepseek-v4-pro" },
|
|
347
|
+
"B": { "role": "implementation", "model_hint": "kimi-k2.6" },
|
|
348
|
+
"C": { "role": "feasibility", "model_hint": "qwen3.6-plus" }
|
|
349
|
+
},
|
|
350
|
+
"consensus_threshold": 0.95,
|
|
351
|
+
"max_rounds": 5
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
> **注意**:`model_hint` 是建议模型,实际使用的模型取决于 Qoder 的模型调度策略。
|
|
356
|
+
|
|
357
|
+
### 降级方案
|
|
358
|
+
|
|
359
|
+
如果多模型 subagent 调用不可用,降级为**单模型多角色模式**:
|
|
360
|
+
- 同一模型分别扮演 Expert A/B/C(不同 system prompt)
|
|
361
|
+
- 明确标注降级警告:`[DEGRADED] 单模型多角色模式,失去跨 provider 匿名性保护`
|
|
362
|
+
|
|
363
|
+
详细配置和执行指南参见 `references/qoder-multi-model.md`。
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
319
367
|
## Anti-Patterns
|
|
320
368
|
|
|
321
369
|
| ❌ 错误 | ✅ 正确 |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Qoder Multi-Model Review Guide for Delphi Review
|
|
2
|
+
|
|
3
|
+
**Version:** v0.6.0
|
|
4
|
+
**Platform:** Qoder IDE
|
|
5
|
+
**Status:** Active
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Overview
|
|
10
|
+
|
|
11
|
+
Delphi Review requires multi-expert anonymous consensus review using at least 2 different LLM providers. In Qoder, this is achieved through Agent subagent dispatch, leveraging Qoder's built-in multi-model capability.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Expert Configuration
|
|
16
|
+
|
|
17
|
+
### 2.1 Configuration File
|
|
18
|
+
|
|
19
|
+
Create `.qoder/delphi-config.json` in project root:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"experts": {
|
|
24
|
+
"A": {
|
|
25
|
+
"role": "architecture",
|
|
26
|
+
"focus": "System design, scalability, maintainability, SOLID principles",
|
|
27
|
+
"model_hint": "deepseek-v4-pro"
|
|
28
|
+
},
|
|
29
|
+
"B": {
|
|
30
|
+
"role": "implementation",
|
|
31
|
+
"focus": "Code quality, testing strategy, error handling, performance",
|
|
32
|
+
"model_hint": "kimi-k2.6"
|
|
33
|
+
},
|
|
34
|
+
"C": {
|
|
35
|
+
"role": "feasibility",
|
|
36
|
+
"focus": "Practical constraints, timeline, team capability, risk assessment",
|
|
37
|
+
"model_hint": "qwen3.6-plus"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"consensus_threshold": 0.95,
|
|
41
|
+
"max_rounds": 5,
|
|
42
|
+
"providers_required": 2
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2.2 Model Selection Rules
|
|
47
|
+
|
|
48
|
+
- Experts MUST come from **at least 2 different providers**
|
|
49
|
+
- **Forbidden**: Anthropic, OpenAI, Google (foreign models)
|
|
50
|
+
- **Recommended domestic providers**: DeepSeek, Qwen (Alibaba), Kimi (Moonshot), GLM (Zhipu), MiniMax
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. Execution Flow in Qoder
|
|
55
|
+
|
|
56
|
+
### 3.1 Round 1: Independent Anonymous Review
|
|
57
|
+
|
|
58
|
+
For each expert, dispatch an Agent subagent with the expert's specific prompt:
|
|
59
|
+
|
|
60
|
+
**Expert A (Architecture) — plan-agent**:
|
|
61
|
+
```
|
|
62
|
+
Agent(subagent_type="plan-agent", prompt="""
|
|
63
|
+
You are Expert A in a Delphi consensus review. Your role: Architecture Reviewer.
|
|
64
|
+
Focus: System design, scalability, maintainability, SOLID principles.
|
|
65
|
+
|
|
66
|
+
Review the following design/code and provide your assessment:
|
|
67
|
+
[REVIEW MATERIAL]
|
|
68
|
+
|
|
69
|
+
Output format:
|
|
70
|
+
{
|
|
71
|
+
"expert_id": "A",
|
|
72
|
+
"verdict": "APPROVED|REQUEST_CHANGES|REJECTED",
|
|
73
|
+
"issues": [...],
|
|
74
|
+
"strengths": [...],
|
|
75
|
+
"suggestions": [...]
|
|
76
|
+
}
|
|
77
|
+
""")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Expert B (Implementation) — CodeReview subagent**:
|
|
81
|
+
```
|
|
82
|
+
Agent(subagent_type="CodeReview", prompt="""
|
|
83
|
+
You are Expert B in a Delphi consensus review. Your role: Implementation Reviewer.
|
|
84
|
+
Focus: Code quality, testing strategy, error handling, performance.
|
|
85
|
+
|
|
86
|
+
Review the following code changes:
|
|
87
|
+
[CHANGED FILES]
|
|
88
|
+
""")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Expert C (Feasibility) — plan-agent**:
|
|
92
|
+
```
|
|
93
|
+
Agent(subagent_type="plan-agent", prompt="""
|
|
94
|
+
You are Expert C in a Delphi consensus review. Your role: Feasibility Arbiter.
|
|
95
|
+
Focus: Practical constraints, timeline, team capability, risk assessment.
|
|
96
|
+
|
|
97
|
+
Review the following design/code and provide your assessment:
|
|
98
|
+
[REVIEW MATERIAL]
|
|
99
|
+
|
|
100
|
+
Output format:
|
|
101
|
+
{
|
|
102
|
+
"expert_id": "C",
|
|
103
|
+
"verdict": "APPROVED|REQUEST_CHANGES|REJECTED",
|
|
104
|
+
...
|
|
105
|
+
}
|
|
106
|
+
""")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3.2 Consensus Aggregation
|
|
110
|
+
|
|
111
|
+
After all experts respond:
|
|
112
|
+
1. Parse each expert's JSON output
|
|
113
|
+
2. Compare verdicts:
|
|
114
|
+
- All APPROVED → **Consensus: APPROVED**
|
|
115
|
+
- All REQUEST_CHANGES → Merge issues, present to user
|
|
116
|
+
- Mixed verdicts → **No consensus**, proceed to Round 2
|
|
117
|
+
3. Calculate issue consensus ratio: issues agreed by ≥95% of experts → resolved
|
|
118
|
+
|
|
119
|
+
### 3.3 Round 2+: Controlled Feedback
|
|
120
|
+
|
|
121
|
+
In subsequent rounds, each expert receives other experts' anonymized opinions:
|
|
122
|
+
```
|
|
123
|
+
Agent(subagent_type="plan-agent", prompt="""
|
|
124
|
+
You are Expert A. In Round 1, your verdict was: REQUEST_CHANGES.
|
|
125
|
+
Other experts' anonymized feedback:
|
|
126
|
+
- Expert X: APPROVED, noted [strengths]
|
|
127
|
+
- Expert Y: REQUEST_CHANGES, noted [issues]
|
|
128
|
+
|
|
129
|
+
Review your assessment in light of others' feedback. Maintain your independence.
|
|
130
|
+
[REVIEW MATERIAL]
|
|
131
|
+
""")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3.4 Terminal States
|
|
135
|
+
|
|
136
|
+
| Condition | Action |
|
|
137
|
+
|-----------|--------|
|
|
138
|
+
| All experts APPROVED + ≥95% issue consensus | **APPROVED** — write `.sprint-state/delphi-reviewed.json` |
|
|
139
|
+
| Max rounds reached without consensus | **REJECTED** — present disagreement summary |
|
|
140
|
+
| Any expert REJECTED with critical issues | **REQUEST_CHANGES** — user must fix and re-review |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 4. Degraded Mode
|
|
145
|
+
|
|
146
|
+
When multi-model subagent dispatch is unavailable:
|
|
147
|
+
|
|
148
|
+
### 4.1 Single-Model Multi-Role Mode
|
|
149
|
+
|
|
150
|
+
The orchestrator plays all expert roles sequentially with different prompts:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
⚠️ [DEGRADED] 单模型多角色模式
|
|
154
|
+
失去跨 provider 匿名性保护,但保留多视角评审结构。
|
|
155
|
+
建议:在条件允许时恢复多模型模式重新评审。
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 4.2 Degraded Output Format
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"mode": "design",
|
|
163
|
+
"verdict": "APPROVED",
|
|
164
|
+
"degraded": true,
|
|
165
|
+
"degradation_reason": "Multi-model subagent unavailable",
|
|
166
|
+
"experts": ["A", "B", "C"],
|
|
167
|
+
"model": "single"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. Integration with Sprint Flow
|
|
174
|
+
|
|
175
|
+
### 5.1 Phase 1 (Design Review)
|
|
176
|
+
|
|
177
|
+
- Default: 2 experts (A + B)
|
|
178
|
+
- Complex designs: 3 experts (A + B + C)
|
|
179
|
+
- Output: `.sprint-state/delphi-reviewed.json` + `specification.yaml`
|
|
180
|
+
|
|
181
|
+
### 5.2 Phase 3 (Code Walkthrough)
|
|
182
|
+
|
|
183
|
+
- Default: 2 experts (A + B)
|
|
184
|
+
- Use `CodeReview` subagent for Expert B
|
|
185
|
+
- Output: `.code-walkthrough-result.json`
|
|
186
|
+
|
|
187
|
+
### 5.3 HARD-GATE Enforcement
|
|
188
|
+
|
|
189
|
+
- Phase 2 BUILD cannot start until delphi-review verdict is APPROVED
|
|
190
|
+
- In Qoder: Pre-Edit Gate checks `.sprint-state/delphi-reviewed.json`
|
|
191
|
+
- See sprint-flow SKILL.md for Pre-Edit Gate details
|
|
@@ -297,6 +297,46 @@ Phase 0, 1, 3-6 行为完全不变。
|
|
|
297
297
|
|
|
298
298
|
---
|
|
299
299
|
|
|
300
|
+
## Qoder 平台适配
|
|
301
|
+
|
|
302
|
+
### Subagent Dispatch(替代 OpenCode 的 `task()` API)
|
|
303
|
+
|
|
304
|
+
在 Qoder 环境中,ralph-loop 的 subagent 派发使用以下方式:
|
|
305
|
+
|
|
306
|
+
| 原调用 | Qoder 替代 | 说明 |
|
|
307
|
+
|---------|------------|------|
|
|
308
|
+
| `task(category="unspecified-high", load_skills=["test-driven-development"])` | **orchestrator 直接执行** | ralph-loop 逐 REQ 迭代,每个 REQ 在干净上下文中执行,不需要独立 subagent |
|
|
309
|
+
| `gstack/learn` | **Qoder Memory 系统** | permanent learnings → `UpdateMemory`;contextual learnings → 会话上下文 |
|
|
310
|
+
|
|
311
|
+
### Learnings 持久化(替代 gstack/learn)
|
|
312
|
+
|
|
313
|
+
ralph-loop 的 learnings 分类机制在 Qoder 中通过 Memory 系统实现更优雅的持久化:
|
|
314
|
+
|
|
315
|
+
| Learnings 类型 | 原方案 | Qoder 替代 |
|
|
316
|
+
|---------------|--------|------------|
|
|
317
|
+
| **permanent** | progress.log + AGENTS.md | `UpdateMemory`(project_introduction / expert_experience / development_practice_specification 类型) |
|
|
318
|
+
| **contextual** | progress.log(最近 3 条) | 当前会话上下文传递,不持久化 |
|
|
319
|
+
| **Sprint retro** | gstack/learn | Phase 5 结束时 `UpdateMemory` 写入 |
|
|
320
|
+
|
|
321
|
+
**执行方式**:每个 REQ 完成后,orchestrator 调用 `UpdateMemory` 存储 permanent learnings:
|
|
322
|
+
- 架构决策 → `expert_experience` 类型
|
|
323
|
+
- 接口约定/全局规范 → `development_practice_specification` 类型
|
|
324
|
+
- 项目结构发现 → `project_introduction` 类型
|
|
325
|
+
|
|
326
|
+
### 三层验证 Gate 增强(替代 PostToolUse Hook)
|
|
327
|
+
|
|
328
|
+
在 Qoder 中,三层验证 Gate 增加 principles 检查步骤:
|
|
329
|
+
|
|
330
|
+
| 层级 | 原验证 | Qoder 增强 |
|
|
331
|
+
|------|--------|------------|
|
|
332
|
+
| L1 | typecheck + lint | + **principles check**(`npx -y tsx src/principles/index.ts --files <changed> --format console`) |
|
|
333
|
+
| L2 | 全量测试 | 无变化 |
|
|
334
|
+
| L3 | coverage ≥ 80% | 无变化 |
|
|
335
|
+
|
|
336
|
+
> **注意**:principles check 仅在 src/principles/ 存在时执行(非 xp-gate 项目跳过)。
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
300
340
|
## Anti-Patterns
|
|
301
341
|
|
|
302
342
|
| ❌ 错误 | ✅ 正确 |
|