@balpal4495/quorum 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,97 @@ That's the whole setup. Quorum copies its modules into `quorum/`, merges instruc
22
22
 
23
23
  ---
24
24
 
25
+ ## CLI commands
26
+
27
+ After `npm install -g @balpal4495/quorum` (or `npx @balpal4495/quorum`), you get:
28
+
29
+ | Command | What it does | LLM? |
30
+ |---|---|---|
31
+ | `quorum init` | Scaffold Quorum into a project | No |
32
+ | `quorum status` | Chronicle health — pending proposals, committed entries, recent activity | No |
33
+ | `quorum check --outcome X --design Y` | Deterministic preflight + risk classifier | No |
34
+ | `quorum commit <id>` | Approve and index a pending proposal | No |
35
+ | `quorum sentinel [coverage]` | Chronicle coverage of your source files | No |
36
+
37
+ ### `quorum check` — instant risk triage before the full pipeline
38
+
39
+ ```bash
40
+ quorum check \
41
+ --outcome "migrate auth from sessions to JWT" \
42
+ --design "replace session middleware with HS256 tokens on all routes"
43
+ ```
44
+
45
+ ```
46
+ Preflight
47
+ ⚠ Sensitive areas: auth
48
+ ✗ No rollback strategy mentioned
49
+ ✗ No test strategy mentioned
50
+
51
+ Risk
52
+ Level: CRITICAL
53
+ Council mode: full
54
+ Reasons:
55
+ · authentication or authorisation logic
56
+
57
+ ⚠ Critical risk — human architecture review required before proceeding.
58
+ ```
59
+
60
+ Exit codes: `0` = low/medium, `1` = high, `2` = critical — pipe into CI scripts directly.
61
+ Also accepts JSON on stdin: `echo '{"outcome":"…","design":"…"}' | quorum check --json`
62
+
63
+ ### `quorum status` — see what's pending and what's been learned
64
+
65
+ ```bash
66
+ quorum status
67
+ ```
68
+
69
+ ```
70
+ Chronicle status .chronicle/
71
+
72
+ 8 committed entries (6 accepted, 1 refuted, 1 other)
73
+ 2 pending proposals
74
+
75
+ Pending proposals (awaiting quorum commit <id>)
76
+ a1b2c3d4 JWT key rotation approach needs RS256 not HS256
77
+ oracle/propose.ts, modules/auth/
78
+
79
+ Recent entries
80
+ e5f6a7b8 [accepted] Shadow column migration avoids exclusive lock on 50M rows 2d ago
81
+ ```
82
+
83
+ ### `quorum commit <id>` — the human gate from your terminal
84
+
85
+ ```bash
86
+ quorum commit --list # see pending proposals with full detail
87
+ quorum commit a1b2c3d4 # approve and index (supports partial ID prefix)
88
+ quorum commit a1b2c3d4 --dry-run # preview without writing
89
+ ```
90
+
91
+ Embeds the entry via the local ONNX model, upserts to LanceDB, writes to `.chronicle/committed/`, updates `SUMMARY.md`, and removes the proposal — the full oracle commit in one command. Requires `@xenova/transformers` and `vectordb` to be installed (both are optional deps from `quorum init`).
92
+
93
+ ### `quorum sentinel coverage` — see where Chronicle goes dark
94
+
95
+ ```bash
96
+ quorum sentinel coverage --path modules
97
+ ```
98
+
99
+ ```
100
+ Chronicle coverage modules/
101
+
102
+ ████░░░░░░░░░░░░░░░░ 20% (6/30 files)
103
+
104
+ Covered
105
+ ✓ oracle/propose.ts (3 entries)
106
+ ✓ oracle/query.ts (1 entry)
107
+
108
+ Uncovered (no Chronicle entries reference these files)
109
+ ✗ council/chairman.ts
110
+ ✗ jury/evaluate.ts
111
+
112
+ ```
113
+
114
+ ---
115
+
25
116
  ## Then just talk to your AI
