Milestone 5 - The Proactive Product Owner (Autonomous Scheduling)
Status: delivered (v0.5.0). A durable per-project scheduler (services/scheduler.py) arms a
deduplicated maintenance tick on the existing job queue; Orchestrator.handle_project_tick
self-heals coordination, then (opt-in, outside quiet hours) detects/escalates stalled work and
sends periodic digests. One scope refinement from the plan: scheduling uses an in-process
heartbeat that enqueues durable per-project ticks (the tick work runs on one worker via the
queue's atomic claim), rather than a self-perpetuating queue job - simpler and correct across
restarts and instances.
Theme: Make Bloom proactive, not just reactive. Today every action Bloom takes is
triggered by an inbound webhook - a user message, a PR, or an issue event. Between events Bloom
goes silent: it cannot nudge a ticket that has been stuck for three days (no webhook fires for
"nothing happened"), cannot send a periodic progress digest, and cannot self-heal when an
at-least-once webhook is dropped or missed. M5 introduces a durable scheduler so Bloom
periodically checks in on every active project - reconciling coordination, detecting stalls,
and keeping the owner informed - behaving like a Product Owner who follows up, not one who only
answers when spoken to.
This delivers the PRD's "Autonomous scheduling" future feature, and it closes a specific M4 deferral: time-based stall detection (M4-7 surfaced label-based health but explicitly deferred PR-age / time-based stalls "which would need PR timestamps").
Why this milestone
The PRD and the running system are reactive-only:
- FR-10 (Notifications) promises Bloom "shall proactively notify" on blocked/escalated work and progress - but every notification today is a side effect of a webhook Bloom happens to receive. Nothing fires when work simply stops.
- Guiding Principles: Bloom should "maintain a living understanding of the project throughout its lifecycle" and "escalate to them only when a decision or unblocking genuinely needs a human." Both require Bloom to look at the project on its own cadence.
- Success Metrics reward users who "continue using Bloom throughout the project lifecycle" and "report reduced effort." A PO that follows up proactively drives both; a silent one does not.
- Coordination robustness: delivery is at-least-once and webhooks can be missed entirely (a GitHub outage, a dropped delivery). Without a periodic reconcile, a project can quietly stall with ready work undelegated or a finished milestone left open.
The infrastructure is already in place, so M5 is an extension, not new plumbing:
- The durable job queue (
persistence/jobs.py) already hasrun_after timestamptz(delayed execution) and atomicFOR UPDATE SKIP LOCKEDclaiming - so scheduled jobs are a natural add, and the atomic claim makes a tick run on exactly one worker even across multiple API instances. - The JobRunner pool already drains that queue with retries/backoff/dead-lettering.
Coordinator.reconcileis already deterministic and idempotent - safe to run on a timer.- M4-7 already surfaces plan health (escalated / blocked / changes-requested / in-progress) from live GitHub state; digests and nudges reuse it.
- The
EventBus/SSE stream already exists to reflect proactive activity on the dashboard.
One-liner: Every active project gets a periodic heartbeat: Bloom re-reconciles the plan, flags work that has stalled, and sends the owner a concise digest - on its own, without waiting for a webhook.
Scope
In scope
- Durable scheduler - periodic per-project "tick" jobs on the existing queue (a new
KIND_PROJECT_TICK), enqueued withrun_afterand re-armed each run. Survives restarts, at-least-once, and runs a given tick on exactly one worker. - Self-healing coordination reconcile - each tick re-runs
Coordinator.reconcilefor the project, so ready work still gets delegated and completed milestones still close even if a webhook was missed. Idempotent; skips archived/deleted projects. - Time-based stall detection - classify tickets as stalled / at-risk from GitHub
timestamps (issue
updated_at, PR age) against configurable thresholds - the piece deferred from M4-7. - Proactive nudges & escalation - notify the owner once per stall (fenced, like
notified_milestones, so no spam) and escalate per FR-12 when a ticket is genuinely stuck; emit dashboard events. - Periodic status digest - an opt-in, cadence-configurable digest (e.g. daily) of progress
- plan health to the owner (Telegram) and the dashboard; suppressed when nothing has changed.
- Cadence, quiet-hours & opt-in config - per-deployment settings with sensible defaults; proactive messaging is opt-in and never sent during quiet hours.
Out of scope (future / deferred)
- Full cron expressions or per-user custom schedules - M5 ships fixed cadences + thresholds.
- Autonomous plan changes without approval - M4's approval gates stay; the scheduler nudges and reconciles, it does not rewrite scope on its own.
- The production engineering engine behind the swarm (its own track; explicitly out of scope in the PRD).
Key decisions to lock (asked before building)
- Scheduler mechanism. Extend the existing durable queue (enqueue a
project_tickwithrun_after, re-armed each run) vs. a separate in-processasyncioticker. Recommendation: the durable queue - it already gives persistence, retries, and single-worker execution via the atomic claim, so it is correct across restarts and multiple instances with no new infrastructure. - Tick cadence & stall thresholds. A single project heartbeat (e.g. every 15-60 min) drives reconcile + stall checks; the digest is a coarser cadence (e.g. daily). Thresholds (stall-after N hours) are config with defaults. Recommendation: one heartbeat job per project for reconcile/stalls, a separate daily digest job; all cadences/thresholds configurable.
- Anti-spam. Every proactive message is fenced (notify once per stall; digest suppressed on
no change) reusing the
notified_*pattern. Recommendation: fence in run state, mirroringnotified_milestones. - Opt-in & quiet hours. Proactive messaging is opt-in per deployment, with a quiet-hours window (owner timezone). Recommendation: scheduler/self-healing on by default, proactive messages off until explicitly enabled, and a sane quiet-hours default once enabled.
I'll default to the recommendations if you don't object, and note the decision in each ticket.
Tickets
Ordered by dependency. Each is bite-sized and independently reviewable.
M5-1 - Durable scheduler foundation
Add delayed/recurring scheduling on the existing queue: an enqueue that accepts run_after, a
new KIND_PROJECT_TICK, and a scheduler that arms one tick per active project and re-arms it each
run. The JobRunner dispatches the tick to a new orchestrator entry point.
AC: a tick is enqueued with a future run_after and claimed only when due; re-armed after each
run; runs on exactly one worker (atomic claim); in-memory + Postgres queues both covered; unit
tests for arming/claiming/re-arming.
M5-2 - Self-healing coordination reconcile on tick
The project-tick handler re-runs Coordinator.reconcile for the project (delegate ready work,
roll up finished milestones), idempotently, skipping archived/deleted projects and no-op when
GitHub/coordinator is unconfigured.
AC: a missed issue-close webhook is recovered on the next tick (milestone rolls up; unblocked
work delegates); archived projects are untouched; idempotent (a tick with nothing to do makes no
changes); integration test drives a tick and asserts the reconcile.
M5-3 - Time-based stall detection
Classify open tickets as stalled or at-risk from live GitHub timestamps (issue updated_at;
open-PR age for in-review work) against configurable thresholds, extending M4-7's label-based
health with a time dimension.
AC: a ticket untouched past the threshold is reported stalled; recently-updated work is not;
threshold configurable; unit tests over a synthetic GitHub snapshot with timestamps.
M5-4 - Proactive stall nudges & escalation
On a tick, notify the owner about newly-stalled work once (fenced so a persistent stall is not re-sent every tick) and escalate a genuinely-stuck ticket per FR-12; emit dashboard events. AC: first detection notifies; subsequent ticks for the same stall are silent until it changes; escalation path fires for stuck work; SSE event emitted; tests for notify-once and re-notify-on- change.
M5-5 - Periodic status digest
An opt-in, cadence-configurable digest job summarizing progress + plan health (reusing M4-7's surfacing) to the owner (Telegram) and the dashboard; suppressed when nothing changed since the last digest. AC: a digest is sent on cadence with live counts + health; suppressed when the project is unchanged since the previous digest; opt-in respected; tests for content and suppression.
M5-6 - Cadence, quiet-hours & opt-in config
Settings for the heartbeat/digest cadences, stall thresholds, opt-in flag, and a quiet-hours
window (owner timezone); wired through the app factory with documented defaults.
AC: cadences/thresholds/opt-in/quiet-hours are configurable via env with sane defaults; no
proactive message is sent during quiet hours or when opted out; test_config covers the new
knobs; .env.example and deployment docs updated.
M5-7 - Dashboard surface for proactive activity
Reflect scheduled activity on the dashboard: digest/nudge/reconcile events on the SSE stream and a "last checked / next check" indicator in the project read model. AC: proactive events appear on the live stream; the project detail exposes last/next check; scoped to the owner; API tests cover the read model.
M5-8 - Docs, PRD reconcile & version bump (0.5.0)
Update the PRD (FR-10 proactivity; a new FR-20 Proactive Monitoring & Scheduling; move
"Autonomous scheduling" out of Future Features), architecture (the scheduler + tick loop), this
milestone doc's status, the docs index, and the CHANGELOG; bump the version to 0.5.0.
AC: docs reflect delivered M5; version bumped in pyproject/__init__; CHANGELOG entry;
milestone doc linked from the docs map.
Risks / watch-list
- Multi-instance duplication (M5-1). Two API instances must not both run a project's tick -
rely on the queue's atomic
FOR UPDATE SKIP LOCKEDclaim (one worker per job); never a naive in-process timer per instance. - Notification spam (M5-4/M5-5). A persistent stall or an unchanged project must not produce a message every tick - every proactive send is fenced in run state.
- Clock /
run_aftercorrectness (M5-1). Re-arming must not drift or pile up duplicate ticks; a tick should be idempotent and self-limiting. - Timezone (M5-6). Quiet hours and the daily digest need the owner's timezone; default sanely and make it configurable.
- Never act on archived/deleted projects (M5-2). The tick must respect lifecycle and stop cleanly when a project is archived or deleted.
- Cost/noise of live GitHub reads on every tick - keep one snapshot per tick (as the read models already do) and back off cadence sensibly.
Alternatives considered (other PRD Future Features)
Chosen: Autonomous scheduling - highest cohesion with the just-shipped M4 (reuses live health surfacing, resolves the deferred time-based stall detection), well-seated in the existing durable queue, independent of the out-of-scope engineering engine, and directly advances the "proactive PO" guiding principle and lifecycle-retention success metric.
- Production engineering engine behind the swarm - highest raw value, but the PRD explicitly scopes it "out of this document," and it is a large separate track, not a clean single milestone.
- Specialist review agents (Architecture / Security / QA) - strong follow-on for review depth, but partly hollow while engineer deliverables are simulator stubs; better paired with a real engine.
- Team / organization accounts - valuable extension of M3, but billing/roles is a large, separable concern less aligned with the PO-effectiveness arc.
- Additional sign-in methods / GitLab / Slack-Discord / multi-repo / voice / calendar - each is either small (auth methods, already provider-agnostic), an integration swap behind an existing seam (GitLab, Slack/Discord), or a larger architectural change (multi-repo) with lower near-term product leverage than proactivity.