in progress (core shipped; documented rework outstanding — see §11)

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. ProjectTasksPage now 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_task is cleared (SET_NULL), so no work item is silently deleted with its parent.

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. TaskViewSet now filters owner_id OR assignee_through__member_id (verified test_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 owner is set or cleared and the form remains editable if member loading fails.
    • Given Task.owner changes or is cleared, then that accountable owner is mirrored as a TaskAssignee(role="primary"); the prior owner’s mirrored row is removed and unrelated collaborators are preserved.

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 ProjectStatus rows, then they are stored per project and ordered by order.
    • Given project statuses, when I list them, then they return in order.
    • Given a status, when I patch /reorder/, then its order updates.
    • 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 status is 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 project field, including an explicit null when clearing it.
    • Given the shared Android/iOS task edit form, when I choose a bounded “blocked by” task, then a TaskDependency edge 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_key lets several statuses share one visible board column; the board returns the first status as the primary status plus mapped_statuses metadata for the grouped values.

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() in apps/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 a parent FK 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 create TaskFile, 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 completed uploads 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.

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 as done.
    • 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 todo or in_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_more metadata.
    • 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.

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_date is provided, then the task is valid.
    • Given list or board surfaces, when due_date is 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_create now 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/timeline surface 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.

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
  • 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

CapabilityTrello patternJira patternAsana patternLumi current stateProduct decision
Hierarchychecklist items inside cards; items can become cardssubtasks under issues, plus higher-order issue typessubtasks under tasksparent/child tasks with 3-level capkeep subtasks as primary decomposition unit for now
Board interactiondrag cards between listsdrag issues between columnsdrag tasks between board columnsstatus select on board cardsupgrade to DnD later if the board becomes a primary daily surface
Workflow modellist-based board stagesconfigurable statuses mapped to columnssections / board columns / custom fieldsper-project ProjectStatus plus optional shared column_key groupingacceptable bounded version; still not true Jira-level flexibility because grouping resolves to one primary status and there is no separate board-layer object
Multi-assigneecard membersone assignee plus watchers/reporters or related rolesmultiple collaborators / assignees across work contextexplicit TaskAssignee with roleskeep and deepen; this is strategically correct
Commentscard activity/commentsissue comments/activitytask conversationslinear task commentskeep simple until real need for threads emerges
Filescard attachmentsissue attachmentstask attachmentstask-file rows and manager attach flow on shared mobile detaildeeper evidence types remain out of scope
Custom fieldsboard-level custom fieldscustom fields on issues, richer admin controllocal/global custom fields, even reference fieldsnone in F49likely next after dependencies or alongside them
Dependenciesweak / indirectfirst-class blocked-by / blocks on timelineslighter relationship patterns plus timelinesbounded TaskDependency edges in task detailshipped first slice; deeper sequencing stays out
Multiple viewsboard-firstbacklog, board, timeline, filterslist, board, timeline, calendar, portfoliolist + board; mobile Focus and sortsaved cross-device views, grouping, and portfolio stay out of scope for now
AutomationButler rulesworkflow automationsrules and bundlesnonekeep out until core graph truth is reliable
Saved filtering/groupinglimited board filtersstrong filters, backlog/query workflowsstrong filter/sort/group saved layoutsbasic filters plus native Focus/sortkeep 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:

  1. No false state
    • A task’s workflow, ownership, and completion state must be trustworthy enough to drive CEO reporting.
  2. Fast navigation
    • List, board, and detail should let a manager move from “what is stuck?” to the exact work item in one hop.
  3. Durable context
    • Comments, files, and assignees must survive beyond chat memory.
  4. 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.
  5. 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_NULL
  • Task.status: CharField(max_length=20) with global choices still on the model
  • Task.owner: legacy single-owner field, still heavily used
  • Task.assignees_m2m: M2M through TaskAssignee
  • Task.position: float used for list reorder, not board-column rank
  • Task.due_date: nullable
  • Task.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.organization and Project.organization
  • TaskAssignee uniqueness is (task, member)
  • TaskDependency uniqueness is (blocked_task, blocked_by, relation_type)
  • ProjectStatus uniqueness 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: TaskDependency now models directional blocked_by edges; 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 distinct BoardColumn model, 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:

  1. Dependency edges (predecessor_task, successor_task, typed relation)
  2. Board-layer object distinct from ProjectStatus only if column_key proves too weak for WIP limits, backlog separation, or per-column transition policy
  3. Project-local custom fields first, org-global later, Asana-style
  4. Checklist item object only if subtasks prove too heavy for the smallest units of work, Trello-style
  5. Reference links to related tasks/projects if Lumi needs Asana-style lightweight relationships without formal dependencies

5. API Contract

5.1 Confirmed shipped endpoints

MethodPathAuth/RoleRequestResponseErrors
CRUD/api/project-statuses/read: org member, write: org manager+standard DRF bodypaginated list / object400 invalid cross-org project, 403 role restriction
PATCH/api/project-statuses/<id>/reorder/org manager+{ "order": number }updated status400 if order missing
CRUD/api/task-assignees/read: org member, write: org manager+task, member, rolepaginated list / object400 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: "..." } — create
  • PATCH /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() filters Project.objects.filter(organization_id=request.organization_id)
  • TaskViewSet.get_queryset() filters Task.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

  • TaskSerializer includes:
    • assignees as assignee_list
    • files
    • dependency edges as blocked_by_dependencies and blocking_dependencies
    • subtasks through TaskListSerializer
    • Note: comments is NOT a TaskSerializer field. Task comments are fetched separately via GET /api/comments/?target_type=task&target_id=<uuid> (universal Comment API). The frontend’s TaskDetailPanel makes this as a separate query.
  • Project board returns per-column metadata:
    • status
    • tasks
    • has_more
    • total
  • If custom statuses exist, status is a serialized ProjectStatus; grouped columns include column_key and mapped_statuses
  • If not, status is a string plus status_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_key cannot 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.tsx
  • frontend/src/pages/work/ProjectTasksPage.tsx
  • frontend/src/pages/work/TasksPage.tsx
  • frontend/src/components/work/TaskDetailPanel.tsx
  • frontend/src/hooks/queries/useWorkGraph.ts
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/CreateTaskScreen.kt
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/EditTaskScreen.kt
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/projectboard/ProjectBoardScreen.kt
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/presentation/projectboard/ProjectBoardScreenModel.kt
  • android/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.kt
  • android/shared/src/commonMain/kotlin/com/locuno/lumi/shared/ui/tasks/EditTaskScreen.kt
  • android/app/src/main/kotlin/com/locuno/lumi/android/ui/TaskDetailAttachmentPicker.kt
  • ios/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 / Focus view switch and one accessible sort action in the same compact row. The sort action discloses deterministic Priority / Due date / Recently added choices only when requested instead of permanently consuming a second 48 dp control row. All + Recently added is the default so the existing -created_at ordering remains stable; selecting Focus clears 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 more action only when has_more is 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:

  • tasks
  • projects

