@balpal4495/quorum 0.4.2 → 2.0.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/SETUP.md CHANGED
@@ -12,9 +12,16 @@ If you have Node.js 18+ available, run this from the project root:
12
12
  npx @balpal4495/quorum@latest init
13
13
  ```
14
14
 
15
- This handles all steps below automatically — copying modules, merging instruction files, updating `package.json`, `.gitignore`, and creating the Chronicle directory.
15
+ This handles all steps below automatically — writing Quorum docs, merging instruction files,
16
+ updating `package.json`, `.gitignore`, and creating the Chronicle directory.
16
17
 
17
- After it completes, run `npm install` and wire `setup()` into your entry point (see Step 6 below).
18
+ After it completes, run `npm install`. The CLI is immediately usable:
19
+
20
+ ```bash
21
+ quorum advisor brief
22
+ quorum advisor "what has the team decided about X?"
23
+ quorum check --outcome '...' --design '...'
24
+ ```
18
25
 
19
26
  ---
20
27
 
@@ -29,9 +36,9 @@ Do not skip steps. Do not ask for confirmation between steps — complete the fu
29
36
 
30
37
  Read these files in full before proceeding:
31
38
 
32
- - `quorum/modules/README.md` — module overview and quick-start
33
- - `quorum/modules/AGENTS.md` — file ownership and invariants
34
- - `.github/copilot-instructions.md` — workflow rules for AI agents (installed at project root by init)
39
+ - `quorum/AGENTS.md` — module file ownership and invariants
40
+ - `quorum/CLAUDE.md` — design decisions and constraints
41
+ - `.github/copilot-instructions.md` — workflow rules for AI agents
35
42
 
36
43
  These are your operating instructions for everything that follows.
37
44
 
@@ -52,14 +59,12 @@ Does .chronicle/ exist? yes / no
52
59
 
53
60
  ---
54
61
 
55
- ## Step 3 — Install dependencies
62
+ ## Step 3 — Install Quorum
56
63
 
57
- Open `package.json` at the project root. Add the following to `dependencies` if they are not already present:
64
+ Open `package.json` at the project root. Add to `devDependencies` if not already present:
58
65
 
59
66
  ```json
60
- "zod": "^3.23.0",
61
- "vectordb": "^0.4.0",
62
- "@xenova/transformers": "^2.17.0"
67
+ "@balpal4495/quorum": "^2.0.0"
63
68
  ```
64
69
 
65
70
  Then run:
@@ -70,77 +75,57 @@ npm install
70
75
 
71
76
  If the project uses `yarn` or `pnpm`, use the appropriate installer instead.
72
77
 
73
- > `zod` is required for all structured LLM output validation.
74
- > `vectordb` is the LanceDB adapter (swappable — see `quorum/modules/oracle/adapters/`).
75
- > `@xenova/transformers` is the local ONNX embedder (swappable — see `quorum/modules/oracle/adapters/xenova-embedder.ts`).
76
-
77
78
  ---
78
79
 
79
80
  ## Step 4 — Merge AI instruction files
80
81
 
81
82
  ### 4a. `.github/copilot-instructions.md`
82
83
 
83
- The automated init command (`npx @balpal4495/quorum@latest init`) handles this step automatically — it creates or appends to `.github/copilot-instructions.md` at the project root.
84
+ The automated init command handles this step automatically.
84
85
 
85
- **If you are completing this step manually:**
86
-
87
- Check whether `.github/copilot-instructions.md` already exists.
88
-
89
- **If it does not exist:** Fetch the Quorum copilot instructions from the Quorum GitHub repo (`balpal4495/Quorum`) at `.github/copilot-instructions.md` and write it to `.github/copilot-instructions.md` in the project root.
90
-
91
- **If it already exists and does not contain `<!-- quorum -->`:** Append the Quorum instructions to the existing file, preceded by:
86
+ **If completing manually:** fetch `.github/copilot-instructions.md` from the Quorum GitHub repo (`balpal4495/Quorum`) and write it to `.github/copilot-instructions.md` in the project root. If the file already exists and does not contain `<!-- quorum:start -->`, append:
92
87
 
93
88
  ```markdown
94
89
  ---
95
90
 
