@balpal4495/quorum 1.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 +304 -193
- package/SETUP.md +60 -96
- package/bin/commands/compass.js +422 -0
- package/bin/commands/init.js +65 -60
- 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 +21 -11
- package/bin/init.js +0 -378
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { c } from "../shared/colors.js"
|
|
4
|
+
import { findChronicleDir } from "../shared/chronicle.js"
|
|
5
|
+
import { detectProvider } from "../shared/llm.js"
|
|
6
|
+
|
|
7
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function help() {
|
|
10
|
+
console.log(`
|
|
11
|
+
${c.bold("quorum compass")} — product-direction synthesis
|
|
12
|
+
|
|
13
|
+
${c.bold("Usage:")}
|
|
14
|
+
quorum compass <subcommand> [options]
|
|
15
|
+
|
|
16
|
+
${c.bold("Subcommands:")}
|
|
17
|
+
brief Summarise current product direction (LLM)
|
|
18
|
+
map Map current product behaviours from code + docs (no LLM)
|
|
19
|
+
behavior Answer a product-behaviour question
|
|
20
|
+
opportunities List gaps and opportunities from the behaviour map
|
|
21
|
+
pathways Generate product pathways toward a goal (LLM)
|
|
22
|
+
bets Generate strategic big bets (LLM)
|
|
23
|
+
score <idea> Score a product idea (LLM)
|
|
24
|
+
spec <title> Generate a lightweight product brief (LLM)
|
|
25
|
+
propose Stage a Chronicle entry from a Compass artifact
|
|
26
|
+
outcome Record the outcome of a prior bet or pathway
|
|
27
|
+
|
|
28
|
+
${c.bold("Options:")}
|
|
29
|
+
--area <tag> Focus on a specific product area
|
|
30
|
+
--goal <text> Goal for pathways / bets
|
|
31
|
+
--horizon <text> Horizon for bets (e.g. "6 months")
|
|
32
|
+
--appetite small|medium|large
|
|
33
|
+
--limit <n> Max results to return
|
|
34
|
+
--json Output raw JSON
|
|
35
|
+
--help Show this help
|
|
36
|
+
|
|
37
|
+
${c.bold("Examples:")}
|
|
38
|
+
quorum compass brief
|
|
39
|
+
quorum compass map
|
|
40
|
+
quorum compass map --area advisor
|
|
41
|
+
quorum compass pathways --goal "onboard new agents faster"
|
|
42
|
+
quorum compass bets --horizon "6 months"
|
|
43
|
+
quorum compass score "add Slack integration"
|
|
44
|
+
quorum compass spec "Smart retry backoff"
|
|
45
|
+
quorum compass opportunities --limit 5
|
|
46
|
+
quorum compass propose --from-last
|
|
47
|
+
quorum compass outcome --entry-id <id> --result validated`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function renderBrief(brief) {
|
|
53
|
+
console.log(`\n${c.bold("Compass Brief")} ${c.dim(`(confidence: ${(brief.confidence * 100).toFixed(0)}%)`)}`)
|
|
54
|
+
console.log(`\n${c.bold("Direction:")} ${brief.product_direction}`)
|
|
55
|
+
|
|
56
|
+
if (brief.known_from_chronicle?.length) {
|
|
57
|
+
console.log(`\n${c.bold("From Chronicle:")}`)
|
|
58
|
+
brief.known_from_chronicle.forEach(item => console.log(` ${c.green("✓")} ${item}`))
|
|
59
|
+
}
|
|
60
|
+
if (brief.known_from_behavior?.length) {
|
|
61
|
+
console.log(`\n${c.bold("From code/docs:")}`)
|
|
62
|
+
brief.known_from_behavior.slice(0, 6).forEach(item => console.log(` ${c.green("✓")} ${item}`))
|
|
63
|
+
}
|
|
64
|
+
if (brief.inferred?.length) {
|
|
65
|
+
console.log(`\n${c.bold("Inferred:")}`)
|
|
66
|
+
brief.inferred.forEach(item => console.log(` ${c.yellow("~")} ${item}`))
|
|
67
|
+
}
|
|
68
|
+
if (brief.unknowns?.length) {
|
|
69
|
+
console.log(`\n${c.bold("Unknowns:")}`)
|
|
70
|
+
brief.unknowns.forEach(item => console.log(` ${c.dim("?")} ${item}`))
|
|
71
|
+
}
|
|
72
|
+
if (brief.opportunities?.length) {
|
|
73
|
+
console.log(`\n${c.bold("Opportunities:")}`)
|
|
74
|
+
brief.opportunities.slice(0, 4).forEach(o => console.log(` ${c.cyan("→")} ${o.title}`))
|
|
75
|
+
}
|
|
76
|
+
if (brief.recommended_next_step) {
|
|
77
|
+
console.log(`\n${c.bold("Next step:")} ${brief.recommended_next_step}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderBehaviorMap(map) {
|
|
82
|
+
console.log(`\n${c.bold("Behaviour Map")} ${map.area ? c.dim(`(area: ${map.area})`) : ""} ${c.dim(`(confidence: ${(map.confidence * 100).toFixed(0)}%)`)}`)
|
|
83
|
+
|
|
84
|
+
if (map.behaviors.length > 0) {
|
|
85
|
+
console.log(`\n${c.bold(`Behaviours (${map.behaviors.length}):`)}`)
|
|
86
|
+
map.behaviors.slice(0, 20).forEach(b => {
|
|
87
|
+
console.log(` ${c.green("✓")} ${b.current_behavior.slice(0, 100)}`)
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`\n ${c.dim("No behaviours found.")}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (map.gaps.length > 0) {
|
|
94
|
+
console.log(`\n${c.bold(`Gaps (${map.gaps.length}):`)}`)
|
|
95
|
+
map.gaps.forEach(g => {
|
|
96
|
+
console.log(` ${c.yellow("?")} [${g.area}] ${g.gap}`)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (map.contradictions?.length) {
|
|
101
|
+
console.log(`\n${c.bold(`Contradictions (${map.contradictions.length}):`)}`)
|
|
102
|
+
map.contradictions.slice(0, 5).forEach(ct => {
|
|
103
|
+
console.log(` ${c.red("!")} ${ct.description ?? JSON.stringify(ct).slice(0, 80)}`)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderPathways(pathways) {
|
|
109
|
+
console.log(`\n${c.bold(`Pathways (${pathways.length})`)}`)
|
|
110
|
+
pathways.forEach((p, i) => {
|
|
111
|
+
const score = p.scores?.total ?? "?"
|
|
112
|
+
const label =
|
|
113
|
+
score >= 85 ? c.green(`${score}`) :
|
|
114
|
+
score >= 70 ? c.cyan(`${score}`) :
|
|
115
|
+
score >= 55 ? c.yellow(`${score}`) :
|
|
116
|
+
c.dim(`${score}`)
|
|
117
|
+
|
|
118
|
+
console.log(`\n${c.bold(`${i + 1}. ${p.title}`)} ${c.dim("[")}${label}${c.dim("]")}`)
|
|
119
|
+
if (p.opportunity) console.log(` ${p.opportunity}`)
|
|
120
|
+
if (p.smallest_useful_version) console.log(` ${c.dim("Start:")} ${p.smallest_useful_version}`)
|
|
121
|
+
if (p.suggested_next_step) console.log(` ${c.dim("Next:")} ${p.suggested_next_step}`)
|
|
122
|
+
if (p.assumptions?.length) {
|
|
123
|
+
console.log(` ${c.dim("Assumes:")} ${p.assumptions[0]}`)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderBets(bets) {
|
|
129
|
+
console.log(`\n${c.bold(`Strategic Bets (${bets.length})`)}`)
|
|
130
|
+
bets.forEach((b, i) => {
|
|
131
|
+
const score = b.scores?.total ?? "?"
|
|
132
|
+
console.log(`\n${c.bold(`${i + 1}. ${b.title}`)} ${c.dim(`[${score}]`)}`)
|
|
133
|
+
console.log(` ${c.dim("Thesis:")} ${b.thesis}`)
|
|
134
|
+
if (b.first_experiment) console.log(` ${c.dim("First test:")} ${b.first_experiment}`)
|
|
135
|
+
if (b.kill_criteria?.length) console.log(` ${c.red("Kill if:")} ${b.kill_criteria[0]}`)
|
|
136
|
+
if (b.assumptions?.length) console.log(` ${c.dim("Assumes:")} ${b.assumptions[0]}`)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderScore(score) {
|
|
141
|
+
const total = score.scores?.total ?? 0
|
|
142
|
+
const label =
|
|
143
|
+
total >= 85 ? c.green("Very strong — pursue") :
|
|
144
|
+
total >= 70 ? c.cyan("Strong — pursue small test") :
|
|
145
|
+
total >= 55 ? c.yellow("Plausible — investigate more") :
|
|
146
|
+
total >= 40 ? c.dim("Weak — defer") :
|
|
147
|
+
c.red("Avoid")
|
|
148
|
+
|
|
149
|
+
console.log(`\n${c.bold(`Score: ${total}/100`)} — ${label}`)
|
|
150
|
+
console.log(`Idea: ${score.idea}`)
|
|
151
|
+
if (score.summary) console.log(`Summary: ${score.summary}`)
|
|
152
|
+
|
|
153
|
+
if (score.supporting_reasons?.length) {
|
|
154
|
+
console.log(`\n${c.bold("Strengths:")}`)
|
|
155
|
+
score.supporting_reasons.forEach(r => console.log(` ${c.green("+")} ${r}`))
|
|
156
|
+
}
|
|
157
|
+
if (score.risks?.length) {
|
|
158
|
+
console.log(`\n${c.bold("Risks:")}`)
|
|
159
|
+
score.risks.forEach(r => console.log(` ${c.red("-")} ${r}`))
|
|
160
|
+
}
|
|
161
|
+
if (score.open_questions?.length) {
|
|
162
|
+
console.log(`\n${c.bold("Open questions:")}`)
|
|
163
|
+
score.open_questions.forEach(q => console.log(` ${c.dim("?")} ${q}`))
|
|
164
|
+
}
|
|
165
|
+
if (score.suggested_next_step) {
|
|
166
|
+
console.log(`\n${c.bold("Next step:")} ${score.suggested_next_step}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderOpportunities(opps) {
|
|
171
|
+
if (!opps.length) {
|
|
172
|
+
console.log(c.dim("\nNo gaps or opportunities found from current sources."))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n${c.bold(`Opportunities (${opps.length})`)}`)
|
|
176
|
+
opps.forEach((o, i) => {
|
|
177
|
+
const conf = `${(o.confidence * 100).toFixed(0)}%`
|
|
178
|
+
console.log(`\n${c.bold(`${i + 1}. ${o.title}`)} ${c.dim(`[${o.area}] [${o.evidence_strength}] [${conf}]`)}`)
|
|
179
|
+
if (o.why_it_matters) console.log(` ${o.why_it_matters}`)
|
|
180
|
+
if (o.suggested_next_step) console.log(` ${c.dim("Next:")} ${o.suggested_next_step}`)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderProductBrief(brief) {
|
|
185
|
+
console.log(`\n${c.bold(`Product Brief: ${brief.title}`)}`)
|
|
186
|
+
if (brief.problem) console.log(`\n${c.bold("Problem:")} ${brief.problem}`)
|
|
187
|
+
if (brief.target_user) console.log(`${c.bold("Target user:")} ${brief.target_user}`)
|
|
188
|
+
if (brief.recommended_solution) {
|
|
189
|
+
console.log(`\n${c.bold("Recommended solution:")}`)
|
|
190
|
+
console.log(` ${brief.recommended_solution}`)
|
|
191
|
+
}
|
|
192
|
+
if (brief.smallest_useful_version) {
|
|
193
|
+
console.log(`\n${c.bold("Smallest useful version:")}`)
|
|
194
|
+
console.log(` ${brief.smallest_useful_version}`)
|
|
195
|
+
}
|
|
196
|
+
if (brief.non_goals?.length) {
|
|
197
|
+
console.log(`\n${c.bold("Non-goals:")}`)
|
|
198
|
+
brief.non_goals.forEach(g => console.log(` ${c.dim("✗")} ${g}`))
|
|
199
|
+
}
|
|
200
|
+
if (brief.risks?.length) {
|
|
201
|
+
console.log(`\n${c.bold("Risks:")}`)
|
|
202
|
+
brief.risks.forEach(r => console.log(` ${c.red("-")} ${r}`))
|
|
203
|
+
}
|
|
204
|
+
if (brief.open_questions?.length) {
|
|
205
|
+
console.log(`\n${c.bold("Open questions:")}`)
|
|
206
|
+
brief.open_questions.forEach(q => console.log(` ${c.dim("?")} ${q}`))
|
|
207
|
+
}
|
|
208
|
+
if (brief.suggested_quorum_checks?.length) {
|
|
209
|
+
console.log(`\n${c.bold("Quorum checks:")}`)
|
|
210
|
+
brief.suggested_quorum_checks.forEach(ch => console.log(` ${c.cyan("$")} ${ch}`))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Last-run artifact cache (used by --from-last) ─────────────────────────────
|
|
215
|
+
|
|
216
|
+
let _lastArtifact = null
|
|
217
|
+
|
|
218
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export async function run(argv) {
|
|
221
|
+
const [subcommand, ...rest] = argv
|
|
222
|
+
|
|
223
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
224
|
+
help()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const flags = {}
|
|
229
|
+
const positional = []
|
|
230
|
+
for (let i = 0; i < rest.length; i++) {
|
|
231
|
+
const a = rest[i]
|
|
232
|
+
if (a.startsWith("--")) {
|
|
233
|
+
const key = a.slice(2)
|
|
234
|
+
const val = rest[i + 1] && !rest[i + 1].startsWith("--") ? rest[++i] : true
|
|
235
|
+
flags[key] = val
|
|
236
|
+
} else {
|
|
237
|
+
positional.push(a)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const area = flags["area"]
|
|
242
|
+
const goal = flags["goal"] || positional.join(" ") || undefined
|
|
243
|
+
const horizon = flags["horizon"] || undefined
|
|
244
|
+
const appetite = flags["appetite"] || undefined
|
|
245
|
+
const limitN = flags["limit"] ? parseInt(flags["limit"], 10) : undefined
|
|
246
|
+
const jsonMode = Boolean(flags["json"])
|
|
247
|
+
const entryId = flags["entry-id"] || flags["entryId"] || undefined
|
|
248
|
+
const result = flags["result"] || undefined
|
|
249
|
+
|
|
250
|
+
// ── Load Compass module ────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
const rootDir = process.cwd()
|
|
253
|
+
const chronicleDir = findChronicleDir(rootDir)
|
|
254
|
+
|
|
255
|
+
if (!chronicleDir) {
|
|
256
|
+
console.error(c.red("Error: Chronicle not found. Run 'quorum init' first."))
|
|
257
|
+
process.exit(1)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Lazy import to keep CLI startup fast
|
|
261
|
+
const { createCompass } = await import("../../modules/compass/create.js")
|
|
262
|
+
const { defaultSources } = await import("../../modules/compass/sources/index.js")
|
|
263
|
+
const { createOracleClient } = await import("../../modules/oracle/index.js")
|
|
264
|
+
const { createLanceDBStore } = await import("../../modules/oracle/adapters/lance-db.js")
|
|
265
|
+
const { xenovaEmbed } = await import("../../modules/oracle/adapters/xenova-embedder.js")
|
|
266
|
+
|
|
267
|
+
const vectorStore = await createLanceDBStore(chronicleDir)
|
|
268
|
+
const oracle = createOracleClient({ embedder: xenovaEmbed, vectorStore, chronicleDir })
|
|
269
|
+
|
|
270
|
+
// Only load LLM for subcommands that need it
|
|
271
|
+
const NO_LLM_CMDS = new Set(["map", "opportunities"])
|
|
272
|
+
const llm = NO_LLM_CMDS.has(subcommand) ? undefined : detectProvider()
|
|
273
|
+
|
|
274
|
+
const compass = createCompass({
|
|
275
|
+
oracle,
|
|
276
|
+
llm,
|
|
277
|
+
rootDir,
|
|
278
|
+
chronicleDir,
|
|
279
|
+
sources: defaultSources(),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// ── Route subcommand ───────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
switch (subcommand) {
|
|
286
|
+
case "brief": {
|
|
287
|
+
const data = await compass.brief({ area })
|
|
288
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
289
|
+
renderBrief(data)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "map": {
|
|
294
|
+
const data = await compass.mapBehaviors({ area })
|
|
295
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
296
|
+
renderBehaviorMap(data)
|
|
297
|
+
break
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case "behavior": {
|
|
301
|
+
const question = goal || positional.join(" ")
|
|
302
|
+
if (!question) {
|
|
303
|
+
console.error(c.red('Error: provide a question, e.g. quorum compass behavior "what does quorum do for onboarding?"'))
|
|
304
|
+
process.exit(1)
|
|
305
|
+
}
|
|
306
|
+
const data = await compass.behavior({ question, area })
|
|
307
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
308
|
+
console.log(`\n${c.bold("Behaviour answer:")} ${data.product_implication}`)
|
|
309
|
+
if (data.what_exists?.length) {
|
|
310
|
+
console.log(`\n${c.bold("What exists:")}`)
|
|
311
|
+
data.what_exists.forEach(e => console.log(` ${c.green("✓")} ${e}`))
|
|
312
|
+
}
|
|
313
|
+
if (data.what_appears_missing?.length) {
|
|
314
|
+
console.log(`\n${c.bold("Appears missing:")}`)
|
|
315
|
+
data.what_appears_missing.forEach(m => console.log(` ${c.yellow("?")} ${m}`))
|
|
316
|
+
}
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case "opportunities": {
|
|
321
|
+
const data = await compass.opportunities({ area, goal, limit: limitN })
|
|
322
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
323
|
+
renderOpportunities(data)
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case "pathways": {
|
|
328
|
+
if (!goal) {
|
|
329
|
+
console.error(c.red('Error: --goal is required. Example: quorum compass pathways --goal "onboard new agents faster"'))
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
const data = await compass.pathways({ goal, horizon, appetite, area, limit: limitN })
|
|
333
|
+
_lastArtifact = { kind: "product_pathway", items: data }
|
|
334
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
335
|
+
renderPathways(data)
|
|
336
|
+
console.log(c.dim("\nTip: run 'quorum compass propose --from-last' to stage a Chronicle entry."))
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case "bets": {
|
|
341
|
+
const data = await compass.bigBets({ horizon, goal, appetite })
|
|
342
|
+
_lastArtifact = { kind: "product_bet", items: data }
|
|
343
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
344
|
+
renderBets(data)
|
|
345
|
+
console.log(c.dim("\nTip: run 'quorum compass propose --from-last' to stage a Chronicle entry."))
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
case "score": {
|
|
350
|
+
const idea = goal || positional.join(" ")
|
|
351
|
+
if (!idea) {
|
|
352
|
+
console.error(c.red('Error: provide an idea. Example: quorum compass score "add Slack integration"'))
|
|
353
|
+
process.exit(1)
|
|
354
|
+
}
|
|
355
|
+
const data = await compass.scoreIdea({ idea })
|
|
356
|
+
_lastArtifact = { kind: "product_idea_score", items: [data] }
|
|
357
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
358
|
+
renderScore(data)
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case "spec": {
|
|
363
|
+
const title = goal || positional.join(" ")
|
|
364
|
+
if (!title) {
|
|
365
|
+
console.error(c.red('Error: provide a title. Example: quorum compass spec "Smart retry backoff"'))
|
|
366
|
+
process.exit(1)
|
|
367
|
+
}
|
|
368
|
+
const data = await compass.productBrief({ title })
|
|
369
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
370
|
+
renderProductBrief(data)
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case "propose": {
|
|
375
|
+
if (flags["from-last"]) {
|
|
376
|
+
if (!_lastArtifact?.items?.length) {
|
|
377
|
+
console.error(c.red("Error: no Compass artifact in memory. Run pathways/bets/score first in the same session."))
|
|
378
|
+
process.exit(1)
|
|
379
|
+
}
|
|
380
|
+
const item = _lastArtifact.items[0]
|
|
381
|
+
const result = await compass.propose({ artifact_kind: _lastArtifact.kind, payload: item })
|
|
382
|
+
console.log(c.green(`\n✓ ${result.message}`))
|
|
383
|
+
break
|
|
384
|
+
}
|
|
385
|
+
console.error(c.red('Error: provide --from-last. Example: quorum compass propose --from-last'))
|
|
386
|
+
process.exit(1)
|
|
387
|
+
break
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
case "outcome": {
|
|
391
|
+
if (!entryId) {
|
|
392
|
+
console.error(c.red("Error: --entry-id is required. Example: quorum compass outcome --entry-id abc123 --result validated"))
|
|
393
|
+
process.exit(1)
|
|
394
|
+
}
|
|
395
|
+
if (!result) {
|
|
396
|
+
console.error(c.red("Error: --result is required. Values: validated, partially-validated, invalidated, unclear, superseded"))
|
|
397
|
+
process.exit(1)
|
|
398
|
+
}
|
|
399
|
+
const note = flags["note"] || undefined
|
|
400
|
+
const data = await compass.outcome({ entry_id: entryId, result, note })
|
|
401
|
+
if (jsonMode) { console.log(JSON.stringify(data, null, 2)); break }
|
|
402
|
+
console.log(c.green(`\n✓ ${data.message}`))
|
|
403
|
+
break
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
default: {
|
|
407
|
+
console.error(c.red(`Unknown subcommand: ${subcommand}`))
|
|
408
|
+
help()
|
|
409
|
+
process.exit(1)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
if (err.message?.includes("LLM provider is required") || err.message?.includes("No LLM provider")) {
|
|
414
|
+
console.error(c.red(`\nError: ${err.message}`))
|
|
415
|
+
console.error(c.dim("Set ANTHROPIC_API_KEY or OPENAI_API_KEY to use this subcommand."))
|
|
416
|
+
} else {
|
|
417
|
+
console.error(c.red(`\nCompass error: ${err.message ?? err}`))
|
|
418
|
+
if (process.env.DEBUG) console.error(err.stack)
|
|
419
|
+
}
|
|
420
|
+
process.exit(1)
|
|
421
|
+
}
|
|
422
|
+
}
|
package/bin/commands/init.js
CHANGED
|
@@ -10,12 +10,6 @@ const _require = createRequire(import.meta.url)
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
11
|
const QUORUM_ROOT = path.resolve(__dirname, "../..")
|
|
12
12
|
|
|
13
|
-
const DEPS = { zod: "^3.23.0" }
|
|
14
|
-
const OPTIONAL_DEPS = {
|
|
15
|
-
vectordb: "^0.4.0",
|
|
16
|
-
"@xenova/transformers": "^2.17.0",
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
async function exists(p) {
|
|
20
14
|
return fs.access(p).then(() => true).catch(() => false)
|
|
21
15
|
}
|
|
@@ -29,25 +23,30 @@ function geminiAvailable() {
|
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
async function guardAlreadyInitialized(target) {
|
|
32
|
-
if (await exists(path.join(target, "quorum"
|
|
26
|
+
if (await exists(path.join(target, ".quorum-version"))) {
|
|
33
27
|
console.log(c.yellow("\nQuorum is already initialized in this project."))
|
|
34
|
-
console.log("
|
|
28
|
+
console.log("Run 'npm update @balpal4495/quorum' to upgrade to the latest version.\n")
|
|
35
29
|
process.exit(0)
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
|
|
39
|
-
async function
|
|
40
|
-
log.section("
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
async function writeQuorumDocs(target) {
|
|
34
|
+
log.section("Writing Quorum docs")
|
|
35
|
+
await fs.mkdir(path.join(target, "quorum"), { recursive: true })
|
|
36
|
+
// Host-facing CLAUDE.md — CLI-first operational guide, not module internals
|
|
37
|
+
const claudeSrc = path.join(QUORUM_ROOT, "bin", "templates", "CLAUDE.md")
|
|
38
|
+
const claudeDest = path.join(target, "quorum", "CLAUDE.md")
|
|
39
|
+
if (await exists(claudeSrc)) {
|
|
40
|
+
await fs.copyFile(claudeSrc, claudeDest)
|
|
41
|
+
log.created("quorum/CLAUDE.md")
|
|
42
|
+
}
|
|
43
|
+
// AGENTS.md — module file ownership map
|
|
44
|
+
const agentsSrc = path.join(QUORUM_ROOT, "modules", "AGENTS.md")
|
|
45
|
+
const agentsDest = path.join(target, "quorum", "AGENTS.md")
|
|
46
|
+
if (await exists(agentsSrc)) {
|
|
47
|
+
await fs.copyFile(agentsSrc, agentsDest)
|
|
48
|
+
log.created("quorum/AGENTS.md")
|
|
49
|
+
}
|
|
51
50
|
await fs.copyFile(
|
|
52
51
|
path.join(QUORUM_ROOT, "SETUP.md"),
|
|
53
52
|
path.join(target, "quorum", "SETUP.md"),
|
|
@@ -55,11 +54,9 @@ async function copyModules(target) {
|
|
|
55
54
|
log.created("quorum/SETUP.md")
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await fs.cp(src, dest, { recursive: true })
|
|
62
|
-
log.created("quorum/evals/")
|
|
57
|
+
async function writeQuorumVersion(target, version) {
|
|
58
|
+
await fs.writeFile(path.join(target, ".quorum-version"), version + "\n", "utf8")
|
|
59
|
+
log.created(".quorum-version")
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
async function mergeCopilotInstructions(target) {
|
|
@@ -67,14 +64,15 @@ async function mergeCopilotInstructions(target) {
|
|
|
67
64
|
const src = path.join(QUORUM_ROOT, ".github", "copilot-instructions.md")
|
|
68
65
|
const dest = path.join(target, ".github", "copilot-instructions.md")
|
|
69
66
|
const content = await fs.readFile(src, "utf8")
|
|
67
|
+
const block = `<!-- quorum:start -->\n${content}\n<!-- quorum:end -->`
|
|
70
68
|
await fs.mkdir(path.join(target, ".github"), { recursive: true })
|
|
71
69
|
if (await exists(dest)) {
|
|
72
70
|
const existing = await fs.readFile(dest, "utf8")
|
|
73
|
-
if (existing.includes("<!-- quorum -->")) { log.skipped(".github/copilot-instructions.md (already present)"); return }
|
|
74
|
-
await fs.appendFile(dest, `\n\n---\n\n
|
|
71
|
+
if (existing.includes("<!-- quorum:start -->")) { log.skipped(".github/copilot-instructions.md (already present)"); return }
|
|
72
|
+
await fs.appendFile(dest, `\n\n---\n\n${block}`, "utf8")
|
|
75
73
|
log.appended(".github/copilot-instructions.md")
|
|
76
74
|
} else {
|
|
77
|
-
await fs.writeFile(dest,
|
|
75
|
+
await fs.writeFile(dest, block, "utf8")
|
|
78
76
|
log.created(".github/copilot-instructions.md")
|
|
79
77
|
}
|
|
80
78
|
}
|
|
@@ -82,13 +80,18 @@ async function mergeCopilotInstructions(target) {
|
|
|
82
80
|
async function mergeAgentsMd(target) {
|
|
83
81
|
const dest = path.join(target, "AGENTS.md")
|
|
84
82
|
const section = [
|
|
85
|
-
"",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
83
|
+
"",
|
|
84
|
+
"<!-- quorum:start -->",
|
|
85
|
+
"## Quorum",
|
|
86
|
+
"",
|
|
87
|
+
"See [quorum/AGENTS.md](quorum/AGENTS.md) for module file ownership and internals.",
|
|
88
|
+
"See [.github/copilot-instructions.md](.github/copilot-instructions.md) for workflow rules.",
|
|
89
|
+
"<!-- quorum:end -->",
|
|
90
|
+
"",
|
|
88
91
|
].join("\n")
|
|
89
92
|
if (await exists(dest)) {
|
|
90
93
|
const existing = await fs.readFile(dest, "utf8")
|
|
91
|
-
if (existing.includes("quorum
|
|
94
|
+
if (existing.includes("<!-- quorum:start -->")) { log.skipped("AGENTS.md (already present)"); return }
|
|
92
95
|
await fs.appendFile(dest, section, "utf8")
|
|
93
96
|
log.appended("AGENTS.md")
|
|
94
97
|
} else {
|
|
@@ -98,11 +101,12 @@ async function mergeAgentsMd(target) {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async function mergeClaudeMd(target) {
|
|
101
|
-
const dest
|
|
104
|
+
const dest = path.join(target, "CLAUDE.md")
|
|
102
105
|
const section = `
|
|
103
|
-
|
|
106
|
+
<!-- quorum:start -->
|
|
107
|
+
## Quorum
|
|
104
108
|
|
|
105
|
-
See [quorum/
|
|
109
|
+
See [quorum/CLAUDE.md](quorum/CLAUDE.md) for design decisions and invariants.
|
|
106
110
|
See [.github/copilot-instructions.md](.github/copilot-instructions.md) for workflow rules.
|
|
107
111
|
|
|
108
112
|
## Gemini CLI (optional assistant)
|
|
@@ -127,10 +131,11 @@ source ~/.zshrc && gemini -p "I'm about to change X. What should I watch out for
|
|
|
127
131
|
|
|
128
132
|
You reason about Gemini's output — it assists, you decide. Never pass its response to the
|
|
129
133
|
user unfiltered. If Gemini contradicts what you know from reading the code, trust your reading.
|
|
134
|
+
<!-- quorum:end -->
|
|
130
135
|
`
|
|
131
136
|
if (await exists(dest)) {
|
|
132
137
|
const existing = await fs.readFile(dest, "utf8")
|
|
133
|
-
if (existing.includes("quorum
|
|
138
|
+
if (existing.includes("<!-- quorum:start -->")) { log.skipped("CLAUDE.md (already present)"); return }
|
|
134
139
|
await fs.appendFile(dest, section, "utf8")
|
|
135
140
|
log.appended("CLAUDE.md")
|
|
136
141
|
} else {
|
|
@@ -147,7 +152,7 @@ async function mergeGeminiMd(target) {
|
|
|
147
152
|
if (await exists(src)) { await fs.copyFile(src, dest); log.created("GEMINI.md") }
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
async function updatePackageJson(target) {
|
|
155
|
+
async function updatePackageJson(target, version) {
|
|
151
156
|
log.section("Updating package.json")
|
|
152
157
|
const pkgPath = path.join(target, "package.json")
|
|
153
158
|
let pkg
|
|
@@ -157,30 +162,27 @@ async function updatePackageJson(target) {
|
|
|
157
162
|
pkg = { name: path.basename(target), version: "0.1.0", private: true }
|
|
158
163
|
log.warn("No package.json found — creating a minimal one")
|
|
159
164
|
}
|
|
160
|
-
pkg.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
for (const [name, version] of Object.entries(OPTIONAL_DEPS)) {
|
|
167
|
-
if (!pkg.optionalDependencies[name]) { pkg.optionalDependencies[name] = version; added.push(`${name} (optional)`) }
|
|
165
|
+
pkg.devDependencies = pkg.devDependencies ?? {}
|
|
166
|
+
const quorumRange = `^${version}`
|
|
167
|
+
if (pkg.devDependencies["@balpal4495/quorum"] || pkg.dependencies?.["@balpal4495/quorum"]) {
|
|
168
|
+
log.skipped("package.json (@balpal4495/quorum already present)")
|
|
169
|
+
return
|
|
168
170
|
}
|
|
171
|
+
pkg.devDependencies["@balpal4495/quorum"] = quorumRange
|
|
169
172
|
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8")
|
|
170
|
-
|
|
171
|
-
log.appended(`package.json — added: ${added.join(", ")}`)
|
|
172
|
-
} else {
|
|
173
|
-
log.skipped("package.json (all deps already present)")
|
|
174
|
-
}
|
|
173
|
+
log.appended(`package.json — added @balpal4495/quorum@${quorumRange} to devDependencies`)
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
async function updateGitignore(target) {
|
|
178
177
|
log.section("Updating .gitignore")
|
|
179
178
|
const dest = path.join(target, ".gitignore")
|
|
180
179
|
const block = [
|
|
181
|
-
"",
|
|
180
|
+
"",
|
|
181
|
+
"# Quorum — Chronicle",
|
|
182
182
|
"# entries/ is a binary vector store — do not commit",
|
|
183
|
-
".chronicle/entries/",
|
|
183
|
+
".chronicle/entries/",
|
|
184
|
+
".chronicle/query-log.jsonl",
|
|
185
|
+
"",
|
|
184
186
|
].join("\n")
|
|
185
187
|
if (await exists(dest)) {
|
|
186
188
|
const existing = await fs.readFile(dest, "utf8")
|
|
@@ -195,7 +197,7 @@ async function updateGitignore(target) {
|
|
|
195
197
|
|
|
196
198
|
async function createChronicle(target) {
|
|
197
199
|
log.section("Creating Chronicle")
|
|
198
|
-
await fs.mkdir(path.join(target, ".chronicle", "proposals"),
|
|
200
|
+
await fs.mkdir(path.join(target, ".chronicle", "proposals"), { recursive: true })
|
|
199
201
|
log.created(".chronicle/proposals/")
|
|
200
202
|
await fs.mkdir(path.join(target, ".chronicle", "committed"), { recursive: true })
|
|
201
203
|
log.created(".chronicle/committed/")
|
|
@@ -213,30 +215,33 @@ export async function run(PKG_VERSION) {
|
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
await guardAlreadyInitialized(target)
|
|
216
|
-
await
|
|
217
|
-
await copyEvals(target)
|
|
218
|
+
await writeQuorumDocs(target)
|
|
218
219
|
await mergeCopilotInstructions(target)
|
|
219
220
|
await mergeAgentsMd(target)
|
|
220
221
|
await mergeClaudeMd(target)
|
|
221
222
|
await mergeGeminiMd(target)
|
|
222
|
-
await updatePackageJson(target)
|
|
223
|
+
await updatePackageJson(target, PKG_VERSION)
|
|
223
224
|
await updateGitignore(target)
|
|
224
225
|
await createChronicle(target)
|
|
226
|
+
await writeQuorumVersion(target, PKG_VERSION)
|
|
225
227
|
|
|
226
228
|
const hasGemini = geminiAvailable()
|
|
227
229
|
|
|
228
|
-
console.log(`\n${c.green("✓ Quorum initialized.")}`)
|
|
230
|
+
console.log(`\n${c.green("✓ Quorum initialized.")} ${c.dim(`(v${PKG_VERSION})`)}`)
|
|
229
231
|
console.log("\nNext steps:")
|
|
230
232
|
console.log(" 1. npm install")
|
|
231
|
-
console.log(" 2.
|
|
232
|
-
console.log(c.dim(
|
|
233
|
+
console.log(" 2. Use the CLI:")
|
|
234
|
+
console.log(c.dim(" quorum advisor brief"))
|
|
235
|
+
console.log(c.dim(' quorum advisor "what has the team decided about X?"'))
|
|
236
|
+
console.log(c.dim(" quorum check --outcome '...' --design '...'"))
|
|
237
|
+
console.log("\n For programmatic use:")
|
|
238
|
+
console.log(c.dim(' import { setup } from "@balpal4495/quorum"'))
|
|
233
239
|
console.log(c.dim(' const { oracle, evaluate, deliberate } = await setup({ llm: yourProvider })'))
|
|
234
240
|
console.log("\n Or tell your AI: \"follow quorum/SETUP.md\"")
|
|
235
241
|
|
|
236
242
|
if (!hasGemini) {
|
|
237
243
|
console.log(`\n ${c.dim("Optional: install Gemini CLI for large-context assistance")}`)
|
|
238
244
|
console.log(c.dim(" npm install -g @google/gemini-cli + set GEMINI_API_KEY"))
|
|
239
|
-
console.log(c.dim(" See quorum/SETUP.md Step 10 for details."))
|
|
240
245
|
} else {
|
|
241
246
|
console.log(`\n ${c.green("✓ Gemini CLI detected")} — GEMINI.md written. Set GEMINI_API_KEY if not already set.`)
|
|
242
247
|
}
|