26
117
 
27
118
  Once initialized, open your AI in agent mode and tell it:
@@ -54,7 +145,14 @@ Every proposal goes through Jury (confidence scoring against evidence) and Counc
54
145
 
55
146
  ### You approve what gets remembered
56
147
 
57
- When a decision is made, your AI stages a Chronicle entry using `oracle.propose()`. You approve it with `oracle.commit(proposalId)`. Nothing is indexed without your explicit sign-off.
148
+ When a decision is made, your AI stages a Chronicle entry using `oracle.propose()`. You approve it from the terminal:
149
+
150
+ ```bash
151
+ quorum commit --list # see what's pending
152
+ quorum commit <id> # approve and index
153
+ ```
154
+
155
+ Nothing is indexed without your explicit sign-off.
58
156
 
59
157
  ```
60
158
  .chronicle/
@@ -253,7 +351,12 @@ Sentinel surfaces two things Chronicle can't tell you about itself.
253
351
 
254
352
  **Coverage** — which parts of your codebase has the AI never documented?
255
353
 
256
- **Drift** — do existing Chronicle entries still accurately describe the code, or have they gone stale?
354
+ ```bash
355
+ quorum sentinel coverage --path src # quick check from the terminal
356
+ quorum sentinel coverage --json # machine-readable, for scripts
357
+ ```
358
+
359
+ **Drift** — do existing Chronicle entries still accurately describe the code, or have they gone stale? Drift detection requires an LLM; use `sentinelAssertions({ llm })` in your test suite (the CLI surfaces the message and directs you there).
257
360
 
258
361
  Add `sentinel-pr.yml` (included in `quorum/`) to your GitHub Actions and every PR gets a comment showing a full-project coverage table and a colour-coded heatmap. Changed modules are highlighted. Reviewers see exactly where knowledge is solid and where it goes dark.
259
362
 
package/SETUP.md CHANGED
@@ -176,6 +176,16 @@ It must be called once before any `oracle.query()`, `evaluate()`, or `deliberate
176
176
 
177
177
  If no entry point exists yet, note that `setup()` must be called before first use — do not inline it.
178
178
 
179
+ **Approving Chronicle proposals:** after an agent calls `oracle.propose()`, approve and index the entry from the terminal:
180
+
181
+ ```bash
182
+ quorum commit --list # see pending proposals
183
+ quorum commit <id> # approve and index a proposal
184
+ quorum commit <id> --dry-run # preview without writing
185
+ ```
186
+
187
+ Requires `@xenova/transformers` and `vectordb` (both added in Step 3).
188
+
179
189
  ---
180
190
 
181
191
  ## Step 7 — Verify Chronicle is created
@@ -260,3 +270,17 @@ Key reminders:
260
270
  - **Query Oracle before proposing anything.** `oracle.query("what you're about to do")` first.
261
271
  - **Never call `oracle.commit()` autonomously.** Only `oracle.propose()`. A human commits.
262
272
  - **Chronicle entries are ground truth.** Respect `refuted` entries — do not retry what has already failed.