Confirmed current keys include board/detail keys such as:

  • board_detail_title
  • board_status
  • board_priority
  • board_owner
  • board_due
  • board_assignees
  • board_subtasks
  • board_comments
  • board_files
  • board_add_assignee
  • board_search_assignee
  • board_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

#CaseTypeWhere
1Create custom project statusbackendbackend/tests/test_work_graph.py::TestProjectStatus
2List project statuses in orderbackendbackend/tests/test_work_graph.py::TestProjectStatus
3Reject duplicate status name within one projectbackendbackend/tests/test_work_graph.py::TestProjectStatus
4Create multi-assignee rows with rolesbackendbackend/tests/test_work_graph.py::TestMultiAssignee
5List/delete task assigneesbackendbackend/tests/test_work_graph.py::TestMultiAssignee
6Create/list/delete/edit/resolve task comments (via universal Comment API)backendbackend/tests/test_comments.py::TestComment
7Create/list/delete task filesbackendbackend/tests/test_work_graph.py::TestTaskFiles
8Task detail serializes subtasks/files/assignees; comments fetched via separate Comment APIbackendbackend/tests/test_work_graph.py::TestHierarchicalTasks, TestTaskFiles
9Project board groups tasks by custom and default statusesbackendbackend/tests/test_work_graph.py::TestProjectBoard
10Project board paginates per columnbackendbackend/tests/test_work_graph.py::TestProjectBoard
11Project tasks endpoint respects filtersbackendbackend/tests/test_work_graph.py::TestProjectTasks
12Direct subtasks endpoint returns only direct childrenbackendbackend/tests/test_work_graph.py::TestSubtasks
13My Tasks filter behaviorbackendbackend/tests/test_work_graph.py::TestMyTasksFilter
14Subtask depth limit enforcedbackendbackend/tests/test_work_graph.py::TestSubtaskDepthLimit
15Subtask depth query count / tenant safetybackendbackend/tests/test_work_graph.py::TestSubtaskDepthCTE, TestSubtaskDepthTenantSafety, TestSubtaskDepthQueryCount
16Viewer cannot write statuses / assignees / comments / filesbackendbackend/tests/test_work_graph.py::TestViewerPermissions
17Board page opens and task detail panel renderse2efrontend/tests/e2e/lumi.spec.ts F49 block
18Status change from board card workse2efrontend/tests/e2e/lumi.spec.ts F49 block
19Comment creation from detail panel works against real route contracte2erequired re-check because frontend path contract appears mismatched
20Assignee add/remove from detail panel works against real route contracte2erequired re-check because frontend path contract appears mismatched
21File list and file attach flowe2eweb attach flow exists; browser execution remains a separate proof obligation
22Board drag-and-drop across statusese2enot shipped; add only when implemented
23Mobile task Focus membership, deterministic sort order, compact view/sort disclosure, status/view interaction, stale refresh/action preservation, explicit recovery, and semantic controlsKotlin JVM + source policy + MaestroTaskListPolicyTest, 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.owner as a primary TaskAssignee without 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_by edges
  • 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

