Derived from the canonical F100 CUE contract: **partial**.

Spec: F100 — Issue-to-Action Triage

Status: Derived from the canonical F100 CUE contract: partial. See the generated traceability view and gap view; Markdown does not carry an editable completion claim.

1. Problem & CEO Job

Decide fast + trust the signal. Reported chat and pulse issues are unstructured, forcing the CEO to decide their type and follow-up. Treating every issue as a Risk or inventing severity fills the Brief with unjustified warnings. F100 proposes a Task, Risk, or Decision with honest confidence and traceable evidence, then waits for confirmation before creating a live operational object.

2. User Stories & Acceptance Criteria

  • As a PM or project owner, I want Lumi to classify a reported issue as Task, Risk, or Decision based on content, so that each problem gets the right follow-up (not everything becomes a Risk).
    • Given a signal describing a concrete action with an owner, when Lumi analyzes it, then it proposes a Task candidate (not a Risk) with linked EvidenceSource.
    • Given a signal describing a future threat to a milestone or customer commitment, when Lumi analyzes it, then it proposes a Risk candidate with the affected entity named.
    • Given a signal describing a choice between options requiring authority, when Lumi analyzes it, then it proposes a DecisionRequest candidate.
    • Given a signal that simultaneously describes an action, a future threat, and a needed decision, when Lumi analyzes it, then it may propose multiple candidates linked to the same EvidenceSource.
    • Given a signal with insufficient information to classify, when Lumi analyzes it, then it asks clarifying questions or records MissingData rather than guessing a type.
  • As a CEO, I want every proposed item to cite its source and carry honest confidence, so that I can trust the signal without re-reading the original text.
    • Given any proposed Task, Risk, or DecisionRequest, when rendered, then it links ≥1 EvidenceSource with the verbatim evidence span, and confidence_score is ≤0.80 (keyword) or 0.85–0.90 (LLM) — never 1.0.
    • Given an item where the title or impact involves AI inference not grounded in source text, when rendered, then it is labeled Inference (not Fact) and clearly distinguished.
  • As a CEO, I want to confirm or reject each proposal before it becomes a live operational object, so that Lumi never commits a guess to my operating queue.
    • Given a proposed Task/Risk/DecisionRequest, when the user has not confirmed it, then the item exists only as a candidate — it is not surfaced in the Brief and does not appear in list endpoints for that object type.
    • Given a confirmed proposal, when accepted, then the destination object is created with EvidenceSource linked and appears in the CEO Brief at the next generation.
    • Given a rejected proposal, when rejected, then the rejection is recorded as calibration data (not silently discarded).
  • As a product, I want duplicate signals to merge rather than multiply, so that the Brief stays clean.
    • Given a new signal whose content substantially matches an existing operational object, when Lumi analyzes it, then it links the new EvidenceSource to the existing object instead of proposing a duplicate.
  • As a member, I want all this to happen through channels I already use — assistant chat or pulse — so that I don’t need to learn a new form.
    • Given a problem described in assistant chat (F91), when the user invokes the triage tool, then proposals are rendered inline in the conversation with accept/reject actions.
    • Given a pulse entry containing a problem signal, when extraction runs, then it creates a proposal candidate (not a live object) alongside any extracted commitments/risks/decisions.

3. Scope Boundaries

  • In:
    • Triage analysis of signals arriving via two existing input channels: assistant chat (F91 log_issue or similar tool) and daily pulse (F83/F07 extraction step).
    • LLM-driven type classification (Task vs Risk vs Decision) via updated _EXTRACTION_RULES in backend/apps/ai/prompts.py (per ADR-031).
    • Proposal stage: candidates live in the existing SignalProposal model, piggybacking on F32’s accept/reject/ignore infrastructure.
    • Duplicate detection: text-similarity check against existing open Tasks, Risks, and DecisionRequests in the same org.
    • Fact vs Inference labeling: AI-reasoned fields labelled Inference with confidence score.
    • MissingData recording when signal is too thin to classify.
    • CEO Brief surfacing: confirmed items appear in the Brief at the next generation cycle.
    • EvidenceSource linking on every proposal — single source text per proposal (the input chat message or pulse answer).
  • Out:
    • Bot transport (Telegram bot channel considered out of scope for this spec — F92 @lumi in messaging covers that path separately).
    • A dedicated “Issues” UI — proposals are reviewed inline (assistant chat) or via the existing Signal Inbox page (/intel/inbox).
    • Email sync, OCR, file uploads as input (covered by F33/F87).
    • Automated creation without human confirm (confirm-before-commit at F91 trust level 0).
    • Custom triage rules engine (the LLM _EXTRACTION_RULES block is the rule engine).

