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

9.3 KiB

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

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

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

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

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

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.