@balpal4495/quorum 0.4.0 → 1.0.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.
@@ -0,0 +1,228 @@
1
+ import { spawn, exec } from "child_process"
2
+ import { promisify } from "util"
3
+ import path from "path"
4
+
5
+ const execAsync = promisify(exec)
6
+
7
+ /**
8
+ * Auto-detect an available LLM provider from the environment.
9
+ *
10
+ * Priority:
11
+ * 1. ANTHROPIC_API_KEY → Anthropic Claude
12
+ * 2. OPENAI_API_KEY → OpenAI (or compatible via OPENAI_BASE_URL)
13
+ * 3. GEMINI_API_KEY → Google Gemini (API)
14
+ * 4. OPENAI_BASE_URL (no key) → OpenAI-compatible endpoint (Azure, Groq, Ollama, etc.)
15
+ * 5. OLLAMA_HOST env var → Ollama (explicit host)
16
+ * 6. localhost:11434 probe → Ollama (auto-detect)
17
+ * 7. gemini CLI in PATH → Google Gemini (CLI subprocess)
18
+ *
19
+ * Returns { llm: LLMProvider, name: string } or null.
20
+ */
21
+ export async function detectProvider() {
22
+ if (process.env.ANTHROPIC_API_KEY) {
23
+ return {
24
+ llm: createAnthropicProvider(process.env.ANTHROPIC_API_KEY),
25
+ name: "Anthropic",
26
+ }
27
+ }
28
+
29
+ if (process.env.OPENAI_API_KEY) {
30
+ const base = (process.env.OPENAI_BASE_URL ?? "").replace(/\/$/, "")
31
+ const name = base
32
+ ? `OpenAI-compatible (${new URL(base).hostname})`
33
+ : "OpenAI"
34
+ return {
35
+ llm: createOpenAICompatProvider(process.env.OPENAI_API_KEY, base || "https://api.openai.com/v1"),
36
+ name,
37
+ }
38
+ }
39
+
40
+ if (process.env.GEMINI_API_KEY) {
41
+ return {
42
+ llm: createGeminiProvider(process.env.GEMINI_API_KEY),
43
+ name: "Gemini",
44
+ }
45
+ }
46
+
47
+ if (process.env.OPENAI_BASE_URL) {
48
+ const base = process.env.OPENAI_BASE_URL.replace(/\/$/, "")
49
+ return {
50
+ llm: createOpenAICompatProvider("", base),
51
+ name: `OpenAI-compatible (${new URL(base).hostname})`,
52
+ }
53
+ }
54
+
55
+ const ollamaHost = process.env.OLLAMA_HOST || "http://localhost:11434"
56
+ const ollamaModel = await probeOllama(ollamaHost)
57
+ if (ollamaModel) {
58
+ return {
59
+ llm: createOpenAICompatProvider("", `${ollamaHost}/v1`, ollamaModel),
60
+ name: `Ollama (${ollamaModel})`,
61
+ }
62
+ }
63
+
64
+ const geminiCLI = await probeGeminiCLI()
65
+ if (geminiCLI) {
66
+ return {
67
+ llm: createGeminiCLIProvider(),
68
+ name: "Gemini CLI",
69
+ }
70
+ }
71
+
72
+ return null
73
+ }
74
+
75
+ /** Convenience wrapper — returns the provider function or null. */
76
+ export async function detectLLM() {
77
+ return (await detectProvider())?.llm ?? null
78
+ }
79
+
80
+ /** Convenience wrapper — returns the provider name or null. */
81
+ export async function detectLLMName() {
82
+ return (await detectProvider())?.name ?? null
83
+ }
84
+
85
+ // ── Probe Ollama ───────────────────────────────────────────────────────────────
86
+
87
+ async function probeOllama(host) {
88
+ try {
89
+ const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(1500) })
90
+ if (!res.ok) return null
91
+ const data = await res.json()
92
+ const model = process.env.OLLAMA_MODEL ?? data.models?.[0]?.name
93
+ return model ?? null
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ // ── Provider factories ─────────────────────────────────────────────────────────
100
+
101
+ function createAnthropicProvider(apiKey) {
102
+ return async function llm(messages, model = "claude-3-5-sonnet-20241022") {
103
+ const systemMsg = messages.find(m => m.role === "system")?.content
104
+ const userMessages = messages.filter(m => m.role !== "system")
105
+
106
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
107
+ method: "POST",
108
+ headers: {
109
+ "x-api-key": apiKey,
110
+ "anthropic-version": "2023-06-01",
111
+ "content-type": "application/json",
112
+ },
113
+ body: JSON.stringify({
114
+ model,
115
+ max_tokens: 2048,
116
+ ...(systemMsg ? { system: systemMsg } : {}),
117
+ messages: userMessages,
118
+ }),
119
+ })
120
+
121
+ if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${(await res.text()).slice(0, 200)}`)
122
+ const data = await res.json()
123
+ return data.content?.[0]?.text ?? ""
124
+ }
125
+ }
126
+
127
+ /**
128
+ * OpenAI and OpenAI-compatible endpoints (Azure, Groq, Together, Ollama, etc.).
129
+ * Pass an empty apiKey for endpoints that don't require one.
130
+ * Pass a fixedModel to pin the model (e.g. for Ollama where the model comes from probe).
131
+ */
132
+ function createOpenAICompatProvider(apiKey, baseUrl, fixedModel) {
133
+ return async function llm(messages, model = "gpt-4o") {
134
+ const headers = { "content-type": "application/json" }
135
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
136
+
137
+ const res = await fetch(`${baseUrl}/chat/completions`, {
138
+ method: "POST",
139
+ headers,
140
+ body: JSON.stringify({
141
+ model: fixedModel ?? model,
142
+ messages,
143
+ max_tokens: 2048,
144
+ }),
145
+ })
146
+
147
+ if (!res.ok) throw new Error(`API ${res.status}: ${(await res.text()).slice(0, 200)}`)
148
+ const data = await res.json()
149
+ return data.choices?.[0]?.message?.content ?? ""
150
+ }
151
+ }
152
+
153
+ function createGeminiProvider(apiKey) {
154
+ const defaultModel = process.env.GEMINI_MODEL ?? "gemini-2.0-flash"
155
+
156
+ return async function llm(messages, model = defaultModel) {
157
+ const systemMsg = messages.find(m => m.role === "system")?.content
158
+ const contents = messages
159
+ .filter(m => m.role !== "system")
160
+ .map(m => ({
161
+ role: m.role === "assistant" ? "model" : "user",
162
+ parts: [{ text: m.content }],
163
+ }))
164
+
165
+ const body = {
166
+ contents,
167
+ generationConfig: { maxOutputTokens: 2048 },
168
+ }
169
+ if (systemMsg) body.systemInstruction = { parts: [{ text: systemMsg }] }
170
+
171
+ const res = await fetch(
172
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
173
+ { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) },
174
+ )
175
+
176
+ if (!res.ok) throw new Error(`Gemini API ${res.status}: ${(await res.text()).slice(0, 200)}`)
177
+ const data = await res.json()
178
+ return data.candidates?.[0]?.content?.parts?.[0]?.text ?? ""
179
+ }
180
+ }
181
+
182
+ async function probeGeminiCLI() {
183
+ try {
184
+ await execAsync("which gemini")
185
+ } catch {
186
+ return false
187
+ }
188
+
189
+ // Env vars that indicate Gemini CLI is authenticated
190
+ if (process.env.GOOGLE_GENAI_USE_VERTEXAI || process.env.GOOGLE_APPLICATION_CREDENTIALS) {
191
+ return true
192
+ }
193
+
194
+ // Settings file with a configured auth type
195
+ try {
196
+ const { homedir } = await import("os")
197
+ const { readFile } = await import("fs/promises")
198
+ const raw = await readFile(path.join(homedir(), ".gemini", "settings.json"), "utf8")
199
+ const config = JSON.parse(raw)
200
+ return !!config.selectedAuthType
201
+ } catch {
202
+ return false
203
+ }
204
+ }
205
+
206
+ function createGeminiCLIProvider() {
207
+ return function llm(messages) {
208
+ return new Promise((resolve, reject) => {
209
+ const system = messages.find(m => m.role === "system")?.content ?? ""
210
+ const userContent = messages.filter(m => m.role !== "system").map(m => m.content).join("\n\n")
211
+
212
+ // Pass system instruction via -p; pipe user content via stdin
213
+ const args = system ? ["-p", system] : []
214
+ const child = spawn("gemini", args, { stdio: ["pipe", "pipe", "pipe"] })
215
+
216
+ let out = "", err = ""
217
+ child.stdout.on("data", d => { out += d })
218
+ child.stderr.on("data", d => { err += d })
219
+ child.on("error", reject)
220
+ child.on("close", code => {
221
+ if (code === 0) resolve(out.trim())
222
+ else reject(new Error(`gemini CLI exited ${code}: ${err.slice(0, 200)}`))
223
+ })
224
+ child.stdin.write(userContent)
225
+ child.stdin.end()
226
+ })
227
+ }
228
+ }
package/modules/AGENTS.md CHANGED
@@ -35,6 +35,13 @@ When working inside this folder, follow these rules in addition to the root guid
35
35
  | `council/risk.ts` | Deterministic risk classifier — no LLM. Assigns `low/medium/high/critical` and `council_mode` from design text and refuted evidence. Drives advisor/reviewer fan-out counts. |
36
36
  | `council/deliberate.ts` | Full pipeline orchestration. Calls `oracle.propose()` at the end — never `oracle.commit()`. Risk classifier runs first to set fan-out counts. |
37
37
 
38
+ ### Advisor
39
+ | File | Owns |
40
+ |---|---|
41
+ | `advisor/ask.ts` | Main entry point. Queries Oracle, calls LLM, validates answer against satisfaction threshold (confidence ≥ 0.7, no blockers). Retries up to 2 times with previous answer as context. Throws on bad LLM output — do not add fallbacks. |
42
+ | `advisor/prompt.ts` | SYSTEM_PROMPT, evidence formatter, user prompt builder. The plain-language framing lives here. |
43
+ | `advisor/types.ts` | `AdvisorInput`, `AdvisorAnswer`, `AdvisorOutput`, `AdvisorDeps` types. |
44
+
38
45
  ---
39
46
 
40
47
  ## Extension points
@@ -51,6 +58,7 @@ When working inside this folder, follow these rules in addition to the root guid
51
58
 
52
59
  ## Invariants — do not break these
53
60
 
61
+ - `advisor/ask.ts` never calls `oracle.propose()` or `oracle.commit()`. It is a read-only path.
54
62
  - `oracle.commit()` is never called without explicit human input. `deliberate()` calls `propose()` only.
55
63
  - `jury/evaluate.ts` recomputes `confidence` as the exact average of `confidence_breakdown` dimensions — the LLM value is discarded.
56
64
  - `jury/evaluate.ts` derives `council_brief` from the recomputed confidence — never trusts the LLM value.
package/modules/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ Supplements the root-level instructions. Read this when working inside the `modu
6
6
 
