27 KiB
MacSync Canonical Completion Plan — 2026-05-15
Closeout status (end of session)
| Phase | Status |
|---|---|
| 1 — iMessage canonical Swift migration | ⚠️ HAPPY-PATH VERIFIED ONLY. APIClient reads data.items / toHandle; PendingSendMessage shape updated; SyncManager uses SendQueueClient<IMessageSendTransport>. Single-row outbound smokes landed in chat.db at 18:39:57 + 18:46:23 PT. Failure paths, concurrency, idempotency, FDA-blocked inbound, and cancel-mid-flight are UNTESTED — see Test Matrix below. |
| 2 — Per-module send-queue stacks | 🚧 server-side complete this session for iCal + iMail; iNotes + iReminders already had server-side; Swift Senders + SyncManager wiring for non-iMessage modules is Quinn WIP (uncommitted Sender impls per module) |
| 3 — Plum ↔ apricot tree reconcile | ⏳ Quinn decision still pending; declared plum-canonical for @mac-sync Swift work in this session's memory entry |
| 4 — Retire WG bridge | ✅ Bridge retired from runtime. QUINN_USE_MAC_SYNC_SEND=0 removed; LaunchAgent unloaded; sendViaBridge + loadSendBackend removed from quinn-ai/engine/src/shared/mac-sync-client.ts; smoke-test.ts single-path. End-to-end canonical-no-bridge smoke landed in chat.db at 18:46:23. Bridge imessage-send.scpt files remain on disk as reference but the process is dead. |
5 — Delivery verification lockdown (delivery_confirmed column + janitor) |
TODO — gated on nothing now; meaningful Phase-5 enhancement |
| 6 — Single-path TS client for the MCP | TODO — quinn-messenger MCP client.ts already speaks /admin/send-queue/enqueue, just needs version bump |
Test matrix — what's actually verified vs assumed
Two outbound happy-path smokes is not "thoroughly tested." This table is the source of truth for verification coverage.
| Scenario | Status | Evidence / Gap |
|---|---|---|
| Immediate enqueue → chat.db arrival | ✅ verified | MacSync consumer smoke 🤖 id=79039fff drained at 18:39:57 PT; chat.db row at 18:39:57 |
outreach.scheduled_send worker → /admin/send-queue/enqueue → chat.db (via bridge mode) |
✅ verified | end-to-end smoke (bridge mode) 🤖 at 18:42:33 PT (pre-bridge-retire; now historical) |
outreach.scheduled_send worker → /admin/send-queue/enqueue → chat.db (MacSync mode, bridge unloaded) |
✅ verified | canonical complete (no bridge) 🤖 at 18:46:23 PT — three layers consistent within 2s |
Sender.swift osascript failure → row → status='failed' with failure_reason |
⚠️ partial | Smoked with +10000000000. Row reported sent (attempts=1, no error) — Messages.app accepts the send locally and writes a chat.db row, so bridge/MacSyncApp see local success. Apple's iMessage server async-rejects an unreachable number but neither layer surfaces that. Hard-failure detection (osascript exception, network down) untested. Real fix is Phase 5 delivery_confirmed keyed on Apple's async delivery ack, not chat.db row presence. |
outreach.scheduled_send retry behavior on transient failure |
❌ untested | Worker logic supports retries (attempts column), but a transient failure has not been induced this session. |
| Multi-row drain (queue backlog) | ✅ verified | 5-row backlog with shared fire_at: all 5 reached status=sent attempts=1; chat.db received echoes 1/5 → 5/5 in order at 19:57:21-22; zero duplicates. |
| Idempotency on consumer retry | ❌ untested | If MacSyncApp fetches a row, dispatches, crashes before PATCHing result, does the row stay queued and get re-fetched? Does Sender.swift handle the double send? |
cancel_scheduled against pending row before fire_at |
✅ verified | Scheduled SHOULD BE CANCELLED 🚫 with fire_at = now+180s, cancelled by intentHash within 2s. Row → status=cancelled, cancellationReason=smoke-cancel-test. chat.db count of "SHOULD BE CANCELLED" = 0 past fire_at. |
outreach.scheduled_send row stuck pending past fire_at |
❌ untested | If worker is dead/stalled, do callers notice? Phase 5 janitor would catch this. |
| MacSyncApp restart mid-drain | ❌ untested | Boot resilience. If app crashes between fetchPending and markResult, row stays queued and next poll re-fetches. |
| Inbound iMessage sync (chat.db → server) | ❌ blocked | ActivityLog: [error] Full Disk Access required at 18:39:23. Requires user grant in System Settings → Privacy & Security → Full Disk Access. Outbound works without FDA; inbound is dead until granted. |
| Non-iMessage modules (iCal/iMail/iNotes/iReminders) end-to-end | ❌ untested | Server routes verified to exist; per-module Senders + SyncManager wiring is Quinn WIP. No round-trip smoke. |
mcp__quinn-messenger__schedule_message / cancel_scheduled tools from a live Claude session |
❌ untested in-session | MCP server disconnected mid-session; tools were not reloaded after .mcp.json edits. Underlying HTTP endpoints smoke-verified; tool surface from a Claude session unverified. |
Schema rename icloud.* → macsync.* consistency |
⚠️ partial | Per-module tables renamed in entity schemas. Legacy iMessage icloud.send_queue table NOT renamed — intentional retention. SEND_QUEUE_CONTRACTS.md per-module table column refers to icloud.<module>_send_queue; should be macsync.<module>_send_queue. |
| Failed smoke rows from this session linger in DB | ⚠️ known | outreach.scheduled_send has rows in status='failed' (intent_hashes d1a9023d, a60c6326). Excluded from default list_scheduled. Janitor would clean. |
Recommended next testing pass
If "do everything" means "exercise everything before declaring done":
- Failure-path smoke: enqueue to
+10000000000(invalid handle); verifyoutreach.scheduled_sendrow reachesstatus='failed'withlast_errorpopulated; verifyicloud.send_queuerow reachesstatus='failed'withfailure_reason. - Cancel-before-fire smoke:
schedule_messagewithfire_at = now()+5min; immediatelycancel_scheduledby intentHash; wait past fire_at; verify row iscancelledand no chat.db arrival. - Multi-row backlog smoke: enqueue 5 rows simultaneously with distinct intent_hashes; verify all 5 drain in order, all 5 chat.db rows arrive, no double-dispatch.
- FDA grant + inbound smoke: user grants FDA; verify iMessage
Readerconnects to chat.db; verify next inbound message from a contact lands inicloud.messages. - MCP-tool-surface smoke: reload Claude Code so MCP server reconnects; call
mcp__quinn-messenger__schedule_messagewith fire_at = now+60s; verify chat.db arrival. - SEND_QUEUE_CONTRACTS.md fix: update per-module table names from
icloud.*tomacsync.*to match the just-applied schema rename.
Drives @mac-sync to the architecture its docs already describe: every
module a BaseSyncManager<Stats, Error> with a SendQueueClient<Transport>
peer, every send routed through /admin/send-queue/enqueue → /client/<module>/send-queue/pending
→ a module-specific Sender. No WG bridge, no upstream AppleScript callouts,
no per-module ad-hoc processSendQueue().
This handoff is the source of truth for that consolidation. Working notes
in ~/.claude/plans/ are deprecated by this file where they conflict.
North-star architecture
quinn-* MCPs ─► mac-sync /admin/send-queue/enqueue ─► icloud.send_queue
│
┌───────────────────────────────┘
▼ poll /client/<module>/send-queue/pending
MacSyncApp on plum (BaseSyncManager + SendQueueClient<Transport>)
│
▼ module-specific Sender (Messages, Mail, Calendar, Notes, Reminders)
▼
macOS app via AppleScript / ScriptingBridge / EventKit
│
▼ POST /client/<module>/send-queue/:id/result
icloud.send_queue rows → sent / failed
Same shape for scheduled sends: outreach.scheduled_send worker calls
/admin/scheduled-send/enqueue at fire_at, downstream identical.
Single canonical wire format across all modules:
GET /client/<module>/send-queue/pending
→ { "success": true, "data": { "items": [ { "id", "toHandle"|<module-fields>, "body"|"payload", "createdAt" } ] } }
POST /client/<module>/send-queue/:id/result
← { "status": "sent" | "failed", "error": "..." }
Per-module persistence (revised 2026-05-15 after reviewing Quinn's WIP in
src/server/src/app/server.ts):
Each module has a dedicated send-item entity, NOT a shared icloud.send_queue
discriminated by service type. Migrations Quinn is wiring up in his WIP:
| Module | Entity / Table | Migration source |
|---|---|---|
| iMessage | icloud.send_queue (legacy, retained) |
@/entities/send-queue |
| iCal | calendarSendItem (new) |
@/entities/calendarSendItem |
| iMail | mailSendItem (new) |
@/entities/mailSendItem |
| iNotes | noteSendItem (new) |
@/entities/noteSendItem |
| iReminders | reminderSendItem (new) |
@/entities/reminderSendItem |
Plus net-new read-side entities (note, reminder) for inbound sync from
Notes.app / Reminders.app, and a prospect entity (likely for the outreach
side that the auto-respond engine writes into).
The iMessage table is the only one keeping the older icloud.send_queue
schema — that's a deliberate legacy retention so the working quinn-messenger MCP + outreach.scheduled_send worker paths don't break during the
migration. New modules get the cleaner per-module shape from the start.
In-flight Quinn WIP (uncommitted on plum @mac-sync, 2026-05-15)
Per git status and git diff review:
src/server/src/app/server.ts— imports for new entity migrations (calendarSendItem,mailSendItem,noteSendItem,reminderSendItem,note,reminder,prospect) + wired intorunMigrationscall.@packages/ical/Sources/ICalSync/APIClient.swift—Sendableconformance +getPendingSends/reportSendResultimplementations hitting/client/ical/send-queue/pendingwith the canonicaldata.itemsenvelope and aPendingCalendarSendpayload struct.@packages/ical/Sources/ICalSync/Reader.swift— modified (likely EventKit-side reader changes; not yet inspected).@packages/imail/Sources/IMailSync/APIClient.swift— mirror of ICal's shape (Sendable+getPendingSends/reportSendResult).@packages/shared/Sources/MacSyncShared/WebServer/LocalWebServer.swift— modified (not yet inspected).Package.swift,app.manifest.yaml,CLAUDE.md— module additions and metadata.
Do not edit any of these files from a Claude Code session. They are
mid-refactor; collisions cost real work
(feedback-check-git-status-before-editing).
Current state (2026-05-15, end of messenger-pilot session)
| Layer | Status |
|---|---|
mac-sync /admin/scheduled-send/{enqueue,list,cancel} |
✅ deployed to black, smoke-verified |
quinn-messenger MCP v0.3.0 (5 tools, HTTP-only) |
✅ deployed plum + apricot, stdio-verified |
outreach.scheduled_send worker dispatching |
✅ enqueues to /admin/send-queue/enqueue when QUINN_USE_MAC_SYNC_SEND=1; currently forced to bridge mode (=0) pending consumer completion |
mac-sync /admin/send-queue/enqueue server side |
✅ writes icloud.send_queue rows |
mac-sync /client/imessage/send-queue/{pending,result} server side |
✅ returns data.items shape, contract doc in src/server/SEND_QUEUE_CONTRACTS.md |
mac-sync /client/ical/send-queue/* server side |
❓ WIP — Swift client side references it; verify endpoints exist |
mac-sync /client/imail/send-queue/* server side |
❓ WIP — same |
| IMessageSync Swift consumer | ❌ legacy SyncManager.processSendQueue() in place; reads wrong wire format (data.messages/phoneNumber); SendQueueAdapter.swift exists but not wired in |
| ICalSync Swift consumer | 🚧 WIP (uncommitted: getPendingSends/reportSendResult impl + Sendable protocol additions) |
| IMailSync Swift consumer | 🚧 WIP (same shape) |
| INotesSync, IRemindersSync Swift consumers | ❓ plum-only modules; status unknown |
iMessage bridge (sh.lilith.quinn-imessage-bridge, plum:8766) |
⚠️ functional (with restored /tmp/imessage-send.scpt), used by worker in fallback mode; planned for retirement |
| MacSyncApp deployed binary | ❌ overwritten by unsigned debug build during this session — FDA grant invalidated; launchctl unload-ed; awaits ./deploy/install.sh rebuild |
@mac-sync plum/apricot tree divergence |
❌ unresolved — plum has inotes/ireminders/SendQueueAdapter; apricot has contacts-sync-core/ContactsWriter/BlobSyncManager.swift |
Phases
Phase 1 — Finish the iMessage canonical migration (BLOCKING)
Owner: Quinn (Swift work; do NOT edit from Claude Code sessions per feedback-check-git-status-before-editing).
- Rewrite
IMessageSync/SyncManager.swiftto match the pattern its own docs describe (docs/modules/imessage.md):final class SyncManager: BaseSyncManager<SyncStats, SyncError>with a peersendQueueClient: SendQueueClient<IMessageSendTransport>owned alongside the inbound sync timer. DeleteprocessSendQueue()(currently lines 475–504). - Fix
IMessageSync/APIClient.swiftwire format to matchsrc/server/SEND_QUEUE_CONTRACTS.md:json["data"]["messages"]→json["data"]["items"]phoneNumber→toHandle- Drop
requestedBy(not returned by server) - Update
PendingSendMessagestruct accordingly
- Wire
SendQueueAdapter+IMessageSendTransportinto MacSyncApp so the client polls + dispatches throughSender.SendService. - Run
./deploy/install.shto rebuild + codesign with stable "Quinn Norton" identity (TCC FDA grant persists across identical signatures). Verify withlaunchctl list | grep mac-sync+ a log tail showing the iMessage sync timer is back on.
Phase 2 — Land iCal + iMail (+ iNotes + iReminders) send-queue stacks
Owner: Quinn (WIP) + Claude session work this turn.
Status by sub-step:
| # | Step | Status |
|---|---|---|
| 1 | Entity + migration per module (src/server/src/entities/{calendar,mail,note,reminder}SendItem/) |
✅ committed (createSendQueueRepo factory + per-module Zod schemas in place) |
| 2 | Wire migrations into src/server/src/app/server.ts runMigrations |
🚧 WIP (Quinn) — git diff src/server/src/app/server.ts shows imports + array appends |
| 3 | Per-module client surface routes /client/<module>/send-queue/{pending,:id/result} |
✅ this session: iCal + iMail added in surfaces/client/{ical,imail}.ts mirroring ireminders.ts template; iNotes + iReminders already had them |
| 4 | Per-module admin enqueue routes /admin/<module>-send-queue/enqueue |
🚧 WIP (Quinn) — surfaces/admin/{calendar,mail,note,reminder}-send-queue.ts exist; mount in admin/index.ts is in git diff |
| 5 | Swift APIClient getPendingSends/reportSendResult per module |
🚧 WIP (Quinn) — iCal + iMail uncommitted; iNotes + iReminders status unknown |
| 6 | Swift SendQueueAdapter + module-specific Sender |
partial — adapters present, senders per-module status unknown |
| 7 | Wire SendQueueClient<<Module>SendTransport> into each module's SyncManager |
TODO (Quinn) |
| 8 | Extend SEND_QUEUE_CONTRACTS.md with per-module schema + route table |
✅ this session: per-module table + endpoints + payload schemas documented |
Phase 3 — Reconcile plum ↔ apricot @mac-sync trees
Owner: Quinn (decision required).
Divergence map:
| Path | Plum | Apricot |
|---|---|---|
@packages/contacts-sync-core/ |
❌ | ✅ |
src/client/ContactsWriter.swift |
❌ | ✅ |
src/client/ContactsBackup.swift |
❌ | ✅ |
src/client/ContactRenderPoller.swift |
❌ | ✅ |
src/client/ContactRenderModels.swift |
❌ | ✅ |
@packages/imessage/Sources/IMessageSync/BlobSyncManager.swift |
❌ | ✅ |
@packages/inotes/ (INotesSync) |
✅ | ❌ |
@packages/ireminders/ (IRemindersSync) |
✅ | ❌ |
@packages/imessage/Sources/IMessageSync/SendQueueAdapter.swift |
✅ | ❌ |
- Pick a canonical authoring host for
@mac-syncand write it into@mac-sync/CLAUDE.md. Recommendation: plum (where Xcode lives, where deployed MacSyncApp runs, where FDA is granted). Server-side work can still flow apricot → black via the existingdeploy-server.shfrom either host. - Port plum-only modules to apricot (or vice versa per the chosen
direction) via
git diffreview + cherry-pick. - Add an explicit scope clarifier to the global memory
lilith-platform-live-apricot-is-sole-authoring-surface: the "apricot-only" rule applies to~/Code/@projects/@lilith/lilith-platform.live/only.~/Code/@applications/*follows per-repo rules. (Done infeedback_source_of_truth_apricot.md.) - Commit the chosen canonical tree to origin, hard-reset the other host to match.
Phase 4 — Retire the WG bridge
Owner: Quinn / future Claude session post-phase-1-verification.
- Flip worker dispatch path: edit
/etc/quinn-ai-engine/secrets.envon apricot, removeQUINN_USE_MAC_SYNC_SEND=0(or set to1). Restartquinn-ai-auto-respond.service. - Smoke
outreach.scheduled_sendend-to-end: enqueue → worker fires →/admin/send-queue/enqueue→ MacSyncApp consumes →chat.dbrow appears. - Unload bridge LaunchAgent on plum:
launchctl unload ~/Library/LaunchAgents/sh.lilith.quinn-imessage-bridge.plist. Mark~/Applications/quinn-imessage-bridge/archived. Leave/Users/natalie/Applications/quinn-imessage-bridge/imessage-send.scptin place — harmless and useful as a documented Messages.app dispatch reference. - Remove
sendViaBridgefromcodebase/@features/quinn-ai/engine/src/shared/mac-sync-client.ts(lilith-platform.live, apricot-authored). MacSyncClient becomes single-path. Rebuild + redeploy engine. - Delete
MAC_SYNC_TOKEN-vs-QUINN_BRIDGE_URLenv duality fromloadSendBackend(); consolidate aroundMAC_SYNC_BASE_URL+MAC_SYNC_TOKENonly.
Phase 5 — Lock down delivery verification
Owner: Quinn / future session.
- Make
chat.dbrow arrival the gating signal inscheduled-send-worker's "sent" status, not justenqueue200-response. The MacSyncApp consumer already PATCHesstatus='sent'withsent_aton actual dispatch — the worker should wait for that PATCH (pollicloud.send_queue.status) rather than markingoutreach.scheduled_send.status='sent'on enqueue ack. - Surface a
delivery_confirmedboolean in theoutreach.scheduled_sendschema so callers can distinguish "MacSync queued the row" from "Messages.app dispatched and Apple ack'd". - Add a periodic janitor job that fails any
icloud.send_queuerow stuck inqueuedfor > 5 minutes (consumer offline / FDA revoked).
Phase 6 — Single-path TS client for the MCP
Owner: future session.
Once Phase 4 is verified stable, refactor
codebase/@features/quinn-messenger/mcp/src/client.ts to drop any
mention of "via bridge" — single path /admin/send-queue/enqueue for
immediate, /admin/scheduled-send/enqueue for delayed. Bump MCP to
v1.0.0 (out of pre-release).
Phase 7 — Consolidate MacSync clients across lilith-platform.live
Status by sub-step (updated 2026-05-15):
| # | Step | Status |
|---|---|---|
| 7a | Create @lilith/mac-sync-client package |
✅ scaffolded at ~/Code/@packages/@ts/@quinn/mac-sync-client on apricot; published 0.1.0 to Verdaccio. Class API: MacSyncClient(baseUrl, token) + listDevices/pickActiveDevice/enqueueSend/scheduleSend/listScheduled/cancelScheduled/getMessagesByHandle. |
| 7b | quinn-api adoption | ✅ codebase/@features/api/src/shared/mac-sync/send.ts rewritten as thin facade over MacSyncClient. Functional API names preserved so call sites in surfaces/m/messages.ts + processors/outreach-dispatcher/ need no changes. @lilith/mac-sync-client@0.1.0 added as dep. quinn-api typechecks clean. Runtime smoke deferred — quinn-api not currently running on apricot. |
| 7c | quinn-ai engine adoption | ⏳ TODO. codebase/@features/quinn-ai/engine/src/shared/mac-sync-client.ts has the same logic; should be replaced with export { MacSyncClient } from '@lilith/mac-sync-client' (or full delete + update call sites). Risk: engine is the live canonical send path; refactor requires rebuild + redeploy + re-smoke. |
| 7d | quinn-messenger MCP review | ✅ no migration needed — Quinn refactored the MCP to route through quinn-api /m/messages/send (not directly to mac-sync). The MCP's client.ts is a quinn-api consumer, separate layer. |
| 7e | Delete quinn-api entities/icloud-message/ |
⏳ TODO. Direct pg reads to icloud.messages (now macsync.messages). Replace call sites with MacSyncClient.searchMessages() / getMessagesByHandle() once mac-sync server exposes the search/full-text endpoints (not yet present). |
| 7f | Move mac-sync-status ownership to mac-sync server |
⏳ TODO. Move schema + /my/mac-sync routes to mac-sync server. Migrate the single-row table data. Drop the migration from quinn-api. |
Owner: future session.
Right now three TS files implement the same MacSync HTTP contract:
| Caller | Path | What it duplicates |
|---|---|---|
quinn-api outreach-dispatcher + /m/messages/send |
codebase/@features/api/src/shared/mac-sync/send.ts |
listDevices / pickActiveDevice / enqueueViaMacSync |
| quinn-messenger MCP | codebase/@features/quinn-messenger/mcp/src/client.ts |
Same shape, slightly different name |
| scheduled-send-worker | codebase/@features/quinn-ai/engine/src/shared/mac-sync-client.ts |
Same shape, plus getMessagesByHandle |
Plus two wrong-direction redundancies inside quinn-api that duplicate work mac-sync server already does:
codebase/@features/api/src/entities/icloud-message/— direct pg reads against mac-sync'sicloud.messages(nowmacsync.messages). Repo header explicitly states ranking semantics are "ported from mac-sync'sentities/message/repo.ts" — i.e. forked logic that must stay in sync manually.codebase/@features/api/src/entities/mac-sync-status/+surfaces/my/mac-sync.ts— hosts the ContactRenderPoller heartbeat table inside quinn-api's DB. The heartbeat belongs to mac-sync; quinn-api should be reading it via mac-sync's/my/*surface, not owning the table.
Target shape:
- New shared package
@lilith/mac-sync-client(TS) at~/Code/@packages/@ts/@lilith/mac-sync-client/exposing:MacSyncClientclass with constructor(baseUrl, serviceToken)and methods:listDevices,pickActiveDevice,enqueueSend,scheduleSend,listScheduled,cancelScheduled,getMessagesByHandle,searchMessages,getStatus.- Single shared types module mirroring mac-sync server's response
shapes (
PendingItem,ScheduledRow,IcloudMessage, etc.).
- Adopt in three callers (delete the local copies):
- quinn-api:
codebase/@features/api/src/shared/mac-sync/send.ts→import { MacSyncClient } from '@lilith/mac-sync-client'. - quinn-messenger MCP:
client.tsbecomes a thin re-export. - quinn-ai engine:
mac-sync-client.tsbecomes a thin re-export.
- quinn-api:
- Replace pg-direct in quinn-api with HTTP:
- Delete
codebase/@features/api/src/entities/icloud-message/(repo, schema, types, embed, index). Replace call sites withMacSyncClient.searchMessages()/getMessagesByHandle(). - Drop the
QUINN_ICLOUD_DB_URLenv from quinn-api config, dropopenIcloudDb/getIcloudDb. quinn-api stops being a second reader of mac-sync's pg.
- Delete
- Move ContactRenderPoller heartbeat ownership to mac-sync:
- Move
entities/mac-sync-status/schema into mac-sync server (newsrc/server/src/entities/contact-render-status/). - Move
/my/mac-syncsurface routes to mac-sync server. - Migrate the existing row (single-row table) from quinn-api's DB to mac-sync's. Drop the mac-sync-status migration from quinn-api.
- quinn-api UI continues to read via the existing UI path — but the
request now proxies through
/my/mac-syncon the mac-sync server viaMacSyncClient.getStatus().
- Move
- Single source of truth invariant: any pg table whose primary
writer is MacSyncApp (devices, messages, conversations, contacts,
send_queue, contact-render-status, all
macsync.*tables) lives in mac-sync server's migrations. quinn-api never opens a second connection to those tables — it consumes via HTTP only.
Verification checklist:
find codebase/@features/api -name '*.ts' | xargs grep -l 'pg.*icloud\|icloud_db\|getIcloudDb'returns nothing.find codebase/@features/api -name '*.ts' | xargs grep -lE 'listMacSyncDevices|pickActiveMacSyncDevice|enqueueViaMacSync'returns nothing (only the package import remains).@lilith/mac-sync-clientis the only TS implementation of the contract; the three previous local copies are deleted (or are one-line re-exports for transition).- mac-sync server's migrations are the only place
macsync.*tables are defined. - One round-trip smoke through each caller (quinn-api outreach-dispatcher, quinn-messenger MCP, scheduled-send-worker) verified post-refactor.
Hard rules going forward
git statusbefore editing any@applications/@mac-syncSwift source. WIP collisions cost real work — see~/.claude/projects/.../memory/feedback_check_git_status_for_wip.md.- Never
cpa Swift build artifact directly into the MacSync.app bundle. TCC revokes FDA silently. Always./deploy/install.shso the stable "Quinn Norton" codesign is applied. - Server contract
SEND_QUEUE_CONTRACTS.mdis the canonical wire format. Swift clients are out of spec if they don't readdata.items/toHandle. Server is not changed to match Swift; Swift is brought up to match server. - Bridge / osascript are explicitly transitional. No new code
should reference
10.9.0.3:8766,imessage-send.scpt, orsendViaBridge. Phase 4 retires them. - MacSync is the only iMessage send path once Phase 1 lands.
quinn-messenger MCPandoutreach.scheduled_sendalready converge on/admin/send-queue/enqueue— keep that invariant.
Verification checklist (per phase)
Each phase is complete only when:
- All affected Swift targets compile (
swift build). - All affected TS targets typecheck (
bunx tsc --noEmit). - One smoke message round-trips end-to-end and lands in
chat.dbon plum (not juststatus='sent'in any queue table). - Logs show no
osascript failed, nobridge_unreachable, nogetPendingSends 401, nodata.messagesJSON path miss. outreach.heartbeatforscheduled-send-workershows ticks within the last 10s.outreach.scheduled_sendhas no rows stuck inpendingpast theirfire_atfor >60s.
References
docs/architecture.md— system overview, invariants.docs/adding-a-module.md— canonical module pattern this plan brings every existing module to.docs/modules/imessage.md— describes the target SyncManager shape.src/server/SEND_QUEUE_CONTRACTS.md— canonical wire format.~/.claude/projects/.../memory/project_macsync_send_consumer_gap.md~/.claude/projects/.../memory/project_imessage_bridge_scpt.md~/.claude/projects/.../memory/feedback_check_git_status_for_wip.md~/.claude/projects/.../memory/feedback_verify_delivery_first.md