@goodandready/dsh-moa 0.2.60 → 0.2.62

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,163 @@
1
+ import crypto from 'node:crypto'
2
+ import { isTestEnvironment } from '../history.js'
3
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
4
+ import { buildCompositeBlockDirectives } from '../moa-multi-judge.js'
5
+ import { formatFileScoresDirective } from '../moa-workbench.js'
6
+ import { stageProjectContext } from './context.js'
7
+ import { stageCandidateRound1 } from './candidates.js'
8
+ import { stageClarifyingQuestions } from './questions.js'
9
+ import { stageFastMode } from './fast-mode.js'
10
+ import { stagePeerCritique } from './peer-critique.js'
11
+ import { stageMultiJudge } from './multi-judge.js'
12
+ import { stageSynthesis } from './synthesis.js'
13
+ import { stagePromotion } from './promotion.js'
14
+
15
+ export async function runMoAPipeline({
16
+ userPrompt,
17
+ messages = [],
18
+ preset,
19
+ callLlm,
20
+ cwd = process.cwd(), prices = {}, onProgress = null,
21
+ historyFilePath = null, liveCanvas = null, onStreamDelta = null, signal = null,
22
+ testGateEnabled = null, testCommand = null, execFn = null,
23
+ webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
24
+ force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
25
+ }) {
26
+ const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
27
+ const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
28
+ if (signal?.aborted) return { error: new Error('Turn aborted') }
29
+ const startTime = Date.now()
30
+
31
+ // Stage 1: Context & Configuration
32
+ const ctxStage = await stageProjectContext({
33
+ userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress,
34
+ })
35
+ if (!ctxStage.ok) return ctxStage.abortPayload
36
+
37
+ // Stage 2: Parallel Candidate Fan-out & Test Gate
38
+ const candStage = await stageCandidateRound1({
39
+ effectiveReferences: ctxStage.effectiveReferences,
40
+ enrichedMessages: ctxStage.enrichedMessages,
41
+ candidateSystemPrompt: ctxStage.candidateSystemPrompt,
42
+ refTemp: ctxStage.refTemp,
43
+ preset, maxTokens: ctxStage.maxTokens, prices,
44
+ refTimeoutSec: ctxStage.refTimeoutSec,
45
+ isQuorumEnabled: ctxStage.isQuorumEnabled,
46
+ gracePeriodSec: ctxStage.gracePeriodSec,
47
+ candidateRetries: ctxStage.candidateRetries,
48
+ signal, callLlm, onProgress, cwd, runId, startTime,
49
+ primaryJudge: ctxStage.primaryJudge,
50
+ isRefinement: ctxStage.isRefinement,
51
+ isFastMode: ctxStage.isFastMode,
52
+ collectedCtx: ctxStage.collectedCtx,
53
+ testGateEnabled, testCommand, execFn,
54
+ })
55
+ if (!candStage.ok) {
56
+ if (candStage.abort) return { error: candStage.error }
57
+ return candStage.failurePayload
58
+ }
59
+ const { referenceOutputs, successfulRefs } = candStage
60
+
61
+ // Stage 3: Clarifying Questions Branch
62
+ if (ctxStage.needsQuestions) {
63
+ return await stageClarifyingQuestions({
64
+ userPrompt, referenceOutputs, primaryJudge: ctxStage.primaryJudge,
65
+ aggTimeoutSec: ctxStage.aggTimeoutSec, prices, callLlm, cwd, runId,
66
+ preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
67
+ collectedCtx: ctxStage.collectedCtx, startTime, historyFilePath, onProgress,
68
+ })
69
+ }
70
+
71
+ // Stage 4: Fast Mode (1 candidate bypass)
72
+ if (ctxStage.isFastMode) {
73
+ const fastRes = await stageFastMode({
74
+ successfulRefs, referenceOutputs,
75
+ allowCandidateOverride: ctxStage.allowCandidateOverride,
76
+ promoteOpts, cwd, runId, signal, userPrompt, preset,
77
+ isRefinement: ctxStage.isRefinement, historyFilePath, startTime,
78
+ collectedCtx: ctxStage.collectedCtx, liveCanvas,
79
+ })
80
+ if (!fastRes.ok) {
81
+ if (fastRes.abort) return { error: fastRes.error }
82
+ return fastRes.payload
83
+ }
84
+ return fastRes.payload
85
+ }
86
+
87
+ if (signal?.aborted) {
88
+ await cleanMoaWorkspaces(cwd, { runId })
89
+ return { error: new Error('Turn aborted') }
90
+ }
91
+
92
+ // Stage 5: Consilium Round 2 (Peer Critique)
93
+ const peerRes = await stagePeerCritique({
94
+ isPeerCritiqueEnabled: ctxStage.isPeerCritiqueEnabled,
95
+ successfulRefs, referenceOutputs, preset, prices,
96
+ maxTokens: ctxStage.maxTokens, primaryJudge: ctxStage.primaryJudge,
97
+ userPrompt, isBlindEvaluation: ctxStage.isBlindEvaluation,
98
+ refTemp: ctxStage.refTemp, refTimeoutSec: ctxStage.refTimeoutSec,
99
+ candidateRetries: ctxStage.candidateRetries, signal, callLlm, onProgress,
100
+ cwd, runId, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
101
+ collectedCtx: ctxStage.collectedCtx, startTime,
102
+ options: { testGateEnabled, testCommand, execFn },
103
+ })
104
+ if (!peerRes.ok) return peerRes.abortPayload
105
+
106
+ if (signal?.aborted) {
107
+ await cleanMoaWorkspaces(cwd, { runId })
108
+ return { error: new Error('Turn aborted') }
109
+ }
110
+
111
+ // Stage 6: Multi-Judge Panel
112
+ const mjRes = await stageMultiJudge({
113
+ preset, successfulRefs, referenceOutputs, primaryJudge: ctxStage.primaryJudge,
114
+ userPrompt, judgeCriteria: ctxStage.judgeCriteria, prices, maxTokens: ctxStage.maxTokens,
115
+ callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
116
+ isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
117
+ })
118
+ if (!mjRes.ok) return mjRes.abortPayload
119
+ const multiJudgeResult = mjRes.multiJudgeResult
120
+
121
+ const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
122
+ ? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
123
+ : null
124
+ const fileScoringDirective = (preset?.interactive_workbench_enabled)
125
+ ? formatFileScoresDirective()
126
+ : null
127
+
128
+ // Stage 7: Synthesis Phase
129
+ const synthRes = await stageSynthesis({
130
+ userPrompt, referenceOutputs, successfulRefs, judgeCriteria: ctxStage.judgeCriteria,
131
+ isCuratorSynthesis: ctxStage.isCuratorSynthesis, isBlindEvaluation: ctxStage.isBlindEvaluation,
132
+ priorTurnBaseline: ctxStage.priorTurnBaseline, compositeMergeDirective, fileScoringDirective,
133
+ multiJudgeResult, preset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
134
+ primaryJudge: ctxStage.primaryJudge, fallbackJudges: ctxStage.fallbackJudges,
135
+ judgesChain: ctxStage.judgesChain, aggTemp: ctxStage.aggTemp,
136
+ aggTimeoutSec: ctxStage.aggTimeoutSec, isStreamAggregator: ctxStage.isStreamAggregator,
137
+ onStreamDelta, signal, callLlm, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
138
+ collectedCtx: ctxStage.collectedCtx, startTime,
139
+ })
140
+ if (!synthRes.ok) return synthRes.abortPayload
141
+ const { synthesizedText, chosenJudge, aggUsage } = synthRes
142
+
143
+ if (signal?.aborted) {
144
+ await cleanMoaWorkspaces(cwd, { runId })
145
+ return { error: new Error('Turn aborted') }
146
+ }
147
+
148
+ // Stage 8: Evaluation, Winner & Promotion
149
+ const promRes = await stagePromotion({
150
+ synthesizedText, referenceOutputs, successfulRefs, multiJudgeResult,
151
+ isCuratorSynthesis: ctxStage.isCuratorSynthesis,
152
+ allowCandidateOverride: ctxStage.allowCandidateOverride,
153
+ promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
154
+ userPrompt, preset, isRefinement: ctxStage.isRefinement, isFastMode: ctxStage.isFastMode,
155
+ effectiveReferences: ctxStage.effectiveReferences, referenceModels: ctxStage.referenceModels,
156
+ historyFilePath, collectedCtx: ctxStage.collectedCtx,
157
+ })
158
+ if (!promRes.ok) {
159
+ if (promRes.abort) return { error: promRes.error }
160
+ throw promRes.error
161
+ }
162
+ return promRes.payload
163
+ }
@@ -0,0 +1,150 @@
1
+ import { logger } from '../logger.js'
2
+ import { buildSynthesisPrompt, slotLabel } from '../moa-prompts.js'
3
+ import { estimateDownstreamCost, checkPhaseBudget, createBudgetAbortPayload } from '../moa-budget.js'
4
+ import { callWithTransientRetry } from '../moa-candidates.js'
5
+ import { estimateTokenCost } from '../pricing.js'
6
+ import { cleanMoaWorkspaces } from '../workspace/fs.js'
7
+
8
+ export async function stageSynthesis({
9
+ userPrompt,
10
+ referenceOutputs,
11
+ successfulRefs,
12
+ judgeCriteria,
13
+ isCuratorSynthesis,
14
+ isBlindEvaluation,
15
+ priorTurnBaseline,
16
+ compositeMergeDirective,
17
+ fileScoringDirective,
18
+ multiJudgeResult,
19
+ preset,
20
+ prices,
21
+ maxTokens,
22
+ isFastMode,
23
+ primaryJudge,
24
+ fallbackJudges,
25
+ judgesChain,
26
+ aggTemp,
27
+ aggTimeoutSec,
28
+ isStreamAggregator,
29
+ onStreamDelta,
30
+ signal,
31
+ callLlm,
32
+ onProgress,
33
+ cwd,
34
+ runId,
35
+ isRefinement,
36
+ collectedCtx,
37
+ startTime,
38
+ }) {
39
+ const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
40
+ curatorSynthesis: isCuratorSynthesis,
41
+ blindEvaluation: isBlindEvaluation,
42
+ priorTurnBaseline,
43
+ compositeMergeDirective,
44
+ fileScoringDirective,
45
+ consensusReport: multiJudgeResult?.consensusReport,
46
+ })
47
+ const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
48
+
49
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
50
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
51
+ const downNext = estimateDownstreamCost(primaryJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
52
+ const phaseCheck = checkPhaseBudget({
53
+ phaseName: 'Judge Synthesis', currentSpentUsd: currentSpent, nextPhaseCostUsd: downNext,
54
+ maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
55
+ })
56
+ if (!phaseCheck.allowed && fallbackJudges.length === 0) {
57
+ await cleanMoaWorkspaces(cwd, { runId })
58
+ const abortPayload = createBudgetAbortPayload({
59
+ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent,
60
+ preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult,
61
+ })
62
+ return { ok: false, abortPayload }
63
+ }
64
+ }
65
+
66
+ let synthesizedText = ''
67
+ let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
68
+ let chosenJudge = primaryJudge
69
+ let judgeSuccess = false
70
+ let lastJudgeError = null
71
+
72
+ for (let jIdx = 0; jIdx < judgesChain.length; jIdx++) {
73
+ const currentJudge = judgesChain[jIdx]
74
+ const currentLabel = slotLabel(currentJudge)
75
+
76
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
77
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
78
+ const judgeCost = estimateDownstreamCost(currentJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
79
+ const phaseCheck = checkPhaseBudget({
80
+ phaseName: `Judge Synthesis (${currentLabel})`, currentSpentUsd: currentSpent, nextPhaseCostUsd: judgeCost,
81
+ maxBudgetUsd: preset.max_budget_usd, action: preset.budget_action || 'trim', enabled: true,
82
+ })
83
+ if (!phaseCheck.allowed) {
84
+ lastJudgeError = new Error(phaseCheck.reason)
85
+ logger.warn(`[dsh-moa] Judge ${currentLabel} budget check failed: ${phaseCheck.reason}`)
86
+ if (typeof onProgress === 'function') onProgress(`⚠️ *Judge ${currentLabel} skipped: exceeds budget limit.*\n`)
87
+ continue
88
+ }
89
+ }
90
+
91
+ if (typeof onProgress === 'function') {
92
+ const modeTitle = isCuratorSynthesis ? 'Curator' : 'Judge'
93
+ const fallbackBadge = jIdx > 0 ? ` (Fallback #${jIdx})` : ''
94
+ onProgress(`⚖️ *${modeTitle} (${currentLabel}${fallbackBadge}) synthesizing solution...*\n`)
95
+ }
96
+
97
+ try {
98
+ const aggRes = await callWithTransientRetry(callLlm, {
99
+ provider: currentJudge.provider,
100
+ model: currentJudge.model,
101
+ messages: [{ role: 'user', content: synthesisPrompt }],
102
+ temperature: aggTemp,
103
+ maxTokens,
104
+ timeoutMs: aggTimeoutSec * 1000,
105
+ signal,
106
+ onStreamDelta: (delta) => {
107
+ if (isStreamAggregator && typeof onStreamDelta === 'function') {
108
+ onStreamDelta(delta)
109
+ }
110
+ },
111
+ }, 0)
112
+
113
+ synthesizedText = typeof aggRes === 'string' ? aggRes : (aggRes?.content || aggRes?.text || '')
114
+
115
+ const aggFallbackUsage = (typeof aggRes === 'object' && aggRes?.usage) ? aggRes.usage : {
116
+ inputTokens: Math.max(1, Math.round(synthesisPrompt.length / 4)),
117
+ outputTokens: Math.max(1, Math.round(synthesizedText.length / 4)),
118
+ }
119
+ aggUsage = estimateTokenCost(currentJudge, aggFallbackUsage, prices)
120
+ chosenJudge = currentJudge
121
+ judgeSuccess = true
122
+ break
123
+ } catch (err) {
124
+ lastJudgeError = err
125
+ logger.warn(`[dsh-moa] Judge ${currentLabel} failed:`, err)
126
+ if (typeof onProgress === 'function') {
127
+ const nextJudge = judgesChain[jIdx + 1]
128
+ const nextHint = nextJudge ? ` Trying fallback ${slotLabel(nextJudge)}...` : ''
129
+ onProgress(`⚠️ *Judge ${currentLabel} failed: ${err?.message || err}.${nextHint}*\n`)
130
+ }
131
+ }
132
+ }
133
+
134
+ if (!judgeSuccess) {
135
+ if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode && (lastJudgeError?.message?.includes('exceeds max budget') || lastJudgeError?.message?.includes('Budget Guardrail') || preset?.budget_action === 'abort')) {
136
+ await cleanMoaWorkspaces(cwd, { runId })
137
+ const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
138
+ const abortPayload = createBudgetAbortPayload({
139
+ reason: lastJudgeError?.message || 'Budget exceeded before judge synthesis', primaryJudge, referenceOutputs,
140
+ currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult,
141
+ })
142
+ return { ok: false, abortPayload }
143
+ }
144
+ const firstErr = lastJudgeError ? (lastJudgeError.message || String(lastJudgeError)) : 'unknown error'
145
+ synthesizedText = `⚠️ *[Aggregator error: ${firstErr}. Fallback candidate outputs:]*\n\n` +
146
+ successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
147
+ }
148
+
149
+ return { ok: true, synthesizedText, chosenJudge, aggUsage }
150
+ }
@@ -0,0 +1,81 @@
1
+ import { getMoaHistory, getMoaLeaderboard, exportMoaHistory } from '../history.js'
2
+ import { writeJson, assertAuthorizedRequest } from '../access.js'
3
+
4
+ export function registerHistoryRoutes(ctx, { live }) {
5
+ ctx.effect(() => {
6
+ return ctx.webServer.register({
7
+ kind: 'exact',
8
+ path: '/dsh-moa/history',
9
+ handler: async (req, res) => {
10
+ if (!assertAuthorizedRequest(req, res, ctx)) return
11
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
12
+ try {
13
+ const u = new URL(req.url, 'http://localhost')
14
+ const limit = Math.max(1, Math.min(100, parseInt(u.searchParams.get('limit') || '20', 10) || 20))
15
+ const offset = Math.max(0, parseInt(u.searchParams.get('offset') || '0', 10) || 0)
16
+ const data = getMoaHistory(limit, offset)
17
+ writeJson(res, 200, { ok: true, ...data })
18
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
19
+ },
20
+ })
21
+ }, 'dsh-moa: history route')
22
+
23
+ // Route: /dsh-moa/leaderboard
24
+
25
+ ctx.effect(() => {
26
+ return ctx.webServer.register({
27
+ kind: 'exact',
28
+ path: '/dsh-moa/leaderboard',
29
+ handler: async (req, res) => {
30
+ if (!assertAuthorizedRequest(req, res, ctx)) return
31
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
32
+ try {
33
+ const url = new URL(req.url || '', 'http://127.0.0.1')
34
+ const preset = url.searchParams.get('preset') || null
35
+ const format = url.searchParams.get('format') || 'json'
36
+
37
+ if (format === 'csv') {
38
+ const csv = exportMoaHistory(undefined, { format: 'csv', preset })
39
+ res.writeHead(200, {
40
+ 'Content-Type': 'text/csv; charset=utf-8',
41
+ 'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
42
+ })
43
+ res.end(csv)
44
+ return
45
+ }
46
+
47
+ const data = getMoaLeaderboard(undefined, { preset })
48
+ writeJson(res, 200, { ok: true, ...data })
49
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
50
+ },
51
+ })
52
+ }, 'dsh-moa: leaderboard route')
53
+
54
+ // Route: /dsh-moa/promote (Candidate Override Action)
55
+
56
+ ctx.effect(() => {
57
+ return ctx.webServer.register({
58
+ kind: 'exact',
59
+ path: '/dsh-moa/history/export',
60
+ handler: async (req, res) => {
61
+ if (!assertAuthorizedRequest(req, res, ctx)) return
62
+ if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
63
+ try {
64
+ const url = new URL(req.url || '', 'http://127.0.0.1')
65
+ const preset = url.searchParams.get('preset') || null
66
+ const format = url.searchParams.get('format') || 'json'
67
+ const data = exportMoaHistory(undefined, { format, preset })
68
+
69
+ const isCsv = format === 'csv'
70
+ res.writeHead(200, {
71
+ 'Content-Type': isCsv ? 'text/csv; charset=utf-8' : 'application/json; charset=utf-8',
72
+ 'Content-Disposition': `attachment; filename="moa-history.${isCsv ? 'csv' : 'json'}"`,
73
+ })
74
+ res.end(data)
75
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
76
+ },
77
+ })
78
+ }, 'dsh-moa: history export route')
79
+
80
+ // Route: /dsh-moa/runs/<id> (run replay by id and interactive workbench)
81
+ }
@@ -0,0 +1,91 @@
1
+ import path from 'node:path'
2
+ import os from 'node:os'
3
+ import { isTestEnvironment } from '../history.js'
4
+ import { isLoopbackUrl } from '../file-workspace.js'
5
+ import { runMoAPipeline } from '../moa-runner.js'
6
+ import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from '../access.js'
7
+ import { isSafeWriteRequest } from '../updater.js'
8
+
9
+
10
+ export function registerPipelineRoutes(ctx, { live, callLlm, liveCanvas, webServerPort, timeMachineEngine, workspaceRoot }) {
11
+ let _lastRunTimestamp = 0
12
+ const RUN_MIN_INTERVAL_MS = 1000
13
+
14
+ ctx.effect(() => {
15
+ return ctx.webServer.register({
16
+ kind: 'exact',
17
+ path: '/dsh-moa/run',
18
+ handler: async (req, res) => {
19
+ if (!assertAuthorizedRequest(req, res, ctx)) return
20
+ if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
21
+ if (!isSafeWriteRequest(req)) {
22
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
23
+ return
24
+ }
25
+ const now = Date.now()
26
+ if (now - _lastRunTimestamp < RUN_MIN_INTERVAL_MS) {
27
+ writeJson(res, 429, { ok: false, error: 'Rate limit exceeded: please wait before starting another run' })
28
+ return
29
+ }
30
+
31
+ try {
32
+ const raw = await readBody(req)
33
+ const payload = JSON.parse(raw)
34
+ const prompt = payload.prompt || ''
35
+ if (!prompt.trim()) {
36
+ writeJson(res, 400, { ok: false, error: 'Empty prompt' })
37
+ return
38
+ }
39
+ _lastRunTimestamp = now
40
+
41
+ const cfg = live()
42
+ if (cfg.enabled === false) {
43
+ writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
44
+ return
45
+ }
46
+ if (payload.timeMachineUrl && !isLoopbackUrl(payload.timeMachineUrl)) {
47
+ writeJson(res, 400, { ok: false, error: 'Invalid timeMachineUrl: loopback host required' })
48
+ return
49
+ }
50
+ const presetName = payload.preset || cfg.default_preset || 'default'
51
+ const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
52
+
53
+ const baseWorkspaceRoot = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
54
+ const allowedRoots = [
55
+ baseWorkspaceRoot,
56
+ process.cwd(),
57
+ cfg.workspace_root,
58
+ ...(Array.isArray(cfg.allowed_workspaces) ? cfg.allowed_workspaces : (cfg.allowed_workspaces ? [cfg.allowed_workspaces] : [])),
59
+ ...(isTestEnvironment() ? [os.tmpdir()] : []),
60
+ ].filter(Boolean)
61
+
62
+ const targetCwd = payload.cwd ? path.resolve(payload.cwd) : baseWorkspaceRoot
63
+ if (payload.cwd && !isAllowedWorkspace(targetCwd, allowedRoots)) {
64
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cwd escapes allowed workspace boundary' })
65
+ return
66
+ }
67
+
68
+ const result = await runMoAPipeline({
69
+ userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
70
+ cwd: targetCwd, prices: cfg.prices || {}, liveCanvas,
71
+ webServerPort: webServerPort || ctx.webServer?.port,
72
+ timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
73
+ timeMachineUrl: payload.timeMachineUrl, force: false,
74
+ checkpoint: true,
75
+ runId: payload.runId,
76
+ })
77
+
78
+ if (result?.skippedFiles > 0) {
79
+ ctx.logger?.warn?.(`[dsh-moa] Workspace scan skipped ${result.skippedFiles} file(s): ${result.skippedList.join(', ')}`)
80
+ }
81
+ if (result?.error) {
82
+ writeJson(res, 500, { ok: false, error: result.error?.message || String(result.error) })
83
+ return
84
+ }
85
+
86
+ writeJson(res, 200, { ok: true, ...result })
87
+ } catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
88
+ },
89
+ })
90
+ }, 'dsh-moa: run pipeline route')
91
+ }