@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/CLAUDE.md +102 -42
- package/README.md +317 -223
- package/SETUP.md +68 -94
- package/bin/commands/advisor.js +301 -0
- package/bin/commands/commit.js +42 -52
- package/bin/commands/evolve.js +285 -0
- package/bin/commands/growth.js +139 -0
- package/bin/commands/init.js +59 -60
- package/bin/commands/sentinel.js +1 -1
- package/bin/quorum.js +28 -0
- package/bin/shared/llm.js +228 -0
- package/modules/AGENTS.md +8 -0
- package/modules/CLAUDE.md +8 -2
- package/modules/README.md +72 -13
- package/modules/advisor/ask.ts +87 -0
- package/modules/advisor/index.ts +2 -0
- package/modules/advisor/prompt.ts +50 -0
- package/modules/advisor/types.ts +26 -0
- package/modules/setup.ts +15 -0
- package/package.json +4 -2
- package/bin/init.js +0 -378
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 —
|
|
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
|
|
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/
|
|
33
|
-
- `quorum/
|
|
34
|
-
- `.github/copilot-instructions.md` — workflow rules for AI agents
|
|
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
|
|
62
|
+
## Step 3 — Install Quorum
|
|
56
63
|
|
|
57
|
-
Open `package.json` at the project root. Add
|
|
64
|
+
Open `package.json` at the project root. Add to `devDependencies` if not already present:
|
|
58
65
|
|
|
59
66
|
```json
|
|
60
|
-
"
|
|
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
|
|
84
|
+
The automated init command handles this step automatically.
|
|
84
85
|
|
|
85
|
-
**If
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
149
|
+
Skip this step if you are using only the CLI (`quorum advisor`, `quorum check`, etc.).
|
|
167
150
|
|
|
168
|
-
|
|
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 "
|
|
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
|
-
|
|
162
|
+
Must be called once before any `oracle.query()`, `evaluate()`, `deliberate()`, or `ask()` call.
|
|
180
163
|
|
|
181
|
-
|
|
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:**
|
|
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
|
-
|
|
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 —
|
|
210
|
-
|
|
211
|
-
Confirm the modules are working in this environment:
|
|
187
|
+
## Step 8 — Verify the CLI works
|
|
212
188
|
|
|
213
189
|
```bash
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
233
|
-
3.
|
|
234
|
-
4.
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
-
- **
|
|
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
|
+
}
|