Skip to content

feat: Self-Guardian subsystem + CI fix#30

Open
Aparnap2 wants to merge 8 commits into
mainfrom
feat/phase-2-self-guardian
Open

feat: Self-Guardian subsystem + CI fix#30
Aparnap2 wants to merge 8 commits into
mainfrom
feat/phase-2-self-guardian

Conversation

@Aparnap2

@Aparnap2 Aparnap2 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Internal monitoring subsystem that observes agent behavior, detects deviations from declared authority, and generates compliance reports.

Changes

Self-Guardian Subsystem (Phase 2)

  • SchemasAgentObservation, DeviationType (6 values), SelfGuardianAlert, SelfGuardianReport
  • ObservationCollector — thread-safe in-memory buffer (10K cap) with filtered queries
  • SelfGuardianDetector — deterministic analysis against AUTHORITY_MANIFEST:
    • unauthorized_tool — tool not in agent's allowed_tool_ids (critical)
    • external_facing_violation — agent marked external-facing accessing internal data (warning)
    • Failure pattern detection (info)
  • generate_report() — aggregates observations into per-agent summaries
  • 25 unit tests — all passing

CI Fix

  • Replaced actions/setup-python@v5 with astral-sh/setup-uv@v5 across all jobs
  • Fixes local testing with act -P ubuntu-latest=node:20-slim
  • setup-python fails on Debian-slim (no Python 3.13 binaries)
  • setup-uv is lightweight, works on any Linux, matches project's uv dependency

Summary by CodeRabbit

  • New Features

    • Added richer command-center views for alert lineage and operating status.
    • Expanded live event streaming across chat, mission updates, approvals, and sessions.
    • Introduced new local setup targets and Docker stacks for dev, LLM ops, and showcase environments.
    • Added new agent tools for scheduling check-ins, drafting investor updates, pausing payment retries, and flagging churn risk.
  • Bug Fixes

    • Improved push and CI safeguards to prevent broken changes from reaching shared branches.
    • Made mission-state updates and audit logging more reliable and more informative.

Aparnap2 added 8 commits June 28, 2026 19:33
…CE loop, MissionState, docs

- Tool calling surface with ToolRegistry + 4 tools (pause retry, draft update,
  schedule checkin, flag churn) wired to HITL manager with correct string tiers
- SSEHub event-type filtering: Subscription.Types gates Broadcast fan-out;
  3 new SSE endpoints (/events/mission, /events/hitl, /events/session)
- Slack consolidation: extended SocketModeClient for interactive events;
  removed Mockoon dead code from slack.py
- ACE loop completed: _send_feedback_signal() now calls curator.update()
- MissionState: added prepared_brief, pending_decisions, last_updated_by
  in Python dataclass + SQL queries (DB already done in migration 004)
- Updated AGENTS.md, README.md, and all 5 .opencode/context files
- Fixed hitl_queue_test.go: NewHandler now takes 2 args
- pause_payment_retry: Stripe POST /v1/subscriptions/{id} with
  collection_method=send_invoice; httpx-based, MOCK_MODE guard
- draft_investor_update: loads MissionState + chat_completion()
  for bounded (300 token) investor email draft
- schedule_customer_checkin: Slack chat_postMessage via
  SlackClient to configurable channel; mock when no SLACK_BOT_TOKEN
- flag_churn_risk: updates churn_risk_users field via
  get_mission_state() + update_mission_state(); decision journal log
- generate_prepared_brief(): 2-sentence LLM summary triggered
  after MissionState write when prepared_brief is empty
- update_mission_state() accepts generate_brief: bool = True param
…nability, dashboard

- Agent authority manifest: 5 agents with role/permissions/tool allowlists/
  escalation tiers/mission field ownership. Machine-readable Pydantic model.
- Alert lineage: AlertLineage schema attached to every GuardianMessage.
  pattern_id + source_metrics + mission_context + raise_timeline_risk are
  deterministic (code-only). suggested_tool_ids from HITLManager. owner_agent
  from manifest.
- MissionState explainability: 3 new fields (last_update_reason,
  last_changed_fields, active_agent_roles) across Python dataclass, SQL
  migration 005, and Go read model.
- Curator write verification: StrategyDelta Pydantic model for structured
  audit trail. update_strategy_confidence() returns ConfidenceUpdateResult
  + StrategyDelta tuple. PostgreSQL audit with JSONL fallback.
- Dashboard operating layer: 2 new Go handlers (APICommandAlertLineage,
  APICommandOperatingLayer) with HTMX partials. 5 new Go tests.
- All 24 Go web tests pass. Python assertions all pass.
last_changed_fields and active_agent_roles are Python lists but
the DB columns are JSONB. asyncpg requires json.dumps() before
passing list values to JSONB columns. Added import json and
serialization at the parameter boundary.
Add lightweight control plane with agent registry, policy engine, and
audit logging. Implement prompt risk and output risk scanners for
investor-facing workflows. Force HITL approve on all external-facing
outputs. Extend MissionState with policy_state explainability.

New modules:
- src/control_plane/ — registry, policy engine, audit logger
- src/risk/ — prompt risk scanner (9 rules), output risk scanner (9 rules)
- src/schemas/control_plane.py — PolicyDecision, RiskScanResult,
  AgentRegistration, AuditEvent Pydantic v2 models
- infrastructure/migrations/004_control_plane_audit.sql
- tests/unit/test_control_plane.py — 25 tests

Modified:
- authority_manifest.py — added allowed_models, external_facing,
  data_classification to all 5 agents
- mission_state.py — added policy_state field with full DB roundtrip
- draft_investor_update.py — risk-gated: prompt scan -> LLM ->
  output scan -> audit log, always requires_approval: True
- schemas/__init__.py — exported new control plane models

Preserves Sarthi's guardian architecture, deterministic watchlists,
and bounded LLM usage. Zero regressions (799 unit tests pass).
Add three Docker Compose profiles matching machine constraints:
- dev: PostgreSQL + Redis only (~2GB, daily coding)
- llmops: adds Jaeger + MinIO + Langfuse (~4-5GB, tracing/eval)
- showcase: full stack Temporal + Redpanda + Qdrant + Langfuse (~9GB, demo only)

Container names and volumes are profile-scoped to prevent collisions.
All profiles share iterateswarm-net for compatible service discovery.
Add local-first CI with pre-push validation:
- make ci-fast: vet + fmt + ruff + unit tests (pre-commit gate)
- make ci-local: ci-fast + actionlint (pre-push gate)
- Pre-push hook: blocks direct pushes to main, runs actionlint
- GitHub Actions CI: typecheck -> lint -> unit-tests + workflow-lint
- Integration test job exists as disabled placeholder

Also fix pre-existing bugs discovered by local gates:
- Fix orphaned except in hubspot.py (SyntaxError)
- Fix missing try: block in quickbooks.py (SyntaxError)
- Add missing lineage field in test_agentic_comprehensive.py
- Fix broad "llm" substring match in test_qa_agent.py
- Ignore env-var-dependent and Docker-dependent tests in ci-fast

Config drift is now caught locally before push.
Add Self-Guardian internal monitoring subsystem (Phase 2):
- AgentObservation, DeviationType, SelfGuardianAlert, SelfGuardianReport schemas
- ObservationCollector: thread-safe in-memory buffer (10K cap)
- SelfGuardianDetector: analyzes observations against AUTHORITY_MANIFEST
  - unauthorized_tool detection (critical)
  - external_facing_violation detection (warning)
  - failure pattern detection (info)
  - generate_report() aggregation
- 25 unit tests, all passing

CI fix: replace actions/setup-python@v5 with astral-sh/setup-uv@v5
- setup-python fails on Debian-slim containers (no Python 3.13 binaries)
- setup-uv works on any Linux base, matches project's uv dependency
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a Python AI control plane (registry, policy engine, audit logging), risk scanners, a global tool registry with four HITL tools, and a self-guardian deviation detector. It adds Slack SocketMode integration with an ACE feedback loop, extends MissionState with explainability fields, reworks the Go SSEHub with event-type filtering, adds new command-center endpoints/templates, updates documentation, CI workflow, git hooks, Makefile targets, and adds three Docker Compose local runtime profiles.

Changes

CI, Git Hooks and Build Tooling

Layer / File(s) Summary
Pre-push hook
.githooks/pre-push
Blocks direct pushes to main/master and optionally runs actionlint before allowing a push.
CI workflow restructure
.github/workflows/ci.yml, .gitignore
Replaces build/test jobs with typecheck, lint, unit-tests, workflow-lint, and disabled integration-tests jobs; ignores the actionlint binary.
Local dev/CI Makefile targets
Makefile
Adds dev/llmops/showcase compose profile targets and ci-fast/ci-local/ci-actionlint/ci-setup-hooks targets.

Architecture and Contributor Documentation

Layer / File(s) Summary
ADR and domain docs
.opencode/context/adr/..., .opencode/context/domain/..., .opencode/context/standards/coding-standards.md, .opencode/context/templates/architecture-overview.md
Documents SSEHub filtering, ToolRegistry/HITL tiers, and Slack SocketMode decisions, tradeoffs, and risks.
README and AGENTS.md
README.md, AGENTS.md
Updates decision tables, route map, tool calling surface, SSE chat system, and test coverage counts.

AI Control Plane, Risk Scanning and Tool Registry

