@lanjak/council 0.0.0-stage → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +522 -2
- package/dist/artifacts.d.ts +24 -0
- package/dist/artifacts.js +227 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/blocks.d.ts +12 -0
- package/dist/blocks.js +197 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chairman.d.ts +7 -0
- package/dist/chairman.js +51 -0
- package/dist/chairman.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +110 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/debate.d.ts +15 -0
- package/dist/commands/debate.js +172 -0
- package/dist/commands/debate.js.map +1 -0
- package/dist/commands/home.d.ts +1 -0
- package/dist/commands/home.js +27 -0
- package/dist/commands/home.js.map +1 -0
- package/dist/commands/hook.d.ts +3 -0
- package/dist/commands/hook.js +138 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/plan.d.ts +6 -0
- package/dist/commands/plan.js +23 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/review.d.ts +18 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +23 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/council.d.ts +3 -0
- package/dist/council.js +62 -0
- package/dist/council.js.map +1 -0
- package/dist/debate/loop.d.ts +28 -0
- package/dist/debate/loop.js +190 -0
- package/dist/debate/loop.js.map +1 -0
- package/dist/debate/prompts.d.ts +10 -0
- package/dist/debate/prompts.js +47 -0
- package/dist/debate/prompts.js.map +1 -0
- package/dist/debate/render.d.ts +22 -0
- package/dist/debate/render.js +112 -0
- package/dist/debate/render.js.map +1 -0
- package/dist/debate/session.d.ts +7 -0
- package/dist/debate/session.js +85 -0
- package/dist/debate/session.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/exit.d.ts +7 -0
- package/dist/exit.js +13 -0
- package/dist/exit.js.map +1 -0
- package/dist/hooks/gate.d.ts +12 -0
- package/dist/hooks/gate.js +41 -0
- package/dist/hooks/gate.js.map +1 -0
- package/dist/hooks/payload.d.ts +8 -0
- package/dist/hooks/payload.js +63 -0
- package/dist/hooks/payload.js.map +1 -0
- package/dist/hooks/state.d.ts +6 -0
- package/dist/hooks/state.js +73 -0
- package/dist/hooks/state.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +14 -0
- package/dist/input.js +30 -0
- package/dist/input.js.map +1 -0
- package/dist/output.d.ts +3 -0
- package/dist/output.js +80 -0
- package/dist/output.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +48 -0
- package/dist/prompts.js.map +1 -0
- package/dist/providers/anthropic.d.ts +12 -0
- package/dist/providers/anthropic.js +98 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +37 -0
- package/dist/providers/base.js +7 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/constants.d.ts +2 -0
- package/dist/providers/constants.js +11 -0
- package/dist/providers/constants.js.map +1 -0
- package/dist/providers/index.d.ts +6 -0
- package/dist/providers/index.js +20 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +12 -0
- package/dist/providers/openai-compatible.js +68 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/runners/plan.d.ts +14 -0
- package/dist/runners/plan.js +45 -0
- package/dist/runners/plan.js.map +1 -0
- package/dist/runners/review.d.ts +14 -0
- package/dist/runners/review.js +47 -0
- package/dist/runners/review.js.map +1 -0
- package/dist/stdin.d.ts +9 -0
- package/dist/stdin.js +51 -0
- package/dist/stdin.js.map +1 -0
- package/dist/telemetry.d.ts +5 -0
- package/dist/telemetry.js +135 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +2 -0
- package/dist/verdict.js +12 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +54 -4
- package/skills/council/SKILL.md +82 -0
package/README.md
CHANGED
|
@@ -1,3 +1,523 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">council</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">Multi-LLM adversarial review council - an <a href="https://github.com/kunchenguid/axi">AXI</a>.</p>
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`council` sends a single prompt to several independent LLM judges and returns a synthesized review. It is useful when you want more than one model to look at a plan, decision, or code change before you act. Output is in [TOON](https://toonformat.dev/) so agents can read it cheaply.
|
|
8
|
+
|
|
9
|
+
It is provider-agnostic. If a provider speaks the OpenAI chat completions API, you can add it to your council by setting a few environment variables.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g @lanjak/council
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run without installing:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx -y @lanjak/council <command>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Configure providers
|
|
24
|
+
|
|
25
|
+
Pick a short key for each provider, then set `COUNCIL_PROVIDERS` plus per-provider env vars.
|
|
26
|
+
|
|
27
|
+
For example, with OpenAI and Groq:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
export COUNCIL_PROVIDERS="openai,groq"
|
|
31
|
+
|
|
32
|
+
export OPENAI_API_KEY="sk-..."
|
|
33
|
+
export OPENAI_BASE_URL="https://api.openai.com/v1"
|
|
34
|
+
export OPENAI_MODEL="gpt-4o"
|
|
35
|
+
export OPENAI_DISPLAY_NAME="OpenAI"
|
|
36
|
+
|
|
37
|
+
export GROQ_API_KEY="gsk_..."
|
|
38
|
+
export GROQ_BASE_URL="https://api.groq.com/openai/v1"
|
|
39
|
+
export GROQ_MODEL="llama-3.1-70b-versatile"
|
|
40
|
+
export GROQ_DISPLAY_NAME="Groq"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Required for each provider:
|
|
44
|
+
|
|
45
|
+
- `<KEY>_API_KEY` - the provider API key
|
|
46
|
+
- `<KEY>_BASE_URL` - the OpenAI-compatible base URL
|
|
47
|
+
- `<KEY>_MODEL` - the model ID to call
|
|
48
|
+
|
|
49
|
+
Optional:
|
|
50
|
+
|
|
51
|
+
- `<KEY>_DISPLAY_NAME` - human-readable name shown in output (defaults to the key)
|
|
52
|
+
- `<KEY>_MAX_TOKENS` - response token cap per call (defaults to 32768 for
|
|
53
|
+
OpenAI-compatible providers and 8192 for Anthropic providers)
|
|
54
|
+
- `<KEY>_EFFORT` - reasoning effort: `low`, `medium`, `high`, or `xhigh`;
|
|
55
|
+
overrides `COUNCIL_EFFORT` when set
|
|
56
|
+
- `<KEY>_TEMPERATURE` - sampling temperature; when unset the field is omitted
|
|
57
|
+
entirely and the provider's own default applies (some endpoints reject any
|
|
58
|
+
explicit value - Kimi's k3 only accepts 1)
|
|
59
|
+
- `<KEY>_KIND` - `openai` (default) or `anthropic`. Selects the wire
|
|
60
|
+
protocol spoken to `<KEY>_BASE_URL`. For `anthropic`, `<KEY>_BASE_URL` is
|
|
61
|
+
optional (defaults to `https://api.anthropic.com`) and, when set, is the
|
|
62
|
+
API root - not a full `/v1/messages` path. Note Anthropic's `temperature`
|
|
63
|
+
range is 0-1, unlike OpenAI-compatible's 0-2; a value above 1 copied from
|
|
64
|
+
an existing OpenAI-style entry will fail at request time.
|
|
65
|
+
|
|
66
|
+
### Per-task token budget and reasoning effort
|
|
67
|
+
|
|
68
|
+
The calling agent should choose `--max-tokens <n>` for each task. Use a small
|
|
69
|
+
budget for reviewing a finished plan and a large budget for scanning a codebase.
|
|
70
|
+
The value must be a positive integer. It applies to every judge call in `review`,
|
|
71
|
+
`plan`, or `debate` and overrides `<PREFIX>_MAX_TOKENS`.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
council plan "Review this finished plan" --file plan.md --max-tokens 4096
|
|
75
|
+
council review "Scan this codebase" --file src --max-tokens 32768 --effort high
|
|
76
|
+
council debate "Choose an approach" --max-tokens 8192 --effort medium --no-participate
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Reasoning effort accepts `low`, `medium`, `high`, or `xhigh`. Precedence is
|
|
80
|
+
`--effort`, then `<PREFIX>_EFFORT`, then `COUNCIL_EFFORT`. With nothing configured,
|
|
81
|
+
the request omits effort. Set `COUNCIL_EFFORT=medium` to use medium effort by default.
|
|
82
|
+
Invalid configured levels fail at config load. OpenAI-compatible requests send
|
|
83
|
+
`reasoning_effort`; Anthropic requests send `output_config.effort`. The model
|
|
84
|
+
and endpoint must support the selected level. Paused debates retain per-run
|
|
85
|
+
controls when resumed.
|
|
86
|
+
|
|
87
|
+
New state is written under `$XDG_STATE_HOME/council` (default:
|
|
88
|
+
`~/.local/state/council`). Debate sessions use its `debates/` subdirectory.
|
|
89
|
+
Session loading checks `council` first, then falls back to `council-axi` if
|
|
90
|
+
the new session file is absent. Hook edit tracking reads both locations.
|
|
91
|
+
Session deletion and expiry cleanup cover both locations.
|
|
92
|
+
Environment variable names stay the same.
|
|
93
|
+
|
|
94
|
+
### Example providers
|
|
95
|
+
|
|
96
|
+
These are not built in. Add the ones you have keys for.
|
|
97
|
+
|
|
98
|
+
**OpenAI**
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
OPENAI_API_KEY=sk-...
|
|
102
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
103
|
+
OPENAI_MODEL=gpt-4o
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Moonshot Kimi**
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
KIMI_API_KEY=sk-...
|
|
110
|
+
KIMI_BASE_URL=https://api.moonshot.ai/v1
|
|
111
|
+
KIMI_MODEL=kimi-k3
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**DeepSeek**
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
DEEPSEEK_API_KEY=sk-...
|
|
118
|
+
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
|
119
|
+
DEEPSEEK_MODEL=deepseek-chat
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Xiaomi MiMo**
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
MIMO_API_KEY=sk-...
|
|
126
|
+
MIMO_BASE_URL=https://api.xiaomimimo.com/v1
|
|
127
|
+
MIMO_MODEL=mimo-v2.5-pro
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Groq**
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
GROQ_API_KEY=gsk_...
|
|
134
|
+
GROQ_BASE_URL=https://api.groq.com/openai/v1
|
|
135
|
+
GROQ_MODEL=llama-3.1-70b-versatile
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Local LM Studio / Ollama / vLLM**
|
|
139
|
+
|
|
140
|
+
Anything with an OpenAI-compatible endpoint works:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
LOCAL_API_KEY=not-needed
|
|
144
|
+
LOCAL_BASE_URL=http://localhost:1234/v1
|
|
145
|
+
LOCAL_MODEL=local-model
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Anthropic (Claude)**
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
CLAUDE_KIND=anthropic
|
|
152
|
+
CLAUDE_API_KEY=sk-ant-...
|
|
153
|
+
CLAUDE_MODEL=claude-opus-5
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Usage
|
|
157
|
+
|
|
158
|
+
Check which providers are authenticated:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
$ council setup
|
|
162
|
+
providers[2]{name,authenticated,detail}:
|
|
163
|
+
openai,true,OpenAI API key is set
|
|
164
|
+
groq,true,Groq API key is set
|
|
165
|
+
help[2]:
|
|
166
|
+
Set COUNCIL_PROVIDERS and per-provider env vars to add judges
|
|
167
|
+
Example: COUNCIL_PROVIDERS=openai OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_MODEL=gpt-4o
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Run an adversarial review:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
$ council review "Should we add a caching layer here?"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Or pick a subset:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
$ council review "Should we add a caching layer here?" --models openai,groq
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Pressure-test a plan:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
$ council plan "Should we migrate auth to a separate service?"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Attach files, a directory, or a diff:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
$ council review "is this sound?" --file plan.md --file src/
|
|
192
|
+
$ council review "what did I break?" --diff
|
|
193
|
+
$ git diff | council review "check this" --stdin
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Example output:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
council[review]: "Should we add a caching layer here?"
|
|
200
|
+
judges: 2 of 2 responded
|
|
201
|
+
judges[2]{provider,model,status,verdict}:
|
|
202
|
+
openai,gpt-4o,success,Ship after adding cache invalidation
|
|
203
|
+
groq,llama-3.1-70b-versatile,success,Ship but measure hit ratio first
|
|
204
|
+
synthesis:
|
|
205
|
+
## Council review synthesis (2 judges)
|
|
206
|
+
|
|
207
|
+
### openai (gpt-4o)
|
|
208
|
+
Ship after adding cache invalidation.
|
|
209
|
+
|
|
210
|
+
### groq (llama-3.1-70b-versatile)
|
|
211
|
+
Ship but measure hit ratio first.
|
|
212
|
+
|
|
213
|
+
**Key points:**
|
|
214
|
+
- Ship after adding cache invalidation
|
|
215
|
+
- Ship but measure hit ratio first
|
|
216
|
+
help[1]: Run `npx -y @lanjak/council review "<prompt>" --models openai,groq`
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Usage telemetry (optional)
|
|
220
|
+
|
|
221
|
+
Telemetry is off unless you set `OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. Set `OTEL_SDK_DISABLED=true` to disable both log export and the `traceparent` header.
|
|
222
|
+
|
|
223
|
+
When enabled, council sends a W3C `traceparent` header to OpenAI-compatible and Anthropic providers, with one trace per run and one span per model call. After each call, it exports one OTLP/HTTP JSON log record with context only: trace and span IDs, role (`judge` or `debate`), requested model, device (hostname), outcome (`ok`, `error`, `timeout` or `rate_limited`) and duration. It never exports prompt or response text, token counts or cost. The gateway owns token and cost reporting.
|
|
224
|
+
|
|
225
|
+
Council appends `/v1/logs` to `OTEL_EXPORTER_OTLP_ENDPOINT`. If set, `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` takes precedence and is used as the complete URL. Export headers come from `OTEL_EXPORTER_OTLP_HEADERS`; resource attributes come from `OTEL_RESOURCE_ATTRIBUTES`. Both accept comma-separated `key=value` pairs with percent-encoded values.
|
|
226
|
+
|
|
227
|
+
Exports run in the background with a 1.5-second timeout per request. Model calls do not wait for exports, and export failures do not fail a run. Before exit, the CLI waits once for pending exports, for at most 1.5 seconds total. Library calls return without waiting for exports; an immediate process exit can discard pending records.
|
|
228
|
+
|
|
229
|
+
## Library usage
|
|
230
|
+
|
|
231
|
+
council is importable as an ESM library, so a programmatic consumer never
|
|
232
|
+
has to shell out or parse stdout.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { runReview } from '@lanjak/council';
|
|
236
|
+
|
|
237
|
+
const result = await runReview({
|
|
238
|
+
prompt: 'Review this change',
|
|
239
|
+
diff: { range: 'main...HEAD' },
|
|
240
|
+
maxTokens: 8192,
|
|
241
|
+
effort: 'medium',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (result.blocked) {
|
|
245
|
+
for (const b of result.hardBlocks) {
|
|
246
|
+
console.error(`${b.provider}: [${b.severity}] ${b.title} ${b.location ?? ''}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Deciding whether to gate
|
|
252
|
+
|
|
253
|
+
Three orthogonal signals, so you choose your own policy:
|
|
254
|
+
|
|
255
|
+
| Field | Meaning |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `blocked` | A judge we could read cleanly reported at least one hard block |
|
|
258
|
+
| `unknownJudges` | A judge answered, but its footer could not be parsed |
|
|
259
|
+
| `availableCount < totalCount` | A provider was unreachable |
|
|
260
|
+
|
|
261
|
+
`blocked` never inflates from salvage. A judge whose footer was truncated lands
|
|
262
|
+
in `unknownJudges`, and whatever blocks survived land in `diagnosticBlocks`.
|
|
263
|
+
|
|
264
|
+
A strict gate treats all three as reasons to stop:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
if (result.blocked || result.unknownJudges.length > 0 ||
|
|
268
|
+
result.availableCount < result.totalCount) {
|
|
269
|
+
parkForHuman();
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`loadConfig` is exported but **unstable**: configuration is being reworked and
|
|
274
|
+
its shape will change.
|
|
275
|
+
|
|
276
|
+
`runReview`, `runPlan`, and `runDebate` accept the exported `RunControls`
|
|
277
|
+
fields: `maxTokens` (a number) and `effort` (the exported `Effort` type).
|
|
278
|
+
These fields override each provider's configuration. When omitted, provider
|
|
279
|
+
settings and token defaults apply; effort is omitted if unconfigured.
|
|
280
|
+
For library debates, pass these controls in `DebateRunOptions` on each call,
|
|
281
|
+
including calls that resume a debate.
|
|
282
|
+
|
|
283
|
+
## Debate mode
|
|
284
|
+
|
|
285
|
+
`review` and `plan` ask judges independently, in parallel, and synthesize the
|
|
286
|
+
disagreement for you to resolve. `debate` instead makes judges argue with
|
|
287
|
+
each other in sequence, round by round, until they converge on a verdict or
|
|
288
|
+
a round cap is hit. Reach for it when a `review` comes back split and you
|
|
289
|
+
want the judges to actually confront each other's reasoning instead of you
|
|
290
|
+
mediating.
|
|
291
|
+
|
|
292
|
+
Each round, judges speak in turn and each sees every prior turn's full
|
|
293
|
+
response before writing their own. Judges rotate through the same order each
|
|
294
|
+
round. A debate needs at least 2 judges (`NO_QUORUM` otherwise).
|
|
295
|
+
|
|
296
|
+
By default you are a participant: the caller sits in the rotation as one more
|
|
297
|
+
judge, is attacked like one, and its verdict gates consensus like everyone
|
|
298
|
+
else's. The command pauses when your turn comes up and tells you exactly how
|
|
299
|
+
to continue (see "Taking your turn" below). Pass `--no-participate` for a
|
|
300
|
+
judges-only debate that runs to consensus or the round cap without ever
|
|
301
|
+
pausing - use it for unattended runs and from the library, where there is no
|
|
302
|
+
human to take a turn.
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
$ council debate "Should we add a caching layer here?" --models openai,groq
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Options for `debate "<prompt>"` (same artifact flags as `review`/`plan`):
|
|
309
|
+
|
|
310
|
+
- `-m, --models <models>` - comma-separated provider list
|
|
311
|
+
- `--max-rounds <n>` - maximum debate rounds (default: 5)
|
|
312
|
+
- `--max-tokens <n>` - positive token budget for each judge call
|
|
313
|
+
- `--effort <level>` - `low`, `medium`, `high`, or `xhigh`; overrides env settings
|
|
314
|
+
- `--full` - include the complete round-by-round transcript in the output
|
|
315
|
+
- `-f, --file <path>` - attach a file or directory (repeatable)
|
|
316
|
+
- `--diff [range]` - attach git diff (default: HEAD)
|
|
317
|
+
- `--stdin` - attach artifact content from stdin
|
|
318
|
+
- `--no-participate` - judges only; never pauses for your turn. Use for
|
|
319
|
+
unattended runs where there is no human to take a turn.
|
|
320
|
+
|
|
321
|
+
### Verdict tags
|
|
322
|
+
|
|
323
|
+
Every judge turn must end with a verdict line:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
VERDICT: AGREE
|
|
327
|
+
VERDICT: DISAGREE
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The tag can follow any amount of free-form reasoning, is matched
|
|
331
|
+
case-insensitively, and only the last matching line in a turn counts.
|
|
332
|
+
Consensus is reached when every active participant's latest verdict is
|
|
333
|
+
`AGREE`. A turn with no verdict tag counts as `DISAGREE` - the debate fails
|
|
334
|
+
safe toward continuing rather than toward a false consensus.
|
|
335
|
+
|
|
336
|
+
### Taking your turn
|
|
337
|
+
|
|
338
|
+
The debate runs the other judges' turns normally, then pauses on your turn
|
|
339
|
+
and prints a session id plus the exact command to continue with:
|
|
340
|
+
|
|
341
|
+
```sh
|
|
342
|
+
$ council debate "Should we add a caching layer here?" --models openai,groq
|
|
343
|
+
council[debate]: awaiting your turn
|
|
344
|
+
status: awaiting-caller (round 1 of 5, turn 3 of 3)
|
|
345
|
+
session: dbt-a1b2c3
|
|
346
|
+
transcript:
|
|
347
|
+
...
|
|
348
|
+
help[2]:
|
|
349
|
+
It is your turn. Read the transcript above, take a position, attack the
|
|
350
|
+
weakest points of the other judges' latest turns, then respond with:
|
|
351
|
+
echo "<your turn, ending with VERDICT: AGREE or VERDICT: DISAGREE>" | npx -y @lanjak/council debate turn dbt-a1b2c3 --stdin
|
|
352
|
+
|
|
353
|
+
$ echo "I agree the cache adds complexity but the hit ratio data supports it.
|
|
354
|
+
VERDICT: AGREE" | council debate turn dbt-a1b2c3 --stdin
|
|
355
|
+
council[debate]: "Should we add a caching layer here?"
|
|
356
|
+
judges: 3 of 3 responded
|
|
357
|
+
...
|
|
358
|
+
consensus: reached in 1 of 5 rounds
|
|
359
|
+
...
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
If the round is not yet resolved, `debate turn` pauses again with a new
|
|
363
|
+
transcript slice (only turns you have not seen) and the same session id; run
|
|
364
|
+
it again until the final synthesis prints. Sessions are stored under the XDG
|
|
365
|
+
state directory and expire 24 hours after creation - an expired or unknown
|
|
366
|
+
session id fails with `SESSION_NOT_FOUND`.
|
|
367
|
+
|
|
368
|
+
### Session commands
|
|
369
|
+
|
|
370
|
+
`debate turn <session-id> ["<response>"] [--stdin] [--full]` submits your
|
|
371
|
+
turn in a paused debate. Provide the response as either a positional
|
|
372
|
+
argument or via `--stdin`, never both or neither. Like judge turns, your
|
|
373
|
+
response must end with `VERDICT: AGREE` or `VERDICT: DISAGREE`.
|
|
374
|
+
|
|
375
|
+
`debate abort <session-id>` deletes a paused session. It is idempotent - it
|
|
376
|
+
is not an error to abort a session that has already finished or expired.
|
|
377
|
+
|
|
378
|
+
### Cost
|
|
379
|
+
|
|
380
|
+
Debate calls are serial, not parallel: each turn waits on the previous one,
|
|
381
|
+
and every turn's prompt carries the full transcript so far, so later rounds
|
|
382
|
+
cost more per call than earlier ones. `--max-rounds` (default 5) is the main
|
|
383
|
+
lever for keeping cost bounded - lower it for cheap exploratory debates,
|
|
384
|
+
raise it for questions worth grinding on.
|
|
385
|
+
|
|
386
|
+
## Attaching artifacts
|
|
387
|
+
|
|
388
|
+
```sh
|
|
389
|
+
council review "is this sound?" --file plan.md --file src/
|
|
390
|
+
council review "what did I break?" --diff # git diff HEAD
|
|
391
|
+
council review "review this range" --diff HEAD~3
|
|
392
|
+
git diff | council review "check this" --stdin
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Files and directories are embedded in the prompt with path labels (remote
|
|
396
|
+
judges have no filesystem access). Directories respect `.gitignore` files at
|
|
397
|
+
or below the attached path (a repo-root `.gitignore` above the attached
|
|
398
|
+
directory is not consulted); `node_modules`/`.git` are always skipped; binary
|
|
399
|
+
files are skipped with a
|
|
400
|
+
warning. The combined artifact budget defaults to 400 KB - explicit `--file`
|
|
401
|
+
inputs first, then the diff, then directory expansions; anything past the cap
|
|
402
|
+
is truncated or omitted and named in the output's `warnings` section. Override
|
|
403
|
+
with `COUNCIL_MAX_ARTIFACT_BYTES`.
|
|
404
|
+
|
|
405
|
+
A `--file` value prefixed `cmd:` runs the rest as a shell command and attaches
|
|
406
|
+
its stdout as an artifact block - for content with no physical file at all
|
|
407
|
+
(a doc stored in a database, fetched through its own CLI, say), so you don't
|
|
408
|
+
need to write it to a temp file first just to pass `--file` a path:
|
|
409
|
+
|
|
410
|
+
```sh
|
|
411
|
+
council review "..." --file 'cmd:my-notes-tool page get "Projects/x/spec.md"'
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
A failing command is a warning, not a fatal error - the run continues with
|
|
415
|
+
whatever other artifacts were given. Empty output produces no block. The
|
|
416
|
+
command runs in the same directory `--file`/`--diff` resolve relative paths
|
|
417
|
+
against.
|
|
418
|
+
|
|
419
|
+
### Hard blocks
|
|
420
|
+
|
|
421
|
+
Judges end their review with a machine-readable footer, so a consumer never has
|
|
422
|
+
to read the prose to find out whether something blocks:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
COUNCIL-BLOCKS-KEY: 7f3c2a91
|
|
426
|
+
BLOCKERS: 2
|
|
427
|
+
BLOCKER: critical | no auth check on the admin route | src/api.ts:42
|
|
428
|
+
BLOCKER: high | unsanitized input reaches the query builder | src/db.ts:88
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
A clean review emits `BLOCKERS: none`. The key line is generated per run - see
|
|
432
|
+
[Security](#security).
|
|
433
|
+
|
|
434
|
+
Everything in `hardBlocks` blocks by definition. Severity (`critical` or `high`)
|
|
435
|
+
ranks urgency for a human reader and never gates, so you never have to pick a
|
|
436
|
+
threshold. Non-blocking nits stay in the prose.
|
|
437
|
+
|
|
438
|
+
If a judge answers but its footer cannot be read, it is reported under
|
|
439
|
+
`unknown` rather than being guessed at in either direction.
|
|
440
|
+
|
|
441
|
+
## Exit codes
|
|
442
|
+
|
|
443
|
+
| Code | Meaning |
|
|
444
|
+
|---|---|
|
|
445
|
+
| 0 | Success |
|
|
446
|
+
| 1 | `CouncilError` |
|
|
447
|
+
| 2 | Bad CLI usage |
|
|
448
|
+
| 3 | Hard block found (only with `--fail-on-block` or `--strict`) |
|
|
449
|
+
| 4 | Unparseable judge (only with `--fail-on-unknown` or `--strict`) |
|
|
450
|
+
|
|
451
|
+
## Security
|
|
452
|
+
|
|
453
|
+
Judges tag their hard-block footer with a per-run key so that artifact content
|
|
454
|
+
quoted back in a response is never mistaken for a verdict. The key is generated
|
|
455
|
+
fresh for every run.
|
|
456
|
+
|
|
457
|
+
This defends against a reviewed artifact accidentally or naively spoofing a
|
|
458
|
+
verdict. It does not defend against a deliberate prompt injection that instructs
|
|
459
|
+
the judge to emit the key. Treat results over untrusted third-party code as
|
|
460
|
+
advisory.
|
|
461
|
+
|
|
462
|
+
## Agent integration
|
|
463
|
+
|
|
464
|
+
`council` ships with an installable Agent Skill in `skills/council/SKILL.md`. Copy it into your agent's skill directory, or point your agent at the CLI directly:
|
|
465
|
+
|
|
466
|
+
```sh
|
|
467
|
+
npx -y @lanjak/council review "..." --models openai,groq
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Harness hooks
|
|
471
|
+
|
|
472
|
+
`council hook <event>` is a portable lifecycle entrypoint any agent
|
|
473
|
+
harness can call. It reads the harness's JSON payload from stdin (or
|
|
474
|
+
`--payload '<json>'`), best-effort, and never hard-fails on unknown shapes.
|
|
475
|
+
|
|
476
|
+
- `council hook session-start` - prints available providers and usage as
|
|
477
|
+
context. Always exits 0.
|
|
478
|
+
- `council hook post-edit` - records edited file paths for the session.
|
|
479
|
+
Always exits 0. Harnesses must wait for this command's exit before firing
|
|
480
|
+
`stop`.
|
|
481
|
+
- `council hook stop` - review gate. If edits are pending, the council
|
|
482
|
+
reviews `git diff HEAD` for those paths. Majority-fail verdict blocks with
|
|
483
|
+
exit 2 and the synthesis on stdout. Anything else exits 0; provider outages
|
|
484
|
+
fail open (edits kept for manual re-review).
|
|
485
|
+
|
|
486
|
+
Claude Code / openclaude wiring (`~/.claude/settings.json`):
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"hooks": {
|
|
491
|
+
"SessionStart": [
|
|
492
|
+
{ "hooks": [{ "type": "command", "command": "council hook session-start" }] }
|
|
493
|
+
],
|
|
494
|
+
"PostToolUse": [
|
|
495
|
+
{ "matcher": "Edit|Write|MultiEdit",
|
|
496
|
+
"hooks": [{ "type": "command", "command": "council hook post-edit" }] }
|
|
497
|
+
],
|
|
498
|
+
"Stop": [
|
|
499
|
+
{ "hooks": [{ "type": "command", "command": "council hook stop" }] }
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Other harnesses (pi, opencode, codex, Gemini CLI, goose): point their
|
|
506
|
+
session-start / after-edit / stop lifecycle events at the same three commands.
|
|
507
|
+
What a harness does with hook stdout and exit codes varies - consult its docs.
|
|
508
|
+
Payload formats are parsed best-effort; if your harness's shape is not
|
|
509
|
+
recognized, the hooks degrade gracefully (session tracking falls back to a
|
|
510
|
+
cwd-based key with a stderr warning).
|
|
511
|
+
|
|
512
|
+
## Development
|
|
513
|
+
|
|
514
|
+
```sh
|
|
515
|
+
npm install
|
|
516
|
+
npm test # vitest
|
|
517
|
+
npm run build # tsc -> dist
|
|
518
|
+
npm run dev -- review "..." --models openai
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## License
|
|
522
|
+
|
|
523
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ArtifactBlock {
|
|
2
|
+
label: string;
|
|
3
|
+
content: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ArtifactBundle {
|
|
7
|
+
blocks: ArtifactBlock[];
|
|
8
|
+
totalBytes: number;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface AssembleOptions {
|
|
12
|
+
files?: string[];
|
|
13
|
+
diff?: {
|
|
14
|
+
range?: string;
|
|
15
|
+
paths?: string[];
|
|
16
|
+
};
|
|
17
|
+
stdin?: string;
|
|
18
|
+
capBytes?: number;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_CAP_BYTES = 400000;
|
|
22
|
+
export declare function assembleArtifacts(options: AssembleOptions): ArtifactBundle;
|
|
23
|
+
export declare function formatArtifactPreamble(bundle: ArtifactBundle): string;
|
|
24
|
+
export declare function formatBytes(bytes: number): string;
|