"Orchid is a symbiotic ecosystem of specialized AI agents, cultivated and orchestrated to transform ideas into reliable software systems."
Orchid is an AI agent orchestration framework you install once and run against any project.
Who is it for: Developers and small teams who want AI agents doing real work — writing code, running tests, summarizing research, calling APIs — not just answering questions. Solo devs get a tireless pair programmer. Teams get a shared agentic OS where every user has isolated credentials, budgets, and tool access.
What it does:
- Turns a plain repo into an agentic workspace: discuss requirements with an AI product manager, generate architecture docs and a task board, then execute tasks with specialized agents (developer, tester, reviewer, researcher)
- Runs tasks in parallel with dependency-aware scheduling, provider fallback, and cost tracking
- Simulates an async coworker bench via scheduled tasks — set up recurring agent prompts, MCP tool calls, or shell commands on a cron schedule, and wake up to completed work: daily standup summaries, nightly test runs, automated research briefs
- Serves a web UI, Telegram/Slack bots, and a REST API from a single
orchid serveprocess
Multi-user mode (V3):
Deploy as a shared agentic OS. Each user gets an encrypted credential vault, per-user MCP server access, and LLM/CPU spend limits — managed through two React SPAs (User Portal /app/, Admin Console /admin/).
Interface options: CLI · REST API · React web UI · Telegram · Slack · cron scheduler · remote worker nodes
See CHANGELOG.md for full version history.
~/orchid/ ← install here
~/projects/webtron/ ← your project (any git repo)
~/projects/blog/ ← another project
git clone git@github.com:scheidydude/orchid.git ~/LocalAI/orchid
cd ~/LocalAI/orchid
uv tool install .
# Config goes in ~/.config/orchid/.env (XDG standard, chmod 600)
bash scripts/setup-config.sh
# Then edit ~/.config/orchid/.env and set ANTHROPIC_API_KEY
# llama.cpp is expected at http://localhost:8080/v1 (set LLAMA_BASE_URL to override)# Create a new project with the guided wizard
orchid new "2D space shooter web game" --name webtron
# Or scaffold into an existing repo
orchid init ~/projects/webtron --name webtron --description "2D space shooter"
# Check lifecycle phase and task status
orchid --project ~/projects/webtron --status
orchid --project ~/projects/webtron --phase
# Run the planning workflow (discuss → requirements → tasks)
orchid --project ~/projects/webtron --discuss
orchid --project ~/projects/webtron --approve # advance to next phase
# Run all pending tasks autonomously
orchid --project ~/projects/webtron --mode auto
# Run a single specific task
orchid --project ~/projects/webtron --run-task T015
# Interactive chat with an agent
orchid --project ~/projects/webtron --mode interactive
# Persistent tmux session
./scripts/start_session.sh ~/projects/webtronOrchid V3 turns a single-user orchestrator into a multi-tenant agentic OS where each user gets isolated credentials, project access, MCP servers, and LLM spend limits — all managed through two React SPAs served by the same orchid serve process.
orchid serve --port 7842
│
├── /api/auth/* JWT, OIDC, API keys, audit log
├── /api/user/* vault, MCP servers, notifications
├── /api/admin/* MCP catalog, budget reset, runs monitor, config
├── /api/scheduler/* cron task manager (per-user)
├── /app/* User Portal SPA (React, build: orchid/interfaces/portal/)
└── /admin/* Admin Console SPA (React, build: orchid/interfaces/admin/)
After login, users land on the User Portal. Pages:
| Page | What you can do |
|---|---|
| Dashboard | Scheduled tasks (status, next/last run, enable/disable toggle) + projects |
| Settings → Profile | Change username / email / password |
| Settings → API Keys | Create and revoke API keys |
| Settings → Credentials | Per-user encrypted vault — store ANTHROPIC_API_KEY, OPENAI_API_KEY, etc. |
| Settings → Notifications | Email / Telegram / Slack toggles + channel IDs |
| Settings → MCP Servers | View admin-granted servers; add private servers (if enabled) |
Credential vault keys matching known provider env vars (ANTHROPIC_API_KEY, OPENAI_API_KEY, …) are automatically injected into scheduled task execution — users supply their own API keys without admin involvement.
Admin-role users see the Admin Console at /admin/. Tabs:
| Tab | What you can do |
|---|---|
| Users | List, invite (email link), edit role/email/projects, deactivate |
| MCP Catalog | Define shared MCP servers; set scope (shared/private/admin-only); grant/revoke per role or user ID |
| Audit Log | Paginated, filterable by user/action; expandable detail JSON |
| Quotas | Set per-user budget_usd (LLM spend cap) and cpu_budget_seconds (daily wall-clock cap); usage bars; reset spend counter |
| Task Monitor | Live view of all users' scheduled task runs; filter by user/status; expandable output/error |
| System Config | Toggle allow_user_mcp, allow_user_projects; set default quota values for new users |
Each user gets a Fernet-encrypted vault derived from ORCHID_VAULT_KEY:
# Required env var — set in ~/.config/orchid/.env
ORCHID_VAULT_KEY=<random-32-char-secret>Key derivation: HKDF-SHA256(ORCHID_VAULT_KEY, salt=b"orchid-vault-v1", info=user_id.encode()). Each user has a distinct Fernet key. Compromising one user's key does not expose others. Rotating ORCHID_VAULT_KEY invalidates all vaults — document this in your ops runbook.
Admin defines shared MCP servers once. Users see only the servers they're authorised for:
scope: shared— all users with matching rolescope: admin-only— admins onlyscope: private— explicit grants onlyrequires_credential: GMAIL_TOKEN— user must have that vault key to connect
Users can also add private MCP server definitions (admin can disable this with web.allow_user_mcp: false).
Set budget_usd per user. Every Anthropic API call in agent_tool tasks accrues cost. If budget_used_usd >= budget_usd, the task run fails with BudgetExceededError. 0.0 = unlimited.
CPU budget (cpu_budget_seconds) caps total wall-clock seconds of task execution per day (UTC midnight reset).
Reset a user's spend counter via the Quotas tab or:
POST /api/admin/users/{user_id}/budget/reset
# User Portal
cd orchid/interfaces/portal && npm install && npm run build
# Admin Console
cd orchid/interfaces/admin && npm install && npm run buildDev servers (hot reload):
# Portal: http://localhost:5174
cd orchid/interfaces/portal && npm run dev
# Admin: http://localhost:5175
cd orchid/interfaces/admin && npm run devBy default Orchid stores users in ~/.config/orchid/users.json. For production multi-node deployments, switch to PostgreSQL:
# Install the extra
uv pip install 'orchid[postgres]'
# Point Orchid at Postgres (add to ~/.config/orchid/.env)
ORCHID_AUTH_STORE_DSN=postgresql://orchid:password@localhost/orchid
# Migrate existing users from JSON → Postgres (idempotent, skips existing rows)
orchid migrate-to-postgres --dsn postgresql://orchid:password@localhost/orchid
# Dry-run first to preview what will be migrated
orchid migrate-to-postgres --dsn ... --dry-runget_store() automatically uses PostgresUserStore when ORCHID_AUTH_STORE_DSN is set. Tables are created on first connect; schema migrations are idempotent (ALTER TABLE … ADD COLUMN IF NOT EXISTS).
When web.allow_user_projects is enabled (default: true), non-admin users can create projects. Projects are stored under a user-scoped path:
~/.config/orchid/projects/{user_id}/{project_name}/
Ownership is tracked in ~/.config/orchid/projects/registry.json. Admins see all projects; users see only projects in their User.projects allowlist (empty = unrestricted).
Toggle project creation for non-admins:
PUT /api/admin/config {"web.allow_user_projects": false}
Scheduled task completions (success or failure) send DMs if the user has configured a Telegram chat ID or Slack user ID in their notification settings (Settings → Notifications in the portal).
Requires --telegram / --slack flags on orchid serve:
orchid serve --watch-dir ~/LocalAI --telegram --slackThe orchid CLI integrates with a running orchid serve instance for authenticated operations. Once logged in, vault credential injection, budget enforcement, and per-user MCP catalog ACLs apply to every CLI run automatically.
# Authenticate — saves session to ~/.config/orchid/cli_session.json (mode 0600)
orchid login --server http://localhost:7842 --username alice
# Show current user, role, and budget/CPU usage
orchid whoami
# Revoke server-side token and delete local session
orchid logoutNo session = silent fallback to anonymous single-user mode. Zero breaking change.
User management (admin only):
orchid user list # table: all users, roles, budget usage
orchid user invite alice@example.com # create invite link (email auto-sent if SMTP set)
orchid user invite alice@example.com --role admin
orchid user budget-reset <user_id> # reset budget_used_usd to 0Scheduler:
orchid scheduler list # list your scheduled tasks
orchid scheduler run <task_id> # trigger a task immediatelyAudit log (admin only):
orchid audit # last 50 events
orchid audit --limit 200 --user alice --action LOGINAdmins create users via invite — no open registration:
- Admin:
POST /api/admin/inviteororchid user invite EMAIL→ returnsinvite_url - Admin sends URL to user (email auto-sent if SMTP configured)
- User visits URL → sets password → account activated
| Role | Permissions |
|---|---|
admin |
All endpoints; admin console; see all users/tasks/runs |
user |
Own tasks/vault/credentials/MCP servers; portal only |
readonly |
Read-only; no task execution |
Orchid V2 introduces a lifecycle state machine that guides a project from idea to working software:
NEW → DISCUSSING → REQUIREMENTS → PLANNING → READY → EXECUTING → COMPLETE
orchid --project ~/projects/webtron --discussChat with the DiscussionAgent (powered by Claude) to clarify what you're building. The agent asks questions, surfaces ambiguities, and records your answers.
orchid --project ~/projects/webtron --approveAdvancing past DISCUSSING runs the ProductManagerAgent, which generates:
REQUIREMENTS.md— user stories, acceptance criteriaARCHITECTURE.md— recommended stack and component design
Then the ProjectManagerAgent generates:
MILESTONES.md— delivery phases with success metricstasks.md— fully formed task list ready to execute
orchid --project ~/projects/webtron --mode autoOrchid picks the next pending task, routes it to the right agent and model, and loops until complete. Gates between phases require --approve to advance.
orchid --project PATH --phase show current phase
orchid --project PATH --discuss start/continue discussion
orchid --project PATH --approve [--auto] approve current gate
orchid --project PATH --artifacts list planning artifact status
orchid --project PATH --get-result T001 print task result output
orchid --project PATH --run-task T015 run a single specific taskAfter orchid init and a planning run:
webtron/
├── CLAUDE.md ← hot memory: context, decisions, current focus
├── tasks.md ← task board: parsed by orchid, edited by you
├── REQUIREMENTS.md ← generated by PM agent
├── ARCHITECTURE.md ← generated by PM agent
├── MILESTONES.md ← generated by PM agent
├── .orchid.yaml ← optional per-project config
└── .orchid/ ← runtime data (gitignored)
├── decisions.json
├── project.state.json ← lifecycle phase state
├── discussion/
│ ├── conversation.jsonl
│ └── context.md
├── task_results.json ← TaskResultStore (--get-result)
├── task_metrics.jsonl ← per-task timing, iterations, action counts
└── session_logs/
project: webtron
description: "2D space shooter web game"
model_preference: auto # claude | local | auto
agent_roles:
- developer
- researcher
- reviewer
context_files: # extra files loaded into every agent prompt
- README.md
- docs/architecture.md
# memory:
# compression_threshold: 8000
# gates: # override phase-transition gate behaviour
# ready_to_executing: auto # auto | human (default: human)- [ ] **T001** Build player ship `type:code_generate` `p1` `agent:developer`
- src/player.py — movement, shooting, collision
- [ ] **T002** Write unit tests `type:code_generate` `p2`
- [ ] **T003** Review T001 `type:review` `p2` `agent:reviewer` `needs:T001`
- [ ] **T099** Rollup sprint results `type:rollup` `rollup:T001,T002,T003` `output:SPRINT1.md`
- [~] **T004** Skip this task `type:draft` `p3` # skipped tasks excluded from auto runsTask types: code_generate draft review summarize search plan critique synthesize rollup verify
Priorities: p1 high · p2 normal · p3 low
Status: [ ] TODO · [x] DONE · [~] SKIP · [!] BLOCKED
# Core run modes
orchid --project PATH --mode auto run all pending tasks
orchid --project PATH --mode interactive chat with agent
orchid --project PATH --status task board + hot memory (--project defaults to cwd)
orchid --project PATH --add-task "title" add a task quickly
orchid --project PATH --add-task "title" --type review --priority 1
# Lifecycle (V2)
orchid --project PATH --phase show current lifecycle phase
orchid --project PATH --discuss open discussion with AI PM
orchid --project PATH --approve advance to next phase (human gate)
orchid --project PATH --approve --auto advance without confirmation
orchid --project PATH --artifacts list planning artifact existence
orchid --project PATH --get-result T001 print saved task output
orchid --project PATH --run-task T015 run a single specific task
# Task management
orchid task add --title "..." --type code_generate --project PATH
orchid task done --id T001 --project PATH
orchid task block --id T002 --project PATH
orchid task skip --id T015 --project PATH # mark task as skipped ([~])
# Interfaces
orchid serve --watch-dir ~/LocalAI --port 7842 persistent multi-project server
orchid serve --bots start central bot server (Telegram + Slack)
orchid serve --telegram start Telegram bot via central server
orchid serve --slack start Slack bot via central server
orchid web --project PATH [--port 7842] single-session web UI
# Deprecated (use `orchid serve --telegram` or `orchid serve --slack` instead)
orchid telegram --project PATH → DEPRECATED, use `orchid serve --telegram`
orchid slack --project PATH → DEPRECATED, use `orchid serve --slack`
# Session checkpointing
orchid --project PATH --list-checkpoints list available checkpoints
orchid --project PATH --rewind CHECKPOINT_ID restore session to checkpoint (no persist)
orchid --project PATH --resume CHECKPOINT_ID restore and approve gate (continue from checkpoint)
# Output format
orchid --project PATH --mode auto --output-format stream-json emit NDJSON event stream
# Hooks
orchid hooks list [--project PATH] [--section tasks|phases|agent|session] [--event E] [--type shell|http|python]
orchid hooks show [--project PATH] HOOK_ID
orchid hooks validate [--project PATH]
orchid hooks test [--project PATH] HOOK_ID
orchid hooks stats [--project PATH]
orchid hooks add [--project PATH] --event E --type shell --cmd "..."
orchid hooks remove [--project PATH] HOOK_ID
# MCP servers
orchid mcp ls [--project PATH] list tools from all configured MCP servers
orchid mcp call [--project PATH] TOOL [ARGS] call a specific MCP tool
# Auth (requires orchid serve running)
orchid login [--server URL] [--username U] authenticate, save session locally
orchid logout revoke token + delete session file
orchid whoami show user, role, email, budget usage
# User management (admin only; requires session)
orchid user list list all users with budget/CPU usage
orchid user invite EMAIL [--role R] create invite link
orchid user budget-reset USER_ID reset a user's LLM spend counter
# Scheduler (requires session)
orchid scheduler list list your scheduled tasks
orchid scheduler run TASK_ID trigger a task immediately
# Audit log (admin only; requires session)
orchid audit [--limit N] [--user UID] [--action A]
# Diagnostics
orchid --check-providers probe all configured providers
orchid --project PATH --tail tail the live session log
orchid --project PATH --inject "text" inject message into running agent
orchid --project PATH --recall "query" semantic search over memory
orchid --project PATH --search "query" web search
orchid --project PATH --mode auto --trace log each ReAct iteration for debugging
Orchid ships a React-based web interface for managing projects, planning, tasks, agent runs, vector memory recall, and session history.
Persistent server with auto-discovery (recommended):
# Scans ~/LocalAI and ~/Documents/Development for orchid projects automatically
orchid serve --watch-dir ~/LocalAI --watch-dir ~/Documents/Development
# Explicit projects
orchid serve --project ~/myapp --project ~/other/projectSingle or multi-project:
orchid web --project ~/projects/webtron
orchid web --project ~/projects/webtron --project ~/projects/blogOpen http://localhost:7842 in your browser.
Health check: GET /health → {"status": "ok", "projects": N} — used by Traefik and systemd to confirm the server is up.
- Login page — checks
/api/auth/meon load; shows a centered sign-in form if unauthenticated; logout button in header; no flash of UI before auth resolves - PM Dashboard tab — project health at a glance: MilestoneProgress, DependencyGraph (cytoscape.js DAG), SessionBurndown, PhaseTimeline, TaskTiming table from
task_metrics.jsonl(deduplicates by task so re-run tasks show current status) - Planning tab — V2 lifecycle panel: phase indicator, discussion chat, artifact viewer, gate approval panel, NewProject wizard
- Discussion history — persistent chat history with AI PM, view previous conversations and context
- Discussion streaming — real-time WebSocket feed of PM agent responses as they stream
- Task board — view, create, and update task status; skip tasks with [~] status
- Agent stream — live WebSocket feed of agent activity during a run
- Decision log — full history of architectural decisions
- Session history — browse and replay past agent session logs
- Vector recall — semantic search over embedded session memory
- Hot memory — view the project's CLAUDE.md context in real time
- Run controls — start and stop agent runs from the browser; run single tasks with ▶ button
- Settings tab — machine profile editor and provider availability status
- Project Config tab — view and edit .orchid.yaml settings per project
- Mobile-responsive UI — full PWA support: hamburger drawer navigation, touch-adapted controls, swipe gestures, safe-area insets, offline-capable via service worker manifest
- Active/Inactive project grouping — projects grouped by activity status in sidebar
- Auto-discovery — projects in watched directories appear automatically;
adding
.orchid.yamlto a directory (viaorchid init) registers it within seconds without restarting the server - Project switcher — sidebar shows all projects with task counts, filesystem path, last session timestamp, and live status indicator
The Project Config tab provides a dedicated interface for managing per-project settings:
- View and edit
.orchid.yamlconfiguration - Set model preferences (claude/local/auto)
- Configure agent roles
- Manage context files loaded into agent prompts
- Override gate behaviour (auto vs human approval)
- Shell mode settings (blocklist/allowlist)
Changes are saved immediately and reflected in subsequent agent runs.
Projects in the sidebar are automatically grouped by activity status:
- Active projects — those with recent sessions or pending tasks
- Inactive projects — projects with no recent activity
This grouping helps you quickly focus on projects that need attention while keeping completed or dormant projects accessible but visually separated.
A ready-to-install service file is provided:
# Install and start
sudo cp scripts/orchid-serve.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now orchid-serve
# Or use the install script
bash scripts/install-orchid-serve.sh
# Logs
sudo journalctl -u orchid-serve -fThe service watches /home/dave/LocalAI and /home/dave/Documents/Development
by default. Edit scripts/orchid-serve.service to change watch directories.
Orchid ships three separate React SPAs. Build all three for a complete deployment:
# Power-user project SPA (existing web UI)
cd orchid/interfaces/web_ui
npm install && npm run build # outputs to web_ui/dist/
# User Portal (/app/)
cd orchid/interfaces/portal
npm install && npm run build # outputs to portal/dist/
# Admin Console (/admin/)
cd orchid/interfaces/admin
npm install && npm run build # outputs to admin/dist/When installing via uv tool install, build first:
cd orchid/interfaces/web_ui && npm run build
cd orchid/interfaces/portal && npm run build
cd orchid/interfaces/admin && npm run build
cd ~/LocalAI/orchid && uv tool install . --forceRun the Vite dev server alongside the FastAPI backend for hot-reload:
# Terminal 1 — backend
orchid web --project ~/projects/webtron --dev
# Terminal 2 — frontend (proxies API calls to :7842)
cd orchid/interfaces/web_ui
npm run dev
# Open http://localhost:5173Frameworks and core libraries
| Library | Version | Role |
|---|---|---|
| React | 18 | UI component frameworks |
| React DOM | 18 | Browser rendering |
Build tooling
| Tool | Version | Role |
|---|---|---|
| Vite | 5 | Dev server, bundler, HMR |
Vite plugins and add-ons
| Plugin | Version | Role |
|---|---|---|
| @vitejs/plugin-react | 4 | JSX transform and React Fast Refresh |
Runtime: none beyond React — no routing library, no state management
library, no CSS framework. Styles are a single hand-written index.css
using CSS custom properties (variables) for the dark theme.
Backend serving: FastAPI StaticFiles serves web_ui/dist/ in
production. The SPA catch-all route returns index.html for all
non-API paths.
Orchid V2.1 introduces a central bot server that unifies Telegram and Slack
bot management under a single orchid serve command. This replaces the legacy
per-project bot commands with a scalable multi-project architecture.
# Start the central server with both Telegram and Slack bots
orchid serve --bots --port 7842
# Start only Telegram bot
orchid serve --telegram --port 7842
# Start only Slack bot
orchid serve --slack --port 7842
# Combine with project watching
orchid serve --bots --watch-dir ~/LocalAI --watch-dir ~/Documents/DevelopmentConfigure bot tokens in ~/.config/orchid/.env:
# Telegram bot token (required for --telegram or --bots)
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
# Slack bot token (required for --slack or --bots)
SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-AbCdEfGhIjKlMnOpQrStUvWxThe legacy bot commands have been deprecated:
| Old Command | New Command |
|---|---|
orchid telegram --project PATH |
orchid serve --telegram |
orchid slack --project PATH |
orchid serve --slack |
The old commands launched bot instances tied to a single project. The new central server manages multiple projects simultaneously and routes commands based on channel-to-project mappings.
Telegram bot commands use underscores and support project context:
| Command | Description |
|---|---|
/orchid_start |
Start a new orchid session in the current project |
/orchid_status |
Show task board and current phase |
/orchid_projects |
List all available projects |
/orchid_switch <project> |
Switch to a different project |
/orchid_phase |
Show current lifecycle phase |
/orchid_tasks |
List pending tasks |
/orchid_approve |
Approve current lifecycle gate |
/orchid_discuss |
Start discussion with AI PM |
/orchid_help |
Show available commands |
Slack bot commands use hyphens and follow Slack's command conventions:
| Command | Description |
|---|---|
/orchid-status |
Show task board and current phase |
/orchid-projects |
List all available projects |
/orchid-switch <project> |
Switch to a different project |
/orchid-phase |
Show current lifecycle phase |
/orchid-tasks |
List pending tasks |
/orchid-approve |
Approve current lifecycle gate |
/orchid-discuss |
Start discussion with AI PM |
/orchid-help |
Show available commands |
The central bot supports channel-to-project mapping for team workflows:
# Each channel can be bound to a specific project
# Commands in that channel automatically target the bound project
Channel: #webtron-dev → Project: ~/projects/webtron
Channel: #blog-team → Project: ~/projects/blog
Channel: #general → Project: (user-selected via /orchid_switch)
How routing works:
- When a command is received, the bot looks up the channel ID
- If the channel is mapped to a project, that project is used
- If not mapped, the user's last-selected project is used
- Users can override with
/orchid_switch <project>or/orchid_switch <project>
The central bot maintains state in two JSON files (stored in ~/.config/orchid/):
Maps Slack channel IDs to projects:
{
"channels": {
"C0123456789": {
"project_path": "/home/user/projects/webtron",
"bound_at": "2026-03-22T10:00:00Z",
"bound_by": "user123"
},
"C9876543210": {
"project_path": "/home/user/projects/blog",
"bound_at": "2026-03-22T11:30:00Z",
"bound_by": "user456"
}
}
}Tracks Telegram user sessions and project selections:
{
"users": {
"123456789": {
"username": "john_doe",
"current_project": "/home/user/projects/webtron",
"last_active": "2026-03-22T12:00:00Z"
}
},
"projects": {
"/home/user/projects/webtron": {
"active_sessions": 2,
"last_command": "2026-03-22T12:05:00Z"
}
}
}The central server can manage unlimited projects simultaneously:
# Watch multiple directories for projects
orchid serve --bots \
--watch-dir ~/LocalAI \
--watch-dir ~/Documents/Development \
--watch-dir ~/projects
# Explicitly register projects
orchid serve --bots \
--project ~/projects/webtron \
--project ~/projects/blog \
--project ~/projects/api-serviceProjects are auto-discovered by scanning for .orchid.yaml files in watched
directories. Adding a new project (via orchid init) registers it within
seconds without restarting the server.
Install the central bot as a service:
# Copy the service template (includes --bots flag)
sudo cp scripts/orchid-serve.service /etc/systemd/system/
# Edit to configure environment variables
sudo nano /etc/systemd/system/orchid-serve.service
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable --now orchid-serve
# View logs
sudo journalctl -u orchid-serve -fThe service template includes documentation for TELEGRAM_BOT_TOKEN and
SLACK_BOT_TOKEN environment variables.
Orchid fires lifecycle events at key points in the agent loop. Hooks let you run shell commands, HTTP requests, or Python callables in response — without modifying core code.
| Section | Events |
|---|---|
| Session | session.start, session.end |
| Phase | phase.transition |
| Task | task.start, task.complete, task.blocked, task.skipped |
| Agent | agent.thought, agent.tool_use, agent.tool_result |
hooks:
tasks:
- event: task.complete
type: shell
cmd: "notify-send 'Orchid' 'Task {{ task_id }} done'"
blocking: false
- event: task.blocked
type: http
url: "https://hooks.slack.com/services/..."
method: POST
body: '{"text": "Task {{ task_id }} blocked: {{ reason }}"}'
session:
- event: session.end
type: python
module: "my_project.hooks"
function: "on_session_end"
blocking: trueShell hooks are sandboxed by the existing shell allowlist. HTTP hooks respect a configurable timeout. Hook errors are logged but never crash the agent loop.
orchid hooks list --project ~/projects/webtron
orchid hooks validate --project ~/projects/webtron
orchid hooks test --project ~/projects/webtron HOOK_ID
orchid hooks stats --project ~/projects/webtronHTTP hooks use a per-event-type circuit breaker. After 5 consecutive failures the breaker opens and blocks further requests for a configurable window. This prevents a failing webhook from stalling the agent loop.
hooks:
tasks:
- event: task.complete
type: http
url: "https://hooks.slack.com/..."
circuit_breaker:
enabled: true
failure_threshold: 5
recovery_timeout_s: 60Every shell and HTTP hook execution is written to .orchid/audit_log.jsonl:
Agents are granted only the tools appropriate for their role:
| Agent | Restrictions |
|---|---|
| TesterAgent | Read tools + bash; no write_file, no git commit/push |
| ReviewerAgent | Read tools only; no write_file, no bash, no git |
| ResearcherAgent | Read tools + search; no write_file |
| DeveloperAgent | Unrestricted (intentional) |
Configure overrides in .orchid.yaml:
agents:
allowed_tools:
researcher: ["read_file", "list_dir", "bash", "search"]Agents can read and write git state directly through built-in tool functions. Enable in .orchid.yaml:
agents:
git_tools_enabled: trueAvailable tools injected into the agent ReAct loop:
| Tool | Description |
|---|---|
git_status |
Working tree status |
git_diff |
Staged and unstaged diffs |
git_log |
Commit history with optional path filter |
git_commit |
Stage and commit files |
git_push |
Push to remote |
git_pull |
Pull from remote |
git_branch_list |
List branches |
git_branch_create |
Create branch |
git_branch_delete |
Delete branch |
git_checkout |
Switch branch |
git_stash |
Stash working changes |
git_stash_pop |
Pop stash |
Git tools are available to DeveloperAgent by default. TesterAgent and ReviewerAgent have restricted tool sets and cannot commit or push.
Orchid builds a dependency graph from needs: annotations and dispatches tasks in parallel groups:
T001 (no deps) ─┐
T002 (no deps) ─┤─ group 1 (parallel)
T003 (no deps) ─┘
T004 needs:T001,T002 ─┐
T005 needs:T003 ─┘─ group 2 (parallel, runs after group 1)
T006 needs:T004,T005 ─── group 3 (sequential)
Tasks within a group run concurrently via ThreadPoolExecutor. Provider semaphores cap concurrent API calls:
# orchid.defaults.yaml — override in .orchid.yaml
runner:
max_parallel: 4
provider_concurrency:
claude: 3
openrouter: 3
openai: 3Sub-tasks delegated by agents can run in isolated git worktrees:
# .orchid.yaml
worktree:
enabled: true # false by default
max_worktrees: 10
auto_cleanup: trueEach delegated task gets its own checkout at .orchid/worktrees/<task_id>/. Worktrees are cleaned up after the task completes. Task IDs are path-sanitized to prevent directory traversal.
Agents can create new tasks at runtime using the spawn_task tool:
Action: spawn_task
Action Input: {"title": "Write integration test for auth module", "type": "code_generate", "priority": 2}
New tasks are injected into tasks.md with a unique ID, recorded in the session log, and picked up in the next dispatch cycle. The tool uses thread-local session references so parallel tasks can each spawn children independently.
DeveloperAgent's system prompt documents spawn_task with usage rules (what to spawn, when to defer instead).
Pre-instantiated agents are cached and reused across tasks via an LRU pool. This eliminates repeated model initialization overhead on long runs.
# .orchid.yaml
agent_pool:
enabled: true # false by default
max_size: 8
idle_ttl_seconds: 300Pool is keyed by (agent_type, model_key). LRU eviction removes least-recently-used agents when the pool is full. An idle-eviction thread removes agents that have been unused longer than idle_ttl_seconds. Falls back to direct instantiation if the pool is disabled or errors.
Token usage and estimated cost are recorded per task in .orchid/cost_ledger.jsonl:
# Daily spend summary is logged at session end
# Fields: task_id, model, input_tokens, output_tokens, cache_read_tokens,
# cache_write_tokens, cost_usd, timestamp# .orchid.yaml
cost:
budget_usd: 10.00 # daily budget cap (0 = no limit)
budget_warn_pct: 0.80 # warn at 80% of budget
budget_stop: true # halt if budget exceeded
enforce_budget: false # gate: enable cost-aware routing
prefer_local_under_pressure: false # prefer local model when rate-limitedWhen enforce_budget: true, tasks that would exceed the daily budget raise BudgetBlockedError and halt execution. When prefer_local_under_pressure: true, 429 rate-limit responses trigger automatic fallback to the local model for subsequent tasks.
HTTP 429 responses from any provider are detected and recorded. The cost scheduler tracks rate pressure per provider and can automatically select a cheaper alternative.
When a provider returns a retriable error (429, 502, 503, timeout), Orchid tries the next provider in an ordered fallback chain before marking the task BLOCKED.
Configure per task type in .orchid.yaml:
providers:
task_types:
code_generate:
name: claude
fallback: [openrouter, local] # tried in order on 429/502/503Global settings in orchid.defaults.yaml (override in project config):
providers:
fallback_on_errors: [429, 503, 502] # HTTP codes that trigger fallback
max_fallback_attempts: 3 # max providers tried per taskFailed providers are marked rate-pressured so CostAwareScheduler skips them for subsequent tasks in the same run. Successful fallback is logged at WARNING level.
Orchid can connect to any Model Context Protocol server and expose its tools to the agent ReAct loop automatically.
| Transport | Use case |
|---|---|
stdio |
Local process (subprocess.Popen over stdin/stdout) |
http |
Remote or sidecar MCP server (httpx sync) |
# .orchid.yaml
mcp_servers:
- name: filesystem
transport: stdio
cmd: ["npx", "@modelcontextprotocol/server-filesystem", "/tmp"]
- name: remote-tools
transport: http
url: "http://localhost:9000/mcp"MCP tools are discovered at session start and injected alongside built-in tools. The MCPManager connects all configured servers before task execution and tears down on session end.
orchid mcp ls --project ~/projects/webtron # list available tools
orchid mcp call --project ~/projects/webtron echo '{"msg": "hello"}'Orchid captures a checkpoint of session state before executing each task. Checkpoints let you rewind to any prior task boundary and re-run from there.
# List checkpoints for a project
orchid --project ~/projects/webtron --list-checkpoints
# Rewind session state to a checkpoint (does not persist — run --mode auto to continue)
orchid --project ~/projects/webtron --rewind CP-20260504-T012
# Rewind and approve the gate (resume execution from checkpoint)
orchid --project ~/projects/webtron --resume CP-20260504-T012Orchid keeps the 5 most recent checkpoints per project and prunes older ones at session end. Checkpoints are stored in .orchid/checkpoints/.
Pass --output-format stream-json to emit a newline-delimited JSON (NDJSON) event stream instead of the default Rich terminal output. Each line is a typed event:
orchid --project ~/projects/webtron --mode auto --output-format stream-json | jq .{"type": "session_start", "session_id": "...", "timestamp": "..."}
{"type": "task_start", "task_id": "T015", "title": "Build login form", ...}
{"type": "agent_thought", "task_id": "T015", "thought": "I need to...", ...}
{"type": "tool_use", "task_id": "T015", "tool": "write_file", "args": {...}}
{"type": "tool_result", "task_id": "T015", "result": "ok", ...}
{"type": "task_complete", "task_id": "T015", "duration_s": 42.1, ...}
{"type": "session_end", "tasks_done": 3, ...}The web server also exposes an NDJSON streaming endpoint at GET /api/projects/{id}/stream.
Orchid V2.2–V2.4 close a full set of OS-grade reliability, isolation, and preemption gaps, making it safe to run in production and viable for team and enterprise deployments.
A process-wide shutdown.py event propagates from SIGTERM → uvicorn lifespan → BackgroundRunner.graceful_shutdown() → every running agent's ReAct iteration. On shutdown signal, each agent saves a final ReAct checkpoint before exiting, allowing restart recovery to resume from the saved point.
# orchid.defaults.yaml
runner:
shutdown_timeout: 30 # seconds to wait for tasks to finish before force-killThe systemd service uses KillMode=mixed + TimeoutStopSec=35 so workers get the full 30 s grace period.
Tasks left IN_PROGRESS by a crash are detected on next startup via a per-project .orchid/running marker file. For each orphaned task:
- ReAct checkpoint ≤ 24 h old → resumed from the saved iteration (conversation history restored, loop continues at
iteration + 1). - No checkpoint or too old → reset to
TODOand re-run from scratch.
The orchestrator wires the checkpoint onto the agent via agent._resume_checkpoint before calling agent.run(). Manual recovery:
orchid --project PATH --recoverEvery task now runs in an isolated child process by default (isolation.subprocess_enabled: true). A pre-forked WorkerPool eliminates the ~0.8 s Python interpreter startup cost:
isolation:
subprocess_enabled: true # default: on
subprocess_workers: 4 # pre-forked pool size (0 = one-shot per task)
max_task_seconds: 0 # wall-clock timeout; 0 = no limit
resource_limits:
max_as_gb: 4 # child address space cap
max_cpu_s: 600 # child CPU seconds cap
max_files: 256 # child open file descriptor limitWorkers are replaced automatically on death. The OS-level RLIMIT_AS, RLIMIT_CPU, and RLIMIT_NOFILE limits are applied in every child via preexec_fn. On timeout, the child receives SIGTERM (5 s grace) before SIGKILL.
An in-process threading.Event cancellation path is also available for non-subprocess mode.
TaskWatchdog runs as a background daemon thread. If a task remains IN_PROGRESS for longer than the configured threshold with no iteration progress, it fires a task.stuck hook event and marks the task BLOCKED.
watchdog:
enabled: true
stall_threshold_minutes: 30DependencyGraph.has_cycle() is checked after every spawn_task() call at runtime. A dynamically-injected task that would create a cycle raises CycleError immediately, preventing silent scheduler deadlock.
FileLockRegistry (orchid/locks.py) serializes parallel agents writing to the same file. write_file and append_file acquire a per-path threading.Lock before writing and release it on completion. Last-write-wins corruption in parallel task groups is eliminated.
Any running task can be paused. In-process agents pause at the next ReAct iteration boundary, save a checkpoint, and park on a threading.Event; resume() unparks without loss of conversation history. Subprocess pool workers receive SIGSTOP (freezes mid-instruction) and SIGCONT to unfreeze.
Web UI: Task Board shows a ⏸ button on the running task and a ▶ Resume button when paused.
API:
POST /api/projects/{id}/tasks/{task_id}/suspend
POST /api/projects/{id}/tasks/{task_id}/resume
Priority dispatch: _priority_score(task) = p1→30 / p2→20 / p3→10 + age bonus from task ID. The scheduler uses this score (descending) for both topological ordering and parallel group dispatch, so higher-priority tasks always head the queue.
runner:
preemption_enabled: false # opt-in (default off)
preemption_min_runtime_s: 30 # don't preempt a task that started < 30s agoEvery ws.send_json() is wrapped in asyncio.wait_for(timeout=5s). Slow or dead clients are evicted from the connection pool on timeout instead of stalling the broadcast loop. A 30 s heartbeat ping detects silent disconnects.
web:
ws_send_timeout: 5.0 # seconds before a slow client is disconnected
ws_heartbeat_s: 30.0 # ping intervalPer-iteration latency: if agents.max_iteration_seconds is set, the agent tracks consecutive slow iterations and cancels (with checkpoint) after 3 strikes.
agents:
max_iteration_seconds: 120 # 0 = disabledCPU accounting: subprocess mode measures RUSAGE_CHILDREN delta before/after each child. cpu_seconds is stored in TokenRecord, persisted to cost_ledger.jsonl, and shown in the PM Dashboard task timing table.
Per-user CPU quotas: set via admin API:
curl -X PUT /api/auth/users/alice \
-d '{"cpu_budget_seconds": 3600}' # 1 hour/day capCostScheduler.check_cpu_budget() raises BudgetBlockedError when the daily limit is exhausted.
BaseAgent saves a ReActCheckpoint every 5 iterations to .orchid/checkpoints/mid-<task_id>.json. On cancellation, shutdown, or latency budget breach the agent saves a final checkpoint immediately before stopping. The orchestrator loads this checkpoint on the next run and resumes the conversation from the saved iteration.
Each agent instance has an AgentMailbox — a thread-safe message queue. Agents can send and receive structured messages via send_message / receive_message ReAct tools, enabling synchronous inter-agent coordination without spawning new tasks.
BaseAgent.run() enforces a per-agent-type max_iterations limit defined in orchid.defaults.yaml (and overridable in .orchid.yaml). Exceeding the cap raises MaxIterationsError, caught by the orchestrator and recorded as a blocked task. This bounds runaway agents regardless of token spend.
agents:
max_iterations:
developer: 50
researcher: 30
reviewer: 20CAPABILITY_REGISTRY (orchid/capability.py) declares the required tools, memory access, and network permissions for each agent type as AgentCapability dataclasses. The orchestrator validates agent capabilities at spawn time against the project's tool registry.
Orchid can dispatch tasks to remote worker nodes via HTTP. The RemoteDispatcher selects an available node from a pool, sends a RemoteTaskRequest, streams back typed events, and merges the remote worker's cost ledger into the local ledger.
# .orchid.yaml
remote:
workers:
- url: "http://worker-1:8001"
max_parallel: 2
- url: "http://worker-2:8001"
max_parallel: 2Start a worker node:
# On the remote machine
orchid worker --port 8001Worker nodes expose /health, /task (POST), and /ledger (GET) endpoints via FastAPI (orchid/remote/worker_server.py).
orchid serve ships a complete JWT auth stack. Set JWT_SECRET in ~/.config/orchid/.env to activate it.
Token flow: POST /api/auth/login → HttpOnly orchid_access cookie (JWT, 8 h) + orchid_refresh cookie (30 days). On page load the portal silently calls POST /api/auth/refresh if the access token has expired, so users are not prompted to log in again during a normal working day. The middleware accepts either cookie or Authorization: Bearer header, and recognises API keys by the ok_ prefix.
Roles: user (default), admin, readonly. Admins can update any user's role, project list, or active status via PUT /api/auth/users/{id}. Deactivated users lose all sessions; records are preserved for audit.
API keys (POST /api/auth/apikeys): scoped bearer tokens for CI/scripts. Secret shown once. Use require_scope("tasks:run") on any endpoint to enforce scope. JWT sessions (interactive logins) always bypass scope checks.
OAuth / SSO: Google, Microsoft Entra ID, or any OIDC provider. Configure in ~/.config/orchid/config.yaml:
auth:
providers:
- type: google
client_id: "${GOOGLE_CLIENT_ID}"
client_secret: "${GOOGLE_CLIENT_SECRET}"
redirect_uri: "https://your-host/api/auth/oauth/google/callback"
- type: entra
tenant_id: "${AZURE_TENANT_ID}"
client_id: "${AZURE_CLIENT_ID}"
client_secret: "${AZURE_CLIENT_SECRET}"
redirect_uri: "https://your-host/api/auth/oauth/entra/callback"Mobile / PKCE: GET /api/auth/oauth/{p}/start?code_challenge=… stores the S256 challenge; POST /api/auth/oauth/{p}/token requires code_verifier and returns JSON tokens (no cookies).
Audit log: every login, logout, token refresh, API key create/revoke, OAuth login, task run, and admin action is appended to ~/.config/orchid/audit/audit-YYYY-MM-DD.jsonl. Files are rotated daily and never deleted. GET /api/audit (admin-only, paginated) exposes the log.
Per-user project scoping: set User.projects via PUT /api/auth/users/{id} to restrict which projects a user can run tasks against. Empty list = unrestricted. Admins bypass all restrictions.
Web UI login: the React frontend checks /api/auth/me on load and shows a sign-in form for unauthenticated users. A logout button in the header clears the session.
Pluggable auth storage: auth data defaults to ~/.config/orchid/users.json (zero deps). For multi-node or enterprise deployments, set ORCHID_AUTH_STORE_DSN to a PostgreSQL DSN and install the driver:
uv pip install 'orchid[postgres]'
# ~/.config/orchid/.env
ORCHID_AUTH_STORE_DSN=postgresql://user:pass@host:5432/orchidTables (orchid_users, orchid_refresh_tokens, orchid_api_keys, orchid_oauth_accounts) are created automatically on first start. See docs/auth-store-backends.md for migration, connection pool tuning, and rollback.
Orchid V2.5 adds a cron-based scheduler so any user can run recurring agent prompts, MCP tool calls, or shell commands on a schedule — without a separate cron daemon.
task_type |
What runs | Required config keys |
|---|---|---|
agent_prompt |
Sends a prompt to an LLM provider (Claude / local) | prompt (required), system, provider, mcp_servers |
mcp_tool |
Calls one tool on a configured MCP server | server, tool, args |
shell |
Runs a shell command via the existing allowlist | command, timeout_sec (default 60) |
All endpoints require authentication. Non-admin users see only their own tasks.
GET /api/scheduler/tasks list tasks
POST /api/scheduler/tasks create task (returns 201)
GET /api/scheduler/tasks/{id} get task
PUT /api/scheduler/tasks/{id} update task
DELETE /api/scheduler/tasks/{id} delete task
POST /api/scheduler/tasks/{id}/run trigger immediate run (async, returns queued:true)
GET /api/scheduler/tasks/{id}/runs run history for one task
GET /api/scheduler/runs run history (admin: all users; user: own)
curl -X POST /api/scheduler/tasks \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Daily standup summary",
"schedule": "0 9 * * 1-5",
"task_type": "agent_prompt",
"config": {
"prompt": "Summarise yesterday'\''s git log for the webtron project.",
"provider": "claude"
},
"notify_on_failure": true,
"notify_on_success": false
}'Standard five-field cron: minute hour day-of-month month day-of-week. All times UTC.
"0 9 * * *" daily at 09:00 UTC
"*/15 * * * *" every 15 minutes
"0 2 * * 0" weekly, Sunday at 02:00 UTC
Each run is appended to ~/.config/orchid/cron/runs.jsonl. Runs older than 30 days are pruned automatically on startup. The API returns runs newest-first.
{"run_id": "run_a1b2c3d4", "task_id": "stask_00ff1234", "owner_id": "u1",
"task_name": "Daily standup summary", "task_type": "agent_prompt",
"started_at": "2026-05-25T09:00:01Z", "finished_at": "2026-05-25T09:00:08Z",
"status": "success", "output": "Yesterday: 3 commits…", "error": ""}Successful runs write scheduled_task_run to the audit log; failures write scheduled_task_failed (with truncated error detail).
The User Portal task form (/app/) includes several helpers to reduce manual JSON editing:
Schedule builder — click "🗓 Schedule builder" to open a visual picker with frequency (every N minutes / hourly / daily / weekly / monthly), time-of-day dropdowns in your local timezone (auto-detected), day-of-week toggles, and a live preview showing both the local time and the UTC cron expression that will be stored.
Enable / disable toggle — each task row in the dashboard has a checkbox that flips enabled on/off with a single click. Disabled tasks remain visible in the list; the cron engine skips them at their scheduled time.
Export / Import — the modal header has "↓ Export" (downloads taskname.orchid-task.json) and "↑ Import" (reads a JSON file and populates all form fields). Use this to share task definitions between users or back up configs.
Save & Test / Test — a button always visible in the modal footer:
- New / duplicate task — labelled "▶ Save & Test": saves the task first (creating it in the scheduler), then triggers an immediate run. The modal stays open during the run. On success a toast fires and the modal auto-closes after 3 s. On failure the modal stays open with an inline error so you can edit and retry.
- Existing task — labelled "▶ Test": triggers an immediate run without saving (use "Save changes" first if you made edits).
- A failed Save & Test stores the new task ID so clicking Test again reruns that task rather than creating a duplicate.
MCP tool browser — for mcp_tool and agent_tool task types a "🔌 Browse MCP" button appears next to the type selector:
mcp_toolmode: two-panel server → tool list; selecting a tool shows its description and argument schema (name, type, required badge, description); "Usetool_name" fills the config JSON with{server, tool, args}pre-populated from the schema.agent_toolmode: checkbox list of available servers (pre-ticked if already in config); Apply merges theserversarray while preservingpromptandsystemfields.
The modal is 900 px wide with a 12-row resizable textarea, giving enough room to edit complex JSON configs directly.
Task Wizard — available on new tasks via the "✨ Wizard" pill in the modal header. A chat overlay interviews the user in plain language, then calls POST /api/scheduler/wizard which runs two LLM passes via the system-configured provider: a conversational turn that gathers requirements and signals TASK_READY, followed by a structured extraction pass that outputs the complete task JSON (name, description, schedule, task_type, config with real values, MCP server list when needed). Clicking "Apply to form →" on the returned config card populates all form fields without overwriting them with blank templates.
When Docker is available, tasks can run inside a minimal container image for the strongest isolation boundary.
isolation:
container: true # requires Docker on host
container_image: "python:3.12-slim"ContainerRunner (orchid/container_runner.py) falls back gracefully to subprocess isolation when Docker is unavailable.
Checkpoints can be exported to a portable JSON file for transfer between machines or archival:
orchid --project PATH --export-checkpoint CP-20260508-T042 > checkpoint.json
orchid --project PATH --import-checkpoint checkpoint.jsonshutdown.py process-wide threading.Event for graceful SIGTERM propagation
agent_registry.py global task_id → agent map for suspend/resume without coupling
orchestrator.py main loop: pick task → plan (Claude) → dispatch agent
session.py state lifecycle: load, save, compress hot memory; RLock for parallel safety
config.py three-layer merge: defaults → .orchid.yaml → CLI flags
lifecycle.py V2 state machine: NEW → DISCUSSING → … → COMPLETE
gates.py human|auto gate system for lifecycle transitions
machine_profile.py developer preferences at ~/.config/orchid/machine-profile.yaml
discovery.py auto-discovery: scan watch_dirs, watchdog inotify watcher
agent_manager.py per-project agent loop threads, APScheduler cron support
scheduler.py DependencyGraph, parallel group detection, topological sort; has_cycle(); _priority_score() with age bonus
agent_pool.py LRU cache of pre-instantiated agents; idle eviction thread
worktree.py WorktreeManager: isolated git worktrees per delegated task
subprocess_runner.py SubprocessRunner: WorkerPool (pre-forked N workers) + one-shot fallback; RLIMIT_* via preexec_fn; RUSAGE_CHILDREN CPU accounting; SIGSTOP/SIGCONT suspend/resume per task
worker_protocol.py TaskContext, WorkerEvent, WorkerResult dataclasses for subprocess protocol
watchdog.py TaskWatchdog: background thread; fires task.stuck hook on stall
locks.py FileLockRegistry: per-path threading.Lock for parallel write safety
mailbox.py AgentMailbox: thread-safe per-agent message queue
capability.py CAPABILITY_REGISTRY: AgentCapability declarations, get_capability()
container_runner.py ContainerRunner: Docker-based isolation (opt-in, graceful fallback)
agents/
base.py ReAct loop (Reason → Act → Observe), tool dispatch, allowed_tools filter
discussion_agent.py elicits requirements via conversation (Claude, cached)
product_manager.py generates REQUIREMENTS.md + ARCHITECTURE.md
project_manager.py generates MILESTONES.md + tasks.md
developer.py code generation/editing; unrestricted tools; git + spawn_task documented
tester.py QA verification: runs tests, structured output, no code writes
researcher.py search and summarize (local model)
reviewer.py critique and quality gate (Claude API)
delegator.py delegate sub-tasks to agents; worktree isolation; pool-based acquisition
memory/
state.py tasks.md + CLAUDE.md read/write
decisions.py append-only JSON Lines decision log
vector.py ChromaDB embedded vector store
tools/
models.py unified call() for Claude API + llama.cpp
filesystem.py read_file, write_file, list_dir, append_file
shell.py bash execution with blocklist + optional allowlist + timeout
git.py 12 git tool functions: status, diff, log, commit, push, pull, branch ops
task_injection.py spawn_task() agent tool; threading.local session ref for parallel safety
interfaces/
cli.py Typer CLI entry point
web_server.py FastAPI REST + WebSocket backend (V2 planning endpoints)
web_ui/ React + Vite frontend source
telegram_bot.py Telegram bot (python-telegram-bot)
slack_bot.py Slack bot (slack-bolt, Socket Mode)
background_runner.py non-blocking executor shared by Telegram and Slack
central_bot.py V2.1 central bot server (Telegram + Slack routing)
providers/
registry.py 8-layer provider resolution chain; resolve_chain() returns (primary, [fallbacks])
base.py ProviderBase ABC: availability cache, optimize_for_caching(); RetriableProviderError
anthropic.py Claude API — explicit prompt caching, session stats, retry
local.py llama.cpp OpenAI-compat endpoint — implicit KV cache detection
ollama.py Ollama — implicit KV cache, stable-prefix ordering
openai.py OpenAI / OpenRouter
bedrock.py AWS Bedrock (boto3, lazy import)
hooks/
events.py hook event constants (session, phase, task, agent)
types.py ShellHook, HTTPHook, PythonHook dataclasses
registry.py hook registry: fire events, blocking/non-blocking execution
loader.py load hooks from .orchid.yaml; circuit breaker + audit wired here
schema.py Pydantic validation for hook config
circuit_breaker.py CircuitBreakerRegistry: per-event-type breakers, open/half-open/closed states
audit.py AuditLogger: thread-safe JSONL writer → .orchid/audit_log.jsonl
mcp/
types.py MCPTool, MCPResult dataclasses
client.py MCPClient ABC + MCPClientError
stdio_client.py subprocess.Popen transport (JSON-RPC 2.0 over stdin/stdout)
http_client.py httpx.Client transport
adapter.py MCPAdapter: wraps client, caches tool list
manager.py MCPManager: multi-server lifecycle, tool injection
remote/
types.py WorkerNode, RemoteTaskRequest, RemoteTaskResponse dataclasses
worker_server.py FastAPI worker node server (/health, /task, /ledger)
dispatcher.py RemoteDispatcher: node selection, retry, ledger merge
auth/
types.py User, RefreshToken, ApiKey, OAuthAccount, AuditEvent dataclasses
base.py BaseUserStore ABC — 27 abstract methods, implemented by both backends
store.py FileUserStore (JSON, default) + get_store() singleton factory (auto-selects backend)
store_postgres.py PostgresUserStore — ThreadedConnectionPool, auto-schema, UPSERT-safe
jwt.py hash_password, verify_password, issue/verify access tokens, issue/verify refresh tokens, issue/verify API keys
middleware.py get_current_user, get_optional_user, require_auth(role=), require_scope(scope=)
audit.py AuditStore: append-only JSONL daily rotation; AuditAction constants; make_event()
providers/
base.py OIDCProvider ABC; link_or_create_user() with email-match linking
oidc_generic.py GenericOIDCProvider: discovery doc fetch, code exchange, userinfo; PKCE S256
google.py GoogleOIDCProvider (pre-set discovery URL)
entra.py EntraOIDCProvider (tenant-aware discovery URL)
registry.py ProviderRegistry: register() + from_config(yaml)
cron/
types.py ScheduledTask, TaskRun dataclasses; _new_task_id/run_id/utcnow helpers
store.py TaskRunStore: append-only JSONL run history; 30-day pruning; thread-safe
executor.py TaskExecutor: dispatches agent_prompt/mcp_tool/shell; always returns TaskRun, never raises
engine.py CronEngine: APScheduler BackgroundScheduler wrapper; get_engine() singleton; add/remove/run_now
api.py register_routes(): installs all /api/scheduler/* endpoints on FastAPI app
output/
events.py typed event dataclasses (SessionStart, TaskStart, AgentThought, …)
emitter.py EmitterProtocol + NullEmitter
ndjson_emitter.py NDJSONEmitter (stream) + NDJSONBufferEmitter (in-memory)
ws_emitter.py WebSocket emitter for web server streaming
checkpoint/
schema.py CheckpointMetadata, Checkpoint, CheckpointEntry, ReActCheckpoint dataclasses
store.py CheckpointStore: save, load, list, delete, prune; save/load_react_checkpoint
restore.py rewind_session(), list_checkpoints(), export_checkpoint(), resume_orphaned_tasks()
cost/
ledger.py CostLedger: JSONL-backed token/cost recorder; node_id field; merge_from_file() for remote ledger merge
scheduler.py CostScheduler: budget cap enforcement, per-user quotas, 429 rate-limit backoff, provider selection
| Task type | Default model |
|---|---|
orchestrate review plan critique synthesize rollup |
Claude API |
draft code_generate summarize search transform verify |
Local llama.cpp |
Override per-project with model_preference: claude in .orchid.yaml, or per-task with the model: tag.
Orchid automatically minimises API token costs via provider-specific caching strategies:
Anthropic (explicit caching): System prompts ≥ 2048 chars are automatically wrapped with cache_control: {type: ephemeral}. Pass cacheable_prefix=N to cache the first N messages. The DiscussionAgent separates static instructions (always cached) from conversation history. Cache write/read counts are logged at session close.
llama.cpp / Ollama (implicit KV caching): cache_prompt: true is sent with every request so the server retains KV attention state for repeated prompt prefixes. optimize_for_caching() always places stable content before dynamic content to maximise prefix reuse. Cache hits are detected heuristically from response timings (< 1.0 ms/token = cache hit).
Config in .orchid.yaml:
caching:
enabled: true
anthropic:
cache_control: true
min_cacheable_chars: 2048
local:
cache_prompt_hint: truepytest -m "not network" --ignore=tests/test_integration.py --ignore=tests/test_metrics.py # 1550+ tests, no API calls required
ruff check orchid/ # 0 errors| File | What it covers |
|---|---|
tests/conftest.py |
Autouse fixtures reset store singleton, ledger singleton, shutdown event, and agent registry between every test |
tests/test_auth.py |
134 auth endpoint tests — JWT, refresh tokens, API keys, OAuth/OIDC, PKCE, audit log, user management |
tests/test_shutdown.py |
Global shutdown event; agent raises + saves checkpoint on shutdown signal |
tests/test_agent_registry.py |
Registry register/deregister/get; concurrent access safety |
tests/test_graceful_shutdown.py |
BackgroundRunner.graceful_shutdown() — signals cancel events, waits for futures, timeout returns False; marker file write/remove |
tests/test_orphan_recovery.py |
resume_orphaned_tasks() — fresh checkpoint keeps IN_PROGRESS, stale/missing resets to TODO |
tests/test_worker_pool.py |
WorkerPool submit/shutdown; _apply_resource_limits() |
tests/test_suspend_resume.py |
_priority_score() ordering; scheduler dispatch order; agent suspend/resume threading; runner suspend_task/resume_task; subprocess pool SIGSTOP/SIGCONT |
tests/test_cpu_accounting.py |
WorkerResult.cpu_seconds; ledger daily_cpu_for_user(); check_cpu_budget() raises; 3-strike latency cancel |
tests/test_cron_types.py |
ScheduledTask and TaskRun dataclass defaults, ID format, uniqueness, UTC timestamps |
tests/test_cron_store.py |
TaskRunStore append/get/prune/filter; UserStore scheduled-task CRUD and persistence |
tests/test_cron_executor.py |
TaskExecutor dispatch for all three task types; mocked providers and MCP; never-raises guarantee |
tests/test_cron_engine.py |
CronEngine singleton, start/stop, job registration, invalid cron skip, add/update/remove/run_now |
tests/test_cron_api.py |
/api/scheduler/* endpoint integration tests: CRUD, run-now, access scoping, admin override |
| Mark | Usage |
|---|---|
network |
Tests requiring real network (excluded by default: -m "not network") |
slow |
Tests taking >5 s (excluded in fast runs: -m "not slow") |
CI runs automatically on push/PR via GitHub Actions (.github/workflows/ci.yml).
Languages
- Python — entire backend: orchestrator, agents, providers, CLI, web server, tests
- JavaScript / JSX — React frontend components and hooks
- Bash — install scripts and service helpers
- CSS — web UI dark theme (index.css)
- HTML — Vite's index.html entry point
Configuration and data formats
- YAML — orchid.defaults.yaml, .orchid.yaml project configs, Traefik routing, machine-profile.yaml
- TOML — pyproject.toml (package metadata, build config, tool settings)
- JSON — package.json, lock files, project.state.json, task_results.json, slack-channels.json, telegram-state.json
- JSONL — append-only session logs, decisions, discussion conversation history
Other
- Markdown — README.md, CLAUDE.md, tasks.md, REQUIREMENTS.md, ARCHITECTURE.md, MILESTONES.md, templates
- systemd unit — orchid-serve.service and service templates
See docs/getting-started.md for a full walkthrough with examples.
Orchid is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
You are free to use, modify, and distribute Orchid under the terms of the AGPL-3.0. If you distribute Orchid or run it as a network service, you must make your modifications available under the same license.
Copyright (c) 2026 David Scheiderman

{"timestamp": "...", "event": "task.complete", "hook_type": "shell", "cmd": "notify-send ...", "exit_code": 0, "duration_ms": 12}