Registered Office — full connected module
Status: Delivery spec for the complete ROF product. Not code. Does not replace frozen D1-5-ROF.
Date: 14 October 2026
Sidebar name: Registered Office
Frozen module code: ROF
Harness workflow: ROF.HANDLE_MAIL (already stubbed in @breezycorp/agent-runtime)
RBAC product (today): CORRESPONDENCE — same product, older name. New screens say Registered Office.
Sole system: the monorepo. aima-mvp is reference only. No further feature work lands in AIMA.
This document is the connected build: AIMA’s working mail pipeline + Helpdesk tickets + the frozen ROF loop (draft → human release → send to client → store → deadline flag). It is one module with two outbound loops, not two products.
1. What the full module is
The firm is the registered office for client companies. Physical and agency mail arrives here. Staff must:
- Take the scan in.
- Split a multi-client PDF into letters.
- Know which company and which account manager.
- Tell the mailroom the scan was processed (and give ticket numbers).
- Open a Helpdesk ticket so Secretarial / Accounting can do the real work.
- Draft a client email (“we received this at your registered office”).
- Let the account manager release that email (nothing goes to the client without that click).
- Store the scan and the notification on the company.
- If the letter implies a statutory date, flag a deadline. Do not file at ACRA or reply to IRAS — that is Secretarial or Tax.
ROF routes and notifies. It does not execute the letter.
2. Three pieces we already have — connect them
| Piece | What it is today | Role in the connected module |
|---|---|---|
AIMA / @breezycorp/correspondence | Working OCR, split, classify, extract, match scoring | The read engine. Do not rewrite. |
| Helpdesk | Chatwoot inbox → ticket; Mailboxes settings; reply job | The front door (which inbox) and the staff work queue (tickets). |
ROF.HANDLE_MAIL | Stub workflow: extract → resolve → draft → rules → tier-3 release → apply (send, store, flag deadline) | The client-notify spine. Replace mock extract with AIMA output. |
Do not add a fourth Gmail poller. Do not keep AIMA as a second app.
3. Binding decisions
| # | Decision |
|---|---|
| D1 | No code in aima-mvp. |
| D2 | No correspondence.gmail-poll. Chatwoot is the mailbox. |
| D3 | The fork is a mailbox setting, not a subject keyword. |
| D4 | No Helpdesk ticket on arrival for that inbox. |
| D5 | One Helpdesk ticket per extracted letter, after match (or manual match). |
| D6 | Reply on the scanner Chatwoot thread when the run has ticketed what it can. |
| D7 | Match Entity (UEN / name), then HelpdeskCustomer. No live Odoo JSON-RPC. |
| D8 | Reuse AIMA logic/ + ocr/. Rewrite only persistence and wiring. |
| D9 | Sidebar label is Registered Office (/dashboard/rof). Helpdesk stays Helpdesk. They link both ways. |
| D10 | Client email is a tier-3 ROF.RELEASE_NOTIFICATION. Never auto-send to the client. |
| D11 | Deadlines are flags only. ROF does not file or compute the full statutory calendar. |
| D12 | One scan email = one Run. One logical letter = one Item. One item = one ticket + one release (when ticketable). |
| D13 | Storage keys are server-owned. Product segment on disk: registered-office. |
4. End-to-end (full module)
A later email on the same scanner conversation, while the run is still open, belongs to that run. After the run is DONE, a new scanner email on that thread starts a new run linked to the previous one.
5. Inbox setting
Where: Helpdesk → Settings → Mailboxes.
Switch (default off):
Treat as registered-office mail
This inbox does not open a ticket on arrival. Mail goes to Registered Office. Tickets and the client draft happen after processing.
Field name: registeredOfficeIntake (boolean, default false).
UI copy may say “Registered-office mail”. Do not name the column unprocessed.
Rules — mailbox
| ID | Rule | Severity |
|---|---|---|
| R-MB-01 | New mailboxes default off. Fail-safe: ordinary mail still becomes tickets. | Block |
| R-MB-02 | More than one mailbox may be on (mailroom + backup). | — |
| R-MB-03 | Turning the flag on does not rewrite historical tickets. | — |
| R-MB-04 | Turning the flag off does not delete open runs. New mail uses the ticket path; open runs finish. | — |
| R-MB-05 | Whitelist and junk filter are not applied on an RO inbox. The sender is the scanner. | Block |
| R-MB-06 | Out-of-office auto-reply is off on an RO inbox. The only automated mail on that thread is the done-reply. | Block |
| R-MB-07 | Done-reply and client notify from a ticket need a Chatwoot bot token. Without it, process and tickets still run; send is failed + retryable. | Warn |
| R-MB-08 | Subject keywords are not the fork. ROSMAIL / ROSDOC / Physical Mail Scan are parsed inside the run for MRID/DRID only. | — |
6. Why the ticket is late
| Helpdesk assumes | Registered-office mail |
|---|---|
| Sender is the client | Sender is the scanner |
| One email, one company | One PDF, many addressees |
| Match from From/To/Cc | Match from OCR UEN / legal name |
A ticket on arrival is an Unassigned row with no company and a 12-page PDF. The Run is the process. The ticket is an output.
7. Objects
All new/changed tables: tenant_id, RLS, fail-closed. Do not revive parked CorrespondenceDocument / GmailIntakeQueue.
Prefer names RofRun / RofItem (module code ROF). If the first migration already says Correspondence*, keep the table names and use ROF in the UI — do not dual-write.
7.1 RofRun
One inbound scanner email.
| Field | Meaning |
|---|---|
| mailbox, tenant | The flagged inbox |
| chatwoot conversation + message ids | Dedupe and scanner reply |
| subject, from, RFC Message-ID | Display + MRID parse |
| MRID | From subject or minted ROSMAIL + date + sequence |
| source File | Original PDF, SHA-256 |
| previousRunId | If this is a follow-up scan on a DONE thread |
| status | §8 |
| counts | items, review, matched, ticketed, released |
| scannerReplyMessageId | The done-reply, when sent |
Idempotency: (tenantId, chatwootMessageId) unique.
Duplicate scan: same SHA-256 on the same mailbox within a short window → do not start a second run; attach a note on the existing one.
7.2 RofItem
One logical letter after split.
| Field | Meaning |
|---|---|
| run, index, page range | Split identity |
| DRID | Minted per item |
| label, confidence, method, rationale | Classification |
| name, UEN, document type, document date, sender name/address | Extract |
| impliedDeadlineType, impliedDueDate | From extract / rules |
| split File | Sliced PDF |
| matchStatus | pending / matched / ambiguous / no_match / error / manual |
| entityId, helpdeskCustomerId | Platform + ticket company |
| matchScore, matchMethod | uen / name_exact / name_fuzzy / manual |
| ticketId | One item, one ticket |
| agentRunId | The ROF.HANDLE_MAIL run for this item |
| draft subject/body | Client notification |
| releaseStatus | none / drafted / awaiting_release / released / skipped |
| deferredAt, closedAt, ticketedAt, releasedAt | AIMA-style parks |
A run with no usable attachment still exists (REVIEW, zero items). Mail is never silent-dropped.
7.3 Reused rows
| Row | Use |
|---|---|
File | Original + each split + stored notification artefact |
Entity | Client master |
HelpdeskCustomer / HelpdeskTicket | Staff work |
AgentRun / ProposedAction / ReviewTask | ROF.HANDLE_MAIL + tier-3 release |
Document (ROF_INBOUND, ROF_NOTIFICATION) | Already written by rof-apply |
StatutoryDeadline | Flag only, via upsertStatutoryDeadline |
AuditEvent / recordEvent | Lifecycle |
7.4 Hidden scanner thread
The done-reply must not create a staff-visible Unassigned ticket.
| Preference | How |
|---|---|
| First | Send on Chatwoot conversation + mailbox bot token without a ticket |
| Fallback | Hidden system ticket per run, excluded from every Helpdesk queue |
8. Status machines
8.1 Run
| ID | Rule |
|---|---|
| R-RN-01 | INTAKE → PROCESSING only after the source File exists. |
| R-RN-02 | PROCESSING → REVIEW if any non-deferred, non-closed item needs review (§12). |
| R-RN-03 | PROCESSING → READY only if every remaining item is MATCHED or MANUAL_MATCH, or the operator chose Continue with only deferred/closed leftovers. |
| R-RN-04 | Ticket create before scanner reply so the email can cite HD-…. |
| R-RN-05 | Scanner-reply failure does not roll back tickets. Stay REPLYING, retry. |
| R-RN-06 | DONE is terminal for this scan. Un-deferring later creates a follow-up ticket, not a second done-reply. |
| R-RN-07 | Client release can still be in flight while the run is ITEMS_OPEN. The run is not DONE until every ticketable item is released or skipped. |
| R-RN-08 | FAILED is retryable. Do not ticket from FAILED. |
ITEMS_OPEN means: mailroom loop finished; client loop may still be on Release.
8.2 Item
| ID | Rule |
|---|---|
| R-IT-01 | Ticket only from MATCHED or MANUAL_MATCH. Never from AMBIGUOUS / NO_MATCH / ERROR. |
| R-IT-02 | DEFERRED does not block READY if the operator Continues. No ticket until un-deferred and matched. |
| R-IT-03 | CLOSED = junk / blank verso / duplicate. No ticket, no client email. |
| R-IT-04 | One item → at most one ticket (ticketId unique). |
| R-IT-05 | One item → at most one ROF.HANDLE_MAIL AgentRun. |
| R-IT-06 | SKIPPED notify still keeps the ticket. The letter was received; staff may still act. |
8.3 Helpdesk ticket
Normal helpdesk machine (new_mail → … → closed). ROF does not own it after create.
9. Mapping onto ROF.HANDLE_MAIL
The stub workflow is already:
ingestMail → extract → resolveEntity → draftNotification → evalRules → release (proposes ROF.RELEASE_NOTIFICATION)
Connect it like this. Do not invent a second harness.
| Stub step | Connected implementation |
|---|---|
| 1 ingestMail | helpdesk.ingest + stored File. Channel is Chatwoot, not EMAIL_M365. |
| 2 extract | AIMA OCR + classify + extract. Not MockMailExtractionAdapter in production. |
| 3 resolveEntity | AIMA match scoring against Entity (§11). |
| 4 draftNotification | Existing draftRofNotification (Claude) + templates per letter type. |
| 5 evalRules | Existing SG_ROF_PACK (§13). |
| 6 release | Existing tier-3 proposed action + ReviewTask. |
| apply | Existing rof-apply: send, ROF_INBOUND + ROF_NOTIFICATION Documents, upsertStatutoryDeadline. Send goes through Helpdesk/Chatwoot to the client contact, not the scanner. |
Granularity: the stub assumed one AgentRun per scan. The connected module starts one AgentRun per RofItem once that item is ticketed (entity is known). The RofRun is the mailroom container; the AgentRun is the client-notify loop.
10. Read pipeline (AIMA — do not rewrite)
| Step | Keep from | Change |
|---|---|---|
| Bytes | helpdesk.attachment | Chatwoot URL, not Gmail download |
| Store + hash | @breezycorp/storage | _inbox/… then file under entity registered-office/documents/… |
| MRID/DRID | mail-id.ts, mail-subject.ts | Keep |
| OCR | Vision async GCS + pdf-parse | Keep; do not force sync Vision adapter |
| Split | barcode + Vision + Claude pages | Keep |
| Classify | document-classify.ts | Keep labels |
| Extract rows | ocr-clients-pipeline.ts | Inject Claude as today |
| Review predicates | ocr-clients-review.ts | Rename odoo_match_* → match_* |
| Score | odoo-match-helpers.ts | Candidates = Entity rows |
| Odoo JSON-RPC | — | Dead on the live path |
| Gmail dispatch | — | Scanner = helpdesk.reply. Client = rof-apply after Release |
Un-quarantine packages/correspondence logic/ + ocr/. Point RofAdapters.extraction at that pipeline.
11. Classification rules
Labels (unchanged from AIMA):
IRAS · ACRA · MOM · BANK_FINANCIAL · LEGAL · UTILITY_PROPERTY · GENERAL · UNKNOWN
| ID | Rule |
|---|---|
| R-CL-01 | Rules run first. Each label has weighted phrase patterns (IRAS full name, GST+tax, ACRA full name, MOM, bank/statement, legal/ECT, utility/property, etc.). |
| R-CL-02 | Winner = highest weighted score. UNKNOWN if nothing fires or scores tie with no clear winner. |
| R-CL-03 | If rules confidence is below the LLM threshold (default 90) and classification LLM is on, call Claude. Method becomes rules_then_llm or llm. |
| R-CL-04 | LLM cannot invent a new label. Output must be one of the eight. |
| R-CL-05 | Operator may override the label. Override is audited. Team on the ticket uses the final label. |
| R-CL-06 | Do not rename labels to SOP vocabulary in this build. Mapping tables can come later. |
Letter type → implied deadline
Used by extract + SG.ROF.DEADLINE_FLAGGED.
| Signal on the letter | impliedDeadlineType |
|---|---|
| Annual return / AR reminder / ACRA AR | ANNUAL_RETURN |
| AGM / annual general meeting | AGM |
| ECI / estimated chargeable | ECI |
| Form C / C-S / income tax return | TAX_RETURN |
| GST return / F5 | GST_RETURN |
| Pass expiry / renewal (MOM) | PASS_RENEWAL |
| None of the above | none |
| ID | Rule |
|---|---|
| R-CL-07 | Deadline type is a flag hint, not a computed due date. If OCR has a date, store impliedDueDate; else apply may use “flag as due today” only as a last resort (current stub). Prefer an extracted date. |
| R-CL-08 | ROF never creates a FilingTransaction. |
12. Match rules
Order per item:
normalizeUen→Entity.registrationNumber(tenant, not deleted). One hit →matched, methoduen.- Else score
legalName,formerNames, trading-name metadata (normalizeOrgName,bestNameScore). - Then HelpdeskCustomer for the ticket.
Score bands (AIMA / SOP)
| Band | Score | Outcome |
|---|---|---|
| High | ≥ 92 | matched if a clear winner |
| Mid | 85–91 | ambiguous, except a single mid candidate → matched (live AIMA behaviour) |
| Low | < 85 | no_match |
| Tie | top two within 1 point | ambiguous |
| ID | Rule |
|---|---|
| R-MT-01 | Never match from the scanner From-line. |
| R-MT-02 | No address match in this build. |
| R-MT-03 | No live Odoo res.partner search. |
| R-MT-04 | Soft-deleted entities are invisible. |
| R-MT-05 | Two high-scoring entities → ambiguous, never pick silently. |
| R-MT-06 | On match, resolve HelpdeskCustomer (entity link, else UEN/name/externalRef). If none, create one from the Entity. A matched item must not have a null customer. |
| R-MT-07 | Account manager = Entity.assignedExecutiveId, else group relationship manager. Same person on the ticket’s accountManagerId and on the release ReviewTask. |
| R-MT-08 | Manual match requires a picked Entity. Audited as method manual. |
| R-MT-09 | Contact for client notify: HelpdeskContact preferred RO-mail recipient, else any client contact email, else company email. Missing contact blocks Release (R-RL-03), not ticket create. |
13. Review rules (match quality)
An item needs review when it is not deferred / closed / ticketed, and any of:
| ID | Condition |
|---|---|
| R-RV-01 | Extract or classification confidence < 70 (AIMA) / < 0.7 on the ROF pack warn. |
| R-RV-02 | matchStatus is missing, error, no_match, or ambiguous. |
| R-RV-03 | UEN empty/Null and status is not matched / manual. |
| R-RV-04 | Split PDF error. |
| R-RV-05 | OCR page mismatch or forced single-document fallback (ocr_page_mismatch, multi_invoice_suspected). |
| R-RV-06 | Run has zero items. |
A run stays in REVIEW while any non-deferred, non-closed item needs review.
Operator actions
| Action | Rule |
|---|---|
| Manual match | Clears R-RV-02/03 for that item. |
| Correct label | Audited. Does not clear a match miss by itself. |
| Re-run OCR | Run → PROCESSING. Previous items replaced, not merged blindly. |
| Defer | Park. Continue may still READY the run. |
| Close without ticket | Junk/duplicate. |
| Continue | Allowed only if leftovers are deferred or closed. |
14. Frozen ROF pack (release gate)
Already in packages/rules SG_ROF_PACK v0.1.0. These run on evalRules before Release, not on ticket create.
| ID | Pack rule | Severity | Meaning |
|---|---|---|---|
| R-PK-01 | SG.ROF.ADDRESSEE_RESOLVED | Block | No Entity → cannot Release. |
| R-PK-02 | SG.ROF.EXTRACTION_CONFIDENCE | Warn | Low confidence → closer AM attention on the Release screen (highlight), not a silent send. |
| R-PK-03 | SG.ROF.DEADLINE_FLAGGED | Block | If impliedDeadlineType is set, a deadline flag must be prepared (apply upserts it). Cannot Release “and forget” a statutory hint. |
ROF does not execute Secretarial rules (SG.SEC.*). Those belong to the ticket’s later work.
15. Helpdesk ticket rules
Created when the run hits TICKETING, one per ticketable item.
| Ticket field | Value |
|---|---|
| Subject | [<LABEL>] <type or name> — <legalName> |
| Description | UEN, pages, date, match method, link to /dashboard/rof/runs/:id |
| Customer | HelpdeskCustomer from R-MT-06 |
| Account manager | R-MT-07 |
| Team | §15.1 |
| Status | new_mail |
| Priority | Unset |
| Mailbox | The RO mailbox (so a later client send can go out) |
| Chatwoot conversation | Not the scanner thread |
| Attachment | Split PDF only |
| Source | REGISTERED_OFFICE + rofItemId so Helpdesk can link back |
15.1 Team from final label
| Label | Team |
|---|---|
| IRAS, BANK_FINANCIAL | accounting |
| ACRA, LEGAL, MOM | secretarial |
| UTILITY_PROPERTY, GENERAL, UNKNOWN | miscellaneous |
| ID | Rule |
|---|---|
| R-TK-01 | Missing/deactivated team → mailbox defaultTeamId → Unassigned. Never drop the ticket. |
| R-TK-02 | Scanner email is never a ticket. |
| R-TK-03 | Zero-item runs never ticket. |
| R-TK-04 | Helpdesk list may filter source=REGISTERED_OFFICE. |
| R-TK-05 | Ticket detail shows “From Registered Office” + deep link to the item. |
| R-TK-06 | Helpdesk SLA starts when the ticket is created, not when the scan arrived. |
| R-TK-07 | Closing the ticket does not Release the client email, and Release does not close the ticket. Two jobs. |
16. Scanner done-reply rules
Audience: mailroom. Not the client.
| ID | Rule |
|---|---|
| R-SR-01 | One email per run, after tickets exist. |
| R-SR-02 | Body: MRID; each ticketed item (label, client, UEN, pages, HD-…); each deferred/closed/unmatched line so the scan was seen. |
| R-SR-03 | No statutory advice. No “dear director”. |
| R-SR-04 | Idempotent on scannerReplyMessageId. |
| R-SR-05 | Failure → retry. Do not recreate tickets. |
| R-SR-06 | Not visible as an Unassigned ticket (R-§7.4). |
17. Client notification and Release (full ROF)
This is FR-ROF-004 / 005 / 006. The human gate is Release, not “create ticket”.
17.1 Draft
Started per item after ticket create (ROF.HANDLE_MAIL steps 4–6).
Templates (defaults; tenant can replace later):
| Letter | Subject pattern | Body intent |
|---|---|---|
| IRAS / tax / GST / ECI | Mail received for {legalName} — IRAS | We received {type} from IRAS at your registered office on {date}. Your accounting team has it (ticket {HD}). |
| ACRA / AR / AGM | Mail received for {legalName} — ACRA | Same, secretarial team. |
| MOM | Mail received for {legalName} — MOM | Same. |
| Bank / utility / general | Mail received for | Neutral receipt. |
| Implied deadline | Same + one sentence | This letter appears to relate to {deadlineType}. Your team will confirm dates. |
Claude (draftRofNotification, rof.draft.v1) may write 2–4 plain sentences. It cannot change the Entity, invent a deadline type, or mark the letter Released.
| ID | Rule |
|---|---|
| R-DR-01 | Draft language follows the entity’s client language when set; else English. |
| R-DR-02 | Draft is a proposal. Sending is Release only. |
| R-DR-03 | Draft failure falls back to the template. The item still reaches AWAITING_RELEASE. |
17.2 Release screen
Account manager (or ROF reviewer) sees: split PDF, classification, Entity, draft subject/body (editable), implied deadline, Helpdesk ticket link.
Actions: Release · Edit and release · Skip notify · Send back to review (wrong company).
| ID | Rule |
|---|---|
| R-RL-01 | No client send without Release. Tier 3. ROF.RELEASE_NOTIFICATION. |
| R-RL-02 | Release blocked if Entity unresolved (R-PK-01). |
| R-RL-03 | Release blocked if there is no client recipient email. Operator must add a contact or Skip. |
| R-RL-04 | Release blocked if implied deadline is set and not flagged for upsert (R-PK-03). The apply path always flags; the UI must show the flag. |
| R-RL-05 | Low confidence (R-PK-02) highlights the screen; AM can still Release. |
| R-RL-06 | Skip notify is explicit and audited. Ticket remains. |
| R-RL-07 | Apply is idempotent: existing ROF_NOTIFICATION Document for that AgentRun → no second send (rof-apply already). |
| R-RL-08 | Send uses the Helpdesk/Chatwoot path to the client contact. New thread, not the scanner thread. |
| R-RL-09 | After send: ROF_INBOUND + ROF_NOTIFICATION Documents on the Entity; notification row; ENGAGEMENT_SIGNAL. |
| R-RL-10 | ROF never files with a regulator (no tier 4 here). |
18. Deadline flag rules
| ID | Rule |
|---|---|
| R-DL-01 | Only when impliedDeadlineType is set. |
| R-DL-02 | Writer is upsertStatutoryDeadline (shared). Types: ANNUAL_RETURN, AGM, ECI, TAX_RETURN, GST_RETURN, PASS_RENEWAL. |
| R-DL-03 | Due date = extracted date if present; otherwise do not invent a statutory calendar — store a flag the AM can edit on Release. |
| R-DL-04 | Flag is not “Secretarial has filed”. Status stays UPCOMING until that module meets it. |
| R-DL-05 | Cannot Release an implied-deadline letter without the flag (R-PK-03). |
| R-DL-06 | Ticket description includes the deadline type so the working team sees it. |
19. Sidebar and screens
Registered Office is a Module, next to Payroll / Bookkeeping / Helpdesk. Not a child of Helpdesk.
Modules
Payroll
Bookkeeping
Financials
Registered Office
Inbox /dashboard/rof
Needs review /dashboard/rof/review
Release /dashboard/rof/release
Helpdesk
Dashboard / tickets / teams
Settings ← mailbox switch lives hereHarness nav already declares { path: '/dashboard/rof', label: 'Registered office mail' }. Use that.
| Screen | Who | What |
|---|---|---|
| Inbox | ROF operator | Runs: status, MRID, item counts, aging, ticket counts, release counts |
| Run detail | ROF operator | Original PDF, item cards, rematch/defer/close, ticket + release links, scanner-reply preview |
| Needs review | ROF operator | Only items that fail §13 |
| Release | Account manager / ROF reviewer | Scan + draft; Release / Skip |
| Company documents | Anyone with entity access | ROF_INBOUND + ROF_NOTIFICATION on the company |
| Helpdesk ticket | Helpdesk roles | Work the letter; link back to the item |
| ID | Rule |
|---|---|
| R-UI-01 | CORRESPONDENCE_OPERATOR / ROF_OPERATOR see Inbox + Review. |
| R-UI-02 | Release additionally needs ROF.RELEASE_NOTIFICATION / reviewer / AM on that entity. |
| R-UI-03 | Helpdesk settings (mailbox flag) stay on Helpdesk admin. ROF Inbox may show a read-only “these inboxes are RO”. |
| R-UI-04 | Do not show scan runs inside Helpdesk → All tickets. |
| R-UI-05 | Aging: minutes since received on Review; hours since drafted on Release. |
Staff-only upload (Chatwoot down) is allowed on Inbox after slice 1. No public AIMA-style upload page.
20. Storage and tenancy
| ID | Rule |
|---|---|
| R-ST-01 | RLS on every new table. No tenant context → zero rows. |
| R-ST-02 | Original scan: spade/_inbox/yyyy/mm/dd/<id>.pdf — no client filename. |
| R-ST-03 | After match: file split PDF under …/<entity>/registered-office/documents/<period>/…. |
| R-ST-04 | Keys never appear in request or response. Signed download only. |
| R-ST-05 | SHA-256 on ingest. |
21. Jobs and API (conceptual)
| Job | Does |
|---|---|
helpdesk.ingest | If flag: create RofRun, no ticket. Else: today. |
helpdesk.attachment | Fetch bytes into File. |
rof.process-run | AIMA pipeline on the run. |
rof.complete-mailroom | Tickets + scanner reply; start ROF.HANDLE_MAIL per item. |
agent.run-start / resume | Existing harness for draft + release. |
helpdesk.reply | Scanner done-reply; later, client send from apply if that is the transport. |
Staff API under /ops/rof (list/detail/review actions) plus existing /ops/rof/mail-intake, /ops/helpdesk/admin/mailboxes, and the shared review-inbox approve for ROF.RELEASE_NOTIFICATION.
Do not revive archived /ops/correspondence against old models.
22. Frozen FR mapping
| Requirement | Connected module |
|---|---|
| FR-ROF-001 ingest, run per item | Chatwoot RO inbox; RofRun per email; RofItem per letter |
| FR-ROF-002 extract sender, entity, type | AIMA classify + extract |
| FR-ROF-003 resolve entity + AM | §12 |
| FR-ROF-004 draft client notification | §17.1 |
| FR-ROF-005 human release, tier 3 | §17.2 |
| FR-ROF-006 send and record delivery | rof-apply + Chatwoot to client |
| FR-ROF-007 store original + notification | ROF_INBOUND / ROF_NOTIFICATION |
| FR-ROF-008 queue + aging | Inbox / Review / Release |
| FR-ROF-009 deadline flag, do not execute | §18 |
| FR-ROF-010 events | RUN_LIFECYCLE, STEP_EXECUTED, HUMAN_DECISION, ENGAGEMENT_SIGNAL |
D1-5-ROF open item “mail source”: decided — Chatwoot inbox with registeredOfficeIntake. M365 is not this build.
Open item “templates”: default table in §17.1; tenant override later.
Helpdesk tickets are how ROF “may create a TICKET” (D1-5-ROF §6). In this build they are always created for ticketable items, because that is the firm’s work queue.
23. What stays out of the app
Physical MI0–MI4, envelope stamping, MIE tray, Gmail labels, consolidating many letters into one client email per window, SOP D0–D7 names, address match, AIMA public upload, live Odoo match, a second Gmail poller.
24. Delivery slices
Each slice is shippable. Do not skip 1.
| Slice | Ships | Prove |
|---|---|---|
| 1 Fork | Mailbox flag. Run, no ticket. | RO inbox → no HD. Support inbox → HD. |
| 2 Pipeline | AIMA OCR/split/classify/extract/match. | Fixture → N items, UEN match. |
| 3 Inbox + Review | /dashboard/rof + rematch. | Unmatched item fixed by hand. |
| 4 Tickets + scanner reply | HD per item, done-reply. | Two clients → two tickets, one scanner email. |
| 5 Draft + Release | ROF.HANDLE_MAIL for real extract; Release screen; rof-apply. | No client email without Release. Notification stored on Entity. |
| 6 Deadlines | Flag + R-PK-03. | AR letter cannot Release without a deadline row. |
Slices 1–4 are the mailroom loop (old “phase 1”). Slices 5–6 complete ROF. The module is not done at slice 4.
25. Acceptance (full module)
- AIMA is not deployed as the product.
- An admin can mark the RO inbox from Helpdesk Settings. Default off.
- That inbox never opens a ticket on arrival. Other inboxes unchanged.
- Multi-client PDF → many items → many tickets, companies set, split PDFs attached.
- Ambiguous / unreadable items stay on Needs review. No silent tickets.
- Scanner thread gets one done-reply citing
HD-…. - Sidebar has Registered Office (Inbox, Needs review, Release) linked to Helpdesk both ways.
- A draft is proposed per ticketed item. No client send without Release.
- Release stores inbound + notification on the Entity.
- Implied statutory letter raises a deadline flag; ROF does not file.
- Low-confidence extract is highlighted on Release.
- Unresolved addressee cannot Release.
- Apply is idempotent (no double send).
- Every transition is audited.
- No Gmail poller, no Supabase, no second Next app.
26. Reference
Frozen: greenfield_prd/Phase-E-Module-Specifications/D1-5-ROF-registered-office.md
Harness (keep, connect): packages/agent-runtime/src/modules/rof.ts, rof-apply.ts, adapters/rof-adapters.ts
Rules: packages/rules/src/packs/sg-rof.ts
AIMA behaviour: aima-mvp/src/lib/document-classify.ts, ocr-clients-pipeline.ts, ocr-clients-review.ts, odoo-match-helpers.ts
Prefer monorepo copies: packages/correspondence/src/logic/, src/ocr/
Helpdesk: packages/helpdesk/src/intake.ts, apps/worker/src/handlers/helpdesk/ingest.ts, Mailboxes tab
Parked first port (do not implement from): correspondence-module.md
27. One-line summary
Registered Office is its own sidebar module. A flagged Helpdesk inbox feeds AIMA’s pipeline. Each letter becomes a Helpdesk ticket and a drafted client email. The mailroom gets a done-reply. The client gets nothing until Release. Deadlines are flagged, not filed. AIMA is the reference. The monorepo is the product.