Two pollers feed one orchestrator. Every PM‑, tenant‑, and vendor‑facing message is staged as a draft; the only human gates are the two amber boxes.
1 · Boot & transport automated
server.mjs wires everything together at startup. No decisions — pure plumbing.
Launches Messages.appwith the injected dylib so it can fire typing dots / read receipts / sends that AppleScript can't reach.
Starts the Drafts UIon :7878 — the cockpit Andrew works out of.
Starts both pollerschat‑poller (inbound iMessage) and issue‑poller (new WOs), plus the scheduled sender and the AppFolio Playwright runner on :9773.
↓
2 · Work‑order intake + enrich automated
triggers/issue-poller.mjs + triggers/enrich-issue.mjs. Polls Supabase issues_v2 every 5s. Sets the WO up before the LLM ever sees it.
Detects the new WOacross mapped workspaces (prod/test); dedups on a cursor so a crash never double‑fires.
Enriches deterministicallyAppFolio reports API → real unit + clean description; a mini‑LLM (gpt‑5.4‑mini) writes a 3–7 word title and makes the urgency call (PMS flag is just one input).
Pulls the vendor roster~36 vendors as the inline candidate list (also the name→id map).
↓
3 · Triage + PM‑ping draft automated (generation)
core/orchestrator.mjs running the process_work_order skill, new_issue phase. One LLM turn (gpt‑5.4) on the enriched issue.
read_memorysurfaces vendor beliefs + per‑property quirks for the trade.
set_vendorwrites the pick back to issues_v2 (powers the dashboard + downstream vendor draft).
send_textproduces the 2‑ or 4‑line summary — "Unit 1 829 Ocean Park / Has a leaky faucet. / Should I send Yonic?" — but in sendMode:'draft', so it lands as a draft, not a text.
Off‑hours holdif outside Mon–Fri 7am–7pm, the draft gets hold_until = next window open (even urgent ones — Andrew can override).
↓
4 · The PM‑ping gate Andrew in the loop
ui/index.mjs Drafts UI. Nothing reaches the PM groupchat without a human tap here.
Andrew reviews / edits the summarythen Send, Copy, Dismiss, or Schedule. Edits are diffed and logged (drives the send‑without‑edit KPI).
On Sendthe dylib texts the PM groupchat. A scheduled/held draft Andrew approved auto‑fires at the next work‑hours open — automated, but only after his approval.
triggers/chat-poller.mjs polls chat.db every 1s; runs the incoming_user_message phase. Key fact: this whole path is sendMode:'draft' — even a one‑word "got it" ack is staged, never auto‑texted.
Read receipt fires instantlythe moment the message is observed, before any LLM work.
Decides intentdispatch approval / vendor swap / clarifying question / status‑update (silent) / learning‑only — gated by how many open WOs are in the recent‑sends list.
Drafts the dispatchon an approval: ack + draft_tenant + draft_vendor → three draft rows, all for human review.
Learnswrite_memory records the decision (see stage 7). Independent of dispatch.
↓
6 · Tenant / vendor dispatch Andrew in the loop
Drafts UI again. AppFolio has no write API, so the actual send is human‑driven — assisted by the Playwright runner.
Andrew reviews the tenant + vendor draftstemplated bodies still carry a [phone] placeholder he fills in.
Sends into AppFolioeither copies by hand, or uses the streamed "Send via AppFolio" panel (Playwright drives the WO Texts widget) and clicks Send/Save himself.
↺
7 · The learning loop automated (background)
Runs fire‑and‑forget off the same PM‑reply turn. Closes the loop back into stage 3's read_memory.
write_memory → observationevery PM decision (confirm, override, preference, reason) is logged as cheap evidence.
belief‑former consolidatesasync per observation — promotes repeated signals into durable beliefs with confidence.
sessionizerpersists the transcript and groups it into conversational sessions via an LLM boundary judge.
Optional overrideAndrew can hand‑edit or delete beliefs in the Memory tab — not required for the loop to run.
8 · Proactive follow‑up not built
CLAUDE.md describes the agent "keeping tabs on open work orders, following up in a few days." There is no follow‑up / stale‑WO poller in the code today.
No nudge mechanismthe loop is reactive — it acts on new WOs and PM replies, but nothing re‑surfaces a WO that's gone quiet. This is the obvious next build.
The invariant that defines the product today
The agent drafts; the human sends. Every PM-, tenant-, and vendor-facing message is staged for review.
send_text hard‑refuses a live send to a PM handle, and both pollers run sendMode:'draft'. The only things that touch the wire unattended are read receipts, typing dots, and approved scheduled sends. So the agent is fully autonomous at perceiving, enriching, deciding, and drafting — and gated at the two moments a message would leave the building.
Capability
Status
Where
Detect + enrich new WO
automated
issue-poller, enrich-issue
Pick vendor + draft PM summary
automated
orchestrator · new_issue
Send PM the summary
human tap
Drafts UI
Read + interpret PM reply
automated
chat-poller · incoming_user_message
Ack + draft tenant/vendor msgs
automated
draft_tenant / draft_vendor
Send tenant/vendor into AppFolio
human tap
Drafts UI + Playwright runner
Record decisions → beliefs
automated
write_memory → belief‑former
Follow up on stale WOs
not built
—
Note: the incoming_anon_message → demo skill path is the one live‑send surface (1:1 chats with unknown numbers, for demos). It's a separate surface from the production work‑order loop above and is excluded here.