@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/README.md +48 -2
- package/README.ru.md +287 -0
- package/README.zh.md +243 -0
- package/docs/README.ru.md +47 -1
- package/docs/README.zh.md +11 -1
- package/docs/design/DESIGN.md +4 -0
- package/docs/plans/59-power-pack-plan.md +73 -0
- package/lib/client.js +210 -3
- package/lib/file-workspace.js +53 -5
- package/lib/history.js +56 -2
- package/lib/index.js +86 -2
- package/lib/moa-parser.js +27 -3
- package/lib/moa-prompts.js +75 -10
- package/lib/moa-runner.js +68 -57
- package/package.json +1 -1
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
|
|
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
|
|
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**: [🚀
|
|
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
|
}
|
package/lib/moa-prompts.js
CHANGED
|
@@ -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 (/^(
|
|
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 = ['
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 = `###
|
|
432
|
-
'1.
|
|
433
|
-
'2.
|
|
434
|
-
'3.
|
|
435
|
-
'
|
|
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,
|