Layer / File(s) Summary
Control-plane schemas and authority manifest
apps/ai/src/schemas/control_plane.py, apps/ai/src/schemas/__init__.py, apps/ai/src/agents/authority_manifest.py
Adds Pydantic control-plane schemas and an AgentAuthority manifest with lookup helpers.
Registry and Policy Engine
apps/ai/src/control_plane/registry.py, apps/ai/src/control_plane/policy.py
Implements a thread-safe agent registry and deterministic policy evaluation rules.
Audit logging
apps/ai/src/control_plane/__init__.py, apps/ai/src/control_plane/audit.py, apps/ai/infrastructure/migrations/004_control_plane_audit.sql
Adds AuditLogger persisting audit_log rows via a new migration.
Strategy-confidence audit trail
apps/ai/src/agents/cofounder/curator.py
Adds StrategyDelta records with Postgres/JSONL persistence for confidence updates.
Risk scanners
apps/ai/src/risk/prompt_risk.py, apps/ai/src/risk/output_risk.py, apps/ai/src/risk/__init__.py
Adds deterministic regex-based prompt/output risk scanning.
Tool registry and tools
apps/ai/src/agents/tools/*, apps/ai/src/hitl/manager.py
Adds ToolDef registry with pattern/tier helpers, four HITL tools, and HITLManager.resolve_suggested_tools.
Integration fixes and tests
apps/ai/src/integrations/hubspot.py, apps/ai/src/integrations/quickbooks.py, apps/ai/tests/unit/test_control_plane.py, apps/ai/tests/unit/test_qa_agent.py
Fixes exception handling and adds control-plane unit tests.

Self-Guardian Deviation Monitoring

Layer / File(s) Summary
Schemas
apps/ai/src/schemas/self_guardian.py, apps/ai/src/self_guardian/__init__.py
Adds DeviationType, AgentObservation, SelfGuardianAlert, and SelfGuardianReport models.
Detector and collector
apps/ai/src/self_guardian/detector.py, apps/ai/src/self_guardian/monitor.py
Implements deviation analysis and a capped in-memory observation buffer.
Tests
apps/ai/tests/unit/test_self_guardian.py
Adds tests for schemas, collector, and detector behavior.

Slack SocketMode and ACE Feedback Loop

Layer / File(s) Summary
SlackClient SocketMode
apps/ai/src/integrations/slack_client.py, apps/ai/pyproject.toml
Adds slack-sdk dependency and SocketMode listener plus guardian alert delivery methods.
Guardian delivery integration
apps/ai/src/integrations/slack.py
Replaces Mockoon delivery with SlackClient.send_guardian_alert.
ACE loop wiring
apps/ai/src/integrations/slack_buttons.py
Adds Reflector, Graphiti, and Curator update steps in feedback signal handling.
Guardian lineage schema
apps/ai/src/schemas/guardian.py, apps/ai/tests/unit/test_agentic_comprehensive.py
Adds AlertLineage field to GuardianMessage.

Mission-State Explainability and Go SSEHub Command Center

Layer / File(s) Summary
Python MissionState fields
apps/ai/src/session/mission_state.py, apps/ai/src/session/brief_generator.py
Adds prepared_brief, explainability fields, and a brief generator function.
Go migration
apps/core/internal/db/migrations/005_mission_state_explainability.sql
Adds explainability columns to mission_states.
SSEHub filtering
apps/core/internal/web/sse_hub.go
Reworks subscriptions to support per-subscriber event-type filtering.
Command-center handlers/templates
apps/core/internal/web/handler.go, apps/core/internal/web/templates/partials/command_alert_lineage.html, apps/core/internal/web/templates/partials/command_operating_layer.html, apps/core/internal/web/command_center_test.go, apps/core/hitl_queue_test.go
Adds mission/HITL/session SSE endpoints and alert-lineage/operating-layer HTML endpoints.

Local Docker Compose Runtime Profiles

Layer / File(s) Summary
Dev profile
docker-compose.dev.yml
Defines Postgres and Redis services for local dev.
LLMOps profile
docker-compose.llmops.yml
Adds Jaeger, MinIO, and Langfuse services.
Showcase profile
docker-compose.showcase.yml
Adds Temporal, Redpanda, and Qdrant to a full local stack.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant SSEHub
  participant Postgres

  Client->>Handler: GET /events/mission (tenant)
  Handler->>SSEHub: Subscribe(tenantID, "mission-update")
  SSEHub-->>Handler: Subscription{ID, Channel, Types}
  Handler-->>Client: connected, heartbeat
  Postgres->>Handler: mission_state row updated
  Handler->>SSEHub: Broadcast(event type="mission-update")
  SSEHub->>Handler: filtered payload via sub.Channel
  Handler-->>Client: stream payload bytes
Loading
sequenceDiagram
  participant SlackButton
  participant PolicyEngine
  participant ToolRegistry
  participant ControlTool
  participant AuditLogger

  SlackButton->>PolicyEngine: evaluate(agent, requested_tool)
  PolicyEngine-->>SlackButton: PolicyDecision
  SlackButton->>ToolRegistry: get_tools_for_patterns(triggered_patterns)
  ToolRegistry-->>SlackButton: candidate ToolDefs
  SlackButton->>ControlTool: execute(tenant_id, ...)
  ControlTool->>AuditLogger: log_event(AuditEvent)
  AuditLogger-->>ControlTool: success/failure
Loading

Possibly related PRs

  • Aparnap2/Track_Guard#27: Both PRs modify the same .github/workflows/ci.yml to restructure the Go/Python job pipeline and triggers.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: the Self-Guardian subsystem and a CI fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-2-self-guardian

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

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

Copy link
Copy Markdown
Contributor

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 comprehensive agent control plane, including an agent registry, policy engine, and audit logging, alongside a Self-Guardian monitoring subsystem for tracking agent behavior. It also implements a centralized Tool Registry with HITL tier mapping, a Slack SocketMode integration with an ACE feedback loop, and an upgraded Go SSEHub with event-type filtering. While the architectural enhancements are solid, several critical runtime bugs and issues were identified in the review: indentation errors in the QuickBooks integration, incorrect asynchronous calls to the audit logger, blocking synchronous calls in the brief generator, database table name mismatches, and incorrect SSE event-type subscriptions in the Go handlers. Additionally, passing serialized JSON strings to asyncpg JSONB columns will fail, and the observation collector's buffer eviction can be optimized using a deque.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +95 to +100
def _fetch_invoices() -> list:
"""Single HTTP attempt — retried by ``retry_with_backoff``."""
response = httpx.get(url, params=params, headers=headers, timeout=30.0)
response.raise_for_status()
data = response.json()
query_response = data.get("QueryResponse", {}) or {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The indentation of invoices on line 101 is incorrect (8 spaces instead of 12), placing it outside the _fetch_invoices function scope. Since it references query_response which is local to _fetch_invoices, this will raise a NameError at runtime. Please indent line 101 and subsequent lines to be inside the function.

"reason": "Output risk scan blocked draft",
},
)
_AUDIT.log(audit_event)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The AuditLogger class does not have a log method (only log_event which is asynchronous). Calling _AUDIT.log(audit_event) will raise an AttributeError at runtime. It should be awaited using await _AUDIT.log_event(audit_event).

Suggested change
_AUDIT.log(audit_event)
await _AUDIT.log_event(audit_event)

"output_scan": output_scan.model_dump(),
},
)
_AUDIT.log(audit_event)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The AuditLogger class does not have a log method. Calling _AUDIT.log(audit_event) will raise an AttributeError at runtime. It should be awaited using await _AUDIT.log_event(audit_event).

Suggested change
_AUDIT.log(audit_event)
await _AUDIT.log_event(audit_event)

if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
pool.submit(asyncio.run, _pg_write).result(timeout=5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Passing the coroutine function _pg_write directly to asyncio.run will raise a ValueError because asyncio.run expects a coroutine object (the result of calling the function). Use a lambda to call the function: pool.submit(lambda: asyncio.run(_pg_write())).

Suggested change
pool.submit(asyncio.run, _pg_write).result(timeout=5)
pool.submit(lambda: asyncio.run(_pg_write())).result(timeout=5)

import asyncio

async def _pg_write() -> None:
conn = await asyncpg.connect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Calling asyncpg.connect() without arguments will attempt to connect using default environment variables, which will fail if the database is on a non-standard port or requires specific credentials. Pass the configured database URL instead.

            from src.config.database import get_database_url
            conn = await asyncpg.connect(get_database_url("iterateswarm"))

// APICommandMissionEvents is an SSE endpoint for mission state updates (event type: "mission-update").
func (h *Handler) APICommandMissionEvents(c *fiber.Ctx) error {
tenantID := c.Query("tenant_id", "default")
sub := h.sseHub.Subscribe(tenantID, "mission-update")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The endpoint subscribes to "mission-update", but the documented and expected event type is "mission". This mismatch will cause published mission events to be filtered out and never delivered to subscribers.

Suggested change
sub := h.sseHub.Subscribe(tenantID, "mission-update")
sub := h.sseHub.Subscribe(tenantID, "mission")

// APICommandHITLEvents is an SSE endpoint for HITL approval events (event type: "hitl-item").
func (h *Handler) APICommandHITLEvents(c *fiber.Ctx) error {
tenantID := c.Query("tenant_id", "default")
sub := h.sseHub.Subscribe(tenantID, "hitl-item")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The endpoint subscribes to "hitl-item", but the documented and expected event type is "hitl". This mismatch will cause published HITL events to be filtered out and never delivered to subscribers.

Suggested change
sub := h.sseHub.Subscribe(tenantID, "hitl-item")
sub := h.sseHub.Subscribe(tenantID, "hitl")

// APICommandSessionEvents is an SSE endpoint for agent message events (event type: "agent-message").
func (h *Handler) APICommandSessionEvents(c *fiber.Ctx) error {
tenantID := c.Query("tenant_id", "default")
sub := h.sseHub.Subscribe(tenantID, "agent-message")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The endpoint subscribes to "agent-message", but the documented and expected event type is "session". This mismatch will cause published session events to be filtered out and never delivered to subscribers.

Suggested change
sub := h.sseHub.Subscribe(tenantID, "agent-message")
sub := h.sseHub.Subscribe(tenantID, "session")

Comment on lines +53 to +58
json.dumps(
event.policy_decision.model_dump() if event.policy_decision else None
),
event.approval_state,
event.outcome,
json.dumps(event.details) if event.details else None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Passing a JSON-serialized string (via json.dumps) to a JSONB column in asyncpg will raise a type mismatch error because asyncpg expects a Python dict or list for JSONB placeholders. Pass the dictionary directly instead of serializing it to a string.

Suggested change
json.dumps(
event.policy_decision.model_dump() if event.policy_decision else None
),
event.approval_state,
event.outcome,
json.dumps(event.details) if event.details else None,
event.policy_decision.model_dump() if event.policy_decision else None,
event.approval_state,
event.outcome,
event.details,

Comment on lines +26 to +38
def __init__(self) -> None:
self._observations: list[AgentObservation] = []
self._lock = threading.Lock()

def record_observation(self, observation: AgentObservation) -> None:
"""Record a single agent observation.

If the buffer is at capacity, the oldest entry is removed.
"""
with self._lock:
if len(self._observations) >= _MAX_BUFFER_SIZE:
self._observations.pop(0)
self._observations.append(observation)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using list.pop(0) on a list of up to 10,000 elements is an $O(N)$ operation because all subsequent elements must be shifted. Using collections.deque with maxlen=_MAX_BUFFER_SIZE is much more efficient ($O(1)$) and automatically handles eviction when capacity is reached.

    def __init__(self) -> None:
        from collections import deque
        self._observations = deque(maxlen=_MAX_BUFFER_SIZE)
        self._lock = threading.Lock()

    def record_observation(self, observation: AgentObservation) -> None:
        """Record a single agent observation.

        If the buffer is at capacity, the oldest entry is removed.
        """
        with self._lock:
            self._observations.append(observation)
            log.debug(
                "Observation recorded for agent=%s action=%s success=%s",
                observation.agent_name,
                observation.action,
                observation.success,
            )

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Note

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
.opencode/context/standards/coding-standards.md (1)

140-143: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the Python target version to 3.13.

This doc still says Python 3.11+, which conflicts with the repo’s declared Python 3.13 target.

Suggested fix
-- Python 3.11+ with type hints everywhere
+- Python 3.13 with type hints everywhere
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.opencode/context/standards/coding-standards.md around lines 140 - 143, The
coding standards doc is outdated: update the Python target version from Python
3.11+ to Python 3.13 so it matches the repo’s declared baseline. Adjust the
bullet in the standards section that currently mentions the Python version,
keeping the rest of the guidance unchanged.

Source: Coding guidelines

apps/core/internal/web/handler.go (1)

1582-1625: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move Unsubscribe into the stream writer
defer h.sseHub.Unsubscribe(tenantID, sub.ID) runs when the handler returns, but the SSE body writer runs later, so sub.Channel gets closed before the loop can consume from it and the connection drops after the first frame. Apply the same fix to APICommandMissionEvents, APICommandHITLEvents, and APICommandSessionEvents.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/core/internal/web/handler.go` around lines 1582 - 1625, The SSE handlers
unsubscribe too early because the deferred Unsubscribe runs when the handler
returns, before the stream writer finishes consuming from the subscription
channel. Move the unsubscribe cleanup into the SetBodyStreamWriter closure in
APICommandChatEvents, and apply the same change to APICommandMissionEvents,
APICommandHITLEvents, and APICommandSessionEvents so sub.Channel stays open for
the lifetime of the stream.
apps/ai/src/integrations/quickbooks.py (1)

86-173: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Critical: query_response referenced out of scope — always raises NameError, silently masked into mock data.

_fetch_invoices() (lines 95-100) computes query_response as a local variable inside the nested function, but the function is never called. Line 101 then references query_response directly, which is undefined at that scope — this will raise NameError on every execution of the real (non-mock) API path. Because the entire block is wrapped by the broad except Exception as e: at lines 171-173, this NameError is silently caught and the function always falls back to _MOCK_DATA — meaning the production QuickBooks integration is completely non-functional whenever credentials ARE configured, and this will go unnoticed since it masquerades as a normal "mock fallback."

The docstring "Single HTTP attempt — retried by retry_with_backoff" also implies a retry decorator was intended but is missing from the shown code.

🐛 Proposed fix
         def _fetch_invoices() -> list:
             """Single HTTP attempt — retried by ``retry_with_backoff``."""
             response = httpx.get(url, params=params, headers=headers, timeout=30.0)
             response.raise_for_status()
             data = response.json()
-            query_response = data.get("QueryResponse", {}) or {}
-        invoices = query_response.get("Invoice", [])
+            return data.get("QueryResponse", {}) or {}
+
+        query_response = _fetch_invoices()
+        invoices = query_response.get("Invoice", [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/integrations/quickbooks.py` around lines 86 - 173,
`query_response` is being read outside the scope of `_fetch_invoices()`, so the
real QuickBooks path always throws `NameError` and falls back to `_MOCK_DATA`.
Move the invoice parsing logic into `_fetch_invoices()` or return the parsed
`QueryResponse` data from that helper, then call `_fetch_invoices()` from the
main QuickBooks flow before using `invoices`. Keep the existing `logger.error`
fallback behavior, but ensure the symbols `_fetch_invoices`, `query_response`,
and `invoices` are wired so the non-mock path can complete successfully.
apps/ai/src/agents/cofounder/curator.py (1)

291-341: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Pass prior_confidence into the strategy update
slack_buttons._send_feedback_signal calls update_strategy_confidence(...) without prior_confidence, so each StrategyDelta is written as if the strategy started at 0.0. Thread the current confidence through this call (or resolve it inside the helper) so the audit trail reflects the real accumulated value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/agents/cofounder/curator.py` around lines 291 - 341,
update_strategy_confidence is currently building StrategyDelta from a default
prior_confidence of 0.0, so the audit trail can be wrong when callers do not
pass the current value. Update the call path from
slack_buttons._send_feedback_signal to thread the real strategy confidence into
update_strategy_confidence, or have update_strategy_confidence resolve the
current confidence before constructing StrategyDelta. Make sure the
StrategyDelta prior_confidence and new_confidence fields reflect the accumulated
strategy state rather than a reset baseline.
🟡 Minor comments (8)
.github/workflows/ci.yml-78-88 (1)

78-88: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale/misleading instructions in the placeholder job.

The comment at Line 82 suggests enabling via if: github.event_name == 'push', while the echoed message at Line 88 says to enable by "removing 'if: false'" — but no if: false condition actually exists on this job. Since there's no if: guard at all, the job currently runs on every workflow trigger (harmlessly printing echo statements). The instructions should be made consistent to avoid confusing future maintainers trying to enable real integration tests.

✏️ Suggested fix
   integration-tests:
     name: Integration Tests
     runs-on: ubuntu-latest
     needs: [unit-tests]
-    # Enable by adding: if: github.event_name == 'push'  (or remove this comment)
+    if: false  # Flip to true (or add a real condition) once Docker-based integration tests are ready
     steps:
       - uses: actions/checkout@v4
       - name: Docker compose integration
         run: |
           echo "Integration tests require Docker and are disabled by default."
-          echo "Enable by removing 'if: false' from ci.yml"
+          echo "Enable by removing/adjusting the 'if:' condition above."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 78 - 88, The placeholder guidance in
the integration-tests job is inconsistent and misleading: the job currently has
no if guard, yet the comments mention enabling it via github.event_name ==
'push' and removing if: false. Update the messaging in the integration-tests job
so the comment and echoed instructions match the actual behavior, and reference
the integration-tests step in ci.yml to keep the placeholder enablement
instructions accurate for future maintainers.
Makefile-408-420 (1)

408-420: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align ci-fast with the CI unit-test scope
make ci-fast skips four extra tests that the unit-tests job runs, so it can pass locally while CI still fails. Match the workflow ignore list or add a note explaining the intentional divergence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 408 - 420, The ci-fast target is not aligned with the
unit-tests workflow because it omits several tests that CI still runs. Update
the ci-fast recipe to match the same pytest ignore list used by the unit-tests
job, or explicitly document the intentional difference in the Makefile target.
Use the ci-fast and Python unit tests steps in the Makefile as the place to keep
the scopes consistent.
docker-compose.showcase.yml-68-72 (1)

68-72: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Update the Temporal healthcheck
temporalio/auto-setup:latest no longer ships tctl, so this healthcheck will go unhealthy on newer image tags. Switch to the temporal CLI or a gRPC probe; pinning the image is only a short-term stopgap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.showcase.yml` around lines 68 - 72, The healthcheck for the
Temporal service still relies on tctl inside temporalio/auto-setup:latest, which
is no longer available in newer image tags. Update the healthcheck in the
service definition to use the temporal CLI or a gRPC-based probe instead, and
keep the change localized to the healthcheck stanza so the rest of the compose
setup remains unchanged.
README.md-7-7 (1)

7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the empty badge target.

(#) is a no-op link and triggers MD042; use a real anchor or drop the wrapper.

Suggested fix
-[![Tests](https://img.shields.io/badge/tests-393%2B%20passing-brightgreen)](#)
+![Tests](https://img.shields.io/badge/tests-393%2B%20passing-brightgreen)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 7, The Tests badge in the README uses a no-op link target,
which should be replaced or removed. Update the badge markup so the link points
to a real anchor or relevant destination, or drop the link wrapper entirely;
keep the change localized to the README badge entry.

Source: Linters/SAST tools

.opencode/context/domain/chat-flow.md-142-147 (1)

142-147: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the SSE domain count.

This section says “Three SSE event domains” but lists four (chat, mission, hitl, session).

Suggested fix
-**Three SSE event domains** (2026-06-28):
+**Four SSE event domains** (2026-06-28):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.opencode/context/domain/chat-flow.md around lines 142 - 147, Update the SSE
domain summary in the chat-flow docs so the count matches the listed domains:
the section currently says “Three SSE event domains” but the bullets enumerate
four domains (`chat`, `mission`, `hitl`, `session`). Adjust the heading text and
keep the existing domain names and endpoint references intact in this
documentation block.
.opencode/context/templates/architecture-overview.md-39-41 (1)

39-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Call out the blocked HITL tier here.

The ToolRegistry docs now use four routing outcomes (auto, review, approve, blocked), so “3-tier routing” is stale.

Suggested fix
-| **HITL Manager** | `src/hitl/` | 3-tier routing (auto/review/approve), guardrail-aware route_extended, confidence scoring |
+| **HITL Manager** | `src/hitl/` | 4-tier routing (auto/review/approve/blocked), guardrail-aware route_extended, confidence scoring |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.opencode/context/templates/architecture-overview.md around lines 39 - 41,
The HITL Manager description is outdated because it still says “3-tier routing”
while the current routing outcomes include blocked. Update the architecture
overview entry for the HITL Manager in the template so it explicitly lists the
blocked tier alongside auto, review, and approve, and keep the wording aligned
with the route_extended and confidence scoring behavior in src/hitl/.
.opencode/context/adr/001-sarthi-v4-architecture-evolution.md-195-200 (1)

195-200: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the Slack button count.

The text says five actions, but the list only names four. Either update the count or add the missing action.

Suggested fix
-  - Slack buttons: routes five button actions: `acknowledge`, `dispute`, `show_breakdown`, `log_decision`.
+  - Slack buttons: routes four button actions: `acknowledge`, `dispute`, `show_breakdown`, `log_decision`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.opencode/context/adr/001-sarthi-v4-architecture-evolution.md around lines
195 - 200, The Slack button action summary is inconsistent because the count
says five actions while only four are listed. Update the wording in this ADR
snippet to match the actual set of actions, or add the missing action name if
there are truly five; check the `slack_buttons.py` action list and the
`open_decision_modal`/`_send_feedback_signal()` flow so the documented count
matches the implemented buttons.
apps/ai/src/agents/tools/flag_churn_risk.py-37-39 (1)

37-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Substring membership causes incorrect dedup.

segment_id not in existing performs a substring check against the comma-joined string, not a segment-level membership test. E.g. with existing = "seg10,seg2" and segment_id = "seg1", "seg1" in "seg10" is truthy, so a genuinely new segment is skipped. Split on commas and compare exact tokens.

🐛 Proposed fix
-    existing = state.churn_risk_users or ""
-    if segment_id not in existing:
-        state.churn_risk_users = (existing + "," + segment_id).strip(",")
+    existing = state.churn_risk_users or ""
+    segments = [s for s in existing.split(",") if s]
+    if segment_id not in segments:
+        segments.append(segment_id)
+        state.churn_risk_users = ",".join(segments)
         state.last_updated_by = "flag_churn_risk_tool"
         await update_mission_state(state)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/agents/tools/flag_churn_risk.py` around lines 37 - 39, The dedup
check in flag_churn_risk.py is using substring matching on
state.churn_risk_users, which can wrongly skip a new segment when its id appears
inside another id. Update the logic in the churn risk update flow to parse the
comma-separated state.churn_risk_users value into exact segment tokens, compare
segment_id against those tokens, and only append when it is not already present.
Keep the existing state.churn_risk_users field and the append behavior, but base
the membership test on exact item equality rather than a raw string contains
check.
🧹 Nitpick comments (11)
.github/workflows/ci.yml (1)

22-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Typecheck job never fails.

Both go vet (Line 24) and mypy (Line 29) are piped through || true, so the "Type Check" job always succeeds regardless of errors found. Given unit-tests (Line 54) depends only on typecheck and lint succeeding, this gate provides no actual enforcement. If this is intentional for a soft rollout, consider at least surfacing failures via job summary/annotations so regressions aren't silently swallowed indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 22 - 29, The Type Check job is
swallowing failures in the Go vet and Python mypy steps, so the job never blocks
on type errors. Update the workflow steps in the Type Check job to remove the
unconditional success behavior, or otherwise explicitly capture and propagate
the exit status from go vet and mypy while still filtering gen/go output. Use
the existing Go vet and Python mypy step names to locate the commands, and if
you keep a soft rollout, add a visible failure signal such as job summary or
annotations instead of `|| true`.
apps/ai/src/integrations/slack_buttons.py (1)

102-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Redundant exception tuple and silent failure swallow the entire ACE loop.

ImportError is a subclass of Exception, so except (ImportError, Exception) is equivalent to except Exception. More importantly, all three ACE steps (score_from_button, update_strategy_confidence, curator.update) discard every error with a bare pass. If Reflector/Curator/Graphiti wiring silently regresses, the feedback loop becomes a no-op with zero signal in logs. Consider logging at least a warning per failed step.

♻️ Suggested change (Step 1 shown; apply to all three)
-    except (ImportError, Exception):
-        pass
+    except Exception:
+        logger.warning("ACE step 1 (reflector scoring) failed", exc_info=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/integrations/slack_buttons.py` around lines 102 - 130, The ACE
loop in slack_buttons.py is swallowing failures in the step handlers, and the
repeated except (ImportError, Exception) is redundant because ImportError is
already covered by Exception. Update the three try/except blocks around
score_from_button, update_strategy_confidence, and Curator.update to catch the
appropriate exception set once and emit a warning or error with step-specific
context instead of passing silently, so failures are observable without breaking
the flow.
apps/ai/src/self_guardian/detector.py (2)

68-150: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Only the first matched deviation is ever reported per observation.

An observation that is simultaneously unauthorized (critical) and failed (info) will only surface the UNAUTHORIZED_TOOL alert — the failure is silently dropped, and generate_report's deviation counts will not reflect it either. Consider accumulating all applicable deviations (or at minimum documenting this first-match priority explicitly) so downstream consumers aren't misled about severity/counts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/self_guardian/detector.py` around lines 68 - 150, The detector in
SelfGuardian only returns the first matching alert from the check chain, so
later deviations like failed observations are dropped and never counted. Update
the alert logic in the detector method that evaluates observation checks to
accumulate all applicable deviations for a single observation, or explicitly
document and encode the priority if first-match behavior is intended. Make sure
the report generation path that consumes these alerts still sees every detected
deviation, including UNAUTHORIZED_TOOL and STATE_CORRUPTION, rather than only
the first one returned.

126-148: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

STATE_CORRUPTION is a semantic mismatch for generic failure detection.

Any unsuccessful observation is tagged STATE_CORRUPTION, but that deviation type implies actual corrupted internal state, not a transient operational failure (e.g., a CRM timeout). This can mislead operators reading Self-Guardian alerts/reports about the real nature of the issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/self_guardian/detector.py` around lines 126 - 148, The
failed-observation branch in `detector.py` is using
`DeviationType.STATE_CORRUPTION` for any unsuccessful observation, which is
semantically too strong for generic execution failures. Update the
`SelfGuardianAlert` creation in the failed-observation check to use the
deviation type that represents operational/action failure rather than corrupted
state, and keep the existing message/description wording aligned with that
classification. Use the `observation.success` check and the `SelfGuardianAlert`
constructor in this block to locate and adjust the mapping.
apps/ai/src/self_guardian/monitor.py (1)

26-44: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use collections.deque(maxlen=...) instead of list.pop(0) for O(1) eviction.

Once the buffer is full, every new observation triggers an O(n) pop(0) shift of up to 10,000 elements. A deque(maxlen=_MAX_BUFFER_SIZE) evicts the oldest entry automatically in O(1) and removes the manual capacity check.

♻️ Proposed refactor
+from collections import deque
+
 class ObservationCollector:
     def __init__(self) -> None:
-        self._observations: list[AgentObservation] = []
+        self._observations: deque[AgentObservation] = deque(maxlen=_MAX_BUFFER_SIZE)
         self._lock = threading.Lock()

     def record_observation(self, observation: AgentObservation) -> None:
         with self._lock:
-            if len(self._observations) >= _MAX_BUFFER_SIZE:
-                self._observations.pop(0)
             self._observations.append(observation)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/self_guardian/monitor.py` around lines 26 - 44, The observation
buffer in the monitor is doing O(n) eviction with list.pop(0) in
AgentObservationMonitor.record_observation, which should be replaced with an
O(1) bounded deque. Update AgentObservationMonitor.__init__ to initialize
_observations as collections.deque(maxlen=_MAX_BUFFER_SIZE), remove the manual
length check and pop(0) logic in record_observation, and keep the append/logging
behavior unchanged.
docker-compose.llmops.yml (2)

90-95: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Hardcoded secrets in committed compose file.

NEXTAUTH_SECRET, SALT, and the DB password embedded in DATABASE_URL are hardcoded plaintext. Checkov also flags this (CKV_SECRET_4). Even for local/dev tooling, prefer sourcing these from a .env file via ${VAR} interpolation so no secret-like value is committed, even a placeholder one.

🔒 Suggested fix
   environment:
-    DATABASE_URL: postgresql://iterateswarm:iterateswarm@postgres:5432/iterateswarm
-    NEXTAUTH_SECRET: dev-secret-do-not-use-in-prod
+    DATABASE_URL: postgresql://${POSTGRES_USER:-iterateswarm}:${POSTGRES_PASSWORD:-iterateswarm}`@postgres`:5432/${POSTGRES_DB:-iterateswarm}
+    NEXTAUTH_SECRET: ${LANGFUSE_NEXTAUTH_SECRET}
     NEXTAUTH_URL: http://localhost:4000
-    SALT: dev-salt-do-not-use-in-prod
+    SALT: ${LANGFUSE_SALT}

Based on learnings, "Use feature branches, conventional commits, never commit to main, keep secrets in .env, and regenerate sqlc code after schema changes."

Also applies to: 114-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.llmops.yml` around lines 90 - 95, The compose service
environment block contains committed secret-like values; update the
configuration in the Docker Compose service definition to source
`NEXTAUTH_SECRET`, `SALT`, and the password portion of `DATABASE_URL` from
environment variables instead of hardcoded plaintext. Use `${VAR}` interpolation
in the existing environment section so the values come from a local `.env` file
or runtime environment, and keep the change consistent anywhere the same compose
service variables are repeated.

Sources: Learnings, Linters/SAST tools


5-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicated postgres/redis stack across profiles blocks running tiers together.

docker-compose.dev.yml, docker-compose.llmops.yml, and docker-compose.showcase.yml each fully redefine postgres/redis with identical host ports (5433, 6379) and the same iterateswarm-net network name. If a developer starts more than one profile at once (e.g., dev + llmops to incrementally add services, as the "adds" framing in the PR description implies), the host port bindings will collide.

Consider using Compose's file-merging (docker compose -f docker-compose.dev.yml -f docker-compose.llmops.yml up) or extends, so llmops/showcase only declare their additional services and reuse the base postgres/redis definitions.

Also applies to: 27-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.llmops.yml` around lines 5 - 22, The postgres/redis stack is
being duplicated across profile compose files, which causes host port and
network conflicts when profiles are run together. Update the llmops compose
definitions so they reuse the base `postgres` and `redis` services instead of
redeclaring them, using Compose file merging or `extends`, and keep
`docker-compose.llmops.yml` focused on only the additional llmops services.
Ensure the shared service names and bindings remain defined in one place so
`postgres` and `redis` can be combined safely with dev/showcase.
apps/core/internal/web/templates/partials/command_operating_layer.html (1)

58-61: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

ActiveAgentRoles likely renders raw JSON text instead of individual role badges.

Per the handler.go context snippet, ActiveAgentRoles is populated from active_agent_roles::text — a serialized JSON array cast to a string (e.g. ["Finance specialist","Ops specialist"]). Rendering it directly in a single pill (Line 60) would show the raw JSON syntax to founders instead of one badge per role, unlike the multi-badge pattern used for SuggestedActions in the sibling command_alert_lineage.html template.

♻️ Proposed fix (requires handler to pass a slice)
-        {{if .ActiveAgentRoles}}
-        <div class="flex flex-wrap gap-1.5 mt-1 ml-5.5">
-            <span class="text-[10px] px-2 py-0.5 rounded-full bg-blue-900/30 text-blue-400">{{.ActiveAgentRoles}}</span>
-        </div>
+        {{if .ActiveAgentRoles}}
+        <div class="flex flex-wrap gap-1.5 mt-1 ml-5.5">
+            {{range .ActiveAgentRoles}}
+            <span class="text-[10px] px-2 py-0.5 rounded-full bg-blue-900/30 text-blue-400">{{.}}</span>
+            {{end}}
+        </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/core/internal/web/templates/partials/command_operating_layer.html`
around lines 58 - 61, ActiveAgentRoles is being rendered as a single raw JSON
string instead of separate role badges. Update the data flow so the handler
parses active_agent_roles into a slice before passing it to
command_operating_layer.html, then change the template to iterate over
.ActiveAgentRoles like the multi-badge pattern used for SuggestedActions in
command_alert_lineage.html. Keep the existing ActiveAgentRoles symbol as the
entry point for the fix and replace the single pill rendering with one badge per
role.
apps/core/internal/web/handler.go (1)

1627-1763: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated SSE streaming boilerplate into a shared helper.

APICommandMissionEvents, APICommandHITLEvents, and APICommandSessionEvents are near-identical copies of APICommandChatEvents, differing only in the event-type filter and the connected message text. Consolidating into one helper reduces the maintenance surface (e.g., the unsubscribe fix above only needs to land once).

♻️ Suggested helper
func (h *Handler) streamSSE(c *fiber.Ctx, eventType, connectedText string) error {
	tenantID := c.Query("tenant_id", "default")
	sub := h.sseHub.Subscribe(tenantID, eventType)

	c.Set("Content-Type", "text/event-stream")
	c.Set("Cache-Control", "no-cache")
	c.Set("Connection", "keep-alive")

	done := c.Context().Done()
	c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
		defer h.sseHub.Unsubscribe(tenantID, sub.ID)
		defer func() { recover() }()

		fmt.Fprintf(w, "event: connected\ndata: {\"status\":\"connected\",\"text\":%q}\n\n", connectedText)
		w.Flush()

		heartbeat := time.NewTicker(30 * time.Second)
		defer heartbeat.Stop()

		for {
			select {
			case <-heartbeat.C:
				if _, err := fmt.Fprintf(w, "event: heartbeat\ndata: {}\n\n"); err != nil {
					return
				}
				w.Flush()
			case msgBytes, ok := <-sub.Channel:
				if !ok {
					return
				}
				if _, err := fmt.Fprintf(w, "%s", msgBytes); err != nil {
					return
				}
				w.Flush()
			case <-done:
				return
			}
		}
	})
	return nil
}

Each handler then becomes e.g. return h.streamSSE(c, "mission-update", "Connected to mission events").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/core/internal/web/handler.go` around lines 1627 - 1763, The SSE handlers
APICommandMissionEvents, APICommandHITLEvents, and APICommandSessionEvents
duplicate the same streaming setup and should be refactored into a shared helper
like streamSSE to reduce maintenance overhead. Move the common tenant lookup,
subscription, headers, body stream writer, heartbeat loop, and unsubscribe
handling into the helper, parameterized by eventType and connected message text,
then have each handler call it with its specific values.
apps/ai/src/schemas/control_plane.py (1)

56-80: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

approval_state lacks Literal typing unlike sibling escalation_tier.

AuditEvent.approval_state is documented as "auto"/"review"/"approve"/"blocked" but typed str | None, while the equivalent escalation_tier field on AgentRegistration correctly uses Literal[...]. This is a minor consistency/validation gap in the audit trail schema.

♻️ Proposed fix
-    approval_state: str | None = None
+    approval_state: Literal["auto", "review", "approve", "blocked"] | None = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/schemas/control_plane.py` around lines 56 - 80, Update AuditEvent
in control_plane.py so approval_state uses a Literal type matching the
documented values ("auto", "review", "approve", "blocked") instead of a plain
str | None, keeping it consistent with the sibling escalation_tier field on
AgentRegistration. Adjust the type annotation and any related schema
expectations in AuditEvent so the model enforces the allowed states while still
allowing None if intended.
apps/ai/tests/unit/test_qa_agent.py (1)

104-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrowed LLM-detection checks may miss real LLM integrations.

Restricting to "from langchain"/"import llm"/"openai"/"langchain" substrings avoids the docstring false-positive, but now misses other LLM SDK imports (e.g. anthropic, from src.llm import ... without the literal "import llm" phrasing, or a helper call like call_model()). This weakens the test's actual guarantee that the QA graph stub has no LLM integration. Also, line 106 ("from langchain") is redundant with line 109 ("langchain"), which already subsumes it.

♻️ Proposed fix
-        # Check for actual LLM integration (imports/calls), not substring "llm"
-        # which matches the docstring and internal config.llm module.
-        assert "from langchain" not in content.lower()
-        assert "import llm" not in content.lower()
-        assert "openai" not in content.lower()
-        assert "langchain" not in content.lower()
+        # Check for actual LLM integration (imports/calls), not the bare
+        # substring "llm" which also matches the docstring/config.llm module.
+        import re
+        assert not re.search(r"^\s*(from|import)\s+\S*llm\S*", content, re.MULTILINE | re.IGNORECASE)
+        assert not re.search(r"^\s*(from|import)\s+(openai|anthropic|langchain)\b", content, re.MULTILINE | re.IGNORECASE)

As per path instructions for apps/ai/tests/**/*.py: "Run and maintain the Python test suite under apps/ai/tests/."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/tests/unit/test_qa_agent.py` around lines 104 - 109, The
LLM-detection assertion in test_qa_agent.py is too narrow and can miss real
integrations, so broaden the check in the test around the QA graph stub to
detect more SDK/import patterns and LLM helper calls rather than only the
current literal substrings. Update the assertions near the existing content
checks to cover additional common providers and wrapper calls used by the
codebase, and remove the redundant "from langchain" check since the broader
"langchain" match already covers it. Keep the test focused on verifying that the
stubbed graph has no actual LLM integration by using symbols like the QA
graph/test content scan in this test module.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 14-88: The workflow jobs are using the default GITHUB_TOKEN
permissions and `actions/checkout` is persisting credentials, which is broader
access than needed. Add an explicit least-privilege job-level `permissions`
block for each job in `ci.yml`, keep `workflow-lint` overridden with
`pull-requests: write` for `reviewdog/action-actionlint`, and set
`persist-credentials: false` on every `actions/checkout@v4` step in the
`typecheck`, `lint`, `unit-tests`, `workflow-lint`, and `integration-tests`
jobs.

In `@apps/ai/src/agents/cofounder/curator.py`:
- Around line 436-444: The _file_audit_write fallback currently writes audit
records to a hardcoded /tmp/strategy_audit.jsonl path, which is insecure and not
durable. Update _file_audit_write to resolve the audit file location from a
configurable environment variable instead of a fixed /tmp value, and default it
to a restricted-permission directory suitable for compliance logs. Ensure the
path is created securely with appropriate permissions before appending, and
consider adding basic concurrency-safe write handling so multiple writers do not
corrupt the audit trail.

In `@apps/ai/src/agents/tools/draft_investor_update.py`:
- Line 97: The audit logging in draft_investor_update is calling the wrong API
and not awaiting the async write. Update both audit paths in the
draft_investor_update flow to use AuditLogger.log_event(...) instead of
_AUDIT.log(...), and ensure each call is awaited so the coroutine actually runs.
Check the blocked-path and success-path audit records around the existing
audit_event handling so both are recorded correctly.

In `@apps/ai/src/control_plane/audit.py`:
- Around line 20-24: The audit logger interface is mismatched: callers use
_AUDIT.log(...) but AuditLogger only exposes log_event(...), which will fail at
runtime. Update the audit path to call await log_event(...) consistently, or add
a log() shim on AuditLogger that forwards to log_event so existing call sites
continue to work. Use the AuditLogger class and its log_event method to locate
the change and keep the async behavior intact.

In `@apps/ai/src/control_plane/policy.py`:
- Around line 74-91: The degraded-health branch in policy evaluation is setting
blocked_reason too early, which hides tool permission failures and incorrectly
clears allowed models. Update the decision logic in the policy function around
the degraded-health handling so “degraded” only triggers requires_human_approval
and does not populate blocked_reason unless there is an actual block, then let
the tool check record tool_not_allowed when applicable. Also adjust the
allowed_models assignment so it only becomes empty when a real blocked_reason is
present, keeping degraded cases aligned with the PolicyDecision.blocked_reason
contract.
- Around line 21-99: PolicyEngine.evaluate currently ignores
agent.escalation_tier, so agents marked review or approve can still pass without
human involvement. Update evaluate in PolicyEngine to incorporate
escalation_tier alongside the existing health, classification, external-facing,
and tool checks: treat blocked as a hard deny, set requires_human_approval for
review and approve tiers as intended, and ensure the final PolicyDecision
reflects the tier even when no other rule triggers.

In `@apps/ai/src/integrations/slack_client.py`:
- Around line 42-67: The Slack Socket Mode setup is using async patterns with a
synchronous SocketModeClient. Update the listener in slack_client.py around
_process and the connection flow so connect() is called without await, and
replace both req.ack() calls with
client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))
using the same client instance. If you want to keep async behavior, switch to
the aiohttp Socket Mode client instead of this sync client.

In `@apps/ai/src/risk/output_risk.py`:
- Line 45: The OC009 logic in output_risk.py is inverted because
_MISSING_APPROVAL is included in _ALL_OUTPUT_PATTERNS and then also checked
later in the disclaimer branch. Remove _MISSING_APPROVAL from
_ALL_OUTPUT_PATTERNS so the scan in the output risk evaluation only covers real
risk patterns, and keep OC009 emission solely in the has_draft_disclaimer check
inside the output risk function that builds the risk flags.

In `@apps/ai/src/schemas/control_plane.py`:
- Around line 39-53: Update the schema types in control_plane.py so
PolicyDecision.data_classification and AgentRegistration.data_classification use
a Literal union of the fixed classification values instead of plain str. Keep
the allowed values aligned with PolicyEngine and _RESTRICTED_CLASSIFICATIONS so
invalid typos are rejected at validation time. Make the change in the
PolicyDecision and AgentRegistration model definitions, and ensure any related
docstrings/types still reflect the same fixed set.

In `@apps/ai/src/self_guardian/detector.py`:
- Around line 72-77: The logging in detector.py is still exposing sensitive
observation data verbatim. Update the unauthorized-tool handling in the
self_guardian detector path so any use of observation fields like action,
target_entity, and error_message is redacted or truncated before being written
into the warning log, description, or suggested_action. Keep the existing logic
in the detector/observation flow, but sanitize those fields at the point where
they are formatted or copied so internal or credential-like values are not
emitted.

In `@apps/ai/src/session/brief_generator.py`:
- Around line 29-38: The synchronous chat_completion call inside
generate_prepared_brief is blocking the event loop and has no timeout. Move the
brief generation work off the event loop (for example by wrapping
chat_completion in an async executor/helper) and add a bounded timeout so
update_mission_state is not stalled by a slow or hanging LLM request. Keep the
fix localized to generate_prepared_brief and the chat_completion invocation so
the shared mission-state write path stays responsive.
- Around line 18-49: In generate_prepared_brief, the MissionState write is using
a stale full object and can overwrite unrelated concurrent changes through
update_mission_state’s upsert behavior. Change this flow so only the
brief-related fields are persisted here, or make the save conditional/versioned
to detect concurrent updates. Keep the fix localized to generate_prepared_brief
and the update_mission_state path that performs the ON CONFLICT DO UPDATE write.

In `@apps/ai/src/session/mission_state.py`:
- Around line 182-198: update_mission_state is overwriting existing
explainability data with None whenever generate_brief is true and callers omit
update_reason/changed_fields. In update_mission_state, only assign
state.last_update_reason and state.last_changed_fields when those arguments are
explicitly provided, and otherwise preserve any existing values already on the
MissionState instance from get_mission_state or caller-set fields. Keep the
behavior localized to update_mission_state so plain await
update_mission_state(state) does not clear audit-trail history.
- Around line 210-217: Add `policy_state` support consistently in the mission
state persistence path: either migrate the `mission_states` table to include the
missing column or remove it from `get_mission_state` and `update_mission_state`
so they no longer read/write it. Check the `mission_state.py` query builders
around `get_mission_state`/`update_mission_state`, since the current
INSERT/SELECT logic references `policy_state` even though the schema drift
migration only covers the other fields. Make the schema and the two session
methods match so Postgres no longer throws a missing-column error.
- Around line 277-283: The mission state save/query path in mission_state.py now
references pending_decisions and policy_state, but the session schema migrations
never add those columns, so the insert/select will fail. Update the session
layer migration set, including 001_session_layer.sql and any later migration
definitions, to add the missing mission_states columns in the same order/types
expected by MissionStateRepository’s save/load logic so the database schema
matches the fields used in mission_state.py.
- Around line 168-175: The MissionState read path is returning raw serialized
strings for structured fields instead of the declared list/dict types. In the
MissionState construction logic, deserialize pending_decisions,
last_changed_fields, active_agent_roles, and policy_state before passing them
into MissionState, or apply a JSON codec at the connection level so the
dataclass receives decoded values. Use the MissionState read/row-mapping code to
locate the fix.

In `@docker-compose.showcase.yml`:
- Around line 131-146: Update the qdrant service in docker-compose.showcase.yml
so the persistent volume is mounted to Qdrant’s actual storage path instead of
/var/lib/qdrant. Use the qdrant service definition and its volumes entry to
point sarthi-showcase-qdrant-data at /qdrant/storage, keeping the rest of the
service configuration unchanged.

---

Outside diff comments:
In @.opencode/context/standards/coding-standards.md:
- Around line 140-143: The coding standards doc is outdated: update the Python
target version from Python 3.11+ to Python 3.13 so it matches the repo’s
declared baseline. Adjust the bullet in the standards section that currently
mentions the Python version, keeping the rest of the guidance unchanged.

In `@apps/ai/src/agents/cofounder/curator.py`:
- Around line 291-341: update_strategy_confidence is currently building
StrategyDelta from a default prior_confidence of 0.0, so the audit trail can be
wrong when callers do not pass the current value. Update the call path from
slack_buttons._send_feedback_signal to thread the real strategy confidence into
update_strategy_confidence, or have update_strategy_confidence resolve the
current confidence before constructing StrategyDelta. Make sure the
StrategyDelta prior_confidence and new_confidence fields reflect the accumulated
strategy state rather than a reset baseline.

In `@apps/ai/src/integrations/quickbooks.py`:
- Around line 86-173: `query_response` is being read outside the scope of
`_fetch_invoices()`, so the real QuickBooks path always throws `NameError` and
falls back to `_MOCK_DATA`. Move the invoice parsing logic into
`_fetch_invoices()` or return the parsed `QueryResponse` data from that helper,
then call `_fetch_invoices()` from the main QuickBooks flow before using
`invoices`. Keep the existing `logger.error` fallback behavior, but ensure the
symbols `_fetch_invoices`, `query_response`, and `invoices` are wired so the
non-mock path can complete successfully.

In `@apps/core/internal/web/handler.go`:
- Around line 1582-1625: The SSE handlers unsubscribe too early because the
deferred Unsubscribe runs when the handler returns, before the stream writer
finishes consuming from the subscription channel. Move the unsubscribe cleanup
into the SetBodyStreamWriter closure in APICommandChatEvents, and apply the same
change to APICommandMissionEvents, APICommandHITLEvents, and
APICommandSessionEvents so sub.Channel stays open for the lifetime of the
stream.

---

Minor comments:
In @.github/workflows/ci.yml:
- Around line 78-88: The placeholder guidance in the integration-tests job is
inconsistent and misleading: the job currently has no if guard, yet the comments
mention enabling it via github.event_name == 'push' and removing if: false.
Update the messaging in the integration-tests job so the comment and echoed
instructions match the actual behavior, and reference the integration-tests step
in ci.yml to keep the placeholder enablement instructions accurate for future
maintainers.

In @.opencode/context/adr/001-sarthi-v4-architecture-evolution.md:
- Around line 195-200: The Slack button action summary is inconsistent because
the count says five actions while only four are listed. Update the wording in
this ADR snippet to match the actual set of actions, or add the missing action
name if there are truly five; check the `slack_buttons.py` action list and the
`open_decision_modal`/`_send_feedback_signal()` flow so the documented count
matches the implemented buttons.

In @.opencode/context/domain/chat-flow.md:
- Around line 142-147: Update the SSE domain summary in the chat-flow docs so
the count matches the listed domains: the section currently says “Three SSE
event domains” but the bullets enumerate four domains (`chat`, `mission`,
`hitl`, `session`). Adjust the heading text and keep the existing domain names
and endpoint references intact in this documentation block.

In @.opencode/context/templates/architecture-overview.md:
- Around line 39-41: The HITL Manager description is outdated because it still
says “3-tier routing” while the current routing outcomes include blocked. Update
the architecture overview entry for the HITL Manager in the template so it
explicitly lists the blocked tier alongside auto, review, and approve, and keep
the wording aligned with the route_extended and confidence scoring behavior in
src/hitl/.

In `@apps/ai/src/agents/tools/flag_churn_risk.py`:
- Around line 37-39: The dedup check in flag_churn_risk.py is using substring
matching on state.churn_risk_users, which can wrongly skip a new segment when
its id appears inside another id. Update the logic in the churn risk update flow
to parse the comma-separated state.churn_risk_users value into exact segment
tokens, compare segment_id against those tokens, and only append when it is not
already present. Keep the existing state.churn_risk_users field and the append
behavior, but base the membership test on exact item equality rather than a raw
string contains check.

In `@docker-compose.showcase.yml`:
- Around line 68-72: The healthcheck for the Temporal service still relies on
tctl inside temporalio/auto-setup:latest, which is no longer available in newer
image tags. Update the healthcheck in the service definition to use the temporal
CLI or a gRPC-based probe instead, and keep the change localized to the
healthcheck stanza so the rest of the compose setup remains unchanged.

In `@Makefile`:
- Around line 408-420: The ci-fast target is not aligned with the unit-tests
workflow because it omits several tests that CI still runs. Update the ci-fast
recipe to match the same pytest ignore list used by the unit-tests job, or
explicitly document the intentional difference in the Makefile target. Use the
ci-fast and Python unit tests steps in the Makefile as the place to keep the
scopes consistent.

In `@README.md`:
- Line 7: The Tests badge in the README uses a no-op link target, which should
be replaced or removed. Update the badge markup so the link points to a real
anchor or relevant destination, or drop the link wrapper entirely; keep the
change localized to the README badge entry.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 22-29: The Type Check job is swallowing failures in the Go vet and
Python mypy steps, so the job never blocks on type errors. Update the workflow
steps in the Type Check job to remove the unconditional success behavior, or
otherwise explicitly capture and propagate the exit status from go vet and mypy
while still filtering gen/go output. Use the existing Go vet and Python mypy
step names to locate the commands, and if you keep a soft rollout, add a visible
failure signal such as job summary or annotations instead of `|| true`.

In `@apps/ai/src/integrations/slack_buttons.py`:
- Around line 102-130: The ACE loop in slack_buttons.py is swallowing failures
in the step handlers, and the repeated except (ImportError, Exception) is
redundant because ImportError is already covered by Exception. Update the three
try/except blocks around score_from_button, update_strategy_confidence, and
Curator.update to catch the appropriate exception set once and emit a warning or
error with step-specific context instead of passing silently, so failures are
observable without breaking the flow.

In `@apps/ai/src/schemas/control_plane.py`:
- Around line 56-80: Update AuditEvent in control_plane.py so approval_state
uses a Literal type matching the documented values ("auto", "review", "approve",
"blocked") instead of a plain str | None, keeping it consistent with the sibling
escalation_tier field on AgentRegistration. Adjust the type annotation and any
related schema expectations in AuditEvent so the model enforces the allowed
states while still allowing None if intended.

In `@apps/ai/src/self_guardian/detector.py`:
- Around line 68-150: The detector in SelfGuardian only returns the first
matching alert from the check chain, so later deviations like failed
observations are dropped and never counted. Update the alert logic in the
detector method that evaluates observation checks to accumulate all applicable
deviations for a single observation, or explicitly document and encode the
priority if first-match behavior is intended. Make sure the report generation
path that consumes these alerts still sees every detected deviation, including
UNAUTHORIZED_TOOL and STATE_CORRUPTION, rather than only the first one returned.
- Around line 126-148: The failed-observation branch in `detector.py` is using
`DeviationType.STATE_CORRUPTION` for any unsuccessful observation, which is
semantically too strong for generic execution failures. Update the
`SelfGuardianAlert` creation in the failed-observation check to use the
deviation type that represents operational/action failure rather than corrupted
state, and keep the existing message/description wording aligned with that
classification. Use the `observation.success` check and the `SelfGuardianAlert`
constructor in this block to locate and adjust the mapping.

In `@apps/ai/src/self_guardian/monitor.py`:
- Around line 26-44: The observation buffer in the monitor is doing O(n)
eviction with list.pop(0) in AgentObservationMonitor.record_observation, which
should be replaced with an O(1) bounded deque. Update
AgentObservationMonitor.__init__ to initialize _observations as
collections.deque(maxlen=_MAX_BUFFER_SIZE), remove the manual length check and
pop(0) logic in record_observation, and keep the append/logging behavior
unchanged.

In `@apps/ai/tests/unit/test_qa_agent.py`:
- Around line 104-109: The LLM-detection assertion in test_qa_agent.py is too
narrow and can miss real integrations, so broaden the check in the test around
the QA graph stub to detect more SDK/import patterns and LLM helper calls rather
than only the current literal substrings. Update the assertions near the
existing content checks to cover additional common providers and wrapper calls
used by the codebase, and remove the redundant "from langchain" check since the
broader "langchain" match already covers it. Keep the test focused on verifying
that the stubbed graph has no actual LLM integration by using symbols like the
QA graph/test content scan in this test module.

In `@apps/core/internal/web/handler.go`:
- Around line 1627-1763: The SSE handlers APICommandMissionEvents,
APICommandHITLEvents, and APICommandSessionEvents duplicate the same streaming
setup and should be refactored into a shared helper like streamSSE to reduce
maintenance overhead. Move the common tenant lookup, subscription, headers, body
stream writer, heartbeat loop, and unsubscribe handling into the helper,
parameterized by eventType and connected message text, then have each handler
call it with its specific values.

In `@apps/core/internal/web/templates/partials/command_operating_layer.html`:
- Around line 58-61: ActiveAgentRoles is being rendered as a single raw JSON
string instead of separate role badges. Update the data flow so the handler
parses active_agent_roles into a slice before passing it to
command_operating_layer.html, then change the template to iterate over
.ActiveAgentRoles like the multi-badge pattern used for SuggestedActions in
command_alert_lineage.html. Keep the existing ActiveAgentRoles symbol as the
entry point for the fix and replace the single pill rendering with one badge per
role.

In `@docker-compose.llmops.yml`:
- Around line 90-95: The compose service environment block contains committed
secret-like values; update the configuration in the Docker Compose service
definition to source `NEXTAUTH_SECRET`, `SALT`, and the password portion of
`DATABASE_URL` from environment variables instead of hardcoded plaintext. Use
`${VAR}` interpolation in the existing environment section so the values come
from a local `.env` file or runtime environment, and keep the change consistent
anywhere the same compose service variables are repeated.
- Around line 5-22: The postgres/redis stack is being duplicated across profile
compose files, which causes host port and network conflicts when profiles are
run together. Update the llmops compose definitions so they reuse the base
`postgres` and `redis` services instead of redeclaring them, using Compose file
merging or `extends`, and keep `docker-compose.llmops.yml` focused on only the
additional llmops services. Ensure the shared service names and bindings remain
defined in one place so `postgres` and `redis` can be combined safely with
dev/showcase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d9c4481-ce7b-45ff-a5f1-acba67ac5be7

📥 Commits

Reviewing files that changed from the base of the PR and between e4fb0f0 and 7ad79b8.

⛔ Files ignored due to path filters (1)
  • apps/ai/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (56)
  • .githooks/pre-push
  • .github/workflows/ci.yml
  • .gitignore
  • .opencode/context/adr/001-sarthi-v4-architecture-evolution.md
  • .opencode/context/domain/architecture.md
  • .opencode/context/domain/chat-flow.md
  • .opencode/context/standards/coding-standards.md
  • .opencode/context/templates/architecture-overview.md
  • AGENTS.md
  • Makefile
  • README.md
  • apps/ai/infrastructure/migrations/004_control_plane_audit.sql
  • apps/ai/pyproject.toml
  • apps/ai/src/agents/authority_manifest.py
  • apps/ai/src/agents/cofounder/curator.py
  • apps/ai/src/agents/tools/__init__.py
  • apps/ai/src/agents/tools/draft_investor_update.py
  • apps/ai/src/agents/tools/flag_churn_risk.py
  • apps/ai/src/agents/tools/pause_payment_retry.py
  • apps/ai/src/agents/tools/schedule_customer_checkin.py
  • apps/ai/src/control_plane/__init__.py
  • apps/ai/src/control_plane/audit.py
  • apps/ai/src/control_plane/policy.py
  • apps/ai/src/control_plane/registry.py
  • apps/ai/src/hitl/manager.py
  • apps/ai/src/integrations/hubspot.py
  • apps/ai/src/integrations/quickbooks.py
  • apps/ai/src/integrations/slack.py
  • apps/ai/src/integrations/slack_buttons.py
  • apps/ai/src/integrations/slack_client.py
  • apps/ai/src/risk/__init__.py
  • apps/ai/src/risk/output_risk.py
  • apps/ai/src/risk/prompt_risk.py
  • apps/ai/src/schemas/__init__.py
  • apps/ai/src/schemas/control_plane.py
  • apps/ai/src/schemas/guardian.py
  • apps/ai/src/schemas/self_guardian.py
  • apps/ai/src/self_guardian/__init__.py
  • apps/ai/src/self_guardian/detector.py
  • apps/ai/src/self_guardian/monitor.py
  • apps/ai/src/session/brief_generator.py
  • apps/ai/src/session/mission_state.py
  • apps/ai/tests/unit/test_agentic_comprehensive.py
  • apps/ai/tests/unit/test_control_plane.py
  • apps/ai/tests/unit/test_qa_agent.py
  • apps/ai/tests/unit/test_self_guardian.py
  • apps/core/hitl_queue_test.go
  • apps/core/internal/db/migrations/005_mission_state_explainability.sql
  • apps/core/internal/web/command_center_test.go
  • apps/core/internal/web/handler.go
  • apps/core/internal/web/sse_hub.go
  • apps/core/internal/web/templates/partials/command_alert_lineage.html
  • apps/core/internal/web/templates/partials/command_operating_layer.html
  • docker-compose.dev.yml
  • docker-compose.llmops.yml
  • docker-compose.showcase.yml
💤 Files with no reviewable changes (1)
  • apps/ai/src/integrations/hubspot.py

Comment thread .github/workflows/ci.yml
Comment on lines +14 to +88
typecheck:
name: Type Check
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/core
steps:
- uses: actions/checkout@v6

- name: Setup Go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: apps/core/go.sum

- name: Check formatting
go-version: "1.24"
- name: Go vet
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "Unformatted files:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Build
run: go build ./...

- name: Test (unit – no Docker)
run: go test ./internal/web/... ./internal/agents/... ./internal/workflow/... -v -count=1 -timeout=60s
cd apps/core && go vet ./... 2>&1 | grep -v 'gen/go' || true
- name: Set up uv
uses: astral-sh/setup-uv@v5
- name: Python mypy
run: |
cd apps/ai && uv sync --group dev && uv run mypy src/ --ignore-missing-imports || true

python:
name: Python – Lint & Unit Tests
lint:
name: Lint & Format
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/ai
steps:
- uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
python-version-file: ${{ env.PYTHON_VERSION_FILE }}
go-version: "1.24"
- name: Go fmt
run: |
cd apps/core && test -z "$(go fmt ./...)" || (echo "Go files not formatted:"; go fmt ./...; exit 1)
- name: Set up uv
uses: astral-sh/setup-uv@v5
- name: Ruff check
run: |
cd apps/ai && uv run ruff check src/ || echo "ruff not configured, skipping"
- name: Ruff format
run: |
cd apps/ai && uv run ruff format --check src/ || echo "ruff format not configured, skipping"

- name: Install uv
uses: astral-sh/setup-uv@v6
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: [typecheck, lint]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
enable-cache: true
cache-dependency-glob: "apps/ai/uv.lock"

- name: Sync dependencies
run: uv sync --locked --all-extras --dev
go-version: "1.24"
- name: Go test
run: |
cd apps/core && go test ./... -count=1 -timeout=5m
- name: Set up uv
uses: astral-sh/setup-uv@v5
- name: Python unit tests
run: |
cd apps/ai && uv run pytest tests/unit/ -q --timeout=60 --ignore=tests/unit/test_guardian_schemas.py --ignore=tests/unit/test_curator_graphiti.py -x

- name: Lint with ruff
run: uv run ruff check src/ tests/
workflow-lint:
name: Workflow Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: reviewdog/action-actionlint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Run unit tests
run: uv run pytest tests/unit/ -v --timeout=60 -x -q
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [unit-tests]
# Enable by adding: if: github.event_name == 'push' (or remove this comment)
steps:
- uses: actions/checkout@v4
- name: Docker compose integration
run: |
echo "Integration tests require Docker and are disabled by default."
echo "Enable by removing 'if: false' from ci.yml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add an explicit least-privilege permissions block and disable credential persistence.

None of the five jobs declare a permissions key, so the GITHUB_TOKEN falls back to the repo/org default (which can be permissive read/write). Combined with actions/checkout steps (Lines 18, 35, 56, 73, 84) that leave persist-credentials: true (the default), any dependency pulled in by uv sync, pip, ruff, or pytest in these jobs gets access to a token that can potentially write to the repo. None of these jobs need write access or a persisted git credential.

🔒 Proposed fix
+permissions:
+  contents: read
+
 jobs:
   typecheck:
     name: Type Check
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Apply the same persist-credentials: false to the checkout steps in lint, unit-tests, workflow-lint, and integration-tests. Give workflow-lint pull-requests: write (needed by reviewdog to post inline comments) as a job-level override.

You should therefore make sure that the GITHUB_TOKEN is granted the minimum required permissions. It's good security practice to set the default permission for the GITHUB_TOKEN to read access only for repository contents. the persisted token is doing nothing useful, and it is potentially a problem. Any third party action or package in that job now has access to your token. Depending on your permissions, that could be an elevated token that can open pull requests, write to your repo, or worse.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 18-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 35-35: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 56-56: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 73-73: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 84-84: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 14-29: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[warning] 31-49: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[warning] 51-67: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)


[warning] 69-76: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 14 - 88, The workflow jobs are using
the default GITHUB_TOKEN permissions and `actions/checkout` is persisting
credentials, which is broader access than needed. Add an explicit
least-privilege job-level `permissions` block for each job in `ci.yml`, keep
`workflow-lint` overridden with `pull-requests: write` for
`reviewdog/action-actionlint`, and set `persist-credentials: false` on every
`actions/checkout@v4` step in the `typecheck`, `lint`, `unit-tests`,
`workflow-lint`, and `integration-tests` jobs.

Source: Linters/SAST tools

Comment on lines +436 to +444
def _file_audit_write(delta: StrategyDelta) -> None:
"""Fallback: append StrategyDelta as JSON line to /tmp/strategy_audit.jsonl."""
import os

audit_path = "/tmp/strategy_audit.jsonl"
line = delta.model_dump_json() + "\n"
with open(audit_path, "a") as f:
f.write(line)
log.info(f"[Audit] Wrote strategy delta to {audit_path}: {delta.domain}/{delta.strategy_key}") No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Hardcoded /tmp path for audit log (CWE-377).

Writing audit records to a fixed, world-writable /tmp/strategy_audit.jsonl is insecure and unreliable for a compliance-relevant audit trail — the file can be tampered with by other local processes/users, may be cleared on reboot/container restart, and has no locking against concurrent writers. Make the path configurable via env var and write to a directory with restricted permissions.

🛡️ Proposed fix
 def _file_audit_write(delta: StrategyDelta) -> None:
     """Fallback: append StrategyDelta as JSON line to /tmp/strategy_audit.jsonl."""
     import os

-    audit_path = "/tmp/strategy_audit.jsonl"
+    audit_path = os.getenv("STRATEGY_AUDIT_LOG_PATH", "/var/log/sarthi/strategy_audit.jsonl")
+    os.makedirs(os.path.dirname(audit_path), exist_ok=True)
     line = delta.model_dump_json() + "\n"
     with open(audit_path, "a") as f:
         f.write(line)
📝 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
def _file_audit_write(delta: StrategyDelta) -> None:
"""Fallback: append StrategyDelta as JSON line to /tmp/strategy_audit.jsonl."""
import os
audit_path = "/tmp/strategy_audit.jsonl"
line = delta.model_dump_json() + "\n"
with open(audit_path, "a") as f:
f.write(line)
log.info(f"[Audit] Wrote strategy delta to {audit_path}: {delta.domain}/{delta.strategy_key}")
def _file_audit_write(delta: StrategyDelta) -> None:
"""Fallback: append StrategyDelta as JSON line to /tmp/strategy_audit.jsonl."""
import os
audit_path = os.getenv("STRATEGY_AUDIT_LOG_PATH", "/var/log/sarthi/strategy_audit.jsonl")
os.makedirs(os.path.dirname(audit_path), exist_ok=True)
line = delta.model_dump_json() + "\n"
with open(audit_path, "a") as f:
f.write(line)
log.info(f"[Audit] Wrote strategy delta to {audit_path}: {delta.domain}/{delta.strategy_key}")
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 439-439: Do not hardcode temporary file or directory names
Context: "/tmp/strategy_audit.jsonl"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)


[warning] 441-441: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(audit_path, "a")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/agents/cofounder/curator.py` around lines 436 - 444, The
_file_audit_write fallback currently writes audit records to a hardcoded
/tmp/strategy_audit.jsonl path, which is insecure and not durable. Update
_file_audit_write to resolve the audit file location from a configurable
environment variable instead of a fixed /tmp value, and default it to a
restricted-permission directory suitable for compliance logs. Ensure the path is
created securely with appropriate permissions before appending, and consider
adding basic concurrency-safe write handling so multiple writers do not corrupt
the audit trail.