7
7
  ## What these modules are
8
8
 
9
- Three portable TypeScript modules — Oracle, Jury, Council — that form the knowledge and reasoning layer of an agentic workflow. They are designed to be dropped into any Node.js codebase.
9
+ Five portable TypeScript modules — Advisor, Oracle, Jury, Council, Sentinel — that form the knowledge and reasoning layer of an agentic workflow. They are designed to be dropped into any Node.js codebase.
10
10
 
11
11
  The entry point for a host application is `setup.ts`. Everything else is internal.
12
12
 
@@ -21,7 +21,13 @@ No module imports a specific LLM provider, vector store, or embedder. All extern
21
21
  In `jury/evaluate.ts`, after parsing the LLM response, `confidence` is recomputed as the exact average of the four `confidence_breakdown` dimensions. The LLM's stated `confidence` value is discarded. `council_brief` is then derived from this recomputed value. Do not remove either override.
22
22
 
23
23
  ### Throw on bad LLM output — never default to passing
24
- Both `jury/evaluate.ts` and `council/chairman.ts` throw if the LLM returns non-JSON or output that fails Zod validation. This is intentional. A silently passing Jury score is worse than an error. Do not add fallbacks or defaults.
24
+ `jury/evaluate.ts`, `council/chairman.ts`, and `advisor/ask.ts` all throw if the LLM returns non-JSON or output that fails schema validation. This is intentional. A silently passing score is worse than an error. Do not add fallbacks or defaults.
25
+
26
+ ### Advisor is a read-only path
27
+ `advisor/ask.ts` queries Oracle and calls the LLM — it never calls `oracle.propose()` or `oracle.commit()`. It has no side effects on Chronicle. Do not add write calls to the Advisor path.
28
+
29
+ ### Advisor validation loop
30
+ `advisor/ask.ts` retries the LLM call up to `MAX_RETRIES` (2) times when the answer does not meet the satisfaction threshold (confidence ≥ 0.7, no blockers). The previous answer is included as context in the retry prompt. After the retry budget is exhausted, the best answer is returned regardless. Do not increase `MAX_RETRIES` without considering LLM cost implications.
25
31
 
