Task — engineering-spec@1

"CRM CUO crm.next-action@1 skill — AI-ranked top-3 next moves per open deal with rationale and deep-links"

draftTASK-CRM-005
module crm · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-CRM-001, TASK-CUO-101 · blocks none

§1 — Description (BCP-14 normative)

The CRM service MUST ship crm.next-action@1 skill at services/crm/src/next_action/ returning top-3 AI-ranked actions per deal, grounded in deal context, rate-limited, 3 memory audit kinds.

  1. MUST register skill at CUO via skill_handler.rs::register() per DEC-1650 — invoked via CUO crm.next-action@1.
  1. MUST validate next_action_kind against closed enum per DEC-1651.
  1. MUST build context at context_builder.rs::build(deal_id):
  1. MUST rank at ranker.rs::rank(context) via TASK-AI-003 with structured prompt:
  1. MUST enforce rate limit per DEC-1654 — 100 calls/user/day; return 429 when exceeded.
  1. MUST define table at migration 0005: ``sql CREATE TABLE crm_next_action_suggestions ( suggestion_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, deal_id UUID NOT NULL, suggestions JSONB NOT NULL, requested_by UUID NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','executed','dismissed','expired')), executed_kind TEXT CHECK (executed_kind IS NULL OR executed_kind IN ('send_email','schedule_call','share_proposal','request_intro','escalate_to_decision_maker','follow_up','mark_lost')), executed_at TIMESTAMPTZ, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX next_action_user_day_idx ON crm_next_action_suggestions(tenant_id, requested_by, created_at DESC); ALTER TABLE crm_next_action_suggestions ENABLE ROW LEVEL SECURITY; CREATE POLICY next_action_rls ON crm_next_action_suggestions USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON crm_next_action_suggestions FROM cyberos_app; GRANT UPDATE (status, executed_kind, executed_at) ON crm_next_action_suggestions TO cyberos_app; ``
  1. MUST auto-expire pending suggestions after 7 days (cron via TASK-MCP-007).
  1. MUST emit 3 memory audit kinds per DEC-1655. PII per TASK-MEMORY-111: rationale text SHA-256 hashed.
  1. MUST thread trace_id from CUO call → context → AI → audit.
  1. MUST NOT return >3 suggestions per DEC-1650.
  1. MUST NOT exceed user rate limit per DEC-1654.

§2 — Why this design

Why top-3 (DEC-1650)? Cognitive load research: >5 options paralyzes; 3 optimizes selection.

Why rationale required (DEC-1653)? CDO won't follow black-box AI; explainability drives adoption.

Why rate limit (DEC-1654)? AI cost per call is non-trivial; runaway usage breaks budget.

Why context from TASK-CRM-002 (DEC-1652)? Activity feed is the truth source; without it AI hallucinates.


§3 — API contract

POST   /v1/crm/next-action       body: {deal_id}
POST   /v1/crm/next-action/{id}/execute    body: {kind}  (records executed_kind)
POST   /v1/crm/next-action/{id}/dismiss

Sample response:

{
  "suggestion_id": "uuid",
  "deal_id": "uuid",
  "suggestions": [
    {
      "kind": "send_email",
      "summary": "Follow up on proposal — no reply in 5 days",
      "rationale": "Last email Jun 1 unanswered; account history shows 7-day response pattern.",
      "confidence_score": 0.85,
      "deep_link": "/email/threads/abc-123"
    },
    {
      "kind": "schedule_call",
      "summary": "Push for decision call this week",
      "rationale": "Deal age 45d, stage 'proposal' for 14d; similar deals close after exec call.",
      "confidence_score": 0.75,
      "deep_link": "/calendar/new?contact_id=..."
    },
    {
      "kind": "request_intro",
      "summary": "Ask current contact to introduce CFO",
      "rationale": "Decision-maker not yet engaged; CFO buyer signals from similar deals.",
      "confidence_score": 0.65,
      "deep_link": "/crm/contacts/...../add-stakeholder"
    }
  ]
}

§4 — Acceptance criteria

  1. CUO skill registered as crm.next-action@1. 2. Returns exactly 3 suggestions (or fewer if AI can't fill). 3. Enum 7 + cardinality test. 4. Each suggestion has all 5 fields. 5. Rationale 1-2 sentences. 6. Confidence_score 0-1. 7. Deep_link non-empty. 8. Context built from TASK-CRM-002 activities. 9. Rate limit 100/user/day. 10. 3 memory audit kinds emitted. 11. PII scrubbed (rationale SHA256). 12. RLS denies cross-tenant. 13. Trace_id preserved. 14. Execute records executed_kind. 15. Dismiss → status=dismissed. 16. 7-day expiry via cron. 17. Append-only suggestions table. 18. AI returns invalid JSON → sev-2 + retry once. 19. No deal_id (closed deal) → 404. 20. CDO/CRO role required.

§5 — Verification

#[tokio::test]
async fn returns_top_3() {
    let ctx = TestContext::with_open_deal_and_activities(20).await;
    let r = ctx.next_action(ctx.deal_id).await;
    assert_eq!(r.suggestions.len(), 3);
    for s in &r.suggestions {
        assert!(!s.rationale.is_empty());
        assert!(s.confidence_score >= 0.0 && s.confidence_score <= 1.0);
    }
}

#[tokio::test]
async fn rate_limit_enforced() {
    let ctx = TestContext::with_user().await;
    for _ in 0..100 {
        ctx.next_action_for_random_deal().await;
    }
    let r = ctx.next_action_for_random_deal().await;
    assert_eq!(r.status_code, 429);
}

#[tokio::test]
async fn execute_records_kind() {
    let ctx = TestContext::with_suggestion().await;
    ctx.execute_suggestion(ctx.suggestion_id, "send_email").await;
    let row = ctx.fetch_suggestion(ctx.suggestion_id).await;
    assert_eq!(row.status, "executed");
    assert_eq!(row.executed_kind.as_deref(), Some("send_email"));
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-CRM-001, TASK-CUO-101. Cross-module: TASK-CRM-002 (activity context), TASK-AI-003 (LLM), TASK-MCP-007 (expiry cron), TASK-AUTH-101 (role), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
AI returns <3 suggestionsaccept feweruse what AI returnedinherent
AI invalid JSONparse errorretry 1x, fallback sev-2inherent
AI hallucinated kind (not in enum)filterdrop, fewer suggestionsinherent
Deal closed (won/lost)check stage404inherent
Rate limit window edgeday boundary checkrolling 24h or calendar dayinherent
Context too large (>50k tokens)truncate to last 10 actsinherentinherent
AI provider quotarate limit upstreamsev-2; degrade gracefullyinherent
Expiry cron skippednext run catchesinherentmanual run
Execute kind not in original suggestionsaccept (user override)record anywayinherent
Cross-tenant suggestion lookupRLS404inherent

§11 — Implementation notes


End of TASK-CRM-005 spec.