"ESOP M&A acceleration trigger — Board declares M&A event + 5-business-day Member notice + full vesting acceleration for all active grants"
§1 — Description (BCP-14 normative)
The ESOP service MUST ship M&A acceleration at services/esop/src/ma/ with Board declaration + acceleration cron + 5-business-day member notice, 5 memory audit kinds.
- MUST validate
ma_event_statusagainst closed enum per DEC-2301.
- MUST require board threshold per DEC-2302 (reuse TASK-ESOP-003 board threshold logic).
- MUST run acceleration at
acceleration_runner.rs::accelerate(ma_event)per DEC-2303:
- For all active grants: set vested = total_shares
- Insert special accrual row (kind='ma_acceleration')
- Update grant.status = 'accelerated'
- MUST notify members within 5 business days at
member_notifier.rs::notify(ma_event)per DEC-2300:
- For each affected member: send email via TASK-EMAIL-009
- Track per-member notification_status
- MUST define tables at migration
0006: ```sql CREATE TABLE esop_ma_events ( ma_event_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, event_description TEXT NOT NULL, declared_by UUID NOT NULL, declared_at TIMESTAMPTZ NOT NULL DEFAULT now(), status TEXT NOT NULL DEFAULT 'declared' CHECK (status IN ('declared','accelerating','members_notified','completed','dismissed')), acceleration_started_at TIMESTAMPTZ, all_notified_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, trace_id CHAR(32) ); ALTER TABLE esop_ma_events ENABLE ROW LEVEL SECURITY; CREATE POLICY ma_rls ON esop_ma_events USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON esop_ma_events FROM cyberos_app; GRANT UPDATE (status, acceleration_started_at, all_notified_at, completed_at) ON esop_ma_events TO cyberos_app;
CREATE TABLE esop_ma_board_signs ( sign_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, ma_event_id UUID NOT NULL REFERENCES esop_ma_events(ma_event_id), board_member_id UUID NOT NULL, signed_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (ma_event_id, board_member_id) ); ALTER TABLE esop_ma_board_signs ENABLE ROW LEVEL SECURITY; CREATE POLICY ma_signs_rls ON esop_ma_board_signs USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON esop_ma_board_signs FROM cyberos_app;
CREATE TABLE esop_ma_member_notices ( notice_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, ma_event_id UUID NOT NULL REFERENCES esop_ma_events(ma_event_id), member_id UUID NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent','failed')), sent_at TIMESTAMPTZ, email_message_id UUID, UNIQUE (ma_event_id, member_id) ); ALTER TABLE esop_ma_member_notices ENABLE ROW LEVEL SECURITY; CREATE POLICY notices_rls ON esop_ma_member_notices USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON esop_ma_member_notices FROM cyberos_app; GRANT UPDATE (status, sent_at, email_message_id) ON esop_ma_member_notices TO cyberos_app; ```
- MUST expose endpoints: ``
text POST /v1/esop/ma-events (CEO declares) POST /v1/esop/ma-events/{id}/board-sign (board member) POST /v1/esop/ma-events/{id}/accelerate (auto on threshold) GET /v1/esop/ma-events/{id} (status)``
- MUST emit 5 memory audit kinds per DEC-2304. PII per TASK-MEMORY-111: descriptions SHA256.
- MUST thread trace_id from declare → sign → accelerate → notify → audit.
- MUST NOT accelerate without board threshold per DEC-2302.
- MUST NOT skip member notice per DEC-2300 (5-business-day deadline tracked).
§2 — Why this design
Why board threshold (DEC-2302)? M&A acceleration = major equity event; requires governance.
Why 5-business-day notice (DEC-2300)? Industry standard for change-of-control disclosure; allows members to plan.
Why per-member notice tracking (DEC-2300)? Audit trail proves notification delivered.
§3 — API contract
Sample M&A event:
POST /v1/esop/ma-events
{
"event_description": "Acquisition by AcmeCorp, closing 2026-Q3"
}
Sample status:
{
"ma_event_id": "uuid",
"status": "completed",
"board_signs_count": 3,
"members_notified_count": 30,
"completed_at": "2026-06-01T10:00:00Z"
}
§4 — Acceptance criteria
- ma_event_status enum cardinality 5. 2. Board threshold (≥3 default). 3. All active grants accelerated. 4. vested = total_shares set. 5. 5-business-day notice tracked. 6. Per-member notice via TASK-EMAIL-009. 7. 5 memory audit kinds emitted. 8. PII scrubbed (desc SHA256). 9. RLS denies cross-tenant. 10. CEO-only declare. 11. Board member sign-only. 12. Trace_id preserved. 13. Append-only via REVOKE except status cols. 14. UNIQUE on (ma_event_id, board_member_id). 15. UNIQUE on (ma_event_id, member_id) for notices. 16. status workflow enforced. 17. Per-member notice failure isolated. 18. Cancelled grants excluded. 19. bigint shares. 20. Dismiss allowed pre-acceleration.
§5 — Verification
#[tokio::test]
async fn board_threshold_required() {
let ctx = TestContext::with_declared_ma().await;
ctx.board_sign(ctx.b1).await;
ctx.board_sign(ctx.b2).await;
let m = ctx.fetch_ma(ctx.ma_id).await;
assert_eq!(m.status, "declared"); // still
ctx.board_sign(ctx.b3).await;
let m2 = ctx.fetch_ma(ctx.ma_id).await;
assert_eq!(m2.status, "accelerating");
}
#[tokio::test]
async fn all_grants_accelerated() {
let ctx = TestContext::with_5_active_grants_and_ma_signed().await;
ctx.run_acceleration(ctx.ma_id).await;
for g_id in ctx.grant_ids() {
let g = ctx.fetch_grant(g_id).await;
assert_eq!(g.status, "accelerated");
let a = ctx.fetch_latest_accrual(g_id).await;
assert_eq!(a.vested_cumulative, g.total_shares);
}
}
#[tokio::test]
async fn members_notified_in_5_bd() {
let ctx = TestContext::with_accelerated_ma().await;
ctx.run_notifier(ctx.ma_id).await;
let notices = ctx.fetch_notices(ctx.ma_id).await;
let sent = notices.iter().filter(|n| n.status == "sent").count();
assert_eq!(sent, ctx.member_count());
}
// 5.4..5.10
§7 — Dependencies
Upstream: TASK-ESOP-001. Cross-module: TASK-ESOP-002 (accrual rows), TASK-EMAIL-009 (notice), TASK-MCP-007 (deadline cron), TASK-AUTH-101 (CEO + board), TASK-MEMORY-111 (PII).
§10 — Failure modes
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Board threshold not reached | inherent | stays declared | get signs |
| Acceleration mid-run crash | resume | partial | retry |
| Notice send fail per-member | per-row | isolate; retry | inherent |
| 5-day deadline missed | cron alert | sev-1 + escalate | manual notify |
| Cancelled grant in active list | filter | skip | inherent |
| Cross-tenant declare | RLS | 403 | inherent |
| Duplicate sign | UNIQUE | second skip | inherent |
| Member has no email | sev-2 | skip + manual notify | data fix |
| Concurrent acceleration | UPDATE WHERE | first wins | inherent |
| Dismiss post-acceleration | reject | 409 | inherent |
§11 — Implementation notes
- §11.1 Cron via TASK-MCP-007 for 5-business-day countdown; sev-1 alert if deadline approaches with pending notices.
- §11.2 Acceleration creates TASK-ESOP-002 accrual row with kind='ma_acceleration'.
- §11.3 memory audit body: ma_event_id, status, counts; description SHA256.
- §11.4 Per-member notice via TASK-EMAIL-009; email template includes vested shares + next steps.
- §11.5 Business-day calc: VN public holidays from TASK-HR-005 policy.
End of TASK-ESOP-006 spec.