@goodandready/dsh-moa 0.2.12 → 0.2.14

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/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 {
@@ -137,7 +148,7 @@ export function formatMoAResponse({ moaResult, presetName }) {
137
148
  }
138
149
 
139
150
  if (moaResult?.liveCanvas?.previewUrl) {
140
- parts.push(`> 🎨 **Live Canvas**: [🚀 Открыть ${moaResult.liveCanvas.title || 'превью'} в Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Открыть в новой вкладке](${moaResult.liveCanvas.previewUrl})`)
151
+ parts.push(`> 🎨 **Live Canvas**: [🚀 Open ${moaResult.liveCanvas.title || 'preview'} in Live Canvas](${moaResult.liveCanvas.previewUrl}) | [↗ Open in new tab](${moaResult.liveCanvas.previewUrl})`)
141
152
  }
142
153
 
143
154
  // Cost tracking card
@@ -155,7 +166,7 @@ export function formatMoAResponse({ moaResult, presetName }) {
155
166
  parts.push('')
156
167
  const cleanJudgeContent = hasPromoted
157
168
  ? stripOrSummarizeCode(moaResult?.content || '')
158
- : (moaResult?.content || '(нет ответа)')
169
+ : (moaResult?.content || '(no response)')
159
170
  parts.push(cleanJudgeContent)
160
171
  parts.push('')
161
172
  }
@@ -166,7 +177,7 @@ export function formatMoAResponse({ moaResult, presetName }) {
166
177
  parts.push('')
167
178
  refs.forEach((ref, i) => {
168
179
  const statusIcon = ref.ok ? '✅' : '⚠️'
169
- const fileBadge = ref.files?.length ? ` (${ref.files.length} файл(ов))` : ''
180
+ const fileBadge = ref.files?.length ? ` (${ref.files.length} file${ref.files.length > 1 ? 's' : ''})` : ''
170
181
  const costBadge = ref.costUsd > 0 ? ` [~\$${ref.costUsd.toFixed(4)}]` : ''
171
182
  parts.push(`#### ${statusIcon} Model ${i + 1}: ${ref.label}${fileBadge}${costBadge}`)
172
183
  parts.push('')
@@ -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
  }
@@ -33,6 +33,35 @@ Penalize and strictly downgrade candidates exhibiting any of the following flaws
33
33
  8. 🚫 Context Amnesia & Regressions:
34
34
  - Dropping or breaking previously functioning project features while adding new code.`
35
35
 
36
+ export const ROLE_PERSONA_PROMPTS = {
37
+ minimalist: `### 🎯 Specialized Engineering Persona: THE MINIMALIST (Ponytail)
38
+ - Prioritize the standard library and native platform capabilities over third-party dependencies.
39
+ - Zero boilerplate, no premature abstractions, interfaces, or unneeded layers.
40
+ - Write the shortest, cleanest working solution with maximum readability.`,
41
+
42
+ robustness: `### 🎯 Specialized Engineering Persona: ROBUSTNESS & DEFENSIVE DESIGN
43
+ - Emphasize paranoid input validation, boundary checking, and comprehensive error handling.
44
+ - Anticipate network failures, null/undefined edge cases, malformed data, and race conditions.
45
+ - Ensure grace under failure and informative, actionable error reporting.`,
46
+
47
+ performance: `### 🎯 Specialized Engineering Persona: HIGH PERFORMANCE & EFFICIENCY
48
+ - Prioritize asymptotic time and space complexity, minimal memory allocations, and zero redundant work.
49
+ - Optimize hot paths, streaming data processing, and efficient algorithmic structures.
50
+ - Document the Big-O complexity and performance characteristics of your solution.`,
51
+
52
+ tester: `### 🎯 Specialized Engineering Persona: TESTABILITY & VERIFICATION
53
+ - Structure code for maximum testability, modular decoupling, and clear side-effect boundaries.
54
+ - Include comprehensive unit/integration test specifications verifying happy paths and edge cases.
55
+ - Emphasize deterministic behavior and assertions.`,
56
+
57
+ general: '',
58
+ }
59
+
60
+ export const SYNTAX_CORRECTION_DIRECTIVE = `### ⚠️ Syntax Warning & Auto-Fix Directive:
61
+ Some candidate proposals have detected syntax flaws (annotated with [⚠️ Syntax Warning]).
62
+ CRITICAL JUDGE DIRECTIVE: If a candidate with a syntax flaw presents superior architecture, algorithm, or engineering logic compared to other candidates, DO NOT reject them solely for this syntax flaw!
63
+ Instead, you MUST correct the syntax error directly during synthesis/assembly and declare that candidate the winner.`
64
+
36
65
  export const LANGUAGE_MIRRORING_DIRECTIVE = `### 🌐 Language Mirroring Requirement
37
66
  CRITICAL: You MUST write your entire analysis, reasoning, verdict, explanations and instructions in the EXACT SAME LANGUAGE as the user's prompt (e.g. Russian if the prompt is written in Russian, English if in English). Do NOT switch or translate to English unless explicitly requested by the user.`
38
67
 
@@ -99,21 +128,22 @@ export function isBroadPromptRequiringQuestions(userPrompt = '', messages = [])
99
128
  const p = userPrompt.trim()
100
129
  const wordCount = p.split(/\s+/).length
101
130
 
102
- if (/^(да|нет|1|2|3|4|ок|погнали|давай|yes|no)\b/i.test(p) && wordCount <= 5) {
131
+ if (/^(yes|no|1|2|3|4|ok|sure|是|否|好|да|нет|ок|погнали|давай)\b/i.test(p) && wordCount <= 5) {
103
132
  return false
104
133
  }
105
134
 
106
135
  const hasRecentQuestion = messages.some((m) => {
107
136
  const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '')
108
- return text.includes('Уточнение требований') || text.includes('опросник') || text.includes('Вариант 1')
137
+ return text.includes('Clarification of Requirements') || text.includes('需求澄清') || text.includes('Уточнение требований') || text.includes('опросник') || text.includes('Option 1') || text.includes('Вариант 1')
109
138
  })
110
139
  if (hasRecentQuestion) {
111
140
  return false
112
141
  }
113
142
 
114
143
  const creationTriggers = [
144
+ 'make', 'build', 'create', 'generate', 'develop', 'design',
145
+ '制作', '创建', '构建', '开发', '设计',
115
146
  'сделай', 'создай', 'напиши', 'разработай', 'придумай', 'реализуй',
116
- 'make', 'build', 'create', 'generate', 'develop',
117
147
  ]
118
148
  const startsWithCreation = creationTriggers.some((t) => p.toLowerCase().startsWith(t))
119
149
 
@@ -121,7 +151,7 @@ export function isBroadPromptRequiringQuestions(userPrompt = '', messages = [])
121
151
  return true
122
152
  }
123
153
 
124
- const vagueNouns = ['приложение', 'игру', 'сервис', 'сайт', 'лендинг', 'калькулятор', 'виджет', 'дашборд', 'app', 'game', 'tool', 'website']
154
+ const vagueNouns = ['app', 'game', 'tool', 'website', 'dashboard', 'widget', 'service', 'landing', 'calculator', '应用', '游戏', '工具', '网站', '仪表盘', '服务', 'приложение', 'игру', 'сервис', 'сайт', 'лендинг', 'калькулятор', 'виджет', 'дашборд']
125
155
  if (vagueNouns.some((n) => p.toLowerCase().includes(n))) {
126
156
  if (wordCount <= 12) return true
127
157
  }
@@ -154,6 +184,37 @@ At the end, add a note that the user can answer briefly (e.g.: "1, 2, dark theme
154
184
  /**
155
185
  * Builds the curator synthesis prompt with component analysis, model recommendation, and optional blind review.
156
186
  */
187
+ /**
188
+ * Builds the Round 2 Consilium / Peer Critique prompt for candidate models.
189
+ */
190
+ export function buildPeerCritiquePrompt(userPrompt, myProposal, otherProposals = [], isBlind = true) {
191
+ const othersText = otherProposals
192
+ .map((p, idx) => {
193
+ const label = isBlind ? `Candidate ${idx + 1}` : (p.label || `Candidate ${idx + 1}`)
194
+ return `### ${label} Alternative Proposal:
195
+ ${p.text}`
196
+ })
197
+ .join('\n\n---\n\n')
198
+
199
+ return `You are participating in Round 2 (Consilium / Peer Critique & Solution Refinement) of an MoA ensemble.
200
+
201
+ Original User Request:
202
+ ${userPrompt}
203
+
204
+ Your Initial Solution (Round 1):
205
+ ${myProposal}
206
+
207
+ Alternative Solutions from Peer Candidates:
208
+ ${othersText}
209
+
210
+ Instructions:
211
+ 1. 🔍 Peer Critique: Briefly evaluate the other candidates' solutions. Point out any hidden bugs, race conditions, antipatterns, or edge-case omissions in their logic.
212
+ 2. 🛡️ Defend & Refine: Adopt the strongest architectural ideas or optimizations from competitors to enhance your own solution. Fix any oversights in your own code.
213
+ 3. 🚀 Final Refined Deliverable: Present your complete, production-ready, improved code with full explanations.
214
+
215
+ ${LANGUAGE_MIRRORING_DIRECTIVE}`
216
+ }
217
+
157
218
  export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCriteria = '', options = {}) {
158
219
  const isBlind = Boolean(options.blindEvaluation)
159
220
 
@@ -166,9 +227,10 @@ export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], j
166
227
  if (referenceOutputs.length >= 3 && textContent.length > 3000) {
167
228
  textContent = stripOrSummarizeCode(textContent)
168
229
  }
230
+ const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
169
231
  const header = isBlind
170
- ? `Candidate ${i + 1}:${fileSummary}`
171
- : `Candidate ${i + 1} — ${r.label}:${fileSummary}`
232
+ ? `Candidate ${i + 1}:${syntaxNote}${fileSummary}`
233
+ : `Candidate ${i + 1} — ${r.label}:${syntaxNote}${fileSummary}`
172
234
  return `${header}\n${textContent}`
173
235
  })
