Spec: F49 — Work Graph 2.0 (the contractor’s job/site/task data graph)
Status: in progress (core shipped; documented rework outstanding — see §11)
1. Problem & CEO Job
See clearly + decide fast + trust the signal. Lumi beats Excel and Zalo for tasks but lacks the operating graph a fit-out job needs: parent tasks, handoffs, accountable people, comments, files, and project-specific workflows. Without it, the CEO sees a tidy board while memory stays in chat and spreadsheets.
F49 makes Projects and Tasks the contractor’s structured graph of sites, tasks, owners, dependencies, and status. It replaces shared Excel + Zalo, not a project-management tool the contractor already owns. Jira, Asana, and Trello inform only serious-work capabilities that strengthen this graph; hierarchy, shared ownership, workflows, comments, files, and project views are its minimum trusted-system-of-record structure.
2. User Stories & Acceptance Criteria
2.1 Hierarchy and decomposition
- As a project manager, I want to break a task into subtasks, so that one site activity can be decomposed into tractable owned work.
- Given a parent task, when I create a child task with
parent_task=<id>, then the child is saved and appears in the parent task detail response. - Given a task at depth 3, when I try to create another nested child under it, then the API rejects the request with a validation error.
- Given a task detail panel, when subtasks exist, then I can see them with at least title, status, and owner context.
- Given a task detail panel, when I submit a subtask title inline, then a new child task is created.
- Given a task detail panel on Android or iOS, when I tap a child task row, then the shell opens that child task’s detail route.
- Given the main board or project list view, when a parent has children, then hierarchy is visible inline without needing the side panel.
ProjectTasksPagenow orders children under their visible parent and indents them with a branch marker. - Given a parent task, when it is deleted, then child-task behavior is explicit and documented. Current product contract: children are retained and
parent_taskis cleared (SET_NULL), so no work item is silently deleted with its parent.
- Given a parent task, when I create a child task with
2.2 Shared ownership and assignment semantics
- As a team lead, I want more than one assignee on a task, so that execution, support, and observation are visible without duplicating tasks.
- Given a task, when I add several assignees through
TaskAssignee, then each(task, member)pair is unique and each assignee has a role. - Given task detail, when assignees exist, then the UI shows multiple assignee chips with role labels.
- Given the task detail endpoint, when a task has assignees, then the serialized task includes assignee data.
- Given a member role of
viewer, when they try to create or delete assignee rows, then the API rejects the write. - Given “My Tasks”, when I filter for my work, then tasks where I am any assignee role are included.
TaskViewSetnow filtersowner_idORassignee_through__member_id(verifiedtest_my_tasks_true_includes_assigned_tasks). - Given multiple assignee roles, when notifications fire, then primary / secondary / observer delivery rules are distinct and documented. Current shipped contract: primary and secondary assignees receive in-app
NotificationEvent(category="work", module="work"); observers are silent; self-assignment is silent; push delivery for work assignments is intentionally out of scope until F77/F36 grows a dedicated work-assignment push preference/category. - Given an owner creates a task for, or changes a task to, another active member, then the new accountable owner receives the same localized in-app work notification as an explicit primary-assignee add; self-assignment remains silent.
- Given the shared Android/iOS create or edit task form, when I choose an active human member or “Unassigned”, then the submitted task
owneris set or cleared and the form remains editable if member loading fails. - Given
Task.ownerchanges or is cleared, then that accountable owner is mirrored as aTaskAssignee(role="primary"); the prior owner’s mirrored row is removed and unrelated collaborators are preserved.
- Given a task, when I add several assignees through
2.3 Project-specific workflows
- As a delivery owner, I want project-specific statuses, so that one job can run “Survey -> Procurement -> Install -> Handover” while another runs a different flow.
- Given a project, when I create
ProjectStatusrows, then they are stored per project and ordered byorder. - Given project statuses, when I list them, then they return in order.
- Given a status, when I patch
/reorder/, then itsorderupdates. - Given a board view, when the project has custom statuses, then columns are grouped by those statuses.
- Given a board view, when the project has no custom statuses, then the board falls back to task default statuses.
- Given task create or update, when
statusis not in the project’s custom workflow, then the API rejects it._validate_project_status()enforces this on create and update. - Given the shared Android/iOS task edit form, when I choose a project or “No project”, then the edit sends the writable
projectfield, including an explicitnullwhen clearing it. - Given the shared Android/iOS task edit form, when I choose a bounded “blocked by” task, then a
TaskDependencyedge is added through the existing manager-guarded endpoint. - Given the frontend, when a manager wants to maintain the workflow, then there is a shipped status-settings UI at
/work/projects/:id/workflow(ProjectWorkflowPage): create, rename, color, expected-days, terminal flag, reorder, and delete with in-use protection. - Given a board, when the team wants Jira-like grouping behavior, then several statuses may map into one visible column. Shipped contract:
ProjectStatus.column_keylets several statuses share one visible board column; the board returns the first status as the primary status plusmapped_statusesmetadata for the grouped values.
- Given a project, when I create
2.4 Discussion on the work item
- As a team member, I want comments attached to the task, so that operational discussion stays with the work instead of escaping into chat.
- Given a task, when I post a comment, then it is persisted and returned in task detail and comment-list responses.
- Given comments exist, when the task detail panel opens, then comments render in chronological order.
- Given a viewer role, when they try to create a comment, then the API rejects the write.
- Given an
@mention, when the mentioned member belongs to the organization, then a push notification is created through F29 mention parsing and the F36 push channel (notify_comment()inapps/comments/services.py; thread-follow notifications for prior participants are also sent). - Given a comment thread, edits (
PATCH /api/comments/<id>/), soft-delete (DELETE /api/comments/<id>/), and resolve (POST /api/comments/<id>/resolve/) are supported by the universal Comment API. The Comment model also ships aparentFK for threading (replies). Reactions and thread-splits remain out of scope.
2.5 Files and evidence on the task
- As a team member, I want files on the task, so that drawings, photos, contracts, screenshots, and approvals stay attached to the work.
- Given a task and an existing
Upload, when I createTaskFile, then the attachment is persisted and visible through task detail. - Given the task detail panel, when files exist, then the panel renders a file section.
- Given a viewer role, when they try to create a
TaskFile, then the API rejects the write. - Given the frontend, when I need to attach a new file, then there is a shipped upload flow using F34 pre-signing (presign -> S3 POST -> complete -> attach) in
TaskDetailPanel. - Given upload policy, when a file is attached, then the shared F34 MIME allowlist and size caps gate the presign, only
completeduploads may attach to a task, and task-detail failure copy explains pending-upload vs invalid-type errors. Orphan cleanup remains governed by the uploads subsystem rather than task-specific logic. - Given a manager opens task detail on Android or iOS, when they choose a file from the device picker, then the shared F34 upload completes and the task-file row refreshes into the mobile detail view.
- Given a task and an existing
2.6 Project surfaces: list, board, and later timeline
- As a project manager, I want task views at project scope, so that I can plan and triage without jumping task by task.
- Given
/api/projects/<id>/tasks/, when I open the project tasks page, then I can see a flat project task list with status and priority filtering. - Given
/api/projects/<id>/board/, when I open the board, then tasks are grouped by status into columns. - Given a task card in the board or project list, when I click it, then a detail panel opens.
- Given a task on the board, when I change its status from the card select, then the task is updated.
- Given a project board on Android or iOS, when I open it, then a compact project hero exposes the board snapshot, project context, and an explicit Add task action.
- Given a project with tasks in the shared Android or iOS project directory, when I scan the list, then the card presents the server-authored raw
done/total task count as one compact localized progress fact; projects without tasks do not render a meaningless zero-progress label, and custom project terminal statuses are not inferred asdone. - Given a populated project board in a compact-width mobile layout, when I scan workflow stages, then a compact localized column picker keeps one selected column full width instead of clipping a desktop-width column into the viewport; selected state is exposed to assistive technology, and changing the picker preserves the existing project-scoped board and Move destinations.
- Given project detail on Android or iOS, when I need the project’s live work view, then the bottom-anchored Open project board action is the primary mobile action, routes to the same project-scoped board without losing the project id, keeps Edit secondary, and occupies measured layout space so the action rail never covers project context.
- Given project detail on Android or iOS, when server capabilities permit owner transfer, member add, or conversation creation, then those secondary section-header actions use compact 48 dp icon targets with localized assistive labels and stable automation identities instead of wide persistent text pills.
- Given a task card on the shared Android or iOS project board, when I tap the card, then the shell opens that task’s existing detail route in one hop; the separate Move action remains available without changing the route.
- Given an empty or populated project board, when I create a task, then the existing shared task form preserves the project and the saved task appears on this board without a manual refresh.
- Given a populated project board or project detail surface, when pull-to-refresh fails, then the last committed snapshot remains visible with a scoped warning and an explicit Retry action; a first-load failure still uses the blocking error state.
- Given canonical workflow keys such as
todoorin_progress, when the shared board renders them, then it uses the member’s locale while preserving tenant-authored labels for custom statuses. - Given a board benchmark comparable to Trello/Jira/Asana, when I move a task between statuses, then drag-and-drop is the primary affordance (
@hello-pangea/dnd); cross-column moves persist status + rank in one write path. - Given a board with many tasks, when a column exceeds the first page, then per-column “Load more” merges the next page into board state using the column
offset/has_moremetadata. - Given the shared Android or iOS task list, when I need an executive triage slice, then one compact control row lets me switch between the complete loaded list and a local Focus view (overdue, due today/tomorrow, or high/critical priority) and open an accessible sort chooser for priority, due date, or recently added. The first task must not be displaced by a permanently expanded sort row. After a populated refresh fails, the last committed directory and local triage stay visible behind one concise warning with explicit Retry; repeated refresh or task-action failure retains that snapshot, while an initial empty failure remains blocking. The view is process-local over the loaded tenant snapshot; saved cross-device views, grouping, and full scheduling remain out of scope.
- Given
The deterministic iOS simulator proof for this bounded board lifecycle is
scripts/run-ios-project-board-proof.sh: it owns a disposable Vietnamese tenant, builds and installs
the current source on one explicit simulator, runs critical flow 100, verifies the created task’s
project in SQLite, and proves fixture cleanup. This is exact-artifact simulator evidence, not a
signed named-iPhone or named Android claim; those remain in F49.GAP030.
The deterministic Android directory-recovery proof is
scripts/run-android-projects-recovery-proof.sh: it owns one managed emulator, a disposable
Vietnamese two-project tenant, an isolated backend, an allowlisted failure proxy, exact APK
custody, flow 151, database custody, fixture cleanup, evidence retention, and emulator shutdown.
The 2026-08-09 run passed with prodDebug APK SHA-256
e2b85f15044775ac08029f2c5ce284bc0bd9d42cf6c586eadfa32c6ab85234b7; it is managed-emulator
mechanics evidence, not named-device or accessibility proof.
2.7 Deadlines and schedule awareness
- As a task creator, I want due dates to remain optional, so that we do not fabricate dates just to satisfy the UI.
- Given task create, when no
due_dateis provided, then the task is valid. - Given list or board surfaces, when
due_dateis null, then the UI presents a “No deadline” state. - Given a parent task with a due date, when a subtask is created without its own due date, then it inherits the parent’s due date by default; an explicit child due date wins, and an undated parent leaves the child undated. (Backend shipped 2026-06-17:
TaskViewSet.perform_createnow applies the default server-side so every client gets the same rule. Tests:backend/tests/test_work_graph.py::TestSubtaskDepthLimit.) - Given timeline or schedule planning, when a project needs sequencing, then a shipped timeline view exists. The shipped
/work/projects/:id/timelinesurface groups existing tasks by due date, marks overdue/today/upcoming/no-deadline buckets, and opens the same task detail panel from each row. It is a schedule view, not a formal dependency graph.
- Given task create, when no
3. Scope Boundaries
-
In:
- Hierarchical tasks via
parent_task - Multi-assignee task rows with role semantics
- Shared Android/iOS create/edit selection of the single accountable owner, including explicit removal
- Per-project customizable statuses
- Task comments
- Task files linked to F34 uploads
- Bounded dependency edges (
blocked_by/blocks) for contractor handoffs - Project task list and project board surfaces
- Shared Android/iOS task-list Focus triage and deterministic local sorting over the loaded tenant snapshot
- Depth limits, tenant isolation, and basic role restrictions
- Benchmark-informed product contract describing what Lumi should and should not copy from Trello, Jira, and Asana
- Hierarchical tasks via
-
Out:
- Sprint planning, backlog grooming, epics, and story points as first-class Scrum constructs
- Time tracking, capacity planning, workload balancing, resource heatmaps
- Full dependency scheduling engine, critical path, automatic rescheduling, or Gantt-style dependency math
- Portfolio / cross-project rollup view
- Native forms intake, rules automation builder, or Butler-style automation engine
- Checklists as a first-class parallel object distinct from subtasks
- WIP limits, backlog separation, and any board-layer object more complex than
ProjectStatus.column_key - Timeline/Gantt implementation in the current shipped surface
Benchmark interpretation
- Trello teaches: low-friction cards, visible checklists, simple custom fields, easy board mental model.
- Jira teaches: configurable workflows, status-to-column mapping, dependencies, backlog separation, stronger governance.
- Asana teaches: multiple views over the same work, reusable/local custom fields, project/portfolio relationships, richer planning surfaces.
Lumi should copy only what strengthens the contractor operating graph. That means: multi-view work, per-project workflow, better metadata, attachments, comments, future dependencies, and explicit ownership semantics. It does not mean importing Scrum ceremony, generic enterprise reporting sprawl, or feature-count parity for its own sake.
3.1 Benchmark capability matrix
| Capability | Trello pattern | Jira pattern | Asana pattern | Lumi current state | Product decision |
|---|---|---|---|---|---|
| Hierarchy | checklist items inside cards; items can become cards | subtasks under issues, plus higher-order issue types | subtasks under tasks | parent/child tasks with 3-level cap | keep subtasks as primary decomposition unit for now |
| Board interaction | drag cards between lists | drag issues between columns | drag tasks between board columns | status select on board cards | upgrade to DnD later if the board becomes a primary daily surface |
| Workflow model | list-based board stages | configurable statuses mapped to columns | sections / board columns / custom fields | per-project ProjectStatus plus optional shared column_key grouping | acceptable bounded version; still not true Jira-level flexibility because grouping resolves to one primary status and there is no separate board-layer object |
| Multi-assignee | card members | one assignee plus watchers/reporters or related roles | multiple collaborators / assignees across work context | explicit TaskAssignee with roles | keep and deepen; this is strategically correct |
| Comments | card activity/comments | issue comments/activity | task conversations | linear task comments | keep simple until real need for threads emerges |
| Files | card attachments | issue attachments | task attachments | task-file rows and manager attach flow on shared mobile detail | deeper evidence types remain out of scope |
| Custom fields | board-level custom fields | custom fields on issues, richer admin control | local/global custom fields, even reference fields | none in F49 | likely next after dependencies or alongside them |
| Dependencies | weak / indirect | first-class blocked-by / blocks on timelines | lighter relationship patterns plus timelines | bounded TaskDependency edges in task detail | shipped first slice; deeper sequencing stays out |
| Multiple views | board-first | backlog, board, timeline, filters | list, board, timeline, calendar, portfolio | list + board; mobile Focus and sort | saved cross-device views, grouping, and portfolio stay out of scope for now |
| Automation | Butler rules | workflow automations | rules and bundles | none | keep out until core graph truth is reliable |
| Saved filtering/grouping | limited board filters | strong filters, backlog/query workflows | strong filter/sort/group saved layouts | basic filters plus native Focus/sort | keep triage local and bounded; add saved views only after repeatable user demand |
3.2 Competitive standard Lumi should actually hold itself to
For F49, “comparable to top tools” should not mean “match their feature count.” That is the wrong standard and leads to product sprawl.
The right standard is:
- No false state
- A task’s workflow, ownership, and completion state must be trustworthy enough to drive CEO reporting.
- Fast navigation
- List, board, and detail should let a manager move from “what is stuck?” to the exact work item in one hop.
- Durable context
- Comments, files, and assignees must survive beyond chat memory.
- Model reality better than spreadsheet + chat
- If the tool cannot represent how a contractor actually hands work off, it loses to Excel even with a prettier UI.
- Strict honesty about what is and is not shipped
- Overclaiming parity with Jira/Asana/Trello is product debt disguised as confidence.
4. Data Model
4.1 Existing and new work-graph models
Confirmed current models
class ProjectStatus(UUIDModel):
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="statuses")
name = models.CharField(max_length=50)
column_key = models.CharField(max_length=50, blank=True, default="")
color = models.CharField(max_length=7, default="#6B7280")
order = models.PositiveIntegerField(default=0)
is_terminal = models.BooleanField(default=False)
expected_days = models.PositiveIntegerField(null=True, blank=True)
class TaskAssignee(UUIDModel):
task = models.ForeignKey("Task", on_delete=models.CASCADE, related_name="assignee_through")
member = models.ForeignKey("organizations.OrganizationMember", on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default="primary")
# NOTE (2026-06-21): TaskComment was DELETED in migration
# backend/apps/work/migrations/0013_delete_taskcomment.py after its rows were
# backfilled into the universal Comment model (apps/comments/models.py,
# migration 0003_migrate_task_deal_comments). Task comments now use the
# polymorphic Comment model with target_type="task" and target_id=<task uuid>.
# The Comment model ships: parent (threading), edited_at (edits), resolved_at /
# resolved_by (resolve), deleted_at (soft delete). See §2.4 and §5.1.
class TaskFile(UUIDModel):
task = models.ForeignKey("Task", on_delete=models.CASCADE, related_name="files")
upload = models.ForeignKey("uploads.Upload", on_delete=models.PROTECT)
uploaded_by = models.ForeignKey("organizations.OrganizationMember", on_delete=models.CASCADE)
class TaskDependency(UUIDModel):
organization = models.ForeignKey("organizations.Organization", on_delete=models.CASCADE, related_name="task_dependencies")
blocked_task = models.ForeignKey("Task", on_delete=models.CASCADE, related_name="blocked_by_dependencies")
blocked_by = models.ForeignKey("Task", on_delete=models.CASCADE, related_name="blocking_dependencies")
relation_type = models.CharField(max_length=20, default="blocked_by")
4.2 Current task fields F49 depends on
Task.parent_task: self-FK,SET_NULLTask.status:CharField(max_length=20)with global choices still on the modelTask.owner: legacy single-owner field, still heavily usedTask.assignees_m2m: M2M throughTaskAssigneeTask.position: float used for list reorder, not board-column rankTask.due_date: nullableTask.evidence_sources: already present, but not deeply integrated into F49 UI yet
4.3 Model rules that are already true
- Tenant ownership lives on
Task.organizationandProject.organization TaskAssigneeuniqueness is(task, member)TaskDependencyuniqueness is(blocked_task, blocked_by, relation_type)ProjectStatusuniqueness is(project, name)- Comment ordering is chronological by
created_at - File rows point to upload records rather than storing blob metadata directly
4.4 Model rules that are still only desired, not enforced enough
- Per-project status validation: desired, not enforced
- Primary assignee sync with
Task.owner: enforced on every task save. Changing or clearing the owner removes only the prior owner’s mirrored primary row and preserves unrelated collaborators. - Notification semantics by assignee role: modeled in-app only. Primary and secondary assignees create actionable work notification events; observers and self-assignment stay silent; push-channel semantics remain deliberately unimplemented.
- Dependency edges:
TaskDependencynow models directionalblocked_byedges; deeper sequencing is still out of scope - Checklist vs subtask distinction: not modeled; subtasks are doing all decomposition work
- Column mapping separate from status values: partially modeled through
ProjectStatus.column_key. Several statuses can share one visible board column, but there is no distinctBoardColumnmodel, WIP limit, backlog separation, or per-column transition policy.
4.5 Strategic model direction from benchmark research
If F49 is taken to “serious PM surface” parity later, the next structural additions should be:
- Dependency edges (
predecessor_task,successor_task, typed relation) - Board-layer object distinct from
ProjectStatusonly ifcolumn_keyproves too weak for WIP limits, backlog separation, or per-column transition policy - Project-local custom fields first, org-global later, Asana-style
- Checklist item object only if subtasks prove too heavy for the smallest units of work, Trello-style
- Reference links to related tasks/projects if Lumi needs Asana-style lightweight relationships without formal dependencies
5. API Contract
5.1 Confirmed shipped endpoints
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| CRUD | /api/project-statuses/ | read: org member, write: org manager+ | standard DRF body | paginated list / object | 400 invalid cross-org project, 403 role restriction |
| PATCH | /api/project-statuses/<id>/reorder/ | org manager+ | { "order": number } | updated status | 400 if order missing |
| CRUD | /api/task-assignees/ | read: org member, write: org manager+ | task, member, role | paginated list / object | 400 duplicate or cross-org member, 403 role restriction |
The shipped flat assignee resource also supports PATCH /api/task-assignees/<id>/ for manager-level
role updates; backend proof ownership is F49.AC001.ASSIGNEE_PATCH, separate from runtime evidence.
It also supports full-replacement PUT /api/task-assignees/<id>/; backend proof ownership is
F49.AC001.ASSIGNEE_PUT, with runtime execution tracked in F49.GAP024.
The shipped flat dependency resource also supports full-replacement PUT /api/task-dependencies/<id>/;
backend proof ownership is F49.AC001.DEPENDENCY_PUT, with runtime execution tracked in F49.GAP025.
The shipped flat task-file resource also supports full-replacement PUT /api/task-files/<id>/;
backend proof ownership is F49.AC001.FILE_PUT, with runtime execution tracked in F49.GAP026.
The shipped flat assignee list and detail reads are contract-owned as
F49.AC001.ASSIGNEE_LIST and F49.AC001.ASSIGNEE_DETAIL_TENANT; this records source and backend
proof ownership without claiming browser, native, or deployed execution.
The exact POST /api/task-assignees/ creation surface is owned by TaskAssigneeViewSet with
collected proof F49.AC001.ASSIGNEE_CREATE; guarded backend execution remains explicit in
F49.GAP017.
The exact POST /api/task-dependencies/ creation surface is owned by TaskDependencyViewSet with
collected proof F49.AC001.DEPENDENCY_CREATE; guarded backend execution remains explicit in
F49.GAP018, including manager authorization, tenant validation, self-dependency rejection, and
duplicate protection.
The shipped flat dependency resource also supports PATCH /api/task-dependencies/<id>/; its
backend proof ownership is F49.AC001.DEPENDENCY_PATCH, separate from runtime evidence.
The shipped flat dependency list and detail reads are contract-owned as
F49.AC001.DEPENDENCY_LIST and F49.AC001.DEPENDENCY_DETAIL; the bounded tenant and role
boundaries remain governed by the existing F49 gaps.
The shipped flat task-file list/detail reads are contract-owned as F49.AC001.FILE_LIST and
F49.AC001.FILE_DETAIL_TENANT, without inferring browser, native, or deployed execution.
The shipped flat task-file resource also supports PATCH /api/task-files/<id>/; its backend proof
ownership is F49.AC001.FILE_PATCH, separate from runtime evidence.
The exact POST /api/task-files/ creation surface is owned by TaskFileViewSet with collected proof
F49.AC001.FILE_CREATE; guarded backend execution remains explicit in F49.GAP019, including
manager authorization, tenant validation, uploaded-by handling, and viewer denial.
The nested task assignee, dependency, and file create/list surfaces are contract-owned as
F49.AC001.NESTED_ASSIGNEE_CREATE, F49.AC001.NESTED_ASSIGNEE_LIST,
F49.AC001.NESTED_DEPENDENCY_CREATE, F49.AC001.NESTED_DEPENDENCY_LIST,
F49.AC001.NESTED_FILE_CREATE, and F49.AC001.NESTED_FILE_LIST.
The exact POST /api/tasks/<id>/assignees/ creation surface is owned by TaskViewSet.assignees
with collected proof F49.AC001.NESTED_ASSIGNEE_CREATE; guarded backend execution remains explicit
in F49.GAP020, including manager authorization, task scoping, cross-tenant member rejection,
viewer denial, and assignment notification behavior.
The exact POST /api/tasks/<id>/dependencies/ creation surface is owned by TaskViewSet.dependencies
with collected proof F49.AC001.NESTED_DEPENDENCY_CREATE; guarded backend execution remains explicit
in F49.GAP021, including manager authorization, task scoping, duplicate and cross-tenant rejection,
self-dependency rejection, and viewer denial.
The exact POST /api/tasks/<id>/files/ creation surface is owned by TaskViewSet.files with
collected proof F49.AC001.NESTED_FILE_CREATE; guarded backend execution remains explicit in
F49.GAP022, including manager authorization, completed-upload and MIME validation, inferred
uploader ownership, and viewer denial.
| GET | /api/comments/?target_type=task&target_id=<uuid> | org member | optional: resolved, after (cursor) | { results: [...], next_after } (keyset paginated) | 400 missing target params |
| POST | /api/comments/ | org member | { target_type: "task", target_id, body, parent?, upload?, visibility?, origin? } | comment object, 201 | 400 missing/invalid body or target, 404 parent not found |
| PATCH | /api/comments/<id>/ | author or manager+ | { body } | updated comment object | 400 empty body, 403 not author or manager |
| DELETE | /api/comments/<id>/ | author or manager+ | none | 204 (soft delete) | 403 not author or manager |
| POST | /api/comments/<id>/resolve/ | org member | { resolved: true|false } | updated comment object | 400 if non-root comment |
| CRUD | /api/task-files/ | read: org member, write: org manager+ | task, upload, uploaded_by | paginated list / object | 400 invalid task/upload, 403 role restriction |
| CRUD | /api/task-dependencies/ | read: org member, write: org manager+ | blocked_task, blocked_by, relation_type | paginated list / object | 400 duplicate or self-link, 404 cross-tenant task, 403 role restriction |
| GET | /api/tasks/<id>/tasks/ | org member | none | paginated direct subtasks | 404 cross-tenant task |
| GET | /api/projects/<id>/board/ | org member with project visibility | limit, offset | { "columns": [...] } | 404 cross-tenant project |
| GET | /api/projects/<id>/tasks/ | org member with project visibility | standard task filters | paginated tasks | 404 cross-tenant project |
| CRUD | /api/tasks/ | org member | standard task fields | task object | 400 validation errors |
| POST | /api/tasks/<id>/complete/ | org member | none | task object | 404 cross-tenant task |
| POST | /api/tasks/<id>/reopen/ | org member | none | task object | 404 cross-tenant task |
| POST | /api/tasks/<id>/reorder/ | org member | { "position": float } | task object | 400 invalid or missing position |
| GET, POST | /api/projects/<id>/statuses/ | read: org member; write: manager+ | { "name", "column_key", "color", "order", "is_terminal", "expected_days" } | status list / new status | 400 invalid project, 403 role restriction |
| PATCH, DELETE | /api/projects/<id>/statuses/<status_id>/ | manager+ | PATCH body | updated status / 204 | 403 role restriction; DELETE blocked while tasks use it |
| GET, POST | /api/tasks/<id>/assignees/ | read: org member; write: manager+ | { "member", "role" } | assignee list / new assignee | 400 duplicate or cross-org member, 403 role restriction |
| DELETE | /api/tasks/<id>/assignees/<assignee_id>/ | manager+ | none | 204 | 404 cross-tenant assignee |
| (comments moved — see consolidated /api/comments/ rows above) | | | | | |
| GET, POST | /api/tasks/<id>/files/ | read: org member; write: manager+ | { "upload" } | file list / new task file | 400 missing upload, 403 role restriction |
| DELETE | /api/tasks/<id>/files/<file_id>/ | manager+ | none | 204 | 404 cross-tenant file |
| GET, POST | /api/tasks/<id>/dependencies/ | read: org member; write: manager+ | { "blocked_by", "relation_type?" } | { blocked_by: [...], blocks: [...] } / new dependency | 400 missing blocker, duplicate, or self-link; 404 cross-tenant blocker; 403 role restriction |
| DELETE | /api/tasks/<id>/dependencies/<dependency_id>/ | manager+ | none | 204 | 404 cross-tenant dependency |
5.2 Route contract consistency
Resolved. All work-graph nested paths are consistent between frontend client and backend:
/api/projects/<id>/statuses/and/api/projects/<id>/statuses/<status_id>//api/tasks/<id>/assignees/and/api/tasks/<id>/assignees/<assignee_id>//api/tasks/<id>/files/and/api/tasks/<id>/files/<file_id>//api/tasks/<id>/dependencies/and/api/tasks/<id>/dependencies/<dependency_id>/
Comments are no longer a nested task route. As of 2026-06-21, task comments were consolidated into the universal Comment model (F81, apps/comments/). The canonical comment path for tasks is the flat, polymorphic endpoint:
GET /api/comments/?target_type=task&target_id=<uuid>— list (keyset paginated)POST /api/comments/with body{ target_type: "task", target_id: <uuid>, body: "..." }— createPATCH /api/comments/<id>/,DELETE /api/comments/<id>/,POST /api/comments/<id>/resolve/
The old nested routes /api/tasks/<id>/comments/ and /api/task-comments/ are deleted (migration 0013). Any client calling those routes gets 404. The flat resource endpoints for other objects (/api/project-statuses/, /api/task-assignees/, /api/task-files/, /api/task-dependencies/) remain available for backward compatibility. The canonical task-detail interaction for non-comment objects is the nested path.
5.3 Tenant scoping
All F49 endpoints must scope by request.organization_id.
Current shipped pattern:
ProjectViewSet.get_queryset()filtersProject.objects.filter(organization_id=request.organization_id)TaskViewSet.get_queryset()filtersTask.objects.filter(organization_id=request.organization_id)- Supporting viewsets filter through related task/project organization IDs
- Detail routes rely on queryset scoping or explicit
get_object_or_404(..., organization_id=request.organization_id)
5.4 Response shape notes
TaskSerializerincludes:- assignees as
assignee_list - files
- dependency edges as
blocked_by_dependenciesandblocking_dependencies - subtasks through
TaskListSerializer - Note:
commentsis NOT a TaskSerializer field. Task comments are fetched separately viaGET /api/comments/?target_type=task&target_id=<uuid>(universal Comment API). The frontend’sTaskDetailPanelmakes this as a separate query.
- assignees as
Project boardreturns per-column metadata:statustaskshas_moretotal
- If custom statuses exist,
statusis a serializedProjectStatus; grouped columns includecolumn_keyandmapped_statuses - If not,
statusis a string plusstatus_display
5.5 Benchmark-driven API gaps
To approach Jira/Asana/Trello seriousness, F49 eventually needs:
- nested convenience routes or client normalization so UI contracts are coherent
- project-status validation in task create/update
- a distinct board-column model only if
ProjectStatus.column_keycannot express needed workflow rules - status-config endpoints discoverable from project scope
- saved view / grouping contract if list and board become more powerful
6. UI / UX
6.1 Confirmed shipped surfaces
frontend/src/pages/work/ProjectBoardPage.tsxfrontend/src/pages/work/ProjectTasksPage.tsxfrontend/src/pages/work/TasksPage.tsxfrontend/src/components/work/TaskDetailPanel.tsxfrontend/src/hooks/queries/useWorkGraph.tsandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/CreateTaskScreen.ktandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/EditTaskScreen.ktandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/projectboard/ProjectBoardScreen.ktandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/presentation/projectboard/ProjectBoardScreenModel.ktandroid/app/src/main/kotlin/com/locuno/lumi/android/ui/LumiAppLaneEntriesV2.kt(the production V2 Work tab opens this shared Work Graph lane)android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/taskdetail/TaskDetailScreen.ktandroid/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/EditTaskScreen.ktandroid/app/src/main/kotlin/com/locuno/lumi/android/ui/TaskDetailAttachmentPicker.ktios/Lumi/KMP/TaskDetailKMPView.swift
6.2 Confirmed current UX behavior
- Project board renders columns from
/api/projects/:id/board/ - Task cards open a side panel
- Board cards change status through a
Select, not drag-and-drop - Project tasks page uses
DataTable - Task detail panel shows sections for assignees, subtasks, comments, files, and dependencies; shared mobile detail also shows a project fact when present, opens the task at either end of a dependency edge, and lets an authorized user attach a device file.
- Shared mobile detail renders existing assignees with an authorized remove action and now offers a capability-gated mobile add-assignee control; this parity slice does not change the owner form contract.
- Tapping a child task in shared mobile detail opens that child task’s detail route.
- Inline subtask create exists
- Comment composer exists
- Assignee add/remove UI exists
- Shared Android/iOS create and edit forms expose the accountable owner as an optional assignee picker. “Unassigned” clears
Task.owner; multi-assignee role editing remains in task detail. - Shared Android/iOS edit forms expose a nullable project picker and a bounded “blocked by” dependency picker; project clearing serializes
project: null. - File section includes the shipped F34-backed attach flow on web and the manager-authorized device picker flow on shared mobile.
- Dependency section shows blockers and blocked dependents, with add/remove of blocker edges from the panel
- Native task lists expose an ephemeral
All/Focusview switch and one accessible sort action in the same compact row. The sort action discloses deterministicPriority/Due date/Recently addedchoices only when requested instead of permanently consuming a second 48 dp control row.All + Recently addedis the default so the existing-created_atordering remains stable; selectingFocusclears any server-backed status filter and reloads the complete task snapshot. No saved view or cross-device persistence is implied. - Native task lists use stale-while-revalidate for populated refreshes: the last committed tasks and local Focus/sort state remain visible behind one compact warning, Retry re-enters refresh without dropping the snapshot, and task-action failures reconcile against the same current-or-stale directory. Only a first load without tasks uses the blocking error state.
- Native task list, task-detail, and subtask status badges resolve canonical statuses through the shared locale catalog (
tasks_status_*) so the Vietnamese surface does not fall back to hardcoded English labels; unknown future statuses retain a humanized defensive fallback. - Native project boards request the first 50 tasks per column, expose a bounded
Load moreaction only whenhas_moreis true, and append only the selected column’s response while preserving every other visible column. A failed page keeps the existing board on screen and exposes an exact retry action for that column; a refresh clears stale page-retry state. - Native project boards and project detail use stale-while-revalidate for refreshes: a failed refresh keeps the last committed board/project visible, labels the transport failure locally, and exposes an explicit Retry action. Only a first load without a snapshot uses the blocking error state.
- Native project lists use the same recovery contract: a refresh failure keeps the last committed directory visible with localized product copy and an exact Retry action. Repeated failure keeps the same snapshot; list, board, and detail recovery surfaces do not render raw transport text.
6.2.1 Shared mobile task-detail visual contract
The Android and iOS task-detail surface uses a compact top header with the localized screen
title (Task / Nhiệm vụ) and no duplicate page title in the content when the native iOS shell
already owns the title. The first content surface is a layered hero sheet that makes the work
item, status, project, accountable owner, deadline, priority, description, and field-execution state
glanceable. Assignees, subtasks, dependencies, files, and comments use icon-led section headers
and grouped sheets with hairline dividers rather than a stack of isolated cards. Authorized users
can add an active human member from the assignee section; selecting a member submits the nested
task-assignee route, keeps existing committed rows visible until the server responds, and refreshes
the task from the authored response. Member loading, no-candidate, add failure, and retry states
stay local to that section. Add failures use localized product copy rather than raw transport
messages, and Retry remains bound to the exact selected member. These are visual and information-hierarchy rules only: existing task
actions, permissions, relation semantics, refresh behavior, and empty/loading/error states remain
unchanged. Repeated comment edit/delete
actions use icon-only controls with localized accessibility labels; the task-level edit CTA remains
labeled. Named-device Android and signed-iOS visual runs are separate proof obligations and are not
inferred from source review.
Empty assignee, subtask, dependency, and file states use one flat support row beneath the section header instead of an additional card shell. The row keeps a muted leading glyph and compact copy, while any add/upload control remains a separate, full-size action with its existing permission and accessibility contract. This preserves the empty-state explanation without turning a mostly empty task into a vertical stack of competing surfaces.
Project board cards are the entry point to the same task-detail route used by the task list. The card surface is a semantic navigation target on Android and iOS, while the inline Move action is a separate target that opens the existing status picker and never accidentally navigates. Shells own the destination callback so notification, list, board, and child-task entry points converge on one allowlisted task-detail route.
6.3 Required states
Every F49 surface must explicitly design:
- empty
- loading
- error
- populated
- no-permission
- partial-data mismatch when side-panel nested endpoints fail
Current code visibly handles some of these, but the spec needs the full contract because benchmark-grade work surfaces fail in partial, not just total, ways.
6.4 Benchmark conclusions for Lumi UX
From Trello
- Keep the card mental model lightweight
- Small metadata should be glanceable on the card front
- Checklist/subtask progress visibility matters
From Jira
- Workflow configuration belongs near the board
- Status/column semantics must be explicit
- Complex workflows need admin affordances, not ad hoc edits
From Asana
- The same work item should support list, board, and later timeline views
- Custom fields and cross-view consistency become essential once the work model matures
- Side-panel detail should be rich enough that users do not need route thrash
6.5 Lumi-specific UX rules
- The board cannot drift into decorative SaaS theater. It must stay operational, dense, and scannable.
- The side panel is the workbench, not a marketing card.
- Local contractor vocabulary matters more than generic agile jargon.
- The “best AI workspace” version of F49 is not the loudest one. It is the one with the fewest false states and the shortest path from stale signal to corrective action.
6.6 i18n
Current confirmed namespaces:
tasksprojects
Confirmed current keys include board/detail keys such as:
board_detail_titleboard_statusboard_priorityboard_ownerboard_dueboard_assigneesboard_subtasksboard_commentsboard_filesboard_add_assigneeboard_search_assigneeboard_subtask_placeholder- project board/list keys in
projects
All seven shipped shared-resource locales (en, vi, ar, fil, id, lv, ms) must remain updated together for any F49 UI expansion.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Create custom project status | backend | backend/tests/test_work_graph.py::TestProjectStatus |
| 2 | List project statuses in order | backend | backend/tests/test_work_graph.py::TestProjectStatus |
| 3 | Reject duplicate status name within one project | backend | backend/tests/test_work_graph.py::TestProjectStatus |
| 4 | Create multi-assignee rows with roles | backend | backend/tests/test_work_graph.py::TestMultiAssignee |
| 5 | List/delete task assignees | backend | backend/tests/test_work_graph.py::TestMultiAssignee |
| 6 | Create/list/delete/edit/resolve task comments (via universal Comment API) | backend | backend/tests/test_comments.py::TestComment |
| 7 | Create/list/delete task files | backend | backend/tests/test_work_graph.py::TestTaskFiles |
| 8 | Task detail serializes subtasks/files/assignees; comments fetched via separate Comment API | backend | backend/tests/test_work_graph.py::TestHierarchicalTasks, TestTaskFiles |
| 9 | Project board groups tasks by custom and default statuses | backend | backend/tests/test_work_graph.py::TestProjectBoard |
| 10 | Project board paginates per column | backend | backend/tests/test_work_graph.py::TestProjectBoard |
| 11 | Project tasks endpoint respects filters | backend | backend/tests/test_work_graph.py::TestProjectTasks |
| 12 | Direct subtasks endpoint returns only direct children | backend | backend/tests/test_work_graph.py::TestSubtasks |
| 13 | My Tasks filter behavior | backend | backend/tests/test_work_graph.py::TestMyTasksFilter |
| 14 | Subtask depth limit enforced | backend | backend/tests/test_work_graph.py::TestSubtaskDepthLimit |
| 15 | Subtask depth query count / tenant safety | backend | backend/tests/test_work_graph.py::TestSubtaskDepthCTE, TestSubtaskDepthTenantSafety, TestSubtaskDepthQueryCount |
| 16 | Viewer cannot write statuses / assignees / comments / files | backend | backend/tests/test_work_graph.py::TestViewerPermissions |
| 17 | Board page opens and task detail panel renders | e2e | frontend/tests/e2e/lumi.spec.ts F49 block |
| 18 | Status change from board card works | e2e | frontend/tests/e2e/lumi.spec.ts F49 block |
| 19 | Comment creation from detail panel works against real route contract | e2e | required re-check because frontend path contract appears mismatched |
| 20 | Assignee add/remove from detail panel works against real route contract | e2e | required re-check because frontend path contract appears mismatched |
| 21 | File list and file attach flow | e2e | web attach flow exists; browser execution remains a separate proof obligation |
| 22 | Board drag-and-drop across statuses | e2e | not shipped; add only when implemented |
| 23 | Mobile task Focus membership, deterministic sort order, compact view/sort disclosure, status/view interaction, stale refresh/action preservation, explicit recovery, and semantic controls | Kotlin JVM + source policy + Maestro | TaskListPolicyTest, TasksScreenModelTest, scripts/tests/test_mobile_tasks_focus_sort_policy.py, .maestro/critical/108_tasks_focus_sort.yaml, .maestro/critical/157_android_tasks_refresh_recovery.yaml |
8. Current Behavior
Confirmed current behavior
- Work-graph models exist and are tested
- Task detail serialization includes comments, files, subtasks, and assignee list
- Project board and project task-list endpoints exist
- Board page and project task page exist
- Task detail panel exists
- Inline subtask add exists
- Assignee chips and comment composer exist
- Shared mobile task detail now offers an active-human assignee picker for users with
members.manage; failed adds retain an exact retry action and do not optimistically mutate rows. - Shared Android/iOS task create/edit forms can choose, change, or clear the accountable owner
- Backend task saves mirror
Task.owneras a primaryTaskAssigneewithout deleting unrelated collaborator roles - Per-project custom statuses exist in the backend
- Depth limit is enforced at 3 nested levels
- Tenant isolation and viewer write restrictions are tested
Confirmed missing or weaker-than-spec behavior
- No checklist object
- No automation/rules layer
- No board-layer object beyond
ProjectStatus.column_key - No full dependency scheduling engine beyond bounded
blocked_byedges - Native task lists now have a local, process-ephemeral Focus triage and deterministic sort layer over the already loaded tenant snapshot. Saved cross-device views, grouping, and full scheduling remain absent by design.
9. Error States
| # | Scenario | User-visible | Internal handling | Logging / retry |
|---|---|---|---|---|
| 1 | Task nested deeper than 3 levels | validation error on subtask create | _subtask_depth() rejects create | backend test exists; user must create elsewhere |
| 2 | Cross-tenant task/project/status access | 404 or 400 depending on route | queryset scoping / explicit object lookup | security-critical; no retry hint |
| 3 | Viewer attempts write | 403 | role permission check in viewset | expected, no retry unless role changes |
| 4 | Invalid board column page params | 400 or server-side failure if malformed | current code casts int() directly | needs hardened UX copy if exposed |
| 5 | Frontend calls nested route that backend does not expose | task panel subsection fails or entire mutation fails | route mismatch between client and server | must be fixed structurally, not papered over |
| 6 | Status changed to a value outside project workflow | currently may succeed if it matches global Task.STATUS_CHOICES | missing project-specific validation | this is a trust-the-signal defect |
| 7 | Status deleted while tasks still use it | current behavior under-defined | no clear migration/reset policy in shipped code | needs explicit product rule |
| 8 | File section empty because upload flow is absent, not because there are no files | Shared task detail renders the upload hint only when an authorized picker is mounted; otherwise it keeps a neutral, compact no-files row | TaskDetailBody derives one canAttachTaskFile capability from both host picker and server permission | source contract F49.AC004.FILES_EMPTY_CAPABILITY |
| 9 | Board has more tasks than first page per column | Native and web boards show a bounded per-column Load more affordance; native page errors keep visible tasks and offer an exact retry | API supports limit/offset; native ProjectBoardScreenModel merges only the requested column and reconciles total / has_more | shared JVM tests plus Android Maestro flow 110; named-device fidelity remains open |
| 10 | Comment or assignee mutation succeeds in backend route tests but fails in real UI route path | user sees broken interaction | integration contract mismatch | requires E2E proof after fix |
| 11 | Task-detail member list fails or an assignee add is rejected | existing assignees remain visible; the section shows localized product copy, an exact-member retry action, and no optimistic row | shared model keeps the selected member retry intent and commits only the server response | JVM/source proof plus guarded Android emulator flow 99 cover role gating, candidate filtering, a real duplicate rejection, failure preservation, retry, and final server custody |
10. Diagrams & Docs Touched
frontend/DIAGRAMS.md- section
Work Graph 2.0 — Board + Task Detail Data Flow - documents the nested task routes the backend now exposes; keep the diagram in sync with the route table above
- section
backend/DIAGRAMS.md- task / project workflow relationships
- any future dependency graph or column-mapping split
GLOSSARY.md- if F49 introduces formal terms such as watcher, dependency, work item, or workflow column mapping
docs/MVP_SCOPE.md- current row still describes F49 as
mini-Jira: subtasks, multi-assignee, workflows; that description is directionally fine but underspecified relative to this richer contract
- current row still describes F49 as
11. Implementation Audit (2026-06-15)
Verdict: partially strong foundation, but not yet honestly comparable to top work-management products unless the comparison is limited to “has subtasks, assignees, comments, files, list view, board view, and per-project statuses.” The old spec blurred benchmark ambition and shipped reality. This rewrite separates them.
What the current implementation gets right
-
The data model is materially better than a flat task list.
ProjectStatus,TaskAssignee, andTaskFileare real persisted models. Task comments are served by the universalCommentmodel inapps/comments/(TaskCommentwas deleted in migration 0013 on 2026-06-21 after a data backfill).
-
Tenant isolation and permission rails are present in the backend.
- This matters more than feature count. A work graph that leaks cross-tenant data is unusable.
-
Subtask depth is constrained with explicit tests and a recursive-query implementation.
- That is a defensible product opinion, not a missing feature.
-
Project-level task surfaces exist both as list and board.
- That is the minimum shape needed to grow toward Asana/Jira-grade multi-view work.
-
Task detail is already the richest operational work surface in the feature.
- Comments, assignees, files, and subtasks belong there.
Where spec and code still disagree, ordered by priority
-
[P1] Column semantics are still too simple for Jira-like workflows
ProjectStatus.column_keynow allows many statuses to share one visible board column, but there is still no distinct board-layer object, no WIP limits, and no backlog separation.- Decision: acceptable for Lumi now, but must be called a bounded grouping rule, not Jira parity.
-
[P3] Dependency model is intentionally bounded
TaskDependencynow captures explicit handoff blockers in task detail (blocked_byand reverseblocks).- Decision: this is enough for contractor handoffs now; do not claim Jira/Asana sequencing, critical path, dependency calendars, or automatic rescheduling.
Sequenced rework roadmap (to reach benchmark-grade work management)
The gaps above are not a flat backlog. They have a dependency order. Build in this sequence so that every phase improves the trustworthiness of the work graph before adding more surface area.
Phase A — Contract truth (done — now foundation, not blocker)
- ✅ Unify the task-panel route contract: nested routes are now shipped end to end.
- ✅ Add strict per-project status validation on task create/update (
_validate_project_status). - ✅ Fix
my_tasksso assignee roles participate in the filter, not onlyowner.- Exit test passed: task detail mutations work in the real UI, invalid project-specific statuses are rejected, and assignee work appears in My Tasks.
Phase B — Board credibility (done — DnD + load-more are live)
4. ✅ Replace the board-card status Select as the primary interaction with drag-and-drop status moves (@hello-pangea/dnd).
5. ✅ Persist status move + rank in one write path so board order cannot drift on cross-column moves.
6. ✅ Finish per-column pagination with a real Load more flow.
- Exit test passed: a manager can triage a full project from the board without falling back to the task list for basic movement or pagination.
Phase C — Workflow administration (make project-specific workflows real, not backend-only)
7. ✅ Ship manager-facing status settings UI: create, rename, color, reorder, terminal-state semantics, delete protection (ProjectWorkflowPage).
8. ✅ Define and enforce deletion behavior for statuses still used by tasks.
9. ✅ Add ProjectStatus.column_key so several statuses may share one visible board column without introducing a separate board-layer model.
Phase D — Evidence completeness (make the task the durable operating record)
10. ✅ Add F34-backed file attach flow in the task detail panel.
11. ✅ Finish task-file upload policy at the attach surface: reuse F34 MIME/size gate, reject pending uploads, and surface specific failure copy in TaskDetailPanel.
12. ✅ Extend the F34 attach path to shared mobile task detail, wire child-task navigation, and make nullable project/dependency editing reachable.
13. ✅ Add notification semantics for comment mentions where they sharpen execution rather than add noise. (2026-06-21 update: comment handling was consolidated into the universal Comment model in apps/comments/. Notifications are now dispatched by notify_comment() in apps/comments/services.py, which also covers thread-follow notifications. TestTaskCommentMentions no longer exists in test_work_graph.py; equivalent tests live in backend/tests/test_comments.py::TestNotifications.)
Phase E — Higher-order graph power (only after the core is trustworthy)
14. ✅ Dependency edges for real contractor handoffs (blocked_by, blocks, bounded TaskDependency relation). Backend shipped TaskDependency + nested /api/tasks/<id>/dependencies/ create/list/delete, with duplicate/self/cross-tenant protections and task-detail serialization. Web shipped a TaskDetailPanel dependency section that shows blockers and blocked dependents, adds blockers from same-project task candidates, and removes blocker edges.
15. ✅ Timeline / schedule view over existing task due dates (/work/projects/:id/timeline).
16. Custom fields or lightweight checklist objects only if users prove subtasks are the wrong granularity.
- Exit test: Lumi models real handoff chains and schedule risk better than spreadsheet + chat, without importing Scrum theater.
Note: The roadmap above now reflects shipped reality. The remaining active F49 work is any future board-layer object beyond ProjectStatus.column_key if workflow rules outgrow it. The timeline view is shipped as a due-date schedule surface; formal dependency sequencing remains out of scope.
Benchmark parity scorecard
Honest self-assessment of where F49 stands against a Jira/Asana/Trello-class serious work surface. Scores are 0 (absent) to 5 (benchmark-grade). This is judgment, not telemetry. Its job is to stop the spec from drifting back into all-green fiction.
| Dimension | Top-tool bar | Lumi now | Target | Gap |
|---|---|---|---|---|
| Hierarchy / decomposition | 5 | 4 | 4 | at target; checklist items and higher-order issue types remain explicitly out of scope |
| Multi-assignee ownership | 5 | 4 | 4 | owner-vs-assignee clarity |
| Workflow configurability | 5 | 4 | 4 | deeper board-layer object if column_key proves insufficient |
| Board interaction | 5 | 4 | 4 | board-layer policy beyond column_key if workflow rules outgrow it |
| Task detail depth | 5 | 4 | 5 | richer evidence actions; named-device mobile visual proof remains open |
| Files / evidence | 5 | 4 | 4 | deeper evidence actions; named-device mobile upload proof remains open |
| Comments / collaboration | 5 | 4 | 4 | linear only; thread semantics explicitly out of scope |
| Multi-view consistency | 5 | 4 | 4 | saved view / sort-group filters not yet shipped |
| Dependency / sequencing power | 5 | 3 | 3 | bounded dependency edges shipped; full scheduling engine remains out |
| Workflow admin ergonomics | 5 | 4 | 4 | deeper board-layer policy beyond column_key |
| Signal trustworthiness | 5 | 4 | 5 | bounded board-layer policy; route contract and status validation resolved |
Weighted read: Phases A–E are complete. The object model, board, admin, and detail panel are all at or
near their target scores. The one dimension still below target is signal trustworthiness (4/5 vs 5/5
target) — the remaining gap is bounded board-layer policy; all route-contract and status-validation
false states are resolved. The right move is not feature sprawl. It is to escalate board-layer policy
only if ProjectStatus.column_key proves too simple for real workflow rules.
The exact GET /api/project-statuses/ surface is registered to ProjectStatusViewSet with
F49.AC001.PROJECT_STATUS_LIST; repository evidence remains separate from browser, native,
deployed, Redis, Sentry, and production evidence.
The exact POST /api/project-statuses/ creation surface is now separately registered to
ProjectStatusViewSet with F49.AC001.PROJECT_STATUS_CREATE; its guarded backend execution and
cross-tenant validation remain explicit in F49.GAP015.
The exact nested POST /api/projects/{id}/statuses/ creation surface is registered to
ProjectViewSet.statuses with F49.AC001.NESTED_STATUS_CREATE; default-status seeding and its
guarded backend execution remain explicit in F49.GAP016.
The exact PATCH /api/project-statuses/{id}/ surface is separately registered to
ProjectStatusViewSet with F49.AC001.PROJECT_STATUS_PATCH; repository evidence remains
separate from backend execution, browser, native, deployed, Redis, Sentry, and production evidence.
The exact full-replacement PUT /api/project-statuses/{id}/ surface is separately registered to
ProjectStatusViewSet with F49.AC001.PROJECT_STATUS_PUT; backend execution remains explicit in
F49.GAP023 and no browser, native, deployed, Redis, Sentry, or production evidence is inferred.
The exact PATCH /api/project-statuses/{id}/reorder/ action is separately registered with
F49.AC001.PROJECT_STATUS_REORDER; repository evidence remains separate from backend execution,
browser, native, deployed, Redis, Sentry, and production evidence.
The exact nested PATCH /api/projects/{id}/statuses/{status_id}/ action is separately registered
with F49.AC001.NESTED_STATUS_PATCH; repository evidence remains separate from backend execution,
browser, native, deployed, Redis, Sentry, and production evidence.
The exact GET /api/projects/{id}/tasks/ surface is registered to ProjectViewSet.tasks with
F49.AC001.PROJECT_TASK_LIST; repository evidence remains separate from browser, native,
deployed, Redis, Sentry, and production evidence.
The exact GET /api/tasks/{id}/tasks/ direct-subtask surface is registered to
TaskViewSet.tasks with F49.AC001.SUBTASK_LIST; repository evidence remains separate from
backend execution, browser, native, deployed, Redis, Sentry, and production evidence.
2026-06-25 — Mobile (shared KMP) Work Graph 2.0 surfaces
Type: Feature (mobile / shared Compose Multiplatform)
Scope: android/ only. No backend or web change. The backend JSON contract was
verified field-by-field against backend/apps/work/serializers.py and
backend/apps/comments/serializers.py before writing DTOs.
Summary:
- Expanded the task DTO surface in
android/shared/.../api/LumiDtos.kt: addedTaskDetailResponse(mirrors the fullTaskSerializer:assignee_list,files,blocked_by_dependencies,blocking_dependencies,subtasks,parent_task, owner/project/team names) plus nestedTaskAssigneeDto,TaskFileDto,TaskDependencyDto,TaskSummaryDto(=TaskListSerializer),ProjectStatusDto,BoardResponse/BoardColumnResponse(with aJsonElementstatusfield + typed accessors to handle the backend’s polymorphic custom-object-vs-string column shape), andCommentDto/CommentListResponse(generic comments API,target_type="task"). Added create-request DTOs (TaskAssigneeCreateRequest,TaskDependencyCreateRequest,CommentCreateRequest) and aparent_taskfield on the existingTaskCreateRequestfor inline subtask creation. - Task detail screen (
shared/.../presentation/taskdetail/+shared/.../ui/taskdetail/TaskDetailScreen.kt) mirroring the webTaskDetailPanel: header, assignees+roles, subtasks (with inline create), dependencies (blocked-by / blocking), files (read-only list), comments (list + create). Task and comments load independently so a partial-data failure does not blank the workbench (§6.3). - Project board (kanban) screen (
shared/.../presentation/projectboard/+shared/.../ui/projectboard/ProjectBoardScreen.kt) mirroringProjectBoardPage: horizontally scrollable columns grouped byProjectStatus.column_key(or default statuses), per-column counts, and a tap-to-move action (DnD is unavailable in shared Compose, so move-via-menu is the accepted fallback per §3.1). - API client: added
workTaskDetail,taskAssignees/addTaskAssignee/removeTaskAssignee,taskFiles,taskDependencies/addTaskDependency/removeTaskDependency,createSubtask,projectBoard,projectTasks,comments/createCommentinLumiApiClient.kt. - Nav:
TaskDetailNavKey(taskId)+ProjectBoardNavKey(projectId)registered inLumiApp.kt(model built via newSharedKmpViewModel.taskDetailModel/projectBoardModelfactories); F80 deep-links fortask/projectnow route to these detail surfaces when an id is present. iOS/macOS view-controller factories added in bothMainViewController.ktfiles. - i18n: 38 new
taskdetail_*/board_*keys added to bothvalues/strings.xmlandvalues-vi/strings.xml(verified en/vi at parity). - Tests:
TaskDetailScreenModelTest(8 cases) andProjectBoardScreenModelTest
2026-08-22 — Project rows and detail surface server-counted overdue work
Type: Shared Android/iOS presentation correctness
Summary:
- Projects list rows showed done/total progress but nothing distinguished a stuck project from a healthy one; project detail showed only the total task count.
- The backend projects queryset now annotates
active_task_count(todo/in_progress/waiting) andoverdue_task_count(active past due date) with the same archived-task exclusion astask_count, so the buckets always describe one consistent set. The serializer exposes both. - Shared rows render a red localized overdue signal (
projectOverdueSignal) only when the count is positive; zero and unknown stay quiet — no decorative badges. Detail facts add Active tasks and Overdue tasks rows from the same snapshot.
Tests:
backend/tests/api/test_projects.py::test_active_and_overdue_task_counts_describe_open_workDtoContractCrmPrimaryContractTest.projectResponseWithAllBackendFieldsDeserializes(extended fixture)ProjectsScreenLogicTest.projectOverdueSignalRendersOnlyPositiveServerCountsscripts/tests/test_project_board_visual_policy.py::test_project_rows_surface_server_counted_overdue_signal.maestro/critical/151_android_projects_refresh_recovery.yaml+scripts/run-android-projects-recovery-proof.sh
Proof boundary:
- Guarded flow 151 re-passed on identical built/installed prodDebug APK SHA-256
f1a7bdb7…db42f4(Lumi_Pixel7_API35):2/4 đã xong+ red1 quá hạnon the stuck row, quiet healthy row, recovery journey intact, custody 4/2/1, cleanup0:0:0:0. Named-device, iOS runtime, accessibility-service, store, deployed, and production proof remain open.
2026-08-12 — Android task-file attachment receives exact lifecycle proof
Type: Runtime proof and harness repair
Summary:
- Extended guarded Android flow 99 through the real system document picker, F34 presign/object POST/HEAD/complete lifecycle, nested task-file attach, refreshed file row, and exact SQLite custody.
- Made the clean-worktree runner explicit about its backend interpreter and Android SDK, cold-booted the approved AVD so shared Downloads is mounted, used a PID-scoped fixture, and verified exact file removal before sole-emulator shutdown.
- Registered
F49.AC004.TASK_FILE_ANDROID_RUNTIME_POLICYand retained current WRK-03 screenshots.
Evidence:
- Identical built/installed prodDebug APK SHA-256
8c0c8693f0502715501e6a8522a4f1f430654ff89ed45b9e19b05022e1a04d91. - One completed Upload and one TaskFile owned by the proof user; Android emulator mechanics only.
Open boundary:
- Named Android, current-source iOS, signed named-iPhone, screen-reader, store, deployed, and
production proof remain open under
F49.GAP029.
12. Contract Graph reconciliation (2026-07-15)
The canonical Contract Graph slice follows executable repository evidence rather than the
historical benchmark notes above. Backend work-graph tests are split across
backend/tests/work/test_work_graph_resources.py, test_work_graph_routes.py,
test_work_graph_depth.py, and test_work_graph_security.py; there is no single
backend/tests/test_work_graph.py file in the current checkout.
The current web implementation and focused tests establish that project-specific status
validation, grouped board columns, depth-four rejection, nested task-file routes, and a
cross-tenant project-status boundary are shipped. ProjectBoardPage also contains the
drag-and-drop and per-column load-more paths, and ProjectTimelinePage is a shipped due-date
schedule view. These source and repository-test facts do not establish browser execution,
native/device behavior, live Redis/Sentry/provider behavior, or production availability.
Those remain explicit Contract Graph gaps. The bounded TaskDependency relation is not a
critical-path or automatic-rescheduling engine, and saved views, WIP policy, portfolio rollups,
and automation rules remain outside this contract.
2026-08-03 — Native project-board pagination and recoverable page errors
Type: UX feature completion (shared mobile)
Summary:
- Added a production
boardPage(projectId, limit, offset)repository seam while keeping existing board fakes source-compatible. ProjectBoardScreenModelnow requests a bounded page for one column, appends only that column, reconcilestotal/has_more, and refuses to overwrite a board that was refreshed while the page was in flight.- The shared board renders
Load moreonly for columns with remaining work. A failed page keeps visible tasks and exposes a retry action tied to the failed column instead of requiring the user to rediscover the scroll position. - Added localized copy for all seven shared mobile locales, JVM coverage for merge/no-op/failure paths, and one Android lifecycle-owned Maestro flow with 51 disposable tasks.
Tests / proof:
ProjectBoardScreenModelTestpagination cases andscripts/tests/test_project_board_visual_policy.py..maestro/critical/110_android_project_board_load_more.yamlthroughscripts/run-android-project-board-pagination-proof.sh; the runner must be wrapped by the machine-wide resource guard and keeps named-device gaps open.
Reason:
- A board that silently stops at the first 50 tasks is a false view of work. Pagination must be explicit, bounded, and recoverable without turning the mobile board into a second task-list app.
2026-07-24 — Bug fix: inline subtask creation refreshes its parent
Type: Bug Fix
Summary:
- Inline subtask creation already sent the correct
parent_taskand could succeed at the API. - The success callback invalidated
taskSubtaskswith an empty task ID, while the open task-detail panel subscribed to the parent-specific key. - Successful creation now invalidates the exact parent task’s subtask collection.
Reason:
- The stale cache made a successful write look like a failed create because the new child did not appear in the open panel.
Proof:
frontend/src/test/hooks/useWorkGraph.test.tsx(@lcg:F49.AC002.SUBTASK_REFRESH) asserts both the wire payload and exact parent-key invalidation.- Browser and deployed behavior remain governed by
F49.GAP001; the unit proof does not close those evidence boundaries.
The exact GET /api/projects/{id}/statuses/ surface is registered to ProjectViewSet.statuses
with F49.AC001.NESTED_STATUS_LIST; repository evidence remains separate from browser, native,
deployed, Redis, Sentry, and production evidence.
The exact GET /api/project-statuses/{id}/ surface is registered to ProjectStatusViewSet with
the dedicated F49.AC001.PROJECT_STATUS_DETAIL member retrieval proof.
The exact GET /api/projects/{id}/board/ surface is registered to ProjectViewSet.board with
F49.AC001.BOARD_API; browser, native, deployed, Redis, Sentry, and production evidence remain
separate.
The Contract Graph also assigns the shipped work-graph deletion routes to their exact router and
viewset action owners. This does not promote runtime, browser, native, operational, or production
evidence.
(5 cases) in commonTest; plus 4 new DtoContractTest cases that deserialize
real backend-shaped JSON for TaskDetailResponse, both board column shapes, and
CommentListResponse.
Deliberately left for follow-up:
- File upload from mobile is not shipped — only the read-only file list. The F34 presign → S3 → complete flow has no shared-KMP DTO/client yet; adding it is a separate task. The UI shows a “attach from the desktop app” hint instead of a dead empty state (§6.3 / §9.8).
- Assignee/dependency mutation UI is not wired into the screen (the client methods and DTOs exist); the detail screen currently renders these read-only plus inline subtask + comment create. Add the add/remove affordances next.
Verification (full Android Gradle build is blocked in this environment —
dl.google.com / AGP com.android.lint:9.0.0 denied by egress; confirmed by a
failed ./gradlew :shared:jvmTest). Substituted static verification:
- (a) Every DTO field name was checked against the backend serializer field lists
(
TaskSerializer,TaskListSerializer,TaskAssigneeSerializer,TaskFileSerializer,TaskDependencySerializer,ProjectStatusSerializer, board action payload,CommentSerializer). - (b) Every Lumi UI primitive used (
LumiPageTitle,LumiCard,LumiSectionHeaderwithbadge,LumiBadgeincl.showDot,LumiInlineActionwithenabled,LumiMultilineField,LumiStatusBanner,LumiSkeletonList(rows=…),LumiMeta,LumiTone) and color tokens (PaperWhite,CivicNavy,SoftSlate) were grep- confirmed againstLumiMobilePrimitives.ktsignatures; no raw hex colors and no hardcoded user-facing strings in the new screens. - (c) Screen models follow the exact lifecycle of the reference trio
(
TasksScreenModel):LoadStatemachine,testScopeinjection,initload,runCatching{}.foldsuccess/error, mutate→reload. - (d) ADR-025 DTO gate (
android/scripts/check-no-native-dtos.sh) passes — all new DTOs live inshared/api/LumiDtos.kt; all referenced i18n keys exist with en/vi parity.
12. Open Questions / Decisions
-
Should F49 adopt full dependency scheduling next, or custom fields first?
- My judgment: custom fields first if metadata variance across projects is already painful. Full dependency scheduling should wait for proof that the bounded
blocked_bygraph is not enough.
- My judgment: custom fields first if metadata variance across projects is already painful. Full dependency scheduling should wait for proof that the bounded
-
Should
Task.ownerremain the dominant ownership field?- Current code still treats it as primary. Multi-assignee maturity likely requires either strict synchronization or a clearer “accountable owner vs contributors” split.
-
Should Lumi add Trello-style checklist items, or keep using subtasks for the smallest unit?
- My judgment: keep subtasks until users show a strong need for lighter-weight completion items. A second decomposition object too early will muddy the graph.
-
Is
ProjectStatus.column_keyenough, or does Lumi eventually need a separate board-column object?- Current judgment:
column_keyis the right bounded step. Escalate only when the product genuinely needs WIP limits, backlog separation, or per-column transition policy.
- Current judgment:
-
Should comments, assignees, and files become nested task routes or stay flat resources?
- Either can work. The wrong answer is keeping the frontend and backend inconsistent.
13. Bug / Change Log
2026-08-29 — Owner assignment notification parity (feedback #136)
Type: Backend correctness / notification delivery
Gap: The locked owner-to-primary-assignee synchronization created the mirrored TaskAssignee
row but bypassed _notify_task_assignment(). A manager could therefore assign a task to another
member through the task create/edit owner field without an in-app work notification, while the
explicit assignee route notified correctly.
Fix contract: the owner-sync service accepts the acting membership and notifies when a new task is created for another member or the accountable owner changes. Self-assignment and observer rows remain silent; the existing localized notification payload and task route are reused.
Tests: backend/tests/work/test_work_graph_resources.py owner-create and owner-change notification
regressions, plus the existing assignee-role notification matrix.
2026-08-12 — Mobile project cards expose real task progress
Type: Shared Android/iOS project-directory UX
- The project API already returned tenant-scoped
task_countanddone_task_count, but the shared mobile DTO discarded the rawdonecount and reduced project cards to a weak total-task label. - Shared Android/iOS now decode both annotations and present one compact localized raw
done/total fact for populated projects. Empty projects remain quiet, and legacy snapshots without a validdonecount retain the existing total-task fallback; custom terminal statuses are not inferred. - Guarded Android flow 151 rendered
2/3 đã xong, preserved it through search and a bounded repeated list outage, verified exact task/project custody, cleaned0:0:0:0, and shut down the runner-owned emulator on byte-identical prodDebug APK SHA-256231b6e1f587c0537b17d5498647772a49fe52daf98529c54a712e8de06271879. The current-source release XCFramework rebuilt for arm64 device and simulator with fingerprintade881e32ee60765b0f52914234a201e4e17fb95, and the iOS app compiled and linked for the existing arm64 simulator destination. Named-device, iOS runtime, accessibility-service, store, deployed, and production proof remain open.
2026-08-12 — Mobile project directory finds work by recognizable context
Type: Shared Android/iOS directory UX
- Project list required managers to scan every loaded row even though company and contact directories already support calm local search.
- The project directory now searches the already loaded tenant snapshot by project name, owner, or team with Vietnamese tone-mark folding; it preserves the query through refresh failure and provides an authored no-match state with one explicit clear action.
- Search does not add a backend request, saved view, new filter system, or second primary action. Shared model/filter tests, source policy, and the guarded Android Projects lifecycle own proof.
- Guarded Android flow 151 passed on byte-identical built/installed prodDebug APK SHA-256
c3fc77e80be2badf51fbb6534c033b773b714b3d408b21c4b7e98045097f7334: unaccentedBinh DuongfoundNhà máy Bình Dương, no-match clearing recovered both rows, the existing bounded list/detail outages recovered through explicit Retry, custody held, cleanup returned0:0:0, and the runner-owned emulator shut down. Named-device, iOS search/outage, screen-reader-service, store, deployed, and production proof remain open.
2026-08-10 — Project Detail refresh recovery stays compact and keeps context
Type: Shared Android/iOS recovery UX and Android proof repair
- Project Detail already kept
Error(last = project)visible after refresh failure, but its warning still used the spacious default banner and the registered Android Projects journey never failed the exact detail request. - The warning now uses one compact localized line with explicit Retry. Repeated refresh failure retains the same project and successful retry removes only the warning.
- Guarded Android flow 151 now continues through the existing Project Board → Project Detail route,
injects two failures on both the list and exact detail paths, preserves project facts and actions,
forwards both user retries, verifies exact project custody and cleanup, and retains WRK-04/WRK-05
evidence for APK SHA-256
81536d08932e48af9fdccec245d4d4fb55ade1f9b26d2ba7c0752bc9e3030d8b. - This is managed-emulator mechanics evidence. iOS outage, named-device, accessibility-service, store-binary, deployed, provider, and production proof remain open.
2026-08-10 — Project Detail actions no longer cover project context
Type: Shared Android/iOS layout correctness fix
- Project Detail drew its bottom action rail over a full-height scroll viewport. A fixed bottom padding let the final content scroll clear, but could not stop intermediate project sections from settling underneath Delete, Edit, and Open project board.
- The same permission-gated action rail is now measured below a weighted scroll viewport. Open project board remains the single bottom-anchored primary action, Edit/Delete remain secondary, and no route or mutation behavior changes.
- Source policy and guarded Android flow 155 require the field-day digest and all three actions in
the retained frame. The managed Android run passed on identical built/installed prodDebug APK
SHA-256
db756d93a31941cb2b2a01d08af669b48139d8ee35655e6f5a9a91660f115287; visual inspection confirmed the complete digest card ends above the measured rail, both controlled retries recovered, fixture cleanup returned0:0:0:0:0, and the sole emulator shut down. Named Android/iPhone, screen-reader-service, store, deployed, and production evidence remain separate obligations.
2026-08-10 — Mobile project-detail secondary actions preserve the work viewport
Type: Shared Android/iOS UX density bug fix
- Project Detail previously rendered wide persistent text pills for owner transfer, member add, and conversation creation. In Vietnamese those secondary controls consumed most of each section header and pushed the manager’s field-day digest farther down the phone viewport.
- The same permission-gated callbacks now use the shared 48 dp icon action with localized spoken labels and the existing stable automation identities. The live project board remains the sole full-width primary action.
- Team Detail keeps its existing visible conversation label; route, permission, dialog, mutation, stale-state, and localization behavior are unchanged.
- Source policy and guarded Android flow 155 passed on identical built/installed prodDebug APK
SHA-256
f5c30e6ef7d5d3f04c2f788f81451eca903b71ecad0e4232a8b47e0101b61695; the inspected Vietnamese frame retains all three compact controls, project context, and the start of the field digest. Named Android/iPhone, screen-reader-service, store, deployed, and production evidence remain separate obligations.
2026-08-10 — Mobile task-detail secondary composers disclose on demand
Type: Shared Android/iOS UX density bug fix
- Task Detail previously mounted the full subtask and comment inputs for every read, so secondary creation controls displaced Dependencies, Files, Comments, and the primary Edit action on a phone.
- Subtasks and Comments now keep one accessible Add action in their section header and mount the existing composer only after an explicit tap. Cancel closes presentation without discarding the model-owned draft; failed submissions remain open with the exact draft, and successful submissions clear and collapse the composer.
- The same runtime pass showed the separately floating Delete action covering an open composer. Delete now shares the bottom action rail with Edit, keeping the destructive action accessible without painting over scroll content.
- Routes, mutation payloads, permissions, list rendering, and existing localized copy are unchanged. Source policy covers the disclosure seams; Android runtime, named-device, accessibility-service, store, deployed, and production evidence remain separate obligations.
2026-08-09 — Mobile task triage reveals sort choices on demand
Type: Shared Android/iOS UX density bug fix
- The task directory previously stacked separate 48 dp rows for view, sort, and status controls, pushing the first task partly above the usable viewport on a Pixel 7-sized Android runtime.
All/Focusand one accessible sort action now share a compact row; tapping sort reveals the same three deterministic local choices without changing network, persistence, or saved-view behavior.- The task list returns to its first item when view, sort, or status changes, preventing a newly reordered first task from inheriting the prior list offset and appearing clipped.
- Source policy and the exact-artifact Android lifecycle cover the compact disclosure path. Named Android/iPhone and screen-reader-service evidence remain separate proof obligations.
2026-08-10 — Mobile task refresh recovery preserves the working directory
Type: Recovery UX bug fix (shared Android/iOS)
- The model retained a populated refresh snapshot in
LoadState.Error.last, but the Tasks screen replaced it with a blocking error; Retry and task-action failures could then discard it by reading onlySuccessstate. - Populated refresh failures now keep the last committed tasks and local triage visible behind one concise warning. Explicit Retry re-enters refresh with the same snapshot, and task mutations reconcile against current-or-stale data. Initial empty failures remain blocking.
- Focused shared tests and exact-artifact guarded Android flow 157 cover repeated outage, explicit recovery, task custody, evidence retention, cleanup, and runtime shutdown. iOS, named-device, screen-reader-service, store, deployed, and production evidence remain separate obligations.
2026-08-09 — Native project recovery preserves directory context and product copy
Type: Recovery UX bug fix (shared Android/iOS)
- Project list refresh failures now retain and render the last committed directory instead of replacing it with a blocking error screen.
- Retry carries the same stale snapshot through repeated failure, while a true first-load failure keeps the existing blocking recovery state.
- Project list, board, and detail recovery banners now use localized product copy without appending raw transport messages. A managed Android run retained both populated rows across two injected 503 responses (the request plus Lumi’s one automatic gateway retry), exposed a compact one-line Retry, forwarded the user’s retry, and recovered without restarting. It also replaced raw ISO due dates with compact numeric metadata appropriate to the Vietnamese locale. Named-device, signed-iOS, accessibility, store, and deployed proof remain open.
2026-08-09 — Android task-detail assignee recovery uses localized exact-member state
Type: Production-surface UX and runtime-proof fix (shared Android/iOS)
- Replaced raw nested-assignee transport errors with localized product copy while retaining the exact selected member for Retry and leaving committed assignees visible.
- Added stable member-bound automation semantics and a guarded Android lifecycle that induces a real duplicate rejection, removes the competing row, retries successfully, verifies one final primary assignee in SQLite, retains exact-artifact screenshots, and owns runtime shutdown.
- Built/installed prodDebug APK SHA-256 was
4b8120d55182c15b12da3acd60220ccaaf44d1c935aca37fa910cf50c36f3067. Named-device, signed-iOS, accessibility-service, store, and deployed proof remain open.
2026-08-05 — Project board cards open task detail
Type: Mobile navigation repair (shared Android/iOS)
- Board task cards now open the existing Task Detail route in one tap on both native shells.
- The existing Move action remains a separate, accessible target and keeps the board in place.
- Added source and iOS/Android Maestro journey coverage; named-device and store evidence remain separate from the managed runtime proof.
2026-08-06 — Mobile project boards focus one column at a time
Type: Mobile UX density refinement (shared Android/iOS)
- The populated board previously rendered every status as a fixed 280dp horizontal column. On a phone, the first column was readable but the next header was clipped, so the surface looked like an unfinished desktop canvas and required a horizontal swipe before the user could understand the workflow.
- Shared mobile now presents a localized, count-bearing column picker and a full-width selected column only in compact-width layouts; larger mobile/tablet widths retain the multi-column board. The picker exposes its single-selected state to assistive technology. The existing Move action still receives every board destination, and a refresh that removes the selected status falls back to the first available column without losing the project or task route.
- Added shared JVM selection/fallback coverage and a source-contract proof. The existing guarded iOS Project Board lifecycle remains the runtime vehicle; named Android, signed named-iPhone, accessibility-service, and store-binary evidence remain explicit gaps.
2026-08-06 — Project detail leads with live work
Type: Mobile action hierarchy refinement (shared Android/iOS)
- Project detail previously put the live board route in the scrolling body while the pinned bottom action promoted Edit. That made the next work decision less obvious and duplicated the board affordance across two visual zones.
- When the shell provides the existing project-board route, the shared screen now makes Open project board the single full-width primary action in the thumb zone. Edit and Delete remain compact secondary actions; projects without a board callback retain Edit as their primary action.
- Added a source-contract proof for the action hierarchy. This is source/local-test evidence; named Android, signed named-iPhone, accessibility-service, and store-binary evidence remain separate gaps.
2026-08-06 — Task-detail file empties stay capability-honest
Type: UX correctness fix (shared Android/iOS)
- The Files section previously appended the upload hint whenever a task had no files, even when the current host did not provide a picker or the member lacked relationship-management permission. That copy described a path the user could not take.
- Shared task detail now derives
canAttachTaskFilefrom both the mounted picker and the server-owned capability. The attach action and upload hint are shown only in that branch; every other empty state stays a neutral localized “no files” row. - Added the
F49.AC004.FILES_EMPTY_CAPABILITYsource proof. Populated file rendering and the existing manager-authorized upload path are unchanged; named-device and accessibility proof remain open under the existing F49 task-detail gap.
2026-08-05 — Mobile task detail empty states are compact
Type: UX improvement (shared Android/iOS)
- Replaced the four empty relation cards with flat support rows so task detail preserves hierarchy without pushing the edit and comment workbench below the fold.
- Kept existing copy, icons, permissions, add/upload controls, and accessibility behavior intact.
- Added a source-contract proof under
F49.AC004.EMPTY_STATESand a fixture-owned iOS simulator flow 147 (build 185, artifacta9c7bde3…c19386eb) that reached every empty relation section; named Android and signed named-iPhone proof remains open under the existing F49 task-detail gap.
2026-08-04 — Native task status badges respect the active locale
Type: Localization bug fix (shared Android/iOS)
- Task list, task-detail, and subtask badges now resolve canonical statuses through the shared
tasks_status_*resource keys instead of hardcoded English labels. - Unknown future status values retain the existing humanized fallback, so the defensive behavior
remains stable while known
en/vilabels follow the selected locale. - Added shared JVM coverage for the resource mapping and registered it under
F49.AC004.
2026-08-04 — Shared mobile task detail: add assignee with recoverable state
Type: UX feature (shared Android/iOS)
- Added a capability-gated member picker to the shared task-detail assignee section, matching the existing web relationship workflow while keeping the mobile surface compact.
- Active human organization members are the only candidates. The model retains the exact failed member selection for Retry and never inserts an assignee optimistically; the task is refreshed from the authored server response after a successful add.
- Added shared JVM and source-contract coverage. Named Android and signed-iOS rendered proof remain open under the existing F49 task-detail gap until their lifecycle runners execute.
2026-07-20 — iOS Create Task removes the duplicated keyboard-height gap
Type: Bug Fix (shared mobile form geometry)
- TestFlight build 150 rendered Create Task inside SwiftUI’s keyboard-reduced viewport and then applied the shared Compose IME inset again, leaving a large blank band.
- Create/Edit Task now inherit the platform-aware shared inset contract: Android keeps root IME padding, while iOS relies on the already-reduced KMP host viewport.
- Source policy and critical iOS flow 104 protect the platform boundary and the actual keyboard-open Create Task geometry. Named-device proof remains open.
2026-07-19 — Mobile project board: turn the empty state into a workbench
Type: UX feature (shared Android/iOS)
- Replaced the plain board title and empty text with a compact project hero, work snapshot, layered empty state, and localized create-task affordances.
- Routed board creation through the existing shared task form with the current project ID, preserving project context on Android and iOS.
- Added stable
projectboard_*seams for source policy and Maestro coverage; named-device visual evidence remains an explicit contract gap.
2026-07-18 — Mobile task forms: accountable assignee selection and removal
Type: Feature + data-integrity fix (shared Android/iOS + backend)
- Added an optional active-human-member assignee picker to shared KMP task create/edit forms.
- Replaced the stale mobile
assigned_to_idpayload with the backend-ownedownerfield and made edit-formowner: nullexplicit so “Unassigned” is an actual removal, not an omitted patch field. - Mirrored
Task.owneras a primaryTaskAssigneeinside the locked task API transaction; owner changes and clears remove only the prior owner’s mirrored row and preserve unrelated collaborators. - Added shared screen-model/serialization regression coverage, backend synchronization proofs, and critical Maestro flow 94. Emulator execution remains distinct from named-device proof.
2026-07-14 — Mobile: make Edit Project keyboard-safe (FRM-09)
Type: UX fix (shared KMP form layout)
Summary:
- Replaced the shared
EditProjectScreensuccess-stateBoxoverlay pattern with the provenCreateProjectScreenlayout: a weighted, scrollable form column plus an in-flow Save CTA. - Kept
imePadding()on the root and added theedit_project_savetest tag so the named-device keyboard audit can verify that Save remains above the IME without covering the start/due-date fields. - Added
EditProjectKeyboardSourceTestto prevent the bottom-aligned overlay from returning silently.
Scope boundary:
- This is source-level hardening only. PM-owned Android/iOS named-device screenshots, keyboard interaction, and FRM-09 scoring remain open until the mobile audit run.
Tests:
git diff --checkPYENV_VERSION=system python3 scripts/check_docs_truth.pybash scripts/check-platform-parity.sh
2026-07-12 — Task detail sheets escape transformed app-shell containers (feedback #50)
Type: Bug Fix (frontend overlay geometry)
Summary:
- The task detail overlay and its loading state now render through
document.body, so an ancestor transform or overflow context cannot redefine the fixed panel’s containing block. - Both the loaded panel and skeleton use
h-full, matching the viewport-sized overlay contract already used by the deal detail sheet.
Tests:
frontend/src/test/work/TaskDetailPanel.test.tsxasserts the loaded and loading dialogs are body-portal children and full-height where applicable.
2026-07-02 — Docs: correct stale benchmark scorecard after Phase A–E completion
Type: Documentation Fix
Summary:
- The §11 benchmark parity scorecard retained 2/5 scores for “Board interaction” and “Signal trustworthiness” even though Phase A (route contract + status validation) and Phase B (DnD + load-more + move+rank) were both marked ✅ in the same §11 section — a direct internal contradiction.
- Updated all six dimensions whose scores lagged behind completed phases:
- Board interaction: 2 → 4 (Phase B shipped)
- Signal trustworthiness: 2 → 4 (Phase A shipped)
- Workflow configurability: 3 → 4 (Phase A + Phase C shipped)
- Workflow admin ergonomics: 3 → 4 (Phase C shipped)
- Comments / collaboration: 3 → 4 (Phase D shipped)
- Multi-view consistency: 3 → 4 (Phase E + inline hierarchy shipped)
- Also refreshed gap text for “Hierarchy / decomposition” (inline hierarchy and parent-delete rule are now ✅; gap text updated to reflect remaining out-of-scope items) and “Task detail depth” (attach flow + mention semantics are ✅; gap trimmed to richer evidence actions and mobile file-attach).
- Updated the scorecard summary paragraph to reflect the current state: Phases A–E complete; signal trustworthiness is the one dimension still below target (4/5 vs 5/5) due to bounded board-layer policy.
Reason:
- Spec-internal contradiction: completed phases were marked ✅ while the scorecard still showed the pre-completion scores. A false low score in a spec that the team trusts for honest self-assessment is itself a trust defect.
Impact:
- Documentation only. No code, API, test, or registry-status change.
Tests:
- None (documentation fix).
2026-07-02 — Docs: align spec to 2026-06-21 comment consolidation; fix broken E2E route
Type: Documentation Fix + Bug Fix (E2E test)
Summary:
- The spec still documented the deleted
TaskComment(UUIDModel)model (removed in migration0013_delete_taskcomment.pyon 2026-06-21) as if it were the shipped design, and described the old nested routes/api/tasks/<id>/comments/and/api/task-comments/as “resolved end-to-end”. Both routes return 404 since the consolidation. - The authoritative design is the universal
Commentmodel inapps/comments/with polymorphictarget_type="task". Task comments are served atGET/POST /api/comments/withtarget_typeandtarget_idquery/body params, plusPATCH/DELETE/resolveon/api/comments/<id>/. - Updated §4.1: replaced the
TaskCommentclass definition with a note explaining the deletion and the consolidation intoapps/comments/models.py. - Updated §2.4: corrected
_notify_comment_mentions in TaskViewSet→notify_comment()inapps/comments/services.py; updated the “out of scope” claim to reflect that edits, soft-delete, resolve, and threading (viaparentFK) ARE supported by the Comment model. - Updated §5.1: replaced old comment route rows with the actual
GET/POST /api/comments/,PATCH/DELETE /api/comments/<id>/,POST /api/comments/<id>/resolve/rows. - Updated §5.2: replaced the stale “nested paths shipped end-to-end including /tasks/
/comments/” with accurate documentation of the consolidated flat endpoint. - Updated §5.4: removed the incorrect claim that
TaskSerializerincludes acommentsfield (it was never added after consolidation); added note that comments are fetched separately via the Comment API. - Updated §7 row 6:
TestTaskCommentsintest_work_graph.py→TestCommentinbackend/tests/test_comments.py; row 8 dropped theTestTaskCommentsreference. - Updated §11 Phase D step 12: annotated with 2026-06-21 note explaining
TestTaskCommentMentionswas removed when the comment system was consolidated; equivalent tests are nowTestNotificationsinbackend/tests/test_comments.py. - Fixed
frontend/tests/e2e/f49-work-graph.spec.ts: the first test (nested task comments route accepts POST) was POSTing to the deleted routehttp://127.0.0.1:8000/api/tasks/${taskId}/comments/. Changed to POSThttp://127.0.0.1:8000/api/comments/with body{ target_type: 'task', target_id: taskId, body: '...' }, matching theCommentViewSet.create()contract exactly.
Impact:
- Spec: all comment-related claims now match the shipped
apps/comments/implementation. - E2E: the broken test now calls the live endpoint and will receive 201 instead of 404.
Tests:
cd frontend && ./node_modules/.bin/tsc --noEmit→ 0 errors (verified).- E2E route fix confirmed by reading
apps/comments/views.py CommentViewSet.create().
2026-06-25 — UX: pull-to-refresh on TaskDetailScreen and ProjectBoardScreen
Type: UX improvement (mobile / shared KMP)
Summary:
TaskDetailScreenandProjectBoardScreenwere missing pull-to-refresh, violatingmobile-ux-patterns.md §2which mandates PTR on every screen that fetches its own record.- Added
isRefreshing: Boolean+refresh()toTaskDetailScreenModelandProjectBoardScreenModel; wrapped scrollable content inLumiRefreshable. TaskDetailScreen: task and comments refresh independently (stale-while-revalidate per §6.3 — partial failure leaves the other section intact).ProjectBoardScreen: outerColumnwrapped inLumiRefreshable;MoveDialogoverlay stays outside so it renders on top.- Test counts updated:
TaskDetailScreenModelTest8 → 12 cases (success, task-failure, comments-failure, guard);ProjectBoardScreenModelTest5 → 7 cases (success, failure).
Reason:
- Enforcing PTR parity across all data-backed detail screens; closes the gap left when the screens were first shipped.
2026-06-17 — Feature: subtask due-date inheritance default
Type: Feature
Summary:
- Closed the remaining F49 due-date defaulting gap for subtasks.
TaskViewSet.perform_createnow applies one server-side rule for every client: if a subtask is created without its owndue_date, it inherits the parent task’sdue_date; if the child provides adue_date, that explicit value wins; if the parent is undated, the child stays undated.- Added three focused backend tests in
TestSubtaskDepthLimitcovering inherit / explicit override / undated parent. - Updated §2.7 so the acceptance criterion matches shipped behavior.
Reason:
- F49 still had an explicit unimplemented acceptance criterion for subtask due-date defaulting, which left clients free to drift into inconsistent behavior.
Impact:
- Backend task creation is now deterministic across web, mobile, and API callers.
- No frontend change was required because the default is enforced at the API boundary.
Tests:
cd backend && ./.venv/bin/python -m pytest tests/test_work_graph.py -k "subtask and due_date" -q
2026-06-18 — Feature: assignee-role notification semantics
Type: Feature + Documentation Change
Summary:
- Shipped the remaining F49 assignee notification contract for task assignment on both the nested task route (
/api/tasks/<id>/assignees/) and the flat assignee resource (/api/task-assignees/). - Added
_notify_task_assignment()inbackend/apps/work/views.pyto create in-appNotificationEventrows for actionable assignee roles. - Locked the role semantics to the current product contract:
primaryandsecondarycreatecategory="work"notification events,observeris silent, and self-assignment is silent. - Added focused backend coverage in
TestTaskAssigneeNotifications. - Updated §2.2 and §4.4 so the spec no longer claims this behavior is missing.
Reason:
- F49 still had an explicit unfinished acceptance criterion for assignee-role notification semantics, which left the work graph under-specified at the moment ownership changed.
Impact:
- The task assignment surfaces now distinguish between accountable/supporting assignment and visibility-only assignment without widening scope into new push preferences.
- Work assignment notifications are currently in-app only; there is still no F36 push preference/category for work assignments.
Tests:
cd backend && ./.venv/bin/python -m pytest tests/test_work_graph.py -q
2026-06-18 — Feature: grouped workflow columns via ProjectStatus.column_key
Type: Feature + Documentation Change
Summary:
- Added persisted
ProjectStatus.column_keyso several project statuses may share one visible board column. - Updated
GET /api/projects/<id>/board/to group custom statuses bycolumn_keyand return grouped metadata viastatus.column_keyandstatus.mapped_statuses. - Extended
ProjectWorkflowPageso managers can create and edit the column key from the workflow settings surface. - Added focused backend coverage for grouped board columns and focused frontend coverage for the board hint and workflow payload.
Reason:
- F49 still had one explicit unfinished workflow acceptance criterion: several statuses mapping into one visible board column.
Impact:
- Backend board payloads now express truthful grouped workflow columns without inventing a second board-column model.
- Frontend workflow settings expose the persisted grouping rule, and grouped columns explain which statuses they contain.
- The remaining F49 gap is any future board-layer policy beyond
column_key; dependency edges are now shipped as a boundedTaskDependencyslice, not a full sequencing engine.
Tests:
cd backend && ./.venv/bin/python -m pytest tests/test_work_graph.py -qcd frontend && pnpm exec vitest run src/test/ProjectBoardPage.test.tsx src/test/ProjectWorkflowPage.test.tsx
2026-06-16 — Spec brought back into truth with shipped code + comment mention push shipped
Type: Feature + Documentation Change
Summary:
- Shipped task comment
@mentionpush notifications via_notify_comment_mentions()inTaskViewSet(reuses F29parse_mentions+ F36 push channel). Notifications are best-effort: a failure cannot break comment creation. - Fixed a stray indentation error in
backend/apps/work/views.pythat had left the file unparseable. - Reverted an unnecessary
user_cfixture rename (“UserC” → “User C”) becausenormalize_handle()already resolves both forms to the same token. - Added
TestTaskCommentMentionsandTestNestedRoutePermissionsAndErrorstobackend/tests/test_work_graph.py. - Updated F49 spec to reflect shipped truth:
- §5.1 now lists the nested project-status / task-assignee / task-comment / task-file routes.
- §5.2 replaces the “route mismatch” contradiction with a “resolved” note.
- §10 diagram note now says the routes are exposed and diagrams should stay in sync.
- §11 Implementation Audit removes the route-mismatch, board-overclaim, workflow-enforcement, status-admin, my-tasks, file-attach, and pagination gaps (all now shipped) and renumbers remaining gaps.
- Sequenced roadmap updated to show Phases A–D as shipped with only status-to-column mapping, upload-policy completeness, dependency edges, and timeline view still open.
Reason:
- The F49 spec was lagging behind the actual implementation. Keeping spec and code aligned is required by the registry policy; a stale spec is a bug.
Impact:
- Backend:
_notify_comment_mentions()is now live;TaskViewSetsyntax is valid; comment and nested route tests are green. - Frontend: no changes in this turn, but the spec now accurately describes the nested routes and board DnD already shipped.
- Docs: F49 spec is now an honest map of what is shipped vs what remains.
Tests:
backend: DJANGO_SETTINGS_MODULE=config.settings.test .venv/bin/python -m pytest tests/test_work_graph.py tests/test_f68_drag_and_drop.py -q→ 114 passed.frontend: ./node_modules/.bin/tsc --noEmit→ 0 errors.frontend: pnpm exec playwright test tests/e2e/f49-work-graph.spec.ts --project=chromium --reporter=list→ 5 passed.
2026-06-14 — Strategy v2 realignment (positioning + vertical vocabulary)
Type: Documentation Change
Summary:
- §title: “Mini-Jira: Tasks, Projects, Workflows” → “the contractor’s job/site/task data graph”.
- §1: removed the “use external tools (Asana, Jira, ClickUp)” and “keeps teams inside Lumi instead of exporting their work to a dedicated PM tool” framing; repositioned F49 as the vertical data graph that is the moat — the structured operating memory (công trình/site, tasks, owners, dependencies, status) replacing the shared Excel + Zalo group; recast the example with fit-out job/site vocabulary (công trình, thầu phụ, khảo sát → thi công → nghiệm thu).
Reason:
- Realign to competitive-strategy-v2.md and the registry Positioning doctrine (strategy v2, 2026-06-14): do not position against US PM tools (the target contractor doesn’t own them); AI is table-stakes, the data graph is the moat; Vietnamese-first vertical vocabulary.
Impact:
- Positioning / vocabulary only. No model, field, API, test, or status change. Any behavior change is tracked in §11 Implementation Audit, not claimed done here.
Tests:
- None (documentation change).
Notes:
- All code-level names (
TaskAssignee/ProjectStatus/TaskComment/TaskFile, §4 fields, §5 API paths, §7 test classes) left untouched. The §3 “not a scrum tool” scope boundary is a feature exclusion (no sprint/iteration management), not a competitor-positioning line, and was preserved.
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §9 Edge Cases (9 items) absorbed into §11 Implementation Audit note.
- §12 Implementation Audit converted from narrative + Implementation Order to canonical 4-column table (10 rows, all ✅ including B-r03 depth limit fix).
- Sections renumbered: §10 ES → §9, §11 Diags → §10, §12 Audit → §11, §13 OQ → §12, §14 BL → §13.
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-06-13 — T3-A: added board/project-tasks/subtasks backend read endpoints
Type: Feature
Summary:
- Added
GET /api/projects/<id>/board/(kanban with per-column pagination usinglimit/offset, returns columns withstatus,tasks,has_more,total). - Added
GET /api/projects/<id>/tasks/(flat project task list applying existing TaskViewSet filters). - Added
GET /api/tasks/<id>/tasks/(direct subtasks byparent_task). - Updated F49 spec §12 Implementation Audit and Implementation Order with shipped/not-built status.
- Updated MVP_SCOPE.md F49 row: API column → ✅.
Reason:
- Three endpoints specified in F49 §5.1 API Contract were missing — needed for frontend board and project task views.
Impact:
- Backend only. Frontend can now build against these endpoints (see T3 report A-r06 for exact response shapes).
Tests:
- TestProjectBoard (8 tests): custom statuses, empty columns, per-column pagination, default limit, cross-tenant isolation, serializer fields, fallback to default statuses.
- TestProjectTasks (6 tests): list, empty, status filter, priority filter, cross-tenant isolation, scope, serializer.
- TestSubtasks (5 tests): direct subtasks, empty, depth-1 only, cross-tenant isolation, serializer.
Notes:
- The
boardendpoint handles two modes: projects with customProjectStatusrows (returns full status objects with color/order/terminal) and projects using defaultSTATUS_CHOICES(returns flat status strings). - The
tasksandsubtasksendpoints useTaskListSerializer(lighter thanTaskSerializer, no nested subtasks/comments/files). - All endpoints scope queries via
organization_id=self.request.organization_idfor tenant isolation.
2026-08-03 — Mobile task list Focus and sort triage
Type: UX feature
Summary:
- Added a shared Android/iOS
All/Focusview switch for local executive triage. Focus includes incomplete overdue, due-today/tomorrow, and high/critical-priority work. - Added deterministic local
Priority,Due date, andRecently addedsorting over the loaded task snapshot;All + Recently addedremains the default to preserve the existing server order. - Selecting Focus clears an active server-backed status filter and reloads the complete list. No new endpoint, required form, saved view, grouping, or cross-device persistence was added.
Proof boundary:
- Kotlin policy/model tests and a source policy cover behavior and semantic controls. The registered Maestro flow covers reachability only; named-device and deployed evidence remain separate proof obligations.
2026-06-13 — Spec hardening: numbered edge cases, detailed error matrix, status → agreed
Type: Documentation Change
Summary:
- Replaced §9 loose bullet points with 9 numbered edge cases (9.1–9.9) covering accountability owner for multi-assignee, workflow template cross-tenant copying, parent task closing with open subtasks, depth limit enforcement, custom status deletion with active tasks, task moving between projects, @mention to non-org-member, orphaned file cleanup, and board view with 500+ tasks.
- Replaced §10 two-sentence prose with 10-row detailed error matrix (user-visible/internal/log/retry).
- Updated spec status from draft to agreed.
Reason:
- Spec needed hardening per TEMPLATE.md. Edge cases were loose prose, error states lacked structured detail.
Impact:
- Spec authors and future implementation agents.
Tests:
- Spec-only change — no code tests affected.
2026-06-14 — T5-G: “My Tasks” + “Top-level only” filters + missing board_* i18n keys
Type: Feature
Summary:
- Added “Top-level only” Checkbox filter to
TasksPage.tsx— passesparent_task__isnull=trueto/api/tasks/. - Added “My Tasks” Checkbox filter to
TasksPage.tsx— passesmy_tasks=trueto/api/tasks/(backend param to be wired in TaskViewSet). - Added 4 missing i18n keys to
en/tasks.jsonandvi/tasks.json:filter_top_level,filter_my_tasks,board_add_assignee,board_search_assignee,board_no_members_found,board_subtask_placeholder. - Updated spec §2 acceptance criteria checkboxes: task detail subtask list, “Top-level only” filter, multi-assignee badges, “My Tasks” filter — all marked
[x]. - Added unit tests in
src/test/TasksPage.test.tsxverifying filter params.
Reason:
- F49 Implementation Audit in spec §12 had
- [ ]items for these frontend features despite code shipping in earlier rounds. Locale keys used in TaskDetailPanel were missing from both locale files.
Impact:
- Frontend only. Backend
my_tasksquery param not yet implemented — filter UI exists, backend support deferred.
2026-06-16 — Spec brought back into truth with shipped code + comment mention push shipped
Type: Feature + Documentation Change
Summary:
- Shipped task comment
@mentionpush notifications via_notify_comment_mentions()inTaskViewSet(reuses F29parse_mentions+ F36 push channel). Notifications are best-effort: a failure cannot break comment creation. - Fixed a stray indentation error in
backend/apps/work/views.pythat had left the file unparseable. - Reverted an unnecessary
user_cfixture rename (“UserC” → “User C”) becausenormalize_handle()already resolves both forms to the same token. - Added
TestTaskCommentMentionsandTestNestedRoutePermissionsAndErrorstobackend/tests/test_work_graph.py. - Updated F49 spec to reflect shipped truth:
- §5.1 now lists the nested project-status / task-assignee / task-comment / task-file routes.
- §5.2 replaces the “route mismatch” contradiction with a “resolved” note.
- §10 diagram note now says the routes are exposed and diagrams should stay in sync.
- §11 Implementation Audit removes the route-mismatch, board-overclaim, workflow-enforcement, status-admin, my-tasks, file-attach, and pagination gaps (all now shipped) and renumbers remaining gaps.
- Sequenced roadmap updated to show Phases A–D as shipped with only status-to-column mapping, upload-policy completeness, dependency edges, and timeline view still open.
Reason:
- The F49 spec was lagging behind the actual implementation. Keeping spec and code aligned is required by the registry policy; a stale spec is a bug.
Impact:
- Backend:
_notify_comment_mentions()is now live;TaskViewSetsyntax is valid; comment and nested route tests are green. - Frontend: no changes in this turn, but the spec now accurately describes the nested routes and board DnD already shipped.
- Docs: F49 spec is now an honest map of what is shipped vs what remains.
Tests:
backend: DJANGO_SETTINGS_MODULE=config.settings.test .venv/bin/python -m pytest tests/test_work_graph.py tests/test_f68_drag_and_drop.py -q→ 114 passed.frontend: ./node_modules/.bin/tsc --noEmit→ 0 errors.frontend: pnpm exec playwright test tests/e2e/f49-work-graph.spec.ts --project=chromium --reporter=list→ 5 passed.
2026-06-16 — Expanded benchmark-grade roadmap and parity scorecard
Type: Documentation Change
Summary:
- Added a sequenced rework roadmap to F49, ordered by dependency rather than by feature wish-list: contract truth → board credibility → workflow administration → evidence completeness → higher-order graph power.
- Added a benchmark parity scorecard covering hierarchy, ownership, workflow configurability, board interaction, files/evidence, dependency power, and signal trustworthiness.
- Made the hidden thesis explicit: F49 is not blocked by lack of feature count; it is blocked by a few false states that make the work graph untrustworthy.
Reason:
- The previous rewrite correctly separated shipped reality from aspiration, but it still lacked an execution-grade path from “promising foundation” to “serious work surface comparable to top tools.”
Impact:
- Documentation only. No code, API, test, or registry-status change.
- Future implementation agents now have a concrete order of operations instead of a flat backlog.
Tests:
- None (documentation change).
2026-06-15 — F49 spec rewritten to separate benchmark target from shipped reality
Type: Documentation Change
Summary:
- Rewrote the F49 spec from top to bottom.
- Grounded the spec in current repo reality: actual models, endpoints, tests, pages, and known gaps.
- Added benchmark-aware product framing using official Jira, Asana, and Trello documentation.
- Added an explicit benchmark capability matrix and a stricter definition of what “comparable to top tools” should mean for Lumi.
- Explicitly documented the frontend/backend route mismatch and the board DnD overclaim.
Reason:
- The previous spec mixed implemented behavior, intended behavior, and benchmark aspiration in one layer, which made it too easy to overclaim shipped capability.
Impact:
- Future agents now have a stricter source of truth for F49, especially around route contracts, workflow enforcement, benchmark-informed next steps, and honest shipped-vs-gap boundaries.
Tests:
- No new code tests in this spec-only rewrite.
- The rewrite cites existing backend tests in
backend/tests/test_work_graph.pyas authoritative evidence.
Notes:
- This rewrite is intentionally more contrarian than the previous version: it treats inaccurate shipped claims as defects even when the general feature direction is right.
2026-06-19 — Work project views visual QC pass
Type: Polish
Summary:
- Added screenshot-flow coverage for the three project-detail work surfaces: board, timeline, and workflow (
frontend/screenshots/27-work-project-views/, kept local only and not committed). - Fixed a trust-breaking language leak where built-in board columns still rendered English labels like
TO DOandWAITINGinside the Vietnamese UI when the API returned Englishstatus_displayvalues. - Fixed the timeline date formatter so project schedule buckets follow the active UI locale instead of the browser default.
Reason:
- The visual audit exposed a mismatch between the language selected in the shell and the language shown in the highest-signal project surfaces. For CEO-facing work views, mixed-language chrome undermines confidence faster than almost any spacing issue.
Impact:
frontend/src/pages/work/ProjectBoardPage.tsxfrontend/src/pages/work/ProjectTimelinePage.tsxfrontend/src/test/ProjectBoardPage.test.tsxfrontend/src/test/ProjectTimelinePage.test.tsxfrontend/tests/e2e/screenshot-flow.spec.ts
Tests:
frontend/src/test/ProjectBoardPage.test.tsxfrontend/src/test/ProjectTimelinePage.test.tsx- Playwright screenshot flow filtered to
27-work-project-views
2026-06-19 — Project tasks page visual QC + deep-link repair
Type: Polish + Bug Fix
Summary:
- Added screenshot-flow coverage for the project task-list page and its detail panel (
frontend/screenshots/28-work-project-tasks/, kept local only and not committed). - Upgraded the page header so the task-list surface now matches the calmer project detail rhythm used by board, timeline, and workflow.
- Formatted due dates in the active locale instead of dumping raw ISO values into the table.
- Formatted the shared task detail panel due date in the active locale as well, so the board/task-list detail surface no longer flips back to raw ISO inside the right drawer.
- Repaired the task-detail deep link contract by shipping
/work/projects/:id/tasks/:taskIdalongside the list route, so clicking a board card into the project task detail no longer navigates to a non-existent page.
Reason:
- The visual pass exposed one missing page in screenshot coverage and a real navigation break: the board already tried to open a task detail URL that the router did not serve.
Impact:
frontend/src/pages/work/ProjectTasksPage.tsxfrontend/src/components/work/TaskDetailPanel.tsxfrontend/src/App.tsxfrontend/src/routes/routeTree.tsfrontend/src/test/ProjectTasksPage.test.tsxfrontend/src/test/TaskDetailPanel.test.tsxfrontend/src/test/tanstack-router.test.tsfrontend/tests/e2e/screenshot-flow.spec.ts
Tests:
frontend/src/test/ProjectTasksPage.test.tsxfrontend/src/test/TaskDetailPanel.test.tsxfrontend/src/test/tanstack-router.test.ts- Playwright screenshot flow filtered to
28-work-project-tasks
2026-07-02 — Docs: correct stale page paths in §6.1
Type: Documentation Fix
Summary:
- §6.1 “Confirmed shipped surfaces” listed three pages under the old flat
pages/root. All three moved intopages/work/during the IA restructure. Fixed:frontend/src/pages/ProjectBoardPage.tsx→frontend/src/pages/work/ProjectBoardPage.tsxfrontend/src/pages/ProjectTasksPage.tsx→frontend/src/pages/work/ProjectTasksPage.tsxfrontend/src/pages/TasksPage.tsx→frontend/src/pages/work/TasksPage.tsx
- No code changes; later §13 entries and §11 impact lists already used the correct paths.
12. Contract Graph reconciliation (2026-07-15)
The canonical Contract Graph slice follows executable repository evidence rather than the
historical benchmark notes above. Backend work-graph tests are split across
backend/tests/work/test_work_graph_resources.py, test_work_graph_routes.py,
test_work_graph_depth.py, and test_work_graph_security.py; there is no single
backend/tests/test_work_graph.py file in the current checkout.
The current web implementation and focused tests establish that project-specific status
validation, grouped board columns, depth-four rejection, nested task-file routes, and a
cross-tenant project-status boundary are shipped. ProjectBoardPage also contains the
drag-and-drop and per-column load-more paths, and ProjectTimelinePage is a shipped due-date
schedule view. These source and repository-test facts do not establish browser execution,
native/device behavior, live Redis/Sentry/provider behavior, or production availability.
Those remain explicit Contract Graph gaps. The bounded TaskDependency relation is not a
critical-path or automatic-rescheduling engine, and saved views, WIP policy, portfolio rollups,
and automation rules remain outside this contract.