Task — engineering-spec@1

"OKR Monday-morning CUO digest — auto-progress + check-ins → founder summary delivered via email/chat at 08:00 Monday"

draftTASK-OKR-006
module okr · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-OKR-005, TASK-CUO-101 · blocks none

§1 — Description (BCP-14 normative)

The OKR service MUST ship Monday digest at services/okr/src/digest/ triggered 08:00 Monday tenant_tz, assembled from TASK-OKR-004/005, summarized via TASK-CUO-101, delivered via email + chat, 4 memory audit kinds.

  1. MUST schedule cron Monday 08:00 tenant_tz per DEC-2010 via TASK-MCP-007.
  1. MUST validate digest_delivery against closed enum per DEC-2011.
  1. MUST assemble at assembler.rs::assemble(tenant, week) per DEC-2012:
  1. MUST summarize via TASK-CUO-101 + TASK-AI-003 per DEC-2013 — structured prompt with sections.
  1. MUST deliver per recipient preference per DEC-2010 at deliverer.rs::deliver:
  1. MUST define tables at migration 0006: ```sql CREATE TABLE okr_digest_recipients ( tenant_id UUID NOT NULL, user_id UUID NOT NULL, delivery_pref TEXT NOT NULL DEFAULT 'both' CHECK (delivery_pref IN ('email','chat','both','none_skipped')), enabled BOOLEAN NOT NULL DEFAULT true, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, user_id) ); ALTER TABLE okr_digest_recipients ENABLE ROW LEVEL SECURITY; CREATE POLICY recipients_rls ON okr_digest_recipients USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); GRANT UPDATE (delivery_pref, enabled, updated_at) ON okr_digest_recipients TO cyberos_app;

CREATE TABLE okr_digest_runs ( run_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, iso_week CHAR(8) NOT NULL, content_jsonb JSONB NOT NULL, recipients_count INT NOT NULL, delivered_count INT NOT NULL, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, iso_week) ); ALTER TABLE okr_digest_runs ENABLE ROW LEVEL SECURITY; CREATE POLICY digest_runs_rls ON okr_digest_runs USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON okr_digest_runs FROM cyberos_app; ```

  1. MUST emit 4 memory audit kinds per DEC-2014. PII per TASK-MEMORY-111: content text SHA-256 hashed.
  1. MUST thread trace_id from cron → assembler → CUO → deliverer → audit.
  1. MUST NOT send to non-opted-in users per DEC-2010 (recipient table is opt-in).
  1. MUST NOT skip TASK-CUO-101 per DEC-2013 (raw assembled data not delivered).
  1. MUST be idempotent per week (UNIQUE on iso_week).

§2 — Why this design

Why Monday 08:00 (DEC-2010)? Founders open inbox first; digest sets the week's priorities.

Why 4 sections (DEC-2012)? Bounded; addresses CEO's typical questions (risk + win + gaps).

Why CUO tone (DEC-2013)? Direct AI output feels robotic; CUO adds context + framing.

Why opt-in (DEC-2010)? Not every founder wants email noise; let them choose chat-only or skip.


§3 — API contract

PUT  /v1/okr/digest/recipients/{user_id}   body: {delivery_pref, enabled}
GET  /v1/okr/digest/runs                   (list past digests)
POST /v1/okr/digest/trigger                (CEO manual trigger)

Sample digest content:

{
  "iso_week": "2026-W20",
  "sections": {
    "at_risk": [
      {"kr_id": "uuid", "title": "Q2 revenue $500k", "confidence": 4, "trend": "declining"}
    ],
    "making_progress": [...],
    "missing_checkins": ["KR-A", "KR-B"],
    "drift_alerts": [{"kr_id": "uuid", "drift_pct": 25.5}]
  },
  "cuo_summary": "We're tracking well on engineering velocity but Q2 revenue confidence dropped 3 points..."
}

§4 — Acceptance criteria

  1. digest_delivery enum cardinality 4. 2. Monday 08:00 cron. 3. Recipient opt-in via table. 4. 4 sections assembled. 5. CUO summary generated. 6. Email delivery via TASK-EMAIL-009. 7. Chat delivery via TASK-CHAT-005. 8. 'both' sends both. 9. 'none_skipped' logs only. 10. 4 memory audit kinds emitted. 11. PII scrubbed (content SHA256). 12. RLS denies cross-tenant. 13. CEO-only manual trigger. 14. Trace_id preserved. 15. UNIQUE(tenant_id, iso_week). 16. Append-only runs via REVOKE. 17. Empty sections degrade gracefully. 18. CUO failure → degrade to raw content + sev-2. 19. Delivery failure per recipient isolated. 20. Recipient pref updates effective next run.

§5 — Verification

#[tokio::test]
async fn assembles_4_sections() {
    let ctx = TestContext::with_krs_at_risk_and_progress().await;
    let digest = ctx.assemble_digest(this_week()).await;
    assert!(digest.sections.at_risk.len() <= 3);
    assert!(digest.sections.making_progress.len() <= 3);
}

#[tokio::test]
async fn delivers_per_recipient_pref() {
    let ctx = TestContext::with_3_recipients_diff_prefs().await;
    ctx.run_monday_digest().await;
    let email_sent = ctx.email_send_count().await;
    let chat_sent = ctx.chat_send_count().await;
    assert_eq!(email_sent, 2);  // email + both
    assert_eq!(chat_sent, 2);   // chat + both
}

#[tokio::test]
async fn idempotent_per_week() {
    let ctx = TestContext::with_recipients().await;
    ctx.run_monday_digest().await;
    ctx.run_monday_digest().await;
    let runs = ctx.fetch_digest_runs(this_iso_week()).await;
    assert_eq!(runs.len(), 1);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-OKR-005, TASK-CUO-101. Cross-module: TASK-OKR-004 (drift alerts), TASK-EMAIL-009 (email), TASK-CHAT-005 (chat), TASK-MCP-007 (cron), TASK-AI-003 (LLM via CUO), TASK-AUTH-101 (CEO role), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Cron skippedcatch-upsev-3inherent
Duplicate runUNIQUEskipinherent
0 KRs in tenantinherentskip digestinherent
CUO summarize failsretrydegrade to raw content + sev-2inherent
Email send failsper-recipient isolationsev-2inherent
Chat send failsper-recipient isolationsev-2inherent
Empty sectioninherentomit sectioninherent
Recipient disabledfilterskipinherent
Cross-tenant recipientRLS0 rowsinherent
Content > 50k charstruncatesev-3inherent

§11 — Implementation notes


End of TASK-OKR-006 spec.