@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.
@@ -0,0 +1,285 @@
1
+ import { promises as fs } from "fs"
2
+ import path from "path"
3
+ import { randomUUID } from "crypto"
4
+ import { c } from "../shared/colors.js"
5
+ import { findChronicleDir, readCommitted, entryText } from "../shared/chronicle.js"
6
+ import { detectProvider } from "../shared/llm.js"
7
+
8
+ function parseArgs(argv) {
9
+ const args = { dryRun: false, json: false }
10
+ for (const arg of argv) {
11
+ if (arg === "--dry-run") args.dryRun = true
12
+ if (arg === "--json") args.json = true
13
+ }
14
+ return args
15
+ }
16
+
17
+ const SYSTEM_PROMPT = `You are a Chronicle Analyst for Quorum — a persistent knowledge store that gives AI coding assistants long-term memory across sessions.
18
+
19
+ You will receive a list of committed Chronicle entries. Your task is to find quality improvements. Be conservative — it is correct to propose nothing when entries are all distinct and healthy. Never consolidate entries that are merely related; only merge when they are genuinely saying the same thing.
20
+
21
+ Three types of improvement:
22
+
23
+ CONSOLIDATE — two or more entries that cover the same ground, expressed redundantly or overlapping in scope. Propose merging into one sharper entry that supersedes them.
24
+
25
+ RESOLVE_CONTRADICTION — a validated entry that a newer entry implicitly supersedes or contradicts. Propose marking the older entry as refuted.
26
+
27
+ PROMOTE_OPEN — an entry with status "open" that other entries have since confirmed or validated. Propose elevating to "validated" with a justified confidence score.
28
+
29
+ Return ONLY valid JSON — no prose, no markdown fences — with this exact shape:
30
+ {
31
+ "actions": [
32
+ {
33
+ "type": "consolidate",
34
+ "entry_ids": ["full-uuid-1", "full-uuid-2"],
35
+ "synthesised": {
36
+ "topic": "short label",
37
+ "decision": "One precise sentence: what was decided and why.",
38
+ "key_insight": "same as decision",
39
+ "affected_areas": ["path/to/file.ts"],
40
+ "scope": ["tag1", "tag2"],
41
+ "alternatives_considered": [],
42
+ "rejected_reason": [],
43
+ "status": "validated",
44
+ "confidence": 0.9
45
+ },
46
+ "reason": "why these entries should be merged"
47
+ },
48
+ {
49
+ "type": "resolve_contradiction",
50
+ "stale_entry_id": "full-uuid",
51
+ "superseding_entry_id": "full-uuid",
52
+ "reason": "why the older entry is now superseded"
53
+ },
54
+ {
55
+ "type": "promote_open",
56
+ "entry_id": "full-uuid",
57
+ "new_confidence": 0.85,
58
+ "reason": "which other entries confirm this"
59
+ }
60
+ ],
61
+ "no_action_reason": "brief note on why the rest need no change, or empty string"
62
+ }`
63
+
64
+ function formatEntries(entries) {
65
+ return entries.map(e => {
66
+ const lines = [
67
+ `ID: ${e.id}`,
68
+ `Topic: ${e.topic ?? "(none)"}`,
69
+ `Status: ${e.status}`,
70
+ `Confidence: ${e.confidence}`,
71
+ `Decision: ${e.decision ?? e.key_insight}`,
72
+ `Affected areas: ${(e.affected_areas ?? []).join(", ")}`,
73
+ `Scope: ${(e.scope ?? []).join(", ")}`,
74
+ ]
75
+ if (e.alternatives_considered?.length) lines.push(`Alternatives considered: ${e.alternatives_considered.join("; ")}`)
76
+ if (e.rejected_reason?.length) lines.push(`Rejected reason: ${e.rejected_reason.join("; ")}`)
77
+ if (e.supersedes) lines.push(`Supersedes: ${e.supersedes}`)
78
+ if (e.superseded_by) lines.push(`Superseded by: ${e.superseded_by}`)
79
+ return lines.join("\n")
80
+ }).join("\n\n---\n\n")
81
+ }
82
+
83
+ function validateAction(action) {
84
+ if (!action || typeof action.type !== "string") return false
85
+ if (action.type === "consolidate")
86
+ return Array.isArray(action.entry_ids) && action.entry_ids.length >= 2 && action.synthesised
87
+ if (action.type === "resolve_contradiction")
88
+ return typeof action.stale_entry_id === "string" && typeof action.superseding_entry_id === "string"
89
+ if (action.type === "promote_open")
90
+ return typeof action.entry_id === "string" && typeof action.new_confidence === "number"
91
+ return false
92
+ }
93
+
94
+ function spinner(msg) {
95
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
96
+ let i = 0
97
+ const interval = setInterval(() => {
98
+ process.stdout.write(`\r ${c.cyan(frames[i++ % frames.length])} ${msg}`)
99
+ }, 80)
100
+ return { stop: (final) => { clearInterval(interval); process.stdout.write(`\r ${final}\n`) } }
101
+ }
102
+
103
+ function renderEvolvePassthrough(entries) {
104
+ console.log(`\n${c.bold("Chronicle evolution analysis")} ${c.dim(`${entries.length} entries`)}\n`)
105
+ console.log(c.dim(" No LLM configured — outputting Chronicle for agent analysis.\n"))
106
+ console.log(formatEntries(entries))
107
+ console.log(c.dim("─".repeat(60)))
108
+ console.log(`\n${c.bold("Analysis request")}\n`)
109
+ console.log(" Review the Chronicle entries above and identify quality improvements:")
110
+ console.log(" · consolidate — entries covering the same ground (merge into one stronger entry)")
111
+ console.log(" · resolve — a validated entry superseded or contradicted by a newer one")
112
+ console.log(" · promote — an 'open' entry confirmed by other entries (elevate to validated)")
113
+ console.log("")
114
+ console.log(" For each improvement, create a proposal using the template in CLAUDE.md:")
115
+ console.log(c.dim(" node -e \"const { randomUUID } = require('crypto'); ...\" (see CLAUDE.md)"))
116
+ console.log(c.dim(" Then run: quorum commit --list\n"))
117
+ }
118
+
119
+ export async function run(argv) {
120
+ const args = parseArgs(argv)
121
+
122
+ const chronicleDir = await findChronicleDir(process.cwd())
123
+ if (!chronicleDir) {
124
+ console.error(`\n${c.red("No .chronicle/ directory found.")} Run ${c.bold("quorum init")} first.\n`)
125
+ process.exit(1)
126
+ }
127
+
128
+ const entries = await readCommitted(chronicleDir)
129
+ if (entries.length === 0) {
130
+ console.log(`\n${c.dim("No committed entries — nothing to evolve.")}\n`)
131
+ return
132
+ }
133
+
134
+ const provider = await detectProvider()
135
+ if (!provider) {
136
+ renderEvolvePassthrough(entries)
137
+ return
138
+ }
139
+ const { llm, name: llmName } = provider
140
+
141
+ console.log(`\n${c.bold("Quorum evolve")} ${c.dim(`${entries.length} entries · via ${llmName}`)}\n`)
142
+
143
+ const spin = spinner(`Analysing ${entries.length} Chronicle entries…`)
144
+
145
+ let raw
146
+ try {
147
+ raw = await llm([
148
+ { role: "system", content: SYSTEM_PROMPT },
149
+ {
150
+ role: "user",
151
+ content: `Here are the ${entries.length} committed Chronicle entries:\n\n${formatEntries(entries)}\n\nAnalyse and return your proposed improvements as JSON.`,
152
+ },
153
+ ])
154
+ spin.stop(`${c.green("✓")} Analysis complete`)
155
+ } catch (err) {
156
+ spin.stop(`${c.red("✗")} LLM call failed`)
157
+ console.error(c.dim(` ${err.message}\n`))
158
+ process.exit(1)
159
+ }
160
+
161
+ let parsed
162
+ try {
163
+ const cleaned = raw.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim()
164
+ parsed = JSON.parse(cleaned)
165
+ } catch {
166
+ console.error(`\n${c.red("Could not parse LLM response as JSON.")}\n`)
167
+ console.error(c.dim(raw.slice(0, 500)))
168
+ process.exit(1)
169
+ }
170
+
171
+ const actions = (parsed.actions ?? []).filter(validateAction)
172
+
173
+ if (args.json) {
174
+ console.log(JSON.stringify({ actions, no_action_reason: parsed.no_action_reason ?? "" }, null, 2))
175
+ return
176
+ }
177
+
178
+ if (actions.length === 0) {
179
+ console.log(`\n ${c.green("✓")} Chronicle is clean — no improvements identified.`)
180
+ if (parsed.no_action_reason) console.log(`\n ${c.dim(parsed.no_action_reason)}`)
181
+ console.log("")
182
+ return
183
+ }
184
+
185
+ console.log(`\n ${c.bold(String(actions.length))} improvement${actions.length === 1 ? "" : "s"} found\n`)
186
+
187
+ if (args.dryRun) {
188
+ for (const action of actions) {
189
+ if (action.type === "consolidate") {
190
+ console.log(` ${c.cyan("consolidate")} ${action.entry_ids.map(id => id.slice(0, 8)).join(" + ")}`)
191
+ console.log(` ${c.dim(action.reason)}`)
192
+ console.log(` ${c.dim("→")} ${action.synthesised.decision ?? action.synthesised.key_insight}`)
193
+ } else if (action.type === "resolve_contradiction") {
194
+ console.log(` ${c.yellow("resolve")} ${action.stale_entry_id.slice(0, 8)} → refuted (superseded by ${action.superseding_entry_id.slice(0, 8)})`)
195
+ console.log(` ${c.dim(action.reason)}`)
196
+ } else if (action.type === "promote_open") {
197
+ console.log(` ${c.green("promote")} ${action.entry_id.slice(0, 8)} → validated (confidence ${action.new_confidence})`)
198
+ console.log(` ${c.dim(action.reason)}`)
199
+ }
200
+ console.log("")
201
+ }
202
+ console.log(c.dim(" (Dry run — no proposals written.)\n"))
203
+ return
204
+ }
205
+
206
+ // Write proposals
207
+ const proposalsDir = path.join(chronicleDir, "proposals")
208
+ await fs.mkdir(proposalsDir, { recursive: true })
209
+
210
+ const entryMap = new Map(entries.map(e => [e.id, e]))
211
+ let stagedCount = 0
212
+
213
+ for (const action of actions) {
214
+ const proposalId = randomUUID()
215
+ let proposal
216
+
217
+ if (action.type === "consolidate") {
218
+ const sources = action.entry_ids.map(eid => entryMap.get(eid)).filter(Boolean)
219
+ const mergedAreas = [...new Set(sources.flatMap(e => e.affected_areas ?? []))]
220
+ const mergedScope = [...new Set(sources.flatMap(e => e.scope ?? []))]
221
+ proposal = {
222
+ schema_version: 2,
223
+ ...action.synthesised,
224
+ affected_areas: action.synthesised.affected_areas?.length ? action.synthesised.affected_areas : mergedAreas,
225
+ scope: action.synthesised.scope?.length ? action.synthesised.scope : mergedScope,
226
+ key_insight: action.synthesised.decision ?? action.synthesised.key_insight,
227
+ decision: action.synthesised.decision ?? action.synthesised.key_insight,
228
+ supersedes: action.entry_ids,
229
+ source_module: "evolve",
230
+ evidence_cited: action.entry_ids,
231
+ _evolve_action: "consolidate",
232
+ _evolve_reason: action.reason,
233
+ }
234
+ } else if (action.type === "resolve_contradiction") {
235
+ const stale = entryMap.get(action.stale_entry_id)
236
+ proposal = {
237
+ schema_version: 2,
238
+ key_insight: stale ? entryText(stale) : action.stale_entry_id,
239
+ decision: stale ? entryText(stale) : action.stale_entry_id,
240
+ topic: stale?.topic ?? "contradiction-resolution",
241
+ affected_areas: stale?.affected_areas ?? [],
242
+ scope: [...(stale?.scope ?? []), "evolution"],
243
+ status: "refuted",
244
+ confidence: stale?.confidence ?? 0.5,
245
+ source_module: "evolve",
246
+ evidence_cited: [action.superseding_entry_id],
247
+ supersedes: action.stale_entry_id,
248
+ _evolve_action: "resolve_contradiction",
249
+ _evolve_reason: action.reason,
250
+ }
251
+ } else if (action.type === "promote_open") {
252
+ const original = entryMap.get(action.entry_id)
253
+ if (!original) continue
254
+ const { id: _id, timestamp: _ts, ...rest } = original
255
+ proposal = {
256
+ ...rest,
257
+ schema_version: 2,
258
+ status: "validated",
259
+ confidence: action.new_confidence,
260
+ supersedes: action.entry_id,
261
+ source_module: "evolve",
262
+ _evolve_action: "promote_open",
263
+ _evolve_reason: action.reason,
264
+ }
265
+ }
266
+
267
+ if (!proposal) continue
268
+ await fs.writeFile(path.join(proposalsDir, `${proposalId}.json`), JSON.stringify(proposal, null, 2))
269
+ stagedCount++
270
+
271
+ if (action.type === "consolidate") {
272
+ console.log(` ${c.green("✓")} ${c.cyan("consolidate")} ${action.entry_ids.map(id => id.slice(0, 8)).join(" + ")}`)
273
+ console.log(` ${c.dim(action.reason)}`)
274
+ } else if (action.type === "resolve_contradiction") {
275
+ console.log(` ${c.green("✓")} ${c.yellow("resolve")} ${action.stale_entry_id.slice(0, 8)} → refuted`)
276
+ console.log(` ${c.dim(action.reason)}`)
277
+ } else if (action.type === "promote_open") {
278
+ console.log(` ${c.green("✓")} ${c.green("promote")} ${action.entry_id.slice(0, 8)} → validated (${action.new_confidence})`)
279
+ console.log(` ${c.dim(action.reason)}`)
280
+ }
281
+ console.log("")
282
+ }
283
+
284
+ console.log(` ${c.green(String(stagedCount))} proposal${stagedCount === 1 ? "" : "s"} staged — run ${c.bold("quorum commit --list")} to review\n`)
285
+ }
@@ -0,0 +1,139 @@
1
+ import { c } from "../shared/colors.js"
2
+ import { findChronicleDir, readCommitted, readProposals, entryText } from "../shared/chronicle.js"
3
+
4
+ const STALLED_DAYS = 14
5
+ const SLOW_DAYS = 7
6
+
7
+ function parseArgs(argv) {
8
+ const args = { json: false }
9
+ for (const arg of argv) {
10
+ if (arg === "--json") args.json = true
11
+ }
12
+ return args
13
+ }
14
+
15
+ function weekKey(timestamp) {
16
+ const d = new Date(timestamp)
17
+ const day = d.getUTCDay() || 7
18
+ const mon = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - day + 1))
19
+ return mon.toISOString().slice(0, 10)
20
+ }
21
+
22
+ export async function run(argv) {
23
+ const args = parseArgs(argv)
24
+
25
+ const chronicleDir = await findChronicleDir(process.cwd())
26
+ if (!chronicleDir) {
27
+ console.error(`\n${c.red("No .chronicle/ directory found.")} Run ${c.bold("quorum init")} first.\n`)
28
+ process.exit(1)
29
+ }
30
+
31
+ const [entries, proposals] = await Promise.all([
32
+ readCommitted(chronicleDir),
33
+ readProposals(chronicleDir),
34
+ ])
35
+
36
+ const now = Date.now()
37
+ const msPerDay = 86_400_000
38
+ const sorted = [...entries].sort((a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? ""))
39
+
40
+ const in7Days = entries.filter(e => e.timestamp && (now - new Date(e.timestamp).getTime()) < 7 * msPerDay).length
41
+ const in30Days = entries.filter(e => e.timestamp && (now - new Date(e.timestamp).getTime()) < 30 * msPerDay).length
42
+
43
+ const newest = sorted[sorted.length - 1]
44
+ const daysSince = newest?.timestamp
45
+ ? Math.floor((now - new Date(newest.timestamp).getTime()) / msPerDay)
46
+ : null
47
+
48
+ let status
49
+ if (entries.length === 0) status = "empty"
50
+ else if (daysSince !== null && daysSince >= STALLED_DAYS) status = "stalled"
51
+ else if (daysSince !== null && daysSince >= SLOW_DAYS) status = "slow"
52
+ else if (in7Days >= 3) status = "thriving"
53
+ else status = "healthy"
54
+
55
+ if (args.json) {
56
+ console.log(JSON.stringify({
57
+ status,
58
+ totalEntries: entries.length,
59
+ pendingProposals: proposals.length,
60
+ commitsLast7Days: in7Days,
61
+ commitsLast30Days: in30Days,
62
+ daysSinceLastCommit: daysSince,
63
+ newestEntryTimestamp: newest?.timestamp ?? null,
64
+ }, null, 2))
65
+ return
66
+ }
67
+
68
+ const statusLabel = {
69
+ empty: c.red("EMPTY — Chronicle has no committed entries"),
70
+ stalled: c.red(`STALLED — no commits in ${daysSince} days`),
71
+ slow: c.yellow(`SLOW — no commits in ${daysSince} day${daysSince === 1 ? "" : "s"}`),
72
+ thriving: c.green("THRIVING"),
73
+ healthy: c.green("HEALTHY"),
74
+ }[status]
75
+
76
+ console.log(`\n${c.bold("Chronicle growth")}\n`)
77
+ console.log(` Status ${statusLabel}`)
78
+ console.log(` Total entries ${c.bold(String(entries.length))}`)
79
+ console.log(` Last 7 days ${in7Days === 0 ? c.yellow("0") : c.green(String(in7Days))} commit${in7Days === 1 ? "" : "s"}`)
80
+ console.log(` Last 30 days ${in30Days} commit${in30Days === 1 ? "" : "s"}`)
81
+
82
+ if (daysSince !== null) {
83
+ const col = daysSince >= STALLED_DAYS ? c.red : daysSince >= SLOW_DAYS ? c.yellow : c.green
84
+ console.log(` Last commit ${col(`${daysSince} day${daysSince === 1 ? "" : "s"} ago`)} ${c.dim(newest.timestamp.slice(0, 10))}`)
85
+ }
86
+
87
+ console.log(` Pending ${proposals.length} proposal${proposals.length === 1 ? "" : "s"} awaiting ${c.bold("quorum commit")}`)
88
+
89
+ // Weekly sparkline (last 8 weeks)
90
+ if (entries.length > 0) {
91
+ const weeks = new Map()
92
+ for (const e of entries) {
93
+ if (!e.timestamp) continue
94
+ const k = weekKey(e.timestamp)
95
+ weeks.set(k, (weeks.get(k) ?? 0) + 1)
96
+ }
97
+ const weekKeys = [...weeks.keys()].sort().reverse().slice(0, 8)
98
+ if (weekKeys.length > 0) {
99
+ console.log(`\n ${c.bold("Weekly commits")}`)
100
+ for (const wk of weekKeys) {
101
+ const n = weeks.get(wk)
102
+ const bar = "▪".repeat(n)
103
+ const col = n >= 3 ? c.green : n >= 1 ? c.cyan : c.dim
104
+ console.log(` ${c.dim("w/c")} ${wk} ${col(bar || "—")} ${col(String(n))}`)
105
+ }
106
+ }
107
+ }
108
+
109
+ // Recent learnings
110
+ const recent = [...entries]
111
+ .filter(e => e.timestamp)
112
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
113
+ .slice(0, 7)
114
+ if (recent.length > 0) {
115
+ console.log(`\n ${c.bold("Recent learnings")}`)
116
+ for (const e of recent) {
117
+ const text = entryText(e).slice(0, 72)
118
+ const trail = entryText(e).length > 72 ? "…" : ""
119
+ const date = e.timestamp.slice(0, 10)
120
+ const col = e.status === "refuted" ? c.red : e.status === "open" ? c.yellow : c.dim
121
+ console.log(` ${c.dim(e.id.slice(0, 8))} ${text}${trail} ${col(date)}`)
122
+ }
123
+ }
124
+
125
+ // Actionable advice when not healthy
126
+ if (status === "stalled" || status === "slow" || status === "empty") {
127
+ console.log(`\n ${c.yellow("Action needed:")}`)
128
+ if (proposals.length > 0) {
129
+ console.log(` ${proposals.length} proposal${proposals.length === 1 ? " is" : "s are"} staged and ready to commit.`)
130
+ console.log(` Run ${c.bold("quorum commit --list")} to review them.`)
131
+ } else {
132
+ console.log(` No proposals are staged.`)
133
+ console.log(` At the end of every session, create proposals for significant decisions.`)
134
+ console.log(` ${c.dim("See CLAUDE.md for the proposal format and session protocol.")}`)
135
+ }
136
+ }
137
+
138
+ console.log("")
139
+ }
@@ -55,6 +55,13 @@ async function copyModules(target) {
55
55
  log.created("quorum/SETUP.md")
56
56
  }
