@goodandready/dsh-moa 0.2.38 → 0.2.40

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
@@ -17,6 +17,7 @@ import { registerMoaRoutes } from './routes.js'
17
17
  import { resolvePresetForPrompt } from './moa-router.js'
18
18
  import { createLiveCanvasClient } from './live-canvas.js'
19
19
  import { registerPluginUpdater } from './updater.js'
20
+ import { bestEffort } from './best-effort.js'
20
21
 
21
22
  export const name = '@goodandready/dsh-moa'
22
23
  export { collectProjectContext, formatProjectContext } from './file-workspace.js'
@@ -36,15 +37,22 @@ export function apply(ctx, config) {
36
37
  if (ctx.logger) setLogger(ctx.logger)
37
38
  refreshCatalogInBackground()
38
39
 
39
- let localConfig = plainConfig(config ?? {})
40
- const getConfig = () => localConfig
40
+ let localOverrides = null
41
+ const getSourceConfig = () => {
42
+ let liveCandidate = ctx.fiber?.config
43
+ if (!liveCandidate && ('config' in ctx)) {
44
+ bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
45
+ }
46
+ const liveSource = plainConfig(liveCandidate ?? config ?? {})
47
+ return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
48
+ }
41
49
  const live = () => {
42
50
  try {
43
- const raw = getConfig() ?? {}
51
+ const raw = getSourceConfig() ?? {}
44
52
  const validated = plainConfig(Config(plainConfig(raw)))
45
- return plainConfig(validated) ?? localConfig
53
+ return plainConfig(validated) ?? raw
46
54
  } catch {
47
- return localConfig
55
+ return getSourceConfig()
48
56
  }
49
57
  }
50
58
 
@@ -72,17 +80,13 @@ export function apply(ctx, config) {
72
80
 
73
81
  const saveConfig = async (newFields) => {
74
82
  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)
83
+ if (settingsService && typeof settingsService.replace === 'function') {
84
+ await settingsService.replace(NS, plain)
85
+ } else if (settingsService && typeof settingsService.update === 'function') {
86
+ await settingsService.update(NS, plain)
84
87
  }
85
- return localConfig
88
+ localOverrides = { ...(localOverrides || {}), ...plain }
89
+ return live()
86
90
  }
87
91
 
88
92
  const callLlm = createLlmCaller(ctx)
@@ -110,6 +114,8 @@ export function apply(ctx, config) {
110
114
  saveConfig,
111
115
  Config,
112
116
  plainConfig,
117
+ webServerPort: ctx.webServer?.port,
118
+ timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
113
119
  })
114
120
 
115
121
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
@@ -166,14 +172,15 @@ export function apply(ctx, config) {
166
172
  if (userText.startsWith('/moa')) {
167
173
  const cfg = live()
168
174
  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 ')
175
+ const effectiveDefault = cfg.default_preset || 'default'
176
+ const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
177
+ const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
171
178
  if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
172
179
  try {
173
180
  const routed = await resolvePresetForPrompt({
174
181
  prompt: parsed.prompt || userText,
175
182
  presets: cfg.presets || [],
176
- defaultPreset: parsed.presetName || cfg.default_preset || 'default',
183
+ defaultPreset: parsed.presetName || effectiveDefault,
177
184
  routingModel: cfg.smart_routing_model,
178
185
  callLlm,
179
186
  enabled: true,
@@ -185,7 +192,12 @@ export function apply(ctx, config) {
185
192
  // fallback silently
186
193
  }
187
194
  }
188
- const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
195
+ const targetPreset = (cfg.presets || []).find((p) => p.name === (parsed.presetName || effectiveDefault))
196
+ || (!isExplicitPreset ? (cfg.presets || []).find((p) => p.name === effectiveDefault) || cfg.presets?.[0] : null)
197
+ if (!targetPreset) {
198
+ logger.warn(`[dsh-moa] Unknown explicit preset "${parsed.presetName}" in /moa command.`)
199
+ return next()
200
+ }
189
201
  const aggProvider = targetPreset?.aggregator?.provider || ''
190
202
  const aggModel = targetPreset?.aggregator?.model || ''
191
203
  const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
@@ -200,7 +212,7 @@ export function apply(ctx, config) {
200
212
  prices: cfg.prices || {},
201
213
  liveCanvas,
202
214
  webServerPort: ctx.webServer?.port,
203
- timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
215
+ timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
204
216
  }
205
217
 
206
218
  if (event.signal) {
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
@@ -328,7 +333,7 @@ ctx.effect(() => {
328
333
  runId,
329
334
  keepMoa: true,
330
335
  port: ctx.webServer?.port,
331
- timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
336
+ timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
332
337
  force: Boolean(body?.force),
333
338
  })
334
339
  writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
@@ -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.reflect?.get?.('timeMachineEngine', false),
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.40",
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",