26
32
  ### oracle.commit() is a human gate
27
33
  `council/deliberate.ts` calls `oracle.propose()` at the end of every deliberation. It never calls `oracle.commit()`. If you see a code path that calls `oracle.commit()` without explicit human input, that is a bug.
package/modules/README.md CHANGED
@@ -1,11 +1,12 @@
1
- # Oracle · Jury · Council · Sentinel
1
+ # Advisor · Oracle · Jury · Council · Sentinel
2
2
 
3
- Four portable modules for the knowledge and reasoning layer of any agentic workflow.
3
+ Five portable modules for the knowledge and reasoning layer of any agentic workflow.
4
4
  Drop the `modules/` folder into your project and wire up the dependencies.
5
5
 
6
6
  ```
7
- OracleJury → Council → human gate → Executor
8
- Sentinelcoverage + drift + PR coverage map
7
+ Advisorplain-language questions answered from Chronicle
8
+ Oracle → Jury Council → human gate → Executor
9
+ Sentinel → coverage + drift + PR coverage map
9
10
  ```
10
11
 
11
12
  ---
@@ -14,6 +15,7 @@ Sentinel → coverage + drift + PR coverage map
14
15
 
15
16
  | Module | Responsibility | LLM? |
16
17
  |---|---|---|
18
+ | **Advisor** | Ask a plain-language question — synthesises Chronicle evidence into a concise answer with an internal validation loop | Yes |
17
19
  | **Oracle** | Query and write interface to Chronicle (the persistent knowledge store) | No |
18
20
  | **Jury** | Evaluate a design against Oracle evidence — produces a confidence score | Yes |
19
21
  | **Council** | Adversarial validation via parallel advisor/reviewer fan-out — produces a verdict | Yes |
@@ -107,18 +109,34 @@ Recommended `tsconfig.json` settings:
107
109
 
108
110
  ## Quick start
109
111
 
112
+ ```typescript
113
+ import { setup } from "./modules/setup"
114
+
115
+ // The simplest entry point — wires all modules from one call
116
+ const { oracle, evaluate, deliberate, ask } = await setup({ llm: myLLMProvider })
117
+
118
+ // Ask a plain-language question (Advisor)
119
+ const answer = await ask("what did the team decide about authentication?")
120
+ // answer.what_we_know, .recommendation, .next_step, .risks, .blockers, .retries
121
+
122
+ // Or run the full evaluation pipeline for a proposed design:
123
+ const evidence = await oracle.query("authentication patterns in this codebase")
124
+ ```
125
+
126
+ ### Manual wiring (without setup())
127
+
110
128
  ```typescript
111
129
  import { createOracleClient, xenovaEmbed, createLanceDBStore } from "./modules/oracle"
112
130
  import { evaluate } from "./modules/jury"
113
131
  import { deliberate } from "./modules/council"
114
132
 
115
- // 1. Wire Oracle (no LLM required)
133
+ // Wire Oracle manually
116
134
  const oracle = createOracleClient({
117
135
  embedder: xenovaEmbed,
118
136
  vectorStore: await createLanceDBStore(".chronicle"),
119
137
  })
120
138
 
121
- // 2. Retrieve evidence for the task at hand
139
+ // Retrieve evidence for the task at hand
122
140
  const evidence = await oracle.query("authentication patterns in this codebase")
123
141
 
124
142
  // 3. Jury evaluates the design against the evidence
@@ -167,6 +185,47 @@ if (verdict.satisfied) {
167
185
 
168
186
  ---
169
187
 
188
+ ## Advisor
189
+
190
+ The Advisor is the plain-language interface to Chronicle. Use it to answer questions rather than to evaluate designs. It is a **read-only** path — it never calls `oracle.propose()` or `oracle.commit()`.
191
+
192
+ ```typescript
193
+ import { ask } from "./modules/advisor"
194
+
195
+ const answer = await ask(
196
+ { question: "What did the team decide about session handling?", evidence },
197
+ { llm: myLLMProvider },
198
+ )
199
+ ```
200
+
201
+ Or via `setup()`, which queries Oracle automatically:
202
+
203
+ ```typescript
204
+ const { ask } = await setup({ llm: myLLMProvider })
205
+ const answer = await ask("What did the team decide about session handling?")
206
+ ```
207
+
208
+ ### Advisor output
209
+
210
+ ```typescript
211
+ interface AdvisorOutput {
212
+ question: string
213
+ confidence: number // 0–1 — how confident the answer is given the evidence
214
+ what_we_know: string // plain-language summary of relevant Chronicle knowledge
215
+ risks: string[] // real risks worth knowing
216
+ blockers: string[] // hard blockers — must be resolved before acting (often empty)
217
+ recommendation: string // one clear recommended action
218
+ next_step: string // specific next step or quorum command
219
+ retries: number // how many retry attempts were needed (0 = first try succeeded)
220
+ }
221
+ ```
222
+
223
+ ### Validation loop
224
+
225
+ Advisor validates its own answer before returning. If `confidence < 0.7` or `blockers.length > 0`, it retries the LLM call with the previous answer as context — up to 2 retries. After the retry budget is exhausted, the best answer is returned regardless. Throws on non-JSON or schema-invalid LLM output.
226
+
227
+ ---
228
+
170
229
  ## LLM provider interface
171
230
 
172
231
  The `LLMProvider` type is a simple function. Wire it to any provider:
@@ -284,14 +343,14 @@ const risk = classifyRisk(outcome, design, evidence)
284
343
  // risk.council_mode — "jury-only" | "lite" | "full"
285
344
  ```
