112 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Licensing
RoboCo is licensed under AGPL-3.0 (see LICENSE). Copyright (c) 2026 Renzo Franceschini. Do NOT reintroduce an MIT or other license reference anywhere (README, headers, package metadata) — the project is AGPL.
Contributions require a signed Contributor License Agreement (CLA.md), automated via the CLA Assistant workflow (.github/workflows/cla.yml). The CLA preserves the option to dual-license / offer a commercial edition later; keep copyright assignment language intact. See CONTRIBUTING.md.
Project Overview
RoboCo is an AI Agentic Company - a virtual organization of 25 AI agents + 1 human CEO, designed to operate as a complete software development workforce. The system implements a structured organizational hierarchy with formal communication protocols, task management, and quality controls.
Core Architecture
CEO (Renzo - Human)
|
+-- Intake (on-demand interviewer: chats only with the CEO to draft a task)
+-- Secretary (on-demand chief-of-staff: reads company state, runs gated CEO directives)
+-- PR Reviewer (read-only: the main reviewer — inbound external/fork + internal PRs, and the root→master in-path gate)
|
+-- Board (3 agents)
+-- Product Owner
+-- Head of Marketing
+-- Auditor (silent observer, reports to CEO)
|
+-- Main PM (coordinates all cells)
|
+-- Backend Cell (6 agents: 2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer)
+-- Frontend Cell (6 agents: 2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer)
+-- UX/UI Cell (6 agents: 2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer)
Hardware Infrastructure
- Olares One (Powerhouse): Intel Ultra 9 + RTX 5090, runs Claude Code instances and AI inference - NOT YET ARRIVED
- UGREEN NAS (Warehouse): 36TB RAID6, 128GB RAM, hosts PostgreSQL, Redis
- Pi Cluster (Operations): Monitoring, notifications, smart home
Multi-Agent Workspace Structure
Each agent gets their own git clone of a project, enabling parallel development without conflicts:
{ROBOCO_WORKSPACES_ROOT}/ # Default: /data/workspaces
+-- {project-slug}/
+-- {team}/
+-- {agent-slug}/
+-- [git repository]
Example:
/data/workspaces/
+-- roboco/
+-- backend/
| +-- be-dev-1/ # be-dev-1's workspace
| +-- be-dev-2/ # be-dev-2's workspace
+-- frontend/
+-- fe-dev-1/
+-- fe-dev-2/
Note: the Next.js control panel now lives at roboco/panel/ inside this repo (no longer a separate roboco-panel project or workspace).
Key Configuration (roboco/config.py):
ROBOCO_WORKSPACES_ROOT: Root directory for workspaces (default:/data/workspaces)ROBOCO_WORKSPACE_AUTO_CLONE: Auto-clone repos on first access (default:true)ROBOCO_WORKSPACE_CLONE_TIMEOUT: Clone timeout in seconds (default:300)
On a Python workspace, WorkspaceService runs uv sync --extra dev (not plain uv sync) so the clone's .venv carries the full gate toolchain (ruff/mypy/xenon/pytest) — the lint/type/complexity tools live in the dev extra, which plain uv sync skips. Without it an agent's make quality fails on ruff: command not found and the agent can't gate its own work.
Because the clone is shared across a dev's tasks, a fresh claim git-resets the workspace to a clean tree (git reset --hard) before checking out the new task's branch — discarding abandoned uncommitted cruft from a finished task while preserving all commits and the gitignored .venv. A resume short-circuits before this, so committed work is never reset.
Terminal completion and cancellation also force-delete the task's local branch ref and its .previews/ video-render dir in the assignee's clone (alongside the existing worktree removal), skipping any branch that coincides with an environment-ladder rung; a PM/CEO can additionally sweep older backlog branches project-wide via POST /git/branches/cleanup or the Git page's "Clean Up Stale Branches" button.
Git Workflow
Branch Naming Convention
Branch names follow the pattern: {type}/{team}/{task-hierarchy}
Types: feature, bug, chore, docs, hotfix
Task Hierarchy: Uses -- separator (not /) to avoid git ref conflicts.
Examples:
- Root task:
feature/backend/ABC12345 - Subtask:
feature/backend/ABC12345--DEF67890 - Sub-subtask:
feature/backend/ABC12345--DEF67890--GHI11111
Commit Format
Commits are automatically prefixed with the task ID:
[{task-id[:8]}] {message}
Example:
[ABC12345] Add user authentication endpoint
Work Sessions
When a developer claims a task, a WorkSession is created that tracks:
- Branch name and base/target branches
- All commits made during the session
- Files modified
- PR number/URL when created
- Merge status and who merged
A task has at most one active WorkSession: re-claiming a task (pool release, reaper unclaim, escalation redirect) supersedes any prior agent's stale active session, enforced both at the service layer and by a DB partial-unique index (migration 047). Without it, duplicate active sessions made the one-row active lookup raise and crashed the claim/plan flow into a respawn loop.
A developer's clone is shared across all their tasks, so push and PR-head operate on the task's recorded branch by name, independent of the clone's current checkout — fixing the BRANCH_MISMATCH / "No commits between" failures when the clone was parked on a later task's branch. A missing local task-branch ref is first recovered from origin/<branch> before the push-by-name.
Stale-state family fixes (2026-07-24, #683/#690/#692). Three independent chokepoints used to trust whatever a clone/worktree happened to have on disk instead of checking origin, all closed the same week off one live incident: GitService.rebase_onto_base no longer opens with an unconditional reset --hard origin/<head_branch> (#683) — it classifies local vs origin post-fetch first, so a committed-but-unpushed dev tip (routine mid-rework, since commit never pushes) is never silently discarded, and a genuine two-sided divergence returns {"status": "diverged", ...} untouched rather than guessing a side to keep. The reader-side twin, GitService._resolve_head_ref (backing diff/list_changed_files/read_file_at_branch/roboco_git_log), now prefers origin/<branch> over a local ref whenever origin carries commits the local ref lacks (#690) — a QA/PM/reviewer clone parked on pre-rebase history no longer stays frozen there across every later review round. And WorkspaceService.ensure_worktree_self_heal stops treating an ALREADY-PRESENT per-task worktree as a pure venv+chown no-op (#692): every spawn now runs it through _refresh_present_worktree, role-aware via can_author (foundation.identity.WORKTREE_AUTHOR_ROLES) — fast-forward when behind for every role without ever discarding an author's dirty uncommitted edits, leave alone when strictly ahead, hard-reset only for a non-author reader when diverged. Together these close the "reviewer keeps bouncing a task back for fixes that already landed, because it's still looking at its own frozen round-1 checkout" incident class.
Git Credentials
Git authentication is managed per-project through encrypted GitHub PATs:
- Each project stores its own git token - no global fallback
- Tokens are encrypted at rest using Fernet symmetric encryption
- API never exposes tokens - only returns
has_git_token: boolean - Self-service via UI - users set/update tokens in project settings
Project fields:
| Field | Description |
|---|---|
git_token_encrypted |
Fernet-encrypted GitHub PAT (DB column) |
has_git_token |
Boolean indicator for API responses |
Token flow:
- User creates project in UI, enters GitHub PAT
- Token encrypted and stored in
projects.git_token_encrypted - WorkspaceService decrypts token when cloning repos
- GitService decrypts token for PR operations (gh CLI)
HTTPS URLs require tokens - attempting to clone without a token will raise WorkspaceError.
Forge providers (GitHub + Gitea + GitLab)
The REST surface (PRs, CI status, reviews, labels, releases) is provider-routed (roboco/services/forge/): GitProvider is the ~20-method transport contract, GitHubProvider, GiteaProvider, and GitLabProvider implement it, and GitService._forge returns a ForgeRouter that picks the transport per call from RepoRef.host — None (github.com/GHE) rides GitHub, a registered Gitea/GitLab host rides that instance's provider, so GitService's call sites never know which forge they're on. A project opts in via projects.git_provider (gitlab.com auto-detects like github.com; self-hosted instances set it explicitly; "github" doubles as the GHE escape hatch with ROBOCO_GITHUB_API_BASE_URL) — panel: the Forge select on the project settings page's identity card (/projects/[id]/settings, Wave C — was the edit-project dialog, now deleted). The host→provider(+scheme — plain-http LAN instances are supported) map is in-memory per process, self-healing: ProjectService.get/get_by_slug re-register on every read. Both non-GitHub providers adapt their wire contracts back into the GitHub shapes GitService classifies (forge/shaping.py ShapedResponse): Gitea — token auth scheme, duplicate-PR 409→422, commit statuses reshaped into check_runs/workflow_runs, Do-keyed POST merge, slash-encoded refs; GitLab — MR iid→number, source/target_branch→head/base, per-file diffs reassembled into unified-diff text, approve-vs-note review routing (no request-changes verb exists), pipelines/statuses CI reshapes, reviewer-request skipped (needs numeric ids). Neither has GitHub's server-side merges API: their merge_branch returns a shaped 501 and GitService.sync_env_branch runs the shared local-git fallback (_local_merge_branch: throwaway clone → merge → push; conflict aborts with the remote untouched, same status vocabulary). Plain git (clone/fetch/push) is forge-agnostic — the Basic-auth x-access-token:<token> extraheader works on Gitea/GitLab unchanged (verified live on Gitea). The env-gated tests/e2e_smoke/test_gitea_live.py is the live contract suite (self-seeding against a dockerized gitea/gitea; it caught the slash-encoding and http-scheme gaps).
Protected Branches
projects.protected_branches (operator-declared, panel: a chips editor on the project settings page's placement card) is unioned — never replacing, only tightening — into GitService's hardcoded safety floor. Two scopes, deliberately different: _protected_branches_for (rebase refusal + sync_task_branch's force-push refusal) is the field alone unioned with the hardcoded {master, main} floor and fails OPEN on a lookup error (a wrongly-blocked rebase over a transient DB blip is the worse tradeoff, and a skipped rebase gets no free retry); _protected_branches_for_deletion — consulted ONLY by the shared _delete_remote_branch_best_effort chokepoint every remote-delete path routes through (task-branch cleanup on cancel, the stale-branch sweep, and post-merge PR-source cleanup) — additionally unions in the project's environment-ladder rung branches (effective_environments, so a null ladder's synthesized single rung off default_branch is protected too, e.g. a renamed trunk like trunk) and fails CLOSED on a lookup error (skip the delete entirely; deletion is best-effort so a skipped one just retries at the next sweep, whereas silently proceeding on an unresolvable project could delete a real declared rung for good). Matching is exact and case-sensitive; an empty protected_branches list degrades to exactly the prior hardcoded-only behavior.
Task Lifecycle
Task States
The complete task lifecycle is defined in roboco/foundation/policy/lifecycle.py (roboco/enforcement/task_lifecycle.py is a backwards-compat shim over it):
In-path PR-review gate (awaiting_pr_review): each assembled PR is reviewed before the PM merges. The cell PM's submit_up opens the cell→root PR and the Main PM's submit_root opens the root→master PR; both enter awaiting_pr_review, where a reviewer pr_passes it on to awaiting_pm_review or pr_fails it back to needs_revision — the merge-level reject the PM otherwise lacks. Leaf dev tasks and branchless coordination roots skip the gate. pr_pass additionally refuses while the assembled PR's own CI (its head commit's checks, GitService.get_pr_ci_status) is failing, pending, or unresolvable — a repo with no CI configured passes through with an evidence note; pr_fail stays available regardless so a reviewer is never stuck waiting on CI. The reviewer prompt requires a per-AC file:line walk (a silently dropped deliverable is an automatic fail) and the gate's diff/conventions base resolves the task's REAL parent branch (resolve_parent_branch, the parent task's own branch_name) instead of deriving it from the branch-name string, so a cross-team hop (e.g. cell→root) no longer attributes inherited base-branch content to the task under review.
Sequence is the bar. A task with a parent and effective sequence N (COALESCE(sequence, 0)) cannot be claimed while any same-parent sibling with a strictly lower effective sequence is non-terminal — assignee-blind and, for MegaTask batch root-subtasks, independent of and stricter than dependency_ids (every other same-parent context is reachability-aware — see the sequence-drift fix below), enforced in TaskService._validate_claim_preconditions (the claim chokepoint itself) so every claim path crosses it. Ties run parallel; cancelled siblings never block; sequence 0 and parentless tasks are unaffected. Delegation stamps sequence from the collision DAG (stamp_wave_sequence: 1 + max same-parent dependency sequence, or 0 when independent) instead of a raw per-sibling ordinal, so fully independent siblings tie and run in parallel while colliding/ordered work ascends — PM-authored sequences are never rewritten. tasks.parent_task_id is indexed (migration 069) since the guard's sibling probe runs on every claim; the dispatcher pre-filters dependency/sequence-held tasks (TaskService.is_pending_claim_blocked) before attempting a doomed claim.
Sequence-drift fix (2026-07-24): the bar is reachability-aware outside MegaTask batches. stamp_wave_sequence stamps each new sibling's wave from a partial, per-task view of the graph at delegate time — fine within one connected chain, but two INDEPENDENT dev-task streams under the same parent (unconnected, stamp_wave_sequenced incrementally over time) can land on the same raw sequence number by coincidence, and the old edge-agnostic bar phantom-held a wave-N sibling behind a totally unrelated wave-(N-1) sibling from a different stream. _claim_blocked_by_sequence now branches on is_batch_root_subtask(task.batch_id, task.parent_task_id): a MegaTask root-subtask (whose sequence is a one-shot, globally-computed Kahn wave index from PrompterService._build_confirm_batch — a deliberate staged-release barrier) keeps the original strict, edge-agnostic rule unchanged; every other same-parent context routes through the pure sequence_blocker_id (roboco/services/sequencing.py), which only lets a lower-sequence candidate block when it's a real (transitive) predecessor via dependency_ids UNIONED with completed_dependency_ids (the union matters — _unblock_dependents prunes a completed dependency's edge into completed_dependency_ids the moment it lands, almost always before the dependent is ever claimed) — a task with NO dependency edge onto any same-parent sibling at all still falls back to the original raw bar unchanged (preserves the #452 edge-less-PM-delegation scenario exactly). The hold also now surfaces cleanly: TaskService.sequence_hold_reason + a proactive _sequencing_claim_guard in the gateway's _run_claim_guards return a dedicated Envelope.sequence_held (naming the blocker) on both the PENDING and NEEDS_REVISION reclaim paths, instead of claim()'s bare None return reaching the verb runner and getting misdiagnosed as a "concurrent transition" invalid_state. give_me_work's two offer paths (TaskService.list_pending_for_agent and the Choreographer's _drop_dependency_held over list_assigned_for_agent) both now also consult the sequence bar (the latter via is_pending_claim_blocked, extended to NEEDS_REVISION) so the dispatcher never offers a task the claim gate is about to reject.
PM-turn elimination (auto-submit). When every child of an assembled, PR-bearing parent goes terminal, the orchestrator's closure dispatcher (_maybe_spawn_pm_closure → _closure_handled_without_pm → _try_auto_submit) runs the real submit_up/submit_root gate system-side as the owning PM instead of spawning the PM for that turn — same verb, same guards (ownership, notes, journal:decision, subtasks-terminal, parent-AC coverage, branch), authorized via the internal API with the PM's own identity headers. This is unconditional — the turn cut IS the flow, no kill-switch. Success lands the task on awaiting_pr_review with an audited task.auto_submitted row and no PM spawn; ANY refusal (branchless/umbrella parent, a gate rejection — freshness, AC coverage, a subtask-terminal race — or a transport error) falls back to spawning the PM exactly as before — that fallback is the sole safety net — with the refusal reason threaded into the PM's closure prompt so it isn't rediscovering it blind.
Revision findings ledger (always-on, no flag — core lifecycle). QA/PR-gate/PM/CEO bounce feedback used to be prose-only: issues: list[str] flattened into free text with no structural anchor, request_changes/ceo_reject had no structured note at all, and the raw dev_notes append both used was silently overwritten by the very next note(scope='handoff') call — a live data-loss bug. fail_review (QA), pr_fail (in-path PR gate), request_changes (PM merge reject), and ceo_reject all now take structured findings: list[dict] — validated into Finding (file repo-relative ≤300 chars/no .., line ≥1, severity blocker|major|minor|nit, criterion must match an AC id or its exact text, expected/actual ≤300, fix ≤500, evidence ≤2000) — with a soft nudge above 5 findings and a hard reject above 10 in one call (roboco/services/gateway/choreographer/findings.py). issues=[...] still works this release as a shim (each string → a file-less severity=major finding, deprecation-logged) and merges with findings rather than one silently dropping the other. Every producer inserts one append-only row per finding into task_review_findings (migration 071; origin qa|pr_gate|pm|ceo, round = revision_count+1 read pre-transition, status open→addressed→verified|waived) via ReviewFindingsRepository, then writes a structured note whose summary IS the deterministic per-finding rendering [F-id8] file:line (severity) — expected → actual → fix, mirrored into qa_notes/pr_reviewer_notes/the new pm_notes column (new PmReviewContent "pm_review" content type). ceo_reject now validates its reason (previously could 500 on an empty/trivial one) and stamps it as one origin=ceo blocker finding; on a branchless coordination root — which routes to pending via admin_set_status, bypassing the normal audit chokepoint — it bumps revision_count and emits task.ceo_reject directly instead of silently skipping both. New audit events task.request_changes/task.ceo_reject join task.qa_fail/task.pr_fail in _audit_events_for so rework metrics attribute every bounce kind, not just QA/PR-gate.
Resolution: i_am_done/submit_up/submit_root all gain resolved_findings ({finding_id, commit?, note?}), gated by a new Requirement.FINDINGS_ADDRESSED — every OPEN finding on the task must be named (a fuzzy 8-char-prefix match against [F-id8]) or the envelope rejects, listing the still-open ids. pass_review/pr_pass/complete bulk-verify their own origin's addressed findings same-transaction (a stamp failure fails the verb outright, not best-effort); ceo_approve stamps ceo-origin best-effort. mark_waived is wired to the auditor-only waive_finding flow verb (severity-scoped: blocker/major must be fixed, never waived; only minor/nit open findings are waivable, with a required note and a task.finding_waived audit event; no task status change).
Delivery: evidence()/build_task_handoff carry revision_findings (open only, capped) so a bounced dev finally gets what developer.md promises instead of nothing; claim_review/claim_gate_review additionally carry prior_findings (the full ledger) so a round-2+ reviewer checks prior findings instead of re-deriving them blind. The orchestrator's REVISION_REQUIRED dev prompt and the PM triage "bounced" block render open findings inline with the same rendering; A2A fail bodies share it. GET /api/tasks/{id}/findings (capped 500, SQL-aggregated per-origin/status summary + total/truncated) backs the panel's task-detail Findings tab and a bounced xN header chip (revision_count); metrics attribute pm_rejects/ceo_rejects + open/total findings counts per task; vault task notes render a capped ## Findings section (fail-open fetch, never blocks the note write).
Role-Based Transitions
All status transitions are validated through the enforcement layer. Key restrictions:
| Transition | Allowed Roles |
|---|---|
backlog → pending (activate) |
PM roles only |
pending → claimed (claim) |
Role must match task type (QA for awaiting_qa, etc.) |
claimed → pending (unclaim) |
Assignee or PM |
awaiting_qa → awaiting_documentation (pass) |
QA only |
awaiting_qa → needs_revision (fail) |
QA only |
awaiting_documentation → awaiting_pm_review |
Documenter or Developer (parallel completion) |
in_progress → awaiting_pr_review (submit_up / submit_root) |
PM roles (opens the assembled cell→root / root→master PR) — or the orchestrator running the same verb system-side as the owning PM once all children are terminal |
awaiting_pr_review → awaiting_pm_review (pr_pass) |
PR reviewer only |
awaiting_pr_review → needs_revision (pr_fail) |
PR reviewer only |
awaiting_pm_review → completed |
PM roles only |
awaiting_pm_review → needs_revision (request_changes) |
PM roles only — the merge-level reject with structured findings (see "Revision findings ledger" above) |
awaiting_pm_review → awaiting_ceo_approval |
PM roles only |
awaiting_ceo_approval → completed/needs_revision/cancelled |
CEO only |
Any → cancelled |
PM roles only |
Unclaim Operation: Agents can release claimed tasks back to the pool using unclaim(). This transitions claimed → pending and optionally reassigns to another agent.
Board never owns a coordination root: a Board role (Product Owner / Head of Marketing) is never assigned a Main-PM coordination root (delivery root or MegaTask root-subtask) via escalation or reassignment — Board roles have no unblock verb, so such a hand-off would deadlock. The transition is diverted to the pool for a role-matched Main-PM reclaim.
Git Integration Requirements
All tasks follow git workflow. PR is created BEFORE QA review (not after) so QA can review the real PR diff on GitHub and downstream PM/CEO approval chain off a PR that already exists:
- claimed -> in_progress:
branch_nameis auto-set on claim (hierarchical branches) - verifying -> awaiting_qa (submit-qa): Requires
self_verified,commits,pr_number(PR open), and at least oneprogress_updatesentry - awaiting_qa -> awaiting_documentation (pass-qa): Requires
pr_numberand substantive QA notes - awaiting_documentation -> awaiting_pm_review: Requires
docs_complete=True(PR already exists from step 2 above) - awaiting_pm_review -> awaiting_ceo_approval: Must have
pr_numberset and all subtasks in a terminal state
CEO Approval Workflow
Major tasks are escalated to CEO for final approval:
- PM reviews and approves, escalates to
awaiting_ceo_approval - CEO can:
- Approve: Merges PR, task ->
completed - Request changes: Task ->
needs_revision - Cancel: Task ->
cancelled
- Approve: Merges PR, task ->
Data Models
Core Models (roboco/models/)
| Model | Purpose |
|---|---|
Task |
Atomic unit of work with acceptance criteria |
Project |
Git repository configuration and CI/CD commands |
WorkSession |
Links agent work to task, tracks branch/commits/PR |
Agent |
AI agent with role, team, capabilities |
Notification |
Formal notification requiring acknowledgment |
Journal |
Agent personal log for reflections/learnings |
Task Model Key Fields
# Git configuration (all tasks follow git workflow)
task_type: TaskType # code, documentation, research, planning, design, administrative
project_id: UUID # Project this task works on (required)
branch_name: str # Branch for this task (auto-created on claim)
work_session_id: UUID # Active work session
# PR tracking (parallel execution in awaiting_documentation)
pr_number: int # GitHub/GitLab PR number
pr_url: str # Full URL to PR
docs_complete: bool # Documenter has finished
pr_created: bool # Developer has created PR
# Commits linked to task
commits: list[CommitRef] # All commits made for this task
Communication Model
Agents coordinate via task state + task detail fields, not a channel/session backbone. Two comms primitives sit alongside that: A2A (dm + read_a2a, direct peer-to-peer, same-cell only — see docs/rag/tools/a2a-tools.md) for informal contact, and Notifications (notify, ack-required, sent by PMs/Board only) for formal signals. The CEO is the one asymmetric participant: from the panel it can open a direct 1:1 A2A conversation with any DM-capable agent at any time, but an agent can never initiate to the CEO — only reply in-thread once the CEO has opened one. A CEO-authored DM wakes an offline recipient via the a2a_request notification dispatch path, a wake same-cell dm never triggers.
Agent learnings (note scope='learning') broadcast as knowledge-share notifications only to other agents — the human / human-driven roles (CEO, prompter, secretary) are excluded, since agent knowledge-sharing is noise in a human's inbox.
Notification re-escalation backoff (always-on). sweep_expired_notifications (roboco/services/notification_delivery.py) re-escalates a still-unacked ack-required notification past its expires_at to the recipient's up-role (the PM's PM, or the CEO) — but only when a per-notification backoff schedule says it's due, not on every ~60s sweep tick forever. Each row carries reescalation_count / last_reescalated_at / reescalation_delivered_count (migration 079): the first re-escalation fires at expiry, each one after that doubles the wait from ROBOCO_NOTIFICATION_REESCALATION_BASE_SECONDS (default 1h, capped at 24h), and past ROBOCO_NOTIFICATION_MAX_REESCALATIONS (default 5) the row is logged once as permanently-unacked and left alone for good — the due/wait/capped decision is a pure function (reescalation_decision, roboco/foundation/policy/communications.py). The attempt slot is claimed by a compare-and-set UPDATE ... WHERE reescalation_count = :n BEFORE any delivery is attempted (the 60s dedup guard elsewhere does NOT backstop this — BLOCKER_ESCALATION, the type every re-escalation fires as, is excluded from the loop-prone dedup set), so two sweep ticks racing the same row can never both deliver. Legacy rows read as count=0 and keep the original first-fire semantics.
Key Principles
- Everything is a task - All work is tracked and documented
- No work without a task - Create task record first
- No task without acceptance criteria - How do we know it's done?
- No closure without documentation - Future agents need context
- Communication is constant - Stream reasoning, log everything
- State is sacred - If interrupted, state must be recoverable
- The Auditor sees all - Quality monitored silently
- Commits linked to tasks - Every commit references its task ID
- CEO approves major changes - Escalation path for important work
Agent Gateway
Agents do not call the API or per-domain MCP tools directly. They go through two thin MCP servers (roboco-flow, roboco-do) backed by the server-side Choreographer in roboco/services/gateway/. The Choreographer composes the existing services (TaskService, JournalService, GitService, etc.) into intent-verb sequences. Tracing, claim-locking, evidence assembly, and remediation hints are all centralized there.
Each agent gets a spawn manifest at /app/tool-manifest.json listing the verbs its role is allowed to call. The orchestrator builds the manifest from roboco/services/gateway/role_config.py and mounts it read-only into the agent container.
Verb surface (canonical source: lifecycle.intents_for_role; every role also gets i_am_idle)
| Role | Flow verbs (beyond i_am_idle) |
|---|---|
| developer | give_me_work, i_will_work_on, open_pr, i_am_done, i_am_blocked, resume, sync_branch, unclaim |
| qa | give_me_work, claim_review, pass_review, fail_review, i_am_blocked, resume, unclaim |
| documenter | give_me_work, claim_doc_task, i_documented, i_am_blocked, resume, unclaim |
| cell_pm | give_me_work, i_will_plan, delegate, complete, request_changes, submit_up, triage, unblock, escalate_up, reassign, resume, unclaim |
| main_pm | give_me_work, i_will_plan, delegate, complete, request_changes, submit_root, triage, triage_all, unblock, escalate_up, escalate_to_ceo, resume, unclaim |
| pr_reviewer | give_me_work, claim_pr_review, post_pr_review (inbound external/fork PRs), claim_gate_review, pr_pass, pr_fail (in-path assembled-PR gate), unclaim |
| product_owner | triage, escalate_to_ceo |
| head_marketing | triage, escalate_to_ceo |
| auditor | triage, waive_finding (read-only; carries dm/read_a2a as a content tool so it can reply to a CEO-opened DM, but never initiates) |
| prompter | (none beyond i_am_idle — not a delivery-lifecycle role; intake interviewer, human-only) |
| secretary | (none beyond i_am_idle — human-only chief-of-staff; reads company state + runs gated CEO directives) |
Content tools (do_server) — most roles: commit, note, dm, read_a2a, evidence. Delivery roles (developer / qa / documenter / cell_pm / main_pm) also get draft_playbook (draft a curated playbook for the KB). Product Owner additionally gets five Board Program proposal verbs, all product_owner-only: propose_roadmap (Printer, the weekly roadmap cycle), propose_bug_hunt (Pest Control), propose_gap_fill (Spackle), propose_rebalance (Scales), propose_friction_fixes (Dogfood). Head of Marketing additionally gets six, all head_marketing-only: propose_feature_spotlight (spotlight), propose_market_brief (Periscope), propose_messaging_fixes (Mirror), propose_editorial_post (Megaphone), propose_campaign (War Room), propose_conversation_replies (Barfly). Both Board roles also carry pitch — see "Board Program registry" / "RoboCo X account" below. Auditor is restricted to note (scope=reflect) + evidence + dm/read_a2a, plus the playbook-curation verbs approve_playbook / reject_playbook / archive_playbook (a bounded, deliberate expansion — KB curation, not agent comms), two more Board Program proposal verbs auditor-only — propose_quality_report (Sentinel) and propose_playbook_drafts (Librarian, drafting straight via PlaybookService rather than draft_playbook, which the Auditor still never carries) — plus propose_postmortem (Coroner, event-triggered, no cron) and, when the Obsidian vault is armed, curate_vault (writes one narrative paragraph onto a just-completed root's vault note — see "Obsidian vault V1+V2" below). The auditor's dm/read_a2a exists so it can read and reply in-thread when the CEO opens a DM with it (mid-task, stuck) — it still never initiates peer A2A (agents_config.can_a2a_direct refuses it unconditionally as sender), preserving it as a silent observer to other agents. The pr_reviewer likewise now carries dm/read_a2a for the same CEO-reachability reason, on top of posting its change-request on the PR itself; its only INITIATION target stays its owning cell_pm/main_pm. The prompter (intake) and secretary are restricted to note + evidence — human-only, no dm/notify, they have their own dedicated chat pages instead. The note/journal write returns as soon as the entry is persisted; RAG indexing (Ollama embedding) runs fire-and-forget, so the tool no longer times out under concurrent load.
MCP servers running per agent container
| Server | Purpose |
|---|---|
roboco-flow |
Intent verbs (give_me_work, i_am_done, claim_review, complete, ...) |
roboco-do |
Content tools (commit, note, dm, read_a2a, evidence) |
roboco-git-readonly |
Read-only git: status, log, diff, branches |
roboco-optimal |
RAG: roboco_ask_mentor, roboco_kb_search |
roboco-docs |
Project docs file management (selected roles) |
playwright |
Structured browser tools (navigate/snapshot/evaluate/screenshot) — fe-qa/ux-qa role-gated, plus the Product Owner's Dogfood (board_dogfood) spawns task-scoped only (AgentOrchestrator._is_dogfood_spawn, not a blanket product_owner grant); not image-gated, the wrapper entrypoint points it at the image's own baked chromium-headless-shell |
Every verb returns a standardized Envelope:
- ok:
{status, task_id, next, evidence?, context_briefing} - error:
{error, message, remediate, missing}
The next field tells the agent what to call next; the remediate field on errors tells them exactly how to fix and retry. Agents should not guess state — trust the response. The verb runner re-checks the task after each composed atomic action and, on a concurrent mid-verb state change, fails fast with a clean INVALID_STATE (re-fetch + re-issue) rather than crashing on a None dereference.
Agent Providers
Agent backends are pluggable. roboco/llm/providers/ defines an AgentProvider lifecycle ABC (base.py) and a ProviderRegistry keyed by ModelProvider (registry.py), with ClaudeCodeProvider (default), GrokCliProvider, GeminiCliProvider, CodexCliProvider, and KimiCliProvider. The orchestrator resolves a provider at spawn from the agent's ModelProvider; when no dedicated provider is registered it falls back to the built-in Claude Code spawn. ModelProvider (roboco/models/base.py) is ANTHROPIC (default), GROK, GEMINI, OPENAI, KIMI, LOCAL, OLLAMA_CLOUD — OPENAI routes through the official Codex CLI on a ChatGPT subscription (CodexCliProvider), not a reserved/unimplemented value. The seam is additive: only GROK/GEMINI/OPENAI/KIMI route through their dedicated providers; Anthropic / Ollama Cloud / self-hosted spawns are unchanged, and every provider gets the same MCP gateway + tool-manifest wiring by construction.
Cost-tiered routing + routing presets. ModelRoutingService (roboco/services/llm.py) resolves (provider, model) per agent at spawn from model_assignments with precedence AGENT_SLUG > ROLE(:complexity) > ROLE > GLOBAL; the compound ROLE:complexity rung (e.g. developer:low) reuses the existing ROLE scope + scope_value column — no schema change — to pin a role to a cheaper model at a given task's estimated_complexity without touching the plain ROLE row everything else still uses. apply_mode('cost_tiered') additively seeds _COST_TIERED_SEED, unlike every other mode's wipe-then-seed; the seed is now an empty tuple (see "haiku retired from delivery-lifecycle roles" below) — the mode stays wired for a future above-floor re-seed, it just ships inert. On top of modes, routing presets (RoutingPresetTable) let an operator name-and-snapshot the FULL current routing state (mode + every assignment row, AGENT_SLUG pins included) via save_routing_preset, then restore it wholesale later with apply_routing_preset — a full swap, validate-every-entry-first so an invalid payload never triggers the wipe, unlike a mode's pin-preserving behavior. Panel: the AI routing settings card (ai-routing-card.tsx) exposes preset save/apply/delete alongside the existing mode buttons and the complexity-override editor.
Haiku retired from delivery-lifecycle roles (2026-07-24). Haiku can't reliably emit the structured envelopes the lifecycle now runs on — pass_review's per-AC criteria_verified, delegate's covers_parent_criteria, the findings ledger — so a haiku QA/PM claims, gets validation-rejected, idles, respawns, and loops without progress (a live incident: an fe-qa on haiku looped four awaiting_qa tasks to zero progress). ROLE_MODEL_MAP's qa/documenter defaults move from haiku to sonnet; the cost_tiered seed's developer:low → haiku entry retires to empty (the floor below would upgrade it anyway); and a structured-verb capability floor (_below_capability_floor/_floor_below_capability, roboco/services/llm.py) upgrades ANY below-floor Anthropic assignment to sonnet at resolution — from a pin, a ROLE row, or a future map edit — in both the assignment and legacy paths. Non-Anthropic providers are untouched (an Anthropic-tier floor, not a provider policy); pr_reviewer/auditor stay on opus.
Grok runtime. GROK agents run xAI's official grok CLI (model grok-build) authenticated by a SuperGrok subscription, not a metered API key — so a Grok workforce can't stall mid-task on out-of-credits. The host ~/.grok/auth.json is mounted read-only into each agent (GrokCliProvider._append_grok_auth_mount; ROBOCO_HOST_GROK_DIR is the host mount source, set up once with grok login). It reaches parity with the Claude path by construction: same MCP gateway + manifest, per-role tool-removal and git-operation deny rules, a prompt-injection guard on the task prompt, headless tool auto-approval, and per-agent token/cost capture from the grok session store. It covers both one-shot delivery roles and the interactive Intake (Prompter) and Secretary chats (per-turn grok -p with session resume).
Token auto-refresh. The grok access token has a fixed ~6h server-set TTL and the CLI cannot refresh it headlessly — on an expired token it hangs forever at an interactive login prompt. The orchestrator mints a fresh token from the offline-access refresh token (xAI's OIDC refresh_token grant) before expiry and rewrites the shared auth.json in place (roboco/llm/providers/grok_auth.py refresh_if_stale, run once per dispatch tick; the orchestrator's ~/.grok mount is read-write so it can rewrite it). As a backstop the agent entrypoint runs python -m roboco.llm.providers.grok_auth --check and refuses to start (exit 78) on a missing/expired token instead of hanging.
Gemini runtime (V1: one-shot delivery roles only, no interactive Intake/Secretary). GEMINI agents run Google's official gemini CLI (GA ids gemini-2.5-pro/-flash/-flash-lite, pinned via ROBOCO_GEMINI_CLI_MODEL) authenticated by an OAuth login, not a metered key. The host ~/.gemini (from a one-time interactive gemini login, ROBOCO_HOST_GEMINI_DIR) is mounted read-only at a staging path; the entrypoint COPIES it into a container-local, writable ~/.gemini so the CLI's own in-process token refresh (google-auth-library) can write back locally without ever touching the host copy. Unlike grok's single-use refresh token (which needs one orchestrator-side writer serializing every refresh, grok_auth.py), Google's refresh token is reusable, so each container refreshing its own copy independently is safe with no orchestrator refresh daemon — roboco/llm/providers/gemini.py's module docstring spells out the contrast. Tool scoping has no CLI-flag equivalent to grok's --disallowed-tools/--deny: it's expressed entirely through a rendered TOML Policy Engine (~/.gemini/policies/roboco.toml, deny-only rules keyed by toolName/commandPrefix) plus settings.json (security.auth.selectedType for headless OAuth, experimental.enableAgents=false for the fleet-wide subagent ban, advanced.autoConfigureMemory=false), all rendered by roboco/llm/providers/gemini_cli_config.py; --approval-mode yolo is universal (headless auto-approval). Usage/cost capture (gemini_cli_usage.py) reads the run's own --output-format stream-json terminal result event for per-model token stats — no session-file scraping — and prices each of the three GA models at its own rate before flattening to the grok-shaped usage.json; the same module also remaps a quota/rate-limit error (no dedicated CLI exit code — parsed from the run's JSON error.type) to exit 75, while exit 41 (the CLI's own auth-failure code) passes straight through, so the orchestrator parks the GEMINI provider on either exactly like it does for grok's exit-75/78.
Codex runtime (V1: one-shot delivery roles only, no interactive Intake/Secretary). OPENAI agents run OpenAI's official codex CLI (model pinned via ROBOCO_CODEX_CLI_MODEL, default gpt-5.3-codex — codex has no reliable default) authenticated by a ChatGPT subscription, not a metered API key. The host ~/.codex (from a one-time codex login, ROBOCO_HOST_CODEX_DIR) is mounted read-only as a DIRECTORY, not a single auth.json file — a single-file bind mount pins the inode, so the orchestrator's atomic tmp+rename refresh would never reach a running container, the same concern grok's mount documents; the entrypoint symlinks ~/.codex/auth.json to the RO mount while codex's own writable state (config.toml, rules/, sessions/) lives in the image's own ~/.codex. roboco/llm/providers/codex_auth.py runs the orchestrator-side refresh loop (refresh_if_stale, mirroring grok_auth.py): the access token is a JWT whose exp claim is the only expiry signal (unlike grok's bundle there's no sibling expires_at field), and the refresh-token grant against auth.openai.com/oauth/token is single-use, guarded by the same process-wide lock + re-check-inside-the-lock pattern that protects grok's rotation from a concurrent double-burn. Tool scoping has no CLI-flag equivalent to grok's --disallowed-tools: a per-role --sandbox level (workspace-write for developer, read-only for every other role) plus one shared ~/.codex/rules/default.rules execpolicy file (Starlark prefix_rules denying git-mutation/destructive/raw-package-manager commands, rendered by roboco/llm/providers/codex_cli_config.py) do the job instead; the CLI has no verified system-prompt-file mechanism, so the composed role blueprint is prepended to the task prompt itself rather than mounted separately. Codex has no exit-code taxonomy (every failure exits 1), so codex_cli_sniff.py classifies a run's terminal state (rate_limit/auth/none) from ONLY the structured error.message field of its JSONL events plus stderr — never the model's own transcript, which could false-positive on ordinary on-topic prose (this repo's own prompts use the phrase "quota-limited"). Usage capture (codex_cli_usage.py) sums turn.completed events' real input/output/cache-read/cache-write split — a genuine four-bucket split, unlike grok's output-only fallback — into the grok-shaped usage.json.
Kimi runtime (V1: one-shot delivery roles only, no interactive Intake/Secretary). KIMI agents run Moonshot AI's official kimi (kimi-code) CLI (model alias pinned via ROBOCO_KIMI_CLI_MODEL, default kimi-code/k3 — aliases are namespaced under the login-managed kimi-code provider, kimi-code/kimi-for-coding is the cheaper cost lever) authenticated by a Kimi subscription (OAuth device-code login via kimi login), not a metered key. Auth is the one structural departure from codex/gemini: Moonshot's refresh token is rotation-with-short-reuse-grace, not truly reusable — two independent per-container copies of one credential snapshot eventually cross-invalidate each other's tokens (live-verified: a real login died and needed a fresh device-code approval). So the host ~/.kimi-code (ROBOCO_HOST_KIMI_DIR) is mounted read-write and SHARED — every container plus the orchestrator redeem the SAME rotating chain — while the entrypoint keeps a container-local writable ~/.kimi-code for config.toml/mcp.json/AGENTS.md (rendered fresh) and only symlinks credentials/ and oauth/ (the lock dir) in from the shared mount; the CLI's own cross-process lock (oauth/kimi-code.lock) serializes redemptions, so there is still no orchestrator refresh daemon — the CLI refreshes itself. kimi login's managed [providers."managed:kimi-code"]/[models."kimi-code/<alias>"]/[services.moonshot_*] config.toml blocks are account-fixed, not ours to discover per-container (the symlink step deliberately does NOT carry the host's own config.toml forward), so kimi_cli_config.py renders them as constants with the CLI's own model names rather than reading them off any mount. Tool scoping is the rendered [[permission.rules]] deny-first array (-p has no CLI-flag tool-removal equivalent); the same bash-guard-hook.sh the Claude/grok paths install is wired as a [[hooks]] entry, but a kimi hooks entry only tolerates event/matcher/command/timeout fields (an env key silently drops the WHOLE hooks section), so ROBOCO_GUARD_SKIP_GIT=1 rides a wrapper script's own export instead of a hook env block. Kimi has no exit-code taxonomy for -p either (a claimed 75/1 split is unverified noise), so kimi_cli_sniff.py classifies a run's terminal state from ONLY a structured error field off any JSONL event plus stderr — the model's own echoed assistant/tool content can never reach the classifier. Usage capture (kimi_cli_usage.py) sums wire.jsonl's real inputOther/output/inputCacheRead/inputCacheCreation 4-bucket split (session id read from the run's own terminal stdout event, falling back to the newest on-disk session dir) into the grok-shaped usage.json. The entrypoint maps a rate-limit/quota sniff to exit 75 and a missing/expired credential to exit 78, so the orchestrator parks the KIMI provider on either exactly like codex/gemini. Like the rest of the fleet's CLI runtimes (2026-07-28 policy), the roboco-agent-kimi image installs the CLI with no version pin — latest at build, always adapt — stamping the resolved version to /etc/kimi-cli-version for provenance.
Self-Healing & Feature Flags
Self-healing CI loop (default-off). RoboCo can watch its own repository's CI (a single named workflow) and, on a detected regression, open a fix task that is held out of dispatch until the CEO approves it (it terminates at awaiting_ceo_approval), then dispatch it through the normal delivery flow. It is dormant by default and armed by ROBOCO_SELF_HEAL_ENABLED plus a second opt-in ROBOCO_SELF_HEAL_ORIGINATE_ENABLED; origination is bounded by ROBOCO_SELF_HEAL_MAX_OPEN_TASKS / _MAX_PER_CYCLE so it can't flood the backlog. It never auto-merges or self-deploys (roboco/services/self_heal_engine.py).
Multi-repo CI-watch (default-off). The fan-out generalization of self-heal: instead of RoboCo's single own repo, it watches every project the operator opts into (projects.ci_watch_enabled, migration 048) and, on a red CI conclusion on that project's default branch, opens one fix task into that project's lifecycle that rides the normal delivery flow (+ PR-review gate) and never auto-merges. It reuses the exact hardened per-project GitService.get_latest_ci_conclusion (a missing signal is "unknown", never a false green; per-project errors are isolated and never abort the sweep), and is bounded + deduped per repo by git_url (a monorepo's cell-projects share one fix task) with per-cycle / rolling caps. Armed by ROBOCO_CI_WATCH_ENABLED (+ _INTERVAL_SECONDS / _MAX_OPEN_TASKS / _MAX_PER_CYCLE / _DEFAULT_WORKFLOW) and per-project ci_watch_enabled / ci_watch_workflow; MultiProjectCITelemetrySource (roboco/services/telemetry/source.py) + CiWatchEngine (roboco/services/ci_watch_engine.py) + a dedicated orchestrator _ci_watch_loop. The single-repo self-heal loop is untouched.
Dependency-update bot (default-off). A per-project engine mirroring the self-heal/CI-watch shape: weekly (default) it probes whether a dependency upgrade would change a project's lockfiles and, if so, opens one "update dependencies" task that rides the normal delivery flow (+ PR-review gate) and never auto-merges. Detection is read-only — WorkspaceService.dry_upgrade_changes_lockfile runs the project's dep_update_command (e.g. uv lock --upgrade) in a throwaway clone of the read clone and diffs the lockfile paths (dep_update_paths, or inferred uv.lock/pnpm-lock.yaml); the read clone is never mutated, nothing is committed/pushed, and a null/failing command originates nothing (fail-safe). A project participates only when projects.dep_update_command is set (migration 049); bounded + deduped per git_url with per-cycle/rolling caps. Armed by ROBOCO_DEP_UPDATE_ENABLED (+ _INTERVAL_SECONDS default 604800 / _MAX_OPEN_TASKS / _MAX_PER_CYCLE); DepUpdateEngine (roboco/services/dep_update_engine.py) + a dedicated _dep_update_loop.
Docs-divergence sync (default-off). Keeps the public docs site honest per release: when a release publishes and the docs site has drifted, DocsSyncEngine (roboco/services/docs_sync_engine.py) opens ONE docs-update task that rides the normal delivery flow (+ PR-review gate) and never auto-merges. Release-triggered, not polling; requires the docs-site repo (roboco-website) registered as a project with a git token; bounded + deduped like the other originate-one-task engines. Armed by ROBOCO_DOCS_SYNC_ENABLED.
Gated release manager (default-off). The autonomy that automates cutting a release up to the decision. A default-off background loop (ReleaseManagerEngine + _release_manager_loop) runs the deterministic readiness sweep (ReleaseReadinessService.assess, roboco/services/release_readiness.py) — diff-since-tag → conventional-commit classification → semver bump → version-reference completeness (the missed-ref guard) → CHANGELOG completeness → docs-drift (agent count) → migration single-head → gate state — and, past a threshold (ROBOCO_RELEASE_MIN_COMMITS, or any feat/security) with a green gate, originates ONE release proposal held for the CEO. The proposal is a source='release_manager' task owned by the Secretary, HELD (confirmed_by_human=False) and skipped by every dispatcher — acted on only by the CEO-gated routes, never delivered. The CEO approves or rejects-with-changes in the panel (release-proposal-card.tsx; GET/POST /api/release/proposal{,/approve,/reject}, CEO-only); approval runs the fail-closed ReleaseExecutor (roboco/services/release_executor.py): write the bumps across the canonical set (derived from the previous chore(release): commit) + the CHANGELOG entry, run make quality (abort before commit on red), commit chore(release): X.Y.Z (signed) + push, wait for green release-commit CI (abort before publish on red), then gh release create vX.Y.Z. Idempotent (an already-published version is a no-op) and never publishes without the CEO. Correctness is deterministic code, not agent judgment; the only generative step is the CHANGELOG prose, which the CEO reviews. Armed by ROBOCO_RELEASE_MANAGER_ENABLED (+ ROBOCO_RELEASE_MIN_COMMITS / _INTERVAL_SECONDS). Auto-deploy stays out of scope — publishing builds images; deploying to the NAS is the CEO's manual step.
Organizational memory loop (default-off). Closes the learn→reuse loop so agents stop cold-respawning blind. Three parts, all gated by ROBOCO_ORG_MEMORY_ENABLED: ① capture — at task completion TaskService._completion_learnings_for distills ONE high-signal lesson (Problem→Approach→Gotcha, ≤120 words) via the local model (MemoryDistiller, roboco/services/memory_distiller.py) instead of the noisy raw-notes/duration capture (flag-off keeps the legacy capture); journal indexing excludes is_private reflections from the shared corpus. ② retrieve (keystone) — on claim, _briefing_for injects context_briefing["institutional_memory"]: top-K (ROBOCO_ORG_MEMORY_TOP_K) relevance-floored (ROBOCO_ORG_MEMORY_MIN_SCORE) lessons + approved playbooks from a role-shaped query (EvidenceRepo.similar_memory over the LEARNINGS + PLAYBOOKS pgvector indexes); below the floor nothing is injected (no briefing bloat). ③ playbooks — a first-class curated procedure store: PlaybookTable (migration 050), the PLAYBOOKS OptimalService index, the draft_playbook content verb (delivery roles), Auditor approve_playbook/reject_playbook/archive_playbook curation (approval indexes it), and the panel review queue (playbook-review-queue.tsx; /api/playbooks Auditor/CEO routes). Distillation runs on the local model only — never a cloud LLM in the hot path; every step is best-effort (a failure never blocks completion or the briefing).
Sandboxed dev DB/Redis/Mongo (default-off). Per-project opt-in (projects.sandbox_services, migration 057); when armed (ROBOCO_SANDBOX_DB_ENABLED), provisioning is on-demand (2026-07-08), not eager at spawn: a developer or QA agent calls the request_sandbox do-verb (role-scoped to _DEV_DO/_QA_DO in role_config.py; services omitted means the project's whole opted-in set) and ContentActions.request_sandbox (roboco/services/gateway/content_actions.py) walks a guard chain — flag off; no active project-bound task; project not opted into any service; a requested service outside the opted set (remediate names the allowed set); orchestrator handle unavailable (the one retryable guard) — before calling AgentOrchestrator.ensure_sandbox, which always provisions the project's whole opted-in set regardless of the requested subset (so a later subset/superset request within that set is a guaranteed cache hit and can never trigger a mid-session teardown of a live container the agent is using), verifies a cache hit is still live before trusting it (evicting + re-provisioning on a dead container), serializes concurrent calls for one agent behind a per-slug asyncio.Lock, and caches the result in-memory per agent slug (_sandbox_info) — the verb filters the returned creds back down to what this call actually asked for. Sibling containers get random per-sandbox creds, tmpfs data dir, memory/cpu caps, labeled roboco.sandbox=1; creds return in the verb's envelope evidence (SandboxInfo.as_payload()), one entry per service including a ready-to-export env sub-dict (ROBOCO_TEST_DB_* / ROBOCO_TEST_REDIS_* / ROBOCO_TEST_MONGO_*) — never injected as container env, so no spawn-time creds delivery exists at all. Spawn itself only injects a cheap marker env ROBOCO_SANDBOX_SERVICES_AVAILABLE=<csv> (never creds) for an opted-in project, plus a briefing line naming request_sandbox() explicitly, in place of the legacy prod-creds gate-env injection (_append_gate_env, which points agents at RoboCo's own production Postgres under ROBOCO_TOOLCHAIN_MATCH_ENABLED) — sandbox replaces, never coexists with, prod creds. A provisioning failure now surfaces as a retryable envelope on the verb, never a spawn refusal — sandbox trouble can no longer block dispatch. A sandbox is torn down at end-of-engagement, not just at container removal: AgentOrchestrator.release_sandbox(agent_slug) is called (best-effort, never failing the verb; a fast cache-check no-op when nothing was ever requested) by the Choreographer on the SUCCESSFUL exit of i_am_done / unclaim / i_am_idle / pass_review / fail_review / i_documented, so a sidecar doesn't outlive the work that requested it. Lifetime still tracks the agent container 1:1 as the backstop: teardown at every removal path plus an orphan janitor at startup + each reaper tick (grace-windowed so a sweep can't reap a sandbox whose request is still mid-flight; the pre-spawn stale-clear likewise spares a just-requested sandbox) also evicts the _sandbox_info cache entry. Known ceiling: the cache is in-memory only — an orchestrator restart forgets live sandboxes, so the next request_sandbox call re-provisions (the pre-clear tears down any still-running stale container) and returns fresh creds. Docker-in-agent stays structurally absent throughout. The service set is a pluggable engine registry (roboco/models/sandbox.py): each engine declares its image, run args, readiness probe, and ROBOCO_TEST_* env; VALID_SANDBOX_SERVICES is derived from the registry, and the provisioner + the verb's payload builder iterate it, so adding an engine (e.g. mongo) is one class + one registry line — no branch edited in the provisioner or the env emitter. Extensions/modules on the fly (2026-07-13): a project may declare sandbox_extensions (migration 072, jsonb null) — a per-service extension/module map (e.g. {"postgres": ["vector", "postgis"], "redis": ["search"]}) activated post-ready via docker exec (CREATE EXTENSION IF NOT EXISTS / MODULE LOAD) then verified; request_sandbox(extensions=...) unions a per-call override with the project's standing set, bounded to the opted set + a fixed allowlist (SANDBOX_PG_EXTENSIONS = vector/postgis/pg_trgm/citext/uuid-ossp, SANDBOX_REDIS_MODULES = search/json/bloom — plpython3u excluded by construction; mongo has none). No default set — opters set extensions explicitly, existing opters stay bare. A bare request uses the light upstream image; features pull a kitchen-sink image (image_for(features)), so the pgvector+postgis intersection just works. Cache-by-features: a cached entry satisfies a new call iff services are a subset AND per-service requested features are a subset of cached features; a superset re-provisions (rotates creds). The evidence entry carries available_extensions. Set the full set in project settings so agents request subsets.
Cloud auth via FastAPI Users (default-off). Lets the panel/API be safely exposed beyond localhost without touching the CEO's local no-login flow while off. Gated by ROBOCO_CLOUD_AUTH_ENABLED (+ ROBOCO_CLOUD_AUTH_EMAIL / _PASSWORD / _SECRET / _COOKIE_MAX_AGE; Settings fails loud at startup if the flag is on with no secret). Off: get_agent_context (roboco/api/deps.py) and the WS _require_panel_token gate (roboco/api/websocket.py) are byte-for-byte unchanged (header-trust). On: header-trust is dead for humans — any agent-role claim (ceo OR a privileged main_pm/cell_pm/board role) with no valid HMAC token or session cookie is 401, closing the header-spoof hole on the host-published :8000 port for every role, not just ceo (real agents always carry a signed token, so they're unaffected); the agent-fleet HMAC path (and the orchestrator's system self-PATCH) keeps working unmodified in both modes; a valid session cookie authenticates as the single seeded CEO user. New users table (migration 058, UserTable in roboco/db/tables.py) backs FastAPI Users' SQLAlchemyUserDatabase; no registration router — roboco/api/auth/seed.py idempotently upserts exactly one row from env at startup (by primary key, so an email change renames the row instead of duplicating it). roboco/api/auth/backend.py wires a cookie transport (httponly, secure, samesite=lax) + a JWTStrategy subclass that binds each token to a fingerprint of the current hashed_password, so rotating the seeded password invalidates every prior session. Session lifetime is sliding: every authenticated request through get_agent_context re-mints + re-sets the cookie (_slide_session_cookie), so an active session never expires — only genuine inactivity past cloud_auth_cookie_max_age (default 30 days) logs out. GET /api/auth/status is always mounted (public); /api/auth/login + /api/auth/logout mount only when armed (roboco/api/auth/routes.py, mirroring apply_guard's conditional mount). A second route mints the identical cookie without a password: POST /api/telegram/webapp-auth (roboco/api/routes/telegram.py), mounted only when telegram_miniapp_enabled AND cloud_auth_enabled are both armed — see the Telegram bridge entry below. Panel: (auth)/login/page.tsx + proxy.ts (the Next 16 rename of middleware.ts; probes /auth/status over the docker-internal orchestrator URL, not through nginx, and fails open to "off" on any probe error/timeout) gate the (dashboard) group; client.ts adds withCredentials + a 401→/login redirect. nginx needs no changes (/api/auth/* rides the existing /api/ proxy location) — but its own static X-Agent-Token injection (ROBOCO_PANEL_AGENT_TOKEN) is itself a valid HMAC credential that bypasses login when present, so a deployment arming cloud auth for real public exposure should leave that token unset (the two are alternative human-auth tiers, not layered).
RoboCo X account (default-off). The Head-of-Marketing voice on X (Twitter): drafts a post when a release publishes, drafts replies to meaningful mentions, and — a third, independent capability — periodically investigates RoboCo's own shipped features and drafts a spotlight for an under-publicized one. NOTHING auto-posts across any of the three; every tweet is held in a panel queue for the CEO to edit/approve. Gated by ROBOCO_X_ENGINE_ENABLED (+ _MENTIONS_INTERVAL_SECONDS / _MENTIONS_MAX_PER_CYCLE / _MENTIONS_MIN_ENGAGEMENT / _MAX_OPEN_POSTS / X_ACCOUNT_USER_ID); inert without credentials regardless. Mirrors the ReleaseManagerEngine held-artifact shape: XEngine (roboco/services/x_engine.py) originates a held task (source x_post / x_reply / x_feature, confirmed_by_human=False, Secretary-owned, skipped by every dispatcher) whose marker payload carries a body clamped to 280 chars. Release posts hook ReleaseProposalService.approve's publish-success branch via a small draft_release_post seam; mentions ride a dedicated _x_mentions_poll_loop (no webhook infra exists) deduped by a x_seen_mentions ledger + per-cycle/open caps — both are local-model-drafted (never a cloud LLM in the hot path). The spotlight half is the one exception to "no agent spawn": gated by its own sub-switch ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED (+ _INTERVAL_SECONDS, default 3 days) on top of x_engine_enabled — now also the x_feature entry in the Board Program registry (see "Board Program registry" below), armed through the same program_armed chokepoint with these two flags as its legacy alias, migrated behavior-identical — _x_feature_spotlight_loop opens a held PENDING exploration task (source=x_feature_exploration, team=Board, assigned to Head of Marketing, carrying a x_seen_features dedup-ledger snapshot marker) that _dispatch_pm_work routes (mirroring ROADMAP_SOURCE) to a one-shot real cloud-LLM spawn of the Head of Marketing — full read tools, investigates CHANGELOG.md/feature-flags/docs/map/charter/KB, calls the Head-of-Marketing-only propose_feature_spotlight do-tool exactly once, which marks the feature slug seen (x_seen_features table, migration 061) and materializes a brand-new source=x_feature held draft (completing the exploration task as a side effect — a deliberate asymmetry from propose_roadmap, which instead leaves its own task open). The four OAuth 1.0a secrets live Fernet-encrypted in a singleton x_credentials row (migration 059, all-or-nothing set/clear, mirroring the git-token pattern; the API only ever returns has_credentials) — decryption is server-side only, agents never hold creds or egress. XPostService.approve (CEO-only route) is the ONLY caller of x_client.post_tweet: it posts under a Redis single-flight lock, re-reads the committed task state inside the lock and commits COMPLETED before releasing so a concurrent approve can't double-post, and is idempotent (an already-posted draft is a no-op). The hand-rolled OAuth 1.0a HMAC-SHA1 signer (roboco/services/x_client.py) adds no dependency; a NullXClient makes the unconfigured path a graceful no-op (research-engine posture). All three draft kinds share one voice: XEngine._voice_guide reads the CEO-editable company_goals.brand_voice charter field (migration 061, panel-editable in Business → Goals) and appends it to a generic baseline (_HOM_VOICE) — the baseline alone until the CEO supplies a real sample. Panel: x-post-queue.tsx (editable draft + 280 counter, approve/reject, a sourceMeta-driven label/icon per source including "Feature spotlight") + x-credentials-card.tsx (4 write-only secret inputs).
RoboCo video engine (default-off). Full subsystem doctrine: .claude/rules/video-engine.md (auto-loads when working under this subsystem's files).
Board Program registry (per-program default-off). Twelve new programs plus the two pre-existing origination cycles (roadmap/Printer, x_feature/spotlight — migrated onto the registry byte-for-byte, their own flags kept working as legacy aliases) all now ride one generic engine instead of bespoke per-engine loops. BoardProgram (roboco/foundation/policy/board_programs.py) is a frozen registry entry — key, role (the solo explorer), trigger (cron/metric/event), source (the exploration task's tasks.source marker), default_interval_seconds, max_items_per_cycle, scope (project/org) — for all fourteen entries in PROGRAMS. BoardProgramEngine (roboco/services/board_programs.py) runs the uniform lifecycle TRIGGER → EXPLORE → PROPOSE → DECIDE → MATERIALIZE → LEARN: the orchestrator's _board_program_loop ticks run_due_programs on a floor interval (the shortest registered cadence, clamped 300s-3600s), which for each enabled CRON program checks the board_program_cycles dedup ledger (migration 087 — one row per cycle, auto-closes when its exploration task goes terminal) and program_due, then originates via a per-program _ORIGINATORS callable (each program's own engine's run_cycle) and records the cycle row; open_program_cycle is the same path minus the cron-due check, used by the CEO's panel "run now" and by event/metric triggers. Arming has no master flag: program_armed (the single chokepoint every origination path routes through) reads a per-program settings-store row (board_program.{key}.enabled) falling back to the legacy boot flag only for roadmap/x_feature — every new program is settings-store-only, defaulting OFF until the CEO flips its toggle. Scope + dual polarity: projects.board_programs (migration 088, nullable jsonb) — a scope="project" program (it reads one repo: Pest Control, Spackle, Dogfood, Mirror) needs an affirmative per-project opt-in ("pest_control"; null/absent = out) before a cycle even opens (_scope_gate); a scope="org" program (it reads the org's process/market: Printer, Scales, Periscope, Megaphone, Barfly, War Room, Coroner, Librarian, Sentinel, x_feature) runs org-wide by default and is excluded per-project only by the opposite-polarity "!key" entry — one pure helper, project_participates, implements both. Panel: the project settings page's budget/ops card exposes both forms as checkboxes. LEARN: BoardProgramEngine.record_decision accrues each CEO approve/reject onto the cycle row's decisions jsonb; prior_cycle_context renders the last two closed cycles ("proposed N, approved N; rejected: X — reason") back into the next cycle's exploration prompt, closing the amnesia the old roadmap/spotlight engines had. Dispatchers skip every program source (_is_non_dev_dispatch_source / _dispatch_board_program_exploration's dict-dispatch table) exactly like board_roadmap before; every exploration is a solo one-shot spawn reusing the _board_dispatched tracker + respawn breaker, bypassing the two-reviewer board-review gate. Panel: the Board Programs page (Business section, board-programs-card.tsx → GET /api/board-programs, POST /api/board-programs/{key}/run-now, CEO-only) lists every entry's live enablement/cadence/last-cycle summary/opted-in projects with a toggle and a "run now" button.
The fourteen programs: Printer/roadmap (PO, weekly cron, org) — the pre-existing roadmap cycle, now LEARN-fed and Periscope-briefed; propose_roadmap → backlog tasks. Pest Control/pest_control (PO, weekly cron + a rework-rate-spike metric accelerator, project-scoped) — hunts latent bugs in the findings ledger/rework hotspots/ponytail: debt; propose_bug_hunt (evidence-required) → ≤5 backlog tasks. Spackle/spackle (PO, biweekly cron, project-scoped) — audits half-shipped surface area (routes with no panel, flags with no docs); propose_gap_fill → ≤5 backlog tasks. Scales/scales (PO, monthly cron, org) — reviews the live backlog against the charter; propose_rebalance → a held per-item plan whose approval MUTATES the live task in place (reprioritize or cancel), never creates one. Dogfood/dogfood (PO, event — release-publish hook or CEO run-now, project-scoped) — walks the product as a user; the one program whose spawn also mounts the Playwright MCP, task-scoped via AgentOrchestrator._is_dogfood_spawn (not a role-wide grant); propose_friction_fixes (walked-path evidence required) → ≤5 backlog tasks. Periscope/periscope (HoM, weekly cron, org) — market/competitor research with mandatory source_url citations; propose_market_brief → a held ceo_report, no task, and feeds forward into Printer's prompt. Megaphone/megaphone (HoM, 3-day cron, org) — the standing editorial calendar off shipped-task/CHANGELOG digests; propose_editorial_post → the existing X held-draft queue. Mirror/mirror (HoM, quarterly cron, project-scoped) — audits README/docs-site/website messaging against shipped reality; propose_messaging_fixes → ≤5 backlog docs tasks. Barfly/barfly (HoM, 2-day cron, org) — engages adjacent X conversations from a screened candidate list (injection_guard.screen_external_text, candidate-id-bound so a draft can't target an invented tweet); propose_conversation_replies → held X drafts, materialized as standalone link-posts (commentary + the conversation's /i/web/status/ URL, @handles stripped) — never threaded replies, since X's 2026-02-23 policy 403s programmatic replies into conversations that don't @mention the account on every non-Enterprise API tier (the same policy the mentions-poll x_reply drafts DO satisfy — they thread via in_reply_to_tweet_id; note a URL-bearing post costs $0.20 vs $0.015 on pay-per-use). Every post outcome (posted / post_failed) writes an x_post.* audit row at the XPostService._post chokepoint. War Room/war_room (HoM, event — release-publish hook WarRoomEngine.open_for_release or CEO run-now, org) — plans a 2-6 post campaign with strictly-ascending publish_after timestamps (guidance only, nothing auto-schedules); propose_campaign → held X drafts as one batch. Coroner/coroner (Auditor, event only — task bounced revision_count>=3, cancelled after work started, or budget-blocked, wired at TaskService's bounce/cancel chokepoints + the orchestrator's budget-block path, org) — one autopsy at a time, no cron; propose_postmortem (process_change.kind one of playbook/prompt_fix/conventions_rule/other) → a held process-change item, or drafts straight into the pending-playbook queue when kind='playbook'. Librarian/librarian (Auditor, biweekly cron, org) — mines journals/learnings for undrafted repeated patterns; propose_playbook_drafts (the Auditor's one exception to "curates but doesn't draft") → 1-3 real DRAFT playbooks via PlaybookService directly, landing in the same curation queue a later Auditor spawn reviews. Sentinel/sentinel (Auditor, weekly cron, org) — waiver/findings/conventions/budget drift watch; propose_quality_report → a held ceo_report, no task. Feature spotlight/x_feature (HoM — unchanged, see "RoboCo X account" below). Every artifact is HELD; the CEO is the only path to materialization — nothing auto-starts, auto-posts, or auto-merges.
Obsidian vault V1+V2 (default-off). Full subsystem doctrine: .claude/rules/obsidian-vault.md (auto-loads when working under this subsystem's files).
Fable-mode (default-off). Full opus-fable-playbook adoption: makes the fleet behave more like Fable 5 on the existing model tiers (the tiers stay — Fable 5 the model is not an option). Two levers, both gated by ROBOCO_FABLE_MODE_ENABLED: ① doctrine — fable_doctrine_layer() (roboco/agents/factories/_base.py) composes the vendored behavioral doctrine (agents/prompts/doctrine/fable.md, from github.com/rennf93/opus-fable-playbook MIT output-styles/fable.md, YAML frontmatter stripped) into compose_prompt's layer tuple immediately after base.md — universal cross-role doctrine, the same tier as the base rules, ahead of role/team/identity layers so those keep their specificity precedence. ② hooks — 5 vendored scripts under docker/scripts/fable-*.sh (stop-gate, bash-discipline, honesty-nudge, prompt-nudge, precompact; session-start.sh deliberately SKIPPED — its doctrine card is redundant with ① and its output-style check is inapplicable here) are installed alongside RoboCo's own hooks, never replacing them: AgentOrchestrator._fable_hook_groups() appends them AFTER RoboCo's own per-event entries in the Claude-path settings.json (isolated into its own helper to protect _generate_agent_settings's xenon budget); the grok path installs only honesty-nudge (write_grok_fable_hooks, roboco/llm/providers/grok_cli_config.py) — a deliberately conservative V1 scope, since a grok PreToolUse/Stop hook deny cancels the entire run (verified live) while PostToolUse never denies. Off by default: the spawn path (composed prompt, settings.json, grok hooks) is byte-for-byte unchanged when the flag is off. No new eval harness for Fable-specific measurement — that rides the existing rework/spawn-waste/revision_count dashboard (see "Delivery observability" below); the separate golden-task eval harness (roboco/eval/, see below) is an offline CLI bench for a (role, model/provider) cohort, unrelated to Fable-mode's own on/off measurement. Armed on the NAS deploy like the rest; left OFF in docker-compose.registry.yml.
Ponytail (bundled with Fable-mode). Rides ROBOCO_FABLE_MODE_ENABLED — no separate flag. Vendors the ponytail "lazy senior dev" build-laziness doctrine (agents/prompts/doctrine/ponytail.md + ethos sibling, MIT, Copyright (c) 2026 DietrichGebert — trimmed, YAML frontmatter stripped) into every composed system prompt via ponytail_doctrine_layer (roboco/agents/factories/_base.py), slotted immediately after the Fable doctrine layer and gated on the same flag. Role-scoped: developers (AgentRole.DEVELOPER) get the full ladder (YAGNI → reuse-in-this-codebase → stdlib → native-platform → installed-dep → one-line → minimal); every other role gets the ethos-only cut (ponytail-ethos.md) — the code-mechanics rungs are dropped so they can't leak into prose artifacts (task plans, review notes, docs). Both files carry a 5-point RoboCo preamble (the ethos sibling adds a 6th: free-text field obligations) that makes the ladder yield to the Architectural Conventions Standard (placement), the 80% coverage gate + QA review + self-verification, the per-team design bar, task hygiene (everything-is-a-task / commits-linked / state-is-sacred), and reviewer feedback (needs_revision / pr_fail / request_changes) — the overlap mitigation is scoping, not deletion, and it rides ponytail's own "when NOT to be lazy" clause. Developer intensity is tunable via ROBOCO_PONYTAIL_INTENSITY (lite / full / ultra, default full; roboco/config.py ponytail_intensity, a string value — not a feature flag): full enforces the ladder, lite builds what's asked and names the lazier alternative, ultra is YAGNI-extremist (deletion before addition, challenge the requirement). Non-developers get no dial — ultra is wrong for prose artifacts, so the ethos runs a fixed restrained stance. Prompt-only: no hooks, no grok-path changes — ponytail adds no hook surface, so bundling it under the Fable flag changes only the composed prompt, not the spawn hooks. The Fable flag's description in roboco/config.py names both doctrines.
Golden-task eval harness (source-checkout-only offline CLI). roboco/eval/ replays a fixed set of BenchTaskSpec fixtures (roboco/eval/fixtures.py) through the REAL delivery lifecycle in a disposable environment reused from tests/e2e_smoke/harness.py (fake GitHub REST, a real local git origin, a throwaway DB) — real isolation, not a mock. EvalRunner.run_cohort (roboco/eval/runner.py) scores each fixture on deterministic metrics (final status, revision_count, cycle time, tokens/cost via the agent_spawn_sessions task_id join) plus a local-model judge comparing the final PR diff + notes against the fixture's checked-in expectations, nested under a "non_deterministic": true-marked "judge" object so a naive cohort diff never mistakes judge noise for a real regression. agent_spawn_sessions.doctrine_version (migration 081) is stamped at spawn-session finalize from the composed prompt layers, so a cohort's model + doctrine combination (e.g. Fable-mode on vs. off) is durably identifiable after the fact. Real-spawn is wired: OrchestratorStageSpawner drives a real AgentOrchestrator.spawn_agent per turn (constructed the same way the production dispatcher builds it), and _generate_mcp_config honors the patched settings.api_url (set to the harness's disposable stack URL in _bench_environment) so a spawned container's MCP servers resolve to the throwaway orchestrator, never the REAL production one — even though _seed_company seeds agents under their REAL production UUIDs (correct: orchestrator-internal helpers keyed by the static registry resolve exactly as in a real deployment; the isolation is about the URL, not the UUID). python -m roboco.eval run works end-to-end for a developer-role cohort; it needs a Docker daemon + built agent images for the real spawn path. The injectable scripted StageSpawner (see tests/e2e_smoke/test_eval_bench.py) remains the unit-test fallback that proves the runner's polling/scoring/DB plumbing without touching Docker; tests/unit/runtime/test_eval_mcp_config_isolation.py pins the no-production-reach guarantee without a Docker daemon. Scoped to developer-role fixtures only (run_cohort refuses any other role) and only runs from a source checkout (tests/e2e_smoke isn't shipped in containers or wheels); bench runs also patch every vault flag off so a bench task/note/journal write never lands in the operator's real Obsidian vault.
Env-branches ladder + EnvSyncEngine (default-off ROBOCO_ENV_SYNC_ENABLED). Replaces a project's single default_branch with an ordered environment ladder: nullable projects.environments JSONB (migration 073), an ordered list[{name, branch}] where index 0 is the head rung (where dev/cell/leaf PRs land) and index -1 is the prod rung (where the gated release executor commits + tags); middle rungs are intermediates (qa/stag). A null ladder degenerates to a single-branch ladder synthesized from default_branch at read time (roboco/models/env_branches.py: head_branch / prod_branch / ladder_pairs / promotion_chain) — no backfill, byte-for-byte legacy behavior until the CEO declares a real split. Every former default_branch consumer now routes through the shim: the PR target and per-agent clone (WorkspaceService.ensure_workspace / ensure_read_clone), the CI branch, the release executor's clone/commit/tag target (_ReleaseContext.prod_branch) plus its full-chain head→…→prod promotion before bumping (promote_env_chain, fail-closed promotion_failed on a merge conflict), and release_readiness's diff baseline (prod..head instead of last_tag..HEAD) with a tag-drift cross-check (_tag_drift_gaps — the last tag's commit vs. prod tip disagreeing flags a hotfix that landed on prod after the tag). EnvSyncEngine (roboco/services/env_sync_engine.py) cascades the ladder prod→…→head via GitHub's merges API: a clean merge auto-pushes straight to the lower rung, a conflict opens ONE idempotent sync PR + a Main-PM coordination task and stops that project's cascade for the cycle — the cascade's target is never the prod rung by construction, so "only the CEO merges master" still holds. Bounded + deduped per repo (one open env_sync task at a time). Panel: an environment-ladder editor on the project edit dialog.
Telegram notifications bridge V1+V2+V3 (default-off ROBOCO_TELEGRAM_ENABLED). Full subsystem doctrine: .claude/rules/telegram-bridge.md (auto-loads when working under this subsystem's files).
Possibilities matrix (default-off ROBOCO_POSSIBILITIES_MATRIX_ENABLED). A work-already-done fast path on i_am_done: when a claimed/in_progress task already has commits, an open PR, every acceptance criterion addressed, and no open findings (_work_appears_done), the dev submits straight to QA in one call instead of the standard multi-turn plan/journal/local-gate derivation. _i_am_done_fast_path still runs the non-negotiable guards — ownership, branch-pushed, not-behind-base, conventions, FINDINGS_ADDRESSED — and trusts the PR's own CI-green signal as the quality-gate proxy (_fast_path_quality_verdict, the same signal pr_pass trusts); a repo with no CI signal falls back to the local make quality gate (plus the toolchain-match guard when ROBOCO_TOOLCHAIN_MATCH_ENABLED is armed), and a known-red CI refuses the fast path outright rather than shipping it to QA. The orchestrator's dev spawn prompt steers a matching task to a WORK_ALREADY_DONE state that tells the dev to call i_am_done directly instead of re-deriving what's already done.
Reviewer/PM collision map (always-on). The collision surface authored at delegate time (intends_to_touch / adds_migration / touches_shared) used to be consumed once by SequencingService to wire dependency edges and never shown to a reviewer again. build_collision_context (roboco/services/gateway/choreographer/collision.py, pure — no DB/IO) now surfaces it for a task under review: same-parent siblings that would collide — overlapping declared file globs, or both adding a migration (the Alembic-head collision needs no file overlap) — rendered with the overlapping globs and, where the caller hands real touched files, a declared-vs-actual drift flag. Capped at 10 siblings / 5 globs. The same builder feeds QA's claim_review evidence, the PR-gate's claim_gate_review evidence, the PM's i_will_plan planning briefing (no drift there — no work yet), and the panel's GET /api/tasks/{id}/collision-map route backing a Collision tab on the task detail page.
Delegation detail-fidelity (always-on, 2026-07-16). Details no longer thin out at hand-off in either direction. DOWN: delegate refuses any child that doesn't declare covers_parent_criteria mapping onto the parent's real acceptance criteria (matched by id or exact text; an unresolvable ref is rejected naming the valid criteria, never silently dropped — previously the mapping was optional and coverage surfaced only at submit_up's roll-up gate, after the whole wave had already run); the success envelope carries parent_ac_coverage {covered, uncovered} so a wave-planning PM sees remaining gaps in the same turn, while multi-wave planning stays legal. UP: pass_review requires criteria_verified — one {criterion, evidence} entry per task acceptance criterion (the findings ledger's id-or-exact-text matcher, soup-checked and length-capped evidence), rejecting with the unverified criteria named; entries render deterministically into qa_notes as [AC] <criterion> — verified: <evidence> lines, so a gestalt "looks good" pass is structurally impossible. Video briefs stopped being prose-only: an enumerable feature list (release highlights, or input_props.highlights carried onto a reject re-author) becomes its own acceptance criterion ("Every brief-named feature appears as its own fully readable scene: …", bounded to the AC caps; a re-author without highlights carries "every point in the CEO rejection feedback is visibly addressed"), so the dropped-scene class — a four-feature brief shipping three scenes past every gate — is caught by the QA per-AC stamp instead of the CEO's eyeball.
Task/project cost budgets (default-off ROBOCO_TASK_BUDGETS_ENABLED). tasks.budget_usd + projects.monthly_budget_usd (migration 080). Claim-time: a project-month-spend guard (project_budget_exceeded_guard, roboco/services/gateway/claim_guards.py) applies only to WORK-STARTING claims (i_will_work_on / i_will_plan) — review/doc/gate/inbound-PR claims are exempt so in-flight work can always finish reviewing and merging even at cap; spend counts closed sessions' estimated_cost_usd plus open sessions priced live from token snapshots. Sweep-side: the orchestrator's existing budget sweep also prices the active task's own spend against budget_usd (explicit-input only — a null budget means no cap, resolved by effective_task_budget_usd in roboco/foundation/policy/agent_loop.py; the earlier per-TaskType default table blocked an unbudgeted coordination root one opus turn in and was removed); on breach the task is BLOCKED (HUMAN resolver, budget marker) BEFORE the graceful stop so the unclaim no-ops and the dispatcher never respawns onto it, and the CEO notification names both recovery steps — unblock on a budget-blocked task re-checks live spend and refuses while still over, so there's no silent re-breach loop. Off => neither cap is ever consulted regardless of field values. Panel: budget inputs on both the project and task dialogs (a 0 is rejected — it would silently block everything); spend math is consolidated in TaskService.task_spend_usd.
PR labeler (always-on). derive_pr_labels (roboco/foundation/policy/pr_labels.py, pure) derives the org-structure label vocabulary every fleet PR now carries: to {base_branch} — the PR's REAL resolved target branch, verbatim (never assumed from is_root_pr, so a project with a renamed/non-standard trunk or an env-ladder rung gets an accurate label instead of a hardcoded master/slave), root for an assembled root PR, MegaTask for a batch-carrying task, and a layer label (main-pm for a Main-PM coordination root, cell/{team} for a cell-assembled PR, else subtask/{team} for a leaf dev PR). Applied best-effort at all three PR-opening sites in GitService so a human triaging the PR queue sees which tree and which org layer a PR belongs to at a glance.
Feature flags / company-in-a-box. Env-gated, default-off subsystems toggle from the panel's Settings → Feature Flags card (panel/src/components/settings/feature-flags-card.tsx) instead of hand-editing env: web research (ROBOCO_RESEARCH_ENABLED), the strategy engine (ROBOCO_STRATEGY_ENGINE_ENABLED), pitch provisioning (ROBOCO_PROVISIONING_*), external / internal PR review, the agent-runtime toolchain match (ROBOCO_TOOLCHAIN_MATCH_ENABLED), the architectural-conventions standard (ROBOCO_CONVENTIONS_ENABLED), gateway-health recovery (ROBOCO_GATEWAY_HEALTH_ENABLED), multi-repo CI-watch (ROBOCO_CI_WATCH_ENABLED), the dependency-update bot (ROBOCO_DEP_UPDATE_ENABLED), the gated release manager (ROBOCO_RELEASE_MANAGER_ENABLED), the organizational memory loop (ROBOCO_ORG_MEMORY_ENABLED), the sandboxed dev DB/Redis (ROBOCO_SANDBOX_DB_ENABLED), the RoboCo X account (ROBOCO_X_ENGINE_ENABLED), the RoboCo video engine (ROBOCO_VIDEO_ENGINE_ENABLED), the legacy board roadmap/spotlight flags (ROBOCO_ROADMAP_ENGINE_ENABLED, ROBOCO_X_FEATURE_SPOTLIGHT_ENABLED) — the other twelve Board Programs arm per-program on the dedicated Board Programs page (Business section) instead, not this card, since each toggles its own board_program.{key}.enabled settings-store row rather than an env-backed flag — Fable-mode (ROBOCO_FABLE_MODE_ENABLED), the vault weekly report + KB ingest (ROBOCO_VAULT_REPORT_ENABLED / ROBOCO_VAULT_KB_ENABLED), the env-sync cascade (ROBOCO_ENV_SYNC_ENABLED), the Telegram notifications bridge (ROBOCO_TELEGRAM_ENABLED, + inbound commands/actionable buttons sub-switch ROBOCO_TELEGRAM_INBOUND_ENABLED), the possibilities matrix (ROBOCO_POSSIBILITIES_MATRIX_ENABLED), the docs-divergence sync (ROBOCO_DOCS_SYNC_ENABLED), task/project cost budgets (ROBOCO_TASK_BUDGETS_ENABLED), and the self-heal flags above. Cloud auth (ROBOCO_CLOUD_AUTH_ENABLED) is deliberately NOT on this card — like ROBOCO_DB_NETWORK_ISOLATED, it's a compose/env-coupled flag a runtime toggle can't safely flip mid-session. A toggle persists in the settings store and takes effect on the next backend restart; an unset flag falls back to its environment / config default.
Architectural Conventions Standard
Per-project architectural standard (default-off). Beyond the make-style gates (which check syntax/types/tests, not where code lives), each project can carry a repo-canonical .roboco/conventions.yml — an architecture map (which definition kinds belong in which modules), a toggleable rule set, custom regex rules, and waivers — so an agent cannot land a Pydantic model defined inside a router or a # noqa / # type: ignore. Placement of a helper (any top-level function) only warns — too blunt to hard-block; thin_routes doesn't count an explicit db.commit(); and a small allowlist of unavoidable framework suppressions (ruff TC001–TC003, pydantic prop-decorator) is exempt. Gated by ROBOCO_CONVENTIONS_ENABLED; fully inert when off. RoboCo itself ships a canonical .roboco/conventions.yml.
Effective map. Consumers read the effective map — auto-derived defaults (from a repo scan + BUILTIN_RULES, excluding tests//docs/ trees) overlaid by the committed file — so behaviour is identical whether the file is present, absent, or partial. ConventionsService (roboco/services/conventions.py) builds it, caches it per (project, HEAD sha) in project_conventions_cache (migration 043), renders the per-task baseline constraints + the ambient prompt block, and scaffolds/restores the file via a PR (GitService.open_conventions_pr). The committed file + scan are read from a dedicated project-level read clone the service ensures on demand (WorkspaceService.ensure_read_clone, pinned to the default branch's HEAD) — the backfill that makes the standard resolve even for a project created before it existed, with no manual workspace_path. The schema lives in roboco/foundation/policy/conventions/ (pure).
Validator. A single Python CLI, python -m roboco.conventions check --root <repo> --files <a> <b> ... (roboco/conventions/), uses tree-sitter (Python + TypeScript grammars, shipped in the agent image) to classify each changed definition and flag forbidden placements + hygiene + custom-rule matches as JSONL findings, after waiver filtering. Precision over recall (it abstains when uncertain so a block gate can't false-positive-strand a task) and fail-loud (a validator that cannot run exits 3 so the gate blocks, never silently passes).
Threading + enforcement. The standard reaches the work two ways: an ambient "Architectural Standard" block injected at spawn (compose_prompt) and an auto-attached ## Constraints section on every project task (TaskService.create). Enforcement is deterministic: a block-level finding refuses i_am_done (dev pre-submit) and pr_pass (the in-path PR gate) with the offending file:line + fix hint; findings also surface in QA's claim_review evidence (convention_findings). A false positive is relieved by a waiver the dev commits in their branch — accountable, reviewed in the PR. The panel's per-project Conventions tab (a page-level tab on /projects/[id]/settings, Wave C — was a tab inside the edit-project dialog) shows the map + health and offers Save / Restore.
Design Bar
FE/UX-UI design bar (prompt-only, always on). Frontend and UX/UI team agents carry a design-taste bar distilled from Leonxlnx/taste-skill (MIT) in their team prompts, so agent-authored UI stops defaulting to generic-AI layout/fonts/motion. It's a ## Design bar section appended to agents/prompts/teams/frontend.md and agents/prompts/teams/ux_ui.md, reached by every cell role on those teams (dev/QA/PM/Documenter) via the team prompt layer, plus a pointer in the shared agents/prompts/roles/developer.md so fe-dev/ux-dev know to look for it without leaking the content into be-dev's prompt. It covers three tuning dials — DESIGN_VARIANCE / MOTION_INTENSITY / VISUAL_DENSITY (1-10 each; dense product UI like the panel defaults to 2-3 / 2-3 / 7-8) — plus typography/hierarchy, spacing/layout, motion, and "AI tells to avoid" rules, scoped to respect a project's existing design system (fonts, colors, radius) rather than silently override it. Prompt-only: compose_prompt itself is unchanged, no new verb/gate/state; guarded by tests/unit/agents/test_design_bar_layer.py.
Niche aesthetics + Image direction (the deferred taste-skill half, prompt-only, always on). Two more Leonxlnx/taste-skill (MIT) distillations on top of the core Design bar. A ## Niche aesthetic vocabularies section (identical body in both frontend.md and ux_ui.md) names three opt-in visual systems — industrial brutalist, minimalist editorial, premium agency — each keyed to the same three dials (a vocabulary changes what the dials produce, not whether they apply); picked only when a task brief explicitly calls for one, never a default. A ## Image direction section lives in ux_ui.md only (that team owns visual-asset/motion-composition work): composition variety, palette discipline, anti-slop imagery, iconography, mockup/device-frame conventions, and cross-asset set consistency, distilled from taste-skill's imagegen skills; frontend.md carries a one-line pointer to it instead of duplicating the content. Guarded by the same tests/unit/agents/test_design_bar_layer.py.
MegaTask (sequenced batch intake)
MegaTask lets the CEO describe several tasks in one Intake chat and ship them as one collision-aware, sequenced batch — even across projects that don't share a codebase (the motivating case: a SaaS app + its OSS core engine + a framework adapter). It is a core capability, not a feature flag (additive + opt-in by nature: proposed only when the CEO asks for several tasks; single-task intake is byte-for-byte unchanged), branded "MegaTask" on every user-facing surface while internal names stay technical (batch_id, SequencingService).
The umbrella model. A MegaTask's identity is a real umbrella task — branchless, no PR of its own — over N root-subtasks, each a real Main-PM coordination root with its own project_id, branch, and PR. Hierarchy: Umbrella (Main PM) → N Root-subtasks (Main PM) → Cell tasks (cell PMs) → Dev subtasks. One extra Main-PM layer on top of the normal model. The umbrella is the single board-review / CEO-approve / Main-PM-coordinate unit, so the batch plugs into the existing coordination-root flow for free (task tree, progress rollup, CEO queue).
Identity predicate (single source of truth). roboco/foundation/policy/batch.py: is_batch_umbrella (batch_id set AND parent_task_id None), is_batch_root_subtask (batch_id set AND parented), is_branchless_coordination ((no-project AND product) OR umbrella). Every git-exemption site consults it so the umbrella's exemptions can't drift: the orchestrator's _is_coordination_task, the claim→in_progress branch gate (GitContext.is_coordination), _ensure_branch_for_task (returns "" for an umbrella), and the CEO-reject routing. submit_root hard-rejects an umbrella (it assembles no PR); umbrella completion reuses the existing branchless path (all_subtasks_terminal, PR waived → escalate to CEO).
Sequencing. The pure SequencingService.analyze(surfaces, cell_of, cell_capacity) (roboco/services/sequencing.py; schema in roboco/foundation/policy/sequencing/) turns each draft's collision surface — intends_to_touch (globs), adds_migration, touches_shared — into a dependency DAG + Kahn-layered waves: file-overlap serializes (more-important first by (priority, idx)), migration-adders chain serially, a shared-surface edit runs after each non-shared task it overlaps (file-overlap-conditioned), independent tasks run in parallel; cell-contention only warns. Correctness lives in code, not agent judgment. The columns tasks.batch_id + intends_to_touch / adds_migration / touches_shared are migration 046.
Intake + create path. The intake chat can be scoped to a MegaTask (a multi-project picker → StartLiveRequest.project_ids); the orchestrator clones each repo (_clone_intake_scope / _slugs_for_project_ids, the multi-repo machinery products already used). The intake agent proposes the whole batch with one propose_batch tool call — wired on both runtimes (the Claude SDK driver emits one batch stream chunk; the grok intake_server POSTs a batch relay event). The panel's third intake scope accumulates it into a Review-MegaTask card → POST /prompter/live/{session}/confirm-batch. PrompterService.confirm_live_batch builds the umbrella + N root-subtasks (via create_task_from_draft + a BatchPlacement) and wires the analyzer edges through add_dependency. The Board route holds the root-subtasks in BACKLOG until approve_and_start releases them (_activate_batch_root_subtasks); the Main-PM route dispatches wave 0 at once. The Product Owner + Head of Marketing review the whole batch (their identity prompts carry a MegaTask section).
Board-review → redraft loop (batch parity). A first board-route batch confirm PARKS the intake session against the umbrella instead of reaping it — the same keep-alive loop single drafts get. When both board reviewers finish, the orchestrator injects a batch-aware brief into the still-live chat (_compose_parked_intake_redraft → compose_batch_redraft_message: every live root-subtask's snapshot + the board's decision notes + an explicit one-propose_batch-call re-proposal instruction); the revised batch re-confirms with BatchConfirmRequest.task_id set, which routes to PrompterService.update_live_batch — an in-place update, not a new batch: umbrella prose re-composed, live root-subtasks positionally patched (cancel+recreate only on a per-item scope change; create/cancel on count changes), dependency edges rewired to the fresh wave plan, and the create path's _validate_batch_scope gate re-applied so a redraft can't collapse the batch to one project or drift outside the scoped repos. Every reader uses the CANCELLED-excluding get_live_subtasks view, so multi-round redrafts survive earlier cancels. The cold fallback (POST /prompter/live/re-interview/{task_id}, the task-detail "Re-draft with board feedback" button) now handles the branchless umbrella by recovering its multi-repo scope from the live children (TaskService.distinct_projects_for_batch) and returning project_ids so the panel re-enters batch mode. On the re-confirm, route="main_pm" approves-and-starts (releasing the BACKLOG children) and route="board" clears board_review_complete for another review round; a redraft re-confirm always reaps (parity with single drafts — later rounds ride the cold path). Panel side: confirmBatch's board branch keeps the chat open and threads batchRedraftTaskIdRef (persisted with the chat) into the next confirm.
Services
Core services in roboco/services/:
| Service | Purpose |
|---|---|
TaskService |
Task CRUD and state transitions |
WorkSessionService |
Git session management, PR lifecycle |
WorkspaceService |
Multi-agent workspace resolution and cloning |
ProjectService |
Project/repository management |
NotificationService |
Formal notifications |
JournalService |
Agent journals and entries |
OptimalService |
RAG queries (in-house pgvector engine) |
PermissionsService |
Role-based access control |
Configuration
Key settings in roboco/config.py (env prefix: ROBOCO_):
Docker Deployment
Container Architecture
The system runs as Docker Compose services. All Dockerfiles live under docker/ at the project root; every service uses context: . plus dockerfile: docker/<name>.Dockerfile.
Quickstart (registry pull-and-run)
make quickstart runs scripts/bootstrap.sh: idempotent one-command bring-up for the pull-and-run deploy (docker-compose.registry.yml). A fresh .env is copied from .env.example and the three required secrets are injected using the documented one-liners (the panel token via the exact HMAC formula issue_panel_token uses), with a standing-credential warning (louder if cloud auth is detected) — compose's :? guard refuses an empty token unconditionally. A reused .env is never touched, but the three required vars are pre-validated with pointed remedies instead of compose's opaque interpolation error. It then pulls + up -d + runs a doctor-style readiness sweep grounded in real surfaces (root /health, /api/auth/status through nginx, the verbatim "Alembic upgrade finished" log line, ollama list), each stage failing loud with the exact command to run next. .github/workflows/release.yml's pull-smoke job (fresh runner, own GHCR login, needs publish-images) literally pulls the registry compose against the just-published tag on every release — guarding the missing-image regression class that already happened once.
Single Entry Point
nginx is the only externally-exposed service. It listens on localhost:3000 and routes:
/api/*and/ws/*→orchestrator:8000- everything else →
panel:3000
This avoids CORS since the browser sees one origin. The Next.js code uses relative URLs (/api, /ws) and lets nginx do the dispatch.
Network topology (DB isolation)
Two user-defined bridges: roboco_default (the agent mesh — panel, nginx, ollama, every spawned agent container, and their sandbox DB/Redis sidecars) and roboco_data (postgres + redis ONLY). The orchestrator is the only multi-homed service (both networks), so agent containers cannot resolve or TCP-reach roboco-postgres:5432 / roboco-redis:6379 at all — network membership is the containment (redis has no auth). Agent↔agent A2A (:9000), orchestrator→agent SDK polls (:9000), MCP→orchestrator (:8000), and host-published ports (15432/16379/11435) are unaffected; docker exec/inspect paths ride the daemon socket, not the network. ROBOCO_DB_NETWORK_ISOLATED (config default false) is set true by the compose files that carry this topology and suppresses the legacy _append_gate_env prod-creds injection (unreachable creds are worse than none); DB-needing projects use the sandbox opt-in instead. The flag is deliberately NOT in the panel feature-flags card — it must travel with the compose networks: stanzas.
WebSocket streams
The orchestrator exposes WebSocket endpoints under /ws (router in roboco/api/websocket.py, ConnectionManager + broadcast_* helpers):
| Endpoint | Purpose |
|---|---|
/ws/agents/{id}, /ws/notifications/{id} |
Per-resource live streams |
/ws/system |
Operator/system-wide stream (no per-agent keying) — the rate-limit lifecycle (RATE_LIMIT_HIT / RATE_LIMIT_LIFTED), live usage (USAGE_SNAPSHOT, pushed to the usage dashboard), and A2A message events (a2a.message frames) |
Server-side events reach these sockets through roboco/api/websocket_bridge.py, which subscribes to the StreamEventBus and forwards each event to the matching connections. To add a new live event: define an EventType (dotted value), publish it to the bus, add a _handle_* forwarder in websocket_bridge, and consume it on the panel via the useWebSocket("/<endpoint>", …) hook — do not stand up a parallel endpoint or client stack. A2A_MESSAGE_SENT is the worked example: A2AService.send publishes it (excerpt-capped payload), the bridge forwards it to /ws/system as an a2a.message frame, and the panel's useA2ALiveStream hook (a second consumer of that same shared /ws/system connection) consumes it to invalidate-on-frame.
Rate limiting & usage
- Provider rate limits are tracked in Redis (
RateLimitStateTracker,roboco/services/gateway/). On a provider 429 an agent callsi_am_blocked(reason="rate_limited"); the spawn gate then queues (never drops) further work for that provider, and a background probe-and-resume loop in the orchestrator clears the limit and revives parked agents when it lifts. - Provider overloads reuse the same park-and-probe break. A persistent model-API overload (HTTP 529 / 500 / 503 — the SDK already retries transient ones) parks the provider exactly like a 429 instead of crash-retrying the agent straight back into the overload and burning tokens; the overload is detected orchestrator-side from the dead container's log markers, and the background loop revives the parked work when it recovers. The same break also catches the Claude session-limit 429 (the org's 5-hour usage window): an agent exiting with a 0-token session-limit rejection parks the provider and is auto-revived when the window resets, instead of fleet-wide crash-respawning straight back into the limit. Gated by
ROBOCO_OVERLOAD_BREAK_ENABLED(default-on). - Gateway-health recovery closes a blind spot in the stale-claim reaper: the heartbeat is bumped only by gateway verbs, so a broken-but-alive agent (a corrupted
/app/.venvso no gateway tool imports) goes heartbeat-stale yet keeps its container up, and the reaper's live-skip would protect it forever. On a stale-heartbeat live container the reaper now probes the gateway out-of-band (_probe_gateway_health→docker execthe gateway venv imports) and, once broken pastROBOCO_GATEWAY_HEALTH_GRACE_SECONDS(a transient probe miss is tolerated), kills + evicts it (_maybe_recover_broken_gateway) so it falls through to release + respawn; healthy or inconclusive probes spare it. Gated byROBOCO_GATEWAY_HEALTH_ENABLED(default-on). It is the third leg beside the shipped bash-guard/appblock (prevents the self-corruption) and the reaper Docker-liveness fallback (stops over-reaping live containers). The bash-guard hook also denies rawuv run/uv pip/uv lock/add/remove,pip/pip3 install/uninstall,conda install/create/run, andpoetry run/install/addwhenever a Makefile is present in the workspace, remediating tomake quality/gate/lint/testinstead (a Makefile-less project is unaffected); the grok path mirrors this with a native--denyrule set (_RAW_PM_DENY) that nudges the model back tomakewithout canceling the run. - Tool-call budget. A per-session counter (
docker/scripts/post-tool-budget-hook.sh→ the in-container SDK server) warns an agent at 100 tool calls and halts (auto-substitutes, releasing the task) at 300 (BudgetPolicy.tool_call_warn_at/_halt_at,roboco/foundation/policy/agent_loop.py; env overridesROBOCO_AGENT_TOOL_CALL_WARN/_HALT). Raised from a 150 hard cap, which repeatedly halted legitimate multi-file work mid-task. A same-window loop detector (same tool + args repeated pastROBOCO_AGENT_LOOP_THRESHOLD) is a separate, tighter check that can deny the repeating call before the budget cap is ever reached. - PM coordinator concurrency. A Main / Cell PM plans and delegates many root tasks in parallel — the actual work then runs in the delegated children/cells, not in the PM's own hands. The claim-time concurrency guards that keep a developer to one task at a time (
already_active/paused, inroboco/services/gateway/claim_guards.py) are therefore skipped for the coordinator PM roles (_COORDINATOR_ROLES = {main_pm, cell_pm}, consulted in_run_claim_guards); only a genuine upstream sequence dependency (unmet_dependency, which parks the task back topending) holds a PM's root back. Without this a single PM that claimed one root could never plan a second — it thrashed between its claimed roots and respawned forever, burning tokens for zero progress (the livei_am_idle-auto-paused-umbrella deadlock). Thepausedguard also excludes the target task itself, so a PM re-entering its own paused umbrella never self-blocks. - Orchestrator runtime-state durability. The PM-respawn loop breaker (
_pm_respawn_tracker, the(agent_slug, task_id) → strike-countcircuit breaker) is DB-durable via therespawn_trackertable (migration 051): each gate mutation write-throughs fire-and-forget on the_bg_tasksset (_schedule_respawn_persist→_persist_respawn_record), andrestore_respawn_tracker()repopulates it atstart(), validating each row against live tasks (terminal/missing rows are evicted). Kept only in memory it reset tocount=1on every restart and re-burned the whole strike threshold (4 spawns) against a still-wedged task. It mirrors theWaitingRecordTable/restore_waiting_recordspattern: best-effort (a DB hiccup degrades to in-memory-only — it can only ever suppress a spawn, never manufacture one) and inert when the table is empty. The companion_instancesregistry is reconciled-from-Docker (not persisted) at startup via_readopt_running_agents, so the reaper's liveness path and the spawn gate's_is_agent_activecheck see surviving containers immediately after a restart. - Token usage is captured per agent session from the Claude Code transcript via the SDK server's
/usage/sync(hook → orchestrator finalize →agent_spawn_sessions→daily_usage_rollups→ dashboard). Cost uses provider-aware pricing inroboco/billing/pricing.py(Anthropic priced; local/Ollama intentionally$0). The token sweep also publishesUSAGE_SNAPSHOTto/ws/system, so the dashboard's "Token Usage & Cost" panel updates live and falls back to HTTP polling when the stream is down. - Delivery observability (the panel's Metrics → "Delivery" tab) shows how work flows, computed by
MetricsServicefrom data already captured — no new feature flag. Per-stage cycle time and the bottleneck distribution are reconstructed from theaudit_logtransition journey (each generictask.<status>event marks entry into a status; the namedtask.qa_fail/task.pr_failevents are excluded from the reconstruction). Rework rate readstasks.revision_count— incremented once per transition intoneeds_revisionat the single chokepointTaskService._emit_status_transition_audit— and attributes each bounce to the QA / PR-reviewer via those named audit events; rework cost joinsagent_spawn_sessions.task_id. Read-only endpoints:/dashboard/metrics/{cycle-time,bottlenecks,rework,scorecard/agent/{id},scorecard/team/{team}}.
Database migrations
Schema changes ship as Alembic migrations under alembic/versions/. Run:
docker compose exec orchestrator alembic upgrade head
after pulling any change that adds a new migration.
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
404 /api/embed |
Model not pulled | Check docker logs roboco-ollama-init |
All connection attempts failed |
API not ready | Orchestrator starts before FastAPI lifespan completes |
| Healthcheck failing | Wrong endpoint | Use ollama list not curl |
Blueprint Reference
The organizational structure, communication matrix, role descriptions, and access-control model are documented inline above and in the user-facing documentation site at docs.roboco.tech (the roboco-website repo — Next.js MDX, the canonical docs site as of the 2026-07-03 docs-site split: docs/internal/specs/2026-07-03-docs-site-split.md). This repo's old MkDocs-built user tree is gone; .github/workflows/docs.yml now only deploys the committed docs-redirects/ stubs (meta-refresh + canonical) so every URL the old Pages site published keeps resolving, to docs.roboco.tech. docs/rag/ remains the agent-facing RAG corpus (never published); docs/map/ is the agent-facing exhaustive codebase map — both directories are auto-indexed into the KB at startup and re-indexed periodically on file changes (OptimalService.AUTO_INDEX_DIRS, roboco/services/optimal.py), so docs/map/*.md is roboco_kb_search-able the same as docs/rag/*.md; docs/internal/ holds specs and working notes; the old root usage.md / deployment.md now link straight to docs.roboco.tech.