57
57
 
58
+ async function copyEvals(target) {
59
+ const src = path.join(QUORUM_ROOT, "evals")
60
+ const dest = path.join(target, "quorum", "evals")
61
+ await fs.cp(src, dest, { recursive: true })
62
+ log.created("quorum/evals/")
63
+ }
64
+
58
65
  async function mergeCopilotInstructions(target) {
59
66
  log.section("Merging AI instruction files")
60
67
  const src = path.join(QUORUM_ROOT, ".github", "copilot-instructions.md")
@@ -207,6 +214,7 @@ export async function run(PKG_VERSION) {
207
214
 
208
215
  await guardAlreadyInitialized(target)
209
216
  await copyModules(target)
217
+ await copyEvals(target)
210
218
  await mergeCopilotInstructions(target)
211
219
  await mergeAgentsMd(target)
212
220
  await mergeClaudeMd(target)
@@ -1,4 +1,4 @@
1
- import { promises as fs, Dirent } from "fs"
1
+ import { promises as fs } from "fs"
2
2
  import path from "path"
3
3
  import { c } from "../shared/colors.js"
4
4
  import { findChronicleDir, readCommitted } from "../shared/chronicle.js"
package/bin/init.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Drops Quorum into an existing Node.js project.
6
6
  * Run from the target project root:
7
7
  *
8
- * npx github:balpal4495/Quorum init
8
+ * npx @balpal4495/quorum@latest init
9
9
  *
10
10
  * Zero external dependencies — uses only Node.js built-ins.
11
11
  * Requires Node.js 18+.
@@ -104,6 +104,13 @@ async function copyModules() {
104
104
  log.created("quorum/SETUP.md")
105
105
  }
106
106
 
107
+ async function copyEvals() {
108
+ const src = path.join(QUORUM_ROOT, "evals")
109
+ const dest = path.join(TARGET, "quorum", "evals")
110
+ await fs.cp(src, dest, { recursive: true })
111
+ log.created("quorum/evals/")
112
+ }
113
+
107
114
  async function mergeCopilotInstructions() {
108
115
  log.section("Merging AI instruction files")
109
116
 
@@ -309,6 +316,7 @@ async function main() {
309
316
 
310
317
  await guardAlreadyInitialized()
311
318
  await copyModules()
319
+ await copyEvals()
312
320
  await mergeCopilotInstructions()
313
321
  await mergeAgentsMd()
314
322
  await mergeClaudeMd()
package/bin/quorum.js CHANGED
@@ -20,13 +20,23 @@ function help() {
20
20
  ${c.bold("quorum")} ${c.dim(`v${PKG_VERSION}`)} — portable reasoning layer for agentic codebases
21
21
 
22
22
  ${c.bold("Usage:")}
23
+ ${c.cyan("quorum advisor")} ${c.dim('"question"')} Ask a plain-language question (uses LLM)
24
+ ${c.cyan("quorum advisor query")} ${c.dim('"topic"')} Search Chronicle entries (no LLM)
25
+ ${c.cyan("quorum advisor brief")} High-level Chronicle summary (no LLM)
23
26
  ${c.cyan("quorum init")} Scaffold Quorum into a project
24
27
  ${c.cyan("quorum status")} Show Chronicle health and pending proposals
25
28
  ${c.cyan("quorum check")} --outcome <x> --design <y> Preflight + risk (no LLM)
26
29
  ${c.cyan("quorum commit")} <id> Approve and index a Chronicle proposal
27
30
  ${c.cyan("quorum sentinel")} [coverage] Chronicle coverage of source files
31
+ ${c.cyan("quorum growth")} Chronicle learning health and growth rate
32
+ ${c.cyan("quorum evolve")} Consolidate and improve Chronicle entries (uses LLM)
28
33
  ${c.cyan("quorum --version")} Print version
29
34
 
35
+ ${c.bold("quorum advisor")} subcommands:
36
+ ask ${c.dim('"question"')} Ask with LLM synthesis + validation loop
37
+ query ${c.dim('"topic"')} Chronicle lookup (no LLM, instant)
38
+ brief Chronicle summary (no LLM, instant)
39
+
30
40
  ${c.bold("quorum check")} flags:
31
41
  --outcome -o What you want to achieve
32
42
  --design -d How you plan to do it
@@ -62,6 +72,12 @@ async function cli() {
62
72
 
63
73
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
64
74
 
75
+ if (command === "advisor") {
76
+ const { run } = await import(path.join(__dirname, "commands/advisor.js"))
77
+ await run(rest)
78
+ return
79
+ }
80
+
65
81
  if (command === "init") {
66
82
  const { run } = await import(path.join(__dirname, "commands/init.js"))
67
83
  await run(PKG_VERSION)
@@ -92,6 +108,18 @@ async function cli() {
92
108
  return
93
109
  }
94
110
 
111
+ if (command === "growth") {
112
+ const { run } = await import(path.join(__dirname, "commands/growth.js"))
113
+ await run(rest)
114
+ return
115
+ }
116
+
117
+ if (command === "evolve") {
118
+ const { run } = await import(path.join(__dirname, "commands/evolve.js"))
119
+ await run(rest)
120
+ return
121
+ }
122
+
95
123
  console.error(`${c.red(`Unknown command: ${command}`)}`)
96
124
  console.error(`Run ${c.bold("quorum help")} for usage.`)
97
125
  process.exit(1)