Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
{
"mcpServers": {
"kyora": {
"command": "npx",
"args": ["-y", "kyora-sh"],
"command": "bunx",
"args": ["@kyora-sh/mcp"],
"env": {
"KYORA_DATA_DIR": "${CLAUDE_PLUGIN_DATA}/data"
"KYORA_DATA_DIR": ".kyora"
}
},
"kyora-council": {
"command": "bun",
"args": ["packages/council/mcp/src/index.ts"]
}
}
}
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Tools that make coding agents trustworthy — by grounding them in what actually
- [Engines](#engines)
- [CI](#ci)
- [Configuration](#configuration)
- [kyora council — summon other model families](#kyora-council)
- [Repo layout](#repo-layout)
- [Development](#development)
- [License](#license)
Expand Down Expand Up @@ -155,7 +156,23 @@ Full setup, secret-seeding commands, and security notes: [`action/README.md`](ac

### Configuration

`kyora-review.config.json` at the repo root sets defaults (`engines`, `verify`, `post`, `base`, `failOn`, `maxDiffBytes`, `timeoutMs`), plus per-engine overrides — `bin`, `args` (with `{prompt}`/`{schema}`/`{schemaJson}`/`{out}` tokens), and `env` — so a vendor CLI changing its flags is a config edit, not a code change. CLI reference: [`packages/review/cli`](packages/review/cli).
`kyora-review.config.json` at the repo root sets defaults (`engines`, `verify`, `post`, `base`, `failOn`, `maxDiffBytes`, `timeoutMs`), plus per-engine overrides — `bin`, `args` (with `{prompt}`/`{schema}`/`{schemaJson}`/`{out}` tokens), `argsChat`, `argsWrite`, and `env` — so a vendor CLI changing its flags is a config edit, not a code change. The same overrides apply to [kyora council](#kyora-council), which spawns the same CLIs. CLI reference: [`packages/review/cli`](packages/review/cli).

## kyora council

The same engine pool, pointed at your own work instead of a diff. Your agent is one lineage with one set of blind spots; the council MCP server lets it ask the others — before it commits to something expensive to undo — and delegate work to them on separate subscriptions.

```json
{
"mcpServers": {
"kyora-council": { "command": "bunx", "args": ["@kyora-sh/council"] }
}
}
```

`council_convene` puts the same question to several families at once and returns their independent takes; `agent_spawn` hands a task to whichever capable family has the most quota left; `agent_fanout` runs different tasks across different families at once. Engine auth and `kyora-review.config.json` overrides are shared with kyora review, so both halves see the same engines configured the same way.

There is also an optional `PostToolUse` hook that watches for genuinely high-stakes moments — migrations, auth changes, irreversible operations — with a cheap model, and reminds the agent that a second lineage is available. Details: [`packages/council/mcp`](packages/council/mcp).

## Repo layout

Expand All @@ -165,6 +182,7 @@ packages/state/mcp @kyora-sh/mcp MCP server (published)
packages/state/nora @kyora/nora semantic doc indexing + search (local embeddings)
packages/state/db @kyora/db embedded PostgreSQL (PGLite) + vector search
packages/review/cli @kyora-sh/review multi-engine review CLI (published)
packages/council/mcp @kyora-sh/council cross-family council + subagents over MCP
packages/tooling/* shared eslint/tsconfig
action/ GitHub Action for kyora review
apps/reckon SWE-bench-style eval harness
Expand Down
6 changes: 3 additions & 3 deletions packages/council/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Your agent is one lineage with one set of blind spots. This gives it a way to as
| `council_task` | delegate concrete work to one family (`write: true` lets it edit files) |
| `council_result` | collect a background council or task by job id |
| `agent_spawn` | spawn a subagent from another family — you don't pick who, the least-spent capable family is chosen |
| `agent_fanout` | several *different* tasks in parallel, one per family |
| `agent_fanout` | several *different* tasks at once, one per family (read-only tasks run in parallel; `write: true` runs them in turn, since they share one checkout) |
| `council_models` | which families are summonable, and the models and efforts each accepts |
| `council_status` | who can be summoned right now, with remaining quota |

Expand All @@ -34,7 +34,7 @@ Councils are quota-aware: members are seated highest-remaining-quota first, engi
}
```

Engine auth is shared with [`@kyora-sh/review`](../../review/cli)run `kyora-review doctor` to see who is ready.
Engine auth *and configuration* are shared with [`@kyora-sh/review`](../../review/cli): run `kyora-review doctor` to see who is ready, and the `overrides` block in `kyora-review.config.json` at your repo root applies to both — a vendor CLI changing its flags is one config edit, not two. Councils use the `argsChat` / `argsWrite` overrides where a review uses `args`.

## High-stakes watcher (optional)

Expand All @@ -45,7 +45,7 @@ A `PostToolUse` hook that watches your agent's work with a cheap model and remin
"hooks": {
"PostToolUse": [
{ "matcher": "Write|Edit|MultiEdit|Bash",
"hooks": [{ "type": "command", "command": "bunx @kyora-sh/council kyora-stakes-hook" }] }
"hooks": [{ "type": "command", "command": "bunx @kyora-sh/council hook" }] }
]
}
}
Expand Down
5 changes: 2 additions & 3 deletions packages/council/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"homepage": "https://kyora.sh",
"main": "./dist/index.js",
"bin": {
"kyora-council": "./dist/index.js",
"kyora-stakes-hook": "./dist/hook.js"
"kyora-council": "./dist/index.js"
},
"files": [
"dist",
Expand All @@ -23,7 +22,7 @@
"access": "public"
},
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target bun --minify && bun build ./src/hook.ts --outdir ./dist --target bun --minify && for f in index hook; do printf '#!/usr/bin/env bun\\n' | cat - ./dist/$f.js > ./dist/$f.tmp && mv ./dist/$f.tmp ./dist/$f.js && chmod +x ./dist/$f.js; done",
"build": "bun build ./src/index.ts --outdir ./dist --target bun --minify && printf '#!/usr/bin/env bun\\n' | cat - ./dist/index.js > ./dist/tmp.js && mv ./dist/tmp.js ./dist/index.js && chmod +x ./dist/index.js",
"check-types": "tsc --noEmit",
"test": "bun test",
"dev": "bun --watch src/index.ts",
Expand Down
88 changes: 88 additions & 0 deletions packages/council/mcp/src/council.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { afterAll, describe, expect, test } from "bun:test"
import { DEFAULT_COOLDOWN_MINUTES, DEFAULT_TIMEOUT_MS, newJobId, reconcile, runConfig, type Job } from "./council"

const running: Job = {
id: "council-1",
kind: "council",
status: "running",
question: "?",
engines: ["glm"],
startedAt: Date.now() - 60_000,
deadlineAt: Date.now() + 60_000,
}

describe("reconcile", () => {
test("leaves a job that is still within its deadline alone", () => {
expect(reconcile(running).status).toBe("running")
})

test("fails a job that outlived its deadline without recording a result", () => {
const stranded = reconcile({ ...running, deadlineAt: Date.now() - 1 })
expect(stranded.status).toBe("failed")
expect(stranded.error).toContain("deadline")
})

test("never rewrites a job that already finished", () => {
const done: Job = { ...running, status: "done", deadlineAt: Date.now() - 1, replies: [] }
expect(reconcile(done).status).toBe("done")
})
})

describe("newJobId", () => {
test("is unique for jobs started in the same millisecond", () => {
const stamp = Date.now()
const ids = [newJobId("council", stamp), newJobId("council", stamp), newJobId("council", stamp)]
expect(new Set(ids).size).toBe(3)
})

test("stays filename-safe so it round-trips through the job store", () => {
expect(newJobId("agent")).toMatch(/^agent-[0-9a-z]+-[0-9a-z]+$/)
})
})

const roots: string[] = []

async function checkout(config?: unknown): Promise<string> {
const root = await mkdtemp(join(tmpdir(), "kyora-council-test-"))
roots.push(root)
await Bun.$`git -C ${root} init --quiet`.quiet().nothrow()
if (config) await writeFile(join(root, "kyora-review.config.json"), JSON.stringify(config))
return root
}

afterAll(async () => {
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })))
})

describe("runConfig", () => {
test("falls back to defaults in a checkout with no config", async () => {
const config = await runConfig(await checkout())
expect(config).toEqual({
timeoutMs: DEFAULT_TIMEOUT_MS,
cooldownMinutes: DEFAULT_COOLDOWN_MINUTES,
overrides: {},
})
})

test("picks up the same engine overrides kyora-review reads", async () => {
const root = await checkout({
timeoutMs: 30_000,
cooldownMinutes: 5,
overrides: { codex: { bin: "/opt/codex", argsChat: ["talk", "{prompt}"] } },
})
const config = await runConfig(root)
expect(config.timeoutMs).toBe(30_000)
expect(config.cooldownMinutes).toBe(5)
expect(config.overrides.codex?.bin).toBe("/opt/codex")
expect(config.overrides.codex?.argsChat).toEqual(["talk", "{prompt}"])
})

test("ignores review-only keys rather than carrying them into a run", async () => {
const config = await runConfig(await checkout({ base: "develop", failOn: "major", maxDiffBytes: 5 }))
expect(config).not.toHaveProperty("base")
expect(config).not.toHaveProperty("failOn")
})
})
Loading
Loading