Spec: F97 — Project Progress Updates
Status: shipped for the MVP scope (Phase 1 + assistant capture Phase 2)
1. Problem & CEO Job
See clearly + trust the signal. Members and PMs report through assistant chat (F91) and Daily Pulse (F07/F83), but Lumi does not attach those reports to a project. Project-specific progress, blockers, and commitments therefore reach the Brief as generic items, forcing the CEO to re-sort them. F97 disambiguates the project at capture, groups progress, and proposes evidence-linked Task/Risk/Decision items in the right project.
Source issue: https://github.com/streamentry/lumi-fb/issues/19
2. User Stories & Acceptance Criteria
-
As a project member, I want to report progress via the assistant chat or Daily Pulse without naming the project every time, so that my update costs one sentence.
- Given a pulse submission or chat message containing a project reference (name or acronym), when processed, then Lumi resolves to a visible Project (F04) record in the org with confidence ≥0.70.
- Given a project-marked chat update with no single confident match, when processed, then the proposal is tagged
needs_contextand Lumi asks one concise clarifying question rather than guessing. Generic unscoped captures retain their existing non-project flow.
-
As a PM, I want Lumi to aggregate per-project progress from multiple members’ updates over the last N days, so that I see a project status without chasing individuals.
- Given project-scoped signals in the last 7 days, when aggregated, then the output includes a deterministic, evidence-honest summary of done tasks, next tasks, blockers, and pending decisions where those states exist; each returned signal carries its EvidenceSource links and confidence.
- Given no new signals for a project in 7 days, when the project detail loads, then the Progress card shows a localized “no recent updates” note — stale silence is surfaced, not hidden.
-
As a PM, I want Lumi to propose Task, Risk, or Decision items from project updates, so that I can accept or edit in one tap rather than manually entering them.
- Given a project update contains an actionable commitment, when extracted, then a Task proposal or existing pulse commitment-to-task proposal carries the project FK and source evidence.
- Given a project update describes a blocker or material risk to timeline/cost/quality, when extracted, then a Risk proposal is created or proposed with the project FK, risk type, severity, and evidence.
- Given a project update asks for a decision, when extracted, then a DecisionRequest proposal is created (linked to the project) with the decision question and options if stated. Chat decision proposals remain read-only until the existing F91 decision executor is shipped.
-
As a CEO, I want project progress to appear in the CEO Brief (F12) when there are significant changes, so that I know which projects need my attention without opening a separate surface.
- Given a project-scoped task, risk, or decision is included in the Brief, then the existing Brief section exposes project context and the item’s evidence links; urgency filtering remains owned by F12/F101.
-
As a member, I want to see a project’s latest progress summary when I open the project detail, so that I catch up without re-reading every pulse.
- Given a project board loads, when there are project-scoped signals from the last 7 days, then the inline
ProjectProgressCardrenders the aggregated summary and lazily opens each available source EvidenceSource.
- Given a project board loads, when there are project-scoped signals from the last 7 days, then the inline
3. Scope Boundaries
- In: Project disambiguation from existing pulse/chat input (F91 assistant path + F07/F83 pulse form). Per-project aggregation of progress signals. Proposal of project-scoped Task/Risk/Decision items. Brief integration: project-scoped items appear in their Brief section with project context. Project detail page progress summary card.
- Delegated to F99: commitment deduplication by content fingerprint and EvidenceSource merge; F97 carries project context into that existing proposal path. F99 owns the duplicate merge acceptance and proof; it is not an F97 shipped criterion.
- Out: Bot transport (Telegram/WhatsApp integrations — separate spec). Standalone project reporting dashboard (deferred; the Brief + project detail page are sufficient for MVP). Full-editor project timeline/Gantt (F04 §3 explicitly out). Milestone tracking (not in scope — requires timeline model). ERP/material/BOQ integration (parent contract #15 explicitly out).
Assistant path (F91 doctrine): This feature is completable entirely through the assistant conversation. A member sends a progress update in the chat; Lumi confirms the project, proposes extracted items. The pulse form is the secondary path. No new intake forms. Form parity: the project detail page’s progress section is a read-only view, not a form.
4. Data Model
No new models. Additions to existing models:
PulseEntry (existing, extended): no schema change. Project disambiguation happens at extraction time, not at submission — the answers_json is unchanged.
SignalInboxItem (existing, extended): project FK already exists (F32 §4). This spec fills the gap: when a chat message is processed via the agent loop (F91), if project is detected, the resulting SignalInboxItem (or the extraction output directly) links the project.
Task (existing, work.Task): project FK already exists (F04). No change.
Risk (existing, operations.Risk): project FK — verify existence; if absent, add nullable FK to work.Project. Each risk item carries evidence_sources M2M (already exists).
DecisionRequest (existing, operations.DecisionRequest): project FK — same check as Risk; add nullable FK to work.Project if absent. Each decision carries evidence_sources M2M (already exists).
BriefItem (existing): no new FK needed. Project context for BriefItems is derived via related_risk.project / related_task.project / related_decision.project at generation time. The Brief generator resolves project context by joining through the related object’s project FK.
All additions follow existing project FK pattern (nullable, SET_NULL on delete). Scale rule: all new FKs use composite indexes (organization, project_id, created_at DESC) for the paginated project signal queries.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| GET | /api/projects/<id>/progress/ | member (project visibility) | ?days=7&cursor= | {summary: str?, signals: [{type, id, title, status, confidence, created_at, evidence_source_ids}...], cursor: str?} | 404 (cross-tenant/visibility); 403 |
| The MVP uses the GET response as the aggregate read path; no separate POST aggregation route is needed. |
Tenant scoping: all queries filter via organization_id = request.organization_id. Project visibility filtering uses project’s existing visibility field (team/private/organization).
The current aggregation summary is deterministic rather than LLM-generated: it counts only states present in the returned evidence-backed rows. Each sourced item includes linked EvidenceSource ids and confidence. A future LLM summary must preserve this distinction and never replace the source rows.
6. UI / UX
- Route additions: None. Progress is surfaced in existing pages:
/work/projects/:id/board(project board page) — inline “Progress” section above the kanban board/intel/brief(Brief page) — existing sections now carryproject_nameon each item card when a project is linked/intel/chat(Assistant) — confirmed progress updates appear as action proposal cards
- New component(s):
ProjectProgressCard— an expandable card in the project board page showing aggregated progress summary + individual sourced items. States: loading, empty (“No recent updates”), populated, error. Uses shadcn<Card>and the existing EvidencePopover for lazy source inspection. - Brief item enhancement: BriefItem cards gain an optional
project_badge— a small chip with the project name, clicking which filters the page or links to/work/projects/:id. Render only whenrelated_item.projectis non-null. - i18n namespace:
projectsextended with keys:progress_title,progress_empty,progress_item_done,progress_item_next,progress_item_blocked,progress_item_risk,progress_item_decision,progress_badge,progress_since,progress_evidence_link,progress_needs_context,progress_expand,progress_collapse, andprogress_error. - Assistant path (F91 doctrine): Primary path. After confirming a proposal, the assistant shows the project context on the action card. The pulse form is the secondary path (no new intake).
- States: loading (skeleton), empty (muted “Chưa có cập nhật cho project này trong 7 ngày qua”), populated (grouped timeline), error (toast + retry).
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Pulse text with explicit project name → project resolved, signal created with project FK | backend | backend/tests/test_projects.py::TestProjectProgress |
| 2 | Pulse text with ambiguous project reference → clarification asked via needs_context | backend | backend/tests/test_projects.py::TestProjectProgress |
| 3 | Project progress aggregation: 3 signals over 5 days → summary + 3 items returned | backend | backend/tests/test_projects.py::TestProjectProgress |
| 4 | Project progress aggregation: no signals in 7 days → empty state | backend | backend/tests/test_projects.py::TestProjectProgress |
| 5 | Duplicate commitment from two sources → one Task created, both EvidenceSources appended | backend | backend/tests/test_projects.py::TestProjectDedup |
| 6 | Cross-tenant: project from org A not accessible from org B | backend | backend/tests/test_multitenancy.py::TestProjectTenantIsolation |
| 7 | Cross-tenant: progress signal from org A not leaked to org B | backend | backend/tests/test_multitenancy.py::TestProjectTenantIsolation |
| 8 | Brief generation: project-scoped items appear with project badge | backend | backend/tests/test_briefs.py::TestBriefProjectScope |
| 9 | Brief generation: project-scoped items filtered by visibility (team/private) | backend | backend/tests/test_briefs.py::TestBriefProjectScope |
| 10 | Project progress card: populated, empty, and error states | frontend unit | frontend/src/test/work/ProjectProgressCard.test.tsx |
| 11 | Project progress page: populated state renders summary + items | e2e | deferred to the existing F49 project-board E2E surface |
| 12 | Assistant project context survives capture → serializer → confirm | backend | backend/tests/assistant/test_f91_p25_capture_confirmable.py::TestCaptureProposalRoundTrip |
8. Current Behavior
Project names and two-word acronyms are resolved during both pulse extraction paths and assistant run-capture, scoped to active/paused projects visible to the current member. A single safe match is carried into the proposal and confirm payload; a project-marked chat update with no single match is shown as needs_context and is not given a write-capable action. Private and team projects are rechecked at confirmation time.
GET /api/projects/<id>/progress/ returns tenant- and visibility-scoped Task/Risk/Decision rows from the requested window, with evidence ids and a deterministic summary. ProjectProgressCard renders loading, populated, empty, and error states above the project board. Brief project badges and source popovers reuse the existing F101/F86 surfaces.
The implementation intentionally does not invent a project for generic unscoped captures. Alias vocabulary and cross-project acknowledgement remain explicit follow-up work owned by F83 rather than hidden heuristics in F97. Commitment deduplication is F99-owned and is not claimed by this contract.
9. Error States
| # | Error | User message | HTTP | Logged |
|---|---|---|---|---|
| ES1 | Project not found during disambiguation | ”Lumi chưa xác định được project — vui lòng bổ sung tên project.” | n/a (in-thread) | Yes |
| ES2 | Cross-tenant project access | 404 | 404 | No |
| ES3 | Project visibility (team/private) blocks access to progress | 404 | 404 | No |
| ES4 | AI extraction failure during project signal processing | Signal saved as failed; retry available via pulse /process/ | 200 (pulse saved) | Yes |
| ES6 | LLM provider unavailable for aggregation | Aggregation remains available because the MVP summary is deterministic; no model call is required | 200 | Yes |
10. Diagrams & Docs Touched
- backend/DIAGRAMS.md: add Project → Risk, Project → DecisionRequest arrows; note project FK on existing models.
- GLOSSARY.md: add “Project progress signal”, “Project disambiguation”.
- docs/specs/F04-projects.md: add forward reference to F97 under §12 Open Questions.
- docs/specs/F12-brief.md: add forward reference to F97 for project-scoped Brief items.
11. Implementation Audit (2026-07-12)
Phase 1 + assistant capture Phase 2 built. Backend + frontend implementation complete for the MVP scope. Codebase state:
- ✅
work.Projectexists (F04) withstatus+visibilityfields. - ✅
work.TaskhasprojectFK (pre-existing). - ✅
operations.Riskandoperations.DecisionRequest— nullableprojectFK added (migration0014_f97_project_fk_on_risk_and_decision.py), composite indexesrisk_org_proj_created_idxanddecision_org_proj_created_idx. - ✅ Pulse extraction pipeline (
apps/ai/services/extraction.py) — project resolution hooked into both LLM path (_extract_via_llm) and keyword path (_extract_pulse_entry_atomic). - ✅
apps/ai/services/project_disambiguation.py—resolve_project_from_text()pure function (exact name 0.95, acronym 0.85, threshold 0.70);resolve_projects_from_text()for cross-project mentions. - ✅ Assistant
run-captureproject context: resolved project metadata is serialized on proposals and carried into confirmable Task/Risk actions; ambiguous project-marked chat updates ask for clarification before creating an action. - ✅
GET /api/projects/<id>/progress/?days=7— returns{summary, signals, cursor}with visibility enforcement (private / team / org). - ✅ Progress summary: deterministic done/next/blocked/blocker/pending-decision counts, with empty-state copy in the card.
- ✅
BriefItemSerializer— addedproject_nameandproject_idSerializerMethodFields; traverses directrisk.projectanddecision.projectFKs andtask→projectchain; select_related updated. - ✅
ProjectProgressCardReact component — TanStack Query, expandable card, type-tagged signal rows (task/risk/decision), skeleton + error + empty states. - ✅
ProjectBoardPage—ProjectProgressCardrendered above kanban columns. - ✅
BriefPage— project badge link (data-testid="brief-project-badge") linking to/work/projects/:id/board. - ✅ i18n: progress keys and assistant project-context keys are present in
en,vi, and all 59 other locale files. - ✅ Backend tests: project progress, visibility, action payload, and chat capture round-trip coverage pass; the focused F97 run is 44 tests green.
- ✅ Frontend tests: ProjectProgressCard populated/empty/error states, board integration, and ProposalCard project context pass; TypeScript is clean.
- ✅ TypeScript: 0 errors.
Deferred by boundary or ownership:
- A dedicated
resolve_projecttool is unnecessary for the current capture contract; resolution runs insiderun-captureand is covered by the round-trip test. - LLM-written aggregation prose is intentionally deferred; the deterministic summary is the honest MVP output.
- Cross-project pulse acknowledgement and alias support remain future F83 design work.
- Commitment deduplication remains F99-owned.
- A dedicated
/work/projects/:id/progressroute remains unnecessary while the inline board card is sufficient.
12. Open Questions / Decisions
- Project disambiguation scope: Exact names and two-word acronyms ship first. Nicknames/project codes should be added through the existing
OrgPulseConfiguration.custom_company_vocabcontract, with tests before enabling automatic attachment. - Pulse vs chat path divergence: Both paths now call the shared resolver; pulse remains batch extraction while chat can ask an immediate clarification question.
- Risk/DecisionRequest
projectFK: Determine whether these models need a new FK or can derive project viaevidence_sources→SignalInboxItem.project. The latter avoids a new migration but requires a join at query time. Recommendation: add nullable FK for direct query performance (the join path is available as fallback for historical data). - Cross-project signals: A single pulse or message may mention multiple projects. Should Lumi create progress items for each project, or require the member to separate them? Recommendation: create for each resolved project, with a mention of the ambiguity in the response (“Ghi nhận cập nhật cho 2 project: Duy Tân và Vincom.”).
- UI for project progress on project detail page: Should this be a dedicated route (
/work/projects/:id/progress) or an inline section on the existing detail page? The latter reduces navigation; choose inline section first, promote to a tab if content grows.
13. Bug / Change Log
14. Contract Graph evidence boundary
The canonical contract is contracts/features/F97.cue. It registers the shipped backend and frontend proof locations without treating source inspection or proof collection as execution. Missing backend/frontend dependencies, the authenticated browser journey remain explicit gaps. F99-owned duplicate commitment merging is outside F97 scope and is not represented as an F97 acceptance gap.
2026-07-10 — Initial spec
Type: Feature
Summary:
- Draft spec for F97 — project progress updates via existing channels (assistant chat + Daily Pulse). Covers project disambiguation, per-project aggregation, proposal of project-scoped Task/Risk/Decision items, deduplication, and Brief integration.
Reason:
- Tester feedback (issue #19): members/PMs need to report progress without leaving existing channels; Lumi needs to aggregate per-project status and propose evidence-linked items.
2026-07-12 — Assistant capture and honest progress summary
Type: Feature completion
Summary:
- Added member-visible project resolution to assistant capture and both pulse extraction paths.
- Carried resolved project metadata through proposal serialization and confirmable Task/Risk actions, with a clarification gate for ambiguous project-marked chat updates.
- Replaced the count-only progress sentence with a deterministic summary of done, next, blocked, blocker, and pending-decision states; added frontend states and EvidenceSource popovers.
Verification:
- Backend focused F97 suite: 44 passed.
- Frontend ProjectProgressCard, ProjectBoardPage, and ProposalCard suites: 32 passed; TypeScript and locale parity passed.