@balpal4495/quorum 0.3.0 → 0.4.2
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 +109 -6
- package/SETUP.md +34 -6
- package/bin/commands/check.js +122 -0
- package/bin/commands/commit.js +210 -0
- package/bin/commands/init.js +244 -0
- package/bin/commands/sentinel.js +160 -0
- package/bin/commands/status.js +117 -0
- package/bin/init.js +9 -1
- 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/modules/council/deliberate.ts +15 -5
- package/modules/council/types.ts +1 -1
- package/package.json +2 -2
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,11 +163,11 @@ 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
|
|
|
68
|
-
### Every merged PR
|
|
166
|
+
### Every merged PR can create a Chronicle proposal automatically
|
|
69
167
|
|
|
70
|
-
|
|
168
|
+
Quorum ships a GitHub Actions workflow (`chronicle-on-merge.yml`) that 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.
|
|
71
169
|
|
|
72
|
-
|
|
170
|
+
To enable this in your project, copy `.github/workflows/chronicle-on-merge.yml` and `scripts/chronicle-pr.js` from the [Quorum repo](https://github.com/balpal4495/Quorum) into your own repo.
|
|
73
171
|
|
|
74
172
|
---
|
|
75
173
|
|
|
@@ -253,9 +351,14 @@ 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
|
-
|
|
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
|
+
To get a coverage comment on every PR, copy `.github/workflows/sentinel-pr.yml` from the [Quorum repo](https://github.com/balpal4495/Quorum) into your project. Every PR then 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
|
|
|
260
363
|
---
|
|
261
364
|
|
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.
|
|
@@ -176,6 +180,16 @@ It must be called once before any `oracle.query()`, `evaluate()`, or `deliberate
|
|
|
176
180
|
|
|
177
181
|
If no entry point exists yet, note that `setup()` must be called before first use — do not inline it.
|
|
178
182
|
|
|
183
|
+
**Approving Chronicle proposals:** after an agent calls `oracle.propose()`, approve and index the entry from the terminal:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
quorum commit --list # see pending proposals
|
|
187
|
+
quorum commit <id> # approve and index a proposal
|
|
188
|
+
quorum commit <id> --dry-run # preview without writing
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Requires `@xenova/transformers` and `vectordb` (both added in Step 3).
|
|
192
|
+
|
|
179
193
|
---
|
|
180
194
|
|
|
181
195
|
## Step 7 — Verify Chronicle is created
|
|
@@ -260,3 +274,17 @@ Key reminders:
|
|
|
260
274
|
- **Query Oracle before proposing anything.** `oracle.query("what you're about to do")` first.
|
|
261
275
|
- **Never call `oracle.commit()` autonomously.** Only `oracle.propose()`. A human commits.
|
|
262
276
|
- **Chronicle entries are ground truth.** Respect `refuted` entries — do not retry what has already failed.
|
|
277
|
+
|
|
278
|
+
### CLI quick reference
|
|
279
|
+
|
|
280
|
+
These commands are available globally after `npm install -g @balpal4495/quorum`:
|
|
281
|
+
|
|
282
|
+
| Command | What it does |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `quorum status` | Chronicle health — pending proposals, committed entries |
|
|
285
|
+
| `quorum check --outcome X --design Y` | Preflight + risk classifier (no LLM) |
|
|
286
|
+
| `quorum commit --list` | List pending proposals |
|
|
287
|
+
| `quorum commit <id>` | Approve and index a proposal |
|
|
288
|
+
| `quorum sentinel coverage [--path <dir>]` | Chronicle coverage of source files |
|
|
289
|
+
|
|
290
|
+
`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
|
+
}
|