"reason": "Output risk scan blocked draft",
},
)
_AUDIT.log(audit_event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Audit calls are broken: wrong method name and not awaited.

AuditLogger defines only async def log_event(...), so _AUDIT.log(audit_event) raises AttributeError. Even after renaming, log_event is a coroutine and must be awaited, otherwise the audit write silently never runs (unawaited-coroutine warning). Both blocked-path (Line 97) and success-path (Line 120) audit records would be lost.

🐛 Proposed fix
-        _AUDIT.log(audit_event)
+        await _AUDIT.log_event(audit_event)

Also applies to: 120-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/agents/tools/draft_investor_update.py` at line 97, The audit
logging in draft_investor_update is calling the wrong API and not awaiting the
async write. Update both audit paths in the draft_investor_update flow to use
AuditLogger.log_event(...) instead of _AUDIT.log(...), and ensure each call is
awaited so the coroutine actually runs. Check the blocked-path and success-path
audit records around the existing audit_event handling so both are recorded
correctly.

Comment on lines +20 to +24
async def log_event(
self,
event: AuditEvent,
conn: asyncpg.Connection | None = None,
) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites of the audit logger to confirm the method name used.
rg -nP '_AUDIT\.\w+\(' apps/ai/src
rg -nP 'def\s+(log|log_event)\b' apps/ai/src/control_plane/audit.py

Repository: Aparnap2/Track_Guard

Length of output: 353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## audit.py\n'
sed -n '1,220p' apps/ai/src/control_plane/audit.py

printf '\n## draft_investor_update.py\n'
sed -n '1,220p' apps/ai/src/agents/tools/draft_investor_update.py

printf '\n## audit logger references\n'
rg -nP '\b_AUDIT\.(log|log_event)\b|class\s+AuditLogger\b|def\s+(log|log_event)\b' apps/ai/src

Repository: Aparnap2/Track_Guard

Length of output: 7194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file list\n'
git ls-files apps/ai/src/control_plane/audit.py apps/ai/src/agents/tools/draft_investor_update.py

printf '\n## audit.py outline\n'
ast-grep outline apps/ai/src/control_plane/audit.py --view expanded

printf '\n## draft_investor_update.py outline\n'
ast-grep outline apps/ai/src/agents/tools/draft_investor_update.py --view expanded

Repository: Aparnap2/Track_Guard

Length of output: 763


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for path in [
    Path('apps/ai/src/control_plane/audit.py'),
    Path('apps/ai/src/agents/tools/draft_investor_update.py'),
]:
    print(f'\n## {path}')
    text = path.read_text()
    for needle in ['class AuditLogger', 'def log_event', 'def log(', '__getattr__', '_AUDIT =', 'AuditLogger(']:
        if needle in text:
            print(f'contains: {needle}')
PY

Repository: Aparnap2/Track_Guard

Length of output: 345


Expose log() or update callers
_AUDIT.log(...) is called, but AuditLogger only defines log_event(...). That will raise AttributeError when the audit path runs; either add a log() shim or switch the call sites to await log_event(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/control_plane/audit.py` around lines 20 - 24, The audit logger
interface is mismatched: callers use _AUDIT.log(...) but AuditLogger only
exposes log_event(...), which will fail at runtime. Update the audit path to
call await log_event(...) consistently, or add a log() shim on AuditLogger that
forwards to log_event so existing call sites continue to work. Use the
AuditLogger class and its log_event method to locate the change and keep the
async behavior intact.

Comment on lines +21 to +99
def evaluate(
self,
agent: AgentRegistration,
requested_tool: str | None = None,
) -> PolicyDecision:
"""Evaluate whether an agent action is permitted.

Args:
agent: The registered agent attempting the action.
requested_tool: The specific tool being requested, if any.

Returns:
PolicyDecision with approval state and constraints.
"""
approved_tools: list[str] = []
requires_human_approval = False
blocked_reason: str | None = None

# 1. Health check — unhealthy agents are blocked
if agent.health_status == "unhealthy":
blocked_reason = "agent_health_unhealthy"
log.warning(
"Policy block: agent %s is unhealthy",
agent.agent_name,
)
return PolicyDecision(
data_classification=agent.data_classification,
allowed_model_classes=[],
requires_human_approval=True,
blocked_reason=blocked_reason,
approved_tools=[],
)

# 2. Data classification check — restricted data blocks
if agent.data_classification.lower() in _RESTRICTED_CLASSIFICATIONS:
blocked_reason = "data_classification_restricted"
log.warning(
"Policy block: agent %s has restricted classification: %s",
agent.agent_name,
agent.data_classification,
)
return PolicyDecision(
data_classification=agent.data_classification,
allowed_model_classes=[],
requires_human_approval=True,
blocked_reason=blocked_reason,
approved_tools=[],
)

# 3. External-facing check — forces human approval
if agent.external_facing:
requires_human_approval = True

# 4. Degraded health — forces review but not full block
if agent.health_status == "degraded":
requires_human_approval = True
blocked_reason = "agent_health_degraded"

# 5. Tool permission check
if requested_tool is not None:
if requested_tool in agent.allowed_tools:
approved_tools.append(requested_tool)
else:
if blocked_reason is None:
blocked_reason = f"tool_not_allowed: {requested_tool}"
requires_human_approval = True

# 6. Determine allowed model classes
allowed_models = agent.allowed_models
if blocked_reason is not None:
allowed_models = []

return PolicyDecision(
data_classification=agent.data_classification,
allowed_model_classes=allowed_models,
requires_human_approval=requires_human_approval,
blocked_reason=blocked_reason,
approved_tools=approved_tools,
) No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
git ls-files | rg 'apps/ai/src/control_plane/(policy|authority_manifest|.*registration.*|.*authority.*)\.py$|apps/ai/src/.*/.*\.py$'
printf '\n--- search escalation_tier ---\n'
rg -n "escalation_tier|PolicyEngine\.evaluate|class AgentRegistration|class AgentAuthority|requires_human_approval" apps/ai/src -g '*.py'
printf '\n--- outline policy.py ---\n'
ast-grep outline apps/ai/src/control_plane/policy.py --view expanded
printf '\n--- outline authority_manifest.py ---\n'
ast-grep outline apps/ai/src/control_plane/authority_manifest.py --view expanded

Repository: Aparnap2/Track_Guard

Length of output: 12900


🏁 Script executed:

set -euo pipefail

printf '\n--- outline apps/ai/src/schemas/control_plane.py ---\n'
ast-grep outline apps/ai/src/schemas/control_plane.py --view expanded

printf '\n--- outline apps/ai/src/agents/authority_manifest.py ---\n'
ast-grep outline apps/ai/src/agents/authority_manifest.py --view expanded

printf '\n--- outline apps/ai/src/control_plane/registry.py ---\n'
ast-grep outline apps/ai/src/control_plane/registry.py --view expanded

printf '\n--- policy.py lines 1-140 ---\n'
cat -n apps/ai/src/control_plane/policy.py | sed -n '1,140p'

printf '\n--- control_plane.py lines 1-160 ---\n'
cat -n apps/ai/src/schemas/control_plane.py | sed -n '1,160p'

printf '\n--- authority_manifest.py lines 1-180 ---\n'
cat -n apps/ai/src/agents/authority_manifest.py | sed -n '1,180p'

printf '\n--- registry.py lines 1-120 ---\n'
cat -n apps/ai/src/control_plane/registry.py | sed -n '1,120p'

Repository: Aparnap2/Track_Guard

Length of output: 20628


🏁 Script executed:

set -euo pipefail
printf '\n--- global escalation_tier references ---\n'
rg -n "escalation_tier" apps/ai/src -g '*.py'
printf '\n--- approval_policy.py outline and relevant lines ---\n'
ast-grep outline apps/ai/src/orchestrators/approval_policy.py --view expanded
cat -n apps/ai/src/orchestrators/approval_policy.py | sed -n '1,220p'

Repository: Aparnap2/Track_Guard

Length of output: 3187


🏁 Script executed:

set -euo pipefail
printf '\n--- PolicyEngine call sites ---\n'
rg -n "PolicyEngine\(|evaluate\(" apps/ai/src -g '*.py'

printf '\n--- approval_state / requires_human_approval uses ---\n'
rg -n "requires_human_approval|approval_state|blocked_reason" apps/ai/src -g '*.py'

Repository: Aparnap2/Track_Guard

Length of output: 2902


Honor agent.escalation_tier in PolicyEngine.evaluate

evaluate() only uses health, classification, external-facing, and tool checks. Agents registered with escalation_tier="review" or "approve" can still be auto-approved here when those other rules don’t trigger, so the declared HITL tier is ignored. Fold the tier into this decision and treat blocked as a hard block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/control_plane/policy.py` around lines 21 - 99,
PolicyEngine.evaluate currently ignores agent.escalation_tier, so agents marked
review or approve can still pass without human involvement. Update evaluate in
PolicyEngine to incorporate escalation_tier alongside the existing health,
classification, external-facing, and tool checks: treat blocked as a hard deny,
set requires_human_approval for review and approve tiers as intended, and ensure
the final PolicyDecision reflects the tier even when no other rule triggers.

Comment on lines +168 to 175
prepared_brief=row["prepared_brief"],
pending_decisions=row["pending_decisions"],
last_updated_by=row["last_updated_by"],
last_update_reason=row["last_update_reason"],
last_changed_fields=row["last_changed_fields"],
active_agent_roles=row["active_agent_roles"],
policy_state=row["policy_state"],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path("apps/ai/src/session/mission_state.py")
print(p.exists(), p)
if p.exists():
    lines = p.read_text().splitlines()
    for start in (1, 1):
        pass
PY

Repository: Aparnap2/Track_Guard

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mission_state.py outline =="
ast-grep outline apps/ai/src/session/mission_state.py --view expanded || true

echo
echo "== relevant slices =="
nl -ba apps/ai/src/session/mission_state.py | sed -n '1,260p'

echo
echo "== search for JSON field usage and tests =="
rg -n "pending_decisions|last_changed_fields|active_agent_roles|policy_state|prepared_brief|burn_alert|records_update_reason_and_policy_state" apps/ai -S

Repository: Aparnap2/Track_Guard

Length of output: 2241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("apps/ai/src/session/mission_state.py")
text = path.read_text().splitlines()
for start, end in [(1, 260)]:
    for i, line in enumerate(text[start-1:end], start):
        print(f"{i:4d}: {line}")
PY

echo
echo "== tests mentioning mission state =="
python3 - <<'PY'
from pathlib import Path
import re

for path in Path("apps/ai").rglob("*test*.py"):
    txt = path.read_text()
    if "mission_state" in txt or "burn_alert" in txt or "last_changed_fields" in txt or "policy_state" in txt:
        print(f"\n--- {path} ---")
        for i, line in enumerate(txt.splitlines(), 1):
            if re.search(r"mission_state|burn_alert|last_changed_fields|policy_state|pending_decisions|active_agent_roles", line):
                print(f"{i:4d}: {line}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 22769


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
for path in Path("apps/ai").rglob("*.sql"):
    txt = path.read_text(errors="ignore")
    if "mission_states" in txt:
        print(f"\n--- {path} ---")
        for i, line in enumerate(txt.splitlines(), 1):
            if "mission_states" in line or "pending_decisions" in line or "last_changed_fields" in line or "active_agent_roles" in line or "policy_state" in line:
                print(f"{i:4d}: {line}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 2381


🌐 Web query:

asyncpg jsonb default decoding Python objects or raw text documentation

💡 Result:

By default, asyncpg does not automatically decode PostgreSQL json and jsonb types into Python objects [1][2]. Instead, it returns them as raw strings (text) [3][4][2]. To have asyncpg automatically decode these types into Python objects, you must explicitly register a custom type codec using the connection's set_type_codec method [1][5][2]. This is typically done by utilizing the standard library's json module [3][1]. Example configuration: import json import asyncpg async def setup_json_codec(conn): await conn.set_type_codec( 'json', encoder=json.dumps, decoder=json.loads, schema='pg_catalog') await conn.set_type_codec( 'jsonb', encoder=json.dumps, decoder=json.loads, schema='pg_catalog') Once configured, asyncpg will automatically handle the conversion between Python dictionaries/lists and PostgreSQL JSON/JSONB types during query execution [1]. If you are using a connection pool, you can use the init parameter of create_pool to apply this codec to all connections in the pool [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mission_states schema slice =="
python3 - <<'PY'
from pathlib import Path
path = Path("apps/ai/src/session/001_session_layer.sql")
text = path.read_text().splitlines()
for i, line in enumerate(text, 1):
    if 12 <= i <= 55:
        print(f"{i:4d}: {line}")
PY

echo
echo "== search for JSON codecs / asyncpg setup =="
python3 - <<'PY'
from pathlib import Path
for path in Path("apps/ai").rglob("*.py"):
    txt = path.read_text(errors="ignore")
    if "set_type_codec" in txt or "asyncpg.connect(" in txt or "create_pool(" in txt:
        print(f"\n--- {path} ---")
        for i, line in enumerate(txt.splitlines(), 1):
            if "set_type_codec" in line or "asyncpg.connect(" in line or "create_pool(" in line:
                print(f"{i:4d}: {line}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 3082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search for the four fields in schema/migrations =="
python3 - <<'PY'
from pathlib import Path
patterns = ["pending_decisions", "last_changed_fields", "active_agent_roles", "policy_state"]
for path in Path("apps/ai").rglob("*"):
    if path.suffix not in {".sql", ".py", ".md", ".yml", ".yaml"}:
        continue
    try:
        txt = path.read_text(errors="ignore")
    except Exception:
        continue
    if any(p in txt for p in patterns):
        print(f"\n--- {path} ---")
        for i, line in enumerate(txt.splitlines(), 1):
            if any(p in line for p in patterns):
                print(f"{i:4d}: {line}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 2648


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
path = Path("apps/ai/src/session/mission_state.py")
text = path.read_text().splitlines()
for start, end in [(260, 300)]:
    for i in range(start, min(end, len(text)) + 1):
        print(f"{i:4d}: {text[i-1]}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 1975


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

paths = [
    Path("apps/ai/tests/integration/test_mission_state.py"),
    Path("apps/ai/src/session/mission_state.py"),
]
for path in paths:
    print(f"\n== {path} ==")
    text = path.read_text().splitlines()
    for i, line in enumerate(text, 1):
        if path.name == "test_mission_state.py":
            if 80 <= i <= 240:
                print(f"{i:4d}: {line}")
        else:
            if 106 <= i <= 300:
                print(f"{i:4d}: {line}")
PY

Repository: Aparnap2/Track_Guard

Length of output: 16936


Deserialize the structured MissionState fields on read.
pending_decisions, last_changed_fields, active_agent_roles, and policy_state are passed straight into MissionState here, so callers get raw string values instead of the declared list/dict types. Decode them (or register a JSON codec on the connection) before returning the dataclass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/session/mission_state.py` around lines 168 - 175, The
MissionState read path is returning raw serialized strings for structured fields
instead of the declared list/dict types. In the MissionState construction logic,
deserialize pending_decisions, last_changed_fields, active_agent_roles, and
policy_state before passing them into MissionState, or apply a JSON codec at the
connection level so the dataclass receives decoded values. Use the MissionState
read/row-mapping code to locate the fix.

Comment on lines +182 to +198
async def update_mission_state(state: MissionState, generate_brief: bool = True, update_reason: str | None = None, changed_fields: list[str] | None = None) -> bool:
"""Update MissionState in database atomically.

Per PRD Section 11: Updated atomically.

Args:
state: MissionState to persist
generate_brief: Whether to auto-generate prepared_brief if missing
update_reason: Why this write happened
changed_fields: Which fields were modified in this update

Returns:
True if successful, False otherwise
"""
if generate_brief:
state.last_update_reason = update_reason
state.last_changed_fields = changed_fields

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

update_mission_state silently nulls out explainability fields on most calls.

When generate_brief=True (the default) and a caller doesn't pass update_reason/changed_fields, Lines 196-198 unconditionally overwrite state.last_update_reason/state.last_changed_fields with None before persisting — even if state was loaded via get_mission_state and already carried real audit-trail values, or if the caller set them directly on state before calling this function. Any plain await update_mission_state(state) call will wipe existing explainability history in the DB.

🐛 Proposed fix
-    if generate_brief:
-        state.last_update_reason = update_reason
-        state.last_changed_fields = changed_fields
+    if update_reason is not None:
+        state.last_update_reason = update_reason
+    if changed_fields is not None:
+        state.last_changed_fields = changed_fields
📝 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 update_mission_state(state: MissionState, generate_brief: bool = True, update_reason: str | None = None, changed_fields: list[str] | None = None) -> bool:
"""Update MissionState in database atomically.
Per PRD Section 11: Updated atomically.
Args:
state: MissionState to persist
generate_brief: Whether to auto-generate prepared_brief if missing
update_reason: Why this write happened
changed_fields: Which fields were modified in this update
Returns:
True if successful, False otherwise
"""
if generate_brief:
state.last_update_reason = update_reason
state.last_changed_fields = changed_fields
async def update_mission_state(state: MissionState, generate_brief: bool = True, update_reason: str | None = None, changed_fields: list[str] | None = None) -> bool:
"""Update MissionState in database atomically.
Per PRD Section 11: Updated atomically.
Args:
state: MissionState to persist
generate_brief: Whether to auto-generate prepared_brief if missing
update_reason: Why this write happened
changed_fields: Which fields were modified in this update
Returns:
True if successful, False otherwise
"""
if update_reason is not None:
state.last_update_reason = update_reason
if changed_fields is not None:
state.last_changed_fields = changed_fields
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/session/mission_state.py` around lines 182 - 198,
update_mission_state is overwriting existing explainability data with None
whenever generate_brief is true and callers omit update_reason/changed_fields.
In update_mission_state, only assign state.last_update_reason and
state.last_changed_fields when those arguments are explicitly provided, and
otherwise preserve any existing values already on the MissionState instance from
get_mission_state or caller-set fields. Keep the behavior localized to
update_mission_state so plain await update_mission_state(state) does not clear
audit-trail history.

Comment on lines +210 to +217
guardrail_blocking, investor_facing_alert, created_at,
prepared_brief, pending_decisions, last_updated_by,
last_update_reason, last_changed_fields, active_agent_roles,
policy_state
)
VALUES ($1, NOW(), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, NOW())
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, NOW(),
$26, $27, $28, $29, $30, $31, $32)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for prior migrations defining these columns on mission_states
rg -n "prepared_brief|pending_decisions|last_updated_by|policy_state" --type sql
fd -e sql . apps/core/internal/db/migrations apps/ai/infrastructure/migrations 2>/dev/null | sort

Repository: Aparnap2/Track_Guard

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mission_state.py locations =="
fd -a 'mission_state.py' .

echo
echo "== migration files mentioning mission_states or relevant columns =="
rg -n --hidden --glob '*.sql' --glob '*.py' --glob '*.md' \
  'mission_states|prepared_brief|pending_decisions|last_updated_by|policy_state|last_update_reason|last_changed_fields|active_agent_roles' \
  apps 2>/dev/null || true

echo
echo "== candidate migration directories =="
fd -a -e sql . apps 2>/dev/null | sed -n '1,200p'

Repository: Aparnap2/Track_Guard

Length of output: 13976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  apps/ai/src/session/001_session_layer.sql \
  apps/core/internal/db/migrations/004_resolve_mission_state_drift.sql \
  apps/ai/src/session/mission_state.py
do
  echo "== $f =="
  wc -l "$f"
  echo
done

echo "== 001_session_layer.sql (first 180 lines) =="
sed -n '1,180p' apps/ai/src/session/001_session_layer.sql

echo
echo "== 004_resolve_mission_state_drift.sql =="
cat -n apps/core/internal/db/migrations/004_resolve_mission_state_drift.sql

echo
echo "== mission_state.py relevant slices =="
sed -n '80,310p' apps/ai/src/session/mission_state.py

Repository: Aparnap2/Track_Guard

Length of output: 16336


Add policy_state to mission_states or stop persisting it here. 004_resolve_mission_state_drift.sql already covers prepared_brief, pending_decisions, and last_updated_by, but policy_state is still absent from the table. Both get_mission_state and update_mission_state read/write that column, so calls will hit a Postgres column error and fall back to the broad exception handlers. apps/ai/src/session/mission_state.py

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/session/mission_state.py` around lines 210 - 217, Add
`policy_state` support consistently in the mission state persistence path:
either migrate the `mission_states` table to include the missing column or
remove it from `get_mission_state` and `update_mission_state` so they no longer
read/write it. Check the `mission_state.py` query builders around
`get_mission_state`/`update_mission_state`, since the current INSERT/SELECT
logic references `policy_state` even though the schema drift migration only
covers the other fields. Make the schema and the two session methods match so
Postgres no longer throws a missing-column error.

Comment on lines +277 to +283
state.prepared_brief,
state.pending_decisions,
state.last_updated_by,
state.last_update_reason,
json.dumps(state.last_changed_fields) if state.last_changed_fields else '[]',
json.dumps(state.active_agent_roles) if state.active_agent_roles else '[]',
json.dumps(state.policy_state) if state.policy_state else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
ast-grep outline apps/ai/src/session/mission_state.py --view expanded

printf '\n--- mission_state.py slice ---\n'
sed -n '240,320p' apps/ai/src/session/mission_state.py

printf '\n--- search for policy_state / pending_decisions / asyncpg usage ---\n'
rg -n "pending_decisions|policy_state|asyncpg|json.dumps" apps/ai/src/session apps/ai/tests apps/ai/src -g '!**/__pycache__/**'

printf '\n--- locate test_mission_state_policy_state_field ---\n'
rg -n "test_mission_state_policy_state_field|PolicyDecision" apps/ai -g '!**/__pycache__/**'

Repository: Aparnap2/Track_Guard

Length of output: 19355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- mission_state.py top/model definitions ---\n'
sed -n '1,120p' apps/ai/src/session/mission_state.py

printf '\n--- test_control_plane relevant section ---\n'
sed -n '340,390p' apps/ai/tests/unit/test_control_plane.py

printf '\n--- PolicyDecision schema ---\n'
sed -n '1,140p' apps/ai/src/schemas/control_plane.py

printf '\n--- search for custom json/jsonb codecs on asyncpg connections ---\n'
rg -n "set_type_codec|jsonb|json" apps/ai/src apps/ai/tests -g '!**/__pycache__/**'

Repository: Aparnap2/Track_Guard

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- mission_states table migration ---\n'
rg -n "mission_states|pending_decisions|policy_state" apps/ai/infrastructure apps/ai/src -g '*.sql' -g '*.py'

printf '\n--- trust_battery_db write path ---\n'
sed -n '1,140p' apps/ai/src/services/trust_battery_db.py

printf '\n--- mission_state update helper around parameters ---\n'
sed -n '220,290p' apps/ai/src/session/mission_state.py

printf '\n--- state writers assigning policy_state ---\n'
rg -n "policy_state\s*=" apps/ai/src apps/ai/tests -g '!**/__pycache__/**'

Repository: Aparnap2/Track_Guard

Length of output: 14289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- mission_states schema ---\n'
sed -n '1,140p' apps/ai/src/session/001_session_layer.sql

printf '\n--- look for jsonb columns in other mission_state-related migrations ---\n'
rg -n "pending_decisions|policy_state|jsonb|JSONB" apps/ai/infrastructure apps/ai/src/session -g '*.sql' -g '*.py'

Repository: Aparnap2/Track_Guard

Length of output: 5590


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- all mission_states DDL / ALTERs ---\n'
rg -n "mission_states\s*\(|ALTER TABLE mission_states|ADD COLUMN.*pending_decisions|ADD COLUMN.*policy_state" apps/ai -g '*.sql' -g '*.py'

printf '\n--- inspect any later migration files in session directory ---\n'
fd -t f . apps/ai/src/session apps/ai/infrastructure/migrations apps/ai/infrastructure | sort

Repository: Aparnap2/Track_Guard

Length of output: 3328


Add the missing mission_states columns
apps/ai/src/session/mission_state.py writes pending_decisions and policy_state, but apps/ai/src/session/001_session_layer.sql and the later migrations never add those columns. The insert/select will fail with column ... does not exist, so the save path returns False and drops the update.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 280-280: use jsonify instead of json.dumps for JSON output
Context: json.dumps(state.last_changed_fields)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 281-281: use jsonify instead of json.dumps for JSON output
Context: json.dumps(state.active_agent_roles)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 282-282: use jsonify instead of json.dumps for JSON output
Context: json.dumps(state.policy_state)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ai/src/session/mission_state.py` around lines 277 - 283, The mission
state save/query path in mission_state.py now references pending_decisions and
policy_state, but the session schema migrations never add those columns, so the
insert/select will fail. Update the session layer migration set, including
001_session_layer.sql and any later migration definitions, to add the missing
mission_states columns in the same order/types expected by
MissionStateRepository’s save/load logic so the database schema matches the
fields used in mission_state.py.

Comment on lines +131 to +146
qdrant:
image: qdrant/qdrant:latest
container_name: sarthi-showcase-qdrant
ports:
- "6333:6333" # REST API
- "6334:6334" # gRPC
volumes:
- sarthi-showcase-qdrant-data:/var/lib/qdrant
networks:
- iterateswarm-net
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
interval: 10s
timeout: 5s
retries: 5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '125,150p' docker-compose.showcase.yml

Repository: Aparnap2/Track_Guard

Length of output: 870


🌐 Web query:

Qdrant default storage path /qdrant/storage documentation

💡 Result:

In the Qdrant container environment, the default internal directory where data is stored is /qdrant/storage [1][2]. When running Qdrant locally outside of a container, the default storage path is./storage, which is relative to the current working directory from which the application is executed [3][4][5]. Key details regarding storage paths: * Container Deployments: For persistent storage in Docker, you must map a host directory or volume to the internal /qdrant/storage path [1][2]. If you do not mount this directory, all data will be lost when the container is removed [6]. * Configuration: The storage path can be customized via the configuration file (typically config/config.yaml) under the storage_path setting [3][4], or by using the QDRANTSTORAGESTORAGE_PATH environment variable [5][1]. * Snapshots: Similarly, the default internal path for snapshots is /qdrant/snapshots [1]. This can also be configured via the snapshots_path setting in the configuration file [3][4]. Users are advised to ensure that the volume mapping in their Docker or Kubernetes configuration correctly targets the internal /qdrant/storage directory to ensure data persistence [1][2].

Citations:


Mount Qdrant data at /qdrant/storage

Qdrant persists data in /qdrant/storage inside the container, so mounting the volume to /var/lib/qdrant leaves the real storage directory unbacked and data will be lost when the container is recreated.

🐛 Proposed fix
     volumes:
-      - sarthi-showcase-qdrant-data:/var/lib/qdrant
+      - sarthi-showcase-qdrant-data:/qdrant/storage
📝 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
qdrant:
image: qdrant/qdrant:latest
container_name: sarthi-showcase-qdrant
ports:
- "6333:6333" # REST API
- "6334:6334" # gRPC
volumes:
- sarthi-showcase-qdrant-data:/var/lib/qdrant
networks:
- iterateswarm-net
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
interval: 10s
timeout: 5s
retries: 5
qdrant:
image: qdrant/qdrant:latest
container_name: sarthi-showcase-qdrant
ports:
- "6333:6333" # REST API
- "6334:6334" # gRPC
volumes:
- sarthi-showcase-qdrant-data:/qdrant/storage
networks:
- iterateswarm-net
healthcheck:
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"]
interval: 10s
timeout: 5s
retries: 5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.showcase.yml` around lines 131 - 146, Update the qdrant
service in docker-compose.showcase.yml so the persistent volume is mounted to
Qdrant’s actual storage path instead of /var/lib/qdrant. Use the qdrant service
definition and its volumes entry to point sarthi-showcase-qdrant-data at
/qdrant/storage, keeping the rest of the service configuration unchanged.

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.

1 participant