@balpal4495/quorum 0.4.0 → 1.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 +226 -176
- package/SETUP.md +24 -10
- 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 +8 -0
- package/bin/commands/sentinel.js +1 -1
- package/bin/init.js +9 -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/council/deliberate.ts +15 -5
- package/modules/council/types.ts +1 -1
- package/modules/setup.ts +15 -0
- package/package.json +1 -1
package/SETUP.md
CHANGED
|
@@ -31,7 +31,7 @@ Read these files in full before proceeding:
|
|
|
31
31
|
|
|
32
32
|
- `quorum/modules/README.md` — module overview and quick-start
|
|
33
33
|
- `quorum/modules/AGENTS.md` — file ownership and invariants
|
|
34
|
-
-
|
|
34
|
+
- `.github/copilot-instructions.md` — workflow rules for AI agents (installed at project root by init)
|
|
35
35
|
|
|
36
36
|
These are your operating instructions for everything that follows.
|
|
37
37
|
|
|
@@ -80,16 +80,20 @@ If the project uses `yarn` or `pnpm`, use the appropriate installer instead.
|
|
|
80
80
|
|
|
81
81
|
### 4a. `.github/copilot-instructions.md`
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
Copy `quorum/.github/copilot-instructions.md` to `.github/copilot-instructions.md`.
|
|
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.
|
|
85
84
|
|
|
86
|
-
**If
|
|
87
|
-
|
|
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:
|
|
88
92
|
|
|
89
93
|
```markdown
|
|
90
94
|
---
|
|
91
95
|
|
|
92
|
-
<!--
|
|
96
|
+
<!-- quorum -->
|
|
93
97
|
```
|
|
94
98
|
|
|
95
99
|
Do not replace or overwrite existing content.
|
|
@@ -113,7 +117,7 @@ Append to it:
|
|
|
113
117
|
|
|
114
118
|
## Quorum modules
|
|
115
119
|
|
|
116
|
-
See [quorum/modules/AGENTS.md](quorum/modules/AGENTS.md) for Oracle, Jury, and
|
|
120
|
+
See [quorum/modules/AGENTS.md](quorum/modules/AGENTS.md) for Advisor, Oracle, Jury, Council, and Sentinel internals.
|
|
117
121
|
```
|
|
118
122
|
|
|
119
123
|
### 4c. `CLAUDE.md`
|
|
@@ -135,7 +139,7 @@ Append to it:
|
|
|
135
139
|
|
|
136
140
|
## Quorum modules
|
|
137
141
|
|
|
138
|
-
See [quorum/modules/CLAUDE.md](quorum/modules/CLAUDE.md) for Oracle, Jury, and
|
|
142
|
+
See [quorum/modules/CLAUDE.md](quorum/modules/CLAUDE.md) for Advisor, Oracle, Jury, Council, and Sentinel internals.
|
|
139
143
|
```
|
|
140
144
|
|
|
141
145
|
---
|
|
@@ -166,13 +170,15 @@ Add the following import and call at startup, **before** any agent or workflow c
|
|
|
166
170
|
```typescript
|
|
167
171
|
import { setup } from "./quorum/modules/setup"
|
|
168
172
|
|
|
169
|
-
const { oracle, evaluate, deliberate } = await setup({
|
|
173
|
+
const { oracle, evaluate, deliberate, ask } = await setup({
|
|
170
174
|
llm: yourLLMProvider, // replace with your project's LLM provider function
|
|
171
175
|
})
|
|
172
176
|
```
|
|
173
177
|
|
|
174
178
|
`setup()` creates `.chronicle/` directories, warms the embedder, and wires all module dependencies.
|
|
175
|
-
It must be called once before any `oracle.query()`, `evaluate()`, or `
|
|
179
|
+
It must be called once before any `oracle.query()`, `evaluate()`, `deliberate()`, or `ask()` call.
|
|
180
|
+
|
|
181
|
+
`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. Use it to answer questions rather than to evaluate designs.
|
|
176
182
|
|
|
177
183
|
If no entry point exists yet, note that `setup()` must be called before first use — do not inline it.
|
|
178
184
|
|
|
@@ -267,6 +273,7 @@ Gemini and use it for large-context tasks.
|
|
|
267
273
|
You are now operating under Quorum. The rules in `quorum/modules/AGENTS.md` and `.github/copilot-instructions.md` apply to all subsequent work.
|
|
268
274
|
|
|
269
275
|
Key reminders:
|
|
276
|
+
- **Ask Advisor for context.** `quorum advisor "what has the team decided about X?"` before starting any meaningful work.
|
|
270
277
|
- **Query Oracle before proposing anything.** `oracle.query("what you're about to do")` first.
|
|
271
278
|
- **Never call `oracle.commit()` autonomously.** Only `oracle.propose()`. A human commits.
|
|
272
279
|
- **Chronicle entries are ground truth.** Respect `refuted` entries — do not retry what has already failed.
|
|
@@ -277,10 +284,17 @@ These commands are available globally after `npm install -g @balpal4495/quorum`:
|
|
|
277
284
|
|
|
278
285
|
| Command | What it does |
|
|
279
286
|
|---|---|
|
|
287
|
+
| `quorum advisor "question"` | Ask a plain-language question — answer synthesised from Chronicle (needs LLM) |
|
|
288
|
+
| `quorum advisor query "topic"` | Search Chronicle entries by keyword (no LLM) |
|
|
289
|
+
| `quorum advisor brief` | High-level Chronicle summary (no LLM) |
|
|
280
290
|
| `quorum status` | Chronicle health — pending proposals, committed entries |
|
|
281
291
|
| `quorum check --outcome X --design Y` | Preflight + risk classifier (no LLM) |
|
|
282
292
|
| `quorum commit --list` | List pending proposals |
|
|
283
293
|
| `quorum commit <id>` | Approve and index a proposal |
|
|
284
294
|
| `quorum sentinel coverage [--path <dir>]` | Chronicle coverage of source files |
|
|
295
|
+
| `quorum growth` | Chronicle learning health — growth rate, last commit, pending proposals |
|
|
296
|
+
| `quorum evolve` | Consolidate Chronicle — merges duplicates, resolves contradictions, promotes open entries |
|
|
285
297
|
|
|
286
298
|
`quorum check` exit codes: `0` = low/medium risk · `1` = high · `2` = critical
|
|
299
|
+
|
|
300
|
+
`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 (Claude Code, Copilot, Codex, Gemini) 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
|
+
}
|
package/bin/commands/commit.js
CHANGED
|
@@ -119,23 +119,15 @@ export async function run(argv) {
|
|
|
119
119
|
return
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// ── Check optional dependencies
|
|
123
|
-
console.log(`\n${c.bold("Checking dependencies")}`)
|
|
122
|
+
// ── Check optional embedding dependencies ─────────────────────────────────
|
|
124
123
|
const hasXenova = await checkDep("@xenova/transformers")
|
|
125
124
|
const hasLanceDB = await checkDep("vectordb")
|
|
125
|
+
const canEmbed = hasXenova && hasLanceDB
|
|
126
126
|
|
|
127
|
-
if (!
|
|
128
|
-
console.
|
|
129
|
-
console.
|
|
130
|
-
process.exit(1)
|
|
131
|
-
}
|
|
132
|
-
if (!hasLanceDB) {
|
|
133
|
-
console.error(`\n ${c.red("✗")} vectordb not installed`)
|
|
134
|
-
console.error(c.dim(" Run: npm install vectordb\n"))
|
|
135
|
-
process.exit(1)
|
|
127
|
+
if (!canEmbed) {
|
|
128
|
+
console.log(`\n${c.dim(" Embedding deps not found — committing to JSON store only.")}`)
|
|
129
|
+
console.log(c.dim(" Install @xenova/transformers + vectordb to enable semantic search.\n"))
|
|
136
130
|
}
|
|
137
|
-
console.log(` ${c.green("✓")} @xenova/transformers`)
|
|
138
|
-
console.log(` ${c.green("✓")} vectordb`)
|
|
139
131
|
|
|
140
132
|
// ── Build entry ────────────────────────────────────────────────────────────
|
|
141
133
|
const entry = {
|
|
@@ -144,46 +136,44 @@ export async function run(argv) {
|
|
|
144
136
|
timestamp: new Date().toISOString(),
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
// ── Embed
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
139
|
+
// ── Embed + index in vector store (optional) ───────────────────────────────
|
|
140
|
+
if (canEmbed) {
|
|
141
|
+
const spin = spinner("Embedding and indexing…")
|
|
142
|
+
try {
|
|
143
|
+
const { pipeline } = (await import("@xenova/transformers")).default ?? await import("@xenova/transformers")
|
|
144
|
+
const embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2")
|
|
145
|
+
const embeddingText = [
|
|
146
|
+
entryText(entry),
|
|
147
|
+
...(entry.affected_areas ?? []),
|
|
148
|
+
...(entry.scope ?? []),
|
|
149
|
+
].join(" ")
|
|
150
|
+
const output = await embedder(embeddingText, { pooling: "mean", normalize: true })
|
|
151
|
+
const vector = Array.from(output.data)
|
|
152
|
+
spin.stop(`${c.green("✓")} Embedded (${vector.length}-dim)`)
|
|
153
|
+
|
|
154
|
+
const storeSpin = spinner("Indexing in vector store…")
|
|
155
|
+
try {
|
|
156
|
+
const lancedb = (await import("vectordb")).default ?? (await import("vectordb"))
|
|
157
|
+
const tableDir = path.join(chronicleDir, "entries")
|
|
158
|
+
const db = await lancedb.connect(tableDir)
|
|
159
|
+
const row = { id: entry.id, vector, payload: JSON.stringify(entry) }
|
|
160
|
+
const names = await db.tableNames()
|
|
161
|
+
if (names.includes("entries")) {
|
|
162
|
+
const table = await db.openTable("entries")
|
|
163
|
+
await table.delete(`id = '${entry.id.replace(/'/g, "''")}'`)
|
|
164
|
+
await table.add([row])
|
|
165
|
+
} else {
|
|
166
|
+
await db.createTable("entries", [row], { metric: "cosine" })
|
|
167
|
+
}
|
|
168
|
+
storeSpin.stop(`${c.green("✓")} Indexed in vector store`)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
storeSpin.stop(`${c.yellow("⚠")} Vector store write failed — JSON commit will proceed`)
|
|
171
|
+
console.error(c.dim(` ${err.message}`))
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
spin.stop(`${c.yellow("⚠")} Embedding failed — JSON commit will proceed`)
|
|
175
|
+
console.error(c.dim(` ${err.message}`))
|
|
181
176
|
}
|
|
182
|
-
storeSpin.stop(`${c.green("✓")} Indexed in vector store`)
|
|
183
|
-
} catch (err) {
|
|
184
|
-
storeSpin.stop(`${c.red("✗")} Vector store write failed`)
|
|
185
|
-
console.error(c.red(`\n ${err.message}\n`))
|
|
186
|
-
process.exit(1)
|
|
187
177
|
}
|
|
188
178
|
|
|
189
179
|
// ── Write committed file ───────────────────────────────────────────────────
|