@goodandready/dsh-moa 0.2.12 → 0.2.13

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/docs/README.ru.md CHANGED
@@ -16,11 +16,21 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <a href="../README.md"><b>🇬🇧 English</b></a> •
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
20
  <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
28
+ <br><br>
29
+ 🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -143,8 +153,44 @@ dsh plugin --profile web add @goodandready/dsh-moa
143
153
 
144
154
  ---
145
155
 
156
+ ## ⚡ 10 встроенных пресетов и инженерные персоны
157
+
158
+ В версии v0.2.13 добавлены 10 специализированных пресетов под реальные задачи разработки:
159
+
160
+ | Имя пресета | Назначение | Судья по умолчанию | Рецензия (Round 2) | Слепая оценка |
161
+ | :--- | :--- | :--- | :---: | :---: |
162
+ | `default` | Сбалансированная генерация | `codex:gpt-5.6-sol` | Опционально | Выкл |
163
+ | `code-review` | Глубокое взаимное ревью кода | `codex:gpt-5.6-sol` | Вкл | Вкл |
164
+ | `fast-audit` | Сверхбыстрый экспресс-аудит (Fast Mode) | `codex:gpt-5.6-sol` | Выкл | Выкл |
165
+ | `deep-architect` | Проектирование распределенных систем | `codex:gpt-5.6-sol` | Вкл | Выкл |
166
+ | `bug-hunter` | Поиск скрытых багов и граничных случаев | `codex:gpt-5.6-sol` | Вкл | Выкл |
167
+ | `refactor-cleanup` | Чистка легаси и удаление оверинжиниринга | `codex:gpt-5.6-sol` | Выкл | Выкл |
168
+ | `frontend-ui` | Качественные адаптивные веб-интерфейсы | `codex:gpt-5.6-sol` | Выкл | Выкл |
169
+ | `security-audit` | Анализ уязвимостей и санитайзинг | `codex:gpt-5.6-sol` | Вкл | Вкл |
170
+ | `math-logic` | Алгоритмы, математическая логика и доказательства | `codex:gpt-5.6-sol` | Вкл | Выкл |
171
+ | `creative-brainstorm`| Нестандартные идеи и дивергентный поиск | `codex:gpt-5.6-sol` | Выкл | Выкл |
172
+
173
+ ### Инженерные персоны кандидатов (`role_persona`)
174
+ Задавайте кандидатам четкие роли для максимального разнообразия подходов:
175
+ - **`minimalist` (Ponytail)**: решение на стандартной библиотеке, 0 лишних зависимостей, минимальный код.
176
+ - **`robustness`**: защитное программирование, валидация границ, идемпотентность, устойчивость к сбоям.
177
+ - **`performance`**: минимальная алгоритмическая сложность, экономия памяти, zero-copy.
178
+ - **`tester`**: test-driven архитектура, 100% покрытие веток, четкие ассерты.
179
+ - **`general`**: сбалансированный подход общего назначения.
180
+
181
+ ---
182
+
183
+ ## 🤝 Консилиум Раунд 2, Синтаксический гейт и Override
184
+
185
+ - **Консилиум (Раунд 2)**: флаг `peer_critique_enabled: true` включает этап взаимного ревью, в котором кандидаты изучают предложения оппонентов и дорабатывают код перед финальным судейством.
186
+ - **Синтаксический гейт**: мгновенная валидация JS/JSON через `node:vm` и `JSON.parse`. Если в коде кандидата есть опечатка, он помечается `[⚠️ Syntax Warning]`, а судья получает жесткую директиву: *если архитектура кандидата превосходит конкурентов, исправить синтаксис в финальном решении и присудить ему победу*.
187
+ - **Пользовательский выбор кандидата (Override)**: флаг `allow_candidate_override: true` сохраняет папки `.moa/candidate-N/`. Любой кандидат может быть повышен в проект командой `/moa promote <runId> <candidateIndex>` или кнопкой в карточке настроек.
188
+
189
+ ---
190
+
146
191
  ## ⚙️ Конфигурация (`settings.yaml`)
147
192
 