4. Data Model

No new models. F100 reuses the existing spine:

  • SignalInboxItem — the capture record for pulse-sourced signals. When pulse extraction produces a problem signal, a SignalInboxItem is created alongside any existing extraction items (commitments/risks/decisions).
  • SignalProposal — proposed candidates from triage. New proposal_type values are already supported (task, risk, commitment, decision, approval). Triage produces proposals with confidence_score following F86 policy. A status field ai_inference boolean on the proposal or a fact_vs_inference label in payload_json marks AI-reasoned content.
  • EvidenceSource — linked to every proposal. source_type updated to support chat as a new value alongside existing manual_note.
  • No new FK from SignalInboxItem to Task — proposals already carry accepted_object_id.

The pulse extraction flow (backend/apps/ai/services.py extract_pulse_entry) already creates Commitment/Risk/DecisionRequest objects directly. F100 adds a triage step: when the extraction LLM classifies a signal as a “problem” that maps to one of the three types, it creates a SignalProposal (via signals/services.py) instead of directly creating the operational object. The direct creation path stays for clear commitments/risks/decisions; the proposal path is for ambiguous/problem signals that benefit from human review.

Status transitions: SignalProposal status already has proposed → accepted | rejected. No new states needed.

5. API Contract

No new endpoints. F100 reuses the existing F32 proposal infrastructure:

MethodPathRoleExisting?F100 use
POST/api/signal-inbox/memberF32Capture pulse-sourced problem signals as inbox items
POST/api/signal-inbox/<id>/extract/memberF32Run triage to generate proposals (currently keyword; F100 adds LLM path)
POST/api/signal-proposals/<id>/accept/managerF32Confirm a triage proposal → create live object
POST/api/signal-proposals/<id>/reject/managerF32Reject a triage proposal → calibration data
POST/api/signal-inbox/<id>/ignore/memberF32Dismiss without action

Assistant chat (F91) triage: The assistant tool triage_issue (new F91 tool contract, not a REST endpoint) submits the issue text to the same analysis path and renders proposals as inline conversation cards with confirm/reject buttons. No new backend endpoint required — the F91 agent calls extract_proposals via the existing services.py.

Tenant scoping: All inherited — inbox items, proposals, and destination objects are scoped to request.tenant via F32’s existing SignalInboxItemViewSet and SignalProposalViewSet.

EvidenceSource linking: Every triage proposal carries one EvidenceSource with:

  • source_type: "chat" (from F91 assistant) or "manual_note" (from pulse, matching existing F32)
  • content_text: the full input message or pulse answer text
  • evidence_span: verbatim substring supporting the classification

Confidence values:

  • LLM-classified: 0.85–0.90 (per ADR-031 and F86)
  • Keyword fallback: 0.65 (existing F32 default)

6. UI / UX

  • Assistant chat (F91): A new tool contract triage_issue that the assistant exposes when the user describes a project problem. The agent renders proposals as inline rich cards: type badge, title, evidence span, confidence, and two buttons (Xác nhận / Bỏ qua). This is the primary review surface — no new page required.
  • Pulse-sourced proposals: Appear in the existing Signal Inbox page (/intel/inbox) as proposal cards with the same shape. No new UI component beyond what F32 already ships.
  • States: empty (no problem signals detected), loading, proposals pending, proposals accepted/rejected per existing F32 handling.
  • i18n: new namespace triage with keys for: tool description, proposal card labels, confirmation flow, inference label, missing-data prompt. Both en and vi updated together.
  • Assistant path (F91 doctrine): This is the primary path. The assistant conversation IS the intake surface. The /intel/inbox page is the optional direct-path fallback for reviewing pulse-sourced proposals. No new required forms.

