Files
syndarix/docs/adrs/ADR-014-client-approval-flow.md
Felipe Cardoso f138417486 fix: Resolve ADR/Requirements inconsistencies from comprehensive review
## ADR Compliance Section Fixes

- ADR-007: Fixed invalid NFR-501 and TC-002 references
  - NFR-501 → NFR-402 (Fault tolerance)
  - TC-002 → Core Principle (self-hostability)

- ADR-008: Fixed invalid NFR-501 reference
  - Added TC-006 (pgvector extension)

- ADR-011: Fixed invalid FR-201-205 and NFR-201 references
  - Now correctly references FR-401-404 (Issue Tracking series)

- ADR-012: Fixed invalid FR-401, FR-402, NFR-302 references
  - Now references new FR-800 series (Cost & Budget Management)

- ADR-014: Fixed invalid FR-601-605 and FR-102 references
  - Now correctly references FR-203 (Autonomy Level Configuration)

## ADR-007 Model Identifier Fix

- Changed "claude-sonnet-4-20250514" to "claude-3-5-sonnet-latest"
- Matches documented primary model (Claude 3.5 Sonnet)

## New Requirements Added

- FR-801: Real-time cost tracking
- FR-802: Budget configuration (soft/hard limits)
- FR-803: Budget alerts
- FR-804: Cost analytics

This resolves all HIGH priority inconsistencies identified by the
4-agent parallel review of ADRs against requirements and architecture.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 14:13:26 +01:00

282 lines
9.3 KiB
Markdown

