Whilly v4.0 โ distributed orchestrator with Postgres-backed task queue, FastAPI control plane, and remote workers
Project description
Whilly Orchestrator
๐ v4.1 โ release-ready. Whilly is a distributed orchestrator for AI coding agents: Postgres-backed task queue, FastAPI control plane, remote workers over HTTP, and an append-only
eventsaudit log. The legacy v3.x single-process loop has been retired โ it lives at tagv3-finalfor teams that still need it. There is no backwards compatibility with v3.x runtime state โ seedocs/Whilly-v4-Migration-from-v3.md.
๐ท๐บ ะัะฐัะบะพะต ะพะฟะธัะฐะฝะธะต ะฝะฐ ััััะบะพะผ
"I'm helping โ and I've read TRIZ." โ Whilly Wiggum
What's new in v4.1
The v4.1 cleanup mission closed seven backlog items and brought the system to a
release-ready state. Headline changes (full detail in CHANGELOG.md):
- Pure decision gate (
whilly/core/gates.py) +whilly plan apply --strictโ REJECT verdicts are skipped viarepo.skip_taskand emittask.skippedevents scoped to the currentplan_idonly (cross-plan collisions are refused). - Per-task TRIZ analyzer (
whilly/core/triz.py) โ replaces the v3 plan-level analyzer. Subprocess toclaudewith a hard 25 s timeout (under the 30 s claim visibility window), fail-open on missing CLI / timeout / malformed JSON, and anevents.detailjsonb column for findings. Opt in withWHILLY_TRIZ_ENABLED=1. - Per-worker bearer auth (migration
004_per_worker_bearer) โworkers.token_hashis now nullable with a partial UNIQUE on non-null values. Thewhilly worker registerCLI mints plaintext bearers. Bearer identity is bound to the requestworker_id(403 on mismatch). Register stays bootstrap-gated byWHILLY_WORKER_BOOTSTRAP_TOKEN.WHILLY_WORKER_TOKENis deprecated โ one-shot warning, suppress withWHILLY_SUPPRESS_WORKER_TOKEN_WARNING=1. - Plan budget guard (migration
005_plan_budget) โplans.budget_usd/plans.spent_usd;events.plan_id NOT NULLandevents.task_idnullable to support the plan-level sentinel.whilly plan create --budget USDcaps spend strictly (claim gate uses<);plan.budget_exceededis emitted exactly once per crossing with{plan_id, budget_usd, spent_usd, crossing_task_id, reason, threshold_pct: 100}. Concurrent claims serialise onFOR UPDATE OF t SKIP LOCKED. - Lifespan event flusher (
whilly/api/event_flusher.py) โ bounded asyncio.Queue flushed on a 100 ms timer or 500-row threshold (whichever fires first viaasyncio.Event-driven wake),tempfile + os.replacecheckpoint, graceful drain onSIGTERM/SIGINT. TRIZ events route through the flusher whenTaskRepository(event_flusher=...)is wired; local-worker callers fall back to directINSERT. - Forge intake (migrations
006_plan_github_ref,007_plan_prd_file) โwhilly forge intake owner/repo/Nshells out togh issue view, normalises the issue into a Whilly plan via the PRD-wizard pipeline, persists it withplans.github_issue_ref+plans.prd_file, then flips the issue labelwhilly-pending โ whilly-in-progress. Idempotent re-runs are enforced by a partial UNIQUE; concurrent intake stays at-most-once on the GitHub side via a creator-vs-loser flag.plan.createdevent emitted in the same transaction as the inserts.GET /api/v1/plans/{id}exposesgithub_issue_refandprd_file. - Cleanup โ
whilly/cli_legacy.pydeleted;WHILLY_WORKTREEandWHILLY_USE_WORKSPACEare silent no-ops on v4.task.createdevents emitted per inserted task row;plan.appliedevents emitted once perwhilly plan applyinvocation with{tasks_count, skipped_count, warned_count, strict}. The full Flow A fingerprint (gate verdict โ state-machine transition โ audit row) is observable within 200 ms.
Architecture
Three boxes / containers / VMs talk to each other:
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Postgres 15+ โ โโโโโโโ โ Control plane โ โโโโโโโ โ whilly-worker โ
โ plans / tasks / โ asyncpg โ FastAPI + asyncpg โ HTTP โ httpx โ claim โ run โ
โ workers / events โ โ + lifespan flusher โ TLS โ Claude CLI subprocess โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
Postgres is the single source of truth โ operational tables (plans, tasks,
workers) plus an append-only events audit log. The state machine
(whilly/core/state_machine.py) gates every transition server-side. Workers are
stateless pollers; the control plane is a transaction shaper. See
docs/Whilly-v4-Architecture.md for the
hexagonal layout, the core / adapters split, scheduling, and lock semantics.
Migration chain
001_initial_schema
โโ 002_workers_status
โโ 003_events_detail (events.detail jsonb NULL โ TASK-104b)
โโ 004_per_worker_bearer (workers.token_hash nullable + partial UNIQUE โ TASK-101)
โโ 005_plan_budget (plans.budget_usd / spent_usd; events.plan_id NOT NULL โ TASK-102)
โโ 006_plan_github_ref (plans.github_issue_ref + partial UNIQUE โ TASK-108a)
โโ 007_plan_prd_file (plans.prd_file โ TASK-108a)
Every migration is round-trippable via alembic upgrade head โ downgrade base โ upgrade head with byte-equal final schema.
Quick start
# 1. Postgres on the control-plane box (or any reachable host)
docker compose up -d # boots postgres:15-alpine via docker-compose.yml
export WHILLY_DATABASE_URL=postgresql://whilly:whilly@localhost:5432/whilly
# 2. Install + apply migrations
pip install -e '.[server,dev]' # control-plane install closure
alembic upgrade head # applies every revision through 007
# 3a. Have an idea, not a plan? PRD wizard generates both.
whilly init "build a CLI tool for monitoring API endpoints" --slug api-monitor
# โ docs/PRD-api-monitor.md saved + plan 'api-monitor' imported into Postgres
# 3b. Already have tasks.json? Import directly. Add --strict to skip
# decision-gate REJECTs as they're imported.
whilly plan import path/to/tasks.json
whilly plan apply path/to/tasks.json --strict
whilly plan show <plan_id> # ASCII DAG of the imported plan
# 3c. Cap spend up-front (per-plan budget guard).
whilly plan create --id my-plan --name "My plan" --budget 5.00
# 3d. Pull a GitHub issue into a plan via Forge.
whilly forge intake mshegolev/whilly-orchestrator/123
# 4a. All-in-one local mode โ control plane embedded in the worker process.
whilly run --plan <plan_id>
# 4b. Distributed mode โ control plane + remote worker on different hosts.
# a) on the control-plane box:
export WHILLY_WORKER_BOOTSTRAP_TOKEN=$(openssl rand -hex 32)
uvicorn 'whilly.adapters.transport.server:create_app' --factory --port 8000
# b) on the worker box (only needs httpx โ pull the slim install):
pip install whilly-orchestrator[worker]
WORKER_TOKEN=$(whilly worker register \
--connect https://control.example.com:8000 \
--bootstrap-token "$WHILLY_WORKER_BOOTSTRAP_TOKEN" \
--plan <plan_id>)
whilly-worker \
--connect https://control.example.com:8000 \
--token "$WORKER_TOKEN" \
--plan <plan_id>
# 5. Watch progress live
whilly dashboard --plan <plan_id> # Rich Live TUI over the tasks table
A complete reproducible single-host demo (Postgres + control plane + remote
worker, all on loopback) lives in docs/demo-remote-worker.sh.
CLI surface
whilly <command> dispatches to a sub-CLI; whilly --help prints the routing
block.
| Command | Purpose |
|---|---|
whilly plan import <file> |
Validate + persist a plan JSON to Postgres (idempotent on plan_id). |
whilly plan apply <file> [--strict] |
Import + run the decision gate. With --strict, REJECTs are skipped via repo.skip_task and emit task.skipped events. |
whilly plan create --id <id> --name <name> [--budget USD] |
Mint an empty plan with an optional spend cap. |
whilly plan export <plan_id> |
Round-trip canonical JSON to stdout. |
whilly plan show <plan_id> |
ASCII dependency-graph render with status badges. |
whilly plan reset <plan_id> |
Reset task statuses to pending (soft) or wipe rows (--hard). |
whilly init "<idea>" --slug <slug> |
PRD wizard โ plan import in one step. |
whilly run --plan <id> |
All-in-one local worker (asyncpg-direct). |
whilly dashboard --plan <id> |
Rich Live TUI over the tasks table. |
whilly worker register --connect <url> --bootstrap-token <tok> |
Mint a per-worker bearer (TASK-101). |
whilly-worker --connect <url> --token <bearer> --plan <id> |
Standalone remote-worker entry (httpx-only closure). |
whilly forge intake owner/repo/N |
GitHub Issue โ Whilly plan + label transition. |
Configuration
Almost everything is controlled by env vars (see whilly/config.py
for the full set; whilly --help per subcommand for flag details).
| Variable | Purpose |
|---|---|
WHILLY_DATABASE_URL |
asyncpg DSN โ required for the control plane and whilly run. |
WHILLY_WORKER_BOOTSTRAP_TOKEN |
Required for POST /workers/register and whilly worker register. |
WHILLY_WORKER_TOKEN |
Deprecated legacy shared bearer; one-shot warning. Suppress with WHILLY_SUPPRESS_WORKER_TOKEN_WARNING=1. Use per-worker bearers instead. |
WHILLY_TRIZ_ENABLED |
Opt-in to the per-task TRIZ analyzer hook on FAIL transitions. |
WHILLY_RUN_LIVE_LLM |
Gate live claude CLI smoke tests (skipped by default). |
WHILLY_BUDGET_USD |
Legacy global cap; per-plan budgets via whilly plan create --budget are the v4.1 way. |
WHILLY_CLAUDE_PROXY_URL |
Inject HTTPS_PROXY/NO_PROXY into the spawned claude env only. See docs/Whilly-Claude-Proxy-Guide.md. |
CLAUDE_BIN |
Path to the claude CLI binary (default: claude on PATH). |
WHILLY_WORKTREE and WHILLY_USE_WORKSPACE are silent no-ops โ the v3
worktree / workspace machinery was removed in TASK-107.
Install
Two install closures, picked by deployment shape:
# Control-plane box (FastAPI + asyncpg + alembic + sqlalchemy)
pip install 'whilly-orchestrator[server]'
# Remote-worker box (httpx-only โ no asyncpg / FastAPI)
pip install 'whilly-orchestrator[worker]'
# Both, e.g. for a single-host demo or CI
pip install 'whilly-orchestrator[all]'
# Contributor / dev (editable, with ruff / mypy / pytest / testcontainers)
pip install -e '.[dev]'
The .importlinter core-purity contract enforces the split: whilly.core
cannot import asyncpg, fastapi, httpx, subprocess, uvicorn, or alembic โ CI
fails on regression. A worker VM that installs [worker] will never pull
asyncpg / fastapi by accident.
Both server and worker shapes need Claude CLI
on PATH (or CLAUDE_BIN) to actually run agents.
Development
pip install -e '.[dev]'
ruff check whilly tests # lint (CI-equivalent)
ruff format --check whilly tests # format check
mypy --strict whilly/core/ # strict types on the pure domain layer
lint-imports # core-purity contract
pytest -q # unit + integration (โ1530+ tests)
alembic upgrade head # apply the migration chain on a dev DB
Test layout:
tests/unit/โ pure, no DB.tests/integration/โ testcontainers + asyncpg; per-test ephemeral Postgres.
Live tests gated by WHILLY_RUN_LIVE_LLM=1 (live claude smoke for the TRIZ
analyzer) are skipped by default.
Conventions in CLAUDE.md: line length 120, target py312, ruff
is the source of truth, structured logging via logging.getLogger(__name__),
deprecation via log.warning(...) + env-flag suppression (not Python's
DeprecationWarning).
Documentation
CLAUDE.mdโ coding conventions and architecture pointers.docs/Whilly-v4-Architecture.mdโ hexagonal layout, scheduling, locks.docs/Whilly-v4-Worker-Protocol.mdโ HTTP wire protocol, auth, long-polling.docs/Whilly-v4-Migration-from-v3.mdโ env-var mapping and breaking changes.docs/Whilly-Init-Guide.mdโwhilly initPRD-wizard flow.docs/Whilly-Claude-Proxy-Guide.mdโ Claude CLI through HTTPS proxy / SSH tunnel.CHANGELOG.mdโ full release notes.
Credits
- Technique lineage: Ghuntley's original Ralph Wiggum loop post โ the pattern Whilly descends from.
- "I'm helping!" stamina, plus a Decision Gate, per-task TRIZ, and a PRD wizard on top โ Ralph's smarter brother.
License
MIT โ see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whilly_orchestrator-4.2.0.tar.gz.
File metadata
- Download URL: whilly_orchestrator-4.2.0.tar.gz
- Upload date:
- Size: 495.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30943e13007f861414ef5dd13ba8a175a31626274ffef36f20e7a016b745c7be
|
|
| MD5 |
2a3a30ec4b8dbfdc70f2c1464e82f2df
|
|
| BLAKE2b-256 |
c4a817d2314627133da12148943bc636a40d06a1975fd780a3603a62daeebaa7
|
Provenance
The following attestation bundles were made for whilly_orchestrator-4.2.0.tar.gz:
Publisher:
release.yml on mshegolev/whilly-orchestrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whilly_orchestrator-4.2.0.tar.gz -
Subject digest:
30943e13007f861414ef5dd13ba8a175a31626274ffef36f20e7a016b745c7be - Sigstore transparency entry: 1409792877
- Sigstore integration time:
-
Permalink:
mshegolev/whilly-orchestrator@0c214ef97ddb808b7f8b22fa9711f0b11cfdceab -
Branch / Tag:
refs/tags/v4.2.0 - Owner: https://github.com/mshegolev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0c214ef97ddb808b7f8b22fa9711f0b11cfdceab -
Trigger Event:
push
-
Statement type:
File details
Details for the file whilly_orchestrator-4.2.0-py3-none-any.whl.
File metadata
- Download URL: whilly_orchestrator-4.2.0-py3-none-any.whl
- Upload date:
- Size: 497.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dddef4ec1e70ee29c4b874d843748ec1dfd7fd1219f0a56fa52496ccc11f85af
|
|
| MD5 |
ed2a65678fb4f2e2db50e43aaaed4965
|
|
| BLAKE2b-256 |
30ee384e7e4f3f2b28381fdf57be58fd58a58a07003ce67c7cee6a13083f52de
|
Provenance
The following attestation bundles were made for whilly_orchestrator-4.2.0-py3-none-any.whl:
Publisher:
release.yml on mshegolev/whilly-orchestrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whilly_orchestrator-4.2.0-py3-none-any.whl -
Subject digest:
dddef4ec1e70ee29c4b874d843748ec1dfd7fd1219f0a56fa52496ccc11f85af - Sigstore transparency entry: 1409792896
- Sigstore integration time:
-
Permalink:
mshegolev/whilly-orchestrator@0c214ef97ddb808b7f8b22fa9711f0b11cfdceab -
Branch / Tag:
refs/tags/v4.2.0 - Owner: https://github.com/mshegolev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0c214ef97ddb808b7f8b22fa9711f0b11cfdceab -
Trigger Event:
push
-
Statement type: