Council runner¶
src/council_runner.py
is the planner that turns a toolbox declaration into a concrete RunPlan
the hook system can execute.
Responsibilities¶
- Resolve the toolbox — merge global + per-repo config.
- Compute scope — walk the current diff or full repo, honoring
scope.analysisand optionalscope.filesglobs. - Graph-blast expansion — for
dynamicscope, add every file that imports a changed module (via the knowledge graph edge map). - Enforce budget — drop files until the plan fits within
budget.max_tokens(estimated by line count × heuristic). - Honor dedup — skip if the same file set was run within
dedup.window_secondsand policy isuser-configurable. - Persist — write the plan to
~/.claude/toolbox-runs/<plan_hash>.jsonfor downstream reads.
RunPlan¶
@dataclass(frozen=True)
class RunPlan:
plan_hash: str
toolbox: str
agents: tuple[str, ...]
files: tuple[str, ...]
source: str # "slash" | "pre-commit" | ...
guardrail: bool
budget: Budget
created_at: float
The plan_hash is deterministic (sha256 of toolbox|sorted(files)|agents),
which lets dedup work across triggers without any additional state.
CLI¶
# Build and persist a plan for the named toolbox
python -m council_runner build ship-it
# Build without persisting (useful for inspection)
python -m council_runner build ship-it --dry-run
# Show a previously persisted plan
python -m council_runner show <plan_hash>
# List recent plans
python -m council_runner list --limit 10
Budget estimation¶
Token estimates are intentionally rough. The runner assumes ~4 tokens per
line of source, then sorts files by recency (newest first) and greedily
takes until max_tokens is reached. If a single file exceeds the budget,
the plan is truncated rather than dropped — the council still runs on a
partial view.
This cheap estimate is fine because the council itself enforces its own
budgets; council_runner's job is just to stay in the right ballpark.
Dedup window¶
Dedup compares the sorted file list, not the plan hash — that way a toolbox and its re-run with a newer budget still dedup correctly.
Graph-blast expansion¶
For dynamic scope, council_runner reads the graph edge map produced
by scan_repo.py and walks imports one hop out from each changed file.
It stops at one hop to keep scope bounded; deep graph walks are reserved
for explicit full mode.
Related¶
- Hooks & triggers — how a plan gets executed.
- Verdicts & guardrails — what the council leaves behind.