286
345
 
287
- | Risk | Triggers | Advisor + Reviewer count |
288
- |---|---|---|
289
- | Low | Nothing sensitive detected | 1 + 1 |
290
- | Medium | Cache, queues, deployments, rate limiting | 1 + 2 |
291
- | High | DB migrations, permissions, PII, secrets | 5 + 5 |
292
- | Critical | Auth, payments, crypto, data deletion | 5 + 5 |
346
+ | Risk | Triggers | Council mode | Advisor + Reviewer |
347
+ |---|---|---|---|
348
+ | Low | Nothing sensitive detected | jury-only — skipped | 0 + 0 |
349
+ | Medium | Cache, queues, deployments, rate limiting | lite | 1 + 2 |
350
+ | High | DB migrations, permissions, PII, secrets | full | 5 + 5 |
351
+ | Critical | Auth, payments, crypto, data deletion | full + human flag | 5 + 5 |
293
352
 
294
- Refuted entries in the evidence pack always elevate risk by at least one level.
353
+ Refuted entries in the evidence pack always elevate risk by at least one level. `jury-only` means Council is not called at all — Jury alone is sufficient for low-risk designs.
295
354
 
296
355
  ### Council output routing
297
356
 
@@ -0,0 +1,87 @@
1
+ import { z } from "zod"
2
+ import type { AdvisorInput, AdvisorOutput, AdvisorAnswer, AdvisorDeps } from "./types"
3
+ import { SYSTEM_PROMPT, buildUserPrompt } from "./prompt"
4
+
5
+ const SATISFACTION_THRESHOLD = 0.7
6
+ const MAX_RETRIES = 2
7
+
8
+ const AdvisorAnswerSchema = z.object({
9
+ confidence: z.number().min(0).max(1),
10
+ what_we_know: z.string().min(1),
11
+ risks: z.array(z.string()),
12
+ blockers: z.array(z.string()),
13
+ recommendation: z.string().min(1),
14
+ next_step: z.string().min(1),
15
+ })
16
+
17
+ async function callLLM(
18
+ input: AdvisorInput,
19
+ deps: AdvisorDeps,
20
+ attempt: number,
21
+ previous: AdvisorAnswer | null,
22
+ ): Promise<AdvisorAnswer> {
23
+ const { llm, model } = deps
24
+
25
+ let userPrompt = buildUserPrompt(input.question, input.evidence)
26
+
27
+ if (attempt > 0 && previous) {
28
+ userPrompt += [
29
+ "",
30
+ `## Previous Answer (attempt ${attempt} — did not meet quality threshold)`,
31
+ `Confidence: ${previous.confidence.toFixed(2)} (need ≥ ${SATISFACTION_THRESHOLD})`,
32
+ previous.blockers.length > 0
33
+ ? `Unresolved blockers: ${previous.blockers.join("; ")}`
34
+ : "",
35
+ "Please produce a more specific and concrete answer.",
36
+ ].filter(Boolean).join("\n")
37
+ }
38
+
39
+ const raw = await llm(
40
+ [
41
+ { role: "system", content: SYSTEM_PROMPT },
42
+ { role: "user", content: userPrompt },
43
+ ],
44
+ model,
45
+ )
46
+
47
+ let parsed: unknown
48
+ try {
49
+ const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim()
50
+ parsed = JSON.parse(cleaned)
51
+ } catch {
52
+ throw new Error(`Advisor: LLM returned non-JSON. Raw (first 300 chars): ${raw.slice(0, 300)}`)
53
+ }
54
+
55
+ const result = AdvisorAnswerSchema.safeParse(parsed)
56
+ if (!result.success) {
57
+ throw new Error(`Advisor: LLM output failed validation. Issues: ${JSON.stringify(result.error.issues)}`)
58
+ }
59
+
60
+ return result.data
61
+ }
62
+
63
+ /**
64
+ * Ask the Advisor a plain-language question.
65
+ *
66
+ * Internally calls the LLM and validates the answer against a satisfaction
67
+ * threshold (confidence ≥ 0.7, no blockers). Retries up to MAX_RETRIES times
68
+ * with the previous answer included as context. Returns the best answer found
69
+ * within the retry budget regardless of whether the threshold was met.
70
+ *
71
+ * Throws if the LLM returns non-JSON or output that fails schema validation.
72
+ */
73
+ export async function ask(input: AdvisorInput, deps: AdvisorDeps): Promise<AdvisorOutput> {
74
+ let last: AdvisorAnswer | null = null
75
+
76
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
77
+ const answer = await callLLM(input, deps, attempt, last)
78
+ last = answer
79
+
80
+ const satisfied = answer.confidence >= SATISFACTION_THRESHOLD && answer.blockers.length === 0
81
+ if (satisfied || attempt === MAX_RETRIES) {
82
+ return { ...answer, question: input.question, retries: attempt }
83
+ }
84
+ }
85
+
86
+ return { ...last!, question: input.question, retries: MAX_RETRIES }
87
+ }
@@ -0,0 +1,2 @@
1
+ export { ask } from "./ask"
2
+ export type { AdvisorInput, AdvisorOutput, AdvisorDeps } from "./types"
@@ -0,0 +1,50 @@
1
+ import type { OracleResult } from "../shared/types"
2
+ import { entryText } from "../shared/types"
3
+
4
+ export const SYSTEM_PROMPT = `You are the Quorum Advisor — the plain-language interface to a team's collective knowledge.
5
+
6
+ You receive a question from a developer or engineering manager, along with relevant Chronicle evidence.
7
+ Synthesise that evidence into a clear, concise answer a human can act on.
8
+
9
+ Rules:
10
+ - Write for a human who does not know what "Chronicle entries" or "vector search" mean.
11
+ - Be direct. One clear recommendation, not a list of options unless genuinely necessary.
12
+ - If Chronicle has relevant evidence, reference it plainly: "the team already decided X".
13
+ - If Chronicle has no evidence, say so honestly — do not invent history.
14
+ - Blockers are hard blockers only — things that MUST be resolved before moving forward.
15
+ - risks are real concerns worth knowing, not theoretical edge cases.
16
+
17
+ Return ONLY valid JSON matching this schema (no markdown fences, no explanation):
18
+ {
19
+ "confidence": <number 0–1 — how confident are you given the available evidence>,
20
+ "what_we_know": <string — what Chronicle knows about this topic. Plain English, 1–3 sentences.>,
21
+ "risks": [<string — each real risk, plain English, one per item. Empty array if none.>],
22
+ "blockers": [<string — hard blockers only. Empty array if none.>],
23
+ "recommendation": <string — one clear recommended action>,
24
+ "next_step": <string — the specific next thing to do, e.g. a quorum command or a decision>
25
+ }`
26
+
27
+ export function formatEvidence(evidence: OracleResult[]): string {
28
+ if (evidence.length === 0) {
29
+ return "Chronicle has no prior entries on this topic."
30
+ }
31
+ return evidence
32
+ .map(e => {
33
+ const text = entryText(e)
34
+ const statusTag =
35
+ e.status === "refuted" ? " [REJECTED]" :
36
+ e.status === "validated" ? " [VALIDATED]" : ""
37
+ return `[${e.id.slice(0, 8)}]${statusTag} ${text}\n Areas: ${e.affected_areas.join(", ")}`
38
+ })
39
+ .join("\n\n")
40
+ }
41
+
42
+ export function buildUserPrompt(question: string, evidence: OracleResult[]): string {
43
+ return [
44
+ "## Question",
45
+ question,
46
+ "",
47
+ "## Chronicle Evidence",
48
+ formatEvidence(evidence),
49
+ ].join("\n")
50
+ }
@@ -0,0 +1,26 @@
1
+ import type { OracleResult, LLMProvider } from "../shared/types"
2
+
3
+ export interface AdvisorInput {
4
+ question: string
5
+ evidence: OracleResult[]
6
+ }
7
+
8
+ export interface AdvisorAnswer {
9
+ confidence: number
10
+ what_we_know: string
11
+ risks: string[]
12
+ blockers: string[]
13
+ recommendation: string
14
+ next_step: string
15
+ }
16
+
17
+ export interface AdvisorOutput extends AdvisorAnswer {
18
+ question: string
19
+ /** Number of retries taken before the answer met the satisfaction threshold. */
20
+ retries: number
21
+ }
22
+
23
+ export interface AdvisorDeps {
24
+ llm: LLMProvider
25
+ model?: string
26
+ }
@@ -10,8 +10,6 @@ const DEFAULT_ADVISOR_COUNT = 5
10
10
  const DEFAULT_REVIEWER_COUNT = 5
