@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 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/
@@ -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 creates a Chronicle proposal automatically
166
+ ### Every merged PR can create a Chronicle proposal automatically
69
167
 
70
- 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.
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
- This means the gap between "PR merged" and "Chronicle knows about it" is now zero.
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
- **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
- 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.
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
- - `quorum/.github/copilot-instructions.md` — workflow rules for AI agents
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
- **If it does not exist:**
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 it already exists:**
87
- Append the entire contents of `quorum/.github/copilot-instructions.md` to the existing file, preceded by this separator:
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
- <!-- Quorum: appended by setup -->
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
+ }