7. Test Plan

#CaseTypeWhere
1Triage classifies action signal → Task proposalbackendbackend/tests/signals/test_triage.py::TestTriageClassification
2Triage classifies threat signal → Risk proposalbackendsame
3Triage classifies choice signal → Decision proposalbackendsame
4Triage classifies multi-signal → multiple proposalsbackendsame
5Triage with insufficient data → MissingData recordedbackendsame
6Proposed item has confidence in correct band (0.85–0.90 LLM, ≤0.80 keyword)backendbackend/tests/signals/test_triage.py::TestTriageConfidence
7Inference-labeled fields distinguished from fact fieldsbackendsame
8Accepted proposal creates Task with EvidenceSourcebackendReuses F32 test_signals.py::TestProposalAcceptance
9Duplicate signal links to existing object instead of creating new proposalbackendbackend/tests/signals/test_triage.py::TestTriageDedup
10Cross-tenant proposal access blockedbackendbackend/tests/test_multitenancy.py (existing F32 coverage extended)
11F91 assistant tool triage_issue invokes the same analysis pathbackendbackend/tests/assistant/test_triage_tool.py
12Triage proposal inline cards render in chat with accept/rejecte2efrontend/tests/e2e/triage.spec.ts :: describe("Issue-to-action triage")
13Pulse-sourced proposals appear in /intel/inboxe2esame (reuses F32 inbox page)

8. Current Behavior

The signal inbox (F32) captures free-text notes and generates keyword-based proposals for five types (task, commitment, risk, decision, approval). Pulse extraction (F08/F83) creates Commitment/Risk/DecisionRequest objects directly from pulse answers via either the LLM path (_EXTRACTION_RULES) or keyword fallback; F100’s triage path adds an intermediate proposal stage for ambiguous signals. The assistant chat (F91) now exposes the triage-issue tool, which returns the same proposal material without creating an operational object.

9. Error States

#ErrorUser messageHTTPLogged
E1Triage input too thin for any classificationInline MB text asking clarifying questions; no proposal createdn/aStructured warning logged
E2Duplicate detected — merge confirmationInline card: “This matches Task X — link as additional evidence?“n/aMerge recorded in audit
E3LLM provider fails during triageFalls through to keyword patterns; confidence lowered to ≤0.80200 (degraded)Warning logged with provider error
E4Cross-tenant proposal accessStandard 404404No (F32 existing behavior)

10. Diagrams & Docs Touched

  • DIAGRAMS.md — ingress pipeline: chat/pulse → triage → proposal → confirm → Brief
  • backend/DIAGRAMS.md — signals ER and state machine, pulse extraction sequence
  • frontend/DIAGRAMS.md — /intel/inbox route (unchanged)
  • GLOSSARY.md — add triage_proposal and fact_vs_inference entries
  • ADR-031 — updated _EXTRACTION_RULES for triage signal types
  • F86 extraction faithfulness — confidence and grounding contract
  • F32 signal inbox — reused proposal infrastructure
  • F91 assistant-first — new tool contract triage_issue

11. Implementation Audit (2026-07-10)