11
11
  const LITE_ADVISOR_COUNT = 1
12
12
  const LITE_REVIEWER_COUNT = 2
13
- const JURY_ONLY_ADVISOR_COUNT = 1
14
- const JURY_ONLY_REVIEWER_COUNT = 1
15
13
 
16
14
  /**
17
15
  * Run the Council deliberation pipeline.
@@ -44,14 +42,26 @@ export async function deliberate(
44
42
 
45
43
  // Classify risk to determine Council mode and advisor/reviewer counts
46
44
  const risk = classifyRisk(input.outcome, input.design, input.evidence)
45
+
46
+ if (risk.council_mode === "jury-only") {
47
+ return {
48
+ satisfied: true,
49
+ verdict: "Skipped — low-risk design passed by Jury without Council review.",
50
+ blockers: [],
51
+ warnings: [],
52
+ challenges: [],
53
+ evidence_cited: [],
54
+ citation_validation: { valid_ids: [], hallucinated_ids: [] },
55
+ advisor_split: { proceed: 0, redesign: 0, "investigate-more": 0 },
56
+ recommendation: "proceed",
57
+ }
58
+ }
59
+
47
60
  let defaultAdvisors = DEFAULT_ADVISOR_COUNT
48
61
  let defaultReviewers = DEFAULT_REVIEWER_COUNT
49
62
  if (risk.council_mode === "lite") {
50
63
  defaultAdvisors = LITE_ADVISOR_COUNT
51
64
  defaultReviewers = LITE_REVIEWER_COUNT
52
- } else if (risk.council_mode === "jury-only") {
53
- defaultAdvisors = JURY_ONLY_ADVISOR_COUNT
54
- defaultReviewers = JURY_ONLY_REVIEWER_COUNT
55
65
  }
56
66
  const advisorCount = deps.advisorCount ?? defaultAdvisors
57
67
  const reviewerCount = deps.reviewerCount ?? defaultReviewers
@@ -94,7 +94,7 @@ export type RiskLevel = "low" | "medium" | "high" | "critical"
94
94
  /**
95
95
  * Determines which Council mode to use.
96
96
  * skip → Oracle query only, no LLM validation
97
- * jury-only → Jury scores, no Council fan-out
97
+ * jury-only → Jury scores, Council skipped entirely (low-risk fast path)
98
98
  * lite → Jury + 1–2 reviewers (no full advisor fan-out)
99
99
  * full → Full Council (default 5 advisors + 5 reviewers + Chairman)
100
100
  */
