@balpal4495/quorum 3.4.0 → 3.6.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/README.md CHANGED
@@ -205,6 +205,79 @@ quorum advisor brief
205
205
 
206
206
  ---
207
207
 
208
+ ## LLM setup
209
+
210
+ LLM-powered commands (`advisor`, `evolve`, `check`, `compass`, `serve`) auto-detect whichever provider is available. No config file is needed — Quorum picks the first working option from the list below.
211
+
212
+ **Recommended: use a CLI tool you're already logged into.** No API key required.
213
+
214
+ ### Option 1 — Claude Code CLI (recommended)
215
+
216
+ If you have [Claude Code](https://claude.ai/code) installed and are logged in, Quorum uses it automatically.
217
+
218
+ ```bash
219
+ # Verify it's working
220
+ echo "say hi" | claude --print
221
+ ```
222
+
223
+ Quorum detects the `Claude Code-credentials` keychain entry (macOS) or a session file in `~/.claude/sessions/`. If the command above works, Quorum will use it.
224
+
225
+ ### Option 2 — GitHub Copilot CLI
226
+
227
+ If you have the [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli) installed and are logged in via VS Code, Quorum uses it automatically.
228
+
229
+ ```bash
230
+ # Verify it's working
231
+ copilot -p "say hi"
232
+ ```
233
+
234
+ Quorum detects `~/.copilot/session-state/` having at least one session (created after first auth). If the command above works, Quorum will use it.
235
+
236
+ ### Option 3 — API keys
237
+
238
+ Set any one of these environment variables:
239
+
240
+ | Variable | Provider |
241
+ |---|---|
242
+ | `ANTHROPIC_API_KEY` | Anthropic Claude |
243
+ | `OPENAI_API_KEY` | OpenAI |
244
+ | `GEMINI_API_KEY` | Google Gemini |
245
+ | `OPENAI_BASE_URL` | Any OpenAI-compatible endpoint (Azure, Groq, etc.) |
246
+
247
+ ```bash
248
+ export ANTHROPIC_API_KEY=sk-ant-...
249
+ ```
250
+
251
+ ### Option 4 — Gemini CLI
252
+
253
+ If you have the [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed and authenticated, Quorum uses it automatically.
254
+
255
+ ```bash
256
+ # Verify it's working
257
+ gemini -p "say hi"
258
+ ```
259
+
260
+ ### Option 5 — Ollama (local, last resort)
261
+
262
+ If Ollama is running at `localhost:11434`, Quorum uses the first available model. Set `OLLAMA_MODEL` to pin a specific model, or `OLLAMA_HOST` for a non-default address.
263
+
264
+ ```bash
265
+ ollama serve
266
+ export OLLAMA_MODEL=llama3.2 # optional
267
+ ```
268
+
269
+ ### Checking what was detected
270
+
271
+ ```bash
272
+ quorum serve # startup line shows: LLM: Claude Code CLI
273
+ ```
274
+
275
+ ### No LLM — still useful
276
+
277
+ Without any provider, `advisor query`, `advisor brief`, `check`, and `coverage` all work with no LLM. Commands output Chronicle evidence and a synthesis request that your agent (Claude Code, Copilot, Codex) can answer inline.
278
+
279
+ ---
280
+
208
281
  ## Upgrading from v1
209
282
 
210
283
  If your project has a `quorum/modules/` folder (the v1 vendored pattern), migrate in one step:
@@ -380,6 +453,43 @@ Writes to `.chronicle/committed/`, updates `SUMMARY.md`, removes the proposal. A
380
453
 
381
454
  ---
382
455
 
456
+ ### `quorum serve` — governance UI + MCP server
457
+
458
+ ```bash
459
+ quorum serve # starts on http://localhost:4242
460
+ quorum serve --port 8080 # custom port
461
+ quorum serve --no-llm # disable LLM auto-detection
462
+ ```
463
+
464
+ Starts a single HTTP server that provides:
465
+
466
+ - **Governance UI** at `/` — browse committed entries, review and approve pending proposals (with inline evidence, Jury breakdown, and Council conditions), track Chronicle health, and run Compass product-direction analysis.
467
+ - **MCP server** at `POST /mcp` — JSON-RPC 2.0 endpoint exposing 10 tools and 6 resources to any MCP-compatible agent.
468
+ - **REST API** — used by the UI and accessible directly:
469
+
470
+ | Route | Description |
471
+ |---|---|
472
+ | `GET /api/entries?q=<query>` | Search committed Chronicle entries |
473
+ | `GET /api/proposals` | List pending proposals |
474
+ | `GET /api/coverage` | Chronicle coverage map |
475
+ | `GET /api/growth` | Chronicle health score |
476
+ | `GET /api/compass?subcommand=map\|brief\|opportunities\|bets` | Compass product direction |
477
+ | `PATCH /api/proposals/:id` | Edit a pending proposal before committing |
478
+
479
+ **MCP resources** (readable by agents via `resources/read`):
480
+
481
+ ```
482
+ chronicle://summary chronicle://proposals
483
+ chronicle://coverage chronicle://growth
484
+ chronicle://compass chronicle://entry/{id}
485
+ ```
486
+
487
+ **MCP tools**: `quorum_query`, `quorum_brief`, `quorum_stage`, `quorum_pending`, `quorum_coverage`, `quorum_growth`, `quorum_help`, `quorum_advisor`\*, `quorum_check`, `quorum_compass`\*
488
+
489
+ \* LLM-powered tools auto-activate when `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY` is set. Without a key they return a `no-llm` status with CLI fallback instructions.
490
+
491
+ ---
492
+
383
493
  ### `quorum growth` — is Chronicle actually learning?
384
494
 
385
495
  ```bash
@@ -565,7 +675,7 @@ Refuted entries always elevate risk by at least one level. Citation validation s
565
675
 
566
676
  ### LLM auto-detection
567
677
 
568
- Quorum finds whichever LLM is available: `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` → `OPENAI_BASE_URL` → Ollama at `localhost:11434` authenticated `gemini` CLI. When running inside an AI agent with no separate key, commands output Chronicle evidence and a synthesis request — the agent answers inline.
678
+ Quorum tries providers in this order: **Claude Code CLI** → **Copilot CLI** → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → `GEMINI_API_KEY` → `OPENAI_BASE_URL` → Gemini CLIOllama. See [LLM setup](#llm-setup) for how to configure each option.
569
679
 
570
680
  ---
571
681
 
@@ -474,7 +474,7 @@ describe("POST /mcp — tools/call", () => {
474
474
  expect(typeof result.content).toBe("string")
475
475
  })
476
476
 
477
- it("quorum_advisor returns todo status", async () => {
477
+ it("quorum_advisor returns no-llm status when no provider is configured", async () => {
478
478
  const { dir } = await makeChronicle(tmpDir)
479
479
  srv = await startServer(tmpDir, dir)
480
480
 
@@ -485,7 +485,7 @@ describe("POST /mcp — tools/call", () => {
485
485
 
486
486
  expect(body.error).toBeUndefined()
487
487
  const result = JSON.parse(body.result.content[0].text)
488
- expect(result.status).toBe("todo")
488
+ expect(result.status).toBe("no-llm")
489
489
  })
490
490
 
491
491
  it("returns -32601 for unknown tool name", async () => {
@@ -555,7 +555,7 @@ describe("unknown route", () => {
555
555
  // ── MCP Resources ─────────────────────────────────────────────────────────────
556
556
 
557
557
  describe("POST /mcp — resources/list", () => {
558
- it("returns all five chronicle:// resources", async () => {
558
+ it("returns all six chronicle:// resources", async () => {
559
559
  const { dir } = await makeChronicle(tmpDir)
560
560
  srv = await startServer(tmpDir, dir)
561
561
 
@@ -570,7 +570,8 @@ describe("POST /mcp — resources/list", () => {
570
570
  expect(uris).toContain("chronicle://coverage")
571
571
  expect(uris).toContain("chronicle://growth")
572
572
  expect(uris).toContain("chronicle://entry/{id}")
573
- expect(uris).toHaveLength(5)
573
+ expect(uris).toContain("chronicle://compass")
574
+ expect(uris).toHaveLength(6)
574
575
  })
575
576
 
576
577
  it("each resource has a name, description, and mimeType", async () => {
@@ -489,37 +489,37 @@ describe("toolHelp", () => {
489
489
  })
490
490
  })
491
491
 
492
- // ── TODO placeholder tools ────────────────────────────────────────────────────
492
+ // ── LLM-powered tools (no-llm fallback when no provider configured) ───────────
493
493
 
494
- describe("toolAdvisor (TODO placeholder)", () => {
494
+ describe("toolAdvisor", () => {
495
495
  it("throws when question is missing", async () => {
496
496
  await expect(toolAdvisor({})).rejects.toThrow("question is required")
497
497
  })
498
498
 
499
- it("returns status=todo with a message", async () => {
499
+ it("returns status=no-llm with a message when no provider is set", async () => {
500
500
  const result = await toolAdvisor({ question: "what was decided about retries?" })
501
- expect(result.status).toBe("todo")
502
- expect(result.message).toMatch(/todo|quorum advisor/i)
501
+ expect(result.status).toBe("no-llm")
502
+ expect(result.message).toMatch(/quorum advisor/i)
503
503
  })
504
504
  })
505
505
 
506
- describe("toolCheck (TODO placeholder)", () => {
506
+ describe("toolCheck", () => {
507
507
  it("throws when neither outcome nor design is provided", async () => {
508
508
  await expect(toolCheck({})).rejects.toThrow("outcome or design is required")
509
509
  })
510
510
 
511
- it("returns status=todo with a message", async () => {
511
+ it("returns preflight and risk when called with outcome and design", async () => {
512
512
  const result = await toolCheck({ outcome: "ship safely", design: "use feature flags" })
513
- expect(result.status).toBe("todo")
514
- expect(result.message).toMatch(/todo|quorum check/i)
513
+ expect(result).toHaveProperty("preflight")
514
+ expect(result).toHaveProperty("risk")
515
515
  })
516
516
  })
517
517
 
518
- describe("toolCompass (TODO placeholder)", () => {
519
- it("returns status=todo with a message", async () => {
518
+ describe("toolCompass", () => {
519
+ it("returns status=no-llm with a message when no provider is set", async () => {
520
520
  const result = await toolCompass({ subcommand: "brief" })
521
- expect(result.status).toBe("todo")
522
- expect(result.message).toMatch(/todo|quorum compass/i)
521
+ expect(result.status).toBe("no-llm")
522
+ expect(result.message).toMatch(/quorum compass/i)
523
523
  })
524
524
  })
525
525
 
@@ -0,0 +1,108 @@
1
+ import { probeAll, detectProvider } from "../shared/llm.js"
2
+ import { c } from "../shared/colors.js"
3
+
4
+ function row(detected, name, note, suffix = "") {
5
+ const icon = detected ? c.green("✓") : c.dim("·")
6
+ const label = detected ? c.bold(name) : c.dim(name)
7
+ const right = detected
8
+ ? (note ? c.dim(` ${note}`) : "") + (suffix ? ` ${suffix}` : "")
9
+ : (note ? ` ${c.dim(note)}` : "")
10
+ return ` ${icon} ${label.padEnd(26)}${right}`
11
+ }
12
+
13
+ export async function run(args) {
14
+ const test = args.includes("--test")
15
+
16
+ console.log("")
17
+ console.log(`${c.bold("quorum llm")} ${c.dim("— LLM provider status")}`)
18
+ console.log("")
19
+
20
+ // Run all probes in parallel with active provider detection
21
+ const [providers, active] = await Promise.all([probeAll(), detectProvider()])
22
+ const activeName = active?.name ?? null
23
+
24
+ console.log(" Provider scan")
25
+ console.log(c.dim(" ─────────────────────────────────────────────────────"))
26
+
27
+ // Normalize names for active matching (gatherCandidates uses "Gemini", probeAll uses "Gemini API")
28
+ const norm = n => n.replace(/ API$/, "").replace(/ \(.*?\)$/, "").toLowerCase()
29
+
30
+ for (const p of providers) {
31
+ const isActive = p.detected && !!activeName && norm(p.name) === norm(activeName)
32
+
33
+ const badges = []
34
+ if (isActive) badges.push(c.green("← active"))
35
+ if (p.detected && p.id === "ollama" && p.note) badges.push(c.dim(`(${p.note})`))
36
+ const suffix = badges.join(" ")
37
+
38
+ console.log(row(p.detected, p.name, null, suffix))
39
+ if (!p.detected && p.note) {
40
+ console.log(` ${c.dim(p.note)}`)
41
+ }
42
+ }
43
+
44
+ console.log("")
45
+
46
+ if (!activeName) {
47
+ console.log(` ${c.yellow("No provider detected.")}`)
48
+ console.log("")
49
+ printSetupGuide()
50
+ return
51
+ }
52
+
53
+ console.log(` Active: ${c.bold(activeName)}`)
54
+ console.log("")
55
+
56
+ if (!test) {
57
+ console.log(c.dim(" Run 'quorum llm --test' to send a live request and verify it works."))
58
+ console.log("")
59
+ return
60
+ }
61
+
62
+ // ── Live test ──────────────────────────────────────────────────────────────
63
+ process.stdout.write(` Testing ${c.bold(activeName)}… `)
64
+ const t0 = Date.now()
65
+
66
+ try {
67
+ const result = await active.llm([
68
+ { role: "user", content: "Respond with exactly the word OK and nothing else." },
69
+ ])
70
+ const ms = Date.now() - t0
71
+ const ok = /\bOK\b/i.test(result?.trim() ?? "")
72
+ if (ok) {
73
+ console.log(`${c.green("✓")} ${c.dim(`(${ms}ms)`)}`)
74
+ } else {
75
+ console.log(`${c.yellow("✓ (unexpected response)")} ${c.dim(`(${ms}ms)`)}`)
76
+ console.log(c.dim(` → ${(result ?? "").slice(0, 120)}`))
77
+ }
78
+ } catch (err) {
79
+ console.log(c.red("✗"))
80
+ console.log(` ${c.red(err.message?.slice(0, 200) ?? String(err))}`)
81
+ console.log("")
82
+ console.log(` ${c.yellow("The detected provider failed.")} Check that you're signed in, then retry.`)
83
+ }
84
+ console.log("")
85
+ }
86
+
87
+ function printSetupGuide() {
88
+ console.log(" Quickest options:")
89
+ console.log("")
90
+ console.log(` ${c.bold("A")} ${c.bold("Claude Code CLI")} ${c.dim("(no API key needed)")}`)
91
+ console.log(c.dim(" Install and sign in: https://claude.ai/code"))
92
+ console.log(c.dim(" Quorum auto-detects it once you're signed in."))
93
+ console.log("")
94
+ console.log(` ${c.bold("B")} ${c.bold("GitHub Copilot CLI")} ${c.dim("(no API key needed)")}`)
95
+ console.log(c.dim(" Install VS Code + GitHub Copilot Chat extension, then sign in."))
96
+ console.log(c.dim(" Quorum auto-detects it once a session exists."))
97
+ console.log("")
98
+ console.log(` ${c.bold("C")} ${c.bold("API key")} ${c.dim("(Anthropic, OpenAI, or Gemini)")}`)
99
+ console.log(c.dim(" export ANTHROPIC_API_KEY=sk-ant-…"))
100
+ console.log(c.dim(" export OPENAI_API_KEY=sk-…"))
101
+ console.log(c.dim(" export GEMINI_API_KEY=…"))
102
+ console.log("")
103
+ console.log(` ${c.bold("D")} ${c.bold("Ollama")} ${c.dim("(local, free)")}`)
104
+ console.log(c.dim(" brew install ollama && ollama serve && ollama pull llama3.2"))
105
+ console.log("")
106
+ console.log(` ${c.dim("After setup, run 'quorum llm' again to confirm detection.")}`)
107
+ console.log("")
108
+ }
@@ -1,6 +1,7 @@
1
1
  import { c } from "../shared/colors.js"