Built in round 02 (fleet-C). Full implementation shipped in this commit:

  • backend/apps/signals/services.py — extract_proposals() calls the triage path with bounded LLM confidence and keyword fallback when the provider fails.
  • backend/apps/ai/prompts.py _EXTRACTION_RULES — the signal type descriptions already cover commitment/risk/decision. F100 adds triage-specific rules for distinguishing Task vs Risk vs Decision from problem signals.
  • backend/apps/signals/models.py — SignalProposal.proposal_type already supports task, risk, decision. No model changes needed.
  • backend/apps/operations/models.py — Risk and DecisionRequest already accept evidence_sources.
  • backend/apps/work/models.py — Task already has evidence_sources M2M.
  • F32’s /api/signal-inbox/<id>/extract/ — the endpoint already generates proposals and marks the item as reviewed. F100 rewrites the underlying extract_proposals() to use LLM classification.
  • F32’s accept/reject/ignore flow — reused without change.
  • F91 assistant loop — the triage-issue tool is registered in backend/apps/ai/toolbelt_registry_triage.py and calls the shared triage path.
  • backend/apps/pulses/models.py EvidenceSource.source_type — currently "manual_note", "chat_note", "pulse", "seed", "connector", "upload", "confirmation". F100 uses "chat" and "manual_note" only.
  • Duplicate detection — detect_duplicates() performs bounded word-overlap checks against open Tasks/Risks/DecisionRequests in the same organization; it returns a possible duplicate for confirmation rather than mutating the destination.

Shipped-vs-new summary:

CapabilityShipped (F32, F08, F83)New in F100
Text captureWeb, Telegram, connector, emailAssistant chat via F91 tool triage-issue
Proposal generationKeyword patterns onlyLLM classification via _TRIAGE_EXTRACTION_RULES in prompts.py + triage.py service
ConfirmationAccept/reject in inbox UIInline in assistant chat + inbox (same F32 flow)
Evidence linkingEvidenceSource on every accepted itemSame pattern, no change
Type classificationBroad keyword match (commitment/risk/decision)Task vs Risk vs Decision via LLM triage
Duplicate mergeNoneWord-overlap dedup via detect_duplicates()
Fact vs InferenceNot labeledai_inference flag in payload_json
CEO Brief surfacingOnly confirmed itemsSame pattern, no change

What shipped:

  • backend/apps/ai/services/triage.py — triage_issue(), _triage_via_llm(), _triage_keyword_fallback(), detect_duplicates()
  • backend/apps/ai/prompts.py — added _TRIAGE_EXTRACTION_RULES
  • backend/apps/signals/services.py — extract_proposals() upgraded to use triage with ai_inference payload flag
  • backend/apps/ai/toolbelt_registry_triage.py — F91 assistant tool triage-issue registration
  • backend/tests/platform/test_triage.py — 16 tests covering all 13 spec §7 cases
  • backend/tests/coverage/test_coverage_triage.py — 32 coverage-gap tests

Constraints honored:

  • Confidence discipline: 0.85–0.90 LLM, ≤0.80 keyword, never 1.0
  • EvidenceSource linked on every proposal
  • Cross-tenant isolation on all querysets
  • LLM_PROVIDER=mock falls through to keyword fallback
  • ai_inference flag distinguishes AI-reasoned from keyword-matched proposals

12. Open Questions / Decisions

  • Should duplicate detection be automatic (silently link) or confirm-before-merge? Recommendation: confirm-before-merge — show “This matches existing Task X — link as evidence or create new proposal?” so the user retains control.
  • Should triage proposals from pulse use the same SignalProposal table or a separate proposal model? Recommendation: same SignalProposal table — F32’s existing model already supports all required types, and it keeps the accept/reject flow uniform.
  • What is the minimum text length for triage? Very short signals (<20 chars) should produce a “Need more detail” response rather than a low-confidence guess. Decision deferred to implementation.
  • Does EvidenceSource need a new source_type for assistant chat? Currently chat_note exists in messaging (apps/messaging/services.py) but SignalInboxItem uses manual_note. Recommendation: reuse chat_note for assistant-originated signals, or add chat to the enum. ADR needed if the enum is extended.

13. Bug / Change Log

DateChange
2026-07-10Round 02 (fleet-C): Full implementation shipped. triage.py service with LLM+keyword classification, extract_proposals() upgraded, triage-issue F91 tool registered, 48 tests, 97.05% coverage.
2026-07-14Contract Graph migration: Canonical F100 ownership and executable proof IDs added; live-provider corpus and browser journey remain explicit gaps.