Spec: F90 — Field Job Execution Ledger
Status: partial — source and registered proofs exist; backend/frontend runners, browser runtime, named-device runtime, and the F84 sign-off-pending path remain open Builds on: F05 · F49 · F83 · F84 · F87 · F12
1. Problem & CEO Job
Serves: see clearly · decide fast · trust the signal.
Lumi already captures field signal through adaptive pulse prompts (F83), field-service vocabulary (F84), multimodal upload capture (F87), and the executive Brief (F12). The missing piece is more basic and more dangerous to get wrong: once a service call or site task exists, Lumi does not yet have a trustworthy task-level execution ledger. A technician can report “đã tới”, “kẹt vật tư”, or “quay lại mai” in a pulse, but the task itself has no compact execution history that a manager can read, audit, or summarize into tomorrow’s operating queue.
F90 adds that ledger without creating a parallel “job” product. A field job is still a Task; a site/package is still a Project; evidence is still EvidenceSource; executive exceptions still flow through the existing Risk, Commitment, DecisionRequest, Brief, and Today spine. The narrow SaaS wedge is Vietnamese field-service and specialty-contractor execution. The long-term operating-layer bet is broader: every work object can eventually carry a sourced, append-only history of what happened, what blocked it, and what the organization should do next.
2. User Stories & Acceptance Criteria
2.1 Technician — one-tap milestones
- As a field technician, I want to update the field status on my assigned task in one tap, so that my manager sees what happened without chasing me in chat.
- Given a
Taskwithexecution_mode="field_job", when I submit an event (accepted,on_the_way,arrived,started,blocked,waiting_material,waiting_approval,paused,resumed,completed,needs_return_visit,left_site,cancelled), then a task event is appended and the task’s latest execution state updates. - Given
execution_mode="generic", when I call a field execution endpoint, then the API returns409and creates no event. - Given I submit
blocked,waiting_material,waiting_approval, orneeds_return_visit, when the event has a note or upload, then the task timeline shows that context and marks it as evidence-bearing.
- Given a
2.2 Supervisor — worklogs with provenance
- As a supervisor, I want task worklogs that are separate from general comments, so that operational proof and report lines do not disappear into discussion threads.
- Given a field task, when I add a worklog with text or a completed upload, then the worklog is stored on the task and scoped to the org.
- Given a text worklog, when it is saved, then Lumi creates or links an
EvidenceSourceso later AI or report output cites the worklog text. - Given an upload that is cross-tenant or not
completed, when I attach it to a worklog or event, then the API rejects the write with no partial row. - Given a worklog visibility of
internal,manager_only, orreportable, when the field day digest is generated, then onlyreportableand allowed internal rows appear in manager-visible report sections.
2.3 Manager — day digest, not formal reporting sprawl
- As a manager, I want a daily field digest for a project or task, so that I can see what moved, what is blocked, and what needs follow-up without building a formal report package.
- Given a project and date, when I request the field day digest, then Lumi groups events/worklogs into completed work, active/in-progress work, blockers, return visits, approval holds, and evidence references.
- Given no field events or worklogs exist for that date, when I request the digest, then Lumi returns empty sections with no generated narrative.
- Given a
manager_onlyworklog exists, when a non-manager requests the digest, then that row is omitted or the request is forbidden according to the endpoint’s role.
2.4 CEO — exceptions flow into the existing queue
- As a CEO, I want field exceptions to reach the Brief and Today only when they deserve attention, so that I do not inherit another dashboard to monitor.
- Given a field task has an unresolved
blocked,waiting_material,waiting_approval, orneeds_return_visitevent, when the next Brief is generated, then it can produce a normalBriefItembacked by the task/worklog evidence. - Given downstream field-service extraction from F84 produces a linked sign-off-pending signal on the same task/project context, when the next Brief is generated, then that signal can appear through the same queue path without requiring a new field-event type.
- Given a field event is routine (
arrived,started,left_site) and has no material note, when the Brief is generated, then no executive item is created. - Given a field exception is resolved by a later event (
resumed,completed, or an explicit manager resolution), when the Brief is regenerated, then the stale exception is not repeated.
- Given a field task has an unresolved
2.5 Admin / creator — opt in per task
- As a task creator, I want to mark only the relevant tasks as field jobs, so that normal office tasks stay simple.
- Given task create/edit, when
execution_mode="field_job"is selected, then field controls are available on that task. - Given task create/edit with no field mode selected, then the task remains
genericand the existing task UI stays unchanged.
- Given task create/edit, when
2.6 Mobile user — same work graph, not a web-only detour
- As a CEO, manager, or employee using Lumi on iPhone or Android, I want to run the same field task from the mobile Work Graph surfaces, so that I never need the web app just to update or review a live site job.
- Given a mobile user opens a field task from Pulse, Brief, notifications, or Work Graph, when task detail loads, then the same F90 primitives are available there: latest execution state, milestone capture (member+), worklog capture (member+), execution timeline, and task day digest readback.
- Given a manager opens a field project or field task on mobile, when they review the work, then latest execution badges and the project/task field day digest are visible inside the same Work Graph flow with
manager_onlyvisibility enforced by role. - Given a previously loaded task or project digest and a transient digest refresh failure, when the manager remains on detail, then the last server-authored digest stays visible beside one compact localized warning; tapping Retry reloads only the digest and clears the warning on success.
- Given a viewer or non-manager lacks a write/read permission in web, when they open the same mobile surface, then mobile mirrors that permission truth instead of inventing a looser native rule.
- Given a task already has field execution history, when a mobile user edits it, then the task cannot silently downgrade back to
genericand hide its live ledger.
3. Scope Boundaries
3.1 Product decisions
| Decision | Rationale |
|---|---|
Task remains the work-order/job object. | F49 already defines the contractor work graph. A separate Job table would fork ownership, permissions, search, comments, files, and Brief links. |
| F90 ships a ledger before it ships a suite. | Milestones + worklogs are the durable primitive. Time sheets, customer reports, GPS, and offline sync are later layers, not the foundation. |
| Field execution is task-scoped, not org-global. | A company may use Lumi for sales/admin tasks and field jobs side by side. execution_mode="field_job" turns the extra controls on only where they belong. |
| Execution state does not silently rewrite workflow status. | Task.status may be default or project-custom. F90 v1 records what happened in execution_state; any workflow status change remains explicit until a tested mapping exists. |
| Worklogs are evidence, not a second chat/comment system. | F49 comments remain discussion. F90 worklogs are reportable source material with visibility and provenance. |
| The first report is a projection. | A daily field digest is generated from events/worklogs. Snapshot/PDF/customer export come only after the projection proves useful. |
3.2 In / out
- In:
Task.execution_mode, append-only task execution events, task worklogs with evidence, project/task field day digest projection, and Brief/Today exception integration through the existing queue models. - Out: a new
Job/WorkOrdermodel, inventory/material master data, payroll, employee-day attendance (owned separately by F106, including its attendance photo and point-GPS evidence), task-level GPS/selfie enforcement, customer portal, customer signatures, PDF exports, formal daily report snapshots, accounting/invoicing, generic workflow automation, and native offline queues. - Deferred: time entries and manager approval of labor minutes. The ledger can derive rough durations from events, but payable/approved time is a later spec slice after real customer validation. F106 attendance is not a task-time or payable-labor substitute.
- Deferred: customer-facing completion reports. F90 keeps reportable worklogs structured so a future export can use them, but does not promise customer delivery in the first build.
3.3 Deferred scope gates
These are deliberately not part of the first F90 build:
| Future slice | Gate before build |
|---|---|
| Time entries / labor approval | At least 3 pilot orgs ask for labor review and agree on non-payroll wording |
| Customer-facing report / PDF / share link | Managers manually use the in-app digest as a customer proof substitute in pilots |
| GPS / offline sync | Field users hit real connectivity/location proof failures in pilots |
| Inventory/material usage | Missing-material signals are frequent enough that a non-inventory blocker is no longer enough |
4. Data Model
4.1 Reused spine
work.Taskis the field job / service call.work.Projectgroups jobs, sites, or packages.uploads.Uploadstores files/photos.pulses.EvidenceSourceanchors provenance.Risk,Commitment,DecisionRequest,Brief, andBriefItemremain the executive queue.
4.2 Task additions
class Task(UUIDModel):
execution_mode = models.CharField(
max_length=20,
choices=[("generic", "Generic"), ("field_job", "Field job")],
default="generic",
)
execution_state = models.CharField(max_length=24, blank=True, default="")
execution_updated_at = models.DateTimeField(null=True, blank=True)
Task.statusremains the planning/workflow state from F05/F49.execution_stateis a denormalized latest event for filtering and display. The event ledger is authoritative.- F90 v1 does not auto-mutate
Task.statusfrom events. A future sync rule may be added only if it respects F49 custom project statuses and has explicit tests. - Append these indexes to the existing
Task.Meta.indexes; do not replacedb_table, ordering, or existing indexes:("organization", "execution_mode", "execution_state", "-id")("organization", "project", "execution_mode", "-id")
4.3 New TaskExecutionEvent
class TaskExecutionEvent(UUIDModel):
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="execution_events")
actor = models.ForeignKey(OrganizationMember, null=True, blank=True, on_delete=models.SET_NULL)
actor_display_name = models.CharField(max_length=255, blank=True)
event_type = models.CharField(max_length=24, choices=EVENT_TYPE_CHOICES)
occurred_at = models.DateTimeField()
note = models.TextField(blank=True)
upload = models.ForeignKey("uploads.Upload", null=True, blank=True, on_delete=models.PROTECT)
evidence_source = models.ForeignKey("pulses.EvidenceSource", null=True, blank=True, on_delete=models.SET_NULL)
source = models.CharField(max_length=16, choices=[("manual", "Manual"), ("pulse", "Pulse"), ("import", "Import")], default="manual")
metadata_json = models.JSONField(default=dict, blank=True)
class Meta:
indexes = [
models.Index(fields=["organization", "task", "-occurred_at", "-id"]),
models.Index(fields=["organization", "event_type", "-occurred_at", "-id"]),
models.Index(fields=["organization", "task", "event_type", "-occurred_at", "-id"]),
]
Rules:
- Events are append-only. Corrections are new events.
occurred_atdefaults to server time when omitted.actor_display_nameis captured at write time so the ledger remains readable if a member account is later removed.- Event notes that may feed AI/report output create or link an
EvidenceSource. - Uploads must be completed and same-org.
4.4 New TaskWorklog
class TaskWorklog(UUIDModel):
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="worklogs")
author = models.ForeignKey(OrganizationMember, null=True, blank=True, on_delete=models.SET_NULL)
author_display_name = models.CharField(max_length=255, blank=True)
visibility = models.CharField(
max_length=16,
choices=[("internal", "Internal"), ("manager_only", "Manager only"), ("reportable", "Reportable")],
default="internal",
)
raw_text = models.TextField(blank=True)
upload = models.ForeignKey("uploads.Upload", null=True, blank=True, on_delete=models.PROTECT)
evidence_source = models.ForeignKey("pulses.EvidenceSource", null=True, blank=True, on_delete=models.SET_NULL)
metadata_json = models.JSONField(default=dict, blank=True)
class Meta:
indexes = [
models.Index(fields=["organization", "task", "-created_at", "-id"]),
models.Index(fields=["organization", "visibility", "-created_at", "-id"]),
]
Rules:
- At least one of
raw_textoruploadis required. TaskCommentremains general discussion.TaskWorklogis reportable operating evidence.reportablemeans eligible for internal manager/project digest. It does not mean customer-visible yet.author_display_nameis captured at write time for audit continuity.
4.5 No first-slice time-entry table
F90 deliberately does not add TaskTimeEntry in v1. Derived durations can be computed from event pairs (started → paused/completed/left_site) for rough operating visibility. Payable time, approval, billing flags, overtime, and payroll/accounting exports require a later spec because the wrong data model there would be expensive and trust-damaging. F90 events never create or imply an F106 attendance record.
4.6 Field day digest storage
No DailyFieldReport table in v1. The digest is a read projection over Task, TaskExecutionEvent, TaskWorklog, uploads, and downstream signals. Add snapshots only if real usage proves that generated reports need stable historical copies or customer export.
5. API Contract
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
GET | /api/tasks/<id>/execution-events/ | viewer+ | — | ordered task events | 404 cross-tenant |
POST | /api/tasks/<id>/execution-events/ | member+ | {event_type, occurred_at?, note?, upload_id?} | created event + updated task latest state | 409 generic task |
GET | /api/tasks/<id>/worklogs/ | viewer+ | — | ordered visible worklogs | 404 cross-tenant |
POST | /api/tasks/<id>/worklogs/ | member+ | {raw_text?, upload_id?, visibility} | created worklog | 400 invalid upload / empty payload |
GET | /api/projects/<id>/field-day/ | manager+ | date | project field day digest | 404 cross-tenant |
GET | /api/tasks/<id>/field-day/ | member+ | date | task field day digest | 404 cross-tenant |
Tenant scoping:
- Every query filters by
organization_id=request.organization_id. - Cross-tenant task/project/upload ids return
404. - Upload ids are resolved only within the caller’s organization and must be
completed.
AI / evidence:
- F90 does not introduce a second extraction engine.
- Event notes and worklog text create/link
EvidenceSource; downstream AI uses the shared F08/F84/F86 path. - Do not add new
EvidenceSource.source_typechoices for v1. Use the existingsource_type="task"withmetadata_json.kindvalues such asfield_execution_eventorfield_worklog. - PR-1/PR-2 persist evidence only. They do not create downstream
Risk/Commitment/DecisionRequestrows or run a new save-time extraction/writeback loop. - Routine status taps without note/upload do not need an
EvidenceSourcebecause they are user-authored structured events, not AI output. - Any AI-created item derived from F90 data must cite the source worklog/event evidence and obey F86 grounding.
5.1 Conversational parity (F91)
The milestone form remains an optional direct path. A member can instead say
what happened in the Lumi conversation and confirm a log_execution_event
action. The action resolves task_title inside the current organization,
requires event_type, accepts note and occurred_at only when explicitly
stated, and uses the same append-only event service as the REST endpoint.
It mirrors the form’s IsOrgMember / non-viewer write gate and has
autonomy_ceiling=1 like log_worklog; it never silently creates an event.
Field-task validation, latest-state ordering, and source_type="task" event
evidence therefore remain one contract across form and chat.
6. UI / UX
- Web primary surface:
TaskDetailPanel.tsxshows the field ledger only whentask.execution_mode="field_job". - Web task create/edit:
TasksPage.tsxandProjectBoardPage.tsxexpose a simple field-job type selector;TaskDetailPanel.tsxcan edit the type later. It must not make generic task creation slower. - Web project surfaces:
ProjectTasksPage.tsxandProjectBoardPage.tsxshow latest execution state for field jobs in compact badges. - Shared mobile surface: F90 lives under F79
Work Graph, not a sixth mobile tab and not a native-only field mini-app. The target shared surfaces are KMPTasks,TaskDetail,Projects, andProjectDetail, with the iOS/Android shells only owning navigation chrome and platform affordances. - Mobile task/project detail: field tasks show milestone buttons, latest status, worklog composer, execution timeline, and task day digest inside the same detail screen. Project detail and task list surfaces show compact latest-state badges and a manager-visible project day digest. A failed digest refresh keeps the last committed digest visible and exposes one digest-scoped Retry action rather than reloading or blanking the whole detail route.
- Digest surface: first slice lives inside task/project context on all surfaces rather than a separate reporting app.
- States: generic task, no events yet, loading, error, populated, no permission, stale-object refresh after a conflict, and stale digest with exact retry.
- Web components: existing shadcn
Button,Badge,Card,Textarea,Skeleton,Dialog, upload affordances from F34. No stock palette or new component family. - Mobile components: shared KMP UI stays on the existing Lumi mobile component language from F30/F78; no platform-specific second design system for field execution.
- i18n: web keeps
fieldExecution+tasks; mobile keeps the same vocabulary in shared Compose resources / native strings so field status labels do not diverge by platform. - No duplicate workflow: Pulse, Brief, notification, and Work Graph deep links must land in the same live task/project detail surfaces rather than split “field detail” routes by entry point.
- Conversational path:
/intel/chatcan propose and confirm a field milestone; the ActionCard shows the task, milestone, and note before commit and links back to the same task detail surface after execution.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Field task accepts execution events and updates latest execution state; generic task rejects | backend | backend/tests/test_field_execution.py |
| 2 | Worklog requires text or upload, creates/links evidence, rejects cross-tenant/incomplete upload | backend | backend/tests/test_field_execution.py |
| 3 | Field day digest groups events/worklogs into sections and never invents narrative on empty data | backend | backend/tests/test_field_execution.py |
| 4 | Cross-tenant access is blocked on every new endpoint | backend | backend/tests/test_field_execution.py |
| 5 | Task detail shows field ledger only for field jobs; milestone/worklog writes preserve user input on failure | frontend | frontend/src/test/TaskDetailPanel.test.tsx |
| 6 | Global/project task create surfaces keep the generic path fast while allowing field_job opt-in | frontend | frontend/src/test/TasksPage.test.tsx frontend/src/test/ProjectBoardPage.test.tsx |
| 7 | Project task/board surfaces show latest execution badge for field jobs | frontend | frontend/src/test/ProjectTasksPage.test.tsx frontend/src/test/ProjectBoardPage.test.tsx |
| 8 | Field day digest renders empty, loading, error, and populated states | frontend | frontend/src/test/FieldDayDigest.test.tsx |
| 9 | End-to-end field flow: field task → arrived/blocked/completed → worklog → project day digest → Brief exception | e2e | frontend/tests/e2e/lumi.spec.ts top-level test("field execution blocker reaches project digest and brief via API", …) (~line 1489); the test is not wrapped in a describe("Field Execution") block |
| 10 | Shared KMP DTO/API layer decodes F90 task fields, events, worklogs, and digests without inventing a mobile-only contract | shared KMP | android/shared/src/commonTest/kotlin/com/locuno/lumi/shared/api/LumiApiClientTest.kt |
| 11 | Shared TaskDetailScreenModel / ProjectDetailScreenModel preserve note text on failed field writes, enforce role-aware visibility, and refresh list/detail badges or manager-only digests after a save without leaking viewer/member access | shared KMP | android/shared/src/commonTest/kotlin/com/locuno/lumi/shared/presentation/taskdetail/TaskDetailScreenModelTest.kt .../projects/ProjectDetailScreenModelTest.kt |
| 12 | iOS Work Graph routing opens the shared field task/project detail/edit surfaces, keeps F90 context-only inside the five-lane shell, and never falls back to a web-only path for in-scope F90 actions | iOS | ios/UnitTests/Workspace/WorkspaceTests.swift ios/UnitTests/Workspace/RuleOfTwoTests.swift ios/UnitTests/Workspace/TaskDetailKMPViewTests.swift |
| 13 | Android Work Graph routing opens the shared field task/project detail, keeps F90 inside the five-lane shell, and does not grow a duplicate field row outside Tasks / Projects | Android | Android unit/instrumented shell tests under android/app/src/test/ and android/app/src/androidTest/ plus shared KMP coverage |
| 14 | Mobile role parity: CEO/owner, manager, employee, and viewer each see the same allowed F90 reads/writes they see on web | iOS + Android | platform unit/UI suites |
| 15 | Mobile deep-link parity: Pulse/Brief/notification entry points all resolve to the same F90 task/project detail surface | iOS + Android | platform navigation tests |
| 16 | A conversational field milestone resolves a tenant-local field task, preserves note/evidence, and updates the execution projection only after confirmation | backend + e2e | backend/tests/assistant/test_assistant_actions_work_lifecycle_executors_2.py frontend/tests/e2e/lumi.part02.spec.ts |
8. Current Behavior
F90 has source-backed backend, web, and mobile implementation slices. The contract status is partial because the registered runners and target-runtime evidence are not available in this checkout, and the F84 sign-off-pending downstream criterion remains unchecked. Lumi now has:
- task/project/work-graph objects (F05, F49),
- generic task comments/files (F49),
- role-aware pulse prompts and field-service vocabulary (F83, F84),
- multimodal uploads (F87),
- executive consumption surfaces (F12, F13),
- backend ledger primitives (
Task.execution_mode,TaskExecutionEvent,TaskWorklog, tenant-safe task endpoints), - web task/project surfaces for field-job create, edit, milestone capture, worklogs, latest-state scanability, and in-context field day digests,
- an explicit execution-history lock contract so backend, web, and shared mobile all preserve the same “no hidden downgrade back to generic” rule once field history exists,
- Brief candidate projection for grounded unresolved field exceptions backed by field-event/worklog evidence.
- an F91
log_execution_eventaction so field milestones can be captured from chat with the same confirm-only form parity contract.
Deferred by scope:
- customer-facing report/export remains deferred by §3.
- GPS / offline queues remain deferred by §3.
- time-entry / labor approval remains deferred by §3.
9. Error States
| # | Error | User-visible behavior | Internal handling |
|---|---|---|---|
| ES1 | Field endpoint called on generic task | fieldExecution.error_not_field_task | 409; no partial row |
| ES2 | Upload not completed or wrong org | fieldExecution.error_upload_invalid | validate upload org + status="completed" before create |
| ES3 | Empty worklog payload | fieldExecution.error_empty_worklog | 400; no row |
| ES4 | Manager-only digest requested by member | no permission state | 403 |
| ES5 | Digest date has no data | explicit empty state | 200 with empty sections |
| ES6 | Downstream digest/Brief enrichment fails after save | saved source remains visible; derived signal absent until retry/regenerate | log exception; never lose user input |
| ES7 | Unsupported event transition | validation message only if transition rules are later added | v1 allows append-only events; no strict FSM |
10. Diagrams & Docs Touched
When implementation changes code, update these in the same PR:
- backend/DIAGRAMS.md: task adjunct models and execution event flow.
- frontend/DIAGRAMS.md: task detail/project day digest route flow.
- GLOSSARY.md:
field job,execution event,worklog,field day digest. - docs/specs/F84-field-service-ontology.md: keep exception output mapping aligned when field events add new source contexts.
11. Implementation Audit (2026-07-16)
Current source-backed slices: backend ledger, web field ledger, digest projection, Brief/Today exception consumption, shared KMP F90 task/project detail behavior, and iOS/Android shell parity are implemented and registered for verification. Runtime status remains partial until the named contract gaps close.
What now exists in repo:
Task.execution_mode,Task.execution_state, andTask.execution_updated_at- append-only
TaskExecutionEvent - tenant-safe
GET/POST /api/tasks/<id>/execution-events/ - evidence creation/linking for material event notes or completed uploads
TaskWorklog- tenant-safe
GET/POST /api/tasks/<id>/worklogs/ - visibility-aware worklog reads with
manager_onlyhidden from non-manager roles - evidence creation/linking for text worklogs or completed uploads
TaskDetailPanelfield milestones, worklog composer, and execution timeline for field jobs only- task create/edit surfaces that can opt into
execution_mode="field_job"without changing the default generic path - task detail payload truth (
has_execution_history) that keeps web and mobile edit flows aligned with the backend lock against downgrading a historical field task back togeneric - latest execution badge/timestamp on project task list and board cards
- shared role-fixture proof that
owner/admin/manager/memberretain the writable mobile field-task flow whileviewerstays read-only, and that project digest visibility remains manager-gated - concrete iOS unit proof that shared Workspace/Work Graph inventory, deep links, and task/project/detail/edit hosts all stay inside the same F90 surfaces rather than falling back to web or a second native field app
- concrete Android unit proof that Work Graph and Workspace lane routing keep F90 under the existing
Tasks/Projectsroots, with the same shared role matrix and invalidation behavior
The implementation surfaces exist, but blocking evidence gaps remain: backend and frontend runners are unavailable locally, no browser or named-device runtime artifacts are attached, and the unchecked F84 sign-off-pending downstream criterion needs executable reconciliation. The remaining mobile work in Lumi is broader F30 platform completion, but it does not replace F90’s named-device evidence requirement.
The exact GET /api/projects/{id}/field-day/ and GET /api/tasks/{id}/field-day/ surfaces are
registered to ProjectViewSet.field_day and TaskExecutionMixin.field_day with
F90.AC004.PROJECT_API and F90.AC004.TASK_API; this repository proof does not establish browser, named-device,
deployed, or production evidence.
The exact GET /api/tasks/{id}/worklogs/ collection surface is registered to
TaskExecutionMixin.worklogs with F90.AC002.WORKLOG_LIST; repository evidence remains separate
from backend execution, browser, named-device, deployed, Redis, Sentry, and production evidence.
The exact POST /api/tasks/{id}/worklogs/ create surface is registered to
TaskExecutionMixin.worklogs with F90.AC002.WORKLOG_CREATE; backend execution remains an explicit
F90.GAP007 until the guarded pytest command runs successfully.
The repo still constrains the remaining design as follows:
| # | Existing reality | Decision for F90 |
|---|---|---|
| 1 | Task and Project are already the work graph | Reuse them; no Job/WorkOrder table |
| 2 | F49 already ships comments, files, dependencies, workflows, assignees | Add only execution events and reportable worklogs |
| 3 | F84 already maps field concepts onto the existing spine | Reuse its signal language; do not create another taxonomy |
| 4 | F87/F34 already own uploads | Reuse completed Upload rows only |
| 5 | F08/F86 own AI extraction and grounding | Worklogs/events become evidence; extraction stays shared |
| 6 | Time tracking is explicitly out of F05 and not shipped elsewhere | Defer payable/approved time instead of slipping it into F90 v1 |
| 7 | No daily report model exists | Use a projection first |
12. Resolved Rollout Decisions (2026-06-29)
execution_modeis manual in v1. Project- or industry-level defaults can be considered later only after teams show repeated setup friction.- Event/worklog writes do not synchronously create downstream
Risk,Commitment, orDecisionRequestrows. PR-5 reads unresolved field exceptions through the existing Brief candidate path first. - v1 does not add a retry queue for post-save enrichment. Saved evidence plus the next digest/Brief regeneration is enough until real failure rates prove otherwise.
13. Bug / Change Log
2026-08-09 - Shared mobile field-day digest recovery
Type: Mobile recovery UX
Summary:
- Kept the last task/project field-day digest visible when a later digest request fails.
- Added a compact localized warning with an exact digest Retry action on both shared detail surfaces.
- Kept retry scoped to the digest endpoint so project/task context, drafts, and unrelated sections do not reload.
- Added focused KMP model/source-contract regressions and guarded Android flow 155. The approved
Lumi_Pixel7_API35emulator passed project and task outage/recovery on exact built/installed APK SHA-256350542ac3fa5ee79a429d8479e3e0cb91ff46ad33757767ddd22e1cb73ba359d; iOS, named-device, accessibility, store, deployed, and production proof remain open.
Reason:
- The shared models stored only a successful digest as retry context, while the UI replaced that digest with a warning that offered no action. A transient digest failure therefore hid the manager’s last trustworthy field snapshot and made recovery depend on refreshing the whole detail screen.
2026-07-16 — Contract Graph migration and evidence-status correction
Type: Contract Graph + Documentation
Summary:
- Added the canonical
contracts/features/F90.cuecontract with stable requirement/proof IDs for the backend ledger, evidence, role boundaries, digest, Brief, web, and shared-native surfaces. - Added runner-native markers and a source guard without treating source inspection or collected native tests as browser/device proof.
- Reconciled the prior
shippedstatus topartialbecause the local backend/frontend runners are unavailable, browser and named-device artifacts are absent, and §2.4 still contains an unchecked F84 sign-off-pending criterion. - The unchecked F84 sign-off-pending path is an explicit missing-proof boundary, not a contradiction with the current partial status; its owner and reproduction action remain in
F90.GAP005.
2026-07-16 — Task execution-event collection ownership
Type: Contract Graph + backend proof registration
Summary:
- Assigned
GET /api/tasks/{id}/execution-events/to F90 with its exactTaskExecutionMixin.execution_eventsaction locator. - Registered the existing append-only reverse-chronological pytest as
F90.AC001.EVENT_LIST; collection is source-backed proof registration, not executed backend evidence.
2026-07-16 — Task execution-event create ownership
Type: Contract Graph + backend proof registration
Summary:
- Assigned
POST /api/tasks/{id}/execution-events/to F90 with the same exactTaskExecutionMixin.execution_eventsaction locator. - Registered
TestFieldExecutionEvents.test_field_task_accepts_execution_event_and_updates_latest_stateasF90.AC001.EVENT_CREATE; guarded backend execution remains explicit inF90.GAP006.
2026-07-16 — Task field-day digest ownership
Type: Contract Graph + backend proof registration
Summary:
- Assigned
GET /api/tasks/{id}/field-day/to F90 with the exactTaskExecutionMixin.field_dayaction locator. - Registered the existing tenant-scoped, role-filtered task digest pytest as
F90.AC004.TASK_API; collection is source-backed proof registration, not executed backend evidence.
2026-07-16 — Task worklog collection ownership
Type: Contract Graph + backend proof registration
Summary:
- Assigned
GET /api/tasks/{id}/worklogs/to F90 with the exactTaskExecutionMixin.worklogsaction locator. - Registered the existing manager/member visibility pytest as
F90.AC002.WORKLOG_LIST; collection is source-backed proof registration, not executed backend evidence.
2026-07-13 — Conversational milestone parity (F91 / P33-10)
Type: Backend + web + assistant parity
Summary:
- Added
log_execution_eventas the conversational twin of the field milestone form. - Reused the shared append-only event service for REST and assistant writes, including field-task validation, latest-state projection, and task evidence metadata.
- Kept the direct milestone form, added ActionCard rendering for task/milestone/note, and added a pure chat E2E confirm path.
Decision:
- The action is member-only, confirm-before-commit, and
autonomy_ceiling=1, matchinglog_worklog. It does not create downstream intelligence rows at save time; existing F90 digest/Brief projections consume the durable event and evidence.
2026-07-02 — §7 Row 9 e2e test reference corrected (batch 12)
Type: Documentation Truth Fix
Summary:
- §7 Row 9: Corrected the E2E test reference from
describe("Field Execution")(no such describe block exists) to the actual top-leveltest("field execution blocker reaches project digest and brief via API", …)at ~line 1489 offrontend/tests/e2e/lumi.spec.ts(verified by grep: the test is ungrouped, not nested in anydescribe).
2026-07-02 — Spec drift fix: checked shipped ACs in §2
Type: Documentation
Summary:
- 19 of 20
[ ]ACs in §2 flipped to[x]— confirmed shipped against §11 audit (2026-06-29). - Checked: §2.1 task-event append + 409 on generic task + evidence-bearing blocked events; §2.2 worklog storage + EvidenceSource linking + upload rejection + visibility-aware digest; §2.3 digest grouping + empty-data no-narrative + manager_only gating; §2.4 Brief exception projection + routine-event no-Brief + resolved-exception not-repeated; §2.5 field_job opt-in + generic task unchanged; §2.6 all four mobile parity ACs (shared primitives, manager digest, role mirror, no-downgrade lock).
- Left
[ ]: §2.4 AC about F84 downstream sign-off-pending signal appearing through same queue path — this specific integration scenario is not explicitly enumerated in §11 rows (§11 explicitly says PR-1/PR-2 do not create downstream rows at save time). - No code changes.
2026-07-01 — Field-day digest backend proof made calendar-stable
Type: Test hardening
Summary:
- Stabilized the backend field-day digest proof by pinning reportable worklog timestamps to the requested digest day inside
backend/tests/test_field_execution.py. - Kept the project/task digest assertions aligned with the real contract: only events and worklogs that fall inside the requested day window should appear in the digest evidence section.
Reason:
- The digest projection already filtered worklogs by
created_atcorrectly, but the test fixture created evidence worklogs at wall-clocknowwhile asserting against the fixed date2026-06-29. - Once the real calendar moved past the seeded digest date, the test began failing even though the implementation was correct.
Impact:
- No shipped F90 behavior changed.
- The backend regression proof is now deterministic instead of depending on the day the suite happens to run.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_field_execution.py -x --tb=short
2026-06-29 — Initial draft from pulse-v3 advisor brief
Type: Documentation Change
Summary:
- Added F90 as a draft feature contract for the missing field job execution ledger: task-scoped milestones, reportable worklogs, project/task day digest, and queue integration.
- Kept job/work-order identity on
Taskand deferred time sheets, GPS, customer reports, inventory, and payroll-adjacent functionality.
Reason:
pulse-v3-brief.mdmixes ideas Lumi already has with one important missing primitive. Without a narrow spec, implementation would likely duplicate F83/F84/F87/F12 or create a parallel field-service app beside Tasks.
Impact:
- Creates a buildable contract for the field execution layer while protecting Lumi’s existing spine.
Tests:
2026-06-30 — P32 mobile parity closed with shared role proof + concrete iOS/Android shell tests
Type: Mobile + Docs
Summary:
- Closed the remaining F90 mobile parity gap without inventing a second product surface: shared KMP role tests, Android route/breadth tests, and focused iOS XCTest slices now prove that field-task/task-project execution stays inside the same mobile Work Graph surfaces for owner/admin/manager/member/viewer.
- Strengthened the iOS Workspace suite by replacing placeholder assertions with concrete route-contract and shared-screen construction checks for the shipped Workspace, Intelligence, Company, Account, and desktop-handoff rows.
- Updated the spec/plan/registry truth so F90 is marked shipped on iOS and Android while broader F30 mobile closeout remains tracked separately.
Reason:
- The code had already reached the intended F90 product shape, but the docs were still speaking as if mobile parity were merely “mostly there.” That mismatch would create fake product debt and keep the registry stale.
Impact:
- F90 is now an explicitly shipped cross-surface feature: backend, web, iOS, and Android all converge on the same field execution ledger contract.
Tests:
cd android && ./gradlew :shared:jvmTest --tests 'com.locuno.lumi.shared.presentation.taskdetail.TaskDetailScreenModelTest' --tests 'com.locuno.lumi.shared.presentation.projects.ProjectDetailScreenModelTest' --tests 'com.locuno.lumi.shared.api.LumiApiClientTest'cd android && ./gradlew :app:testProdDebugUnitTest --tests 'com.locuno.lumi.android.FieldExecutionRoutingTests' --tests 'com.locuno.lumi.android.WorkspaceDirectoryTests' --tests 'com.locuno.lumi.android.WorkBreadthTests' --tests 'com.locuno.lumi.android.AccountBreadthTests'cd ios && xcodebuild test -project Lumi.xcodeproj -scheme Lumi -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -only-testing:UnitTests/Workspace/RuleOfTwoTests -only-testing:UnitTests/Workspace/WorkspaceDirectoryTests -only-testing:UnitTests/Workspace/WorkBreadthTests -only-testing:UnitTests/Workspace/IntelligenceBreadthTests -only-testing:UnitTests/Workspace/CompanyBreadthTests -only-testing:UnitTests/Workspace/AccountBreadthTests -only-testing:UnitTests/Workspace/DesktopHandoffTests -only-testing:UnitTests/Workspace/TaskDetailKMPViewTests CODE_SIGNING_ALLOWED=NO
2026-06-29 — PR-1 backend ledger primitive landed
Type: Backend + Docs
Summary:
- Added
Task.execution_mode,execution_state, andexecution_updated_at. - Added append-only
TaskExecutionEventplus tenant-safe task-scoped execution-event endpoints. - Kept
Task.statusindependent from field execution state and fixed the projection so “latest” followsoccurred_at, not write time.
Reason:
- The execution ledger is the minimum trustworthy primitive needed before worklogs, digests, or Brief integration can be added without inventing a parallel field-service product.
Impact:
- Lumi can now model field-task progress as an auditable task-level ledger while preserving the existing task/project/workflow spine.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_field_execution.py tests/test_work_graph.py -x --tb=short
2026-06-29 — PR-2 backend worklogs landed
Type: Backend + Docs
Summary:
- Added
TaskWorklogwithinternal,manager_only, andreportablevisibility. - Added tenant-safe
GET/POST /api/tasks/<id>/worklogs/on field tasks only. - Preserved provenance rules by creating/linking
EvidenceSourcerows for text worklogs and completed uploads, while keeping manager-only rows hidden from non-manager reads.
Reason:
- Worklogs are the second durable primitive of F90. Without them, field proof would either stay trapped in generic comments or force an eventual second note system beside the task spine.
Impact:
- Lumi now has task-scoped reportable operating notes with provenance and role-aware visibility, still without introducing a parallel field-service app or workflow layer.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_field_execution.py tests/test_work_graph.py -x --tb=short
2026-06-29 — PR-3 web field ledger landed
Type: Frontend + Docs
Summary:
- Added field milestones, worklog composer, execution timeline, and task-type editing to
TaskDetailPanel. - Added task-type selection to
TasksPageandProjectBoardPagecreate flows. - Surfaced latest execution badge/timestamp on project task list and board cards, and added the
fieldExecutioni18n namespace to the shipped locale set.
Reason:
- F90 needed one honest web slice that lets teams create a field job, record what happened, and scan project progress without forcing a second app or a create-generic-then-edit detour.
Impact:
- Web users can now opt a task into field mode, capture milestone/worklog evidence in place, and see the latest field state where they already manage project work. Digest and Brief integration remain deferred.
Tests:
cd frontend && ./node_modules/.bin/tsc --noEmitcd frontend && pnpm test -- src/test/TaskDetailPanel.test.tsx src/test/ProjectTasksPage.test.tsx src/test/ProjectBoardPage.test.tsx src/test/TasksPage.test.tsx src/test/i18n.test.tscd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_field_execution.py tests/test_work_graph.py -x --tb=short
2026-06-29 — PR-4/PR-5 digest + Brief exception path landed
Type: Backend + Frontend + E2E + Docs
Summary:
- Added read-projection field day digest endpoints for project/task context and rendered the digest inside
ProjectTasksPageandTaskDetailPanel. - Added grounded Brief candidates for unresolved field exceptions, with evidence linked from field events/worklogs and
related_taskpointing at the live field task so the Brief stays in one action queue instead of spawning duplicate follow-up tasks. - Added an API-level Playwright regression proving a blocked field task appears in the project digest and then in the generated Brief.
Reason:
- F90 was still missing the manager read surface and the CEO escalation path. Shipping the ledger without those two projections would leave field proof trapped at the task level and force managers back into chat chasing.
Impact:
- Managers can now scan today’s field movement in the same work surface they already use, and CEOs can see only the grounded unresolved field exceptions that deserve attention without a second alert center.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_field_execution.py -k 'field_day_digest or brief' -x --tb=shortcd frontend && ./node_modules/.bin/tsc --noEmitcd frontend && pnpm test -- src/test/FieldDayDigest.test.tsx src/test/TaskDetailPanel.test.tsx src/test/ProjectTasksPage.test.tsxcd frontend && pnpm exec playwright test tests/e2e/lumi.spec.ts -g "field execution blocker reaches project digest and brief via API" --reporter=list
Notes:
- The Playwright regression is intentionally Chromium-only because it is an API/state integration check, not a browser-rendering compatibility check. Running it under parallel multi-engine workers races the same org/day Brief row and adds SQLite lock noise without increasing confidence.
2026-06-29 — Mobile deep-link parity landed for notifications
Type: KMP + iOS + Android + Docs
Summary:
- Made shared mobile notification rows actionable instead of read-only by wiring route taps through the existing KMP notification surface.
- Added iOS and Android route mappers that reuse existing backend
routestrings and land task/project/work notifications in the already-shipped shared work graph surfaces rather than a second field flow. - Added platform routing tests covering notification-to-task/project parity alongside the earlier Pulse/Brief deep-link checks.
Reason:
- F90 explicitly requires Pulse, Brief, notifications, and Work Graph to converge on the same mobile field task/project detail surfaces. Leaving notification rows non-actionable would keep a hidden web detour in one of the highest-frequency entry points.
Impact:
- Mobile field execution routing is now materially closer to the F90 contract: notifications can drive users back into the same shared task/project surfaces used elsewhere, without introducing a mobile-only deep-link contract.
Tests:
cd android && ./gradlew :app:testProdDebugUnitTest --tests "com.locuno.lumi.android.FieldExecutionRoutingTests"cd ios && xcodebuild test -project Lumi.xcodeproj -scheme Lumi -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -only-testing:UnitTests/WorkspaceDirectoryTests -only-testing:UnitTests/WorkBreadthTests CODE_SIGNING_ALLOWED=NO(host runtime may block on first-boot simulator migration)
2026-06-29 — Mobile route normalization hardened for canonical task notification paths
Type: iOS + Android + Docs
Summary:
- Extended both mobile shell route mappers so field-task notifications/deep links accept the canonical
/work/tasks/<id>shape in addition to the older?task=<id>query-string fallback. - Added unit coverage on both platforms to keep task notifications converged on the same shared task-detail surface regardless of which backend route form arrives.
Reason:
- The shared notification DTO contract already carried
/work/tasks/<id>in real fixtures. Leaving Android/iOS task routing narrower than that contract would create a silent mobile-only detour risk even though project routes already handled the path-style form.
Impact:
- Task notifications now stay as robust as project notifications on both platforms, keeping F90 field-task entry truly converged across Pulse, Brief, notifications, search, and Work Graph.
Tests:
cd android && ./gradlew :app:testProdDebugUnitTest --tests "com.locuno.lumi.android.FieldExecutionRoutingTests"cd ios && xcodebuild test -project Lumi.xcodeproj -scheme Lumi -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -only-testing:UnitTests/WorkBreadthTests CODE_SIGNING_ALLOWED=NO(host runtime may block on first-boot simulator migration)
2026-06-29 — Mobile parity contract and plan added
Type: Documentation Change
Summary:
- Extended F90 to define the iOS/Android parity contract inside the existing F30/F78/F79 architecture.
- Clarified that mobile F90 lives in shared KMP task/project surfaces under
Work Graph, not in a second native-only field workflow. - Added mobile/shared test expectations and a dedicated native implementation plan.
Reason:
- Backend/web F90 is now real, so leaving mobile unspecified would either create a second contract later or encourage ad-hoc native implementations that diverge from the shipped ledger semantics.
Impact:
- Gives the mobile implementation one narrow, explicit path: reuse the existing backend contract and shared KMP architecture until CEO/owner, manager, and employee can all run field work without falling back to web.
Tests:
- Documentation-only in this PR; execution moves to the dedicated mobile parity plan.
2026-06-29 — Task deep-link contract hardened across backend, web, and mobile
Type: Backend + Frontend + Docs
Summary:
- Standardized task-assignment work notifications onto the canonical task detail route shape:
/work/tasks?task=<id>. - Taught
TasksPageto honor the same task deep-link on initial load, open task detail by writing that query param, and clear bothtaskandcommentparams on close. - Added frontend regression coverage for initial deep-link open, click-to-route, and close-to-clear behavior, plus backend assertions on the emitted notification route.
Reason:
- The repo already emitted task comment notifications to
/work/tasks?task=<id>&comment=<id>, while task-assignment notifications could still route to the project page and the web Tasks screen relied on local-only panel state. That split undermined F90’s “one live task surface” rule and left mobile/web parity weaker than the written contract.
Impact:
- Work notifications, comment notifications, web task detail, and the existing mobile route mappers now all converge on the same task-focused route contract instead of depending on entry-point-specific behavior.
Tests:
cd backend && DJANGO_SETTINGS_MODULE=config.settings.test ./.venv/bin/python -m pytest tests/test_work_graph.py -k "TaskAssigneeNotifications" -x --tb=shortcd frontend && pnpm test -- src/test/TasksPage.test.tsxcd frontend && ./node_modules/.bin/tsc --noEmit
2026-06-29 — Shared mobile notification contract now preserves task payload context
Type: KMP + iOS + Android + Docs
Summary:
- Extended the shared mobile notification DTO to keep backend
payload_jsoninstead of silently dropping it during decode. - Added iOS and Android unit coverage proving comment-bearing task notification routes such as
/work/tasks?task=<id>&comment=<id>still resolve to the same shared task-detail surface.
Reason:
- F90 parity depends on notifications being a first-class entry point into the live field task. Dropping backend payload context in KMP or leaving comment-bearing task routes unproven on native shells would keep a subtle backend/mobile contract gap in one of the highest-frequency re-entry paths.
Impact:
- Shared mobile now preserves the full backend notification event shape, and both native shells explicitly prove that task notifications with comment context still land in the same F90 task surface rather than diverging into a second route.
Tests:
cd android && ./gradlew :shared:jvmTest --tests "com.locuno.lumi.shared.api.LumiApiClientTest" --tests "com.locuno.lumi.shared.presentation.notifications.NotificationsScreenModelTest"cd android && ./gradlew :app:testProdDebugUnitTest --tests "com.locuno.lumi.android.FieldExecutionRoutingTests"cd ios && xcodebuild test -project Lumi.xcodeproj -scheme Lumi -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' -only-testing:UnitTests/WorkBreadthTests CODE_SIGNING_ALLOWED=NO