quorum-orchestrator 0.1.0__tar.gz
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.
- quorum_orchestrator-0.1.0/.github/workflows/ci.yml +59 -0
- quorum_orchestrator-0.1.0/.github/workflows/release.yml +37 -0
- quorum_orchestrator-0.1.0/.gitignore +9 -0
- quorum_orchestrator-0.1.0/.python-version +1 -0
- quorum_orchestrator-0.1.0/CLAUDE.md +224 -0
- quorum_orchestrator-0.1.0/PKG-INFO +200 -0
- quorum_orchestrator-0.1.0/README.md +181 -0
- quorum_orchestrator-0.1.0/docs/architecture.md +406 -0
- quorum_orchestrator-0.1.0/docs/guide.md +667 -0
- quorum_orchestrator-0.1.0/docs/images/tui.png +0 -0
- quorum_orchestrator-0.1.0/docs/images/web-dark.png +0 -0
- quorum_orchestrator-0.1.0/docs/images/web-light.png +0 -0
- quorum_orchestrator-0.1.0/examples/steward.py +238 -0
- quorum_orchestrator-0.1.0/integrations/claude-code/.claude-plugin/plugin.json +5 -0
- quorum_orchestrator-0.1.0/integrations/claude-code/README.md +74 -0
- quorum_orchestrator-0.1.0/integrations/claude-code/commands/adopt.md +19 -0
- quorum_orchestrator-0.1.0/integrations/claude-code/hooks/hooks.json +24 -0
- quorum_orchestrator-0.1.0/integrations/codex/README.md +90 -0
- quorum_orchestrator-0.1.0/integrations/codex/hooks.json +13 -0
- quorum_orchestrator-0.1.0/integrations/codex/prompts/quorum-adopt.md +20 -0
- quorum_orchestrator-0.1.0/integrations/opencode/README.md +72 -0
- quorum_orchestrator-0.1.0/integrations/opencode/commands/quorum-adopt.md +18 -0
- quorum_orchestrator-0.1.0/integrations/opencode/plugin/quorum.js +108 -0
- quorum_orchestrator-0.1.0/pyproject.toml +58 -0
- quorum_orchestrator-0.1.0/src/quorum/__init__.py +6 -0
- quorum_orchestrator-0.1.0/src/quorum/__main__.py +10 -0
- quorum_orchestrator-0.1.0/src/quorum/actor.py +57 -0
- quorum_orchestrator-0.1.0/src/quorum/agent.py +131 -0
- quorum_orchestrator-0.1.0/src/quorum/agents/__init__.py +24 -0
- quorum_orchestrator-0.1.0/src/quorum/agents/harness_run.py +89 -0
- quorum_orchestrator-0.1.0/src/quorum/agents/manager.py +189 -0
- quorum_orchestrator-0.1.0/src/quorum/agents/prompt_agent.py +42 -0
- quorum_orchestrator-0.1.0/src/quorum/cli.py +1498 -0
- quorum_orchestrator-0.1.0/src/quorum/config.py +266 -0
- quorum_orchestrator-0.1.0/src/quorum/default_prompts/manager.md +55 -0
- quorum_orchestrator-0.1.0/src/quorum/default_prompts/task-preamble.md +37 -0
- quorum_orchestrator-0.1.0/src/quorum/fsio.py +259 -0
- quorum_orchestrator-0.1.0/src/quorum/herdr.py +91 -0
- quorum_orchestrator-0.1.0/src/quorum/home.py +171 -0
- quorum_orchestrator-0.1.0/src/quorum/llm/__init__.py +91 -0
- quorum_orchestrator-0.1.0/src/quorum/llm/cli_backend.py +57 -0
- quorum_orchestrator-0.1.0/src/quorum/llm/proxy_backend.py +24 -0
- quorum_orchestrator-0.1.0/src/quorum/messages.py +263 -0
- quorum_orchestrator-0.1.0/src/quorum/projects.py +176 -0
- quorum_orchestrator-0.1.0/src/quorum/prompts.py +37 -0
- quorum_orchestrator-0.1.0/src/quorum/registry.py +55 -0
- quorum_orchestrator-0.1.0/src/quorum/runner.py +420 -0
- quorum_orchestrator-0.1.0/src/quorum/sandbox.py +327 -0
- quorum_orchestrator-0.1.0/src/quorum/supervisor.py +377 -0
- quorum_orchestrator-0.1.0/src/quorum/tasks.py +362 -0
- quorum_orchestrator-0.1.0/src/quorum/tui/__init__.py +0 -0
- quorum_orchestrator-0.1.0/src/quorum/tui/app.py +250 -0
- quorum_orchestrator-0.1.0/src/quorum/views.py +262 -0
- quorum_orchestrator-0.1.0/src/quorum/web/__init__.py +0 -0
- quorum_orchestrator-0.1.0/src/quorum/web/app.py +153 -0
- quorum_orchestrator-0.1.0/src/quorum/web/static/index.html +375 -0
- quorum_orchestrator-0.1.0/tests/bin/fake_harness.py +146 -0
- quorum_orchestrator-0.1.0/tests/bin/fake_llm.py +32 -0
- quorum_orchestrator-0.1.0/tests/bin/opencode_plugin_driver.mjs +53 -0
- quorum_orchestrator-0.1.0/tests/conftest.py +56 -0
- quorum_orchestrator-0.1.0/tests/test_adopt.py +234 -0
- quorum_orchestrator-0.1.0/tests/test_cli.py +538 -0
- quorum_orchestrator-0.1.0/tests/test_example_steward.py +123 -0
- quorum_orchestrator-0.1.0/tests/test_fsio.py +100 -0
- quorum_orchestrator-0.1.0/tests/test_harness_integration.py +139 -0
- quorum_orchestrator-0.1.0/tests/test_herdr.py +172 -0
- quorum_orchestrator-0.1.0/tests/test_integrations.py +227 -0
- quorum_orchestrator-0.1.0/tests/test_llm.py +85 -0
- quorum_orchestrator-0.1.0/tests/test_manager.py +223 -0
- quorum_orchestrator-0.1.0/tests/test_messages.py +117 -0
- quorum_orchestrator-0.1.0/tests/test_nono_integration.py +187 -0
- quorum_orchestrator-0.1.0/tests/test_projects.py +67 -0
- quorum_orchestrator-0.1.0/tests/test_prompt_agent.py +143 -0
- quorum_orchestrator-0.1.0/tests/test_sandbox.py +309 -0
- quorum_orchestrator-0.1.0/tests/test_supervisor.py +320 -0
- quorum_orchestrator-0.1.0/tests/test_tasks.py +308 -0
- quorum_orchestrator-0.1.0/tests/test_tui.py +110 -0
- quorum_orchestrator-0.1.0/tests/test_web.py +111 -0
- quorum_orchestrator-0.1.0/uv.lock +759 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
with:
|
|
15
|
+
enable-cache: true
|
|
16
|
+
- run: uv sync --locked
|
|
17
|
+
- run: uv run ruff check .
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: astral-sh/setup-uv@v5
|
|
28
|
+
with:
|
|
29
|
+
enable-cache: true
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
- run: uv sync --locked --all-extras
|
|
32
|
+
# Unit tests only; test_sandbox.py covers the nono glue with a fake
|
|
33
|
+
# nono_py (fail-closed behavior, capability derivation, exec mapping).
|
|
34
|
+
- run: uv run pytest -m "not nono_integration"
|
|
35
|
+
|
|
36
|
+
# Real sandbox enforcement: installs the actual nono-py wheel and verifies
|
|
37
|
+
# the kernel enforces the capability set quorum derives — allowed writes
|
|
38
|
+
# succeed, out-of-capability writes fail, project dirs are read-only, and
|
|
39
|
+
# Mode 2/3 work end-to-end. GitHub's ubuntu runners have Landlock enabled,
|
|
40
|
+
# and the support gate below fails loudly if that ever changes, so these
|
|
41
|
+
# tests can never silently self-skip in CI.
|
|
42
|
+
nono-integration:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: astral-sh/setup-uv@v5
|
|
47
|
+
with:
|
|
48
|
+
enable-cache: true
|
|
49
|
+
- run: uv sync --locked --extra nono
|
|
50
|
+
- name: Assert sandbox support on this runner
|
|
51
|
+
run: |
|
|
52
|
+
uv run python - <<'EOF'
|
|
53
|
+
import sys
|
|
54
|
+
import nono_py
|
|
55
|
+
info = nono_py.support_info()
|
|
56
|
+
print(info)
|
|
57
|
+
sys.exit(0 if info.is_supported else 1)
|
|
58
|
+
EOF
|
|
59
|
+
- run: uv run pytest -m nono_integration -v -ra
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag is pushed. Auth is PyPI "trusted
|
|
4
|
+
# publishing" (OIDC): the quorum-orchestrator project on PyPI trusts this
|
|
5
|
+
# repo + workflow + environment, so no API token is stored anywhere.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
- run: uv sync --locked
|
|
19
|
+
- run: uv run pytest -m "not nono_integration"
|
|
20
|
+
- run: uv build
|
|
21
|
+
- uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write # required for trusted publishing
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv sync --all-extras # dev setup (extras: web, nono; the TUI is a core dep)
|
|
9
|
+
uv run pytest # full suite
|
|
10
|
+
uv run pytest tests/test_tasks.py::test_run_creates_worktree_and_streams_transcript
|
|
11
|
+
uv run pytest -m "not nono_integration" # what CI's unit-test matrix runs
|
|
12
|
+
uv run pytest -m nono_integration -v # real kernel sandbox tests (need [nono] + Landlock/Seatbelt)
|
|
13
|
+
QUORUM_HARNESS_TESTS=1 uv run pytest -m "codex_integration or opencode_integration" -v
|
|
14
|
+
# real codex/opencode adoption tests (binaries + auth; spend tokens)
|
|
15
|
+
uv run ruff check . # lint (line-length 100; E4,E7,E9,F,I,UP,B)
|
|
16
|
+
uv run quorum <cmd> # run the CLI from a checkout
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The PyPI distribution is `quorum-orchestrator` (plain `quorum` was taken); the
|
|
20
|
+
import name and CLI command stay `quorum`. Releases: bump `version` in
|
|
21
|
+
pyproject.toml, tag `vX.Y.Z`, push the tag — `.github/workflows/release.yml`
|
|
22
|
+
builds with uv and publishes to PyPI via trusted publishing (OIDC, no token).
|
|
23
|
+
|
|
24
|
+
`tests/test_nono_integration.py` self-skips when nono-py is missing or the platform
|
|
25
|
+
lacks Landlock/Seatbelt; a dedicated CI job asserts support so it can never silently
|
|
26
|
+
skip there. `test_web.py` needs the `web` extra (it `importorskip`s FastAPI).
|
|
27
|
+
|
|
28
|
+
## What quorum is
|
|
29
|
+
|
|
30
|
+
Bring-your-own-harness orchestration for long-running coding tasks: the user
|
|
31
|
+
registers projects, queues tasks in plain English, and a configured harness CLI
|
|
32
|
+
(claude / codex / opencode / anything) executes each task as a sequence of *runs*
|
|
33
|
+
in a per-task git worktree. Supervision is **itself harness-driven**: the one
|
|
34
|
+
built-in agent (the manager) runs the same harness over a situation digest and
|
|
35
|
+
acts through the quorum CLI. Quorum ships the manager and the comms substrate
|
|
36
|
+
(file-based board + inboxes), **not** prescriptive worker agents, and supervision
|
|
37
|
+
policy lives in `prompts/manager.md`, not Python.
|
|
38
|
+
|
|
39
|
+
Read `docs/architecture.md` first — it is the design record. The three invariants
|
|
40
|
+
below govern nearly every change:
|
|
41
|
+
|
|
42
|
+
1. **No privileged infrastructure.** `quorum up` is one ordinary process hosting an
|
|
43
|
+
APScheduler `BackgroundScheduler` — foreground by default, or detached with
|
|
44
|
+
`up --detach` the same way task runs detach (`quorum down` SIGTERMs it and polls
|
|
45
|
+
the lock). Task runs are detached child processes (they survive supervisor
|
|
46
|
+
restarts). No cron, systemd, daemonization frameworks, root, or open ports (the
|
|
47
|
+
web dashboard is opt-in, localhost-only).
|
|
48
|
+
2. **All state is plain files** under `QUORUM_HOME` (resolution: `--home` > `$QUORUM_HOME`
|
|
49
|
+
> `./quorum-home` if present > `~/.quorum`). No database. Adding new durable state
|
|
50
|
+
means adding a file layout, documented in `docs/architecture.md`.
|
|
51
|
+
3. **Fail loudly, recover automatically.** Views/dashboards degrade gracefully (pure
|
|
52
|
+
file readers; work with the supervisor stopped) and a harness that ignores the
|
|
53
|
+
report protocol is still observed passively — but supervision has **no no-LLM
|
|
54
|
+
fallback by design**: without a working harness the manager's tick raises every
|
|
55
|
+
time, and its `auto_pause = false` config keeps the schedule firing so it
|
|
56
|
+
self-recovers when the LLM service returns. Do not add degraded supervision paths.
|
|
57
|
+
|
|
58
|
+
These are the project's *current* design commitments, not gospel. Quorum is
|
|
59
|
+
evolving: any recorded stance — including the big three above and smaller ones
|
|
60
|
+
noted per-layer below (e.g. the TUI/web being pure readers) — is open to
|
|
61
|
+
deliberate revision when a change is worth it. Don't contort a feature to fit
|
|
62
|
+
an old rule; propose breaking the rule, and when it changes, update this file
|
|
63
|
+
and `docs/architecture.md` in the same commit so the record stays true.
|
|
64
|
+
|
|
65
|
+
### Layers
|
|
66
|
+
|
|
67
|
+
- `fsio.py` — the primitives everything else stands on: `atomic_write_*` (dot-prefixed
|
|
68
|
+
tmp in the same dir + fsync + rename), `append_jsonl`, ULID generation, `sorted_entries`
|
|
69
|
+
(skips dotfiles), and a pid-lock built on `O_EXCL` rather than `flock` so it behaves
|
|
70
|
+
identically under any sandbox-granted filesystem. **Never write state with plain
|
|
71
|
+
`open(...,'w')`** — readers must never observe a partial file.
|
|
72
|
+
- `messages.py` — one `Message` schema over two channels: an append-only board
|
|
73
|
+
(`messages/board/<topic>/`, filenames `<utc-compact>-<ULID>.json` so lexicographic
|
|
74
|
+
order is chronological) and maildir-style inboxes (`new/` → `cur/` claimed by
|
|
75
|
+
`os.rename`, so exactly one claimant wins). Task guidance (`task-<id>` inboxes) and
|
|
76
|
+
the supervisor control channel both ride this; no new transports.
|
|
77
|
+
- `tasks.py` — the task substrate: `Task`/`TaskStore` over `tasks/<id>/task.json`,
|
|
78
|
+
`report()` (the harness's return channel), path helpers shared by runner/manager/
|
|
79
|
+
views/CLI. **Status is a free-form reported string**; only `TERMINAL_STATUSES`
|
|
80
|
+
(`done`/`blocked`/`cancelled`) mean anything to quorum. `short_id` is the ULID's
|
|
81
|
+
random *tail* (the head is a same-instant-shared timestamp); `resolve()` accepts
|
|
82
|
+
unique prefixes or suffixes. `workdir_git_state` is the stranded-work probe
|
|
83
|
+
(dirty/unpushed in a task's workdir) surfaced by views and the manager digest —
|
|
84
|
+
the preamble tells harnesses to commit+push with plain git before reporting done.
|
|
85
|
+
`attached = true` marks an *adopted* live interactive session (`quorum task
|
|
86
|
+
adopt`): workdir = the user's checkout, no worktree, liveness from
|
|
87
|
+
`tasks/<id>/attached.json` (rewritten by `task hook-session-start`/
|
|
88
|
+
`hook-stop`/`hook-session-end`, which also learn the session id by cwd
|
|
89
|
+
match and deliver pending inbox guidance — as the Stop-hook block-protocol
|
|
90
|
+
JSON, or bare text via `hook-stop --format text` for shims that inject the
|
|
91
|
+
continuation themselves); `task detach` reverts it. One adapter per
|
|
92
|
+
harness under `integrations/` (claude-code, codex, opencode — the last is
|
|
93
|
+
a fail-soft JS plugin, kept a dumb pipe over the same CLI entry points),
|
|
94
|
+
kept true by `tests/test_integrations.py` (the opencode plugin is driven
|
|
95
|
+
for real under node, skipped when node is absent). The adapters ship
|
|
96
|
+
inside the wheel (hatch force-include → `quorum/integrations`) so
|
|
97
|
+
`quorum integration list|install` works from a package install;
|
|
98
|
+
`cli._integrations_root()` falls back to the repo dir in a checkout.
|
|
99
|
+
- `runner.py` — one harness run: `runner.lock` pid-lock → git worktree under
|
|
100
|
+
`worktrees/<id>` (branch `quorum/<short-id>`) → claim task inbox → compose prompt
|
|
101
|
+
(preamble + task + guidance) → substitute `{prompt}`/`{session}` into the
|
|
102
|
+
`[harness.<name>]` argv template → stream stdout to `transcript.jsonl`, capturing
|
|
103
|
+
`session_id`/`thread_id`. A harness with `inject = "stream-json"` also gets
|
|
104
|
+
mid-run guidance: `GuidancePump` holds stdin open, forwards inbox messages as
|
|
105
|
+
stream-json user turns, and closes stdin at the first idle `result` event (the
|
|
106
|
+
manager reuses the pump over the `manager` inbox). The runner **never sets task
|
|
107
|
+
status**, and refuses attached tasks outright — a substrate rail (same class as
|
|
108
|
+
`runner.lock`, a deliberate narrow bend of "the cap is the only rail") protecting
|
|
109
|
+
the user's live checkout. `launch_detached` spawns `python -m quorum task run`
|
|
110
|
+
in a new session.
|
|
111
|
+
- `agents/manager.py` — the flagship builtin, and it makes **no decisions in Python**:
|
|
112
|
+
its tick builds a situation digest (`build_digest`, pure over files — task
|
|
113
|
+
statuses, runner liveness, quiet time, report/transcript tails, the manager's own
|
|
114
|
+
action journal with then-vs-now outcomes, user directives from the `manager`
|
|
115
|
+
inbox), renders `prompts/manager.md`, and runs the configured harness
|
|
116
|
+
synchronously (cwd=home, tagged with the `actor.py` env protocol —
|
|
117
|
+
`QUORUM_ACTOR=manager`, per-run `QUORUM_ACTOR_RUN`, the resolved cap in
|
|
118
|
+
`QUORUM_ACTOR_CAP` — bounded by `run_timeout_seconds`, stdout →
|
|
119
|
+
`state/manager/transcript.jsonl`).
|
|
120
|
+
The harness acts via the quorum CLI; the CLI's `_actor_guard` auto-journals
|
|
121
|
+
every mutating action to the acting agent's journal and enforces the per-run
|
|
122
|
+
action cap (`max_actions_per_run`) — the only rail, a rate limit, never a veto.
|
|
123
|
+
Failures raise; directives are rejected back to `new/` on crash.
|
|
124
|
+
`agents/harness_run.py` holds the extracted run mechanics (`run_agent_harness`)
|
|
125
|
+
shared with `agents/prompt_agent.py::PromptAgent` (builtin `prompt`) — the
|
|
126
|
+
generic sibling: renders `prompts/<name>.md` (no digest, no wake condition;
|
|
127
|
+
conditional behavior belongs in the prompt) and runs the harness with the
|
|
128
|
+
same journal/cap rails under `state/agents/<name>/`. Prompt agents are
|
|
129
|
+
usually file-defined and created by `quorum agent create` or the web form.
|
|
130
|
+
- `agent.py` — `Agent` (synchronous, idempotent `tick()`) plus `AgentContext`, the single
|
|
131
|
+
seam through which agents touch the world: `ctx.bus`, `ctx.projects`, `ctx.llm`,
|
|
132
|
+
`ctx.prompt()`, `ctx.load_state()/save_state()`, `ctx.log_action()`, `ctx.now()`.
|
|
133
|
+
Agents take a clock as a callable — use `ctx.now()`, never `datetime.now()`.
|
|
134
|
+
`tick_lock_path` is held by both the supervisor wrapper and `agent run-once`.
|
|
135
|
+
- `supervisor.py` — one scheduler job per enabled agent, wrapped by `run_agent_tick`
|
|
136
|
+
for crash isolation: heartbeat files, an `agent.error` post to the `system` topic,
|
|
137
|
+
auto-pause after `MAX_CONSECUTIVE_FAILURES` (5). A 15s `_control` job claims the
|
|
138
|
+
`supervisor` inbox (`quorum agent pause|resume|run-now|reload`); `agent.reload`
|
|
139
|
+
is the hot-add/edit/remove path for file-defined agents (re-reads config, one
|
|
140
|
+
message for all mutations — handled *before* the job-exists guard). Pause is
|
|
141
|
+
durable: `_schedule_agent` creates the job paused when the heartbeat says
|
|
142
|
+
`paused`. An hourly janitor archives expired board messages and returns
|
|
143
|
+
crash-orphaned `cur/` claims to `new/`.
|
|
144
|
+
- `views.py` — the shared read-model assembled purely from files (`overview` includes
|
|
145
|
+
`attention_summary`, a time-windowed read of the `attention` topic that `status`,
|
|
146
|
+
the TUI banner, and the web header all surface — the board has no read-state, so
|
|
147
|
+
"needs a look" is time-bounded, not tracked); `quorum status`, the
|
|
148
|
+
web app, and the TUI are all pure readers of it. `agent_rows` estimates a stale
|
|
149
|
+
`next_run` from the schedule (`next_run_estimated`); `agent_detail` adds journal +
|
|
150
|
+
per-agent actions. Write affordances stay thin bus/config calls shared with the
|
|
151
|
+
CLI: nudging a task (TUI+web), and in the web only, board posts, project edits,
|
|
152
|
+
agent create (via `config.create_agent`) and pause/resume/run-now/reload.
|
|
153
|
+
- `actor.py` — the actor-identity env protocol: who a quorum CLI call is acting
|
|
154
|
+
as, name-generic over harness-driven agents. An agent tags the harness it
|
|
155
|
+
spawns (`actor_env(name, run_id, cap)`), the CLI resolves `current_actor()`
|
|
156
|
+
for journaling and message attribution, and the runner `strip_actor_env`s
|
|
157
|
+
spawned children so they act as themselves. Also owns `journal_path`/
|
|
158
|
+
`transcript_path` (manager at `state/manager/`, others at `state/agents/<name>/`).
|
|
159
|
+
- `registry.py` — resolves an agent `type` string: builtin short name (`manager`,
|
|
160
|
+
`prompt`), else `module:Class` with `QUORUM_HOME/plugins` prepended to `sys.path`.
|
|
161
|
+
- `llm/` — `LLMBackend` is a one-method protocol for *plugin agents'* small
|
|
162
|
+
completions — neither task harnesses nor the manager go through it. `LLMClient.complete()`
|
|
163
|
+
**never raises**; `None` means "no LLM today". No module outside `llm/` may assume
|
|
164
|
+
the `cli` backend (`proxy` is a reserved seam).
|
|
165
|
+
- `sandbox.py` — the *only* module that imports `nono_py`, always lazily and inside
|
|
166
|
+
functions. It **fails closed**. `build_capabilities` (supervisor/LLM) blocks network
|
|
167
|
+
unless `[llm]` is set; `build_task_capabilities` (per-run) grants the worktree, the
|
|
168
|
+
project's `.git` (shared object store), and `[sandbox].task_read/task_write` extras,
|
|
169
|
+
with network open.
|
|
170
|
+
- `herdr.py` — the *only* module that talks to a herdr server (terminal
|
|
171
|
+
multiplexer with agent-aware panes), over its unix-socket newline-JSON API.
|
|
172
|
+
**Fails soft** — the deliberate opposite of sandbox.py's fail-closed: herdr
|
|
173
|
+
absent/broken degrades every call to `None`/`False`, never breaking a digest
|
|
174
|
+
or nudge. Two narrow uses, both for attached tasks with a `herdr_pane`:
|
|
175
|
+
`agent_state` (pane status into the digest) and `ring_doorbell` (a
|
|
176
|
+
`task nudge` pokes the pane that guidance is waiting — the payload stays in
|
|
177
|
+
the maildir inbox; herdr is a doorbell, never a second transport). Optional
|
|
178
|
+
`[herdr]` table (`socket` override, `enabled`).
|
|
179
|
+
- `config.py` — `config.toml` is user-owned and **quorum never writes it back**; machine
|
|
180
|
+
state goes to JSON. The one config location quorum may write is `agents/<name>.toml`
|
|
181
|
+
(file-defined agents, atomic whole-file writes via `write_agent_file`/`create_agent`;
|
|
182
|
+
merged over `[agents.*]` at load, file wins; names validated + reserved-checked).
|
|
183
|
+
`[harness.<name>]` tables are argv templates; `[tasks]` holds
|
|
184
|
+
worktree/default-harness; `AgentConfig.auto_pause=false` exempts an agent from the
|
|
185
|
+
5-failure auto-pause (the manager uses it). Schedules are validated by regex and
|
|
186
|
+
translated to APScheduler trigger kwargs by `parse_schedule`.
|
|
187
|
+
- `projects.py` — `projects/<slug>.json` is canonical, but a `.quorum.toml` marker inside
|
|
188
|
+
the project directory merges over it at read time. Agents and views must go through
|
|
189
|
+
`ProjectRegistry` and must only ever *read* project dirs — task writes happen in
|
|
190
|
+
worktrees.
|
|
191
|
+
- `prompts.py` — `QUORUM_HOME/prompts/<name>.md` overrides the packaged
|
|
192
|
+
`default_prompts/` (`task-preamble`, `manager`); deleting a file restores the
|
|
193
|
+
default. `format_map` with a missing-key-preserving dict. Re-running `quorum
|
|
194
|
+
init` upgrades seeded-but-never-edited copies, recognized by hash — **when you
|
|
195
|
+
change a file in `default_prompts/`, append the replaced version's sha256 to
|
|
196
|
+
`home.py::SUPERSEDED_PROMPT_HASHES`** (`git show HEAD:src/quorum/default_prompts/<name> | shasum -a 256`).
|
|
197
|
+
- `examples/steward.py` — the one shipped example plugin (file organizer with undo),
|
|
198
|
+
loaded by path in `tests/test_example_steward.py` so the docs' worked example stays
|
|
199
|
+
true. Not a builtin; users copy it into `plugins/`.
|
|
200
|
+
|
|
201
|
+
### Adding an agent
|
|
202
|
+
|
|
203
|
+
Builtins live in `agents/__init__.py::BUILTIN_NAMES` (`manager`, `prompt`) —
|
|
204
|
+
prefer plugins unless quorum itself needs the behavior. The user-facing contract is
|
|
205
|
+
`docs/guide.md#writing-your-own-agents`: idempotent `tick()`, dedupe repeat
|
|
206
|
+
announcements through `load_state()/save_state()`, raising is safe.
|
|
207
|
+
|
|
208
|
+
### Testing idioms
|
|
209
|
+
|
|
210
|
+
`tests/conftest.py` provides `home` (scaffolded `QUORUM_HOME` in `tmp_path`, exported via
|
|
211
|
+
`$QUORUM_HOME`), `clock` (a `FakeClock` passed as `AgentContext(now=...)`), and `fake_llm`.
|
|
212
|
+
`tests/bin/fake_harness.py` is a fake coding harness (echoes argv/prompt, emits a
|
|
213
|
+
`session_id`; `report` mode calls `python -m quorum task report`; `manager_act` /
|
|
214
|
+
`manager_flood` modes act like a manager — each `[harness.*]` table pins its mode via
|
|
215
|
+
its `env` field, so a fake task harness and a fake manager harness coexist). Runner and
|
|
216
|
+
manager tests build real git repos and run the loop for real; when a test needs a
|
|
217
|
+
"live" runner, its lock holds pid 1 — never `os.getpid()`, which same-process lock
|
|
218
|
+
takeover treats as stale.
|
|
219
|
+
`test_sandbox.py` injects a fake `nono_py` via `sys.modules`; `test_nono_integration.py`
|
|
220
|
+
exercises real kernel enforcement.
|
|
221
|
+
|
|
222
|
+
Docs are part of the deliverable here: a change to the file layout, message protocol,
|
|
223
|
+
task/run lifecycle, or sandbox modes should update `docs/architecture.md` (and the
|
|
224
|
+
user-facing `docs/guide.md`) in the same commit.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: quorum-orchestrator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bring-your-own-harness orchestration for long-running coding tasks: file-based, sandboxable, monitored.
|
|
5
|
+
Author: Kevin Doherty
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: apscheduler<4,>=3.10
|
|
9
|
+
Requires-Dist: pydantic>=2
|
|
10
|
+
Requires-Dist: textual>=0.60
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Provides-Extra: nono
|
|
13
|
+
Requires-Dist: nono-py; extra == 'nono'
|
|
14
|
+
Provides-Extra: tui
|
|
15
|
+
Provides-Extra: web
|
|
16
|
+
Requires-Dist: fastapi>=0.110; extra == 'web'
|
|
17
|
+
Requires-Dist: uvicorn>=0.29; extra == 'web'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# quorum
|
|
21
|
+
|
|
22
|
+
**Orchestrate long-running coding tasks with the harness you already use —
|
|
23
|
+
supervised by that same harness.** Point quorum at your repos, queue tasks
|
|
24
|
+
in plain English, and let your coding agent CLI — `claude`, `codex`,
|
|
25
|
+
`opencode`, anything that takes a prompt — do the work in isolated git
|
|
26
|
+
worktrees. The supervisor is an agent too: quorum's **manager** periodically
|
|
27
|
+
hands your harness a digest of everything happening (every task's status,
|
|
28
|
+
output, and liveness, plus its own past actions and their outcomes) and lets
|
|
29
|
+
it decide — launch, nudge, relaunch, spin up follow-up work, or escalate to
|
|
30
|
+
you. Supervision policy is a prompt you can edit, not code. One process, no
|
|
31
|
+
root, no cron, no database; everything is a plain file you can `cat`.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
quorum task add my-api "add rate limiting to the public endpoints, then open a PR"
|
|
35
|
+
│
|
|
36
|
+
┌─ quorum up ─────────────────────────────────────────────────────┐
|
|
37
|
+
│ manager your harness, reading the whole situation and │
|
|
38
|
+
│ acting: launch, nudge, relaunch, task add, │
|
|
39
|
+
│ escalate — every action journaled and auditable │
|
|
40
|
+
│ │
|
|
41
|
+
│ task runs your harness (claude/codex/opencode/...) working │
|
|
42
|
+
│ in worktrees/<task>/, reporting progress back │
|
|
43
|
+
│ through `quorum task report` and reading guidance │
|
|
44
|
+
│ from `quorum task inbox` │
|
|
45
|
+
└──────────────────── all state in ~/.quorum ─────────────────────┘
|
|
46
|
+
▲ ▲ ▲
|
|
47
|
+
quorum status quorum tui quorum web
|
|
48
|
+
quorum manager tell (steer with `n`) (localhost only)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
Needs Python 3.11+.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv tool install quorum-orchestrator # includes the TUI dashboard
|
|
57
|
+
uv tool install "quorum-orchestrator[web]" # + the localhost web dashboard
|
|
58
|
+
uvx --from quorum-orchestrator quorum --help # zero-install trial run
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(or `pip install "quorum-orchestrator[web]"` if you don't use uv.)
|
|
62
|
+
|
|
63
|
+
The PyPI distribution is `quorum-orchestrator`; the command it installs is
|
|
64
|
+
plain `quorum` (and the import name is `quorum` too).
|
|
65
|
+
|
|
66
|
+
From a checkout: `uv sync --all-extras`, then prefix commands with `uv run`.
|
|
67
|
+
|
|
68
|
+
## Five minutes to a running task
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
quorum init # scaffold ~/.quorum
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Tell quorum how to invoke your harness: the scaffolded
|
|
75
|
+
`~/.quorum/config.toml` ships ready-to-uncomment blocks for claude, codex,
|
|
76
|
+
and opencode, plus a template for any custom agentic binary — uncomment one
|
|
77
|
+
and set `default_harness`. (Runs are unattended, so the harness needs
|
|
78
|
+
permission to act without asking; the shipped blocks use a scoped tool
|
|
79
|
+
allowlist, not a blanket bypass.)
|
|
80
|
+
|
|
81
|
+
Register a repo and queue work:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
quorum doctor # verify the setup end to end
|
|
85
|
+
quorum project add ~/work/my-api
|
|
86
|
+
quorum task add my-api "fix the flaky auth tests and open a PR"
|
|
87
|
+
quorum up --detach # supervisor in the background (`quorum down` stops it)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
(`quorum up` without `--detach` runs it in the foreground — Ctrl-C stops.)
|
|
91
|
+
Then:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
quorum status # supervisor, agents, tasks, deadlines
|
|
95
|
+
quorum task tail a3f2k9 -f # live harness transcript
|
|
96
|
+
quorum task nudge a3f2k9 "prefer the retry approach over sleeps"
|
|
97
|
+
quorum manager tell "the api task is urgent; park everything else"
|
|
98
|
+
quorum manager journal # what the manager did, and why
|
|
99
|
+
quorum tui # dashboard; select a task, press n to steer
|
|
100
|
+
quorum web # http://127.0.0.1:8787
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## What it looks like
|
|
104
|
+
|
|
105
|
+
<picture>
|
|
106
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/kvndhrty/quorum/main/docs/images/web-dark.png">
|
|
107
|
+
<img alt="quorum web dashboard" src="https://raw.githubusercontent.com/kvndhrty/quorum/main/docs/images/web-light.png">
|
|
108
|
+
</picture>
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
## Adopt a live session
|
|
113
|
+
|
|
114
|
+
The work is already underway in an interactive session? Don't re-queue it —
|
|
115
|
+
adopt it:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
quorum integration install codex # once per harness (also: opencode, claude-code)
|
|
119
|
+
quorum task adopt "refactoring the auth flow" # from the session's directory
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The session becomes an *attached* task: the manager observes it (liveness,
|
|
123
|
+
git state, reports) but never runs it — your nudges and the manager's pokes
|
|
124
|
+
are delivered *inside* the live session by the harness's hook the next time
|
|
125
|
+
it stops. `quorum task detach` hands it back to the headless runner. See
|
|
126
|
+
[docs/guide.md#adopting-a-live-session](https://github.com/kvndhrty/quorum/blob/main/docs/guide.md#adopting-a-live-session)
|
|
127
|
+
and the per-harness adapters under
|
|
128
|
+
[integrations/](https://github.com/kvndhrty/quorum/tree/main/integrations).
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
- **A task is a sequence of harness runs.** Each run executes in the task's
|
|
133
|
+
own git worktree (`~/.quorum/worktrees/<id>`), so parallel tasks on one
|
|
134
|
+
repo never collide and your checkout stays clean. Session ids are captured
|
|
135
|
+
from the harness's output so follow-up runs can `--resume`.
|
|
136
|
+
- **Status is the harness's own words.** The run prompt teaches a simple
|
|
137
|
+
protocol — report progress with `quorum task report`, check guidance with
|
|
138
|
+
`quorum task inbox` — and the conventional flow is
|
|
139
|
+
`planning → executing → reviewing → pr → done`. Quorum records what the
|
|
140
|
+
harness says; it never enforces a state machine.
|
|
141
|
+
- **The manager is an agent, not a ruleset.** Each cycle it compiles a
|
|
142
|
+
situation digest — including its own recent actions and whether they
|
|
143
|
+
changed anything — and your harness decides what to do, with real
|
|
144
|
+
authority: `task run`, `task nudge`, `task add`, `task cancel`, escalate.
|
|
145
|
+
Every action is auto-journaled (`quorum manager journal`); the journal
|
|
146
|
+
feeds back into the next digest so the manager never loops on an
|
|
147
|
+
intervention that isn't working. Steer it with `quorum manager tell`.
|
|
148
|
+
- **Guidance is a message, not a keystroke.** Your nudges and the manager's
|
|
149
|
+
pokes travel the same file-based inbox; the next run starts with them in
|
|
150
|
+
its prompt, and a cooperative harness picks them up mid-run.
|
|
151
|
+
- **Failure is loud and recovery is automatic.** If your LLM service goes
|
|
152
|
+
down, every harness-driven tick fails visibly — and keeps being scheduled,
|
|
153
|
+
so the first tick after service returns reads the world from files and
|
|
154
|
+
relaunches whatever died. No degraded fallback mode to babysit.
|
|
155
|
+
- **All state is files** under `QUORUM_HOME` (default `~/.quorum`): task
|
|
156
|
+
records, transcripts, a message board, inboxes — written with atomic
|
|
157
|
+
tmp+rename. The TUI and web dashboard are pure readers and work even when
|
|
158
|
+
the supervisor is down. Copy the directory and your whole setup moves.
|
|
159
|
+
|
|
160
|
+
## Supervision policy is a prompt
|
|
161
|
+
|
|
162
|
+
`~/.quorum/prompts/manager.md` is the manager's constitution: how patient it
|
|
163
|
+
is, when it escalates, how it words its pokes, when creating follow-up work
|
|
164
|
+
is warranted. Edit it to retune your manager; delete it to restore the
|
|
165
|
+
default. (An optional `[llm]` section separately gives *plugin* agents a
|
|
166
|
+
small-completion client — the manager and tasks run your full harness
|
|
167
|
+
directly.)
|
|
168
|
+
|
|
169
|
+
## Optional sandbox
|
|
170
|
+
|
|
171
|
+
Quorum pairs naturally with [nono](https://github.com/nolabs-ai/nono)
|
|
172
|
+
(kernel-enforced sandboxing via Landlock/Seatbelt): wrap the whole thing with
|
|
173
|
+
`nono run --profile quorum -- quorum up`, or set `[sandbox] use_nono = true`
|
|
174
|
+
to confine each task run to its worktree plus `QUORUM_HOME`. Fails closed:
|
|
175
|
+
if sandboxing was requested and nono-py is missing, nothing runs unsandboxed.
|
|
176
|
+
See [docs/guide.md](https://github.com/kvndhrty/quorum/blob/main/docs/guide.md#sandboxing).
|
|
177
|
+
|
|
178
|
+
## Customizing
|
|
179
|
+
|
|
180
|
+
- **Configure** harnesses, schedules, and the manager's action budget in
|
|
181
|
+
`config.toml` — quorum never rewrites that file.
|
|
182
|
+
- **Retune** the task preamble and the manager's policy by editing
|
|
183
|
+
`~/.quorum/prompts/*.md`; delete a file to restore the default.
|
|
184
|
+
- **Extend** with your own agents: drop a ~20-line Python file into
|
|
185
|
+
`~/.quorum/plugins/` — [examples/steward.py](https://github.com/kvndhrty/quorum/blob/main/examples/steward.py) is a
|
|
186
|
+
complete worked example (a rule-based file organizer with undo).
|
|
187
|
+
|
|
188
|
+
Everything above, in depth: **[docs/guide.md](https://github.com/kvndhrty/quorum/blob/main/docs/guide.md)**.
|
|
189
|
+
Design record: [docs/architecture.md](https://github.com/kvndhrty/quorum/blob/main/docs/architecture.md).
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
uv sync --all-extras
|
|
195
|
+
uv run pytest
|
|
196
|
+
uv run ruff check .
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Repo conventions and the layer-by-layer map live in
|
|
200
|
+
[CLAUDE.md](https://github.com/kvndhrty/quorum/blob/main/CLAUDE.md).
|