Event-driven orchestrator that pulls tickets from a file, runs Agent X (dev), opens a GitHub PR, runs Agent Y (review), merges on success, and advances to the next ticket.
- File-backed ticket source (
.charles/tickets.json) - Persistent orchestrator state machine (
.charles/state.json) - GitHub webhook subscription endpoint (
/github/webhook) - Pluggable Dev/Review agents (default model config:
gpt-5.3-codex) - One-ticket-at-a-time processing
- Startup warning when no PR-triggered GitHub Actions workflow is detected
- Merge gate supports reviewer
APPROVEor review comment marker (PR reviewed by ...)
export PYTHONPATH=src
python -m pip install -e ".[dev]"For multi-project usage, keep shared defaults and secrets in a global config directory:
mkdir -p ~/.config/charles
cp config/default.toml ~/.config/charles/default.toml
cp .env.example ~/.config/charles/.envThen add per-project overrides only where needed:
mkdir -p .charles
cp config/local.example.toml .charles/local.tomlThen edit:
~/.config/charles/default.tomlfor shared defaults across repositories~/.config/charles/.envfor shared secrets (GITHUB_TOKEN,GITHUB_WEBHOOK_SECRET).charles/local.tomlonly for project-specific overrides (for examplegithub.base_branch, custom commands)
Charles always infers GitHub owner/repo from git remote get-url origin (must point to github.com).
Base branch defaults to origin/HEAD (for example main/master), with fallback to main.
Default agent commands use package-installed entrypoints, so they work across repositories.
Config precedence is:
- Environment variables
- Project
.env - Global
~/.config/charles/.env - Project
.charles/local.toml - Project
config/default.toml - Global
~/.config/charles/local.toml - Global
~/.config/charles/default.toml - Built-in defaults
python -m orchestratorYou can also run:
charles-orchestratorIf your repo is missing PR-triggered CI workflow files, you can scaffold one:
charles-orchestrator --createThis creates .github/workflows/charles-pr-checks.yml when no PR-triggered workflow is found locally.
Commit and push that file so GitHub starts sending successful check_run/check_suite events for PRs.
curl http://localhost:8080/tickSubscribe repository webhook to:
- URL:
http://<host>:8080/github/webhook - Events:
pull_request,pull_request_review,pull_request_review_comment,issue_comment,check_suite,check_run - Secret: same as
GITHUB_WEBHOOK_SECRET
Run ngrok against the orchestrator app port (default 8080):
ngrok http 8080Important:
- Use
8080(or your configuredORCH_PORT) as the ngrok target. - Do not run
ngrok http 4040. Port4040is ngrok's local inspector/API, not your app.
When your local tunnel URL changes, run:
charles-update-github-webhook --from-ngrokThis reads the current HTTPS URL from ngrok's local API (http://127.0.0.1:4040/api/tunnels) and updates the GitHub webhook to:
https://<current-tunnel-domain>/github/webhook
If using cloudflared (or any other tunnel), pass URL explicitly:
charles-update-github-webhook --public-url https://your-subdomain.trycloudflare.comInstead of editing JSON manually, reset failed tickets in the default runtime file (.charles/tickets.json):
charles-reset-failed-ticketsYou can still pass --tickets-file explicitly for non-default locations.
Global defaults file (~/.config/charles/default.toml) and optional project files (config/default.toml, .charles/local.toml) use this structure:
[orchestrator]
model = "gpt-5.3-codex"
host = "0.0.0.0"
port = 8080
log_level = "INFO"
[github]
# Optional override. Defaults to origin/HEAD (fallback "main").
# base_branch = "main"
[agents]
dev_command = "charles-dev-agent-codex"
review_command = "charles-review-agent-claude"
proposal_eval_command = "charles-proposal-eval-codex"
# Optional: only accept review events/comments from this login.
# reviewer_login = "your-claude-bot-login"When reviewer identity matches PR author, GitHub rejects APPROVE. Charles falls back to COMMENT
and also posts a PR conversation comment PR reviewed by <reviewer>. Merge gating accepts either
an APPROVE review or that marker comment.
GitHub owner/repo are inferred from git remote get-url origin when origin points to github.com.
Base branch is inferred from refs/remotes/origin/HEAD unless overridden.
Runtime files are always repo-local:
.charles/tickets.json.charles/state.json
Supported env vars (GITHUB_TOKEN, GITHUB_BASE_BRANCH, GITHUB_WEBHOOK_SECRET, ORCH_MODEL, ORCH_HOST, ORCH_PORT, LOG_LEVEL, DEV_AGENT_COMMAND, REVIEW_AGENT_COMMAND, PROPOSAL_EVAL_COMMAND, REVIEWER_LOGIN).
Optional path override env vars:
ORCH_GLOBAL_CONFIG_FILE,ORCH_GLOBAL_LOCAL_CONFIG_FILEORCH_CONFIG_FILE,ORCH_LOCAL_CONFIG_FILEORCH_GLOBAL_ENV_FILEORCH_ENV_FILE
GET /healthz-> health checkGET /tick-> trigger orchestrator progress from idlePOST /github/webhook-> GitHub event ingestion
- This is an MVP with explicit interfaces for swapping
TicketSource(e.g., Jira/Trello) and agent adapters. - In production, use GitHub App auth, stronger retry policies, and robust deduplication for webhook deliveries.