@balpal4495/quorum 0.1.9 → 0.2.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 +108 -237
- package/modules/council/advisors.ts +4 -1
- package/modules/council/chairman.ts +2 -1
- package/modules/council/deliberate.ts +5 -0
- package/modules/council/reviewers.ts +2 -1
- package/modules/jury/evaluate.ts +3 -2
- package/modules/oracle/propose.ts +19 -3
- package/modules/oracle/query.ts +3 -2
- package/modules/oracle/summary.ts +2 -1
- package/modules/sentinel/drift.ts +7 -3
- package/modules/sentinel/review.ts +2 -1
- package/modules/setup.ts +2 -1
- package/modules/shared/types.ts +39 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,327 +1,198 @@
|
|
|
1
1
|
# Quorum
|
|
2
2
|
|
|
3
|
-
**Quorum gives AI
|
|
3
|
+
**Quorum gives your AI coding assistant memory and judgment.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
When Claude Code, Copilot, or Cursor works in your codebase, it forgets everything between sessions. It retries approaches that already failed. It contradicts decisions made last week. It has no idea what the team has already learned.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npx @balpal4495/quorum@latest init
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
That's it. Quorum copies itself into your project, merges instruction files for your AI, and creates the knowledge store directory. Run `npm install` and you're ready.
|
|
7
|
+
Quorum fixes this. It installs a persistent knowledge store into your project and gives your AI a structured workflow for querying it before proposing solutions, validating designs before acting, and writing new knowledge back — with you approving every write.
|
|
12
8
|
|
|
13
9
|
---
|
|
14
10
|
|
|
15
|
-
##
|
|
11
|
+
## Get started in one command
|
|
12
|
+
|
|
13
|
+
Run this from your project root:
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx @balpal4495/quorum@latest init
|
|
17
|
+
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Then run `npm install`.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|---|---|
|
|
23
|
-
| **Oracle** | Stores and retrieves project knowledge — decisions, investigations, outcomes |
|
|
24
|
-
| **Jury** | Scores a proposed design against that knowledge — gives you confidence before acting |
|
|
25
|
-
| **Council** | A panel of advisors challenges the design and a Chairman gives a final verdict |
|
|
26
|
-
| **Sentinel** | Shows you which parts of the codebase the AI knows nothing about — and flags stale knowledge |
|
|
21
|
+
That's the whole setup. Quorum copies its modules into `quorum/`, merges instruction files for your AI (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`), and creates the Chronicle knowledge store at `.chronicle/`.
|
|
27
22
|
|
|
28
23
|
---
|
|
29
24
|
|
|
30
|
-
##
|
|
25
|
+
## Then just talk to your AI
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
Once initialized, open your AI in agent mode and tell it:
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
oracle.query() → jury.evaluate() → council.deliberate() → human gate → Executor
|
|
36
|
-
```
|
|
29
|
+
> "Follow quorum/SETUP.md"
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
sequenceDiagram
|
|
46
|
-
participant Agent as AI Agent
|
|
47
|
-
participant Oracle
|
|
48
|
-
participant Jury
|
|
49
|
-
participant Council
|
|
50
|
-
participant Human
|
|
51
|
-
participant Chronicle
|
|
52
|
-
|
|
53
|
-
Agent->>Oracle: query(text)
|
|
54
|
-
Oracle->>Chronicle: vector + BM25 search
|
|
55
|
-
Chronicle-->>Agent: ranked evidence
|
|
56
|
-
|
|
57
|
-
Agent->>Jury: evaluate(design, evidence)
|
|
58
|
-
Jury-->>Agent: confidence score + gaps
|
|
59
|
-
|
|
60
|
-
Agent->>Council: deliberate(design, evidence, jury_output)
|
|
61
|
-
Council-->>Agent: verdict + proposal
|
|
62
|
-
|
|
63
|
-
alt Council satisfied
|
|
64
|
-
Agent->>Human: surface verdict for approval
|
|
65
|
-
Human->>Oracle: commit(proposalId)
|
|
66
|
-
Oracle->>Chronicle: index entry
|
|
67
|
-
else not satisfied
|
|
68
|
-
Note over Agent: revise and retry
|
|
69
|
-
end
|
|
70
|
-
```
|
|
31
|
+
Your AI reads the instruction files, wires the modules into your project's entry point, runs the tests, and reports what it did. From that point it operates under Quorum — querying Chronicle before every proposal, running designs through Jury and Council, and staging entries for your approval.
|
|
32
|
+
|
|
33
|
+
**Works with:**
|
|
34
|
+
- Claude Code (`claude` CLI or VS Code extension)
|
|
35
|
+
- GitHub Copilot (agent mode)
|
|
36
|
+
- Cursor
|
|
37
|
+
- Any other AI that can read files and run terminal commands
|
|
71
38
|
|
|
72
39
|
---
|
|
73
40
|
|
|
74
|
-
##
|
|
41
|
+
## What changes after setup
|
|
75
42
|
|
|
76
|
-
###
|
|
43
|
+
### Your AI now has a memory
|
|
77
44
|
|
|
78
|
-
|
|
45
|
+
Before proposing anything, your AI queries Chronicle — the project's knowledge store. If a similar approach was tried and rejected, it knows. If a design decision was made last month, it knows.
|
|
79
46
|
|
|
80
|
-
|
|
81
|
-
[abc-123] Tried symmetric JWT (HS256) in March. Rejected — no way to rotate keys
|
|
82
|
-
without invalidating all active sessions. Use RS256 with short-lived tokens.
|
|
83
|
-
confidence: 0.91 · status: committed
|
|
84
|
-
```
|
|
47
|
+
> *"I queried Chronicle before proposing the Redis session approach. Entry `[abc-123]` shows we rejected this in March — key rotation wasn't viable. I'm proposing JWT with RS256 instead."*
|
|
85
48
|
|
|
86
|
-
|
|
49
|
+
### Your AI validates designs before acting
|
|
87
50
|
|
|
88
|
-
|
|
51
|
+
Every proposal goes through Jury (confidence scoring against evidence) and Council (adversarial panel review) before it reaches you. Low-confidence or contested ideas get challenged internally first.
|
|
89
52
|
|
|
90
|
-
|
|
53
|
+
> *"Jury scored this 0.41 — gaps in lock strategy and rollback plan. Council flagged the same issue. I've revised the migration plan to use a shadow column approach before bringing it to you."*
|
|
91
54
|
|
|
92
|
-
|
|
55
|
+
### You approve what gets remembered
|
|
56
|
+
|
|
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.
|
|
93
58
|
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
design: "ALTER TABLE, backfill with default false, then add constraint",
|
|
100
|
-
evidence,
|
|
101
|
-
})
|
|
102
|
-
// jury.confidence: 0.41 — gaps: ["no lock strategy", "no rollback plan"]
|
|
103
|
-
|
|
104
|
-
const verdict = await deliberate({
|
|
105
|
-
outcome: "Add NOT NULL column users.verified",
|
|
106
|
-
design: "ALTER TABLE, backfill with default false, then add constraint",
|
|
107
|
-
evidence,
|
|
108
|
-
jury_output: jury,
|
|
109
|
-
})
|
|
110
|
-
// verdict.satisfied: false
|
|
111
|
-
// verdict.verdict: "No lock strategy specified. On a table this size, a naive ALTER TABLE
|
|
112
|
-
// will take an exclusive lock for minutes. Use a shadow column pattern
|
|
113
|
-
// or pg_repack."
|
|
59
|
+
```
|
|
60
|
+
.chronicle/
|
|
61
|
+
proposals/ ← AI-staged entries waiting for your approval
|
|
62
|
+
committed/ ← approved entries, indexed and searchable
|
|
63
|
+
SUMMARY.md ← auto-generated weekly context for your AI to read
|
|
114
64
|
```
|
|
115
65
|
|
|
116
|
-
|
|
66
|
+
Commit `.chronicle/committed/` to git. Future sessions — and your teammates' sessions — start with that context.
|
|
117
67
|
|
|
118
68
|
---
|
|
119
69
|
|
|
120
|
-
|
|
70
|
+
## Real examples
|
|
121
71
|
|
|
122
|
-
|
|
72
|
+
### An agent that remembers a past failure
|
|
73
|
+
|
|
74
|
+
Your AI is about to propose symmetric JWT signing. Oracle returns:
|
|
123
75
|
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
76
|
+
```
|
|
77
|
+
[abc-123] Tried HS256 JWT in March. Rejected — no way to rotate keys without
|
|
78
|
+
invalidating all active sessions. Decision: RS256 with short-lived tokens.
|
|
79
|
+
status: committed · confidence: 0.91
|
|
128
80
|
```
|
|
129
81
|
|
|
130
|
-
|
|
82
|
+
Jury flags it as a direct conflict. The agent revises before Council even sees it.
|
|
131
83
|
|
|
132
84
|
---
|
|
133
85
|
|
|
134
|
-
|
|
86
|
+
### Onboarding a new session to an established project
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
import { setup } from "./quorum/modules/setup"
|
|
88
|
+
Day one of a new Claude Code session. Before touching anything:
|
|
138
89
|
|
|
139
|
-
const { oracle, evaluate, deliberate } = await setup({
|
|
140
|
-
llm: myLLMProvider, // any function that calls your LLM — see wiring below
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// Query what Chronicle knows
|
|
144
|
-
const evidence = await oracle.query("authentication patterns in this codebase")
|
|
145
|
-
|
|
146
|
-
// Evaluate a proposed design
|
|
147
|
-
const jury = await evaluate({
|
|
148
|
-
outcome: "Add OAuth2 login via GitHub",
|
|
149
|
-
design: "Use passport-github2, store sessions in Redis, 1-hour TTL",
|
|
150
|
-
evidence,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Get a Council verdict
|
|
154
|
-
const verdict = await deliberate({
|
|
155
|
-
outcome: "Add OAuth2 login via GitHub",
|
|
156
|
-
design: "Use passport-github2, store sessions in Redis, 1-hour TTL",
|
|
157
|
-
evidence,
|
|
158
|
-
jury_output: jury,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
if (verdict.satisfied) {
|
|
162
|
-
// → surface verdict.proposal to a human for approval
|
|
163
|
-
// → human calls oracle.commit(proposalId) to index it
|
|
164
|
-
// → Executor proceeds
|
|
165
|
-
} else {
|
|
166
|
-
// verdict.verdict contains the specific objection
|
|
167
|
-
// verdict.recommendation is "redesign" or "investigate-more"
|
|
168
|
-
}
|
|
169
90
|
```
|
|
91
|
+
> query Chronicle for: authentication, session handling, token strategy
|
|
170
92
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import type { LLMProvider } from "./quorum/modules/shared/types"
|
|
93
|
+
6 entries found:
|
|
94
|
+
- HS256 rejected (key rotation problem) → use RS256
|
|
95
|
+
- Redis sessions tried and removed (memory overhead at scale)
|
|
96
|
+
- Current approach: RS256 JWT, 15-min expiry, refresh rotation in httpOnly cookies
|
|
97
|
+
- Upcoming: OAuth migration planned for Q3
|
|
179
98
|
```
|
|
180
99
|
|
|
181
|
-
|
|
182
|
-
// Anthropic
|
|
183
|
-
const llm: LLMProvider = async (messages, model = "claude-3-5-sonnet-20241022") => {
|
|
184
|
-
const system = messages.find(m => m.role === "system")?.content ?? ""
|
|
185
|
-
const user = messages.filter(m => m.role !== "system")
|
|
186
|
-
const res = await anthropic.messages.create({ model, system, messages: user, max_tokens: 2048 })
|
|
187
|
-
return res.content[0].type === "text" ? res.content[0].text : ""
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// OpenAI
|
|
191
|
-
const llm: LLMProvider = async (messages, model = "gpt-4o") => {
|
|
192
|
-
const res = await openai.chat.completions.create({ model, messages })
|
|
193
|
-
return res.choices[0].message.content ?? ""
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Per-step model overrides (optional)
|
|
197
|
-
const { oracle, evaluate, deliberate } = await setup({
|
|
198
|
-
llm,
|
|
199
|
-
models: {
|
|
200
|
-
jury: "gpt-4o-mini",
|
|
201
|
-
council: {
|
|
202
|
-
frame: "gpt-4o-mini",
|
|
203
|
-
advisors: "gpt-4o-mini",
|
|
204
|
-
reviewers: "gpt-4o",
|
|
205
|
-
chairman: "gpt-4o",
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
})
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Oracle requires no LLM — only Jury, Council, and Sentinel drift checks need one.
|
|
100
|
+
The AI works with full project context from the first message — no archaeology through git history.
|
|
212
101
|
|
|
213
102
|
---
|
|
214
103
|
|
|
215
|
-
|
|
104
|
+
### Validating a risky database change
|
|
216
105
|
|
|
217
|
-
|
|
106
|
+
An agent proposes adding a `NOT NULL` column to a 50M-row table. Jury returns:
|
|
218
107
|
|
|
219
108
|
```
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
SUMMARY.md ← auto-generated weekly context, rebuilt on every commit
|
|
109
|
+
confidence: 0.41
|
|
110
|
+
gaps: ["no lock strategy documented", "no rollback plan"]
|
|
111
|
+
council_brief: challenge
|
|
224
112
|
```
|
|
225
113
|
|
|
226
|
-
|
|
114
|
+
Council's Chairman gives a verdict:
|
|
227
115
|
|
|
228
116
|
```
|
|
229
|
-
|
|
230
|
-
|
|
117
|
+
satisfied: false
|
|
118
|
+
verdict: "On a table this size, a naive ALTER TABLE takes an exclusive lock for minutes.
|
|
119
|
+
Specify a shadow column pattern or pg_repack. No rollback plan documented."
|
|
231
120
|
```
|
|
232
121
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
There are no auto-commits. Ever.
|
|
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.
|
|
236
123
|
|
|
237
124
|
---
|
|
238
125
|
|
|
239
|
-
##
|
|
126
|
+
## What's inside
|
|
240
127
|
|
|
241
|
-
|
|
128
|
+
Four portable TypeScript modules installed into `quorum/modules/`:
|
|
242
129
|
|
|
243
|
-
|
|
130
|
+
| Module | What it does |
|
|
131
|
+
|---|---|
|
|
132
|
+
| **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 final verdict. |
|
|
135
|
+
| **Sentinel** | Shows which files the AI knows nothing about, flags stale knowledge, and posts a coverage map on every PR. |
|
|
244
136
|
|
|
245
|
-
|
|
246
|
-
import { coverage } from "./quorum/modules/sentinel"
|
|
137
|
+
The modules live in your repo — readable by any AI working in the codebase. Nothing is hidden in `node_modules`.
|
|
247
138
|
|
|
248
|
-
|
|
249
|
-
// report.percentage — 34%
|
|
250
|
-
// report.uncoveredFiles — ["src/auth/session.ts", "src/payments/stripe.ts", ...]
|
|
251
|
-
```
|
|
139
|
+
---
|
|
252
140
|
|
|
253
|
-
|
|
141
|
+
## Sentinel — coverage and drift
|
|
254
142
|
|
|
255
|
-
|
|
256
|
-
import { detectDrift } from "./quorum/modules/sentinel"
|
|
143
|
+
Sentinel surfaces two things Chronicle can't tell you about itself.
|
|
257
144
|
|
|
258
|
-
|
|
259
|
-
// report.flags — entries where the key_insight may no longer match the code
|
|
260
|
-
```
|
|
145
|
+
**Coverage** — which parts of your codebase has the AI never documented?
|
|
261
146
|
|
|
262
|
-
|
|
147
|
+
**Drift** — do existing Chronicle entries still accurately describe the code, or have they gone stale?
|
|
263
148
|
|
|
264
|
-
|
|
265
|
-
import { describe } from "vitest"
|
|
266
|
-
import { sentinelAssertions } from "./quorum/modules/sentinel"
|
|
267
|
-
|
|
268
|
-
describe("sentinel", () => {
|
|
269
|
-
sentinelAssertions({
|
|
270
|
-
chronicleDir: ".chronicle",
|
|
271
|
-
codebasePath: "src",
|
|
272
|
-
llm: myLLMProvider, // omit to skip drift tests
|
|
273
|
-
minCoveragePercent: 50, // 0 = advisory only (default — safe for new projects)
|
|
274
|
-
}).forEach(a => a())
|
|
275
|
-
})
|
|
276
|
-
```
|
|
149
|
+
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.
|
|
277
150
|
|
|
278
|
-
|
|
151
|
+
---
|
|
279
152
|
|
|
280
|
-
|
|
153
|
+
## For custom agent pipelines
|
|
281
154
|
|
|
282
|
-
|
|
155
|
+
If you're building your own agent workflow programmatically, the modules expose a clean TypeScript API. Wire your LLM provider and call directly:
|
|
283
156
|
|
|
284
|
-
|
|
157
|
+
```typescript
|
|
158
|
+
import { setup } from "./quorum/modules/setup"
|
|
285
159
|
|
|
286
|
-
|
|
287
|
-
|---|---|---|
|
|
288
|
-
| Oracle | No | `oracle.query()` / `oracle.propose()` / `oracle.commit()` |
|
|
289
|
-
| Jury | Yes | `evaluate(input, deps)` |
|
|
290
|
-
| Council | Yes | `deliberate(input, deps)` |
|
|
291
|
-
| Sentinel | Optional | `coverage()` / `detectDrift()` / `sentinelAssertions()` |
|
|
160
|
+
const { oracle, evaluate, deliberate } = await setup({ llm: myLLMProvider })
|
|
292
161
|
|
|
293
|
-
|
|
294
|
-
|
|
162
|
+
const evidence = await oracle.query("authentication patterns")
|
|
163
|
+
const jury = await evaluate({ outcome, design, evidence })
|
|
164
|
+
const verdict = await deliberate({ outcome, design, evidence, jury_output: jury })
|
|
165
|
+
```
|
|
295
166
|
|
|
296
|
-
|
|
167
|
+
The `LLMProvider` type is a simple function — wire OpenAI, Anthropic, or anything else:
|
|
297
168
|
|
|
298
|
-
|
|
169
|
+
```typescript
|
|
170
|
+
// Anthropic
|
|
171
|
+
const llm = async (messages, model = "claude-3-5-sonnet-20241022") => {
|
|
172
|
+
const res = await anthropic.messages.create({ model, messages, max_tokens: 2048 })
|
|
173
|
+
return res.content[0].type === "text" ? res.content[0].text : ""
|
|
174
|
+
}
|
|
299
175
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
176
|
+
// OpenAI
|
|
177
|
+
const llm = async (messages, model = "gpt-4o") => {
|
|
178
|
+
const res = await openai.chat.completions.create({ model, messages })
|
|
179
|
+
return res.choices[0].message.content ?? ""
|
|
180
|
+
}
|
|
181
|
+
```
|
|
305
182
|
|
|
306
|
-
|
|
183
|
+
Full API reference: [modules/README.md](modules/README.md)
|
|
307
184
|
|
|
308
185
|
---
|
|
309
186
|
|
|
310
187
|
## Releases
|
|
311
188
|
|
|
312
|
-
Quorum is published
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
git tag v0.2.0 && git push origin v0.2.0
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
GitHub Actions publishes to npm automatically via OIDC Trusted Publishing — no stored tokens.
|
|
189
|
+
Quorum is published as `@balpal4495/quorum`. New versions release automatically when a semver tag is pushed — via GitHub Actions and OIDC Trusted Publishing, no stored tokens.
|
|
319
190
|
|
|
320
191
|
---
|
|
321
192
|
|
|
322
193
|
## Docs
|
|
323
194
|
|
|
324
|
-
- [
|
|
325
|
-
- [modules/
|
|
195
|
+
- [SETUP.md](SETUP.md) — full bootstrap sequence (the file you point your AI at)
|
|
196
|
+
- [modules/README.md](modules/README.md) — TypeScript API reference
|
|
197
|
+
- [modules/AGENTS.md](modules/AGENTS.md) — file ownership map
|
|
326
198
|
- [modules/CLAUDE.md](modules/CLAUDE.md) — design decisions and invariants
|
|
327
|
-
- [SETUP.md](SETUP.md) — manual bootstrap sequence (for AI-assisted setup)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { AdvisorPersona } from "./personas"
|
|
3
4
|
|
|
4
5
|
export interface AdvisorResponse {
|
|
@@ -12,7 +13,9 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
12
13
|
}
|
|
13
14
|
return evidence
|
|
14
15
|
.map(e =>
|
|
15
|
-
`[${e.id}] (${e.status})
|
|
16
|
+
`[${e.id}] (${e.status})
|
|
17
|
+
${entryText(e)}
|
|
18
|
+
Areas: ${e.affected_areas.join(", ")}${e.scope ? " | " + e.scope.join(", ") : ""}`,
|
|
16
19
|
)
|
|
17
20
|
.join("\n\n")
|
|
18
21
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
3
|
+
import { entryText } from "../shared/types"
|
|
3
4
|
import type { AdvisorResponse } from "./advisors"
|
|
4
5
|
import type { ReviewerResponse } from "./reviewers"
|
|
5
6
|
import type { CouncilOutput } from "./types"
|
|
@@ -29,7 +30,7 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
29
30
|
return evidence
|
|
30
31
|
.map(
|
|
31
32
|
e =>
|
|
32
|
-
`[${e.id}] (${e.status}, confidence: ${e.confidence.toFixed(2)}) ${e
|
|
33
|
+
`[${e.id}] (${e.status}, confidence: ${e.confidence.toFixed(2)}) ${entryText(e)}`,
|
|
33
34
|
)
|
|
34
35
|
.join("\n")
|
|
35
36
|
}
|
|
@@ -82,8 +82,13 @@ export async function deliberate(
|
|
|
82
82
|
.slice(0, 200)
|
|
83
83
|
|
|
84
84
|
await oracle.propose({
|
|
85
|
+
schema_version: 2,
|
|
86
|
+
topic: input.outcome.slice(0, 80),
|
|
87
|
+
decision: keyInsight,
|
|
85
88
|
key_insight: keyInsight,
|
|
86
89
|
affected_areas: extractAffectedAreas(input.outcome, input.design),
|
|
90
|
+
alternatives_considered: verdict.challenges,
|
|
91
|
+
rejected_reason: verdict.satisfied ? [] : [verdict.verdict.slice(0, 200)],
|
|
87
92
|
status: "open",
|
|
88
93
|
confidence: input.jury_output.confidence,
|
|
89
94
|
source_module: "council",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LLMProvider, OracleResult } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { AdvisorResponse } from "./advisors"
|
|
3
4
|
|
|
4
5
|
export interface ReviewerResponse {
|
|
@@ -20,7 +21,7 @@ function anonymise(responses: AdvisorResponse[]): string {
|
|
|
20
21
|
function formatEvidenceSummary(evidence: OracleResult[]): string {
|
|
21
22
|
if (evidence.length === 0) return "No Oracle evidence available."
|
|
22
23
|
return evidence
|
|
23
|
-
.map(e => `[${e.id}] (${e.status}) ${e
|
|
24
|
+
.map(e => `[${e.id}] (${e.status}) ${entryText(e)}`)
|
|
24
25
|
.join("\n")
|
|
25
26
|
}
|
|
26
27
|
|
package/modules/jury/evaluate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JuryInput, JuryOutput, JuryDeps } from "./types"
|
|
2
2
|
import type { OracleResult } from "../shared/types"
|
|
3
|
+
import { entryText } from "../shared/types"
|
|
3
4
|
import { JuryOutputSchema } from "./schema"
|
|
4
5
|
|
|
5
6
|
const CONFIDENCE_THRESHOLD = 0.6
|
|
@@ -12,8 +13,8 @@ function formatEvidence(evidence: OracleResult[]): string {
|
|
|
12
13
|
.map(e =>
|
|
13
14
|
[
|
|
14
15
|
`[${e.id}] status=${e.status} confidence=${e.confidence.toFixed(2)} score=${e.score.toFixed(3)}`,
|
|
15
|
-
`Insight: ${e
|
|
16
|
-
`Areas: ${e.affected_areas.join(", ")}`,
|
|
16
|
+
`Insight: ${entryText(e)}`,
|
|
17
|
+
`Areas: ${e.affected_areas.join(", ")}${e.scope ? " | " + e.scope.join(", ") : ""}`,
|
|
17
18
|
e.outcome ? `Outcome: ${e.outcome}` : null,
|
|
18
19
|
]
|
|
19
20
|
.filter(Boolean)
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from "crypto"
|
|
|
4
4
|
import { exec } from "child_process"
|
|
5
5
|
import { promisify } from "util"
|
|
6
6
|
import type { ChronicleEntry, SimilarityWarning } from "../shared/types"
|
|
7
|
+
import { entryText } from "../shared/types"
|
|
7
8
|
import type { OracleDeps } from "./types"
|
|
8
9
|
import { updateSummary } from "./summary"
|
|
9
10
|
|
|
@@ -27,6 +28,21 @@ function validateEntry(entry: Omit<ChronicleEntry, "id" | "timestamp">): void {
|
|
|
27
28
|
`Distil to a single clear sentence.`,
|
|
28
29
|
)
|
|
29
30
|
}
|
|
31
|
+
if (entry.decision !== undefined) {
|
|
32
|
+
const d = entry.decision.trim()
|
|
33
|
+
if (d.length < INSIGHT_MIN_LENGTH) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`decision too short (${d.length} chars, min ${INSIGHT_MIN_LENGTH}). ` +
|
|
36
|
+
`Write a specific, complete sentence describing the decision.`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
if (d.length > INSIGHT_MAX_LENGTH) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`decision too long (${d.length} chars, max ${INSIGHT_MAX_LENGTH}). ` +
|
|
42
|
+
`Distil to a single clear sentence.`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
if (!entry.affected_areas || entry.affected_areas.filter(a => a.trim()).length === 0) {
|
|
31
47
|
throw new Error(`affected_areas must contain at least one non-empty entry.`)
|
|
32
48
|
}
|
|
@@ -40,7 +56,7 @@ async function checkSimilarity(
|
|
|
40
56
|
deps: OracleDeps,
|
|
41
57
|
): Promise<SimilarityWarning | undefined> {
|
|
42
58
|
try {
|
|
43
|
-
const text = [entry.
|
|
59
|
+
const text = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
44
60
|
const vector = await deps.embedder(text)
|
|
45
61
|
const results = await deps.vectorStore.search(vector, 3)
|
|
46
62
|
if (results.length === 0) return undefined
|
|
@@ -116,8 +132,8 @@ export async function commit(
|
|
|
116
132
|
timestamp: new Date().toISOString(),
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
// Embed the
|
|
120
|
-
const embeddingText = [entry.
|
|
135
|
+
// Embed the primary text + areas + scope tags for richer retrieval
|
|
136
|
+
const embeddingText = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
121
137
|
const vector = await deps.embedder(embeddingText)
|
|
122
138
|
await deps.vectorStore.upsert(entry.id, vector, entry)
|
|
123
139
|
|
package/modules/oracle/query.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChronicleEntry, OracleResult, QueryOptions } from "../shared/types"
|
|
2
|
+
import { entryText } from "../shared/types"
|
|
2
3
|
import type { OracleDeps } from "./types"
|
|
3
4
|
import { bm25Score, extractDomainTerms } from "./bm25"
|
|
4
5
|
import { appendQueryLog } from "./log"
|
|
@@ -62,13 +63,13 @@ export async function query(
|
|
|
62
63
|
// ── Pass 2: BM25 re-ranking with query enrichment ─────────────────────────
|
|
63
64
|
const topInsights = candidates
|
|
64
65
|
.slice(0, Math.min(5, candidates.length))
|
|
65
|
-
.map(c => c.entry
|
|
66
|
+
.map(c => entryText(c.entry))
|
|
66
67
|
const domainTerms = extractDomainTerms(topInsights)
|
|
67
68
|
const enrichedQuery =
|
|
68
69
|
domainTerms.length > 0 ? `${text} ${domainTerms.join(" ")}` : text
|
|
69
70
|
|
|
70
71
|
const documents = candidates.map(c =>
|
|
71
|
-
[c.entry.
|
|
72
|
+
[entryText(c.entry), ...c.entry.affected_areas, ...(c.entry.scope ?? [])].join(" "),
|
|
72
73
|
)
|
|
73
74
|
const bm25Scores = bm25Score(enrichedQuery, documents)
|
|
74
75
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
|
|
5
6
|
const SUMMARY_WEEKS = 12
|
|
6
7
|
const DIRECTIVE =
|
|
@@ -29,7 +30,7 @@ function workRefLabel(entry: ChronicleEntry): string {
|
|
|
29
30
|
function renderEntry(entry: ChronicleEntry): string {
|
|
30
31
|
const areas = entry.affected_areas.join(", ")
|
|
31
32
|
const id = entry.id.slice(0, 8)
|
|
32
|
-
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entry
|
|
33
|
+
return `- **[${id}]** ${areas} — \`${entry.status}\` (${entry.confidence.toFixed(2)}) — ${entryText(entry)}`
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry, DriftFlag, DriftReport, LLMProvider } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
|
|
5
6
|
const FILE_CONTENT_LIMIT = 3000
|
|
6
7
|
|
|
@@ -73,7 +74,10 @@ async function evaluateDrift(
|
|
|
73
74
|
{
|
|
74
75
|
role: "user",
|
|
75
76
|
content:
|
|
76
|
-
`Documented insight
|
|
77
|
+
`Documented insight:
|
|
78
|
+
"${entryText(entry)}"
|
|
79
|
+
|
|
80
|
+
` +
|
|
77
81
|
`Current source:\n${fileSection}\n\n` +
|
|
78
82
|
`Does this insight still accurately describe the code above?\n` +
|
|
79
83
|
`{"stillValid": boolean, "confidence": number, "reasoning": "one sentence"}`,
|
|
@@ -86,7 +90,7 @@ async function evaluateDrift(
|
|
|
86
90
|
const parsed = JSON.parse(match[0]) as { stillValid?: unknown; confidence?: unknown; reasoning?: unknown }
|
|
87
91
|
return {
|
|
88
92
|
entryId: entry.id,
|
|
89
|
-
keyInsight: entry
|
|
93
|
+
keyInsight: entryText(entry),
|
|
90
94
|
affectedFiles: files.map(f => f.filePath),
|
|
91
95
|
stillValid: Boolean(parsed.stillValid),
|
|
92
96
|
confidence: typeof parsed.confidence === "number" ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5,
|
|
@@ -96,7 +100,7 @@ async function evaluateDrift(
|
|
|
96
100
|
// Parse failure → conservative: flag for human review
|
|
97
101
|
return {
|
|
98
102
|
entryId: entry.id,
|
|
99
|
-
keyInsight: entry
|
|
103
|
+
keyInsight: entryText(entry),
|
|
100
104
|
affectedFiles: files.map(f => f.filePath),
|
|
101
105
|
stillValid: false,
|
|
102
106
|
confidence: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import type { ChronicleEntry } from "../shared/types"
|
|
4
|
+
import { entryText } from "../shared/types"
|
|
4
5
|
import { coverage as runCoverage } from "./coverage"
|
|
5
6
|
|
|
6
7
|
function extractModule(filePath: string): string {
|
|
@@ -192,7 +193,7 @@ export async function reviewContext(
|
|
|
192
193
|
lines.push(`**${stat.name}/**`)
|
|
193
194
|
const relevant = allEntries.filter(e => stat.entryIds.includes(e.id))
|
|
194
195
|
for (const entry of relevant) {
|
|
195
|
-
lines.push(`- \`[${entry.id.slice(0, 8)}]\` ${entry
|
|
196
|
+
lines.push(`- \`[${entry.id.slice(0, 8)}]\` ${entryText(entry)}`)
|
|
196
197
|
lines.push(` *${entry.status} — confidence ${entry.confidence.toFixed(2)}*`)
|
|
197
198
|
}
|
|
198
199
|
lines.push("")
|
package/modules/setup.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createLanceDBStore } from "./oracle/adapters/lance-db"
|
|
|
6
6
|
import { evaluate } from "./jury/evaluate"
|
|
7
7
|
import { deliberate } from "./council/deliberate"
|
|
8
8
|
import type { LLMProvider, OracleClient } from "./shared/types"
|
|
9
|
+
import { entryText } from "./shared/types"
|
|
9
10
|
import type { JuryInput, JuryOutput, JuryDeps } from "./jury/types"
|
|
10
11
|
import type { CouncilInput, CouncilOutput, CouncilDeps, CouncilModels } from "./council/types"
|
|
11
12
|
|
|
@@ -124,7 +125,7 @@ export async function setup(options: SetupOptions): Promise<Modules> {
|
|
|
124
125
|
for (const file of missing) {
|
|
125
126
|
const raw = await fs.readFile(path.join(committedDir, file), "utf8")
|
|
126
127
|
const entry = JSON.parse(raw) as import("./shared/types").ChronicleEntry
|
|
127
|
-
const embeddingText = [entry.
|
|
128
|
+
const embeddingText = [entryText(entry), ...entry.affected_areas, ...(entry.scope ?? [])].join(" ")
|
|
128
129
|
const vector = await embedder(embeddingText)
|
|
129
130
|
await vectorStore.upsert(entry.id, vector, entry)
|
|
130
131
|
}
|
package/modules/shared/types.ts
CHANGED
|
@@ -29,12 +29,19 @@ export type WorkRef = {
|
|
|
29
29
|
/**
|
|
30
30
|
* A durable knowledge record stored in Chronicle.
|
|
31
31
|
* This is the canonical unit of institutional memory.
|
|
32
|
+
*
|
|
33
|
+
* Schema versions:
|
|
34
|
+
* v1 (no schema_version field): key_insight is the primary text field.
|
|
35
|
+
* v2 (schema_version: 2): decision is the primary text field; key_insight is a copy
|
|
36
|
+
* of decision written for backwards compatibility. Always use entryText() to read.
|
|
32
37
|
*/
|
|
33
38
|
export type ChronicleEntry = {
|
|
34
39
|
id: string
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
// ── v1 fields (required — always present) ───────────────────────────────
|
|
42
|
+
/** The core finding or decision, in one clear sentence. v2: copy of decision. */
|
|
36
43
|
key_insight: string
|
|
37
|
-
/**
|
|
44
|
+
/** File paths or system areas this entry applies to. Used by Sentinel for file matching. */
|
|
38
45
|
affected_areas: string[]
|
|
39
46
|
status: "validated" | "refuted" | "open"
|
|
40
47
|
/** 0–1. How strongly this was confirmed at write time. */
|
|
@@ -48,6 +55,36 @@ export type ChronicleEntry = {
|
|
|
48
55
|
/** The unit of work that triggered this entry. Used to build SUMMARY.md temporal context. */
|
|
49
56
|
work_ref?: WorkRef
|
|
50
57
|
timestamp: string
|
|
58
|
+
|
|
59
|
+
// ── v2 fields (optional — absent on legacy entries) ──────────────────────
|
|
60
|
+
/** 2 = decision record format. Absent = v1 legacy entry. */
|
|
61
|
+
schema_version?: 2
|
|
62
|
+
/** Short label for this decision. e.g. "auth/session strategy" */
|
|
63
|
+
topic?: string
|
|
64
|
+
/** The decision itself — the primary text field in v2. Use entryText() to read. */
|
|
65
|
+
decision?: string
|
|
66
|
+
/** Domain/category tags. Additive — does NOT replace affected_areas. e.g. ["auth", "sessions"] */
|
|
67
|
+
scope?: string[]
|
|
68
|
+
/** Approaches that were considered but not chosen. */
|
|
69
|
+
alternatives_considered?: string[]
|
|
70
|
+
/** Why the alternatives were rejected. */
|
|
71
|
+
rejected_reason?: string[]
|
|
72
|
+
/** ID of the Chronicle entry this supersedes. */
|
|
73
|
+
supersedes?: string | null
|
|
74
|
+
/** ID of the Chronicle entry that superseded this one. */
|
|
75
|
+
superseded_by?: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the primary text for a Chronicle entry regardless of schema version.
|
|
80
|
+
* v2 entries use decision; v1 entries use key_insight.
|
|
81
|
+
* All callsites that render or embed entry text must use this function.
|
|
82
|
+
*
|
|
83
|
+
* Accepts any object with key_insight and optional decision — works for both
|
|
84
|
+
* full ChronicleEntry and Omit<ChronicleEntry, "id" | "timestamp"> from propose().
|
|
85
|
+
*/
|
|
86
|
+
export function entryText(entry: { key_insight: string; decision?: string }): string {
|
|
87
|
+
return entry.decision ?? entry.key_insight
|
|
51
88
|
}
|
|
52
89
|
|
|
53
90
|
/**
|