Files
roboco/docs/map/product-strategy-research-pitch.md
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

501 lines
78 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Slice Map — product-strategy-research-pitch
## Purpose
The product / strategy / research / pitch slice covers the "company layer" above the delivery lifecycle: registering the git repositories agents work on (`Project`), mapping cells to repos within a product (`Product`), rendering role-specific kanban views (`Kanban`), the singleton company charter (`CompanyGoals`), the dormant goal-drift watcher (`StrategyEngine`), the pluggable web-search capability for Board/PM agents (`Research` + `ResearchQuota`), Board pitches with CEO-approve → auto-provision (`Pitch`), and the single GitHub repo-creation service that backs provisioning (`GitHubProvisioning`). Together these are the CEO/Board-facing surface that originates work and feeds it into the normal delivery lifecycle, plus the per-cell routing keystone that the gateway delegate path consults at runtime.
## Files
| Path | Role | approx LOC |
|------|------|------------|
| `roboco/services/project.py` | CRUD + git-token encryption + cell access control for Projects (git repos) | 604 |
| `roboco/services/product.py` | Product CRUD + per-cell `project_for` routing resolver + idempotent cell-map replace | 152 |
| `roboco/services/kanban.py` | Role-specific kanban board views (dev/qa/documenter/pm/main-pm/board) from task data | 587 |
| `roboco/services/company_goals.py` | CRUD for the singleton company charter (north star + objectives + constraints + policy + brand_voice + company_name); `resolve_product_name` is the shared product-name fallback chain `XEngine`/`VideoEngine` both call | 110 |
| `roboco/services/strategy_engine.py` | Dormant "engine 2": assesses company state vs goals, notify-only to CEO | 111 |
| `roboco/services/research.py` | Pluggable web-search/fetch — provider adapters (Tavily/Brave/Exa/Null) + clamping service | 431 |
| `roboco/services/research_quota.py` | Per-agent UTC-daily Redis quota counter for research calls (fail-open) | 78 |
| `roboco/services/pitch.py` | Board pitch CRUD + CEO approve → provision repos/Projects(+Product) + seed Main-PM task | 274 |
| `roboco/services/github_provisioning.py` | The only service that CREATES repos for pitch provisioning — now provider-aware (GitHub/Gitea/GitLab, Phase 4 forge parity), despite the GitHub-flavored name (kept for backward compatibility) | 232 |
| `roboco/services/roadmap_engine.py` | Dormant weekly engine: originates ONE held roadmap-exploration task for the Product Owner (default off) | 111 |
| `roboco/services/roadmap_service.py` | CEO's per-item approve/reject glue over a held roadmap cycle; approve materializes a BACKLOG task | 211 |
| `roboco/api/routes/roadmap.py` | CEO-only routes: list open cycles, approve/reject one item | 124 |
| `roboco/services/x_engine.py` | Dormant "engine 4": drafts X (Twitter) release posts (event hook), mention replies (poll), and feature-spotlight explorations (dormant interval, spawns Head of Marketing), ALL held for CEO approval (default off); prompt builders take a `product_name` param resolved via `CompanyGoalsService.resolve_product_name` instead of hardcoding "RoboCo" | 871 |
| `roboco/services/x_post_service.py` | CEO's approve/reject over a held X draft; approve posts via a Redis single-flight lock, idempotent on already-posted AND on already-rejected (CANCELLED) | 298 |
| `roboco/services/x_client.py` | OAuth 1.0a HMAC-SHA1 X API client (`LiveXClient`) + `NullXClient` (no creds, never egresses) + `build_x_client` factory | 318 |
| `roboco/services/x_credentials.py` | Singleton Fernet-encrypted OAuth 1.0a credential CRUD; decrypts server-side only | 140 |
| `roboco/api/routes/x.py` | CEO-only routes: list open X posts, approve/reject one draft | 164 |
| `roboco/api/schemas/x.py` | `XPostResponse` + `XMentionRefModel` / `XFeatureRefModel` response shapes | 73 |
| `roboco/foundation/policy/board_programs.py` | Board Program registry: `BoardProgram` dataclass, `PROGRAMS` (14 entries), `program_due`/`project_participates`/`validate_board_programs_field` (pure) | 295 |
| `roboco/services/board_programs.py` | `BoardProgramEngine` — trigger/dedup/originate/LEARN over every registered program; `program_armed` settings-store chokepoint; `pick_rotation_target` for project-scoped round-robin | 566 |
| `roboco/api/routes/board_programs.py` | CEO-only routes: list every program's live status, `POST /{key}/run-now` | 103 |
| `roboco/services/pest_control_engine.py` | `PestControlEngine` — weekly+metric cron, project-scoped; evidence context (rework hotspots, recurring/waived findings) | 199 |
| `roboco/services/spackle_engine.py` | `SpackleEngine` — biweekly cron, project-scoped gap-fill audit | 142 |
| `roboco/services/scales_engine.py` | `ScalesEngine` — monthly cron, org-scoped portfolio rebalance; stale-backlog snapshot | 176 |
| `roboco/services/dogfood_engine.py` | `DogfoodEngine` — event-only, project-scoped; real `_ORIGINATORS` binding (unlike Coroner's stub) | 152 |
| `roboco/services/periscope_engine.py` | `PeriscopeEngine` — weekly cron, org-scoped market-research brief; `latest_brief_context` feeds Printer | 157 |
| `roboco/services/megaphone_engine.py` | `MegaphoneEngine` — 3-day cron, org-scoped editorial calendar; shipped-this-week digest + Unreleased changelog | 191 |
| `roboco/services/mirror_engine.py` | `MirrorEngine` — quarterly cron, project-scoped messaging-drift audit | 143 |
| `roboco/services/barfly_engine.py` | `BarflyEngine` — 2-day cron, org-scoped; screens X search candidates through `injection_guard`, marks seen | 223 |
| `roboco/services/war_room_engine.py` | `WarRoomEngine` — event-triggered (`open_for_release` release hook + CEO run-now); campaign posts w/ `publish_after` | 231 |
| `roboco/services/coroner_engine.py` | `CoronerEngine` — event-only (`open_for_incident`, no `run_cycle`/cron path); incident + transition-history context | 237 |
| `roboco/services/sentinel_engine.py` | `SentinelEngine` — weekly cron, org-scoped drift report (waivers/findings/conventions/spend) | 261 |
| `roboco/services/librarian_engine.py` | `LibrarianEngine` — biweekly cron, org-scoped proactive playbook mining | 232 |
## Key Symbols
| Name | Kind | File:Line | Responsibility |
|------|------|-----------|----------------|
| `ProjectService` | class | project.py:23 | Project CRUD, slug lookup, cell listing, workspace path, git-token encrypt/decrypt, agent access control |
| `ProjectService._assert_git_url_allowed` | method | project.py:40 | Reject `git_url` matching a protected/denylisted repo |
| `ProjectService.create` | method | project.py:56 | Register project, encrypt token, best-effort conventions scaffold |
| `ProjectService._maybe_scaffold_conventions` | method | project.py:126 | Lazy-imports conventions service; scaffold hiccup is non-fatal |
| `ProjectService.update` | method | project.py:166 | Selective field apply; empty-string `git_token` clears, None leaves unchanged |
| `ProjectService.delete` | method | project.py:226 | Abandons ACTIVE work sessions first; optional on-disk workspace cleanup; DB RESTRICT on tasks |
| `ProjectService.get_decrypted_token` / `_by_slug` | method | project.py:441 / 471 | On-demand Fernet decrypt of git token (never cached) |
| `ProjectService.check_agent_access` | method | project.py:559 | Cell membership + optional `allowed_agents` allowlist |
| `get_project_service` | factory | project.py:602 | Session-bound constructor |
| `ProductService` | class | product.py:15 | Product CRUD + cell→project routing |
| `ProductService.project_for` | method | product.py:87 | **Per-cell routing keystone** — resolve Project for (product, team); None → caller falls back to parent task's project |
| `ProductService.distinct_project_ids` | method | product.py:103 | Distinct repos a product spans (one Main-PM integration branch each) |
| `ProductService._replace_cells` | method | product.py:122 | Idempotent full cell-map replace via cascade collection; flushes DELETEs before INSERTs to avoid `uq_product_projects_product_team` 409 |
| `get_product_service` | factory | product.py:150 | Session-bound constructor |
| `KanbanService` | class | kanban.py:30 | Role-specific board generation with optional swimlanes |
| `KanbanService._load_subtask_counts` | method | kanban.py:47 | Batch-count direct children per parent in ONE grouped query (fixes the always-0 stub; #198) |
| `KanbanService._task_to_card` | method | kanban.py:61 | Task → KanbanCard; accepts optional `subtask_counts` dict for real subtask counts |
| `KanbanService.get_dev_board` | method | kanban.py:117 | Dev cell board with optional priority/assignee swimlanes |
| `KanbanService.get_qa_board` / `get_documenter_board` / `get_pm_board` | method | kanban.py:335 / 366 / 400 | Role-filtered flat boards |
| `KanbanService.get_main_pm_board` / `_flat` | method | kanban.py:419 / 435 | Cross-cell view (team swimlanes / team columns) |
| `KanbanService.get_board_kanban` | method | kanban.py:538 | Board roadmap (P0/P1 only) |
| `KanbanService.get_board_stats` | method | kanban.py:558 | Status-count aggregation |
| `CompanyGoalsService` | class | company_goals.py:35 | Singleton charter CRUD |
| `SINGLETON_ID` | constant | company_goals.py:23 | Fixed UUID 0…0 — charter is one row |
| `CompanyGoalsService.get` / `upsert` | method | company_goals.py:38 / 44 | Partial-key upsert; caller commits |
| `StrategyEngine` | class | strategy_engine.py:47 | Assess company state vs goals |
| `StrategyEngine.assess` | method | strategy_engine.py:52 | Pure read; emits `idle` + `stranded_blocked` observations |
| `StrategyEngine.run_cycle` | method | strategy_engine.py:92 | No-op unless flag on; assess + notify CEO (notify-only, never spends/builds) |
| `StrategyObservation` | dataclass | strategy_engine.py:38 | Frozen (kind, summary, detail) |
| `ResearchService` | class | research.py:337 | Provider-agnostic entry point; clamps result/char caps |
| `SearchProvider` | ABC | research.py:80 | Adapter base: shared/owned httpx client, `_request_json`, abstract `search`, default-unsupported `fetch` |
| `TavilyProvider` / `BraveProvider` / `ExaProvider` | class | research.py:162 / 212 / 242 | Concrete adapters (Brave has no `fetch`) |
| `NullProvider` | class | research.py:288 | Graceful stub — empty results, `configured=False`, never raises |
| `build_provider` | func | research.py:317 | Select adapter by name; NullProvider when no key or unknown name |
| `get_research_service` | factory | research.py:391 | Build service from settings (provider + caps) |
| `SearchHit` / `SearchOutcome` / `FetchOutcome` | dataclass | research.py:45 / 55 / 65 | Normalised result shapes |
| `ResearchError` / `ResearchUnsupportedError` | exception | research.py:32 / 36 | Provider failure vs unsupported-op (route maps 502 vs 501) |
| `ResearchQuotaTracker` | class | research_quota.py:34 | Per-agent/day Redis `INCR` counter, 24h expiry, fail-open |
| `ResearchQuotaTracker.check_and_consume` | method | research_quota.py:51 | Atomic INCR-then-compare; over-limit still bumps counter |
| `QuotaStatus` | dataclass | research_quota.py:24 | (allowed, used, limit, day) |
| `PitchService` | class | pitch.py:56 | Pitch CRUD + approve/reject |
| `PitchService.approve` | method | pitch.py:109 | Provision repos → register topology → seed Main-PM task; idempotent on re-approval |
| `PitchService._provision_repos` | method | pitch.py:163 | One repo per target cell (multi-cell suffixes cell); reuses existing Project by slug |
| `PitchService._register_topology` | method | pitch.py:201 | Multi-cell → Product (reuse existing by slug + refresh cell map); single-cell → seed project only |
| `PitchService._seed_main_pm_task` | method | pitch.py:234 | Creates PENDING Main-PM CODE task (`source="pitch"`, `confirmed_by_human=True`) |
| `PitchService._proposed_or_raise` | method | pitch.py:152 | 404 if missing, 409 if not `proposed` (no re-deciding) |
| `GitHubProvisioningService` | class | github_provisioning.py:81 | Create private repos for pitch provisioning — Phase 4 forge parity: provider-dispatched via `_build_provider`, not GitHub-only despite the class name |
| `GitHubProvisioningService.enabled` | prop | github_provisioning.py:123 | True only when master switch + token + org all set; ALSO requires `ROBOCO_PROVISIONING_HOST` when the provider is gitlab/gitea (self-hosted needs a host, github.com doesn't) |
| `GitHubProvisioningService.create_repo` | method | github_provisioning.py:142 | Provider-dispatched repo creation with `auto_init=true`; handles the "already exists" response idempotently across all three forges via `_fetch_existing_repo`/`_is_already_exists` (#83/#84) |
| `GitHubProvisioningService._fetch_existing_repo` | method | github_provisioning.py:203 | GET the existing repo and reconstruct `ProvisionedRepo` — called on an "already exists" response to reuse an orphaned repo from a rolled-back prior approval |
| `_build_provider` | func | github_provisioning.py:68 | Picks the concrete provider (`GitHubProvider`/`GiteaProvider`/`GitLabProvider` — a `Union`, not the `GitProvider` ABC, since provisioning needs `client=`/`timeout=` kwargs the ABC doesn't declare) by `ROBOCO_PROVISIONING_PROVIDER` |
| `_is_already_exists` | func | github_provisioning.py:61 | Matches GitHub's 422, Gitea's 409/422, and GitLab's reshaped 422 "already exists"/"has already been taken" by status+phrase |
| `ProvisionedRepo` / `ProvisioningError` / `ProvisioningDisabledError` | dataclass/exc | github_provisioning.py | Result + error types |
| `RoadmapEngine` | class | roadmap_engine.py:49 | Dormant "engine 3": mirrors the release-manager "detect → originate a CEO-gated artifact → hold" shape, but the artifact is a cycle the PO *authors*, not a report the engine assembles |
| `RoadmapEngine.run_cycle` | method | roadmap_engine.py:54 | No-op unless `roadmap_engine_enabled`, a cycle is already open (`list_open_roadmap_cycles`), or the RoboCo project isn't resolvable; else opens ONE held PENDING exploration task assigned to the Product Owner |
| `RoadmapService` | class | roadmap_service.py:50 | List / approve / reject items within the open roadmap cycle(s) |
| `RoadmapService.approve_item` | method | roadmap_service.py:59 | Materialize one proposed item as a BACKLOG task via `PrompterService.create_task_from_draft`; idempotent per item |
| `RoadmapService.reject_item` | method | roadmap_service.py:108 | Record the CEO's reason; idempotent; an already-approved item cannot be rejected |
| `RoadmapService._find_item` | method | roadmap_service.py:146 | Resolve (exploration task, deep-copied cycle payload, one item) — deep copy so mutation doesn't poison SQLAlchemy's dirty-check before `markers.set_roadmap_cycle` reassigns |
| `RoadmapService._maybe_complete_cycle` | staticmethod | roadmap_service.py:202 | Completes the exploration task once every item on it is terminal (approved/rejected) |
| `RoadmapItemResult` | dataclass | roadmap_service.py:37 | Outcome of one approve/reject call (status/item_id/materialized_task_id/detail) |
| `get_roadmap_engine` / `get_roadmap_service` | factory | roadmap_engine.py:109 / roadmap_service.py:209 | Session-bound constructors |
| `XEngine` | class | x_engine.py:230 | Dormant "engine 4": mirrors the release-manager "detect → originate a CEO-gated artifact → hold" shape across THREE responsibilities — release posts, mention replies, feature spotlights |
| `XEngine._voice_guide` | method | x_engine.py:259 | `_voice_guide(product_name)`: baseline house-voice constant (`_HOM_VOICE`) plus the CEO's `company_goals.brand_voice` sample when set — feeds release/reply prompts AND is the mechanism the HoM identity file points to for its own drafting; `product_name` is resolved once per call site via `CompanyGoalsService.resolve_product_name(project)` (project's own name → charter `company_name` → "RoboCo" literal), not hardcoded |
| `_hom_voice` / `_HOM_VOICE_GUIDE` | func / const | x_engine.py | The baseline voice fed into every local-model draft prompt (release/reply) — ported from the reasoning-backed Head-of-Marketing identity's own VOICE GUIDE (previously a one-sentence stub reaching only the off-by-default spotlight path) plus a slop-ban list (banned words, em-dash ban, exclamation-pileup ban, "X isn't just Y" ban, rule-of-three ban) adapted from the ux_ui team's "AI tells to avoid", and three fixed style exemplars. Both prompts now target well-under-240-chars so the 280 clamp never truncates mid-sentence. |
| `changelog_highlights` | func | x_engine.py | Pure extraction of the bold feature-lead lines (`- **Headline (#N).**`) from a curated CHANGELOG release entry, stripping PR refs and trailing periods, capped at `limit`; `approve()` prefers these over raw `change_summary` (per-commit subjects) for the release-post caption, falling back to `change_summary` only when the changelog yields nothing |
| `XEngine.redraft_from_rejection` | method | x_engine.py | Routes a CEO's non-blank rejection reason into a fresh held draft of the SAME source (release/reply/spotlight), mirroring `VideoEngine.reauthor_from_rejection` — reads the rejected draft's source-specific reference marker, asks the local model to revise the rejected body with the reason folded in as guidance via `_revision_prompt`; a local-model failure or empty output originates NOTHING (a degraded copy is worse than none). Deduped per underlying item (`_redraft_already_open`, excludes CANCELLED so a further redraft is allowed once THAT one is also rejected) under a per-identity Redis lock (`_acquire_redraft_lock`, mirroring `XPostService`'s lock style) so two racing rejects can't both originate. |
| `XEngine.draft_release_post` | method | x_engine.py:279 | Event-driven hook (called from `ReleaseProposalService.approve`'s publish-success branch); local-model-drafted, deduped per version, capped by `x_max_open_posts`; resolves and threads `product_name` from the release's own project |
| `XEngine.run_cycle` | method | x_engine.py:353 | Periodic mentions poll; no-op unless `x_engine_enabled` AND `x_replies_enabled`; filters bot-like/low-engagement mentions, dedupes by mention id (`XSeenMentionTable`); each mention's text is run through `screen_external_text` before the local-model reply prompt sees it; resolves `product_name` once per cycle and threads it through `_originate_reply`/`_draft_reply_body`/`_reply_prompt` |
| `screen_external_text` | function | foundation/policy/injection_guard.py:95 | Shared screen-and-neutralize guard for unattended attacker-writable text feeds (X mentions, vault inbox notes): wraps the text in an untrusted-content envelope and flags any matched injection-pattern LINE inline — nothing is removed, so the CEO-facing draft still shows what the source really said |
| `XEngine.open_feature_spotlight_exploration` | method | x_engine.py:487 | No-ops unless `x_engine_enabled` AND `x_feature_spotlight_enabled`, no creds, a cycle already open, the open-post cap reached, or project unresolvable; else opens ONE held PENDING exploration task for the Head of Marketing (`source=x_feature_exploration`) carrying a `x_seen_features` marker snapshot; the description is built by `_feature_exploration_description(product_name)`, no longer the fixed `_FEATURE_EXPLORATION_DESCRIPTION` string |
| `XEngine.materialize_feature_spotlight` | method | x_engine.py:841 | Called from the `propose_feature_spotlight` do-tool: marks the feature slug seen (`XSeenFeatureTable`), creates the held draft (`source=x_feature`, identical shape to a release/reply draft), completes the exploration task |
| `XPostService.approve` | method | x_post_service.py:92 | The ONLY caller of `x_client.post_tweet`; Redis single-flight lock, re-reads task under lock, idempotent on an already-posted draft (`already_posted`); a CANCELLED draft is refused both pre-lock and re-checked under lock (`already_rejected`) — a stale approve (e.g. a queued Telegram button) can't resurrect a draft the CEO already rejected |
| `XPostService.reject` | method | x_post_service.py:251 | Records the CEO's reason; cancels the held draft; a non-blank reason schedules `XEngine.redraft_from_rejection` via `defer_after_commit` (a fresh session, never blocks or fails the HTTP response) so the feedback re-enters the draft flow instead of dying with the cancel |
| `XClient` / `NullXClient` / `LiveXClient` | ABC/class | x_client.py:150 / 166 / 186 | `NullXClient.configured` is False (no creds) — drafting still runs (content nobody can post is a no-op upstream), just never originates; `LiveXClient` signs OAuth 1.0a HMAC-SHA1 |
| `build_x_client` | factory | x_client.py:306 | Returns `LiveXClient` when credentials decrypt, else `NullXClient` |
| `XCredentialsService.set_credentials` / `.get_decrypted` | method | x_credentials.py:61 / 116 | All-or-nothing Fernet-encrypted singleton credential set/clear; decrypts server-side only, never exposed to agents |
| `get_x_engine` | factory | x_engine.py:869 | Session-bound constructor (optional injected `XClient` for tests) |
| `CompanyGoalsService.resolve_product_name` | method | company_goals.py:79 | The shared product-name fallback chain: `project.name` if set, else the charter's `company_name`, else the "RoboCo" literal — single source so `XEngine`/`VideoEngine` can't drift apart on branding |
| `task_project_fields` | func | api/schemas/project_fields.py:19 | `(project_slug, project_name)` or `(None, None)` for a task response — `sa_inspect(task).unloaded` guard before touching `task.project` (a freshly-created task can have an unloaded relationship); shared by the X and video queue response builders so a multi-project CEO can tell drafts apart via the panel's `ProjectBadge` |
| `BoardProgram` | dataclass | foundation/policy/board_programs.py:31 | Frozen registry entry: `key`/`role`/`trigger`/`source`/`default_interval_seconds`/`max_items_per_cycle`/`scope` |
| `PROGRAMS` | dict | foundation/policy/board_programs.py:45 | All 14 registered programs, keyed by `key` |
| `program_due` | func | foundation/policy/board_programs.py:225 | Pure cron-due check; METRIC/EVENT programs always return False (opened by their own hooks, never the loop) |
| `project_participates` | func | foundation/policy/board_programs.py:241 | Dual-polarity scope predicate — affirmative opt-in for `scope="project"`, opt-out (`"!key"`) for `scope="org"` |
| `validate_board_programs_field` | func | foundation/policy/board_programs.py:258 | Rejects an unknown key or a polarity mismatched to the program's own scope |
| `BoardProgramEngine` | class | services/board_programs.py:293 | Trigger/dedup/originate/LEARN over every registered program |
| `BoardProgramEngine.run_due_programs` | method | services/board_programs.py:302 | Originates a cycle for every enabled+due CRON program, then every metric predicate that fires off-schedule; one program's failure never blocks the rest |
| `BoardProgramEngine.open_program_cycle` | method | services/board_programs.py:373 | Enabled+scope+dedup only, no cron-due check — the CEO "run now" / strategy-engine idle-trigger seam |
| `BoardProgramEngine.record_decision` / `prior_cycle_context` | method | services/board_programs.py:422 / 466 | LEARN: accrue a CEO approve/reject onto the cycle row; render the last N closed cycles for the next exploration prompt |
| `program_armed` | func | services/board_programs.py:274 | THE arming chokepoint — settings-store `board_program.{key}.enabled`, falling back to a legacy flag only for `roadmap`/`x_feature` |
| `pick_rotation_target` | func | services/board_programs.py:191 | Shared round-robin for project-scoped programs (Pest Control/Spackle/Mirror/Dogfood): never-explored first, else oldest `last_opened_at` |
| `PestControlEngine.run_cycle` / `evidence_context` | method | pest_control_engine.py:79 / 152 | Weekly+metric cron; server-assembles rework-hotspot/recurring/waived-finding evidence for the PO's prompt |
| `SpackleEngine.run_cycle` | method | spackle_engine.py:71 | Biweekly cron, project-scoped gap-fill audit |
| `ScalesEngine.run_cycle` / `_stale_backlog_snapshot` | method | scales_engine.py:70 / 140 | Monthly cron; snapshots BACKLOG/PENDING tasks older than 30 days for the rebalance prompt |
| `DogfoodEngine.run_cycle` | method | dogfood_engine.py:78 | Event-only; real originator (unlike Coroner's stub) — needs no external incident id, picks the next opted-in project via `pick_rotation_target` |
| `PeriscopeEngine.run_cycle` / `latest_brief_context` | method | periscope_engine.py:71 / 126 | Weekly cron; the latest closed brief is injected into Printer's own exploration prompt |
| `MegaphoneEngine.run_cycle` / `digest_context` | method | megaphone_engine.py:74 / 133 | 3-day cron; server-assembles the shipped-this-week digest + Unreleased CHANGELOG section |
| `MirrorEngine.run_cycle` | method | mirror_engine.py:71 | Quarterly cron, project-scoped messaging-drift audit |
| `BarflyEngine.run_cycle` / `_screen_and_mark` | method | barfly_engine.py:88 / 145 | 2-day cron; screens each X search candidate through `injection_guard.screen_external_text` before it reaches the HoM's prompt |
| `WarRoomEngine.run_cycle` / `open_for_release` | method | war_room_engine.py:111 / 120 | `run_cycle` is the CEO on-demand blank-brief path (reachable via `open_program_cycle`); `open_for_release` bypasses `_ORIGINATORS` entirely, called from the release-publish hook with pre-curated highlights |
| `CoronerEngine.open_for_incident` / `incident_context` | method | coroner_engine.py:75 / 195 | The ONLY way a Coroner cycle opens — called directly from three chokepoints (bounce>=3, cancel-after-work, budget-block), never the cron loop; `_ORIGINATORS["coroner"]` is an always-`None` stub that only exists so the dict covers the registry 1:1 |
| `SentinelEngine.run_cycle` / `evidence_context` | method | sentinel_engine.py:79 / 131 | Weekly cron; server-assembles waiver-trend/open-findings/conventions-hotspot/spend evidence |
| `LibrarianEngine.run_cycle` / `mining_context` | method | librarian_engine.py:89 / 140 | Biweekly cron; server-assembles recurring learning-journal topics + existing playbook titles to mine against |
| `get_pest_control_engine` / `get_spackle_engine` / `get_scales_engine` / `get_dogfood_engine` / `get_periscope_engine` / `get_megaphone_engine` / `get_mirror_engine` / `get_barfly_engine` / `get_war_room_engine` / `get_coroner_engine` / `get_sentinel_engine` / `get_librarian_engine` / `get_board_program_engine` | factory | each engine's own file | Session-bound constructors, one per engine |
## Data Flow
Two distinct flows originate work into the delivery lifecycle:
**Pitch flow (CEO-driven origination).** A Board member creates a pitch (`PitchService.create``PitchTable` status `proposed`). The CEO approves via `POST /api/pitch/{id}/approve``PitchService.approve`. Approval calls `GitHubProvisioningService.create_repo` once per target cell (repo name `{slug}-{cell}` when multi-cell, else `{slug}`), then `ProjectService.create` to register each repo as a Project (git token stored from `settings.provisioning_token`). For multi-cell pitches, `ProductService.create` registers a Product with the cell→project map; for single-cell, the lone project is the seed. `_seed_main_pm_task` then creates a PENDING Main-PM CODE task (`source="pitch"`, `confirmed_by_human=True`) assigned to `main-pm`, which the normal dispatcher picks up. The pitch row moves to `provisioned` with `provisioned_product_id` / `provisioned_project_ids` / `seed_task_id` recorded.
**Strategy flow (dormant watcher).** `Orchestrator._strategy_engine_loop` (created at startup) returns immediately unless `strategy_engine_enabled`; otherwise each `strategy_engine_interval_seconds` it opens a DB context and calls `StrategyEngine.run_cycle``assess`. `assess` reads `TaskService.list_in_progress_or_claimed` and `list_long_running_blocked` against `CompanyGoalsService.get()`; if idle-with-goals or stranded-blocked, it sends the CEO an ack-notification via `NotificationService.send_ack_notification`. Notify-only — never originates work.
**Research flow (on-demand agent capability).** A Board/PM agent calls the `roboco-search` MCP tool (mounted only when `research_enabled` and role is research-eligible, orchestrator line 2914) → `/api/research/{search,fetch}` route. The route enforces the per-agent daily quota via the module-level `ResearchQuotaTracker` singleton (Redis INCR, fail-open), then calls `get_research_service()``ResearchService.search/fetch` → selected provider adapter. Result count and char size are clamped to `research_max_results` / `research_fetch_max_chars`. The provider key lives only server-side; the agent never egresses.
**Routing flow (runtime keystone).** `ProductService.project_for(product_id, team)` is called from the gateway delegate path to resolve which Project a cell works on within a product; None falls back to the parent task's project.
**Roadmap flow (dormant weekly originator, default off).** `Orchestrator._roadmap_engine_loop` returns immediately unless `roadmap_engine_enabled`; otherwise each `roadmap_interval_seconds` (default weekly) it opens a DB context and calls `RoadmapEngine.run_cycle`, which no-ops if a roadmap-source task is already open or the RoboCo project isn't resolvable, else opens ONE held PENDING exploration task (`source=board_roadmap`, `confirmed_by_human=False`) assigned to the Product Owner. The normal board one-shot dispatch (`_dispatch_roadmap_exploration`) spawns the PO, who explores the charter/releases/metrics/projects and calls the `propose_roadmap` do-tool exactly once with a themed goal + 3-7 item drafts (persisted as an `orchestration_markers` payload). The CEO reviews the cycle in the panel's Roadmap Review Queue and approves/rejects each item individually via `/api/roadmap/cycles/{id}/items/{id}/{approve,reject}``RoadmapService`; an approved item materializes as a BACKLOG task (`source=roadmap`) through `PrompterService.create_task_from_draft` — nothing auto-starts, normal PM activation takes it from BACKLOG. Once every item is terminal, the exploration task itself completes.
**X (Twitter) flow (three originators, one held queue, default off).** Unlike every other engine on this page, `XEngine` never spawns an agent for release posts or mention replies — `draft_release_post` (event hook off `ReleaseProposalService.approve`'s publish-success branch) and `run_cycle` (periodic mentions poll, `Orchestrator._x_mentions_poll_loop`) both draft via a raw local-model chat completion, never a cloud LLM. The feature-spotlight half is the exception: `Orchestrator._x_feature_spotlight_loop` (dormant unless BOTH `x_engine_enabled` AND `x_feature_spotlight_enabled`) opens a DB context each `x_feature_spotlight_interval_seconds` and calls `XEngine.open_feature_spotlight_exploration`, which no-ops on the usual guards (creds, one-open-cycle dedup, the shared `x_max_open_posts` cap, project resolvability) or else opens ONE held PENDING exploration task (`source=x_feature_exploration`) assigned to the Head of Marketing, carrying a snapshot of already-covered feature slugs (`x_seen_features` marker). The board dispatcher's `_dispatch_pm_work` special-cases this source (mirroring `ROADMAP_SOURCE`) to call `_dispatch_feature_spotlight_exploration`, a one-shot spawn of the real Head-of-Marketing agent (full read tools) who investigates CHANGELOG.md/feature-flags/docs/map/charter/KB and calls the `propose_feature_spotlight` do-tool exactly once; that verb materializes a brand-new held draft task (`source=x_feature`) and completes the exploration task as a side effect — a deliberate asymmetry from `propose_roadmap`, which instead writes a marker onto the SAME task and leaves it open. Every draft from all three paths — release, reply, spotlight — lands in the identical held-task shape (`TaskTable`, `confirmed_by_human=False`, `assigned_to=secretary-1`, body in `orchestration_markers.x_draft_body`) rendered by the panel's X Post Queue and acted on only by `XPostService.approve`/`.reject`; nothing here ever calls `x_client.post_tweet` itself. `XEngine._voice_guide` (a live `CompanyGoalsService.get()` read, never hardcoded) feeds a baseline house-voice constant plus the CEO's optional `brand_voice` charter sample into every one of the two local-model prompts, and the Head of Marketing's own identity prompt points it at the same charter field for its cloud-LLM-authored spotlight body.
**Board Program flow (registry, no master flag, default off per program).** The orchestrator's `_board_program_loop` ticks `BoardProgramEngine.run_due_programs` on a floor interval (shortest registered cadence, clamped 300s-3600s). Per CRON program: `program_armed` (settings-store `board_program.{key}.enabled`, falling back to a legacy flag only for `roadmap`/`x_feature`) → `_scope_gate` (a `scope="project"` program needs at least one project with the key in `projects.board_programs`) → dedup against `board_program_cycles` (migration `087`, one open row per program, auto-closed once its exploration task goes terminal) → `program_due``_ORIGINATORS[key]` calls that program's own `run_cycle`, which opens ONE held PENDING exploration task assigned to the program's role (Product Owner: Pest Control/Spackle/Scales/Dogfood; Head of Marketing: Periscope/Megaphone/Mirror/Barfly/War Room; Auditor: Sentinel/Librarian) and records a fresh `board_program_cycles` row. `run_due_programs` separately evaluates every registered metric predicate (`_METRIC_PREDICATES`, today only Pest Control's 7-day rework-rate check against `ROBOCO_PEST_REWORK_THRESHOLD`) after the same scope/dedup gates, so an off-schedule accelerator never re-pays a multi-query metric check on a tick that was always going to be rejected. `open_program_cycle(key)` is the same path minus cron-due — used by the CEO panel's "run now" (`POST /api/board-programs/{key}/run-now`), the Strategy Engine's `idle` observation (Printer only — the design's `stranded_blocked` → Coroner fold was never wired), and Dogfood's release-publish hook. Coroner is the exception to the whole loop: its `trigger=event` means `program_due` always refuses it, and its ONLY real entry point is `CoronerEngine.open_for_incident`, called directly from three chokepoints — `TaskService`'s bounce-past-`revision_count>=3` transition, `TaskService`'s cancel-after-work path, and the orchestrator's budget-block path — never the cron loop. War Room's release cycle similarly bypasses `_ORIGINATORS` via `open_for_release`, called from the same release-publish hook as `draft_release_post`/Dogfood, carrying pre-curated highlights so campaign posts never invent a feature.
Every exploration task dispatches through `_dispatch_board_program_exploration` — a dict-dispatch table (not an `if`/`elif` chain, xenon budget) keyed by `task['source']`, routing to a dedicated one-shot spawner (`_dispatch_pest_control_exploration`, etc.) that bypasses `_handle_board_assigned_task`'s two-reviewer board-review gate entirely; every dispatcher shares the `_board_dispatched` one-shot tracker + respawn breaker. The agent calls its program's ONE proposal verb (`propose_bug_hunt`/`propose_gap_fill`/`propose_rebalance`/`propose_friction_fixes` for the PO; `propose_market_brief`/`propose_editorial_post`/`propose_messaging_fixes`/`propose_campaign`/`propose_conversation_replies` for the HoM; `propose_postmortem`/`propose_playbook_drafts`/`propose_quality_report` for the Auditor — all in `roboco/services/gateway/content_actions.py`) exactly once. Materialization varies by program: most (Pest Control/Spackle/Mirror/roadmap) create BACKLOG tasks with a per-item CEO decision identical to the roadmap flow; Scales instead MUTATES a live task in place on approval (reprioritize or cancel — never creates one); Periscope/Sentinel complete their exploration task in the same call as a held report with no per-item queue; Megaphone/Barfly/War Room/spotlight land in the existing X held-draft queue; Coroner materializes a held process-change item or drafts straight into the pending-playbook queue (`kind='playbook'`); Librarian drafts 1-3 real DRAFT playbooks directly via `PlaybookService`, bypassing `draft_playbook` entirely (an explicit invariant: the Auditor curates but does not draft, except here). LEARN closes the loop: `BoardProgramEngine.record_decision` accrues each CEO verdict onto the cycle row's `decisions` jsonb, and `prior_cycle_context` renders the last two closed cycles back into the NEXT cycle's exploration prompt.
Project-scoped programs (Pest Control/Spackle/Mirror/Dogfood) additionally use `pick_rotation_target` to round-robin across their opted-in projects — never-explored beats explored, else oldest `last_opened_at` wins, read from the programs' own exploration tasks (not the LEARN ledger, since a project-scoped engine's `run_cycle` can be called directly, outside the loop). `projects.board_programs` (migration `088`) governs opt-in/opt-out with dual polarity per `project_participates` — a plain key for a `scope="project"` program, `"!key"` to exclude a project from a `scope="org"` program's default-eligible output.
**Read-only views.** `KanbanService` builds role-specific boards from `TaskTable` queries on demand for the kanban API; `CompanyGoalsService.get` is read by the briefing injector into every agent's `context_briefing`.
## Mermaid
```mermaid
flowchart TD
Board[Board member] -->|create| Pitch[PitchService.create]
CEO -->|approve /api/pitch/:id/approve| Approve[PitchService.approve]
Approve --> Prov[GitHubProvisioningService.create_repo]
Prov --> GH[(GitHub org repos)]
Approve --> ProjReg[ProjectService.create per cell]
Approve --> Topo{_register_topology}
Topo -->|multi-cell| ProdCreate[ProductService.create]
Topo -->|single-cell| SeedProj[seed_project_id]
Approve --> Seed[_seed_main_pm_task]
Seed --> MainPmTask[PENDING Main-PM task source=pitch]
MainPmTask --> Dispatcher[normal delivery lifecycle]
subgraph StrategyLoop[dormant — strategy_engine_enabled]
Loop[Orchestrator._strategy_engine_loop] -->|interval| Cycle[StrategyEngine.run_cycle]
Cycle --> Assess[assess]
Assess --> Goals[(CompanyGoalsService.get)]
Assess --> Tasks[(TaskService list_in_progress / long_running_blocked)]
Assess -->|notify-only| NotifyCEO[CEO notification]
end
subgraph ResearchFlow[on-demand — research_enabled]
Agent[Board/PM agent] -->|roboco-search MCP| Route[/api/research/]
Route --> Quota[ResearchQuotaTracker.check_and_consume]
Quota --> Redis[(Redis INCR/day)]
Route --> RS[ResearchService]
RS --> Prov2{"Tavily|Brave|Exa|Null"}
Prov2 -->|provider API| Web[(Web)]
end
subgraph RoadmapLoop[dormant — roadmap_engine_enabled]
RLoop[Orchestrator._roadmap_engine_loop] -->|interval, default weekly| RCycle[RoadmapEngine.run_cycle]
RCycle -->|held PENDING task| PO[Product Owner spawn]
PO -->|propose_roadmap do-tool| Payload[(orchestration_markers cycle payload)]
CEO -->|approve/reject per item /api/roadmap| RSvc[RoadmapService]
RSvc -->|approve| Backlog[BACKLOG task via PrompterService]
RSvc -->|all items terminal| Complete[exploration task completes]
end
subgraph XEngineFlow[dormant — x_engine_enabled]
RelHook[ReleaseProposalService.approve publish] --> XDraftRelease[XEngine.draft_release_post]
XMentLoop[Orchestrator._x_mentions_poll_loop] -->|interval| XRunCycle[XEngine.run_cycle]
XDraftRelease --> XChat[local-model chat, _voice_guide]
XRunCycle --> XChat
XSpotLoop["Orchestrator._x_feature_spotlight_loop (x_feature_spotlight_enabled)"] -->|interval, default 3d| XOpen[XEngine.open_feature_spotlight_exploration]
XOpen -->|held PENDING task| HoM[Head of Marketing spawn]
HoM -->|propose_feature_spotlight do-tool| XMat[XEngine.materialize_feature_spotlight]
XMat --> XQueue[(held X post/reply/feature draft)]
XDraftRelease --> XQueue
XRunCycle --> XQueue
CEO -->|approve/reject /api/x/posts| XSvc[XPostService]
XSvc -->|approve, single-flight lock| Tweet[(x_client.post_tweet)]
end
subgraph BoardProgramLoop["Board Program registry — 14 entries, per-program settings-store arming"]
BPLoop[Orchestrator._board_program_loop] -->|floor interval| RunDue[BoardProgramEngine.run_due_programs]
RunDue --> Armed{program_armed settings-store}
Armed -->|CRON, scope+dedup+due| Origin[program._ORIGINATORS run_cycle]
RunDue --> MetricCheck[_run_due_metric_predicates: pest_control rework-spike]
MetricCheck --> Origin
CoronerHook["TaskService bounce/cancel + budget-block hook"] --> CoronerOpen[CoronerEngine.open_for_incident]
ReleaseHook[ReleaseProposalService.approve publish] --> WarRoomOpen[WarRoomEngine.open_for_release]
ReleaseHook --> DogfoodRun[DogfoodEngine.run_cycle]
Origin -->|held PENDING task| Explorer["PO / HoM / Auditor spawn (solo, board-review gate bypassed)"]
CoronerOpen -->|held PENDING task| Explorer
WarRoomOpen -->|held PENDING task| Explorer
Explorer -->|ONE propose_* verb| ProposeVerb[ContentActions.propose_*]
ProposeVerb --> Materialize{materializer}
Materialize -->|backlog tasks, per-item decision| Backlog[(BACKLOG task)]
Materialize -->|held report, no queue| Report[(CEO report)]
Materialize -->|held X draft| XQueue2[(X post queue)]
Materialize -->|mutate live task| LiveTask[(reprioritize / cancel)]
Materialize -->|playbook draft| PlaybookQ[(pending-playbook queue)]
CEO -->|approve/reject per item| Learn[BoardProgramEngine.record_decision]
Learn -->|LEARN| Ledger[(board_program_cycles.decisions)]
Ledger -->|prior_cycle_context| Origin
end
```
## Logical Tree
```
product-strategy-research-pitch
├── project.py — ProjectService
│ ├── CRUD (create/get/get_by_slug/get_or_raise/update/delete)
│ ├── _assert_git_url_allowed (protected denylist)
│ ├── _maybe_scaffold_conventions (flag-gated, non-fatal)
│ ├── queries (list_all / list_by_cell)
│ ├── workspace (set_workspace_path / update_sync_state)
│ ├── git token (get_decrypted_token / _by_slug) — Fernet, on-demand
│ └── access control (add/remove_allowed_agent / check_agent_access)
├── product.py — ProductService
│ ├── CRUD
│ ├── project_for (per-cell routing keystone)
│ ├── distinct_project_ids (Main-PM integration-branch set)
│ └── _replace_cells (idempotent map replace, DELETE-before-INSERT flush)
├── kanban.py — KanbanService
│ ├── _load_subtask_counts (batch child-count query, fixes always-0 stub)
│ ├── _task_to_card (accepts subtask_counts map)
│ ├── dev board (flat / swimlane by priority|assignee)
│ ├── qa / documenter / pm boards (flat, status-filtered; qa excludes VERIFYING; documenter scoped to task_type=documentation)
│ ├── main-pm board (cross-cell swimlane / flat team columns + Coordination column)
│ ├── board roadmap (P0/P1)
│ └── board stats
├── company_goals.py — CompanyGoalsService (singleton SINGLETON_ID)
│ ├── get (empty defaults if unset)
│ └── upsert (partial-key)
├── strategy_engine.py — StrategyEngine (dormant)
│ ├── assess (idle + stranded_blocked observations)
│ └── run_cycle (flag-gated, notify-only)
├── research.py
│ ├── SearchProvider ABC + Tavily/Brave/Exa/Null adapters
│ ├── ResearchService (clamping entry point)
│ ├── build_provider / get_research_service
│ └── helpers (_as_float / _truncated_fetch)
├── research_quota.py — ResearchQuotaTracker (Redis, fail-open)
│ └── check_and_consume (INCR-then-compare)
├── pitch.py — PitchService
│ ├── CRUD + list_pitches
│ ├── reject / approve
│ ├── _provision_repos (idempotent on re-approval)
│ ├── _register_topology (Product vs seed-project)
│ └── _seed_main_pm_task (PENDING Main-PM task)
├── github_provisioning.py — GitHubProvisioningService
│ ├── enabled (master+token+org)
│ └── create_repo (POST /orgs/{org}/repos, auto_init)
├── roadmap_engine.py — RoadmapEngine (dormant, roadmap_engine_enabled)
│ └── run_cycle (one held exploration task for the Product Owner; one-open-cycle dedup)
├── roadmap_service.py — RoadmapService
│ ├── list_open_cycles
│ ├── approve_item (materialize BACKLOG task, idempotent)
│ ├── reject_item (record reason, idempotent)
│ └── _maybe_complete_cycle (completes exploration task once all items terminal)
├── x_engine.py — XEngine (dormant, x_engine_enabled)
│ ├── _voice_guide (baseline + CEO brand_voice charter sample, live DB read)
│ ├── draft_release_post (event hook; local-model; dedup per version)
│ ├── run_cycle (mentions poll; x_replies_enabled sub-switch; bot/engagement filter)
│ ├── open_feature_spotlight_exploration (x_feature_spotlight_enabled sub-switch; one-open-cycle dedup; seen-features marker)
│ ├── materialize_feature_spotlight (called from propose_feature_spotlight; marks seen, holds draft, completes exploration)
│ └── _originate_post (shared held-task origination, all three sources)
├── x_post_service.py — XPostService
│ ├── approve (single-flight lock, idempotent, only caller of x_client.post_tweet)
│ └── reject (record reason, cancel draft)
├── x_client.py — XClient ABC / NullXClient / LiveXClient
│ └── build_x_client (creds present → LiveXClient, else NullXClient)
├── x_credentials.py — XCredentialsService (singleton, Fernet-encrypted)
│ ├── set_credentials (all-or-nothing)
│ └── get_decrypted (server-side only)
├── foundation/policy/board_programs.py — pure registry (no IO)
│ ├── BoardProgram (frozen dataclass) + PROGRAMS (14 entries)
│ ├── program_due (cron-due check)
│ ├── project_participates (dual-polarity scope predicate)
│ └── validate_board_programs_field
├── services/board_programs.py — BoardProgramEngine
│ ├── run_due_programs / _run_due_metric_predicates (cron + metric pass)
│ ├── open_program_cycle (enabled+scope+dedup, no cron-due — "run now"/idle-trigger seam)
│ ├── _scope_gate / opted_in_projects
│ ├── _dedup_state / _maybe_close / _latest_cycle (board_program_cycles ledger)
│ ├── record_decision / prior_cycle_context (LEARN)
│ ├── program_armed (settings-store arming chokepoint)
│ └── pick_rotation_target (shared project-scoped round-robin)
├── pest_control_engine.py / spackle_engine.py / scales_engine.py / dogfood_engine.py — Product Owner programs
│ └── each: run_cycle (CRON) + a program-specific evidence/context builder; dogfood_engine also binds a real _ORIGINATORS entry despite being event-only
├── periscope_engine.py / megaphone_engine.py / mirror_engine.py / barfly_engine.py / war_room_engine.py — Head of Marketing programs
│ └── each: run_cycle + context builder; war_room_engine also exposes open_for_release (release-hook bypass of _ORIGINATORS); barfly_engine screens candidates through injection_guard
├── coroner_engine.py / sentinel_engine.py / librarian_engine.py — Auditor programs
│ └── coroner_engine: open_for_incident is the ONLY real entry point (event-only, no run_cycle path through the loop); sentinel_engine/librarian_engine: run_cycle (CRON) + context builder
└── api/routes/board_programs.py — CEO-only status + run-now routes
```
## Dependencies
**Internal (roboco):**
- `roboco.config.settings` — all flags/caps (every file).
- `roboco.services.base.BaseService``Project/Product/Kanban/CompanyGoals/Strategy/Pitch` (log + session).
- `roboco.db.tables``ProjectTable`, `ProductTable`, `ProductProjectTable`, `TaskTable`, `AgentTable`, `CompanyGoalsTable`, `PitchTable`, `WorkSessionTable`.
- `roboco.models.*``ProjectCreate/Update`, `ProductCreate/Update/ProductCellMapping`, `TaskCreateRequest`, `PitchCreate/PitchStatus`, `base` (Team/TaskStatus/TaskType/Complexity/TaskNature), `kanban`.
- `roboco.foundation.identity.Team` (product), `roboco.foundation.policy…` indirectly via task.
- `roboco.utils.crypto``encrypt_token`/`decrypt_token`/`EncryptionError` (project).
- `roboco.utils.converters``require_uuid`/`to_python_uuid` (kanban, pitch).
- `roboco.services.conventions` — lazy-imported in `ProjectService._maybe_scaffold_conventions`.
- `roboco.services.work_session` — lazy in `ProjectService.delete`.
- `roboco.services.workspace` — lazy in `ProjectService.delete` (delete_workspaces).
- `roboco.services.task``StrategyEngine` (`list_in_progress_or_claimed`, `list_long_running_blocked`), `PitchService._seed_main_pm_task`; `RoadmapEngine`/`RoadmapService` (`ROADMAP_SOURCE`/`ROADMAP_ITEM_SOURCE`, `list_open_roadmap_cycles`, `TaskCreateRequest`).
- `roboco.services.prompter``RoadmapService._materialize` lazy-imports `get_prompter_service` (`create_task_from_draft`, the same confirmed-by-CEO-approval path pitch items use).
- `roboco.foundation.policy.content.markers``RoadmapService`/`api/routes/roadmap.py` (`get_roadmap_cycle`/`set_roadmap_cycle`, the cycle payload persisted on `orchestration_markers`).
- `roboco.foundation.identity``RoadmapEngine._originate` (`AGENTS["product-owner"]`/`AGENTS["system"]`).
- `roboco.services.agent``PitchService` (`get_by_slug("main-pm")`).
- `roboco.services.notification``StrategyEngine.run_cycle`.
- `roboco.services.github_provisioning``PitchService.approve`.
- `roboco.services.project` / `product``PitchService`.
- `roboco.runtime.orchestrator` — runs `_strategy_engine_loop` + `_roadmap_engine_loop`/`_dispatch_roadmap_exploration` + `_board_program_loop`/`_dispatch_board_program_exploration`; mounts `roboco-search` MCP when `research_enabled`, `playwright` MCP task-scoped for Dogfood.
- `roboco.services.gateway.content_actions.ContentActions` — the fourteen `propose_*` do-verbs (one per program) that author each program's proposal; `roboco.api.schemas.v1.do` — the matching `*Input`/`Propose*Request` pydantic schemas.
- `roboco.services.metrics.MetricsService.get_rework_metrics` — Pest Control's off-schedule metric predicate.
- `roboco.foundation.policy.injection_guard.screen_external_text` — Barfly screens every candidate conversation through it before the HoM's prompt sees it.
- `roboco.services.playbook.PlaybookService` — Coroner (`kind='playbook'`) and Librarian both draft directly into it, never through the `draft_playbook` do-tool.
**External:**
- `sqlalchemy` (async ext) — all DB-backed services.
- `httpx` — research providers + GitHub provisioning (async client, owned-or-injected).
- `redis.asyncio``ResearchQuotaTracker`.
- `pydantic` (via models) — `ProjectCreate/Update` etc.
- Provider HTTP APIs: Tavily, Brave Search, Exa, GitHub REST.
## Entry Points
- **Routes (`roboco/api/routes/`):**
- `project.py` — Project CRUD endpoints → `get_project_service`.
- `product.py` — Product CRUD endpoints → `get_product_service`.
- `kanban.py` — 7 board endpoints (dev/qa/documenter/pm/main-pm/main-pm-flat/board + stats) → `get_kanban_service`.
- `company_goals.py``GET /api/company-goals`, `PUT /api/company-goals` (CEO) → `get_company_goals_service`.
- `pitch.py` — pitch CRUD + `POST /api/pitch/{id}/approve` (CEO) + `/reject``get_pitch_service`.
- `research.py``POST /api/research/search`, `/fetch``get_research_service` + module-level `ResearchQuotaTracker`.
- `prompter_live.py``get_project_service` for project lookup during intake.
- `dashboard.py``get_product_service` / `get_project_service` for dashboard views.
- `roadmap.py``GET /api/roadmap/cycles`, `POST /cycles/{id}/items/{id}/{approve,reject}` (CEO-only) → `get_roadmap_service`.
- `x.py``GET /api/x/posts`, `POST /posts/{id}/{approve,reject}` (CEO-only) → `get_x_post_service`.
- `board_programs.py``GET /api/board-programs` (list all 14 with live status), `POST /api/board-programs/{key}/run-now` (CEO-only) → `get_board_program_engine`.
- **Orchestrator loop tick:** `_strategy_engine_loop` (orchestrator.py:6360) — created at `start()` (line 1010), cancelled in shutdown (line 1075); ticks every `strategy_engine_interval_seconds`, calls `StrategyEngine.run_cycle`. `_roadmap_engine_loop` (orchestrator.py:7462) — same lifecycle shape, ticks every `roadmap_interval_seconds` (default weekly), calls `RoadmapEngine.run_cycle`; `_dispatch_roadmap_exploration` (orchestrator.py:10284) spawns the Product Owner once per open exploration task. `_x_mentions_poll_loop` (orchestrator.py:7509) ticks every `x_mentions_interval_seconds`, calls `XEngine.run_cycle`. `_x_feature_spotlight_loop` (orchestrator.py:7571) — same lifecycle shape, dormant unless BOTH `x_engine_enabled` AND `x_feature_spotlight_enabled`, ticks every `x_feature_spotlight_interval_seconds` (default 3 days), calls `XEngine.open_feature_spotlight_exploration`; `_dispatch_feature_spotlight_exploration` (orchestrator.py:10424) spawns the Head of Marketing once per open exploration task — `_dispatch_pm_work` routes `source=x_feature_exploration` to it BEFORE the generic `_BOARD_AGENTS` check (mirroring the roadmap source's own early branch), so it never falls into the two-reviewer board-review gate. `_board_program_loop` (orchestrator.py:9224) — same lifecycle shape, ticks on a floor interval (`_board_program_interval_seconds`: shortest registered program cadence, clamped 300s-3600s), calls `BoardProgramEngine.run_due_programs`; `_dispatch_board_program_exploration` (a module-level dict-dispatch function, not a method — orchestrator.py:948) routes each program's held exploration task to its own one-shot dispatcher (`_dispatch_pest_control_exploration`, `_dispatch_periscope_exploration`, etc.), each spawning its program's role solo, bypassing the two-reviewer board-review gate exactly like the roadmap/spotlight dispatchers already did.
- **MCP mount (orchestrator spawn):** `roboco-search` MCP mounted into Board/PM agent containers only when `research_enabled` (orchestrator.py:2914); the MCP server calls the `/api/research/*` routes. `playwright` MCP mounted task-scoped (not role-blanket) for a `board_dogfood` spawn only, via `_is_dogfood_spawn` (orchestrator.py:3834).
- **Event hooks (bypass the loop entirely):** `TaskService`'s bounce-into-`needs_revision` transition and cancel-after-work path both call `CoronerEngine.open_for_incident` directly (`services/task.py:812` / `:1392`); the orchestrator's budget-block path calls it too (`orchestrator.py:8311`); `ReleaseProposalService.approve`'s publish-success branch calls `WarRoomEngine.open_for_release` (`services/release_proposal.py:330`) alongside the pre-existing `XEngine.draft_release_post` hook.
- **Service-to-service:** `ProjectService` called by `WorkspaceService`, `GitService`, `PitchService`, `task`, `docs`, `cockpit`, `secretary`, gateway choreographer; `ProductService.project_for` called from gateway delegate path; `CompanyGoalsService.get` called by briefing injector; `BoardProgramEngine` called by every program's own engine (dedup/record) and by `StrategyEngine.run_cycle` (Printer's `idle` trigger).
- **No CLI / lifespan entry points** for this slice.
## Config Flags
| Flag | Default | File:Line | Effect |
|------|---------|-----------|--------|
| `ROBOCO_CONVENTIONS_ENABLED` | `False` | config.py:227 | `ProjectService.create` best-effort scaffolds `.roboco/conventions.yml` PR; inert when off |
| `ROBOCO_RESEARCH_ENABLED` | `True` | config.py:245 | Mounts `roboco-search` MCP into Board/PM containers (orchestrator:2914) |
| `ROBOCO_RESEARCH_PROVIDER` | `tavily` | config.py:252 | `tavily`/`brave`/`exa`/`null` adapter selection |
| `ROBOCO_RESEARCH_API_KEY` | `None` | config.py:262 | Server-side only; unset → `NullProvider` |
| `ROBOCO_RESEARCH_MAX_RESULTS` | `5` | config.py:269 | Hard cap on `web_search` results (120) |
| `ROBOCO_RESEARCH_FETCH_MAX_CHARS` | `20000` | config.py:275 | Hard cap on `web_fetch` extracted chars |
| `ROBOCO_RESEARCH_TIMEOUT_SECONDS` | `15.0` | config.py:280 | Outbound provider HTTP timeout |
| `ROBOCO_RESEARCH_DAILY_QUOTA_PER_AGENT` | `50` | config.py:286 | Per-agent/day call ceiling (Redis, fail-open) |
| `ROBOCO_PROVISIONING_ENABLED` | `True` | config.py:309 | Pitch auto-provisioning master switch (still inert without token+org) |
| `ROBOCO_PROVISIONING_TOKEN` | `""` | config.py:316 | GitHub PAT for repo creation (server-side only) |
| `ROBOCO_PROVISIONING_ORG` | `""` | config.py:323 | GitHub org where repos are provisioned |
| `ROBOCO_GITHUB_API_BASE_URL` | `https://api.github.com` | config.py:327 | Override for GitHub Enterprise |
| `ROBOCO_PROVISIONING_TIMEOUT_SECONDS` | `30.0` | config.py:331 | Outbound GitHub provisioning timeout |
| `ROBOCO_PROVISIONING_REPO_PRIVATE` | `True` | config.py:336 | Whether provisioned repos are private |
| `ROBOCO_PROVISIONING_PROVIDER` | `github` | config.py:570 | Phase 4 forge parity: `github`/`gitlab`/`gitea` selects the concrete provisioning target via `_build_provider` |
| `ROBOCO_PROVISIONING_HOST` | `""` | config.py:579 | Self-hosted forge host (e.g. `gitlab.example.com`); `.enabled` additionally requires this when `provisioning_provider` is gitlab/gitea (ignored for github/gitlab.com) |
| `ROBOCO_STRATEGY_ENGINE_ENABLED` | `False` | config.py:348 | Master switch — loop never starts when off |
| `ROBOCO_STRATEGY_ENGINE_INTERVAL_SECONDS` | `1800` | config.py:354 | Seconds between strategy assessment passes |
| `ROBOCO_STRATEGY_STRANDED_BLOCKED_MINUTES` | `120` | config.py:360 | Blocked-task threshold for "stranded" observation |
| `ROBOCO_PROTECTED_GIT_URLS` | `[]` | config.py:770 | Denylist — `ProjectService` rejects `git_url` containing any entry |
| `ROBOCO_ENCRYPTION_KEY` | `""` | config.py:295 | Fernet key for git-token encrypt/decrypt |
| `ROBOCO_ROADMAP_ENGINE_ENABLED` | `False` | config.py:865 | Master switch — `_roadmap_engine_loop` never opens an exploration cycle when off |
| `ROBOCO_ROADMAP_INTERVAL_SECONDS` | `604800` | config.py:875 | Seconds between roadmap-exploration cycles (default weekly) |
| `ROBOCO_ROADMAP_MIN_ITEMS_PER_CYCLE` | `3` | config.py:880 | Minimum item drafts `propose_roadmap` must submit for a themed cycle |
| `ROBOCO_ROADMAP_MAX_ITEMS_PER_CYCLE` | `7` | config.py:885 | Maximum item drafts per cycle |
| `ROBOCO_PEST_REWORK_THRESHOLD` | `0.3` | config.py:1432 | 7-day rework rate above which Pest Control's metric predicate opens a cycle off-schedule, on top of its weekly cron. The ONLY env-settable knob among the twelve new Board Programs — every other one arms exclusively via its own settings-store row (`board_program.{key}.enabled`, no `ROBOCO_*_ENABLED` flag exists for them) |
## Gotchas
- **~~Pitch partial-failure orphans GitHub repos~~ — RESOLVED (536bbb64).** `GitHubProvisioningService.create_repo` now treats a GitHub 422 "name already exists" response as an idempotent signal: it calls `_fetch_existing_repo` and returns the existing repo's `ProvisionedRepo` instead of erroring. Combined with the Project-by-slug and Product-by-slug reuse already in place, re-approval is now idempotent end-to-end — no manual intervention needed. The initial partial failure still leaves an orphaned GitHub repo, but the re-approval path recovers it automatically.
- **`ResearchQuotaTracker` INCRs before the limit check (research_quota.py:65).** An over-limit call still increments the counter (documented as fine for a ceiling). It also fails open on any Redis error (`allowed=True`) — research must not break because the cache is down. The route-level `_quota_tracker` is a module-level singleton sharing one Redis connection across requests.
- **`ProductService._replace_cells` flushes DELETEs before INSERTs (product.py:143).** This is load-bearing: SQLAlchemy otherwise orders INSERTs before DELETEs for the same table, which would collide the new `(product_id, team)` rows with not-yet-deleted old ones on `uq_product_projects_product_team` and 409 on any re-mapping of a team. Refactoring away the intermediate flush reintroduces the 409.
- **~~`KanbanService._task_to_card` hardcodes `subtask_count = 0`~~ — FIXED (c71f9b3b / 536bbb64).** The new `_load_subtask_counts` method (kanban.py:47) batch-counts direct children per parent in a single grouped SQL query and passes the result map into each `_task_to_card` call; `has_subtasks` and `subtask_count` now reflect real data.
- **~~`KanbanService.get_main_pm_board_flat` drops non-backend/frontend/ux_ui tasks silently~~ — FIXED (536bbb64 / b3558d4e).** A "Coordination" column (kanban.py:480) now catches non-cell-team tasks (Main PM, Board, fullstack, system, …). The column routing uses a dict-dispatch (status-key wins over team-key, fallback `"coordination"`) so no card is built and discarded.
- **`ProjectService.delete` is gated by DB RESTRICT on tasks (project.py:282).** Callers must cancel tasks first or the DB raises IntegrityError (route maps to 409). Active work sessions are abandoned first; `delete_workspaces=True` does `shutil.rmtree` on resolved paths — destructive, opt-in, best-effort.
- **~~`ProjectService.update` skips None-set fields~~ — FIXED (536bbb64).** `git_token` semantics unchanged (empty string clears, `None` leaves unchanged). All other fields now use `model_dump(exclude_unset=True, exclude={"git_token"})``exclude_none=True` was removed (#197), so a field the caller explicitly sets to `None` now clears the stored value instead of being silently skipped.
- **Strategy loop sleeps a full interval before the first cycle (orchestrator.py:6375).** `await asyncio.sleep(interval)` runs before the first `run_cycle`, so on startup there is a guaranteed `strategy_engine_interval_seconds` delay before the first assessment.
- **`StrategyEngine.run_cycle` catches nothing itself; the orchestrator wraps each cycle in `except Exception` (orchestrator.py:6380).** A failing `assess` is logged and retried forever on the next tick — the CEO is never notified that the engine itself is broken.
- **`build_provider` returns `NullProvider` for an unknown provider name (research.py:326- 328).** A typo in `ROBOCO_RESEARCH_PROVIDER` (validated by pydantic pattern, so unlikely) would silently degrade to empty results rather than erroring.
- **`GitHubProvisioningService.enabled` requires master + token + org (github_provisioning.py:64).** `provisioning_enabled` defaults `True`, so the flag alone is not enough — an operator who toggles the flag without setting token/org still gets `enabled=False` and `approve` raises `ProvisioningDisabledError`.
- **`PitchService._seed_main_pm_task` requires a `main-pm` agent row (pitch.py:241-243).** If the agent slug is missing it raises `ValidationError` after provisioning has already happened — another partial-failure window (repos + Product created, no seed task).
- **The Strategy Engine's `stranded_blocked` → Coroner fold was designed but never wired.** The internal design spec (`docs/internal/specs/2026-07-24-board-programs-design.md` §3) proposed both `StrategyEngine` signals becoming Board Program triggers — `idle` → Printer (roadmap) and `stranded_blocked` → Coroner. Only the `idle`→roadmap half shipped (`strategy_engine.py:95-101`'s own docstring: "`stranded_blocked` stays notify-only (Coroner is Phase 2 — its event hook lands then)"). Coroner is reachable only through its own three chokepoints (bounce/cancel/budget-block); a long-stranded blocked task never triggers an autopsy on its own. Not a bug — a deliberately scoped-down Phase 1, but a real gap between the design doc and the shipped code worth knowing before assuming the fold is complete.
- **`XPostService.approve` did NOT check for a CANCELLED (already-rejected) task before Wave 5 (`11915f36`, PR #551).** Before the fix, approving a draft the CEO had already rejected would proceed straight to posting it — reachable via the Telegram inbound bridge's inline Approve button (targets a draft by id regardless of its current status) and equally via a replayed HTTP `POST /api/x/posts/{id}/approve`. The guard now returns `already_rejected` both before acquiring the lock and again after re-reading the task under lock.
## Drift from CLAUDE.md
- **Research provider set vs CLAUDE.md.** CLAUDE.md's "Technology Stack" / feature-flags section lists web research under `ROBOCO_RESEARCH_ENABLED` only; it does not enumerate the provider adapters (`tavily`/`brave`/`exa`/`null`) or the per-agent daily quota (`ROBOCO_RESEARCH_DAILY_QUOTA_PER_AGENT`, default 50). Code: config.py:252/286, research.py:310. Not a contradiction — an omission in the doc.
- **CLAUDE.md says the strategy engine "never spends, builds, or auto-approves" and is default-OFF.** Code matches exactly (`strategy_engine_enabled` default `False`, config.py:348; `run_cycle` notify-only, strategy_engine.py:92-106). No drift.
- **CLAUDE.md says pitch provisioning is gated by `ROBOCO_PROVISIONING_*`.** Code matches (`provisioning_enabled` + `_token` + `_org` + `_repo_private` + `_timeout_seconds`, config.py:309-336; `GitHubProvisioningService.enabled` requires all three, github_provisioning.py:64). No drift.
- **CLAUDE.md does not mention `ProductService.project_for` as the per-cell routing keystone**, though it does describe product cell-routing as a feature. Code: product.py:87, called from the gateway delegate path. Doc omission, not contradiction.
- **CLAUDE.md does not mention `ROBOCO_PROTECTED_GIT_URLS`** (the project denylist, config.py:770, project.py:40). Doc omission.
- **CLAUDE.md's service table does not list `KanbanService`, `CompanyGoalsService`, `StrategyEngine`, `ResearchService`, `PitchService`, `GitHubProvisioningService`.** The CLAUDE.md "Services" table is explicitly a non-exhaustive "Core services" list, so this is an acknowledged omission rather than drift.
- **No contradictions between CLAUDE.md claims and actual code were found in this slice.** All documented flags, defaults, and behaviors (default-off strategy engine, server-side- only keys, notify-only engine, pitch→provision→normal-lifecycle, CEO-only approve) match the code.
- **CLAUDE.md's "Board Program registry" entry documents the shipped scope accurately, including the `stranded_blocked`→Coroner gap.** Code matches: `program_armed` has no master flag (services/board_programs.py:274), the strategy-engine fold is `idle`-only (strategy_engine.py:95-101). No drift.
## Changes Since Baseline
`git log --oneline fd10cc862c2020b3f639cdb686d427b0198a2441..HEAD -- <slice files>` and `git diff --stat` for the nine in-scope files both return **empty** — no commit between the baseline (`fd10cc86` "Update ci.yml") and HEAD (`3aff6e04` "Chore: Close gaps (#285)") touched any file in this slice. The two commits ahead of baseline (`15effce0` "141 Gaps fill-in (#283)", `3aff6e04` "Chore: Close gaps (#285)") modified other files only.
**No logic-touching commits to list. Impact: none — this slice is byte-for-byte unchanged since the baseline.**
> Post-snapshot updates (since 2026-06-29): three commits landed on this slice's files.
> - `536bbb64` (Chore/all/logical gaps sweep #286, 2026-06-30): `github_provisioning.py` — added `_GITHUB_REPO_EXISTS_STATUS = 422` sentinel and `_fetch_existing_repo` method; `create_repo` now handles 422 "already exists" idempotently, resolving the orphaned-repo partial-failure risk (#83/#84). `pitch.py` docstring updated to reflect new idempotency guarantee. `project.py` `update()` — removed `exclude_none=True` from `model_dump` so explicit-None fields now clear stored values (#197).
> - `c71f9b3b` ([chore] logical-gaps: kanban board column coverage + status-class fixes, 2026-06-30): `kanban.py` — added `_load_subtask_counts` batch query; `_task_to_card` now takes a `subtask_counts` dict and populates real subtask counts (#198). Added "Other" fallback column in `_build_columns` to prevent any task-card from being built-then-discarded. `get_qa_board`: removed `VERIFYING` from QA statuses (dev self-verification, not a QA state). `get_documenter_board`: added `task_type == DOCUMENTATION` filter. `get_main_pm_board_flat`: broadened status filter to include `PENDING`/`CLAIMED`/`COMPLETED` and added proper column routing (incoming/distributed/done). Added "Coordination" column for non-cell-team tasks (#196).
> - `b3558d4e` ([chore] complexity: split 5 C-rank blocks to <=B, 2026-06-30): `kanban.py` `get_main_pm_board_flat` — refactored if/elif routing to a dict-dispatch (`status_col` + `team_col` maps) for xenon complexity gate; no functional change.
> - **v0.18.0** (2026-07-04): the X feature-spotlight content in this slice (`XEngine` feature-spotlight methods, `_x_feature_spotlight_loop`/`_dispatch_feature_spotlight_exploration`, migration 061, `x_feature_spotlight_enabled`) was authored directly into this file's Files/Key Symbols/Data Flow/Mermaid/Logical Tree/Entry Points sections at implementation time rather than landing as a dated delta — noted here for changelog continuity; the body text above is current as of this date. (Config Flags is unchanged — the X-engine flags live in deployment-tooling.md's comprehensive list, not here.)
> - `11915f36` (PR #551, Telegram V2 security follow-up, 2026-07-17): `x_post_service.py` — `XPostService.approve`/`_approve_locked` add a CANCELLED-task guard (pre-lock and re-checked under lock) returning a new `already_rejected` status, closing a live-reproduced approve-after-reject hole reachable via a stale Telegram Approve button (or a replayed HTTP call).
> - `57b9e76b` (#607, "release caption uses curated CHANGELOG headlines, not commit subjects"): `draft_release_post` used to feed `highlights=list(report.change_summary)` — raw per-commit subjects — so the announcement model parroted the top commit's literal text. New pure `changelog_highlights()` extracts the bold feature leads from the curated release entry (`report.drafted_changelog`); `approve()` prefers those, falling back to `change_summary` only when the changelog yields nothing. The video pipeline's captions were already good (the authoring dev reads the changelog directly) — this brings X captions to the same source.
> - `16fa018a` (#615, "real voice guide, slop ban, and caption craft"): release/reply drafts previously ran on a one-sentence voice stub while the reasoning-backed Head-of-Marketing voice guide only reached the off-by-default spotlight path. `_hom_voice`/`_HOM_VOICE_GUIDE` port the full VOICE GUIDE (confident-not-hedgy, one idea per post, no emoji spam, plain text, never invent facts) plus a slop-ban list (em dashes, "game-changer"/"seamless"/etc., exclamation pileups, "X isn't just Y" constructions, rule-of-three chains) and three fixed style exemplars into every drafting prompt; both prompts target well-under-240-chars so the 280 clamp never truncates mid-sentence. An empty `brand_voice` now nudges the CEO exactly once (a durable `system_settings` marker, `_BRAND_VOICE_NUDGE_KEY`) instead of silently shipping baseline voice forever; a failed reply draft now skips origination instead of shipping a generic "Thanks for the mention!".
> - `4585a248` (#648, "redraft loop on CEO reject"): `XPostService.reject` with a non-blank reason now schedules `XEngine.redraft_from_rejection` after its commit (`defer_after_commit`, fresh session, never blocks/fails the reject) — mirroring `VideoEngine.reauthor_from_rejection`. Deduped via `_redraft_already_open` (excludes CANCELLED, so a further redraft is allowed once THAT one is itself rejected) under a per-identity Redis lock (`_acquire_redraft_lock`/`_REDRAFT_RELEASE_SCRIPT`, mirroring `XPostService`'s own lock style) so two racing rejects can't stack duplicate drafts. A local-model failure or empty revision output originates nothing — a degraded copy is worse than none.
> - `7e01c0ce` (PR #570, "project-branded drafts + project badges", 2026-07-18): migration 075 adds `company_goals.company_name`; `CompanyGoalsService.resolve_product_name` (company_goals.py:79) is the new single fallback chain (project name → charter `company_name` → "RoboCo") consumed by both `XEngine._voice_guide`/`draft_release_post` and `VideoEngine` (see `docs/map/video-engine.md`) so their prompt builders stop hardcoding "RoboCo". New `roboco/api/schemas/project_fields.py`'s `task_project_fields` helper adds `project_slug`/`project_name` to the X and video post-queue API responses (`api/routes/x.py`, `api/routes/video.py`); the panel renders them via a shared `ProjectBadge` — see `docs/map/panel.md`.
> - `461a6e1a`+`96401f4c`+`5f32d876` (Phases 1/2-3/4, 2026-07-18/19, #571/#575/#581) — Phase 4 makes `GitHubProvisioningService` provider-aware: `_build_provider` (github_provisioning.py:68) dispatches to `GitHubProvider`/`GiteaProvider`/`GitLabProvider` by `ROBOCO_PROVISIONING_PROVIDER`, `.enabled` additionally requires `ROBOCO_PROVISIONING_HOST` for gitlab/gitea, and `_is_already_exists` (github_provisioning.py:61) matches the "already exists" idempotency signal across all three forges' differing status codes/phrasing. The forge transport package itself (`GitProvider`/`ForgeRouter`/provider implementations) is documented in `docs/map/worksession-git.md` — this slice only covers the provisioning consumer.
> - `a0baf94b` ("agnosticism-residue", agnosticism audit items B6/B8): `x_engine.py`'s remaining hardcoded `"RoboCo"` literals (the reply-prompt builder and the feature-spotlight exploration description — `draft_release_post`/`_voice_guide` were already fixed by `7e01c0ce` above) are threaded out: `_reply_prompt` gains a `product_name` param, `_FEATURE_EXPLORATION_DESCRIPTION` (a module constant) becomes `_feature_exploration_description(product_name)` (a function), and `run_cycle`/`open_feature_spotlight_exploration` each resolve `product_name` once via `resolve_product_name` and thread it through.
> - **Board Program registry (2026-07-24, #689/#699 + the Phase 2/3 program train).** The single largest addition to this slice since the baseline: `foundation/policy/board_programs.py` (`BoardProgram`/`PROGRAMS`/`program_due`/`project_participates`) + `services/board_programs.py` (`BoardProgramEngine`) + `api/routes/board_programs.py` generalize the roadmap/spotlight shape into one registry-driven engine (migrations `087` `board_program_cycles` LEARN ledger, `088` `projects.board_programs` scoping column), migrating `roadmap` and `x_feature` onto it byte-for-byte (Phase 1) before adding twelve new programs across all three Board roles (Phase 2/3): Pest Control/Spackle/Scales/Dogfood (`pest_control_engine.py`/`spackle_engine.py`/`scales_engine.py`/`dogfood_engine.py`, Product Owner), Periscope/Megaphone/Mirror/Barfly/War Room (`periscope_engine.py`/`megaphone_engine.py`/`mirror_engine.py`/`barfly_engine.py`/`war_room_engine.py`, Head of Marketing), and Coroner/Sentinel/Librarian (`coroner_engine.py`/`sentinel_engine.py`/`librarian_engine.py`, Auditor). Arming has no master flag — `program_armed` reads a per-program settings-store row exclusively, except `roadmap`/`x_feature`'s legacy env-flag fallback. `StrategyEngine.run_cycle`'s `idle` observation now also triggers a Printer cycle via `BoardProgramEngine.open_program_cycle("roadmap")` (the `stranded_blocked`→Coroner half of the same design was NOT built — see Gotchas). Fourteen new `propose_*` do-verbs land in `content_actions.py` + `api/schemas/v1/do.py`; the Playwright MCP grant is task-scoped to Dogfood only, not a role-wide product_owner grant.
## Regression Risks
No commit since `fd10cc86` modified any file in this slice, so there are **no recent-change regressions** to flag. The table below lists *standing* structural risks already present in the code (not introduced by recent changes) that a future change in this slice or a caller could trip.
| Title | File:Line | Claim | Severity |
|-------|-----------|-------|----------|
| ~~Pitch partial-failure orphans GitHub repos~~ **RESOLVED 536bbb64** | pitch.py / github_provisioning.py | `create_repo` now handles GitHub 422 "already exists" by fetching the existing repo; re-approval is idempotent end-to-end. Initial partial failure still orphans the repo on GitHub, but re-approval recovers it automatically. | ~~medium~~ |
| Seed-task failure after provisioning | pitch.py:241-243 | `_seed_main_pm_task` raises `ValidationError` if `main-pm` agent is missing — after repos + Product are already created. Another partial-failure window with no rollback. | medium |
| Strategy engine failure is silent | orchestrator.py:6380, strategy_engine.py:92 | A failing `assess` is caught by the orchestrator's broad `except Exception`, logged, and retried next tick; the CEO is never notified that the engine is broken — looks dormant while actually erroring. | low |
| Quota INCR-then-compare + fail-open | research_quota.py:51-73 | Over-limit calls still bump the counter (documented); Redis outage fails open (`allowed=True`), so a quota bypass during a Redis outage is by design. | low |
| `_replace_cells` flush ordering is load-bearing | product.py:135-143 | The intermediate `flush()` (DELETEs before INSERTs) prevents a 409 on `uq_product_projects_product_team`. Refactoring it away reintroduces the unique-constraint collision on any team re-mapping. | low |
| ~~Flat main-PM board drops non-cell teams~~ **RESOLVED 536bbb64/b3558d4e** | kanban.py | Added "Coordination" column; dict-dispatch routing ensures all tasks are placed. | ~~low~~ |
| ~~`ProjectService.update` skips None-set fields~~ **RESOLVED 536bbb64** | project.py | `exclude_none=True` removed from `model_dump`; explicit-None fields now clear stored values (#197). | ~~low~~ |
| ~~`subtask_count` always 0 in kanban cards~~ **RESOLVED c71f9b3b/536bbb64** | kanban.py | `_load_subtask_counts` batch-loads real child counts; `_task_to_card` uses them. | ~~low~~ |
## Health
This slice is internally coherent and consistent with CLAUDE.md: every documented flag, default, and behavior matches the code, and the two slices-of-flow (CEO-driven pitch origination into the normal lifecycle; dormant notify-only strategy watcher) are cleanly separated and default-safe. The services follow a uniform `BaseService` + session-bound factory pattern, provider/research quotas fail open where cost-control (not security) is the goal, and the provisioning path is inert without token+org. The pitch approval path's external-side-effect non-atomicity remains (GitHub repo creation cannot roll back with the DB transaction), but re-approval is now idempotent end-to-end: `create_repo` handles GitHub 422 "already exists" by fetching the existing repo, and Project/Product rows are reused by slug, so a CEO re-approving after a partial failure recovers cleanly. The remaining open risk is `_seed_main_pm_task` failing after repos are already created (missing `main-pm` agent row). Post-snapshot three commits updated this slice's files, resolving four standing risks (kanban subtask counts, flat-board dropped cards, `project.update` None-field skip, and the pitch re-approval collision).