Skip to main content

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 has run_after timestamptz (delayed execution) and atomic FOR UPDATE SKIP LOCKED claiming - 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.reconcile is 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

  1. Durable scheduler - periodic per-project "tick" jobs on the existing queue (a new KIND_PROJECT_TICK), enqueued with run_after and re-armed each run. Survives restarts, at-least-once, and runs a given tick on exactly one worker.
  2. Self-healing coordination reconcile - each tick re-runs Coordinator.reconcile for the project, so ready work still gets delegated and completed milestones still close even if a webhook was missed. Idempotent; skips archived/deleted projects.
  3. 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.
  4. 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.
  5. 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.
  6. 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)

  1. Scheduler mechanism. Extend the existing durable queue (enqueue a project_tick with run_after, re-armed each run) vs. a separate in-process asyncio ticker. 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.
  2. 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.
  3. 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, mirroring notified_milestones.
  4. 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 LOCKED claim (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_after correctness (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.