Version: see root VERSION (currently 0.2.0)
A Python daemon that connects Jira (Server/DC or Cloud) to OpenCode / Oh My OpenAgent. It discovers work from a board poll, runs AI agents in isolated temporary Git clones, posts progress back to Jira, and can push feature branches and open GitLab merge requests.
- Polls a Jira board for To Do issues that match trigger labels and/or bot assignee
- Routes work from a per-issue
{params}block (Mode: planorMode: build) - Runs OpenCode agents (Prometheus planning, Atlas build, Oracle consult) in temp clones
- Reports plans, progress, errors, and completion as Jira comments
- Pushes work branches and opens merge requests when build mode finishes successfully
- Serves a localhost ops dashboard (tasks, poll monitor, safe settings) in the same process
There is no HTTP webhook intake. Discovery is board polling only. Comment-driven bot commands are not a primary path (legacy plan-start labels still exist; see Workflows).
┌─────────────────────────── Jira (REST v2 + Agile) ───────────────────────────┐
│ Board / sprint → To Do + label or bot assignee → poll every N seconds │
└───────────────────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────── JIRA Virtual Developer (one process) ────────────────┐
│ Board Poller → Job Processor → Agent Runner (opencode serve + --dir) │
│ │ │ │
│ │ ├─ temp clone: feature/{ISSUE} from issue {params} │
│ │ ├─ Jira comments (progress / plan / error / done) │
│ │ └─ GitLab push + MR (build mode) │
│ │ │
│ Ops dashboard (FastAPI + React SPA) · stuck-job monitor · JSON state │
└───────────────────────────────────────────────────────────────────────────────┘
| Component | Role |
|---|---|
| Board poller | Sole intake. Reads board/sprint issues; writes a poll snapshot for the UI |
| Job processor | State machine, concurrency limits, plan vs build routing, fail + Jira notify |
| Agent runner | Spawns OpenCode with plan/build mode prompts; streams session logs |
| Jira client | REST API v2 + Agile; Bearer (on-prem PAT) or Basic (Cloud email+token) |
| Git manager | Clone, branch, commit identity, push, MR via glab / GitLab API |
| State store | Per-issue JSON under .jira-agent/state/; job records for the dashboard |
| Ops dashboard | REST + WebSocket + static SPA from web/dist |
| Setting | Role |
|---|---|
DEFAULT_AGENT (e.g. atlas) |
OpenCode persona for both Mode: plan and Mode: build |
| Plan vs build text | agent/PLAN_PROMPT.md vs agent/BUILD_PROMPT.md (mode only) |
| Oracle | Architecture Q&A when routing detects consultative wording |
- Python 3.12+ recommended (3.10–3.13 also used on Windows offline wheels)
- OpenCode CLI on
PATH(OPENCODE_CLI, defaultopencode) with oh-my-openagent plugin - Git
- glab (GitLab CLI) when push/MR is enabled
- Jira access (board browse, comment, optional transitions)
- GitLab PAT with clone/push/MR rights when using remote workspaces
# From repo root
cp .env.example .env
# Edit .env — at least JIRA_HOST, JIRA_API_TOKEN, JIRA_BOARD_ID, GITLAB_* as needed
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Optional installer (deps + OpenCode + glab heuristics)
./install.sh
python cli.py init
python cli.py startOps dashboard (default): http://127.0.0.1:8080
Stop the daemon with Ctrl+C.
CI builds virtual_developer-windows-x64-*.zip (see packaging/windows/README.md).
:: Extract zip so install.bat sits next to vendor\ and src\
install.bat
:: Or, if OpenCode is already installed on this PC (skip OpenCode install):
install-dashboard.bat
:: Or, use the Python already on PATH (does not create .venv):
install-dashboard-system-python.bat
:: Edit .env, then:
start.bat
:: or: start-backend.bat / start-frontend.batOpen the OpenCode TUI only via start-opencode.bat from the project folder (after full install.bat) — not bare opencode from your user profile (home-as-project causes long black-screen indexing).
See Dockerfile and .github/workflows/docker.yml if you run containerized builds.
Every issue the bot should work on needs a {params} … {params} block in the description (or summary fields scanned by the parser). Repository URL is per issue, not a global env var.
{params}
Repository: https://gitlab.example.com/group/your-repo.git
Source branch: feature/PROJ-123
Target branch: develop
Mode: plan
{params}
| Field | Meaning |
|---|---|
| Repository | GitLab clone URL (aliases: Repo, GitLab, Project URL) |
| Source branch | Work / MR source branch. If missing or equal to a base name (main/develop/…), work branch becomes feature/{ISSUE_KEY} |
| Target branch | Must exist on remote; work is based on it; MR merges into it |
| Mode | plan — plan only, append plan to Jira, no push. build — implement, push, open MR |
Mode aliases: planning/prometheus → plan; execute/execution/atlas/implement → build.
Incomplete templates cause a user-visible Jira comment with the format help (see src/issue_git_spec.py).
All of the following roughly apply for first intake:
- Issue is on the configured board
- Status looks like To Do (name or
statusCategorynew/backlog-like) - Has a trigger label (
TRIGGER_LABELS, defaultai-assist,bot) and/or assignee name looks like the bot (whenTRIGGER_ON_ASSIGNMENT=true) - Not already in-flight (
planning/executing) — poll noise never restarts live work
To Do + trigger = rework (intentional). A ticket in a To Do-like column with
bot / ai-assist (or bot assignee) is eligible, including after a previous
completed / error / cancelled run. The poller re-queues that work
(reset and run again). After accept, the bot moves the board to In Progress
so the next poll does not start another job until the issue is To Do again.
The exception is a successful plan (plan_ready): sitting on To Do with only
bot does not auto-build — see
Plans never auto-start.
- Poller accepts issue → state
planning - Prometheus runs in a temp clone
- Plan posted to Jira (comment + description) → local state
plan_ready, labelai-plan-ready - Bot stops. The ticket may still show To Do on the board with
bot— that is normal.
After planning finishes, the issue is waiting for an explicit implement signal.
Sitting on To Do with only bot / ai-assist will not start coding.
To Do + bot → Mode: plan runs → plan_ready + ai-plan-ready
│
still To Do + bot alone │ no further work
▼
waiting (not stuck)
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
Add label Open a NEW issue (Do not rely on
ai-start-work with Mode: build Mode: build alone
or ai-execute (same {params}) on the plan ticket)
while still To Do
│ │
└──────────► build / implement ◄──────────────────┘
| What you see | What it means |
|---|---|
To Do + bot + local plan_ready |
Plan done; waiting for start signal |
Label ai-plan-ready |
Bot finished planning (not a start label) |
Labels ai-start-work or ai-execute on To Do |
Start implementation on that same ticket |
New ticket with Mode: build + trigger label |
Independent build run (recommended for clean history) |
Daemon log Skip cold-start requeue … plan_ready |
Correct — daemon restart will not re-plan or auto-build |
How to implement after a plan
- Same ticket: while status is To Do, add label
ai-start-workorai-execute
(next poll starts the build path), or - New ticket: create an issue with the same
{params}repo/branches andMode: build, plus a trigger label (bot/ai-assist).
Changing the plan ticket to Mode: build alone does not auto-start
(product rule so plans are reviewed before code). Dashboard Start is also
disabled for the same reason.
- Poller accepts issue → prepare git workspace from
{params} - Atlas (orchestrator) implements against the plan / description
- On success: push branch, open MR, comment completion →
completed - On failure: state
errorand Jira error comment (_fail_issue/post_error)
Consultative questions without implementation keywords may route to Oracle (read-only style advice). Implementation language forces plan/build paths instead.
pending → planning | executing → (plan_ready) → completed | error | cancelled
| Status | Meaning for operators |
|---|---|
planning / executing |
Agent running — poller will not restart from board noise |
plan_ready |
Plan finished; not an error. Needs start label or new Mode: build issue |
completed |
Done (build delivered or soft no-op completion). Move back to To Do (with trigger) to rework. |
error |
Failed; fix description / params, then return to To Do (or edit text) to rework. |
cancelled |
Operator cancel. To Do + trigger is still rework — move it back to To Do (or leave it there) to run again. |
Stuck in-flight jobs are watchdogged by the daemon. Startup recovers orphaned disk planning/executing states to error.
Enabled by default with the daemon (DASHBOARD_ENABLED=true).
| URL | http://127.0.0.1:8080 |
| Stack | FastAPI in-daemon + WebSocket /ws + React SPA (web/) |
| Auth | None in v1 — keep bind host localhost unless you put a proxy/auth in front |
Frontend is display-only. Filtering, poll math, and settings rules live on the backend.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Liveness + version |
| GET | /api/meta |
Version + server time |
| GET | /api/tasks |
Agent task list |
| GET | /api/jobs |
Paginated job history |
| GET | /api/jobs/{id} |
Job detail |
| DELETE | /api/jobs/{id} |
Delete job record |
| GET | /api/tasks/{key} |
Task detail for issue |
| POST | /api/tasks/{key}/cancel |
Cancel live work (preferred over CLI when daemon runs) |
| GET | /api/poll |
Last poll snapshot + countdown |
| GET/PATCH | /api/settings |
Safe settings (no token values) |
| GET | /api/models |
Available OpenCode models |
| GET | /api/dashboard |
Full envelope |
| WS | /ws |
Live pushes |
Writable runtime settings (examples): board id, poll interval, trigger labels, trigger_on_assignment, max_concurrent_jobs, default model, project_repositories (saved git remotes for Scheduled → New issue).
DASHBOARD_ALLOW_REMOTE=false forces non-loopback hosts back to 127.0.0.1.
cd web && npm install && npm run build
# dist/ is served by the daemon; Vite dev: npm run dev (proxies to :8080)Copy .env.example → .env. Secrets must never be committed.
| Variable | Description |
|---|---|
JIRA_HOST |
Base URL (no trailing slash preferred) |
JIRA_API_TOKEN |
Cloud API token or on-prem personal access token |
JIRA_EMAIL |
Cloud/dev only — with token uses HTTP Basic. Leave empty for Bearer PAT (prod) |
JIRA_PROJECTS |
Comma-separated project keys (reference / allow-list style) |
JIRA_BOARD_ID |
Agile board id to poll (required for discovery) |
Auth summary:
- Prod / on-prem:
JIRA_HOST+JIRA_API_TOKEN→Authorization: Bearer … - Cloud (dev):
JIRA_HOST+JIRA_EMAIL+JIRA_API_TOKEN→ Basic email:token
TLS verify is currently off for typical on-prem certs; do not “fix” that without a deliberate secure path.
| Variable | Default | Description |
|---|---|---|
POLL_INTERVAL_SECONDS |
30 |
Board poll interval |
MAX_CONCURRENT_JOBS |
6 |
Parallel agent jobs |
POLL_DISPATCH_WORKERS |
8 |
Parallel dispatch/transitions per poll cycle |
DASHBOARD_ENABLED |
true |
Serve ops UI with daemon |
DASHBOARD_HOST |
127.0.0.1 |
Bind host |
DASHBOARD_PORT |
8080 |
HTTP port |
DASHBOARD_ALLOW_REMOTE |
false |
Allow non-loopback bind |
| Variable | Default |
|---|---|
TRIGGER_LABELS |
ai-assist,bot |
TRIGGER_ON_ASSIGNMENT |
true |
TRIGGER_MENTIONS |
@DevBot,@AI |
| Variable | Description |
|---|---|
GITLAB_PAT |
Clone / push / MR token |
GITLAB_ALLOWED_HOSTS |
Required when PAT is set — comma-separated hosts that may receive the PAT (fail-closed) |
GIT_USER_NAME / GIT_USER_EMAIL |
Commit identity in temp clones |
Repo URL and branches always come from the issue {params} block.
| Variable | Default | Description |
|---|---|---|
OPENCODE_CLI |
opencode |
CLI binary/command |
DEFAULT_MODEL |
(see .env.example) |
Passed to OpenCode serve as the job model |
DEFAULT_AGENT |
atlas |
OpenCode agent for plan and build jobs |
AGENT_PROMPTS_DIR |
agent |
Dir with PLAN_PROMPT.md + BUILD_PROMPT.md only |
SISYPHUS_PLANS_DIR |
.sisyphus/plans |
Plan markdown location |
AGENT_TASK_TIMEOUT_SECONDS |
1800 |
Per-attempt timeout |
AGENT_TASK_MAX_RETRIES |
3 |
Retries with exponential backoff |
TEMP_DIR_BASE |
.temp |
Temp clone root. On Windows keep this short (e.g. C:\vd\t) — clones use {remote12}_{hash12} so nested build/.../Debug trees stay under MAX_PATH. |
TEMP_CLEANUP_POLICY |
age / never |
Cleanup policy (see .env.example) |
List or set models:
python cli.py models
python cli.py models --set provider/model-idpython cli.py --help
python cli.py --version
# Lifecycle
python cli.py init # dirs + .env from example
python cli.py start # daemon: poller + dashboard + monitor
python cli.py config # safe config dump
python cli.py models # list OpenCode models
# Issue ops (need Jira config)
python cli.py process PROJ-123 # force-process one issue
python cli.py process PROJ-123 --dry-run
python cli.py status # active issues table
python cli.py show PROJ-123
python cli.py cancel PROJ-123 # state cancel; kill live agent via dashboard if daemon is up
python cli.py costs # token/cost rollup from state files
# Local agent smoke (no Jira)
python cli.py test-issue -t "Fix bugs" -d "Fix calculator divide by zero" -p sample_project
python cli.py test-issue -t "Plan feature" -d "..." --plan-only --model provider/id
# Simulated Jira (in-memory REST only; does not push to the daemon)
python cli.py simulate start-server --port 7001
python cli.py simulate create-issue -s "Title" -d "..." -a DevBot -l ai-assist
python cli.py simulate list-issues
python cli.py simulate show-issue SIM-1001virtual_developer/
├── cli.py # Click CLI entry
├── VERSION # SemVer product version
├── .env.example # Config template
├── requirements.txt
├── agent/PLAN_PROMPT.md # Plan mode prompt
├── agent/BUILD_PROMPT.md # Build mode prompt
├── src/
│ ├── daemon.py # Process entry: poller + dashboard + monitor
│ ├── config.py
│ ├── processor.py # Job lifecycle
│ ├── git_manager.py
│ ├── issue_git_spec.py # {params} parser
│ ├── jira/ # client, poller, simulated client
│ ├── orchestrator/ # agent_runner, prompts, workflow_router
│ ├── reporter/ # Jira comments
│ ├── state/ # models, manager, job_store
│ └── dashboard/ # FastAPI API, schemas, poll snapshot
├── web/ # React + Vite + Tailwind SPA → web/dist
├── packaging/windows/ # Offline Windows dist
├── sample_project/ # Calculator with intentional bugs for test-issue
├── tests/ # Pytest suite
└── AGENTS.md # Contributor / AI agent rules for this repo
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt pytest pytest-asyncio pytest-cov
.venv/bin/python -m pytest tests/ --ignore=tests/test_logical_issues.py -q- Prefer unit tests with mocks (no live Jira in CI).
tests/test_logical_issues.pyis excluded from the default green suite: it documents desired behaviour that is still incorrect until fixed.- Do not commit
.coverage,htmlcov/, or.pytest_cache/.
Default development branch is develop (not main).
feature/* → MR into develop → (release) develop → main
Commits and MR titles use Conventional Commits:
feat(dashboard): show poll countdown
fix(poller): do not requeue in-flight issues
Full rules: AGENTS.md. For target product repos that agents work in, branch feature/{JIRA_ISSUE_ID} and conventional commit policy live in agent/BUILD_PROMPT.md / commitMsgFormat.md.
.jira-agent/
state/ # per-issue JSON (status, tokens, plan path, metadata)
sessions/ # agent stdout/stderr session logs (not auto-deleted by temp cleanup)
.temp/ # per-issue git clones (cleanup policy from env)
.sisyphus/plans/ # plan markdown when using local plans dir
logs/ # local log directory (stdout/stderr; not configured via env)
| Symptom | What to check |
|---|---|
| Poller idle / no jobs | JIRA_BOARD_ID, issue in To Do, trigger label or bot assignee, python cli.py process KEY |
Ticket on To Do with bot but bot does nothing |
If local status is plan_ready, that wait is intentional (bot alone does not auto-build). Add ai-start-work / ai-execute, or open a new Mode: build issue. See Plans never auto-start. If local status is completed / error / cancelled, To Do + trigger is rework — check the poll snapshot will_process and logs. |
| 401 / 403 from Jira | Token, Cloud needs JIRA_EMAIL for API tokens, host URL, project permissions |
| Agent never starts | opencode / plugin install, DEFAULT_MODEL, session logs under .jira-agent/sessions/ |
| Git / MR fails | Issue {params} complete, GITLAB_PAT, GITLAB_ALLOWED_HOSTS includes that host, glab available |
| Dashboard unreachable | Daemon running? DASHBOARD_* bind, open http://127.0.0.1:8080 |
| Windows TUI black screen | Use start-opencode.bat from project dir; re-run install.bat; see packaging/windows/ diag notes |
Stuck planning/executing |
Restart daemon (orphan recovery) or cancel from dashboard; check watchdog logs |
python cli.py config
python cli.py show PROJ-123
# DEBUG=true python cli.py start- Keep
.envout of git (tokens, PATs). - Dashboard has no auth — localhost only unless you knowingly expose it.
GITLAB_ALLOWED_HOSTSprevents sending the PAT to arbitrary hosts from issue text.- Prefer a dedicated Jira bot account with least privilege.
- Never log raw API tokens or PATs.
| Doc | Purpose |
|---|---|
| AGENTS.md | Coding standards, Jira rules, dashboard rules, Windows packaging hard-won fixes |
| agent/PLAN_PROMPT.md | Plan-mode prompt |
| agent/BUILD_PROMPT.md | Build-mode prompt |
| packaging/windows/README.md | Offline zip design and versioning |
.env.example |
Full environment template with comments |
| web/README.md | Frontend notes (if present) |
MIT