273
+
274
+ ### CLI quick reference
275
+
276
+ These commands are available globally after `npm install -g @balpal4495/quorum`:
277
+
278
+ | Command | What it does |
279
+ |---|---|
280
+ | `quorum status` | Chronicle health — pending proposals, committed entries |
281
+ | `quorum check --outcome X --design Y` | Preflight + risk classifier (no LLM) |
282
+ | `quorum commit --list` | List pending proposals |
283
+ | `quorum commit <id>` | Approve and index a proposal |
284
+ | `quorum sentinel coverage [--path <dir>]` | Chronicle coverage of source files |
285
+
286
+ `quorum check` exit codes: `0` = low/medium risk · `1` = high · `2` = critical
@@ -0,0 +1,122 @@
1
+ import { createInterface } from "readline"
2
+ import { c } from "../shared/colors.js"
3
+ import { runPreflight, classifyRisk } from "../shared/patterns.js"
4
+
5
+ function parseArgs(argv) {
6
+ const args = { outcome: "", design: "", json: false }
7
+ for (let i = 0; i < argv.length; i++) {
8
+ if ((argv[i] === "--outcome" || argv[i] === "-o") && argv[i + 1]) { args.outcome = argv[++i]; continue }
9
+ if ((argv[i] === "--design" || argv[i] === "-d") && argv[i + 1]) { args.design = argv[++i]; continue }
10
+ if (argv[i] === "--json") { args.json = true; continue }
11
+ }
12
+ return args
13
+ }
14
+
15
+ async function readStdin() {
16
+ if (process.stdin.isTTY) return null
17
+ return new Promise((resolve) => {
18
+ let data = ""
19
+ const rl = createInterface({ input: process.stdin })
20
+ rl.on("line", (line) => { data += line + "\n" })
21
+ rl.on("close", () => resolve(data.trim()))
22
+ })
23
+ }
24
+
25
+ function riskColor(level) {
26
+ switch (level) {
27
+ case "low": return c.green(level.toUpperCase())
28
+ case "medium": return c.yellow(level.toUpperCase())
29
+ case "high": return c.red(level.toUpperCase())
30
+ case "critical": return `${c.bold(c.red("CRITICAL"))}`
31
+ default: return level.toUpperCase()
32
+ }
33
+ }
34
+
35
+ function exitCodeForLevel(level) {
36
+ if (level === "critical") return 2
37
+ if (level === "high") return 1
38
+ return 0
39
+ }
40
+
41
+ export async function run(argv) {
42
+ const args = parseArgs(argv)
43
+
44
+ // Accept JSON from stdin if no flags provided
45
+ if (!args.outcome && !args.design) {
46
+ const stdin = await readStdin()
47
+ if (stdin) {
48
+ try {
49
+ const parsed = JSON.parse(stdin)
50
+ args.outcome = parsed.outcome ?? ""
51
+ args.design = parsed.design ?? ""
52
+ } catch {
53
+ console.error(c.red("stdin: expected JSON with { outcome, design } or use --outcome / --design flags"))
54
+ process.exit(1)
55
+ }
56
+ }
57
+ }
58
+
59
+ if (!args.outcome && !args.design) {
60
+ console.error(`\n${c.bold("quorum check")} — run preflight and risk classifier (no LLM required)\n`)
61
+ console.error("Usage:")
62
+ console.error(` quorum check --outcome "what you want to achieve" --design "how you plan to do it"`)
63
+ console.error(` echo '{"outcome":"...","design":"..."}' | quorum check`)
64
+ console.error(` quorum check --outcome "..." --design "..." --json\n`)
65
+ console.error("Exit codes: 0 = low/medium risk 1 = high risk 2 = critical risk\n")
66
+ process.exit(1)
67
+ }
68
+
69
+ const preflight = runPreflight(args.outcome, args.design)
70
+ const risk = classifyRisk(args.outcome, args.design)
71
+
72
+ if (args.json) {
73
+ console.log(JSON.stringify({ preflight, risk }, null, 2))
74
+ process.exit(exitCodeForLevel(risk.level))
75
+ }
76
+
77
+ // ── Human-readable output ─────────────────────────────────────────────────
78
+ console.log(`\n${c.bold("Preflight")}`)
79
+
80
+ if (preflight.touches_sensitive_area) {
81
+ console.log(` ${c.yellow("⚠")} Sensitive areas: ${c.yellow(preflight.sensitive_areas.join(", "))}`)
82
+ } else {
83
+ console.log(` ${c.green("✓")} No sensitive areas detected`)
84
+ }
85
+
86
+ console.log(preflight.rollback_mentioned
87
+ ? ` ${c.green("✓")} Rollback strategy mentioned`
88
+ : ` ${c.dim("✗")} No rollback strategy mentioned`)
89
+
90
+ console.log(preflight.test_strategy_mentioned
91
+ ? ` ${c.green("✓")} Test strategy mentioned`
92
+ : ` ${c.dim("✗")} No test strategy mentioned`)
93
+
94
+ console.log(`\n${c.bold("Risk")}`)
95
+ console.log(` Level: ${riskColor(risk.level)}`)
96
+ console.log(` Council mode: ${c.dim(risk.council_mode)}`)
97
+
98
+ if (risk.reasons.length > 0 && risk.reasons[0] !== "no sensitive patterns detected") {
99
+ console.log(` Reasons:`)
100
+ for (const reason of risk.reasons) {
101
+ console.log(` ${c.dim("·")} ${reason}`)
102
+ }
103
+ }
104
+
105
+ // ── Actionable guidance ───────────────────────────────────────────────────
106
+ if (risk.level === "critical") {
107
+ console.log(`\n ${c.red("⚠ Critical risk — human architecture review required before proceeding.")}`)
108
+ console.log(` ${c.dim(" Run the full Jury + Council pipeline and get explicit approval.")}`)
109
+ } else if (risk.level === "high") {
110
+ console.log(`\n ${c.yellow("⚠ High risk — full Council deliberation recommended.")}`)
111
+ if (!preflight.rollback_mentioned) {
112
+ console.log(` ${c.dim(" Add a rollback strategy before submitting for review.")}`)
113
+ }
114
+ } else if (risk.level === "medium") {
115
+ console.log(`\n ${c.dim(" Medium risk — Jury + lite Council review.")}`)
116
+ } else {
117
+ console.log(`\n ${c.dim(" Low risk — Jury-only review sufficient.")}`)
118
+ }
119
+
120
+ console.log("")
121
+ process.exit(exitCodeForLevel(risk.level))
122
+ }
@@ -0,0 +1,210 @@
1
+ import { promises as fs } from "fs"
2
+ import path from "path"
3
+ import { randomUUID } from "crypto"
4
+ import { exec } from "child_process"
5
+ import { promisify } from "util"
6
+ import { c } from "../shared/colors.js"
7
+ import { findChronicleDir, entryText, updateSummary } from "../shared/chronicle.js"
8
+
9
+ const execAsync = promisify(exec)
10
+
11
+ function parseArgs(argv) {
12
+ const args = { id: null, dryRun: false, list: false }
13
+ for (let i = 0; i < argv.length; i++) {
14
+ if (argv[i] === "--dry-run") { args.dryRun = true; continue }
15
+ if (argv[i] === "--list") { args.list = true; continue }
16
+ if (!argv[i].startsWith("-")) args.id = argv[i]
17
+ }
18
+ return args
19
+ }
20
+
21
+ async function checkDep(name) {
22
+ try {
23
+ await import(name)
24
+ return true
25
+ } catch {
26
+ return false
27
+ }
28
+ }
29
+
30
+ function spinner(msg) {
31
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
32
+ let i = 0
33
+ const interval = setInterval(() => {
34
+ process.stdout.write(`\r ${c.cyan(frames[i++ % frames.length])} ${msg}`)
35
+ }, 80)
36
+ return { stop: (finalMsg) => { clearInterval(interval); process.stdout.write(`\r ${finalMsg}\n`) } }
37
+ }
38
+
39
+ export async function run(argv) {
40
+ const args = parseArgs(argv)
41
+
42
+ const chronicleDir = await findChronicleDir(process.cwd())
43
+ if (!chronicleDir) {
44
+ console.error(`\n${c.red("No .chronicle/ directory found.")} Run ${c.bold("quorum init")} first.\n`)
45
+ process.exit(1)
46
+ }
47
+
48
+ // ── --list: show pending proposals ────────────────────────────────────────
49
+ if (args.list || (!args.id && argv.length === 0)) {
50
+ const proposalsDir = path.join(chronicleDir, "proposals")
51
+ let files
52
+ try { files = await fs.readdir(proposalsDir) } catch { files = [] }
53
+ const proposals = []
54
+ for (const f of files) {
55
+ if (!f.endsWith(".json")) continue
56
+ try {
57
+ const raw = await fs.readFile(path.join(proposalsDir, f), "utf8")
58
+ proposals.push({ id: f.replace(".json", ""), ...JSON.parse(raw) })
59
+ } catch { /* skip */ }
60
+ }
61
+ if (proposals.length === 0) {
62
+ console.log(`\n${c.dim("No pending proposals.")}\n`)
63
+ return
64
+ }
65
+ console.log(`\n${c.bold("Pending proposals")}\n`)
66
+ for (const p of proposals) {
67
+ console.log(` ${c.cyan(p.id)}`)
68
+ console.log(` ${c.bold("key_insight:")} ${entryText(p)}`)
69
+ console.log(` ${c.bold("areas:")} ${(p.affected_areas ?? []).join(", ")}`)
70
+ console.log(` ${c.bold("confidence:")} ${p.confidence}`)
71
+ if (p.status) console.log(` ${c.bold("status:")} ${p.status}`)
72
+ console.log("")
73
+ }
74
+ console.log(c.dim(` quorum commit <id> to approve and index a proposal`))
75
+ console.log("")
76
+ return
77
+ }
78
+
79
+ if (!args.id) {
80
+ console.error(`\n${c.bold("quorum commit")} — approve and index a Chronicle proposal\n`)
81
+ console.error("Usage:")
82
+ console.error(` quorum commit <proposalId> Commit and index the proposal`)
83
+ console.error(` quorum commit <proposalId> --dry-run Preview without writing`)
84
+ console.error(` quorum commit --list List pending proposals\n`)
85
+ process.exit(1)
86
+ }
87
+
88
+ // ── Find proposal (supports partial ID prefix) ────────────────────────────
89
+ const proposalsDir = path.join(chronicleDir, "proposals")
90
+ let files
91
+ try { files = await fs.readdir(proposalsDir) } catch { files = [] }
92
+
93
+ const match = files.find(f => f === `${args.id}.json` || f.startsWith(args.id))
94
+ if (!match) {
95
+ console.error(`\n${c.red("Proposal not found:")} ${args.id}`)
96
+ console.error(c.dim(` Run ${c.bold("quorum commit --list")} to see pending proposals.\n`))
97
+ process.exit(1)
98
+ }
99
+
100
+ const proposalId = match.replace(".json", "")
101
+ const proposalPath = path.join(proposalsDir, match)
102
+
103
+ let raw
104
+ try { raw = await fs.readFile(proposalPath, "utf8") } catch {
105
+ console.error(`\n${c.red("Could not read proposal:")} ${proposalPath}\n`)
106
+ process.exit(1)
107
+ }
108
+ const partial = JSON.parse(raw)
109
+
110
+ // ── Dry run ────────────────────────────────────────────────────────────────
111
+ if (args.dryRun) {
112
+ console.log(`\n${c.bold("Dry run")} — would commit proposal ${c.cyan(proposalId.slice(0, 8))}\n`)
113
+ console.log(` ${c.bold("key_insight:")} ${entryText(partial)}`)
114
+ console.log(` ${c.bold("areas:")} ${(partial.affected_areas ?? []).join(", ")}`)
115
+ console.log(` ${c.bold("status:")} ${partial.status}`)
116
+ console.log(` ${c.bold("confidence:")} ${partial.confidence}`)
117
+ if (partial.scope?.length) console.log(` ${c.bold("scope:")} ${partial.scope.join(", ")}`)
118
+ console.log(`\n ${c.dim("(No changes made.)")}\n`)
119
+ return
120
+ }
121
+
122
+ // ── Check optional dependencies ────────────────────────────────────────────
123
+ console.log(`\n${c.bold("Checking dependencies")}`)
124
+ const hasXenova = await checkDep("@xenova/transformers")
125
+ const hasLanceDB = await checkDep("vectordb")
126
+
127
+ if (!hasXenova) {
128
+ console.error(`\n ${c.red("✗")} @xenova/transformers not installed`)
129
+ console.error(c.dim(" Run: npm install @xenova/transformers\n"))
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)
136
+ }
137
+ console.log(` ${c.green("✓")} @xenova/transformers`)
138
+ console.log(` ${c.green("✓")} vectordb`)
139
+
140
+ // ── Build entry ────────────────────────────────────────────────────────────
141
+ const entry = {
142
+ ...partial,
143
+ id: randomUUID(),
144
+ timestamp: new Date().toISOString(),
145
+ }
146
+
147
+ // ── Embed ──────────────────────────────────────────────────────────────────
148
+ const spin = spinner("Loading embedder (first run may take 30s)…")
149
+ let vector
150
+ try {
151
+ const { pipeline } = (await import("@xenova/transformers")).default ?? await import("@xenova/transformers")
152
+ const embedder = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2")
153
+ const embeddingText = [
154
+ entryText(entry),
155
+ ...(entry.affected_areas ?? []),
156
+ ...(entry.scope ?? []),
157
+ ].join(" ")
158
+ const output = await embedder(embeddingText, { pooling: "mean", normalize: true })
159
+ vector = Array.from(output.data)
160
+ spin.stop(`${c.green("✓")} Embedded (${vector.length}-dim)`)
161
+ } catch (err) {
162
+ spin.stop(`${c.red("✗")} Embedding failed`)
163
+ console.error(c.red(`\n ${err.message}\n`))
164
+ process.exit(1)
165
+ }
166
+
167
+ // ── Store in LanceDB ───────────────────────────────────────────────────────
168
+ const storeSpin = spinner("Indexing in Chronicle…")
169
+ try {
170
+ const lancedb = (await import("vectordb")).default ?? (await import("vectordb"))
171
+ const tableDir = path.join(chronicleDir, "entries")
172
+ const db = await lancedb.connect(tableDir)
173
+ const row = { id: entry.id, vector, payload: JSON.stringify(entry) }
174
+ const names = await db.tableNames()
175
+ if (names.includes("entries")) {
176
+ const table = await db.openTable("entries")
177
+ await table.delete(`id = '${entry.id.replace(/'/g, "''")}'`)
178
+ await table.add([row])
179
+ } else {
180
+ await db.createTable("entries", [row], { metric: "cosine" })
181
+ }
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
+ }
188
+
189
+ // ── Write committed file ───────────────────────────────────────────────────
190
+ const committedDir = path.join(chronicleDir, "committed")
191
+ const committedPath = path.join(committedDir, `${entry.id}.json`)
192
+ await fs.mkdir(committedDir, { recursive: true })
193
+ await fs.writeFile(committedPath, JSON.stringify(entry, null, 2), "utf8")
194
+
195
+ // Git add — best-effort
196
+ try { await execAsync(`git add "${committedPath}"`) } catch { /* not in git, or git unavailable */ }
197
+
198
+ // Update SUMMARY.md — best-effort
199
+ try { await updateSummary(chronicleDir) } catch { /* never fail a commit */ }
200
+
201
+ // Remove proposal
202
+ await fs.unlink(proposalPath)
203
+
204
+ // ── Result ─────────────────────────────────────────────────────────────────
205
+ console.log(`\n${c.green("✓ Committed")} ${c.dim(entry.id)}`)
206
+ console.log(` ${c.bold("key_insight:")} ${entryText(entry)}`)
207
+ console.log(` ${c.bold("areas:")} ${(entry.affected_areas ?? []).join(", ")}`)
208
+ console.log(` ${c.bold("status:")} ${entry.status}`)
209
+ console.log("")
210
+ }