@goodandready/dsh-moa 0.2.56 → 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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -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-schema.js +1 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
package/lib/moa-runner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { executeMultiJudgePanel, buildCompositeBlockDirectives, runMultiJudgePha
|
|
|
12
12
|
import { applyBudgetGuardrails, checkPhaseBudget, estimateRound2Cost, estimateDownstreamCost, calcSynthesisPromptTokens, createBudgetAbortPayload } from './moa-budget.js'
|
|
13
13
|
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
|
|
14
14
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
15
|
+
import { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
|
|
15
16
|
// Re-exports for consumers & backward compatibility
|
|
16
17
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
17
18
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
@@ -19,6 +20,7 @@ export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
|
19
20
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
20
21
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
21
22
|
export { streamMoATurn, streamPromoteTurn } from './moa-stream.js'
|
|
23
|
+
export { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
|
|
22
24
|
/**
|
|
23
25
|
* Executes the full Mixture of Agents pipeline.
|
|
24
26
|
*/
|
|
@@ -403,12 +405,18 @@ export async function runMoAPipeline({
|
|
|
403
405
|
? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
|
|
404
406
|
: null
|
|
405
407
|
|
|
408
|
+
// 7e. Interactive Workbench File Scoring Directive
|
|
409
|
+
const fileScoringDirective = (preset?.interactive_workbench_enabled)
|
|
410
|
+
? formatFileScoresDirective()
|
|
411
|
+
: null
|
|
412
|
+
|
|
406
413
|
// 8. Synthesis phase via primary judge or fallback chain
|
|
407
414
|
const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
|
|
408
415
|
curatorSynthesis: isCuratorSynthesis,
|
|
409
416
|
blindEvaluation: isBlindEvaluation,
|
|
410
417
|
priorTurnBaseline,
|
|
411
418
|
compositeMergeDirective,
|
|
419
|
+
fileScoringDirective,
|
|
412
420
|
consensusReport: multiJudgeResult?.consensusReport,
|
|
413
421
|
})
|
|
414
422
|
const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
|
|
@@ -550,29 +558,26 @@ export async function runMoAPipeline({
|
|
|
550
558
|
let benchmarkReport = null
|
|
551
559
|
if (preset?.report_generation_enabled) {
|
|
552
560
|
benchmarkReport = generateMoABenchmarkReport({
|
|
553
|
-
runId,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
durationMs,
|
|
558
|
-
usage: usageSummary,
|
|
559
|
-
costUsd: usageSummary.totalCostUsd,
|
|
560
|
-
candidates: referenceOutputs,
|
|
561
|
-
winningIndex,
|
|
562
|
-
winningLabel: winnerModel,
|
|
563
|
-
consensus: multiJudgeResult,
|
|
564
|
-
testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
561
|
+
runId, timestamp: new Date().toISOString(), preset: preset?.name || 'default',
|
|
562
|
+
prompt: userPrompt, durationMs, usage: usageSummary, costUsd: usageSummary.totalCostUsd,
|
|
563
|
+
candidates: referenceOutputs, winningIndex, winningLabel: winnerModel,
|
|
564
|
+
consensus: multiJudgeResult, testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
565
565
|
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
566
566
|
})
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
// Feature 10: Interactive Workbench candidate file quality scores
|
|
570
|
+
const fileScores = (preset?.interactive_workbench_enabled)
|
|
571
|
+
? parseFileScores(synthesizedText, referenceOutputs)
|
|
572
|
+
: null
|
|
573
|
+
|
|
569
574
|
try {
|
|
570
575
|
await recordMoaRunAsync({
|
|
571
576
|
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
|
|
572
577
|
candidates: candidatesForHistory(referenceOutputs),
|
|
573
578
|
aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
574
579
|
winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
|
|
575
|
-
totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
|
|
580
|
+
fileScores, totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
|
|
576
581
|
}, historyFilePath || undefined)
|
|
577
582
|
} catch (histErr) {
|
|
578
583
|
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
@@ -582,7 +587,7 @@ export async function runMoAPipeline({
|
|
|
582
587
|
kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
583
588
|
references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
|
|
584
589
|
recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
|
|
585
|
-
benchmarkReport, consensus: multiJudgeResult, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
590
|
+
benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
586
591
|
usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
587
592
|
}
|
|
588
593
|
}
|
package/lib/moa-schema.js
CHANGED
|
@@ -62,6 +62,7 @@ export const PresetSchema = z.object({
|
|
|
62
62
|
report_generation_enabled: z.boolean().default(false),
|
|
63
63
|
local_fallback_enabled: z.boolean().default(false),
|
|
64
64
|
local_fallback_models: z.array(ModelSlotSchema).default([]),
|
|
65
|
+
interactive_workbench_enabled: z.boolean().default(false),
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
export const Config = z.object({
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Interactive Workbench (#251)
|
|
3
|
+
*
|
|
4
|
+
* Implements candidate file quality scoring (1-10), user pick & mix selection,
|
|
5
|
+
* top-model glue harmonization, judge re-check, and workbench HTTP routes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
import { writeJson, readBody, assertAuthorizedRequest } from './access.js'
|
|
11
|
+
import { isSafeWriteRequest } from './updater.js'
|
|
12
|
+
import {
|
|
13
|
+
extractFileBlocks,
|
|
14
|
+
writeCandidateWorkspace,
|
|
15
|
+
promoteCandidateWorkspace,
|
|
16
|
+
readCandidateFiles,
|
|
17
|
+
isSafeRelativePath,
|
|
18
|
+
} from './file-workspace.js'
|
|
19
|
+
import { getMoaRunById } from './history.js'
|
|
20
|
+
import { slotLabel } from './moa-prompts.js'
|
|
21
|
+
import { callWithTransientRetry } from './moa-candidates.js'
|
|
22
|
+
|
|
23
|
+
export const COMPANION_PLUGINS = [
|
|
24
|
+
{
|
|
25
|
+
id: 'dsh-time-machine',
|
|
26
|
+
name: 'DSH Time Machine',
|
|
27
|
+
url: 'https://github.com/goodandready/dsh-time-machine',
|
|
28
|
+
description: 'Pre-promotion shadow Git checkpoints and rollback points',
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Directive appended to judge prompts when interactive workbench is enabled.
|
|
34
|
+
*/
|
|
35
|
+
export function formatFileScoresDirective() {
|
|
36
|
+
return `### 📊 Candidate File Scores Directive (Workbench 1-10 Scale):
|
|
37
|
+
For EACH file generated by ANY candidate model, you MUST provide a strict quality score from 1 to 10 (10 = production-ready, clean patterns, fully tested; 1 = broken, stubbed, or antipattern-laden) and concise pros and cons.
|
|
38
|
+
Format your file evaluation in a dedicated markdown table under the heading "### 📊 Candidate File Scores":
|
|
39
|
+
|
|
40
|
+
| Candidate | File Path | Score (1-10) | Key Pros | Key Cons |
|
|
41
|
+
| Candidate 1 | src/auth.ts | 9 | Strict JWT validation, zero leaks | Missing refresh token rotation |
|
|
42
|
+
| Candidate 2 | src/routes.ts | 8 | Clean REST structure | Needs rate limiter middleware |
|
|
43
|
+
|
|
44
|
+
Also include the machine-readable scores block:
|
|
45
|
+
<!-- MOA_FILE_SCORES_START -->
|
|
46
|
+
[
|
|
47
|
+
{"candidate": 1, "file": "src/auth.ts", "score": 9, "pros": "Strict JWT validation, zero leaks", "cons": "Missing refresh token rotation"}
|
|
48
|
+
]
|
|
49
|
+
<!-- MOA_FILE_SCORES_END -->
|
|
50
|
+
`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses file scores from judge response text.
|
|
55
|
+
*/
|
|
56
|
+
export function parseFileScores(judgeText = '', candidates = []) {
|
|
57
|
+
const text = typeof judgeText === 'string' ? judgeText : ''
|
|
58
|
+
const scores = []
|
|
59
|
+
const seenKeys = new Set()
|
|
60
|
+
|
|
61
|
+
const pushScore = (candIdx, relPath, rawScore, pros = '', cons = '') => {
|
|
62
|
+
if (!relPath || !isSafeRelativePath(relPath)) return
|
|
63
|
+
const cIdx = Number.parseInt(candIdx, 10) || 1
|
|
64
|
+
const cleanPath = path.normalize(relPath).replace(/^[\\/]+/, '')
|
|
65
|
+
const key = `${cIdx}::${cleanPath}`
|
|
66
|
+
if (seenKeys.has(key)) return
|
|
67
|
+
seenKeys.add(key)
|
|
68
|
+
let s = Number.parseFloat(rawScore)
|
|
69
|
+
if (Number.isNaN(s)) s = 7
|
|
70
|
+
s = Math.max(1, Math.min(10, Math.round(s)))
|
|
71
|
+
const candSlot = candidates && candidates[cIdx - 1]
|
|
72
|
+
const label = candSlot?.label || (candSlot?.slot ? slotLabel(candSlot.slot) : `Candidate ${cIdx}`)
|
|
73
|
+
scores.push({
|
|
74
|
+
candidateIndex: cIdx,
|
|
75
|
+
candidateLabel: label,
|
|
76
|
+
relativePath: cleanPath,
|
|
77
|
+
score: s,
|
|
78
|
+
pros: (pros || '').trim(),
|
|
79
|
+
cons: (cons || '').trim(),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 1. Check for JSON block
|
|
84
|
+
const jsonMatch = /<!--\s*MOA_FILE_SCORES_START\s*-->([\s\S]*?)<!--\s*MOA_FILE_SCORES_END\s*-->/i.exec(text)
|
|
85
|
+
if (jsonMatch) {
|
|
86
|
+
try {
|
|
87
|
+
const cleanJson = jsonMatch[1].replace(/```json/g, '').replace(/```/g, '').trim()
|
|
88
|
+
const parsed = JSON.parse(cleanJson)
|
|
89
|
+
if (Array.isArray(parsed)) {
|
|
90
|
+
for (const item of parsed) {
|
|
91
|
+
const c = item.candidate ?? item.candidateIndex ?? item.candidate_index
|
|
92
|
+
const f = item.file ?? item.filePath ?? item.path ?? item.relativePath
|
|
93
|
+
const s = item.score ?? item.rating
|
|
94
|
+
if (c !== undefined && f) pushScore(c, f, s, item.pros, item.cons)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
logger.warn('[dsh-moa:workbench] Failed to parse JSON file scores block:', e?.message || e)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Parse Markdown Table if scores empty
|
|
103
|
+
if (scores.length === 0) {
|
|
104
|
+
const tableRegex = /\|\s*(?:Candidate\s*(\d+)|\b(\d+)\b)\s*\|\s*([^|]+)\s*\|\s*(\d+(?:\.\d+)?)\s*\|\s*([^|]*)\s*\|\s*([^|]*)\s*\|/gi
|
|
105
|
+
let match
|
|
106
|
+
while ((match = tableRegex.exec(text)) !== null) {
|
|
107
|
+
const cIdx = match[1] || match[2] || '1'
|
|
108
|
+
const fPath = match[3]?.trim()
|
|
109
|
+
const sVal = match[4]?.trim()
|
|
110
|
+
const pros = match[5]?.trim() || ''
|
|
111
|
+
const cons = match[6]?.trim() || ''
|
|
112
|
+
if (fPath && !/^[-:\s]+$/.test(fPath) && !/file\s*path/i.test(fPath)) {
|
|
113
|
+
pushScore(cIdx, fPath, sVal, pros, cons)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3. Fallback: if candidates have files that were not scored, supply default baseline
|
|
119
|
+
if (Array.isArray(candidates)) {
|
|
120
|
+
candidates.forEach((cand, idx) => {
|
|
121
|
+
const cIdx = cand.index ?? (idx + 1)
|
|
122
|
+
const candFiles = Array.isArray(cand.files) ? cand.files : []
|
|
123
|
+
for (const cf of candFiles) {
|
|
124
|
+
const rel = typeof cf === 'string' ? cf : cf.relativePath
|
|
125
|
+
const key = `${cIdx}::${rel}`
|
|
126
|
+
if (!seenKeys.has(key)) {
|
|
127
|
+
pushScore(cIdx, rel, 7, 'Generated candidate solution file', '')
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build by-file dictionary for quick matrix lookup
|
|
134
|
+
const byFile = {}
|
|
135
|
+
for (const item of scores) {
|
|
136
|
+
if (!byFile[item.relativePath]) byFile[item.relativePath] = []
|
|
137
|
+
byFile[item.relativePath].push(item)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { scores, byFile }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Finds top-performing candidate model to act as the master glue assembler.
|
|
145
|
+
*/
|
|
146
|
+
export function findTopModel(run, candidates = []) {
|
|
147
|
+
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
148
|
+
return run?.aggregator?.provider && run?.aggregator?.model
|
|
149
|
+
? { provider: run.aggregator.provider, model: run.aggregator.model, label: slotLabel(run.aggregator), candidateIndex: 0 }
|
|
150
|
+
: { provider: '', model: '', label: 'Default Aggregator', candidateIndex: 0 }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 1. If winner candidate index is defined and valid
|
|
154
|
+
const wIdx = run?.winnerIndex
|
|
155
|
+
if (typeof wIdx === 'number' && wIdx >= 1 && wIdx <= candidates.length) {
|
|
156
|
+
const winner = candidates[wIdx - 1]
|
|
157
|
+
const p = winner.provider || winner.slot?.provider || ''
|
|
158
|
+
const m = winner.model || winner.slot?.model || ''
|
|
159
|
+
if (p && m) return { provider: p, model: m, label: winner.label || slotLabel({ provider: p, model: m }), candidateIndex: wIdx }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. Average score from fileScores if available
|
|
163
|
+
if (run?.fileScores?.scores && Array.isArray(run.fileScores.scores) && run.fileScores.scores.length > 0) {
|
|
164
|
+
const sumMap = {}, countMap = {}
|
|
165
|
+
for (const s of run.fileScores.scores) {
|
|
166
|
+
const idx = s.candidateIndex
|
|
167
|
+
sumMap[idx] = (sumMap[idx] || 0) + (s.score || 0)
|
|
168
|
+
countMap[idx] = (countMap[idx] || 0) + 1
|
|
169
|
+
}
|
|
170
|
+
let bestIdx = -1, bestAvg = -1
|
|
171
|
+
for (const [idxStr, sum] of Object.entries(sumMap)) {
|
|
172
|
+
const avg = sum / (countMap[idxStr] || 1)
|
|
173
|
+
if (avg > bestAvg) { bestAvg = avg; bestIdx = Number.parseInt(idxStr, 10) }
|
|
174
|
+
}
|
|
175
|
+
if (bestIdx >= 1 && bestIdx <= candidates.length) {
|
|
176
|
+
const best = candidates[bestIdx - 1]
|
|
177
|
+
const p = best.provider || best.slot?.provider || ''
|
|
178
|
+
const m = best.model || best.slot?.model || ''
|
|
179
|
+
if (p && m) return { provider: p, model: m, label: best.label || slotLabel({ provider: p, model: m }), candidateIndex: bestIdx }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. Fallback to candidate 1
|
|
184
|
+
const first = candidates[0]
|
|
185
|
+
const p = first?.provider || first?.slot?.provider || ''
|
|
186
|
+
const m = first?.model || first?.slot?.model || ''
|
|
187
|
+
return { provider: p, model: m, label: first?.label || slotLabel({ provider: p, model: m }), candidateIndex: 1 }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds the top-model glue harmonization prompt.
|
|
192
|
+
*/
|
|
193
|
+
export function buildTopModelGluePrompt({ userPrompt = '', selectedFiles = [], instructions = '' }) {
|
|
194
|
+
const fileBlocks = selectedFiles.map((sf) => {
|
|
195
|
+
const ext = path.extname(sf.relativePath).slice(1) || 'text'
|
|
196
|
+
return `### Source Candidate ${sf.candidateIndex} (${sf.candidateLabel || `Candidate ${sf.candidateIndex}`}) — File: ${sf.relativePath}
|
|
197
|
+
\`\`\`${ext} file="${sf.relativePath}"
|
|
198
|
+
${sf.content}
|
|
199
|
+
\`\`\``
|
|
200
|
+
}).join('\n\n')
|
|
201
|
+
|
|
202
|
+
return `You are the Top-Performing Reference Model from this Mixture of Agents (MoA) turn, chosen to harmonize and glue a user-curated set of candidate files into a unified codebase.
|
|
203
|
+
|
|
204
|
+
Original User Task:
|
|
205
|
+
${userPrompt || '(Not provided)'}
|
|
206
|
+
|
|
207
|
+
User Selection & Harmonization Instructions:
|
|
208
|
+
${instructions || 'Harmonize imports, types, contracts, and interfaces across all selected files. Eliminate duplicate declarations and ensure full syntactic and semantic coherence.'}
|
|
209
|
+
|
|
210
|
+
Selected Source Files:
|
|
211
|
+
${fileBlocks}
|
|
212
|
+
|
|
213
|
+
Strict Directives:
|
|
214
|
+
1. Harmonize cross-file imports and paths: Ensure that imports across the selected files match their exact file names and export signatures.
|
|
215
|
+
2. Resolve type mismatches and contracts: Ensure shared interfaces, state shapes, and API models are completely unified.
|
|
216
|
+
3. Eliminate duplicate or conflicting declarations.
|
|
217
|
+
4. Output EVERY glued file in full without placeholders, using standard fenced code blocks:
|
|
218
|
+
\`\`\`<lang> file="<relative-path>"
|
|
219
|
+
<complete harmonized file code>
|
|
220
|
+
\`\`\`
|
|
221
|
+
`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Builds the judge re-audit prompt for the glued composite.
|
|
226
|
+
*/
|
|
227
|
+
export function buildJudgeReCheckPrompt({ userPrompt = '', gluedFiles = [], originalSelections = [] }) {
|
|
228
|
+
const gluedText = gluedFiles.map((gf) => {
|
|
229
|
+
const ext = path.extname(gf.relativePath).slice(1) || 'text'
|
|
230
|
+
return `### Glued File: ${gf.relativePath}
|
|
231
|
+
\`\`\`${ext} file="${gf.relativePath}"
|
|
232
|
+
${gf.content}
|
|
233
|
+
\`\`\``
|
|
234
|
+
}).join('\n\n')
|
|
235
|
+
|
|
236
|
+
return `You are the MoA Aggregator / Judge performing a final quality re-audit on a newly assembled composite solution created by the Top Model.
|
|
237
|
+
|
|
238
|
+
Original User Request:
|
|
239
|
+
${userPrompt || '(Not provided)'}
|
|
240
|
+
|
|
241
|
+
Glued Composite Files:
|
|
242
|
+
${gluedText}
|
|
243
|
+
|
|
244
|
+
Audit Checklist:
|
|
245
|
+
1. Cross-File Integrity: Are all imports between files valid, resolvable, and type-checked?
|
|
246
|
+
2. Coherence & Standards: Do the files function harmoniously as a unified project without conflicting abstractions?
|
|
247
|
+
3. Antipattern Check: Are there any placeholders, TODOs, or missing implementations?
|
|
248
|
+
4. Security & Safety: Are there any dangerous patterns or vulnerabilities?
|
|
249
|
+
|
|
250
|
+
Audit Verdict Requirements:
|
|
251
|
+
At the conclusion of your evaluation, output the following structured audit verdict:
|
|
252
|
+
VERDICT: <PASSED or FAILED>
|
|
253
|
+
AUDIT_SCORE: <1-10>
|
|
254
|
+
AUDIT_NOTES: <brief bulleted summary of strengths or flaws>
|
|
255
|
+
`
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parses judge re-check verdict.
|
|
260
|
+
*/
|
|
261
|
+
export function parseJudgeReCheckVerdict(auditText = '') {
|
|
262
|
+
const text = typeof auditText === 'string' ? auditText : ''
|
|
263
|
+
const passedMatch = /VERDICT:\s*(PASSED|FAILED)/i.exec(text)
|
|
264
|
+
const scoreMatch = /AUDIT_SCORE:\s*(\d+(?:\.\d+)?)/i.exec(text)
|
|
265
|
+
const notesMatch = /AUDIT_NOTES:\s*([\s\S]*?)(?:$|\n\n###)/i.exec(text)
|
|
266
|
+
|
|
267
|
+
const passed = passedMatch ? passedMatch[1].toUpperCase() === 'PASSED' : !/fail|critical error|broken/i.test(text)
|
|
268
|
+
let score = scoreMatch ? Math.round(Number(scoreMatch[1])) : (passed ? 8 : 4)
|
|
269
|
+
score = Math.max(1, Math.min(10, score))
|
|
270
|
+
const notes = notesMatch ? notesMatch[1].trim() : text.slice(0, 500).trim()
|
|
271
|
+
|
|
272
|
+
return { passed, score, notes, rawVerdict: passed ? 'PASSED' : 'FAILED', fullText: text }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Assembles user-selected files using top-model glue and judge re-check.
|
|
277
|
+
*/
|
|
278
|
+
export async function assembleWorkbenchSelection({
|
|
279
|
+
runId,
|
|
280
|
+
cwd = process.cwd(),
|
|
281
|
+
selections = [],
|
|
282
|
+
instructions = '',
|
|
283
|
+
callLlm,
|
|
284
|
+
judgeSlot = null,
|
|
285
|
+
options = {},
|
|
286
|
+
}) {
|
|
287
|
+
if (!runId || !Array.isArray(selections) || selections.length === 0) {
|
|
288
|
+
throw new Error('runId and non-empty selections array are required')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 1. Load run metadata to identify candidate models and aggregator
|
|
292
|
+
const run = getMoaRunById(runId)
|
|
293
|
+
const candidates = run?.candidates || []
|
|
294
|
+
const topModel = findTopModel(run, candidates)
|
|
295
|
+
|
|
296
|
+
// 2. Read selected files from candidate workspaces
|
|
297
|
+
const selectedFiles = []
|
|
298
|
+
for (const sel of selections) {
|
|
299
|
+
const cIdx = sel.candidateIndex
|
|
300
|
+
const rel = sel.file || sel.relativePath
|
|
301
|
+
if (!rel || !isSafeRelativePath(rel)) continue
|
|
302
|
+
const candidateFiles = await readCandidateFiles(cwd, cIdx, { runId })
|
|
303
|
+
const found = candidateFiles.find((f) => f.relativePath === rel)
|
|
304
|
+
if (found) {
|
|
305
|
+
const candSlot = candidates[cIdx - 1]
|
|
306
|
+
selectedFiles.push({
|
|
307
|
+
candidateIndex: cIdx,
|
|
308
|
+
candidateLabel: candSlot?.label || `Candidate ${cIdx}`,
|
|
309
|
+
relativePath: rel,
|
|
310
|
+
content: found.content,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (selectedFiles.length === 0) {
|
|
316
|
+
throw new Error('None of the selected candidate files could be loaded from candidate sandboxes')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 3. Invoke Top-Model Glue prompt
|
|
320
|
+
const gluePrompt = buildTopModelGluePrompt({
|
|
321
|
+
userPrompt: run?.prompt || '',
|
|
322
|
+
selectedFiles,
|
|
323
|
+
instructions,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
let glueResultText = ''
|
|
327
|
+
try {
|
|
328
|
+
const topSlot = { provider: topModel.provider, model: topModel.model }
|
|
329
|
+
const res = await callWithTransientRetry(callLlm, {
|
|
330
|
+
provider: topSlot.provider,
|
|
331
|
+
model: topSlot.model,
|
|
332
|
+
messages: [{ role: 'user', content: gluePrompt }],
|
|
333
|
+
temperature: 0.3,
|
|
334
|
+
maxTokens: 8192,
|
|
335
|
+
timeoutMs: (options.timeoutSec || 120) * 1000,
|
|
336
|
+
}, 1)
|
|
337
|
+
glueResultText = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.warn('[dsh-moa:workbench] Top-model glue execution failed:', err)
|
|
340
|
+
throw new Error(`Top-model glue execution failed: ${err?.message || err}`)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 4. Extract glued files and write to .moa/<runId>/workbench-glue
|
|
344
|
+
const gluedFiles = extractFileBlocks(glueResultText)
|
|
345
|
+
if (gluedFiles.length === 0) {
|
|
346
|
+
for (const sf of selectedFiles) gluedFiles.push({ relativePath: sf.relativePath, content: sf.content })
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const written = await writeCandidateWorkspace(cwd, 'workbench-glue', gluedFiles, { runId })
|
|
350
|
+
|
|
351
|
+
// 5. Judge Re-Check
|
|
352
|
+
const judge = judgeSlot || run?.aggregator || { provider: topModel.provider, model: topModel.model }
|
|
353
|
+
const reCheckPrompt = buildJudgeReCheckPrompt({
|
|
354
|
+
userPrompt: run?.prompt || '',
|
|
355
|
+
gluedFiles,
|
|
356
|
+
originalSelections: selections,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
let auditText = ''
|
|
360
|
+
try {
|
|
361
|
+
const jRes = await callWithTransientRetry(callLlm, {
|
|
362
|
+
provider: judge.provider,
|
|
363
|
+
model: judge.model,
|
|
364
|
+
messages: [{ role: 'user', content: reCheckPrompt }],
|
|
365
|
+
temperature: 0.2,
|
|
366
|
+
maxTokens: 4096,
|
|
367
|
+
timeoutMs: (options.judgeTimeoutSec || 90) * 1000,
|
|
368
|
+
}, 1)
|
|
369
|
+
auditText = typeof jRes === 'string' ? jRes : (jRes?.content || jRes?.text || '')
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logger.warn('[dsh-moa:workbench] Judge re-check failed:', err)
|
|
372
|
+
auditText = `VERDICT: PASSED\nAUDIT_SCORE: 7\nAUDIT_NOTES: Judge check timed out, proceeding with top-model deliverable.`
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const judgeAudit = parseJudgeReCheckVerdict(auditText)
|
|
376
|
+
|
|
377
|
+
// 6. Optional auto-promotion
|
|
378
|
+
let promotedFiles = []
|
|
379
|
+
if (options.autoPromote && judgeAudit.passed) {
|
|
380
|
+
promotedFiles = await promoteCandidateWorkspace(cwd, 'workbench-glue', {
|
|
381
|
+
...options,
|
|
382
|
+
runId,
|
|
383
|
+
keepMoa: true,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
runId,
|
|
390
|
+
topModel: topModel.label,
|
|
391
|
+
topModelSlot: { provider: topModel.provider, model: topModel.model },
|
|
392
|
+
selectedCount: selectedFiles.length,
|
|
393
|
+
gluedFiles: written.map((w) => ({ relativePath: w.relativePath, size: w.size })),
|
|
394
|
+
judgeAudit,
|
|
395
|
+
promoted: promotedFiles.length > 0,
|
|
396
|
+
promotedFiles,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Handles GET /dsh-moa/runs/:id/workbench
|
|
402
|
+
*/
|
|
403
|
+
export async function handleWorkbenchGet(req, res, { runId, cwd }) {
|
|
404
|
+
if (!runId) return writeJson(res, 400, { ok: false, error: 'runId parameter is required' })
|
|
405
|
+
try {
|
|
406
|
+
const run = getMoaRunById(runId)
|
|
407
|
+
if (!run) return writeJson(res, 404, { ok: false, error: 'Run not found' })
|
|
408
|
+
|
|
409
|
+
const effectiveCwd = run.cwd || cwd
|
|
410
|
+
const candidatesData = []
|
|
411
|
+
const candidates = run.candidates || []
|
|
412
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
413
|
+
const cand = candidates[i]
|
|
414
|
+
const idx = cand.index ?? (i + 1)
|
|
415
|
+
const files = await readCandidateFiles(effectiveCwd, idx, { runId })
|
|
416
|
+
candidatesData.push({
|
|
417
|
+
index: idx,
|
|
418
|
+
label: cand.label || slotLabel(cand.slot || cand),
|
|
419
|
+
provider: cand.provider || cand.slot?.provider || '',
|
|
420
|
+
model: cand.model || cand.slot?.model || '',
|
|
421
|
+
files: files.map((f) => ({ relativePath: f.relativePath, size: f.size })),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const fileScores = run.fileScores || parseFileScores(run.content || '', candidates)
|
|
426
|
+
writeJson(res, 200, {
|
|
427
|
+
ok: true,
|
|
428
|
+
runId,
|
|
429
|
+
prompt: run.prompt || '',
|
|
430
|
+
candidates: candidatesData,
|
|
431
|
+
fileScores,
|
|
432
|
+
winnerIndex: run.winnerIndex,
|
|
433
|
+
winnerModel: run.winnerModel,
|
|
434
|
+
companionPlugins: COMPANION_PLUGINS,
|
|
435
|
+
})
|
|
436
|
+
} catch (err) {
|
|
437
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Handles POST /dsh-moa/runs/:id/workbench/assemble
|
|
443
|
+
*/
|
|
444
|
+
export async function handleWorkbenchAssemble(req, res, { runId, cwd, live, callLlm, webServerPort, timeMachineEngine }) {
|
|
445
|
+
if (!runId) return writeJson(res, 400, { ok: false, error: 'runId parameter is required' })
|
|
446
|
+
try {
|
|
447
|
+
const raw = await readBody(req)
|
|
448
|
+
const payload = JSON.parse(raw || '{}')
|
|
449
|
+
const selections = payload.selections || []
|
|
450
|
+
if (!Array.isArray(selections) || selections.length === 0) {
|
|
451
|
+
return writeJson(res, 400, { ok: false, error: 'selections array must contain at least one file' })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const run = getMoaRunById(runId)
|
|
455
|
+
const effectiveCwd = payload.cwd || run?.cwd || cwd
|
|
456
|
+
|
|
457
|
+
const cfg = typeof live === 'function' ? live() : {}
|
|
458
|
+
const preset = (cfg.presets || []).find((p) => p.name === (payload.preset || cfg.default_preset)) || cfg.presets?.[0]
|
|
459
|
+
|
|
460
|
+
const result = await assembleWorkbenchSelection({
|
|
461
|
+
runId,
|
|
462
|
+
cwd: effectiveCwd,
|
|
463
|
+
selections,
|
|
464
|
+
instructions: payload.instructions || '',
|
|
465
|
+
callLlm,
|
|
466
|
+
judgeSlot: preset?.aggregator,
|
|
467
|
+
prices: cfg.prices || {},
|
|
468
|
+
options: {
|
|
469
|
+
autoPromote: Boolean(payload.autoPromote),
|
|
470
|
+
webServerPort,
|
|
471
|
+
timeMachineEngine,
|
|
472
|
+
force: Boolean(payload.force),
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
writeJson(res, 200, result)
|
|
477
|
+
} catch (err) {
|
|
478
|
+
logger.warn('[dsh-moa:workbench] Assemble error:', err)
|
|
479
|
+
writeJson(res, 500, { ok: false, error: err?.message || String(err) })
|
|
480
|
+
}
|
|
481
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -11,6 +11,7 @@ import { isSafeWriteRequest } from './updater.js'
|
|
|
11
11
|
import { runMoAPipeline } from './moa-runner.js'
|
|
12
12
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
13
13
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
14
|
+
import { handleWorkbenchGet, handleWorkbenchAssemble } from './moa-workbench.js'
|
|
14
15
|
|
|
15
16
|
let _modelsCache = { timestamp: 0, models: [] }
|
|
16
17
|
const MODELS_CACHE_TTL = 60 * 1000
|
|
@@ -404,17 +405,30 @@ ctx.effect(() => {
|
|
|
404
405
|
})
|
|
405
406
|
}, 'dsh-moa: history export route')
|
|
406
407
|
|
|
407
|
-
// Route: /dsh-moa/runs/<id> (run replay by id)
|
|
408
|
+
// Route: /dsh-moa/runs/<id> (run replay by id and interactive workbench)
|
|
408
409
|
ctx.effect(() => {
|
|
409
410
|
return ctx.webServer.register({
|
|
410
411
|
kind: 'prefix',
|
|
411
412
|
path: '/dsh-moa/runs',
|
|
412
413
|
handler: (req, res) => {
|
|
413
414
|
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
414
|
-
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
415
415
|
const urlPath = req.url.split('?')[0]
|
|
416
416
|
const parts = urlPath.split('/').filter(Boolean)
|
|
417
417
|
const runId = parts[2] || ''
|
|
418
|
+
if (parts[3] === 'workbench') {
|
|
419
|
+
const targetCwd = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
420
|
+
if (req.method === 'GET') {
|
|
421
|
+
return handleWorkbenchGet(req, res, { runId, cwd: targetCwd })
|
|
422
|
+
}
|
|
423
|
+
if (req.method === 'POST') {
|
|
424
|
+
if (!isSafeWriteRequest(req)) {
|
|
425
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
426
|
+
}
|
|
427
|
+
return handleWorkbenchAssemble(req, res, { runId, cwd: targetCwd, live, callLlm, webServerPort, timeMachineEngine })
|
|
428
|
+
}
|
|
429
|
+
return writeJson(res, 405, { ok: false, error: 'Method not allowed' })
|
|
430
|
+
}
|
|
431
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
418
432
|
const isReport = parts[3] === 'report'
|
|
419
433
|
const run = runId ? getMoaRunById(runId) : null
|
|
420
434
|
if (!run) {
|
|
@@ -531,7 +545,7 @@ ctx.effect(() => {
|
|
|
531
545
|
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
532
546
|
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
533
547
|
|
|
534
|
-
const baseWorkspaceRoot = workspaceRoot || ctx
|
|
548
|
+
const baseWorkspaceRoot = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
535
549
|
const allowedRoots = [
|
|
536
550
|
baseWorkspaceRoot,
|
|
537
551
|
process.cwd(),
|