#ScenarioUser-visibleInternal handlingLogging / retry
1Task nested deeper than 3 levelsvalidation error on subtask create_subtask_depth() rejects createbackend test exists; user must create elsewhere
2Cross-tenant task/project/status access404 or 400 depending on routequeryset scoping / explicit object lookupsecurity-critical; no retry hint
3Viewer attempts write403role permission check in viewsetexpected, no retry unless role changes
4Invalid board column page params400 or server-side failure if malformedcurrent code casts int() directlyneeds hardened UX copy if exposed
5Frontend calls nested route that backend does not exposetask panel subsection fails or entire mutation failsroute mismatch between client and servermust be fixed structurally, not papered over
6Status changed to a value outside project workflowcurrently may succeed if it matches global Task.STATUS_CHOICESmissing project-specific validationthis is a trust-the-signal defect
7Status deleted while tasks still use itcurrent behavior under-definedno clear migration/reset policy in shipped codeneeds explicit product rule
8File section empty because upload flow is absent, not because there are no filesShared task detail renders the upload hint only when an authorized picker is mounted; otherwise it keeps a neutral, compact no-files rowTaskDetailBody derives one canAttachTaskFile capability from both host picker and server permissionsource contract F49.AC004.FILES_EMPTY_CAPABILITY
9Board has more tasks than first page per columnNative and web boards show a bounded per-column Load more affordance; native page errors keep visible tasks and offer an exact retryAPI supports limit/offset; native ProjectBoardScreenModel merges only the requested column and reconciles total / has_moreshared JVM tests plus Android Maestro flow 110; named-device fidelity remains open
10Comment or assignee mutation succeeds in backend route tests but fails in real UI route pathuser sees broken interactionintegration contract mismatchrequires E2E proof after fix
11Task-detail member list fails or an assignee add is rejectedexisting assignees remain visible; the section shows localized product copy, an exact-member retry action, and no optimistic rowshared model keeps the selected member retry intent and commits only the server responseJVM/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
  • 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

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

  1. The data model is materially better than a flat task list.

    • ProjectStatus, TaskAssignee, and TaskFile are real persisted models. Task comments are served by the universal Comment model in apps/comments/ (TaskComment was deleted in migration 0013 on 2026-06-21 after a data backfill).
  2. 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.
  3. Subtask depth is constrained with explicit tests and a recursive-query implementation.

    • That is a defensible product opinion, not a missing feature.
  4. 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.
  5. 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

  1. [P1] Column semantics are still too simple for Jira-like workflows

    • ProjectStatus.column_key now 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.
  2. [P3] Dependency model is intentionally bounded

    • TaskDependency now captures explicit handoff blockers in task detail (blocked_by and reverse blocks).
    • 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)

  1. ✅ Unify the task-panel route contract: nested routes are now shipped end to end.
  2. ✅ Add strict per-project status validation on task create/update (_validate_project_status).
  3. ✅ Fix my_tasks so assignee roles participate in the filter, not only owner.
    • 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.