2
2
  import { findChronicleDir } from "../shared/chronicle.js"
3
3
  import { createServer } from "../mcp/server.js"
4
+ import { detectProvider } from "../shared/llm.js"
4
5
 
5
6
  function parseArgs(argv) {
6
7
  const portIdx = argv.indexOf("--port")
@@ -9,11 +10,12 @@ function parseArgs(argv) {
9
10
  : Number(argv.find(a => /^\d{2,5}$/.test(a)) ?? 3000)
10
11
  const hostIdx = argv.indexOf("--host")
11
12
  const host = hostIdx !== -1 ? argv[hostIdx + 1] : "localhost"
12
- return { port: isNaN(port) ? 3000 : port, host }
13
+ const noLlm = argv.includes("--no-llm")
14
+ return { port: isNaN(port) ? 3000 : port, host, noLlm }
13
15
  }
14
16
 
15
17
  export async function run(argv) {
16
- const { port, host } = parseArgs(argv)
18
+ const { port, host, noLlm } = parseArgs(argv)
17
19
 
18
20
  const projectRoot = process.cwd()
19
21
  const chronicleDir = await findChronicleDir(projectRoot)
@@ -23,14 +25,31 @@ export async function run(argv) {
23
25
  process.exit(1)
24
26
  }
25
27
 
26
- const server = await createServer({ projectRoot, chronicleDir })
28
+ // Auto-detect LLM provider unless --no-llm is passed
29
+ let llmProvider = null
30
+ let llmName = null
31
+ if (!noLlm) {
32
+ const detected = await detectProvider()
33
+ if (detected) {
34
+ llmProvider = detected.llm
35
+ llmName = detected.name
36
+ }
37
+ }
38
+
39
+ const server = await createServer({ projectRoot, chronicleDir, llm: llmProvider })
27
40
 
28
41
  server.listen(port, host, () => {
29
42
  const base = `http://${host}:${port}`
30
43
  console.log(`\n${c.bold("Quorum")} ${c.dim(`serving ${projectRoot}`)}\n`)
31
44
  console.log(` ${c.cyan("UI")} ${c.dim(base + "/")}`)
32
45
  console.log(` ${c.cyan("MCP")} ${c.dim(base + "/mcp")}`)
33
- console.log(` ${c.cyan("Chronicle")} ${c.dim(chronicleDir)}\n`)
46
+ console.log(` ${c.cyan("Chronicle")} ${c.dim(chronicleDir)}`)
47
+ if (llmName) {
48
+ console.log(` ${c.cyan("LLM")} ${c.dim(llmName + " (advisor/check/compass enabled)")}`)
49
+ } else {
50
+ console.log(` ${c.cyan("LLM")} ${c.dim("none — LLM tools return CLI fallback hints")}`)
51
+ }
52
+ console.log()
34
53
  console.log(c.bold("Claude Desktop") + c.dim(" — add to claude_desktop_config.json:"))
35
54
  console.log(c.dim(JSON.stringify({
36
55
  mcpServers: { quorum: { type: "streamable-http", url: `${base}/mcp` } }
package/bin/mcp/server.js CHANGED
@@ -27,8 +27,11 @@ import {
27
27
  toolBrief,
28
28
  toolCoverage,
29
29
  toolGrowth,
30
+ toolCompass,
30
31
  commitProposal,
31
32
  deleteProposal,
33
+ updateProposal,
34
+ setLLM,
32
35
  } from "./tools.js"
33
36
 
34
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -124,10 +127,16 @@ async function handleMCP(body, defaultProjectRoot) {
124
127
  description: "Chronicle health score, entry counts, and guidance.",
125
128
  mimeType: "application/json",
126
129
  },
130
+ {
131
+ uri: "chronicle://compass",
132
+ name: "Compass product direction",
133
+ description: "Latest Compass map: behaviours, gaps, and opportunities detected from the codebase.",
134
+ mimeType: "application/json",
135
+ },
127
136
  {
128
137
  uriTemplate: "chronicle://entry/{id}",
129
138
  name: "Chronicle entry",
130
- description: "A single committed Chronicle entry by ID (prefix or full UUID).",
139
+ description: "A single committed Chronicle entry by id or 8-char prefix.",
131
140
  mimeType: "application/json",
132
141
  },
133
142
  ],
@@ -160,6 +169,11 @@ async function handleMCP(body, defaultProjectRoot) {
160
169
  return rpcOk(id, { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] })
161
170
  }
162
171
 
172
+ if (uri === "chronicle://compass") {
173
+ const result = await toolCompass({ subcommand: "map", projectRoot: defaultProjectRoot })
174
+ return rpcOk(id, { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }] })
175
+ }
176
+
163
177
  // chronicle://entry/{id}
164
178
  const entryMatch = uri.match(/^chronicle:\/\/entry\/(.+)$/)
165
179
  if (entryMatch) {
@@ -198,7 +212,9 @@ function setCORS(res) {
198
212
 
199
213
  // ── Server factory ────────────────────────────────────────────────────────────
200
214
 
201
- export async function createServer({ projectRoot, chronicleDir }) {
215
+ export async function createServer({ projectRoot, chronicleDir, llm = null }) {
216
+ // Wire LLM into tools module so advisor/check/compass MCP tools work
217
+ setLLM(llm)
202
218
  let uiHtml
203
219
  try {
204
220
  uiHtml = await fs.readFile(UI_PATH, "utf8")
@@ -284,6 +300,24 @@ export async function createServer({ projectRoot, chronicleDir }) {
284
300
  }
285
301
  }
286
302
 
303
+ // ── REST: edit/patch proposal ───────────────────────────────────────────
304
+ if (proposalMatch && req.method === "PATCH") {
305
+ try {
306
+ const body = await readBody(req)
307
+ const result = await updateProposal(proposalMatch[1], body, chronicleDir)
308
+ return json(res, 200, result)
309
+ } catch (err) {
310
+ return json(res, 400, { error: err.message })
311
+ }
312
+ }
313
+
314
+ // ── REST: compass ───────────────────────────────────────────────────────
315
+ if (pathname === "/api/compass" && req.method === "GET") {
316
+ const subcommand = new URL(req.url, "http://localhost").searchParams.get("subcommand") ?? "map"
317
+ const result = await toolCompass({ subcommand, projectRoot })
318
+ return json(res, 200, result)
319
+ }
320
+
287
321
  // ── Web UI ──────────────────────────────────────────────────────────────
288
322
  if ((pathname === "/" || pathname === "/index.html") && req.method === "GET") {
289
323
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" })
package/bin/mcp/tools.js CHANGED
@@ -263,26 +263,70 @@ export async function toolHelp({ topic } = {}) {
263
263
  return { topic, content: section }
264
264
  }
265
265
 
266
- // ── [TODO] LLM-powered placeholders ──────────────────────────────────────────
267
- // These tools require a live LLM provider wired into quorum serve (--llm flag).
268
- // They are registered so AI clients can discover them, but return a clear
269
- // "not yet available" message rather than silently failing.
266
+ // ── LLM-powered tools ─────────────────────────────────────────────────────────
267
+ // These are activated when quorum serve detects an LLM provider.
268
+ // Without a provider they return a clear CLI-fallback hint.
270
269
 
271
- const TODO_MESSAGE = (name) =>
272
- `${name} requires an LLM provider. This is planned — run 'quorum ${name.replace("quorum_", "")}' from the CLI for now, or watch for a future 'quorum serve --llm' flag.`
270
+ let _llm = null // set by createTools() at server startup
273
271
 
274
- export async function toolAdvisor({ question } = {}) {
272
+ const NO_LLM = (name) => ({
273
+ status: "no-llm",
274
+ message: `${name} requires an LLM provider. No provider was detected at startup. ` +
275
+ `Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY and restart quorum serve. ` +
276
+ `Alternatively run 'quorum ${name.replace("quorum_", "")}' from the CLI.`,
277
+ })
278
+
279
+ export async function toolAdvisor({ question, projectRoot } = {}) {
275
280
  if (!question) throw new Error("question is required")
276
- return { status: "todo", message: TODO_MESSAGE("quorum_advisor") }
281
+ if (!_llm) return NO_LLM("quorum_advisor")
282
+
283
+ const { ask } = await import("../../dist/advisor/index.js")
284
+ const { chronicleDir } = await resolve(projectRoot)
285
+ const { createOracleClient } = await import("../../dist/oracle/index.js")
286
+ const { xenovaEmbed } = await import("../../dist/oracle/adapters/xenova-embedder.js")
287
+ const { createLanceDBStore } = await import("../../dist/oracle/adapters/lance-db.js")
288
+
289
+ const store = await createLanceDBStore(chronicleDir)
290
+ const oracle = createOracleClient({ store, embed: xenovaEmbed })
291
+ const result = await ask({ question, oracle, llm: _llm })
292
+ return result
277
293
  }
278
294
 
279
- export async function toolCheck({ outcome, design } = {}) {
295
+ export async function toolCheck({ outcome, design, projectRoot } = {}) {
296
+ // quorum check is LLM-free — uses the same preflight + risk classifier as the CLI
280
297
  if (!outcome && !design) throw new Error("outcome or design is required")
281
- return { status: "todo", message: TODO_MESSAGE("quorum_check") }
298
+ const { runPreflight, classifyRisk } = await import("../shared/patterns.js")
299
+ const preflight = runPreflight(outcome ?? "", design ?? "")
300
+ const risk = classifyRisk(outcome ?? "", design ?? "")
301
+ return { preflight, risk }
282
302
  }
283
303
 
284
- export async function toolCompass({ subcommand } = {}) {
285
- return { status: "todo", message: TODO_MESSAGE("quorum_compass") }
304
+ export async function toolCompass({ subcommand = "brief", goal, idea, projectRoot } = {}) {
305
+ if (!_llm) return NO_LLM("quorum_compass")
306
+
307
+ const { chronicleDir } = await resolve(projectRoot)
308
+ // Delegate to the compass CLI command handler for now
309
+ const { run: compassRun } = await import("../commands/compass.js")
310
+ // Capture stdout
311
+ const captured = []
312
+ const origWrite = process.stdout.write.bind(process.stdout)
313
+ process.stdout.write = (chunk, ...rest) => { captured.push(String(chunk)); return true }
314
+ try {
315
+ const extraArgs = []
316
+ if (subcommand === "pathways" && goal) extraArgs.push("--goal", goal)
317
+ if (subcommand === "score" && idea) extraArgs.push("--idea", idea)
318
+ await compassRun([subcommand, ...extraArgs])
319
+ } finally {
320
+ process.stdout.write = origWrite
321
+ }
322
+ return { subcommand, output: captured.join("").trim() }
323
+ }
324
+
325
+ /**
326
+ * Call once at server startup to wire the LLM provider into LLM-powered tools.
327
+ */
328
+ export function setLLM(llmProvider) {
329
+ _llm = llmProvider
286
330
  }
287
331
 
288
332
  // ── Proposal commit (human-gate — UI only, never an MCP AI tool) ──────────────
@@ -316,6 +360,27 @@ export async function deleteProposal(proposalId, chronicleDir) {
316
360
  return { deleted: match.replace(".json", "") }
317
361
  }
318
362
 
363
+ export async function updateProposal(proposalId, patch, chronicleDir) {
364
+ const ALLOWED = ["topic", "decision", "key_insight", "status", "confidence",
365
+ "affected_areas", "scope", "alternatives_considered", "rejected_reason"]
366
+ const proposalsDir = path.join(chronicleDir, "proposals")
367
+ const files = await fs.readdir(proposalsDir).catch(() => [])
368
+ const match = files.find(f => f === `${proposalId}.json` || f.startsWith(proposalId))
369
+ if (!match) throw new Error(`Proposal not found: ${proposalId}`)
370
+
371
+ const proposalPath = path.join(proposalsDir, match)
372
+ const raw = await fs.readFile(proposalPath, "utf8")
373
+ const current = JSON.parse(raw)
374
+
375
+ // Only apply allowed fields — never let PATCH overwrite id/proposalId/schema_version
376
+ for (const key of ALLOWED) {
377
+ if (patch[key] !== undefined) current[key] = patch[key]
378
+ }
379
+
380
+ await fs.writeFile(proposalPath, JSON.stringify(current, null, 2), "utf8")
381
+ return { updated: match.replace(".json", ""), topic: current.topic }
382
+ }
383
+
319
384
  // ── MCP tool registry ─────────────────────────────────────────────────────────
320
385
 
321
386
  export const MCP_TOOLS = [
@@ -416,7 +481,7 @@ export const MCP_TOOLS = [
416
481
  // ── [TODO] LLM-powered tools ──
417
482
  {
418
483
  name: "quorum_advisor",
419
- description: "[TODO] Ask a plain-language question answered from Chronicle using an LLM. Requires 'quorum serve --llm'. Use 'quorum advisor' CLI for now.",
484
+ description: "Ask a plain-language question answered from Chronicle using an LLM. Returns a synthesised answer with evidence citations. Auto-activated when quorum serve detects an API key.",
420
485
  inputSchema: {
421
486
  type: "object",
422
487
  properties: {
@@ -428,7 +493,7 @@ export const MCP_TOOLS = [
428
493
  },
429
494
  {
430
495
  name: "quorum_check",
431
- description: "[TODO] Run instant risk triage on a design against Chronicle evidence. Requires 'quorum serve --llm'. Use 'quorum check' CLI for now.",
496
+ description: "Run instant risk triage on a design against Chronicle patterns no LLM required. Returns preflight flags and a risk level (low/medium/high/critical).",
432
497
  inputSchema: {
433
498
  type: "object",
434
499
  properties: {
@@ -440,7 +505,7 @@ export const MCP_TOOLS = [
440
505
  },
441
506
  {
442
507
  name: "quorum_compass",
443
- description: "[TODO] Product-direction synthesis — behaviours, pathways, bets, idea scoring. Requires 'quorum serve --llm'. Use 'quorum compass' CLI for now.",
508
+ description: "Product-direction synthesis — behaviours, pathways, bets, idea scoring. Auto-activated when quorum serve detects an LLM provider. Use subcommand: brief | map | pathways | bets | score | opportunities.",
444
509
  inputSchema: {
445
510
  type: "object",
446
511
  properties: {
package/bin/quorum.js CHANGED
@@ -23,6 +23,8 @@ ${c.bold("Usage:")}
23
23
  ${c.cyan("quorum advisor")} ${c.dim('"question"')} Ask a plain-language question (uses LLM)
24
24
  ${c.cyan("quorum advisor query")} ${c.dim('"topic"')} Search Chronicle entries (no LLM)
25
25
  ${c.cyan("quorum advisor brief")} High-level Chronicle summary (no LLM)
26
+ ${c.cyan("quorum llm")} Show LLM provider status and setup guide
27
+ ${c.cyan("quorum llm --test")} Send a live test request to the active provider
26
28
  ${c.cyan("quorum init")} Scaffold Quorum into a project
27
29
  ${c.cyan("quorum status")} Show Chronicle health and pending proposals
28
30
  ${c.cyan("quorum check")} --outcome <x> --design <y> Preflight + risk (no LLM)
@@ -116,6 +118,12 @@ async function cli() {
116
118
  return
117
119
  }
118
120
 
121
+ if (command === "llm") {
122
+ const { run } = await import(path.join(__dirname, "commands/llm.js"))
123
+ await run(rest)
124
+ return
125
+ }
126
+
119
127
  if (command === "init") {
120
128
  const { run } = await import(path.join(__dirname, "commands/init.js"))
121
129
  await run(PKG_VERSION)