Files
roboco/roboco/api/app.py
T
401f8a2cc9 feat(board): Board Programs — the complete twelve-program catalog (Phases 1-3) (#699)
* feat(board): Pest Control — the first project-scoped Board Program

The Product Owner hunts latent defects (what the org records but nobody
reads): a weekly cycle — accelerated off-schedule when the trailing-7-day
rework rate crosses pest_rework_threshold, with the cheap dedup/scope gates
evaluated before the metrics queries — opens one held exploration task
against the least-recently-explored opted-in project (deterministic
round-robin; opted_in_projects gains a stable ORDER BY), with server-
assembled evidence in the spawn prompt (rework hotspots, recurring-findings
and waived-minor ledger aggregates, all capped) plus prior-cycle LEARN
context. The PO calls the new PO-only propose_bug_hunt verb once: ≤5 items,
evidence required per item, targets validated against pest_control
participation. CEO decides per item — approve materializes a BACKLOG task
(source pest_control, never auto-starts), reject records the reason; both
feed the LEARN ledger by exploration task id; all-terminal completes the
cycle. Telegram queue pushes carry working Approve/Reject handlers
mirroring the roadmap kind. Doctrine: board.md Pest Control section +
product-owner verb entry + regenerated verb tables.

* feat(panel): Pest Control review queue

Command Center gains the pest review queue (per-item approve/reject with
reason, mirroring the roadmap queue); the Programs card and the project
settings participates-in checkboxes pick the new program up registry-driven
— the settings section renders for the first time now that a project-scoped
program exists.

* feat(board): Periscope — HoM market-research brief program

Weekly org-scoped cycle: a solo HoM spawn researches the market (web
research with mandatory source URLs — uncited findings are rejected) and
files one structured brief via the new HoM-only propose_market_brief verb:
headline, cited findings, threats/opportunities, positioning note, all
soup-checked and screened through the injection guard at persist time
(web-derived text later reaches prompts; flags recorded, content never
dropped). A brief is a report, not a proposal: the verb completes the
exploration in the same call (the x_feature asymmetry), the cycle ledger
auto-closes, and the CEO gets a best-effort notification with no
approve/reject surface (periscope deliberately never joins Telegram's
action kinds). The latest brief is injected into the roadmap exploration
prompt — Periscope feeds Printer, the first cross-role program input.

* feat(panel): Market Briefs tab (read-only)

Business page gains a Market Briefs tab listing Periscope briefs —
headline, cited findings, threats/opportunities — read-only by design; a
report has nothing to approve.

* feat(board): Coroner — event-triggered Auditor postmortems

The first EVENT program: no cron — three best-effort hooks open an autopsy
when a task bounces to its 3rd revision (the audit chokepoint), is
cancelled after work started, or is budget-blocked; all gated on arming +
one-open-autopsy dedup, none can fail the underlying transition. A solo
Auditor spawn reads the incident (server-assembled findings + transition
context) and files one propose_postmortem: incident summary, root cause,
failed stage (validated against the real status vocabulary), and ONE
process change — a playbook-kind change drafts via PlaybookService
directly into the normal pending-curation queue; the briefed draft_playbook
manifest grant was deliberately NOT added, preserving the existing
'auditor curates but never drafts' invariant test. Complete-at-propose
(report asymmetry), cycle ledger auto-closes, CEO notified link-only.
Integrated as a union with Periscope across the shared program surfaces.

* feat(panel): Coroner postmortems card

Read-only postmortems list under Business → Programs — incident, root
cause, failed stage, process change; nothing to approve, the process-change
artifact (a draft playbook) rides the existing curation queue.

* feat(board): Sentinel — Auditor drift-watch quality reports

Weekly org-scoped cycle: a solo Auditor spawn receives a server-assembled
drift context (waived-findings trend, open findings by severity,
conventions-violation hotspots, top spend — all capped, pure ORM) and files
one propose_quality_report: headline, 1-7 area-validated items with
evidence and suggested actions, overall assessment. Report semantics —
complete-at-propose, cycle auto-closes, CEO notified display-only (never on
Telegram's approve/reject surface); items are structured so a later
convert-to-task control is cheap. Integration adopts Sentinel's module-
level dict-dispatch for board-program routing (xenon-driven), folding all
prior programs in; app router mounting extracted to a helper for the same
budget.

* feat(panel): Quality Reports tab (read-only)

Business page gains the Sentinel quality-reports tab — headline, per-area
observations with evidence and suggested actions; read-only, a report has
nothing to approve.

* feat(board): Spackle — gap-fill audit program

Biweekly project-scoped PO cycle over the half-shipped surface area: API
routes without panel surfaces (and vice versa), armed flags without docs,
docs promises the code doesn't keep, dead-end tabs — the inventory diffing
is the PO's own read-tool work, ordered by the spawn prompt with file:line
citations required; the server injects only prior-cycle LEARN and the
rotation target. Rotation is now a shared module-level helper
(pick_rotation_target, parameterized by source) both project-scoped
engines use — pest_control delegates to it, behavior-identical, with a
cross-pollution test proving the two programs' rotations stay independent.
propose_gap_fill mirrors the bug-hunt verb (≤5 items, two-sided evidence
required, participation gate); per-item CEO decide materializes BACKLOG
source=spackle tasks; full Telegram kind incl. approve/reject handlers.
All seven program routers now mount from one helper.

* feat(panel): Spackle gap-fill review queue

Command Center gains the gap-fill queue mirroring the pest-control one —
per-item approve/reject with the two-sided gap evidence rendered.

* feat(board): Scales — monthly portfolio rebalance

Org-scoped PO cycle over the stale backlog: the spawn receives a capped
stale-task snapshot (BACKLOG/PENDING unclaimed >30 days) plus the charter
and prior-cycle LEARN, and files one propose_rebalance — 1-7 items, each a
resolvable task_ref with action reprioritize (validated new priority) or
cancel, rationale required. Per-item CEO decide: approve EXECUTES the
action (audited priority update, or the normal cancel path) — the first
program whose materializer mutates existing tasks instead of creating
them; reject records the reason; LEARN by exploration task id;
all-terminal completes the cycle. Full Telegram decide-kind wiring.
Integrated as the eight-program union (registry, dict dispatch, routers
helper, teardown enumerations).

* feat(panel): Scales rebalance review queue

Command Center gains the rebalance queue — per-item approve/reject with
the action, target task, and rationale rendered.

* feat(board): Mirror — quarterly positioning audit

Project-scoped HoM cycle over messaging surfaces: README claims vs shipped
reality, docs-site promises vs code, charter alignment — the audit is the
HoM's own read-tool work with citations required; the server injects the
charter, prior-cycle LEARN, and the shared rotation target. propose_
messaging_fixes mirrors the gap-fill verb (≤5 items, drift evidence naming
claim + contradicting reality, participation gate); per-item CEO decide
materializes BACKLOG source=mirror documentation tasks; full Telegram
decide-kind wiring. Nine-program union across the shared surfaces.

* feat(panel): Mirror messaging-fixes review queue

* feat(board): Megaphone — HoM standing editorial calendar

Cron cycle (3 days, org-scoped, gated on X credentials — drafting content
nobody can post is pointless): the HoM receives a shipped-this-week digest
plus Unreleased changelog bullets and files one propose_editorial_post
(angle-validated, ≤280, brand voice) that materializes a held x_editorial
draft through the SAME X-queue origination chokepoint release posts use —
zero new approval surface, notifications and CEO decide for free.
Complete-at-propose; cycle auto-closes. Ten-program union.

* feat(panel): x_editorial source labels in the X queue surfaces

* feat(board): Librarian — proactive playbook mining

Biweekly org-scoped Auditor cycle: mines recurring non-private learning
journals (≥2-count grouping with a recency fallback) against the existing
playbook-title inventory and files one propose_playbook_drafts — 1-3
drafts, each with the repeated-pattern evidence that justifies it,
duplicate titles rejected in-batch and against the live store. Drafts are
created via PlaybookService directly (the Coroner precedent — the
'auditor curates but never drafts' do-verb invariant stays intact and
tested) and land in the normal pending-curation queue the Auditor's own
triage already surfaces; no new panel surface. Complete-at-propose;
display-only CEO notification. Eleven-program union.

* feat(board): War Room — release campaign planning

EVENT program with a REAL originator (unlike coroner's stub): a release
publish hooks a campaign brief beside the release-post seam, and the CEO's
run-now originates on demand — the cron loop never fires it. The HoM
designs a 2-6 post arc (teaser → launch → follow-up → spotlight; 280-cap,
future strictly-ascending publish_after, stage vocabulary) and one
propose_campaign call materializes each post as a held x_campaign draft
through the X-queue chokepoint. V1 is manual-cadence by design: publish_
after renders as queue guidance and the CEO approves each post at its
moment — nothing auto-posts, ever; the auto-schedule upgrade is a
documented ceiling. Twelve-program union: full registry complete.

* feat(panel): x_campaign labels + publish-after guidance in the X queue

* feat(board): Barfly — adjacent-conversation replies

Cron cycle (2 days, org-scoped, X-credentials gated): the engine searches
X for conversations where RoboCo is relevant but unmentioned (new OAuth-
signed search_recent on the client; queries + candidate cap configurable),
screens every fetched tweet through the injection guard (stored unclamped
— a clamp was truncating the candidate under the envelope, caught by the
dev's own tests), dedupes via the existing x_seen_mentions ledger (no
migration; also prevents double-drafting against the mentions poll), and
opens one held HoM exploration carrying the screened candidates. propose_
conversation_replies enforces candidate-id-only replies (≤5, 280-cap);
each materializes a held x_barfly draft through the X-queue chokepoint,
threaded via a new in_reply_to seam on post_tweet that only x_barfly
drafts use. The X redraft machinery is now dict-dispatch over per-source
extractors with reply-ref carry for x_barfly. Thirteen-program registry.
War Room's test fakes gained the new abstract search_recent stub.

* feat(board): Dogfood — the PO walks the product

The fourteenth and final registry entry, completing the catalog. EVENT
program (release-publish hook beside the war-room hook + CEO run-now, both
through the same real originator; the cron loop never fires it), project-
scoped with shared rotation. The permission surface is the careful part:
the PO's dogfood spawn — and ONLY that spawn — gets the Playwright MCP
mounted, via a task-scoped fail-closed probe mirroring the video-authoring
precedent (a PO spawned for roadmap/pest/scales never sees browser tools;
tested both ways); the PM agent image bakes chromium unconditionally like
the ux image, the mount stays task-gated in code. The walk targets the
rotation target's live surfaces (panel_base_url only when the target is
the org's own project, honest degradation otherwise); propose_friction_
fixes files ≤5 walked-path-evidenced items; per-item CEO decide
materializes BACKLOG source=dogfood tasks; full Telegram decide kind.
Also: megaphone/librarian/war_room arming keys restored to the settings
validator — their panel toggles would have been rejected (dropped in
earlier unions; the same silent-arming class the drill killed once
already).

* feat(panel): Dogfood friction review queue

* chore(board): final whole-branch sweep fixes

The night's closing adversarial pass over the integrated fourteen-program
registry found ONE functional defect — the war-room test fakes' post_tweet
predated Barfly's in_reply_to_tweet_id kwarg (LSP violation, the only red
in an otherwise fully green gate) — plus doc/test drift, all fixed: the
source-parity test completes to fourteen (spackle/mirror were silently
absent while its neighboring comment claimed full coverage), the PO
identity doc gains its missing Dogfood verb, the auditor quick-list gains
propose_postmortem, three stale comments corrected (rotation docstring,
panel registry header, X source enumerations), the dogfood release-hook
gains the exception-swallow test its four sibling hooks already had, and
the CHANGELOG's Unreleased section documents the whole Board Programs
train. Full make quality: exit 0, all gates green.

* docs: full documentation sweep for the Board Programs train

CLAUDE.md's roadmap-engine entry superseded by the Board Program registry
entry (all fourteen programs, arming, scoping, LEARN, guardrails) with the
role verb tables and playwright row refreshed; docs/rag gains the agent-
facing architecture doc plus full propose_* call-shape sections in the
three board role docs, and corrects the strategy-engine section to shipped
reality (only idle→roadmap is wired); docs/map covers the registry + all
twelve engines with flags, gotchas, and drift notes. The 0.27.0 reference
inventory confirmed only the release-executor's canonical set carries the
version — left for the 0.28.0 cut.

* feat(board): human titles + descriptions on every program surface

Raw registry keys rendered as bare panel labels — an operator reading
x_feature had no idea what enabling or running it does. The registry
dataclass gains title/description (test-enforced non-empty for every
entry, unique titles), the API passes them through, and every surface
renders title-with-description-tooltip instead of the key: the Programs
card (label, toggle hint, run-now toast), and the project settings
participates-in/excluded-from checkboxes.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-25 17:13:32 +02:00

702 lines
27 KiB
Python

"""
FastAPI Application Factory
Creates and configures the FastAPI application with all routes,
middleware, and event handlers.
"""
import asyncio
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from roboco.api.auth.routes import mount_cloud_auth
from roboco.api.auth.seed import ensure_seed_user_startup
from roboco.api.deps import _auth_required, get_orchestrator_or_none
from roboco.api.middleware import setup_middleware
from roboco.api.routes.a2a import router as a2a_router
from roboco.api.routes.a2a import wellknown_router as a2a_wellknown_router
from roboco.api.routes.agents import router as agents_router
from roboco.api.routes.board_programs import router as board_programs_router
from roboco.api.routes.cockpit import router as cockpit_router
from roboco.api.routes.company_goals import router as company_goals_router
from roboco.api.routes.coroner import router as coroner_router
from roboco.api.routes.dashboard import router as dashboard_router
from roboco.api.routes.docs import router as docs_router
from roboco.api.routes.dogfood import router as dogfood_router
from roboco.api.routes.git import router as git_router
from roboco.api.routes.github_app import router as github_app_router
from roboco.api.routes.health import router as health_router
from roboco.api.routes.journals import router as journals_router
from roboco.api.routes.kanban import router as kanban_router
from roboco.api.routes.mirror import router as mirror_router
from roboco.api.routes.notifications import router as notifications_router
from roboco.api.routes.optimal import router as optimal_router
from roboco.api.routes.orchestrator import router as orchestrator_router
from roboco.api.routes.periscope import router as periscope_router
from roboco.api.routes.pest_control import router as pest_control_router
from roboco.api.routes.pitch import router as pitch_router
from roboco.api.routes.playbooks import router as playbooks_router
from roboco.api.routes.product import router as product_router
from roboco.api.routes.project import router as project_router
from roboco.api.routes.prompter_live import router as prompter_live_router
from roboco.api.routes.provider import router as provider_router
from roboco.api.routes.release import router as release_router
from roboco.api.routes.research import router as research_router
from roboco.api.routes.roadmap import router as roadmap_router
from roboco.api.routes.scales import router as scales_router
from roboco.api.routes.secretary import router as secretary_router
from roboco.api.routes.secretary_live import router as secretary_live_router
from roboco.api.routes.sentinel import router as sentinel_router
from roboco.api.routes.settings import router as settings_router
from roboco.api.routes.spackle import router as spackle_router
from roboco.api.routes.stream import router as stream_router
from roboco.api.routes.system import router as system_router
from roboco.api.routes.tasks import router as tasks_router
from roboco.api.routes.telegram import mount_telegram_miniapp_auth
from roboco.api.routes.telegram import router as telegram_router
from roboco.api.routes.usage import router as usage_router
from roboco.api.routes.v1 import do as do_module
from roboco.api.routes.v1 import flow_auditor as flow_auditor_module
from roboco.api.routes.v1 import flow_board as flow_board_module
from roboco.api.routes.v1 import flow_cell_pm as flow_cell_pm_module
from roboco.api.routes.v1 import flow_dev as flow_dev_module
from roboco.api.routes.v1 import flow_doc as flow_doc_module
from roboco.api.routes.v1 import flow_main_pm as flow_main_pm_module
from roboco.api.routes.v1 import flow_pr_reviewer as flow_pr_reviewer_module
from roboco.api.routes.v1 import flow_qa as flow_qa_module
from roboco.api.routes.video import router as video_router
from roboco.api.routes.video import tiktok_router
from roboco.api.routes.work_session import router as work_session_router
from roboco.api.routes.x import router as x_router
from roboco.api.websocket import router as ws_router
from roboco.config import settings
from roboco.db.base import close_db, get_session_factory, init_db
from roboco.logging import get_logger, setup_logging
from roboco.security import apply_guard, guarded_lifespan
from roboco.services.extraction import ExtractionPipeline, ExtractionService
from roboco.services.learning import get_learning_service
from roboco.services.optimal import close_optimal_service, get_optimal_service
from roboco.services.playbook import PlaybookService
from roboco.services.rag_index_failures import backfill_unindexed_journals, reclaim_due
from roboco.services.settings import apply_persisted_feature_flags
from roboco.services.transcription import TranscriptionService
# Setup logging before anything else
setup_logging()
logger = get_logger(__name__)
class _AppServices:
"""Holder for application service instances (initialized in lifespan)."""
transcription: TranscriptionService | None = None
extraction: ExtractionPipeline | None = None
async def _reconcile_unindexed_playbooks(app: FastAPI) -> None:
"""Re-index APPROVED playbooks left ``indexed_ok=False`` by a failed
post-commit embed (e.g. an Ollama restart mid-approval-burst). Best-effort:
a failure here never blocks startup — rows stay unindexed and the next
startup retries them. Skipped when RAG is disabled (no optimal) or the
org-memory loop is off (the index is inert).
"""
if app.state.optimal is None or not settings.org_memory_enabled:
return
try:
async with get_session_factory()() as session:
svc = PlaybookService(session)
reconciled = await svc.reconcile_unindexed_approved()
if reconciled:
logger.info(
"Playbook reconcile: re-indexed unindexed approved",
count=reconciled,
)
except Exception as e:
logger.warning("Playbook reconcile failed; continuing", error=str(e))
async def _reclaim_rag_index_failures(app: FastAPI) -> None:
"""Reclaim dead-lettered RAG index writes (embedder 429 after retries, etc.).
Best-effort: a failure here never blocks startup — due rows stay in the
dead-letter and the next startup retries them. Skipped when RAG is
disabled (no optimal).
"""
if app.state.optimal is None:
return
try:
reclaimed = await reclaim_due(app.state.optimal)
if reclaimed:
logger.info(
"RAG index dead-letter reclaim: re-indexed rows",
count=reclaimed,
)
except Exception as e:
logger.warning("RAG index dead-letter reclaim failed; continuing", error=str(e))
async def _backfill_unindexed_journals(app: FastAPI) -> None:
"""Re-index journal/learning entries silently zero-chunked before the
per-index chunk-floor fix (see ``backfill_unindexed_journals``'s
docstring). Best-effort: a failure here never blocks startup — the rows
stay and the next startup retries them. Skipped when RAG is disabled.
"""
if app.state.optimal is None:
return
try:
await backfill_unindexed_journals(app.state.optimal)
except Exception as e:
logger.warning("Journal/learning RAG backfill failed; continuing", error=str(e))
async def _reconcile_rag_indexes(app: FastAPI) -> None:
"""Run all RAG index reconcile passes: playbooks, dead-letter reclaim,
and the journals/learnings zero-chunk backfill."""
await _reconcile_unindexed_playbooks(app)
await _reclaim_rag_index_failures(app)
await _backfill_unindexed_journals(app)
logger.info("RAG index reconcile finished")
def _log_reconcile_outcome(task: asyncio.Task[None]) -> None:
"""Surface a background-reconcile crash; each pass already swallows its
own errors, so anything landing here is an unexpected bug, not a retry."""
if task.cancelled():
return
exc = task.exception()
if exc is not None:
logger.error("Background RAG reconcile crashed", error=repr(exc))
def _schedule_rag_reconcile(app: FastAPI) -> asyncio.Task[None]:
"""Schedule the reconcile without awaiting it (see lifespan comment)."""
task = asyncio.create_task(_reconcile_rag_indexes(app))
task.add_done_callback(_log_reconcile_outcome)
app.state.rag_reconcile_task = task
return task
def _cancel_rag_reconcile(app: FastAPI) -> None:
"""Stop a still-running background reconcile at shutdown."""
task = getattr(app.state, "rag_reconcile_task", None)
if task is not None and not task.done():
task.cancel()
async def _apply_flag_overrides() -> None:
"""Overlay panel-persisted feature-flag overrides onto the live config so
the rest of startup (and the dispatch loops) read the panel's choices;
unset flags keep their env/config default. Best-effort — a failure here
must not block startup, the env defaults still apply."""
try:
async with get_session_factory()() as flags_db:
applied_flags = await apply_persisted_feature_flags(flags_db)
if applied_flags:
logger.info("Applied persisted feature-flag overrides", flags=applied_flags)
except Exception as e:
logger.warning("Feature-flag overlay failed; using env defaults", error=str(e))
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
"""
Application lifespan manager.
Handles startup and shutdown events.
"""
logger.info(
"Starting RoboCo API",
version=settings.app_version,
environment=settings.environment,
)
if not _auth_required():
logger.warning(
"Agent auth is in HEADER-TRUST mode (ROBOCO_AGENT_AUTH_REQUIRED is "
"not set to true): the API accepts X-Agent-Id / X-Agent-Role without "
"verifying a signed token, so any client that can reach it may act as "
"any role, including 'ceo'. Acceptable only on a trusted private "
"network. Set ROBOCO_AGENT_AUTH_REQUIRED=true and do NOT expose this "
"API to untrusted networks.",
)
# Startup: apply Alembic migrations (+ create_all fallback for fresh DBs).
# init_db runs on every environment now — migrations are idempotent via
# alembic_version, and this is the only way new schema (e.g. enum value
# additions like NotificationType.APPROVAL) reaches the running DB.
await init_db()
logger.info("Database initialized")
# Cloud auth: idempotently upsert the single seeded CEO login user.
# No-op unless ROBOCO_CLOUD_AUTH_ENABLED (see ensure_seed_user_startup).
await ensure_seed_user_startup()
await _apply_flag_overrides()
# Initialize Phase 2 services
_AppServices.transcription = TranscriptionService()
await _AppServices.transcription.start()
extraction_service = ExtractionService()
_AppServices.extraction = ExtractionPipeline(extraction_service)
# Store in app state for access in routes
app.state.transcription = _AppServices.transcription
app.state.extraction = _AppServices.extraction
# Initialize OptimalService (RAG) - BLOCKS until fully ready
# This ensures /health only returns 200 when RAG is operational
# Typical initialization time: 30-90 seconds (embedding + indexing)
try:
logger.info("Initializing OptimalService (RAG)...")
optimal_service = await get_optimal_service()
app.state.optimal = optimal_service
logger.info("OptimalService (RAG) initialized successfully")
except Exception as e:
logger.warning(
"OptimalService (RAG) initialization failed - RAG features disabled",
error=str(e),
)
app.state.optimal = None
# Wire the learning-propagation singleton to OptimalService. Without this,
# record_learning() raises "not initialized" and every task completion logs
# "Failed to extract learnings". Skipped when RAG is disabled (no optimal).
if app.state.optimal is not None:
try:
learning_service = await get_learning_service()
await learning_service.initialize(app.state.optimal)
logger.info("LearningPropagationService initialized")
except Exception as e:
logger.warning("LearningPropagationService init failed", error=str(e))
# Reconcile RAG index state in the BACKGROUND: re-index APPROVED playbooks
# left unindexed by a failed post-commit embed, reclaim dead-lettered
# index writes, and backfill zero-chunk journals/learnings. Never awaited
# here — uvicorn binds the socket only after this lifespan completes, and
# a 200-entry backfill behind a busy Ollama held the API down for 30+
# minutes when this was a blocking await (2026-07-09 deploy).
_schedule_rag_reconcile(app)
logger.info("All services initialized, API ready")
yield
# Shutdown
logger.info("Shutting down RoboCo API")
_cancel_rag_reconcile(app)
if _AppServices.transcription:
await _AppServices.transcription.stop()
# Stop the orchestrator BEFORE closing the DB / OptimalService. stop()
# cancels the background loops, stops the agents (finalizing work sessions
# + agent state via DB writes), and drains fire-and-forget bg writes
# (respawn_tracker upserts, audit-log rows) — all needing the DB still
# open. Closing the DB first silently dropped those final writes (the old
# order, where only bootstrap's finally block called stop() AFTER lifespan
# had already closed the DB). Best-effort: a stop error must not block the
# resource teardown below. No-op when no orchestrator is wired (tests,
# skip_orchestrator). bootstrap's finally block re-calls stop() as a safety
# net; stop() is idempotent (guarded by the _stopped flag) so the second
# call is a no-op.
orchestrator = get_orchestrator_or_none()
if orchestrator is not None:
try:
await orchestrator.stop()
except Exception as e:
logger.warning("Orchestrator stop failed during shutdown", error=str(e))
# Close Phase 3 services
await close_optimal_service()
await close_db()
logger.info("Shutdown complete")
def _mount_v1_routers(app: FastAPI) -> None:
"""Mount every API v1 (intent-verb + content-tool) router."""
app.include_router(flow_dev_module.router)
app.include_router(flow_qa_module.router)
app.include_router(flow_doc_module.router)
app.include_router(flow_cell_pm_module.router)
app.include_router(flow_main_pm_module.router)
app.include_router(flow_board_module.router)
app.include_router(flow_auditor_module.router)
app.include_router(flow_pr_reviewer_module.router)
app.include_router(do_module.router)
def _mount_board_program_routers(app: FastAPI, api_prefix: str) -> None:
"""Mount every Board Program route — the generic registry route plus each
program's own per-item / read-only surface. Grouped into one helper
(mirrors ``_mount_v1_routers``) to keep ``create_app``'s own statement
count from growing unbounded as programs are added."""
# Board roadmap engine — the CEO approves/rejects items within a held
# roadmap cycle. Approving materializes a BACKLOG task; nothing auto-starts.
app.include_router(roadmap_router, prefix=f"{api_prefix}/roadmap", tags=["Roadmap"])
# Board Programs — the generic registry status + off-schedule "run now"
# (roadmap + x_feature today; every later program rides the same route).
app.include_router(
board_programs_router,
prefix=f"{api_prefix}/board-programs",
tags=["Board Programs"],
)
# Pest Control (Board Program) — the CEO approves/rejects items within a
# held bug-hunt cycle. Approving materializes a BACKLOG task; nothing
# auto-starts.
app.include_router(
pest_control_router,
prefix=f"{api_prefix}/pest-control",
tags=["Pest Control"],
)
# Periscope (Board Program) — the CEO reads filed market-research briefs.
# Read-only: a brief is a report, not a queue item; nothing to approve.
app.include_router(
periscope_router, prefix=f"{api_prefix}/periscope", tags=["Periscope"]
)
# Coroner (Board Program) — read-only Postmortems list. A postmortem
# completes atomically at propose_postmortem time; there is nothing here
# for the CEO to approve/reject.
app.include_router(coroner_router, prefix=f"{api_prefix}/coroner", tags=["Coroner"])
# Sentinel (Board Program) — the CEO reads filed org-wide quality-drift
# reports. Read-only: a report is a report, not a queue item; nothing to
# approve.
app.include_router(
sentinel_router, prefix=f"{api_prefix}/sentinel", tags=["Sentinel"]
)
# Spackle (Board Program) — the CEO approves/rejects items within a held
# gap-fill cycle. Approving materializes a BACKLOG task; nothing
# auto-starts.
app.include_router(spackle_router, prefix=f"{api_prefix}/spackle", tags=["Spackle"])
# Scales (Board Program) — the CEO approves/rejects items within a held
# portfolio-rebalance cycle. Approving EXECUTES the item against the live
# target task (reprioritize or cancel); nothing here creates a task.
app.include_router(scales_router, prefix=f"{api_prefix}/scales", tags=["Scales"])
# Mirror (Board Program) — the CEO approves/rejects items within a held
# messaging-fixes cycle. Approving materializes a BACKLOG docs task;
# nothing auto-starts.
app.include_router(mirror_router, prefix=f"{api_prefix}/mirror", tags=["Mirror"])
# Dogfood (Board Program) — the CEO approves/rejects items within a held
# friction-fix cycle. Approving materializes a BACKLOG task; nothing
# auto-starts.
app.include_router(dogfood_router, prefix=f"{api_prefix}/dogfood", tags=["Dogfood"])
def create_app() -> FastAPI:
"""
Create and configure the FastAPI application.
Returns:
Configured FastAPI application instance.
"""
app = FastAPI(
title="RoboCo API",
description="AI Agents Company - Messaging and Task Management API",
version=settings.app_version,
docs_url="/docs", # if settings.debug else None,
redoc_url="/redoc", # if settings.debug else None,
# Wraps the existing lifespan with fastapi-guard's when armed (drives the
# middleware's redis/geo/agent init); returns it unchanged when off.
lifespan=guarded_lifespan(lifespan),
)
# ==========================================================================
# Middleware
# ==========================================================================
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=settings.cors_allow_credentials,
allow_methods=["*"],
allow_headers=["*"],
)
# Setup custom middleware (error handling, logging, correlation IDs)
setup_middleware(app)
# fastapi-guard HTTP security layer — no-op unless ROBOCO_GUARD_ENABLED.
# Mounted last so SecurityMiddleware is outermost and blocks hostile traffic
# before it reaches the app (guard does its own request logging); order can
# be tuned during passive-mode calibration.
apply_guard(app)
# ==========================================================================
# Routes
# ==========================================================================
# Health check
app.include_router(health_router, tags=["Health"])
# A2A Protocol: Well-known endpoints at root level
# (/.well-known/agent.json, /agents/{id}/.well-known/agent.json)
app.include_router(a2a_wellknown_router, tags=["A2A Protocol"])
# API v1
api_prefix = "/api"
app.include_router(
agents_router,
prefix=f"{api_prefix}/agents",
tags=["Agents"],
)
app.include_router(
settings_router,
prefix=f"{api_prefix}/settings",
tags=["Settings"],
)
app.include_router(
company_goals_router,
prefix=f"{api_prefix}/company-goals",
tags=["Company"],
)
app.include_router(
notifications_router,
prefix=f"{api_prefix}/notifications",
tags=["Notifications"],
)
# Phase 2: Stream processing and permissions
app.include_router(
stream_router,
prefix=f"{api_prefix}/stream",
tags=["Stream Processing"],
)
# Phase 3: Intelligence - Optimal API and Journal API
app.include_router(
optimal_router,
prefix=f"{api_prefix}/optimal",
tags=["Optimal API"],
)
app.include_router(
journals_router,
prefix=f"{api_prefix}/journals",
tags=["Journals"],
)
# Web research — pluggable external search/fetch for Board + PM agents.
app.include_router(
research_router,
prefix=f"{api_prefix}/research",
tags=["Research"],
)
# Cockpit — the CEO's read-only "is the business winning?" summary.
app.include_router(
cockpit_router,
prefix=f"{api_prefix}/cockpit",
tags=["Cockpit"],
)
# Release manager — the CEO approves/rejects a held release proposal.
app.include_router(
release_router,
prefix=f"{api_prefix}/release",
tags=["Release"],
)
# Playbooks — the Auditor (or CEO) curates the drafted playbook library.
app.include_router(
playbooks_router,
prefix=f"{api_prefix}/playbooks",
tags=["Playbooks"],
)
# X (Twitter) engine — the CEO approves/rejects held posts/replies and
# manages credentials. Nothing here posts except an explicit approve.
app.include_router(
x_router,
prefix=f"{api_prefix}/x",
tags=["X"],
)
# Board Programs — the generic registry route + each program's own
# per-item / read-only surface (roadmap, board-programs, pest control,
# periscope, coroner, sentinel, spackle, scales). Grouped into one helper
# to keep this function's own statement count from growing unbounded as
# programs are added (mirrors ``_mount_v1_routers`` below).
_mount_board_program_routers(app, api_prefix)
# Video engine — the CEO requests an on-demand marketing video; the
# release/spotlight triggers open the same UX/UI authoring task via their
# own hooks. Nothing renders or posts from this route alone.
app.include_router(
video_router,
prefix=f"{api_prefix}/video",
tags=["Video"],
)
# TikTok credentials — write-only OAuth2 secrets for the video engine's
# inbox-upload poster (mirrors /x/credentials).
app.include_router(
tiktok_router,
prefix=f"{api_prefix}/tiktok",
tags=["TikTok"],
)
# Telegram notifications bridge — CEO-managed bot-token + chat-id credentials
# (write-only); the fan-out itself runs server-side from the CEO producers.
app.include_router(
telegram_router,
prefix=f"{api_prefix}/telegram",
tags=["Telegram"],
)
# Telegram Mini App sign-in — public, pre-auth; mounted only when both
# telegram_miniapp_enabled and cloud_auth_enabled are armed.
mount_telegram_miniapp_auth(app, f"{api_prefix}/telegram")
# Pitches — Board proposals + CEO approve -> auto-provision origination path.
app.include_router(
pitch_router,
prefix=f"{api_prefix}/pitches",
tags=["Pitches"],
)
# Secretary — the CEO's chief-of-staff: company-state reads + gated directives.
app.include_router(
secretary_router,
prefix=f"{api_prefix}/secretary",
tags=["Secretary"],
)
# Secretary live chat — panel <-> Secretary container bridge.
app.include_router(
secretary_live_router,
prefix=f"{api_prefix}/secretary",
tags=["Secretary"],
)
# Phase 5: Management - Tasks, Kanban, Dashboards
app.include_router(
tasks_router,
prefix=f"{api_prefix}/tasks",
tags=["Tasks"],
)
app.include_router(
kanban_router,
prefix=f"{api_prefix}/kanban",
tags=["Kanban"],
)
app.include_router(
dashboard_router,
prefix=f"{api_prefix}/dashboard",
tags=["Dashboard"],
)
# Phase 7: Agent Runtime
app.include_router(
orchestrator_router,
prefix=f"{api_prefix}/orchestrator",
tags=["Orchestrator"],
)
# A2A Protocol: API endpoints
app.include_router(
a2a_router,
prefix=f"{api_prefix}/a2a",
tags=["A2A Protocol"],
)
# Git Integration
app.include_router(
git_router,
prefix=f"{api_prefix}/git",
tags=["Git Operations"],
)
# GitHub App integration — CEO-managed credentials + the "Select repo"
# picker's installation/repository listing (see roboco/services/github_app_auth.py).
app.include_router(
github_app_router,
prefix=f"{api_prefix}/github-app",
tags=["GitHub App"],
)
# Project Management
app.include_router(
project_router,
prefix=f"{api_prefix}/projects",
tags=["Projects"],
)
# Product Management
app.include_router(
product_router,
prefix=f"{api_prefix}/products",
tags=["Products"],
)
# AI Providers (model routing + Ollama-cloud fallback)
app.include_router(
provider_router,
prefix=f"{api_prefix}/providers",
tags=["Providers"],
)
# Prompter live chat — panel <-> spawned intake agent (SSE + relay)
app.include_router(
prompter_live_router,
prefix=f"{api_prefix}/prompter",
tags=["Prompter"],
)
# Work Sessions
app.include_router(
work_session_router,
prefix=f"{api_prefix}/work-sessions",
tags=["Work Sessions"],
)
# Documentation
app.include_router(
docs_router,
prefix=f"{api_prefix}/docs",
tags=["Documentation"],
)
# Token Usage Analytics
app.include_router(
usage_router,
prefix=f"{api_prefix}/usage",
tags=["Usage Analytics"],
)
# System monitoring (rate-limits, etc.)
app.include_router(
system_router,
prefix=f"{api_prefix}/system",
tags=["System"],
)
# Cloud auth — /auth/status is always public; login/logout mount only
# when ROBOCO_CLOUD_AUTH_ENABLED (mirrors apply_guard's conditional mount).
mount_cloud_auth(app, f"{api_prefix}/auth")
# API v1 — intent-verb flow + content-tool endpoints (each module owns
# its own prefix); grouped into one helper to keep create_app's own
# statement count from growing unbounded as roles are added.
_mount_v1_routers(app)
# ==========================================================================
# WebSocket
# ==========================================================================
app.include_router(ws_router, prefix="/ws", tags=["WebSocket"])
return app
# Create the default application instance
app = create_app()