@lanjak/council 0.0.0-stage → 0.5.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.
Files changed (108) hide show
  1. package/README.md +512 -2
  2. package/dist/artifacts.d.ts +24 -0
  3. package/dist/artifacts.js +227 -0
  4. package/dist/artifacts.js.map +1 -0
  5. package/dist/blocks.d.ts +12 -0
  6. package/dist/blocks.js +197 -0
  7. package/dist/blocks.js.map +1 -0
  8. package/dist/chairman.d.ts +7 -0
  9. package/dist/chairman.js +51 -0
  10. package/dist/chairman.js.map +1 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +109 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/debate.d.ts +15 -0
  15. package/dist/commands/debate.js +172 -0
  16. package/dist/commands/debate.js.map +1 -0
  17. package/dist/commands/home.d.ts +1 -0
  18. package/dist/commands/home.js +27 -0
  19. package/dist/commands/home.js.map +1 -0
  20. package/dist/commands/hook.d.ts +3 -0
  21. package/dist/commands/hook.js +138 -0
  22. package/dist/commands/hook.js.map +1 -0
  23. package/dist/commands/plan.d.ts +6 -0
  24. package/dist/commands/plan.js +23 -0
  25. package/dist/commands/plan.js.map +1 -0
  26. package/dist/commands/review.d.ts +18 -0
  27. package/dist/commands/review.js +37 -0
  28. package/dist/commands/review.js.map +1 -0
  29. package/dist/commands/setup.d.ts +1 -0
  30. package/dist/commands/setup.js +23 -0
  31. package/dist/commands/setup.js.map +1 -0
  32. package/dist/config.d.ts +17 -0
  33. package/dist/config.js +101 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/council.d.ts +3 -0
  36. package/dist/council.js +61 -0
  37. package/dist/council.js.map +1 -0
  38. package/dist/debate/loop.d.ts +28 -0
  39. package/dist/debate/loop.js +188 -0
  40. package/dist/debate/loop.js.map +1 -0
  41. package/dist/debate/prompts.d.ts +10 -0
  42. package/dist/debate/prompts.js +47 -0
  43. package/dist/debate/prompts.js.map +1 -0
  44. package/dist/debate/render.d.ts +22 -0
  45. package/dist/debate/render.js +112 -0
  46. package/dist/debate/render.js.map +1 -0
  47. package/dist/debate/session.d.ts +7 -0
  48. package/dist/debate/session.js +85 -0
  49. package/dist/debate/session.js.map +1 -0
  50. package/dist/errors.d.ts +5 -0
  51. package/dist/errors.js +18 -0
  52. package/dist/errors.js.map +1 -0
  53. package/dist/exit.d.ts +7 -0
  54. package/dist/exit.js +13 -0
  55. package/dist/exit.js.map +1 -0
  56. package/dist/hooks/gate.d.ts +12 -0
  57. package/dist/hooks/gate.js +41 -0
  58. package/dist/hooks/gate.js.map +1 -0
  59. package/dist/hooks/payload.d.ts +8 -0
  60. package/dist/hooks/payload.js +63 -0
  61. package/dist/hooks/payload.js.map +1 -0
  62. package/dist/hooks/state.d.ts +6 -0
  63. package/dist/hooks/state.js +73 -0
  64. package/dist/hooks/state.js.map +1 -0
  65. package/dist/index.d.ts +16 -0
  66. package/dist/index.js +16 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/input.d.ts +14 -0
  69. package/dist/input.js +30 -0
  70. package/dist/input.js.map +1 -0
  71. package/dist/output.d.ts +3 -0
  72. package/dist/output.js +80 -0
  73. package/dist/output.js.map +1 -0
  74. package/dist/prompts.d.ts +3 -0
  75. package/dist/prompts.js +48 -0
  76. package/dist/prompts.js.map +1 -0
  77. package/dist/providers/anthropic.d.ts +12 -0
  78. package/dist/providers/anthropic.js +98 -0
  79. package/dist/providers/anthropic.js.map +1 -0
  80. package/dist/providers/base.d.ts +35 -0
  81. package/dist/providers/base.js +7 -0
  82. package/dist/providers/base.js.map +1 -0
  83. package/dist/providers/constants.d.ts +2 -0
  84. package/dist/providers/constants.js +11 -0
  85. package/dist/providers/constants.js.map +1 -0
  86. package/dist/providers/index.d.ts +6 -0
  87. package/dist/providers/index.js +20 -0
  88. package/dist/providers/index.js.map +1 -0
  89. package/dist/providers/openai-compatible.d.ts +12 -0
  90. package/dist/providers/openai-compatible.js +68 -0
  91. package/dist/providers/openai-compatible.js.map +1 -0
  92. package/dist/runners/plan.d.ts +14 -0
  93. package/dist/runners/plan.js +43 -0
  94. package/dist/runners/plan.js.map +1 -0
  95. package/dist/runners/review.d.ts +14 -0
  96. package/dist/runners/review.js +45 -0
  97. package/dist/runners/review.js.map +1 -0
  98. package/dist/stdin.d.ts +9 -0
  99. package/dist/stdin.js +51 -0
  100. package/dist/stdin.js.map +1 -0
  101. package/dist/types.d.ts +114 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/verdict.d.ts +2 -0
  105. package/dist/verdict.js +12 -0
  106. package/dist/verdict.js.map +1 -0
  107. package/package.json +54 -4
  108. package/skills/council/SKILL.md +82 -0
