@goodandready/dsh-moa 0.2.55 → 0.2.57
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 +3 -2
- package/README.ru.md +2 -1
- package/README.zh.md +4 -0
- package/lib/client.js +495 -0
- package/lib/index.js +9 -0
- package/lib/moa-prompts.js +2 -0
- package/lib/moa-runner.js +19 -14
- package/lib/moa-sandbox.js +287 -0
- package/lib/moa-schema.js +1 -0
- package/lib/moa-test-gate.js +46 -155
- package/lib/moa-workbench.js +481 -0
- package/lib/routes.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ Single-model AI generation often suffers from blind spots, single-perspective bi
|
|
|
51
51
|
8. **Fast Mode & Custom Judge Criteria**: Ultra-fast single-model preset for quick tasks and customizable evaluation guidelines for the judge.
|
|
52
52
|
9. **Run History & Win-Rate Leaderboard**: Persistent logging of every run kind (synthesis, fast mode, questionnaire) with built-in REST endpoints (`/dsh-moa/history`, `/dsh-moa/leaderboard`, `/dsh-moa/runs/<id>`).
|
|
53
53
|
10. **Live Canvas 1-Click Preview (optional)**: when the `@goodandready/dsh-live-canvas` plugin is installed in the same profile, the promoted HTML is pushed to its sandbox and the MoA answer carries a one-click preview link; without it the step is skipped silently.
|
|
54
|
+
11. **Interactive Workbench (File Scoring 1–10, User Pick & Mix, Top-Model Glue & Judge Re-Check)**: Judge scores each candidate file (1–10) with pros and cons. Users can mix and match files across proposals in the WebUI. The turn's top-performing reference model harmonizes cross-file imports and contracts, followed by a final judge re-audit before promotion. Companion integration with [`dsh-time-machine`](https://github.com/goodandready/dsh-time-machine) for safe pre-promotion rollback checkpoints.
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
@@ -150,7 +151,7 @@ Inspect line-by-line differences between candidate proposals and the curator's s
|
|
|
150
151
|
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).
|
|
151
152
|
|
|
152
153
|
### 9. Automated Test Execution Gate
|
|
153
|
-
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.
|
|
154
|
+
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 under Node.js native permission model (`--permission`) and an ephemeral runtime fence (`lib/moa-sandbox.js`). Without OS-level container isolation (bubblewrap), child process execution is strictly prohibited (`--allow-child-process` is omitted) to prevent candidate scripts from stripping `NODE_OPTIONS` and escaping sandbox restrictions (#260). Package manager commands (`npm test`, `pnpm test`) are resolved directly to underlying `package.json` test scripts, while any evasion patterns (`env -u NODE_OPTIONS`, `unset NODE_OPTIONS`, computed shell evasion) are detected and fail-closed prior to execution. The judge receives concrete test outcomes, durations, and output logs to ground decisions in objective verification.
|
|
154
155
|
|
|
155
156
|
### 10. Multi-Judge Panel & Consensus Voting
|
|
156
157
|
When `multi_judge_enabled: true`, candidate solutions are independently evaluated by a panel of judge models. Winner selection supports `majority`, `highest_score`, or `unanimous` consensus strategies.
|
|
@@ -335,7 +336,7 @@ npm test
|
|
|
335
336
|
## 🛠️ Internal Tooling & Development
|
|
336
337
|
|
|
337
338
|
For local verification and package integrity validation:
|
|
338
|
-
- `npm test`: runs the full test suite (
|
|
339
|
+
- `npm test`: runs the full test suite (294 tests)
|
|
339
340
|
- `./deploy.sh`: local infrastructure validation script (verifies package size, identity parity in package.json/cordis.patch.yml/client.js, and tests). Excluded from the published npm package.
|
|
340
341
|
|
|
341
342
|
## 📄 License
|
package/README.ru.md
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
8. **Быстрый режим (Fast Mode) и критерии судьи**: Режим для одиночных быстрых задач без судьи и гибкая настройка фокуса оценки (безопасность, производительность, минимализм).
|
|
52
52
|
9. **История запусков и лидерборд моделей**: Персистентное логирование всех видов запусков (синтез, fast mode, опросник) и REST-эндпоинты (`/dsh-moa/history`, `/dsh-moa/leaderboard`, `/dsh-moa/runs/<id>`).
|
|
53
53
|
10. **Live Canvas 1-клик предпросмотр (опционально)**: если в профиле установлен `@goodandready/dsh-live-canvas`, промоученный HTML отправляется в его песочницу, а ответ MoA содержит ссылку на предпросмотр в 1 клик; без плагина шаг тихо пропускается.
|
|
54
|
+
11. **Интерактивный воркбенч (Оценка файлов 1–10, выбор в UI, склейка лучшей моделью и повторный аудит)**: Судья оценивает каждый сгенерированный файл каждого кандидата (1–10) с указанием ключевых плюсов и минусов. Пользователь комбинирует желаемый набор файлов в WebUI; лучшая модель раунда вызывается для гармонизации импортов и контрактов, после чего судья выполняет финальную валидацию перед промоушеном. Совместимо с [`dsh-time-machine`](https://github.com/goodandready/dsh-time-machine) для безопасных теневых снимков Git.
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
@@ -150,7 +151,7 @@ graph TD
|
|
|
150
151
|
Перед промоушном файлов победителя в корень рабочей области `dsh-moa` автоматически обращается к сервису `dsh-time-machine` для создания снимка (`moa-pre-promotion: candidate-N`). Если плагин недоступен или создание снимка завершилось ошибкой, перезапись файлов блокируется во избежание потери несохранённых данных (переопределяется параметром `force: true`, только если создание снимков намеренно отключено).
|
|
151
152
|
|
|
152
153
|
### 9. Автоматический шлюз выполнения тестов (Automated Test Execution Gate)
|
|
153
|
-
При включении `test_gate_enabled: true` и указании тестовой команды (`test_command`, например `npm test` или `pytest`) каждый кандидат запускается в изолированном оверлейном окружении
|
|
154
|
+
При включении `test_gate_enabled: true` и указании тестовой команды (`test_command`, например `npm test` или `pytest`) каждый кандидат запускается в изолированном оверлейном окружении под нативной моделью прав Node.js (`--permission`) и рантайм-фенсом (`lib/moa-sandbox.js`). При отсутствии изоляции уровня ОС (bubblewrap) создание дочерних процессов строго запрещено (флаг `--allow-child-process` исключён), что предотвращает обход песочницы через сброс `NODE_OPTIONS` (#260). Команды пакетных менеджеров (`npm test`, `pnpm test`) разрешаются напрямую в скрипты `package.json`, а любые векторы уклонения (`env -u NODE_OPTIONS`, `unset NODE_OPTIONS`, base64/eval/pipes) блокируются до запуска. Судья получает объективный вердикт с кодами возврата, временем исполнения и логами тестов до вынесения итогового решения.
|
|
154
155
|
|
|
155
156
|
### 10. Коллегия судей и консенсусное голосование (Multi-Judge Panel)
|
|
156
157
|
При активации опции `multi_judge_enabled` решения кандидатов оценивает независимая панель судейских моделей с поддержкой стратегий голосования: `majority` (большинство), `highest_score` (наивысший средний балл) или `unanimous` (единогласный консенсус).
|
package/README.zh.md
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
8. **快速模式与自定义评审标准**:面向快速任务的单模型极简管线,以及可自定义的裁判评审准则。
|
|
52
52
|
9. **运行历史与胜率排行榜**:对每一类运行(综合、快速模式、问卷)进行持久化记录,并内置 REST 端点(`/dsh-moa/history`、`/dsh-moa/leaderboard`、`/dsh-moa/runs/<id>`)。
|
|
53
53
|
10. **Live Canvas 一键预览(可选)**:当同一 profile 中安装了 `@goodandready/dsh-live-canvas` 时,提升到项目根目录的 HTML 会被推入其沙箱,MoA 回答附带一键预览链接;未安装时该步骤静默跳过。
|
|
54
|
+
11. **交互式工作台 (1-10分文件打分、自由混搭、最优模型粘合与裁判复审)**:裁判对每个候选模型生成的每个文件进行 1–10 分打分并列出优缺点。用户可在 WebUI 中自由勾选组合所需文件;当轮最高分模型被调用以粘合协同跨文件导入与类型接口,随后由裁判进行最终复审。支持配套插件 [`dsh-time-machine`](https://github.com/goodandready/dsh-time-machine) 在文件提升前创建影子 Git 回滚快照。
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
@@ -150,6 +151,9 @@ graph TD
|
|
|
150
151
|
### 8. 提升前 Git 快照检查点 (`dsh-time-machine`)
|
|
151
152
|
在将胜出候选者的文件提升并覆盖至项目根目录前,`dsh-moa` 自动调用本地 `dsh-time-machine` 建立影子 Git 检查点 (`moa-pre-promotion: candidate-N`)。如 `dsh-time-machine` 未安装或无法访问,文件提升将被阻断以防止未备份的项目文件被意外覆盖(仅在明确不需要检查点时可通过 `force: true` 覆盖)。
|
|
152
153
|
|
|
154
|
+
### 9. 自动化测试执行网关 (Automated Test Execution Gate)
|
|
155
|
+
当启用 `test_gate_enabled: true` 并配置 `test_command`(如 `npm test` 或 `pytest`)时,候选代码将在临时沙箱覆盖层中执行。沙箱基于 Node.js 原生权限模型(`--permission`)与运行时防护栏(`lib/moa-sandbox.js`)。在无操作系统级容器隔离(bubblewrap)时,严格禁用子进程创建(省略 `--allow-child-process`),杜绝候选代码通过剥离 `NODE_OPTIONS` 逃逸沙箱 (#260)。包管理器命令(`npm test`)直接映射到底层脚本,并在执行前严密拦截任何规避意图(`env -u NODE_OPTIONS`、`unset NODE_OPTIONS` 等)。裁判在最终决策前将获取客观的测试结果、耗时与输出日志。
|
|
156
|
+
|
|
153
157
|
### 18. 平滑降级与本地容灾恢复 (Local Fallback Resilience)
|
|
154
158
|
当 `local_fallback_enabled: true` 时,因网络中断、超时或速率限制(429/500)而失败的候选模型会自动使用配置的本地模型(Ollama / MiniPC)恢复。每个后备模型都拥有独立的超时执行配额与关联主 turn signal 的隔离取消控制器,防止云端超时耗尽本地恢复时间。同时严格检测终止错误与流中断,确保安全容灾且杜绝已取消 turn 的延迟文件提升。
|
|
155
159
|
|
package/lib/client.js
CHANGED
|
@@ -204,6 +204,31 @@ window.__ModuleLoader__.load({
|
|
|
204
204
|
'diff.select_run': 'Run:',
|
|
205
205
|
'diff.latest_run': 'Latest / Active Workspace',
|
|
206
206
|
'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
|
|
207
|
+
|
|
208
|
+
// Interactive Workbench (#251)
|
|
209
|
+
'aggregator.workbench_title': '🛠️ Interactive Workbench (1-10 File Scoring & Top-Model Glue)',
|
|
210
|
+
'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.',
|
|
211
|
+
'aggregator.workbench_companion': 'Companion Plugin:',
|
|
212
|
+
'aggregator.workbench_companion_note': 'safe shadow Git checkpoints before file promotion',
|
|
213
|
+
'workbench.title': '🛠️ Interactive Synthesis Workbench',
|
|
214
|
+
'workbench.desc': 'Inspect candidate file quality scores (1–10), mix and match files across proposals, harmonize with top-model glue, and run a final judge audit.',
|
|
215
|
+
'workbench.select_run': 'MoA Run:',
|
|
216
|
+
'workbench.latest_run': 'Select Run…',
|
|
217
|
+
'workbench.no_runs': 'No MoA runs with file candidates recorded yet.',
|
|
218
|
+
'workbench.empty_files': 'No candidate files found for this run.',
|
|
219
|
+
'workbench.file_header': 'Project File',
|
|
220
|
+
'workbench.candidate_header': 'Candidate Versions & Scores (1-10)',
|
|
221
|
+
'workbench.instructions_label': 'Harmonization Instructions (Optional):',
|
|
222
|
+
'workbench.instructions_placeholder': 'e.g. Ensure all imports match, unify TypeScript types, align function signatures...',
|
|
223
|
+
'workbench.assemble_btn': '🚀 Harmonize & Assemble (Top-Model Glue)',
|
|
224
|
+
'workbench.assembling': '⏳ Harmonizing files with Top Model...',
|
|
225
|
+
'workbench.recheck_title': '⚖️ Judge Re-Audit Verdict',
|
|
226
|
+
'workbench.audit_passed': 'PASSED',
|
|
227
|
+
'workbench.audit_failed': 'FAILED',
|
|
228
|
+
'workbench.promote_btn': 'Promote Harmonized Files to Workspace',
|
|
229
|
+
'workbench.promoted_ok': '✅ Harmonized deliverable promoted to workspace root!',
|
|
230
|
+
'workbench.pros': 'Pros:',
|
|
231
|
+
'workbench.cons': 'Cons:',
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
const zh = {
|
|
@@ -383,6 +408,31 @@ window.__ModuleLoader__.load({
|
|
|
383
408
|
'diff.select_run': '运行记录:',
|
|
384
409
|
'diff.latest_run': '最新 / 当前工作区',
|
|
385
410
|
'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
|
|
411
|
+
|
|
412
|
+
// Interactive Workbench (#251)
|
|
413
|
+
'aggregator.workbench_title': '🛠️ 交互式工作台 (1-10分文件打分与最优模型粘合)',
|
|
414
|
+
'aggregator.workbench_desc': '裁判为每个候选文件打分 (1-10) 并标注优缺点。在 UI 中自由组合各候选文件,由当轮最高分模型进行代码粘合统一,最终由裁判复审。',
|
|
415
|
+
'aggregator.workbench_companion': '配套插件:',
|
|
416
|
+
'aggregator.workbench_companion_note': '文件提升前安全创建影子 Git 快照',
|
|
417
|
+
'workbench.title': '🛠️ 交互式合成工作台',
|
|
418
|
+
'workbench.desc': '查看候选模型的文件质量打分 (1–10),自由混搭组合目标文件,由最高分模型粘合统一并执行最终裁判复审。',
|
|
419
|
+
'workbench.select_run': 'MoA 运行:',
|
|
420
|
+
'workbench.latest_run': '选择运行记录…',
|
|
421
|
+
'workbench.no_runs': '暂无生成文件的 MoA 记录。',
|
|
422
|
+
'workbench.empty_files': '此运行记录未发现候选文件。',
|
|
423
|
+
'workbench.file_header': '项目文件',
|
|
424
|
+
'workbench.candidate_header': '候选版本与评分 (1-10)',
|
|
425
|
+
'workbench.instructions_label': '粘合协同指令(可选):',
|
|
426
|
+
'workbench.instructions_placeholder': '例如:对齐导入路径、统一 TypeScript 类型、规范导出接口...',
|
|
427
|
+
'workbench.assemble_btn': '🚀 粘合与装配 (最优模型)',
|
|
428
|
+
'workbench.assembling': '⏳ 正在通过最优模型协同粘合文件...',
|
|
429
|
+
'workbench.recheck_title': '⚖️ 裁判复审判定',
|
|
430
|
+
'workbench.audit_passed': '通过 (PASSED)',
|
|
431
|
+
'workbench.audit_failed': '未通过 (FAILED)',
|
|
432
|
+
'workbench.promote_btn': '提升粘合结果至项目根目录',
|
|
433
|
+
'workbench.promoted_ok': '✅ 粘合方案已成功提升至项目工作区!',
|
|
434
|
+
'workbench.pros': '优势:',
|
|
435
|
+
'workbench.cons': '不足:',
|
|
386
436
|
}
|
|
387
437
|
|
|
388
438
|
|
|
@@ -1023,6 +1073,408 @@ window.__ModuleLoader__.load({
|
|
|
1023
1073
|
)
|
|
1024
1074
|
)
|
|
1025
1075
|
}
|
|
1076
|
+
function InteractiveWorkbenchSection({ t, currentRefs = [] }) {
|
|
1077
|
+
const [historyRuns, setHistoryRuns] = React.useState([])
|
|
1078
|
+
const [selectedRunId, setSelectedRunId] = React.useState('')
|
|
1079
|
+
const [workbenchData, setWorkbenchData] = React.useState(null)
|
|
1080
|
+
const [loading, setLoading] = React.useState(false)
|
|
1081
|
+
const [error, setError] = React.useState(null)
|
|
1082
|
+
const [selections, setSelections] = React.useState({})
|
|
1083
|
+
const [instructions, setInstructions] = React.useState('')
|
|
1084
|
+
const [assembling, setAssembling] = React.useState(false)
|
|
1085
|
+
const [assembleResult, setAssembleResult] = React.useState(null)
|
|
1086
|
+
const [assembleError, setAssembleError] = React.useState(null)
|
|
1087
|
+
const [promoting, setPromoting] = React.useState(false)
|
|
1088
|
+
const [promoted, setPromoted] = React.useState(false)
|
|
1089
|
+
|
|
1090
|
+
React.useEffect(() => {
|
|
1091
|
+
fetch('/dsh-moa/history?limit=30', { cache: 'no-store' })
|
|
1092
|
+
.then((r) => r.json())
|
|
1093
|
+
.then((data) => {
|
|
1094
|
+
if (data && data.ok && Array.isArray(data.runs)) {
|
|
1095
|
+
setHistoryRuns(data.runs)
|
|
1096
|
+
if (!selectedRunId && data.runs.length > 0) {
|
|
1097
|
+
const runWithFiles = data.runs.find((r) => r.candidates?.some((c) => c.files?.length > 0)) || data.runs[0]
|
|
1098
|
+
if (runWithFiles?.id) setSelectedRunId(runWithFiles.id)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
})
|
|
1102
|
+
.catch(() => {})
|
|
1103
|
+
}, [])
|
|
1104
|
+
|
|
1105
|
+
React.useEffect(() => {
|
|
1106
|
+
if (!selectedRunId) return
|
|
1107
|
+
setLoading(true)
|
|
1108
|
+
setError(null)
|
|
1109
|
+
setAssembleResult(null)
|
|
1110
|
+
setAssembleError(null)
|
|
1111
|
+
setPromoted(false)
|
|
1112
|
+
|
|
1113
|
+
fetch('/dsh-moa/runs/' + encodeURIComponent(selectedRunId) + '/workbench', { cache: 'no-store' })
|
|
1114
|
+
.then(async (r) => {
|
|
1115
|
+
if (!r.ok) {
|
|
1116
|
+
const errJson = await r.json().catch(() => ({}))
|
|
1117
|
+
throw new Error(errJson.error || ('HTTP ' + r.status))
|
|
1118
|
+
}
|
|
1119
|
+
return r.json()
|
|
1120
|
+
})
|
|
1121
|
+
.then((data) => {
|
|
1122
|
+
setWorkbenchData(data)
|
|
1123
|
+
const initial = {}
|
|
1124
|
+
if (data && data.fileScores && data.fileScores.byFile) {
|
|
1125
|
+
for (const [file, scores] of Object.entries(data.fileScores.byFile)) {
|
|
1126
|
+
if (Array.isArray(scores) && scores.length > 0) {
|
|
1127
|
+
const sorted = [...scores].sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
1128
|
+
initial[file] = sorted[0].candidateIndex
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} else if (data && Array.isArray(data.candidates)) {
|
|
1132
|
+
for (const cand of data.candidates) {
|
|
1133
|
+
for (const f of (cand.files || [])) {
|
|
1134
|
+
if (!initial[f.relativePath]) initial[f.relativePath] = cand.index
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
setSelections(initial)
|
|
1139
|
+
})
|
|
1140
|
+
.catch((err) => {
|
|
1141
|
+
setError(err?.message || String(err))
|
|
1142
|
+
})
|
|
1143
|
+
.finally(() => setLoading(false))
|
|
1144
|
+
}, [selectedRunId])
|
|
1145
|
+
|
|
1146
|
+
const handleAssemble = () => {
|
|
1147
|
+
if (!selectedRunId || Object.keys(selections).length === 0) return
|
|
1148
|
+
setAssembling(true)
|
|
1149
|
+
setAssembleResult(null)
|
|
1150
|
+
setAssembleError(null)
|
|
1151
|
+
setPromoted(false)
|
|
1152
|
+
|
|
1153
|
+
const selArray = Object.entries(selections).map(([file, candidateIndex]) => ({
|
|
1154
|
+
file,
|
|
1155
|
+
candidateIndex: Number(candidateIndex),
|
|
1156
|
+
}))
|
|
1157
|
+
|
|
1158
|
+
fetch('/dsh-moa/runs/' + encodeURIComponent(selectedRunId) + '/workbench/assemble', {
|
|
1159
|
+
method: 'POST',
|
|
1160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1161
|
+
body: JSON.stringify({
|
|
1162
|
+
selections: selArray,
|
|
1163
|
+
instructions,
|
|
1164
|
+
autoPromote: false,
|
|
1165
|
+
}),
|
|
1166
|
+
})
|
|
1167
|
+
.then(async (r) => {
|
|
1168
|
+
const resJson = await r.json().catch(() => ({}))
|
|
1169
|
+
if (!r.ok || !resJson.ok) {
|
|
1170
|
+
throw new Error(resJson.error || ('HTTP ' + r.status))
|
|
1171
|
+
}
|
|
1172
|
+
return resJson
|
|
1173
|
+
})
|
|
1174
|
+
.then((data) => {
|
|
1175
|
+
setAssembleResult(data)
|
|
1176
|
+
})
|
|
1177
|
+
.catch((err) => {
|
|
1178
|
+
setAssembleError(err?.message || String(err))
|
|
1179
|
+
})
|
|
1180
|
+
.finally(() => setAssembling(false))
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const handlePromote = () => {
|
|
1184
|
+
if (!selectedRunId) return
|
|
1185
|
+
setPromoting(true)
|
|
1186
|
+
fetch('/dsh-moa/promote', {
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1189
|
+
body: JSON.stringify({
|
|
1190
|
+
runId: selectedRunId,
|
|
1191
|
+
candidate: 'workbench-glue',
|
|
1192
|
+
force: true,
|
|
1193
|
+
}),
|
|
1194
|
+
})
|
|
1195
|
+
.then(async (r) => {
|
|
1196
|
+
const resJson = await r.json().catch(() => ({}))
|
|
1197
|
+
if (!r.ok || !resJson.ok) throw new Error(resJson.error || ('HTTP ' + r.status))
|
|
1198
|
+
setPromoted(true)
|
|
1199
|
+
})
|
|
1200
|
+
.catch((err) => {
|
|
1201
|
+
setAssembleError('Promotion failed: ' + (err?.message || String(err)))
|
|
1202
|
+
})
|
|
1203
|
+
.finally(() => setPromoting(false))
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Collect all files from byFile or candidates
|
|
1207
|
+
const allFiles = []
|
|
1208
|
+
if (workbenchData?.fileScores?.byFile) {
|
|
1209
|
+
for (const file of Object.keys(workbenchData.fileScores.byFile)) {
|
|
1210
|
+
allFiles.push(file)
|
|
1211
|
+
}
|
|
1212
|
+
} else if (Array.isArray(workbenchData?.candidates)) {
|
|
1213
|
+
const fileSet = new Set()
|
|
1214
|
+
for (const c of workbenchData.candidates) {
|
|
1215
|
+
for (const f of (c.files || [])) fileSet.add(f.relativePath)
|
|
1216
|
+
}
|
|
1217
|
+
allFiles.push(...Array.from(fileSet))
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return React.createElement(
|
|
1221
|
+
'div',
|
|
1222
|
+
{ className: 'moa-section-card', style: { marginTop: 12 } },
|
|
1223
|
+
React.createElement('div', { className: 'moa-section-title' }, t('workbench.title') || '🛠️ Interactive Synthesis Workbench'),
|
|
1224
|
+
React.createElement('div', { className: 'moa-section-desc' }, t('workbench.desc') || 'Inspect 1–10 file scores across candidate models, pick & mix desired files, harmonize with top-model glue, and execute judge re-audit.'),
|
|
1225
|
+
|
|
1226
|
+
/* Run Selection Bar */
|
|
1227
|
+
React.createElement(
|
|
1228
|
+
'div',
|
|
1229
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, flexWrap: 'wrap' } },
|
|
1230
|
+
React.createElement('span', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('workbench.select_run') || 'MoA Run:'),
|
|
1231
|
+
React.createElement(
|
|
1232
|
+
'select',
|
|
1233
|
+
{
|
|
1234
|
+
className: 'moa-select',
|
|
1235
|
+
style: { height: 28, fontSize: 12, padding: '0 8px', minWidth: 260, maxWidth: '100%' },
|
|
1236
|
+
value: selectedRunId,
|
|
1237
|
+
onChange: (e) => setSelectedRunId(e.target.value),
|
|
1238
|
+
},
|
|
1239
|
+
historyRuns.length === 0 && React.createElement('option', { value: '' }, t('workbench.no_runs') || 'No runs recorded'),
|
|
1240
|
+
historyRuns.map((r) => {
|
|
1241
|
+
const fileCount = (r.candidates || []).reduce((acc, c) => acc + (c.files?.length || 0), 0)
|
|
1242
|
+
const label = `${(r.prompt || '').slice(0, 36)}... [${fileCount} files, winner: ${r.winnerModel || 'none'}]`
|
|
1243
|
+
return React.createElement('option', { key: r.id, value: r.id }, label)
|
|
1244
|
+
})
|
|
1245
|
+
),
|
|
1246
|
+
/* Companion Plugin Badge */
|
|
1247
|
+
React.createElement(
|
|
1248
|
+
'div',
|
|
1249
|
+
{ style: { marginLeft: 'auto', fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #888)', display: 'flex', alignItems: 'center', gap: 4 } },
|
|
1250
|
+
'🔗 Companion: ',
|
|
1251
|
+
React.createElement(
|
|
1252
|
+
'a',
|
|
1253
|
+
{
|
|
1254
|
+
href: 'https://github.com/goodandready/dsh-time-machine',
|
|
1255
|
+
target: '_blank',
|
|
1256
|
+
rel: 'noopener noreferrer',
|
|
1257
|
+
style: { color: 'var(--dsw-alias-state-brand-primary, #3b82f6)', textDecoration: 'underline' },
|
|
1258
|
+
},
|
|
1259
|
+
'dsh-time-machine'
|
|
1260
|
+
)
|
|
1261
|
+
)
|
|
1262
|
+
),
|
|
1263
|
+
|
|
1264
|
+
loading && React.createElement('div', { style: { padding: '16px 0', fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, '⏳ Loading workbench data...'),
|
|
1265
|
+
error && React.createElement('div', { style: { padding: '8px 12px', background: '#fee2e2', color: '#b91c1c', borderRadius: 6, fontSize: 12, marginTop: 8 } }, error),
|
|
1266
|
+
|
|
1267
|
+
/* File Matrix */
|
|
1268
|
+
!loading && !error && allFiles.length === 0 && React.createElement(
|
|
1269
|
+
'div',
|
|
1270
|
+
{ style: { padding: '14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: 8 } },
|
|
1271
|
+
t('workbench.empty_files') || 'No candidate files found for this run.'
|
|
1272
|
+
),
|
|
1273
|
+
|
|
1274
|
+
!loading && !error && allFiles.length > 0 && React.createElement(
|
|
1275
|
+
'div',
|
|
1276
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 } },
|
|
1277
|
+
allFiles.map((file) => {
|
|
1278
|
+
const fileScores = workbenchData?.fileScores?.byFile?.[file] || []
|
|
1279
|
+
const selectedCand = selections[file]
|
|
1280
|
+
|
|
1281
|
+
return React.createElement(
|
|
1282
|
+
'div',
|
|
1283
|
+
{
|
|
1284
|
+
key: file,
|
|
1285
|
+
style: {
|
|
1286
|
+
padding: '10px 12px',
|
|
1287
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1288
|
+
borderRadius: 8,
|
|
1289
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1290
|
+
},
|
|
1291
|
+
},
|
|
1292
|
+
/* File Title Bar */
|
|
1293
|
+
React.createElement(
|
|
1294
|
+
'div',
|
|
1295
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
|
|
1296
|
+
React.createElement(
|
|
1297
|
+
'span',
|
|
1298
|
+
{ style: { fontFamily: 'monospace', fontSize: 13, fontWeight: 700, color: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))' } },
|
|
1299
|
+
'📄 ' + file
|
|
1300
|
+
),
|
|
1301
|
+
React.createElement(
|
|
1302
|
+
'span',
|
|
1303
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)' } },
|
|
1304
|
+
fileScores.length + ' candidate proposal(s)'
|
|
1305
|
+
)
|
|
1306
|
+
),
|
|
1307
|
+
|
|
1308
|
+
/* Candidate Choices for this file */
|
|
1309
|
+
React.createElement(
|
|
1310
|
+
'div',
|
|
1311
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
1312
|
+
fileScores.map((scoreItem) => {
|
|
1313
|
+
const isChecked = selectedCand === scoreItem.candidateIndex
|
|
1314
|
+
const scoreVal = scoreItem.score || 7
|
|
1315
|
+
const scoreBg = scoreVal >= 8 ? '#10b981' : (scoreVal >= 5 ? '#f59e0b' : '#ef4444')
|
|
1316
|
+
|
|
1317
|
+
return React.createElement(
|
|
1318
|
+
'label',
|
|
1319
|
+
{
|
|
1320
|
+
key: scoreItem.candidateIndex,
|
|
1321
|
+
style: {
|
|
1322
|
+
display: 'flex',
|
|
1323
|
+
alignItems: 'flex-start',
|
|
1324
|
+
gap: 8,
|
|
1325
|
+
cursor: 'pointer',
|
|
1326
|
+
padding: '6px 8px',
|
|
1327
|
+
background: isChecked ? 'rgba(59, 130, 246, 0.08)' : 'transparent',
|
|
1328
|
+
borderRadius: 6,
|
|
1329
|
+
border: isChecked ? '1px solid var(--dsw-alias-state-brand-primary, #3b82f6)' : '1px solid transparent',
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
React.createElement('input', {
|
|
1333
|
+
type: 'radio',
|
|
1334
|
+
name: 'file-' + file,
|
|
1335
|
+
checked: isChecked,
|
|
1336
|
+
onChange: () => setSelections((prev) => ({ ...prev, [file]: scoreItem.candidateIndex })),
|
|
1337
|
+
style: { marginTop: 3, accentColor: 'var(--dsw-alias-state-brand-primary, #3b82f6)' },
|
|
1338
|
+
}),
|
|
1339
|
+
React.createElement(
|
|
1340
|
+
'div',
|
|
1341
|
+
{ style: { flex: 1, fontSize: 12 } },
|
|
1342
|
+
React.createElement(
|
|
1343
|
+
'div',
|
|
1344
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
1345
|
+
React.createElement('span', { style: { fontWeight: 600 } }, scoreItem.candidateLabel || ('Candidate ' + scoreItem.candidateIndex)),
|
|
1346
|
+
React.createElement(
|
|
1347
|
+
'span',
|
|
1348
|
+
{
|
|
1349
|
+
style: {
|
|
1350
|
+
fontSize: 10,
|
|
1351
|
+
fontWeight: 700,
|
|
1352
|
+
color: '#fff',
|
|
1353
|
+
background: scoreBg,
|
|
1354
|
+
padding: '1px 6px',
|
|
1355
|
+
borderRadius: 4,
|
|
1356
|
+
},
|
|
1357
|
+
},
|
|
1358
|
+
'Score: ' + scoreVal + '/10'
|
|
1359
|
+
)
|
|
1360
|
+
),
|
|
1361
|
+
scoreItem.pros && React.createElement(
|
|
1362
|
+
'div',
|
|
1363
|
+
{ style: { fontSize: 11, color: '#16a34a', marginTop: 2 } },
|
|
1364
|
+
'⭐ ' + (t('workbench.pros') || 'Pros:') + ' ' + scoreItem.pros
|
|
1365
|
+
),
|
|
1366
|
+
scoreItem.cons && React.createElement(
|
|
1367
|
+
'div',
|
|
1368
|
+
{ style: { fontSize: 11, color: '#dc2626', marginTop: 1 } },
|
|
1369
|
+
'⚠️ ' + (t('workbench.cons') || 'Cons:') + ' ' + scoreItem.cons
|
|
1370
|
+
)
|
|
1371
|
+
)
|
|
1372
|
+
)
|
|
1373
|
+
})
|
|
1374
|
+
)
|
|
1375
|
+
)
|
|
1376
|
+
}),
|
|
1377
|
+
|
|
1378
|
+
/* Harmonization Instructions Input */
|
|
1379
|
+
React.createElement(
|
|
1380
|
+
'div',
|
|
1381
|
+
{ style: { marginTop: 6, display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
1382
|
+
React.createElement('span', { style: { fontSize: 12, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('workbench.instructions_label') || 'Harmonization Instructions (Optional):'),
|
|
1383
|
+
React.createElement('input', {
|
|
1384
|
+
type: 'text',
|
|
1385
|
+
className: 'moa-input',
|
|
1386
|
+
style: { height: 32, fontSize: 12, padding: '0 8px' },
|
|
1387
|
+
placeholder: t('workbench.instructions_placeholder') || 'e.g. Ensure all imports match, unify TypeScript types, align function signatures...',
|
|
1388
|
+
value: instructions,
|
|
1389
|
+
onChange: (e) => setInstructions(e.target.value),
|
|
1390
|
+
})
|
|
1391
|
+
),
|
|
1392
|
+
|
|
1393
|
+
/* Action Button */
|
|
1394
|
+
React.createElement(
|
|
1395
|
+
'button',
|
|
1396
|
+
{
|
|
1397
|
+
className: 'moa-btn moa-btn-primary',
|
|
1398
|
+
style: { height: 36, fontSize: 13, fontWeight: 600, marginTop: 6 },
|
|
1399
|
+
disabled: assembling || Object.keys(selections).length === 0,
|
|
1400
|
+
onClick: handleAssemble,
|
|
1401
|
+
},
|
|
1402
|
+
assembling ? (t('workbench.assembling') || '⏳ Harmonizing files with Top Model...') : (t('workbench.assemble_btn') || '🚀 Harmonize & Assemble (Top-Model Glue)')
|
|
1403
|
+
),
|
|
1404
|
+
|
|
1405
|
+
assembleError && React.createElement(
|
|
1406
|
+
'div',
|
|
1407
|
+
{ style: { padding: '8px 12px', background: '#fee2e2', color: '#b91c1c', borderRadius: 6, fontSize: 12, marginTop: 8 } },
|
|
1408
|
+
assembleError
|
|
1409
|
+
),
|
|
1410
|
+
|
|
1411
|
+
/* Harmonization Result Card */
|
|
1412
|
+
assembleResult && React.createElement(
|
|
1413
|
+
'div',
|
|
1414
|
+
{
|
|
1415
|
+
style: {
|
|
1416
|
+
marginTop: 10,
|
|
1417
|
+
padding: '12px',
|
|
1418
|
+
background: 'rgba(16, 185, 129, 0.05)',
|
|
1419
|
+
border: '1px solid #10b981',
|
|
1420
|
+
borderRadius: 8,
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
React.createElement(
|
|
1424
|
+
'div',
|
|
1425
|
+
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
|
|
1426
|
+
React.createElement('div', { style: { fontWeight: 700, fontSize: 13, color: '#047857' } }, '✨ Harmonization Complete! Top Model: ' + (assembleResult.topModel || 'Master')),
|
|
1427
|
+
React.createElement(
|
|
1428
|
+
'span',
|
|
1429
|
+
{
|
|
1430
|
+
style: {
|
|
1431
|
+
fontSize: 11,
|
|
1432
|
+
fontWeight: 700,
|
|
1433
|
+
color: '#fff',
|
|
1434
|
+
background: assembleResult.judgeAudit?.passed ? '#10b981' : '#ef4444',
|
|
1435
|
+
padding: '2px 8px',
|
|
1436
|
+
borderRadius: 4,
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
'Judge: ' + (assembleResult.judgeAudit?.rawVerdict || (assembleResult.judgeAudit?.passed ? 'PASSED' : 'FAILED')) + ' (' + (assembleResult.judgeAudit?.score || 8) + '/10)'
|
|
1440
|
+
)
|
|
1441
|
+
),
|
|
1442
|
+
|
|
1443
|
+
/* Audit notes */
|
|
1444
|
+
assembleResult.judgeAudit?.notes && React.createElement(
|
|
1445
|
+
'div',
|
|
1446
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginBottom: 8, background: 'var(--dsw-alias-bg-layer-2)', padding: '6px 8px', borderRadius: 4 } },
|
|
1447
|
+
assembleResult.judgeAudit.notes
|
|
1448
|
+
),
|
|
1449
|
+
|
|
1450
|
+
/* Glued files */
|
|
1451
|
+
React.createElement(
|
|
1452
|
+
'div',
|
|
1453
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-label-primary)', marginBottom: 8 } },
|
|
1454
|
+
'Glued Files: ' + (assembleResult.gluedFiles || []).map((f) => `${f.relativePath} (${f.size}B)`).join(', ')
|
|
1455
|
+
),
|
|
1456
|
+
|
|
1457
|
+
/* Promotion Button */
|
|
1458
|
+
!promoted && React.createElement(
|
|
1459
|
+
'button',
|
|
1460
|
+
{
|
|
1461
|
+
className: 'moa-btn moa-btn-success',
|
|
1462
|
+
style: { height: 32, fontSize: 12, fontWeight: 600 },
|
|
1463
|
+
disabled: promoting,
|
|
1464
|
+
onClick: handlePromote,
|
|
1465
|
+
},
|
|
1466
|
+
promoting ? '⏳ Promoting to workspace...' : (t('workbench.promote_btn') || 'Promote Harmonized Files to Workspace')
|
|
1467
|
+
),
|
|
1468
|
+
|
|
1469
|
+
promoted && React.createElement(
|
|
1470
|
+
'div',
|
|
1471
|
+
{ style: { fontSize: 12, fontWeight: 600, color: '#059669', marginTop: 4 } },
|
|
1472
|
+
t('workbench.promoted_ok') || '✅ Harmonized deliverable promoted to workspace root!'
|
|
1473
|
+
)
|
|
1474
|
+
)
|
|
1475
|
+
)
|
|
1476
|
+
)
|
|
1477
|
+
}
|
|
1026
1478
|
function MoAEditor({
|
|
1027
1479
|
t,
|
|
1028
1480
|
presets,
|
|
@@ -1713,6 +2165,44 @@ window.__ModuleLoader__.load({
|
|
|
1713
2165
|
)
|
|
1714
2166
|
),
|
|
1715
2167
|
|
|
2168
|
+
/* Interactive Workbench & File Scoring (#251) */
|
|
2169
|
+
React.createElement(
|
|
2170
|
+
'div',
|
|
2171
|
+
{ 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 } },
|
|
2172
|
+
React.createElement(
|
|
2173
|
+
'label',
|
|
2174
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
|
|
2175
|
+
React.createElement('input', {
|
|
2176
|
+
type: 'checkbox',
|
|
2177
|
+
checked: Boolean(currentPreset.interactive_workbench_enabled),
|
|
2178
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, interactive_workbench_enabled: e.target.checked })),
|
|
2179
|
+
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
2180
|
+
}),
|
|
2181
|
+
t('aggregator.workbench_title') || '🛠️ Interactive Workbench (1-10 File Scoring & Top-Model Glue)'
|
|
2182
|
+
),
|
|
2183
|
+
React.createElement(
|
|
2184
|
+
'div',
|
|
2185
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
2186
|
+
t('aggregator.workbench_desc') || 'Judge scores each candidate file (1–10). Mix and match candidate files in the UI; the top model harmonizes them into a unified project followed by judge re-audit.'
|
|
2187
|
+
),
|
|
2188
|
+
React.createElement(
|
|
2189
|
+
'div',
|
|
2190
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #888)', marginLeft: 22, marginTop: 2, display: 'flex', alignItems: 'center', gap: 4 } },
|
|
2191
|
+
'🔗 ' + (t('aggregator.workbench_companion') || 'Companion Plugin:'),
|
|
2192
|
+
React.createElement(
|
|
2193
|
+
'a',
|
|
2194
|
+
{
|
|
2195
|
+
href: 'https://github.com/goodandready/dsh-time-machine',
|
|
2196
|
+
target: '_blank',
|
|
2197
|
+
rel: 'noopener noreferrer',
|
|
2198
|
+
style: { color: 'var(--dsw-alias-state-brand-primary, #3b82f6)', textDecoration: 'underline' },
|
|
2199
|
+
},
|
|
2200
|
+
'dsh-time-machine'
|
|
2201
|
+
),
|
|
2202
|
+
'(' + (t('aggregator.workbench_companion_note') || 'safe shadow Git checkpoints before file promotion') + ')'
|
|
2203
|
+
)
|
|
2204
|
+
),
|
|
2205
|
+
|
|
1716
2206
|
/* Graceful Degradation & Local Fallback (#175, #176) */
|
|
1717
2207
|
React.createElement(
|
|
1718
2208
|
'div',
|
|
@@ -2204,6 +2694,11 @@ window.__ModuleLoader__.load({
|
|
|
2204
2694
|
t,
|
|
2205
2695
|
currentRefs,
|
|
2206
2696
|
}),
|
|
2697
|
+
/* ── SECTION 4c: INTERACTIVE WORKBENCH (FILE PICK & MIX) ── */
|
|
2698
|
+
React.createElement(InteractiveWorkbenchSection, {
|
|
2699
|
+
t,
|
|
2700
|
+
currentRefs,
|
|
2701
|
+
}),
|
|
2207
2702
|
/* ── PLUGIN UPDATES ── */
|
|
2208
2703
|
React.createElement(
|
|
2209
2704
|
'div',
|
package/lib/index.js
CHANGED
|
@@ -325,3 +325,12 @@ export function apply(ctx, config) {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
328
|
+
export {
|
|
329
|
+
COMPANION_PLUGINS,
|
|
330
|
+
formatFileScoresDirective,
|
|
331
|
+
parseFileScores,
|
|
332
|
+
findTopModel,
|
|
333
|
+
assembleWorkbenchSelection,
|
|
334
|
+
handleWorkbenchGet,
|
|
335
|
+
handleWorkbenchAssemble,
|
|
336
|
+
} from './moa-workbench.js'
|
package/lib/moa-prompts.js
CHANGED
|
@@ -272,6 +272,7 @@ ${referenceOutputs.some((r) => r.testResult) ? `\n${TEST_GATE_DIRECTIVE}\n` : ''
|
|
|
272
272
|
${referenceOutputs.some((r) => r.syntaxWarning) ? `
|
|
273
273
|
${SYNTAX_CORRECTION_DIRECTIVE}
|
|
274
274
|
` : ''}
|
|
275
|
+
${options.fileScoringDirective ? `\n${options.fileScoringDirective}\n` : ''}
|
|
275
276
|
Instructions:
|
|
276
277
|
Your response MUST be structured into three clear parts:
|
|
277
278
|
|
|
@@ -350,6 +351,7 @@ ${referenceOutputs.some((r) => r.syntaxWarning) ? `\n${SYNTAX_CORRECTION_DIRECTI
|
|
|
350
351
|
${options.priorTurnBaseline ? `\n### 🔄 Prior Turn Context Baseline:\n${options.priorTurnBaseline}\n` : ''}
|
|
351
352
|
${consensusReportText ? `\n### ⚖️ Multi-Judge Consensus Advisory:\n${consensusReportText}\n` : ''}
|
|
352
353
|
${options.compositeMergeDirective ? `\n${options.compositeMergeDirective}\n` : ''}
|
|
354
|
+
${options.fileScoringDirective ? `\n${options.fileScoringDirective}\n` : ''}
|
|
353
355
|
Instructions:
|
|
354
356
|
Your response MUST be structured into three clear parts:
|
|
355
357
|
|