@balpal4495/quorum 2.0.0 → 3.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.
- package/.github/copilot-instructions.md +29 -6
- package/README.md +69 -2
- package/bin/commands/compass.js +422 -0
- package/bin/commands/init.js +13 -7
- package/bin/commands/migrate-v2.js +136 -0
- package/bin/commands/sentinel.js +1 -1
- package/bin/commands/sync.js +97 -0
- package/bin/quorum.js +35 -0
- package/bin/templates/CLAUDE.md +101 -0
- package/modules/README.md +57 -10
- package/modules/compass/behavior.ts +161 -0
- package/modules/compass/create.ts +365 -0
- package/modules/compass/evidence/collect.ts +109 -0
- package/modules/compass/index.ts +7 -0
- package/modules/compass/prompts/index.ts +230 -0
- package/modules/compass/prompts/system.ts +24 -0
- package/modules/compass/propose.ts +152 -0
- package/modules/compass/schemas.ts +121 -0
- package/modules/compass/score.ts +77 -0
- package/modules/compass/sources/index.ts +413 -0
- package/modules/compass/types.ts +431 -0
- package/modules/setup.ts +33 -0
- package/package.json +19 -11
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { promises as fs } from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { fileURLToPath } from "url"
|
|
4
|
+
import { createRequire } from "module"
|
|
5
|
+
import { c, log } from "../shared/colors.js"
|
|
6
|
+
|
|
7
|
+
const _require = createRequire(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const QUORUM_ROOT = path.resolve(__dirname, "../..")
|
|
10
|
+
|
|
11
|
+
async function exists(p) {
|
|
12
|
+
return fs.access(p).then(() => true).catch(() => false)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function rmrf(p) {
|
|
16
|
+
await fs.rm(p, { recursive: true, force: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readJson(p) {
|
|
20
|
+
return JSON.parse(await fs.readFile(p, "utf8"))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function detectV1Artifacts(target) {
|
|
24
|
+
const artifacts = []
|
|
25
|
+
const vendoredModules = path.join(target, "quorum", "modules")
|
|
26
|
+
const vendoredEvals = path.join(target, "quorum", "evals")
|
|
27
|
+
if (await exists(vendoredModules)) artifacts.push({ path: vendoredModules, label: "quorum/modules/" })
|
|
28
|
+
if (await exists(vendoredEvals)) artifacts.push({ path: vendoredEvals, label: "quorum/evals/" })
|
|
29
|
+
return artifacts
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function removeVendoredArtifacts(artifacts) {
|
|
33
|
+
for (const artifact of artifacts) {
|
|
34
|
+
await rmrf(artifact.path)
|
|
35
|
+
log.ok(`Removed ${artifact.label}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function ensurePackageJsonDependency(target, version) {
|
|
40
|
+
const pkgPath = path.join(target, "package.json")
|
|
41
|
+
if (!await exists(pkgPath)) return
|
|
42
|
+
const pkg = await readJson(pkgPath)
|
|
43
|
+
const already = pkg.dependencies?.["@balpal4495/quorum"] || pkg.devDependencies?.["@balpal4495/quorum"]
|
|
44
|
+
if (already) {
|
|
45
|
+
log.skipped("package.json (@balpal4495/quorum already listed)")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
pkg.devDependencies = pkg.devDependencies ?? {}
|
|
49
|
+
pkg.devDependencies["@balpal4495/quorum"] = `^${version}`
|
|
50
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8")
|
|
51
|
+
log.appended(`package.json — added @balpal4495/quorum@^${version} to devDependencies`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function refreshHostDocs(target) {
|
|
55
|
+
// Replace quorum/CLAUDE.md with the host-facing template (not module internals)
|
|
56
|
+
const claudeSrc = path.join(QUORUM_ROOT, "bin", "templates", "CLAUDE.md")
|
|
57
|
+
const claudeDest = path.join(target, "quorum", "CLAUDE.md")
|
|
58
|
+
if (await exists(claudeSrc) && await exists(claudeDest)) {
|
|
59
|
+
const current = await fs.readFile(claudeDest, "utf8")
|
|
60
|
+
// Only replace if it looks like the old module-internal version
|
|
61
|
+
if (current.includes("Key design decisions to preserve") || current.includes("Dependency injection throughout")) {
|
|
62
|
+
await fs.copyFile(claudeSrc, claudeDest)
|
|
63
|
+
log.ok("quorum/CLAUDE.md — replaced module-internal doc with host-facing operational guide")
|
|
64
|
+
} else {
|
|
65
|
+
log.skipped("quorum/CLAUDE.md (does not look like v1 module-internal doc — leaving as-is)")
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeVersionFile(target, version) {
|
|
71
|
+
await fs.writeFile(path.join(target, ".quorum-version"), version + "\n", "utf8")
|
|
72
|
+
log.ok(`.quorum-version — written (${version})`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function run(argv) {
|
|
76
|
+
const target = process.cwd()
|
|
77
|
+
const dryRun = argv.includes("--dry-run")
|
|
78
|
+
const pkg = _require(path.join(QUORUM_ROOT, "package.json"))
|
|
79
|
+
|
|
80
|
+
console.log(c.bold("\nQuorum migrate-v2") + c.dim(` v${pkg.version}`) + (dryRun ? c.yellow(" [dry-run]") : ""))
|
|
81
|
+
console.log(`Target: ${c.dim(target)}\n`)
|
|
82
|
+
|
|
83
|
+
// ── Detect v1 artifacts ──────────────────────────────────────────────────
|
|
84
|
+
log.section("Scanning for v1 artifacts")
|
|
85
|
+
const artifacts = await detectV1Artifacts(target)
|
|
86
|
+
|
|
87
|
+
if (artifacts.length === 0) {
|
|
88
|
+
console.log(c.dim("\n No v1 vendored artifacts found (quorum/modules/, quorum/evals/)."))
|
|
89
|
+
console.log(c.dim(" This project may already be on v2, or was never on v1.\n"))
|
|
90
|
+
|
|
91
|
+
const hasVersionFile = await exists(path.join(target, ".quorum-version"))
|
|
92
|
+
if (!hasVersionFile) {
|
|
93
|
+
console.log(c.yellow(" No .quorum-version file found. Run 'quorum init' to initialize properly.\n"))
|
|
94
|
+
} else {
|
|
95
|
+
const currentVersion = (await fs.readFile(path.join(target, ".quorum-version"), "utf8")).trim()
|
|
96
|
+
console.log(c.green(` .quorum-version: ${currentVersion} ✓\n`))
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("")
|
|
102
|
+
for (const a of artifacts) {
|
|
103
|
+
console.log(` ${c.yellow("⚠")} Found: ${c.bold(a.label)}`)
|
|
104
|
+
}
|
|
105
|
+
console.log("")
|
|
106
|
+
console.log(c.dim(" These directories were copied by quorum init in v1 and are no longer needed."))
|
|
107
|
+
console.log(c.dim(" In v2, modules live in node_modules/@balpal4495/quorum."))
|
|
108
|
+
|
|
109
|
+
if (dryRun) {
|
|
110
|
+
console.log(c.yellow("\n [dry-run] No changes made. Re-run without --dry-run to apply.\n"))
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Remove vendored artifacts ────────────────────────────────────────────
|
|
115
|
+
log.section("Removing v1 vendored artifacts")
|
|
116
|
+
await removeVendoredArtifacts(artifacts)
|
|
117
|
+
|
|
118
|
+
// ── Ensure package.json lists the dependency ─────────────────────────────
|
|
119
|
+
log.section("Updating package.json")
|
|
120
|
+
await ensurePackageJsonDependency(target, pkg.version)
|
|
121
|
+
|
|
122
|
+
// ── Refresh host-facing docs ─────────────────────────────────────────────
|
|
123
|
+
log.section("Refreshing host docs")
|
|
124
|
+
await refreshHostDocs(target)
|
|
125
|
+
|
|
126
|
+
// ── Write version marker ─────────────────────────────────────────────────
|
|
127
|
+
log.section("Version marker")
|
|
128
|
+
await writeVersionFile(target, pkg.version)
|
|
129
|
+
|
|
130
|
+
console.log(`\n${c.green("✓ Migration complete.")}\n`)
|
|
131
|
+
console.log("Next steps:")
|
|
132
|
+
console.log(c.dim(" 1. npm install — install @balpal4495/quorum as a package dep"))
|
|
133
|
+
console.log(c.dim(" 2. quorum sync — refresh instruction blocks"))
|
|
134
|
+
console.log(c.dim(" 3. git rm -r quorum/modules quorum/evals 2>/dev/null — remove from git index if tracked"))
|
|
135
|
+
console.log("")
|
|
136
|
+
}
|
package/bin/commands/sentinel.js
CHANGED
|
@@ -141,7 +141,7 @@ export async function run(argv) {
|
|
|
141
141
|
if (args.subcommand === "drift") {
|
|
142
142
|
console.log(`\n${c.yellow("quorum sentinel drift")} requires an LLM provider and is not available as a standalone CLI command.`)
|
|
143
143
|
console.log(c.dim("\nUse the sentinelAssertions() helper in your test suite instead:"))
|
|
144
|
-
console.log(c.dim("\n import { sentinelAssertions } from \"
|
|
144
|
+
console.log(c.dim("\n import { sentinelAssertions } from \"@balpal4495/quorum\""))
|
|
145
145
|
console.log(c.dim(" const assertions = sentinelAssertions({ llm: yourProvider })"))
|
|
146
146
|
console.log(c.dim(" describe(\"sentinel\", () => { assertions.forEach(a => a()) })\n"))
|
|
147
147
|
process.exit(0)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { promises as fs } from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { fileURLToPath } from "url"
|
|
4
|
+
import { c, log } from "../shared/colors.js"
|
|
5
|
+
import { createRequire } from "module"
|
|
6
|
+
|
|
7
|
+
const _require = createRequire(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const QUORUM_ROOT = path.resolve(__dirname, "../..")
|
|
10
|
+
|
|
11
|
+
async function exists(p) {
|
|
12
|
+
return fs.access(p).then(() => true).catch(() => false)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function syncFile(target, destRelative, freshContent) {
|
|
16
|
+
const dest = path.join(target, destRelative)
|
|
17
|
+
if (!await exists(dest)) {
|
|
18
|
+
log.skipped(`${destRelative} (not present — run quorum init first)`)
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const existing = await fs.readFile(dest, "utf8")
|
|
22
|
+
const startTag = "<!-- quorum:start -->"
|
|
23
|
+
const endTag = "<!-- quorum:end -->"
|
|
24
|
+
const start = existing.indexOf(startTag)
|
|
25
|
+
const end = existing.indexOf(endTag)
|
|
26
|
+
if (start === -1 || end === -1) {
|
|
27
|
+
log.skipped(`${destRelative} (no quorum marker block found)`)
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
const fresh = `${startTag}\n${freshContent}\n${endTag}`
|
|
31
|
+
const updated = existing.slice(0, start) + fresh + existing.slice(end + endTag.length)
|
|
32
|
+
if (updated === existing) {
|
|
33
|
+
log.skipped(`${destRelative} (already up to date)`)
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
await fs.writeFile(dest, updated, "utf8")
|
|
37
|
+
log.appended(`${destRelative} (marker block refreshed)`)
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function syncCopilotInstructions(target) {
|
|
42
|
+
const src = path.join(QUORUM_ROOT, ".github", "copilot-instructions.md")
|
|
43
|
+
if (!await exists(src)) return
|
|
44
|
+
const content = await fs.readFile(src, "utf8")
|
|
45
|
+
await syncFile(target, ".github/copilot-instructions.md", content)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function syncQuorumDocs(target) {
|
|
49
|
+
// Replace quorum/CLAUDE.md and quorum/AGENTS.md wholesale — no marker blocks
|
|
50
|
+
const claudeSrc = path.join(QUORUM_ROOT, "bin", "templates", "CLAUDE.md")
|
|
51
|
+
const agentsSrc = path.join(QUORUM_ROOT, "modules", "AGENTS.md")
|
|
52
|
+
const setupSrc = path.join(QUORUM_ROOT, "SETUP.md")
|
|
53
|
+
|
|
54
|
+
for (const [src, destName] of [[claudeSrc, "quorum/CLAUDE.md"], [agentsSrc, "quorum/AGENTS.md"], [setupSrc, "quorum/SETUP.md"]]) {
|
|
55
|
+
const dest = path.join(target, destName)
|
|
56
|
+
if (!await exists(src)) continue
|
|
57
|
+
if (!await exists(dest)) { log.skipped(`${destName} (not present)`); continue }
|
|
58
|
+
const [fresh, current] = await Promise.all([fs.readFile(src, "utf8"), fs.readFile(dest, "utf8")])
|
|
59
|
+
if (fresh === current) { log.skipped(`${destName} (already up to date)`); continue }
|
|
60
|
+
await fs.writeFile(dest, fresh, "utf8")
|
|
61
|
+
log.appended(`${destName} (updated)`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function syncVersionFile(target) {
|
|
66
|
+
const pkg = _require(path.join(QUORUM_ROOT, "package.json"))
|
|
67
|
+
const dest = path.join(target, ".quorum-version")
|
|
68
|
+
const current = await exists(dest) ? (await fs.readFile(dest, "utf8")).trim() : null
|
|
69
|
+
if (current === pkg.version) {
|
|
70
|
+
log.skipped(`.quorum-version (already ${pkg.version})`)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
await fs.writeFile(dest, pkg.version + "\n", "utf8")
|
|
74
|
+
log.appended(`.quorum-version — updated to ${pkg.version}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function run() {
|
|
78
|
+
const target = process.cwd()
|
|
79
|
+
const pkg = _require(path.join(QUORUM_ROOT, "package.json"))
|
|
80
|
+
|
|
81
|
+
console.log(c.bold("\nQuorum sync") + c.dim(` v${pkg.version}`))
|
|
82
|
+
console.log(`Target: ${c.dim(target)}\n`)
|
|
83
|
+
|
|
84
|
+
if (!await exists(path.join(target, ".quorum-version"))) {
|
|
85
|
+
console.log(c.yellow("Quorum is not initialized here. Run 'quorum init' first.\n"))
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log.section("Refreshing instruction files")
|
|
90
|
+
await syncCopilotInstructions(target)
|
|
91
|
+
|
|
92
|
+
log.section("Refreshing quorum docs")
|
|
93
|
+
await syncQuorumDocs(target)
|
|
94
|
+
await syncVersionFile(target)
|
|
95
|
+
|
|
96
|
+
console.log(`\n${c.green("✓ Sync complete.")}\n`)
|
|
97
|
+
}
|
package/bin/quorum.js
CHANGED
|
@@ -30,6 +30,9 @@ ${c.bold("Usage:")}
|
|
|
30
30
|
${c.cyan("quorum sentinel")} [coverage] Chronicle coverage of source files
|
|
31
31
|
${c.cyan("quorum growth")} Chronicle learning health and growth rate
|
|
32
32
|
${c.cyan("quorum evolve")} Consolidate and improve Chronicle entries (uses LLM)
|
|
33
|
+
${c.cyan("quorum compass")} <subcommand> Product-direction synthesis (brief, map, pathways, bets, score)
|
|
34
|
+
${c.cyan("quorum sync")} Refresh Quorum instruction blocks after npm update
|
|
35
|
+
${c.cyan("quorum migrate-v2")} Migrate from v1 vendored quorum/modules/ to npm package
|
|
33
36
|
${c.cyan("quorum --version")} Print version
|
|
34
37
|
|
|
35
38
|
${c.bold("quorum advisor")} subcommands:
|
|
@@ -52,6 +55,20 @@ ${c.bold("quorum sentinel")} subcommands:
|
|
|
52
55
|
drift (requires LLM — use sentinelAssertions() instead)
|
|
53
56
|
--json Machine-readable output
|
|
54
57
|
|
|
58
|
+
${c.bold("quorum migrate-v2")} flags:
|
|
59
|
+
--dry-run Preview what would be removed without writing
|
|
60
|
+
|
|
61
|
+
${c.bold("quorum compass")} subcommands:
|
|
62
|
+
brief Summarise product direction (LLM)
|
|
63
|
+
map Map behaviours from code + docs (no LLM)
|
|
64
|
+
pathways --goal X Product pathways toward a goal (LLM)
|
|
65
|
+
bets Strategic big bets (LLM)
|
|
66
|
+
score <idea> Score a product idea (LLM)
|
|
67
|
+
spec <title> Generate a product brief (LLM)
|
|
68
|
+
opportunities List gaps from behaviour map (no LLM)
|
|
69
|
+
propose --from-last Stage a Chronicle entry from last artifact
|
|
70
|
+
outcome --entry-id X --result Y Record a bet/pathway outcome
|
|
71
|
+
|
|
55
72
|
${c.bold("Exit codes")} (quorum check):
|
|
56
73
|
0 low / medium risk
|
|
57
74
|
1 high risk
|
|
@@ -120,6 +137,24 @@ async function cli() {
|
|
|
120
137
|
return
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
if (command === "sync") {
|
|
141
|
+
const { run } = await import(path.join(__dirname, "commands/sync.js"))
|
|
142
|
+
await run(rest)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (command === "compass") {
|
|
147
|
+
const { run } = await import(path.join(__dirname, "commands/compass.js"))
|
|
148
|
+
await run(rest)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (command === "migrate-v2") {
|
|
153
|
+
const { run } = await import(path.join(__dirname, "commands/migrate-v2.js"))
|
|
154
|
+
await run(rest)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
123
158
|
console.error(`${c.red(`Unknown command: ${command}`)}`)
|
|
124
159
|
console.error(`Run ${c.bold("quorum help")} for usage.`)
|
|
125
160
|
process.exit(1)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Quorum — How to use this project's memory layer
|
|
2
|
+
|
|
3
|
+
Quorum gives every AI session access to this project's approved decisions, rejected
|
|
4
|
+
approaches, and institutional knowledge. It runs as a CLI tool backed by a local
|
|
5
|
+
`.chronicle/` store.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Start every session with memory
|
|
10
|
+
|
|
11
|
+
Before proposing or planning anything, run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
quorum advisor brief
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This shows the full Chronicle summary — what the team has already learned and approved.
|
|
18
|
+
|
|
19
|
+
Then query for the specific area you are about to work in:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
quorum advisor query "topic of the work"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If relevant entries exist, treat them as ground truth. **Refuted entries are hard stops** —
|
|
26
|
+
do not retry a refuted approach without explicitly surfacing the failure reason to the user.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Check designs before coding
|
|
31
|
+
|
|
32
|
+
For any significant change, run a preflight check first:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
quorum check \
|
|
36
|
+
--outcome "what you want to achieve" \
|
|
37
|
+
--design "how you plan to do it"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Exit codes: `0` = low/medium risk — proceed. `1` = high risk — human review first.
|
|
41
|
+
`2` = critical — stop and get explicit sign-off.
|
|
42
|
+
|
|
43
|
+
Auth, payments, crypto, database migrations, PII, and data deletion always trigger
|
|
44
|
+
elevated risk, regardless of how the design is framed.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Propose what was learned
|
|
49
|
+
|
|
50
|
+
When a significant decision is made or an approach is ruled out, stage a Chronicle
|
|
51
|
+
proposal for human review:
|
|
52
|
+
|
|
53
|
+
The Council stages proposals automatically after deliberation. You can also stage
|
|
54
|
+
them manually by following the Chronicle proposal template in SETUP.md.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
quorum commit --list # see all pending proposals
|
|
58
|
+
quorum commit <id> # approve and index a proposal
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Never call `oracle.commit()` autonomously.** Only propose. A human must approve.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Keep memory healthy
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
quorum growth # is Chronicle actually growing?
|
|
69
|
+
quorum evolve # consolidate duplicate or stale entries (uses LLM)
|
|
70
|
+
quorum sentinel coverage # which source files have Chronicle entries?
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Chronicle structure
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
.chronicle/
|
|
79
|
+
committed/ ← approved entries, indexed and searchable (commit to git)
|
|
80
|
+
proposals/ ← staged entries awaiting your approval (do not commit)
|
|
81
|
+
SUMMARY.md ← auto-generated context, rebuilt on every commit
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Commit `.chronicle/committed/` so every teammate and every new AI session starts with
|
|
85
|
+
the same accumulated knowledge. Never commit `.chronicle/proposals/`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## CLI quick reference
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
quorum advisor brief # full Chronicle summary, no LLM
|
|
93
|
+
quorum advisor query "topic" # keyword search, no LLM
|
|
94
|
+
quorum advisor "plain-language question" # synthesised answer via LLM
|
|
95
|
+
quorum check --outcome "..." --design "..." # instant risk triage
|
|
96
|
+
quorum commit --list # review pending proposals
|
|
97
|
+
quorum commit <id> # approve a Chronicle entry
|
|
98
|
+
quorum growth # Chronicle learning health
|
|
99
|
+
quorum evolve # consolidate stale entries (LLM)
|
|
100
|
+
quorum sync # refresh Quorum instruction blocks after npm update
|
|
101
|
+
```
|
package/modules/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# Advisor · Oracle · Jury · Council · Sentinel
|
|
1
|
+
# Advisor · Oracle · Jury · Council · Sentinel · Compass
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Drop the `modules/` folder into your project and wire up the dependencies.
|
|
3
|
+
Six portable modules for the knowledge, reasoning, and product-direction layer of any agentic workflow.
|
|
5
4
|
|
|
6
5
|
```
|
|
7
6
|
Advisor → plain-language questions answered from Chronicle
|
|
8
7
|
Oracle → Jury → Council → human gate → Executor
|
|
9
8
|
Sentinel → coverage + drift + PR coverage map
|
|
9
|
+
Compass → product-direction synthesis (behaviours, pathways, bets, scoring)
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
---
|
|
@@ -20,6 +20,7 @@ Sentinel → coverage + drift + PR coverage map
|
|
|
20
20
|
| **Jury** | Evaluate a design against Oracle evidence — produces a confidence score | Yes |
|
|
21
21
|
| **Council** | Adversarial validation via parallel advisor/reviewer fan-out — produces a verdict | Yes |
|
|
22
22
|
| **Sentinel** | Chronicle coverage reporting, drift detection, and PR coverage maps | Optional |
|
|
23
|
+
| **Compass** | Product-direction synthesis from Chronicle + codebase — behaviours, opportunities, pathways, bets, idea scoring | Optional |
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -91,6 +92,40 @@ You can substitute any vector store and embedder by implementing the `VectorStor
|
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
95
|
+
## TypeScript runtime requirement
|
|
96
|
+
|
|
97
|
+
Quorum ships TypeScript source (`.ts` files). Programmatic imports require a
|
|
98
|
+
TS-aware runtime or bundler. Plain `node` will not work without a loader.
|
|
99
|
+
|
|
100
|
+
**Supported runtimes:**
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# tsx (recommended — zero config)
|
|
104
|
+
npx tsx your-script.ts
|
|
105
|
+
|
|
106
|
+
# ts-node
|
|
107
|
+
npx ts-node --esm your-script.ts
|
|
108
|
+
|
|
109
|
+
# Bun
|
|
110
|
+
bun your-script.ts
|
|
111
|
+
|
|
112
|
+
# Vitest / Jest with ts transform — works out of the box in test files
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Bundlers:** esbuild, Vite, Rollup, webpack — all supported with standard TS config.
|
|
116
|
+
|
|
117
|
+
**Plain Node.js** (no TS runtime): use the CLI instead:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
quorum advisor "question"
|
|
121
|
+
quorum check --outcome "..." --design "..."
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The CLI is always available after `npm install -g @balpal4495/quorum` and requires no
|
|
125
|
+
TS loader. It is the recommended interface for most host-project use cases.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
94
129
|
## TypeScript
|
|
95
130
|
|
|
96
131
|
Requires TypeScript 4.7+ and `zod` v3.
|
|
@@ -109,8 +144,10 @@ Recommended `tsconfig.json` settings:
|
|
|
109
144
|
|
|
110
145
|
## Quick start
|
|
111
146
|
|
|
147
|
+
### npm users
|
|
148
|
+
|
|
112
149
|
```typescript
|
|
113
|
-
import { setup } from "
|
|
150
|
+
import { setup } from "@balpal4495/quorum"
|
|
114
151
|
|
|
115
152
|
// The simplest entry point — wires all modules from one call
|
|
116
153
|
const { oracle, evaluate, deliberate, ask } = await setup({ llm: myLLMProvider })
|
|
@@ -123,12 +160,22 @@ const answer = await ask("what did the team decide about authentication?")
|
|
|
123
160
|
const evidence = await oracle.query("authentication patterns in this codebase")
|
|
124
161
|
```
|
|
125
162
|
|
|
163
|
+
### Quorum repo contributors
|
|
164
|
+
|
|
165
|
+
Working directly inside the Quorum source tree? Import from the local path instead:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { setup } from "./modules/setup"
|
|
169
|
+
|
|
170
|
+
const { oracle, evaluate, deliberate, ask } = await setup({ llm: myLLMProvider })
|
|
171
|
+
```
|
|
172
|
+
|
|
126
173
|
### Manual wiring (without setup())
|
|
127
174
|
|
|
128
175
|
```typescript
|
|
129
|
-
import { createOracleClient, xenovaEmbed, createLanceDBStore } from "
|
|
130
|
-
import { evaluate } from "
|
|
131
|
-
import { deliberate } from "
|
|
176
|
+
import { createOracleClient, xenovaEmbed, createLanceDBStore } from "@balpal4495/quorum/oracle"
|
|
177
|
+
import { evaluate } from "@balpal4495/quorum/jury"
|
|
178
|
+
import { deliberate } from "@balpal4495/quorum/council"
|
|
132
179
|
|
|
133
180
|
// Wire Oracle manually
|
|
134
181
|
const oracle = createOracleClient({
|
|
@@ -190,7 +237,7 @@ if (verdict.satisfied) {
|
|
|
190
237
|
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
238
|
|
|
192
239
|
```typescript
|
|
193
|
-
import { ask } from "
|
|
240
|
+
import { ask } from "@balpal4495/quorum/advisor"
|
|
194
241
|
|
|
195
242
|
const answer = await ask(
|
|
196
243
|
{ question: "What did the team decide about session handling?", evidence },
|
|
@@ -231,7 +278,7 @@ Advisor validates its own answer before returning. If `confidence < 0.7` or `blo
|
|
|
231
278
|
The `LLMProvider` type is a simple function. Wire it to any provider:
|
|
232
279
|
|
|
233
280
|
```typescript
|
|
234
|
-
import type { LLMProvider } from "
|
|
281
|
+
import type { LLMProvider } from "@balpal4495/quorum"
|
|
235
282
|
|
|
236
283
|
// OpenAI example
|
|
237
284
|
const openaiProvider: LLMProvider = async (messages, model = "gpt-4o") => {
|
|
@@ -276,7 +323,7 @@ interface JuryOutput {
|
|
|
276
323
|
Before the LLM runs, Jury executes a deterministic preflight:
|
|
277
324
|
|
|
278
325
|
```typescript
|
|
279
|
-
import { runPreflight } from "
|
|
326
|
+
import { runPreflight } from "@balpal4495/quorum/jury"
|
|
280
327
|
|
|
281
328
|
const preflight = runPreflight(outcome, design, evidence)
|
|
282
329
|
// preflight.touches_sensitive_area
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import type {
|
|
3
|
+
ProductBehavior, ProductBehaviorGap, ProductBehaviorContradiction, BehaviorMap,
|
|
4
|
+
BehaviorMapInput, ProductSourceFinding, CompassEvidenceRef,
|
|
5
|
+
} from "./types"
|
|
6
|
+
|
|
7
|
+
// ── Deterministic behaviour mapping from source findings ─────────────────────
|
|
8
|
+
|
|
9
|
+
export function mapBehaviorsFromFindings(
|
|
10
|
+
findings: ProductSourceFinding[],
|
|
11
|
+
input: BehaviorMapInput = {},
|
|
12
|
+
): BehaviorMap {
|
|
13
|
+
const behaviors: ProductBehavior[] = []
|
|
14
|
+
const gaps: ProductBehaviorGap[] = []
|
|
15
|
+
const contradictions: ProductBehaviorContradiction[] = []
|
|
16
|
+
|
|
17
|
+
// Group CLI commands into behaviours
|
|
18
|
+
const cliFindings = findings.filter(f => f.kind === "cli")
|
|
19
|
+
for (const f of cliFindings) {
|
|
20
|
+
behaviors.push({
|
|
21
|
+
id: `behavior-cli-${f.id}`,
|
|
22
|
+
area: inferArea(f),
|
|
23
|
+
name: f.title,
|
|
24
|
+
description: f.summary,
|
|
25
|
+
current_behavior: f.summary,
|
|
26
|
+
evidence: [findingToRef(f)],
|
|
27
|
+
basis: ["implemented"],
|
|
28
|
+
confidence: f.confidence,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Extract documented user flows from docs findings
|
|
33
|
+
const docsFindings = findings.filter(f => f.kind === "docs" && f.tags.includes("cli"))
|
|
34
|
+
for (const f of docsFindings) {
|
|
35
|
+
// Only add if not already covered by a CLI finding
|
|
36
|
+
const alreadyPresent = behaviors.some(b =>
|
|
37
|
+
b.current_behavior.toLowerCase().includes(extractCommand(f.summary).toLowerCase()) &&
|
|
38
|
+
extractCommand(f.summary).length > 3,
|
|
39
|
+
)
|
|
40
|
+
if (!alreadyPresent && extractCommand(f.summary)) {
|
|
41
|
+
behaviors.push({
|
|
42
|
+
id: `behavior-docs-${f.id}`,
|
|
43
|
+
area: inferArea(f),
|
|
44
|
+
name: `Documented: ${f.title}`,
|
|
45
|
+
description: f.summary,
|
|
46
|
+
current_behavior: f.summary,
|
|
47
|
+
evidence: [findingToRef(f)],
|
|
48
|
+
basis: ["documented"],
|
|
49
|
+
confidence: f.confidence * 0.9,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cross-reference: documented claims without implementation
|
|
55
|
+
const docsHeadings = findings.filter(f => f.kind === "docs" && !f.tags.includes("cli"))
|
|
56
|
+
const implementedAreas = new Set(behaviors.map(b => b.area))
|
|
57
|
+
|
|
58
|
+
// Detect gaps: central product promises with no CLI surface
|
|
59
|
+
const EXPECTED_AREAS = ["onboarding", "chronicle", "advisor", "review"]
|
|
60
|
+
for (const expected of EXPECTED_AREAS) {
|
|
61
|
+
const hasBehavior = behaviors.some(b => b.area === expected || b.name.toLowerCase().includes(expected))
|
|
62
|
+
if (!hasBehavior) {
|
|
63
|
+
const docRef = docsHeadings.find(f => f.summary.toLowerCase().includes(expected))
|
|
64
|
+
gaps.push({
|
|
65
|
+
id: `gap-${expected}`,
|
|
66
|
+
area: expected,
|
|
67
|
+
gap: `No first-class CLI command found for '${expected}'.`,
|
|
68
|
+
why_it_matters: `'${expected}' appears in product docs but has no dedicated CLI surface.`,
|
|
69
|
+
evidence: docRef ? [findingToRef(docRef)] : [],
|
|
70
|
+
confidence: 0.7,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Gap: no product-direction module (Compass itself)
|
|
76
|
+
const hasCompass = behaviors.some(b => b.name.toLowerCase().includes("compass"))
|
|
77
|
+
if (!hasCompass) {
|
|
78
|
+
gaps.push({
|
|
79
|
+
id: "gap-product-direction",
|
|
80
|
+
area: "product direction",
|
|
81
|
+
gap: "No product behaviour mapping or direction module currently exists.",
|
|
82
|
+
why_it_matters: "Quorum helps agents avoid repeating engineering mistakes, but has no module to help avoid repeating product-direction mistakes.",
|
|
83
|
+
evidence: [],
|
|
84
|
+
confidence: 0.93,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Filter by area if provided
|
|
89
|
+
const filteredBehaviors = input.area
|
|
90
|
+
? behaviors.filter(b =>
|
|
91
|
+
b.area.toLowerCase().includes(input.area!.toLowerCase()) ||
|
|
92
|
+
b.name.toLowerCase().includes(input.area!.toLowerCase()),
|
|
93
|
+
)
|
|
94
|
+
: behaviors
|
|
95
|
+
|
|
96
|
+
const overallConfidence = filteredBehaviors.length === 0
|
|
97
|
+
? 0.5
|
|
98
|
+
: filteredBehaviors.reduce((s, b) => s + b.confidence, 0) / filteredBehaviors.length
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
generated_at: new Date().toISOString(),
|
|
102
|
+
area: input.area,
|
|
103
|
+
behaviors: filteredBehaviors,
|
|
104
|
+
gaps: input.area
|
|
105
|
+
? gaps.filter(g => g.area.toLowerCase().includes(input.area!.toLowerCase()))
|
|
106
|
+
: gaps,
|
|
107
|
+
contradictions,
|
|
108
|
+
confidence: Math.round(overallConfidence * 100) / 100,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Extract documented behaviors for LLM context ─────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function summarizeBehaviorMap(map: BehaviorMap): string {
|
|
115
|
+
const lines: string[] = []
|
|
116
|
+
|
|
117
|
+
if (map.behaviors.length > 0) {
|
|
118
|
+
lines.push("## Current behaviours")
|
|
119
|
+
for (const b of map.behaviors.slice(0, 20)) {
|
|
120
|
+
lines.push(` ✓ ${b.current_behavior.slice(0, 100)}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (map.gaps.length > 0) {
|
|
125
|
+
lines.push("\n## Gaps")
|
|
126
|
+
for (const g of map.gaps) {
|
|
127
|
+
lines.push(` ? ${g.gap}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join("\n")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function findingToRef(f: ProductSourceFinding): CompassEvidenceRef {
|
|
137
|
+
return {
|
|
138
|
+
id: f.id,
|
|
139
|
+
kind: f.kind,
|
|
140
|
+
source: f.source,
|
|
141
|
+
path: f.path,
|
|
142
|
+
summary: f.summary,
|
|
143
|
+
confidence: f.confidence,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function inferArea(f: ProductSourceFinding): string {
|
|
148
|
+
if (f.tags.includes("onboarding") || f.tags.includes("init")) return "onboarding"
|
|
149
|
+
if (f.tags.includes("chronicle") || f.tags.includes("commit") || f.tags.includes("proposal")) return "chronicle review"
|
|
150
|
+
if (f.tags.includes("advisor")) return "memory retrieval"
|
|
151
|
+
if (f.tags.includes("sentinel")) return "coverage"
|
|
152
|
+
if (f.tags.includes("compass")) return "product direction"
|
|
153
|
+
if (f.tags.includes("auth")) return "auth"
|
|
154
|
+
if (f.tags.includes("cli")) return "cli"
|
|
155
|
+
return "general"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractCommand(text: string): string {
|
|
159
|
+
const match = text.match(/`(quorum [^`]+)`/)
|
|
160
|
+
return match?.[1] ?? ""
|
|
161
|
+
}
|