# ADR-014: Client Approval Flow Architecture
**Status:** Accepted
**Date:** 2025-12-29
**Deciders:** Architecture Team
**Related Spikes:** SPIKE-012
---
## Context
Syndarix supports configurable autonomy levels. Depending on the level, agents may require client approval before proceeding with certain actions. We need a flexible approval system that:
- Respects autonomy level configuration
- Provides clear approval UX
- Handles timeouts gracefully
- Supports mobile-friendly approvals
## Decision Drivers
- **Configurability:** Per-project autonomy settings
- **Usability:** Easy approve/reject with context
- **Reliability:** Approvals must not be lost
- **Flexibility:** Support batch and individual approvals
- **Responsiveness:** Real-time notifications
## Decision
**Implement checkpoint-based approval system** with:
- Queue-based approval management
- Confidence-aware routing
- Multi-channel notifications (SSE, email, mobile push)
- Configurable timeout and escalation policies
## Implementation
### Autonomy Levels
| Level | Description | Approval Required |
|-------|-------------|-------------------|
| **FULL_CONTROL** | Approve every significant action | All actions |
| **MILESTONE** | Approve at sprint boundaries | Sprint start/end, major decisions |
| **AUTONOMOUS** | Only critical decisions | Budget, production, architecture |
### Approval Categories
```python
class ApprovalCategory(str, Enum):
CRITICAL = "critical" # Always require approval
MILESTONE = "milestone" # MILESTONE and FULL_CONTROL
ROUTINE = "routine" # FULL_CONTROL only
UNCERTAINTY = "uncertainty" # Low confidence decisions
EXPERTISE = "expertise" # Agent requests human input
```
### Approval Matrix
| Action | FULL_CONTROL | MILESTONE | AUTONOMOUS |
|--------|--------------|-----------|------------|
| Requirements approval | Required | Required | Required |
| Architecture decisions | Required | Required | Required |
| Sprint start | Required | Required | Auto |
| Story implementation | Required | Auto | Auto |
| PR merge | Required | Auto | Auto |
| Sprint completion | Required | Required | Auto |
| Budget threshold exceeded | Required | Required | Required |
| Production deployment | Required | Required | Required |
### Database Schema
```sql
CREATE TABLE approval_requests (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
-- What needs approval
category VARCHAR(50) NOT NULL,
action_type VARCHAR(100) NOT NULL,
title VARCHAR(500) NOT NULL,
description TEXT,
context JSONB NOT NULL,
-- Who requested
requested_by_agent_id UUID,
requested_at TIMESTAMPTZ NOT NULL,
-- Status
status VARCHAR(50) DEFAULT 'pending', -- pending, approved, rejected, expired
decided_by_user_id UUID,
decided_at TIMESTAMPTZ,
decision_comment TEXT,
-- Timeout handling
expires_at TIMESTAMPTZ,
escalation_policy JSONB,
-- AI context
confidence_score FLOAT,
ai_recommendation VARCHAR(50),
reasoning TEXT
);
```
### Approval Service
```python
class ApprovalService:
async def request_approval(
self,
project_id: str,
action_type: str,
category: ApprovalCategory,
context: dict,
requested_by: str,
confidence: float | None = None,
ai_recommendation: str | None = None
) -> ApprovalRequest:
"""Create an approval request and notify stakeholders."""
project = await self.get_project(project_id)
# Check if approval needed based on autonomy level
if not self._needs_approval(project.autonomy_level, category):
return ApprovalRequest(status="auto_approved")
# Create request
request = ApprovalRequest(
project_id=project_id,
category=category,
action_type=action_type,
context=context,
requested_by_agent_id=requested_by,
confidence_score=confidence,
ai_recommendation=ai_recommendation,
expires_at=datetime.utcnow() + self._get_timeout(category)
)
await self.db.add(request)
# Send notifications
await self._notify_approvers(project, request)
return request
async def await_decision(
self,
request_id: str,
timeout: timedelta = timedelta(hours=24)
) -> ApprovalDecision:
"""Wait for approval decision (used in workflows)."""
deadline = datetime.utcnow() + timeout
while datetime.utcnow() < deadline:
request = await self.get_request(request_id)
if request.status == "approved":
return ApprovalDecision.APPROVED
elif request.status == "rejected":
return ApprovalDecision.REJECTED
elif request.status == "expired":
return await self._handle_expiration(request)
await asyncio.sleep(5)
return await self._handle_timeout(request)
async def _handle_timeout(self, request: ApprovalRequest) -> ApprovalDecision:
"""Handle approval timeout based on escalation policy."""
policy = request.escalation_policy or {"action": "block"}
if policy["action"] == "auto_approve":
request.status = "auto_approved"
return ApprovalDecision.APPROVED
elif policy["action"] == "escalate":
await self._escalate(request, policy["escalate_to"])
return await self.await_decision(request.id, timedelta(hours=24))
else: # block
request.status = "expired"
return ApprovalDecision.BLOCKED
```
### Notification Channels
```python
class ApprovalNotifier:
async def notify(self, project: Project, request: ApprovalRequest):
# SSE for real-time dashboard
await self.event_bus.publish(f"project:{project.id}", {
"type": "approval_required",
"request_id": str(request.id),
"title": request.title,
"category": request.category
})
# Email for async notification
await self.email_service.send_approval_request(
to=project.owner.email,
request=request
)
# Mobile push if configured
if project.push_enabled:
await self.push_service.send(
user_id=project.owner_id,
title="Approval Required",
body=request.title,
data={"request_id": str(request.id)}
)
```
### Batch Approval UI
For FULL_CONTROL mode with many routine approvals:
```
┌─────────────────────────────────────────────────────────┐
│ APPROVAL QUEUE (12 pending) │
├─────────────────────────────────────────────────────────┤
│ ☑ PR #45: Add user authentication [ROUTINE] 2h ago │
│ ☑ PR #46: Fix login validation [ROUTINE] 2h ago │
│ ☑ PR #47: Update dependencies [ROUTINE] 1h ago │
│ ☐ Sprint 4 Start [MILESTONE] 30m │
│ ☐ Production Deploy v1.2 [CRITICAL] 15m │
├─────────────────────────────────────────────────────────┤
│ [Approve Selected (3)] [Reject Selected] [Review All] │
└─────────────────────────────────────────────────────────┘
```
### Decision Context Display
```python
class ApprovalContextBuilder:
def build_context(self, request: ApprovalRequest) -> ApprovalContext:
"""Build rich context for approval decision."""
return ApprovalContext(
summary=request.title,
description=request.description,
# What the AI recommends
ai_recommendation=request.ai_recommendation,
confidence=request.confidence_score,
reasoning=request.reasoning,
# Impact assessment
affected_files=request.context.get("files", []),
estimated_impact=request.context.get("impact", "unknown"),
# Agent info
requesting_agent=self._get_agent_info(request.requested_by_agent_id),
# Quick actions
approve_url=f"/api/approvals/{request.id}/approve",
reject_url=f"/api/approvals/{request.id}/reject"
)
```
## Consequences
### Positive
- Flexible autonomy levels support various client preferences
- Real-time notifications ensure timely responses
- Batch approval reduces friction in FULL_CONTROL mode
- AI confidence routing escalates appropriately
### Negative
- Approval latency can slow autonomous workflows
- Complex state management for pending approvals
### Mitigation
- Encourage MILESTONE mode for efficiency
- Configurable timeouts with auto-approve options
- Mobile notifications for quick responses
## Compliance
This decision aligns with:
- FR-203: Autonomy level configuration
- FR-301-305: Workflow checkpoints (approval gates at workflow boundaries)
- NFR-402: Fault tolerance (approval state persistence)
---
*This ADR establishes the client approval flow architecture for Syndarix.*