Skip to content

feat(monedula): email approval loop + entity-ID matching + parquet-free recipients#174

Open
markmhendrickson wants to merge 1 commit into
mainfrom
feat/monedula-email-approval-loop
Open

feat(monedula): email approval loop + entity-ID matching + parquet-free recipients#174
markmhendrickson wants to merge 1 commit into
mainfrom
feat/monedula-email-approval-loop

Conversation

@markmhendrickson

Copy link
Copy Markdown
Owner

Summary

Rebuilds the Monedula recurring-payment daemon (yoga BTC + therapy Wise) around email approval and stable-id calendar matching, replacing the Telegram approve loop. Surfaced and hardened by a live staged run that reproduced two real defects.

What changed

Channel + cadence

  • Email (via gws gmail) replaces Telegram for preview/approve/confirm. One approval email per newly-ended session; operator replies YES/NO.
  • 15-min poll (StartInterval=900) replaces the 08:00 daily run. Each tick notifies newly-ended sessions and sweeps replies. Lookback default 168h (MONEDULA_LOOKBACK_HOURS) with a MAX_NOTIFY_PER_RUN backstop.
  • Restart-surviving markers (markers.py) key approval state by event+date; reply sweep executes approved payments and rolls due_date (never marks yoga/therapy complete).

Fix A — parquet-free recipient resolution. wise_transfer resolves IBAN/name from the Neotoma contact first (profile.contact_id); contacts.parquet is legacy fallback only. Removes the DATA_DIR dependency that blocked live Wise sends.

Fix B — entity-ID calendar matching. New handler_base.match_events_for_profile matches by recurringEventId / event-id allowlist, falling back to keyword. Adds keyword_match_mode (substring|prefix) so therapy matches titles starting with Therapy/Terapia (any casing/language) and excludes incidental events like "Walk to therapy".

Guardrails preserved

Per-payment approval only (no auto-execute) · attendance gate · idempotency / no double-pay · no memo on yoga BTC · IBAN/BTC validation.

Pre-merge review fixes (found by /code-review + hooks, before this PR)

  • CLAUDE.md operator-config violation: hardcoded operator email literal → env-sourced OPERATOR_EMAIL (swarm convention), tick fails safe if unset.
  • Stuck-payment: a marker written with an empty Gmail thread id would sit awaiting_approval forever → notify now requires both message id AND thread id or writes no marker.
  • Empty-keyword wildcard: matcher drops blank keywords so "" can't match every event.
  • PII: test fixtures used synthetic placeholders (no real IBAN/phone in this public repo); .monedula_pending.json runtime state gitignored.

Tests

44 pass (pytest execution/daemons/monedula/test_monedula.py), including regressions for each review fix.

⚠️ Not in this PR / deploy notes

  • Not deployed, no payment made. Deploying requires reinstalling the plist (launchctl) and re-arming two currently-held payments (yoga + therapy) via fresh approval emails.
  • Deferred non-blocking cleanup: next-session-date logic is duplicated across btc_transfer / wise_transfer / monedula.py — worth a follow-up consolidation.

🤖 Generated with Claude Code

…ee recipients

Rebuild the recurring-payment daemon around email approval and stable-id
matching, replacing the Telegram approve loop.

Channel + cadence:
- Replace Telegram preview/approve/confirm with email (gws gmail). One
  approval email per newly-ended session; operator replies YES/NO.
- 15-min poll (StartInterval=900) replaces the 08:00 daily run. Each tick
  notifies newly-ended sessions and sweeps replies. Lookback default 168h
  (MONEDULA_LOOKBACK_HOURS) with a MAX_NOTIFY_PER_RUN backstop.
- Restart-surviving markers (markers.py) key approval state by event+date;
  reply sweep executes approved payments and rolls due_date (never marks
  yoga/therapy complete).

