llm-switcher 1.6.6 → 1.6.7

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/ui.html CHANGED
@@ -910,6 +910,10 @@
910
910
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 14h6m0 0v6m0-6L3 21"/><path d="M20 10h-6m0 0V4m0 6l7-7"/><circle cx="12" cy="12" r="2"/></svg>
911
911
  Idle Compaction<span class="ct" id="ct-compact"></span>
912
912
  </a>
913
+ <a class="nav-link" href="#/goal-check" data-nav="goal-check">
914
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>
915
+ Goal Check<span class="ct" id="ct-goal-check"></span>
916
+ </a>
913
917
  <a class="nav-link" href="#/models" data-nav="models">
914
918
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/></svg>
915
919
  Model Catalog<span class="ct" id="ct-models"></span>
@@ -1218,6 +1222,224 @@
1218
1222
  </div>
1219
1223
  </div>
1220
1224
 
1225
+ <!-- VIEW: Goal Check (Jev Verification Gate) -->
1226
+ <div id="view-goal-check" class="view-panel" style="display:none;">
1227
+ <div class="sec">
1228
+ <!-- Overview Stats -->
1229
+ <div class="stats">
1230
+ <div class="stat">
1231
+ <div class="k">Goal Check Engine</div>
1232
+ <div class="v" id="stat-goal-check-status" style="color:var(--muted); font-size:1.4rem;">Disabled</div>
1233
+ <div class="status" id="stat-goal-check-indicator" style="margin-top:0.3rem;">Evaluator veto inactive</div>
1234
+ </div>
1235
+
1236
+ <div class="stat">
1237
+ <div class="k">Decision Backend</div>
1238
+ <div class="v" id="stat-goal-check-backend" style="color:var(--accent); font-size:1.4rem;">Jev System One</div>
1239
+ <div class="hint" style="margin-top:0.3rem;">Typed 3-axis decision API</div>
1240
+ </div>
1241
+
1242
+ <div class="stat">
1243
+ <div class="k">Active Decision Model</div>
1244
+ <div class="v mono" id="stat-goal-check-model" style="color:var(--mint); font-size:1.15rem; word-break:break-all;">typesafe/jev-latest</div>
1245
+ <div class="hint" style="margin-top:0.3rem;">Evaluates complete &amp; evidence</div>
1246
+ </div>
1247
+
1248
+ <div class="stat">
1249
+ <div class="k">Threshold Gate</div>
1250
+ <div class="v" id="stat-goal-check-thresholds" style="color:var(--code); font-size:1.4rem;">90 / 85 / 10%</div>
1251
+ <div class="hint" style="margin-top:0.3rem;">Complete / Evidence / Unfinished</div>
1252
+ </div>
1253
+ </div>
1254
+
1255
+ <!-- Hero Master Card -->
1256
+ <div class="card pad">
1257
+ <div class="sech">
1258
+ <div>
1259
+ <div style="display:flex; align-items:center; gap:0.6rem;">
1260
+ <h2 style="font-size:1.05rem; font-weight:600;">Goal Check: Jev Completion Gate</h2>
1261
+ <span class="badge outline" id="badge-goal-check">OFF</span>
1262
+ </div>
1263
+ <p class="hint" style="margin-top:0.35rem; max-width:54rem; line-height:1.45;">
1264
+ Verifies Claude Code stopping conditions (including <code>/goal</code>) using independent Jev System One judgments.
1265
+ Intercepts evaluator model verdicts and vetoes completion claims that lack tool-executed implementation evidence, preventing false positives from mere report edits.
1266
+ </p>
1267
+ </div>
1268
+ <div style="display:flex; align-items:center; gap:0.8rem;">
1269
+ <label class="switch" title="Toggle Goal Check Gate">
1270
+ <input type="checkbox" id="goal-check-enabled" onchange="toggleGoalCheckEnabled()">
1271
+ <span></span>
1272
+ </label>
1273
+ </div>
1274
+ </div>
1275
+ <div id="goal-check-status-msg" style="margin-top:0.75rem; font-size:0.82rem; min-height:1.2rem;" aria-live="polite"></div>
1276
+ </div>
1277
+
1278
+ <!-- Configuration Section (Hidden when inactive) -->
1279
+ <div id="goal-check-config-section" style="display:none; flex-direction:column; gap:1.25rem;">
1280
+ <!-- Card 1: Core Provider & Model Settings -->
1281
+ <div class="card">
1282
+ <div class="cardh">
1283
+ <h3 style="font-size:0.95rem; font-weight:600;">Jev Decision Service &amp; Credentials</h3>
1284
+ <span class="badge ok">Gateway Interceptor</span>
1285
+ </div>
1286
+
1287
+ <div class="row">
1288
+ <div class="who">
1289
+ <div class="l">Model Provider Source</div>
1290
+ <div class="meta" id="goal-check-source-meta">Select existing profile to inherit Base URL and credentials, or configure a custom endpoint</div>
1291
+ </div>
1292
+ <div style="display:flex; align-items:center; gap:0.5rem; margin-left:auto; min-width:18rem; max-width:26rem;">
1293
+ <select id="goal-check-source-profile" onchange="onGoalCheckSourceProfileChange()" style="flex:1; font-size:0.8rem;"></select>
1294
+ </div>
1295
+ </div>
1296
+
1297
+ <div class="row">
1298
+ <div class="who">
1299
+ <div class="l">Base URL</div>
1300
+ <div class="meta">Provider Base URL (e.g. https://provider.example/v1). Decision endpoint defaults to &lt;Base URL&gt;/systemone</div>
1301
+ </div>
1302
+ <div style="min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
1303
+ <input type="text" id="goal-check-base-url" placeholder="https://provider.example/v1" oninput="invalidateGoalCheckProbe()" style="font-family:var(--font-mono); font-size:0.8rem;" title="Base URL for Jev decision provider">
1304
+ </div>
1305
+ </div>
1306
+
1307
+ <div class="row">
1308
+ <div class="who">
1309
+ <div class="l">API Key</div>
1310
+ <div class="meta" id="goal-check-key-meta">Leave empty to use credentials from the selected profile, or enter a dedicated key</div>
1311
+ </div>
1312
+ <div style="min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
1313
+ <input type="password" id="goal-check-api-key" placeholder="••••••••••••••••" oninput="invalidateGoalCheckProbe()" autocomplete="off" style="font-family:var(--font-mono); font-size:0.8rem;" title="API key for Jev decision service">
1314
+ </div>
1315
+ </div>
1316
+
1317
+ <div class="row" style="align-items:flex-start;">
1318
+ <div class="who" style="padding-top:0.25rem;">
1319
+ <div class="l">Goal Check Model</div>
1320
+ <div class="meta" id="goal-check-models-status">Jev decision model ID (default typesafe/jev-latest)</div>
1321
+ </div>
1322
+ <div style="display:flex; align-items:center; gap:0.5rem; min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
1323
+ <div class="combo" id="combo-goal-check-model" style="flex:1;">
1324
+ <input type="text" id="goal-check-model" placeholder="typesafe/jev-latest" maxlength="256" oninput="invalidateGoalCheckProbe()" role="combobox" aria-autocomplete="list" aria-expanded="false" autocomplete="off" spellcheck="false" style="font-family:var(--font-mono); font-size:0.8rem;" title="Model ID passed in Jev systemone request">
1325
+ <button type="button" class="combo-toggle" tabindex="-1" aria-label="Show available models">
1326
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
1327
+ </button>
1328
+ <ul class="combo-list" id="goal-check-model-list" role="listbox" aria-label="Available models" hidden></ul>
1329
+ </div>
1330
+ <button type="button" class="btn btn-outline btn-sm" id="btn-reload-goal-check-models" onclick="reloadGoalCheckModels()" title="Fetch model catalog from provider">Reload</button>
1331
+ </div>
1332
+ </div>
1333
+
1334
+ <!-- Action Bar -->
1335
+ <div class="row" style="justify-content:flex-end; gap:0.6rem; padding-top:0.75rem; border-top:1px solid var(--border);">
1336
+ <div id="goal-check-probe-badge" style="margin-right:auto; font-size:0.82rem; display:flex; align-items:center; gap:0.4rem;"></div>
1337
+ <button type="button" class="btn btn-outline btn-sm" id="btn-test-goal-check" onclick="testGoalCheckProbe()" title="Probe Jev endpoint with a synthetic decision test">Test Connection</button>
1338
+ <button type="button" class="btn btn-primary btn-sm" id="btn-save-goal-check" onclick="saveGoalCheckConfig()" title="Save Goal Check settings">Save</button>
1339
+ </div>
1340
+ </div>
1341
+
1342
+ <!-- Card 2: Advanced Thresholds & Boundaries -->
1343
+ <div class="card">
1344
+ <div class="cardh">
1345
+ <h3 style="font-size:0.95rem; font-weight:600;">Advanced Thresholds &amp; Evidence Budget</h3>
1346
+ <span class="badge">Fine Tuning</span>
1347
+ </div>
1348
+
1349
+ <div class="row">
1350
+ <div class="who">
1351
+ <div class="l">Decision Endpoint Override</div>
1352
+ <div class="meta">Leave blank to use default &lt;Base URL&gt;/systemone, or specify absolute URL / custom path</div>
1353
+ </div>
1354
+ <div style="min-width:18rem; flex:1; max-width:26rem; margin-left:auto;">
1355
+ <input type="text" id="goal-check-decision-endpoint" placeholder="empty = <Base URL>/systemone" oninput="invalidateGoalCheckProbe()" style="font-family:var(--font-mono); font-size:0.8rem;" title="Custom path or absolute URL override for Jev typed decision API">
1356
+ </div>
1357
+ </div>
1358
+
1359
+ <div class="row">
1360
+ <div class="who">
1361
+ <div class="l">Completion Threshold (completeMin)</div>
1362
+ <div class="meta">Minimum Jev confidence (0.00 – 1.00) that the entire scope required by the stopping condition is done</div>
1363
+ </div>
1364
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1365
+ <input type="number" id="goal-check-complete-min" min="0" max="1" step="0.01" style="width:6.5rem; text-align:right;" placeholder="0.90" title="Minimum complete score (default: 0.90)">
1366
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">min</span>
1367
+ </div>
1368
+ </div>
1369
+
1370
+ <div class="row">
1371
+ <div class="who">
1372
+ <div class="l">Evidence Threshold (evidenceMin)</div>
1373
+ <div class="meta">Minimum Jev confidence (0.00 – 1.00) that execution evidence in the transcript proves the work</div>
1374
+ </div>
1375
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1376
+ <input type="number" id="goal-check-evidence-min" min="0" max="1" step="0.01" style="width:6.5rem; text-align:right;" placeholder="0.85" title="Minimum evidence score (default: 0.85)">
1377
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">min</span>
1378
+ </div>
1379
+ </div>
1380
+
1381
+ <div class="row">
1382
+ <div class="who">
1383
+ <div class="l">Unfinished Work Ceiling (unfinishedMax)</div>
1384
+ <div class="meta">Maximum allowed Jev score (0.00 – 1.00) for remaining work, deferred tasks, or unverified errors</div>
1385
+ </div>
1386
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1387
+ <input type="number" id="goal-check-unfinished-max" min="0" max="1" step="0.01" style="width:6.5rem; text-align:right;" placeholder="0.10" title="Maximum unfinished score (default: 0.10)">
1388
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">max</span>
1389
+ </div>
1390
+ </div>
1391
+
1392
+ <div class="row">
1393
+ <div class="who">
1394
+ <div class="l">Jev Decision Timeout</div>
1395
+ <div class="meta">Timeout for Jev decision API call in milliseconds</div>
1396
+ </div>
1397
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1398
+ <input type="number" id="goal-check-timeout" min="500" max="60000" step="500" style="width:6.5rem; text-align:right;" placeholder="8000" title="Jev decision call timeout in ms (default: 8000)">
1399
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">ms</span>
1400
+ </div>
1401
+ </div>
1402
+
1403
+ <div class="row">
1404
+ <div class="who">
1405
+ <div class="l">Total Gate Deadline</div>
1406
+ <div class="meta">Total deadline for Native and Jev evaluation before timing out</div>
1407
+ </div>
1408
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1409
+ <input type="number" id="goal-check-total-timeout" min="1000" max="120000" step="1000" style="width:6.5rem; text-align:right;" placeholder="25000" title="Total gate deadline in ms (default: 25000)">
1410
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">ms</span>
1411
+ </div>
1412
+ </div>
1413
+
1414
+ <div class="row">
1415
+ <div class="who">
1416
+ <div class="l">Max Evidence State Size</div>
1417
+ <div class="meta">Maximum characters sent to Jev state (~32k tokens). Exceeding this fails closed as Insufficient evidence</div>
1418
+ </div>
1419
+ <div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
1420
+ <input type="number" id="goal-check-max-chars" min="500" max="500000" step="1000" style="width:6.5rem; text-align:right;" placeholder="60000" title="Maximum state characters (default: 60000)">
1421
+ <span style="font-size:0.8rem; color:var(--muted); min-width:1.5rem;">chars</span>
1422
+ </div>
1423
+ </div>
1424
+ </div>
1425
+
1426
+ <!-- Card 3: Recent Goal Verifications -->
1427
+ <div class="card pad">
1428
+ <div class="sech">
1429
+ <div>
1430
+ <h3 style="font-size:0.95rem; font-weight:600;">Recent Goal Verifications</h3>
1431
+ <p class="hint" style="margin-top:0.2rem;">Live ring buffer of stopping condition evaluator queries and Jev gate verdicts.</p>
1432
+ </div>
1433
+ <div style="display:flex; align-items:center; gap:0.6rem;">
1434
+ <button type="button" class="btn btn-outline btn-sm" onclick="loadGoalCheckLogs()" title="Refresh verification history">Refresh</button>
1435
+ </div>
1436
+ </div>
1437
+ <div id="goal-check-logs-container" style="margin-top:0.75rem;"></div>
1438
+ </div>
1439
+ </div>
1440
+ </div>
1441
+ </div>
1442
+
1221
1443
  <!-- VIEW 2: Discovered Models Catalog -->
1222
1444
  <div id="view-models" class="view-panel" style="display:none;">
1223
1445
  <div class="sec">
@@ -1653,7 +1875,7 @@
1653
1875
  function handleRoute() {
1654
1876
  const hash = window.location.hash || '#/routes';
1655
1877
  const route = hash.replace(/^#\/?/, '').split('/')[0] || 'routes';
1656
- const validRoutes = ['routes', 'compact', 'models', 'logs', 'doctor'];
1878
+ const validRoutes = ['routes', 'compact', 'goal-check', 'models', 'logs', 'doctor'];
1657
1879
  const activeRoute = validRoutes.includes(route) ? route : 'routes';
1658
1880
 
1659
1881
  // Update active nav link
@@ -1675,6 +1897,7 @@
1675
1897
 
1676
1898
  // Lazy load view content
1677
1899
  if (activeRoute === 'compact') loadIdleCompact();
1900
+ if (activeRoute === 'goal-check') loadGoalCheck();
1678
1901
  if (activeRoute === 'models') renderCatalogView();
1679
1902
  if (activeRoute === 'logs') loadInspectorLogs();
1680
1903
  }
@@ -1715,6 +1938,14 @@
1715
1938
  <button class="btn btn-outline" onclick="loadIdleCompact()">Refresh</button>
1716
1939
  <button class="btn btn-primary" onclick="saveIdleCompact()">Save Configuration</button>
1717
1940
  `;
1941
+ } else if (route === 'goal-check') {
1942
+ loadGoalCheck();
1943
+ titleEl.textContent = 'Goal Check';
1944
+ subEl.textContent = 'Independent Jev verification gate for Claude Code stopping conditions and /goal.';
1945
+ actionsEl.innerHTML = `
1946
+ <button class="btn btn-outline" onclick="loadGoalCheck()">Refresh</button>
1947
+ <button class="btn btn-primary" onclick="saveGoalCheckConfig()">Save Configuration</button>
1948
+ `;
1718
1949
  } else if (route === 'models') {
1719
1950
  titleEl.textContent = 'Dynamic Model Catalog';
1720
1951
  subEl.textContent = 'Auto-polled models from upstream providers to guarantee zero 404/drift errors.';
@@ -2286,6 +2517,439 @@
2286
2517
  }
2287
2518
  }
2288
2519
 
2520
+ // ----------------------------------------------------
2521
+ // Goal Check: Jev Completion Gate
2522
+ // ----------------------------------------------------
2523
+ let goalCheckLoadedConfig = null;
2524
+ let goalCheckLoadedModels = [];
2525
+
2526
+ async function loadGoalCheck() {
2527
+ try {
2528
+ const [rCfg, rLogs] = await Promise.all([
2529
+ api('/api/goal-check'),
2530
+ api('/api/goal-check/logs')
2531
+ ]);
2532
+ if (rCfg.ok) {
2533
+ const data = await rCfg.json();
2534
+ goalCheckLoadedConfig = data.goalCheck;
2535
+ showGoalCheck(data.goalCheck, data.status);
2536
+ }
2537
+ if (rLogs.ok) {
2538
+ renderGoalCheckLogs((await rLogs.json()).logs || []);
2539
+ }
2540
+ } catch (err) {
2541
+ console.error('Failed to load Goal Check', err);
2542
+ }
2543
+ }
2544
+
2545
+ function showGoalCheck(c, st) {
2546
+ c = c || {};
2547
+ st = st || {};
2548
+ const enabled = Boolean(c.enabled);
2549
+
2550
+ const elEnabled = document.getElementById('goal-check-enabled');
2551
+ if (elEnabled) elEnabled.checked = enabled;
2552
+
2553
+ const configSec = document.getElementById('goal-check-config-section');
2554
+ if (configSec) configSec.style.display = enabled ? 'flex' : 'none';
2555
+
2556
+ // Badge on hero card
2557
+ const badge = document.getElementById('badge-goal-check');
2558
+ if (badge) {
2559
+ badge.textContent = enabled ? 'ACTIVE' : 'OFF';
2560
+ badge.className = 'badge ' + (enabled ? 'ok' : 'outline');
2561
+ }
2562
+
2563
+ // Nav link count
2564
+ const ctGc = document.getElementById('ct-goal-check');
2565
+ if (ctGc) ctGc.textContent = enabled ? 'ON' : '';
2566
+
2567
+ // Stats
2568
+ const statStatus = document.getElementById('stat-goal-check-status');
2569
+ if (statStatus) {
2570
+ statStatus.textContent = enabled ? 'Active' : 'Disabled';
2571
+ statStatus.style.color = enabled ? 'var(--ok)' : 'var(--muted)';
2572
+ }
2573
+ const statInd = document.getElementById('stat-goal-check-indicator');
2574
+ if (statInd) {
2575
+ statInd.textContent = enabled ? 'Evaluator veto active' : 'Evaluator veto inactive';
2576
+ statInd.className = 'status ' + (enabled ? 'on' : '');
2577
+ }
2578
+ const statModel = document.getElementById('stat-goal-check-model');
2579
+ if (statModel) {
2580
+ statModel.textContent = c.model || 'typesafe/jev-latest';
2581
+ }
2582
+ const statThresh = document.getElementById('stat-goal-check-thresholds');
2583
+ if (statThresh) {
2584
+ const cP = Math.round((c.completeMin ?? 0.90) * 100);
2585
+ const eP = Math.round((c.evidenceMin ?? 0.85) * 100);
2586
+ const uP = Math.round((c.unfinishedMax ?? 0.10) * 100);
2587
+ statThresh.textContent = `${cP} / ${eP} / ${uP}%`;
2588
+ }
2589
+
2590
+ // Populate Profiles
2591
+ populateGoalCheckSourceProfiles(c.sourceProfile);
2592
+
2593
+ // Populate Inputs
2594
+ const elBaseUrl = document.getElementById('goal-check-base-url');
2595
+ if (elBaseUrl) elBaseUrl.value = c.baseURL || '';
2596
+
2597
+ const elApiKey = document.getElementById('goal-check-api-key');
2598
+ if (elApiKey) {
2599
+ elApiKey.value = c.apiKey === '__LLM_SWITCHER_KEEP_KEY__' ? '__LLM_SWITCHER_KEEP_KEY__' : (c.apiKey || '');
2600
+ elApiKey.placeholder = c.hasApiKey ? '•••••••• (Stored in gateway)' : 'API key or leave empty to inherit';
2601
+ }
2602
+
2603
+ const elModel = document.getElementById('goal-check-model');
2604
+ if (elModel) elModel.value = c.model || 'typesafe/jev-latest';
2605
+
2606
+ const elDecisionEp = document.getElementById('goal-check-decision-endpoint');
2607
+ if (elDecisionEp) elDecisionEp.value = c.decisionEndpoint || '';
2608
+
2609
+ const elCompleteMin = document.getElementById('goal-check-complete-min');
2610
+ if (elCompleteMin) elCompleteMin.value = c.completeMin ?? 0.90;
2611
+
2612
+ const elEvidenceMin = document.getElementById('goal-check-evidence-min');
2613
+ if (elEvidenceMin) elEvidenceMin.value = c.evidenceMin ?? 0.85;
2614
+
2615
+ const elUnfinishedMax = document.getElementById('goal-check-unfinished-max');
2616
+ if (elUnfinishedMax) elUnfinishedMax.value = c.unfinishedMax ?? 0.10;
2617
+
2618
+ const elTimeout = document.getElementById('goal-check-timeout');
2619
+ if (elTimeout) elTimeout.value = c.timeoutMs ?? 8000;
2620
+
2621
+ const elTotalTimeout = document.getElementById('goal-check-total-timeout');
2622
+ if (elTotalTimeout) elTotalTimeout.value = c.totalTimeoutMs ?? 25000;
2623
+
2624
+ const elMaxChars = document.getElementById('goal-check-max-chars');
2625
+ if (elMaxChars) elMaxChars.value = c.maxStateChars ?? 60000;
2626
+ }
2627
+
2628
+ function populateGoalCheckSourceProfiles(selectedKey) {
2629
+ const sel = document.getElementById('goal-check-source-profile');
2630
+ if (!sel) return;
2631
+ const profiles = state.config?.profiles || {};
2632
+ const keys = Object.keys(profiles);
2633
+
2634
+ let html = '<option value="">Custom Endpoint (Manual Configuration)</option>';
2635
+ for (const k of keys) {
2636
+ const p = profiles[k];
2637
+ const isSel = (k === selectedKey) ? 'selected' : '';
2638
+ html += `<option value="${escapeHtml(k)}" ${isSel}>${escapeHtml(p.name || k)} (${escapeHtml(p.baseURL || 'no URL')})</option>`;
2639
+ }
2640
+ sel.innerHTML = html;
2641
+ updateGoalCheckSourceMeta();
2642
+ }
2643
+
2644
+ function updateGoalCheckSourceMeta() {
2645
+ const sel = document.getElementById('goal-check-source-profile');
2646
+ const meta = document.getElementById('goal-check-source-meta');
2647
+ const keyMeta = document.getElementById('goal-check-key-meta');
2648
+ if (!sel || !meta) return;
2649
+
2650
+ const chosen = sel.value;
2651
+ if (!chosen) {
2652
+ meta.textContent = 'Custom Endpoint: Manual Base URL and optional API key';
2653
+ if (keyMeta) keyMeta.textContent = 'Enter dedicated API key for this endpoint';
2654
+ return;
2655
+ }
2656
+ const p = state.config?.profiles?.[chosen];
2657
+ if (p) {
2658
+ meta.textContent = `Inheriting from Profile "${p.name || chosen}": ${p.baseURL || 'no Base URL'}`;
2659
+ if (keyMeta) keyMeta.textContent = p.hasApiKey ? 'Using credential from selected profile' : 'Profile has no stored API key; specify one if required';
2660
+ }
2661
+ }
2662
+
2663
+ function onGoalCheckSourceProfileChange() {
2664
+ const sel = document.getElementById('goal-check-source-profile');
2665
+ if (!sel) return;
2666
+ const chosen = sel.value;
2667
+ updateGoalCheckSourceMeta();
2668
+ invalidateGoalCheckProbe();
2669
+
2670
+ const elBaseUrl = document.getElementById('goal-check-base-url');
2671
+ if (chosen && state.config?.profiles?.[chosen]) {
2672
+ const p = state.config.profiles[chosen];
2673
+ if (elBaseUrl && p.baseURL) elBaseUrl.value = p.baseURL;
2674
+ }
2675
+ }
2676
+
2677
+ function invalidateGoalCheckProbe() {
2678
+ const badge = document.getElementById('goal-check-probe-badge');
2679
+ if (badge) badge.innerHTML = '';
2680
+ }
2681
+
2682
+ async function toggleGoalCheckEnabled() {
2683
+ const isEnabled = document.getElementById('goal-check-enabled').checked;
2684
+ const configSec = document.getElementById('goal-check-config-section');
2685
+ if (configSec) configSec.style.display = isEnabled ? 'flex' : 'none';
2686
+ await saveGoalCheckConfig();
2687
+ }
2688
+
2689
+ async function saveGoalCheckConfig() {
2690
+ const status = document.getElementById('goal-check-status-msg');
2691
+ const isEnabled = document.getElementById('goal-check-enabled').checked;
2692
+ const sourceProf = document.getElementById('goal-check-source-profile')?.value || '';
2693
+ const baseURL = document.getElementById('goal-check-base-url')?.value.trim() || '';
2694
+ const apiKeyRaw = document.getElementById('goal-check-api-key')?.value || '';
2695
+ const model = document.getElementById('goal-check-model')?.value.trim() || 'typesafe/jev-latest';
2696
+ const decisionEndpoint = document.getElementById('goal-check-decision-endpoint')?.value.trim() || '';
2697
+
2698
+ const completeMin = Number(document.getElementById('goal-check-complete-min')?.value ?? 0.90);
2699
+ const evidenceMin = Number(document.getElementById('goal-check-evidence-min')?.value ?? 0.85);
2700
+ const unfinishedMax = Number(document.getElementById('goal-check-unfinished-max')?.value ?? 0.10);
2701
+ const timeoutMs = Number(document.getElementById('goal-check-timeout')?.value ?? 8000);
2702
+ const totalTimeoutMs = Number(document.getElementById('goal-check-total-timeout')?.value ?? 25000);
2703
+ const maxStateChars = Number(document.getElementById('goal-check-max-chars')?.value ?? 60000);
2704
+
2705
+ const patch = {
2706
+ enabled: isEnabled,
2707
+ backend: 'jev',
2708
+ sourceProfile: sourceProf,
2709
+ baseURL,
2710
+ model,
2711
+ decisionEndpoint,
2712
+ completeMin,
2713
+ evidenceMin,
2714
+ unfinishedMax,
2715
+ timeoutMs,
2716
+ totalTimeoutMs,
2717
+ maxStateChars
2718
+ };
2719
+
2720
+ if (apiKeyRaw && apiKeyRaw !== '__LLM_SWITCHER_KEEP_KEY__') {
2721
+ patch.apiKey = apiKeyRaw;
2722
+ } else if (apiKeyRaw === '') {
2723
+ patch.apiKey = '';
2724
+ }
2725
+
2726
+ if (status) {
2727
+ status.textContent = 'Saving Goal Check configuration...';
2728
+ status.style.color = 'var(--muted)';
2729
+ }
2730
+
2731
+ try {
2732
+ const res = await api('/api/goal-check', {
2733
+ method: 'POST',
2734
+ headers: { 'Content-Type': 'application/json' },
2735
+ body: JSON.stringify(patch)
2736
+ });
2737
+ const data = await res.json();
2738
+ if (!res.ok) throw new Error(data.error || 'Failed to save configuration');
2739
+
2740
+ goalCheckLoadedConfig = data.goalCheck;
2741
+ showGoalCheck(data.goalCheck, data.status);
2742
+
2743
+ if (status) {
2744
+ status.textContent = 'Saved. Goal Check gate configuration active for stopping condition evaluators.';
2745
+ status.style.color = 'var(--ok)';
2746
+ setTimeout(() => { if (status) status.textContent = ''; }, 4500);
2747
+ }
2748
+ showToast('Goal Check configuration saved');
2749
+ } catch (err) {
2750
+ if (status) {
2751
+ status.textContent = 'Not saved: ' + err.message;
2752
+ status.style.color = 'var(--err)';
2753
+ }
2754
+ showToast('Failed to save: ' + err.message, true);
2755
+ }
2756
+ }
2757
+
2758
+ async function testGoalCheckProbe() {
2759
+ const probeBadge = document.getElementById('goal-check-probe-badge');
2760
+ const testBtn = document.getElementById('btn-test-goal-check');
2761
+ if (testBtn) testBtn.disabled = true;
2762
+
2763
+ const sourceProf = document.getElementById('goal-check-source-profile')?.value || '';
2764
+ const baseURL = document.getElementById('goal-check-base-url')?.value.trim() || '';
2765
+ const apiKeyRaw = document.getElementById('goal-check-api-key')?.value || '';
2766
+ const model = document.getElementById('goal-check-model')?.value.trim() || 'typesafe/jev-latest';
2767
+ const decisionEndpoint = document.getElementById('goal-check-decision-endpoint')?.value.trim() || '';
2768
+ const timeoutMs = Number(document.getElementById('goal-check-timeout')?.value ?? 8000);
2769
+
2770
+ if (probeBadge) {
2771
+ probeBadge.innerHTML = '<span class="status pulse" style="color:var(--accent);">Probing Jev endpoint...</span>';
2772
+ }
2773
+
2774
+ try {
2775
+ const res = await api('/api/goal-check/test', {
2776
+ method: 'POST',
2777
+ headers: { 'Content-Type': 'application/json' },
2778
+ body: JSON.stringify({
2779
+ baseURL,
2780
+ sourceProfile: sourceProf,
2781
+ apiKey: apiKeyRaw,
2782
+ model,
2783
+ decisionEndpoint,
2784
+ timeoutMs
2785
+ })
2786
+ });
2787
+ const data = await res.json();
2788
+ if (!res.ok || !data.ok) {
2789
+ throw new Error(data.error || data.message || 'Probe failed');
2790
+ }
2791
+
2792
+ const scores = data.scores || {};
2793
+ const c = typeof scores.complete === 'number' ? scores.complete.toFixed(2) : '-';
2794
+ const e = typeof scores.evidence === 'number' ? scores.evidence.toFixed(2) : '-';
2795
+ const u = typeof scores.unfinished === 'number' ? scores.unfinished.toFixed(2) : '-';
2796
+
2797
+ if (probeBadge) {
2798
+ probeBadge.innerHTML = `<span class="badge ok">✓ Connected (${data.latencyMs || 0}ms)</span> <span class="hint mono">complete:${c} · ev:${e} · unf:${u}</span>`;
2799
+ }
2800
+ showToast('Jev connection probe succeeded');
2801
+ } catch (err) {
2802
+ if (probeBadge) {
2803
+ probeBadge.innerHTML = `<span class="badge err">✕ ${escapeHtml(err.message)}</span>`;
2804
+ }
2805
+ showToast('Jev probe error: ' + err.message, true);
2806
+ } finally {
2807
+ if (testBtn) testBtn.disabled = false;
2808
+ }
2809
+ }
2810
+
2811
+ async function reloadGoalCheckModels() {
2812
+ const statusEl = document.getElementById('goal-check-models-status');
2813
+ const listEl = document.getElementById('goal-check-model-list');
2814
+ if (statusEl) statusEl.textContent = 'Discovering models from catalog...';
2815
+
2816
+ try {
2817
+ const res = await api('/api/catalog');
2818
+ if (!res.ok) throw new Error('Catalog fetch failed');
2819
+ const catalog = await res.json();
2820
+ const models = new Set(['typesafe/jev-latest', 'typesafe/jev-1.13']);
2821
+
2822
+ for (const item of (catalog || [])) {
2823
+ if (item?.id && (item.id.includes('jev') || item.id.includes('decision'))) {
2824
+ models.add(item.id);
2825
+ }
2826
+ }
2827
+ goalCheckLoadedModels = Array.from(models);
2828
+
2829
+ if (listEl) {
2830
+ listEl.innerHTML = Array.from(models).map(m => {
2831
+ return `<li role="option" class="combo-item" onclick="selectGoalCheckModel('${escapeHtml(m)}')">${escapeHtml(m)}</li>`;
2832
+ }).join('');
2833
+ }
2834
+
2835
+ if (statusEl) statusEl.textContent = `Discovered ${models.size} models. You can also type any custom model ID.`;
2836
+ showToast('Model catalog reloaded');
2837
+ } catch (err) {
2838
+ if (statusEl) statusEl.textContent = 'Catalog reload failed: ' + err.message;
2839
+ }
2840
+ }
2841
+
2842
+ function selectGoalCheckModel(modelId) {
2843
+ const inputEl = document.getElementById('goal-check-model');
2844
+ if (inputEl) inputEl.value = modelId;
2845
+ const listEl = document.getElementById('goal-check-model-list');
2846
+ if (listEl) listEl.hidden = true;
2847
+ invalidateGoalCheckProbe();
2848
+ }
2849
+
2850
+ async function loadGoalCheckLogs() {
2851
+ try {
2852
+ const res = await api('/api/goal-check/logs');
2853
+ if (res.ok) {
2854
+ const data = await res.json();
2855
+ renderGoalCheckLogs(data.logs || []);
2856
+ }
2857
+ } catch (err) {
2858
+ console.error('Failed to load goal check logs', err);
2859
+ }
2860
+ }
2861
+
2862
+ function renderGoalCheckLogs(logs) {
2863
+ const container = document.getElementById('goal-check-logs-container');
2864
+ if (!container) return;
2865
+
2866
+ if (!logs || !logs.length) {
2867
+ container.innerHTML = '<div class="hint" style="text-align:center; padding:1.5rem;">No goal verifications recorded yet. When Claude Code runs stopping condition evaluators or /goal, verdicts appear here.</div>';
2868
+ return;
2869
+ }
2870
+
2871
+ container.innerHTML = logs.map(l => {
2872
+ const dateStr = new Date(l.timestamp).toLocaleTimeString();
2873
+ let badgeClass = 'outline';
2874
+ if (l.verdictType === 'Allowed') badgeClass = 'ok';
2875
+ else if (l.verdictType === 'Blocked') badgeClass = 'err';
2876
+ else if (l.verdictType === 'Unavailable' || l.verdictType === 'Insufficient evidence') badgeClass = 'warn';
2877
+ else if (l.verdictType === 'Unsupported evaluator') badgeClass = 'err';
2878
+
2879
+ const scores = l.jevScores;
2880
+ const scoresStr = scores
2881
+ ? `Complete: ${(scores.complete ?? 0).toFixed(2)} · Evidence: ${(scores.evidence ?? 0).toFixed(2)} · Unfinished: ${(scores.unfinished ?? 0).toFixed(2)}`
2882
+ : 'None';
2883
+
2884
+ const nativeStr = l.nativeVerdict
2885
+ ? (l.nativeVerdict.ok ? '✓ True' : (l.nativeVerdict.impossible ? '✕ Impossible' : '✕ False'))
2886
+ : 'n/a';
2887
+
2888
+ return `
2889
+ <div class="row" style="flex-direction:column; align-items:flex-start; gap:0.4rem; padding:0.75rem 0.5rem; border-bottom:1px solid var(--border);">
2890
+ <div style="display:flex; justify-content:space-between; align-items:center; width:100%; gap:0.5rem;">
2891
+ <div style="display:flex; align-items:center; gap:0.5rem;">
2892
+ <span class="badge ${badgeClass}" style="font-weight:600;">${escapeHtml(l.verdictType || (l.verdict ? 'Allowed' : 'Blocked'))}</span>
2893
+ <span class="mono faint" style="font-size:0.75rem;">${escapeHtml(dateStr)}</span>
2894
+ <span class="mono" style="font-size:0.75rem; color:var(--muted);">${escapeHtml((l.sessionId || '').slice(0, 16))}</span>
2895
+ </div>
2896
+ <div style="display:flex; align-items:center; gap:0.5rem;">
2897
+ <span class="hint mono" style="font-size:0.75rem;">Native: ${escapeHtml(nativeStr)}</span>
2898
+ <span class="hint mono" style="font-size:0.75rem;">${escapeHtml(String(l.latencyMs || 0))}ms</span>
2899
+ </div>
2900
+ </div>
2901
+ <div style="font-size:0.82rem; color:var(--fg); line-height:1.35; margin-top:0.2rem;">
2902
+ <strong>Condition:</strong> ${escapeHtml(l.condition || '(none)')}
2903
+ </div>
2904
+ <div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; color:var(--muted); margin-top:0.15rem;">
2905
+ <span class="mono">${escapeHtml(scoresStr)}</span>
2906
+ <span class="hint" style="max-width:60%; text-overflow:ellipsis; overflow:hidden; white-space:nowrap;" title="${escapeHtml(l.reason || '')}">${escapeHtml(l.reason || '')}</span>
2907
+ </div>
2908
+ </div>
2909
+ `;
2910
+ }).join('');
2911
+ }
2912
+
2913
+ function renderGoalCheckModelOptions(combo, q, current) {
2914
+ const list = combo.querySelector('.combo-list');
2915
+ const defaults = [
2916
+ { id: 'typesafe/jev-latest', label: 'typesafe/jev-latest', desc: 'Default System One decision model' },
2917
+ { id: 'typesafe/jev-1.13', label: 'typesafe/jev-1.13', desc: 'Calibrated System One decision model' }
2918
+ ];
2919
+
2920
+ const all = [...defaults];
2921
+ for (const m of goalCheckLoadedModels) {
2922
+ if (!all.some(it => it.id === m)) {
2923
+ all.push({ id: m, label: m, desc: 'Discovered model' });
2924
+ }
2925
+ }
2926
+
2927
+ const matches = all.filter(it => !q || it.id.toLowerCase().includes(q) || it.label.toLowerCase().includes(q)).slice(0, 100);
2928
+ combo.matches = matches.map(it => it.id);
2929
+
2930
+ if (!matches.length) {
2931
+ list.innerHTML = '<li class="combo-empty">No listed model matches. Custom typed ID will be kept.</li>';
2932
+ return;
2933
+ }
2934
+
2935
+ const matchIds = matches.map(it => it.id);
2936
+ if (!(combo.activeIndex >= 0 && combo.activeIndex < matches.length)) combo.activeIndex = matchIds.indexOf(current);
2937
+
2938
+ list.innerHTML = matches.map((it, i) => {
2939
+ const cut = it.id.lastIndexOf('/') + 1;
2940
+ const prefix = cut ? `<span class="pfx">${markMatch(it.id.slice(0, cut), q)}</span>` : '';
2941
+ const namePart = markMatch(it.id.slice(cut), q);
2942
+ return `<li class="combo-opt" role="option" id="goal-check-model-opt-${i}" data-i="${i}" aria-selected="${i === combo.activeIndex}"`
2943
+ + ` onmousedown="event.preventDefault(); chooseComboOption(this)">`
2944
+ + `<div style="display:flex; flex-direction:column; gap:0.15rem;">`
2945
+ + `<span class="id">${prefix}${namePart}</span>`
2946
+ + `<span class="hint" style="font-size:0.75rem;">${escapeHtml(it.desc)}</span>`
2947
+ + `</div>`
2948
+ + (it.id === current ? '<span class="cur">current</span>' : '') + '</li>';
2949
+ }).join('');
2950
+ setComboActive(combo, combo.activeIndex);
2951
+ }
2952
+
2289
2953
  async function loadStatus() {
2290
2954
  try {
2291
2955
  const res = await api('/api/status');
@@ -2976,6 +3640,10 @@
2976
3640
  renderCompactModelOptions(combo, q, current);
2977
3641
  return;
2978
3642
  }
3643
+ if (input.id === 'goal-check-model') {
3644
+ renderGoalCheckModelOptions(combo, q, current);
3645
+ return;
3646
+ }
2979
3647
 
2980
3648
  const matches = loadedModels.filter(m => !q || m.toLowerCase().includes(q)).slice(0, 200);
2981
3649
  combo.matches = matches;
@@ -3103,6 +3771,9 @@
3103
3771
  if (input.id === 'idle-compact-model') {
3104
3772
  saveIdleCompact();
3105
3773
  }
3774
+ if (input.id === 'goal-check-model') {
3775
+ invalidateGoalCheckProbe();
3776
+ }
3106
3777
  }
3107
3778
 
3108
3779
  function onComboKeydown(e) {
@@ -3448,6 +4119,10 @@
3448
4119
  const compactCombo = document.getElementById('combo-compact-model');
3449
4120
  if (compactCombo) wireCombo(compactCombo);
3450
4121
 
4122
+ // Wire Goal Check model combobox
4123
+ const goalCheckCombo = document.getElementById('combo-goal-check-model');
4124
+ if (goalCheckCombo) wireCombo(goalCheckCombo);
4125
+
3451
4126
  // Trigger initial route
3452
4127
  handleRoute();
3453
4128