@balpal4495/quorum 0.2.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 +223 -11
- package/SETUP.md +30 -0
- package/bin/commands/check.js +122 -0
- package/bin/commands/commit.js +210 -0
- package/bin/commands/init.js +236 -0
- package/bin/commands/sentinel.js +160 -0
- package/bin/commands/status.js +117 -0
- package/bin/quorum.js +103 -0
- package/bin/shared/chronicle.js +129 -0
- package/bin/shared/colors.js +22 -0
- package/bin/shared/patterns.js +83 -0
- package/evals/__tests__/eval.test.ts +31 -0
- package/evals/cases/auth_hs256_rejected.json +46 -0
- package/evals/cases/auth_rs256_valid.json +30 -0
- package/evals/cases/cache_missing_lock.json +31 -0
- package/evals/cases/db_naive_not_null.json +32 -0
- package/evals/cases/logging_pii_leak.json +32 -0
- package/evals/cases/migration_with_rollback.json +43 -0
- package/evals/cases/no_evidence_novel_design.json +16 -0
- package/evals/cases/payment_no_idempotency.json +33 -0
- package/evals/cases/redis_session_rejected.json +32 -0
- package/evals/cases/safe_refactor.json +17 -0
- package/evals/runner.ts +226 -0
- package/modules/AGENTS.md +9 -5
- package/modules/CLAUDE.md +25 -2
- package/modules/README.md +153 -6
- package/modules/council/chairman.ts +84 -14
- package/modules/council/deliberate.ts +24 -4
- package/modules/council/index.ts +6 -1
- package/modules/council/risk.ts +89 -0
- package/modules/council/types.ts +63 -1
- package/modules/jury/evaluate.ts +32 -8
- package/modules/jury/index.ts +3 -1
- package/modules/jury/preflight.ts +101 -0
- package/modules/jury/schema.ts +9 -0
- package/modules/jury/types.ts +20 -1
- package/modules/shared/types.ts +8 -0
- package/package.json +3 -3
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
|
|
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/
|
|
@@ -65,6 +163,12 @@ When a decision is made, your AI stages a Chronicle entry using `oracle.propose(
|
|
|
65
163
|
|
|
66
164
|
Commit `.chronicle/committed/` to git. Future sessions — and your teammates' sessions — start with that context.
|
|
67
165
|
|
|
166
|
+
### Every merged PR creates a Chronicle proposal automatically
|
|
167
|
+
|
|
168
|
+
A GitHub Actions workflow fires when any PR merges to main. It creates a Chronicle proposal capturing the decision, which files changed, and any explicitly deferred items from the PR description. The proposal sits in `proposals/` until you commit it — nothing is auto-indexed.
|
|
169
|
+
|
|
170
|
+
This means the gap between "PR merged" and "Chronicle knows about it" is now zero.
|
|
171
|
+
|
|
68
172
|
---
|
|
69
173
|
|
|
70
174
|
## Real examples
|
|
@@ -111,15 +215,36 @@ gaps: ["no lock strategy documented", "no rollback plan"]
|
|
|
111
215
|
council_brief: challenge
|
|
112
216
|
```
|
|
113
217
|
|
|
114
|
-
Council's Chairman gives a verdict:
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
218
|
+
Council's Chairman gives a structured verdict:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"satisfied": false,
|
|
223
|
+
"blockers": [
|
|
224
|
+
{
|
|
225
|
+
"issue": "Naive ALTER TABLE takes an exclusive lock for minutes on a 50M-row table",
|
|
226
|
+
"evidence": ["db-017"],
|
|
227
|
+
"required_fix": "Use shadow column pattern or pg_repack. Add rollback path."
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
"warnings": [],
|
|
231
|
+
"advisor_split": { "proceed": 0, "redesign": 4, "investigate-more": 1 }
|
|
232
|
+
}
|
|
120
233
|
```
|
|
121
234
|
|
|
122
|
-
The agent revises the plan. You approve the Chronicle entry once it's solid. The reasoning is on record for the next time someone touches that table
|
|
235
|
+
The agent revises the plan. You approve the Chronicle entry once it's solid. The reasoning — including alternatives considered and why they were rejected — is on record for the next time someone touches that table:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"decision": "Use shadow column pattern for NOT NULL migration on users table",
|
|
240
|
+
"alternatives_considered": ["naive ALTER TABLE", "pg_repack"],
|
|
241
|
+
"rejected_reason": ["ALTER TABLE takes exclusive lock for minutes on 50M rows"],
|
|
242
|
+
"scope": ["database", "migrations"],
|
|
243
|
+
"affected_areas": ["db/migrations/", "src/models/user.ts"],
|
|
244
|
+
"validation_plan": ["Confirm 100% backfill before applying NOT NULL constraint", "Test rollback path on staging"],
|
|
245
|
+
"review_after": "2026-08-01"
|
|
246
|
+
}
|
|
247
|
+
```
|
|
123
248
|
|
|
124
249
|
---
|
|
125
250
|
|
|
@@ -130,21 +255,108 @@ Four portable TypeScript modules installed into `quorum/modules/`:
|
|
|
130
255
|
| Module | What it does |
|
|
131
256
|
|---|---|
|
|
132
257
|
| **Oracle** | Query and write interface to Chronicle. No LLM required. |
|
|
133
|
-
| **Jury** | Evaluates a proposed design against Chronicle evidence. Returns a confidence score. |
|
|
134
|
-
| **Council** | A panel of advisors challenges the design independently, reviewers critique anonymously, a Chairman gives a
|
|
258
|
+
| **Jury** | Evaluates a proposed design against Chronicle evidence. Returns a decomposed confidence score and hard-blocker gaps. |
|
|
259
|
+
| **Council** | A panel of advisors challenges the design independently, reviewers critique anonymously, a Chairman gives a structured verdict with blockers and warnings. |
|
|
135
260
|
| **Sentinel** | Shows which files the AI knows nothing about, flags stale knowledge, and posts a coverage map on every PR. |
|
|
136
261
|
|
|
137
262
|
The modules live in your repo — readable by any AI working in the codebase. Nothing is hidden in `node_modules`.
|
|
138
263
|
|
|
139
264
|
---
|
|
140
265
|
|
|
266
|
+
## How Jury works
|
|
267
|
+
|
|
268
|
+
Before calling the LLM, Jury runs a **deterministic preflight** — no LLM required — that checks whether the design touches sensitive areas (auth, database migrations, crypto, payments, PII, secrets), mentions a rollback strategy, and whether any refuted Chronicle entries conflict with the design. These facts are injected into the Jury prompt as hard ground truth.
|
|
269
|
+
|
|
270
|
+
The LLM then scores the design across four dimensions:
|
|
271
|
+
|
|
272
|
+
| Dimension | What it measures |
|
|
273
|
+
|---|---|
|
|
274
|
+
| Evidence support | Do validated Chronicle entries confirm this approach works here? |
|
|
275
|
+
| Feasibility | Do Chronicle entries suggest this is achievable? |
|
|
276
|
+
| Risk | How well does the design address known failure modes? |
|
|
277
|
+
| Completeness | Does the design cover the full outcome? |
|
|
278
|
+
|
|
279
|
+
Confidence is recomputed as the exact average of those four scores — the LLM's stated confidence is discarded. Jury also separates `blocking_gaps` (must resolve before proceeding) from `gaps` (useful but not critical).
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## How Council works
|
|
284
|
+
|
|
285
|
+
Before running the full panel, a **risk classifier** reads the design text and Chronicle evidence and assigns a risk level:
|
|
286
|
+
|
|
287
|
+
| Risk | Council mode | LLM calls |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| Low | 1 advisor + 1 reviewer | 4 |
|
|
290
|
+
| Medium | 1 advisor + 2 reviewers | 5 |
|
|
291
|
+
| High | 5 advisors + 5 reviewers | 12 |
|
|
292
|
+
| Critical | 5 advisors + 5 reviewers (+ human architecture flag) | 12 |
|
|
293
|
+
|
|
294
|
+
Auth, crypto, payments, and data deletion trigger Critical. Database migrations, PII, permissions trigger High. Cache, queues, deployments trigger Medium. Everything else is Low.
|
|
295
|
+
|
|
296
|
+
The Chairman's verdict is **structured**:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"blockers": [
|
|
301
|
+
{
|
|
302
|
+
"issue": "No rollback plan for destructive migration",
|
|
303
|
+
"evidence": ["db-017"],
|
|
304
|
+
"required_fix": "Add shadow-column migration and rollback path before execution"
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
"warnings": [
|
|
308
|
+
{
|
|
309
|
+
"issue": "No integration test for token expiry edge case",
|
|
310
|
+
"suggested_fix": "Add test covering token rotation during concurrent requests"
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
"advisor_split": { "proceed": 2, "redesign": 2, "investigate-more": 1 }
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Blockers must be resolved before the human gate. Warnings can be ticketed. High `advisor_split` disagreement is surfaced explicitly — it means genuine uncertainty, not a safe proceed.
|
|
318
|
+
|
|
319
|
+
Every Oracle ID cited in the verdict is also validated against the evidence pack that was actually sent. Hallucinated citations are flagged in `citation_validation.hallucinated_ids` and stripped from the Chronicle proposal.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Eval suite
|
|
324
|
+
|
|
325
|
+
`evals/` contains canonical test cases — known-bad proposals that Council should block and known-good ones it should pass:
|
|
326
|
+
|
|
327
|
+
| Case | Expected outcome |
|
|
328
|
+
|---|---|
|
|
329
|
+
| Naive NOT NULL migration on large table | Block — no lock strategy |
|
|
330
|
+
| HS256 JWT when RS256 was already chosen | Block — cites refuted entry auth-022 |
|
|
331
|
+
| PII fields logged to stdout | Block — GDPR violation in evidence |
|
|
332
|
+
| Payment charge without idempotency key | Block — duplicate charge risk |
|
|
333
|
+
| Redis sessions (previously removed) | Block — memory overhead already documented |
|
|
334
|
+
| Cache without stampede protection | Block — prior incident in Chronicle |
|
|
335
|
+
| Safe internal rename | Proceed — low risk, no conflicts |
|
|
336
|
+
| RS256 JWT (approved pattern) | Proceed — matches validated Chronicle entry |
|
|
337
|
+
| Migration with rollback + shadow column | Proceed — addresses documented failure mode |
|
|
338
|
+
| Novel WebSocket design, no evidence | Investigate-more — no Chronicle evidence either way |
|
|
339
|
+
|
|
340
|
+
Deterministic assertions (preflight, risk classifier) run on every CI pass. LLM-dependent assertions (confidence bounds, Council recommendation) activate with `EVAL_LLM=1`.
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npx vitest run evals/
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
141
348
|
## Sentinel — coverage and drift
|
|
142
349
|
|
|
143
350
|
Sentinel surfaces two things Chronicle can't tell you about itself.
|
|
144
351
|
|
|
145
352
|
**Coverage** — which parts of your codebase has the AI never documented?
|
|
146
353
|
|
|
147
|
-
|
|
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).
|
|
148
360
|
|
|
149
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.
|
|
150
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
|
|
@@ -197,11 +207,17 @@ If the directory is not created, re-check that `setup()` is being awaited correc
|
|
|
197
207
|
Confirm the modules are working in this environment:
|
|
198
208
|
|
|
199
209
|
```bash
|
|
210
|
+
# Module unit tests
|
|
200
211
|
npx vitest run quorum/modules/
|
|
212
|
+
|
|
213
|
+
# Eval suite — deterministic assertions, no LLM required
|
|
214
|
+
npx vitest run quorum/evals/
|
|
201
215
|
```
|
|
202
216
|
|
|
203
217
|
All tests should pass. If they fail due to missing dependencies, re-run Step 3.
|
|
204
218
|
|
|
219
|
+
The eval suite runs canonical test cases (known-bad proposals that should block, known-good ones that should pass) through the deterministic preflight and risk classifier. These pass without any LLM. If you later want to test Jury confidence and Council recommendations against a real LLM, set `EVAL_LLM=1` when running.
|
|
220
|
+
|
|
205
221
|
---
|
|
206
222
|
|
|
207
223
|
## Step 9 — Report what was done
|
|
@@ -254,3 +270,17 @@ Key reminders:
|
|
|
254
270
|
- **Query Oracle before proposing anything.** `oracle.query("what you're about to do")` first.
|
|
255
271
|
- **Never call `oracle.commit()` autonomously.** Only `oracle.propose()`. A human commits.
|
|
256
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
|
+
}
|