Skip to content

fix(council): share review's engine config, and ship an entry point that runs - #13

Merged
eliahilse merged 1 commit into
mainfrom
fix/council-config-and-entrypoints
Aug 2, 2026
Merged

fix(council): share review's engine config, and ship an entry point that runs#13
eliahilse merged 1 commit into
mainfrom
fix/council-config-and-entrypoints

Conversation

@eliahilse

Copy link
Copy Markdown
Owner

The council package was built next to kyora review and handed the same engine pool, but none of the discipline around it. This fixes what fell out of that.

I also convened the council on itself for this — claude, glm and qwen each read packages/council/mcp/src/ cold and led with the same two problems (misplaced/forged config, and the background-job layer). Their takes shaped what's here.

Entry points don't run

Two bins (kyora-council, kyora-stakes-hook) meant no runner could pick one:

$ npx @kyora-sh/council
npm error could not determine executable to run

and bunx resolves a multi-bin scoped package to the first bin — so the documented hook command

{ "type": "command", "command": "bunx @kyora-sh/council kyora-stakes-hook" }

started an MCP stdio server with kyora-stakes-hook as an argument. The server holds stdin open, so the hook hung on every matching Write/Edit/Bash until it timed out. (Both verified against a two-bin probe package.)

One bin now dispatches by subcommand — kyora-council hook, kyora-council [serve] — and the MCP server moves to server.ts behind startServer(), leaving index.ts as the entry point.

The council could never honor a config

council.ts forged a whole ReviewConfigbase, failOn, maxDiffBytes, maxFindingsPerEngine and five more fields meaningless to a council — purely to satisfy runEngineRaw's signature, and hardcoded overrides: {} while doing it. Every engineStatus(engine, undefined) call was the same thing again.

So this, from the README, was true for review and quietly false for the council:

a vendor CLI changing its flags is a config edit, not a code change

  • runEngineRaw now takes a narrow RunConfig { timeoutMs, cooldownMinutes, overrides }, which ReviewConfig extends. Nothing to forge.
  • The council reads the same kyora-review.config.json as review, resolved per checkout and cached. Config loading moved to a shared config.ts rather than being copied.
  • argsFor fixes inverted precedence: a configured args override was silently discarded whenever the engine defined its own argsWrite/argsChat, so an override worked in review and was ignored in council. EngineOverride gains argsWrite/argsChat, making the council's own invocations overridable at all.

Background jobs could collide and strand

  • Ids were Date.now() in base36 — two jobs starting in the same millisecond shared a filename, and the first result overwrote the second's record. Ids now carry a sequence.
  • A job had no deadline. If the server was restarted mid-run, nothing ever moved the record off running and council_result reported "still running (48231s so far)" forever. A job past its deadline now reads back as failed, with a reason.

Two smaller correctness fixes

  • council_convene_async seated the council twice over live quota — once to report engines, once to run them — so the engines it named were not guaranteed to be the ones that ran. Seating is now a separate step from asking.
  • agent_fanout with write: true ran N write-capable CLIs against one working tree concurrently, with nothing enforcing the "disjoint files" its schema asked for. Write fanouts run in turn until each task can get its own checkout; read-only fanouts stay parallel.

Repo config

.mcp.json pointed at npx -y kyora-sh, which 404s on npm — the server never started for anyone who cloned this. It points at the published @kyora-sh/mcp now, and adds the council so the repo dogfoods it. The council package was also missing from the README's repo layout entirely.

Not in this PR

Two of the three council members argued the engine pool + usage/cooldown layer should be extracted into its own package, with review and council as peers on top of it, instead of council reaching into review's src/ through raw-TypeScript subpath exports. I think they're right, but it's a package move and a publish-surface change — worth doing deliberately rather than folded in here. The narrow RunConfig is the part that makes the current arrangement honest in the meantime.

Verification

bun run check-types, bun run test (57 pass, 0 fail), bun run build all green. New tests cover argsFor precedence, deadline reconciliation, id uniqueness, and the council picking up kyora-review.config.json overrides. The built binary was exercised on all three paths: tools/list over stdio, hook with a real payload, and an unknown subcommand exiting 2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DtUdeCx22CrTpMMA698SFo

…hat runs

The council package was built next to kyora review and given the same engine
pool, but none of the discipline around it. Three things fall out of that:

Entry points. Two bins (`kyora-council`, `kyora-stakes-hook`) meant no runner
could pick one: `npx @kyora-sh/council` failed outright with "could not
determine executable to run", and `bunx @kyora-sh/council kyora-stakes-hook` —
the documented PostToolUse hook — resolved to the first bin and started an MCP
stdio server with the hook's name as an argument, so every matching tool call
hung until the hook timed out. Now one bin dispatches by subcommand
(`kyora-council hook`), and the MCP server moves to server.ts behind
`startServer()`.

Configuration. council.ts forged a whole `ReviewConfig` — `base`, `failOn`,
`maxDiffBytes` and six more fields that mean nothing to a council — just to
satisfy `runEngineRaw`, and hardcoded `overrides: {}` in the process. So the
README's promise that a vendor CLI changing its flags is a config edit held for
review and silently did not for council. `runEngineRaw` now takes a narrow
`RunConfig` that `ReviewConfig` extends, and the council reads the same
`kyora-review.config.json` as review, per checkout. Config loading is shared
rather than copied.

While threading overrides through: `argsFor` fixes inverted precedence where a
configured `args` override was discarded whenever the engine defined its own
`argsWrite`/`argsChat` template, and `EngineOverride` gains `argsWrite`/
`argsChat` so the council's invocations are overridable at all.

Background jobs. Ids were `Date.now()` in base36, so two jobs starting in the
same millisecond overwrote each other's record. A job also had no deadline: if
the server was restarted mid-run, nothing ever moved it off "running" and
`council_result` reported it as in progress indefinitely. Ids now carry a
sequence, and a job past its deadline reads back as failed with a reason.

Also: `council_convene_async` seated the council twice over live quota, so the
engines it named were not guaranteed to be the ones that ran — seating is now a
separate step from asking. `agent_fanout` with `write: true` ran N write-capable
CLIs against one working tree concurrently with nothing enforcing the
"disjoint files" the schema asked for; write fanouts run in turn until each task
can be given its own checkout.

Repo config: .mcp.json pointed at `npx -y kyora-sh`, which does not exist on
npm — the server never started for anyone who cloned this. Points at the
published `@kyora-sh/mcp` now, and adds the council so the repo dogfoods it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtUdeCx22CrTpMMA698SFo
@eliahilse
eliahilse merged commit 6b1d7c6 into main Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant