@goodandready/dsh-moa 0.2.42 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -101,6 +101,8 @@ window.__ModuleLoader__.load({
101
101
  'proposers.add_btn': '+ Add Candidate Model',
102
102
  'proposers.temp_label': 'Candidates Temperature',
103
103
  'proposers.temp_hint': 'Recommended: 0.6 – 0.8 for creative, independent solution exploration.',
104
+ 'proposers.temperature_gradient': 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)',
105
+ 'proposers.candidate_temp_tooltip': 'Candidate Temperature (leave empty for default/gradient)',
104
106
  'stats.title': '📈 MoA Analytics & Telemetry',
105
107
  'stats.desc': 'Session telemetry, historical runs, and model win-rate tracking.',
106
108
  'stats.total_runs': 'Total Runs',
@@ -277,6 +279,8 @@ window.__ModuleLoader__.load({
277
279
  'proposers.add_btn': '+ 添加候选模型',
278
280
  'proposers.temp_label': '候选模型采样温度 (Temperature)',
279
281
  'proposers.temp_hint': '推荐:0.6 – 0.8,鼓励创造性、发散性的独立探索。',
282
+ 'proposers.temperature_gradient': '温度梯度探索(候选模型采样温度从 0.2 到 0.9 递增)',
283
+ 'proposers.candidate_temp_tooltip': '候选模型采样温度(留空使用默认/梯度)',
280
284
  'stats.title': '📈 MoA 数据分析与遥测',
281
285
  'stats.desc': '会话遥测、历史运行追踪及模型胜率统计。',
282
286
  'stats.total_runs': '总运行次数',
@@ -1962,7 +1966,7 @@ window.__ModuleLoader__.load({
1962
1966
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, temperature_gradient_enabled: e.target.checked })),
1963
1967
  style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1964
1968
  }),
1965
- 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
1969
+ t('proposers.temperature_gradient') || 'Temperature Gradient Exploration (0.2 → 0.9 across candidates)'
1966
1970
  )
1967
1971
  ),
