@goodandready/dsh-moa 0.2.38 → 0.2.39

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 CHANGED
@@ -140,7 +140,7 @@ Configure your models in `Settings → Plugins → Mixture of Agents`:
140
140
  Inspect line-by-line differences between candidate proposals and the curator's synthesized deliverable directly in the UI. Features file selection, delta line highlights (added, removed, same), and unified diff rendering via a zero-dependency in-memory LCS algorithm.
141
141
 
142
142
  ### 8. Pre-Promotion Git Checkpoints (`dsh-time-machine`)
143
- Before promoting any winning candidate files over the workspace root, `dsh-moa` invokes the local `dsh-time-machine` service to create a shadow Git checkpoint (`moa-pre-promotion: candidate-N`). If `dsh-time-machine` is absent or unreachable, file promotion proceeds seamlessly via best-effort fallback.
143
+ Before promoting any winning candidate files over the workspace root, `dsh-moa` invokes the local `dsh-time-machine` service to create a shadow Git checkpoint (`moa-pre-promotion: candidate-N`). If `dsh-time-machine` is absent or unreachable, file promotion is blocked (fail-closed) to protect uncheckpointed workspace files against unintended overwrites (override with `force: true` only when automated checkpoints are explicitly unneeded).
144
144
 
145
145
  ### 9. Automated Test Execution Gate
146
146
  When `test_gate_enabled: true` and `test_command` (e.g. `npm test` or `pytest`) are configured, candidate code is executed in an ephemeral sandbox overlay. The judge receives concrete test outcomes, durations, and output logs to ground decisions in objective verification.
package/README.ru.md CHANGED
@@ -140,7 +140,7 @@ graph TD
140
140
  Позволяет визуально сопоставлять построчные различия между решениями моделей-советников и итоговым синтезом судьи прямо в интерфейсе. Поддерживает выбор конкретного файла, подсветку добавленных/удаленных строк и вычисление дельт с помощью легковесного алгоритма LCS без внешних зависимостей.
141
141
 
142
142
  ### 8. Теневые Git-чекпоинты (`dsh-time-machine`)
143
- Перед промоушном файлов победителя в корень рабочей области `dsh-moa` автоматически обращается к сервису `dsh-time-machine` для создания снимка (`moa-pre-promotion: candidate-N`). Если плагин недоступен, промоушн штатно продолжается по схеме best-effort.
143
+ Перед промоушном файлов победителя в корень рабочей области `dsh-moa` автоматически обращается к сервису `dsh-time-machine` для создания снимка (`moa-pre-promotion: candidate-N`). Если плагин недоступен или создание снимка завершилось ошибкой, перезапись файлов блокируется во избежание потери несохранённых данных (переопределяется параметром `force: true`, только если создание снимков намеренно отключено).
144
144
 
145
145
  ### 9. Автоматический шлюз выполнения тестов (Automated Test Execution Gate)
146
146
  При включении `test_gate_enabled: true` и указании тестовой команды (`test_command`, например `npm test` или `pytest`) каждый кандидат запускается в изолированном оверлейном окружении. Судья получает объективный вердикт с кодами возврата, временем исполнения и логами тестов до вынесения итогового решения.
package/README.zh.md CHANGED
@@ -140,7 +140,7 @@ graph TD
140
140
  直接在界面中可视化对比顾问方案之间以及顾问与裁判综合方案之间的逐行差异。支持文件筛选、增删改高亮,基于纯 JavaScript LCS 差异算法,零第三方依赖。
141
141
 
142
142
  ### 8. 提升前 Git 快照检查点 (`dsh-time-machine`)
143
- 在将胜出候选者的文件提升并覆盖至项目根目录前,`dsh-moa` 自动调用本地 `dsh-time-machine` 建立影子 Git 检查点 (`moa-pre-promotion: candidate-N`)。如未安装则静默回退,不阻断提升流程。
143
+ 在将胜出候选者的文件提升并覆盖至项目根目录前,`dsh-moa` 自动调用本地 `dsh-time-machine` 建立影子 Git 检查点 (`moa-pre-promotion: candidate-N`)。如 `dsh-time-machine` 未安装或无法访问,文件提升将被阻断以防止未备份的项目文件被意外覆盖(仅在明确不需要检查点时可通过 `force: true` 覆盖)。
144
144
 