package/modules/setup.ts CHANGED
@@ -5,10 +5,12 @@ import { xenovaEmbed, warmEmbedder } from "./oracle/adapters/xenova-embedder"
5
5
  import { createLanceDBStore } from "./oracle/adapters/lance-db"
6
6
  import { evaluate } from "./jury/evaluate"
7
7
  import { deliberate } from "./council/deliberate"
8
+ import { ask as advisorAsk } from "./advisor/ask"
8
9
  import type { LLMProvider, OracleClient } from "./shared/types"
9
10
  import { entryText } from "./shared/types"
10
11
  import type { JuryInput, JuryOutput, JuryDeps } from "./jury/types"
11
12
  import type { CouncilInput, CouncilOutput, CouncilDeps, CouncilModels } from "./council/types"
13
+ import type { AdvisorOutput } from "./advisor/types"
12
14
 
13
15
  export interface SetupOptions {
14
16
  /**
@@ -69,6 +71,14 @@ export interface Modules {
69
71
  deliberate: (
70
72
  input: Omit<CouncilInput, "jury_output"> & { jury_output: JuryOutput },
71
73
  ) => Promise<CouncilOutput>
74
+
75
+ /**
76
+ * Ask the Advisor a plain-language question.
77
+ * Queries Oracle automatically, synthesises Chronicle evidence into a
78
+ * human-readable answer, and retries internally until the answer meets
79
+ * the confidence threshold (≥ 0.7, no blockers) or the retry budget runs out.
80
+ */
81
+ ask: (question: string) => Promise<AdvisorOutput>
72
82
  }
73
83
 
74
84
  /**
@@ -150,5 +160,10 @@ export async function setup(options: SetupOptions): Promise<Modules> {
150
160
  oracle,
151
161
  models: models.council,
152
162
  }),
163
+
164
+ ask: async (question: string) => {
165
+ const evidence = await oracle.query(question)
166
+ return advisorAsk({ question, evidence }, { llm })
167
+ },
153
168
  }
154
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balpal4495/quorum",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Portable reasoning layer for agentic codebases — Oracle, Jury, Council, Sentinel",
5
5
  "type": "module",
6
6
  "license": "MIT",