Skip to content

Conversation

@Aparnap2
Copy link
Owner

@Aparnap2 Aparnap2 commented Jan 15, 2026

Summary by CodeRabbit

  • New Features

    • GitHub Sentinel: automated PR analysis with blocking/warning messages and Slack notifications
    • ExecOps vertical agents and Approval Inbox UI for reviewable action proposals (release, customer, finance, team)
    • Stripe invoice budget checks and CFO budget analysis; human approval workflow with Slack integration
    • Legacy endpoints preserved for compatibility
  • Infrastructure

    • Postgres + pgvector, Neo4j, Redis, and serverless (Lambda/API Gateway) support
    • Observability hooks added
  • Testing

    • Load tests and extensive integration/unit test suites added

✏️ Tip: You can customize this high-level summary in your review settings.

Aparnap2 and others added 4 commits January 11, 2026 11:12
- Add Graphiti temporal memory layer for Neo4j
- Add pgvector semantic memory layer for PostgreSQL
- Create LangGraph agent with PR parsing, memory queries, violation analysis
- Add GitHub webhook endpoint at /api/v1/webhook/github
- Add GitHub API client for PR comments
- Add LangFuse observability integration
- Add AWS SAM template for Lambda deployment
- Add integration tests for PR enforcement flow
- Add Locust load test configuration
- Update docker-compose with Neo4j, pgvector, Redis

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

Co-Authored-By: Claude <noreply@anthropic.com>
Complete TDD implementation for FounderOS AI service:

Phase 1 - Memory Layer:
- Add temporal memory tests (test_temporal_memory.py)
- Add semantic memory tests (test_semantic_memory.py)
- Fix async mock issues and PGVector API integration

Phase 2 - CTO Agent Enhancement:
- Add diff parsing and AST-based code analysis nodes
- Implement SQL injection, secrets, header detection
- Add policy recommendation generation

Phase 3 - CFO Agent & Handoffs:
- Add budget analysis node with AWS pricing constants
- Implement cost estimation for Lambda, EC2, S3, DynamoDB
- Add CTO-to-CFO handoff logic and policy enforcement

Phase 4 - Slack Integration:
- Add Slack message builder for PR notifications
- Implement webhook handler for interactive callbacks
- Create Slack client for sending notifications

Phase 5 - Observability:
- Add LangFuse tracing integration (lazy loaded)
- Implement metrics collection (PR decisions, violations, budget)
- Add structured JSON logging

Fixes:
- StateGraph state propagation in CFO agent
- Lazy imports for optional langfuse dependency
- Mock patterns for testing without langchain installed

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

Co-Authored-By: Claude <noreply@anthropic.com>
@gitguardian
Copy link

gitguardian bot commented Jan 15, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
24487510 Triggered Generic Password c84541f ai-service/src/ai_service/memory/graphiti_client.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds an ExecOps vertical-agent architecture, GitHub Sentinel PR analysis, memory layers (temporal + semantic), Slack/Stripe/GitHub integrations, a human approval workflow, frontend inbox UI, infrastructure (Neo4j/pgvector/Redis), observability, tests, and load-testing scripts across the AI service and frontend.

Changes

