@balpal4495/quorum 3.6.0 → 3.7.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.
@@ -753,7 +753,13 @@ async function loadLastArtifact(chronicleDir) {
753
753
 
754
754
  // ── Main ─────────────────────────────────────────────────────────────────────
755
755
 
756
- export async function run(argv) {
756
+ /**
757
+ * @param {string[]} argv
758
+ * @param {Function|null} [injectedLlm] - Pre-detected LLM provider. When
759
+ * supplied, provider detection is skipped entirely (avoids the ~1.5 s Ollama
760
+ * probe on every MCP/serve request).
761
+ */
762
+ export async function run(argv, injectedLlm) {
757
763
  const [subcommand, ...rest] = argv
758
764
 
759
765
  if (!subcommand || subcommand === "--help" || subcommand === "-h") {
@@ -794,8 +800,8 @@ export async function run(argv) {
794
800
  }
795
801
 
796
802
  const NO_LLM_CMDS = new Set(["map", "opportunities", "behavior", "propose", "outcome"])
797
- const provider = NO_LLM_CMDS.has(subcommand) ? null : await detectProvider()
798
- const llm = provider?.llm
803
+ const llm = injectedLlm
804
+ ?? (NO_LLM_CMDS.has(subcommand) ? null : (await detectProvider())?.llm)
799
805
 
800
806
  // ── Shared context helper ─────────────────────────────────────────────────
801
807
 
package/bin/mcp/tools.js CHANGED
@@ -304,22 +304,26 @@ export async function toolCheck({ outcome, design, projectRoot } = {}) {
304
304
  export async function toolCompass({ subcommand = "brief", goal, idea, projectRoot } = {}) {
305
305
  if (!_llm) return NO_LLM("quorum_compass")
306
306
 
307
- const { chronicleDir } = await resolve(projectRoot)
308
- // Delegate to the compass CLI command handler for now
309
307
  const { run: compassRun } = await import("../commands/compass.js")
310
- // Capture stdout
308
+
309
+ // Capture stdout — always request JSON so there are no ANSI codes
311
310
  const captured = []
312
311
  const origWrite = process.stdout.write.bind(process.stdout)
313
312
  process.stdout.write = (chunk, ...rest) => { captured.push(String(chunk)); return true }
314
313
  try {
315
- const extraArgs = []
314
+ const extraArgs = ["--json"]
316
315
  if (subcommand === "pathways" && goal) extraArgs.push("--goal", goal)
317
316
  if (subcommand === "score" && idea) extraArgs.push("--idea", idea)
318
- await compassRun([subcommand, ...extraArgs])
317
+ // Pass _llm directly to skip the ~1.5 s provider re-detection on every request
318
+ await compassRun([subcommand, ...extraArgs], _llm)
319
319
  } finally {
320
320
  process.stdout.write = origWrite
321
321
  }
322
- return { subcommand, output: captured.join("").trim() }
322
+
323
+ const raw = captured.join("").trim()
324
+ let data = null
325
+ try { data = JSON.parse(raw) } catch { /* fallback to raw string below */ }
326
+ return { subcommand, data, output: data ? null : raw }
323
327
  }
324
328
 
325
329
  /**
package/bin/ui/app.html CHANGED
@@ -408,18 +408,149 @@
408
408
  .hint-stalled { color: var(--red); }
409
409
 
410
410
  /* ── Compass ── */
411
- .compass-output {
411
+ .cp-header {
412
+ display: flex;
413
+ align-items: baseline;
414
+ gap: 10px;
415
+ margin-bottom: 14px;
416
+ }
417
+ .cp-title {
418
+ font-size: 15px;
419
+ font-weight: 700;
420
+ color: var(--text);
421
+ }
422
+ .cp-badge {
423
+ font-size: 11px;
424
+ padding: 2px 8px;
425
+ border-radius: 20px;
426
+ font-weight: 600;
427
+ letter-spacing: .03em;
428
+ }
429
+ .cp-badge--hi { background: rgba(52,201,122,.15); color: var(--green); }
430
+ .cp-badge--mid { background: rgba(224,185,82,.15); color: var(--yellow); }
431
+ .cp-badge--lo { background: rgba(224,82,82,.15); color: var(--red); }
432
+ .cp-direction {
433
+ font-size: 14px;
434
+ font-weight: 500;
435
+ color: var(--text);
436
+ line-height: 1.6;
437
+ margin-bottom: 20px;
438
+ padding: 12px 14px;
439
+ background: rgba(124,110,255,.08);
440
+ border-left: 3px solid var(--accent);
441
+ border-radius: 0 var(--radius) var(--radius) 0;
442
+ }
443
+ .cp-section {
444
+ margin-bottom: 16px;
445
+ }
446
+ .cp-section-label {
447
+ font-size: 11px;
448
+ font-weight: 700;
449
+ text-transform: uppercase;
450
+ letter-spacing: .08em;
451
+ color: var(--muted);
452
+ margin-bottom: 6px;
453
+ }
454
+ .cp-list {
455
+ list-style: none;
456
+ display: flex;
457
+ flex-direction: column;
458
+ gap: 5px;
459
+ }
460
+ .cp-item {
461
+ display: flex;
462
+ gap: 8px;
463
+ font-size: 13px;
464
+ line-height: 1.5;
465
+ }
466
+ .cp-icon {
467
+ flex-shrink: 0;
468
+ width: 16px;
469
+ text-align: center;
470
+ margin-top: 1px;
471
+ }
472
+ .cp-known .cp-icon { color: var(--green); }
473
+ .cp-inferred .cp-icon { color: var(--yellow); }
474
+ .cp-unknown .cp-icon { color: var(--muted); }
475
+ .cp-next-step {
476
+ margin-top: 20px;
477
+ padding: 12px 14px;
412
478
  background: var(--surface);
413
479
  border: 1px solid var(--border);
414
480
  border-radius: var(--radius);
415
- padding: 16px;
416
- font-family: var(--mono);
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 4px;
484
+ }
485
+ .cp-next-label {
486
+ font-size: 11px;
487
+ font-weight: 700;
488
+ text-transform: uppercase;
489
+ letter-spacing: .08em;
490
+ color: var(--muted);
491
+ }
492
+ .cp-next-text {
493
+ font-size: 13px;
494
+ color: var(--text);
495
+ line-height: 1.5;
496
+ }
497
+ /* map / bets / opportunities */
498
+ .cp-card {
499
+ background: var(--surface);
500
+ border: 1px solid var(--border);
501
+ border-radius: var(--radius);
502
+ padding: 14px;
503
+ margin-bottom: 10px;
504
+ }
505
+ .cp-card-header {
506
+ display: flex;
507
+ align-items: center;
508
+ gap: 10px;
509
+ margin-bottom: 6px;
510
+ }
511
+ .cp-card-title {
512
+ font-size: 13px;
513
+ font-weight: 600;
514
+ color: var(--text);
515
+ flex: 1;
516
+ }
517
+ .cp-card-score {
417
518
  font-size: 12px;
418
- white-space: pre-wrap;
519
+ font-weight: 700;
520
+ padding: 2px 8px;
521
+ border-radius: 20px;
522
+ }
523
+ .cp-score-hi { background: rgba(52,201,122,.15); color: var(--green); }
524
+ .cp-score-mid { background: rgba(224,185,82,.15); color: var(--yellow); }
525
+ .cp-score-lo { background: rgba(110,110,126,.15); color: var(--muted); }
526
+ .cp-card-body {
527
+ font-size: 13px;
419
528
  color: var(--muted);
420
- max-height: 500px;
421
- overflow-y: auto;
422
- line-height: 1.6;
529
+ line-height: 1.5;
530
+ }
531
+ .cp-card-meta {
532
+ font-size: 12px;
533
+ color: var(--muted);
534
+ margin-top: 6px;
535
+ }
536
+ .cp-card-meta span { margin-right: 12px; }
537
+ .cp-area-tag {
538
+ display: inline-block;
539
+ font-size: 11px;
540
+ padding: 1px 6px;
541
+ border-radius: 4px;
542
+ background: rgba(82,168,224,.12);
543
+ color: var(--blue);
544
+ margin-right: 4px;
545
+ }
546
+ .cp-gap-item {
547
+ font-size: 13px;
548
+ color: var(--yellow);
549
+ padding: 8px 12px;
550
+ border-left: 2px solid var(--yellow);
551
+ margin-bottom: 6px;
552
+ background: rgba(224,185,82,.05);
553
+ border-radius: 0 var(--radius) var(--radius) 0;
423
554
  }
424
555
 
425
556
  /* ── Edit modal ── */
@@ -1072,17 +1203,117 @@ async function loadCompass() {
1072
1203
  }
1073
1204
  }
1074
1205
 
1206
+ function confidenceBadge(conf) {
1207
+ const pct = Math.round((conf ?? 0) * 100)
1208
+ const cls = pct >= 70 ? 'cp-badge--hi' : pct >= 45 ? 'cp-badge--mid' : 'cp-badge--lo'
1209
+ return `<span class="cp-badge ${cls}">${pct}% confidence</span>`
1210
+ }
1211
+
1212
+ function cpList(items, cssClass, icon) {
1213
+ if (!items?.length) return ''
1214
+ return `<ul class="cp-list">${items.map(t => `<li class="cp-item ${cssClass}"><span class="cp-icon">${icon}</span><span>${esc(t)}</span></li>`).join('')}</ul>`
1215
+ }
1216
+
1217
+ function scoreClass(total) {
1218
+ return total >= 75 ? 'cp-score-hi' : total >= 55 ? 'cp-score-mid' : 'cp-score-lo'
1219
+ }
1220
+
1221
+ function renderBrief(d) {
1222
+ let html = `<div class="cp-header"><span class="cp-title">Direction Brief</span>${confidenceBadge(d.confidence)}</div>`
1223
+ if (d.product_direction) html += `<div class="cp-direction">${esc(d.product_direction)}</div>`
1224
+
1225
+ if (d.known_from_chronicle?.length) {
1226
+ html += `<div class="cp-section"><div class="cp-section-label">From Chronicle</div>${cpList(d.known_from_chronicle, 'cp-known', '✓')}</div>`
1227
+ }
1228
+ if (d.known_from_behavior?.length) {
1229
+ html += `<div class="cp-section"><div class="cp-section-label">From code / docs</div>${cpList(d.known_from_behavior, 'cp-known', '✓')}</div>`
1230
+ }
1231
+ if (d.inferred?.length) {
1232
+ html += `<div class="cp-section"><div class="cp-section-label">Inferred</div>${cpList(d.inferred, 'cp-inferred', '~')}</div>`
1233
+ }
1234
+ if (d.unknowns?.length) {
1235
+ html += `<div class="cp-section"><div class="cp-section-label">Unknowns</div>${cpList(d.unknowns, 'cp-unknown', '?')}</div>`
1236
+ }
1237
+ if (d.assumptions?.length) {
1238
+ html += `<div class="cp-section"><div class="cp-section-label">Assumptions</div>${cpList(d.assumptions, 'cp-unknown', '·')}</div>`
1239
+ }
1240
+ if (d.recommended_next_step) {
1241
+ html += `<div class="cp-next-step"><span class="cp-next-label">Next step</span><span class="cp-next-text">${esc(d.recommended_next_step)}</span></div>`
1242
+ }
1243
+ return html
1244
+ }
1245
+
1246
+ function renderMap(d) {
1247
+ let html = `<div class="cp-header"><span class="cp-title">Behaviour Map</span>${confidenceBadge(d.confidence)}</div>`
1248
+ if (d.behaviors?.length) {
1249
+ html += `<div class="cp-section"><div class="cp-section-label">Behaviours (${d.behaviors.length})</div>`
1250
+ for (const b of d.behaviors.slice(0, 30)) {
1251
+ html += `<div class="cp-card" style="padding:10px 14px">`
1252
+ html += `<div class="cp-card-header"><span class="cp-card-title">${esc(b.name)}</span><span class="cp-area-tag">${esc(b.area)}</span></div>`
1253
+ html += `<div class="cp-card-body">${esc(b.current_behavior)}</div></div>`
1254
+ }
1255
+ html += `</div>`
1256
+ }
1257
+ if (d.gaps?.length) {
1258
+ html += `<div class="cp-section"><div class="cp-section-label">Gaps (${d.gaps.length})</div>`
1259
+ for (const g of d.gaps) html += `<div class="cp-gap-item">${esc(g.gap)}</div>`
1260
+ html += `</div>`
1261
+ }
1262
+ if (!d.behaviors?.length && !d.gaps?.length) html += `<div class="empty">No behaviours found in this project.</div>`
1263
+ return html
1264
+ }
1265
+
1266
+ function renderBets(items) {
1267
+ if (!items?.length) return `<div class="empty">No bets generated.</div>`
1268
+ let html = `<div class="cp-header"><span class="cp-title">Strategic Bets</span></div>`
1269
+ for (const b of items) {
1270
+ const total = b.scores?.total ?? 0
1271
+ html += `<div class="cp-card">`
1272
+ html += `<div class="cp-card-header"><span class="cp-card-title">${esc(b.title)}</span><span class="cp-card-score ${scoreClass(total)}">${total}</span></div>`
1273
+ if (b.thesis) html += `<div class="cp-card-body">${esc(b.thesis)}</div>`
1274
+ if (b.first_experiment) html += `<div class="cp-card-meta"><span>First test: ${esc(b.first_experiment)}</span></div>`
1275
+ if (b.kill_criteria?.[0]) html += `<div class="cp-card-meta" style="color:var(--red)">Kill if: ${esc(b.kill_criteria[0])}</div>`
1276
+ if (b.assumptions?.length) html += `<div class="cp-card-meta">Assumes: ${esc(b.assumptions[0])}</div>`
1277
+ html += `</div>`
1278
+ }
1279
+ return html
1280
+ }
1281
+
1282
+ function renderOpportunities(items) {
1283
+ if (!items?.length) return `<div class="empty">No gaps or opportunities found.</div>`
1284
+ let html = `<div class="cp-header"><span class="cp-title">Opportunities (${items.length})</span></div>`
1285
+ for (const o of items) {
1286
+ html += `<div class="cp-card">`
1287
+ html += `<div class="cp-card-header"><span class="cp-card-title">${esc(o.title ?? o.gap)}</span><span class="cp-area-tag">${esc(o.area)}</span></div>`
1288
+ if (o.why_it_matters) html += `<div class="cp-card-body">${esc(o.why_it_matters)}</div>`
1289
+ if (o.suggested_next_step) html += `<div class="cp-card-meta">Next: ${esc(o.suggested_next_step)}</div>`
1290
+ html += `</div>`
1291
+ }
1292
+ return html
1293
+ }
1294
+
1075
1295
  function renderCompass(data, subcommand) {
1076
1296
  const el = document.getElementById("compassView")
1077
1297
  if (data.status === "no-llm") {
1078
1298
  el.innerHTML = `<div class="empty">${esc(data.message)}<small>Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and restart quorum serve.</small></div>`
1079
1299
  return
1080
1300
  }
1081
- const output = data.output ?? JSON.stringify(data, null, 2)
1082
- el.innerHTML = `
1083
- <div style="font-size:12px;color:var(--muted);margin-bottom:10px">compass ${esc(subcommand)}</div>
1084
- <div class="compass-output">${esc(output)}</div>
1085
- `
1301
+
1302
+ const d = data.data // structured JSON from compass --json
1303
+ if (!d) {
1304
+ // fallback: raw text (shouldn't happen, but safe)
1305
+ el.innerHTML = `<pre style="font-size:12px;line-height:1.6;white-space:pre-wrap;color:var(--muted)">${esc(data.output ?? JSON.stringify(data, null, 2))}</pre>`
1306
+ return
1307
+ }
1308
+
1309
+ let inner = ''
1310
+ if (subcommand === 'brief') inner = renderBrief(d)
1311
+ else if (subcommand === 'map') inner = renderMap(d)
1312
+ else if (subcommand === 'bets') inner = renderBets(Array.isArray(d) ? d : d.bets ?? [d])
1313
+ else if (subcommand === 'opportunities') inner = renderOpportunities(Array.isArray(d) ? d : [])
1314
+ else inner = `<pre style="font-size:12px;line-height:1.6;white-space:pre-wrap;color:var(--muted)">${esc(JSON.stringify(d, null, 2))}</pre>`
1315
+
1316
+ el.innerHTML = `<div style="padding-bottom:16px">${inner}</div>`
1086
1317
  }
1087
1318
  </script>
1088
1319
  </body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Git-backed memory and design review for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",