193
+
148
194
  Настройка пресетов и пайплайнов моделей доступна в `settings.yaml` или через интерфейс (Настройки → Плагины → Mixture of Agents):
149
195
 
150
196
  ```yaml
package/docs/README.zh.md CHANGED
@@ -16,11 +16,21 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <a href="../README.md"><b>🇬🇧 English</b></a> •
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
20
  <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
28
+ <br><br>
29
+ 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -67,3 +67,5 @@
67
67
  - 2026-09-12 — Реализация кураторского синтеза (`curator_synthesis`), строгой рубрики антипаттернов (`ANTIPATTERNS_RUBRIC`), живого потокового стриминга куратора (`stream_aggregator`), кворума кандидатов с льготным периодом (`quorum_enabled`, `grace_period_sec`), авторетрая транзиентных ошибок (`callWithTransientRetry`) и цепочки запасных судей (`aggregator_fallbacks`). Все опции конфигурируются в пресетах с сохранением 100% обратной совместимости.
68
68
 
69
69
  - 2026-09-12 (вечер) — Декомпозиция lib/moa-runner.js на специализированные модули (`lib/moa-prompts.js`, `lib/moa-parser.js`, `lib/moa-runner.js`) с соблюдением канонического лимита <= 800 строк. Реализация автоматической ротации истории при превышении 10 МБ (`rotateHistoryFileIfNeeded`) и неблокирующего асинхронного сохранения (`recordMoaRunAsync`). Настраиваемые таймауты кандидатов и агрегатора (`reference_timeout_sec`, `aggregator_timeout_sec`).
70
+
71
+ - 2026-09-13 — Расширение функционала MoA (Gitea Issue #59): интерактивный селектор альтернативного кандидата в чате (User Candidate Override: allow_candidate_override), пакет из 10 специализированных пресетов с ролями кандидатов (role_persona), локальный гейт синтаксической проверки (Syntax Pre-check Gate) с директивой автоисправления синтаксиса судьёй, режим взаимного рецензирования («Консилиум» / Peer Critique Round 2: peer_critique_enabled), фильтрация лидерборда по пресетам и выгрузка телеметрии в CSV/JSON.
@@ -0,0 +1,73 @@
1
+ # Plan 59: MoA Power Pack v0.2.13
2
+
3
+ ## Objectives
4
+ Implement the 5 approved feature expansions for `@goodandready/dsh-moa`:
5
+ 1. User Candidate Override (`allow_candidate_override: boolean`).
6
+ 2. 10 specialized built-in presets + candidate role personas (`role_persona`).
7
+ 3. Syntax Pre-check Gate with Judge Auto-Fix Directive.
8
+ 4. Consilium / Peer Critique Round 2 (`peer_critique_enabled: boolean`).
9
+ 5. Analytics filtering by preset and CSV/JSON export.
10
+
11
+ ---
12
+
13
+ ## Technical Specifications
14
+
15
+ ### 1. User Candidate Override
16
+ - **Schema**: `PresetSchema` field `allow_candidate_override: z.boolean().default(false)`.
17
+ - **Backend**:
18
+ - `writeCandidateWorkspace` stores candidates in `.moa-workspaces/<runId>/candidate-<index>`.
19
+ - Add API route `POST /dsh-moa/promote` with `{ runId, candidateIndex }` that promotes specified candidate files to the project root.
20
+ - In `formatMoAResponse`, if enabled, render interactive action hints:
21
+ `🔄 Alternative Candidate Override: /moa promote <runId> <candidateIndex>`
22
+ - **Client**: Add toggle in Section 1 / Section 3 of `lib/client.js`.
23
+
24
+ ### 2. 10 Built-in Presets & Candidate Role Personas
25
+ - **Presets**:
26
+ 1. `default`: General MoA ensemble.
27
+ 2. `code-review`: Zero regressions, backward compatibility, edge cases.
28
+ 3. `fast-audit`: Quorum mitigation, low latency.
29
+ 4. `deep-architect`: System architecture, API contracts, curator synthesis.
30
+ 5. `bug-hunter`: Concurrency, race conditions, memory leaks, null safety.
31
+ 6. `refactor-cleanup`: Ponytail minimalism, stdlib first, zero external dependencies.
32
+ 7. `frontend-ui`: Ergonomics, DSH design tokens, responsive layout, no AI slop.
33
+ 8. `security-audit`: OWASP Top 10, input sanitization, safe error handling.
34
+ 9. `math-logic`: Algorithmic correctness, asymptotic proofs.
35
+ 10. `creative-brainstorm`: High divergence, alternative architectures.
36
+ - **Candidate Roles (`role_persona`)**:
37
+ - `minimalist`: Ponytail style, stdlib only, fewest lines.
38
+ - `robustness`: Paranoid validation, edge cases, defensive typing.
39
+ - `performance`: Memory efficiency, asymptotic complexity.
40
+ - `tester`: TDD, unit test coverage, mock isolation.
41
+ - `general`: Standard expert proposer.
42
+
43
+ ### 3. Syntax Pre-check Gate with Judge Auto-fix Directive
44
+ - **Checker**:
45
+ - JS/MJS/CJS: `new vm.Script(code)` from built-in `node:vm` (instant, memory-safe).
46
+ - JSON: `JSON.parse(code)`.
47
+ - Python: `python3 -m py_compile`.
48
+ - **Judge Directive**:
49
+ - If a candidate has a syntax warning, inform the judge:
50
+ `CRITICAL JUDGE DIRECTIVE: If Candidate X has the best overall architecture or solution despite this syntax error, DO NOT reject them! You MUST correct the syntax error directly during final code synthesis and declare Candidate X the winner.`
51
+
52
+ ### 4. Consilium / Peer Critique Round 2
53
+ - **Schema**: `peer_critique_enabled: z.boolean().default(false)`.
54
+ - **Pipeline**:
55
+ - If enabled and `candidates.length >= 2`, run Round 2:
56
+ - Pass anonymized candidate proposals to each proposer.
57
+ - Collect refined solutions with peer critique.
58
+ - Supply improved proposals to Judge/Curator.
59
+
60
+ ### 5. Analytics Filtering & CSV/JSON Export
61
+ - **Leaderboard Filter**: `getMoaLeaderboard(historyFilePath, limit, filterPreset)`.
62
+ - **Export Endpoints**:
63
+ - `GET /dsh-moa/leaderboard?preset=<name>&format=json|csv`.
64
+ - **Client UI**:
65
+ - Dropdown filter by preset in Section 4.
66
+ - `Export CSV` / `Export JSON` action buttons.
67
+
68
+ ---
69
+
70
+ ## Verification
71
+ - Unit tests for all 5 features in `test/moa-power-pack.test.mjs`.
72
+ - Total test suite passing with 0 regressions.
73
+ - Live deployment to `/home/vadim/.dsh/profiles/web` on MiniPC and endpoint verification.
package/lib/client.js CHANGED
@@ -55,6 +55,14 @@ window.__ModuleLoader__.load({
55
55
  'aggregator.curator_hint': 'When enabled: the curator highlights the finest components of each candidate, applies the antipatterns rubric, and advises which agent model should assemble the solution.',
56
56
  'aggregator.stream_label': 'Live Stream Curator/Judge Thinking',
57
57
  'aggregator.stream_hint': 'Streams aggregator tokens directly to chat in real time for zero-latency initial response.',
58
+ 'aggregator.peer_label': 'Consilium: Peer Critique (Round 2)',
59
+ 'aggregator.peer_hint': 'Candidates critique each other\'s solutions and refine their code before final judge synthesis.',
60
+ 'aggregator.override_label': 'Allow Candidate Override Actions',
61
+ 'aggregator.override_hint': 'Keeps candidate workspaces in .moa to let you apply any candidate files via chat command.',
62
+ 'proposers.role_label': 'Persona:',
63
+ 'leaderboard.filter_all': 'All Presets',
64
+ 'leaderboard.export_csv': 'Export CSV',
65
+ 'leaderboard.export_json': 'Export JSON',
58
66
  'aggregator.blind_label': 'Blind Review (Anonymize Candidates for Judge)',
59
67
  'aggregator.blind_hint': 'When enabled: candidate model names and providers are hidden from the judge prompt to prevent family/brand bias.',
60
68
  'aggregator.timeout_label': 'Judge synthesis timeout (sec):',
@@ -461,6 +469,8 @@ window.__ModuleLoader__.load({
461
469
  hostStatus,
462
470
  stats,
463
471
  leaderboard = [],
472
+ leaderboardPresetFilter = 'all',
473
+ setLeaderboardPresetFilter = () => {},
464
474
  handleSave,
465
475
  saveStatus,
466
476
  reload,
@@ -877,6 +887,38 @@ window.__ModuleLoader__.load({
877
887
  },
878
888
  })
879
889
  ),
890
+ React.createElement(
891
+ 'label',
892
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
893
+ React.createElement('input', {
894
+ type: 'checkbox',
895
+ checked: Boolean(currentPreset.peer_critique_enabled),
896
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, peer_critique_enabled: e.target.checked })),
897
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
898
+ }),
899
+ t('aggregator.peer_label')
900
+ ),
901
+ React.createElement(
902
+ 'div',
903
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
904
+ t('aggregator.peer_hint')
905
+ ),
906
+ React.createElement(
907
+ 'label',
908
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
909
+ React.createElement('input', {
910
+ type: 'checkbox',
911
+ checked: Boolean(currentPreset.allow_candidate_override),
912
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, allow_candidate_override: e.target.checked })),
913
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
914
+ }),
915
+ t('aggregator.override_label')
916
+ ),
917
+ React.createElement(
918
+ 'div',
919
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
920
+ t('aggregator.override_hint')
921
+ ),
880
922
  /* Fallback Judges Chain */
881
923
  React.createElement(
882
924
  'div',
@@ -1052,6 +1094,28 @@ window.__ModuleLoader__.load({
1052
1094
  t,
1053
1095
  })
1054
1096
  ),
1097
+ React.createElement(
1098
+ 'select',
1099
+ {
1100
+ className: 'moa-select',
1101
+ style: { width: 105, height: 32, fontSize: 12, padding: '0 6px' },
1102
+ value: ref.role_persona || 'general',
1103
+ title: t('proposers.role_label'),
1104
+ onChange: (e) => {
1105
+ const newRole = e.target.value
1106
+ updateCurrentPreset((p) => {
1107
+ const refs = [...(p.reference_models || [])]
1108
+ refs[idx] = { ...refs[idx], role_persona: newRole }
1109
+ return { ...p, reference_models: refs }
1110
+ })
1111
+ },
1112
+ },
1113
+ React.createElement('option', { value: 'general' }, 'General'),
1114
+ React.createElement('option', { value: 'minimalist' }, 'Minimalist'),
1115
+ React.createElement('option', { value: 'robustness' }, 'Robustness'),
1116
+ React.createElement('option', { value: 'performance' }, 'Performance'),
1117
+ React.createElement('option', { value: 'tester' }, 'Tester')
1118
+ ),
1055
1119
  currentRefs.length > 1 &&
1056
1120
  React.createElement(
1057
1121
  'button',
@@ -1139,7 +1203,46 @@ window.__ModuleLoader__.load({
1139
1203
  Array.isArray(leaderboard) && leaderboard.length > 0 && React.createElement(
1140
1204
  'div',
1141
1205
  { style: { marginTop: 12, display: 'flex', flexDirection: 'column', gap: 6 } },
1142
- React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('leaderboard.title')),
1206
+ React.createElement(
1207
+ 'div',
1208
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' } },
1209
+ React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t('leaderboard.title')),
1210
+ React.createElement(
1211
+ 'div',
1212
+ { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1213
+ React.createElement(
1214
+ 'select',
1215
+ {
1216
+ className: 'moa-select',
1217
+ style: { height: 26, fontSize: 11, padding: '0 6px' },
1218
+ value: leaderboardPresetFilter,
1219
+ onChange: (e) => setLeaderboardPresetFilter(e.target.value),
1220
+ },
1221
+ React.createElement('option', { value: 'all' }, t('leaderboard.filter_all')),
1222
+ presets.map((p) => React.createElement('option', { key: p.name, value: p.name }, p.name))
1223
+ ),
1224
+ React.createElement(
1225
+ 'a',
1226
+ {
1227
+ href: `/dsh-moa/history/export?format=csv${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
1228
+ download: 'moa-history.csv',
1229
+ className: 'moa-btn',
1230
+ style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
1231
+ },
1232
+ 'CSV'
1233
+ ),
1234
+ React.createElement(
1235
+ 'a',
1236
+ {
1237
+ href: `/dsh-moa/history/export?format=json${leaderboardPresetFilter !== 'all' ? '&preset=' + encodeURIComponent(leaderboardPresetFilter) : ''}`,
1238
+ download: 'moa-history.json',
1239
+ className: 'moa-btn',
1240
+ style: { height: 26, fontSize: 11, padding: '0 8px', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
1241
+ },
1242
+ 'JSON'
1243
+ )
1244
+ )
1245
+ ),
1143
1246
  React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } }, t('leaderboard.desc')),