96
- <!-- quorum -->
91
+ <!-- quorum:start -->
92
+ <content from Quorum repo>
93
+ <!-- quorum:end -->
97
94
  ```
98
95
 
99
- Do not replace or overwrite existing content.
100
-
101
96
  ### 4b. `AGENTS.md`
102
97
 
103
98
  **If it does not exist:**
104
- Create `AGENTS.md` at the project root with this content:
105
99
 
106
100
  ```markdown
107
101
  # Agent Instructions
108
102
 
109
- See [quorum/modules/AGENTS.md](quorum/modules/AGENTS.md) for Quorum module internals.
103
+ <!-- quorum:start -->
104
+ ## Quorum
105
+
106
+ See [quorum/AGENTS.md](quorum/AGENTS.md) for module file ownership and internals.
110
107
  See [.github/copilot-instructions.md](.github/copilot-instructions.md) for workflow rules.
108
+ <!-- quorum:end -->
111
109
  ```
112
110
 
113
- **If it already exists:**
114
- Append to it:
115
-
116
- ```markdown
117
-
118
- ## Quorum modules
119
-
120
- See [quorum/modules/AGENTS.md](quorum/modules/AGENTS.md) for Oracle, Jury, and Council internals.
121
- ```
111
+ **If it already exists:** append the `<!-- quorum:start --> ... <!-- quorum:end -->` block above.
122
112
 
123
113
  ### 4c. `CLAUDE.md`
124
114
 
125
115
  **If it does not exist:**
126
- Create `CLAUDE.md` at the project root with this content:
127
116
 
128
117
  ```markdown
129
118
  # Claude Instructions
130
119
 
131
- See [quorum/modules/CLAUDE.md](quorum/modules/CLAUDE.md) for Quorum module internals.
120
+ <!-- quorum:start -->
121
+ ## Quorum
122
+
123
+ See [quorum/CLAUDE.md](quorum/CLAUDE.md) for design decisions and invariants.
132
124
  See [.github/copilot-instructions.md](.github/copilot-instructions.md) for workflow rules.
125
+ <!-- quorum:end -->
133
126
  ```
134
127
 
135
- **If it already exists:**
136
- Append to it:
137
-
138
- ```markdown
139
-
140
- ## Quorum modules
141
-
142
- See [quorum/modules/CLAUDE.md](quorum/modules/CLAUDE.md) for Oracle, Jury, and Council internals.
143
- ```
128
+ **If it already exists:** append the `<!-- quorum:start --> ... <!-- quorum:end -->` block above.
144
129
 
145
130
  ---
146
131
 
@@ -148,39 +133,37 @@ See [quorum/modules/CLAUDE.md](quorum/modules/CLAUDE.md) for Oracle, Jury, and C
148
133
 
149
134
  **If `.gitignore` does not exist**, create it.
150
135
 
151
- Add the following block if it is not already present:
136
+ Add the following block if not already present:
152
137
 
153
138
  ```gitignore
154
139
  # Quorum — Chronicle
155
140
  # entries/ is a LanceDB binary vector store — do not commit
156
141
  .chronicle/entries/
157
-
158
- # proposals/ contains pending human-approval writes — commit these
159
- # (remove the line above if you want to ignore the whole store)
142
+ .chronicle/query-log.jsonl
160
143
  ```
161
144
 
162
145
  ---
163
146
 
164
- ## Step 6 — Wire setup() into the project
147
+ ## Step 6 — Wire setup() into the project (programmatic use only)
165
148
 
166
- Find the application entry point (e.g. `index.ts`, `server.ts`, `app.ts`, or equivalent).
149
+ Skip this step if you are using only the CLI (`quorum advisor`, `quorum check`, etc.).
167
150
 
168
- Add the following import and call at startup, **before** any agent or workflow code runs:
151
+ For programmatic use, find the application entry point (e.g. `index.ts`, `server.ts`, `app.ts`).
169
152
 
170
153
  ```typescript
171
- import { setup } from "./quorum/modules/setup"
154
+ import { setup } from "@balpal4495/quorum"
172
155
 
