Adds the operator surface for switching the controller pipeline between
the personal fork (drew/cleveragents-core) and the canonical repo
(cleveragents/cleveragents-core) via a MODE env + --prod CLI flag,
backed by safety primitives that make a wrong-mode launch loud rather
than silent.
run-controller-state-machine-pipeline.sh: --prod flag and MODE env
(primary home: .devcontainer/.env) select fork vs prod. After resolving
MODE, the launcher auto-sources the matching overlay file
(.devcontainer/.env.{fork,prod}) and asserts MODE didn't drift during
the source step. The drift assertion uses a readonly snapshot under an
obscure variable name so a stray ``MODE=fork`` in .env.prod aborts the
launch with a clear bash error rather than silently demoting the run.
CONTROLLER_RUN_DIR_ROOT now overrides the trial /tmp path so prod can
use a persistent /var/lib/cleveragents/run dir.
tools/launch_prod.sh (new): sibling to launch_fork.sh with the opposite
safety primitive — affirmative GET /repos/{owner}/{repo} that asserts
the target is non-fork, exists, isn't archived, and the bot has push.
On any failure, no env is exported. Honors HAL_* aliases for parity
with launch_fork.sh and prints a hard-to-miss PROD-MODE banner.
tools/controller/deploy/validate_db_mode.py (new): stamps a _mode_marker
table on each SQLite db (controller DB + telemetry cache) on first use,
asserts a match on every subsequent launch, and moves mismatched files
aside as <name>.<prior-mode>.bak.<ts> — never deletes. The --adopt flag
lets an operator grandfather in already-good pre-marker data without
losing history. Wired into the launcher's startup sequence before
OpenCode and the master start.
tools/_cache_path.py (new): single source of truth for the per-(owner,
repo) Forgejo cache file convention. .opencode/telemetry/server.py and
the launcher both delegate here so the dual-source-truth drift risk is
eliminated. tools/_pipeline_cache.py and tools/controller/db/models.py
documented as not owning the _mode_marker table so future migrations
leave it alone.
.opencode/telemetry/server.py: hosts the llm_activity scraper as a
background subprocess thread (60s cadence, --since-hours 1 in steady
state, full backfill on first tick). Re-homes the cost-telemetry data
path after the pr_state_warmer was retired by the controller migration
— without this the Cost tab freezes when the warmer's loop is gone.
Subprocess (not in-process) for isolation; failures swallowed.
opencode.json: local-claude provider's baseURL now reads
{env:LOCAL_PROXY_URL} instead of the literal http://127.0.0.1:3456/v1,
matching the apiKey pattern already in use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline Telemetry Console
A small, dependency-free HTTP console that consolidates everything you
need to monitor the auto-agents pipeline into a single browser tab.
Stdlib-only Python — no pip install required.
Why
Pipeline state lives in five silos today: the SQLite cache
(merge_cycle / conflict_drive_cycles / llm_activity / pulls /
commits), the running daemon processes, per-driver heartbeat files,
the OpenCode HTTP server, and the live Forgejo API. Tailing five
terminals + ad-hoc sqlite3 + curl is fine for the operator who built
the pipeline; it does not scale to "another dev wants to see what's
happening". This console is the consolidated view.
Run
# Always source the launcher first so the console reads the right
# cache and hits the right Forgejo target.
source tools/launch_fork.sh
# Default: bind 127.0.0.1:8765
python3 .opencode/telemetry/server.py
# Custom host/port
python3 .opencode/telemetry/server.py --host 127.0.0.1 --port 8765
# or via env
TELEMETRY_HOST=127.0.0.1 TELEMETRY_PORT=8765 \
python3 .opencode/telemetry/server.py
# Browse:
# http://127.0.0.1:8765/
The console never writes to Forgejo, OpenCode, or the SQLite cache. It is read-only by design.
Tabs
| Tab | What it shows | Refresh | Source |
|---|---|---|---|
| Overview | Daemon up/down + heartbeat freshness + last 5 cycles per driver | 5 s | /proc walk · file mtime · cache |
| Drivers | Full merge_cycle and conflict_drive_cycles tables (last 50) + 24 h outcome breakdown |
15 s | cache |
| PRs | Open PRs by auto/* label · live Forgejo query · click-through to web UI |
30 s | Forgejo API |
| Velocity | Window stats (24 h / 48 h / 7 d / 30 d) · merged / opened / commits | 5 min | cache |
| LLM | Active OpenCode sessions · message counts · created/updated ages | 10 s | OpenCode API |
| Cost | Token usage and estimated cost by model · selectable window | 60 s | llm_activity table + prices.json |
| About | Environment audit · data-source map · operator notes | static | /api/meta |
Inactive tabs don't poll — switching tabs cancels the previous tab's interval. Server load on a long-lived browser session stays flat.
Connection-status indicator
The pulsing dot in the top-left turns:
- Blue (pulsing) — fetching, normal state
- Green — last fetch succeeded
- Red (fast pulse) — last fetch failed (API unreachable, JSON parse error, server returned non-2xx). Hover to see when it last succeeded; click any tab to retry.
Repo target
The header banner shows <owner>/<repo> and a cache OK /
cache MISSING indicator. If you sourced tools/launch_fork.sh,
expect to see drew/cleveragents-core. If you forgot to source it,
you'll see cleveragents/cleveragents-core (canonical) — restart the
console after sourcing the right shell to switch.
Cost tracking — instrumentation status
The Cost tab reads aggregated token usage from the
llm_activity(model, tokens_in, tokens_out, cached_tokens) table. The
table exists with the right schema; the drivers do not yet write to
it. The tab will display an Instrumentation pending notice while
this is the case, and populate automatically once the writers land.
The token → USD conversion uses a per-model price table in
.opencode/telemetry/prices.json (override) or the seeded defaults in
server.py (_DEFAULT_PRICES). Prices are listed as dollars per
million tokens for in / out / cached_in. To override:
// .opencode/telemetry/prices.json
{
"anthropic/claude-opus-4-6": { "in": 15.0, "out": 75.0, "cached_in": 1.5 },
"CleverThis-15/Qwen3-6-35B-A3B-GGUF-UD-Q3-K-XL": { "in": 0.5, "out": 1.0 }
}
Models without a price entry are surfaced with an unpriced pill so operators know to add an entry rather than treat the row as $0.
Security posture
- Default bind: 127.0.0.1 (loopback only). No remote-by-default.
- No authentication in v1. The loopback bind is the security model.
- The console does not expose secrets —
GITEA_TOKEN,FORGEJO_PAT, etc. are read into the server process and used to authenticate outbound API calls; their values never appear in any endpoint response. The/api/metaendpoint includes a booleanhas_forgejo_tokenso you can confirm presence without leaking the value. - Exposing on a public interface requires a reverse proxy with auth
in front of it. Setting
TELEMETRY_HOST=0.0.0.0is a deliberate decision; the server logs the bind interface at startup so this is visible.
Architecture
.opencode/telemetry/
├── server.py # stdlib HTTPServer, all /api/* + static
├── index.html # tabbed SPA, no build step
├── app.js # vanilla JS, per-tab fetch loops
├── style.css # minimal, prefers-color-scheme aware
├── prices.json # (optional) token-cost overrides
└── README.md # this file
Endpoints under /api/* are documented in server.py's module
docstring. Each is a thin SQLite query, urllib.request call, or
/proc walk — no business logic, easy to extend.
Add a new metric / tab
- Add a query function in
server.pynext to the existing_api_*helpers. - Wire it into
_API_DISPATCHwith a unique/api/...path. - Add a
<section class="pane" data-pane="<name>">toindex.htmland a<button class="tab" data-tab="<name>">in the nav. - Add a
PANES['<name>'] = { intervalMs, refresh }entry inapp.js.
The whole thing fits in four files; you can read it end-to-end in under ten minutes. By design.
Limitations / planned
- No charts in v1. Window stats are shown as numbers; the canvas
rendered by
tools/reports.shkeeps the historical-trend story (weekly throughput, monthly volume, daily series). The console focuses on live/short-window pipeline state, not historical analytics. If we want charts here later, a single<canvas>+ vanilla JS plot is the right way to add them — staying dependency-free. - No SSE / WebSocket. v1 is plain polling. SSE would cut the no-data-changed re-render cost; not worth the complexity until observed.
- No request log persistence. Server only logs warnings; if you
want a full request log, run with
--log-level DEBUG. - HEAD is not implemented. Returns 501. Browsers and the SPA only use GET so this hasn't bitten anyone; trivial to add when needed.
Troubleshooting
| Symptom | Fix |
|---|---|
Header shows cleveragents/cleveragents-core but you wanted the fork |
You forgot source tools/launch_fork.sh. Stop the console, source it, restart. |
cache MISSING pill in header |
The cache file has not been created yet for this (owner, repo). Run any cache-syncing tool (e.g. python3 tools/sync-forgejo-cache.py) once. |
Cost tab shows unpriced for the model your driver actually uses |
Add an entry to prices.json (or the _DEFAULT_PRICES dict in server.py). |
| LLM tab is empty even though OpenCode is running | The console queries GET /session against OPENCODE_URL (default http://127.0.0.1:4096). Set OPENCODE_URL if the server is on a different port. |
PRs tab says forgejo unreachable or no token |
FORGEJO_PAT (or GITEA_TOKEN) is empty in the console's environment. Source launch_fork.sh and restart. |