feat(board): gate CEO Approve & Start on board-review completion

A board/coordination task stays pending throughout board review — that
pending state is what hands it to Main PM on approval — so the CEO's
Approve & Start button was live from the instant the task was created,
before the Product Owner and Head of Marketing had reviewed anything.
That let the CEO approve before the board finished.

Persist a board_review_complete flag the orchestrator sets once BOTH
board reviewers are done, and gate the button on it (the task stays
pending). The same handoff emits the formal CEO notification, so the
CEO gets an actionable signal instead of buried channel chatter.

- alembic 021: add tasks.board_review_complete (default false)
- TaskService.mark_board_review_complete: set the flag without leaving pending
- orchestrator: flag the task + notify CEO once both reviewers go idle
- panel: Approve & Start requires board_review_complete
This commit is contained in:
Renn F
2026-06-03 08:06:41 +02:00
parent 056ff41293
commit ceb4eec6ca
10 changed files with 181 additions and 69 deletions
@@ -0,0 +1,40 @@
"""Add tasks.board_review_complete — the board-review handoff flag.
A board/coordination task (no repo of its own, carries a product) is reviewed
by BOTH the Product Owner and the Head of Marketing before the CEO hands it to
Main PM. The task stays ``pending`` throughout — that pending state is what
drives Main PM dispatch once the CEO approves. The CEO's Approve & Start button
must NOT appear until the board has actually finished reviewing, so we persist a
flag the orchestrator sets once both reviewers are done. Defaults to False;
existing rows backfill to False (no board task has been reviewed retroactively).
Revision ID: 021_task_board_review_complete
Revises: 020_backfill_enum_values
Create Date: 2026-06-03
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
revision = "021_task_board_review_complete"
down_revision = "020_backfill_enum_values"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"tasks",
sa.Column(
"board_review_complete",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)
def downgrade() -> None:
op.drop_column("tasks", "board_review_complete")