1144
1247
  React.createElement(
1145
1248
  'div',
@@ -1257,6 +1360,7 @@ window.__ModuleLoader__.load({
1257
1360
  const [hostStatus, setHostStatus] = React.useState('connecting')
1258
1361
  const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
1259
1362
  const [leaderboard, setLeaderboard] = React.useState([])
1363
+ const [leaderboardPresetFilter, setLeaderboardPresetFilter] = React.useState('all')
1260
1364
 
1261
1365
  React.useEffect(() => {
1262
1366
  if (!scope || !snapshot) return
@@ -1288,7 +1392,10 @@ window.__ModuleLoader__.load({
1288
1392
  setHostStatus('offline')
1289
1393
  })
1290
1394
 
1291
- fetch('/dsh-moa/leaderboard', { cache: 'no-store' })
1395
+ const lbUrl = leaderboardPresetFilter !== 'all'
1396
+ ? `/dsh-moa/leaderboard?preset=${encodeURIComponent(leaderboardPresetFilter)}`
1397
+ : '/dsh-moa/leaderboard'
1398
+ fetch(lbUrl, { cache: 'no-store' })
1292
1399
  .then((r) => r.json())
1293
1400
  .then((data) => {
1294
1401
  if (data && data.ok && Array.isArray(data.models)) {
@@ -1406,6 +1513,8 @@ window.__ModuleLoader__.load({
1406
1513
  hostStatus,
1407
1514
  stats,
1408
1515
  leaderboard,
1516
+ leaderboardPresetFilter,
1517
+ setLeaderboardPresetFilter,
1409
1518
  handleSave,
1410
1519
  saveStatus,
1411
1520
  reload,
@@ -1,3 +1,4 @@
1
+ import vm from 'node:vm'
1
2
  import fs from 'node:fs/promises'
2
3
  import path from 'node:path'
3
4
 
@@ -275,3 +276,48 @@ async function getFilesRecursively(dir) {
275
276
  await scan(dir)
276
277
  return results
277
278
  }
279
+
280
+
281
+ /**
282
+ * Verifies the syntactic validity of generated code files.
283
+ * Uses Node.js in-memory vm.Script for JS (with ES module export/import transformation) and JSON.parse for JSON.
284
+ */
285
+ export function verifyFileSyntax(files = []) {
286
+ if (!Array.isArray(files)) return []
287
+ const warnings = []
288
+
289
+ for (const file of files) {
290
+ if (!file || typeof file.content !== 'string' || !file.relativePath) continue
291
+ const ext = path.extname(file.relativePath).toLowerCase()
292
+
293
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
294
+ try {
295
+ const transformed = file.content
296
+ .replace(/^([ \t]*)export\s+default\s+/gm, '$1')
297
+ .replace(/^([ \t]*)export\s*\{[^}]*\}\s*;?/gm, '$1')
298
+ .replace(/^([ \t]*)export\s+(const|let|var|function|class|async\s+function)\s+/gm, '$1$2 ')
299
+ .replace(/^([ \t]*)import\s+[^;\n]+;?/gm, '$1')
300
+ .replace(/^([ \t]*)export\s+\*\s+from\s+[^;\n]+;?/gm, '$1')
301
+ new vm.Script(transformed)
302
+ } catch (err) {
303
+ warnings.push({
304
+ file: file.relativePath,
305
+ error: err?.message || String(err),
306
+ line: err?.lineNumber || 1,
307
+ })
308
+ }
309
+ } else if (ext === '.json') {
310
+ try {
311
+ JSON.parse(file.content)
312
+ } catch (err) {
313
+ warnings.push({
314
+ file: file.relativePath,
315
+ error: err?.message || String(err),
316
+ line: 1,
317
+ })
318
+ }
319
+ }
320
+ }
321
+
322
+ return warnings
323
+ }
package/lib/history.js CHANGED
@@ -159,7 +159,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = DEFAULT_HISTORY
159
159
  /**
160
160
  * Calculates win-rate leaderboard and model performance statistics across history.
161
161
  */
162
- export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
162
+ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE, options = {}) {
163
163
  try {
164
164
  if (!fs.existsSync(filePath)) {
165
165
  return { totalRuns: 0, models: [] }
@@ -170,11 +170,15 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
170
170
  const totalRuns = lines.length
171
171
 
172
172
  const modelStats = new Map()
173
+ const presetFilter = typeof options === 'string' ? options : (options?.presetFilter || options?.preset || null)
173
174
 
175
+ let filteredRuns = 0
174
176
  for (const line of lines) {
175
177
  try {
176
178
  const item = JSON.parse(line)
177
179
  if (!item) continue
180
+ if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
181
+ filteredRuns++
178
182
 
179
183
  const winner = item.winnerModel ? String(item.winnerModel).trim() : null
180
184
 
@@ -218,7 +222,7 @@ export function getMoaLeaderboard(filePath = DEFAULT_HISTORY_FILE) {
218
222
  }))
219
223
  .sort((a, b) => b.wins - a.wins || b.winRate - a.winRate)
220
224
 
221
- return { totalRuns, models }
225
+ return { totalRuns: presetFilter && presetFilter !== 'all' ? filteredRuns : totalRuns, models, preset: presetFilter }
222
226
  } catch (err) {
223
227
  console.warn('[dsh-moa] Failed to calculate leaderboard:', err)
224
228
  return { totalRuns: 0, models: [] }
@@ -244,3 +248,53 @@ export function getMoaRunById(runId, filePath = DEFAULT_HISTORY_FILE) {
244
248
  return null
245
249
  }
246
250
  }
251
+
252
+ /**
253
+ * Exports history data in JSON or CSV format with optional preset filtering.
254
+ */
255
+ export function exportMoaHistory(filePath = DEFAULT_HISTORY_FILE, options = {}) {
256
+ try {
257
+ if (!fs.existsSync(filePath)) {
258
+ return options?.format === 'csv' ? 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs\n' : '[]'
259
+ }
260
+
261
+ const raw = fs.readFileSync(filePath, 'utf8')
262
+ const lines = raw.trim().split('\n').filter(Boolean)
263
+ const presetFilter = typeof options === 'string' ? options : (options?.presetFilter || options?.preset || null)
264
+ const format = options?.format === 'csv' ? 'csv' : 'json'
265
+
266
+ const records = []
267
+ for (const line of lines) {
268
+ try {
269
+ const item = JSON.parse(line)
270
+ if (!item) continue
271
+ if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
272
+ records.push(item)
273
+ } catch {}
274
+ }
275
+
276
+ if (format === 'csv') {
277
+ const header = 'id,timestamp,preset,winnerModel,totalTokens,totalCostUsd,durationMs,isRefinement,isFastMode'
278
+ const rows = records.map((r) => {
279
+ const escapeCsv = (val) => `"${String(val ?? '').replace(/"/g, '""')}"`
280
+ return [
281
+ escapeCsv(r.id),
282
+ escapeCsv(r.timestamp),
283
+ escapeCsv(r.preset),
284
+ escapeCsv(r.winnerModel),
285
+ Number(r.totalTokens || 0),
286
+ Number(r.totalCostUsd || 0).toFixed(4),
287
+ Number(r.durationMs || 0),
288
+ Boolean(r.isRefinement),
289
+ Boolean(r.isFastMode),
290
+ ].join(',')
291
+ })
292
+ return [header, ...rows].join('\n')
293
+ }
294
+
295
+ return JSON.stringify(records, null, 2)
296
+ } catch (err) {
297
+ console.warn('[dsh-moa] Failed to export history:', err)
298
+ return options?.format === 'csv' ? '' : '[]'
299
+ }
300
+ }
package/lib/index.js CHANGED
@@ -23,7 +23,9 @@ import {
23
23
  getMoaHistory,
24
24
  getMoaLeaderboard,
25
25
  getMoaRunById,
26
+ exportMoaHistory,
26
27
  } from './history.js'
28
+ import { promoteCandidateWorkspace } from './file-workspace.js'
27
29
 
28
30
  import { createLiveCanvasClient } from './live-canvas.js'
29
31
 
@@ -41,6 +43,7 @@ export const PriceRow = z.object({
41
43
  export const ModelSlotSchema = z.object({
42
44
  provider: z.string().default('opencode-go'),
43
45
  model: z.string().default('deepseek-v4-flash'),
46
+ role_persona: z.string().default(''),
44
47
  })
45
48
 
46
49
  export const PresetSchema = z.object({
@@ -59,6 +62,8 @@ export const PresetSchema = z.object({
59
62
  reference_timeout_sec: z.number().default(60),
60
63
  aggregator_timeout_sec: z.number().default(180),
61
64
  blind_evaluation: z.boolean().default(false),
65
+ peer_critique_enabled: z.boolean().default(false),
66
+ allow_candidate_override: z.boolean().default(false),
62
67
  max_tokens: z.number().default(4096),
63
68
  judge_criteria: z.string().default(''),
64
69
  })
@@ -421,7 +426,7 @@ export function apply(ctx, config) {
421
426
  })
422
427
  }, 'dsh-moa: history route')
423
428
 
424
- // Route: /dsh-moa/leaderboard (#11)
429
+ // Route: /dsh-moa/leaderboard
425
430
  ctx.effect(() => {
426
431
  return ctx.webServer.register({
427
432
  kind: 'exact',
@@ -432,7 +437,21 @@ export function apply(ctx, config) {
432
437
  return
433
438
  }
434
439
  try {
435
- const data = getMoaLeaderboard()
440
+ const url = new URL(req.url || '', 'http://127.0.0.1')
441
+ const preset = url.searchParams.get('preset') || null
442
+ const format = url.searchParams.get('format') || 'json'
443
+
444
+ if (format === 'csv') {
445
+ const csv = exportMoaHistory(undefined, { format: 'csv', preset })
446
+ res.writeHead(200, {
447
+ 'Content-Type': 'text/csv; charset=utf-8',
448
+ 'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
449
+ })
450
+ res.end(csv)
451
+ return
452
+ }
453
+
454
+ const data = getMoaLeaderboard(undefined, { preset })
436
455
  writeJson(res, 200, { ok: true, ...data })
437
456
  } catch (err) {
438
457
  writeJson(res, 500, { ok: false, error: err?.message || String(err) })
@@ -441,6 +460,71 @@ export function apply(ctx, config) {
441
460
  })
442
461
  }, 'dsh-moa: leaderboard route')
443
462
 
463
+ // Route: /dsh-moa/promote (Candidate Override Action)
464
+ ctx.effect(() => {
465
+ return ctx.webServer.register({
466
+ kind: 'exact',
467
+ path: '/dsh-moa/promote',
468
+ handler: async (req, res) => {
469
+ if (req.method !== 'POST') {
470
+ writeJson(res, 405, { ok: false, error: 'POST required' })
471
+ return
472
+ }
473
+ try {
474
+ const raw = await readBody(req)
475
+ const body = JSON.parse(raw || '{}')
476
+ const candidateIndex = parseInt(body?.candidateIndex, 10)
477
+ if (isNaN(candidateIndex) || candidateIndex < 1) {
478
+ writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
479
+ return
480
+ }
481
+ const cwd = body?.cwd || process.cwd()
482
+ const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, { keepMoa: true })
483
+ writeJson(res, 200, { ok: true, candidateIndex, promotedFiles })
484
+ } catch (err) {
485
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
486
+ }
487
+ },
488
+ })
489
+ }, 'dsh-moa: promote candidate route')
490
+
491
+ // Route: /dsh-moa/history/export
492
+ ctx.effect(() => {
493
+ return ctx.webServer.register({
494
+ kind: 'exact',
495
+ path: '/dsh-moa/history/export',
496
+ handler: async (req, res) => {
497
+ if (req.method !== 'GET') {
498
+ writeJson(res, 405, { ok: false, error: 'GET required' })
499
+ return
500
+ }
501
+ try {
502
+ const url = new URL(req.url || '', 'http://127.0.0.1')
503
+ const preset = url.searchParams.get('preset') || null
504
+ const format = url.searchParams.get('format') || 'json'
505
+ const data = exportMoaHistory(undefined, { format, preset })
506
+
507
+ if (format === 'csv') {
508
+ res.writeHead(200, {
509
+ 'Content-Type': 'text/csv; charset=utf-8',
510
+ 'Content-Disposition': 'attachment; filename="moa-history.csv"',
511
+ })
512
+ res.end(data)
513
+ return
514
+ }
515
+
516
+ res.writeHead(200, {
517
+ 'Content-Type': 'application/json; charset=utf-8',
518
+ 'Content-Disposition': 'attachment; filename="moa-history.json"',
519
+ })
520
+ res.end(data)
521
+ } catch (err) {
522
+ writeJson(res, 500, { ok: false, error: err?.message || String(err) })
523
+ }
524
+ },
525
+ })
526
+ }, 'dsh-moa: history export route')
527
+
444
528
  // Route: /dsh-moa/runs/<id> (run replay by id)
445
529
  ctx.effect(() => {
446
530
  return ctx.webServer.register({
package/lib/moa-parser.js CHANGED
@@ -45,6 +45,17 @@ export function parseMoACommand(text, presets = []) {
45
45
  return null
46
46
  }
47
47
 
48
+ // Check for promote subcommand: /moa promote <runId> <candidateIndex> or /moa promote <candidateIndex>
49
+ const promoteMatch = /^\/moa\s+promote\s+([a-zA-Z0-9_\-]+)(?:\s+(\d+))?/i.exec(text.trim())
50
+ if (promoteMatch) {
51
+ const hasTwoArgs = Boolean(promoteMatch[2])
52
+ return {
53
+ isPromote: true,
54
+ runId: hasTwoArgs ? promoteMatch[1] : null,
55
+ candidateIndex: hasTwoArgs ? parseInt(promoteMatch[2], 10) : parseInt(promoteMatch[1], 10),
56
+ }
57
+ }
58
+
48
59
  const remainder = text.slice(4).trim()
49
60
  if (!remainder) {
50
61
  return {
@@ -177,5 +188,18 @@ export function formatMoAResponse({ moaResult, presetName }) {
177
188
  })
178
189
  }
179
190
 
191
+ if (moaResult?.allowCandidateOverride && refs.length > 1) {
192
+ const runId = moaResult?.runId || ''
193
+ parts.push('### 🔄 Candidate Override Actions')
194
+ parts.push('To apply an alternative candidate\'s files instead of the judge\'s pick:')
195
+ refs.forEach((ref, idx) => {
196
+ const isWinner = (idx + 1) === moaResult.winningIndex
197
+ const tag = isWinner ? ' *(Current Judge Pick)*' : ''
198
+ const promoteCmd = runId ? `/moa promote ${runId} ${idx + 1}` : `/moa promote ${idx + 1}`
199
+ parts.push(`- \`${promoteCmd}\` — Candidate ${idx + 1} (${ref.label})${tag}`)
200
+ })
201
+ parts.push('')
202
+ }
203
+
180
204
  return parts.join('\n')
181
205
  }