agent-tool-forge 0.3.0
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/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent-registry.js +170 -0
- package/lib/api-client.js +792 -0
- package/lib/api-loader.js +260 -0
- package/lib/auth.d.ts +25 -0
- package/lib/auth.js +158 -0
- package/lib/checks/check-adapter.js +172 -0
- package/lib/checks/compose.js +42 -0
- package/lib/checks/content-match.js +14 -0
- package/lib/checks/cost-budget.js +11 -0
- package/lib/checks/index.js +18 -0
- package/lib/checks/json-valid.js +15 -0
- package/lib/checks/latency.js +11 -0
- package/lib/checks/length-bounds.js +17 -0
- package/lib/checks/negative-match.js +14 -0
- package/lib/checks/no-hallucinated-numbers.js +63 -0
- package/lib/checks/non-empty.js +34 -0
- package/lib/checks/regex-match.js +12 -0
- package/lib/checks/run-checks.js +84 -0
- package/lib/checks/schema-match.js +26 -0
- package/lib/checks/tool-call-count.js +16 -0
- package/lib/checks/tool-selection.js +34 -0
- package/lib/checks/types.js +45 -0
- package/lib/comparison/compare.js +86 -0
- package/lib/comparison/format.js +104 -0
- package/lib/comparison/index.js +6 -0
- package/lib/comparison/statistics.js +59 -0
- package/lib/comparison/types.js +41 -0
- package/lib/config-schema.js +200 -0
- package/lib/config.d.ts +66 -0
- package/lib/conversation-store.d.ts +77 -0
- package/lib/conversation-store.js +443 -0
- package/lib/db.d.ts +6 -0
- package/lib/db.js +1112 -0
- package/lib/dep-check.js +99 -0
- package/lib/drift-background.js +61 -0
- package/lib/drift-monitor.js +187 -0
- package/lib/eval-runner.js +566 -0
- package/lib/fixtures/fixture-store.js +161 -0
- package/lib/fixtures/index.js +11 -0
- package/lib/forge-engine.js +982 -0
- package/lib/forge-eval-generator.js +417 -0
- package/lib/forge-file-writer.js +386 -0
- package/lib/forge-service-client.js +190 -0
- package/lib/forge-service.d.ts +4 -0
- package/lib/forge-service.js +655 -0
- package/lib/forge-verifier-generator.js +271 -0
- package/lib/handlers/admin.js +151 -0
- package/lib/handlers/agents.js +229 -0
- package/lib/handlers/chat-resume.js +334 -0
- package/lib/handlers/chat-sync.js +320 -0
- package/lib/handlers/chat.js +320 -0
- package/lib/handlers/conversations.js +92 -0
- package/lib/handlers/preferences.js +88 -0
- package/lib/handlers/tools-list.js +58 -0
- package/lib/hitl-engine.d.ts +60 -0
- package/lib/hitl-engine.js +261 -0
- package/lib/http-utils.js +92 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +141 -0
- package/lib/init.js +636 -0
- package/lib/manual-entry.js +59 -0
- package/lib/mcp-server.js +252 -0
- package/lib/output-groups.js +54 -0
- package/lib/postgres-store.d.ts +31 -0
- package/lib/postgres-store.js +465 -0
- package/lib/preference-store.d.ts +47 -0
- package/lib/preference-store.js +79 -0
- package/lib/prompt-store.d.ts +42 -0
- package/lib/prompt-store.js +60 -0
- package/lib/rate-limiter.d.ts +30 -0
- package/lib/rate-limiter.js +104 -0
- package/lib/react-engine.d.ts +110 -0
- package/lib/react-engine.js +337 -0
- package/lib/runner/cli.js +156 -0
- package/lib/runner/cost-estimator.js +71 -0
- package/lib/runner/gate.js +46 -0
- package/lib/runner/index.js +165 -0
- package/lib/sidecar.d.ts +83 -0
- package/lib/sidecar.js +161 -0
- package/lib/sse.d.ts +15 -0
- package/lib/sse.js +30 -0
- package/lib/tools-scanner.js +91 -0
- package/lib/tui.js +253 -0
- package/lib/verifier-report.js +78 -0
- package/lib/verifier-runner.js +338 -0
- package/lib/verifier-scanner.js +70 -0
- package/lib/verifier-worker-pool.js +196 -0
- package/lib/views/chat.js +340 -0
- package/lib/views/endpoints.js +203 -0
- package/lib/views/eval-run.js +206 -0
- package/lib/views/forge-agent.js +538 -0
- package/lib/views/forge.js +410 -0
- package/lib/views/main-menu.js +275 -0
- package/lib/views/mediation.js +381 -0
- package/lib/views/model-compare.js +430 -0
- package/lib/views/model-comparison.js +333 -0
- package/lib/views/onboarding.js +470 -0
- package/lib/views/performance.js +237 -0
- package/lib/views/run-evals.js +205 -0
- package/lib/views/settings.js +829 -0
- package/lib/views/tools-evals.js +514 -0
- package/lib/views/verifier-coverage.js +617 -0
- package/lib/workers/verifier-worker.js +52 -0
- package/package.json +123 -0
- package/widget/forge-chat.js +789 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mediation View — Fast-track dialogue for resolving tool registry drift.
|
|
3
|
+
*
|
|
4
|
+
* Reuses forgeStep from forge-engine. Starts at 'description' phase with spec.name
|
|
5
|
+
* pre-filled from config._mediationTarget. Right panel shows overlap matrix.
|
|
6
|
+
*
|
|
7
|
+
* Active phases: description, evals only (skips explore/skeptic/fields/routing/deps).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import blessed from 'blessed';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { resolve } from 'path';
|
|
13
|
+
|
|
14
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function loadEnv(projectRoot) {
|
|
17
|
+
const envPath = resolve(projectRoot, '.env');
|
|
18
|
+
if (!existsSync(envPath)) return {};
|
|
19
|
+
const lines = readFileSync(envPath, 'utf-8').split('\n');
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const t = line.trim();
|
|
23
|
+
if (!t || t.startsWith('#')) continue;
|
|
24
|
+
const eq = t.indexOf('=');
|
|
25
|
+
if (eq === -1) continue;
|
|
26
|
+
out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── View ───────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export function createView({ screen, content, config, navigate, setFooter, screenKey, openPopup, closePopup, startService }) {
|
|
34
|
+
const toolName = config._mediationTarget || null;
|
|
35
|
+
config._mediationTarget = null; // consume — prevent stale reads on re-entry
|
|
36
|
+
|
|
37
|
+
const container = blessed.box({
|
|
38
|
+
top: 0, left: 0, width: '100%', height: '100%', tags: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ── Layout: left chat, right overlap matrix ────────────────────────────
|
|
42
|
+
const chatBox = blessed.box({
|
|
43
|
+
parent: container,
|
|
44
|
+
top: 0, left: 0,
|
|
45
|
+
width: '60%', height: '100%',
|
|
46
|
+
tags: true, scrollable: true,
|
|
47
|
+
border: { type: 'line' },
|
|
48
|
+
label: ` Mediation: ${toolName || '(no target)'} `,
|
|
49
|
+
style: { border: { fg: 'yellow' } }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const rightPanel = blessed.box({
|
|
53
|
+
parent: container,
|
|
54
|
+
top: 0, right: 0,
|
|
55
|
+
width: '40%', height: '50%',
|
|
56
|
+
tags: true, scrollable: true,
|
|
57
|
+
border: { type: 'line' },
|
|
58
|
+
label: ' Overlap Matrix ',
|
|
59
|
+
style: { border: { fg: 'cyan' } }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const specPanel = blessed.box({
|
|
63
|
+
parent: container,
|
|
64
|
+
bottom: 0, right: 0,
|
|
65
|
+
width: '40%', height: '50%',
|
|
66
|
+
tags: true, scrollable: true,
|
|
67
|
+
border: { type: 'line' },
|
|
68
|
+
label: ' Current Spec ',
|
|
69
|
+
style: { border: { fg: '#555555' } }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const inputBox = blessed.textbox({
|
|
73
|
+
parent: container,
|
|
74
|
+
bottom: 0, left: 0,
|
|
75
|
+
width: '60%', height: 3,
|
|
76
|
+
border: { type: 'line' },
|
|
77
|
+
label: ' Your reply ',
|
|
78
|
+
keys: true, inputOnFocus: true,
|
|
79
|
+
style: { border: { fg: 'blue' }, focus: { border: { fg: 'cyan' } } }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
setFooter(' {cyan-fg}Enter{/cyan-fg} send {cyan-fg}b{/cyan-fg} back (abandon mediation)');
|
|
83
|
+
|
|
84
|
+
if (!toolName) {
|
|
85
|
+
chatBox.setContent('\n {red-fg}No mediation target set. Go to Tools & Evals and use Mediate.{/red-fg}');
|
|
86
|
+
screen.render();
|
|
87
|
+
return container;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
91
|
+
let forgeState = null;
|
|
92
|
+
let modelConfig = null;
|
|
93
|
+
let db = null;
|
|
94
|
+
let openAlertId = null;
|
|
95
|
+
let baselinePassRate = null;
|
|
96
|
+
let busy = false;
|
|
97
|
+
const chatHistory = [];
|
|
98
|
+
|
|
99
|
+
function appendChat(role, text) {
|
|
100
|
+
chatHistory.push({ role, text });
|
|
101
|
+
const rendered = chatHistory.map((m) =>
|
|
102
|
+
m.role === 'assistant'
|
|
103
|
+
? `{cyan-fg}Forge:{/cyan-fg} ${m.text}`
|
|
104
|
+
: `{white-fg}You:{/white-fg} ${m.text}`
|
|
105
|
+
).join('\n\n');
|
|
106
|
+
chatBox.setContent('\n' + rendered);
|
|
107
|
+
chatBox.setScrollPerc(100);
|
|
108
|
+
screen.render();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateSpecPanel(spec) {
|
|
112
|
+
if (!spec) { specPanel.setContent(''); return; }
|
|
113
|
+
const lines = [
|
|
114
|
+
spec.name ? `{cyan-fg}name:{/cyan-fg} ${spec.name}` : '',
|
|
115
|
+
spec.description ? `{cyan-fg}desc:{/cyan-fg} ${spec.description}` : '',
|
|
116
|
+
spec.triggerPhrases?.length ? `{cyan-fg}triggers:{/cyan-fg}\n ${spec.triggerPhrases.join('\n ')}` : '',
|
|
117
|
+
].filter(Boolean);
|
|
118
|
+
specPanel.setContent('\n ' + lines.join('\n '));
|
|
119
|
+
screen.render();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Init ───────────────────────────────────────────────────────────────
|
|
123
|
+
async function init() {
|
|
124
|
+
busy = true;
|
|
125
|
+
openPopup?.(); // block global keys during init
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Load env + model config
|
|
129
|
+
const projectRoot = process.cwd();
|
|
130
|
+
const env = loadEnv(projectRoot);
|
|
131
|
+
const { resolveModelConfig } = await import('../api-client.js');
|
|
132
|
+
modelConfig = resolveModelConfig(config, env, 'generation');
|
|
133
|
+
|
|
134
|
+
if (!modelConfig.apiKey) {
|
|
135
|
+
appendChat('assistant', '{red-fg}No API key found. Add one in Settings → API Keys.{/red-fg}');
|
|
136
|
+
closePopup?.();
|
|
137
|
+
busy = false;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Load DB + open alert
|
|
142
|
+
const dbPath = resolve(projectRoot, config?.dbPath || 'forge.db');
|
|
143
|
+
if (existsSync(dbPath)) {
|
|
144
|
+
const { getDb, getDriftAlerts, getToolRegistry } = await import('../db.js');
|
|
145
|
+
db = getDb(dbPath);
|
|
146
|
+
const alerts = getDriftAlerts(db, toolName);
|
|
147
|
+
if (alerts.length > 0) {
|
|
148
|
+
openAlertId = alerts[0].id;
|
|
149
|
+
}
|
|
150
|
+
const regRow = getToolRegistry(db, toolName);
|
|
151
|
+
baselinePassRate = regRow?.baseline_pass_rate ?? null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Load forge engine and create initial state at 'description' phase
|
|
155
|
+
const { createInitialState, forgeStep } = await import('../forge-engine.js');
|
|
156
|
+
forgeState = createInitialState();
|
|
157
|
+
forgeState.phase = 'description';
|
|
158
|
+
forgeState.spec.name = toolName;
|
|
159
|
+
|
|
160
|
+
// Build overlap matrix via one-shot LLM call
|
|
161
|
+
await buildOverlapMatrix();
|
|
162
|
+
|
|
163
|
+
// Kick off description phase
|
|
164
|
+
closePopup?.();
|
|
165
|
+
busy = false;
|
|
166
|
+
await advanceForge(null);
|
|
167
|
+
|
|
168
|
+
} catch (err) {
|
|
169
|
+
appendChat('assistant', `{red-fg}Init error: ${err.message}{/red-fg}`);
|
|
170
|
+
closePopup?.();
|
|
171
|
+
busy = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function buildOverlapMatrix() {
|
|
176
|
+
let matrixContent = ' {#888888-fg}(loading overlap matrix…){/#888888-fg}';
|
|
177
|
+
rightPanel.setContent(matrixContent);
|
|
178
|
+
screen.render();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Get suspects from drift alert
|
|
182
|
+
let suspects = [];
|
|
183
|
+
if (db && openAlertId) {
|
|
184
|
+
const alert = db.prepare('SELECT trigger_tools FROM drift_alerts WHERE id = ?').get(openAlertId);
|
|
185
|
+
if (alert?.trigger_tools) {
|
|
186
|
+
try { suspects = JSON.parse(alert.trigger_tools); } catch (_) {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (suspects.length === 0) {
|
|
191
|
+
rightPanel.setContent(' {#888888-fg}No overlap suspects identified.{/#888888-fg}');
|
|
192
|
+
screen.render();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Assess overlap with one LLM call
|
|
197
|
+
const { llmTurn } = await import('../api-client.js');
|
|
198
|
+
const { getToolsWithMetadata } = await import('../tools-scanner.js');
|
|
199
|
+
const project = config?.project || {};
|
|
200
|
+
const allTools = getToolsWithMetadata(project);
|
|
201
|
+
|
|
202
|
+
const flaggedTool = allTools.find((t) => t.name === toolName);
|
|
203
|
+
const suspectTools = allTools.filter((t) => suspects.includes(t.name));
|
|
204
|
+
|
|
205
|
+
if (!flaggedTool || suspectTools.length === 0) {
|
|
206
|
+
rightPanel.setContent(` {yellow-fg}Suspects: ${suspects.join(', ')}{/yellow-fg}\n {#888888-fg}(tool files not found){/#888888-fg}`);
|
|
207
|
+
screen.render();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const prompt = `Assess description and trigger phrase overlap between the flagged tool and each suspect.
|
|
212
|
+
|
|
213
|
+
Flagged tool:
|
|
214
|
+
Name: ${flaggedTool.name}
|
|
215
|
+
Description: ${flaggedTool.description || '(none)'}
|
|
216
|
+
Triggers: ${(flaggedTool.triggerPhrases || []).join(', ') || '(none)'}
|
|
217
|
+
|
|
218
|
+
Suspects:
|
|
219
|
+
${suspectTools.map((t) => `Name: ${t.name}\nDescription: ${t.description || '(none)'}\nTriggers: ${(t.triggerPhrases || []).join(', ') || '(none)'}`).join('\n\n')}
|
|
220
|
+
|
|
221
|
+
For each suspect, rate overlap as: high / medium / low / none.
|
|
222
|
+
Respond with one line per suspect in this format:
|
|
223
|
+
[suspect_name]: [overlap_level] — [one sentence reason]`;
|
|
224
|
+
|
|
225
|
+
const result = await llmTurn({
|
|
226
|
+
provider: modelConfig.provider,
|
|
227
|
+
apiKey: modelConfig.apiKey,
|
|
228
|
+
model: modelConfig.model,
|
|
229
|
+
messages: [{ role: 'user', content: prompt }],
|
|
230
|
+
maxTokens: 512,
|
|
231
|
+
timeoutMs: 30_000
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const lines = (result.text || '').split('\n').filter((l) => l.trim());
|
|
235
|
+
const matrixLines = lines.map((l) => {
|
|
236
|
+
if (l.includes('high')) return ` {red-fg}${l}{/red-fg}`;
|
|
237
|
+
if (l.includes('medium')) return ` {yellow-fg}${l}{/yellow-fg}`;
|
|
238
|
+
if (l.includes('low')) return ` {green-fg}${l}{/green-fg}`;
|
|
239
|
+
return ` {#888888-fg}${l}{/#888888-fg}`;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
rightPanel.setContent(matrixLines.join('\n') || ' {#888888-fg}(no overlap data){/#888888-fg}');
|
|
243
|
+
screen.render();
|
|
244
|
+
|
|
245
|
+
} catch (_) {
|
|
246
|
+
rightPanel.setContent(' {red-fg}Overlap matrix unavailable{/red-fg}');
|
|
247
|
+
screen.render();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function advanceForge(userInput) {
|
|
252
|
+
if (!forgeState || !modelConfig) return;
|
|
253
|
+
|
|
254
|
+
// Only allow description and evals phases in mediation
|
|
255
|
+
const allowedPhases = ['description', 'evals'];
|
|
256
|
+
if (!allowedPhases.includes(forgeState.phase)) {
|
|
257
|
+
// Skip to description or signal completion
|
|
258
|
+
if (forgeState.phase === 'done' || forgeState.phase === 'verifiers') {
|
|
259
|
+
await handleMediationComplete();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Jump to done if past evals
|
|
263
|
+
forgeState.phase = 'done';
|
|
264
|
+
await handleMediationComplete();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
busy = true;
|
|
269
|
+
try {
|
|
270
|
+
const { forgeStep } = await import('../forge-engine.js');
|
|
271
|
+
const result = await forgeStep({
|
|
272
|
+
state: forgeState,
|
|
273
|
+
userInput,
|
|
274
|
+
modelConfig,
|
|
275
|
+
projectConfig: config,
|
|
276
|
+
projectRoot: process.cwd()
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
forgeState = result.nextState;
|
|
280
|
+
updateSpecPanel(forgeState.spec);
|
|
281
|
+
|
|
282
|
+
if (result.assistantText) {
|
|
283
|
+
appendChat('assistant', result.assistantText);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for write_evals action
|
|
287
|
+
const evalsAction = result.actions?.find((a) => a.type === 'write_evals');
|
|
288
|
+
if (evalsAction) {
|
|
289
|
+
await runMediationEvals();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If done phase reached
|
|
294
|
+
if (forgeState.phase === 'done') {
|
|
295
|
+
await handleMediationComplete();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
} catch (err) {
|
|
300
|
+
appendChat('assistant', `{red-fg}Error: ${err.message}{/red-fg}`);
|
|
301
|
+
} finally {
|
|
302
|
+
busy = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function runMediationEvals() {
|
|
307
|
+
appendChat('assistant', 'Running evals to check if description/triggers resolve the drift…');
|
|
308
|
+
busy = true;
|
|
309
|
+
try {
|
|
310
|
+
const { runEvals } = await import('../eval-runner.js');
|
|
311
|
+
const result = await runEvals(
|
|
312
|
+
toolName,
|
|
313
|
+
config,
|
|
314
|
+
process.cwd(),
|
|
315
|
+
(progress) => {
|
|
316
|
+
appendChat('assistant', ` Case ${progress.done}/${progress.total}: ${progress.passed ? '✓' : '✗'}`);
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const newPassRate = result.total > 0 ? result.passed / result.total : 0;
|
|
321
|
+
const recovered = baselinePassRate != null
|
|
322
|
+
? newPassRate >= baselinePassRate - 0.05
|
|
323
|
+
: newPassRate >= 0.8;
|
|
324
|
+
|
|
325
|
+
appendChat('assistant',
|
|
326
|
+
`Eval complete: ${result.passed}/${result.total} passed (${Math.round(newPassRate * 100)}%). ` +
|
|
327
|
+
(recovered
|
|
328
|
+
? '{green-fg}Recovery threshold met!{/green-fg}'
|
|
329
|
+
: `{yellow-fg}Not yet at baseline (${baselinePassRate != null ? Math.round(baselinePassRate * 100) + '%' : '?'}). Try refining further.{/yellow-fg}`)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (recovered && openAlertId && db) {
|
|
333
|
+
// Resolve the alert and promote the same tool back to 'promoted' state.
|
|
334
|
+
// We do NOT call resolveDrift (which swaps in a replacement); we resolve
|
|
335
|
+
// the alert directly and update the lifecycle.
|
|
336
|
+
const { resolveDriftAlert, updateToolLifecycle } = await import('../db.js');
|
|
337
|
+
resolveDriftAlert(db, openAlertId);
|
|
338
|
+
updateToolLifecycle(db, toolName, {
|
|
339
|
+
lifecycle_state: 'promoted',
|
|
340
|
+
promoted_at: new Date().toISOString()
|
|
341
|
+
});
|
|
342
|
+
appendChat('assistant', '{green-fg}Drift resolved. Navigating to Tools & Evals…{/green-fg}');
|
|
343
|
+
setTimeout(() => navigate('tools-evals'), 2000);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
} catch (err) {
|
|
347
|
+
appendChat('assistant', `{red-fg}Eval error: ${err.message}{/red-fg}`);
|
|
348
|
+
} finally {
|
|
349
|
+
busy = false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function handleMediationComplete() {
|
|
354
|
+
appendChat('assistant', 'Mediation dialogue complete. Returning to Tools & Evals.');
|
|
355
|
+
setTimeout(() => navigate('tools-evals'), 1500);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Input handling ─────────────────────────────────────────────────────
|
|
359
|
+
inputBox.key('enter', async () => {
|
|
360
|
+
if (busy) return;
|
|
361
|
+
const val = inputBox.getValue().trim();
|
|
362
|
+
inputBox.clearValue();
|
|
363
|
+
screen.render();
|
|
364
|
+
if (!val) return;
|
|
365
|
+
appendChat('user', val);
|
|
366
|
+
await advanceForge(val);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
inputBox.key(['escape'], () => {
|
|
370
|
+
// Don't navigate — let global b handle it
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ── Start init ─────────────────────────────────────────────────────────
|
|
374
|
+
init().catch((err) => {
|
|
375
|
+
appendChat('assistant', `{red-fg}Fatal: ${err.message}{/red-fg}`);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
inputBox.focus();
|
|
379
|
+
screen.render();
|
|
380
|
+
return container;
|
|
381
|
+
}
|