145
145
  ### 18. 平滑降级与本地容灾恢复 (Local Fallback Resilience)
146
146
  当 `local_fallback_enabled: true` 时,因网络中断、超时或速率限制(429/500)而失败的候选模型会自动使用配置的本地模型(Ollama / MiniPC)恢复。每个后备模型都拥有独立的超时执行配额与关联主 turn signal 的隔离取消控制器,防止云端超时耗尽本地恢复时间。同时严格检测终止错误与流中断,确保安全容灾且杜绝已取消 turn 的延迟文件提升。
package/lib/index.js CHANGED
@@ -36,15 +36,18 @@ export function apply(ctx, config) {
36
36
  if (ctx.logger) setLogger(ctx.logger)
37
37
  refreshCatalogInBackground()
38
38
 
39
- let localConfig = plainConfig(config ?? {})
40
- const getConfig = () => localConfig
39
+ let localOverrides = null
40
+ const getSourceConfig = () => {
41
+ const liveSource = plainConfig(ctx.config ?? config ?? {})
42
+ return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
43
+ }
41
44
  const live = () => {
42
45
  try {
43
- const raw = getConfig() ?? {}
46
+ const raw = getSourceConfig() ?? {}
44
47
  const validated = plainConfig(Config(plainConfig(raw)))
45
- return plainConfig(validated) ?? localConfig
48
+ return plainConfig(validated) ?? raw
46
49
  } catch {
47
- return localConfig
50
+ return getSourceConfig()
48
51
  }
49
52
  }
50
53
 
@@ -72,17 +75,13 @@ export function apply(ctx, config) {
72
75
 
73
76
  const saveConfig = async (newFields) => {
74
77
  const plain = plainConfig(newFields)
75
- localConfig = { ...localConfig, ...plain }
76
- try {
77
- if (settingsService && typeof settingsService.replace === 'function') {
78
- await settingsService.replace(NS, plain)
79
- } else if (settingsService && typeof settingsService.update === 'function') {
80
- await settingsService.update(NS, plain)
81
- }
82
- } catch (err) {
83
- logger.warn('[dsh-moa] Settings persistence warning:', err?.message || err)
78
+ if (settingsService && typeof settingsService.replace === 'function') {
79
+ await settingsService.replace(NS, plain)
80
+ } else if (settingsService && typeof settingsService.update === 'function') {
81
+ await settingsService.update(NS, plain)
84
82
  }
85
- return localConfig
83
+ localOverrides = { ...(localOverrides || {}), ...plain }
84
+ return live()
86
85
  }
87
86
 
88
87
  const callLlm = createLlmCaller(ctx)
@@ -110,6 +109,8 @@ export function apply(ctx, config) {
110
109
  saveConfig,
111
110
  Config,
112
111
  plainConfig,
112
+ webServerPort: ctx.webServer?.port,
113
+ timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
113
114
  })
114
115
 
115
116
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
@@ -166,14 +167,15 @@ export function apply(ctx, config) {
166
167
  if (userText.startsWith('/moa')) {
167
168
  const cfg = live()
168
169
  if (cfg.enabled !== false) {
169
- const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: cfg.default_preset || 'default' }
170
- const isExplicitPreset = userText.includes('--preset') || userText.includes('-p ')
170
+ const effectiveDefault = cfg.default_preset || 'default'
171
+ const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
172
+ const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
171
173
  if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
172
174
  try {
173
175
  const routed = await resolvePresetForPrompt({
174
176
  prompt: parsed.prompt || userText,
175
177
  presets: cfg.presets || [],
176
- defaultPreset: parsed.presetName || cfg.default_preset || 'default',
178
+ defaultPreset: parsed.presetName || effectiveDefault,
177
179
  routingModel: cfg.smart_routing_model,
178
180
  callLlm,
179
181
  enabled: true,
@@ -185,7 +187,12 @@ export function apply(ctx, config) {
185
187
  // fallback silently
186
188
  }
187
189
  }
188
- const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
190
+ const targetPreset = (cfg.presets || []).find((p) => p.name === (parsed.presetName || effectiveDefault))
191
+ || (!isExplicitPreset ? (cfg.presets || []).find((p) => p.name === effectiveDefault) || cfg.presets?.[0] : null)
192
+ if (!targetPreset) {
193
+ logger.warn(`[dsh-moa] Unknown explicit preset "${parsed.presetName}" in /moa command.`)
194
+ return next()
195
+ }
189
196
  const aggProvider = targetPreset?.aggregator?.provider || ''
190
197
  const aggModel = targetPreset?.aggregator?.model || ''
191
198
  const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
package/lib/moa-budget.js CHANGED
@@ -31,6 +31,7 @@ export function estimateCandidateRunCost(slots = [], prices = {}, promptTokens =
31
31
 
32
32
  /**
33
33
  * Applies budget guardrails before initiating parallel candidate execution.
34
+ * Evaluates candidate runs, peer critique (Round 2) and downstream synthesis phases.
34
35
  */
35
36
  export function applyBudgetGuardrails({
36
37
  references = [],
@@ -39,6 +40,11 @@ export function applyBudgetGuardrails({
39
40
  action = 'trim', // 'trim' | 'abort'
40
41
  prices = {},
41
42
  promptLength = 1000,
43
+ messages = [],
44
+ maxTokens = 2048,
45
+ peerCritiqueEnabled = false,
46
+ aggregator = null,
47
+ judgeModels = [],
42
48
  }) {
43
49
  if (!enabled || typeof maxBudgetUsd !== 'number' || maxBudgetUsd <= 0 || references.length === 0) {
44
50
  return {
@@ -49,15 +55,52 @@ export function applyBudgetGuardrails({
49
55
  }
50
56
  }
51
57
 
52
- const promptTokens = Math.max(50, Math.round(promptLength / 4))
53
- const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens)
58
+ // Calculate full input tokens from userPrompt length + messages context
59
+ let contextChars = Number(promptLength) || 1000
60
+ if (Array.isArray(messages) && messages.length > 0) {
61
+ for (const m of messages) {
62
+ if (typeof m?.content === 'string') {
63
+ contextChars += m.content.length
64
+ } else if (Array.isArray(m?.content)) {
65
+ for (const part of m.content) {
66
+ if (part?.text) contextChars += part.text.length
67
+ }
68
+ }
69
+ }
70
+ }
71
+ const promptTokens = Math.max(50, Math.round(contextChars / 4))
72
+ const expectedOutputTokens = typeof maxTokens === 'number' && maxTokens > 0 ? maxTokens : 2048
73
+
74
+ // Base candidate cost per slot (Round 1)
75
+ const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens, expectedOutputTokens)
76
+
77
+ // Estimate fixed cost for downstream phases (Aggregator / Judge panel)
78
+ let downstreamUsd = 0
79
+ if (aggregator?.provider && aggregator?.model) {
80
+ const aggInput = promptTokens + expectedOutputTokens * references.length
81
+ const aggCost = estimateTokenCost(aggregator, { inputTokens: aggInput, outputTokens: expectedOutputTokens }, prices)
82
+ downstreamUsd += aggCost?.costUsd || 0
83
+ }
84
+ if (Array.isArray(judgeModels) && judgeModels.length > 0) {
85
+ for (const jm of judgeModels) {
86
+ if (jm?.provider && jm?.model) {
87
+ const jInput = promptTokens + expectedOutputTokens * references.length
88
+ const jCost = estimateTokenCost(jm, { inputTokens: jInput, outputTokens: expectedOutputTokens }, prices)
89
+ downstreamUsd += jCost?.costUsd || 0
90
+ }
91
+ }
92
+ }
93
+
94
+ // Multiplier if peer critique (Round 2) is enabled: each candidate runs another turn with input from all peers
95
+ const roundMultiplier = peerCritiqueEnabled ? 2 : 1
96
+ const fullEstimatedUsd = Number(((totalEstimatedUsd * roundMultiplier) + downstreamUsd).toFixed(6))
54
97
 
55
- if (totalEstimatedUsd <= maxBudgetUsd) {
98
+ if (fullEstimatedUsd <= maxBudgetUsd) {
56
99
  return {
57
100
  allowed: true,
58
101
  references,
59
102
  action: 'pass',
60
- estimatedCostUsd: totalEstimatedUsd,
103
+ estimatedCostUsd: fullEstimatedUsd,
61
104
  }
62
105
  }
63
106
 
@@ -66,9 +109,9 @@ export function applyBudgetGuardrails({
66
109
  allowed: false,
67
110
  references: [],
68
111
  action: 'abort',
69
- estimatedCostUsd: totalEstimatedUsd,
112
+ estimatedCostUsd: fullEstimatedUsd,
70
113
  maxBudgetUsd,
71
- reason: `Estimated MoA run cost ($${totalEstimatedUsd.toFixed(4)}) exceeds configured max budget ($${maxBudgetUsd.toFixed(4)}).`,
114
+ reason: `Estimated MoA run cost ($${fullEstimatedUsd.toFixed(4)}) exceeds configured max budget ($${maxBudgetUsd.toFixed(4)}).`,
72
115
  }
73
116
  }
74
117
 
@@ -78,9 +121,23 @@ export function applyBudgetGuardrails({
78
121
  let cumulative = 0
79
122
 
80
123
  for (const item of sorted) {
81
- if (cumulative + item.costUsd <= maxBudgetUsd || kept.length === 0) {
124
+ const candidateCost = item.costUsd * roundMultiplier
125
+ if (cumulative + candidateCost <= maxBudgetUsd) {
82
126
  kept.push(item.slot)
83
- cumulative += item.costUsd
127
+ cumulative += candidateCost
128
+ }
129
+ }
130
+
131
+ if (kept.length === 0) {
132
+ return {
133
+ allowed: false,
134
+ references: [],
135
+ action: 'trim',
136
+ originalCount: references.length,
137
+ trimmedCount: 0,
138
+ estimatedCostUsd: Number((sorted[0]?.costUsd * roundMultiplier || 0).toFixed(6)),
139
+ maxBudgetUsd,
140
+ reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${((sorted[0]?.costUsd || 0) * roundMultiplier).toFixed(4)}.`,
84
141
  }
85
142
  }
86
143
 
package/lib/moa-parser.js CHANGED
@@ -40,7 +40,7 @@ export function parseRecommendedAssembler(judgeText, defaultIndex = 1, candidate
40
40
  /**
41
41
  * Parses a `/moa [preset] <prompt>` command invocation.
42
42
  */
43
- export function parseMoACommand(text, presets = []) {
43
+ export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
44
44
  if (typeof text !== 'string' || !text.startsWith('/moa')) {
45
45
  return null
46
46
  }
@@ -59,8 +59,9 @@ export function parseMoACommand(text, presets = []) {
59
59
  const remainder = text.slice(4).trim()
60
60
  if (!remainder) {
61
61
  return {
62
- presetName: 'default',
62
+ presetName: defaultPreset,
63
63
  prompt: '',
64
+ isExplicit: false,
64
65
  }
65
66
  }
66
67
 
@@ -69,6 +70,16 @@ export function parseMoACommand(text, presets = []) {
69
70
  return {
70
71
  presetName: matchPreset[1],
71
72
  prompt: matchPreset[2] || '',
73
+ isExplicit: true,
74
+ }
75
+ }
76
+
77
+ const matchShortPreset = /^-p\s+([a-zA-Z0-9_-]+)\s*(.*)/s.exec(remainder)
78
+ if (matchShortPreset) {
79
+ return {
80
+ presetName: matchShortPreset[1],
81
+ prompt: matchShortPreset[2] || '',
82
+ isExplicit: true,
72
83
  }
73
84
  }
74
85
 
@@ -80,13 +91,15 @@ export function parseMoACommand(text, presets = []) {
80
91
  return {
81
92
  presetName: matched.name,
82
93
  prompt: parts.slice(1).join(' ').trim(),
94
+ isExplicit: true,
83
95
  }
84
96
  }
85
97
  }
86
98
 
87
99
  return {
88
- presetName: 'default',
100
+ presetName: defaultPreset,
89
101
  prompt: remainder,
102
+ isExplicit: false,
90
103
  }
91
104
  }
92
105
 
package/lib/moa-runner.js CHANGED
@@ -34,25 +34,11 @@ export async function runMoAPipeline({
34
34
  liveCanvas = null,
35
35
  onStreamDelta = null,
36
36
  signal = null,
37
- testGateEnabled = null,
38
- testCommand = null,
39
- execFn = null,
40
- webServerPort = null,
41
- timeMachineEngine = null,
42
- timeMachineUrl = null,
43
- force = isTestEnvironment() ? true : false,
44
- checkpoint = true,
45
- checkpointFn = null,
46
- runId: customRunId = null,
37
+ testGateEnabled = null, testCommand = null, execFn = null,
38
+ webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
39
+ force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
47
40
  }) {
48
- const promoteOpts = {
49
- webServerPort,
50
- timeMachineEngine,
51
- timeMachineUrl,
52
- force: Boolean(force),
53
- checkpoint,
54
- checkpointFn,
55
- }
41
+ const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn }
56
42
  if (signal?.aborted) return { error: new Error('Turn aborted') }
57
43
  const startTime = Date.now()
58
44
  // 1. Resolve configurations
@@ -81,6 +67,11 @@ export async function runMoAPipeline({
81
67
  action: preset?.budget_action || 'trim',
82
68
  prices,
83
69
  promptLength: userPrompt?.length || 1000,
70
+ messages,
71
+ maxTokens,
72
+ peerCritiqueEnabled: isPeerCritiqueEnabled,
73
+ aggregator: primaryJudge,
74
+ judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
84
75
  })
85
76
  if (!budgetCheck.allowed) {
86
77
  return {
@@ -358,7 +349,7 @@ export async function runMoAPipeline({
358
349
  }))
359
350
  const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
360
351
  try {
361
- const slot = referenceModels[cand.index - 1] || { provider: cand.provider, model: cand.model }
352
+ const slot = cand.slot || { provider: cand.provider, model: cand.model }
362
353
  const res = await callWithTransientRetry(callLlm, {
363
354
  provider: slot.provider,
364
355
  model: slot.model,
@@ -523,7 +514,7 @@ export async function runMoAPipeline({
523
514
  const durationMs = Date.now() - startTime
524
515
  const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
525
516
  const winningRef = referenceOutputs[winningIndex - 1]
526
- const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
517
+ const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
527
518
  const finalAggLabel = slotLabel(chosenJudge)
528
519
 
529
520
  // Feature 9: Benchmark & Post-Mortem PR report
package/lib/routes.js CHANGED
@@ -40,7 +40,7 @@ function readBody(req) {
40
40
  })
41
41
  }
42
42
 
43
- export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig }) {
43
+ export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine }) {
44
44
 
45
45
  ctx.effect(() => {
46
46
  return ctx.webServer.register({
@@ -220,7 +220,12 @@ ctx.effect(() => {
220
220
 
221
221
  // Persist via saveConfig (in-memory and DSH profile settings)
222
222
  if (typeof saveConfig === 'function') {
223
- await saveConfig(validated)
223
+ try {
224
+ await saveConfig(validated)
225
+ } catch (perr) {
226
+ writeJson(res, 500, { ok: false, error: 'Settings persistence failed: ' + (perr?.message || String(perr)) })
227
+ return
228
+ }
224
229
  }
225
230
 
226
231
  const unwrapped = typeof plainConfig === 'function' ? plainConfig(validated) : validated
@@ -563,13 +568,12 @@ ctx.effect(() => {
563
568
  const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
564
569
 
565
570
  const result = await runMoAPipeline({
566
- userPrompt: prompt,
567
- messages: payload.messages || [],
568
- preset,
569
- callLlm,
570
- cwd: payload.cwd || process.cwd(),
571
- prices: cfg.prices || {},
572
- liveCanvas,
571
+ userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
572
+ cwd: payload.cwd || process.cwd(), prices: cfg.prices || {}, liveCanvas,
573
+ webServerPort: webServerPort || ctx.webServer?.port,
574
+ timeMachineEngine: timeMachineEngine || ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
575
+ timeMachineUrl: payload.timeMachineUrl, force: payload.force,
576
+ checkpoint: payload.checkpoint, checkpointFn: payload.checkpointFn,
573
577
  })
574
578
 
575
579
  if (result?.skippedFiles > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.38",
3
+ "version": "0.2.39",
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",