diff --git a/AGENTS.md b/AGENTS.md index 7767650..c5c8e3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,7 +105,7 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude _Phase 0 + Phase 1 built; **box and repo at v0.1.0:90** (v89 + v90 installed & verified live 2026-06-18 — `installed-version` = 0.1.0:90, server up on :8080, no errors; the StartOS version-graph traversal logs an inert down-to-39-then-up-to-90 because the per-version `up`/`down` hooks are no-ops — the real SQLite migrations run in-app at startup). **The fundraising grid + email capture is the canonical system of record** (2026-06-16) — vestigial classic-CRM surfaces get pruned/repurposed. Deploy/feature history lives in git log + `start9/0.4/startos/versions/`; longer-term backlog + debt in `ROADMAP.md` / `EVALUATION.md`._ -- **Email-proposal review over Matrix + a `bot` role — INSTALLED on the box (v0.1.0:89 + provisioning UI v0.1.0:90), 2026-06-18; Feature 1 live, Matrix leg pending 2 manual steps.** Feature 1 (the inline source-email popup) is **live now** — frontend + the existing detail endpoint, no bot needed. v90 adds **`bot` to the Settings → Admin edit-user role dropdown** (the teammate-invite form stays member/admin — provisioning an agent account is an admin re-classification, not an invite), giving a click-path to the role v89 added. **Two steps remain to light up the Matrix leg: (1) flip the bot's CRM user member→`bot` in Settings → Admin; (2) Spark deploy** — `git pull` + add `MATRIX_EMAIL_REVIEW_ROOM=!CImVJWFmzNxPcCcrZl:matrix.gilliam.ai` to the Spark `.env` + `docker compose up -d --build`. **Order matters:** the bot caches its JWT (which embeds the role) at login and only re-logins on a 401, not a 403 — so flip the role BEFORE the bot process starts (or restart it after), else it stays stuck on a stale member token. Two asks on the email-capture "proposed grid notes": (1) **inline source-email popup** — each proposed-note card on the Email Capture page gets a **View email** toggle that lazily fetches the existing **`GET /api/email/detail`** and renders from/to/cc/date/subject + scrollable body inline, so you can judge the note against the email (frontend-only, reuses the Communications detail pattern). (2) **CRM→Matrix review bridge** — the CRM (box, no matrix-nio) can't push, so the **intake bot (Spark) PULLS**: new **`GET /api/intake/email-proposals`** returns three work-lists (`to_post`/`open`/`to_close`); the bot posts a review card (metadata + snippet + draft note) to a **dedicated review room** (`MATRIX_EMAIL_REVIEW_ROOM`), records the thread root (`POST .../{id}/matrix`), and relays in-thread **yes / no / NL-edit** (`POST .../{id}/decide`, NL-revise via local Qwen). **Bidirectional sync:** decide on the web → bot announces + closes the thread; decide on Matrix → the web panel's ~25s poll clears the card. State is CRM-side in **`email_proposal_matrix`** (1:1 side row, email-integration migration **`0003`**, additive + idempotent `CREATE TABLE IF NOT EXISTS` — the email runner re-runs every boot via `executescript`, so no `ALTER`), so it survives a bot restart. New **`bot` role** (authenticated, never admin — `require_bot_or_admin`) gates the email-proposal endpoints; the bot's CRM user must be flipped `member→bot` (one-time, kept out of the invite UI). **Deploy split:** endpoints + migration + role + frontend → **s9pk (v89/v90, installed)**; poll loop + review-room handling → **Spark git pull + restart (pending)**. Tests: `backend/test_email_proposal_matrix.py` + `backend/matrix_intake/test_email_proposals.py`; **30/30 suite green**, render-smoke green, migration verified twice on a copy of `data/crm.db` + applied clean on the box. **Next (see the two manual steps above): flip the bot user → `bot`; then Spark deploy with the review room; then live-smoke the web↔Matrix round-trip** (the popup is already verifiable on the box). Guide: `docs/guides/matrix-intake.md` "Email-activity proposal review". +- **Email-proposal review over Matrix + a `bot` role — DEPLOYED & LIVE 2026-06-18 (box v0.1.0:90; Spark bot at commit `2998706`).** Feature 1 (the inline source-email popup) is live on the box. The Matrix leg is live: bot CRM user flipped to `bot` (via the v90 Settings → Admin dropdown), `MATRIX_EMAIL_REVIEW_ROOM=!CImVJWFmzNxPcCcrZl:matrix.gilliam.ai` set on the Spark `.env`, bot joins the room on startup, polls every 20s. **End-to-end proven on deploy:** the whole pending backlog (N=16) posted as review cards to the room and was marked posted on the box (no errors); the bot now idles (to_post empty) holding 16 open threads. **Deploy gotchas learned:** (a) order matters — the bot caches its JWT (role embedded) at login and only re-logins on 401, not 403, so the role must be `bot` *before* the bot process starts; (b) "invited" ≠ "joined" — the bot must `client.join` the room (added in `2998706`) or `room_send` fails M_FORBIDDEN; (c) on first run the bot posts a card per *pending* proposal — fine here at N=16, but a large backlog would flood + get homeserver-429'd (a `since`-floor on `to_post` is the upgrade path if that ever bites). **Smoke-test still pending (manual):** reply `yes`/`no`/edit **in-thread** on a card → grid note + thread closes; approve one on the web → the bot announces + closes its thread (≤20s); confirm a Matrix decision clears the web card (≤25s). Two asks on the email-capture "proposed grid notes": (1) **inline source-email popup** — each proposed-note card on the Email Capture page gets a **View email** toggle that lazily fetches the existing **`GET /api/email/detail`** and renders from/to/cc/date/subject + scrollable body inline, so you can judge the note against the email (frontend-only, reuses the Communications detail pattern). (2) **CRM→Matrix review bridge** — the CRM (box, no matrix-nio) can't push, so the **intake bot (Spark) PULLS**: new **`GET /api/intake/email-proposals`** returns three work-lists (`to_post`/`open`/`to_close`); the bot posts a review card (metadata + snippet + draft note) to a **dedicated review room** (`MATRIX_EMAIL_REVIEW_ROOM`), records the thread root (`POST .../{id}/matrix`), and relays in-thread **yes / no / NL-edit** (`POST .../{id}/decide`, NL-revise via local Qwen). **Bidirectional sync:** decide on the web → bot announces + closes the thread; decide on Matrix → the web panel's ~25s poll clears the card. State is CRM-side in **`email_proposal_matrix`** (1:1 side row, email-integration migration **`0003`**, additive + idempotent `CREATE TABLE IF NOT EXISTS` — the email runner re-runs every boot via `executescript`, so no `ALTER`), so it survives a bot restart. New **`bot` role** (authenticated, never admin — `require_bot_or_admin`) gates the email-proposal endpoints; the bot's CRM user must be flipped `member→bot` (one-time, kept out of the invite UI). **Deploy split:** endpoints + migration + role + frontend → **s9pk (v89/v90, installed)**; poll loop + review-room handling → **Spark git pull + restart (pending)**. Tests: `backend/test_email_proposal_matrix.py` + `backend/matrix_intake/test_email_proposals.py`; **30/30 suite green**, render-smoke green, migration verified twice on a copy of `data/crm.db` + applied clean on the box. **Next (see the two manual steps above): flip the bot user → `bot`; then Spark deploy with the review room; then live-smoke the web↔Matrix round-trip** (the popup is already verifiable on the box). Guide: `docs/guides/matrix-intake.md` "Email-activity proposal review". - **Adopt the Pipeline — grid drives the deal board — DEPLOYED & live-smoked 2026-06-18 (v0.1.0:88; the full +Pipeline → board → advance-stage → remove round-trip is verified on the box). v88 (frontend-only): retired the Pipeline page's "+ New Opportunity" button + its create-by-contact modal** — opportunities are now born **only** from a grid investor row (matches how the team works; the board is view + stage-management; button replaced with a muted "Add deals from the Fundraising Grid" hint). An **"Add to Pipeline"** row action on the fundraising grid opens a seed modal (primary contact / target fund / expected amount / stage / probability) and creates a durably-linked `opportunities` row via the new **`opportunities.fundraising_investor_id`** (migration 0005, additive + reversible). **Grid owns the link + seed; the board owns stage/probability/owner** — a grid save never reseeds a live opp (`POST /api/fundraising/pipeline/link` is idempotent, one live opp/investor). Contact is **reused from the grid's synced `fundraising_contacts.contact_id`** (the `POST /api/contacts` side-door is gone); grid `lead`→owner. Two **read-only** grid columns (Pipeline action + Pipeline Stage) are **injected on read** from the live opp and **stripped on write** (never persisted, never dirty the autosave). **Remove from pipeline** (`POST .../unlink`) **soft-deletes the opp; the grid row stays fully intact**; deleting an investor from the grid archives its orphaned opp (`reconcile_grid_pipeline_links`, after `sync_fundraising_relational`). **Folded in:** the standing P2 soft-delete leak in `handle_pipeline_report` + dashboard pipeline aggregates (archived opps no longer counted). Tests: `backend/test_grid_pipeline_link.py`; 28/28 suite green, render-smoke green; migration verified on a copy of `data/crm.db` and **applied clean on the box**. **Next: live-smoke on the box — add an investor to the pipeline, confirm it lands on the board, advance a stage, and remove (opp archived, grid row intact).** Detail + locked decisions in `ROADMAP.md` "Adopt the Pipeline".