Web console & HTTP API¶
The central dashboard (forgeo web) is the only web interface. Daemons bind no ports — they write live state to daemon.state.json. The dashboard reads every instance directly from its files (backlog.json, runs.jsonl, forgeo.log, BLOCKER.md), so it works whether daemons are running. Remote backlogs are fetched from their provider; unreachable ones return 502 rather than an empty board.
forgeo web # 0.0.0.0:8790 foreground
forgeo web --port 9000 --host 127.0.0.1
forgeo web -d # background
forgeo web status
forgeo web stop
forgeo web --token # bearer auth for /api/*
Binds 0.0.0.0 by default (LAN-visible); use --host 127.0.0.1 to restrict. Implemented with stdlib (forgeo.central); static files in src/forgeo/web/.

Authentication (optional)¶
By default the dashboard is open — anyone reaching the port can read and mutate. On shared hosts, enable bearer auth:
forgeo web --token # generate, print once, save to web.toml
forgeo web --token my-secret # set your own
curl -H "Authorization: Bearer my-secret" http://127.0.0.1:8790/api/instances
Token stored in ~/.config/forgeo/web.toml (0600). Once present, every /api/* requires Authorization: Bearer <token> (401 otherwise). Static assets and /central/login.html stay public; ?token=... in URL auto-signs in. Delete web.toml to return to open.
Pages¶
GET /— home: every instance (name, repo, daemon state, last outcome, next run, per-status counts). Issue providers showOpen in Jira/GitHub/GitLab ↗.GET /instances/<name>/— kanban board, Create tab (with Run at for one-shot schedule), plus Logs, History, Blocker, Config tabs. Header has daemon Start/Stop/Restart buttons.- For
file/httpthis is the primary editor; forjira/github/gitlabit is a read-mostly mirror — banner links to native board, each card/modal links to the native issue, but Forgeo-specific state (blocker_reason,failure_reason,agent_response, retry budget) is shown here. - History tab lists
runs.jsonl(paginated, newest first) with collapsible agent output (run_output_lines). Config tab editsforgeo.yamlviaPUT .../config. - Task click opens a modal (description, criteria, dependencies, timestamps,
run_at,review_branch/sha). Edit patches fields (includingreview_required), Reopen retriesBLOCKED, Complete / Request changes forREVIEW, Delete removesOPEN/BLOCKED/REVIEW.REVIEW/BLOCKED/FAILEDbanners show branch/reasons and retry counts. Waiting on dependencies banner lists unmet deps (REVIEWnot satisfied). - Columns auto-collapse (
OPENstays expanded, others behind "show …" after a few tasks, max 20 cards with "show more"). Search box and status filter are client-side, reflected in?q=…&status=…. GET /style.css,/central/central.js,/central/central.css— shared theme (no frameworks).
Endpoints¶
All return application/json with Cache-Control: no-store. Per-instance paths are under /api/instances/<name>/. Writes are atomic (temp file + rename).
| Method & Path | Description |
|---|---|
GET /api/instances |
List all instances with repo, daemon state, last outcome, counts, provider metadata. |
GET /api/instances/<name>/tasks |
List tasks (creation order). 502 if remote unreachable. |
GET /api/instances/<name>/tasks/{id} |
Single task. 404 if not found. |
POST /api/instances/<name>/tasks |
Create task (auto WEB-### ID). |
POST /api/instances/<name>/tasks/{id}/reopen |
Reopen BLOCKED → OPEN. |
POST /api/instances/<name>/tasks/{id}/complete-review |
Complete REVIEW → COMPLETED (after manual merge). |
POST /api/instances/<name>/tasks/{id}/request-changes |
REVIEW → OPEN. |
PATCH /api/instances/<name>/tasks/{id} |
Edit task fields. |
DELETE /api/instances/<name>/tasks/{id} |
Delete OPEN/BLOCKED/REVIEW task. |
GET /api/instances/<name>/status |
Daemon status + provider metadata. |
GET /api/instances/<name>/config |
Resolved forgeo.yaml as JSON. |
PUT /api/instances/<name>/config |
Validate and persist config. |
POST /api/instances/<name>/start |
Start daemon (409 if running). |
POST /api/instances/<name>/stop |
Stop daemon (no-op if not running). |
POST /api/instances/<name>/restart |
Restart daemon. |
GET /api/instances/<name>/logs?lines=N |
Last N log lines (default 100, max 10000). |
GET /api/instances/<name>/runs?limit=N&offset=M |
Paginated run history, newest first. |
GET /api/instances/<name>/blocker |
BLOCKER.md contents or {"content": null}. |
GET /api/instances¶
curl http://127.0.0.1:8790/api/instances
Each row includes backlog_provider, backlog, backlog_is_issue_provider, external_board_url/external_board_label, backlog_error (null or reason; counts are 0 when unreachable). One unreachable provider never fails the whole listing.
GET /api/instances/<name>/tasks (+ single)¶
curl http://127.0.0.1:8790/api/instances/my-repo/tasks
curl http://127.0.0.1:8790/api/instances/my-repo/tasks/TASK-001
Each task includes:
unsatisfied_dependencies: [{id, status}]— deps notCOMPLETED(missingif absent).retry_budget/retries_remaining— derived fromretries_leftvsfailed_retry_max.external_url— native issue link forjira/github/gitlab.
{
"id": "TASK-001",
"title": "Implement fibonacci module",
"description": "...",
"status": "OPEN",
"created_at": "2026-07-31T10:00:00Z",
"run_at": null,
"dependencies": [],
"unsatisfied_dependencies": [],
"blocker_reason": [],
"failure_reason": [],
"agent_response": null
}
POST /api/instances/<name>/tasks¶
Body requires non-blank title + description; optional acceptance_criteria (string[]), agent_command (string/null), run_at (ISO-8601/null), review_required (bool/null).
curl -X POST http://127.0.0.1:8790/api/instances/my-repo/tasks \
-H 'Content-Type: application/json' \
-d '{"title": "Fibonacci", "description": "With tests.", "run_at": "2026-08-20T12:30:00Z"}'
Returns 201 with created task. Errors: 400 bad body, 404 unknown instance, 409 ID collision.
POST /api/instances/<name>/tasks/{id}/reopen¶
No body. Sets BLOCKED → OPEN, clears blocker_reason, keeps blocked_count.
curl -X POST http://127.0.0.1:8790/api/instances/my-repo/tasks/TASK-001/reopen
Returns 200. Errors: 400 if not BLOCKED, 404 unknown instance/task.
PATCH /api/instances/<name>/tasks/{id}¶
Editable: title, description, acceptance_criteria, dependencies, files_to_modify, agent_command, agent_timeout_seconds, retries_left, run_at, review_required. Omitted fields unchanged; engine fields (status, blocker_reason, review_branch, etc.) are rejected. Bumps updated_at.
curl -X PATCH http://127.0.0.1:8790/api/instances/my-repo/tasks/TASK-001 \
-H 'Content-Type: application/json' \
-d '{"description": "With tests.", "run_at": null}'
Returns 200. Errors: 400 bad body/unknown field, 404 not found.
DELETE /api/instances/<name>/tasks/{id}¶
Only OPEN, BLOCKED or REVIEW (400 otherwise). Returns 200 with deleted task.
POST /api/instances/<name>/tasks/{id}/complete-review / request-changes¶
No body. complete-review: REVIEW → COMPLETED (after manual merge, clears review_branch/sha). request-changes: REVIEW → OPEN. Errors: 400 if not REVIEW, 404 unknown task.
GET /api/instances/<name>/status¶
curl http://127.0.0.1:8790/api/instances/my-repo/status
{
"name": "my-repo",
"repo": "/home/me/site-a",
"interval_minutes": 30,
"daemon_running": true,
"pid": 4242,
"last_outcome": "task",
"next_run_at": "2026-08-01T12:00:00+00:00",
"backlog_provider": "github",
"external_board_url": "https://github.com/owner/repo/issues"
}
next_run_at is null when daemon not running; falls back to runs.jsonl estimate if daemon.state.json missing.
GET /api/instances/<name>/config / PUT /api/instances/<name>/config¶
GET returns resolved forgeo.yaml as JSON. PUT validates against ForgeoConfig and persists atomically:
curl -X PUT http://127.0.0.1:8790/api/instances/my-repo/config \
-H 'Content-Type: application/json' \
-d '{"name": "my-repo", "repo": "/home/me/site-a", "interval_minutes": 60, "agent_command": "claude -p \"$FORGEO_TASK\""}'
Returns 200:
{"saved": true, "restart_required": false, "message": "Config saved. The daemon picks up changes on its next cycle.", "config": {...}}
name must match registry; telegram_bot_token cannot be changed via API (preserved if omitted). Same for backlog_auth, state_dir, task_context. Daemon picks up changes next cycle (except repo/backlog/blocker_file/log_file which need restart). Errors: 400 invalid payload/name/token change, 404 unknown instance, 500 config unavailable.
POST /api/instances/<name>/start|stop|restart¶
curl -X POST http://127.0.0.1:8790/api/instances/my-repo/start
Returns outcome + daemon state:
{"status": "started", "message": "Forgeo 'my-repo' started (pid 4242, interval 30 min).", "daemon_running": true, "pid": 4242}
status is started/already_running/stopped/not_running/restarted/start_failed/stop_failed/restart_failed. Errors: 409 already running (start), 404 unknown instance, 500 config unavailable or lifecycle failed.
GET /api/instances/<name>/logs?lines=N / runs?limit&offset / blocker¶
curl "http://127.0.0.1:8790/api/instances/my-repo/logs?lines=50"
curl "http://127.0.0.1:8790/api/instances/my-repo/runs?limit=5&offset=25"
curl http://127.0.0.1:8790/api/instances/my-repo/blocker
- logs — last N lines (default 100, max 10000).
- runs —
{runs: [...], total, limit, offset}, newest first. Each run hasstarted_at,finished_at,kind(task/refactor),task_id/task_title,outcome,agent_exit_code,commit_sha,duration_seconds,reason,output_logs(bounded tail),retry_count. - blocker —
{"content": "..."}or{"content": null}.
Behavior & errors¶
- Unknown instance →
404 {"error": "unknown instance"}. - Missing data files →
200with empty data anddaemon_running: false. 400malformed body, invalid config, bad status transition, or deletingCOMPLETED/FAILED.409ID collision or start while running.500handler error or lifecycle failure.
Security notes¶
- Default
0.0.0.0exposes everything on the LAN — use--host 127.0.0.1or a firewall, and--tokenon shared hosts. Token is sent as bearer on every/api/*; without TLS it is cleartext — use a TLS proxy or SSH tunnel.?token=...appears in logs; prefer the login page. - Anyone with network access can add/edit/delete tasks and start/stop/restart daemons and change config (except
name/telegram_bot_token). Auth covers all/api/*routes.