@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.
Files changed (111) hide show
  1. package/README.md +522 -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 +110 -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 +62 -0
  37. package/dist/council.js.map +1 -0
  38. package/dist/debate/loop.d.ts +28 -0
  39. package/dist/debate/loop.js +190 -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 +37 -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 +45 -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 +47 -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/telemetry.d.ts +5 -0
  102. package/dist/telemetry.js +135 -0
  103. package/dist/telemetry.js.map +1 -0
  104. package/dist/types.d.ts +114 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/verdict.d.ts +2 -0
  108. package/dist/verdict.js +12 -0
  109. package/dist/verdict.js.map +1 -0
  110. package/package.json +54 -4
  111. package/skills/council/SKILL.md +82 -0
package/README.md CHANGED
@@ -1,3 +1,523 @@
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
+ ## 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;