174
236
  .join('\n\n')
@@ -189,7 +251,9 @@ ${joined}
189
251
  ${ANTIPATTERNS_RUBRIC}
190
252
 
191
253
  ${LANGUAGE_MIRRORING_DIRECTIVE}
192
-
254
+ ${referenceOutputs.some((r) => r.syntaxWarning) ? `
255
+ ${SYNTAX_CORRECTION_DIRECTIVE}
256
+ ` : ''}
193
257
  Instructions:
194
258
  Your response MUST be structured into three clear parts:
195
259
 
@@ -230,9 +294,10 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
230
294
  if (referenceOutputs.length >= 3 && textContent.length > 3000) {
231
295
  textContent = stripOrSummarizeCode(textContent)
232
296
  }
297
+ const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
233
298
  const header = isBlind
234
- ? `Reference ${i + 1}:${fileSummary}`
235
- : `Reference ${i + 1} — ${r.label}:${fileSummary}`
299
+ ? `Reference ${i + 1}:${syntaxNote}${fileSummary}`
300
+ : `Reference ${i + 1} — ${r.label}:${syntaxNote}${fileSummary}`
236
301
  return `${header}\n${textContent}`
237
302
  })
238
303
  .join('\n\n')
@@ -252,7 +317,7 @@ ${joined}
252
317
  ${ANTIPATTERNS_RUBRIC}