1968
1972
  React.createElement(
@@ -1990,7 +1994,7 @@ window.__ModuleLoader__.load({
1990
1994
  min: '0',
1991
1995
  max: '2',
1992
1996
  placeholder: 'Temp',
1993
- title: 'Candidate Temperature (leave empty for default/gradient)',
1997
+ title: t('proposers.candidate_temp_tooltip') || 'Candidate Temperature (leave empty for default/gradient)',
1994
1998
  className: 'moa-input',
1995
1999
  style: { width: 50, height: 32, fontSize: 12, padding: '0 4px', textAlign: 'center' },
1996
2000
  value: (typeof ref.temperature === 'number' && ref.temperature >= 0) ? ref.temperature : '',
@@ -2612,13 +2616,14 @@ window.__ModuleLoader__.load({
2612
2616
  }
2613
2617
  render() {
2614
2618
  if (this.state.hasError) {
2619
+ const t = typeof this.props?.t === 'function' ? this.props.t : (typeof makeT === 'function' ? makeT(en, null) : ((k) => k))
2615
2620
  return React.createElement(
2616
2621
  'div',
2617
2622
  {
2618
2623
  className: 'moa-alert-err',
2619
2624
  style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
2620
2625
  },
2621
- React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ MoA UI Error:'),
2626
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, t('error_boundary.title') || '⚠️ MoA UI Error:'),
2622
2627
  React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
2623
2628
  React.createElement(
2624
2629
  'button',
@@ -2628,7 +2633,7 @@ window.__ModuleLoader__.load({
2628
2633
  style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
2629
2634
  onClick: () => this.setState({ hasError: false, error: null }),
2630
2635
  },
2631
- 'Retry'
2636
+ t('error_boundary.retry') || 'Retry'
2632
2637
  )
2633
2638
  )
2634
2639
  }
@@ -2675,12 +2680,12 @@ window.__ModuleLoader__.load({
2675
2680
  'div',
2676
2681
  { className: 'moa-body' },
2677
2682
  state.status === 'loading'
2678
- ? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, 'Loading MoA settings…')
2683
+ ? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, state.t('status.loading') || 'Loading MoA settings…')
2679
2684
  : state.status === 'unavailable'
2680
2685
  ? React.createElement(
2681
2686
  'div',
2682
2687
  { style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 10 } },
2683
- React.createElement('div', { className: 'moa-alert-err' }, 'MoA service temporarily unavailable'),
2688
+ React.createElement('div', { className: 'moa-alert-err' }, state.t('status.unavailable') || 'MoA service temporarily unavailable'),
2684
2689
  React.createElement(
2685
2690
  'button',
2686
2691
  {
@@ -2689,7 +2694,7 @@ window.__ModuleLoader__.load({
2689
2694
  style: { width: 'fit-content' },
2690
2695
  onClick: state.reload,
2691
2696
  },
2692
- state.t('actions.retry')
2697
+ state.t('status.retry') || state.t('actions.retry')
2693
2698
  )
2694
2699
  )
2695
2700
  : React.createElement(MoAEditor, state)
@@ -2738,7 +2743,7 @@ window.__ModuleLoader__.load({
2738
2743
  locale: NS,
2739
2744
  inject: () => ({ ctx }),
2740
2745
  },
2741
- (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
2746
+ (props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
2742
2747
  )
2743
2748
  } catch (err) {
2744
2749
  console.warn('[dsh-moa] Failed to register ' + slotName + ':', err)
@@ -2761,7 +2766,7 @@ window.__ModuleLoader__.load({
2761
2766
  locale: NS,
2762
2767
  inject: () => ({ ctx }),
2763
2768
  },
2764
- (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
2769
+ (props) => React.createElement(MoAErrorBoundary, props, React.createElement(MoACard, props))
2765
2770
  )
2766
2771
  } catch (err) {
2767
2772
  console.warn('[dsh-moa] Failed to register plugins.item:', err)
@@ -78,7 +78,17 @@ export function resolveCandidateFolder(baseDir, candidateTarget, options = {}) {
78
78
  const sub = /^\d+$/.test(sanitizedTarget) ? `candidate-${sanitizedTarget}` : (sanitizedTarget || 'candidate-1')
79
79
  const rawRunId = typeof options === 'string' ? options : options?.runId
80
80
  const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
81
- const moaRoot = path.join(baseDir, '.moa')
81
+ const moaPath = path.resolve(baseDir, '.moa')
82
+ try {
83
+ const lst = fsSync.lstatSync(moaPath)
84
+ if (lst.isSymbolicLink()) {
85
+ const realMoa = fsSync.realpathSync(moaPath), realBase = fsSync.realpathSync(path.resolve(baseDir))
86
+ if (realMoa !== realBase && !realMoa.startsWith(realBase + path.sep)) fsSync.unlinkSync(moaPath)
87
+ }
88
+ } catch (err) {
89
+ if (err?.code !== 'ENOENT') throw err
90
+ }
91
+ const moaRoot = assertPathContained(baseDir, '.moa')
82
92
  const runRoot = safeRunId ? assertPathContained(moaRoot, safeRunId) : moaRoot
83
93
  return { moaRoot, runRoot, safeRunId, sub, candidateDir: assertPathContained(runRoot, sub) }
84
94
  }
@@ -97,18 +107,16 @@ export function extractFileBlocks(text) {
97
107
  }
98
108
  }
99
109
 
100
- const fenceRegex = /```[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)```/gi
101
- let match
102
- while ((match = fenceRegex.exec(text)) !== null) addFile(match[1].trim(), match[2])
103
-
104
- const commentRegex = /(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi
105
- while ((match = commentRegex.exec(text)) !== null) addFile(match[1].trim(), match[2])
106
-
107
- const headerRegex = /#{1,4}\s+(?:File|Path):\s*([^\s\n\r]+)[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi
108
- while ((match = headerRegex.exec(text)) !== null) addFile(match[1].trim(), match[2])
109
-
110
- const inlineRegex = /```[\w-]*[\r\n]+(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+([\s\S]*?)```/gi
111
- while ((match = inlineRegex.exec(text)) !== null) addFile(match[1].trim(), match[2])
110
+ const patterns = [
111
+ /```[\w-]*\s+(?:file|path|filepath)=["']?([^\s"'\n\r]+)["']?[\r\n]+([\s\S]*?)```/gi,
112
+ /(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
113
+ /#{1,4}\s+(?:File|Path):\s*([^\s\n\r]+)[\r\n]+```[\w-]*[\r\n]+([\s\S]*?)```/gi,
114
+ /```[\w-]*[\r\n]+(?:\/\/|#|<!--)\s*(?:File|Path):\s*([^\s\n\r>]+)(?:\s*-->)?[\r\n]+([\s\S]*?)```/gi,
115
+ ]
116
+ for (const regex of patterns) {
117
+ let match
118
+ while ((match = regex.exec(text)) !== null) addFile(match[1].trim(), match[2])
119
+ }
112
120
 
113
121
  if (files.length === 0) {
114
122
  const htmlBlockRegex = /```(?:html)?[\r\n]+([\s\S]*?)```/gi
@@ -211,15 +219,12 @@ export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
211
219
  * Checks if a task prompt is an iterative refinement / modification.
212
220
  */
213
221
  export function isRefinementTask(userPrompt = '', projectFiles = null) {
214
- if (Array.isArray(projectFiles) && projectFiles.length === 0) return false
222
+ if ((Array.isArray(projectFiles) && projectFiles.length === 0) || !userPrompt?.trim()) return false
215
223
  const p = userPrompt.toLowerCase().trim()
216
- if (!p) return false
217
-
218
- const freshPhrases = ['from scratch', 'new project', 'new app', 'create a new', 'build a new', 'generate a new', '从头开始', '新建项目', '创建项目', '新应用', '新建应用', 'с нуля', 'новый проект', 'новое приложение', 'создай проект', 'создай приложение', 'создай игру', 'создай сервис', 'сделай проект', 'сделай приложение', 'сделай игру']
219
- if (freshPhrases.some((phrase) => p.includes(phrase))) return false
220
-
221
- const modKeywords = ['add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust', 'style', 'css', 'color', 'theme', 'bug', '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题', 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали', 'темн', 'светл', 'цвет', 'кнопк', 'баг']
222
- return modKeywords.some((kw) => p.includes(kw))
224
+ const fresh = ['from scratch', 'new project', 'new app', 'create a new', 'build a new', 'generate a new', '从头开始', '新建项目', '创建项目', '新应用', '新建应用', 'с нуля', 'новый проект', 'новое приложение', 'создай проект', 'создай приложение', 'создай игру', 'создай сервис', 'сделай проект', 'сделай приложение', 'сделай игру']
225
+ if (fresh.some((phrase) => p.includes(phrase))) return false
226
+ const mod = ['add', 'change', 'update', 'fix', 'modify', 'refactor', 'improve', 'enhance', 'adjust', 'style', 'css', 'color', 'theme', 'bug', '添加', '修改', '更新', '修复', '重构', '改进', '调整', '样式', '颜色', '主题', 'добавь', 'измени', 'поменяй', 'исправь', 'обнови', 'переделай', 'доработай', 'удали', 'темн', 'светл', 'цвет', 'кнопк', 'баг']
227
+ return mod.some((kw) => p.includes(kw))
223
228
  }
224
229
 
225
230
  /**
@@ -344,6 +349,9 @@ async function doPromoteCandidateWorkspace(baseDir, winningIndex, options = { ke
344
349
  logger.warn(errMsg)
345
350
  throw new Error(errMsg)
346
351
  }
352
+ if (options.signal?.aborted) {
353
+ throw (options.signal.reason instanceof Error ? options.signal.reason : new Error('Turn aborted'))
354
+ }
347
355
 
348
356
  const promoted = []
349
357
  const baseReal = await fs.realpath(baseDir).catch(() => path.resolve(baseDir))
@@ -392,11 +400,9 @@ export async function cleanMoaWorkspaces(baseDir, options = {}) {
392
400
  const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
393
401
  const moaRoot = path.join(baseDir, '.moa')
394
402
  if (safeRunId) {
395
- const runRoot = path.join(moaRoot, safeRunId)
396
- await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(runRoot, { recursive: true, force: true }))
403
+ await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(path.join(moaRoot, safeRunId), { recursive: true, force: true }))
397
404
  await bestEffort('clean empty moaRoot', async () => {
398
- const remaining = await fs.readdir(moaRoot)
399
- if (remaining.length === 0) await fs.rmdir(moaRoot)
405
+ if ((await fs.readdir(moaRoot)).length === 0) await fs.rmdir(moaRoot)
400
406
  })
401
407
  } else {
402
408
  await bestEffort('cleanMoaWorkspaces', () => fs.rm(moaRoot, { recursive: true, force: true }))
@@ -563,33 +569,24 @@ export function computeLineDiff(oldText = '', newText = '') {
563
569
  const subN = subOld.length, subM = subNew.length
564
570
  const middleItems = []
565
571
 
566
- if (subN === 0) {
567
- for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
568
- } else if (subM === 0) {
569
- for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
570
- } else if (subN > 2000 || subM > 2000) {
572
+ if (subN === 0) for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
573
+ else if (subM === 0) for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
574
+ else if (subN > 2000 || subM > 2000) {
571
575
  for (let i = 0; i < subN; i++) middleItems.push({ type: 'removed', line: subOld[i], oldNum: start + i + 1 })
572
576
  for (let j = 0; j < subM; j++) middleItems.push({ type: 'added', line: subNew[j], newNum: start + j + 1 })
573
577
  } else {
574
578
  const dp = Array.from({ length: subN + 1 }, () => new Uint16Array(subM + 1))
575
579
  for (let i = 0; i < subN; i++) {
576
- for (let j = 0; j < subM; j++) {
577
- dp[i + 1][j + 1] = subOld[i] === subNew[j] ? dp[i][j] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
578
- }
580
+ for (let j = 0; j < subM; j++) dp[i + 1][j + 1] = subOld[i] === subNew[j] ? dp[i][j] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
579
581
  }
580
-
581
- let i = subN, j = subM
582
- const stack = []
582
+ let i = subN, j = subM; const stack = []
583
583
  while (i > 0 || j > 0) {
584
584
  if (i > 0 && j > 0 && subOld[i - 1] === subNew[j - 1]) {
585
- stack.push({ type: 'same', line: subOld[i - 1], oldNum: start + i, newNum: start + j })
586
- i--; j--
585
+ stack.push({ type: 'same', line: subOld[--i], oldNum: start + i + 1, newNum: start + j }); j--
587
586
  } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
588
- stack.push({ type: 'added', line: subNew[j - 1], newNum: start + j })
589
- j--
590
- } else if (i > 0 && (j === 0 || dp[i][j - 1] < dp[i - 1][j])) {
591
- stack.push({ type: 'removed', line: subOld[i - 1], oldNum: start + i })
592
- i--
587
+ stack.push({ type: 'added', line: subNew[--j], newNum: start + j + 1 })
588
+ } else if (i > 0) {
589
+ stack.push({ type: 'removed', line: subOld[--i], oldNum: start + i + 1 })
593
590
  }
594
591
  }
595
592
  middleItems.push(...stack.reverse())
package/lib/history.js CHANGED
@@ -358,7 +358,13 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
358
358
  })
359
359
  .filter(Boolean)
360
360
 
361
- let total = _historyCache?.totalCount
361
+ let total = (_historyCache &&
362
+ _historyCache.filePath === filePath &&
363
+ _historyCache.mtimeMs === stat.mtimeMs &&
364
+ _historyCache.size === stat.size &&
365
+ typeof _historyCache.totalCount === 'number')
366
+ ? _historyCache.totalCount
367
+ : undefined
362
368
  if (typeof total !== 'number') {
363
369
  total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
364
370
  }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto'
1
2
  import path from 'node:path'
2
3
  import { setLogger, logger } from './logger.js'
3
4
  import { refreshCatalogInBackground } from './pricing.js'
@@ -19,6 +20,7 @@ import { resolvePresetForPrompt } from './moa-router.js'
19
20
  import { createLiveCanvasClient } from './live-canvas.js'
20
21
  import { registerPluginUpdater } from './updater.js'
21
22
  import { bestEffort } from './best-effort.js'
23
+ import { getMoaHistory } from './history.js'
22
24
 
23
25
  export const name = '@goodandready/dsh-moa'
24
26
  export { collectProjectContext, formatProjectContext } from './file-workspace.js'
@@ -39,12 +41,24 @@ export function apply(ctx, config) {
39
41
  refreshCatalogInBackground()
40
42
 
41
43
  let localOverrides = null
44
+ let baseAtOverride = {}
42
45
  const getSourceConfig = () => {
43
46
  let liveCandidate = ctx.fiber?.config
44
47
  if (!liveCandidate && ('config' in ctx)) {
45
48
  bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
46
49
  }
47
50
  const liveSource = plainConfig(liveCandidate ?? config ?? {})
51
+ if (localOverrides) {
52
+ for (const key of Object.keys(localOverrides)) {
53
+ if (baseAtOverride[key] !== undefined && JSON.stringify(liveSource[key]) !== baseAtOverride[key]) {
54
+ delete localOverrides[key]
55
+ delete baseAtOverride[key]
56
+ }
57
+ }
58
+ if (Object.keys(localOverrides).length === 0) {
59
+ localOverrides = null
60
+ }
61
+ }
48
62
  return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
49
63
  }
50
64
  const live = () => {
@@ -86,6 +100,15 @@ export function apply(ctx, config) {
86
100
  } else if (settingsService && typeof settingsService.update === 'function') {
87
101
  await settingsService.update(NS, plain)
88
102
  }
103
+ let liveCandidate = ctx.fiber?.config
104
+ if (!liveCandidate && ('config' in ctx)) {
105
+ bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
106
+ }
107
+ const currentLive = plainConfig(liveCandidate ?? config ?? {})
108
+ baseAtOverride = baseAtOverride || {}
109
+ for (const key of Object.keys(plain)) {
110
+ baseAtOverride[key] = JSON.stringify(currentLive[key])
111
+ }
89
112
  localOverrides = { ...(localOverrides || {}), ...plain }
90
113
  return live()
91
114
  }
@@ -176,10 +199,15 @@ export function apply(ctx, config) {
176
199
  const effectiveDefault = cfg.default_preset || 'default'
177
200
  const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
178
201
  if (parsed.isPromote) {
202
+ let promoteRunId = parsed.runId
203
+ if (!promoteRunId) {
204
+ const latestRun = getMoaHistory(1, 0)?.runs?.[0]
205
+ if (latestRun?.id) promoteRunId = latestRun.id
206
+ }
179
207
  const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
180
208
  const turnContext = {
181
209
  isPromoteTurn: true,
182
- runId: parsed.runId,
210
+ runId: promoteRunId,
183
211
  candidateIndex: parsed.candidateIndex,
184
212
  cwd: sessCwd,
185
213
  webServerPort: ctx.webServer?.port,
@@ -192,11 +220,14 @@ export function apply(ctx, config) {
192
220
  const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
193
221
  if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
194
222
  try {
223
+ const activePreset = (cfg.presets || []).find((p) => p.name === (parsed.presetName || effectiveDefault)) || cfg.presets?.[0]
224
+ const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
225
+ || (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
195
226
  const routed = await resolvePresetForPrompt({
196
227
  prompt: parsed.prompt || userText,
197
228
  presets: cfg.presets || [],
198
229
  defaultPreset: parsed.presetName || effectiveDefault,
199
- routingModel: cfg.smart_routing_model,
230
+ routingModel,
200
231
  callLlm,
201
232
  enabled: true,
202
233
  })
@@ -216,8 +247,10 @@ export function apply(ctx, config) {
216
247
  const aggProvider = targetPreset?.aggregator?.provider || ''
217
248
  const aggModel = targetPreset?.aggregator?.model || ''
218
249
  const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
250
+ const runId = crypto.randomUUID()
219
251
 
220
252
  const turnContext = {
253
+ runId,
221
254
  targetPreset,
222
255
  userPrompt: parsed.prompt || userText,
223
256
  messages: messages.slice(0, -1),
package/lib/moa-budget.js CHANGED
@@ -74,25 +74,29 @@ export function applyBudgetGuardrails({
74
74
  // Base candidate cost per slot (Round 1)
75
75
  const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens, expectedOutputTokens)
76
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
77
+ // Estimate cost for downstream phases (Aggregator / Judge panel)
78
+ function calcDownstreamUsd(numCandidates) {
79
+ let downstream = 0
80
+ if (aggregator?.provider && aggregator?.model) {
81
+ const aggInput = promptTokens + expectedOutputTokens * numCandidates
82
+ const aggCost = estimateTokenCost(aggregator, { inputTokens: aggInput, outputTokens: expectedOutputTokens }, prices)
83
+ downstream += aggCost?.costUsd || 0
84
+ }
85
+ if (Array.isArray(judgeModels) && judgeModels.length > 0) {
86
+ for (const jm of judgeModels) {
87
+ if (jm?.provider && jm?.model) {
88
+ const jInput = promptTokens + expectedOutputTokens * numCandidates
89
+ const jCost = estimateTokenCost(jm, { inputTokens: jInput, outputTokens: expectedOutputTokens }, prices)
90
+ downstream += jCost?.costUsd || 0
91
+ }
90
92
  }
91
93
  }
94
+ return downstream
92
95
  }
93
96
 
94
97
  // Multiplier if peer critique (Round 2) is enabled: each candidate runs another turn with input from all peers
95
98
  const roundMultiplier = peerCritiqueEnabled ? 2 : 1
99
+ const downstreamUsd = calcDownstreamUsd(references.length)
96
100
  const fullEstimatedUsd = Number(((totalEstimatedUsd * roundMultiplier) + downstreamUsd).toFixed(6))
97
101
 
98
102
  if (fullEstimatedUsd <= maxBudgetUsd) {
@@ -116,38 +120,43 @@ export function applyBudgetGuardrails({
116
120
  }
117
121
 
118
122
  // Action is 'trim': Sort by estimated cost ascending (cheapest first)
123
+ // Factor downstream synthesis cost into candidate selection (#158)
119
124
  const sorted = [...slotEstimates].sort((a, b) => a.costUsd - b.costUsd)
120
125
  const kept = []
121
- let cumulative = 0
126
+ let cumulativeCandidateCost = 0
122
127
 
123
128
  for (const item of sorted) {
124
129
  const candidateCost = item.costUsd * roundMultiplier
125
- if (cumulative + candidateCost <= maxBudgetUsd) {
130
+ const nextDownstream = calcDownstreamUsd(kept.length + 1)
131
+ if (cumulativeCandidateCost + candidateCost + nextDownstream <= maxBudgetUsd) {
126
132
  kept.push(item.slot)
127
- cumulative += candidateCost
133
+ cumulativeCandidateCost += candidateCost
128
134
  }
129
135
  }
130
136
 
131
137
  if (kept.length === 0) {
138
+ const minCandidateCost = (sorted[0]?.costUsd || 0) * roundMultiplier
139
+ const minTotal = minCandidateCost + calcDownstreamUsd(1)
132
140
  return {
133
141
  allowed: false,
134
142
  references: [],
135
143
  action: 'trim',
136
144
  originalCount: references.length,
137
145
  trimmedCount: 0,
138
- estimatedCostUsd: Number((sorted[0]?.costUsd * roundMultiplier || 0).toFixed(6)),
146
+ estimatedCostUsd: Number(minTotal.toFixed(6)),
139
147
  maxBudgetUsd,
140
- reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${((sorted[0]?.costUsd || 0) * roundMultiplier).toFixed(4)}.`,
148
+ reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${minTotal.toFixed(4)}.`,
141
149
  }
142
150
  }
143
151
 
152
+ const finalEstimatedUsd = Number((cumulativeCandidateCost + calcDownstreamUsd(kept.length)).toFixed(6))
144
153
  return {
145
154
  allowed: true,
146
155
  references: kept,
147
156
  action: 'trim',
148
157
  originalCount: references.length,
149
158
  trimmedCount: kept.length,
150
- estimatedCostUsd: Number(cumulative.toFixed(6)),
159
+ estimatedCostUsd: finalEstimatedUsd,
151
160
  maxBudgetUsd,
152
161
  reason: `Trimmed candidate pool from ${references.length} to ${kept.length} models to stay within budget ($${maxBudgetUsd.toFixed(4)}).`,
153
162
  }
@@ -122,9 +122,11 @@ export async function runReferencesParallel(references, messages, options = {},
122
122
  })
123
123
  reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
124
124
  }, timeoutMs)
125
+ candidateTimer?.unref?.()
125
126
  })
126
127
 
127
128
  const res = await Promise.race([callPromise, timeoutPromise])
129
+ if (candidateTimer) { clearTimeout(candidateTimer); candidateTimer = null }
128
130
  const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
129
131
 
130
132
  const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
@@ -275,6 +277,7 @@ export async function runReferencesParallel(references, messages, options = {},
275
277
  }
276
278
  }
277
279
  } finally {
280
+ if (candidateTimer) clearTimeout(candidateTimer)
278
281
  notifyFinished()
279
282
  }
280
283
  }
@@ -371,7 +371,8 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
371
371
 
372
372
  const winningCand = activeCandidates.find((c) => c.index === winner) || activeCandidates[0]
373
373
  const winnerVotes = votes[winner] || 0
374
- const consensusReport = `Multi-judge consensus (${strategy}): Candidate ${winner} (${winningCand.label}) won with ${winnerVotes}/${validResults.length} votes.`
374
+ const winnerLabel = blind ? '' : ` (${winningCand.label})`
375
+ const consensusReport = `Multi-judge consensus (${strategy}): Candidate ${winner}${winnerLabel} won with ${winnerVotes}/${validResults.length} votes.`
375
376
 
376
377
  if (typeof onProgress === 'function') {
377
378
  onProgress(`⚖️ *${consensusReport}*\n`)
@@ -302,6 +302,10 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
302
302
  }
303
303
 
304
304
  const isBlind = Boolean(options.blindEvaluation)
305
+ let consensusReportText = options.consensusReport
306
+ if (isBlind && consensusReportText) {
307
+ consensusReportText = consensusReportText.replace(/\s*\([^)]+\)/g, '')
308
+ }
305
309
 
306
310
  const joined = referenceOutputs
307
311
  .map((r, i) => {
@@ -344,7 +348,7 @@ ${LANGUAGE_MIRRORING_DIRECTIVE}
344
348
  ${referenceOutputs.some((r) => r.testResult) ? `\n${TEST_GATE_DIRECTIVE}\n` : ''}
345
349
  ${referenceOutputs.some((r) => r.syntaxWarning) ? `\n${SYNTAX_CORRECTION_DIRECTIVE}\n` : ''}
346
350
  ${options.priorTurnBaseline ? `\n### 🔄 Prior Turn Context Baseline:\n${options.priorTurnBaseline}\n` : ''}
347
- ${options.consensusReport ? `\n### ⚖️ Multi-Judge Consensus Advisory:\n${options.consensusReport}\n` : ''}
351
+ ${consensusReportText ? `\n### ⚖️ Multi-Judge Consensus Advisory:\n${consensusReportText}\n` : ''}
348
352
  ${options.compositeMergeDirective ? `\n${options.compositeMergeDirective}\n` : ''}
349
353
  Instructions:
350
354
  Your response MUST be structured into three clear parts:
package/lib/moa-runner.js CHANGED
@@ -38,7 +38,8 @@ export async function runMoAPipeline({
38
38
  webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
39
39
  force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
40
40
  }) {
41
- const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn }
41
+ const runId = (customRunId && String(customRunId).trim()) || crypto.randomUUID()
42
+ const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn, signal }
42
43
  if (signal?.aborted) return { error: new Error('Turn aborted') }
43
44
  const startTime = Date.now()
44
45
  // 1. Resolve configurations
@@ -58,7 +59,7 @@ export async function runMoAPipeline({
58
59
  const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
59
60
  const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
60
61
  const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
61
- const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = customRunId || null
62
+ const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
62
63
  // Feature 6: Budget Guardrail check
63
64
  const budgetCheck = applyBudgetGuardrails({
64
65
  references: referenceModels,
@@ -275,12 +276,18 @@ export async function runMoAPipeline({
275
276
  return { error: new Error('Turn aborted') }
276
277
  }
277
278
  const single = successfulRefs[0]
278
- let promotedFiles = []
279
+ let promotedFiles = [], promoteErr = null
279
280
  if (single.files?.length > 0) {
280
- promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId })
281
- } else {
281
+ try {
282
+ promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId, keepMoa: allowCandidateOverride })
283
+ } catch (e) { promoteErr = e }
284
+ } else if (!allowCandidateOverride) {
282
285
  await cleanMoaWorkspaces(cwd, { runId })
283
286
  }
287
+ if (signal?.aborted || promoteErr?.message?.includes('aborted') || promoteErr?.name === 'AbortError') {
288
+ if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
289
+ return { error: new Error('Turn aborted') }
290
+ }
284
291
 
285
292
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex: single.index })
286
293
  const totalTokens = single.usage?.totalTokens || 0
@@ -289,44 +296,27 @@ export async function runMoAPipeline({
289
296
 
290
297
  try {
291
298
  await recordMoaRunAsync({
292
- cwd,
293
- prompt: userPrompt,
294
- preset: preset?.name || 'default',
295
- isRefinement,
296
- isFastMode: true,
297
- candidates: candidatesForHistory(referenceOutputs),
298
- aggregator: null,
299
- winnerIndex: single.index,
300
- winnerModel: single.label,
301
- promotedFiles,
302
- totalTokens,
303
- totalCostUsd,
304
- durationMs,
299
+ id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default',
300
+ isRefinement, isFastMode: true, candidates: candidatesForHistory(referenceOutputs),
301
+ aggregator: null, winnerIndex: single.index, winnerModel: single.label,
302
+ promotedFiles, totalTokens, totalCostUsd, durationMs,
305
303
  }, historyFilePath || undefined)
306
304
  } catch (histErr) {
307
305
  logger.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
308
306
  }
309
307
 
310
308
  return {
311
- kind: 'synthesis',
312
- content: single.text,
313
- aggregator: 'Fast Mode (Direct)',
314
- references: referenceOutputs,
315
- presetName: preset?.name || 'default',
316
- isRefinement,
317
- isFastMode: true,
318
- winningIndex: single.index,
319
- winnerModel: single.label,
320
- promotedFiles,
309
+ kind: 'synthesis', content: single.text, aggregator: 'Fast Mode (Direct)',
310
+ references: referenceOutputs, presetName: preset?.name || 'default',
311
+ isRefinement, isFastMode: true, winningIndex: single.index,
312
+ winnerModel: single.label, promotedFiles, runId,
321
313
  ...(livePreview ? { liveCanvas: livePreview } : {}),
322
314
  usage: {
323
- totalTokens,
324
- totalCostUsd,
315
+ totalTokens, totalCostUsd,
325
316
  candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
326
317
  aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
327
318
  },
328
- skippedFiles: collectedCtx?.skippedFiles || 0,
329
- skippedList: collectedCtx?.skippedList || [],
319
+ skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [],
330
320
  durationMs,
331
321
  }
332
322
  }
@@ -502,16 +492,26 @@ export async function runMoAPipeline({
502
492
  let promotedFiles = []
503
493
 
504
494
  const keepWorkspaces = allowCandidateOverride
505
- if (synthesizedFiles.length > 0) {
506
- await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
507
- promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
508
- } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
509
- promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
510
- } else if (successfulRefs[0]?.files?.length > 0) {
511
- const fallbackIdx = successfulRefs[0].index
512
- promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
513
- } else if (!keepWorkspaces) {
514
- await cleanMoaWorkspaces(cwd, { runId })
495
+ if (signal?.aborted) {
496
+ if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
497
+ return { error: new Error('Turn aborted') }
498
+ }
499
+ try {
500
+ if (synthesizedFiles.length > 0) {
501
+ await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
502
+ promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
503
+ } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
504
+ promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
505
+ } else if (successfulRefs[0]?.files?.length > 0) {
506
+ const fallbackIdx = successfulRefs[0].index
507
+ promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
508
+ } else if (!keepWorkspaces) {
509
+ await cleanMoaWorkspaces(cwd, { runId })
510
+ }
511
+ } catch (promoteErr) {
512
+ if (!keepWorkspaces) await cleanMoaWorkspaces(cwd, { runId })
513
+ if (signal?.aborted || promoteErr?.message === 'Turn aborted') return { error: new Error('Turn aborted') }
514
+ throw promoteErr
515
515
  }
516
516
 
517
517
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, { winningIndex, synthesizedFiles })
@@ -582,8 +582,7 @@ export async function runMoAPipeline({
582
582
  consensus: multiJudgeResult,
583
583
  ...(livePreview ? { liveCanvas: livePreview } : {}),
584
584
  usage: usageSummary,
585
- skippedFiles: collectedCtx?.skippedFiles || 0,
586
- skippedList: collectedCtx?.skippedList || [],
585
+ skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [],
587
586
  durationMs,
588
587
  }
589
588
  }
package/lib/moa-stream.js CHANGED
@@ -4,7 +4,7 @@ import crypto from 'node:crypto'
4
4
  */
5
5
 
6
6
  import { cleanMoaWorkspaces, promoteCandidateWorkspace } from './file-workspace.js'
7
- import { isTestEnvironment } from './history.js'
7
+ import { isTestEnvironment, getMoaHistory } from './history.js'
8
8
  import { formatMoAResponse } from './moa-parser.js'
9
9
  import { runMoAPipeline } from './moa-runner.js'
10
10
 
@@ -57,6 +57,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
57
57
  timeMachineEngine,
58
58
  force,
59
59
  signal: combinedSignal,
60
+ runId,
60
61
  })
61
62
  .catch((err) => ({ error: err }))
62
63
  .finally(() => {
@@ -152,10 +153,11 @@ export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServer
152
153
  }
153
154
 
154
155
  try {
155
- yield { type: 'text-delta', index: 0, text: `🔄 *Promoting candidate ${candidateIndex}${runId ? ` for run \`${runId}\`` : ''}...*\n\n` }
156
+ const effectiveRunId = runId || getMoaHistory(1, 0).runs[0]?.id || null
157
+ yield { type: 'text-delta', index: 0, text: `🔄 *Promoting candidate ${candidateIndex}${effectiveRunId ? ` for run \`${effectiveRunId}\`` : ''}...*\n\n` }
156
158
 
157
159
  const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, {
158
- runId,
160
+ runId: effectiveRunId,
159
161
  keepMoa: true,
160
162
  port: webServerPort,
161
163
  timeMachineEngine,
@@ -163,7 +165,7 @@ export async function* streamPromoteTurn({ runId, candidateIndex, cwd, webServer
163
165
  })
164
166
 
165
167
  if (!promotedFiles || promotedFiles.length === 0) {
166
- const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${runId ? ` in run \`${runId}\`` : ''}. Workspace may have expired or run does not exist.`
168
+ const noFilesText = `⚠️ **MoA Promote Notice**: No candidate files found for candidate ${candidateIndex}${effectiveRunId ? ` in run \`${effectiveRunId}\`` : ''}. Workspace may have expired or run does not exist.`
167
169
  yield { type: 'text-delta', index: 0, text: noFilesText }
168
170
  yield { type: 'block-end', index: 0, block: { type: 'text', text: noFilesText } }
169
171
  yield { type: 'finish', reason: { kind: 'stop' } }
@@ -115,12 +115,23 @@ export async function runCandidateTestGate({
115
115
  stderr = customRes?.stderr || ''
116
116
  } else {
117
117
  const nodePath = path.join(cwd, 'node_modules')
118
+ const allowedReads = [
119
+ executionDir,
120
+ cwd,
121
+ ...(candidateDir && candidateDir !== executionDir ? [candidateDir] : []),
122
+ ...(process.platform === 'win32'
123
+ ? [process.env.SYSTEMROOT || 'C:\\Windows', path.dirname(process.execPath)]
124
+ : ['/usr', '/lib', '/lib64', '/etc', path.resolve(path.dirname(process.execPath), '..'), process.env.HOME ? path.join(process.env.HOME, '.npmrc') : null]),
125
+ ].filter(Boolean)
126
+ const permOpts = `--permission ${allowedReads.map((p) => `--allow-fs-read=${p}`).join(' ')} --allow-fs-write=${executionDir} --allow-child-process`
127
+
118
128
  const env = getScrubbedEnv({
119
129
  MOA_CANDIDATE_INDEX: String(candidateIndex),
120
130
  MOA_CANDIDATE_DIR: candidateDir,
121
131
  MOA_BASE_DIR: cwd,
122
132
  ...(runId ? { MOA_RUN_ID: String(runId) } : {}),
123
133
  NODE_PATH: process.env.NODE_PATH ? `${nodePath}:${process.env.NODE_PATH}` : nodePath,
134
+ NODE_OPTIONS: process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ${permOpts}` : permOpts,
124
135
  })
125
136
 
126
137
  const runPromise = new Promise((resolve) => {
package/lib/routes.js CHANGED
@@ -356,9 +356,11 @@ ctx.effect(() => {
356
356
  }
357
357
  try {
358
358
  const u = new URL(req.url, 'http://localhost')
359
- const runId = u.searchParams.get('runId') || undefined
359
+ const rawRunId = u.searchParams.get('runId')
360
+ const latestRun = (!rawRunId || rawRunId === 'latest' || rawRunId === 'active') ? getMoaHistory(1, 0).runs[0] : null
361
+ const runId = (rawRunId && rawRunId !== 'latest' && rawRunId !== 'active') ? rawRunId : latestRun?.id
360
362
  const run = runId ? getMoaRunById(runId) : null
361
- const cwd = run?.cwd || u.searchParams.get('cwd') || process.cwd()
363
+ const cwd = run?.cwd || latestRun?.cwd || u.searchParams.get('cwd') || process.cwd()
362
364
  const sanitizeTarget = (val, def) => {
363
365
  const clean = String(val || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
364
366
  return clean || def
@@ -511,11 +513,14 @@ ctx.effect(() => {
511
513
  const raw = await readBody(req)
512
514
  const body = JSON.parse(raw || '{}')
513
515
  const prompt = body?.prompt || ''
516
+ const activePreset = (cfg.presets || []).find((p) => p.name === (cfg.default_preset || 'default')) || cfg.presets?.[0]
517
+ const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
518
+ || (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
514
519
  const result = await resolvePresetForPrompt({
515
520
  prompt,
516
521
  presets: cfg.presets || [],
517
522
  defaultPreset: cfg.default_preset || 'default',
518
- routingModel: cfg.smart_routing_model,
523
+ routingModel,
519
524
  callLlm,
520
525
  enabled: cfg.enabled !== false,
521
526
  })
@@ -575,6 +580,7 @@ ctx.effect(() => {
575
580
  timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
576
581
  timeMachineUrl: payload.timeMachineUrl, force: payload.force,
577
582
  checkpoint: payload.checkpoint, checkpointFn: payload.checkpointFn,
583
+ runId: payload.runId,
578
584
  })
579
585
 
580
586
  if (result?.skippedFiles > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.42",
3
+ "version": "0.2.43",
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",