173
- const { oracle, evaluate, deliberate } = await setup({
156
+ const { oracle, evaluate, deliberate, ask } = await setup({
174
157
  llm: yourLLMProvider, // replace with your project's LLM provider function
175
158
  })
176
159
  ```
177
160
 
178
161
  `setup()` creates `.chronicle/` directories, warms the embedder, and wires all module dependencies.
179
- It must be called once before any `oracle.query()`, `evaluate()`, or `deliberate()` call.
162
+ Must be called once before any `oracle.query()`, `evaluate()`, `deliberate()`, or `ask()` call.
180
163
 
181
- If no entry point exists yet, note that `setup()` must be called before first use do not inline it.
164
+ `ask(question)` is the plain-language interface — it queries Oracle automatically, synthesises Chronicle evidence into a concise answer, and retries internally until the answer meets a confidence threshold.
182
165
 
183
- **Approving Chronicle proposals:** after an agent calls `oracle.propose()`, approve and index the entry from the terminal:
166
+ **Approving Chronicle proposals:**
184
167
 
185
168
  ```bash
186
169
  quorum commit --list # see pending proposals
@@ -188,39 +171,33 @@ quorum commit <id> # approve and index a proposal
188
171
  quorum commit <id> --dry-run # preview without writing
189
172
  ```
190
173
 
191
- Requires `@xenova/transformers` and `vectordb` (both added in Step 3).
192
-
193
174
  ---
194
175
 
195
176
  ## Step 7 — Verify Chronicle is created
196
177
 
197
- Run the project (or call `setup()` in isolation). Confirm that `.chronicle/proposals/` exists after startup.
178
+ Confirm `.chronicle/proposals/` and `.chronicle/committed/` exist:
198
179
 
199
180
  ```bash
200
181
  ls .chronicle/
201
- # expected: proposals/
202
- # entries/ will appear after the first oracle.commit()
182
+ # expected: committed/ proposals/
203
183
  ```
204
184
 
205
- If the directory is not created, re-check that `setup()` is being awaited correctly.
206
-
207
185
  ---
208
186
 
209
- ## Step 8 — Run module tests
210
-
211
- Confirm the modules are working in this environment:
187
+ ## Step 8 — Verify the CLI works
212
188
 
213
189
  ```bash
214
- # Module unit tests
215
- npx vitest run quorum/modules/
216
-
217
- # Eval suite — deterministic assertions, no LLM required
218
- npx vitest run quorum/evals/
190
+ quorum advisor brief
191
+ quorum growth
219
192
  ```
220
193
 
221
- All tests should pass. If they fail due to missing dependencies, re-run Step 3.
194
+ Both commands run without any LLM. If they fail, check that `npm install` completed successfully.
195
+
196
+ To run Quorum's eval suite (optional — tests Quorum's own correctness):
222
197
 
223
- The eval suite runs canonical test cases (known-bad proposals that should block, known-good ones that should pass) through the deterministic preflight and risk classifier. These pass without any LLM. If you later want to test Jury confidence and Council recommendations against a real LLM, set `EVAL_LLM=1` when running.
198
+ ```bash
199
+ npx vitest run node_modules/@balpal4495/quorum/evals/
200
+ ```
224
201
 
225
202
  ---
226
203
 
@@ -229,10 +206,9 @@ The eval suite runs canonical test cases (known-bad proposals that should block,
229
206
  Once all steps are complete, report:
230
207
 
231
208
  1. Which files were created vs appended
232
- 2. Which dependencies were added (if any were already present, note that)
233
- 3. Whether tests passed
234
- 4. The path to `setup()` in the entry point, and the LLM provider that was wired (or a note if it was left as a placeholder)
235
- 5. Any step that could not be completed and why
209
+ 2. Whether `npm install` succeeded
210
+ 3. The path to `setup()` in the entry point if wired (or note if CLI-only)
211
+ 4. Any step that could not be completed and why
236
212
 
237
213
  ---
238
214
 
@@ -240,10 +216,7 @@ Once all steps are complete, report:
240
216
 
241
217
  Skip this step if you do not have Google Gemini CLI installed. Quorum is fully functional without it.
242
218
 
243
- If you do have it (or want to add it later), this enables Claude Code to delegate large-context
244
- analysis to Gemini — useful when a task requires surveying the whole codebase at once.
245
-
246
- **10a. Install Gemini CLI** (if not already installed — requires Node.js 18+):
219
+ **10a. Install Gemini CLI** (if not already installed):
247
220
 
248
221
  ```bash
249
222
  npm install -g @google/gemini-cli
@@ -256,35 +229,36 @@ export GEMINI_API_KEY="your-key-here"
256
229
  export GEMINI_CLI_TRUST_WORKSPACE=true
257
230
  ```
258
231
 
259
- **10c. Create `GEMINI.md`** at the project root so Gemini understands the codebase.
260
- Copy `quorum/modules/AGENTS.md` content as a starting point, or write a brief description of
261
- the project and the Quorum architecture. The `GEMINI.md` in the Quorum repo itself is a
262
- working example.
232
+ **10c. Create `GEMINI.md`** at the project root. Use `quorum/AGENTS.md` content as a starting point, or write a brief description of the project and the Quorum architecture.
263
233
 
264
- Once the key is set and `gemini -p "hello"` responds, Claude Code will automatically detect
265
- Gemini and use it for large-context tasks.
234
+ Once the key is set and `gemini -p "hello"` responds, Claude Code will automatically detect Gemini and use it for large-context tasks.
266
235
 
267
236
  ---
268
237
 
269
238
  ## After setup
270
239
 
271
- You are now operating under Quorum. The rules in `quorum/modules/AGENTS.md` and `.github/copilot-instructions.md` apply to all subsequent work.
240
+ You are now operating under Quorum. The rules in `quorum/AGENTS.md` and `.github/copilot-instructions.md` apply to all subsequent work.
272
241
 
273
242
  Key reminders:
274
- - **Query Oracle before proposing anything.** `oracle.query("what you're about to do")` first.
243
+ - **Ask Advisor for context.** `quorum advisor "what has the team decided about X?"` before starting any meaningful work.
275
244
  - **Never call `oracle.commit()` autonomously.** Only `oracle.propose()`. A human commits.
276
245
  - **Chronicle entries are ground truth.** Respect `refuted` entries — do not retry what has already failed.
277
246
 
278
247
  ### CLI quick reference
279
248
 
280
- These commands are available globally after `npm install -g @balpal4495/quorum`:
281
-
282
249
  | Command | What it does |
283
250
  |---|---|
251
+ | `quorum advisor "question"` | Ask a plain-language question — answer synthesised from Chronicle (needs LLM) |
252
+ | `quorum advisor query "topic"` | Search Chronicle entries by keyword (no LLM) |
253
+ | `quorum advisor brief` | High-level Chronicle summary (no LLM) |
284
254
  | `quorum status` | Chronicle health — pending proposals, committed entries |
285
255
  | `quorum check --outcome X --design Y` | Preflight + risk classifier (no LLM) |
286
256
  | `quorum commit --list` | List pending proposals |
287
257
  | `quorum commit <id>` | Approve and index a proposal |
288
258
  | `quorum sentinel coverage [--path <dir>]` | Chronicle coverage of source files |
259
+ | `quorum growth` | Chronicle learning health — growth rate, last commit, pending proposals |
260
+ | `quorum evolve` | Consolidate Chronicle — merges duplicates, resolves contradictions, promotes open entries |
289
261
 
290
262
  `quorum check` exit codes: `0` = low/medium risk · `1` = high · `2` = critical
263
+
264
+ `quorum advisor ask` and `quorum evolve` auto-detect any available LLM: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `OPENAI_BASE_URL`, Ollama at localhost:11434, or an authenticated `gemini` CLI. When running inside an AI agent with no separate key, they output Chronicle evidence and a synthesis request for the agent to answer inline.
@@ -0,0 +1,301 @@
1
+ import { c } from "../shared/colors.js"
2
+ import { findChronicleDir, readCommitted } from "../shared/chronicle.js"
3
+ import { detectProvider } from "../shared/llm.js"
4
+
5
+ const SATISFACTION_THRESHOLD = 0.7
6
+ const MAX_RETRIES = 2
7
+
8
+ // ── Evidence helpers ──────────────────────────────────────────────────────────
9
+
10
+ function tokenize(text) {
11
+ return text.toLowerCase().split(/\W+/).filter(t => t.length > 2)
12
+ }
13
+
14
+ function scoreEntry(query, entry) {
15
+ const qTokens = new Set(tokenize(query))
16
+ const text = [
17
+ entry.key_insight ?? "",
18
+ entry.decision ?? "",
19
+ ...(entry.affected_areas ?? []),
20
+ ...(entry.scope ?? []),
21
+ ].join(" ")
22
+ const eTokens = tokenize(text)
23
+ const overlap = eTokens.filter(t => qTokens.has(t)).length
24
+ return overlap / Math.sqrt(qTokens.size * eTokens.length + 1)
25
+ }
26
+
27
+ function entryText(entry) {
28
+ return (entry.decision ?? entry.key_insight ?? "").trim()
29
+ }
30
+
31
+ function findRelevant(entries, query, limit = 6) {
32
+ return entries
33
+ .map(e => ({ entry: e, score: scoreEntry(query, e) }))
34
+ .filter(({ score }) => score > 0)
35
+ .sort((a, b) => b.score - a.score)
36
+ .slice(0, limit)
37
+ .map(({ entry }) => entry)
38
+ }
39
+
40
+ function formatEvidenceForLLM(entries) {
41
+ if (entries.length === 0) return "Chronicle has no prior entries on this topic."
42
+ return entries.map(e => {
43
+ const statusTag =
44
+ e.status === "refuted" ? " [REJECTED]" :
45
+ e.status === "validated" ? " [VALIDATED]" : ""
46
+ return `[${(e.id ?? "").slice(0, 8)}]${statusTag} ${entryText(e)}\n Areas: ${(e.affected_areas ?? []).join(", ")}`
47
+ }).join("\n\n")
48
+ }
49
+
50
+ // ── LLM + validation loop ─────────────────────────────────────────────────────
51
+
52
+ const SYSTEM_PROMPT = `You are the Quorum Advisor — the plain-language interface to a team's collective knowledge.
53
+
54
+ You receive a question from a developer or engineering manager, along with relevant Chronicle evidence.
55
+ Synthesise that evidence into a clear, concise answer a human can act on.
56
+
57
+ Rules:
58
+ - Write for a human who does not know what "Chronicle entries" or "vector search" mean.
59
+ - Be direct. One clear recommendation.
60
+ - If Chronicle has relevant evidence, reference it plainly: "the team already decided X".
61
+ - If Chronicle has no evidence, say so honestly — do not invent history.
62
+ - Blockers are hard blockers only — things that MUST be resolved before moving forward.
63
+
64
+ Return ONLY valid JSON matching this schema (no markdown fences, no explanation):
65
+ {
66
+ "confidence": <number 0–1>,
67
+ "what_we_know": <string — what Chronicle knows, plain English, 1–3 sentences>,
68
+ "risks": [<string>],
69
+ "blockers": [<string — hard blockers only, empty array if none>],
70
+ "recommendation": <string — one clear action>,
71
+ "next_step": <string — specific next step or quorum command>
72
+ }`
73
+
74
+ async function callLLM(llm, question, evidence, attempt, previous) {
75
+ let userPrompt = `## Question\n${question}\n\n## Chronicle Evidence\n${formatEvidenceForLLM(evidence)}`
76
+
77
+ if (attempt > 0 && previous) {
78
+ const lines = [
79
+ "",
80
+ `## Previous Answer (attempt ${attempt} — quality threshold not met)`,
81
+ `Confidence: ${previous.confidence.toFixed(2)} (need ≥ ${SATISFACTION_THRESHOLD})`,
82
+ ]
83
+ if (previous.blockers?.length > 0) {
84
+ lines.push(`Unresolved blockers: ${previous.blockers.join("; ")}`)
85
+ }
86
+ lines.push("Please produce a more specific and concrete answer.")
87
+ userPrompt += lines.join("\n")
88
+ }
89
+
90
+ const raw = await llm([
91
+ { role: "system", content: SYSTEM_PROMPT },
92
+ { role: "user", content: userPrompt },
93
+ ])
94
+
95
+ let parsed
96
+ try {
97
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim()
98
+ parsed = JSON.parse(cleaned)
99
+ } catch {
100
+ throw new Error(`LLM returned non-JSON. Raw: ${raw.slice(0, 200)}`)
101
+ }
102
+
103
+ if (typeof parsed.confidence !== "number" || !parsed.what_we_know || !parsed.recommendation) {
104
+ throw new Error("LLM response missing required fields (confidence, what_we_know, recommendation)")
105
+ }
106
+
107
+ return parsed
108
+ }
109
+
110
+ async function runAdvisor(llm, question, evidence) {
111
+ let last = null
112
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
113
+ const answer = await callLLM(llm, question, evidence, attempt, last)
114
+ last = answer
115
+ const satisfied = answer.confidence >= SATISFACTION_THRESHOLD && (answer.blockers?.length ?? 0) === 0
116
+ if (satisfied || attempt === MAX_RETRIES) return { ...answer, retries: attempt }
117
+ }
118
+ return { ...last, retries: MAX_RETRIES }
119
+ }
120
+
121
+ // ── Output renderers ──────────────────────────────────────────────────────────
122
+
123
+ function renderAsk(question, result) {
124
+ console.log(`\n${c.bold("Advisor")}\n`)
125
+ console.log(` ${c.dim("Question:")} ${question}\n`)
126
+
127
+ console.log(`${c.bold("What we know")}`)
128
+ console.log(` ${result.what_we_know}\n`)
129
+
130
+ if (result.blockers?.length > 0) {
131
+ console.log(`${c.bold(c.red("Blockers"))}`)
132
+ for (const b of result.blockers) console.log(` ${c.red("✗")} ${b}`)
133
+ console.log("")
134
+ }
135
+
136
+ if (result.risks?.length > 0) {
137
+ console.log(`${c.bold("Risks")}`)
138
+ for (const r of result.risks) console.log(` ${c.yellow("⚠")} ${r}`)
139
+ console.log("")
140
+ }
141
+
142
+ console.log(`${c.bold("Recommendation")}`)
143
+ console.log(` ${result.recommendation}\n`)
144
+
145
+ console.log(`${c.bold("Next step")}`)
146
+ console.log(` ${c.cyan(result.next_step)}\n`)
147
+
148
+ if (result.retries > 0) {
149
+ console.log(c.dim(` (Refined over ${result.retries + 1} attempts)\n`))
150
+ }
151
+ }
152
+
153
+ function renderQuery(topic, entries) {
154
+ console.log(`\n${c.bold("Chronicle")} ${c.dim(`query: "${topic}"`)}\n`)
155
+
156
+ if (entries.length === 0) {
157
+ console.log(` ${c.dim("No matching entries found.")}\n`)
158
+ return
159
+ }
160
+
161
+ for (const e of entries) {
162
+ const statusColor =
163
+ e.status === "validated" ? c.green :
164
+ e.status === "refuted" ? c.red : c.dim
165
+ console.log(` ${c.cyan((e.id ?? "").slice(0, 8))} ${statusColor(`[${e.status}]`)} ${entryText(e)}`)
166
+ if (e.affected_areas?.length) console.log(` ${c.dim(e.affected_areas.join(", "))}`)
167
+ console.log("")
168
+ }
169
+ }
170
+
171
+ function renderBrief(allEntries) {
172
+ const validated = allEntries.filter(e => e.status === "validated")
173
+ const refuted = allEntries.filter(e => e.status === "refuted")
174
+ const open = allEntries.filter(e => e.status === "open")
175
+ const recent = allEntries.slice(0, 5)
176
+
177
+ console.log(`\n${c.bold("Chronicle Brief")}\n`)
178
+ console.log(` ${c.green(validated.length)} validated ${c.red(refuted.length)} refuted ${c.dim(open.length + " open")}\n`)
179
+
180
+ if (recent.length > 0) {
181
+ console.log(`${c.bold("Recent entries")}`)
182
+ for (const e of recent) {
183
+ const statusColor =
184
+ e.status === "validated" ? c.green :
185
+ e.status === "refuted" ? c.red : c.dim
186
+ console.log(` ${c.cyan((e.id ?? "").slice(0, 8))} ${statusColor(e.status)} ${entryText(e).slice(0, 70)}`)
187
+ }
188
+ console.log("")
189
+ }
190
+ }
191
+
192
+ // ── Subcommand handlers ───────────────────────────────────────────────────────
193
+
194
+ async function cmdAsk(question, chronicleDir) {
195
+ const allEntries = await readCommitted(chronicleDir)
196
+ const evidence = findRelevant(allEntries, question)
197
+ const provider = await detectProvider()
198
+
199
+ if (!provider) {
200
+ renderPassthrough(question, evidence)
201
+ return
202
+ }
203
+
204
+ const { llm, name: llmName } = provider
205
+ process.stdout.write(c.dim(`\n Thinking (${llmName})…`))
206
+ try {
207
+ const result = await runAdvisor(llm, question, evidence)
208
+ process.stdout.write("\r" + " ".repeat(50) + "\r")
209
+ renderAsk(question, result)
210
+ } catch (err) {
211
+ process.stdout.write("\r" + " ".repeat(50) + "\r")
212
+ console.error(`\n${c.red("Advisor failed:")} ${err.message}\n`)
213
+ process.exit(1)
214
+ }
215
+ }
216
+
217
+ function renderPassthrough(question, evidence) {
218
+ console.log(`\n${c.bold("Chronicle evidence")} ${c.dim(`for: "${question}"`)}\n`)
219
+ if (evidence.length === 0) {
220
+ console.log(c.dim(" No relevant Chronicle entries found.\n"))
221
+ } else {
222
+ for (const e of evidence) {
223
+ console.log(` ${c.cyan(e.id.slice(0, 8))} ${c.bold(entryText(e))}`)
224
+ console.log(` ${c.dim(`status: ${e.status} · confidence: ${e.confidence} · areas: ${(e.affected_areas ?? []).join(", ")}`)}`)
225
+ console.log("")
226
+ }
227
+ }
228
+ console.log(c.dim("─".repeat(60)))
229
+ console.log(`\n${c.bold("Synthesis request")}`)
230
+ console.log(`\n ${question}`)
231
+ console.log(`\n${c.dim(" No LLM configured — answer from the Chronicle evidence above.")}`)
232
+ console.log(c.dim(" (Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or run Ollama for auto-synthesis.)\n"))
233
+ }
234
+
235
+ async function cmdQuery(topic, chronicleDir) {
236
+ const allEntries = await readCommitted(chronicleDir)
237
+ const matches = findRelevant(allEntries, topic, 8)
238
+ renderQuery(topic, matches)
239
+ }
240
+
241
+ async function cmdBrief(chronicleDir) {
242
+ const allEntries = await readCommitted(chronicleDir)
243
+ renderBrief(allEntries)
244
+ }
245
+
246
+ // ── Entry point ───────────────────────────────────────────────────────────────
247
+
248
+ export async function run(argv) {
249
+ const [subOrQuestion, ...rest] = argv
250
+
251
+ const chronicleDir = await findChronicleDir(process.cwd())
252
+ if (!chronicleDir) {
253
+ console.error(`\n${c.red("No .chronicle/ directory found.")} Run ${c.bold("quorum init")} first.\n`)
254
+ process.exit(1)
255
+ }
256
+
257
+ // `quorum advisor ask "..."` or `quorum advisor "..."` (default to ask)
258
+ if (subOrQuestion === "ask") {
259
+ const question = rest.join(" ").trim()
260
+ if (!question) return printUsage()
261
+ return cmdAsk(question, chronicleDir)
262
+ }
263
+
264
+ // `quorum advisor query "topic"` — Chronicle lookup, no LLM
265
+ if (subOrQuestion === "query") {
266
+ const topic = rest.join(" ").trim()
267
+ if (!topic) return printUsage()
268
+ return cmdQuery(topic, chronicleDir)
269
+ }
270
+
271
+ // `quorum advisor brief` — high-level Chronicle summary
272
+ if (subOrQuestion === "brief") {
273
+ return cmdBrief(chronicleDir)
274
+ }
275
+
276
+ // No subcommand — treat the whole argv as a question (default: ask)
277
+ const question = argv.join(" ").trim()
278
+ if (!question) return printUsage()
279
+ return cmdAsk(question, chronicleDir)
280
+ }
281
+
282
+ function printUsage() {
283
+ console.log(`
284
+ ${c.bold("quorum advisor")} — ask plain-language questions about your codebase
285
+
286
+ ${c.bold("Subcommands:")}
287
+ ${c.cyan("quorum advisor")} ${c.dim('"question"')} Ask a question (default — uses LLM)
288
+ ${c.cyan("quorum advisor ask")} ${c.dim('"question"')} Ask explicitly
289
+ ${c.cyan("quorum advisor query")} ${c.dim('"topic"')} Search Chronicle entries (no LLM)
290
+ ${c.cyan("quorum advisor brief")} High-level Chronicle summary (no LLM)
291
+
292
+ ${c.bold("Examples:")}
293
+ quorum advisor "what happens if we change the auth system?"
294
+ quorum advisor ask "is it safe to add a NOT NULL column to users?"
295
+ quorum advisor query "authentication"
296
+ quorum advisor brief
297
+
298
+ ${c.dim("ask requires ANTHROPIC_API_KEY or OPENAI_API_KEY in your environment.")}
299
+ `)
300
+ process.exit(1)
301
+ }