253
318
 
254
319
  ${LANGUAGE_MIRRORING_DIRECTIVE}
255
-
320
+ ${referenceOutputs.some((r) => r.syntaxWarning) ? `\n${SYNTAX_CORRECTION_DIRECTIVE}\n` : ''}
256
321
  Instructions:
257
322
  Your response MUST be structured into three clear parts:
258
323
 
package/lib/moa-runner.js CHANGED
@@ -11,53 +11,16 @@
11
11
  */
12
12
 
13
13
  import path from 'node:path'
14
- import {
15
- extractFileBlocks,
16
- collectProjectContext,
17
- isRefinementTask,
18
- writeCandidateWorkspace,
19
- promoteCandidateWorkspace,
20
- cleanMoaWorkspaces,
21
- } from './file-workspace.js'
14
+ import crypto from 'node:crypto'
15
+ import { extractFileBlocks, collectProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
22
16
  import { estimateTokenCost } from './pricing.js'
23
17
  import { recordMoaRun, recordMoaRunAsync } from './history.js'
24
- import {
25
- slotLabel,
26
- cleanAdvisoryMessages,
27
- isBroadPromptRequiringQuestions,
28
- buildQuestionSynthesisPrompt,
29
- buildCuratorSynthesisPrompt,
30
- buildSynthesisPrompt,
31
- SYSTEM_ROLE_PROPOSER,
32
- ANTIPATTERNS_RUBRIC,
33
- } from './moa-prompts.js'
34
- import {
35
- parseWinnerIndex,
36
- parseRecommendedAssembler,
37
- parseMoACommand,
38
- stripOrSummarizeCode,
39
- formatMoAResponse,
40
- } from './moa-parser.js'
41
-
42
- // Re-export prompt and parser functions for external consumers / backwards compatibility
43
- export {
44
- slotLabel,
45
- cleanAdvisoryMessages,
46
- isBroadPromptRequiringQuestions,
47
- buildQuestionSynthesisPrompt,
48
- buildCuratorSynthesisPrompt,
49
- buildSynthesisPrompt,
50
- ANTIPATTERNS_RUBRIC,
51
- } from './moa-prompts.js'
52
-
53
- export {
54
- parseWinnerIndex,
55
- parseRecommendedAssembler,
56
- parseMoACommand,
57
- stripOrSummarizeCode,
58
- formatMoAResponse,
59
- } from './moa-parser.js'
18
+ import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
19
+ import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
60
20
 
21
+ // Re-exports for consumers & backward compatibility
22
+ export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
23
+ export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
61
24
  export { estimateTokenCost } from './pricing.js'
62
25
 
63
26
  export const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
@@ -94,8 +57,6 @@ export async function runReferencesParallel(references, messages, options = {},
94
57
 
95
58
  const advisoryMessages = cleanAdvisoryMessages(messages)
96
59
  const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
97
- // Deterministic prefix for optimal prompt caching hit rate
98
- const fullMessages = [{ role: 'system', content: systemPrompt }, ...advisoryMessages]
99
60
  const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
100
61
  const maxRetries = options.maxRetries ?? 0
101
62
  const quorumEnabled = Boolean(options.quorumEnabled)
@@ -117,6 +78,10 @@ export async function runReferencesParallel(references, messages, options = {},
117
78
 
118
79
  references.forEach((slot, i) => {
119
80
  const label = slotLabel(slot)
81
+ const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
82
+ ? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
83
+ : ''
84
+ const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
120
85
 
121
86
  const runOne = async () => {
122
87
  try {
@@ -125,7 +90,7 @@ export async function runReferencesParallel(references, messages, options = {},
125
90
  {
126
91
  provider: slot.provider,
127
92
  model: slot.model,
128
- messages: fullMessages,
93
+ messages: slotMessages,
129
94
  temperature: options.temperature ?? 0.6,
130
95
  maxTokens: options.maxTokens ?? 4096,
131
96
  timeoutMs,
@@ -144,7 +109,7 @@ export async function runReferencesParallel(references, messages, options = {},
144
109
  const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
145
110
 
146
111
  const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
147
- inputTokens: Math.max(1, Math.round(fullMessages.map((m) => m.content).join('').length / 4)),
112
+ inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
148
113
  outputTokens: Math.max(1, Math.round(text.length / 4)),
149
114
  }
150
115
  const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
@@ -328,6 +293,9 @@ export async function runMoAPipeline({
328
293
  const refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
329
294
  const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
330
295
  const isBlindEvaluation = Boolean(preset?.blind_evaluation)
296
+ const isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
297
+ const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
298
+ const runId = crypto.randomUUID()
331
299
 
332
300
  // 2. Collect project context for refinement tasks
333
301
  const isRefinement = isRefinementTask(userPrompt)
@@ -428,11 +396,11 @@ export async function runMoAPipeline({
428
396
  qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
429
397
  } catch (err) {
430
398
  console.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
431
- questionsContent = `### Уточнение требований по задаче: "${userPrompt}"\n\n` +
432
- '1. **Формат решения**: однофайловый HTML/JS или многомодульный проект?\n' +
433
- '2. **Стиль и визуальное оформление**: минимализм, темная тема или нейтральный интерфейс?\n' +
434
- '3. **Функциональные приоритеты**: базовый MVP или расширенная реализация?\n\n' +
435
- '*Ответьте кратко (например, "1, 2") или доверьтесь выбору по умолчанию.*'
399
+ questionsContent = `### Clarification of Requirements: "${userPrompt}"\n\n` +
400
+ '1. **Architecture & Scope**: Single-file deliverable or multi-module project structure?\n' +
401
+ '2. **Design & Style**: Minimalist, dark mode, or clean neutral theme?\n' +
402
+ '3. **Functional Priorities**: Core MVP or comprehensive extended implementation?\n\n' +
403
+ '*Reply with your preferences (e.g. "1, 2") or proceed with defaults.*'
436
404
  }
437
405
 
438
406
  await cleanMoaWorkspaces(cwd)
@@ -531,6 +499,45 @@ export async function runMoAPipeline({
531
499
  }
532
500
  }
533
501
 
502
+ // 7b. Consilium Round 2 (Peer Critique) if enabled
503
+ if (isPeerCritiqueEnabled && successfulRefs.length > 1) {
504
+ if (typeof onProgress === 'function') {
505
+ onProgress('🤝 *Consilium Round 2: Candidates reviewing peer proposals in parallel...*\n')
506
+ }
507
+ const r2Promises = successfulRefs.map(async (cand) => {
508
+ const opponents = successfulRefs
509
+ .filter((c) => c.index !== cand.index)
510
+ .map((c) => ({ label: isBlindEvaluation ? `Candidate ${c.index}` : c.label, text: c.text }))
511
+ const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
512
+ try {
513
+ const slot = referenceModels[cand.index - 1] || { provider: cand.provider, model: cand.model }
514
+ const res = await callWithTransientRetry(callLlm, {
515
+ provider: slot.provider,
516
+ model: slot.model,
517
+ messages: [{ role: 'user', content: r2Prompt }],
518
+ temperature: refTemp,
519
+ maxTokens,
520
+ timeoutMs: refTimeoutSec * 1000,
521
+ }, candidateRetries)
522
+ const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
523
+ cand.text = refinedText
524
+ const r2Files = extractFileBlocks(refinedText)
525
+ if (r2Files.length > 0) {
526
+ cand.files = r2Files
527
+ cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
528
+ }
529
+ if (typeof res === 'object' && res?.usage) {
530
+ cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
531
+ cand.usage.outputTokens = (cand.usage.outputTokens || 0) + (res.usage.outputTokens || 0)
532
+ cand.usage.totalTokens = (cand.usage.totalTokens || 0) + (res.usage.totalTokens || 0)
533
+ const extraCost = estimateTokenCost(slot, res.usage, prices)
534
+ cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
535
+ }
536
+ } catch {}
537
+ })
538
+ await Promise.allSettled(r2Promises)
539
+ }
540
+
534
541
  // 8. Synthesis phase via primary judge or fallback chain
535
542
  const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
536
543
  curatorSynthesis: isCuratorSynthesis,
@@ -604,15 +611,16 @@ export async function runMoAPipeline({
604
611
  const synthesizedFiles = extractFileBlocks(synthesizedText)
605
612
  let promotedFiles = []
606
613
 
614
+ const keepWorkspaces = allowCandidateOverride
607
615
  if (synthesizedFiles.length > 0) {
608
616
  await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
609
- promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis')
617
+ promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { keepMoa: keepWorkspaces })
610
618
  } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
611
- promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex)
619
+ promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { keepMoa: keepWorkspaces })
612
620
  } else if (successfulRefs[0]?.files?.length > 0) {
613
621
  const fallbackIdx = successfulRefs[0].index
614
- promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx)
615
- } else {
622
+ promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { keepMoa: keepWorkspaces })
623
+ } else if (!keepWorkspaces) {
616
624
  await cleanMoaWorkspaces(cwd)
617
625
  }
618
626
 
@@ -629,6 +637,7 @@ export async function runMoAPipeline({
629
637
  // Record run to history
630
638
  try {
631
639
  recordMoaRun({
640
+ id: runId,
632
641
  prompt: userPrompt,
633
642
  preset: preset?.name || 'default',
634
643
  isRefinement,
@@ -658,6 +667,8 @@ export async function runMoAPipeline({
658
667
  winningIndex,
659
668
  winnerModel,
660
669
  promotedFiles,
670
+ runId,
671
+ allowCandidateOverride,
661
672
  ...(livePreview ? { liveCanvas: livePreview } : {}),
662
673
  usage: {
663
674
  totalTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",