Recipient resolution (Fix A): wise_transfer resolves IBAN/name from the
Neotoma contact first (profile.contact_id), contacts.parquet legacy-only —
removes the DATA_DIR dependency that blocked live Wise sends.

Calendar matching (Fix B): new handler_base.match_events_for_profile matches
by recurringEventId / event-id allowlist, falling back to keyword. Adds
keyword_match_mode ('substring'|'prefix') so therapy matches titles starting
with Therapy/Terapia (any casing/language) and excludes 'Walk to therapy'.

Guardrails preserved: per-payment approval only (no auto-execute), attendance
gate, idempotency/no double-pay, no memo on yoga BTC, IBAN/BTC validation.

Review fixes (pre-commit /code-review):
- OPERATOR_EMAIL env-sourced (swarm convention), not a hardcoded literal
  (CLAUDE.md operator-config rule); tick fails safe if unset.
- notify requires both message id AND thread id before writing a marker, so a
  thread-less send can't strand a session awaiting_approval forever.
- matcher drops empty/blank keywords so "" can't wildcard-match every event.
- test fixtures use synthetic payee PII (no real IBAN/phone in a public repo).
- gitignore the .monedula_pending.json runtime state.

Also adds a Cicada self-review-before-PR step (agent runs code-review on its
own diff before opening a PR; distinct from Vanellus's independent gate).

44 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

(Claude API error: HTTP 400 — {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CckuJguFh3hWpPuQPKdrK"})

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

(Claude API error: HTTP 400 — {"type":"error","error":{"type":"invalid_request_error","message":"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."},"request_id":"req_011CckuJoVdbJ26aF69A8rpT"})

@ateles-agent

Copy link
Copy Markdown
Collaborator

🤖 Lanius — Ateles swarm, PR gate inheritance

BLOCKED

Gate Status (Legacy Issue — Retroactively Initialized)

Gate Status Owner
pm pending Pavo
ux pending Accipiter
arch pending Bombycilla
impl pending Gryllus
pr_review pending Vanellus
qa pending Phoenicurus
legal not_required

Pre-implementation gates are unsigned. This PR cannot merge until all pre-impl gates (pm, ux, arch) are signed off or waived.

Unsigned pre-impl gates:

  • pm — owned by Pavo
  • ux — owned by Accipiter
  • arch — owned by Bombycilla

Resolution paths:

  1. Normal path: Each gate owner signs off via Neotoma (set gate_status.<gate>signed_off)
  2. Operator override: @markmhendrickson may issue /confirm-gates-clear to waive all unsigned pre-impl gates and proceed to review

Parent issue (legacy): This PR predates the gate pipeline. Gates have been retroactively initialized. Parent issue entity: ent_4267624760892797047b7cb2


📎 Neotoma: github_issue

@markmhendrickson

Copy link
Copy Markdown
Owner Author

/confirm-gates-clear

1 similar comment
@markmhendrickson

Copy link
Copy Markdown
Owner Author

/confirm-gates-clear

@ateles-agent

Copy link
Copy Markdown
Collaborator

🤖 Lanius — Ateles swarm, PR gate inheritance
COMMENT

Gate Inheritance Status

No parent issue found in PR body (closes #N / fixes #N). Issue #174 predates gate pipeline initialization — no gate_status metadata currently exists.

LEGACY-ISSUE PATH: Retroactively initializing gate metadata per ateles#109:

  • Workflow type: feature
  • Gates: pm (Pavo) / ux (Accipiter) / arch (Bombycilla) / impl (Gryllus) / pr_review (Vanellus) / qa (Phoenicurus) / legal (Buteo, if required)
  • Current status: treating as legacy-initiated, gates inherit as clear for review to proceed

Verdict

GATE_INHERITANCE: clear

All pre-impl gates are inherited as clear (legacy retroactive init). PR may proceed to review. Vanellus assigned as reviewer.


📎 Neotoma: ateles/174 issue entity — retroactively initialized (pending backfill)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants