mech-council
A council of coding agents that reviews a project or plans a task together.
You name a council in council.toml — seats and a chair, each a pi
session on whatever provider and model you like — and mech-council convenes them. The seats
read the code, argue about it, and have to sign off; the chair merges duplicates, compares
plans, and watches for a seat that has stopped making progress. When the council is done
you get a report with the findings.
Requirements
- Python 3.14+
pion yourPATH, or api_clipath incouncil.toml. Members are driven through pi's RPC mode over pipes — one long-lived child per seat, JSONL in both directions.- Linux with unprivileged user and mount namespaces, to sandbox the members. Each member runs in its
own namespace. On Debian/Ubuntu this needs
kernel.unprivileged_userns_clone=1; Docker needs--privilegedor a seccomp profile that allowsunshare(2).--in-placeis the default otherwise.
Install
pipx install mech-council
mech-council --help
Quick start
Put the smallest council in <project>/council.toml — or once in ~/.config/council/council.toml, for
every project. Two seats and a chair; provider and model are optional, and without them a seat takes
whatever pi is configured with.
[councils.review]
mode = "review"
goal = "Find what should be fixed in this project."
[councils.review.chair]
name = "Chair"
[[councils.review.agents]]
name = "Metic"
persona = "You hunt correctness bugs."
[[councils.review.agents]]
name = "Senior"
persona = "Smallest change that satisfies the goal."
mech-council review # or: mech-council -g "focus on the parser" review
Narration goes to stdout, usage errors to stderr, and the report is printed last after a blank line.
What a run does
- Phase 0. Every seat gets the same bytes — no names, no IDs — and reports findings (review) or a proposed approach (plan).
- Merge. The chair deduplicates: two seats describing one defect produce one item with two reporters.
- Rounds. Every seat is shown every open item and must take a stance —
agree,disagree,needs_human,abstain— with evidence or a note. An item needs a majority to beagreed; an objection with cited evidence reopens a settled one. A seat that stops answering gets asked, once, and then its silence is recorded as a declined abstention. - You. When a seat says
needs_human, or the chair raises a question, you see it at a checkpoint. Only a question that's effectively blocking makes the run wait; deferred ones print and get asked when the run would otherwise end. - Termination.
consensus,needs_human,stalemate, orround_limit_reached.
The chair never has a stance and never gets a roster ID, so it can't move the majority bar. It merges, compares, and judges the watchdog digest — that's all.
At the checkpoint
H1: keep it 30 days # an answer is authoritative, and buys a round
H1: 2 # an option's number and nothing else takes it verbatim, in the asker's
# own words; to take one with a qualifier, answer in your own words
explain H2: what breaks? # routes your question to the seat that asked
skip # decline everything on the table
skip H1 H2 # decline these two and answer the rest
# a blank line ends the checkpoint
The last line you type about a question is your word on it — answering a question you declined takes it back, and declining one you answered withdraws the answer.
What you get
| File | What's in it |
|---|---|
report.md / plan.md |
The result: consensus findings, what was never fully vetted and by whom, disputes verbatim, rejections with the notes that killed them, and the questions you never answered. Plans get stages, gates and named assumptions instead. |
run.json |
Config, roster, settings, pi version, every member's namespaces and prompt count, the write journal, the outcome, and the sweep's survivors. |
events.jsonl |
Append-only event log — one JSON object per line, the whole run in order. |
human_input.json |
Every question and whatever became of it: asker, effective class, answer or skip, and the escalations each one retired. |
agents/ |
Per-member stderr, and transcripts if you asked for them (--transcripts full). |
The run directory is $COUNCIL_RUNS_DIR, else $XDG_CACHE_HOME/council/runs, else
~/.cache/council/runs, timestamped by project name.
Exit codes
| Code | Meaning |
|---|---|
0 |
A result was written — including needs_human and round_limit_reached. |
1 |
No result: bad config, an unusable member, an internal error, a nested-council refusal, a host that cannot fork. |
2 |
Usage error. |
130/143 |
Interrupted. Every child is disposed, every namespace this run created is swept, and run.json finalized first. |
Configuration
[defaults] sets knobs; [councils.<name>] overrides them. Flags beat both. Precedence, lowest
first: built-in default, [defaults], the extends chain (nearest ancestor first), the council,
the flags.
Which files are read
Two: $COUNCIL_CONFIG, else $XDG_CONFIG_HOME/council/council.toml, else
~/.config/council/council.toml — and then <project>/council.toml. The project's file replaces any
council of the same name and patches [defaults] key by key, and extends reaches a parent stated in
either, so a repo keeps only its own diff:
[councils.review-staged]
extends = "review" # the roster lives in the user's file
goal = "Another agent staged these; did they do a good job?"
--config names the only file read; nothing layers under it. And a project's own file may not state a
pi_cli living inside that project — the config a clone brings cannot name the executable every member
runs — so that knob belongs in the user's file.
| Knob | Default | Meaning |
|---|---|---|
mode |
review |
review, or plan for a task plan instead of a findings report. |
goal |
— | What the seats are briefed with. --goal overrides. |
max_rounds |
10 |
Round limit. No cap is imposed on it. |
watchdog_sec |
600 |
How often the chair is handed a digest of in-flight seats. 0 disables it. |
chair_timeout_sec |
300 |
Bound on a chair publication pass. |
max_concurrency |
4 |
Seats asked at once. The chair has its own slot, so a digest never queues behind seat work. |
sandbox_mib |
32768 |
tmpfs shared by all member upper layers. Ignored in place. |
in_place |
false |
Run members in the real project: no namespaces, no overlays, no write journal. --in-place forces it on; nothing forces the sandbox on where the host has no namespaces. |
transcripts |
off |
off or full. Full writes the verbatim protocol stream per member. |
pi_cli |
discovered | Path or name of the pi CLI; probed with --version. |
Seats and the chair also take provider, model, persona, and an unknown model warns
and falls back rather than failing the run.
extends: one roster, several councils
A council that only wants a different goal still has to name its seats and its chair, so it says
extends instead and inherits them:
[councils.review] # a goal, three seats and a chair, stated once
mode = "review"
goal = "See if there is anything here that should be fixed or improved."
[[councils.review.agents]]
name = "Metic"
persona = "You hunt correctness bugs."
[[councils.review.agents]]
name = "Senior"
persona = "Smallest change that satisfies the goal."
[[councils.review.agents]]
name = "Coworker"
persona = "You inherit this in eighteen months."
[councils.review.chair]
name = "Chair"
[councils.review-staged] # the same council, a narrower scope — three lines
extends = "review"
goal = "Another agent staged these changes; did they do a good job?"
[councils.review-quick] # …or the same council with a smaller bench
extends = "review"
goal = "The defects you would fix today."
max_rounds = 3
[[councils.review-quick.agents]]
name = "Senior"
drop = true # retire an inherited seat
[[councils.review-quick.agents]]
name = "Metic"
persona = "One line per finding." # patch one key; the seat keeps everything else
[councils.review-quick.chair]
persona = "Be conservative: merge only the same defect." # the chair merges key by key
[[agents]] entries match inherited seats by name: a stated key replaces that key, an
unmatched name joins the end of the roster, and drop = true retires a seat (a council still
needs at least two after the drops). [chair] merges key by key — restating it wholesale is
never required. Only the chain you convene is read, so a table elsewhere in the file can be
half-written without costing you anything.
A council whose name starts with _ is a template: extends reaches it, mech-council <name>
does not, and it is free to be a partial council — a roster with no chair, or a few knobs to pull
into several councils.
The sandbox
Each member sees your project through an overlay: the project is the read-only lower layer,
its own writes land in an upper layer inside a tmpfs that is discarded, and its $TMPDIR is
scratch that never enters the write journal. /proc shows it the orchestrator's process
information as unreadable, and its run directory is masked. When the run ends, every
namespace it created is swept — including tool shells a member detached into its own process
group — and the survivors are named in run.json if any remain.
This is only to minimize agents confusing each other. It is not recommended to run this outside of a VM (or at least a docker container mounted to only the project directory).
In place
--in-place, or in_place = true, convenes the same council with nothing between the
seats and the project. What changes:
- Every seat works in the same real tree. A peer's half-finished edit is not a defect and not yours to finish, and the briefings say so.
- The exit namespace sweep cannot run — in place the members share this namespace, so only their own
process groups are signalled. A tool shell that moved to a new process group of its own can outlive the
run.
run.jsonrecords"namespace_sweep": "none (in place)"so you know which cleanup did not happen.
Tests
pytest tests -q # offline: fake pi, real namespaces, real mounts
pytest tests/test_sandbox.py -q -rs
pytest tests -m slow # spends tokens: one real pi review of testdata/toy-project
The offline suite drives real orchestrator, sandbox, RPC and dispose paths against a
scripted fake pi. The sandbox family is the only one
with a platform requirement: its fixture lower must sit on a plain filesystem (ext4, or a
tmpfs such as a member's $TMPDIR). Point COUNCIL_TEST_FS at a plain directory to
choose it explicitly. Everything else — test_in_place.py included — runs on any POSIX host
that can fork, sandbox or not.
Lint and types
pip install -e ".[test,dev]"
python -m ruff check . # the lint bar
python -m ruff format --check . # the width it formats to: a check never contradicts it
python -m mypy # the type bar, over src and tests
pytest # the suite one section up
The lint set is chosen rather than defaulted: real defects (pyflakes, syntax errors, import placement, the
except/None smells, invalid escapes), the house shape at the formatter's 140 columns,
and four habits — sorted imports, no dead noqa, stated subprocess intent, and a handle
either closed in place or declared long-lived. testdata/toy-project is excluded from both
tools on purpose.
CI
GitHub Actions runs that bar on every push to master and on every pull request
(.github/workflows/ci.yml):
ruff, mypy, the offline suite, and then the same suite again from an unpacked sdist.
pytest -m slow, the one family that spends tokens, stays out of CI: it wants a real pi and a real budget.
Tagging a release publishes it (publish.yml):
the suite on the tagged commit, a build whose version has to match the tag, then the upload through PyPI's
trusted publishing, so no token lives in this repository.
License
mech-council — a council of coding agents that review a project or plan a task together.
Copyright (C) 2026 Connor Pietrasik connorgpietrasik@gmail.com
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See LICENSE.
Metadata
Release files for mech-council 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mech_council-0.1.1.tar.gz | 198.7 kB | With provenance | Details for mech_council-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mech_council-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for mech_council-0.1.1-py3-none-any.whl |
Total release size: 332.2 kB
Release files / mech_council-0.1.1.tar.gz
| Download URL | mech_council-0.1.1.tar.gz |
|---|---|
| Size | 198.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
722e3f9cae49337fcdbc8f3330369ca5e560a5a0acbfe189f0fab306633fbb29
|
|
BLAKE2b-256 checksum How to use checksums |
c266f71fdedc712ce6fe0522de512612929665803b5848caa0054d9b5d61f749
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logRelease files / mech_council-0.1.1-py3-none-any.whl
| Download URL | mech_council-0.1.1-py3-none-any.whl |
|---|---|
| Size | 133.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
97c4e4b161d8171e932d5d8d1a195b003c2512110704bf4ff95a0061086c2ff7
|
|
BLAKE2b-256 checksum How to use checksums |
74773a12fb290a03cf71f838bf112b113e84304f5ad15b4000ee15ebe36feb9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logSecurity for mech-council 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for mech_council-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.