@goodandready/dsh-moa 0.2.71 → 0.2.72
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 +9 -0
- package/README.ru.md +9 -0
- package/README.zh.md +9 -0
- package/lib/client.js +45 -0
- package/lib/events.js +3 -0
- package/lib/moa-adversarial-tdd.js +4 -1
- package/lib/moa-ast-merge.js +4 -1
- package/lib/moa-schema.js +7 -0
- package/lib/moa-swarm.js +13 -4
- package/lib/pipeline/context.js +5 -0
- package/lib/pipeline/early-consensus.js +324 -0
- package/lib/pipeline/runner.js +53 -6
- package/lib/pipeline/syntax-filter.js +324 -0
- package/lib/tui-settings.js +2 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -180,6 +180,12 @@ Seamlessly retains synthesized baseline code from prior turns while pruning inte
|
|
|
180
180
|
### 18. Graceful Degradation & Local Fallback Resilience
|
|
181
181
|
When `local_fallback_enabled: true`, candidates that fail due to network outages, timeouts, or rate limits (429/500) automatically recover using configured local models (Ollama / MiniPC). Each fallback model operates with an independent execution budget and an isolated controller linked to the parent turn signal, preventing cloud timeouts from starving fallback models. Additionally, terminal errors and aborted streams are strictly classified, ensuring reliable error propagation and failover.
|
|
182
182
|
|
|
183
|
+
### 19. Zero-Token Local Pre-Judge Syntax Filter
|
|
184
|
+
Validates markdown code blocks across all candidate outputs locally using JavaScript/TypeScript AST checking, JSON parsing, and bracket/quote balance verification before calling the Judge LLM. When malformed candidates are detected, they are automatically disqualified or annotated with structured syntax error reports, saving judge tokens and preventing syntax errors from propagating. Configured via `local_preflight_check: true` (enabled by default).
|
|
185
|
+
|
|
186
|
+
### 20. Early Exit upon Semantic Consensus
|
|
187
|
+
Compares normalized candidate proposals using token-level Sørensen-Dice similarity. When models arrive at identical or near-identical solutions (meeting or exceeding `early_exit_threshold`, default: `0.95`), the expensive Judge synthesis phase is automatically bypassed. The consensus solution is promoted directly to the workspace, saving up to 100% of judge token costs while accelerating pipeline latency. Configured via `early_exit_consensus_enabled: true` and `early_exit_threshold: 0.95`.
|
|
188
|
+
|
|
183
189
|
### 6. Live Canvas 1-Click Preview (optional)
|
|
184
190
|
If `@goodandready/dsh-live-canvas` is installed in the same profile, `dsh-moa` pushes the promoted HTML file to the Live Canvas REST contract (`POST /dsh-live-canvas/api/preview`, served by the same harness webServer) and appends a one-click preview link (`/dsh-live-canvas/sandbox/<id>`) to the answer. Without the plugin the step is skipped silently — no errors in the log, no dead links.
|
|
185
191
|
|
|
@@ -289,6 +295,9 @@ dsh-moa:
|
|
|
289
295
|
| `presets[].quorum_enabled` | `boolean` | `false` | Straggler mitigation: proceed with synthesis once >= 60% candidates respond |
|
|
290
296
|
| `presets[].grace_period_sec` | `number` | `10` | Grace period in seconds to wait for stragglers after quorum is reached |
|
|
291
297
|
| `presets[].aggregator_fallbacks` | `array` | `[]` | Ordered fallback judge models tried if primary aggregator encounters transient errors |
|
|
298
|
+
| `presets[].local_preflight_check` | `boolean` | `true` | Zero-token local AST, JSON, and bracket balance verification for candidate code blocks before the Judge panel |
|
|
299
|
+
| `presets[].early_exit_consensus_enabled` | `boolean` | `false` | Bypass Judge evaluation and promote directly when candidate solutions reach high semantic consensus |
|
|
300
|
+
| `presets[].early_exit_threshold` | `number` | `0.95` | Token-level similarity threshold (0.50 to 1.00) required to trigger early exit consensus |
|
|
292
301
|
| `presets[].blind_evaluation` | `boolean` | `false` | Anonymize candidate model names for the judge/curator to eliminate family/brand bias |
|
|
293
302
|
| `presets[].reference_timeout_sec` | `number` | `60` | Per-candidate execution timeout in seconds |
|
|
294
303
|
| `presets[].aggregator_timeout_sec` | `number` | `180` | Aggregator/judge synthesis timeout in seconds |
|
package/README.ru.md
CHANGED
|
@@ -180,6 +180,12 @@ graph TD
|
|
|
180
180
|
### 18. Плавная деградация и локальный фоллбэк
|
|
181
181
|
Опция `local_fallback_enabled` позволяет задать резервные локальные модели (Ollama, vLLM, MiniPC), к которым плагин бесшовно обращается при сбоях сетевых API, таймаутах или превышении лимитов rate limit (429/500). Каждая фоллбэк-модель запускается с независимым бюджетом времени и изолированным контроллером отмены, связанным с сигналом основного хода, предотвращая исчерпание времени ожидания из-за задержек облачных провайдеров. Стриминговые сбои и терминальные ошибки классифицируются типизированными исключениями с гарантированной защитой от позднего промоушна.
|
|
182
182
|
|
|
183
|
+
### 19. Локальный синтаксический шлюз кандидатов без расхода токенов
|
|
184
|
+
Локально парсит блоки кода из Markdown-ответов кандидатов и проверяет их синтаксис (JavaScript, TypeScript, JSON, Python) и баланс скобок/кавычек до вызова дорогостоящей модели-судьи. Кандидаты с синтаксическими ошибками дисквалифицируются или помечаются структурированными аннотациями для судьи, полностью исключая лишний расход токенов на заведомо невалидный код. Включается настройкой `local_preflight_check: true` (включена по умолчанию).
|
|
185
|
+
|
|
186
|
+
### 20. Ранний выход по семантическому консенсусу
|
|
187
|
+
Сравнивает нормализованные решения кандидатов по токенному сходству Сёренсена-Дайса. Если решения моделей совпадают или практически идентичны (достигнут порог `early_exit_threshold`, по умолчанию 0.95), стадия вызова судьи (Judge) пропускается. Согласованный вариант немедленно продвигается в проект, экономя до 100% токенов судейской коллегии и сокращая задержку ответа. Управляется параметрами `early_exit_consensus_enabled: true` и `early_exit_threshold: 0.95`.
|
|
188
|
+
|
|
183
189
|
### 6. Live Canvas 1-клик предпросмотр (опционально)
|
|
184
190
|
Если в профиле установлен `@goodandready/dsh-live-canvas`, `dsh-moa` отправляет промоученный HTML-файл в REST-контракт Live Canvas (`POST /dsh-live-canvas/api/preview`, тот же webServer харнесса) и добавляет к ответу ссылку на предпросмотр (`/dsh-live-canvas/sandbox/<id>`). Без плагина шаг пропускается тихо — без ошибок в журнале и без битых ссылок.
|
|
185
191
|
|
|
@@ -289,6 +295,9 @@ dsh-moa:
|
|
|
289
295
|
| `presets[].quorum_enabled` | `boolean` | `false` | Защита от зависших моделей (stragglers): запуск синтеза при ответе от >= 60% кандидатов |
|
|
290
296
|
| `presets[].grace_period_sec` | `number` | `10` | Грейс-период (в секундах) ожидания оставшихся моделей после достижения кворума |
|
|
291
297
|
| `presets[].aggregator_fallbacks` | `array` | `[]` | Список запасных моделей-судей при сбоях основной модели агрегатора |
|
|
298
|
+
| `presets[].local_preflight_check` | `boolean` | `true` | Локальная проверка AST, JSON и баланса скобок в блоках кода кандидатов без расхода токенов |
|
|
299
|
+
| `presets[].early_exit_consensus_enabled` | `boolean` | `false` | Пропуск этапа судьи и прямое продвижение при достижении семантического консенсуса кандидатов |
|
|
300
|
+
| `presets[].early_exit_threshold` | `number` | `0.95` | Порог токенного сходства (0.50–1.00) для срабатывания раннего выхода по консенсусу |
|
|
292
301
|
| `presets[].blind_evaluation` | `boolean` | `false` | Обезличивание имен кандидатов («Candidate 1», «Candidate 2») для исключения предвзятости судьи |
|
|
293
302
|
| `presets[].reference_timeout_sec` | `number` | `60` | Таймаут опроса каждого кандидата в секундах |
|
|
294
303
|
| `presets[].aggregator_timeout_sec` | `number` | `180` | Таймаут синтеза решения судьей в секундах |
|
package/README.zh.md
CHANGED
|
@@ -157,6 +157,12 @@ graph TD
|
|
|
157
157
|
### 18. 平滑降级与本地容灾恢复 (Local Fallback Resilience)
|
|
158
158
|
当 `local_fallback_enabled: true` 时,因网络中断、超时或速率限制(429/500)而失败的候选模型会自动使用配置的本地模型(Ollama / MiniPC)恢复。每个后备模型都拥有独立的超时执行配额与关联主 turn signal 的隔离取消控制器,防止云端超时耗尽本地恢复时间。同时严格检测终止错误与流中断,确保安全容灾且杜绝已取消 turn 的延迟文件提升。
|
|
159
159
|
|
|
160
|
+
### 19. 零 Token 本地预审语法网关 (Zero-Token Pre-Judge Syntax Filter)
|
|
161
|
+
在调用裁判模型之前,在本地解析所有候选方案的 Markdown 代码块,进行 JavaScript/TypeScript AST 语法校验、JSON 解析以及括号/引号匹配检查。检测到语法损坏的候选将被自动过滤或附带结构化语法错误注解,无需消耗模型 Token 即可杜绝语法缺陷代码流入最终裁决。通过 `local_preflight_check: true` 配置(默认开启)。
|
|
162
|
+
|
|
163
|
+
### 20. 语义共识提前退出机制 (Early Exit upon Semantic Consensus)
|
|
164
|
+
基于 Token 级 Sørensen-Dice 相似度算法比对归一化后的候选方案。当多个候选模型给出高度一致的代码方案(达到或超过 `early_exit_threshold` 阈值,默认 0.95)时,自动跳过高耗费的裁判综合评审阶段,直接将共识方案提升至项目,在大幅缩短响应延迟的同时节省多达 100% 的裁判 Token 支出。通过 `early_exit_consensus_enabled: true` 与 `early_exit_threshold: 0.95` 配置。
|
|
165
|
+
|
|
160
166
|
### 6. Live Canvas 一键预览(可选)
|
|
161
167
|
若同一 profile 中安装了 `@goodandready/dsh-live-canvas`,`dsh-moa` 会将提升后的 HTML 文件推送到 Live Canvas 的 REST 契约(`POST /dsh-live-canvas/api/preview`,由同一 harness webServer 提供服务),并在回答中附上一键预览链接(`/dsh-live-canvas/sandbox/<id>`)。未安装该插件时此步骤静默跳过——日志无报错,也不会出现死链接。
|
|
162
168
|
|
|
@@ -225,6 +231,9 @@ dsh-moa:
|
|
|
225
231
|
| `presets[].reference_models` | `array` | `[...]` | 并行提案阶段并发调用的顾问模型列表 |
|
|
226
232
|
| `presets[].aggregator` | `object` | `{...}` | 负责综合评审、代码审查与裁决胜出者的裁判模型 |
|
|
227
233
|
| `presets[].ask_clarifying_questions` | `boolean` | `true` | 针对宽泛需求合成澄清问卷(预设级别开关) |
|
|
234
|
+
| `presets[].local_preflight_check` | `boolean` | `true` | 在裁判评审前本地验证候选代码块的 AST、JSON 与括号平衡(零 Token 消耗) |
|
|
235
|
+
| `presets[].early_exit_consensus_enabled` | `boolean` | `false` | 当候选方案达成高语义共识时跳过裁判评审并直接提升获胜方案 |
|
|
236
|
+
| `presets[].early_exit_threshold` | `number` | `0.95` | 触发语义共识提前退出的 Token 相似度阈值(0.50 至 1.00) |
|
|
228
237
|
| `presets[].reference_temperature` / `.aggregator_temperature` | `number` | `0.6` / `0.4` | 顾问与裁判的采样温度 |
|
|
229
238
|
| `presets[].max_tokens` | `number` | `4096` | 每次模型调用的最大输出 Token 数 |
|
|
230
239
|
| `presets[].judge_criteria` | `string` | `""` | 传给裁判的可选附加评审准则 |
|
package/lib/client.js
CHANGED
|
@@ -185,6 +185,10 @@ window.__ModuleLoader__.load({
|
|
|
185
185
|
"aggregator.workbench_desc": "Judge scores each candidate file from 1 to 10 with pros/cons. Select desired files across candidates; top model harmonizes and glues them, followed by final judge audit.",
|
|
186
186
|
"aggregator.workbench_companion": "Companion Plugin:",
|
|
187
187
|
"aggregator.workbench_companion_note": "safe shadow Git checkpoints before file promotion",
|
|
188
|
+
"aggregator.preflight_syntax_title": "\u{1F50D} Pre-Judge Local Syntax Gate (Zero-Token Linter)",
|
|
189
|
+
"aggregator.preflight_syntax_desc": "Validate candidate code syntax locally before judge invocation to filter broken code without token costs.",
|
|
190
|
+
"aggregator.early_consensus_title": "\u26A1 Early Exit upon Semantic Consensus",
|
|
191
|
+
"aggregator.early_consensus_desc": "Bypass the expensive judge round when reference models converge on equivalent code solutions.",
|
|
188
192
|
"workbench.title": "\u{1F6E0}\uFE0F Interactive Synthesis Workbench",
|
|
189
193
|
"workbench.desc": "Inspect candidate file quality scores (1\u201310), mix and match files across proposals, harmonize with top-model glue, and run a final judge audit.",
|
|
190
194
|
"workbench.select_run": "MoA Run:",
|
|
@@ -379,6 +383,10 @@ window.__ModuleLoader__.load({
|
|
|
379
383
|
"aggregator.workbench_desc": "\u88C1\u5224\u4E3A\u6BCF\u4E2A\u5019\u9009\u6587\u4EF6\u6253\u5206 (1-10) \u5E76\u6807\u6CE8\u4F18\u7F3A\u70B9\u3002\u5728 UI \u4E2D\u81EA\u7531\u7EC4\u5408\u5404\u5019\u9009\u6587\u4EF6\uFF0C\u7531\u5F53\u8F6E\u6700\u9AD8\u5206\u6A21\u578B\u8FDB\u884C\u4EE3\u7801\u7C98\u5408\u7EDF\u4E00\uFF0C\u6700\u7EC8\u7531\u88C1\u5224\u590D\u5BA1\u3002",
|
|
380
384
|
"aggregator.workbench_companion": "\u914D\u5957\u63D2\u4EF6\uFF1A",
|
|
381
385
|
"aggregator.workbench_companion_note": "\u6587\u4EF6\u63D0\u5347\u524D\u5B89\u5168\u521B\u5EFA\u5F71\u5B50 Git \u5FEB\u7167",
|
|
386
|
+
"aggregator.preflight_syntax_title": "\u{1F50D} \u8BC4\u5BA1\u524D\u672C\u5730\u8BED\u6CD5\u68C0\u67E5\u95E8\u7981\uFF08\u96F6Token\u5F00\u9500\uFF09",
|
|
387
|
+
"aggregator.preflight_syntax_desc": "\u5728\u8C03\u7528\u88C1\u5224\u6A21\u578B\u524D\u5BF9\u5019\u9009\u4EE3\u7801\u8FDB\u884C\u672C\u5730\u8BED\u6CD5\u68C0\u67E5\uFF0C\u96F6Token\u5254\u9664\u8BED\u6CD5\u7834\u635F\u4EE3\u7801\u3002",
|
|
388
|
+
"aggregator.early_consensus_title": "\u26A1 \u8BED\u4E49\u5171\u8BC6\u5FEB\u901F\u76F4\u51FA\uFF08Early Exit\uFF09",
|
|
389
|
+
"aggregator.early_consensus_desc": "\u5F53\u591A\u5019\u9009\u6A21\u578B\u751F\u6210\u4E00\u81F4\u4EE3\u7801\uFF08\u76F8\u4F3C\u5EA6\u8FBE\u6807\uFF09\u65F6\uFF0C\u76F4\u63A5\u8FD4\u56DE\u5171\u8BC6\u89E3\uFF0C\u8DF3\u8FC7\u88C1\u5224\u9636\u6BB5\u8282\u7EA6Token\u4E0E\u8017\u65F6\u3002",
|
|
382
390
|
"workbench.title": "\u{1F6E0}\uFE0F \u4EA4\u4E92\u5F0F\u5408\u6210\u5DE5\u4F5C\u53F0",
|
|
383
391
|
"workbench.desc": "\u67E5\u770B\u5019\u9009\u6A21\u578B\u7684\u6587\u4EF6\u8D28\u91CF\u6253\u5206 (1\u201310)\uFF0C\u81EA\u7531\u6DF7\u642D\u7EC4\u5408\u76EE\u6807\u6587\u4EF6\uFF0C\u7531\u6700\u9AD8\u5206\u6A21\u578B\u7C98\u5408\u7EDF\u4E00\u5E76\u6267\u884C\u6700\u7EC8\u88C1\u5224\u590D\u5BA1\u3002",
|
|
384
392
|
"workbench.select_run": "MoA \u8FD0\u884C\uFF1A",
|
|
@@ -2312,6 +2320,43 @@ window.__ModuleLoader__.load({
|
|
|
2312
2320
|
)
|
|
2313
2321
|
)
|
|
2314
2322
|
),
|
|
2323
|
+
/* Pre-Judge Local Syntax Gate (#303) & Early Semantic Consensus (#299) */
|
|
2324
|
+
React.createElement(
|
|
2325
|
+
"div",
|
|
2326
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6, padding: "8px 12px", background: "var(--dsw-alias-bg-layer-2)", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", marginTop: 8 } },
|
|
2327
|
+
React.createElement(
|
|
2328
|
+
"label",
|
|
2329
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500 } },
|
|
2330
|
+
React.createElement("input", {
|
|
2331
|
+
type: "checkbox",
|
|
2332
|
+
checked: currentPreset.local_preflight_check !== false,
|
|
2333
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, local_preflight_check: e.target.checked })),
|
|
2334
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2335
|
+
}),
|
|
2336
|
+
t("aggregator.preflight_syntax_title") || "\u{1F50D} Pre-Judge Local Syntax Gate (Zero-Token Linter)"
|
|
2337
|
+
),
|
|
2338
|
+
React.createElement(
|
|
2339
|
+
"div",
|
|
2340
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2341
|
+
t("aggregator.preflight_syntax_desc") || "Validate candidate code syntax locally before judge invocation to filter broken code without token costs."
|
|
2342
|
+
),
|
|
2343
|
+
React.createElement(
|
|
2344
|
+
"label",
|
|
2345
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 13, fontWeight: 500, marginTop: 4 } },
|
|
2346
|
+
React.createElement("input", {
|
|
2347
|
+
type: "checkbox",
|
|
2348
|
+
checked: Boolean(currentPreset.early_exit_consensus_enabled),
|
|
2349
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, early_exit_consensus_enabled: e.target.checked })),
|
|
2350
|
+
style: { accentColor: "var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))", cursor: "pointer" }
|
|
2351
|
+
}),
|
|
2352
|
+
t("aggregator.early_consensus_title") || "\u26A1 Early Exit upon Semantic Consensus"
|
|
2353
|
+
),
|
|
2354
|
+
React.createElement(
|
|
2355
|
+
"div",
|
|
2356
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary)", marginLeft: 22 } },
|
|
2357
|
+
t("aggregator.early_consensus_desc") || "Bypass the expensive judge round when reference models converge on equivalent code solutions."
|
|
2358
|
+
)
|
|
2359
|
+
),
|
|
2315
2360
|
/* Multi-Turn Context & Diff Pruning (#176) */
|
|
2316
2361
|
React.createElement(
|
|
2317
2362
|
"div",
|
package/lib/events.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export const MOA_EVENTS = {
|
|
6
6
|
RUN_START: 'moa/run-start',
|
|
7
7
|
CANDIDATE_COMPLETE: 'moa/candidate-complete',
|
|
8
|
+
SYNTAX_VALIDATION: 'moa/syntax-validation',
|
|
9
|
+
EARLY_CONSENSUS: 'moa/early-consensus',
|
|
8
10
|
CRITIQUE_COMPLETE: 'moa/critique-complete',
|
|
9
11
|
JUDGE_VERDICT: 'moa/judge-verdict',
|
|
10
12
|
RUN_FINISH: 'moa/run-finish',
|
|
@@ -28,3 +30,4 @@ export function emitEvent(ctx, eventName, payload) {
|
|
|
28
30
|
/* best-effort event dispatch: listener exceptions must never abort pipeline */
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
|
|
@@ -184,7 +184,9 @@ export async function runAdversarialTddLoop({
|
|
|
184
184
|
try {
|
|
185
185
|
const dest = assertPathContained(candDir, tf.relativePath)
|
|
186
186
|
await fs.rm(dest, { force: true })
|
|
187
|
-
} catch {
|
|
187
|
+
} catch (err) {
|
|
188
|
+
logger.debug('[dsh-moa:tdd] cleanup test file error:', err?.message || err)
|
|
189
|
+
}
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
if (gateRes) {
|
|
@@ -217,3 +219,4 @@ export async function runAdversarialTddLoop({
|
|
|
217
219
|
directive: results.length > 0 ? reportLines.join('\n') : '',
|
|
218
220
|
}
|
|
219
221
|
}
|
|
222
|
+
|
package/lib/moa-ast-merge.js
CHANGED
|
@@ -24,7 +24,9 @@ export function resolveContextLensService(ctx, explicitLens = null) {
|
|
|
24
24
|
const refl = ctx.reflect.get('contextLens') || ctx.reflect.get('dsh-context-lens')
|
|
25
25
|
if (refl) return refl
|
|
26
26
|
}
|
|
27
|
-
} catch {
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.debug('[dsh-moa:ast] context-lens reflection error:', err?.message || err)
|
|
29
|
+
}
|
|
28
30
|
return null
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -228,3 +230,4 @@ export async function buildAstCompositeMergeDirective(candidates = [], options =
|
|
|
228
230
|
directive,
|
|
229
231
|
}
|
|
230
232
|
}
|
|
233
|
+
|
package/lib/moa-schema.js
CHANGED
|
@@ -76,6 +76,9 @@ export const PresetSchema = z.object({
|
|
|
76
76
|
speculative_moa_enabled: z.boolean().default(false),
|
|
77
77
|
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }),
|
|
78
78
|
adversarial_tdd_enabled: z.boolean().default(false),
|
|
79
|
+
local_preflight_check: z.boolean().default(true),
|
|
80
|
+
early_exit_consensus_enabled: z.boolean().default(false),
|
|
81
|
+
early_exit_threshold: z.number().default(0.95),
|
|
79
82
|
})
|
|
80
83
|
|
|
81
84
|
export const Config = z.object({
|
|
@@ -98,6 +101,9 @@ export const Config = z.object({
|
|
|
98
101
|
speculative_moa_enabled: z.boolean().default(false).volatile(),
|
|
99
102
|
speculative_draft_model: ModelSlotSchema.default({ provider: "", model: "" }).volatile(),
|
|
100
103
|
adversarial_tdd_enabled: z.boolean().default(false).volatile(),
|
|
104
|
+
local_preflight_check: z.boolean().default(true).volatile(),
|
|
105
|
+
early_exit_consensus_enabled: z.boolean().default(false).volatile(),
|
|
106
|
+
early_exit_threshold: z.number().default(0.95).volatile(),
|
|
101
107
|
smart_routing_model: ModelSlotSchema.default({ provider: '', model: '' }).volatile(),
|
|
102
108
|
prices: z.dict(PriceRow).default({}).volatile(),
|
|
103
109
|
presets: z.array(PresetSchema).default([
|
|
@@ -159,3 +165,4 @@ export function plainConfig(value) {
|
|
|
159
165
|
}
|
|
160
166
|
return value
|
|
161
167
|
}
|
|
168
|
+
|
package/lib/moa-swarm.js
CHANGED
|
@@ -114,17 +114,23 @@ export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
|
114
114
|
if (wt.isGitWorktree) {
|
|
115
115
|
try {
|
|
116
116
|
await execFileAsync('git', ['worktree', 'remove', '--force', wt.worktreePath], { cwd, timeout: 8000 })
|
|
117
|
-
} catch {
|
|
117
|
+
} catch (err) {
|
|
118
|
+
logger.debug('[dsh-moa:swarm] cleanup worktree remove error:', err?.message || err)
|
|
119
|
+
}
|
|
118
120
|
}
|
|
119
121
|
try {
|
|
120
122
|
await fs.rm(wt.worktreePath, { recursive: true, force: true })
|
|
121
|
-
} catch {
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.debug('[dsh-moa:swarm] cleanup rm error:', err?.message || err)
|
|
125
|
+
}
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
if (await isGitRepository(cwd)) {
|
|
125
129
|
try {
|
|
126
130
|
await execFileAsync('git', ['worktree', 'prune'], { cwd, timeout: 5000 })
|
|
127
|
-
} catch {
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.debug('[dsh-moa:swarm] cleanup prune error:', err?.message || err)
|
|
133
|
+
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
const safeRunId = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '')
|
|
@@ -132,7 +138,9 @@ export async function cleanupSwarmWorktrees(cwd, runId, worktrees = []) {
|
|
|
132
138
|
const swarmRoot = path.join(cwd, '.moa', `swarm-${safeRunId}`)
|
|
133
139
|
try {
|
|
134
140
|
await fs.rm(swarmRoot, { recursive: true, force: true })
|
|
135
|
-
} catch {
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.debug('[dsh-moa:swarm] cleanup swarmRoot error:', err?.message || err)
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -217,3 +225,4 @@ export async function runSubagentSwarm(references, messages, options = {}, callL
|
|
|
217
225
|
await cleanupSwarmWorktrees(cwd, runId, worktrees)
|
|
218
226
|
}
|
|
219
227
|
}
|
|
228
|
+
|
package/lib/pipeline/context.js
CHANGED
|
@@ -38,6 +38,9 @@ export async function stageProjectContext({
|
|
|
38
38
|
const isBlindEvaluation = Boolean(preset?.blind_evaluation)
|
|
39
39
|
const isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
|
|
40
40
|
const allowCandidateOverride = Boolean(preset?.allow_candidate_override)
|
|
41
|
+
const localPreflightCheck = preset?.local_preflight_check !== false
|
|
42
|
+
const earlyExitConsensusEnabled = Boolean(preset?.early_exit_consensus_enabled)
|
|
43
|
+
const earlyExitThreshold = typeof preset?.early_exit_threshold === 'number' ? preset.early_exit_threshold : 0.95
|
|
41
44
|
|
|
42
45
|
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
43
46
|
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
@@ -123,5 +126,7 @@ export async function stageProjectContext({
|
|
|
123
126
|
allowCandidateOverride, collectedCtx, isRefinement, projectContext,
|
|
124
127
|
effectiveReferences, quotaSubstitutions, priorTurnBaseline, candidateSystemPrompt,
|
|
125
128
|
needsQuestions, enrichedMessages,
|
|
129
|
+
localPreflightCheck, earlyExitConsensusEnabled, earlyExitThreshold,
|
|
126
130
|
}
|
|
127
131
|
}
|
|
132
|
+
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Early Exit upon Semantic Consensus (#299)
|
|
3
|
+
* Detects convergence between reference candidates and bypasses judge round when consensus >= threshold.
|
|
4
|
+
* Saves up to 60-80% tokens and latency on requests where candidates converge.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from '../logger.js'
|
|
8
|
+
import { promoteCandidateWorkspace, cleanMoaWorkspaces } from '../workspace/fs.js'
|
|
9
|
+
import { createPromotedPreview } from '../live-canvas.js'
|
|
10
|
+
import { recordMoaRunAsync, candidatesForHistory } from '../history.js'
|
|
11
|
+
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
12
|
+
import { estimateTokenCost } from '../pricing.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes text/code by stripping comments and normalizing whitespace for consensus comparison.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeForConsensus(text = '') {
|
|
20
|
+
if (typeof text !== 'string') return ''
|
|
21
|
+
return text
|
|
22
|
+
// Strip block comments /* ... */
|
|
23
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
24
|
+
// Strip JS/TS single line comments // ...
|
|
25
|
+
.replace(/\/\/.*$/gm, '')
|
|
26
|
+
// Strip Python/Shell comments # ...
|
|
27
|
+
.replace(/(^|\s)#.*$/gm, '$1')
|
|
28
|
+
// Normalize newlines
|
|
29
|
+
.replace(/\r?\n/g, '\n')
|
|
30
|
+
// Collapse horizontal whitespace
|
|
31
|
+
.replace(/[ \t]+/g, ' ')
|
|
32
|
+
// Collapse multi-blank lines
|
|
33
|
+
.replace(/\n\s*\n/g, '\n')
|
|
34
|
+
.trim()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculates Sørensen-Dice similarity coefficient between two code/text snippets (0.0 to 1.0).
|
|
39
|
+
* @param {string} textA
|
|
40
|
+
* @param {string} textB
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function calculateSimilarity(textA = '', textB = '') {
|
|
44
|
+
const normA = normalizeForConsensus(textA)
|
|
45
|
+
const normB = normalizeForConsensus(textB)
|
|
46
|
+
|
|
47
|
+
if (normA === normB) return 1.0
|
|
48
|
+
if (!normA || !normB) return 0.0
|
|
49
|
+
|
|
50
|
+
const tokensA = normA.match(/[\w$]+|[^\s\w]/g) || []
|
|
51
|
+
const tokensB = normB.match(/[\w$]+|[^\s\w]/g) || []
|
|
52
|
+
|
|
53
|
+
if (tokensA.length === 0 && tokensB.length === 0) return 1.0
|
|
54
|
+
if (tokensA.length === 0 || tokensB.length === 0) return 0.0
|
|
55
|
+
|
|
56
|
+
const freqA = new Map()
|
|
57
|
+
for (const t of tokensA) freqA.set(t, (freqA.get(t) || 0) + 1)
|
|
58
|
+
|
|
59
|
+
const freqB = new Map()
|
|
60
|
+
for (const t of tokensB) freqB.set(t, (freqB.get(t) || 0) + 1)
|
|
61
|
+
|
|
62
|
+
let intersection = 0
|
|
63
|
+
for (const [token, countA] of freqA.entries()) {
|
|
64
|
+
const countB = freqB.get(token) || 0
|
|
65
|
+
intersection += Math.min(countA, countB)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dice = (2 * intersection) / (tokensA.length + tokensB.length)
|
|
69
|
+
return Number(dice.toFixed(4))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks if candidates have reached semantic consensus above threshold.
|
|
74
|
+
* @param {Array<object>} candidates
|
|
75
|
+
* @param {number} [threshold=0.95]
|
|
76
|
+
* @returns {{ reached: boolean, similarity: number, winnerIndex: number, consensusIndices: number[], winnerCandidate: object | null, reason: string }}
|
|
77
|
+
*/
|
|
78
|
+
export function checkSemanticConsensus(candidates = [], threshold = 0.95) {
|
|
79
|
+
if (!Array.isArray(candidates) || candidates.length < 2) {
|
|
80
|
+
return {
|
|
81
|
+
reached: false,
|
|
82
|
+
similarity: 0,
|
|
83
|
+
winnerIndex: 0,
|
|
84
|
+
consensusIndices: [],
|
|
85
|
+
winnerCandidate: null,
|
|
86
|
+
reason: 'Insufficient candidates for consensus check',
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const n = candidates.length
|
|
91
|
+
let maxSim = 0
|
|
92
|
+
let bestPair = [0, 1]
|
|
93
|
+
const matrix = Array.from({ length: n }, () => new Float32Array(n))
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < n; i++) {
|
|
96
|
+
matrix[i][i] = 1.0
|
|
97
|
+
for (let j = i + 1; j < n; j++) {
|
|
98
|
+
const sim = calculateSimilarity(candidates[i].text || '', candidates[j].text || '')
|
|
99
|
+
matrix[i][j] = sim
|
|
100
|
+
matrix[j][i] = sim
|
|
101
|
+
if (sim > maxSim) {
|
|
102
|
+
maxSim = sim
|
|
103
|
+
bestPair = [i, j]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (maxSim < threshold) {
|
|
109
|
+
return {
|
|
110
|
+
reached: false,
|
|
111
|
+
similarity: maxSim,
|
|
112
|
+
winnerIndex: 0,
|
|
113
|
+
consensusIndices: [],
|
|
114
|
+
winnerCandidate: null,
|
|
115
|
+
reason: `Max pairwise similarity ${maxSim} is below consensus threshold ${threshold}`,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Find consensus cluster matching threshold with the best pair
|
|
120
|
+
const clusterSet = new Set(bestPair)
|
|
121
|
+
for (let k = 0; k < n; k++) {
|
|
122
|
+
if (k === bestPair[0] || k === bestPair[1]) continue
|
|
123
|
+
if (matrix[k][bestPair[0]] >= threshold && matrix[k][bestPair[1]] >= threshold) {
|
|
124
|
+
clusterSet.add(k)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const consensusIndices = Array.from(clusterSet).map((idx) => candidates[idx].index ?? idx)
|
|
129
|
+
const clusterCandidates = Array.from(clusterSet).map((idx) => candidates[idx])
|
|
130
|
+
|
|
131
|
+
// Select winner from consensus cluster (prioritize valid syntax, then lowest error count)
|
|
132
|
+
let winner = clusterCandidates[0]
|
|
133
|
+
for (const c of clusterCandidates) {
|
|
134
|
+
if (c.syntaxValidation?.valid && !winner.syntaxValidation?.valid) {
|
|
135
|
+
winner = c
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
reached: true,
|
|
141
|
+
similarity: maxSim,
|
|
142
|
+
winnerIndex: winner.index ?? 1,
|
|
143
|
+
consensusIndices,
|
|
144
|
+
winnerCandidate: winner,
|
|
145
|
+
reason: `Semantic consensus reached (${maxSim} >= ${threshold}) across candidates ${consensusIndices.join(', ')}`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Estimates saved tokens and cost from bypassing the judge round.
|
|
151
|
+
* @param {Array<object>} candidates
|
|
152
|
+
* @param {string} prompt
|
|
153
|
+
* @param {number} [maxTokens=4096]
|
|
154
|
+
* @param {object} [prices={}]
|
|
155
|
+
* @returns {{ skippedTokens: number, skippedCostUsd: number }}
|
|
156
|
+
*/
|
|
157
|
+
export function estimateSkippedJudgeUsage(candidates = [], prompt = '', maxTokens = 4096, prices = {}) {
|
|
158
|
+
const promptChars = (prompt?.length || 0) + candidates.reduce((sum, c) => sum + (c.text?.length || 0), 0)
|
|
159
|
+
const estimatedInputTokens = Math.max(100, Math.round(promptChars / 3.8))
|
|
160
|
+
const estimatedOutputTokens = Math.min(Math.max(200, Math.round(maxTokens * 0.25)), 1000)
|
|
161
|
+
const skippedTokens = estimatedInputTokens + estimatedOutputTokens
|
|
162
|
+
|
|
163
|
+
const costEst = estimateTokenCost({ provider: 'default', model: 'judge' }, {
|
|
164
|
+
inputTokens: estimatedInputTokens,
|
|
165
|
+
outputTokens: estimatedOutputTokens,
|
|
166
|
+
}, prices)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
skippedTokens,
|
|
170
|
+
skippedCostUsd: Number((costEst?.costUsd || 0.005).toFixed(5)),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Evaluates candidates for early consensus and executes early exit bypass if reached.
|
|
176
|
+
* @param {object} params
|
|
177
|
+
* @returns {Promise<{ ok: boolean, consensusReached: boolean, payload?: object, error?: Error, abort?: boolean }>}
|
|
178
|
+
*/
|
|
179
|
+
export function stageEarlyConsensus({
|
|
180
|
+
successfulRefs = [],
|
|
181
|
+
referenceOutputs = [],
|
|
182
|
+
preset = {},
|
|
183
|
+
userPrompt = '',
|
|
184
|
+
prices = {},
|
|
185
|
+
maxTokens = 4096,
|
|
186
|
+
cwd = process.cwd(),
|
|
187
|
+
runId = '',
|
|
188
|
+
signal = null,
|
|
189
|
+
promoteOpts = {},
|
|
190
|
+
allowCandidateOverride = false,
|
|
191
|
+
isRefinement = false,
|
|
192
|
+
historyFilePath = null,
|
|
193
|
+
startTime = Date.now(),
|
|
194
|
+
collectedCtx = null,
|
|
195
|
+
liveCanvas = null,
|
|
196
|
+
ctx = null,
|
|
197
|
+
}) {
|
|
198
|
+
const enabled = Boolean(preset?.early_exit_consensus_enabled)
|
|
199
|
+
const threshold = typeof preset?.early_exit_threshold === 'number' ? preset.early_exit_threshold : 0.95
|
|
200
|
+
|
|
201
|
+
if (!enabled || successfulRefs.length < 2) {
|
|
202
|
+
return Promise.resolve({ ok: true, consensusReached: false })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const consensus = checkSemanticConsensus(successfulRefs, threshold)
|
|
206
|
+
if (!consensus.reached || !consensus.winnerCandidate) {
|
|
207
|
+
return Promise.resolve({ ok: true, consensusReached: false, similarity: consensus.similarity })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const winner = consensus.winnerCandidate
|
|
211
|
+
emitEvent(ctx, MOA_EVENTS.EARLY_CONSENSUS, {
|
|
212
|
+
runId,
|
|
213
|
+
similarity: consensus.similarity,
|
|
214
|
+
threshold,
|
|
215
|
+
winnerIndex: consensus.winnerIndex,
|
|
216
|
+
winnerModel: winner.label,
|
|
217
|
+
consensusCandidates: consensus.consensusIndices,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Promote winning candidate files
|
|
221
|
+
let promotedFiles = []
|
|
222
|
+
let promoteErr = null
|
|
223
|
+
const runPromotion = async () => {
|
|
224
|
+
if (winner.files?.length > 0) {
|
|
225
|
+
try {
|
|
226
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, winner.index, {
|
|
227
|
+
...promoteOpts,
|
|
228
|
+
runId,
|
|
229
|
+
keepMoa: allowCandidateOverride,
|
|
230
|
+
})
|
|
231
|
+
} catch (err) {
|
|
232
|
+
promoteErr = err
|
|
233
|
+
}
|
|
234
|
+
} else if (!allowCandidateOverride) {
|
|
235
|
+
await cleanMoaWorkspaces(cwd, { runId })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (promoteErr) {
|
|
239
|
+
if (!allowCandidateOverride) await cleanMoaWorkspaces(cwd, { runId })
|
|
240
|
+
if (signal?.aborted || promoteErr?.message?.includes('aborted')) {
|
|
241
|
+
return { ok: false, consensusReached: true, abort: true, error: new Error('Turn aborted') }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const skippedUsage = estimateSkippedJudgeUsage(successfulRefs, userPrompt, maxTokens, prices)
|
|
246
|
+
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs, {
|
|
247
|
+
winningIndex: winner.index,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0)
|
|
251
|
+
const totalCostUsd = Number(referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0).toFixed(5))
|
|
252
|
+
const durationMs = Date.now() - startTime
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await recordMoaRunAsync({
|
|
256
|
+
id: runId,
|
|
257
|
+
cwd,
|
|
258
|
+
prompt: userPrompt,
|
|
259
|
+
preset: preset?.name || 'default',
|
|
260
|
+
isRefinement,
|
|
261
|
+
isEarlyConsensus: true,
|
|
262
|
+
candidates: candidatesForHistory(referenceOutputs),
|
|
263
|
+
aggregator: 'Early Consensus (Bypassed)',
|
|
264
|
+
winnerIndex: winner.index,
|
|
265
|
+
winnerModel: winner.label,
|
|
266
|
+
promotedFiles,
|
|
267
|
+
totalTokens,
|
|
268
|
+
totalCostUsd,
|
|
269
|
+
skippedJudgeTokens: skippedUsage.skippedTokens,
|
|
270
|
+
durationMs,
|
|
271
|
+
}, historyFilePath || undefined)
|
|
272
|
+
} catch (histErr) {
|
|
273
|
+
logger.warn('[dsh-moa] Failed to record early consensus in history:', histErr?.message || histErr)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
emitEvent(ctx, MOA_EVENTS.RUN_FINISH, {
|
|
277
|
+
runId,
|
|
278
|
+
durationMs,
|
|
279
|
+
totalTokens,
|
|
280
|
+
totalCostUsd,
|
|
281
|
+
promotedFiles,
|
|
282
|
+
winnerIndex: winner.index,
|
|
283
|
+
isEarlyConsensus: true,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
consensusReached: true,
|
|
289
|
+
payload: {
|
|
290
|
+
kind: 'synthesis',
|
|
291
|
+
content: winner.text,
|
|
292
|
+
aggregator: 'Early Consensus (Bypassed)',
|
|
293
|
+
references: referenceOutputs,
|
|
294
|
+
presetName: preset?.name || 'default',
|
|
295
|
+
isRefinement,
|
|
296
|
+
isEarlyConsensus: true,
|
|
297
|
+
winningIndex: winner.index,
|
|
298
|
+
winnerModel: winner.label,
|
|
299
|
+
promotedFiles,
|
|
300
|
+
runId,
|
|
301
|
+
consensusInfo: {
|
|
302
|
+
similarity: consensus.similarity,
|
|
303
|
+
threshold,
|
|
304
|
+
consensusIndices: consensus.consensusIndices,
|
|
305
|
+
},
|
|
306
|
+
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
307
|
+
usage: {
|
|
308
|
+
totalTokens,
|
|
309
|
+
totalCostUsd,
|
|
310
|
+
skippedJudgeTokens: skippedUsage.skippedTokens,
|
|
311
|
+
skippedJudgeCostUsd: skippedUsage.skippedCostUsd,
|
|
312
|
+
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
313
|
+
aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
314
|
+
},
|
|
315
|
+
skippedFiles: collectedCtx?.skippedFiles || 0,
|
|
316
|
+
skippedList: collectedCtx?.skippedList || [],
|
|
317
|
+
durationMs,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return runPromotion()
|
|
323
|
+
}
|
|
324
|
+
|
package/lib/pipeline/runner.js
CHANGED
|
@@ -13,6 +13,8 @@ import { stagePeerCritique } from './peer-critique.js'
|
|
|
13
13
|
import { stageMultiJudge } from './multi-judge.js'
|
|
14
14
|
import { stageSynthesis } from './synthesis.js'
|
|
15
15
|
import { stagePromotion } from './promotion.js'
|
|
16
|
+
import { applySyntaxFilter, formatSyntaxDirective } from './syntax-filter.js'
|
|
17
|
+
import { stageEarlyConsensus } from './early-consensus.js'
|
|
16
18
|
import { emitEvent, MOA_EVENTS } from '../events.js'
|
|
17
19
|
|
|
18
20
|
export async function runMoAPipeline({
|
|
@@ -90,6 +92,16 @@ export async function runMoAPipeline({
|
|
|
90
92
|
})
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
// Pre-Judge Stage 2.5: Zero-Token Local Syntax Gate (#303)
|
|
96
|
+
const syntaxResult = applySyntaxFilter(successfulRefs, { enabled: ctxStage.localPreflightCheck })
|
|
97
|
+
emitEvent(ctx, MOA_EVENTS.SYNTAX_VALIDATION, {
|
|
98
|
+
runId,
|
|
99
|
+
filterApplied: syntaxResult.filterApplied,
|
|
100
|
+
validCount: syntaxResult.validCandidates.length,
|
|
101
|
+
invalidCount: syntaxResult.invalidCandidates.length,
|
|
102
|
+
})
|
|
103
|
+
const syntaxDirective = syntaxResult.filterApplied ? formatSyntaxDirective(referenceOutputs, { blind: ctxStage.isBlindEvaluation }) : null
|
|
104
|
+
|
|
93
105
|
// Stage 3: Clarifying Questions Branch
|
|
94
106
|
if (ctxStage.needsQuestions) {
|
|
95
107
|
return await stageClarifyingQuestions({
|
|
@@ -103,7 +115,8 @@ export async function runMoAPipeline({
|
|
|
103
115
|
// Stage 4: Fast Mode (1 candidate bypass)
|
|
104
116
|
if (ctxStage.isFastMode) {
|
|
105
117
|
const fastRes = await stageFastMode({
|
|
106
|
-
successfulRefs
|
|
118
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
119
|
+
referenceOutputs,
|
|
107
120
|
allowCandidateOverride: ctxStage.allowCandidateOverride,
|
|
108
121
|
promoteOpts, cwd, runId, signal, userPrompt, preset,
|
|
109
122
|
isRefinement: ctxStage.isRefinement, historyFilePath, startTime,
|
|
@@ -116,6 +129,32 @@ export async function runMoAPipeline({
|
|
|
116
129
|
return fastRes.payload
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
// Pre-Judge Stage 4.5: Early Exit upon Semantic Consensus (#299)
|
|
133
|
+
if (ctxStage.earlyExitConsensusEnabled && successfulRefs.length > 1) {
|
|
134
|
+
const earlyRes = await stageEarlyConsensus({
|
|
135
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
136
|
+
referenceOutputs,
|
|
137
|
+
preset,
|
|
138
|
+
userPrompt,
|
|
139
|
+
prices,
|
|
140
|
+
maxTokens: ctxStage.maxTokens,
|
|
141
|
+
cwd,
|
|
142
|
+
runId,
|
|
143
|
+
signal,
|
|
144
|
+
promoteOpts,
|
|
145
|
+
allowCandidateOverride: ctxStage.allowCandidateOverride,
|
|
146
|
+
isRefinement: ctxStage.isRefinement,
|
|
147
|
+
historyFilePath,
|
|
148
|
+
startTime,
|
|
149
|
+
collectedCtx: ctxStage.collectedCtx,
|
|
150
|
+
liveCanvas,
|
|
151
|
+
ctx,
|
|
152
|
+
})
|
|
153
|
+
if (earlyRes.consensusReached) {
|
|
154
|
+
return earlyRes.payload
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
if (signal?.aborted) {
|
|
120
159
|
await cleanMoaWorkspaces(cwd, { runId })
|
|
121
160
|
return { error: new Error('Turn aborted') }
|
|
@@ -124,7 +163,8 @@ export async function runMoAPipeline({
|
|
|
124
163
|
// Stage 5: Consilium Round 2 (Peer Critique)
|
|
125
164
|
const peerRes = await stagePeerCritique({
|
|
126
165
|
isPeerCritiqueEnabled: ctxStage.isPeerCritiqueEnabled,
|
|
127
|
-
successfulRefs
|
|
166
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
167
|
+
referenceOutputs, preset, prices,
|
|
128
168
|
maxTokens: ctxStage.maxTokens, primaryJudge: ctxStage.primaryJudge,
|
|
129
169
|
userPrompt, isBlindEvaluation: ctxStage.isBlindEvaluation,
|
|
130
170
|
refTemp: ctxStage.refTemp, refTimeoutSec: ctxStage.refTimeoutSec,
|
|
@@ -148,7 +188,9 @@ export async function runMoAPipeline({
|
|
|
148
188
|
|
|
149
189
|
// Stage 6: Multi-Judge Panel
|
|
150
190
|
const mjRes = await stageMultiJudge({
|
|
151
|
-
preset,
|
|
191
|
+
preset,
|
|
192
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
193
|
+
referenceOutputs, primaryJudge: ctxStage.primaryJudge,
|
|
152
194
|
userPrompt, judgeCriteria: ctxStage.judgeCriteria, prices, maxTokens: ctxStage.maxTokens,
|
|
153
195
|
callLlm, signal, onProgress, cwd, runId, isRefinement: ctxStage.isRefinement,
|
|
154
196
|
isFastMode: ctxStage.isFastMode, collectedCtx: ctxStage.collectedCtx, startTime,
|
|
@@ -176,11 +218,13 @@ export async function runMoAPipeline({
|
|
|
176
218
|
const rawFileDirective = (preset?.interactive_workbench_enabled)
|
|
177
219
|
? formatFileScoresDirective()
|
|
178
220
|
: null
|
|
179
|
-
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive].filter(Boolean).join("\n\n") || null
|
|
221
|
+
const fileScoringDirective = [rawFileDirective, visualResult?.directive, adversarialTddResult?.directive, syntaxDirective].filter(Boolean).join("\n\n") || null
|
|
180
222
|
|
|
181
223
|
// Stage 7: Synthesis Phase
|
|
182
224
|
const synthRes = await stageSynthesis({
|
|
183
|
-
userPrompt, referenceOutputs,
|
|
225
|
+
userPrompt, referenceOutputs,
|
|
226
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
227
|
+
judgeCriteria: ctxStage.judgeCriteria,
|
|
184
228
|
isCuratorSynthesis: ctxStage.isCuratorSynthesis, isBlindEvaluation: ctxStage.isBlindEvaluation,
|
|
185
229
|
priorTurnBaseline: ctxStage.priorTurnBaseline, compositeMergeDirective, fileScoringDirective,
|
|
186
230
|
multiJudgeResult, preset, prices, maxTokens: ctxStage.maxTokens, isFastMode: ctxStage.isFastMode,
|
|
@@ -200,7 +244,9 @@ export async function runMoAPipeline({
|
|
|
200
244
|
|
|
201
245
|
// Stage 8: Evaluation, Winner & Promotion
|
|
202
246
|
const promRes = await stagePromotion({
|
|
203
|
-
synthesizedText, referenceOutputs,
|
|
247
|
+
synthesizedText, referenceOutputs,
|
|
248
|
+
successfulRefs: syntaxResult.validCandidates.length > 0 ? syntaxResult.validCandidates : successfulRefs,
|
|
249
|
+
multiJudgeResult,
|
|
204
250
|
isCuratorSynthesis: ctxStage.isCuratorSynthesis,
|
|
205
251
|
allowCandidateOverride: ctxStage.allowCandidateOverride,
|
|
206
252
|
promoteOpts, cwd, runId, signal, liveCanvas, startTime, chosenJudge, aggUsage,
|
|
@@ -214,3 +260,4 @@ export async function runMoAPipeline({
|
|
|
214
260
|
}
|
|
215
261
|
return promRes.payload
|
|
216
262
|
}
|
|
263
|
+
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-Token Local Pre-Judge Linter & Syntax Filter (#303)
|
|
3
|
+
* Inspects candidate proposals locally before LLM judge invocation.
|
|
4
|
+
* Disqualifies or penalizes candidates with fatal syntax errors without spending judge tokens.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import vm from 'node:vm'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts markdown fenced code blocks.
|
|
12
|
+
* @param {string} text
|
|
13
|
+
* @returns {Array<{ language: string, code: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export function extractCodeBlocks(text = '') {
|
|
16
|
+
if (typeof text !== 'string') return []
|
|
17
|
+
const blocks = []
|
|
18
|
+
const regex = /(?:^|\n)```([a-zA-Z0-9_-]*)\r?\n([\s\S]*?)\r?\n```/g
|
|
19
|
+
let match
|
|
20
|
+
while ((match = regex.exec(text)) !== null) {
|
|
21
|
+
blocks.push({
|
|
22
|
+
language: (match[1] || '').trim().toLowerCase(),
|
|
23
|
+
code: match[2] || '',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return blocks
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks basic bracket and parenthesis balance.
|
|
31
|
+
* @param {string} code
|
|
32
|
+
* @returns {{ balanced: boolean, error?: string }}
|
|
33
|
+
*/
|
|
34
|
+
function checkBracketBalance(code = '') {
|
|
35
|
+
const stack = []
|
|
36
|
+
const pairs = { '(': ')', '{': '}', '[': ']' }
|
|
37
|
+
let inSingleQuote = false
|
|
38
|
+
let inDoubleQuote = false
|
|
39
|
+
let inTemplateLiteral = false
|
|
40
|
+
let inLineComment = false
|
|
41
|
+
let inBlockComment = false
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < code.length; i++) {
|
|
44
|
+
const ch = code[i]
|
|
45
|
+
const next = code[i + 1]
|
|
46
|
+
|
|
47
|
+
if (inLineComment) {
|
|
48
|
+
if (ch === '\n') inLineComment = false
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (inBlockComment) {
|
|
52
|
+
if (ch === '*' && next === '/') {
|
|
53
|
+
inBlockComment = false
|
|
54
|
+
i++
|
|
55
|
+
}
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if (ch === '/' && next === '/' && !inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
59
|
+
inLineComment = true
|
|
60
|
+
i++
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (ch === '/' && next === '*' && !inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
64
|
+
inBlockComment = true
|
|
65
|
+
i++
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (ch === "'" && !inDoubleQuote && !inTemplateLiteral && code[i - 1] !== '\\') {
|
|
70
|
+
inSingleQuote = !inSingleQuote
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (ch === '"' && !inSingleQuote && !inTemplateLiteral && code[i - 1] !== '\\') {
|
|
74
|
+
inDoubleQuote = !inDoubleQuote
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (ch === '`' && !inSingleQuote && !inDoubleQuote && code[i - 1] !== '\\') {
|
|
78
|
+
inTemplateLiteral = !inTemplateLiteral
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (inSingleQuote || inDoubleQuote || inTemplateLiteral) continue
|
|
83
|
+
|
|
84
|
+
if (ch === '(' || ch === '{' || ch === '[') {
|
|
85
|
+
stack.push({ ch, pos: i })
|
|
86
|
+
} else if (ch === ')' || ch === '}' || ch === ']') {
|
|
87
|
+
if (stack.length === 0) {
|
|
88
|
+
return { balanced: false, error: `Unmatched closing bracket '${ch}' at index ${i}` }
|
|
89
|
+
}
|
|
90
|
+
const top = stack.pop()
|
|
91
|
+
if (pairs[top.ch] !== ch) {
|
|
92
|
+
return { balanced: false, error: `Mismatched bracket: expected '${pairs[top.ch]}', found '${ch}'` }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (stack.length > 0) {
|
|
98
|
+
const top = stack[stack.length - 1]
|
|
99
|
+
return { balanced: false, error: `Unclosed bracket '${top.ch}' opened at index ${top.pos}` }
|
|
100
|
+
}
|
|
101
|
+
return { balanced: true }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validates syntax of a single code block.
|
|
106
|
+
* @param {string} code
|
|
107
|
+
* @param {string} language
|
|
108
|
+
* @returns {{ valid: boolean, error: string | null }}
|
|
109
|
+
*/
|
|
110
|
+
export function validateCodeBlockSyntax(code = '', language = '') {
|
|
111
|
+
const lang = (language || '').toLowerCase().trim()
|
|
112
|
+
const trimmed = code.trim()
|
|
113
|
+
if (!trimmed) return { valid: true, error: null }
|
|
114
|
+
|
|
115
|
+
// 1. JSON validation
|
|
116
|
+
if (lang === 'json') {
|
|
117
|
+
try {
|
|
118
|
+
JSON.parse(trimmed)
|
|
119
|
+
return { valid: true, error: null }
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { valid: false, error: `JSON Parse Error: ${err.message}` }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. JavaScript / TypeScript validation
|
|
126
|
+
if (['js', 'javascript', 'mjs', 'cjs', 'ts', 'typescript', 'jsx', 'tsx'].includes(lang)) {
|
|
127
|
+
// Check bracket balance
|
|
128
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
129
|
+
if (!bracketCheck.balanced) {
|
|
130
|
+
return { valid: false, error: bracketCheck.error }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try vm.Script for JS (stripping import/export and TypeScript type annotations)
|
|
134
|
+
try {
|
|
135
|
+
const sanitized = trimmed
|
|
136
|
+
.replace(/^([ \t]*)export\s+default\s+/gm, '$1')
|
|
137
|
+
.replace(/^([ \t]*)export\s*\{[^}]*\}\s*;?/gm, '$1')
|
|
138
|
+
.replace(/^([ \t]*)export\s+(const|let|var|function|class|async\s+function)\s+/gm, '$1$2 ')
|
|
139
|
+
.replace(/^([ \t]*)import\s+[^;\n]+;?/gm, '$1')
|
|
140
|
+
.replace(/^([ \t]*)export\s+\*\s+from\s+[^;\n]+;?/gm, '$1')
|
|
141
|
+
// Strip TS interface / type declarations
|
|
142
|
+
.replace(/^([ \t]*)interface\s+\w+(\s*<[^>]+>)?(\s+extends\s+[^{]+)?\s*\{[\s\S]*?\}/gm, '')
|
|
143
|
+
.replace(/^([ \t]*)type\s+\w+(\s*<[^>]+>)?\s*=[\s\S]*?;/gm, '')
|
|
144
|
+
|
|
145
|
+
// Wrap in async IIFE to allow top-level await and return statements
|
|
146
|
+
const wrapped = `(async function() {\n${sanitized}\n});`
|
|
147
|
+
new vm.Script(wrapped)
|
|
148
|
+
return { valid: true, error: null }
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// In TS code, type annotations might trigger syntax error in raw vm.Script
|
|
151
|
+
if (['ts', 'typescript', 'tsx'].includes(lang)) {
|
|
152
|
+
// If bracket check passed, treat as valid TS fallback
|
|
153
|
+
return { valid: true, error: null }
|
|
154
|
+
}
|
|
155
|
+
return { valid: false, error: `JS Syntax Error: ${err.message}` }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. Python validation
|
|
160
|
+
if (['py', 'python'].includes(lang)) {
|
|
161
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
162
|
+
if (!bracketCheck.balanced) {
|
|
163
|
+
return { valid: false, error: `Python syntax error: ${bracketCheck.error}` }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const lines = trimmed.split(/\r?\n/)
|
|
167
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
168
|
+
const line = lines[idx].trim()
|
|
169
|
+
if (!line || line.startsWith('#')) continue
|
|
170
|
+
// Lines starting with block keywords must end with ':'
|
|
171
|
+
const blockKeywords = /^(def\s+\w+|class\s+\w+|if\s+|elif\s+|else:|while\s+|for\s+|try:|except(\s+.*)?:|finally:|with\s+)/
|
|
172
|
+
if (blockKeywords.test(line)) {
|
|
173
|
+
// If line is incomplete or doesn't end with ':' (accounting for trailing comments)
|
|
174
|
+
const codePart = line.split('#')[0].trim()
|
|
175
|
+
if (!codePart.endsWith(':') && !codePart.endsWith('\\') && !codePart.endsWith('(')) {
|
|
176
|
+
return { valid: false, error: `Python Syntax Error at line ${idx + 1}: expected ':' at end of '${codePart}'` }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { valid: true, error: null }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Generic fallback: check balanced brackets
|
|
184
|
+
const bracketCheck = checkBracketBalance(trimmed)
|
|
185
|
+
if (!bracketCheck.balanced) {
|
|
186
|
+
return { valid: false, error: bracketCheck.error }
|
|
187
|
+
}
|
|
188
|
+
return { valid: true, error: null }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Validates all code blocks and files in a candidate proposal.
|
|
193
|
+
* @param {object} candidate
|
|
194
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[], blocksCount: number, validCount: number }}
|
|
195
|
+
*/
|
|
196
|
+
export function validateCandidateSyntax(candidate = {}) {
|
|
197
|
+
const errors = []
|
|
198
|
+
const warnings = []
|
|
199
|
+
let blocksCount = 0
|
|
200
|
+
let validCount = 0
|
|
201
|
+
|
|
202
|
+
// 1. Check markdown code blocks
|
|
203
|
+
if (typeof candidate.text === 'string') {
|
|
204
|
+
const blocks = extractCodeBlocks(candidate.text)
|
|
205
|
+
for (const b of blocks) {
|
|
206
|
+
blocksCount++
|
|
207
|
+
const res = validateCodeBlockSyntax(b.code, b.language)
|
|
208
|
+
if (res.valid) {
|
|
209
|
+
validCount++
|
|
210
|
+
} else {
|
|
211
|
+
errors.push(`[Block ${blocksCount} (${b.language || 'code'})]: ${res.error}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. Check candidate workspace files
|
|
217
|
+
if (Array.isArray(candidate.files)) {
|
|
218
|
+
for (const file of candidate.files) {
|
|
219
|
+
if (!file || typeof file.content !== 'string' || !file.relativePath) continue
|
|
220
|
+
blocksCount++
|
|
221
|
+
const ext = path.extname(file.relativePath).toLowerCase().replace('.', '')
|
|
222
|
+
const res = validateCodeBlockSyntax(file.content, ext)
|
|
223
|
+
if (res.valid) {
|
|
224
|
+
validCount++
|
|
225
|
+
} else {
|
|
226
|
+
errors.push(`[File ${file.relativePath}]: ${res.error}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const valid = errors.length === 0
|
|
232
|
+
return {
|
|
233
|
+
valid,
|
|
234
|
+
errors,
|
|
235
|
+
warnings,
|
|
236
|
+
blocksCount,
|
|
237
|
+
validCount,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Applies local zero-token syntax filter across all candidate outputs.
|
|
243
|
+
* @param {Array<object>} candidates
|
|
244
|
+
* @param {object} [options]
|
|
245
|
+
* @returns {{ candidates: Array<object>, validCandidates: Array<object>, invalidCandidates: Array<object>, filterApplied: boolean }}
|
|
246
|
+
*/
|
|
247
|
+
export function applySyntaxFilter(candidates = [], options = {}) {
|
|
248
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
249
|
+
return { candidates: [], validCandidates: [], invalidCandidates: [], filterApplied: false }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const enabled = options.enabled !== false
|
|
253
|
+
if (!enabled) {
|
|
254
|
+
return { candidates, validCandidates: candidates, invalidCandidates: [], filterApplied: false }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const validCandidates = []
|
|
258
|
+
const invalidCandidates = []
|
|
259
|
+
|
|
260
|
+
for (const cand of candidates) {
|
|
261
|
+
const syntax = validateCandidateSyntax(cand)
|
|
262
|
+
cand.syntaxValidation = syntax
|
|
263
|
+
|
|
264
|
+
if (syntax.valid) {
|
|
265
|
+
validCandidates.push(cand)
|
|
266
|
+
} else {
|
|
267
|
+
invalidCandidates.push(cand)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If we have at least one valid candidate and some broken candidates,
|
|
272
|
+
// disqualify broken candidates from winning
|
|
273
|
+
if (validCandidates.length > 0 && invalidCandidates.length > 0) {
|
|
274
|
+
for (const invalid of invalidCandidates) {
|
|
275
|
+
invalid.disqualified = true
|
|
276
|
+
invalid.disqualificationReason = `Failed local pre-judge syntax filter (${invalid.syntaxValidation.errors.length} error(s))`
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
candidates,
|
|
282
|
+
validCandidates: validCandidates.length > 0 ? validCandidates : candidates,
|
|
283
|
+
invalidCandidates,
|
|
284
|
+
filterApplied: true,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Formats an advisory syntax report directive for the judge.
|
|
290
|
+
* @param {Array<object>} candidates
|
|
291
|
+
* @param {object} [options]
|
|
292
|
+
* @param {boolean} [options.blind=false]
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
export function formatSyntaxDirective(candidates = [], options = {}) {
|
|
296
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return ''
|
|
297
|
+
const isBlind = Boolean(options.blind)
|
|
298
|
+
const lines = ['### Pre-Judge Local Syntax Gate Results (0 Tokens Spent):']
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
301
|
+
const cand = candidates[i]
|
|
302
|
+
const v = cand.syntaxValidation
|
|
303
|
+
const label = isBlind ? `Candidate ${cand.index ?? (i + 1)}` : (cand.label || `Candidate ${cand.index ?? (i + 1)}`)
|
|
304
|
+
if (!v) {
|
|
305
|
+
lines.push(`- **${label}**: Not evaluated`)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (v.valid) {
|
|
310
|
+
lines.push(`- **${label}**: PASSED (${v.validCount}/${v.blocksCount} code block(s) syntactically valid)`)
|
|
311
|
+
} else {
|
|
312
|
+
lines.push(`- **${label}**: FAILED (${v.errors.length} syntax error(s)):`)
|
|
313
|
+
for (const err of v.errors.slice(0, 3)) {
|
|
314
|
+
lines.push(` * ${err}`)
|
|
315
|
+
}
|
|
316
|
+
if (v.errors.length > 3) {
|
|
317
|
+
lines.push(` * ...and ${v.errors.length - 3} more syntax error(s)`)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines.join('\n')
|
|
323
|
+
}
|
|
324
|
+
|
package/lib/tui-settings.js
CHANGED
|
@@ -130,7 +130,7 @@ export function applyTuiSettings(ctx, { live }) {
|
|
|
130
130
|
const eff = typeof tuiCtx.effect === 'function' ? tuiCtx.effect : (typeof ctx?.effect === 'function' ? ctx.effect : null)
|
|
131
131
|
if (eff) {
|
|
132
132
|
eff(() => () => {
|
|
133
|
-
try { if (typeof dispose === 'function') dispose() } catch {}
|
|
133
|
+
try { if (typeof dispose === 'function') dispose() } catch (err) { /* best-effort dispose */ }
|
|
134
134
|
}, 'dsh-moa: tui settings')
|
|
135
135
|
}
|
|
136
136
|
} catch (err) {
|
|
@@ -144,3 +144,4 @@ export function applyTuiSettings(ctx, { live }) {
|
|
|
144
144
|
ctx.inject(['tuiSettingsSections'], registerWithService)
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-moa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.72",
|
|
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",
|
|
@@ -72,3 +72,4 @@
|
|
|
72
72
|
"react-dom": "^18.3.1"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
|