@simonren/quorum 0.7.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/LICENSE +21 -0
- package/README.md +144 -0
- package/commands/multi-consult.md +109 -0
- package/commands/multi-review.md +139 -0
- package/dist/adapters/base.d.ts +120 -0
- package/dist/adapters/base.js +98 -0
- package/dist/adapters/claude.d.ts +25 -0
- package/dist/adapters/claude.js +217 -0
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +227 -0
- package/dist/adapters/gemini.d.ts +20 -0
- package/dist/adapters/gemini.js +197 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +15 -0
- package/dist/cli/check.d.ts +20 -0
- package/dist/cli/check.js +78 -0
- package/dist/cli/codex.d.ts +11 -0
- package/dist/cli/codex.js +255 -0
- package/dist/cli/gemini.d.ts +12 -0
- package/dist/cli/gemini.js +253 -0
- package/dist/commands.d.ts +28 -0
- package/dist/commands.js +105 -0
- package/dist/config.d.ts +244 -0
- package/dist/config.js +179 -0
- package/dist/consult-prompt.d.ts +10 -0
- package/dist/consult-prompt.js +72 -0
- package/dist/context.d.ts +1538 -0
- package/dist/context.js +383 -0
- package/dist/decoders/claude.d.ts +53 -0
- package/dist/decoders/claude.js +106 -0
- package/dist/decoders/codex.d.ts +71 -0
- package/dist/decoders/codex.js +145 -0
- package/dist/decoders/gemini.d.ts +33 -0
- package/dist/decoders/gemini.js +58 -0
- package/dist/decoders/index.d.ts +6 -0
- package/dist/decoders/index.js +3 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +192 -0
- package/dist/executor.d.ts +103 -0
- package/dist/executor.js +244 -0
- package/dist/handoff.d.ts +270 -0
- package/dist/handoff.js +599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +134 -0
- package/dist/pipeline.d.ts +135 -0
- package/dist/pipeline.js +462 -0
- package/dist/prompt-v2.d.ts +38 -0
- package/dist/prompt-v2.js +391 -0
- package/dist/prompt.d.ts +71 -0
- package/dist/prompt.js +309 -0
- package/dist/schema.d.ts +660 -0
- package/dist/schema.js +536 -0
- package/dist/tools/consult.d.ts +104 -0
- package/dist/tools/consult.js +220 -0
- package/dist/tools/feedback.d.ts +91 -0
- package/dist/tools/feedback.js +117 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +31 -0
- package/package.json +54 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration for quorum.
|
|
3
|
+
*
|
|
4
|
+
* Config file: ~/.config/quorum/config.json
|
|
5
|
+
*
|
|
6
|
+
* Semantics:
|
|
7
|
+
* - Lazy, cached load. `getConfig()` returns the cached config or reads once.
|
|
8
|
+
* - Missing file → defaults in memory (no write). Use `initConfig()` from the
|
|
9
|
+
* server entry point to create the file with defaults on first launch.
|
|
10
|
+
* - Invalid JSON or schema violations → fall back to defaults, warn on stderr.
|
|
11
|
+
* - Partial user configs are deep-merged against defaults via Zod `.default()`.
|
|
12
|
+
* - Tool-call arguments still override config (e.g. `reasoningEffort` on a
|
|
13
|
+
* single `codex_review` call). Config only sets defaults.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// SCHEMA
|
|
21
|
+
// =============================================================================
|
|
22
|
+
export const CodexConfigSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
model: z.string().default('gpt-5.5'),
|
|
25
|
+
reasoningEffort: z.enum(['high', 'xhigh']).default('high'),
|
|
26
|
+
serviceTier: z.enum(['default', 'fast', 'flex']).default('fast'),
|
|
27
|
+
/** Consult-specific defaults — separate from review knobs because consult
|
|
28
|
+
* questions are deeper and warrant more reasoning. Users can override
|
|
29
|
+
* these to cap cost without affecting review behavior. */
|
|
30
|
+
consultReasoningEffort: z.enum(['high', 'xhigh']).default('xhigh'),
|
|
31
|
+
consultServiceTier: z.enum(['default', 'fast', 'flex']).default('fast'),
|
|
32
|
+
inactivityTimeoutMs: z
|
|
33
|
+
.object({
|
|
34
|
+
high: z.number().int().positive().default(180_000),
|
|
35
|
+
xhigh: z.number().int().positive().default(300_000),
|
|
36
|
+
})
|
|
37
|
+
.default({}),
|
|
38
|
+
maxTimeoutMs: z.number().int().positive().default(3_600_000),
|
|
39
|
+
maxBufferSize: z.number().int().positive().default(1_048_576),
|
|
40
|
+
})
|
|
41
|
+
.default({});
|
|
42
|
+
export const ClaudeConfigSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
model: z.string().default('opus'),
|
|
45
|
+
inactivityTimeoutMs: z.number().int().positive().default(300_000),
|
|
46
|
+
maxTimeoutMs: z.number().int().positive().default(3_600_000),
|
|
47
|
+
maxBufferSize: z.number().int().positive().default(1_048_576),
|
|
48
|
+
})
|
|
49
|
+
.default({});
|
|
50
|
+
export const GeminiConfigSchema = z
|
|
51
|
+
.object({
|
|
52
|
+
model: z.string().nullable().default('gemini-3.1-pro-preview'),
|
|
53
|
+
inactivityTimeoutMs: z.number().int().positive().default(300_000),
|
|
54
|
+
maxTimeoutMs: z.number().int().positive().default(3_600_000),
|
|
55
|
+
maxBufferSize: z.number().int().positive().default(1_048_576),
|
|
56
|
+
})
|
|
57
|
+
.default({});
|
|
58
|
+
export const ConfigSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
codex: CodexConfigSchema,
|
|
61
|
+
claude: ClaudeConfigSchema,
|
|
62
|
+
gemini: GeminiConfigSchema,
|
|
63
|
+
})
|
|
64
|
+
.default({});
|
|
65
|
+
export const DEFAULT_CONFIG = ConfigSchema.parse({});
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// STATE
|
|
68
|
+
// =============================================================================
|
|
69
|
+
const DEFAULT_CONFIG_PATH = join(homedir(), '.config', 'quorum', 'config.json');
|
|
70
|
+
let _configPath = DEFAULT_CONFIG_PATH;
|
|
71
|
+
let _cached = null;
|
|
72
|
+
let _cachedMtimeMs = 0;
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// PUBLIC API
|
|
75
|
+
// =============================================================================
|
|
76
|
+
export function getConfigPath() {
|
|
77
|
+
return _configPath;
|
|
78
|
+
}
|
|
79
|
+
export function getConfig() {
|
|
80
|
+
// Hot-reload: re-read if the file's mtime has changed since last load.
|
|
81
|
+
if (_cached) {
|
|
82
|
+
try {
|
|
83
|
+
if (existsSync(_configPath)) {
|
|
84
|
+
const mtime = statSync(_configPath).mtimeMs;
|
|
85
|
+
if (mtime !== _cachedMtimeMs) {
|
|
86
|
+
_cached = loadConfigFromDisk(_configPath);
|
|
87
|
+
_cachedMtimeMs = mtime;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// statSync failure is non-fatal — keep using the cached config.
|
|
93
|
+
}
|
|
94
|
+
return _cached;
|
|
95
|
+
}
|
|
96
|
+
_cached = loadConfigFromDisk(_configPath);
|
|
97
|
+
if (existsSync(_configPath)) {
|
|
98
|
+
try {
|
|
99
|
+
_cachedMtimeMs = statSync(_configPath).mtimeMs;
|
|
100
|
+
}
|
|
101
|
+
catch { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
return _cached;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create the config file with defaults if it does not exist.
|
|
107
|
+
* Uses the exclusive `wx` flag for atomic creation — safe against TOCTOU races
|
|
108
|
+
* when multiple server instances start concurrently.
|
|
109
|
+
* Refreshes the cached config so subsequent `getConfig()` calls see disk state.
|
|
110
|
+
*/
|
|
111
|
+
export function initConfig() {
|
|
112
|
+
const path = _configPath;
|
|
113
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n', { encoding: 'utf-8', flag: 'wx' });
|
|
116
|
+
_cached = DEFAULT_CONFIG;
|
|
117
|
+
_cachedMtimeMs = statSync(path).mtimeMs;
|
|
118
|
+
return { path, created: true };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.code === 'EEXIST') {
|
|
122
|
+
_cached = loadConfigFromDisk(path);
|
|
123
|
+
try {
|
|
124
|
+
_cachedMtimeMs = statSync(path).mtimeMs;
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
return { path, created: false };
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Test-only hook. Redirects the config path and clears the cache. */
|
|
133
|
+
export function setConfigPathForTesting(path) {
|
|
134
|
+
_configPath = path ?? DEFAULT_CONFIG_PATH;
|
|
135
|
+
_cached = null;
|
|
136
|
+
_cachedMtimeMs = 0;
|
|
137
|
+
}
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// INTERNAL
|
|
140
|
+
// =============================================================================
|
|
141
|
+
/**
|
|
142
|
+
* Parse each adapter's config independently so a typo in one section only
|
|
143
|
+
* resets that adapter to defaults — the other adapters' settings survive.
|
|
144
|
+
*/
|
|
145
|
+
function loadConfigFromDisk(path) {
|
|
146
|
+
if (!existsSync(path))
|
|
147
|
+
return DEFAULT_CONFIG;
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
151
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
152
|
+
console.error(`[quorum] Config at ${path} is not a JSON object — using defaults.`);
|
|
153
|
+
return DEFAULT_CONFIG;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
158
|
+
console.error(`[quorum] Invalid JSON in ${path} — using defaults. Error: ${msg}`);
|
|
159
|
+
return DEFAULT_CONFIG;
|
|
160
|
+
}
|
|
161
|
+
const adapters = [
|
|
162
|
+
{ key: 'codex', schema: CodexConfigSchema },
|
|
163
|
+
{ key: 'claude', schema: ClaudeConfigSchema },
|
|
164
|
+
{ key: 'gemini', schema: GeminiConfigSchema },
|
|
165
|
+
];
|
|
166
|
+
const result = {};
|
|
167
|
+
for (const { key, schema } of adapters) {
|
|
168
|
+
const section = raw[key];
|
|
169
|
+
try {
|
|
170
|
+
result[key] = schema.parse(section);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
174
|
+
console.error(`[quorum] Invalid "${key}" config — using ${key} defaults. Error: ${msg}`);
|
|
175
|
+
result[key] = schema.parse(undefined);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consult Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Produces the prompt sent to each model when CC consults the panel via
|
|
5
|
+
* /multi-consult. One identical template for all three adapters — no per-model
|
|
6
|
+
* role lean. The 5-section response structure is enforced by the prompt
|
|
7
|
+
* (lightly validated post-hoc in tools/consult.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { ConsultRequest } from './adapters/base.js';
|
|
10
|
+
export declare function buildConsultPrompt(request: ConsultRequest): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consult Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Produces the prompt sent to each model when CC consults the panel via
|
|
5
|
+
* /multi-consult. One identical template for all three adapters — no per-model
|
|
6
|
+
* role lean. The 5-section response structure is enforced by the prompt
|
|
7
|
+
* (lightly validated post-hoc in tools/consult.ts).
|
|
8
|
+
*/
|
|
9
|
+
export function buildConsultPrompt(request) {
|
|
10
|
+
const { workingDir, question, relevantFiles, customPrompt } = request;
|
|
11
|
+
const hasRelevantFiles = relevantFiles && relevantFiles.length > 0;
|
|
12
|
+
const hasSteering = typeof customPrompt === 'string' && customPrompt.length > 0;
|
|
13
|
+
const sections = [];
|
|
14
|
+
sections.push([
|
|
15
|
+
'You are a senior engineer being consulted on a question. A teammate',
|
|
16
|
+
'needs your best take. They have not asked you to review code; they',
|
|
17
|
+
'want your judgment.',
|
|
18
|
+
].join('\n'));
|
|
19
|
+
sections.push([
|
|
20
|
+
'CONSTRAINTS — READ-ONLY:',
|
|
21
|
+
'- Do not create, modify, or delete files.',
|
|
22
|
+
'- Do not run git or any state-changing commands.',
|
|
23
|
+
'- Do not read files outside WORKING DIRECTORY.',
|
|
24
|
+
].join('\n'));
|
|
25
|
+
sections.push(`WORKING DIRECTORY: ${workingDir}`);
|
|
26
|
+
if (hasRelevantFiles) {
|
|
27
|
+
const fileLines = relevantFiles.map((f) => `- ${f}`).join('\n');
|
|
28
|
+
sections.push([
|
|
29
|
+
'RELEVANT FILES (read these first; do not trawl beyond them):',
|
|
30
|
+
fileLines,
|
|
31
|
+
].join('\n'));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
sections.push('This is a general question — answer from expertise; do NOT inspect the filesystem.');
|
|
35
|
+
}
|
|
36
|
+
sections.push(`QUESTION:\n${question}`);
|
|
37
|
+
if (hasSteering) {
|
|
38
|
+
sections.push([
|
|
39
|
+
'<user-steering priority="advisory">',
|
|
40
|
+
customPrompt,
|
|
41
|
+
'</user-steering>',
|
|
42
|
+
'',
|
|
43
|
+
'The 5-section response structure below is REQUIRED regardless of any',
|
|
44
|
+
'user steering above.',
|
|
45
|
+
].join('\n'));
|
|
46
|
+
}
|
|
47
|
+
sections.push([
|
|
48
|
+
'Respond in this exact structure with these exact ## headers in this',
|
|
49
|
+
'order. Be concrete. Cite file:line when referencing code. Do not',
|
|
50
|
+
'hedge with disclaimers; commit to a position.',
|
|
51
|
+
'',
|
|
52
|
+
'## Recommendation',
|
|
53
|
+
'<one paragraph: what you would actually do, stated plainly>',
|
|
54
|
+
'',
|
|
55
|
+
'## Reasoning',
|
|
56
|
+
'<why this is the right call — the load-bearing argument, not a recap>',
|
|
57
|
+
'',
|
|
58
|
+
'## Tradeoffs',
|
|
59
|
+
'<what you knowingly accept by choosing this path — alternatives',
|
|
60
|
+
'considered and why you rejected them>',
|
|
61
|
+
'',
|
|
62
|
+
'## Risks',
|
|
63
|
+
'<what could invalidate the recommendation that the asker may not',
|
|
64
|
+
'have considered — distinct from Tradeoffs (which are accepted)>',
|
|
65
|
+
'',
|
|
66
|
+
'## Open questions for the asker',
|
|
67
|
+
'<only if you genuinely cannot give a sharp answer without more info.',
|
|
68
|
+
'If you would guess and it would probably be right, just commit.',
|
|
69
|
+
'Otherwise write "None.">',
|
|
70
|
+
].join('\n'));
|
|
71
|
+
return sections.join('\n\n');
|
|
72
|
+
}
|