DimensionTop-tool barLumi nowTargetGap
Hierarchy / decomposition544at target; checklist items and higher-order issue types remain explicitly out of scope
Multi-assignee ownership544owner-vs-assignee clarity
Workflow configurability544deeper board-layer object if column_key proves insufficient
Board interaction544board-layer policy beyond column_key if workflow rules outgrow it
Task detail depth545richer evidence actions; named-device mobile visual proof remains open
Files / evidence544deeper evidence actions; named-device mobile upload proof remains open
Comments / collaboration544linear only; thread semantics explicitly out of scope
Multi-view consistency544saved view / sort-group filters not yet shipped
Dependency / sequencing power533bounded dependency edges shipped; full scheduling engine remains out
Workflow admin ergonomics544deeper board-layer policy beyond column_key
Signal trustworthiness545bounded 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: added TaskDetailResponse (mirrors the full TaskSerializer: assignee_list, files, blocked_by_dependencies, blocking_dependencies, subtasks, parent_task, owner/project/team names) plus nested TaskAssigneeDto, TaskFileDto, TaskDependencyDto, TaskSummaryDto (=TaskListSerializer), ProjectStatusDto, BoardResponse/BoardColumnResponse (with a JsonElement status field + typed accessors to handle the backend’s polymorphic custom-object-vs-string column shape), and CommentDto/CommentListResponse (generic comments API, target_type="task"). Added create-request DTOs (TaskAssigneeCreateRequest, TaskDependencyCreateRequest, CommentCreateRequest) and a parent_task field on the existing TaskCreateRequest for inline subtask creation.
  • Task detail screen (shared/.../presentation/taskdetail/ + shared/.../ui/taskdetail/TaskDetailScreen.kt) mirroring the web TaskDetailPanel: 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) mirroring ProjectBoardPage: horizontally scrollable columns grouped by ProjectStatus.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/createComment in LumiApiClient.kt.
  • Nav: TaskDetailNavKey(taskId) + ProjectBoardNavKey(projectId) registered in LumiApp.kt (model built via new SharedKmpViewModel.taskDetailModel / projectBoardModel factories); F80 deep-links for task/project now route to these detail surfaces when an id is present. iOS/macOS view-controller factories added in both MainViewController.kt files.
  • i18n: 38 new taskdetail_* / board_* keys added to both values/strings.xml and values-vi/strings.xml (verified en/vi at parity).
  • Tests: TaskDetailScreenModelTest (8 cases) and ProjectBoardScreenModelTest

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) and overdue_task_count (active past due date) with the same archived-task exclusion as task_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_work
  • DtoContractCrmPrimaryContractTest.projectResponseWithAllBackendFieldsDeserializes (extended fixture)
  • ProjectsScreenLogicTest.projectOverdueSignalRendersOnlyPositiveServerCounts
  • scripts/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 + red 1 quá hạn on the stuck row, quiet healthy row, recovery journey intact, custody 4/2/1, cleanup 0: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_POLICY and 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.
  • ProjectBoardScreenModel now requests a bounded page for one column, appends only that column, reconciles total / has_more, and refuses to overwrite a board that was refreshed while the page was in flight.
  • The shared board renders Load more only 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:

  • ProjectBoardScreenModelTest pagination cases and scripts/tests/test_project_board_visual_policy.py.
  • .maestro/critical/110_android_project_board_load_more.yaml through scripts/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_task and could succeed at the API.
  • The success callback invalidated taskSubtasks with 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, LumiSectionHeader with badge, LumiBadge incl. showDot, LumiInlineAction with enabled, LumiMultilineField, LumiStatusBanner, LumiSkeletonList(rows=…), LumiMeta, LumiTone) and color tokens (PaperWhite, CivicNavy, SoftSlate) were grep- confirmed against LumiMobilePrimitives.kt signatures; 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): LoadState machine, testScope injection, init load, runCatching{}.fold success/error, mutate→reload.
  • (d) ADR-025 DTO gate (android/scripts/check-no-native-dtos.sh) passes — all new DTOs live in shared/api/LumiDtos.kt; all referenced i18n keys exist with en/vi parity.