package/README.md CHANGED
@@ -1,3 +1,513 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">council</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ## Library usage
220
+
221
+ council is importable as an ESM library, so a programmatic consumer never
222
+ has to shell out or parse stdout.
223
+
224
+ ```ts
225
+ import { runReview } from '@lanjak/council';
226
+
227
+ const result = await runReview({
228
+ prompt: 'Review this change',
229
+ diff: { range: 'main...HEAD' },
230
+ maxTokens: 8192,
231
+ effort: 'medium',
232
+ });
233
+
234
+ if (result.blocked) {
235
+ for (const b of result.hardBlocks) {
236
+ console.error(`${b.provider}: [${b.severity}] ${b.title} ${b.location ?? ''}`);
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Deciding whether to gate
242
+
243
+ Three orthogonal signals, so you choose your own policy:
244
+
245
+ | Field | Meaning |
246
+ |---|---|
247
+ | `blocked` | A judge we could read cleanly reported at least one hard block |
248
+ | `unknownJudges` | A judge answered, but its footer could not be parsed |
249
+ | `availableCount < totalCount` | A provider was unreachable |
250
+
251
+ `blocked` never inflates from salvage. A judge whose footer was truncated lands
252
+ in `unknownJudges`, and whatever blocks survived land in `diagnosticBlocks`.
253
+
254
+ A strict gate treats all three as reasons to stop:
255
+
256
+ ```ts
257
+ if (result.blocked || result.unknownJudges.length > 0 ||
258
+ result.availableCount < result.totalCount) {
259
+ parkForHuman();
260
+ }
261
+ ```
262
+
263
+ `loadConfig` is exported but **unstable**: configuration is being reworked and
264
+ its shape will change.
265
+
266
+ `runReview`, `runPlan`, and `runDebate` accept the exported `RunControls`
267
+ fields: `maxTokens` (a number) and `effort` (the exported `Effort` type).
268
+ These fields override each provider's configuration. When omitted, provider
269
+ settings and token defaults apply; effort is omitted if unconfigured.
270
+ For library debates, pass these controls in `DebateRunOptions` on each call,
271
+ including calls that resume a debate.
272
+
273
+ ## Debate mode
274
+
275
+ `review` and `plan` ask judges independently, in parallel, and synthesize the
276
+ disagreement for you to resolve. `debate` instead makes judges argue with
277
+ each other in sequence, round by round, until they converge on a verdict or
278
+ a round cap is hit. Reach for it when a `review` comes back split and you
279
+ want the judges to actually confront each other's reasoning instead of you
280
+ mediating.
281
+
282
+ Each round, judges speak in turn and each sees every prior turn's full
283
+ response before writing their own. Judges rotate through the same order each
284
+ round. A debate needs at least 2 judges (`NO_QUORUM` otherwise).
285
+
286
+ By default you are a participant: the caller sits in the rotation as one more
287
+ judge, is attacked like one, and its verdict gates consensus like everyone
288
+ else's. The command pauses when your turn comes up and tells you exactly how
289
+ to continue (see "Taking your turn" below). Pass `--no-participate` for a
290
+ judges-only debate that runs to consensus or the round cap without ever
291
+ pausing - use it for unattended runs and from the library, where there is no
292
+ human to take a turn.
293
+
294
+ ```sh
295
+ $ council debate "Should we add a caching layer here?" --models openai,groq
296
+ ```
297
+
298
+ Options for `debate "<prompt>"` (same artifact flags as `review`/`plan`):
299
+
300
+ - `-m, --models <models>` - comma-separated provider list
301
+ - `--max-rounds <n>` - maximum debate rounds (default: 5)
302
+ - `--max-tokens <n>` - positive token budget for each judge call
303
+ - `--effort <level>` - `low`, `medium`, `high`, or `xhigh`; overrides env settings
304
+ - `--full` - include the complete round-by-round transcript in the output
305
+ - `-f, --file <path>` - attach a file or directory (repeatable)
306
+ - `--diff [range]` - attach git diff (default: HEAD)
307
+ - `--stdin` - attach artifact content from stdin
308
+ - `--no-participate` - judges only; never pauses for your turn. Use for
309
+ unattended runs where there is no human to take a turn.
310
+
311
+ ### Verdict tags
312
+
313
+ Every judge turn must end with a verdict line:
314
+
315
+ ```
316
+ VERDICT: AGREE
317
+ VERDICT: DISAGREE
318
+ ```
319
+
320
+ The tag can follow any amount of free-form reasoning, is matched
321
+ case-insensitively, and only the last matching line in a turn counts.
322
+ Consensus is reached when every active participant's latest verdict is
323
+ `AGREE`. A turn with no verdict tag counts as `DISAGREE` - the debate fails
324
+ safe toward continuing rather than toward a false consensus.
325
+
326
+ ### Taking your turn
327
+
328
+ The debate runs the other judges' turns normally, then pauses on your turn
329
+ and prints a session id plus the exact command to continue with:
330
+
331
+ ```sh
332
+ $ council debate "Should we add a caching layer here?" --models openai,groq
333
+ council[debate]: awaiting your turn
334
+ status: awaiting-caller (round 1 of 5, turn 3 of 3)
335
+ session: dbt-a1b2c3
336
+ transcript:
337
+ ...
338
+ help[2]:
339
+ It is your turn. Read the transcript above, take a position, attack the
340
+ weakest points of the other judges' latest turns, then respond with:
341
+ echo "<your turn, ending with VERDICT: AGREE or VERDICT: DISAGREE>" | npx -y @lanjak/council debate turn dbt-a1b2c3 --stdin
342
+
343
+ $ echo "I agree the cache adds complexity but the hit ratio data supports it.
344
+ VERDICT: AGREE" | council debate turn dbt-a1b2c3 --stdin
345
+ council[debate]: "Should we add a caching layer here?"
346
+ judges: 3 of 3 responded
347
+ ...
348
+ consensus: reached in 1 of 5 rounds
349
+ ...
350
+ ```
351
+
352
+ If the round is not yet resolved, `debate turn` pauses again with a new
353
+ transcript slice (only turns you have not seen) and the same session id; run
354
+ it again until the final synthesis prints. Sessions are stored under the XDG
355
+ state directory and expire 24 hours after creation - an expired or unknown
356
+ session id fails with `SESSION_NOT_FOUND`.
357
+
358
+ ### Session commands
359
+
360
+ `debate turn <session-id> ["<response>"] [--stdin] [--full]` submits your
361
+ turn in a paused debate. Provide the response as either a positional
362
+ argument or via `--stdin`, never both or neither. Like judge turns, your
363
+ response must end with `VERDICT: AGREE` or `VERDICT: DISAGREE`.
364
+
365
+ `debate abort <session-id>` deletes a paused session. It is idempotent - it
366
+ is not an error to abort a session that has already finished or expired.
367
+
368
+ ### Cost
369
+
370
+ Debate calls are serial, not parallel: each turn waits on the previous one,
371
+ and every turn's prompt carries the full transcript so far, so later rounds
372
+ cost more per call than earlier ones. `--max-rounds` (default 5) is the main
373
+ lever for keeping cost bounded - lower it for cheap exploratory debates,
374
+ raise it for questions worth grinding on.
375
+
376
+ ## Attaching artifacts
377
+
378
+ ```sh
379
+ council review "is this sound?" --file plan.md --file src/
380
+ council review "what did I break?" --diff # git diff HEAD
381
+ council review "review this range" --diff HEAD~3
382
+ git diff | council review "check this" --stdin
383
+ ```
384
+
385
+ Files and directories are embedded in the prompt with path labels (remote
386
+ judges have no filesystem access). Directories respect `.gitignore` files at
387
+ or below the attached path (a repo-root `.gitignore` above the attached
388
+ directory is not consulted); `node_modules`/`.git` are always skipped; binary
389
+ files are skipped with a
390
+ warning. The combined artifact budget defaults to 400 KB - explicit `--file`
391
+ inputs first, then the diff, then directory expansions; anything past the cap
392
+ is truncated or omitted and named in the output's `warnings` section. Override
393
+ with `COUNCIL_MAX_ARTIFACT_BYTES`.
394
+
395
+ A `--file` value prefixed `cmd:` runs the rest as a shell command and attaches
396
+ its stdout as an artifact block - for content with no physical file at all
397
+ (a doc stored in a database, fetched through its own CLI, say), so you don't
398
+ need to write it to a temp file first just to pass `--file` a path:
399
+
400
+ ```sh
401
+ council review "..." --file 'cmd:my-notes-tool page get "Projects/x/spec.md"'
402
+ ```
403
+
404
+ A failing command is a warning, not a fatal error - the run continues with
405
+ whatever other artifacts were given. Empty output produces no block. The
406
+ command runs in the same directory `--file`/`--diff` resolve relative paths
407
+ against.
408
+
409
+ ### Hard blocks
410
+
411
+ Judges end their review with a machine-readable footer, so a consumer never has
412
+ to read the prose to find out whether something blocks:
413
+
414
+ ```
415
+ COUNCIL-BLOCKS-KEY: 7f3c2a91
416
+ BLOCKERS: 2
417
+ BLOCKER: critical | no auth check on the admin route | src/api.ts:42
418
+ BLOCKER: high | unsanitized input reaches the query builder | src/db.ts:88
419
+ ```
420
+
421
+ A clean review emits `BLOCKERS: none`. The key line is generated per run - see
422
+ [Security](#security).
423
+
424
+ Everything in `hardBlocks` blocks by definition. Severity (`critical` or `high`)
425
+ ranks urgency for a human reader and never gates, so you never have to pick a
426
+ threshold. Non-blocking nits stay in the prose.
427
+
428
+ If a judge answers but its footer cannot be read, it is reported under
429
+ `unknown` rather than being guessed at in either direction.
430
+
431
+ ## Exit codes
432
+
433
+ | Code | Meaning |
434
+ |---|---|
435
+ | 0 | Success |
436
+ | 1 | `CouncilError` |
437
+ | 2 | Bad CLI usage |
438
+ | 3 | Hard block found (only with `--fail-on-block` or `--strict`) |
439
+ | 4 | Unparseable judge (only with `--fail-on-unknown` or `--strict`) |
440
+
441
+ ## Security
442
+
443
+ Judges tag their hard-block footer with a per-run key so that artifact content
444
+ quoted back in a response is never mistaken for a verdict. The key is generated
445
+ fresh for every run.
446
+
447
+ This defends against a reviewed artifact accidentally or naively spoofing a
448
+ verdict. It does not defend against a deliberate prompt injection that instructs
449
+ the judge to emit the key. Treat results over untrusted third-party code as
450
+ advisory.
451
+
452
+ ## Agent integration
453
+
454
+ `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:
455
+
456
+ ```sh
457
+ npx -y @lanjak/council review "..." --models openai,groq
458
+ ```
459
+
460
+ ## Harness hooks
461
+
462
+ `council hook <event>` is a portable lifecycle entrypoint any agent
463
+ harness can call. It reads the harness's JSON payload from stdin (or
464
+ `--payload '<json>'`), best-effort, and never hard-fails on unknown shapes.
465
+
466
+ - `council hook session-start` - prints available providers and usage as
467
+ context. Always exits 0.
468
+ - `council hook post-edit` - records edited file paths for the session.
469
+ Always exits 0. Harnesses must wait for this command's exit before firing
470
+ `stop`.
471
+ - `council hook stop` - review gate. If edits are pending, the council
472
+ reviews `git diff HEAD` for those paths. Majority-fail verdict blocks with
473
+ exit 2 and the synthesis on stdout. Anything else exits 0; provider outages
474
+ fail open (edits kept for manual re-review).
475
+
476
+ Claude Code / openclaude wiring (`~/.claude/settings.json`):
477
+
478
+ ```json
479
+ {
480
+ "hooks": {
481
+ "SessionStart": [
482
+ { "hooks": [{ "type": "command", "command": "council hook session-start" }] }
483
+ ],
484
+ "PostToolUse": [
485
+ { "matcher": "Edit|Write|MultiEdit",
486
+ "hooks": [{ "type": "command", "command": "council hook post-edit" }] }
487
+ ],
488
+ "Stop": [
489
+ { "hooks": [{ "type": "command", "command": "council hook stop" }] }
490
+ ]
491
+ }
492
+ }
493
+ ```
494
+
495
+ Other harnesses (pi, opencode, codex, Gemini CLI, goose): point their
496
+ session-start / after-edit / stop lifecycle events at the same three commands.
497
+ What a harness does with hook stdout and exit codes varies - consult its docs.
498
+ Payload formats are parsed best-effort; if your harness's shape is not
499
+ recognized, the hooks degrade gracefully (session tracking falls back to a
500
+ cwd-based key with a stderr warning).
501
+
502
+ ## Development
503
+
504
+ ```sh
505
+ npm install
506
+ npm test # vitest
507
+ npm run build # tsc -> dist
508
+ npm run dev -- review "..." --models openai
509
+ ```
510
+
511
+ ## License
512
+
513
+ [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;