Cohort / File(s) Summary
ExecOps Vertical Agents
ai-service/src/ai_service/graphs/vertical_agents.py, ai-service/src/ai_service/graphs/release_hygiene.py, ai-service/src/ai_service/graphs/customer_fire.py, ai-service/src/ai_service/graphs/runway_money.py, ai-service/src/ai_service/graphs/team_pulse.py
New routing and per-vertical graphs, ActionProposalState, shared human_approval_node, and graph factories.
GitHub Sentinel Agent
ai-service/src/ai_service/agent/nodes.py, ai-service/src/ai_service/agent/state.py, ai-service/src/ai_service/agent/__init__.py
LangGraph-based PR intake: parse PRs, fetch/parse diffs, query temporal/semantic memory, analyze violations, budget analysis, recommendations, and CFO handoff.
Approval Workflow
ai-service/src/ai_service/agent/workflow.py
Redis-backed ApprovalState, Slack approval client, HumanApprovalManager, LangGraph human_approval_node, and callback handling.
Supervisor & Guardrails
ai-service/src/ai_service/agent/supervisor.py
Multi-agent supervisor to route events to SRE/CFO/Tech Debt agents, aggregate decisions, and orchestrate approvals.
Tech Debt Agent
ai-service/src/ai_service/agent/tech_debt.py
TODO/deprecated-lib detection, debt scoring, decision thresholds, and agent graph.
Memory Layers
ai-service/src/ai_service/memory/graphiti_client.py, ai-service/src/ai_service/memory/vector_store.py, ai-service/src/ai_service/memory/__init__.py
Temporal memory (Graphiti/Neo4j) for policies and semantic memory (Postgres + pgvector) for similarity search; ingestion and query APIs.
Integrations: GitHub / Slack / Stripe
ai-service/src/ai_service/integrations/github.py, ai-service/src/ai_service/integrations/slack.py, ai-service/src/ai_service/integrations/stripe.py, ai-service/src/ai_service/integrations/webhook.py, ai-service/src/ai_service/integrations/__init__.py
Async GitHub client and webhook handler, Slack message builder/client/handler, Stripe invoice parsing/client/webhook handler and CFO analysis node.
Observability
ai-service/src/ai_service/observability.py
LangFuse tracing wrapper (optional), SentinelMetrics, structured logging, trace_span context manager, and observability orchestration.
Infrastructure / Checkpointer
ai-service/src/ai_service/infrastructure/checkpointer.py, ai-service/src/ai_service/infrastructure/__init__.py
Postgres-backed LangGraph checkpointer utilities, sync/async accessors, manager, and setup helpers.
Main API & Lambda
ai-service/src/ai_service/main.py, ai-service/src/ai_service/lambda_handler.py
New ExecOps endpoints (process_event, proposals list/approve/reject), sentinel routes, legacy compatibility endpoints, and Mangum Lambda handler.
Schemas & Data Models
ai-service/src/ai_service/schemas/action.py, fullstack/prisma/schema.prisma
ActionProposal schemas and event/context models; Prisma schema adds ActionProposal model, event fields, and indexes.
Frontend Inbox & API
fullstack/components/inbox/Inbox.tsx, fullstack/lib/types.ts, fullstack/app/api/actions/route.ts, fullstack/app/api/actions/[id]/route.ts, fullstack/app/api/actions/[id]/reject/route.ts
New inbox UI, types for ActionProposal, and Next.js API routes for listing, creating, approving, and rejecting proposals.
Human approval & Workflow tests
ai-service/src/ai_service/agent/workflow.py, ai-service/tests/integration/test_human_approval.py
Approval lifecycle, Slack interactions, Redis persistence, and comprehensive approval tests.
Load testing
ai-service/loadtest/locustfile.py
New Locust script simulating GitHub webhook events, sentinel/status/health checks, and mixed load.
Docker / infra templates / env
docker-compose.yml, init-scripts/01-pgvector.sql, ai-service/template.yaml, ai-service/.env.example
Docker Compose adds Redis and Neo4j, pgvector image, init script for pgvector; SAM template for Lambda; example env file.
Dependencies & Tooling
ai-service/pyproject.toml
Version bump and new deps: langchain-postgres, graphiti-core, langfuse, psycopg[binary], mangum; dev dep locust; pytest/mypy configs.
Observability & Tests
ai-service/tests/**, fullstack/tests/**, various ai-service/tests/integration and unit files
Large suite of new integration and unit tests covering agents, integrations, memory, observability, and frontend components.
Removed / Deprecated
ai-service/src/ai_service/graphs/sop_graph.py, ai-service/tests/test_sop_graph.py
Legacy SOP graph and its tests removed; legacy endpoints preserved as deprecated for compatibility.
Docs & Summaries
IMPLEMENTATION_SUMMARY.md, exec_ops.md, founderos.md, various CLAUDE.md files, README.md
New architecture docs, implementation summary, PRD rewrite, and contextual CLAUDE notes.

Sequence Diagram(s)

sequenceDiagram
    participant Webhook as External Trigger
    participant EventNorm as Event Normalizer
    participant Router as Vertical Router
    participant Agent as Vertical Agent
    participant Memory as Memory Layer
    participant Approval as Approval Manager
    participant Executor as Action Executor
    participant User as Founder

    Webhook->>EventNorm: send event (GitHub/Stripe/Sentry)
    EventNorm->>Router: normalized event
    Router->>Agent: route to vertical agent
    Agent->>Memory: query temporal & semantic memories
    Memory-->>Agent: policy & context matches
    Agent->>Agent: analyze & draft action
    Agent->>Approval: create proposal & notify
    Approval->>User: notify via Slack/UI
    User->>Approval: approve/reject
    Approval->>Executor: execute if approved
    Executor-->>User: confirm execution
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature/execops pivot #1 — Appears to be the same or directly related change set (shared files and feature pivot: ExecOps vertical agents, GitHub Sentinel, and related integrations).

Poem

🐰
I tunneled through code with nimble paws,
Spun guardrails from logic, not from claws.
Agents now hop, inspect, and advise,
Sending proposals up to founders' eyes.
Hooray — the inbox blooms; approval applause!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Feature/execops pivot' is partially related to the changeset but lacks specificity and clarity about the main change. Consider using a more descriptive title that clearly summarizes the primary change, such as 'Refactor: Pivot from SOP-based to vertical agent architecture with ExecOps' or 'Feature: Implement ExecOps vertical agents and approval inbox system'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 96.95% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.



📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1ff486 and 5c0eccc.

📒 Files selected for processing (1)
  • README.md

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Aparnap2, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request represents a significant architectural pivot, transforming the AI service into a comprehensive ExecOps platform. The focus has shifted to a multi-agent system designed to automate and enforce operational policies, particularly within the software development lifecycle and financial oversight. This change introduces specialized agents, robust memory systems for policy and context, and enhanced monitoring capabilities, all while preparing the service for scalable cloud deployment.

Highlights

  • ExecOps AI Service Introduction: The AI service has been rebranded to 'ExecOps AI Service' and its core functionality has shifted from generic SOP automation to active agent automation for SaaS founders, featuring four vertical agents handling domain-specific workflows with human-in-the-loop approval.
  • GitHub Sentinel for PR Policy Enforcement: A new GitHub Sentinel feature has been implemented, providing automated Pull Request analysis and policy enforcement. This includes parsing PR information, fetching diffs, analyzing code for violations (e.g., SQL injection, hardcoded secrets), and generating recommendations.
  • Multi-Agent Architecture: The system now incorporates a multi-agent architecture with specialized agents, including a CTO agent for code analysis and a CFO agent for budget impact analysis and cost estimation of proposed infrastructure changes.
  • Enhanced Memory and Observability: The service integrates temporal memory using Graphiti (Neo4j) for active policies and semantic memory using pgvector (PostgreSQL) for similar past decisions. Observability has been enhanced with LangFuse integration for tracing, metrics, and structured logging.
  • API Endpoint Refactor and Deprecation: Existing SOP-related API endpoints (/decide, /sops) have been deprecated and replaced with new endpoints (/process_event, /proposals, /proposals/{id}/approve, /proposals/{id}/reject) to align with the new vertical agent and approval workflow.
  • Infrastructure and Deployment Readiness: The project now includes an AWS SAM template (template.yaml) for deployment to AWS Lambda using Mangum, and the docker-compose.yml has been updated to include Neo4j for Graphiti and pgvector for PostgreSQL, preparing the service for a robust production environment.
  • Load Testing Integration: A Locust load testing script has been added to simulate various webhook and API requests, ensuring the new services can handle anticipated traffic.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant new feature, "ExecOps", which includes a GitHub Sentinel agent for PR analysis, temporal and semantic memory capabilities, and integrations with various services. The changes are extensive, adding new agent logic, data models, API endpoints, and infrastructure configurations. While the overall architecture is promising, I've identified several critical issues, including the use of mock implementations for core data queries, bugs in asynchronous client usage, structural problems with module naming, and configuration errors in the deployment template. There are also high-severity issues related to security analysis and hardcoded, potentially incorrect business logic for cost estimation. These critical and high-severity issues must be addressed before this can be considered for production.

Comment on lines +74 to +137
def query_temporal_memory_node(state: AgentState) -> AgentState:
"""Query temporal memory (Neo4j/Graphiti) for active policies.
Args:
state: Current agent state
Returns:
Updated state with temporal policies
"""
from ..memory.graphiti_client import TemporalMemory

# Get PR content for policy search
pr_info = state.get("pr_info", {})
query = f"{pr_info.get('title', '')} {pr_info.get('action', '')}"

# Create mock temporal memory for now
# In production, this would connect to Graphiti
policies: list[PolicyMatch] = []

# Built-in policies based on common patterns
built_in_policies = [
PolicyMatch(
name="no_sql_outside_db",
rule="No direct SQL queries allowed outside db/ folder",
valid_from=datetime(2024, 1, 1),
valid_to=None,
similarity=1.0,
),
PolicyMatch(
name="no_deploy_friday",
rule="No deployments on Fridays",
valid_from=datetime(2024, 1, 1),
valid_to=None,
similarity=0.8,
),
]

policies.extend(built_in_policies)

logger.info(f"Retrieved {len(policies)} temporal policies")
return {**state, "temporal_policies": policies}


def query_semantic_memory_node(state: AgentState) -> AgentState:
"""Query semantic memory (pgvector) for similar past decisions.
Args:
state: Current agent state
Returns:
Updated state with similar contexts
"""
from ..memory.vector_store import SemanticMemory

pr_info = state.get("pr_info", {})
query = pr_info.get("title", "")

# Mock semantic search results for now
# In production, this would search pgvector
similar_contexts = []

logger.info(f"Searched semantic memory for: '{query}'")
return {**state, "similar_contexts": similar_contexts}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The query_temporal_memory_node and query_semantic_memory_node functions are using mock implementations and return hardcoded data, as indicated by the comments "Create mock temporal memory for now" and "Mock semantic search results for now". This is a critical issue as it means the agent's core functionality of querying its knowledge graph and vector store is not implemented, preventing it from making decisions based on real, historical context. These mocks must be replaced with actual implementations that connect to and query Neo4j/Graphiti and PostgreSQL/pgvector.

Comment on lines +251 to +305
class SlackClient:
"""HTTP client for sending Slack messages."""

def __init__(self, webhook_url: str, timeout: float = 10.0) -> None:
"""Initialize Slack client.
Args:
webhook_url: Slack incoming webhook URL
timeout: Request timeout in seconds
"""
self.webhook_url = webhook_url
self.timeout = timeout
self._client: httpx.Client | None = None

def _get_client(self) -> httpx.Client:
"""Get or create HTTP client."""
if self._client is None:
self._client = httpx.Client(timeout=self.timeout)
return self._client

async def send_message(
self,
blocks: list[dict[str, Any]],
text: str,
channel: str | None = None,
) -> bool:
"""Send a message to Slack.
Args:
blocks: Slack block elements
text: Fallback text for notifications
channel: Optional channel override
Returns:
True if message sent successfully
"""
payload: dict[str, Any] = {
"blocks": blocks,
"text": text,
}

if channel:
payload["channel"] = channel

try:
client = self._get_client()
response = await client.post_async(
self.webhook_url, json=payload
)
response.raise_for_status()
logger.info("Slack message sent successfully")
return True
except httpx.HTTPError as e:
logger.error(f"Failed to send Slack message: {e}")
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The SlackClient implementation incorrectly mixes synchronous and asynchronous httpx usage. It is initialized with a synchronous httpx.Client, but the send_message method attempts to make an asynchronous call with await client.post_async(...). This will raise an AttributeError at runtime because httpx.Client has no post_async method and cannot be awaited. The client should be changed to httpx.AsyncClient and the call to await client.post(...).

Comment on lines +191 to +204
async def invalidate_policy(self, name: str, valid_to: datetime) -> bool:
"""Mark a policy as invalid/inactive from a specific time.
Args:
name: Name of the policy to invalidate
valid_to: Time when the policy ends
Returns:
True if policy was found and updated
"""
# Note: Graphiti doesn't have direct update, we add a new episode
# to effectively end the validity of the previous one
logger.info(f"Invalidating policy '{name}' effective from {valid_to}")
return True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The invalidate_policy method is a mock implementation that always returns True and does not perform any action on the database. This is a critical flaw, as it means policies cannot be expired or updated. The temporal aspect of the memory system is incomplete without the ability to properly manage the lifecycle of policies. This method must be implemented to update the policy's validity in the Neo4j graph.

Comment on lines +1 to +437
"""Observability integration for GitHub Sentinel.
This module provides:
- LangFuse tracing integration (optional)
- Metrics collection and reporting
- Structured logging setup
"""

import logging
from contextlib import asynccontextmanager
from dataclasses import dataclass
from datetime import datetime
from typing import Any, AsyncGenerator

from pydantic import BaseModel

logger = logging.getLogger(__name__)

# Lazy imports for optional dependencies
_LangfuseTracer = None
_Langfuse = None


def _get_langfuse_types():
"""Get LangFuse types, importing lazily."""
global _LangfuseTracer, _Langfuse
if _LangfuseTracer is None:
try:
from langfuse.langchain import LangfuseTracer
from langfuse import Langfuse

_LangfuseTracer = LangfuseTracer
_Langfuse = Langfuse
except ImportError:
pass
return _LangfuseTracer, _Langfuse


@dataclass
class SentinelMetrics:
"""Application metrics for monitoring."""

# Counters
prs_processed: int = 0
prs_approved: int = 0
prs_warned: int = 0
prs_blocked: int = 0
violations_found: int = 0
recommendations_generated: int = 0

# Timings (in seconds)
avg_processing_time: float = 0.0
total_processing_time: float = 0.0

# Budget
total_estimated_cost: float = 0.0
budgets_exceeded: int = 0

def to_dict(self) -> dict[str, Any]:
"""Convert metrics to dictionary."""
return {
"prs_processed": self.prs_processed,
"prs_approved": self.prs_approved,
"prs_warned": self.prs_warned,
"prs_blocked": self.prs_blocked,
"violations_found": self.violations_found,
"recommendations_generated": self.recommendations_generated,
"avg_processing_time": self.avg_processing_time,
"total_processing_time": self.total_processing_time,
"total_estimated_cost": self.total_estimated_cost,
"budgets_exceeded": self.budgets_exceeded,
}


class ObservabilityConfig(BaseModel):
"""Configuration for observability features."""

langfuse_public_key: str | None = None
langfuse_secret_key: str | None = None
langfuse_host: str = "https://cloud.langfuse.com"
log_level: str = "INFO"
log_format: str = "json"


class SentinelTracer:
"""LangFuse tracer for GitHub Sentinel.
Provides distributed tracing for PR analysis workflows.
"""

def __init__(
self,
public_key: str | None = None,
secret_key: str | None = None,
host: str = "https://cloud.langfuse.com",
) -> None:
"""Initialize the tracer.
Args:
public_key: Langfuse public key
secret_key: Langfuse secret key
host: Langfuse server URL
"""
self._tracer: Any = None # LangfuseTracer or None
self._langfuse: Any = None # Langfuse or None
self._public_key = public_key
self._secret_key = secret_key
self._host = host

def setup(self) -> bool:
"""Set up the LangFuse tracer.
Returns:
True if tracer was set up successfully
"""
if not self._public_key or not self._secret_key:
logger.debug("LangFuse keys not configured, skipping tracer setup")
return False

try:
LangfuseTracer, Langfuse = _get_langfuse_types()
if Langfuse is None:
logger.warning("Langfuse not installed, skipping tracer setup")
return False

self._langfuse = Langfuse(
public_key=self._public_key,
secret_key=self._secret_key,
host=self._host,
)
self._tracer = LangfuseTracer()
logger.info("LangFuse tracer initialized successfully")
return True
except Exception as e:
logger.error(f"Failed to initialize LangFuse tracer: {e}")
return False

def get_tracer(self) -> Any:
"""Get the LangFuse tracer instance.
Returns:
LangfuseTracer or None if not configured
"""
return self._tracer

def create_generation(
self,
name: str,
input_data: dict[str, Any],
metadata: dict[str, Any] | None = None,
):
"""Create a new generation for tracing.
Args:
name: Name of the generation
input_data: Input data for the generation
metadata: Optional metadata
"""
if not self._langfuse:
return None

return self._langfuse.generation(
name=name,
input=input_data,
metadata=metadata,
)

def flush(self) -> None:
"""Flush pending traces to LangFuse."""
if self._langfuse:
self._langfuse.flush()

def shutdown(self) -> None:
"""Shutdown the tracer and flush pending traces."""
self.flush()
if self._langfuse:
self._langfuse.shutdown()


# Global tracer instance
_tracer_instance: SentinelTracer | None = None


def get_tracer() -> SentinelTracer:
"""Get the global tracer instance."""
global _tracer_instance
if _tracer_instance is None:
_tracer_instance = SentinelTracer()
return _tracer_instance


def setup_tracing(
public_key: str | None = None,
secret_key: str | None = None,
host: str = "https://cloud.langfuse.com",
) -> SentinelTracer:
"""Set up global tracing with LangFuse.
Args:
public_key: Langfuse public key
secret_key: Langfuse secret key
host: Langfuse server URL
Returns:
Configured SentinelTracer instance
"""
global _tracer_instance
_tracer_instance = SentinelTracer(public_key, secret_key, host)
_tracer_instance.setup()
return _tracer_instance


def setup_logging(level: str = "INFO", format: str = "json") -> None:
"""Configure structured logging for the application.
Args:
level: Log level (DEBUG, INFO, WARNING, ERROR)
format: Log format (json, text)
"""
import json

log_level = getattr(logging, level.upper(), logging.INFO)

# Configure root logger
logging.basicConfig(level=log_level)

# Create custom formatter
class StructuredFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
}

# Add exception info if present
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)

# Add extra attributes
if hasattr(record, "trace_id"):
log_data["trace_id"] = record.trace_id
if hasattr(record, "pr_number"):
log_data["pr_number"] = record.pr_number

if format == "json":
return json.dumps(log_data)
else:
return (
f"[{log_data['timestamp']}] {log_data['level']}: "
f"{log_data['message']}"
)

# Apply formatter to handlers
for handler in logging.root.handlers:
handler.setFormatter(StructuredFormatter())


# Metrics tracking
_metrics: SentinelMetrics = SentinelMetrics()


def get_metrics() -> SentinelMetrics:
"""Get the global metrics instance."""
return _metrics


def record_pr_decision(decision: str, processing_time: float) -> None:
"""Record a PR decision for metrics.
Args:
decision: The decision made (approve, warn, block)
processing_time: Time taken to process in seconds
"""
global _metrics

_metrics.prs_processed += 1
_metrics.total_processing_time += processing_time
_metrics.avg_processing_time = (
_metrics.total_processing_time / _metrics.prs_processed
)

if decision == "approve":
_metrics.prs_approved += 1
elif decision == "warn":
_metrics.prs_warned += 1
elif decision == "block":
_metrics.prs_blocked += 1


def record_violations(count: int) -> None:
"""Record violations found.
Args:
count: Number of violations found
"""
global _metrics
_metrics.violations_found += count


def record_recommendations(count: int) -> None:
"""Record recommendations generated.
Args:
count: Number of recommendations generated
"""
global _metrics
_metrics.recommendations_generated += count


def record_budget_impact(estimated_cost: float, exceeds_budget: bool) -> None:
"""Record budget impact.
Args:
estimated_cost: Estimated monthly cost
exceeds_budget: Whether budget was exceeded
"""
global _metrics
_metrics.total_estimated_cost += estimated_cost
if exceeds_budget:
_metrics.budgets_exceeded += 1


@asynccontextmanager
async def trace_span(
name: str,
input_data: dict[str, Any] | None = None,
metadata: dict[str, Any] | None = None,
) -> AsyncGenerator[dict[str, Any], None]:
"""Create a tracing span for an operation.
Args:
name: Name of the span
input_data: Input data for the span
metadata: Optional metadata
Yields:
Span output data
"""
tracer = get_tracer()
generation = None

if tracer and input_data:
generation = tracer.create_generation(name, input_data, metadata)

start_time = datetime.utcnow()
output = {}

try:
yield output
finally:
end_time = datetime.utcnow()
duration = (end_time - start_time).total_seconds()

output["duration_seconds"] = duration
output["completed_at"] = end_time.isoformat()

if generation:
generation.end(output=output)


class SentinelObservability:
"""Main observability class for GitHub Sentinel.
Provides a unified interface for all observability features.
"""

def __init__(
self,
config: ObservabilityConfig | None = None,
) -> None:
"""Initialize observability.
Args:
config: Optional observability configuration
"""
self.config = config or ObservabilityConfig()
self._tracer: SentinelTracer | None = None

def initialize(self) -> None:
"""Initialize all observability components."""
# Setup logging
setup_logging(
level=self.config.log_level,
format=self.config.log_format,
)

# Setup tracing if configured
if self.config.langfuse_public_key and self.config.langfuse_secret_key:
self._tracer = setup_tracing(
public_key=self.config.langfuse_public_key,
secret_key=self.config.langfuse_secret_key,
host=self.config.langfuse_host,
)
logger.info("Observability initialized with LangFuse tracing")
else:
logger.info("Observability initialized without LangFuse (not configured)")

def get_tracer(self) -> SentinelTracer | None:
"""Get the configured tracer."""
return self._tracer

def get_metrics(self) -> SentinelMetrics:
"""Get application metrics."""
return get_metrics()

def flush(self) -> None:
"""Flush all pending traces."""
if self._tracer:
self._tracer.flush()


# Convenience function
def create_observability(
langfuse_public_key: str | None = None,
langfuse_secret_key: str | None = None,
log_level: str = "INFO",
) -> SentinelObservability:
"""Create and initialize observability.
Args:
langfuse_public_key: Langfuse public key
langfuse_secret_key: Langfuse secret key
log_level: Log level
Returns:
Initialized SentinelObservability instance
"""
config = ObservabilityConfig(
langfuse_public_key=langfuse_public_key,
langfuse_secret_key=langfuse_secret_key,
log_level=log_level,
)
observability = SentinelObservability(config)
observability.initialize()
return observability
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This file, observability.py, creates a module with the same name as the observability package (directory) within the same parent package (ai_service). This is a critical structural issue that creates an ambiguous namespace and will lead to unpredictable import behavior or ImportErrors in Python. You should resolve this conflict, for example, by moving the contents of this file into the ai_service/observability package (e.g., in __init__.py) and deleting this file.

Properties:
FunctionName: FounderOS-GitHubSentinel
CodeUri: .
Handler: ai_service.main.handler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The Lambda function handler is specified as ai_service.main.handler. However, the Lambda entry point is defined in ai_service/lambda_handler.py. The main.py file is for local Uvicorn execution and does not contain a Lambda-compatible handler function. This misconfiguration will cause the Lambda deployment to fail at runtime with a "handler not found" error.

      Handler: ai_service.lambda_handler.handler

Comment on lines +532 to +540
def _contains_sql_injection(patch: str) -> bool:
"""Check for SQL injection vulnerabilities."""
# String concatenation in SQL (e.g., "'SELECT * FROM ' + user_id")
if re.search(r"execute\s*\(\s*['\"][^'\"]*['\"]\s*[\+\?]", patch, re.IGNORECASE):
return True
# f-string SQL injection
if re.search(rf"f['\"].*{{\s*.*\s*}}.*['\"]", patch):
return True
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The regex-based detection for SQL injection is not robust and can be easily bypassed. For instance, it doesn't detect vulnerabilities from %-formatting (e.g., "SELECT ... WHERE id = '%s'" % user_id) or more complex f-string injections. Relying on simple regex for security analysis is fragile and can provide a false sense of security. Consider integrating a proper static analysis security testing (SAST) tool or using a more sophisticated library designed for detecting security vulnerabilities in code.

Comment on lines +660 to +690
AWS_PRICING = {
"lambda": {
"per_invoke": 0.0000002, # $0.20 per 1M requests
"per_gb_second": 0.0000166667, # $0.20 per 1M GB-seconds
},
"ec2": {
"t3_micro": 0.0104, # $0.0104 per hour
"t3_small": 0.0208,
"t3_medium": 0.0416,
"t3_large": 0.0832,
},
"s3": {
"storage_per_gb": 0.023, # $0.023 per GB-month
"per_1000_requests": 0.0004, # $0.40 per 1M requests
},
"dynamodb": {
"per_read_unit": 0.00013, # $0.13 per million read units
"per_write_unit": 0.00065, # $0.65 per million write units
},
"rds": {
"t3_micro": 0.017, # $0.017 per hour
"t3_small": 0.034,
"t3_medium": 0.068,
},
"elasticache": {
"t3_micro": 0.014, # $0.014 per hour
},
"redshift": {
"dc2_large": 0.25, # $0.25 per hour
},
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The AWS pricing data is hardcoded in the AWS_PRICING dictionary. Cloud provider pricing changes frequently, and these values will quickly become outdated, leading to inaccurate cost estimations by the CFO agent. This should be refactored to fetch pricing data from a reliable external source, like the AWS Price List API, or at a minimum, be moved to a configuration file that can be updated without requiring a code change and redeployment.

Comment on lines +803 to +807
elif service == "dynamodb":
read_units = usage.get("read_units", 0)
write_units = usage.get("write_units", 0)
cost = (read_units * 26280 * pricing["per_read_unit"]) + \
(write_units * 26280 * pricing["per_write_unit"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The cost calculation for DynamoDB appears to be incorrect due to the use of the magic number 26280. DynamoDB pricing is based on provisioned or on-demand units consumed per second. A monthly cost calculation should involve the number of seconds in a month (approximately 2.6 million), not 26,280. This discrepancy will lead to a significant underestimation of DynamoDB costs. The formula should be reviewed and corrected to reflect the actual pricing model.

Comment on lines +137 to +176
async def search_similar(
self,
query: str,
k: int = 5,
filter_metadata: dict[str, Any] | None = None,
) -> list[ContextMatch]:
"""Search for similar past context.
Args:
query: Search query
k: Number of results
filter_metadata: Optional metadata filters
Returns:
List of matching contexts sorted by similarity
"""
logger.debug(f"Searching for similar context: '{query}'")

search_kwargs = {"k": k}
if filter_metadata:
search_kwargs["filter"] = filter_metadata

docs = await self._vector_store.similarity_search(query, **search_kwargs)

matches: list[ContextMatch] = []
for doc in docs:
match = ContextMatch(
content=doc.page_content,
speaker=doc.metadata.get("speaker", "unknown"),
timestamp=datetime.fromisoformat(
doc.metadata.get("timestamp", datetime.utcnow().isoformat())
),
metadata=doc.metadata,
similarity=0.5, # PGVector doesn't return scores by default
)
matches.append(match)

logger.debug(f"Found {len(matches)} similar contexts")
return matches

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The search_similar method hardcodes the similarity score to 0.5 for all results, which defeats the purpose of a similarity search as the actual relevance of matches is lost. The langchain-postgres library provides a similarity_search_with_score method that returns both the documents and their corresponding similarity scores. You should use this method to retrieve and return the actual scores.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@ai-service/loadtest/locustfile.py`:
- Around line 46-50: The load test POSTs to "/api/v1/webhook/github" using
self.client.post without the X-Hub-Signature-256 header, which causes 401s when
GITHUB_WEBHOOK_SECRET is set; update each webhook call (the self.client.post
invocations) to compute the HMAC-SHA256 of the JSON payload using the configured
secret (from GITHUB_WEBHOOK_SECRET or a test-specific secret) and add the result
as the "X-Hub-Signature-256" header, or alternatively ensure the test
environment unsets GITHUB_WEBHOOK_SECRET so signature validation is disabled for
these requests.

In `@ai-service/src/ai_service/agent/state.py`:
- Around line 33-40: The ContextMatch TypedDict in state.py is missing the
metadata field present on vector_store.ContextMatch; update the ContextMatch
definition to include metadata: dict[str, Any] and ensure you import Any (e.g.,
from typing import Any, TypedDict) so the TypedDict signature matches
vector_store.ContextMatch and prevents type mismatches when real pgvector
results include metadata.
- Around line 23-31: The TypedDict PolicyMatch in state.py uses field name while
graphiti_client.PolicyMatch uses policy_name, causing mismatches; update
state.PolicyMatch to use policy_name: str (and remove or deprecate name) and
update any local mocks in nodes.py and places that populate
AgentState.temporal_policies, or alternatively add explicit conversion where
TemporalMemory.search_policies() results are consumed (e.g., in
fetch_temporal_policies_node or AgentState population code) to map policy_name
-> name if you must keep the existing local shape; ensure all references to
PolicyMatch, AgentState.temporal_policies, and mocks are updated to the unified
field so production graphiti_client.PolicyMatch instances match the expected
shape.

In `@ai-service/src/ai_service/integrations/slack.py`:
- Around line 251-330: The send_message method uses a synchronous httpx.Client
and calls the nonexistent post_async; change SlackClient to use
httpx.AsyncClient throughout: update the attribute type to self._client:
httpx.AsyncClient | None, create the client with
httpx.AsyncClient(timeout=self.timeout) in _get_client (or an async
_get_client), and call await self._client.post(...) in send_message; also make
close async (async def close) and await self._client.aclose() to properly close
the async client, and adjust imports/type hints and any callers accordingly
(methods: SlackClient, _get_client, send_message, close).

In `@ai-service/src/ai_service/integrations/webhook.py`:
- Around line 38-40: The webhook verification currently returns True when
GITHUB_WEBHOOK_SECRET is unset, which fails open; update the verification logic
in the webhook verification function in webhook.py to fail closed by default:
add a SKIP_SIGNATURE_VERIFICATION env flag (e.g. SKIP_SIGNATURE_VERIFICATION =
os.getenv(...).lower() == "true"), and if GITHUB_WEBHOOK_SECRET is missing then
only return True when SKIP_SIGNATURE_VERIFICATION is true (with a warning via
logger), otherwise log an error via logger.error and return False; ensure you
reference and update the GITHUB_WEBHOOK_SECRET and logger checks in that
function and add the new SKIP_SIGNATURE_VERIFICATION flag to configuration.

In `@ai-service/src/ai_service/memory/graphiti_client.py`:
- Around line 34-43: The PolicyMatch dataclass in graphiti_client.py uses the
field policy_name but other code (nodes.py and the PolicyMatch TypedDict in
state.py) expects name, causing attribute errors; change the dataclass field
from policy_name to name and update any assignments in graphiti_client.py (e.g.,
where PolicyMatch instances are constructed in the search_policies logic) to set
name instead of policy_name so all creations and consumers use the same field
name.

In `@ai-service/src/ai_service/memory/vector_store.py`:
- Around line 97-99: The PGVector calls are using sync methods in an async
context; replace add_documents, similarity_search, and delete_collection calls
with their async counterparts (use aadd_documents instead of add_documents in
methods that call self._vector_store.add_documents, use asimilarity_search
instead of similarity_search where similarity queries are executed, and
verify/replace delete_collection with adelete_collection if available) and
ensure each call is awaited (e.g., await self._vector_store.aadd_documents(...),
await self._vector_store.asimilarity_search(...), await
self._vector_store.adelete_collection(...) ), updating all occurrences
referenced (the add_documents call that assigns ids, the second add_documents
occurrence, the similarity_search call, and the delete_collection call).

In `@ai-service/template.yaml`:
- Around line 32-33: The Policies block is incorrectly using a Lambda layer ARN
(AWSLambdaPowertoolsPythonV3) instead of IAM policy ARNs or SAM policy
templates; replace that entry under Policies with the appropriate IAM policies
or SAM templates (e.g., AWSLambdaBasicExecutionRole or SSMParameterReadPolicy
with the ParameterName sub) and ensure the Powertools layer remains only in the
Layers section (remove the arn:aws:lambda:::layer:AWSLambdaPowertoolsPythonV3:75
entry from Policies and add the needed policy identifiers in its place).
- Around line 56-58: The CloudFormation Lambda template includes an unused
AWSSDKPandas layer entry
("arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSSDKPandas-Python312:18")
under the Layers property; remove that layer reference from the template.yaml so
the function's Layers list no longer contains this ARN to avoid unnecessary
cold-start overhead. Ensure you only delete the specific list item and keep the
surrounding Layers structure intact if other layers exist.
🟡 Minor comments (12)
ai-service/README.md-17-18 (1)

17-18: Fix the shell command syntax.

The activate script must be sourced, not executed directly. The current command won't work as written.

📝 Suggested fix
 # Start service
-PYTHONPATH=src .venv/bin/activate && uvicorn ai_service.main:app --host 0.0.0.0 --port 8000
+source .venv/bin/activate && PYTHONPATH=src uvicorn ai_service.main:app --host 0.0.0.0 --port 8000

Or use a single-line alternative:

PYTHONPATH=src uv run uvicorn ai_service.main:app --host 0.0.0.0 --port 8000
init-scripts/01-pgvector.sql-4-7 (1)

4-7: Remove the unused founderos schema or configure PGVector to use it.

The founderos schema is created in the SQL init script, but PGVector is initialized without specifying a schema parameter (lines 55-60 of vector_store.py), causing it to default to the public schema. The integration tests confirm tables are created in public, not founderos. Either use the schema explicitly in the PGVector configuration or remove the unused schema creation.

ai-service/src/ai_service/memory/vector_store.py-84-85 (1)

84-85: Replace deprecated datetime.utcnow() with timezone-aware alternative.

datetime.utcnow() is deprecated in Python 3.12+ (which this project requires). Use datetime.now(timezone.utc) instead.

🔧 Proposed fix
-from datetime import datetime
+from datetime import datetime, timezone

Then update all occurrences:

-            timestamp = datetime.utcnow()
+            timestamp = datetime.now(timezone.utc)

This applies to lines 85, 120, and 167.

ai-service/src/ai_service/integrations/slack.py-368-384 (1)

368-384: Potential ValueError when pr_number is not a valid integer.

If pr_number is None or a non-numeric string, int(pr_number) at line 379 will raise an exception. The check at line 374 only verifies truthiness, not numeric validity.

Proposed fix
     def parse_interaction_callback(
         self, payload: dict[str, Any]
     ) -> dict[str, Any] | None:
         action_id = payload.get("actions", [{}])[0].get("action_id")
         pr_number = payload.get("actions", [{}])[0].get("value")
         user_id = payload.get("user", {}).get("id")
         channel_id = payload.get("channel", {}).get("id")
         message_ts = payload.get("message", {}).get("ts")

         if not action_id or not pr_number:
             return None

+        try:
+            pr_number_int = int(pr_number)
+        except (ValueError, TypeError):
+            return None
+
         return {
             "action_id": action_id,
-            "pr_number": int(pr_number),
+            "pr_number": pr_number_int,
             "user_id": user_id,
             "channel_id": channel_id,
             "message_ts": message_ts,
             "is_valid": action_id.startswith("sentinel_"),
         }
ai-service/src/ai_service/integrations/slack.py-89-98 (1)

89-98: Header block uses wrong text type for emoji.

Slack header blocks with plain_text type don't render emoji shortcodes like :white_check_mark:. Either use mrkdwn type in a section block, or use actual Unicode emoji characters for plain_text headers.

Proposed fix using Unicode emoji
     DECISION_EMOJIS = {
-        Decision.APPROVE: ":white_check_mark:",
-        Decision.WARN: ":warning:",
-        Decision.BLOCK: ":no_entry:",
+        Decision.APPROVE: "✅",
+        Decision.WARN: "⚠️",
+        Decision.BLOCK: "🚫",
     }
ai-service/src/ai_service/memory/graphiti_client.py-160-175 (1)

160-175: Unsafe attribute access on Graphiti search results.

The code accesses edge.source, edge.name, edge.fact, edge.valid_from, edge.valid_to, and edge.score without verifying the edge object structure. If the Graphiti API returns a different structure, this will raise AttributeError.

Proposed fix with safer attribute access
         matches: list[PolicyMatch] = []
         for edge in results[:limit]:
             # Extract policy info from the edge
-            match = PolicyMatch(
-                policy_name=edge.source or edge.name or "unknown",
-                rule=edge.fact,
-                valid_from=edge.valid_from or valid_at,
-                valid_to=edge.valid_to,
-                similarity=getattr(edge, "score", 0.5),
-            )
-            matches.append(match)
+            try:
+                match = PolicyMatch(
+                    policy_name=getattr(edge, "source", None) or getattr(edge, "name", None) or "unknown",
+                    rule=getattr(edge, "fact", ""),
+                    valid_from=getattr(edge, "valid_from", None) or valid_at,
+                    valid_to=getattr(edge, "valid_to", None),
+                    similarity=getattr(edge, "score", 0.5),
+                )
+                matches.append(match)
+            except (AttributeError, TypeError) as e:
+                logger.warning(f"Failed to parse edge result: {e}")
+                continue
ai-service/src/ai_service/integrations/slack.py-409-432 (1)

409-432: Rename format_block_message in slack.py to avoid confusion with the identically-named function in nodes.py.

The function in slack.py (line 409, signature: pr_summary: PRSummarylist[dict[str, Any]]) has the same name as a function in nodes.py (signature: violations: list[Violation]str). They serve completely different purposes. Rename the Slack function to format_slack_blocks or similar to clarify its scope and make the codebase easier to navigate and maintain.

ai-service/src/ai_service/observability.py-260-322 (1)

260-322: Global metrics functions are defined but not integrated into production code; thread-safety issue will become active once they are.

The metrics recording functions (record_pr_decision, record_violations, record_recommendations, record_budget_impact) exist but are never called in the actual webhook handler or main application—only in integration tests. However, the webhook handler clearly needs them: it captures decision-making data (decision, should_block, should_warn, violations) that should flow to metrics.

Once these functions are wired into the webhook handler (or other production code) for concurrent FastAPI request handling, the current lack of synchronization will create race conditions. The global _metrics instance is mutated without locks, and simple integer/float operations are not atomic in Python under concurrent access.

Either:

  1. Remove unused metrics code if it's not needed, or
  2. Before integrating metrics into production handlers, add thread-safety via threading.Lock, atomic operations, or a metrics library like prometheus_client.
ai-service/tests/integration/test_github_sentinel.py-238-246 (1)

238-246: Test for Friday deployment policy is incomplete.

The comment acknowledges this test "would need to mock datetime to test Friday" but the test doesn't mock it, making it non-deterministic. On Fridays it will detect the violation; on other days it won't.

Would you like me to suggest a proper implementation using freezegun or unittest.mock to freeze the date to a Friday?

💡 Example fix using unittest.mock
from unittest.mock import patch
from datetime import datetime

def test_friday_deploy_blocked(self, parsed_state_with_policies):
    """PR on Friday with deploy policy is blocked."""
    # Mock datetime.utcnow() to return a Friday
    friday = datetime(2024, 1, 12, 12, 0, 0)  # Jan 12, 2024 is a Friday
    with patch('ai_service.agent.nodes.datetime') as mock_dt:
        mock_dt.utcnow.return_value = friday
        result = analyze_violations_node(parsed_state_with_policies)
    
    assert result["decision"] == "block"
    assert any(v["type"] == "friday_deploy" for v in result["violations"])
ai-service/src/ai_service/integrations/github.py-115-118 (1)

115-118: Missing authentication headers when fetching diff URL.

The diff fetch creates a new client without the authentication headers. This will fail for private repositories. Use self.headers for consistency.

🐛 Proposed fix
         async with httpx.AsyncClient() as client:
-            response = await client.get(diff_url)
+            response = await client.get(diff_url, headers=self.headers)
             response.raise_for_status()
             return response.text
ai-service/src/ai_service/agent/nodes.py-476-481 (1)

476-481: Duplicate pattern in _find_line_numbers call.

Line 480 passes ["execute(", "execute("] with a duplicate entry. This appears to be a copy-paste error.

Suggested fix
-                line_numbers=_find_line_numbers(patch, ["execute(", "execute("]),
+                line_numbers=_find_line_numbers(patch, ["execute("]),
ai-service/src/ai_service/agent/nodes.py-789-814 (1)

789-814: Duplicate ec2 handling causes dead code.

The ec2 service is handled at lines 791-795, but line 809 includes ec2 again in the elif check alongside rds, elasticache, and redshift. Since the first elif service == "ec2" branch matches first, the ec2 case in line 809 is dead code. Remove ec2 from line 809.

Suggested fix
-        elif service in ("ec2", "rds", "elasticache", "redshift"):
+        elif service in ("rds", "elasticache", "redshift"):
             instance_type = usage.get("instance_type", "t3_micro")
🧹 Nitpick comments (38)
ai-service/tests/conftest.py (1)

11-16: Consider using warnings.warn instead of print.

Using print() for import diagnostics can clutter test output. Consider warnings.warn() which integrates better with pytest's warning capture system.

♻️ Suggested improvement
+import warnings
+
 # Verify the package is accessible
 try:
     import ai_service
-    print(f"ai_service package loaded from: {ai_service.__file__}")
+    # Optionally log in debug mode only
 except ImportError as e:
-    print(f"Warning: Could not import ai_service: {e}")
+    warnings.warn(f"Could not import ai_service: {e}", ImportWarning)
ai-service/README.md (1)

75-83: Add language specifiers to fenced code blocks.

The architecture diagram and other code blocks are missing language specifiers, which affects syntax highlighting and accessibility.

📝 Suggested fix
-```
+```text
 Webhooks → /process_event → Vertical Router → LangGraph StateGraph

Apply similar fixes to the code blocks at lines 87 and 93 (use text or plaintext).

docker-compose.yml (2)

44-55: Hardcoded credentials and password exposure in healthcheck.

The Neo4j password is hardcoded and exposed in the healthcheck command. While acceptable for local development, ensure these are overridden in production via environment variables or secrets management.

🔒 Suggested improvement for healthcheck
     healthcheck:
-      test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "founderos_secret", "RETURN 1"]
+      test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "$${NEO4J_PASSWORD:-founderos_secret}", "RETURN 1"]
       interval: 10s

Consider using a .env file with environment variable substitution:

environment:
  NEO4J_AUTH: ${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-founderos_secret}

23-35: Redis lacks authentication.

The Redis service has no password configured. While fine for local development, consider adding --requirepass for environments exposed beyond localhost.

🔒 Optional: Add Redis password
   redis:
     image: redis:7-alpine
     container_name: founderos-redis
+    command: redis-server --requirepass ${REDIS_PASSWORD:-founderos_secret}
     ports:
       - "6379:6379"
ai-service/loadtest/locustfile.py (1)

8-11: Remove unused json import.

The json module is imported but never used since self.client.post() accepts json= parameter directly.

♻️ Proposed fix
 from locust import HttpUser, task, between, events
 from locust.runners import MasterRunner
 import random
-import json
ai-service/src/ai_service/observability/langfuse.py (2)

85-115: Consider reusing Langfuse client in trace() context manager.

Each call to trace() creates a new Langfuse client instance. For high-throughput scenarios, this could add overhead. Consider caching the client similarly to how _handler is lazily initialized.


77-77: Prefer list unpacking over concatenation.

As suggested by the linter, use unpacking for slightly better performance and clarity.

♻️ Proposed fix
-        callbacks = kwargs.pop("callbacks", []) + [handler]
+        callbacks = [*kwargs.pop("callbacks", []), handler]
ai-service/src/ai_service/observability/__init__.py (1)

1-5: Consider also exporting create_observer.

The langfuse.py module defines create_observer() as a convenience factory, but it's not included in __all__. If this is intentional (preferring direct LangfuseObserver instantiation), the current exports are fine.

♻️ Optional: Export create_observer
-from .langfuse import LangfuseObserver, create_langfuse_handler
+from .langfuse import LangfuseObserver, create_langfuse_handler, create_observer

-__all__ = ["LangfuseObserver", "create_langfuse_handler"]
+__all__ = ["LangfuseObserver", "create_langfuse_handler", "create_observer"]
ai-service/src/ai_service/memory/vector_store.py (1)

159-175: Consider using asimilarity_search_with_score for actual similarity values.

The hardcoded similarity=0.5 loses ranking information. PGVector's asimilarity_search_with_score() returns (Document, score) tuples, which would provide meaningful similarity values for consumers of ContextMatch. Note that PGVector's returned scores are distance-based (lower values indicate higher similarity), so you may need to apply a transformation (e.g., 1 - score or normalize) depending on how consumers expect similarity to be represented.

♻️ Proposed approach
-        docs = await self._vector_store.similarity_search(query, **search_kwargs)
+        docs_with_scores = await self._vector_store.asimilarity_search_with_score(
+            query, **search_kwargs
+        )

         matches: list[ContextMatch] = []
-        for doc in docs:
+        for doc, score in docs_with_scores:
             match = ContextMatch(
                 content=doc.page_content,
                 speaker=doc.metadata.get("speaker", "unknown"),
                 timestamp=datetime.fromisoformat(
-                    doc.metadata.get("timestamp", datetime.utcnow().isoformat())
+                    doc.metadata.get("timestamp", datetime.now(timezone.utc).isoformat())
                 ),
                 metadata=doc.metadata,
-                similarity=0.5,  # PGVector doesn't return scores by default
+                similarity=score,  # TODO: verify if transformation needed (1-score or normalization)
             )
ai-service/src/ai_service/integrations/slack.py (3)

67-78: Annotate mutable class attributes with ClassVar.

These dictionaries are class-level constants that should be annotated with ClassVar to clarify they're not instance attributes.

Proposed fix
+from typing import Any, ClassVar
+
 class SlackMessageBuilder:
     """Build Slack message blocks for PR notifications."""

     # Emojis for decisions
-    DECISION_EMOJIS = {
+    DECISION_EMOJIS: ClassVar[dict[Decision, str]] = {
         Decision.APPROVE: ":white_check_mark:",
         Decision.WARN: ":warning:",
         Decision.BLOCK: ":no_entry:",
     }

     # Colors for attachments (Slack legacy format)
-    DECISION_COLORS = {
+    DECISION_COLORS: ClassVar[dict[Decision, str]] = {
         Decision.APPROVE: "#36a64f",  # Green
         Decision.WARN: "#ffcc00",  # Yellow
         Decision.BLOCK: "#dc3545",  # Red
     }

167-202: Remove unused action_values variable.

The action_values dictionary is defined but never used.

Proposed fix
     def _add_actions(self) -> None:
         """Add action buttons for interactive response."""
-        action_values = {
-            Decision.APPROVE: "approve",
-            Decision.WARN: "warn",
-            Decision.BLOCK: "block",
-        }
-
         # Add buttons for all possible actions
         self.blocks.append(

303-305: Use logging.exception to capture stack trace on error.

When logging errors in exception handlers, logger.exception() automatically includes the stack trace, which aids debugging.

Proposed fix
         except httpx.HTTPError as e:
-            logger.error(f"Failed to send Slack message: {e}")
+            logger.exception(f"Failed to send Slack message: {e}")
             return False
ai-service/src/ai_service/observability.py (3)

134-136: Use logging.exception for better error diagnostics.

When catching exceptions, logger.exception() automatically includes the full stack trace, which is valuable for debugging initialization failures.

Proposed fix
         except Exception as e:
-            logger.error(f"Failed to initialize LangFuse tracer: {e}")
+            logger.exception(f"Failed to initialize LangFuse tracer: {e}")
             return False

213-257: Parameter format shadows built-in name.

The parameter format shadows Python's built-in format() function. Consider renaming to log_format for clarity.

Proposed fix
-def setup_logging(level: str = "INFO", format: str = "json") -> None:
+def setup_logging(level: str = "INFO", log_format: str = "json") -> None:
     """Configure structured logging for the application.

     Args:
         level: Log level (DEBUG, INFO, WARNING, ERROR)
-        format: Log format (json, text)
+        log_format: Log format (json, text)
     """
     import json

     log_level = getattr(logging, level.upper(), logging.INFO)

     # ... inside StructuredFormatter.format method:
-            if format == "json":
+            if log_format == "json":
                 return json.dumps(log_data)

59-72: Consider using dataclasses.asdict for to_dict.

The to_dict method manually maps all fields. Using dataclasses.asdict(self) would be more maintainable and automatically include any new fields.

Proposed fix
+from dataclasses import dataclass, asdict

     def to_dict(self) -> dict[str, Any]:
         """Convert metrics to dictionary."""
-        return {
-            "prs_processed": self.prs_processed,
-            "prs_approved": self.prs_approved,
-            "prs_warned": self.prs_warned,
-            "prs_blocked": self.prs_blocked,
-            "violations_found": self.violations_found,
-            "recommendations_generated": self.recommendations_generated,
-            "avg_processing_time": self.avg_processing_time,
-            "total_processing_time": self.total_processing_time,
-            "total_estimated_cost": self.total_estimated_cost,
-            "budgets_exceeded": self.budgets_exceeded,
-        }
+        return asdict(self)
ai-service/src/ai_service/lambda_handler.py (1)

40-45: Remove unused json import and consider security of binding address.

The json import at line 41 is unused. The 0.0.0.0 binding is flagged by static analysis (S104), but this is intentional for local container testing and acceptable in this context.

Proposed fix
 # For local testing with sam local
 if __name__ == "__main__":
-    import json
     import uvicorn

     # Run the app locally
     uvicorn.run(app, host="0.0.0.0", port=8000)
ai-service/src/ai_service/main.py (1)

169-181: Consider making sentinel capabilities configurable.

The /sentinel/status endpoint returns hard-coded values. If features are conditionally available (e.g., based on configuration or dependencies), consider deriving this response dynamically.

This is acceptable for now, but as the system evolves, you may want to reflect actual runtime capabilities.

ai-service/src/ai_service/memory/graphiti_client.py (1)

60-60: Replace datetime.utcnow() with timezone-aware equivalent.

datetime.utcnow() is deprecated since Python 3.12. Use datetime.now(timezone.utc) instead.

Proposed fix
-from datetime import datetime
+from datetime import datetime, timezone

 # In search_policies method (line 156):
-            valid_at = datetime.utcnow()
+            valid_at = datetime.now(timezone.utc)
ai-service/tests/integration/test_observability.py (2)

47-71: Consider using a fixture for metrics isolation to prevent test pollution.

Multiple tests directly mutate module-level _metrics global state. This can cause test pollution if tests run in a different order or in parallel. Consider extracting this into a pytest fixture with proper teardown.

♻️ Suggested fixture for test isolation
`@pytest.fixture`(autouse=True)
def reset_metrics():
    """Reset metrics before each test."""
    import ai_service.observability
    original = ai_service.observability._metrics
    ai_service.observability._metrics = SentinelMetrics()
    yield
    ai_service.observability._metrics = original

Then remove the manual reset lines from individual tests.


304-312: Same test isolation concern - mutating global _tracer_instance.

This test directly sets ai_service.observability._tracer_instance = None. Same recommendation applies - consider a fixture for consistent cleanup.

ai-service/template.yaml (1)

68-76: CORS configuration is overly permissive for production.

AllowOrigins: "*" and AllowHeaders: "*" permit requests from any origin, which may be acceptable for development but poses security risks in production. Consider restricting to specific domains.

♻️ Suggested restrictive CORS configuration
      Cors:
        AllowMethods:
          - "GET"
          - "POST"
          - "OPTIONS"
        AllowOrigins:
          - "https://github.com"
          - "https://your-dashboard-domain.com"
        AllowHeaders:
          - "Content-Type"
          - "X-Hub-Signature-256"
          - "X-GitHub-Event"
ai-service/tests/integration/test_cfo_agent.py (2)

11-12: Remove unused imports.

AsyncMock, MagicMock, and patch are imported but never used in this test file.

🧹 Proposed fix
 import pytest
 from datetime import datetime
-from unittest.mock import AsyncMock, MagicMock, patch

231-271: Consider invoking the compiled CFO agent graph.

test_cfo_agent_run calls analyze_budget_node directly but doesn't invoke the compiled agent. While the comment explains the graph strips non-AgentState fields, testing the actual graph invocation would provide better end-to-end coverage for the CFO workflow.

ai-service/src/ai_service/integrations/webhook.py (3)

22-25: Module-level configuration should be loaded from environment.

The configuration variables are declared but not populated from environment variables. In production, these should be loaded via os.getenv() or a configuration system to avoid hardcoded empty strings.

♻️ Proposed fix
+import os
+
 # Configuration - should come from environment in production
-GITHUB_WEBHOOK_SECRET: str | None = None  # Set via environment
-GITHUB_TOKEN: str = ""  # Set via environment
-GITHUB_OWNER: str = ""  # Set via environment
-GITHUB_REPO: str = ""  # Set via environment
+GITHUB_WEBHOOK_SECRET: str | None = os.getenv("GITHUB_WEBHOOK_SECRET")
+GITHUB_TOKEN: str = os.getenv("GITHUB_TOKEN", "")
+GITHUB_OWNER: str = os.getenv("GITHUB_OWNER", "")
+GITHUB_REPO: str = os.getenv("GITHUB_REPO", "")

87-88: Move import to module level.

Importing json inside the function body works but is unconventional. Module-level imports are preferred for clarity and slight performance benefit (avoids repeated import lookup).

♻️ Proposed fix

Add to module imports at the top:

 import hashlib
 import hmac
+import json
 import logging

Then remove line 87.


171-176: Fix exception handling per static analysis hints.

The exception block has minor issues flagged by static analysis:

  1. logging.exception already captures the exception; including e in the f-string is redundant.
  2. Use raise ... from e to preserve the exception chain.
♻️ Proposed fix
     except Exception as e:
-        logger.exception(f"Error processing webhook: {e}")
-        raise HTTPException(
+        logger.exception("Error processing webhook")
+        raise HTTPException(
             status_code=500,
-            detail=f"Error processing webhook: {str(e)}"
-        )
+            detail=f"Error processing webhook: {e!r}"
+        ) from e
ai-service/tests/integration/test_cto_agent.py (3)

11-12: Remove unused imports.

AsyncMock, MagicMock, and patch are imported but never used in this test file.

🧹 Proposed fix
 import pytest
 from datetime import datetime
-from unittest.mock import AsyncMock, MagicMock, patch

152-193: Gitleaks false positive - add comment to clarify intentional test data.

Static analysis flags line 163 as a potential leaked API key. Since this is intentional test data for verifying secret detection, consider adding a comment to clarify this is not a real credential.

📝 Proposed fix
         diff_files = [
             {
                 "filename": "src/config.py",
                 "status": "modified",
                 "additions": 5,
                 "deletions": 0,
+                # Intentional fake API key for testing secret detection
                 "patch": '+API_KEY = "sk-1234567890abcdef"',
                 "language": "python",
             },
         ]

424-432: Test could verify graph structure more thoroughly.

test_cto_agent_has_diff_node only asserts the agent is not None. Consider verifying that the expected nodes (like "fetch_diff") are present in the graph for more meaningful coverage.

ai-service/src/ai_service/integrations/github.py (1)

72-80: Consider reusing httpx.AsyncClient for better performance.

Creating a new AsyncClient for each request adds connection overhead. Consider initializing a shared client instance (e.g., in __init__ or as a context manager) to reuse connections across multiple API calls.

♻️ Example approach
class GitHubClient:
    def __init__(self, ...):
        ...
        self._client: httpx.AsyncClient | None = None

    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = httpx.AsyncClient(headers=self.headers)
        return self._client

    async def close(self) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None

    async def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
        client = await self._get_client()
        response = await client.request(method, f"{self.base_url}/{path}", **kwargs)
        response.raise_for_status()
        return response.json()
ai-service/src/ai_service/agent/state.py (1)

142-142: datetime.utcnow() is deprecated.

datetime.utcnow() is deprecated since Python 3.12 and will be removed in a future version. Use timezone-aware datetime.now(timezone.utc) instead.

Suggested fix
-from datetime import datetime
+from datetime import datetime, timezone
-        timestamp=datetime.utcnow(),
+        timestamp=datetime.now(timezone.utc),
ai-service/src/ai_service/agent/nodes.py (7)

83-88: Unused variable query assigned but never used.

The query variable is constructed but never passed to TemporalMemory. If this is intentional placeholder code, consider adding a TODO comment. Otherwise, remove the dead code.

Suggested fix
     # Get PR content for policy search
     pr_info = state.get("pr_info", {})
-    query = f"{pr_info.get('title', '')} {pr_info.get('action', '')}"
+    # TODO: Use query for actual Graphiti search when production-ready
+    # query = f"{pr_info.get('title', '')} {pr_info.get('action', '')}"

     # Create mock temporal memory for now

157-166: Unused variables pr_action and sql_patterns.

Both pr_action (line 158) and sql_patterns (lines 161-166) are assigned but never used in this function. Remove them or add TODO comments if they're placeholders.

Suggested fix
     pr_info = state.get("pr_info", {})
-    temporal_policies = state.get("temporal_policies", [])
-    similar_contexts = state.get("similar_contexts", [])
-
-    violations: list[Violation] = []
-    should_block = False
-    should_warn = False
-
-    # Get diff content from event (mock - in production would fetch)
     pr_title = pr_info.get("title", "").lower()
-    pr_action = pr_info.get("action", "")
-
-    # Check 1: SQL outside db/ folder (simulated)
-    sql_patterns = [
-        r"SELECT\s+.*\s+FROM",
-        r"INSERT\s+INTO",
-        r"UPDATE\s+.*\s+SET",
-        r"DELETE\s+FROM",
-    ]
+    temporal_policies = state.get("temporal_policies", [])
+    similar_contexts = state.get("similar_contexts", [])
+
+    violations: list[Violation] = []
+    should_block = False
+    should_warn = False

359-363: Unused variable repo_full_name.

repo_full_name is assigned but never used. Remove or annotate if planned for future use.

Suggested fix
     # Parse repo info from webhook event
     event = state.get("webhook_event", {})
-    repo = event.get("repository", {})
-    repo_full_name = repo.get("full_name", "")
+    # repo = event.get("repository", {})
+    # repo_full_name = repo.get("full_name", "")  # TODO: Use for GitHub API calls

     # Mock GitHub client for now - in production would use PyGitHub

532-540: f-string without placeholders on line 538.

The regex pattern uses rf"..." but contains no {...} placeholders, making the f prefix unnecessary and potentially confusing.

Suggested fix
     # f-string SQL injection
-    if re.search(rf"f['\"].*{{\s*.*\s*}}.*['\"]", patch):
+    if re.search(r"f['\"].*\{\s*.*\s*\}.*['\"]", patch):
         return True

602-606: Unused variable violation_type.

violation_type is assigned on line 605 but the actual lookup uses violation.get("type", "") again on line 619. Remove the redundant assignment.

Suggested fix
     for violation in violations:
-        violation_type = violation.get("type", "")
         rec = _get_recommendation(violation)
         if rec:
             recommendations.append(rec)

813-813: Use next(iter(...)) instead of list(...)[0].

Converting keys to a list just to get the first element is inefficient. Use next(iter(pricing.keys())) or simply next(iter(pricing)).

Suggested fix
-            key = instance_type if instance_type in pricing else f"{instance_type.split('.')[0]}_micro"
-            hourly_rate = pricing.get(key, pricing.get(list(pricing.keys())[0], 0.02))
+            key = instance_type if instance_type in pricing else f"{instance_type.split('.')[0]}_micro"
+            hourly_rate = pricing.get(key, pricing.get(next(iter(pricing)), 0.02))

874-892: Redundant import of StateGraph.

StateGraph is already imported at the module level (line 12). The import on line 880 inside create_cfo_agent is redundant.

Suggested fix
 def create_cfo_agent() -> StateGraph:
     """Create the CFO agent for budget analysis.

     Returns:
         StateGraph for CFO budget analysis
     """
-    from langgraph.graph import StateGraph
-
     graph = StateGraph(AgentState)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9f5ec and 734c41d.

📒 Files selected for processing (27)
  • ai-service/README.md
  • ai-service/loadtest/locustfile.py
  • ai-service/pyproject.toml
  • ai-service/src/ai_service/agent/__init__.py
  • ai-service/src/ai_service/agent/nodes.py
  • ai-service/src/ai_service/agent/state.py
  • ai-service/src/ai_service/integrations/__init__.py
  • ai-service/src/ai_service/integrations/github.py
  • ai-service/src/ai_service/integrations/slack.py
  • ai-service/src/ai_service/integrations/webhook.py
  • ai-service/src/ai_service/lambda_handler.py
  • ai-service/src/ai_service/main.py
  • ai-service/src/ai_service/memory/__init__.py
  • ai-service/src/ai_service/memory/graphiti_client.py
  • ai-service/src/ai_service/memory/vector_store.py
  • ai-service/src/ai_service/observability.py
  • ai-service/src/ai_service/observability/__init__.py
  • ai-service/src/ai_service/observability/langfuse.py
  • ai-service/template.yaml
  • ai-service/tests/conftest.py
  • ai-service/tests/integration/test_cfo_agent.py
  • ai-service/tests/integration/test_cto_agent.py
  • ai-service/tests/integration/test_github_sentinel.py
  • ai-service/tests/integration/test_observability.py
  • ai-service/tests/integration/test_slack_integration.py
  • docker-compose.yml
  • init-scripts/01-pgvector.sql
🧰 Additional context used
🧬 Code graph analysis (14)
ai-service/src/ai_service/memory/__init__.py (2)
ai-service/src/ai_service/memory/graphiti_client.py (1)
  • TemporalMemory (45-217)
ai-service/src/ai_service/memory/vector_store.py (1)
  • SemanticMemory (30-238)
ai-service/src/ai_service/integrations/__init__.py (1)
ai-service/src/ai_service/integrations/github.py (1)
  • GitHubClient (15-268)
ai-service/src/ai_service/observability/__init__.py (1)
ai-service/src/ai_service/observability/langfuse.py (2)
  • LangfuseObserver (17-121)
  • create_langfuse_handler (124-153)
ai-service/tests/integration/test_cfo_agent.py (1)
ai-service/src/ai_service/agent/nodes.py (6)
  • analyze_budget_node (693-743)
  • estimate_cost_node (760-820)
  • should_handoff_to_cfo (823-846)
  • create_cfo_handoff_state (849-871)
  • create_cfo_agent (874-892)
  • enforce_budget_policy (933-961)
ai-service/tests/integration/test_github_sentinel.py (1)
ai-service/src/ai_service/agent/nodes.py (7)
  • parse_pr_node (19-71)
  • query_temporal_memory_node (74-114)
  • query_semantic_memory_node (117-136)
  • analyze_violations_node (139-240)
  • format_block_message (243-274)
  • format_warning_message (277-305)
  • create_sentinel_agent (308-336)
ai-service/tests/integration/test_slack_integration.py (1)
ai-service/src/ai_service/integrations/slack.py (12)
  • SlackMessageBuilder (63-248)
  • PRSummary (40-60)
  • Decision (21-26)
  • build (222-248)
  • SlackWebhookHandler (333-403)
  • verify_url (346-355)
  • parse_interaction_callback (357-384)
  • get_action_from_callback (386-403)
  • SlackClient (251-330)
  • notify_pr_review (307-324)
  • format_block_message (409-419)
  • create_slack_client (435-444)
ai-service/loadtest/locustfile.py (1)
ai-service/src/ai_service/main.py (3)
  • sentinel_status (170-181)
  • health_check (60-62)
  • list_sops (143-166)
ai-service/tests/integration/test_cto_agent.py (2)
ai-service/src/ai_service/agent/nodes.py (4)
  • fetch_diff_node (339-390)
  • analyze_code_node (442-515)
  • generate_recommendations_node (592-614)
  • create_sentinel_agent (308-336)
ai-service/src/ai_service/agent/state.py (2)
  • create_initial_state (110-143)
  • AgentState (71-107)
ai-service/src/ai_service/lambda_handler.py (2)
ai-service/main.py (1)
  • main (1-2)
ai-service/src/ai_service/main.py (1)
  • lifespan (30-36)
ai-service/src/ai_service/agent/state.py (2)
ai-service/src/ai_service/memory/graphiti_client.py (1)
  • PolicyMatch (35-42)
ai-service/src/ai_service/memory/vector_store.py (1)
  • ContextMatch (20-27)
ai-service/src/ai_service/agent/nodes.py (1)
ai-service/src/ai_service/agent/state.py (4)
  • AgentState (71-107)
  • PolicyMatch (23-30)
  • Violation (42-48)
  • DiffFile (51-59)
ai-service/src/ai_service/memory/graphiti_client.py (2)
ai-service/src/ai_service/agent/state.py (1)
  • PolicyMatch (23-30)
ai-service/src/ai_service/memory/vector_store.py (1)
  • search_policies (215-229)
ai-service/src/ai_service/integrations/slack.py (1)
ai-service/src/ai_service/agent/nodes.py (2)
  • format_block_message (243-274)
  • format_warning_message (277-305)
ai-service/src/ai_service/main.py (1)
ai-service/loadtest/locustfile.py (1)
  • sentinel_status (91-93)
🪛 Checkov (3.2.334)
docker-compose.yml

[medium] 64-65: Basic Auth Credentials

(CKV_SECRET_4)

🪛 Gitleaks (8.30.0)
ai-service/tests/integration/test_cto_agent.py

[high] 163-163: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
ai-service/README.md

[uncategorized] ~68-~68: The official name of this software platform is spelled with a capital “H”.
Context: ...| Release Hygiene | sentry.error, github.deploy | rollback, alert_dev | | **Cus...

(GITHUB)


[uncategorized] ~71-~71: The official name of this software platform is spelled with a capital “H”.
Context: ...email, investigate | | Team Pulse | github.commit, github.activity | calendar_i...

(GITHUB)


[uncategorized] ~71-~71: The official name of this software platform is spelled with a capital “H”.
Context: ...e | | Team Pulse | github.commit, github.activity | calendar_invite, sentiment_...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
ai-service/README.md

75-75: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


87-87: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


93-93: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.11)
ai-service/src/ai_service/memory/__init__.py

6-6: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

ai-service/tests/integration/test_observability.py

163-163: Possible hardcoded password assigned to argument: "secret_key"

(S106)


171-171: Possible hardcoded password assigned to argument: "secret_key"

(S106)


205-205: Possible hardcoded password assigned to argument: "secret_key"

(S106)


211-211: Possible hardcoded password assigned to argument: "secret_key"

(S106)


337-337: Possible hardcoded password assigned to argument: "secret_key"

(S106)

ai-service/src/ai_service/integrations/webhook.py

172-172: Redundant exception object included in logging.exception call

(TRY401)


173-176: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


175-175: Use explicit conversion flag

Replace with conversion flag

(RUF010)

ai-service/tests/integration/test_slack_integration.py

210-210: Possible hardcoded password assigned to argument: "signing_secret"

(S106)


220-220: Possible hardcoded password assigned to argument: "signing_secret"

(S106)


241-241: Possible hardcoded password assigned to argument: "signing_secret"

(S106)


256-256: Possible hardcoded password assigned to argument: "signing_secret"

(S106)


272-272: Possible hardcoded password assigned to argument: "signing_secret"

(S106)


288-288: Possible hardcoded password assigned to argument: "signing_secret"

(S106)

ai-service/loadtest/locustfile.py

39-39: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


40-40: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


41-41: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


58-58: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


77-77: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


78-78: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


79-79: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


112-112: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


129-129: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


130-130: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


148-148: Unused function argument: kwargs

(ARG001)


155-155: Unused function argument: kwargs

(ARG001)

ai-service/src/ai_service/observability/langfuse.py

77-77: Consider [*kwargs.pop("callbacks", []), handler] instead of concatenation

Replace with [*kwargs.pop("callbacks", []), handler]

(RUF005)

ai-service/src/ai_service/lambda_handler.py

45-45: Possible binding to all interfaces

(S104)

ai-service/src/ai_service/agent/nodes.py

87-87: Local variable query is assigned to but never used

Remove assignment to unused variable query

(F841)


158-158: Local variable pr_action is assigned to but never used

Remove assignment to unused variable pr_action

(F841)


161-161: Local variable sql_patterns is assigned to but never used

Remove assignment to unused variable sql_patterns

(F841)


362-362: Local variable repo_full_name is assigned to but never used

Remove assignment to unused variable repo_full_name

(F841)


538-538: f-string without any placeholders

Remove extraneous f prefix

(F541)


605-605: Local variable violation_type is assigned to but never used

Remove assignment to unused variable violation_type

(F841)


813-813: Prefer next(iter(pricing.keys())) over single element slice

Replace with next(iter(pricing.keys()))

(RUF015)

ai-service/src/ai_service/integrations/slack.py

67-71: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


74-78: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


169-169: Local variable action_values is assigned to but never used

Remove assignment to unused variable action_values

(F841)


302-302: Consider moving this statement to an else block

(TRY300)


304-304: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

ai-service/src/ai_service/agent/__init__.py

14-24: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

ai-service/src/ai_service/observability.py

133-133: Consider moving this statement to an else block

(TRY300)


134-134: Do not catch blind exception: Exception

(BLE001)


135-135: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +46 to +50
self.client.post(
"/api/v1/webhook/github",
json=payload,
headers={"Content-Type": "application/json"},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for signature validation in the codebase
rg -n -C5 'X-Hub-Signature|verify_signature|hmac' --type=py

Repository: Aparnap2/founder_os

Length of output: 5255


🏁 Script executed:

#!/bin/bash
# Find the webhook endpoint implementation
rg -n 'webhook.*github|/api/v1/webhook' --type=py -C3

Repository: Aparnap2/founder_os

Length of output: 2280


🏁 Script executed:

#!/bin/bash
# Search more broadly for any signature or auth validation in webhook handlers
rg -n 'def.*webhook|@app.post.*webhook|@.*route.*webhook' --type=py -A10

Repository: Aparnap2/founder_os

Length of output: 8416


Add X-Hub-Signature-256 header to webhook requests or disable signature validation for load tests.

The webhook endpoint validates GitHub signatures when GITHUB_WEBHOOK_SECRET is configured, returning 401 errors for missing or invalid X-Hub-Signature-256 headers. All four webhook requests in the load test (lines 46-50, 65-69, 84-88, 119-123) omit this header and will fail during testing if signature validation is enabled in the target environment.

Either generate valid HMAC-SHA256 signatures for each payload using the configured webhook secret, or configure the load test environment without GITHUB_WEBHOOK_SECRET to skip validation.

🤖 Prompt for AI Agents
In `@ai-service/loadtest/locustfile.py` around lines 46 - 50, The load test POSTs
to "/api/v1/webhook/github" using self.client.post without the
X-Hub-Signature-256 header, which causes 401s when GITHUB_WEBHOOK_SECRET is set;
update each webhook call (the self.client.post invocations) to compute the
HMAC-SHA256 of the JSON payload using the configured secret (from
GITHUB_WEBHOOK_SECRET or a test-specific secret) and add the result as the
"X-Hub-Signature-256" header, or alternatively ensure the test environment
unsets GITHUB_WEBHOOK_SECRET so signature validation is disabled for these
requests.

Comment on lines +23 to +31
class PolicyMatch(TypedDict):
"""A policy retrieved from temporal memory."""

name: str
rule: str
valid_from: datetime
valid_to: datetime | None
similarity: float

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all PolicyMatch occurrences and examine both definitions
rg -n "PolicyMatch" --type=py -A 5 -B 2

Repository: Aparnap2/founder_os

Length of output: 6279


🏁 Script executed:

#!/bin/bash
# Check imports and usage of graphiti_client
rg -n "graphiti_client|from.*memory" --type=py -C 2

Repository: Aparnap2/founder_os

Length of output: 1802


🏁 Script executed:

#!/bin/bash
# Examine the fetch_temporal_policies_node function in nodes.py to see how it uses TemporalMemory
sed -n '75,120p' ai-service/src/ai_service/agent/nodes.py

Repository: Aparnap2/founder_os

Length of output: 1418


🏁 Script executed:

#!/bin/bash
# Search for any conversion or mapping logic between the two PolicyMatch types
rg -n "policy_name|PolicyMatch" --type=py ai-service/src/ai_service/agent/nodes.py

Repository: Aparnap2/founder_os

Length of output: 218


🏁 Script executed:

#!/bin/bash
# Check the full graphiti_client.py to see all methods that return PolicyMatch
cat ai-service/src/ai_service/memory/graphiti_client.py | head -200

Repository: Aparnap2/founder_os

Length of output: 5831


Field name mismatch with graphiti_client.PolicyMatch will cause runtime errors when production code is activated.

Two incompatible PolicyMatch classes exist:

  • state.py (lines 23-31): Uses field name: str (TypedDict)
  • graphiti_client.py (lines 35-41): Uses field policy_name: str (dataclass)

Currently, nodes.py uses mock policies with the state.py definition. However, fetch_temporal_policies_node comments indicate that production code will call TemporalMemory.search_policies(), which returns graphiti_client.PolicyMatch instances with policy_name. These incompatible instances will fail to match the expected PolicyMatch type in AgentState.temporal_policies, causing type errors or silent data loss. Either unify the field names across both classes, or add explicit conversion logic when switching from mock to production.

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/agent/state.py` around lines 23 - 31, The TypedDict
PolicyMatch in state.py uses field name while graphiti_client.PolicyMatch uses
policy_name, causing mismatches; update state.PolicyMatch to use policy_name:
str (and remove or deprecate name) and update any local mocks in nodes.py and
places that populate AgentState.temporal_policies, or alternatively add explicit
conversion where TemporalMemory.search_policies() results are consumed (e.g., in
fetch_temporal_policies_node or AgentState population code) to map policy_name
-> name if you must keep the existing local shape; ensure all references to
PolicyMatch, AgentState.temporal_policies, and mocks are updated to the unified
field so production graphiti_client.PolicyMatch instances match the expected
shape.

Comment on lines +33 to +40
class ContextMatch(TypedDict):
"""A context retrieved from semantic memory."""

content: str
speaker: str
timestamp: datetime
similarity: float

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the ContextMatch definitions in both files
echo "=== ContextMatch in vector_store.py ==="
rg -n "class ContextMatch|ContextMatch.*=" ai-service/src/ai_service/memory/vector_store.py -A 8

echo ""
echo "=== ContextMatch in state.py ==="
rg -n "class ContextMatch|ContextMatch.*=" ai-service/src/ai_service/agent/state.py -A 8

echo ""
echo "=== similar_contexts usage ==="
rg -n "similar_contexts" --type=py -B 2 -A 2

Repository: Aparnap2/founder_os

Length of output: 8362


🏁 Script executed:

#!/bin/bash
# Check where similar_contexts is assigned actual ContextMatch data
echo "=== Assignments to similar_contexts ==="
rg -n "similar_contexts\s*=" --type=py -B 3 -A 1

echo ""
echo "=== Where vector_store results are used ==="
rg -n "query_semantic_memory|vector_store" --type=py -A 5 -B 2

echo ""
echo "=== Check if metadata is accessed ==="
rg -n "\.metadata|metadata\[" --type=py

Repository: Aparnap2/founder_os

Length of output: 13296


Add missing metadata field to ContextMatch TypedDict.

The ContextMatch in state.py is missing the metadata: dict[str, Any] field that is present in vector_store.py. The vector_store.ContextMatch dataclass (lines 20-27 in vector_store.py) includes this field and populates it when creating instances, but the state TypedDict definition omits it. When the semantic memory node transitions from its current mock implementation to real pgvector queries, this type mismatch will cause errors.

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/agent/state.py` around lines 33 - 40, The
ContextMatch TypedDict in state.py is missing the metadata field present on
vector_store.ContextMatch; update the ContextMatch definition to include
metadata: dict[str, Any] and ensure you import Any (e.g., from typing import
Any, TypedDict) so the TypedDict signature matches vector_store.ContextMatch and
prevents type mismatches when real pgvector results include metadata.

Comment on lines +251 to +330
class SlackClient:
"""HTTP client for sending Slack messages."""

def __init__(self, webhook_url: str, timeout: float = 10.0) -> None:
"""Initialize Slack client.
Args:
webhook_url: Slack incoming webhook URL
timeout: Request timeout in seconds
"""
self.webhook_url = webhook_url
self.timeout = timeout
self._client: httpx.Client | None = None

def _get_client(self) -> httpx.Client:
"""Get or create HTTP client."""
if self._client is None:
self._client = httpx.Client(timeout=self.timeout)
return self._client

async def send_message(
self,
blocks: list[dict[str, Any]],
text: str,
channel: str | None = None,
) -> bool:
"""Send a message to Slack.
Args:
blocks: Slack block elements
text: Fallback text for notifications
channel: Optional channel override
Returns:
True if message sent successfully
"""
payload: dict[str, Any] = {
"blocks": blocks,
"text": text,
}

if channel:
payload["channel"] = channel

try:
client = self._get_client()
response = await client.post_async(
self.webhook_url, json=payload
)
response.raise_for_status()
logger.info("Slack message sent successfully")
return True
except httpx.HTTPError as e:
logger.error(f"Failed to send Slack message: {e}")
return False

async def notify_pr_review(self, pr_summary: PRSummary) -> bool:
"""Send PR review notification to Slack.
Args:
pr_summary: The PR review summary
Returns:
True if notification sent successfully
"""
builder = SlackMessageBuilder(pr_summary)
blocks = builder.build()

fallback_text = (
f"PR #{pr_summary.number}: {pr_summary.decision.upper()} - "
f"{pr_summary.title} by {pr_summary.author}"
)

return await self.send_message(blocks, fallback_text)

def close(self) -> None:
"""Close the HTTP client."""
if self._client:
self._client.close()
self._client = None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: httpx.Client is synchronous and has no post_async method.

SlackClient is declared with async methods but uses a synchronous httpx.Client. The method client.post_async() does not exist on httpx.Client. This will raise an AttributeError at runtime.

Use httpx.AsyncClient instead for async operations.

Proposed fix
 class SlackClient:
     """HTTP client for sending Slack messages."""

     def __init__(self, webhook_url: str, timeout: float = 10.0) -> None:
         """Initialize Slack client.

         Args:
             webhook_url: Slack incoming webhook URL
             timeout: Request timeout in seconds
         """
         self.webhook_url = webhook_url
         self.timeout = timeout
-        self._client: httpx.Client | None = None
+        self._client: httpx.AsyncClient | None = None

-    def _get_client(self) -> httpx.Client:
+    async def _get_client(self) -> httpx.AsyncClient:
         """Get or create HTTP client."""
         if self._client is None:
-            self._client = httpx.Client(timeout=self.timeout)
+            self._client = httpx.AsyncClient(timeout=self.timeout)
         return self._client

     async def send_message(
         self,
         blocks: list[dict[str, Any]],
         text: str,
         channel: str | None = None,
     ) -> bool:
         """Send a message to Slack."""
         payload: dict[str, Any] = {
             "blocks": blocks,
             "text": text,
         }

         if channel:
             payload["channel"] = channel

         try:
-            client = self._get_client()
-            response = await client.post_async(
-                self.webhook_url, json=payload
-            )
+            client = await self._get_client()
+            response = await client.post(self.webhook_url, json=payload)
             response.raise_for_status()
             logger.info("Slack message sent successfully")
             return True
         except httpx.HTTPError as e:
-            logger.error(f"Failed to send Slack message: {e}")
+            logger.exception(f"Failed to send Slack message: {e}")
             return False

-    def close(self) -> None:
+    async def close(self) -> None:
         """Close the HTTP client."""
         if self._client:
-            self._client.close()
+            await self._client.aclose()
             self._client = None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class SlackClient:
"""HTTP client for sending Slack messages."""
def __init__(self, webhook_url: str, timeout: float = 10.0) -> None:
"""Initialize Slack client.
Args:
webhook_url: Slack incoming webhook URL
timeout: Request timeout in seconds
"""
self.webhook_url = webhook_url
self.timeout = timeout
self._client: httpx.Client | None = None
def _get_client(self) -> httpx.Client:
"""Get or create HTTP client."""
if self._client is None:
self._client = httpx.Client(timeout=self.timeout)
return self._client
async def send_message(
self,
blocks: list[dict[str, Any]],
text: str,
channel: str | None = None,
) -> bool:
"""Send a message to Slack.
Args:
blocks: Slack block elements
text: Fallback text for notifications
channel: Optional channel override
Returns:
True if message sent successfully
"""
payload: dict[str, Any] = {
"blocks": blocks,
"text": text,
}
if channel:
payload["channel"] = channel
try:
client = self._get_client()
response = await client.post_async(
self.webhook_url, json=payload
)
response.raise_for_status()
logger.info("Slack message sent successfully")
return True
except httpx.HTTPError as e:
logger.error(f"Failed to send Slack message: {e}")
return False
async def notify_pr_review(self, pr_summary: PRSummary) -> bool:
"""Send PR review notification to Slack.
Args:
pr_summary: The PR review summary
Returns:
True if notification sent successfully
"""
builder = SlackMessageBuilder(pr_summary)
blocks = builder.build()
fallback_text = (
f"PR #{pr_summary.number}: {pr_summary.decision.upper()} - "
f"{pr_summary.title} by {pr_summary.author}"
)
return await self.send_message(blocks, fallback_text)
def close(self) -> None:
"""Close the HTTP client."""
if self._client:
self._client.close()
self._client = None
class SlackClient:
"""HTTP client for sending Slack messages."""
def __init__(self, webhook_url: str, timeout: float = 10.0) -> None:
"""Initialize Slack client.
Args:
webhook_url: Slack incoming webhook URL
timeout: Request timeout in seconds
"""
self.webhook_url = webhook_url
self.timeout = timeout
self._client: httpx.AsyncClient | None = None
async def _get_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client."""
if self._client is None:
self._client = httpx.AsyncClient(timeout=self.timeout)
return self._client
async def send_message(
self,
blocks: list[dict[str, Any]],
text: str,
channel: str | None = None,
) -> bool:
"""Send a message to Slack.
Args:
blocks: Slack block elements
text: Fallback text for notifications
channel: Optional channel override
Returns:
True if message sent successfully
"""
payload: dict[str, Any] = {
"blocks": blocks,
"text": text,
}
if channel:
payload["channel"] = channel
try:
client = await self._get_client()
response = await client.post(self.webhook_url, json=payload)
response.raise_for_status()
logger.info("Slack message sent successfully")
return True
except httpx.HTTPError as e:
logger.exception(f"Failed to send Slack message: {e}")
return False
async def notify_pr_review(self, pr_summary: PRSummary) -> bool:
"""Send PR review notification to Slack.
Args:
pr_summary: The PR review summary
Returns:
True if notification sent successfully
"""
builder = SlackMessageBuilder(pr_summary)
blocks = builder.build()
fallback_text = (
f"PR #{pr_summary.number}: {pr_summary.decision.upper()} - "
f"{pr_summary.title} by {pr_summary.author}"
)
return await self.send_message(blocks, fallback_text)
async def close(self) -> None:
"""Close the HTTP client."""
if self._client:
await self._client.aclose()
self._client = None
🧰 Tools
🪛 Ruff (0.14.11)

302-302: Consider moving this statement to an else block

(TRY300)


304-304: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/integrations/slack.py` around lines 251 - 330, The
send_message method uses a synchronous httpx.Client and calls the nonexistent
post_async; change SlackClient to use httpx.AsyncClient throughout: update the
attribute type to self._client: httpx.AsyncClient | None, create the client with
httpx.AsyncClient(timeout=self.timeout) in _get_client (or an async
_get_client), and call await self._client.post(...) in send_message; also make
close async (async def close) and await self._client.aclose() to properly close
the async client, and adjust imports/type hints and any callers accordingly
(methods: SlackClient, _get_client, send_message, close).

Comment on lines +38 to +40
if not GITHUB_WEBHOOK_SECRET:
logger.warning("Webhook secret not configured, skipping verification")
return True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security: Webhook verification fails open when secret is not configured.

When GITHUB_WEBHOOK_SECRET is not set, the function logs a warning but returns True, allowing any request through. This could permit forged webhook events in misconfigured deployments. Consider failing closed (returning False) in production or at minimum making this behavior explicit via a configuration flag.

🔒 Suggested safer default
     if not GITHUB_WEBHOOK_SECRET:
-        logger.warning("Webhook secret not configured, skipping verification")
-        return True
+        logger.error("Webhook secret not configured - rejecting request")
+        return False

Or introduce an explicit SKIP_SIGNATURE_VERIFICATION flag for development:

SKIP_SIGNATURE_VERIFICATION = os.getenv("SKIP_SIGNATURE_VERIFICATION", "false").lower() == "true"

if not GITHUB_WEBHOOK_SECRET:
    if SKIP_SIGNATURE_VERIFICATION:
        logger.warning("Webhook secret not configured, skipping verification (dev mode)")
        return True
    logger.error("Webhook secret not configured - rejecting request")
    return False
🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/integrations/webhook.py` around lines 38 - 40, The
webhook verification currently returns True when GITHUB_WEBHOOK_SECRET is unset,
which fails open; update the verification logic in the webhook verification
function in webhook.py to fail closed by default: add a
SKIP_SIGNATURE_VERIFICATION env flag (e.g. SKIP_SIGNATURE_VERIFICATION =
os.getenv(...).lower() == "true"), and if GITHUB_WEBHOOK_SECRET is missing then
only return True when SKIP_SIGNATURE_VERIFICATION is true (with a warning via
logger), otherwise log an error via logger.error and return False; ensure you
reference and update the GITHUB_WEBHOOK_SECRET and logger checks in that
function and add the new SKIP_SIGNATURE_VERIFICATION flag to configuration.

Comment on lines +191 to +204
async def invalidate_policy(self, name: str, valid_to: datetime) -> bool:
"""Mark a policy as invalid/inactive from a specific time.
Args:
name: Name of the policy to invalidate
valid_to: Time when the policy ends
Returns:
True if policy was found and updated
"""
# Note: Graphiti doesn't have direct update, we add a new episode
# to effectively end the validity of the previous one
logger.info(f"Invalidating policy '{name}' effective from {valid_to}")
return True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

invalidate_policy is a stub that doesn't perform any action.

The method logs that it's invalidating a policy but always returns True without actually doing anything. This could lead to silent failures where callers believe the policy was invalidated.

Consider either:

  1. Implementing the actual invalidation logic
  2. Raising NotImplementedError to make it explicit
  3. Returning False with a warning log
Proposed fix - make stub explicit
     async def invalidate_policy(self, name: str, valid_to: datetime) -> bool:
         """Mark a policy as invalid/inactive from a specific time.

         Args:
             name: Name of the policy to invalidate
             valid_to: Time when the policy ends

         Returns:
             True if policy was found and updated
         """
-        # Note: Graphiti doesn't have direct update, we add a new episode
-        # to effectively end the validity of the previous one
-        logger.info(f"Invalidating policy '{name}' effective from {valid_to}")
-        return True
+        # TODO: Implement policy invalidation
+        # Graphiti doesn't have direct update - consider adding a new episode
+        # to effectively end the validity of the previous one
+        logger.warning(f"invalidate_policy not implemented - '{name}' not invalidated")
+        raise NotImplementedError("Policy invalidation not yet implemented")

Would you like me to help implement the policy invalidation logic or open an issue to track this task?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def invalidate_policy(self, name: str, valid_to: datetime) -> bool:
"""Mark a policy as invalid/inactive from a specific time.
Args:
name: Name of the policy to invalidate
valid_to: Time when the policy ends
Returns:
True if policy was found and updated
"""
# Note: Graphiti doesn't have direct update, we add a new episode
# to effectively end the validity of the previous one
logger.info(f"Invalidating policy '{name}' effective from {valid_to}")
return True
async def invalidate_policy(self, name: str, valid_to: datetime) -> bool:
"""Mark a policy as invalid/inactive from a specific time.
Args:
name: Name of the policy to invalidate
valid_to: Time when the policy ends
Returns:
True if policy was found and updated
"""
# TODO: Implement policy invalidation
# Graphiti doesn't have direct update - consider adding a new episode
# to effectively end the validity of the previous one
logger.warning(f"invalidate_policy not implemented - '{name}' not invalidated")
raise NotImplementedError("Policy invalidation not yet implemented")

Comment on lines +97 to +99
ids = await self._vector_store.add_documents([doc])
logger.info(f"Ingested message from '{speaker}' with ID: {ids[0]}")
return ids
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the vector_store.py file to see the actual code
head -n 250 ai-service/src/ai_service/memory/vector_store.py | tail -n +90

Repository: Aparnap2/founder_os

Length of output: 4413


🏁 Script executed:

# Check what vectorstore is being used and how it's imported
rg -n "import.*vector\|from.*vector\|PGVector\|_vector_store" ai-service/src/ai_service/memory/vector_store.py | head -20

Repository: Aparnap2/founder_os

Length of output: 45


🏁 Script executed:

# Search for langchain-postgres in requirements or dependencies
fd -e "txt" -e "toml" -e "yaml" -e "lock" | xargs grep -l "langchain-postgres" 2>/dev/null | head -5

Repository: Aparnap2/founder_os

Length of output: 88


🏁 Script executed:

# Check the initialization of _vector_store to see what class it is
rg -n "self\._vector_store\s*=" ai-service/src/ai_service/memory/vector_store.py -A 2

Repository: Aparnap2/founder_os

Length of output: 193


🏁 Script executed:

# Search for langchain-postgres version in pyproject.toml
cat ai-service/pyproject.toml | grep -A 5 -B 5 "langchain-postgres"

Repository: Aparnap2/founder_os

Length of output: 349


🌐 Web query:

langchain-postgres PGVector async methods aadd_documents asimilarity_search documentation

💡 Result:

Yes — langchain-postgres’s PGVector exposes async methods including aadd_documents and asimilarity_search. Documentation and async examples:

  • API reference (async methods listed: aadd_documents, aadd_embeddings, asearch, asimilarity_search, asimilarity_search_by_vector, asimilarity_search_with_score, etc.). Example usage shown (await vector_store.aadd_documents(...); results = await vector_store.asimilarity_search(...)). [1][2]

  • Higher‑level tutorial / integration guide with async examples (store.asimilarity_search, async init via PGVector.acreate / PGVectorStore.create, index helpers, connection notes). [3][4]

Quick async usage sketch (from docs):

  • await vector_store.aadd_documents(documents=documents, ids=ids).
  • results = await vector_store.asimilarity_search(query="text", k=4). [1][3]

Sources:
[1] LangChain Postgres PGVector API reference. [2] LangChain Postgres (v2) PGVectorStore API reference. [3] LangChain PGVector integration guide / tutorial. [4] LangChain PGVectorStore docs.


Use async method variants from PGVector.

The methods should use the a-prefixed async variants: aadd_documents() instead of add_documents() (lines 97, 131), asimilarity_search() instead of similarity_search() (line 159), and verify adelete_collection() for line 237. LangChain-postgres PGVector exposes these async methods for use in async contexts.

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/memory/vector_store.py` around lines 97 - 99, The
PGVector calls are using sync methods in an async context; replace
add_documents, similarity_search, and delete_collection calls with their async
counterparts (use aadd_documents instead of add_documents in methods that call
self._vector_store.add_documents, use asimilarity_search instead of
similarity_search where similarity queries are executed, and verify/replace
delete_collection with adelete_collection if available) and ensure each call is
awaited (e.g., await self._vector_store.aadd_documents(...), await
self._vector_store.asimilarity_search(...), await
self._vector_store.adelete_collection(...) ), updating all occurrences
referenced (the add_documents call that assigns ids, the second add_documents
occurrence, the similarity_search call, and the delete_collection call).

Comment on lines +32 to +33
Policies:
- arn:aws:lambda:::layer:AWSLambdaPowertoolsPythonV3:75 # Powertools
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Policy configuration appears incorrect - Lambda layer ARN used instead of IAM policy.

The Policies property expects IAM policy ARNs or SAM policy templates, but a Lambda layer ARN is specified. This will likely cause a deployment error.

🐛 Proposed fix
       Policies:
-        - arn:aws:lambda:::layer:AWSLambdaPowertoolsPythonV3:75  # Powertools
+        - AWSLambdaBasicExecutionRole

If you need specific permissions (e.g., for Secrets Manager, SSM, etc.), use SAM policy templates:

      Policies:
        - AWSLambdaBasicExecutionRole
        - SSMParameterReadPolicy:
            ParameterName: !Sub "founderos/*"

The Powertools layer should only be in the Layers section (which is already handled at line 57).

🤖 Prompt for AI Agents
In `@ai-service/template.yaml` around lines 32 - 33, The Policies block is
incorrectly using a Lambda layer ARN (AWSLambdaPowertoolsPythonV3) instead of
IAM policy ARNs or SAM policy templates; replace that entry under Policies with
the appropriate IAM policies or SAM templates (e.g., AWSLambdaBasicExecutionRole
or SSMParameterReadPolicy with the ParameterName sub) and ensure the Powertools
layer remains only in the Layers section (remove the
arn:aws:lambda:::layer:AWSLambdaPowertoolsPythonV3:75 entry from Policies and
add the needed policy identifiers in its place).

Comment on lines +56 to +58
Layers:
- !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSSDKPandas-Python312:18"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -l "import pandas|from pandas|import numpy|from numpy" --type py ai-service/src/

Repository: Aparnap2/founder_os

Length of output: 45


🏁 Script executed:

find ai-service -type f -name "*.py" | head -20

Repository: Aparnap2/founder_os

Length of output: 915


🏁 Script executed:

rg "import pandas|from pandas|import numpy|from numpy" ai-service/ --type py

Repository: Aparnap2/founder_os

Length of output: 45


🏁 Script executed:

find ai-service -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" -o -name "poetry.lock" -o -name "Pipfile" | xargs cat 2>/dev/null

Repository: Aparnap2/founder_os

Length of output: 1252


🏁 Script executed:

cat -n ai-service/src/ai_service/lambda_handler.py

Repository: Aparnap2/founder_os

Length of output: 1431


🏁 Script executed:

cat -n ai-service/src/ai_service/main.py

Repository: Aparnap2/founder_os

Length of output: 7320


🏁 Script executed:

head -100 ai-service/src/ai_service/graphs/sop_graph.py

Repository: Aparnap2/founder_os

Length of output: 2911


Remove the unnecessary AWSSDKPandas-Python312 Lambda layer.

This layer adds significant cold start overhead but is not used. The codebase has no pandas or numpy imports, and neither library is listed in pyproject.toml dependencies. The service handles webhook processing (FastAPI/Mangum) and LangGraph orchestration—no data science operations requiring pandas.

Current code
      Layers:
        - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSSDKPandas-Python312:18"
🤖 Prompt for AI Agents
In `@ai-service/template.yaml` around lines 56 - 58, The CloudFormation Lambda
template includes an unused AWSSDKPandas layer entry
("arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSSDKPandas-Python312:18")
under the Layers property; remove that layer reference from the template.yaml so
the function's Layers list no longer contains this ARN to avoid unnecessary
cold-start overhead. Ensure you only delete the specific list item and keep the
surrounding Layers structure intact if other layers exist.

Comment on lines +300 to +324
@pytest.mark.asyncio
async def test_sends_notification(self):
"""Sends PR notification to Slack."""
from ai_service.integrations.slack import SlackClient, PRSummary, Decision

with patch("httpx.Client") as mock_client_class:
mock_client = AsyncMock()
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_client.post_async.return_value = mock_response
mock_client_class.return_value = mock_client

client = SlackClient(webhook_url="https://hooks.slack.com/test")
pr = PRSummary(
number=999,
title="Test PR",
author="tester",
decision=Decision.APPROVE,
confidence=1.0,
)

result = await client.notify_pr_review(pr)

assert result is True
mock_client.post_async.assert_called_once()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find SlackClient implementation
find ai-service -name "*.py" -type f | xargs grep -l "class SlackClient"

Repository: Aparnap2/founder_os

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Look for the slack integration module
find ai-service -path "*integrations*slack*" -type f -name "*.py"

Repository: Aparnap2/founder_os

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Search for post_async method definition in the codebase
rg -n "def post_async|post_async\(" --type py ai-service/

Repository: Aparnap2/founder_os

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Read the SlackClient implementation
cat -n ai-service/src/ai_service/integrations/slack.py | head -350

Repository: Aparnap2/founder_os

Length of output: 12726


🏁 Script executed:

#!/bin/bash
# Look at imports and the _get_client method specifically
rg -n "import httpx|class SlackClient|def _get_client|post_async" -A 5 ai-service/src/ai_service/integrations/slack.py | head -100

Repository: Aparnap2/founder_os

Length of output: 872


🏁 Script executed:

#!/bin/bash
# Check httpx documentation/source to confirm post_async doesn't exist
python3 << 'EOF'
import httpx

# Check httpx.Client methods
print("=== httpx.Client methods ===")
print("post method exists:", hasattr(httpx.Client, "post"))
print("post_async method exists:", hasattr(httpx.Client, "post_async"))

# Check httpx.AsyncClient methods  
print("\n=== httpx.AsyncClient methods ===")
print("post method exists:", hasattr(httpx.AsyncClient, "post"))
print("post_async method exists:", hasattr(httpx.AsyncClient, "post_async"))

# Show available methods
print("\n=== httpx.AsyncClient public methods ===")
async_client_methods = [m for m in dir(httpx.AsyncClient) if not m.startswith('_') and callable(getattr(httpx.AsyncClient, m))]
print([m for m in async_client_methods if 'post' in m.lower()])
EOF

Repository: Aparnap2/founder_os

Length of output: 291


Implementation has critical bug: httpx.Client doesn't support post_async() method.

SlackClient._get_client() creates httpx.Client (synchronous), but send_message() attempts to call await client.post_async(), which doesn't exist on either httpx.Client or httpx.AsyncClient.

Fix by either:

  1. Using httpx.AsyncClient instead of httpx.Client, then call await client.post(), or
  2. Making send_message() synchronous using client.post() without await

The test mock is also incorrect—it should be post() not post_async() depending on the client type chosen.

- Implement 4 vertical agents (Release, Customer Fire, Runway, Team Pulse)
- Add LangGraph StateGraphs with human-in-the-loop approval
- Create Inbox UI component with approve/reject workflow
- Add ActionProposal API routes
- Delete legacy sop_graph.py patterns
- Update types and schemas for ExecOps
- Add 26 vertical agent TDD tests
- Add 17 Inbox UI TDD tests
@Aparnap2 Aparnap2 merged commit 5b1934b into main Jan 15, 2026
1 check failed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@ai-service/src/ai_service/agent/supervisor.py`:
- Around line 55-78: Extract the routing_map literal into a single module-level
constant named EVENT_ROUTING_MAP and replace the inline maps in
route_event_to_agent() and the route_to_agent() closure inside
create_guardrails_agent() to reference EVENT_ROUTING_MAP.get(event_type,
"unknown"); ensure the constant is defined once at top-level (so both functions
use the same source of truth), keep the existing logging call in
route_event_to_agent(), and remove the duplicated mapping in the closure.

In `@ai-service/src/ai_service/agent/tech_debt.py`:
- Around line 280-289: Remove the local AgentState TypedDict declaration in
tech_debt.py and replace it with an import from the canonical definition: add
"from .state import AgentState" at the top of the file, delete the AgentState
class block (the duplicate TypedDict) and ensure any references in functions
like those in tech_debt.py still use AgentState from the import; run type checks
to confirm the module now uses the single canonical AgentState type.

In `@ai-service/src/ai_service/agent/workflow.py`:
- Around line 111-141: The HTTP requests to Slack in the workflow are missing
timeouts; update the async calls in the method that posts messages (e.g.,
send_message) to create httpx.AsyncClient with a timeout (or pass timeout to
client.post) — e.g., httpx.AsyncClient(timeout=10) or httpx.Timeout(10.0) — and
apply the same change in the webhook branch and in the update_message method so
all Slack HTTP calls have a bounded timeout.

In `@ai-service/src/ai_service/infrastructure/checkpointer.py`:
- Around line 6-19: The docstring example incorrectly passes the checkpointer
via RunnableConfig; instead, call get_checkpointer() and pass it to
graph.compile(checkpointer=checkpointer) (e.g., compiled_graph =
graph.compile(checkpointer=checkpointer)), then call
compiled_graph.invoke(state, config={"configurable": {"thread_id": event_id}})
or use a RunnableConfig containing only runtime settings (no checkpointers).
Ensure references to get_checkpointer, graph.compile, and graph.invoke (or
compiled_graph.invoke) are updated accordingly and remove checkpointers from the
RunnableConfig example.

In `@ai-service/src/ai_service/integrations/stripe.py`:
- Around line 151-153: The verify_signature method returns MagicMock() when
Stripe is unavailable but MagicMock is not imported, causing a NameError; fix by
either importing MagicMock from unittest.mock at the top of the module or,
better, change the fallback to a production-safe value (e.g., return a simple
dict or raise a descriptive exception) in the verify_signature path and update
any callers accordingly; locate the conditional in the verify_signature method
where STRIPE_AVAILABLE or self._stripe is checked to make the change.

In `@ai-service/test_connections.py`:
- Around line 16-20: Replace the hardcoded Neo4j credentials passed into
TemporalMemory with environment variables: read neo4j URI, user, and password
via os.environ.get (e.g., NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD) and provide
sensible local defaults when missing; update the TemporalMemory instantiation in
test_connections.py to use those variables (and add an import for os at top), or
load from a .env if the project uses python-dotenv, ensuring no plain secrets
remain in the source.
- Around line 48-50: Replace the hardcoded PostgreSQL URI passed to
SemanticMemory (the memory variable initialization) with a value read from an
environment variable (e.g., POSTGRES_CONNECTION_STRING); update the code that
constructs SemanticMemory to fetch os.environ.get("POSTGRES_CONNECTION_STRING")
and either use a sensible default or raise an error/log a clear message if the
env var is missing, so credentials are not hardcoded in the repo.
- Line 5: Replace the hardcoded absolute path in the sys.path.insert call (the
line using sys.path.insert(0, '/home/...')) because it breaks other
environments; either remove that line and rely on the package being
installed/importable, or replace it with a relative-path insertion using os.path
and __file__ (e.g., compute the repo/src path with os.path.dirname(__file__) and
os.path.join) so the test locates the local src directory portably; update the
import strategy and remove any machine-specific literal path.

In `@docker-compose.yml`:
- Around line 45-52: Replace the hardcoded Neo4j password in the NEO4J_AUTH env
var and the healthcheck command by using an environment variable (e.g.,
NEO4J_PASSWORD) or Docker secret instead of the literal "founderos_secret";
update the NEO4J_AUTH value to use substitution (e.g.,
"neo4j/${NEO4J_PASSWORD}") and change the healthcheck test (the cypher-shell
invocation) to read the password from that same env var or secret; ensure the
new NEO4J_PASSWORD is provided via a .env (gitignored) or Docker secret and not
checked into version control.

In `@fullstack/app/api/actions/`[id]/route.ts:
- Around line 82-98: The current fire-and-forget call to
`${AI_SERVICE_URL}/execute_action` (using proposal, id, AI_SERVICE_URL) needs a
timeout and success handling: wrap the fetch with an AbortController +
setTimeout (e.g., 5s), await the response, check response.ok and treat non-2xx
as a logged error, and on successful response update the proposal record/status
to "executed" (use your existing update mechanism for the proposal identified by
id) rather than just logging; ensure errors or timeouts are caught and logged
but do not throw so the overall approval flow still succeeds.

In `@fullstack/app/api/actions/route.ts`:
- Around line 70-83: The fetch to `${AI_SERVICE_URL}/process_event` (the call
that sets const res) has no timeout; wrap it with an AbortController, pass
controller.signal into fetch, start a timer (e.g., from a configurable
AI_SERVICE_TIMEOUT ms) that calls controller.abort() on expiry, and clear the
timer after fetch completes; handle AbortError by throwing a descriptive timeout
Error (instead of a generic network error) so callers know the AI service
request timed out.
- Around line 33-36: The current orderBy on the 'urgency' string field in
route.ts will sort alphabetically, not by priority; change the Prisma schema to
make urgency an enum (e.g., enum Urgency { CRITICAL HIGH MEDIUM LOW }) or an
integer (1=critical..4=low), run a migration so Prisma types update, then update
any code that creates/updates the model to use the new enum/number values and
adjust the orderBy in the query (use the enum/number field and the appropriate
asc/desc) so sorting returns critical before high before medium before low.

In `@fullstack/app/api/ai/decide/CLAUDE.md`:
- Around line 1-11: Add a gitignore rule to exclude generated CLAUDE.md files so
they aren’t committed and don’t cause merge conflicts: update the root
.gitignore to include a pattern like **/CLAUDE.md (or the exact line "#
Auto-generated AI memory context files" followed by **/CLAUDE.md), remove the
five committed CLAUDE.md files from the index with git rm --cached (leave them
locally) and commit the .gitignore change; alternatively, move their generation
to the build/runtime step instead of committing them (files referenced:
fullstack/app/api/ai/decide/CLAUDE.md, fullstack/prisma/CLAUDE.md,
ai-service/src/ai_service/integrations/CLAUDE.md,
ai-service/src/ai_service/agent/CLAUDE.md,
ai-service/src/ai_service/graphs/CLAUDE.md).

In `@fullstack/components/inbox/Inbox.tsx`:
- Around line 16-33: Remove the duplicated local type definitions
(ActionProposalStatus, ActionUrgency, ActionVertical, ActionProposal) and import
the canonical types from fullstack/lib/types.ts instead; update any references
in this file (e.g., the Inbox component props or state that use ActionProposal)
to use the imported ActionProposal, and ensure the imported interface includes
the missing fields noted (event_id, approver_id, rejection_reason) so the shape
matches the shared definition.

In `@fullstack/lib/types.ts`:
- Around line 9-30: The Inbox.tsx duplicates the ActionProposal-related types
and drifts from lib/types.ts (ActionProposalStatus, ActionUrgency,
ActionVertical, ActionType, and ActionProposal); update
fullstack/components/inbox/Inbox.tsx to remove the local type declarations and
import ActionProposal, ActionType, ActionProposalStatus, ActionUrgency and
ActionVertical from fullstack/lib/types.ts, ensure the Inbox component and any
usages accept the stricter ActionType union and include the missing fields
event_id, approver_id, and rejection_reason present on ActionProposal so the
component's props and state align with the canonical types.

In `@fullstack/prisma/schema.prisma`:
- Around line 39-65: The ActionProposal Prisma model is missing the confidence
field referenced elsewhere; add a confidence Float with a sensible default
(e.g., `@default`(0.8)) to the ActionProposal model so it matches the
ActionProposal interface in fullstack/lib/types.ts, the example in
IMPLEMENTATION_SUMMARY.md, and the frontend Inbox display; update the model
definition for ActionProposal to include confidence Float `@default`(0.8)
alongside the other fields.
🟡 Minor comments (29)
ai-service/test_connections.py-69-69 (1)

69-69: Use the public delete_collection() method instead of accessing _vector_store directly.

SemanticMemory provides a public async delete_collection() method. Replace memory._vector_store.delete_collection() with await memory.delete_collection().

ai-service/src/ai_service/agent/tech_debt.py-156-164 (1)

156-164: Multi-line comment handling is incomplete.

Lines 156-160 check for multi-line comment starts (/*, --, <!--) but don't track state to skip subsequent lines until the closing delimiter. TODOs inside multi-line comments (after the first line) will still be counted.

Consider adding multi-line comment state tracking
     in_docstring = False
     docstring_char = None
+    in_multiline_comment = False

     for line in diff.split("\n"):
         stripped = line.strip()

+        # Track multi-line comment state
+        if "/*" in stripped and "*/" not in stripped:
+            in_multiline_comment = True
+            continue
+        if in_multiline_comment:
+            if "*/" in stripped:
+                in_multiline_comment = False
+            continue
+
         # Handle docstrings (both single-line and multi-line)
         ...
-
-        # Skip multi-line comments (opening - already inside)
-        if stripped.startswith("/*") or stripped.startswith("--"):
-            continue
-        if stripped.startswith("<!--"):
-            continue
ai-service/src/ai_service/infrastructure/checkpointer.py-131-139 (1)

131-139: Fix typo "urable" → "durable" and annotate mutable class attributes.

The key "urable" appears to be a typo for "durable". Additionally, mutable class attributes should be annotated with ClassVar per Python best practices.

Proposed fix
+from typing import ClassVar
+
 class GraphCheckpointerConfig:
     """Pre-configured checkpointer settings for different deployment modes."""

-    DEVELOPMENT = {
+    DEVELOPMENT: ClassVar[dict] = {
         "checkpointer": "memory",  # Use in-memory for dev
-        "urable": False,
+        "durable": False,
     }

-    PRODUCTION = {
+    PRODUCTION: ClassVar[dict] = {
         "checkpointer": "postgres",
-        "urable": True,
+        "durable": True,
     }
IMPLEMENTATION_SUMMARY.md-1367-1395 (1)

1367-1395: Add language specifier to test result code blocks.

The test result listings should have a language specifier for proper rendering.

📝 Fix code block syntax
-```
+```text
 ai-service/tests/integration/test_vertical_agents.py

And:

-```
+```text
 fullstack/tests/inbox.test.tsx

Also applies to: 1399-1418

founderos.md-77-85 (1)

77-85: Fix code block language specifiers for policy examples.

Same issue - the language specifier is inside the code block content.

📝 Fix code block syntax
-```
-text# Deployment Policy
+```text
+# Deployment Policy

And similarly for tech_health.md:

-```
-text# Tech Health Policy
+```text
+# Tech Health Policy

Also applies to: 89-96

ai-service/src/ai_service/agent/workflow.py-602-616 (1)

602-616: Unused variables approval_id and decision.

Static analysis flagged these as assigned but never used. Either use them or remove the assignments.

🐛 Remove unused variable assignments
     requires_approval = state.get("requires_approval", False)
-    approval_id = state.get("approval_id")
     resume_value = state.get("resume_value")

     # If no approval required, pass through
     if not requires_approval:
         return {
             **state,
             "human_approved": None,
         }

     # If we have a resume value, process the decision
     if resume_value:
         approved = resume_value.get("approved")
-        decision = resume_value.get("decision", "reject")
ai-service/src/ai_service/agent/workflow.py-446-452 (1)

446-452: Unused loop variable k2 - rename to _k2.

Static analysis correctly identified that k2 is assigned but never used in the nested loop.

🐛 Fix unused variable
         for k, v in context.items():
             if isinstance(v, dict):
-                for k2, v2 in v.items():
+                for _k2, v2 in v.items():
                     context_lines.append(f"*{k}*: {v2}")

Or if the intent was to include the nested key:

-                context_lines.append(f"*{k}*: {v2}")
+                context_lines.append(f"*{k}.{k2}*: {v2}")
founderos.md-129-155 (1)

129-155: Fix Python code block language specifier.

The python identifier should be after the backticks, not on the first line of content.

📝 Fix code block syntax
-```
-python# agent/debt_agent.py
+```python
+# agent/debt_agent.py
founderos.md-159-188 (1)

159-188: Fix Python code block for Slack integration example.

📝 Fix code block syntax
-```
-python# integrations/slack.py
+```python
+# integrations/slack.py
IMPLEMENTATION_SUMMARY.md-1311-1322 (1)

1311-1322: Fix GitHub capitalization for consistency.

Static analysis correctly identified that "GitHub" should be capitalized with a capital "H".

📝 Fix capitalization
-| `github.deploy` | release | smoke_test_verification |
+| `GitHub.deploy` | release | smoke_test_verification |
...
-| `github.commit` | team_pulse | no_action, sentiment_check |
-| `github.activity` | team_pulse | calendar_invite, sentiment_check |
+| `GitHub.commit` | team_pulse | no_action, sentiment_check |
+| `GitHub.activity` | team_pulse | calendar_invite, sentiment_check |

Note: This is for display purposes in documentation. The actual event type strings in code should remain lowercase for consistency with the API.

IMPLEMENTATION_SUMMARY.md-389-396 (1)

389-396: Return type inconsistency in human_approval_node.

The function signature indicates it returns ReleaseHygieneState but line 392 returns ActionProposalState. This is inconsistent with the actual implementation shown in the relevant code snippets.

📝 Fix return type
-def human_approval_node(state: ReleaseHygieneState) -> ReleaseHygieneState:
+def human_approval_node(state: ReleaseHygieneState) -> ActionProposalState:
     """Handle human approval for release actions."""
     # Default approval handling
     return ActionProposalState(

Or keep the return type and change the return:

-    return ActionProposalState(
+    return ReleaseHygieneState(
IMPLEMENTATION_SUMMARY.md-843-846 (1)

843-846: Syntax error in embedded Python code - mixed comment styles.

Lines 845 and 925 use // for comments which is invalid Python syntax. This appears to be a copy-paste error mixing JavaScript comment style.

📝 Fix Python comment syntax
 # =============================================================================
 # ExecOps Endpoints (New)
-// =============================================================================
+# =============================================================================

And at line 925:

 # =============================================================================
 # Legacy Endpoints (Deprecated)
-// =============================================================================
+# =============================================================================
IMPLEMENTATION_SUMMARY.md-26-68 (1)

26-68: Fix fenced code block language specifier.

The architecture diagram code block is missing a language specifier, which triggers markdownlint MD040.

📝 Add language specifier
-```
+```text
 ┌─────────────────────────────────────────────────────────────────┐
founderos.md-36-69 (1)

36-69: Fix fenced code block language specifier.

The language specifier should be after the opening backticks, not inside the code block. The text appears as content rather than specifying the language.

📝 Fix code block syntax
-```
-text┌─────────────────────────────────────────────────────────────┐
+```text
+┌─────────────────────────────────────────────────────────────┐
ai-service/src/ai_service/agent/workflow.py-664-700 (1)

664-700: Unused variable channel and consider moving return to else block.

Line 671 assigns channel but never uses it. Also, static analysis suggests moving the success return to an else block for cleaner error handling.

🐛 Clean up handle_approval_callback
     user = callback_data.get("user", {}).get("id", "unknown")
-    channel = callback_data.get("channel", {}).get("id", "unknown")

     if action_id not in ("approve", "reject"):
         return {"ok": False, "error": "Unknown action"}

     if not approval_manager:
         # Return mock response for testing
         return {
             "ok": True,
             "approval_id": approval_id,
             "decision": action_id,
             "user": user,
         }

     try:
         state = await approval_manager.process_decision(
             approval_id=approval_id,
             decision=action_id,
             approver=user,
         )
-
-        return {
+    except ValueError as e:
+        return {"ok": False, "error": str(e)}
+    else:
+        return {
             "ok": True,
             "approval_id": approval_id,
             "decision": action_id,
             "workflow_id": state.workflow_id,
             "status": state.status,
         }
-    except ValueError as e:
-        return {"ok": False, "error": str(e)}
fullstack/app/api/actions/[id]/reject/route.ts-17-18 (1)

17-18: Handle malformed JSON body gracefully.

The req.json() call will throw if the request body is invalid JSON, resulting in a 500 error. The approve route in [id]/route.ts handles this with .catch(() => ({})).

Proposed fix
-    const body = await req.json();
+    const body = await req.json().catch(() => ({}));
fullstack/app/api/actions/route.ts-23-24 (1)

23-24: Validate parsed integers to prevent NaN values.

parseInt returns NaN for invalid strings, which could cause unexpected behavior with Prisma's take and skip options.

Proposed fix
-    const limit = parseInt(searchParams.get("limit") || "50");
-    const offset = parseInt(searchParams.get("offset") || "0");
+    const limit = Math.min(Math.max(parseInt(searchParams.get("limit") || "50") || 50, 1), 100);
+    const offset = Math.max(parseInt(searchParams.get("offset") || "0") || 0, 0);
ai-service/tests/integration/test_human_approval.py-400-413 (1)

400-413: Incomplete test implementation.

This test creates a state variable but has only pass in the body. Either implement the test logic or remove it to avoid confusion.

Option 1: Implement the test
def test_node_returns_interrupt_for_pending(self):
    """Node should interrupt when approval is pending."""
    from ai_service.agent.workflow import human_approval_node

    state = {
        "decision": "warn",
        "requires_approval": True,
        "workflow_id": "wf_test",
        "approval_id": None,
    }

    result = human_approval_node(state)
    
    # Verify the node signals that approval is needed
    assert result.get("needs_interrupt", False) is True
    # Or check for specific interrupt behavior based on implementation
Option 2: Mark as pending
+    `@pytest.mark.skip`(reason="Implementation pending - LangGraph interrupt behavior")
     def test_node_returns_interrupt_for_pending(self):
ai-service/src/ai_service/graphs/runway_money.py-276-276 (1)

276-276: Unused variable requires_approval from analysis.

The requires_approval value is fetched from analysis but is immediately shadowed by the approval_required logic below (lines 278-284). This variable can be removed as flagged by static analysis.

🔧 Suggested fix
     draft = state.get("draft_action", {})
     action_type = draft.get("action_type", "")
     analysis = state.get("analysis", {})
-    requires_approval = analysis.get("requires_approval", False)
+    # Note: approval_required is determined by action_type, not analysis
ai-service/tests/integration/test_guardrails_e2e.py-95-120 (1)

95-120: Incomplete test: created state is not used in the test.

The test creates an InvoiceContext and state dict but the process_webhook call doesn't use them. The comment on lines 119-120 acknowledges this, but the test doesn't actually validate the intended Stripe invoice approval flow. As flagged by static analysis, state (line 109) and result (line 114) are assigned but never used.

💡 Consider completing the test or adding a TODO
     def test_stripe_invoice_approve(self):
-        """Test Stripe invoice within budget gets approved."""
+        """Test Stripe invoice within budget gets approved.
+        
+        TODO: This test needs to be updated to properly pass invoice_context
+        through the webhook event payload once the integration is complete.
+        """
         from ai_service.agent.supervisor import process_webhook
         from ai_service.integrations.stripe import InvoiceContext

         # Create test invoice
         invoice = InvoiceContext(
             invoice_id="in_test123",
             customer_id="cus_test",
             amount=5000,  # $50
             currency="usd",
             vendor="Vercel",
         )

-        state = {
-            "event_type": "stripe_invoice",
-            "invoice_context": invoice,
-        }
-
-        result = process_webhook(
+        # Note: Full integration test pending - currently just validates
+        # that process_webhook handles stripe_invoice event type
+        _ = process_webhook(
             event_type="stripe_invoice",
-            webhook_event={"type": "invoice.payment_succeeded"},
+            webhook_event={
+                "type": "invoice.payment_succeeded",
+                "invoice_context": invoice,  # Pass context in webhook
+            },
             webhook_action="created",
         )
-        # This will use the invoice_context from state
-        # For full integration, invoice_context should be passed in webhook_event
ai-service/src/ai_service/graphs/release_hygiene.py-22-47 (1)

22-47: ready_to_execute field missing from TypedDict.

Same issue as TeamPulseState - the human_approval_node sets ready_to_execute (line 252) but this field is not declared in ReleaseHygieneState.

ai-service/src/ai_service/graphs/customer_fire.py-22-47 (1)

22-47: ready_to_execute field missing from TypedDict.

Same consistency issue as other vertical State types - the field is set in human_approval_node (line 292) but not declared in the TypedDict.

ai-service/src/ai_service/graphs/customer_fire.py-222-232 (1)

222-232: Dead code: standard_triage branch is unreachable.

The check_vip_node now always sets action_type to either "senior_assign" or "apology_email" (never "standard_triage"), making this else branch unreachable.

Either restore the standard_triage option in check_vip_node or remove this dead code.

ai-service/src/ai_service/graphs/team_pulse.py-292-296 (1)

292-296: ready_to_execute field is not defined in TeamPulseState.

The ready_to_execute field is being set on line 296, but it's not declared in the TeamPulseState TypedDict (lines 22-47). This creates a type inconsistency. While Python allows extra keys at runtime, static type checkers will flag this as an error.

Consider adding ready_to_execute: bool to the TypedDict definition for type safety and consistency with ActionProposalState.

🔧 Suggested fix for TypedDict

Add to TeamPulseState definition around line 45:

     # Approval workflow
     approval_required: bool
     approval_decision: str | None
     approver_id: str | None
     rejection_reason: str | None

+    # Execution tracking
+    ready_to_execute: bool
+
     # Error handling
     error: str | None
fullstack/components/inbox/Inbox.tsx-319-323 (1)

319-323: Props onApprove and onReject from InboxProps are not utilized.

The Inbox component accepts onApprove and onReject props but never uses them. It only uses the internal handleAction. Either remove these props or integrate them as callbacks.

Suggested fix - integrate the callbacks
   const handleAction = async (id: string, action: "approve" | "reject") => {
     setProcessingId(id);
     try {
+      // Call external handler if provided
+      if (action === "approve" && onApprove) {
+        await onApprove(id);
+      } else if (action === "reject" && onReject) {
+        await onReject(id);
+      }
       const res = await fetch(`/api/actions/${id}/${action}`, { method: "POST" });

Or remove unused props:

-interface InboxProps {
-  initialProposals?: ActionProposal[];
-  onApprove?: (id: string) => Promise<void>;
-  onReject?: (id: string) => Promise<void>;
-}
+interface InboxProps {
+  initialProposals?: ActionProposal[];
+}
 
-export function Inbox({ initialProposals = [], onApprove, onReject }: InboxProps) {
+export function Inbox({ initialProposals = [] }: InboxProps) {

Also applies to: 325-326

ai-service/src/ai_service/integrations/stripe.py-382-386 (1)

382-386: Redundant condition in duplicate vendor check.

The condition on lines 383-386 is logically redundant:

is_duplicate = invoice.vendor in duplicate_vendors or (
    invoice.vendor in known_vendors and
    invoice.vendor in duplicate_vendors  # This is already checked above
)

The second part of the or requires invoice.vendor in duplicate_vendors, which is already the first condition.

Proposed fix
     # Check for duplicate vendor
-    is_duplicate = invoice.vendor in duplicate_vendors or (
-        invoice.vendor in known_vendors and
-        invoice.vendor in duplicate_vendors
-    )
+    is_duplicate = invoice.vendor in duplicate_vendors
ai-service/src/ai_service/agent/supervisor.py-477-482 (1)

477-482: Remove extraneous f-string prefix.

Line 478 has an f-string without any placeholders, as flagged by static analysis.

Proposed fix
     lines = [
-        f"👤 *Human Approval Required*",
+        "👤 *Human Approval Required*",
         "",
ai-service/src/ai_service/main.py-242-251 (1)

242-251: Typo in response key: extra space in " verticals".

There's a leading space in the dictionary key " verticals" which will cause issues when clients try to access this field.

Proposed fix
         "sops": [],
         "message": "SOPs are replaced by vertical agents. Use /process_event instead.",
-        " verticals": [
+        "verticals": [
             {"id": "release", "name": "Release Hygiene", "triggers": ["sentry.error", "github.deploy"]},
fullstack/components/inbox/Inbox.tsx-369-391 (1)

369-391: Missing user feedback on action failure and button disabled state during processing.

The handleAction function catches errors and logs to console, but doesn't inform the user. Also, processingId is tracked but not used to disable buttons during processing.

Suggested improvements
+  const [error, setError] = useState<string | null>(null);
+
   const handleAction = async (id: string, action: "approve" | "reject") => {
     setProcessingId(id);
+    setError(null);
     try {
       const res = await fetch(`/api/actions/${id}/${action}`, { method: "POST" });
-      if (res.ok) {
+      if (!res.ok) {
+        throw new Error(`Failed to ${action} proposal`);
+      }
         setProposals((prev) =>
           prev.map((p) =>
             ...
           )
         );
-      }
     } catch (error) {
       console.error(`Failed to ${action} proposal:`, error);
+      setError(`Failed to ${action} proposal. Please try again.`);
     } finally {
       setProcessingId(null);
     }
   };

And update the buttons:

             <button
               onClick={() => onApprove(proposal.id)}
+              disabled={processingId === proposal.id}
               className="flex-1 px-4 py-2 bg-green-600 text-white ..."
             >
-              Approve
+              {processingId === proposal.id ? "Processing..." : "Approve"}
             </button>

Comment on lines +55 to +78
def route_event_to_agent(state: dict) -> str:
"""Route webhook event to the appropriate agent.
Args:
state: Current state with event_type
Returns:
Agent name to route to: "sre_agent", "cfo_agent", "tech_debt_agent", or "unknown"
"""
event_type = state.get("event_type", "")

routing_map = {
"pull_request": "sre_agent",
"github_pull_request": "sre_agent",
"stripe_invoice": "cfo_agent",
"stripe": "cfo_agent",
"tech_debt_alert": "tech_debt_agent",
"tech_debt": "tech_debt_agent",
}

agent = routing_map.get(event_type, "unknown")

logger.info(f"Routing event type '{event_type}' to agent '{agent}'")
return agent
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Duplicate routing logic in two places.

The routing map is defined twice: once in route_event_to_agent() (lines 66-73) and again in route_to_agent() inside create_guardrails_agent() (lines 567-574). This violates DRY and risks divergence.

Suggested refactor to use a single source of truth
+# Module-level routing map (single source of truth)
+EVENT_ROUTING_MAP = {
+    "pull_request": "sre_agent",
+    "github_pull_request": "sre_agent",
+    "stripe_invoice": "cfo_agent",
+    "stripe": "cfo_agent",
+    "tech_debt_alert": "tech_debt_agent",
+    "tech_debt": "tech_debt_agent",
+}
+
 def route_event_to_agent(state: dict) -> str:
     """Route webhook event to the appropriate agent."""
     event_type = state.get("event_type", "")
-
-    routing_map = {
-        "pull_request": "sre_agent",
-        ...
-    }
-
-    agent = routing_map.get(event_type, "unknown")
+    agent = EVENT_ROUTING_MAP.get(event_type, "unknown")
     logger.info(f"Routing event type '{event_type}' to agent '{agent}'")
     return agent

Then reference EVENT_ROUTING_MAP in create_guardrails_agent() as well.

Also applies to: 564-577

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/agent/supervisor.py` around lines 55 - 78, Extract
the routing_map literal into a single module-level constant named
EVENT_ROUTING_MAP and replace the inline maps in route_event_to_agent() and the
route_to_agent() closure inside create_guardrails_agent() to reference
EVENT_ROUTING_MAP.get(event_type, "unknown"); ensure the constant is defined
once at top-level (so both functions use the same source of truth), keep the
existing logging call in route_event_to_agent(), and remove the duplicated
mapping in the closure.

Comment on lines +544 to +585
def create_guardrails_agent() -> StateGraph:
"""Create the unified guardrails StateGraph.
Returns:
Compiled StateGraph with all agents
"""
graph = StateGraph(GuardrailsState)

# Add supervisor node
graph.add_node("supervisor", supervisor_node)

# Add sub-agent nodes
graph.add_node("sre_agent", run_sre_agent)
graph.add_node("cfo_agent", run_cfo_agent)
graph.add_node("tech_debt_agent", run_tech_debt_agent)

# Set entry point
graph.set_entry_point("supervisor")

# Route to appropriate agent based on event type
def route_to_agent(state: GuardrailsState) -> str:
event_type = state.get("event_type", "")

routing = {
"pull_request": "sre_agent",
"github_pull_request": "sre_agent",
"stripe_invoice": "cfo_agent",
"stripe": "cfo_agent",
"tech_debt_alert": "tech_debt_agent",
"tech_debt": "tech_debt_agent",
}

return routing.get(event_type, "supervisor")

graph.add_conditional_edges("supervisor", route_to_agent)

# All agents go to END
graph.add_edge("sre_agent", END)
graph.add_edge("cfo_agent", END)
graph.add_edge("tech_debt_agent", END)

return graph
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the supervisor_node implementation around lines 524-531
sed -n '520,540p' ai-service/src/ai_service/agent/supervisor.py

Repository: Aparnap2/ExecOps

Length of output: 645


🏁 Script executed:

#!/bin/bash
# Let's see the complete supervisor_node function - need more context
sed -n '500,545p' ai-service/src/ai_service/agent/supervisor.py

Repository: Aparnap2/ExecOps

Length of output: 1270


🏁 Script executed:

#!/bin/bash
# Let's check what the agent nodes are - they're defined as graph.add_node("sre_agent", run_sre_agent)
# Let's see the run_sre_agent, run_cfo_agent, run_tech_debt_agent definitions
sed -n '544,585p' ai-service/src/ai_service/agent/supervisor.py

Repository: Aparnap2/ExecOps

Length of output: 1309


🏁 Script executed:

#!/bin/bash
# Let me verify the route_to_agent logic and confirm it routes to agent nodes after supervisor returns
# Also check if there's any state modification that might prevent double execution
rg -n "def route_to_agent|def route_event_to_agent" --type=py -A 15 ai-service/src/ai_service/agent/supervisor.py

Repository: Aparnap2/ExecOps

Length of output: 1172


🏁 Script executed:

#!/bin/bash
# Let's verify this is indeed double execution by checking how these functions are used
# and if there's any state flag that might prevent re-execution
rg -n "final_decision|agent_name" ai-service/src/ai_service/agent/supervisor.py | head -20

Repository: Aparnap2/ExecOps

Length of output: 762


Redundant agent execution in graph structure.

The supervisor_node directly executes agents (lines 524-531), but create_guardrails_agent() then routes the state to the same agent nodes via conditional edges (line 578). This causes agents to execute twice:

  1. Inside supervisor_node: run_sre_agent(state), run_cfo_agent(state), or run_tech_debt_agent(state)
  2. Via graph routing: supervisor → conditional edge → agent node

Fix by either:

  • Having supervisor only update state without executing agents; let graph routing handle execution
  • Or removing conditional edges and routing directly to END after supervisor

Comment on lines +280 to +289
class AgentState(TypedDict):
"""Extended agent state for tech debt analysis."""

pr_info: dict
diff_files: list[dict]
tech_debt_report: dict | None
decision: str
confidence: float
reason: str

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find all AgentState TypedDict definitions
ast-grep --pattern 'class AgentState(TypedDict):
  $$$
'

Repository: Aparnap2/ExecOps

Length of output: 176


🏁 Script executed:

# Check the state.py file for AgentState definition
fd state.py ai-service

Repository: Aparnap2/ExecOps

Length of output: 100


🏁 Script executed:

# Check imports in tech_debt.py
head -50 ai-service/src/ai_service/agent/tech_debt.py

Repository: Aparnap2/ExecOps

Length of output: 1490


🏁 Script executed:

# Read state.py to check for AgentState definition
cat ai-service/src/ai_service/agent/state.py

Repository: Aparnap2/ExecOps

Length of output: 3585


🏁 Script executed:

# Check __init__.py for exports
cat ai-service/src/ai_service/agent/__init__.py

Repository: Aparnap2/ExecOps

Length of output: 623


🏁 Script executed:

# Read tech_debt.py around lines 280-289 and before to see the complete context
sed -n '1,310p' ai-service/src/ai_service/agent/tech_debt.py | tail -60

Repository: Aparnap2/ExecOps

Length of output: 1496


🏁 Script executed:

# Search for all references to AgentState in the codebase
rg "AgentState" ai-service

Repository: Aparnap2/ExecOps

Length of output: 4347


Remove the local AgentState definition and import it from state.py.

AgentState is already defined and exported from ai-service/src/ai_service/agent/state.py and made available via __init__.py. The local definition at lines 280-289 is a duplicate that diverges from the canonical type (missing ~10 fields), creating type inconsistency and maintenance risk. Replace the local definition with: from .state import AgentState.

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/agent/tech_debt.py` around lines 280 - 289, Remove
the local AgentState TypedDict declaration in tech_debt.py and replace it with
an import from the canonical definition: add "from .state import AgentState" at
the top of the file, delete the AgentState class block (the duplicate TypedDict)
and ensure any references in functions like those in tech_debt.py still use
AgentState from the import; run type checks to confirm the module now uses the
single canonical AgentState type.

Comment on lines +111 to +141
import httpx

if self.bot_token:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://slack.com/api/chat.postMessage",
headers={
"Authorization": f"Bearer {self.bot_token}",
"Content-Type": "application/json",
},
json={
"channel": channel,
"blocks": blocks,
"text": text,
},
)
return response.json()
elif self.webhook_url:
async with httpx.AsyncClient() as client:
response = await client.post(
self.webhook_url,
json={
"channel": channel,
"blocks": blocks,
"text": text,
},
)
return {"ok": True, "channel": channel, "ts": str(datetime.utcnow().timestamp())}
else:
logger.warning("No Slack credentials configured")
return {"ok": False, "error": "No credentials"}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing timeout for HTTP requests to Slack API.

The httpx.AsyncClient calls don't specify a timeout, which could cause the workflow to hang indefinitely if Slack is unresponsive.

🐛 Add request timeout
-        if self.bot_token:
-            async with httpx.AsyncClient() as client:
+        if self.bot_token:
+            async with httpx.AsyncClient(timeout=30.0) as client:
                 response = await client.post(

Apply the same change to the webhook branch and update_message method.

🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/agent/workflow.py` around lines 111 - 141, The HTTP
requests to Slack in the workflow are missing timeouts; update the async calls
in the method that posts messages (e.g., send_message) to create
httpx.AsyncClient with a timeout (or pass timeout to client.post) — e.g.,
httpx.AsyncClient(timeout=10) or httpx.Timeout(10.0) — and apply the same change
in the webhook branch and in the update_message method so all Slack HTTP calls
have a bounded timeout.

Comment on lines +6 to +19
Usage:
from ai_service.infrastructure.checkpointer import get_checkpointer
graph = create_release_hygiene_graph()
checkpointer = get_checkpointer()
# Invoke with checkpointing
result = graph.invoke(
state,
config=RunnableConfig(
configurable={"thread_id": event_id},
checkpointers=[checkpointer]
)
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n ai-service/src/ai_service/infrastructure/checkpointer.py | head -30

Repository: Aparnap2/ExecOps

Length of output: 1094


🌐 Web query:

LangGraph RunnableConfig checkpointer API langgraph 0.2.0 documentation

💡 Result:

Summary — LangGraph 0.2.0: RunnableConfig + Checkpointer API

  • RunnableConfig: the runtime configuration object injected into entrypoint callables (parameter name config: RunnableConfig). Use it to pass configurable values (e.g., thread_id) into runs. [1][3]

  • Checkpointer interface (core methods you implement / use):

    • get(config: RunnableConfig) -> Optional[Checkpoint] / aget(...) — fetch checkpoint for config.
    • get_tuple(config: RunnableConfig) -> Optional[CheckpointTuple] — fetch checkpoint tuple.
    • put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, new_versions: ChannelVersions) -> RunnableConfig — store checkpoint and return updated config.
    • put_writes(config: RunnableConfig, writes: Sequence[tuple[str, Any]], task_id: str, task_path: str = '') — store intermediate writes.
    • delete/adelete_thread (delete all checkpoints for a thread) and other async variants. [2][5]
  • Threading / thread_id: when using a checkpointer you must pass a thread identifier (commonly via RunnableConfig.configurable["thread_id"] or RunnableConfig.thread_id in platform APIs) to identify per-run checkpoint threads. [4][2]

  • Packaging / imports in 0.2.x: checkpointer interfaces are provided via the namespace package langgraph_checkpoint; import from langgraph.checkpoint.base / memory / sqlite / postgres. v0.2 introduced breaking renames (thread_ts -> checkpoint_id, parent_ts -> parent_checkpoint_id). See v0.2 release notes. [3]

Sources

  • Functional API / RunnableConfig injection. [1]
  • Checkpoints / Checkpointer API (get, get_tuple, put, put_writes, async variants, thread semantics). [2]
  • LangGraph v0.2 release notes and checkpointer packaging/renames. [3]

Would you like direct links to any specific API reference pages or example code for implementing a custom checkpointer?


🌐 Web query:

LangGraph graph.invoke checkpointer parameter RunnableConfig 0.2.0

💡 Result:

Short answer: graph.invoke (with a checkpointer) expects a RunnableConfig that contains a "configurable" object with at least a thread_id (string). Optional fields include thread_ts (to fetch a specific checkpoint) and other run-time keys; many checkpointer APIs (get/put/list) accept the RunnableConfig to identify which thread/checkpoint to operate on. Example RunnableConfig:

  • Python dict form:
    {"configurable": {"thread_id": "thread-1"}}
  • Java-ish builder example:
    RunnableConfig.builder().threadId("a").build()

Use this config when calling graph.invoke / agraph.ainvoke so the checkpointer stores/loads checkpoints for that thread. (baihezi.com)

If you want, I can show a minimal code snippet (sync and async) for v0.2.0.

Citations:


🌐 Web query:

LangGraph graph.compile checkpointer usage example

💡 Result:

Here's a minimal Python example showing how to pass a checkpointer to graph.compile, save checkpoints, and resume using a thread_id (uses InMemorySaver for testing; use PostgresSaver or another DB-backed saver in production). Sources: LangGraph docs. [1][2]

Code (Python):

from langgraph.graph import StateGraph, START, MessagesState
from langgraph.checkpoint.memory import InMemorySaver

# simple node that appends a model-like reply
def call_model(state: MessagesState):
    return {"messages": state["messages"] + [{"role": "assistant", "content": "hello"}]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_edge(START, "call_model")

# create checkpointer (in-memory for tests)
checkpointer = InMemorySaver()

# compile graph with checkpointer
graph = builder.compile(checkpointer=checkpointer)

# runtime config must include thread_id so checkpoints are stored under a thread
config = {"configurable": {"thread_id": "thread-1"}}

# run (stream/batch/invoke all supported); here we invoke once
result = graph.invoke({"messages": [{"role": "user", "content": "hi"}]}, config=config)
print(result)

Notes:

  • For production persistence use PostgresSaver (example usage shown in docs). [1][3]

References:

  • LangGraph persistence & checkpointers (how-to, thread_id, InMemory/Postgres examples). [1]
  • MISSING_CHECKPOINTER troubleshooting / compile(checkpointer=...) example. [2]
  • PostgresSaver production example. [3]

Fix the docstring usage example — checkpointer must be passed to graph.compile(), not in RunnableConfig.

The example incorrectly shows checkpointers=[checkpointer] in RunnableConfig. According to LangGraph 0.2.0 API, the checkpointer must be passed when compiling the graph: graph = builder.compile(checkpointer=checkpointer). The RunnableConfig passed to graph.invoke() should only contain the runtime config (e.g., configurable={"thread_id": event_id}), not the checkpointer itself.

Correct usage pattern:
from ai_service.infrastructure.checkpointer import get_checkpointer

graph = create_release_hygiene_graph()
checkpointer = get_checkpointer()

# Compile with checkpointer
compiled_graph = graph.compile(checkpointer=checkpointer)

# Invoke with thread_id in config
result = compiled_graph.invoke(
    state,
    config={"configurable": {"thread_id": event_id}}
)
🤖 Prompt for AI Agents
In `@ai-service/src/ai_service/infrastructure/checkpointer.py` around lines 6 -
19, The docstring example incorrectly passes the checkpointer via
RunnableConfig; instead, call get_checkpointer() and pass it to
graph.compile(checkpointer=checkpointer) (e.g., compiled_graph =
graph.compile(checkpointer=checkpointer)), then call
compiled_graph.invoke(state, config={"configurable": {"thread_id": event_id}})
or use a RunnableConfig containing only runtime settings (no checkpointers).
Ensure references to get_checkpointer, graph.compile, and graph.invoke (or
compiled_graph.invoke) are updated accordingly and remove checkpointers from the
RunnableConfig example.

Comment on lines +70 to +83
// Route to AI service for processing
const res = await fetch(`${AI_SERVICE_URL}/process_event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_type,
event_context: event_context || {},
urgency: urgency || "low",
}),
});

if (!res.ok) {
throw new Error(`AI service error: ${res.statusText}`);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout to external AI service call.

The fetch call to the AI service has no timeout, which could cause requests to hang indefinitely if the service is slow or unresponsive.

Proposed fix using AbortController
     // Route to AI service for processing
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
     const res = await fetch(`${AI_SERVICE_URL}/process_event`, {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({
         event_type,
         event_context: event_context || {},
         urgency: urgency || "low",
       }),
+      signal: controller.signal,
     });
+
+    clearTimeout(timeoutId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Route to AI service for processing
const res = await fetch(`${AI_SERVICE_URL}/process_event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_type,
event_context: event_context || {},
urgency: urgency || "low",
}),
});
if (!res.ok) {
throw new Error(`AI service error: ${res.statusText}`);
}
// Route to AI service for processing
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
const res = await fetch(`${AI_SERVICE_URL}/process_event`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_type,
event_context: event_context || {},
urgency: urgency || "low",
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!res.ok) {
throw new Error(`AI service error: ${res.statusText}`);
}
🤖 Prompt for AI Agents
In `@fullstack/app/api/actions/route.ts` around lines 70 - 83, The fetch to
`${AI_SERVICE_URL}/process_event` (the call that sets const res) has no timeout;
wrap it with an AbortController, pass controller.signal into fetch, start a
timer (e.g., from a configurable AI_SERVICE_TIMEOUT ms) that calls
controller.abort() on expiry, and clear the timer after fetch completes; handle
AbortError by throwing a descriptive timeout Error (instead of a generic network
error) so callers know the AI service request timed out.

Comment on lines +1 to +11
<claude-mem-context>
# Recent Activity

<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->

### Jan 15, 2026

| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #12 | 10:57 AM | 🔵 | [Analyzed Full Project Structure for Refactoring] | ~313 |
</claude-mem-context> No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for files with claude-mem-context tags or auto-generated comments

echo "=== Files with claude-mem-context tags ==="
rg -l "claude-mem-context" --type md 2>/dev/null || echo "No matches found"

echo -e "\n=== Files with auto-generated by claude-mem ==="
rg -l "auto-generated by claude-mem" --type md 2>/dev/null || echo "No matches found"

echo -e "\n=== Checking .gitignore for CLAUDE.md exclusions ==="
fd -t f "^\.gitignore$" -x grep -H "CLAUDE" {} 2>/dev/null || echo "No CLAUDE.md entries in .gitignore"

echo -e "\n=== All .gitignore files in repo ==="
fd -t f "^\.gitignore$"

echo -e "\n=== Content of root .gitignore (first 50 lines) ==="
if [ -f ".gitignore" ]; then
  head -50 .gitignore
else
  echo "No root .gitignore found"
fi

Repository: Aparnap2/ExecOps

Length of output: 1346


Exclude auto-generated claude-mem files from version control across the codebase.

This file is part of a pattern: there are 5 auto-generated CLAUDE.md files committed to version control (fullstack/prisma/CLAUDE.md, fullstack/app/api/ai/decide/CLAUDE.md, ai-service/src/ai_service/integrations/CLAUDE.md, ai-service/src/ai_service/agent/CLAUDE.md, and ai-service/src/ai_service/graphs/CLAUDE.md), none of which are currently excluded in .gitignore. These files contain timestamped activity data that regenerates frequently, leading to:

  • Merge conflicts when multiple branches regenerate the same files
  • Noisy git history filled with mechanical updates
  • Maintenance overhead for resolving conflicts

Add this to the root .gitignore:

# Auto-generated AI memory context files
**/CLAUDE.md

Alternatively, if this context is needed at runtime, generate these files during the build process rather than committing them to version control.

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

10-10: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
In `@fullstack/app/api/ai/decide/CLAUDE.md` around lines 1 - 11, Add a gitignore
rule to exclude generated CLAUDE.md files so they aren’t committed and don’t
cause merge conflicts: update the root .gitignore to include a pattern like
**/CLAUDE.md (or the exact line "# Auto-generated AI memory context files"
followed by **/CLAUDE.md), remove the five committed CLAUDE.md files from the
index with git rm --cached (leave them locally) and commit the .gitignore
change; alternatively, move their generation to the build/runtime step instead
of committing them (files referenced: fullstack/app/api/ai/decide/CLAUDE.md,
fullstack/prisma/CLAUDE.md, ai-service/src/ai_service/integrations/CLAUDE.md,
ai-service/src/ai_service/agent/CLAUDE.md,
ai-service/src/ai_service/graphs/CLAUDE.md).

Comment on lines +16 to +33
export type ActionProposalStatus = "pending" | "pending_approval" | "approved" | "rejected" | "executed";
export type ActionUrgency = "low" | "medium" | "high" | "critical";
export type ActionVertical = "release" | "customer_fire" | "runway" | "team_pulse";

export interface ActionProposal {
id: string;
status: ActionProposalStatus;
urgency: ActionUrgency;
vertical: ActionVertical;
action_type: string;
payload: Record<string, unknown>;
reasoning: string;
context_summary: string;
confidence: number;
created_at: string;
approved_at: string | null;
executed_at: string | null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Duplicate type definitions - import from shared types instead.

These types (ActionProposalStatus, ActionUrgency, ActionVertical, ActionProposal) are already defined in fullstack/lib/types.ts. Duplicating them here risks divergence and violates DRY.

Suggested fix
 "use client";
 
 import { useState, useEffect } from "react";
+import type { 
+  ActionProposalStatus, 
+  ActionUrgency, 
+  ActionVertical, 
+  ActionProposal 
+} from "@/lib/types";
 
 // =============================================================================
-// Types
+// Constants
 // =============================================================================
-
-export type ActionProposalStatus = "pending" | "pending_approval" | "approved" | "rejected" | "executed";
-export type ActionUrgency = "low" | "medium" | "high" | "critical";
-export type ActionVertical = "release" | "customer_fire" | "runway" | "team_pulse";
-
-export interface ActionProposal {
-  id: string;
-  status: ActionProposalStatus;
-  ...
-}

Note: The local ActionProposal interface is missing fields present in lib/types.ts (event_id, approver_id, rejection_reason).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type ActionProposalStatus = "pending" | "pending_approval" | "approved" | "rejected" | "executed";
export type ActionUrgency = "low" | "medium" | "high" | "critical";
export type ActionVertical = "release" | "customer_fire" | "runway" | "team_pulse";
export interface ActionProposal {
id: string;
status: ActionProposalStatus;
urgency: ActionUrgency;
vertical: ActionVertical;
action_type: string;
payload: Record<string, unknown>;
reasoning: string;
context_summary: string;
confidence: number;
created_at: string;
approved_at: string | null;
executed_at: string | null;
}
"use client";
import { useState, useEffect } from "react";
import type {
ActionProposalStatus,
ActionUrgency,
ActionVertical,
ActionProposal
} from "@/lib/types";
// =============================================================================
// Constants
// =============================================================================
🤖 Prompt for AI Agents
In `@fullstack/components/inbox/Inbox.tsx` around lines 16 - 33, Remove the
duplicated local type definitions (ActionProposalStatus, ActionUrgency,
ActionVertical, ActionProposal) and import the canonical types from
fullstack/lib/types.ts instead; update any references in this file (e.g., the
Inbox component props or state that use ActionProposal) to use the imported
ActionProposal, and ensure the imported interface includes the missing fields
noted (event_id, approver_id, rejection_reason) so the shape matches the shared
definition.

Comment on lines +9 to +30
export type ActionProposalStatus = "pending" | "pending_approval" | "approved" | "rejected" | "executed";
export type ActionUrgency = "low" | "medium" | "high" | "critical";
export type ActionVertical = "release" | "customer_fire" | "runway" | "team_pulse";
export type ActionType = "email" | "command" | "slack_dm" | "webhook" | "api_call";

export interface ActionProposal {
id: string;
status: ActionProposalStatus;
urgency: ActionUrgency;
vertical: ActionVertical;
action_type: ActionType;
payload: Record<string, unknown>;
reasoning: string;
context_summary: string;
confidence: number;
event_id: string | null;
created_at: string;
approved_at: string | null;
executed_at: string | null;
approver_id: string | null;
rejection_reason: string | null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify duplicate type definitions in Inbox.tsx
rg -n "ActionProposalStatus|ActionUrgency|ActionVertical|ActionProposal" fullstack/components/inbox/Inbox.tsx -A 3

Repository: Aparnap2/ExecOps

Length of output: 4823


🏁 Script executed:

#!/bin/bash
# Check imports in Inbox.tsx
head -30 fullstack/components/inbox/Inbox.tsx

Repository: Aparnap2/ExecOps

Length of output: 989


🏁 Script executed:

#!/bin/bash
# Check complete ActionProposal interface in Inbox.tsx
sed -n '16,40p' fullstack/components/inbox/Inbox.tsx

Repository: Aparnap2/ExecOps

Length of output: 989


🏁 Script executed:

#!/bin/bash
# Check ActionType definition in lib/types.ts
head -10 fullstack/lib/types.ts | grep -A 1 "ActionType"

Repository: Aparnap2/ExecOps

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for ActionType definition in lib/types.ts
rg "ActionType" fullstack/lib/types.ts -A 1

Repository: Aparnap2/ExecOps

Length of output: 210


Consolidate duplicate type definitions from Inbox.tsx to lib/types.ts.

The ActionProposalStatus, ActionUrgency, ActionVertical, and ActionProposal types are duplicated in fullstack/components/inbox/Inbox.tsx (lines 16–35). The Inbox version has two critical differences:

  • action_type: string instead of the stricter ActionType union type ("email" | "command" | "slack_dm" | "webhook" | "api_call")
  • Missing fields: event_id, approver_id, and rejection_reason

Update Inbox.tsx to import these types from lib/types.ts to eliminate drift and ensure consistency across the codebase.

🤖 Prompt for AI Agents
In `@fullstack/lib/types.ts` around lines 9 - 30, The Inbox.tsx duplicates the
ActionProposal-related types and drifts from lib/types.ts (ActionProposalStatus,
ActionUrgency, ActionVertical, ActionType, and ActionProposal); update
fullstack/components/inbox/Inbox.tsx to remove the local type declarations and
import ActionProposal, ActionType, ActionProposalStatus, ActionUrgency and
ActionVertical from fullstack/lib/types.ts, ensure the Inbox component and any
usages accept the stricter ActionType union and include the missing fields
event_id, approver_id, and rejection_reason present on ActionProposal so the
component's props and state align with the canonical types.

Comment on lines +39 to +65
model ActionProposal {
id String @id @default(uuid())
status String @default("pending") // pending, approved, rejected, executed
urgency String // low, high, critical
vertical String // release, customer_fire, runway, team_pulse
// The Action
action_type String // api_call, email, slack_dm, webhook, command
payload Json // {to: '...', body: '...', command: '...'}
reasoning String // Why this action was proposed (LLM + Rules)
// Context
context_summary String // Short summary for inbox display
event_id String? // FK to source event
event Event? @relation(fields: [event_id], references: [id])
// Audit
created_at DateTime @default(now())
approved_at DateTime?
executed_at DateTime?
approver_id String? // User who approved
rejection_reason String?
@@index([status, urgency])
@@index([vertical, created_at])
@@index([event_id])
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing confidence field in ActionProposal model.

The ActionProposal model is missing the confidence field that is referenced in multiple places:

  • fullstack/lib/types.ts defines confidence: number in the ActionProposal interface
  • IMPLEMENTATION_SUMMARY.md shows confidence Float @default(0.8) in the schema
  • The frontend Inbox component displays confidence percentage

Add the confidence field to maintain consistency.

🐛 Add missing confidence field
   // Context
   context_summary String  // Short summary for inbox display
   event_id     String?   // FK to source event
   event        Event?    `@relation`(fields: [event_id], references: [id])
+  
+  // Confidence score from AI analysis
+  confidence   Float     `@default`(0.8)

   // Audit
   created_at   DateTime `@default`(now())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
model ActionProposal {
id String @id @default(uuid())
status String @default("pending") // pending, approved, rejected, executed
urgency String // low, high, critical
vertical String // release, customer_fire, runway, team_pulse
// The Action
action_type String // api_call, email, slack_dm, webhook, command
payload Json // {to: '...', body: '...', command: '...'}
reasoning String // Why this action was proposed (LLM + Rules)
// Context
context_summary String // Short summary for inbox display
event_id String? // FK to source event
event Event? @relation(fields: [event_id], references: [id])
// Audit
created_at DateTime @default(now())
approved_at DateTime?
executed_at DateTime?
approver_id String? // User who approved
rejection_reason String?
@@index([status, urgency])
@@index([vertical, created_at])
@@index([event_id])
}
model ActionProposal {
id String `@id` `@default`(uuid())
status String `@default`("pending") // pending, approved, rejected, executed
urgency String // low, high, critical
vertical String // release, customer_fire, runway, team_pulse
// The Action
action_type String // api_call, email, slack_dm, webhook, command
payload Json // {to: '...', body: '...', command: '...'}
reasoning String // Why this action was proposed (LLM + Rules)
// Context
context_summary String // Short summary for inbox display
event_id String? // FK to source event
event Event? `@relation`(fields: [event_id], references: [id])
// Confidence score from AI analysis
confidence Float `@default`(0.8)
// Audit
created_at DateTime `@default`(now())
approved_at DateTime?
executed_at DateTime?
approver_id String? // User who approved
rejection_reason String?
@@index([status, urgency])
@@index([vertical, created_at])
@@index([event_id])
}
🤖 Prompt for AI Agents
In `@fullstack/prisma/schema.prisma` around lines 39 - 65, The ActionProposal
Prisma model is missing the confidence field referenced elsewhere; add a
confidence Float with a sensible default (e.g., `@default`(0.8)) to the
ActionProposal model so it matches the ActionProposal interface in
fullstack/lib/types.ts, the example in IMPLEMENTATION_SUMMARY.md, and the
frontend Inbox display; update the model definition for ActionProposal to
include confidence Float `@default`(0.8) alongside the other fields.

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