mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* 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>
702 lines
27 KiB
Python
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()
|