12. Open Questions / Decisions

  1. 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_by graph is not enough.
  2. Should Task.owner remain 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.
  3. 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.
  4. Is ProjectStatus.column_key enough, or does Lumi eventually need a separate board-column object?

    • Current judgment: column_key is the right bounded step. Escalate only when the product genuinely needs WIP limits, backlog separation, or per-column transition policy.
  5. 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_count and done_task_count, but the shared mobile DTO discarded the raw done count 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 valid done count 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, cleaned 0:0:0:0, and shut down the runner-owned emulator on byte-identical prodDebug APK SHA-256 231b6e1f587c0537b17d5498647772a49fe52daf98529c54a712e8de06271879. The current-source release XCFramework rebuilt for arm64 device and simulator with fingerprint ade881e32ee60765b0f52914234a201e4e17fb95, 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: unaccented Binh Duong found Nhà 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 returned 0: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 returned 0: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 / Focus and 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 only Success state.
  • 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 canAttachTaskFile from 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_CAPABILITY source 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_STATES and a fixture-owned iOS simulator flow 147 (build 185, artifact a9c7bde3…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/vi labels 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_id payload with the backend-owned owner field and made edit-form owner: null explicit so “Unassigned” is an actual removal, not an omitted patch field.
  • Mirrored Task.owner as a primary TaskAssignee inside 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 EditProjectScreen success-state Box overlay pattern with the proven CreateProjectScreen layout: a weighted, scrollable form column plus an in-flow Save CTA.
  • Kept imePadding() on the root and added the edit_project_save test tag so the named-device keyboard audit can verify that Save remains above the IME without covering the start/due-date fields.
  • Added EditProjectKeyboardSourceTest to 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 --check
  • PYENV_VERSION=system python3 scripts/check_docs_truth.py
  • bash 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.tsx asserts 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 migration 0013_delete_taskcomment.py on 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 Comment model in apps/comments/ with polymorphic target_type="task". Task comments are served at GET/POST /api/comments/ with target_type and target_id query/body params, plus PATCH/DELETE/resolve on /api/comments/<id>/.
  • Updated §4.1: replaced the TaskComment class definition with a note explaining the deletion and the consolidation into apps/comments/models.py.
  • Updated §2.4: corrected _notify_comment_mentions in TaskViewSet → notify_comment() in apps/comments/services.py; updated the “out of scope” claim to reflect that edits, soft-delete, resolve, and threading (via parent FK) 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 TaskSerializer includes a comments field (it was never added after consolidation); added note that comments are fetched separately via the Comment API.
  • Updated §7 row 6: TestTaskComments in test_work_graph.py → TestComment in backend/tests/test_comments.py; row 8 dropped the TestTaskComments reference.
  • Updated §11 Phase D step 12: annotated with 2026-06-21 note explaining TestTaskCommentMentions was removed when the comment system was consolidated; equivalent tests are now TestNotifications in backend/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 route http://127.0.0.1:8000/api/tasks/${taskId}/comments/. Changed to POST http://127.0.0.1:8000/api/comments/ with body { target_type: 'task', target_id: taskId, body: '...' }, matching the CommentViewSet.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:

  • TaskDetailScreen and ProjectBoardScreen were missing pull-to-refresh, violating mobile-ux-patterns.md §2 which mandates PTR on every screen that fetches its own record.
  • Added isRefreshing: Boolean + refresh() to TaskDetailScreenModel and ProjectBoardScreenModel; wrapped scrollable content in LumiRefreshable.
  • TaskDetailScreen: task and comments refresh independently (stale-while-revalidate per §6.3 — partial failure leaves the other section intact).
  • ProjectBoardScreen: outer Column wrapped in LumiRefreshable; MoveDialog overlay stays outside so it renders on top.
  • Test counts updated: TaskDetailScreenModelTest 8 → 12 cases (success, task-failure, comments-failure, guard); ProjectBoardScreenModelTest 5 → 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_create now applies one server-side rule for every client: if a subtask is created without its own due_date, it inherits the parent task’s due_date; if the child provides a due_date, that explicit value wins; if the parent is undated, the child stays undated.
  • Added three focused backend tests in TestSubtaskDepthLimit covering 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() in backend/apps/work/views.py to create in-app NotificationEvent rows for actionable assignee roles.
  • Locked the role semantics to the current product contract: primary and secondary create category="work" notification events, observer is 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_key so several project statuses may share one visible board column.
  • Updated GET /api/projects/<id>/board/ to group custom statuses by column_key and return grouped metadata via status.column_key and status.mapped_statuses.
  • Extended ProjectWorkflowPage so 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 bounded TaskDependency slice, not a full sequencing engine.

Tests:

  • cd backend && ./.venv/bin/python -m pytest tests/test_work_graph.py -q
  • cd 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 @mention push notifications via _notify_comment_mentions() in TaskViewSet (reuses F29 parse_mentions + F36 push channel). Notifications are best-effort: a failure cannot break comment creation.
  • Fixed a stray indentation error in backend/apps/work/views.py that had left the file unparseable.
  • Reverted an unnecessary user_c fixture rename (“UserC” → “User C”) because normalize_handle() already resolves both forms to the same token.
  • Added TestTaskCommentMentions and TestNestedRoutePermissionsAndErrors to backend/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; TaskViewSet syntax 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 using limit/offset, returns columns with status, 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 by parent_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 board endpoint handles two modes: projects with custom ProjectStatus rows (returns full status objects with color/order/terminal) and projects using default STATUS_CHOICES (returns flat status strings).
  • The tasks and subtasks endpoints use TaskListSerializer (lighter than TaskSerializer, no nested subtasks/comments/files).
  • All endpoints scope queries via organization_id=self.request.organization_id for tenant isolation.

2026-08-03 — Mobile task list Focus and sort triage

Type: UX feature

Summary:

  • Added a shared Android/iOS All / Focus view switch for local executive triage. Focus includes incomplete overdue, due-today/tomorrow, and high/critical-priority work.
  • Added deterministic local Priority, Due date, and Recently added sorting over the loaded task snapshot; All + Recently added remains 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 — passes parent_task__isnull=true to /api/tasks/.
  • Added “My Tasks” Checkbox filter to TasksPage.tsx — passes my_tasks=true to /api/tasks/ (backend param to be wired in TaskViewSet).
  • Added 4 missing i18n keys to en/tasks.json and vi/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.tsx verifying 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_tasks query 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 @mention push notifications via _notify_comment_mentions() in TaskViewSet (reuses F29 parse_mentions + F36 push channel). Notifications are best-effort: a failure cannot break comment creation.
  • Fixed a stray indentation error in backend/apps/work/views.py that had left the file unparseable.
  • Reverted an unnecessary user_c fixture rename (“UserC” → “User C”) because normalize_handle() already resolves both forms to the same token.
  • Added TestTaskCommentMentions and TestNestedRoutePermissionsAndErrors to backend/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; TaskViewSet syntax 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.py as 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 DO and WAITING inside the Vietnamese UI when the API returned English status_display values.
  • 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.tsx
  • frontend/src/pages/work/ProjectTimelinePage.tsx
  • frontend/src/test/ProjectBoardPage.test.tsx
  • frontend/src/test/ProjectTimelinePage.test.tsx
  • frontend/tests/e2e/screenshot-flow.spec.ts

Tests:

  • frontend/src/test/ProjectBoardPage.test.tsx
  • frontend/src/test/ProjectTimelinePage.test.tsx
  • Playwright screenshot flow filtered to 27-work-project-views

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/:taskId alongside 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.tsx
  • frontend/src/components/work/TaskDetailPanel.tsx
  • frontend/src/App.tsx
  • frontend/src/routes/routeTree.ts
  • frontend/src/test/ProjectTasksPage.test.tsx
  • frontend/src/test/TaskDetailPanel.test.tsx
  • frontend/src/test/tanstack-router.test.ts
  • frontend/tests/e2e/screenshot-flow.spec.ts

Tests:

  • frontend/src/test/ProjectTasksPage.test.tsx
  • frontend/src/test/TaskDetailPanel.test.tsx
  • frontend/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 into pages/work/ during the IA restructure. Fixed:
    • frontend/src/pages/ProjectBoardPage.tsx → frontend/src/pages/work/ProjectBoardPage.tsx
    • frontend/src/pages/ProjectTasksPage.tsx → frontend/src/pages/work/ProjectTasksPage.tsx
    • frontend/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.