@goodandready/dsh-moa 0.2.61 → 0.2.63

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.
@@ -0,0 +1,99 @@
1
+ import { runReferencesParallel } from '../moa-candidates.js'
2
+ import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
3
+ import { verifyFileSyntax } from '../workspace/diff.js'
4
+ import { executeTestGateForCandidates } from '../moa-test-gate.js'
5
+ import { slotLabel } from '../moa-prompts.js'
6
+
7
+ export async function stageCandidateRound1({
8
+ effectiveReferences,
9
+ enrichedMessages,
10
+ candidateSystemPrompt,
11
+ refTemp,
12
+ preset,
13
+ maxTokens,
14
+ prices,
15
+ refTimeoutSec,
16
+ isQuorumEnabled,
17
+ gracePeriodSec,
18
+ candidateRetries,
19
+ signal,
20
+ callLlm,
21
+ onProgress,
22
+ cwd,
23
+ runId,
24
+ startTime,
25
+ primaryJudge,
26
+ isRefinement,
27
+ isFastMode,
28
+ collectedCtx,
29
+ testGateEnabled,
30
+ testCommand,
31
+ execFn,
32
+ }) {
33
+ const referenceOutputs = await runReferencesParallel(
34
+ effectiveReferences,
35
+ enrichedMessages,
36
+ {
37
+ systemPrompt: candidateSystemPrompt,
38
+ temperature: refTemp,
39
+ temperature_gradient_enabled: Boolean(preset?.temperature_gradient_enabled),
40
+ local_fallback_enabled: Boolean(preset?.local_fallback_enabled),
41
+ local_fallback_models: preset?.local_fallback_models || [],
42
+ maxTokens,
43
+ prices,
44
+ timeoutSec: refTimeoutSec,
45
+ quorumEnabled: isQuorumEnabled,
46
+ gracePeriodSec,
47
+ maxRetries: candidateRetries,
48
+ signal,
49
+ },
50
+ callLlm,
51
+ onProgress
52
+ )
53
+
54
+ if (signal?.aborted) {
55
+ await cleanMoaWorkspaces(cwd, { runId })
56
+ return { ok: false, abort: true, error: new Error('Turn aborted') }
57
+ }
58
+
59
+ const successfulRefs = referenceOutputs.filter((r) => r.ok)
60
+ if (successfulRefs.length === 0) {
61
+ const reasons = referenceOutputs.map((r) => `${r.label}: ${r.error || 'unknown error'}`).join('; ')
62
+ const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
63
+ const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
64
+ const failurePayload = {
65
+ kind: 'failure', content: `⚠️ All advisor models (${referenceOutputs.length}) failed: ${reasons}`,
66
+ aggregator: slotLabel(primaryJudge), references: referenceOutputs, presetName: preset?.name || 'default',
67
+ isRefinement, isFastMode, winningIndex: 0, winnerModel: 'none', promotedFiles: [],
68
+ usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 } },
69
+ skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs: Date.now() - startTime,
70
+ }
71
+ return { ok: false, failurePayload }
72
+ }
73
+
74
+ for (let i = 0; i < referenceOutputs.length; i++) {
75
+ const ref = referenceOutputs[i]
76
+ if (!ref.ok) continue
77
+ const files = extractFileBlocks(ref.text)
78
+ ref.files = files
79
+ if (files.length > 0) {
80
+ ref.syntaxWarning = (verifyFileSyntax(files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
81
+ await writeCandidateWorkspace(cwd, i + 1, files, { runId })
82
+ }
83
+ }
84
+
85
+ if (signal?.aborted) {
86
+ await cleanMoaWorkspaces(cwd, { runId })
87
+ return { ok: false, abort: true, error: new Error('Turn aborted') }
88
+ }
89
+
90
+ await executeTestGateForCandidates({
91
+ cwd,
92
+ referenceOutputs,
93
+ preset,
94
+ options: { testGateEnabled, testCommand, execFn, runId },
95
+ onProgress,
96
+ })
97
+
98
+ return { ok: true, referenceOutputs, successfulRefs }
99
+ }
@@ -0,0 +1,107 @@
1
+ import { collectProjectContext, formatProjectContext, isRefinementTask } from '../workspace/scanner.js'
2
+ import { applyBudgetGuardrails, createBudgetAbortPayload } from '../moa-budget.js'
3
+ import { extractPriorTurnBaseline, pruneMultiTurnMessages } from '../moa-context.js'
4
+ import { REFERENCE_SYSTEM_PROMPT } from '../moa-candidates.js'
5
+ import { isBroadPromptRequiringQuestions } from '../moa-prompts.js'
6
+
7
+ export async function stageProjectContext({
8
+ userPrompt,
9
+ messages = [],
10
+ preset,
11
+ cwd,
12
+ prices,
13
+ maxTokens,
14
+ startTime,
15
+ onProgress,
16
+ }) {
17
+ const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
18
+ ? preset.reference_models : [{ provider: '', model: '' }]
19
+ const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
20
+ ? preset.aggregator : { provider: '', model: '' }
21
+ const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
22
+ const judgesChain = [primaryJudge, ...fallbackJudges]
23
+ const refTemp = typeof preset?.reference_temperature === 'number' ? preset.reference_temperature : 0.6
24
+ const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
25
+ const judgeCriteria = preset?.judge_criteria || ''
26
+ let isFastMode = referenceModels.length === 1 && !preset?.curator_synthesis
27
+ const isCuratorSynthesis = Boolean(preset?.curator_synthesis)
28
+ const isStreamAggregator = preset?.stream_aggregator !== false
29
+ const isQuorumEnabled = Boolean(preset?.quorum_enabled)
30
+ const gracePeriodSec = typeof preset?.grace_period_sec === 'number' ? preset.grace_period_sec : 10
31
+ const candidateRetries = 1
32
+ const refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
33
+ const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
34
+ const isBlindEvaluation = Boolean(preset?.blind_evaluation)
35
+ const isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
36
+ const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
37
+
38
+ const collectedCtx = await collectProjectContext(cwd, 16000)
39
+ const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
40
+ let projectContext = ''
41
+ if (collectedCtx?.skippedFiles > 0 && typeof onProgress === 'function') {
42
+ const preview = collectedCtx.skippedList.slice(0, 3).join(', ')
43
+ const more = collectedCtx.skippedList.length > 3 ? ` (+${collectedCtx.skippedList.length - 3})` : ''
44
+ onProgress(`⚠️ *Workspace scan note: ${collectedCtx.skippedFiles} file(s) skipped (${preview}${more})*\n`)
45
+ }
46
+ if (isRefinement) {
47
+ if (typeof onProgress === 'function') {
48
+ onProgress('🔍 *Reading project files for refinement context...*\n')
49
+ }
50
+ projectContext = formatProjectContext(collectedCtx.files, {
51
+ skippedFiles: collectedCtx.skippedFiles,
52
+ skippedList: collectedCtx.skippedList,
53
+ })
54
+ }
55
+
56
+ const budgetCheck = applyBudgetGuardrails({
57
+ references: referenceModels,
58
+ enabled: preset?.budget_guard_enabled,
59
+ maxBudgetUsd: preset?.max_budget_usd,
60
+ action: preset?.budget_action || 'trim',
61
+ prices,
62
+ promptLength: (userPrompt?.length || 1000) + (judgeCriteria?.length || 0) + (projectContext ? projectContext.length : 0),
63
+ judgeCriteria,
64
+ projectContext,
65
+ messages,
66
+ maxTokens,
67
+ peerCritiqueEnabled: isPeerCritiqueEnabled,
68
+ aggregator: primaryJudge,
69
+ judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
70
+ })
71
+
72
+ if (!budgetCheck.allowed) {
73
+ const abortPayload = createBudgetAbortPayload({
74
+ reason: budgetCheck.reason, primaryJudge, referenceOutputs: [], currentSpentUsd: 0,
75
+ preset, isRefinement, isFastMode, collectedCtx, startTime,
76
+ })
77
+ return { ok: false, abortPayload }
78
+ }
79
+
80
+ const effectiveReferences = budgetCheck.references
81
+ if (effectiveReferences.length === 1 && !preset?.curator_synthesis) {
82
+ isFastMode = true
83
+ }
84
+ if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
85
+ onProgress(`✂️ *${budgetCheck.reason}*\n`)
86
+ }
87
+
88
+ const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
89
+ const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
90
+ const candidateSystemPrompt = projectContext
91
+ ? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
92
+ : REFERENCE_SYSTEM_PROMPT
93
+ const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
94
+ const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
95
+ const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
96
+
97
+ return {
98
+ ok: true,
99
+ referenceModels, primaryJudge, fallbackJudges, judgesChain,
100
+ refTemp, aggTemp, maxTokens, judgeCriteria, isFastMode, isCuratorSynthesis,
101
+ isStreamAggregator, isQuorumEnabled, gracePeriodSec, candidateRetries,
102
+ refTimeoutSec, aggTimeoutSec, isBlindEvaluation, isPeerCritiqueEnabled,
103
+ allowCandidateOverride, collectedCtx, isRefinement, projectContext,
104
+ effectiveReferences, priorTurnBaseline, candidateSystemPrompt,
105
+ needsQuestions, enrichedMessages,
106
+ }
107
+ }
@@ -0,0 +1,100 @@
1
+ import { logger } from '../logger.js'
2
+ import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
3
+ import { createPromotedPreview } from '../live-canvas.js'
4
+ import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
5
+
6
+ export async function stageFastMode({
7
+ successfulRefs,
8
+ referenceOutputs,
9
+ allowCandidateOverride,
10
+ promoteOpts,
11
+ cwd,
12
+ runId,
13
+ signal,
14
+ userPrompt,
15
+ preset,
16
+ isRefinement,
17
+ historyFilePath,
18
+ startTime,
19
+ collectedCtx,
20
+ liveCanvas,
21
+ }) {
22
+ if (signal?.aborted) {
23
+ await cleanMoaWorkspaces(cwd, { runId })
24
+ return { ok: false, abort: true, error: new Error('Turn aborted') }
25
+ }
26
+ const single = successfulRefs[0]
27
+ let promotedFiles = [], promoteErr = null
28
+ if (single.files?.length > 0) {
29
+ try {
30
+ promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId, keepMoa: allowCandidateOverride })
31
+ } catch (e) { promoteErr = e }
32
+ } else if (!allowCandidateOverride) {
33
+ await cleanMoaWorkspaces(cwd, { runId })
34
+ }
35
+ if (promoteErr) {
36
+ if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
37
+ if (signal?.aborted || promoteErr?.message?.includes('aborted') || promoteErr?.name === 'AbortError' || promoteErr?.message === 'Turn aborted') {
38
+ return { ok: false, abort: true, error: new Error('Turn aborted') }
39
+ }
40
+ const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
41
+ const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)).toFixed(5))
42
+ const durationMs = Date.now() - startTime
43
+ try {
44
+ await recordMoaRunAsync({
45
+ id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
46
+ isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
47
+ aggregator: null, winnerIndex: 0, winnerModel: 'none',
48
+ promotedFiles: [], totalTokens, totalCostUsd, durationMs,
49
+ error: promoteErr?.message || String(promoteErr),
50
+ }, historyFilePath || undefined)
51
+ } catch (histErr) {
52
+ logger.warn('[dsh-moa] Failed to record fast synthesis failure in history:', histErr?.message || histErr)
53
+ }
54
+ return {
55
+ ok: false,
56
+ payload: {
57
+ kind: 'failure', error: promoteErr,
58
+ content: '🛑 Checkpoint or Promotion Failure: ' + (promoteErr?.message || String(promoteErr)),
59
+ aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
60
+ presetName: preset?.name || 'default', isRefinement, isFastMode: true,
61
+ winningIndex: 0, winnerModel: 'none', promotedFiles: [], runId,
62
+ usage: {
63
+ totalTokens, totalCostUsd,
64
+ candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
65
+ aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
66
+ },
67
+ skippedFiles: collectedCtx?.skippedFiles || 0,
68
+ skippedList: collectedCtx?.skippedList || [],
69
+ durationMs,
70
+ }
71
+ }
72
+ }
73
+
74
+ const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
75
+ const totalTokens = single.usage?.totalTokens || 0
76
+ const totalCostUsd = Number((single.costUsd || 0).toFixed(5))
77
+ const durationMs = Date.now() - startTime
78
+
79
+ try {
80
+ await recordMoaRunAsync({
81
+ id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
82
+ isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
83
+ aggregator: null, winnerIndex: single.index, winnerModel: single.label,
84
+ promotedFiles, totalTokens, totalCostUsd, durationMs,
85
+ }, historyFilePath || undefined)
86
+ } catch (histErr) {
87
+ logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
88
+ }
89
+
90
+ return {
91
+ ok: true,
92
+ payload: {
93
+ kind: 'synthesis', content: single.text, aggregator: 'Fast Mode (Direct)', references: referenceOutputs,
94
+ presetName: preset?.name || 'default', isRefinement, isFastMode: true, winningIndex: single.index,
95
+ winnerModel: single.label, promotedFiles, runId, ...(livePreview ? { liveCanvas: livePreview } : {}),
96
+ usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 } },
97
+ skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,37 @@
1
+ import { runMultiJudgePhase } from '../moa-multi-judge.js'
2
+ import { createBudgetAbortPayload } from '../moa-budget.js'
3
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
4
+
5
+ export async function stageMultiJudge({
6
+ preset,
7
+ successfulRefs,
8
+ referenceOutputs,
9
+ primaryJudge,
10
+ userPrompt,
11
+ judgeCriteria,
12
+ prices,
13
+ maxTokens,
14
+ callLlm,
15
+ signal,
16
+ onProgress,
17
+ cwd,
18
+ runId,
19
+ isRefinement,
20
+ isFastMode,
21
+ collectedCtx,
22
+ startTime,
23
+ }) {
24
+ const currentRefSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
25
+ const panelPhase = await runMultiJudgePhase({
26
+ preset, successfulRefs, primaryJudge, userPrompt, judgeCriteria, prices, maxTokens, callLlm, signal, onProgress, currentSpent: currentRefSpent,
27
+ })
28
+ if (panelPhase.abort) {
29
+ await cleanMoaWorkspaces(cwd, { runId })
30
+ const abortPayload = createBudgetAbortPayload({
31
+ reason: panelPhase.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentRefSpent,
32
+ preset, isRefinement, isFastMode, collectedCtx, startTime,
33
+ })
34
+ return { ok: false, abortPayload }
35
+ }
36
+ return { ok: true, multiJudgeResult: panelPhase.result }
37
+ }
@@ -0,0 +1,113 @@
1
+ import { logger } from '../logger.js'
2
+ import { estimateRound2Cost, estimateDownstreamCost, checkPhaseBudget, createBudgetAbortPayload } from '../moa-budget.js'
3
+ import { buildPeerCritiquePrompt } from '../moa-prompts.js'
4
+ import { callWithTransientRetry } from '../moa-candidates.js'
5
+ import { extractFileBlocks, writeCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
6
+ import { verifyFileSyntax } from '../workspace/diff.js'
7
+ import { estimateTokenCost } from '../pricing.js'
8
+ import { executeTestGateForCandidates } from '../moa-test-gate.js'
9
+
10
+ export async function stagePeerCritique({
11
+ isPeerCritiqueEnabled,
12
+ successfulRefs,
13
+ referenceOutputs,
14
+ preset,
15
+ prices,
16
+ maxTokens,
17
+ primaryJudge,
18
+ userPrompt,
19
+ isBlindEvaluation,
20
+ refTemp,
21
+ refTimeoutSec,
22
+ candidateRetries,
23
+ signal,
24
+ callLlm,
25
+ onProgress,
26
+ cwd,
27
+ runId,
28
+ isRefinement,
29
+ isFastMode,
30
+ collectedCtx,
31
+ startTime,
32
+ options: { testGateEnabled, testCommand, execFn } = {},
33
+ }) {
34
+ let runRound2 = isPeerCritiqueEnabled && successfulRefs.length > 1
35
+ if (runRound2 && preset?.budget_guard_enabled && preset?.max_budget_usd > 0) {
36
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0)
37
+ const r2Next = estimateRound2Cost(successfulRefs, prices, maxTokens)
38
+ const downNext = estimateDownstreamCost(primaryJudge, preset?.judge_models, successfulRefs.length, prices, maxTokens)
39
+ const phaseCheck = checkPhaseBudget({
40
+ phaseName: 'Consilium Round 2', currentSpentUsd: currentSpent, nextPhaseCostUsd: r2Next + downNext,
41
+ maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
42
+ })
43
+ if (!phaseCheck.allowed) {
44
+ if (phaseCheck.action === 'abort') {
45
+ await cleanMoaWorkspaces(cwd, { runId })
46
+ const abortPayload = createBudgetAbortPayload({
47
+ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent,
48
+ preset, isRefinement, isFastMode, collectedCtx, startTime,
49
+ })
50
+ return { ok: false, abortPayload }
51
+ }
52
+ runRound2 = false
53
+ if (typeof onProgress === 'function') {
54
+ onProgress(`✂️ *Skipping Round 2 to stay within budget ($${preset.max_budget_usd.toFixed(4)})*\n`)
55
+ }
56
+ }
57
+ }
58
+
59
+ if (runRound2) {
60
+ if (typeof onProgress === 'function') {
61
+ onProgress('🤝 *Consilium Round 2: Candidates reviewing peer proposals in parallel...*\n')
62
+ }
63
+ const r2Promises = successfulRefs.map(async (cand) => {
64
+ const opponents = successfulRefs
65
+ .filter((c) => c.index !== cand.index)
66
+ .map((c) => ({
67
+ label: isBlindEvaluation ? `Candidate ${c.index}` : c.label,
68
+ role_persona: c.role_persona || c.slot?.role_persona,
69
+ text: c.text,
70
+ }))
71
+ const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
72
+ try {
73
+ const slot = cand.slot || { provider: cand.provider, model: cand.model }
74
+ const res = await callWithTransientRetry(callLlm, {
75
+ provider: slot.provider,
76
+ model: slot.model,
77
+ messages: [{ role: 'user', content: r2Prompt }],
78
+ temperature: refTemp,
79
+ maxTokens,
80
+ timeoutMs: refTimeoutSec * 1000,
81
+ signal,
82
+ }, candidateRetries)
83
+ const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
84
+ cand.text = refinedText
85
+ const r2Files = extractFileBlocks(refinedText)
86
+ if (r2Files.length > 0) {
87
+ cand.files = r2Files
88
+ cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
89
+ await writeCandidateWorkspace(cwd, cand.index, r2Files, { runId })
90
+ }
91
+ if (typeof res === 'object' && res?.usage) {
92
+ cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
93
+ cand.usage.outputTokens = (cand.usage.outputTokens || 0) + (res.usage.outputTokens || 0)
94
+ cand.usage.totalTokens = (cand.usage.totalTokens || 0) + (res.usage.totalTokens || 0)
95
+ const extraCost = estimateTokenCost(slot, res.usage, prices)
96
+ cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
97
+ }
98
+ } catch (r2Err) {
99
+ logger.warn(`[dsh-moa] Round 2 peer critique failed for candidate ${cand.index}:`, r2Err?.message || r2Err)
100
+ }
101
+ })
102
+ await Promise.allSettled(r2Promises)
103
+ await executeTestGateForCandidates({
104
+ cwd,
105
+ referenceOutputs: successfulRefs,
106
+ preset,
107
+ options: { testGateEnabled, testCommand, execFn, runId },
108
+ onProgress,
109
+ })
110
+ }
111
+
112
+ return { ok: true }
113
+ }
@@ -0,0 +1,114 @@
1
+ import { logger } from '../logger.js'
2
+ import { parseWinnerIndex, parseRecommendedAssembler } from '../moa-parser.js'
3
+ import { extractFileBlocks, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
4
+ import { createPromotedPreview } from '../live-canvas.js'
5
+ import { summarizeMoAUsage } from '../pricing.js'
6
+ import { generateMoABenchmarkReport } from '../moa-report.js'
7
+ import { parseFileScores } from '../moa-workbench.js'
8
+ import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
9
+ import { slotLabel } from '../moa-prompts.js'
10
+
11
+ export async function stagePromotion({
12
+ synthesizedText,
13
+ referenceOutputs,
14
+ successfulRefs,
15
+ multiJudgeResult,
16
+ isCuratorSynthesis,
17
+ allowCandidateOverride,
18
+ promoteOpts,
19
+ cwd,
20
+ runId,
21
+ signal,
22
+ liveCanvas,
23
+ startTime,
24
+ chosenJudge,
25
+ aggUsage,
26
+ userPrompt,
27
+ preset,
28
+ isRefinement,
29
+ isFastMode,
30
+ effectiveReferences,
31
+ referenceModels,
32
+ historyFilePath,
33
+ collectedCtx,
34
+ }) {
35
+ const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
36
+ ? multiJudgeResult.winningCandidateIndex
37
+ : parseWinnerIndex(synthesizedText, 1, referenceOutputs.length)
38
+
39
+ const recommendedAssembler = isCuratorSynthesis
40
+ ? parseRecommendedAssembler(synthesizedText, winningIndex, referenceOutputs.length)
41
+ : null
42
+
43
+ const synthesizedFiles = extractFileBlocks(synthesizedText)
44
+ let promotedFiles = []
45
+ const keepWorkspaces = allowCandidateOverride
46
+
47
+ if (signal?.aborted) {
48
+ if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
49
+ return { ok: false, abort: true, error: new Error('Turn aborted') }
50
+ }
51
+
52
+ try {
53
+ if (synthesizedFiles.length > 0) {
54
+ await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
55
+ promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
56
+ } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
57
+ promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
58
+ } else if (successfulRefs[0]?.files?.length > 0) {
59
+ const fallbackIdx = successfulRefs[0].index
60
+ promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
61
+ } else if (!keepWorkspaces) {
62
+ await cleanMoaWorkspaces(cwd, { runId })
63
+ }
64
+ } catch (promoteErr) {
65
+ if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
66
+ if (signal?.aborted || promoteErr?.message === 'Turn aborted') return { ok: false, abort: true, error: new Error('Turn aborted') }
67
+ throw promoteErr
68
+ }
69
+
70
+ const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
71
+ const durationMs = Date.now() - startTime
72
+ const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
73
+ const winningRef = referenceOutputs[winningIndex - 1]
74
+ const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
75
+ const finalAggLabel = slotLabel(chosenJudge)
76
+
77
+ let benchmarkReport = null
78
+ if (preset?.report_generation_enabled) {
79
+ benchmarkReport = generateMoABenchmarkReport({
80
+ runId, timestamp: new Date().toISOString(), preset: preset?.name || 'default',
81
+ prompt: userPrompt, durationMs, usage: usageSummary, costUsd: usageSummary.totalCostUsd,
82
+ candidates: referenceOutputs, winningIndex, winningLabel: winnerModel,
83
+ consensus: multiJudgeResult, testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
84
+ isComposite: Boolean(preset?.composite_merge_enabled),
85
+ })
86
+ }
87
+
88
+ const fileScores = (preset?.interactive_workbench_enabled)
89
+ ? parseFileScores(synthesizedText, referenceOutputs)
90
+ : null
91
+
92
+ try {
93
+ await recordMoaRunAsync({
94
+ id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
95
+ candidates: candidatesForHistory(referenceOutputs),
96
+ aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
97
+ winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
98
+ fileScores, totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
99
+ }, historyFilePath || undefined)
100
+ } catch (histErr) {
101
+ logger.warn('[dsh-moa] Failed to record run in history:', histErr)
102
+ }
103
+
104
+ return {
105
+ ok: true,
106
+ payload: {
107
+ kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
108
+ references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
109
+ recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
110
+ benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
111
+ usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,79 @@
1
+ import { logger } from '../logger.js'
2
+ import { buildQuestionSynthesisPrompt, slotLabel } from '../moa-prompts.js'
3
+ import { callWithTransientRetry } from '../moa-candidates.js'
4
+ import { estimateTokenCost } from '../pricing.js'
5
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
6
+ import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
7
+
8
+ export async function stageClarifyingQuestions({
9
+ userPrompt,
10
+ referenceOutputs,
11
+ primaryJudge,
12
+ aggTimeoutSec,
13
+ prices,
14
+ callLlm,
15
+ cwd,
16
+ runId,
17
+ preset,
18
+ isRefinement,
19
+ isFastMode,
20
+ collectedCtx,
21
+ startTime,
22
+ historyFilePath,
23
+ onProgress,
24
+ }) {
25
+ if (typeof onProgress === 'function') {
26
+ onProgress('📋 *Judge synthesizes the clarification questionnaire...*\n')
27
+ }
28
+ const questionPrompt = buildQuestionSynthesisPrompt(userPrompt, referenceOutputs)
29
+ let questionsContent = ''
30
+ let qUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
31
+ try {
32
+ const qRes = await callWithTransientRetry(callLlm, {
33
+ provider: primaryJudge.provider,
34
+ model: primaryJudge.model,
35
+ messages: [{ role: 'user', content: questionPrompt }],
36
+ temperature: 0.3,
37
+ maxTokens: 2048,
38
+ timeoutMs: aggTimeoutSec * 1000,
39
+ }, 1)
40
+ questionsContent = typeof qRes === 'string' ? qRes : (qRes?.content || qRes?.text || '')
41
+ const qFallbackUsage = (typeof qRes === 'object' && qRes?.usage) ? qRes.usage : {
42
+ inputTokens: Math.max(1, Math.round(questionPrompt.length / 4)),
43
+ outputTokens: Math.max(1, Math.round(questionsContent.length / 4)),
44
+ }
45
+ qUsage = estimateTokenCost(primaryJudge, qFallbackUsage, prices)
46
+ } catch (err) {
47
+ logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
48
+ questionsContent = `### Clarification of Requirements: "${userPrompt}"\n\n1. **Architecture & Scope**: Single-file deliverable or multi-module project structure?\n2. **Design & Style**: Minimalist, dark mode, or clean neutral theme?\n3. **Functional Priorities**: Core MVP or comprehensive extended implementation?\n\n*Reply with your preferences (e.g. "1, 2") or proceed with defaults.*`
49
+ }
50
+ await cleanMoaWorkspaces(cwd, { runId })
51
+ const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
52
+ const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
53
+ const durationMs = Date.now() - startTime
54
+ try {
55
+ await recordMoaRunAsync({
56
+ cwd,
57
+ prompt: userPrompt,
58
+ preset: preset?.name || 'default',
59
+ isRefinement,
60
+ candidates: candidatesForHistory(referenceOutputs),
61
+ aggregator: { provider: primaryJudge.provider, model: primaryJudge.model, usage: qUsage, costUsd: qUsage.costUsd },
62
+ winnerIndex: -1,
63
+ winnerModel: 'none',
64
+ promotedFiles: [],
65
+ totalTokens,
66
+ totalCostUsd,
67
+ durationMs,
68
+ }, historyFilePath || undefined)
69
+ } catch (histErr) {
70
+ logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
71
+ }
72
+ return {
73
+ kind: 'questions', content: questionsContent, aggregator: slotLabel(primaryJudge),
74
+ references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode,
75
+ winningIndex: 0, winnerModel: 'none', promotedFiles: [],
76
+ usage: { totalTokens, totalCostUsd, candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })), aggregator: qUsage },
77
+ skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
78
+ }
79
+ }