Spec: F31 — Follow-up Missions
Status: partial under Contract Graph audit — shipped backend/web/native source surfaces are present, while browser, named native, live Telegram, and production scheduler evidence remain explicit gaps. Contract: contracts/features/F31.cue · Audit: F31 implementation audit · Test plan: F31 test plan
1. Problem & CEO Job
Decide fast + trust the signal. Brief can surface an issue and F25/F26 can delegate it, but neither chases the answer. If the CEO still pings “đến đâu rồi?” in Zalo, Lumi diagnoses work instead of operating it. F31 closes that loop: send the follow-up, collect the reply, attach it to the source issue, and return the updated state to the CEO.
The wedge is removing executive chase work without becoming a spam engine.
2. User Stories & Acceptance Criteria
- As a CEO or manager, I want to send a targeted follow-up from an existing issue, so that I do not leave Lumi to chase updates manually.
- Given a Task / Commitment / Risk / DecisionRequest / ApprovalRequest / BriefItem, when I tap “Nhắc cập nhật”, then a prefilled one-question draft opens with owner, context, and due-by prefilled.
- Given I edit and send, then the follow-up is delivered by the chosen channel (
telegramfirst, web inbox fallback) and the request becomessent. - Given a manager asks Lumi to send an existing draft by topic, then Lumi
proposes exactly one
send_followupaction, shows the resolved assignee, question, channel, and due time, and sends only after explicit confirmation.
- As an assignee, I want to answer the follow-up in one reply, so that the update costs less than being chased twice.
- Given a Telegram follow-up, when I reply in-thread, then Lumi records the answer and marks the request
replied. - Given a web follow-up, when I answer from the app, then the same request is closed with the new answer attached.
- Given a Telegram follow-up, when I reply in-thread, then Lumi records the answer and marks the request
- As a CEO, I want the reply linked back to the original issue, so that Lumi’s queue updates instead of creating another orphan message.
- Given a reply, then the reply text is stored as an
EvidenceSource, visible from the original source object, and summarized on the next Brief/Today load. - Given multiple follow-ups on the same source item, then the latest visible state is obvious; older requests remain in history.
- Given a reply, then the reply text is stored as an
- As an admin, I want stale issues to produce send-ready chase drafts, so that the org’s operating rhythm does not depend on the CEO remembering every loose end.
- Given an overdue commitment, stale approval, or critical open risk, then Lumi may create a draft follow-up suggestion once per cooldown window.
- Drafts never auto-send in v1. A human reviews and sends them; Lumi proposes, a human dispatches.
- As the product, I want restraint, so that follow-ups improve trust rather than feeling like surveillance.
- Only one open
sentfollow-up may exist per source item + assignee at a time. - Every sent follow-up is visible in Lumi; no hidden nags, no autonomous escalation ladders, no “AI silently messaged your team.”
- Only one open
3. Scope Boundaries
- In: one-question follow-up drafts, send/edit flow, Telegram reply handling, web reply flow, reply evidence capture, source-item linkage, stale-item draft generation, Today/Brief visibility of outstanding follow-ups.
- Out: multi-step campaigns, automatic repeated nagging, voice calls, follow-up analytics dashboards, manager scorecards, sentiment scoring, silent escalations, hidden autonomy.
4. Data Model
- New app
followups. FollowUpRequest— org FK, created_by (OrganizationMember), assignee (OrganizationMember),source_type(brief_item|task|commitment|risk|decision|approval|deal),source_id(UUID),source_snapshot_json,question_text,delivery_channel(telegram|web),status(draft → sent → replied | expired | cancelled),due_at,sent_at,replied_at,latest_reply_text,reply_evidence_sourceFK nullable,auto_generatedbool.FollowUpTemplateis not a model in v1. Auto-draft question text is deterministic from the source object and lives in service code; avoid inventing a template-builder product.
State machine belongs in backend/DIAGRAMS.md. GLOSSARY entries: Follow-up Mission, auto-draft.
5. API Contract
| Method | Path | Min role | Request | Response | Errors |
|---|---|---|---|---|---|
| GET | /api/follow-ups/ | member | ?status=&mine=&source_type=&source_id= | FollowUpRequest list | 401 |
| POST | /api/follow-ups/ | manager | {source_type, source_id, assignee_id, question_text?, due_at?, delivery_channel} | created FollowUpRequest | 400 invalid source/assignee |
| POST | /api/follow-ups/<id>/send/ | manager | optional edited payload | sent FollowUpRequest | 409 already sent/replied |
| POST | /api/follow-ups/<id>/reply/ | member* | {answer_text} | replied FollowUpRequest | 403 non-assignee |
| POST | /api/follow-ups/<id>/cancel/ | manager | — | cancelled FollowUpRequest | 409 already replied |
| POST | /api/follow-ups/drafts/generate/ | admin | optional org-scope trigger | created draft count | 403 |
Conversational twin for POST /api/follow-ups/<id>/send/:
- Assistant action:
send_followupwith{followup_query}. The query resolves onedraftrequest byquestion_textwithinrequest.organizationonly. - Missing, zero-match, or multi-match queries ask for clarification and send nothing. Sent/replied/cancelled requests are not eligible targets.
- The action requires manager authority, has
autonomy_ceiling=0, and remains confirm-only forever because it sends to another person. - Confirmation calls the same
send_follow_up()service as the REST form.
* Telegram replies do not use this endpoint directly; the bot maps the chat reply to the same service path.
Tenant scoping: every source lookup and every assignee lookup is scoped to request.tenant. Reply evidence is stored as a new EvidenceSource row (source_type="manual_note" in v1, with metadata noting follow-up origin) linked to the follow-up and referenced from the originating source item.
6. UI / UX
- New route:
/intel/follow-ups— two tabs max:Chờ trả lờiandBản nháp. - Inline entry points live where the CEO already works:
- Brief item action:
Nhắc cập nhật - Approval / Task / Commitment / Risk detail actions: same label, same interaction
- Brief item action:
- Composer: one textarea, one assignee, one due date, one send button. No workflow-builder UI.
- Outstanding follow-up chips surface in
/work/todayand/intel/briefonly when materially useful: “Đã nhắc Tuấn, chờ trả lời đến 14:00”. - States: empty queue is positive, drafts explain why Lumi proposed them, failure keeps edited text, reply detail is visible from the source object.
- i18n: new
followupsnamespace, en + vi. - The direct send dialog remains available. Chat is an optional second path, surfaced as an action card that previews the target, assignee, question, channel, and due time before its confirmation button.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Create/send/reply/cancel flow; cross-tenant source blocked | backend | new backend/tests/test_followups.py |
| 2 | Only assignee can reply; only manager+ can create/send/cancel | backend | same |
| 3 | Duplicate open follow-up on same source+assignee rejected | backend | same |
| 4 | Telegram reply maps back to correct follow-up and creates EvidenceSource | backend | same + backend/tests/test_telegram.py |
| 5 | Draft-generation respects cooldown and never auto-sends | backend | same |
| 6 | Follow-up visibility from Brief/Today/source detail | e2e | new describe("Follow-ups") + brief regression cases |
| 7 | Conversational send resolves one tenant-scoped draft and calls the shared send service | backend | assistant executor tests |
| 8 | Missing/ambiguous/non-draft target sends nothing; member/viewer cannot confirm | backend | assistant executor + permission tests |
| 9 | send_followup remains ceiling 0 and requires confirmation at every trust level | backend | assistant parity + ceiling guards |
| 10 | Chat → action preview → confirm → request becomes sent; direct dialog remains | e2e | follow-ups.spec.ts |
8. Current Behavior
The dedicated follow-up system is shipped. apps/followups provides create/send/cancel/reply flows, Telegram replies map back into the same service path, replies create EvidenceSource(manual_note) provenance, auto-drafts generate on a cooldown, and /intel/follow-ups exposes the queue in the web UI.
The focused web unit surface also proves that a failed direct send preserves the open dialog and edited question for retry, while the Replied tab renders the latest answer. Browser execution, named native, live Telegram, and production scheduler evidence remain separate gaps.
Before the 2026-07-14 form-parity slice, sending still requires the web dialog: the assistant can create a draft and cancel a follow-up but cannot send an existing F31 draft. The slice adds that missing conversational twin without removing or changing the direct form.
9. Error States
followups.error_invalid_source— source item missing or not in org; no request created.followups.error_invalid_assignee— assignee not an active member of the same org.followups.error_already_open— an open sent follow-up already exists for this source item + assignee.followups.error_reply_forbidden— someone other than the assignee tried to reply.- Delivery failure:
- user message:
followups.error_send_failed - internal: request stays
draft, log delivery error, allow retry
- user message:
- Telegram reply from an unpaired or unknown chat:
- user sees a safe fallback message in Telegram
- Lumi records no reply and logs the event
- Conversational send target missing or blank:
missing_followup_query/no_followup_found; ask for a topic and send nothing. - More than one draft matches:
ambiguous_followup; ask for a narrower topic and never pick the newest silently. - Target is no longer a draft at confirmation time: shared service raises
invalid_status; action fails without a second delivery. - Member/viewer confirmation: 403-equivalent permission failure; manager/owner confirmation is permitted, matching the direct endpoint.
10. Diagrams & Docs Touched
- DIAGRAMS.md signal loop update: issue → follow-up → reply → brief
- backend/DIAGRAMS.md FollowUpRequest state machine
- frontend/DIAGRAMS.md
/intel/follow-upsroute - F12 / F21 / F25 / F26 specs for glue references
11. Implementation Audit (2026-06-11)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | FollowUpRequest model + state machine (draft → sent → replied|expired|cancelled) | ✅ match | apps/followups/models.py; backend/tests/test_followups.py (48 test methods); GLOSSARY + backend/DIAGRAMS.md state machine |
| 2 | POST /api/follow-ups/, /send/, /cancel/ with IsOrgManager gate | ✅ match | FollowUpRequestViewSet; TestCreateFollowUpAPI, TestSendCancelFlow, TestDuplicateOpenFollowUp; dedup enforced (one sent per source+assignee) |
| 3 | POST /api/follow-ups/<id>/reply/ — assignee-only, sent-only | ✅ match | PR-2; TestReplyFlow (assignee success, non-assignee 403, draft-status 409) |
| 4 | Reply creates EvidenceSource(manual_note) linked to source item | ⚠️ false until 2026-07-10 (reply evidence was only linked via reply_evidence_source FK on the FollowUpRequest, never added to the source item’s evidence_sources M2M — so Brief/Today reads missed it) | reply_to_follow_up service; reply_evidence_source FK only; test_followups.py:TestReplyFlow.test_reply_evidence_linked_to_source_item proves the gap |
| 4a | Reply evidence now linked to source item via its evidence_sources M2M | ✅ match from 2026-07-10 | fix in apps/followups/services.py:reply_to_follow_up — after creating EvidenceSource, resolves source and calls source_obj.evidence_sources.add(evidence) for models that support it; TestReplyFlow.test_reply_evidence_linked_to_source_item, test_reply_evidence_linked_to_each_source_type, test_reply_evidence_skipped_for_models_without_m2m |
| 5 | Telegram reply path: bot reply → handle_follow_up_reply → reply service | ✅ match | PR-2; FollowUpReplyHandlerTests; test_webhook_follow_up_reply |
| 6 | Auto-draft generation (POST /api/follow-ups/drafts/generate/), 7-day cooldown | ✅ match | PR-4; generate_follow_up_drafts_for_all_orgs Celery beat task (daily 07:00) |
| 7 | /intel/follow-ups route, followups i18n namespace (en + vi + 9), sidebar entry | ✅ match | PR-3; FollowUpsPage.tsx; frontend/DIAGRAMS.md route tree updated; 29 i18n namespaces green |
| 8 | Only one open sent follow-up per source+assignee (deduplication) | ✅ match | PR-1; dedup check in FollowUpRequestViewSet.create |
| 9 | E2E test describe("Follow-ups") | ⚠️ partial | Dedicated Playwright suite exists at frontend/tests/e2e/follow-ups.spec.ts and proves route/nav/API smoke; explicit create/send/reply and Brief/Today source-loop proof is still not covered end-to-end |
| 10 | Departed assignee: sent follow-up auto-transitions to cancelled | ✅ match | 2026-07-01: apps/organizations/signals.py cancels outstanding sent follow-ups when an assignee leaves active status or is removed; covered by TestAssigneeLifecycle in backend/tests/test_followups.py |
| 11 | iOS + Android: reply action available on status=sent items in shared KMP FollowUpsScreen | ✅ match | 2026-06-22 PR mobile/f31-followup-reply-kmp; FollowUpsScreenModel.{startReply,confirmReply}; ReplyDialog AlertDialog in FollowUpsScreen.kt; 3 new unit tests; dead native FollowUpDetailScreen deleted |
Summary: 11/12 match (row 4 was false demoted, row 4a now true); 1 gap (dedicated E2E exists but remains smoke-depth rather than full follow-up lifecycle proof).
Contract Graph audit note (2026-07-15)
The canonical contract in F31.cue now records the shipped backend, assistant, Telegram-handler, web, and shared-KMP boundaries. Local F31 proof execution passed for the registered pytest groups; the registered Playwright group failed in the unavailable web-server/browser environment and remains an explicit gap. No named native, live Telegram, or production scheduler evidence is inferred.
Edge Cases Catalog (formerly §9, 10 items): E1 departed assignee — closed 2026-07-01 by auto-cancelling outstanding sent follow-ups when the assignee leaves active membership or is removed; E2 reply-on-cancelled error code not defined (§5 only specifies sent-only guard); E3 dual-channel reply racing (no idempotency on latest_reply_text); E4 cooldown-reset semantics after manual cancellation unspecified; E5 source_snapshot_json size limit/truncation not defined; E6 spurious Telegram reply fallback text not specified in spec (falls through to handle_unknown); E7 multiple-follow-up-per-source-item rendering on Today/Brief chips (multiple pending chips unspecified); E8 cross-category draft dedup in auto-generation not described; E9 deleted-source viewing behavior (404 vs disabled link) unspecified; E10 past due_at validation rules not defined. Full catalog in git history.
12. Open Questions / Decisions
- Should a reply optionally update the underlying source item automatically (for example, mark a Task back to
in_progress)? Recommendation: no in v1; keep reply capture and source-state mutation separate. - Should follow-up replies be allowed from multiple channels after send? Recommendation: Telegram + web only; adding email or F29 message replies dilutes reliability too early.
13. Bug / Change Log
2026-08-15 — Avoid server-side cursors across follow-up generation
Type: Scheduled worker reliability hardening
Summary:
generate_follow_up_drafts_for_all_orgs_nownow materialises organizations before running per-organization draft queries and writes. This keeps the daily beat loop aligned with the no-server-side-cursor rule used by the other long-running organization scans.- A regression test protects the loop from returning to
QuerySet.iterator().
Evidence boundary:
- The host incident directly showed this cursor failure in the Hub and mention digests; the follow-up loop had the same unsafe iteration shape but no failure was observed in the captured window. This change is preventive source/test proof, not a claim of a follow-up send.
2026-08-10 — Native Follow-ups: preserve the queue through refresh failure
Type: Mobile recovery UX
Summary:
- Shared KMP now keeps the last committed Follow-ups visible when a populated refresh fails and shows one compact localized warning with explicit Retry instead of replacing the queue.
load(),refresh(), and reply reconciliation use the same stale-or-current snapshot, so repeated failures and a reply started from stale state do not discard unaffected rows..maestro/critical/156_android_followups_refresh_recovery.yamlarms exactly two allowlistedGET /api/follow-ups/failures, proves both sent questions remain actionable, taps Retry, and requires the warning to clear without leaking raw transport detail.- The guarded
Lumi_Pixel7_API35lifecycle installed matching built/installed prodDebug APK SHA-256c716874ee382e1887dd15418686284f486992374c89d6a1681bce33a45ffd3a5, preserved both server rows assent, observed a forwarded recovery request, cleaned the disposable fixture to0:0:0, retained failure/recovery frames plus proxy trace, and shut downemulator-5554.
Honest gate:
- This proves Android emulator recovery mechanics only. Submitted-reply runtime, iOS, named-device,
native screen-reader-service, signed-store, deployed, and production evidence remain open in
F31.GAP002.
2026-07-16 — Android: make the populated reply dialog deterministic (INT-07)
Type: Mobile verification hardening
Summary:
- Added stable selectors for the shared Follow-ups screen, sent card, Reply action, answer field, and dialog send control.
- Added
.maestro/critical/64_followups_reply_dialog.yaml, which asserts the seeded populated state, opens the Reply dialog, and cancels before mutating the shared fixture. - Fixed the canonical demo seed to create that fixture through
create_follow_up()andsend_follow_up(): one tenant-scoped, web-only sent request assigned to the demo CEO, backed by the existing performance-review task. The focused seed regression went red onFollowUpRequest.DoesNotExistbefore passing after the fix. - Guarded
Lumi_Pixel7_API35execution passed 1/1 in 28 seconds with APK SHA-256f2a074ce0ccdb4600b88c8e9686c6aed9086d200fd92c11bf0ce85c6afdd23cb; command evidence:/Users/james/.maestro/tests/2026-07-16_133219.
Honest gate:
- This is source plus one Android emulator UI proof only. The flow cancels deliberately, so it does not prove a submitted reply; named Android floor-device, iPhone 12, and PM scoring remain open.
2026-07-14 — Conversational send parity (issue #1560)
Type: Feature
Summary:
- Add a
send_followupassistant action for sending one existing F31 draft by topic after explicit confirmation. - Keep the web send dialog unchanged as an optional direct path.
- Reuse
send_follow_up(), preserve tenant/manager gates, and hard-code the external-send autonomy ceiling at 0.
Reason:
- Draft creation and cancellation already had conversational paths, but the actual send step was still required-form-only.
Impact:
- A manager can complete the F31 chase loop inside the assistant thread while seeing exactly who will receive what before committing.
Tests:
- Backend parity, executor, permission, tenant-isolation, ambiguity, stale-state, and ceiling tests; ActionCard unit coverage; conversational Playwright flow.
2026-07-10 — Reply evidence linked to source item (issue #1648)
Type: Bug Fix
Summary:
reply_to_follow_up()created anEvidenceSource(manual_note)and setFollowUpRequest.reply_evidence_sourcebut did not add it to the source item’sevidence_sourcesM2M.- F31 §11 row 4 falsely claimed the source was linked. Demoted to ⚠️, proved by
TestReplyFlow.test_reply_evidence_linked_to_source_item. - Fix: after creating evidence, call
resolve_source()andsource_obj.evidence_sources.add(evidence)when the source model supports the M2M (Task, Commitment, Risk, DecisionRequest, BriefItem). ApprovalRequest and Deal lack the field and are skipped gracefully. - Added 11 new backend tests: every supported source type, models without M2M (skipped), cross-tenant isolation, unchanged source status guard, multiple replies per source, web/Telegram parity.
Reason:
- Follow-up replies were invisible to the Brief/Today pipeline because they were stored only on the FollowUpRequest itself rather than propagated to the source item’s
evidence_sources.
Impact:
- Brief/Today reads now include reply evidence for all supported source types.
- Guard: a reply is evidence only — never auto-resolves, dismisses, or changes source status.
Tests:
TestReplyFlow.test_reply_evidence_linked_to_source_item(proof of gap → fix)TestReplyFlow.test_reply_evidence_linked_to_each_source_type(parametrized: commitment, risk, decision, brief_item)TestReplyFlow.test_reply_evidence_skipped_for_models_without_m2m(approval, deal)TestReplyFlow.test_reply_does_not_change_source_statusTestReplyFlow.test_reply_cross_tenant_linkage_blockedTestReplyFlow.test_multiple_replies_on_same_sourceTestReplyFlow.test_telegram_and_web_reply_both_link_evidence- All 71
test_followups.pytests pass (was 60). - Backend coverage 97.01% (≥97% gate).
2026-07-02 — Correct stale page path in historical change log (batch-1 spec audit)
Type: Documentation Fix
Summary:
- The 2026-06-11 change log entry (PR-3) records the original creation path
frontend/src/pages/FollowUpsPage.tsx; the file now lives atfrontend/src/pages/intel/FollowUpsPage.tsxfollowing the app-wide IA restructure. The historical entry is preserved unchanged (per spec policy: never rewrite history). The current location:frontend/src/pages/intel/FollowUpsPage.tsx(verified byfind frontend/src -name FollowUpsPage.tsx). The 2026-06-15 entry records the route rename/follow-ups→/intel/follow-upswhich accompanied the directory move.
2026-07-02 — §11 test count and test-class names corrected (batch 12)
Type: Documentation Truth Fix
Summary:
- §11 Row 1: Updated backend test count from “38 backend tests” (stale from initial PR-1 audit) to “48 test methods” reflecting the current state of
backend/tests/test_followups.py(verified bygrep -c "^ def test_"= 48). - §11 Row 2: Replaced non-existent class name
TestFollowUpViewSetwith the actual test classes that coverPOST /api/follow-ups/,/send/, and/cancel/:TestCreateFollowUpAPI,TestSendCancelFlow,TestDuplicateOpenFollowUp(verified bygrep "^class Test" backend/tests/test_followups.py).
2026-07-02 — Spec drift fix: check shipped ACs against §11 audit
Type: Documentation Truth Fix
Summary:
- Checked 9 of 10 previously open
[ ]ACs in §2 that §11 confirms as implemented:- AC1 (prefilled draft from source item) — rows 1, 2, 7 (model + API + UI with polymorphic templates)
- AC2 (edit-and-send flow +
sentstatus) — rows 2, 7 - AC3 (Telegram reply →
replied) — row 5 - AC4 (web reply → request closed) — row 3
- AC5 (reply stored as EvidenceSource, linked to source) — row 4
- AC7 (auto-draft generation per 7-day cooldown) — row 6
- AC8 (drafts never auto-send — explicit manager action required) — rows 2, 6
- AC9 (dedup: one open
sentper source+assignee) — row 8 - AC10 (every sent follow-up visible in Lumi) — row 7
- Left AC6 (
[ ]): “Given multiple follow-ups on the same source item, then the latest visible state is obvious; older requests remain in history” — no §11 row explicitly confirms multi-follow-up history rendering in the UI.
2026-07-01 — Close departed-assignee lifecycle gap; correct E2E audit wording
Type: Backend fix + documentation truth fix
Summary:
- Added
apps/organizations/signals.pylifecycle hooks so an assignee leavingactivestatus, or being removed entirely, auto-cancels their outstandingsentfollow-ups instead of leaving the chase queue stuck on a departed person. - Added
TestAssigneeLifecyclecoverage inbackend/tests/test_followups.pyfor both suspension and removal. - Corrected §11 row 9: the repo already contains a dedicated
frontend/tests/e2e/follow-ups.spec.tssuite, so the real gap is E2E depth, not E2E absence.
Reason:
- The prior audit mixed two different problems: a genuine product gap (departed assignees) and a stale wording bug (claiming the Follow-ups Playwright suite was missing when it already existed).
Impact:
- F31 now closes sent follow-ups cleanly when the assignee is no longer active, reducing stale executive chase work and avoiding misleading queue state.
- The spec now distinguishes “smoke coverage exists” from “full lifecycle proof still missing,” which is the sharper product truth.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_followups.py -x --tb=short- Attempted:
cd frontend && pnpm exec playwright test tests/e2e/follow-ups.spec.ts --project=chromium --reporter=list- blocked locally because Playwright Chromium is not installed and browser download failed with
EHOSTUNREACH
- blocked locally because Playwright Chromium is not installed and browser download failed with
2026-06-21 — CRUD-scope decision: lifecycle actions, not free delete (recorded)
Type: Decision (no code change)
During the 2026-06-21 full-CRUD audit, Follow-up requests were reviewed for edit/delete affordances. Decision: Follow-up requests are append-only by backend design (http_method_names allows GET/POST only — no PATCH/DELETE) items (each links an EvidenceSource); their UI lifecycle is send / reply / cancel, not free edit/delete. A hard delete would sever the evidence/audit trail (F16) and is intentionally not offered in the operator UI. This is a deliberate scope boundary, not a missing feature.
2026-06-15 — Frontend route namespaced
Frontend route namespaced: /follow-ups -> /intel/follow-ups (app-wide IA restructure; API paths unchanged).
2026-06-14 — Restructure to TEMPLATE.md section order; expand §11 audit (SP fleet)
Type: Documentation Change
Summary:
- Absorbed §9 Edge Cases (10 items) into §11 note; §10 Error States → §9; §11 Diagrams → §10; §12 Implementation Audit (narrative PR-1–PR-4) → §11 (10-row table); §13 Open Questions → §12; §14 Bug/Change Log → §13.
- §11 audit: 10 rows, 8 ✅, 2 gaps (E2E test missing, departed-assignee auto-cancel not implemented).
2026-06-12 — Added Edge Cases section (CMD fleet agent E, round 11)
Type: Documentation Change
Summary:
- Added §9 Edge Cases (10 items covering departed assignee, reply-on-cancelled, dual-channel reply racing, cooldown semantics, snapshot size, spurious Telegram reply, multi-assignee, cross-category draft dedup, deleted source, past due-at).
- Existing Error States renumbered from §9 to §10; Diagrams from §10 to §11; Implementation Audit from §11 to §12; Open Questions from §12 to §13; Bug/Change Log from §13 to §14.
Reason:
- Round 11 spec-hardening: identify gaps without inventing new behavior.
Impact:
- F31 spec is more useful as a review and implementation reference.
Tests:
- No code changes.
Notes:
- Several gaps flagged as explicitly unspecified rather than resolved.
2026-06-12 — Spec sync after ship audit
Type: Doc Fix
Summary:
- Replaced the stale pre-ship
Current Behaviortext with the actual shipped follow-up system state.
Reason:
- The feature was marked shipped in the registry and implementation audit, but §8 still described the pre-feature world.
Impact:
- The spec now matches the repo’s current behavior instead of contradicting its own shipped status.
Tests:
- No code changes. Verification comes from
backend/tests/test_followups.pyandfrontend/tests/e2e/follow-ups.spec.ts.
Notes:
- This is a documentation truth fix only; the shipped F31 contract is unchanged.
2026-06-11 — PR-3: FollowUpsPage UI + i18n namespace
Type: Feature
Summary:
frontend/src/pages/FollowUpsPage.tsx— tabbed queue (Sent / Drafts / Replied) with send dialog, reply dialog, and cancel action.followUpsApiinendpoints.ts(list, create, send, cancel, reply).FollowUpRequesttype intypes/index.ts.followupsi18n namespace:en/followups.json,vi/followups.json, and 9 additional language stubs.- Sidebar
Bellentry under the Intelligence group;follow_upskey added to all sidebar locale files. /follow-upsroute inApp.tsx.frontend/DIAGRAMS.mdroute tree updated.
Reason:
- PR-3 of the P14 implementation plan — gives the CEO a place to see, send, reply to, and cancel follow-ups within the product.
Impact:
- New route
/follow-upsand sidebar nav item. - No backend changes.
Tests:
- Frontend unit tests pass (951/951); 29 i18n namespaces; all language key-parity checks green.
Notes:
- No E2E test yet (F31 PR-4 or a later cleanup will add
describe("Follow-ups")).
2026-06-11 — PR-2: Telegram and web reply ingestion
Type: Feature
Summary:
- New
POST /api/follow-ups/{id}/reply/endpoint: only theassigneemay reply (followups.error_reply_forbiddenotherwise), only on asentrequest (followups.error_invalid_statusotherwise). apps/followups/services.reply_to_follow_upcreates anEvidenceSource(source_type="manual_note")linked viareply_evidence_source, setslatest_reply_text/replied_at, and moves the request toreplied.apps/followups/services.find_telegram_reply_targetfinds the most recent outstanding (sent,delivery_channel="telegram") follow-up for a member.apps/telegram/handlers.handle_follow_up_reply+ webhook wiring: a connected user’s free-text reply (when not mid-pulse) is matched to their outstanding Telegram follow-up and routed throughreply_to_follow_up.
Reason:
- PR-2 of the P14 implementation plan — closes the loop so the CEO’s chase actually produces an evidence-backed answer without the assignee leaving Telegram.
Impact:
- New action route under
/api/follow-ups/{id}/reply/. - Telegram webhook now has a third branch (after
/startetc. and the pulse conversation) for follow-up replies.
Tests:
backend/tests/test_followups.py::TestReplyFlow(assignee reply success, non-assignee 403, draft-status 409, service-level invalid_status,find_telegram_reply_target).backend/tests/test_telegram.py::FollowUpReplyHandlerTestsandWebhookViewTests::test_webhook_follow_up_reply.
Notes:
- An unpaired Telegram chat or a user with no outstanding follow-up falls through to
handle_unknownunchanged. - PR-3 (UI), PR-4 (auto-draft generation) remain.
2026-06-11 — PR-1: backend model + service + manual send API
Type: Feature
Summary:
- New
apps/followupsapp withFollowUpRequestmodel (draft → sent → replied|expired|cancelled), polymorphic source resolution across Task/Commitment/Risk/DecisionRequest/ApprovalRequest/Deal/BriefItem, deterministic Vietnamese question-draft templates, and best-effort Telegram delivery. FollowUpRequestViewSetwith/api/follow-ups/(list/create),/send/,/cancel/;IsOrgManagerrequired for create/send/cancel.- The contract graph declares both shipped read operations explicitly:
GET /api/follow-ups/is tenant-scoped queue retrieval andGET /api/follow-ups/<id>/is tenant-scoped detail retrieval; this ownership does not imply browser or production runtime proof. - The contract graph also declares the shipped create, cancel, and draft-generation operations explicitly; these remain API ownership records and do not imply live scheduler, Telegram, browser, or production evidence.
- Deduplication: only one
sentfollow-up per (organization, source_type, source_id, assignee).
Reason:
- PR-1 of the P14 implementation plan — establishes the core data model and manual send/cancel flow before reply ingestion and UI.
Impact:
- New table
follow_up_requests(migration0001_initial). - New routes registered under
/api/follow-ups/.
Tests:
backend/tests/test_followups.py— 38 tests, 100% coverage ofapps/followups/*.
Notes:
- Auto-draft, never auto-send —
send_follow_up()requires an explicit manager action. - PR-2 (reply ingestion), PR-3 (UI), PR-4 (auto-draft generation) remain.
2026-06-11 — Initial spec
Type: Feature
Summary:
- Added the Follow-up Missions spec to complete Lumi’s control loop after Brief assignment and approvals.
Reason:
- Lumi currently diagnoses and delegates, but does not own the executive chase loop.
Impact:
- Defines the v1 operating model for follow-up drafts, delivery, reply capture, and provenance.
Tests:
- Planned in
backend/tests/test_followups.pyand E2Edescribe("Follow-ups").
Notes:
- Auto-draft is in; auto-send is explicitly out for trust reasons.