@goodandready/dsh-moa 0.2.78 → 0.2.80

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,335 @@
1
+ /**
2
+ * Multi-Round Rebuttal Debate between Candidates (#298)
3
+ *
4
+ * Implements multi-round cross-examination and rebuttal discussion between candidate models
5
+ * before final arbitration by the Judge. Candidates anonymize and analyze peer solutions,
6
+ * identify flaws, security edge-cases, and hallucinations, and refine their own answers.
7
+ * The structured debate protocol is then passed to the Judge for weighted arbitration.
8
+ */
9
+
10
+ import { logger } from '../logger.js'
11
+ import { callWithTransientRetry } from '../moa-candidates.js'
12
+ import { extractFileBlocks, writeCandidateWorkspace } from '../workspace/fs.js'
13
+ import { verifyFileSyntax } from '../workspace/diff.js'
14
+ import { estimateTokenCost } from '../pricing.js'
15
+ import { emitEvent, MOA_EVENTS } from '../events.js'
16
+
17
+ /**
18
+ * Builds the rebuttal prompt for a candidate in a given debate round.
19
+ * @param {object} params
20
+ * @returns {string}
21
+ */
22
+ export function buildRebuttalPrompt({
23
+ userPrompt,
24
+ candidateText,
25
+ opponentAnswers,
26
+ roundNumber = 1,
27
+ totalRounds = 1,
28
+ isBlind = true,
29
+ }) {
30
+ const opponentsFormatted = opponentAnswers
31
+ .map((op, idx) => {
32
+ const label = op.label || `Candidate ${idx + 1}`
33
+ const persona = op.role_persona ? ` [Persona: ${op.role_persona}]` : ''
34
+ return `### ⚔️ Peer Solution [${label}]${persona}:\n${op.text}`
35
+ })
36
+ .join('\n\n')
37
+
38
+ return `You are an expert software engineer participating in a rigorous Multi-Agent Consilium Rebuttal Debate (Round ${roundNumber} of ${totalRounds}).
39
+ Your objective is to stress-test peer solutions, expose bugs, logical vulnerabilities, and hallucinations, and defend and refine your own solution.
40
+
41
+ === ORIGINAL USER REQUEST ===
42
+ ${userPrompt}
43
+
44
+ === YOUR PREVIOUS PROPOSAL ===
45
+ ${candidateText}
46
+
47
+ === ALTERNATIVE PEER SOLUTIONS ===
48
+ ${opponentsFormatted}
49
+
50
+ === REBUTTAL DEBATE INSTRUCTIONS ===
51
+ Analyze the peer solutions with uncompromising technical rigor.
52
+ Structure your response EXACTLY into these three labeled sections:
53
+
54
+ ### CRITIQUE OF PEERS
55
+ For each peer candidate, cite concrete issues:
56
+ - Identify logic bugs, off-by-one errors, uncaught exceptions, or security holes.
57
+ - Highlight missing edge cases or unhandled failure states.
58
+ - Point out inaccurate assumptions or hallucinations.
59
+
60
+ ### REBUTTAL & DEFENSE
61
+ - Address the potential weaknesses or tradeoffs in your own approach.
62
+ - Explain why your architectural or algorithmic choices are superior or more maintainable.
63
+
64
+ ### REFINED SOLUTION
65
+ - Provide your complete, improved, and final solution incorporating valid insights from the debate.
66
+ - Output complete code without placeholders or omissions.`
67
+ }
68
+
69
+ /**
70
+ * Parses structured rebuttal debate output from a model response.
71
+ * @param {string} rawText
72
+ * @returns {{ critiqueText: string, defenseText: string, refinedText: string }}
73
+ */
74
+ export function parseRebuttalResponse(rawText = '') {
75
+ if (typeof rawText !== 'string') {
76
+ return { critiqueText: '', defenseText: '', refinedText: '' }
77
+ }
78
+
79
+ let critiqueText = ''
80
+ let defenseText = ''
81
+ let refinedText = rawText
82
+
83
+ const critiqueMatch = rawText.match(/### CRITIQUE OF PEERS\s*([\s\S]*?)(?=### REBUTTAL & DEFENSE|### REFINED SOLUTION|$)/i)
84
+ if (critiqueMatch) {
85
+ critiqueText = critiqueMatch[1].trim()
86
+ }
87
+
88
+ const defenseMatch = rawText.match(/### REBUTTAL & DEFENSE\s*([\s\S]*?)(?=### REFINED SOLUTION|$)/i)
89
+ if (defenseMatch) {
90
+ defenseText = defenseMatch[1].trim()
91
+ }
92
+
93
+ const refinedMatch = rawText.match(/### REFINED SOLUTION\s*([\s\S]*)$/i)
94
+ if (refinedMatch && refinedMatch[1].trim().length > 50) {
95
+ refinedText = refinedMatch[1].trim()
96
+ }
97
+
98
+ return {
99
+ critiqueText,
100
+ defenseText,
101
+ refinedText,
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Executes a single round of rebuttal debate across all active candidates.
107
+ * @param {object} params
108
+ * @returns {Promise<{ round: number, critiques: Array<object>, updatedCandidates: Array<object> }>}
109
+ */
110
+ export async function executeDebateRound({
111
+ candidates = [],
112
+ userPrompt = '',
113
+ roundNumber = 1,
114
+ totalRounds = 1,
115
+ isBlindEvaluation = true,
116
+ callLlm,
117
+ refTemp = 0.5,
118
+ maxTokens = 4096,
119
+ refTimeoutSec = 60,
120
+ candidateRetries = 2,
121
+ prices = {},
122
+ signal = null,
123
+ onProgress = null,
124
+ cwd = process.cwd(),
125
+ runId = '',
126
+ ctx = null,
127
+ }) {
128
+ if (typeof onProgress === 'function') {
129
+ onProgress(`🗣️ *Rebuttal Debate Round ${roundNumber}/${totalRounds}: Candidates cross-examining peer solutions in parallel...*\n`)
130
+ }
131
+
132
+ const roundCritiques = []
133
+
134
+ const debatePromises = candidates.map(async (cand) => {
135
+ const opponents = candidates
136
+ .filter((c) => c.index !== cand.index)
137
+ .map((c) => ({
138
+ index: c.index,
139
+ label: isBlindEvaluation ? `Candidate ${c.index}` : c.label,
140
+ role_persona: c.role_persona || c.slot?.role_persona,
141
+ text: c.text,
142
+ }))
143
+
144
+ const rebuttalPrompt = buildRebuttalPrompt({
145
+ userPrompt,
146
+ candidateText: cand.text,
147
+ opponentAnswers: opponents,
148
+ roundNumber,
149
+ totalRounds,
150
+ isBlind: isBlindEvaluation,
151
+ })
152
+
153
+ const slot = cand.slot || { provider: cand.provider, model: cand.model }
154
+
155
+ try {
156
+ const res = await callWithTransientRetry(
157
+ callLlm,
158
+ {
159
+ provider: slot.provider,
160
+ model: slot.model,
161
+ messages: [{ role: 'user', content: rebuttalPrompt }],
162
+ temperature: refTemp,
163
+ maxTokens,
164
+ timeoutMs: refTimeoutSec * 1000,
165
+ signal,
166
+ onProgress,
167
+ },
168
+ candidateRetries
169
+ )
170
+
171
+ const parsed = parseRebuttalResponse(res.text)
172
+ const costEst = estimateTokenCost({
173
+ provider: slot.provider,
174
+ model: slot.model,
175
+ inputTokens: res.usage?.inputTokens || 0,
176
+ outputTokens: res.usage?.outputTokens || 0,
177
+ cacheHitTokens: res.usage?.cacheHitTokens || 0,
178
+ prices,
179
+ })
180
+
181
+ const newFiles = extractFileBlocks(parsed.refinedText)
182
+ if (newFiles.length > 0) {
183
+ await writeCandidateWorkspace(cwd, cand.index, newFiles, { runId })
184
+ }
185
+
186
+ const syntaxValidation = verifyFileSyntax(newFiles)
187
+
188
+ const critiqueRecord = {
189
+ candidateIndex: cand.index,
190
+ candidateLabel: isBlindEvaluation ? `Candidate ${cand.index}` : cand.label,
191
+ critique: parsed.critiqueText,
192
+ defense: parsed.defenseText,
193
+ }
194
+ roundCritiques.push(critiqueRecord)
195
+
196
+ return {
197
+ ...cand,
198
+ text: parsed.refinedText,
199
+ files: newFiles.length > 0 ? newFiles : cand.files,
200
+ syntaxValidation: syntaxValidation.valid ? syntaxValidation : cand.syntaxValidation,
201
+ usage: {
202
+ inputTokens: (cand.usage?.inputTokens || 0) + (res.usage?.inputTokens || 0),
203
+ outputTokens: (cand.usage?.outputTokens || 0) + (res.usage?.outputTokens || 0),
204
+ totalTokens: (cand.usage?.totalTokens || 0) + (res.usage?.totalTokens || 0),
205
+ },
206
+ costUsd: Number(((cand.costUsd || 0) + (costEst?.costUsd || 0)).toFixed(5)),
207
+ lastDebateCritique: parsed.critiqueText,
208
+ lastDebateDefense: parsed.defenseText,
209
+ }
210
+ } catch (err) {
211
+ logger.warn(`[dsh-moa] Candidate ${cand.index} rebuttal debate round ${roundNumber} failed:`, err?.message || err)
212
+ return cand
213
+ }
214
+ })
215
+
216
+ const updatedCandidates = await Promise.all(debatePromises)
217
+
218
+ emitEvent(ctx, MOA_EVENTS.DEBATE_ROUND, {
219
+ runId,
220
+ roundNumber,
221
+ totalRounds,
222
+ critiquesCount: roundCritiques.length,
223
+ activeCandidates: updatedCandidates.length,
224
+ })
225
+
226
+ return {
227
+ round: roundNumber,
228
+ critiques: roundCritiques,
229
+ updatedCandidates,
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Formats accumulated debate protocol into a clean markdown directive for the Judge.
235
+ * @param {Array<object>} debateRounds
236
+ * @returns {string}
237
+ */
238
+ export function formatDebateDirective(debateRounds = []) {
239
+ if (!Array.isArray(debateRounds) || debateRounds.length === 0) return ''
240
+
241
+ const sections = []
242
+ sections.push('=== ⚔️ MULTI-ROUND REBUTTAL DEBATE PROTOCOL ===')
243
+ sections.push('The candidate models engaged in peer cross-examination prior to arbitration.')
244
+ sections.push('CRITICAL JUDGE DIRECTIVE: Heavily weight valid vulnerabilities or bugs identified by peers.')
245
+
246
+ for (const r of debateRounds) {
247
+ sections.push(`\n--- Debate Round ${r.round} ---`)
248
+ for (const c of r.critiques || []) {
249
+ if (c.critique) {
250
+ sections.push(`[${c.candidateLabel}] Critiques of peers:\n${c.critique}`)
251
+ }
252
+ if (c.defense) {
253
+ sections.push(`[${c.candidateLabel}] Defense & Justification:\n${c.defense}`)
254
+ }
255
+ }
256
+ }
257
+
258
+ sections.push('\n==============================================')
259
+ return sections.join('\n')
260
+ }
261
+
262
+ /**
263
+ * Orchestrates multi-round rebuttal debate phase (#298).
264
+ * @param {object} params
265
+ * @returns {Promise<{ ok: boolean, updatedCandidates: Array<object>, debateDirective: string, totalCritiques: number, rounds: Array<object> }>}
266
+ */
267
+ export async function runRebuttalDebatePhase({
268
+ candidates = [],
269
+ userPrompt = '',
270
+ preset = {},
271
+ prices = {},
272
+ maxTokens = 4096,
273
+ isBlindEvaluation = true,
274
+ callLlm,
275
+ signal = null,
276
+ onProgress = null,
277
+ cwd = process.cwd(),
278
+ runId = '',
279
+ ctx = null,
280
+ }) {
281
+ const enabled = Boolean(preset?.rebuttal_debate_enabled || preset?.rebuttalDebateEnabled)
282
+ if (!enabled || candidates.length < 2) {
283
+ return {
284
+ ok: true,
285
+ updatedCandidates: candidates,
286
+ debateDirective: '',
287
+ totalCritiques: 0,
288
+ rounds: [],
289
+ }
290
+ }
291
+
292
+ const configuredRounds = preset?.rebuttal_rounds ?? preset?.rebuttalRounds ?? 1
293
+ const totalRounds = Math.min(3, Math.max(1, Number(configuredRounds) || 1))
294
+
295
+ let currentCandidates = [...candidates]
296
+ const debateRounds = []
297
+
298
+ for (let r = 1; r <= totalRounds; r++) {
299
+ if (signal?.aborted) {
300
+ break
301
+ }
302
+
303
+ const roundResult = await executeDebateRound({
304
+ candidates: currentCandidates,
305
+ userPrompt,
306
+ roundNumber: r,
307
+ totalRounds,
308
+ isBlindEvaluation,
309
+ callLlm,
310
+ refTemp: preset?.reference_temperature ?? 0.5,
311
+ maxTokens,
312
+ refTimeoutSec: preset?.reference_timeout_sec ?? 60,
313
+ prices,
314
+ signal,
315
+ onProgress,
316
+ cwd,
317
+ runId,
318
+ ctx,
319
+ })
320
+
321
+ debateRounds.push(roundResult)
322
+ currentCandidates = roundResult.updatedCandidates
323
+ }
324
+
325
+ const totalCritiques = debateRounds.reduce((acc, r) => acc + (r.critiques?.length || 0), 0)
326
+ const debateDirective = formatDebateDirective(debateRounds)
327
+
328
+ return {
329
+ ok: true,
330
+ updatedCandidates: currentCandidates,
331
+ debateDirective,
332
+ totalCritiques,
333
+ rounds: debateRounds,
334
+ }
335
+ }
@@ -1,3 +1,5 @@
1
+ import { applyParetoAdjustment } from './pareto-frontier.js'
2
+ import { evaluateHardBudgetLimit } from './budget-guard.js'
1
3
  import { sanitizeCandidatesForCloudJudge } from './privacy-sanitizer.js'
2
4
  import { evaluateJudgeEscalation } from './escalation.js'
3
5
  import { calculateSelfConsistencyAgreement } from './self-consistency.js'
@@ -23,6 +25,8 @@ import { runSecurityAuditGate } from './security-fuzzer.js'
23
25
  import { runCoveVerificationGate } from './cove-verifier.js'
24
26
  import { runProfilerGate } from './runtime-profiler.js'
25
27
  import { runMutationGate } from './mutation-tester.js'
28
+ import { stageEmbeddingClustering } from './embedding-clustering.js'
29
+ import { runRebuttalDebatePhase } from './rebuttal-debate.js'
26
30
  import { emitEvent, MOA_EVENTS } from '../events.js'
27
31
 
28
32
  export async function runMoAPipeline({
@@ -50,9 +54,36 @@ export async function runMoAPipeline({
50
54
  timestamp: new Date(startTime).toISOString(),
51
55
  })
52
56
 
57
+ // Pre-Stage 0.5: Pareto Frontier Slider & Hard Budget Guard (#307, #319)
58
+ let effectivePreset = preset
59
+ if (effectivePreset?.pareto_slider_enabled) {
60
+ effectivePreset = applyParetoAdjustment(effectivePreset, { ctx, runId, onProgress, prices })
61
+ }
62
+
63
+ if (effectivePreset?.hard_budget_guard_enabled) {
64
+ const budgetRes = evaluateHardBudgetLimit({
65
+ references: effectivePreset.reference_models || [],
66
+ aggregator: effectivePreset.aggregator || {},
67
+ limitUsd: effectivePreset.hard_budget_limit_usd ?? 0.02,
68
+ policy: effectivePreset.hard_budget_policy || 'auto_downgrade',
69
+ prices,
70
+ options: { ctx, runId, onProgress },
71
+ })
72
+ if (!budgetRes.ok) {
73
+ return { error: new Error(budgetRes.error) }
74
+ }
75
+ if (budgetRes.scaled) {
76
+ effectivePreset = {
77
+ ...effectivePreset,
78
+ reference_models: budgetRes.references,
79
+ aggregator: budgetRes.aggregator,
80
+ }
81
+ }
82
+ }
83
+
53
84
  // Stage 1: Context & Configuration
54
85
  const ctxStage = await stageProjectContext({
55
- userPrompt, messages, preset, cwd, prices, maxTokens: preset?.max_tokens, startTime, onProgress, ctx,
86
+ userPrompt, messages, preset: effectivePreset, cwd, prices, maxTokens: effectivePreset?.max_tokens, startTime, onProgress, ctx,
56
87
  })
57
88
  if (!ctxStage.ok) return ctxStage.abortPayload
58
89
 
@@ -90,7 +121,7 @@ export async function runMoAPipeline({
90
121
  })
91
122
  return candStage.failurePayload
92
123
  }
93
- const { referenceOutputs, successfulRefs, adversarialTddResult } = candStage
124
+ let { referenceOutputs, successfulRefs, adversarialTddResult } = candStage
94
125
 
95
126
  for (const ref of referenceOutputs) {
96
127
  emitEvent(ctx, MOA_EVENTS.CANDIDATE_COMPLETE, {
@@ -173,6 +204,32 @@ export async function runMoAPipeline({
173
204
  }
174
205
  }
175
206
 
207
+ // Pre-Judge Stage 4.6: Blind Embedding Clustering for Zero-LLM Fast Consensus (#312)
208
+ if ((preset?.embedding_clustering_enabled || preset?.embeddingClusteringEnabled) && successfulRefs.length > 1) {
209
+ const embedRes = await stageEmbeddingClustering({
210
+ successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
211
+ referenceOutputs,
212
+ preset,
213
+ userPrompt,
214
+ prices,
215
+ maxTokens: ctxStage.maxTokens,
216
+ cwd,
217
+ runId,
218
+ signal,
219
+ promoteOpts,
220
+ allowCandidateOverride: ctxStage.allowCandidateOverride,
221
+ isRefinement: ctxStage.isRefinement,
222
+ historyFilePath,
223
+ startTime,
224
+ collectedCtx: ctxStage.collectedCtx,
225
+ liveCanvas,
226
+ ctx,
227
+ })
228
+ if (embedRes.consensusReached) {
229
+ return embedRes.payload
230
+ }
231
+ }
232
+
176
233
  if (signal?.aborted) {
177
234
  await cleanMoaWorkspaces(cwd, { runId })
178
235
  return { error: new Error('Turn aborted') }
@@ -199,6 +256,35 @@ export async function runMoAPipeline({
199
256
  })
200
257
  }
201
258
 
259
+ // Stage 5.2: Multi-Round Rebuttal Debate between Candidates (#298)
260
+ let debateDirective = null
261
+ if ((preset?.rebuttal_debate_enabled || preset?.rebuttalDebateEnabled) && successfulRefs.length > 1) {
262
+ const debateRes = await runRebuttalDebatePhase({
263
+ candidates: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
264
+ userPrompt,
265
+ preset,
266
+ prices,
267
+ maxTokens: ctxStage.maxTokens,
268
+ isBlindEvaluation: ctxStage.isBlindEvaluation,
269
+ callLlm,
270
+ signal,
271
+ onProgress,
272
+ cwd,
273
+ runId,
274
+ ctx,
275
+ })
276
+ if (debateRes.ok && debateRes.debateDirective) {
277
+ debateDirective = debateRes.debateDirective
278
+ if (debateRes.updatedCandidates?.length > 0) {
279
+ successfulRefs = debateRes.updatedCandidates
280
+ referenceOutputs = referenceOutputs.map((r) => {
281
+ const matching = successfulRefs.find((c) => c.index === r.index || c.label === r.label)
282
+ return matching ? { ...r, text: matching.text } : r
283
+ })
284
+ }
285
+ }
286
+ }
287
+
202
288
  if (signal?.aborted) {
203
289
  await cleanMoaWorkspaces(cwd, { runId })
204
290
  return { error: new Error('Turn aborted') }
@@ -307,7 +393,6 @@ export async function runMoAPipeline({
307
393
  let effectivePrimaryJudge = ctxStage.primaryJudge
308
394
  let effectiveFallbackJudges = ctxStage.fallbackJudges
309
395
  let effectiveJudgesChain = ctxStage.judgesChain
310
- let effectivePreset = preset
311
396
 
312
397
  if (escalationResult.escalated) {
313
398
  emitEvent(ctx, MOA_EVENTS.JUDGE_ESCALATION, {
@@ -344,7 +429,7 @@ export async function runMoAPipeline({
344
429
  preset: effectivePreset,
345
430
  successfulRefs: candidatesForJudge,
346
431
  referenceOutputs: sanitizedReferenceOutputs, primaryJudge: effectivePrimaryJudge,
347
- userPrompt, judgeCriteria: [ctxStage.judgeCriteria, coveDirective, securityDirective, profilerDirective, mutationDirective, privacyDirective].filter(Boolean).join('\n\n'), prices, maxTokens: ctxStage.maxTokens,
432
+ userPrompt, judgeCriteria: [ctxStage.judgeCriteria, debateDirective, coveDirective, securityDirective, profilerDirective, mutationDirective, privacyDirective].filter(Boolean).join('\n\n'), prices, maxTokens: ctxStage.maxTokens,
348
433
  callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
349
434
  isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
350
435
  })
@@ -402,7 +487,7 @@ export async function runMoAPipeline({
402
487
  }
403
488
  }
404
489
 
405
- const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, coveDirective, securityDirective, profilerDirective, mutationDirective, privacyDirective, pruningDirective, ctxStage.stackAwareDirective, ctxStage.developerPreferencesDirective].filter(Boolean).join("\n\n") || null
490
+ const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective, debateDirective, coveDirective, securityDirective, profilerDirective, mutationDirective, privacyDirective, pruningDirective, ctxStage.stackAwareDirective, ctxStage.developerPreferencesDirective].filter(Boolean).join("\n\n") || null
406
491
 
407
492
  // Stage 7: Synthesis Phase
408
493
  const synthRes = await stageSynthesis({
package/lib/pricing.js CHANGED
@@ -152,6 +152,11 @@ export function resolveModelRates(slot = {}, customPrices = {}, cachePath = getD
152
152
  const fullKey = provider ? `${provider}/${model}` : model
153
153
  const colonKey = provider ? `${provider}:${model}` : model
154
154
 
155
+ // 0. Free local edge providers
156
+ if (provider === 'ollama' || provider === 'local') {
157
+ return { input: 0, output: 0, cacheHit: 0 }
158
+ }
159
+
155
160
  // 1. Check custom user overrides in config
156
161
  if (customPrices && typeof customPrices === 'object') {
157
162
  if (customPrices[fullKey]) return normalizeRates(customPrices[fullKey])
@@ -278,9 +283,9 @@ export function resolveModelRates(slot = {}, customPrices = {}, cachePath = getD
278
283
  function normalizeRates(rate) {
279
284
  if (!rate || typeof rate !== 'object') return { ...FALLBACK_RATES }
280
285
  return {
281
- input: Number(rate.input ?? rate.prompt ?? FALLBACK_RATES.input),
282
- output: Number(rate.output ?? rate.completion ?? FALLBACK_RATES.output),
283
- cacheHit: Number(rate.cacheHit ?? rate.cache_hit ?? FALLBACK_RATES.cacheHit),
286
+ input: Number(rate.input ?? rate.prompt ?? rate.prompt_per_million ?? FALLBACK_RATES.input),
287
+ output: Number(rate.output ?? rate.completion ?? rate.completion_per_million ?? FALLBACK_RATES.output),
288
+ cacheHit: Number(rate.cacheHit ?? rate.cache_hit ?? rate.cache_hit_per_million ?? FALLBACK_RATES.cacheHit),
284
289
  }
285
290
  }
286
291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.78",
3
+ "version": "0.2.80",
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",