# Sentinel Architecture - The Gate

**Module:** `empirica.core.sentinel`

The Sentinel is the gate controller of Empirica's cognitive architecture. It governs the transition between noetic (investigation) and praxic (action) phases, enforces compliance, and tracks epistemic transactions.

**Related docs:**
- [NOETIC_PRAXIC_FRAMEWORK.md](./NOETIC_PRAXIC_FRAMEWORK.md) - The noetic/praxic phase framework
- [CASCADE Workflow](../../plugins/claude-code-integration/skills/empirica-framework/references/cascade-workflow.md) - PREFLIGHT→CHECK→POSTFLIGHT phases
- [CONFIGURATION_REFERENCE.md](../reference/CONFIGURATION_REFERENCE.md) - EMPIRICA_SENTINEL_LOOPING and autopilot settings
- [Architecture README](./README.md) - System overview

## Philosophy

The Sentinel doesn't think for the AI - it provides governance:
- **Gate control**: Determines proceed vs investigate based on vectors
- **Compliance**: Enforces domain-specific rules (HIPAA, SOX, etc.)
- **Loop tracking**: Monitors convergence across epistemic cycles
- **Dual defense**: NoeticFilter (cognition) + AxiologicGate (action)

---

## Core Architecture

```
                    ┌─────────────────────┐
                    │      Sentinel       │
                    │   (Orchestrator)    │
                    └─────────┬───────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐   ┌─────────────────┐   ┌─────────────────┐
│ DecisionLogic │   │ EpistemicLoop   │   │ DomainProfile   │
│ (Persona)     │   │ Tracker         │   │ (Compliance)    │
└───────────────┘   └─────────────────┘   └─────────────────┘
```

---

## Classes Reference

### Gate Actions & Enums

#### GateAction
Actions that compliance gates can take:
- `PROCEED` - Continue execution
- `INVESTIGATE` - Return to noetic phase
- `HALT_AND_AUDIT` - Stop and log for audit
- `REQUIRE_HUMAN` - Pause for human approval
- `ESCALATE` - Escalate to higher authority
- `LOG_AND_CONTINUE` - Log concern but proceed

#### LoopMode
Who decides loop count:
- `USER` - User specifies exact count
- `AI` - AI chooses based on task
- `SENTINEL` - Sentinel governs with convergence detection

#### MergeStrategy
Strategies for merging parallel agent results:
- `CONSENSUS` - All agents must agree
- `BEST_SCORE` - Take highest merge_score result
- `WEIGHTED` - Weight by merge_score
- `UNION` - Combine all findings
- `INTERSECTION` - Only common findings

#### GatePhase
Phase during which a gate operates:
- `NOETIC` - Cognition/investigation phase
- `PRAXIC` - Action/execution phase
- `CHECK` - During CHECK gate transition
- `ANY` - Applies to all phases

---

### Dual Defense Layers

#### NoeticFilter
Cognition-level defense layer. Operates during NOETIC phase to filter what investigation paths are allowed.

```python
filter = NoeticFilter(
    filter_id="block_exploits",
    name="Exploit Investigation Block",
    blocked_patterns=[r"exploit", r"vulnerability.*poc"],
    blocked_domains=["security-research", "penetration-testing"],
    action_on_match=GateAction.INVESTIGATE,
    allow_with_justification=True
)

result = filter.evaluate({
    "task": "Research SQL injection techniques",
    "path": "/security/exploits/",
    "domain": "security-research"
})
# Returns: {"filter_id": "block_exploits", "matched_domain": "security-research", ...}
```

**Use cases:**
- Block investigation of exploit development
- Restrict access to sensitive codebase areas
- Prevent deep-diving into user credentials

#### AxiologicGate
Action/value-level defense. Operates during PRAXIC phase to validate actions against value constraints.

```python
gate = AxiologicGate(
    gate_id="critical_delete",
    name="Critical File Deletion Gate",
    action_patterns=[r"delete.*production", r"rm.*-rf"],
    required_vectors={"know": 0.85, "uncertainty": 0.15},
    action_on_violation=GateAction.REQUIRE_HUMAN,
    audit_required=True
)

result = gate.evaluate({
    "action": "delete database",
    "target": "production/data.db",
    "vectors": {"know": 0.6, "uncertainty": 0.4}
})
# Returns violation info due to insufficient vectors
```

**Use cases:**
- Prevent deletion of critical files without confirmation
- Block push to main branch without review
- Require audit trail for sensitive operations

---

### Compliance Framework

#### ComplianceGate
A compliance gate that runs during CHECK phase.

```python
gate = ComplianceGate(
    gate_id="pii_check",
    condition="pii_detected",
    action=GateAction.HALT_AND_AUDIT,
    description="Halt if PII detected without authorization",
    priority="critical"
)
```

**Condition types:**
- Vector-based: `"uncertainty > 0.5"`, `"know < 0.7"`
- Flag-based: `"pii_detected"`, `"high_risk"`
- Custom: `"high_risk"` (uncertainty > 0.6 AND impact > 0.7)

#### DomainProfile
Domain-specific configuration for compliance frameworks.

```python
profile = DomainProfile(
    name="healthcare",
    compliance_framework="HIPAA",
    uncertainty_trigger=0.3,  # More cautious
    confidence_to_proceed=0.85,
    gates=[
        ComplianceGate(
            gate_id="pii_check",
            condition="pii_detected",
            action=GateAction.HALT_AND_AUDIT
        )
    ],
    audit_all_actions=True,
    audit_retention_days=2555  # 7 years for HIPAA
)
```

**Built-in profiles:**
- `general` - Default thresholds
- `healthcare` - HIPAA compliance
- `finance` - SOX compliance

---

### Loop Tracking

#### LoopRecord
Record of a single epistemic transaction (PREFLIGHT → POSTFLIGHT).

```python
record = LoopRecord(
    loop_number=3,
    preflight_vectors={"know": 0.5, "uncertainty": 0.5},
    postflight_vectors={"know": 0.7, "uncertainty": 0.3},
    delta={"know": 0.2, "uncertainty": -0.2},
    findings_count=5,
    unknowns_count=2,
    check_decision="proceed"
)
```

#### EpistemicLoopTracker
Tracks epistemic transactions for convergence detection and termination.

```python
tracker = EpistemicLoopTracker(
    scope_breadth=0.6,      # Higher = more loops expected
    scope_duration=0.5,
    max_loops=5,
    convergence_threshold=0.03,  # Delta below this = converged
    mode=LoopMode.SENTINEL
)

# Start loop at PREFLIGHT
loop_num = tracker.start_loop({"know": 0.5, "uncertainty": 0.5})

# Complete loop at POSTFLIGHT
record = tracker.complete_loop(
    {"know": 0.7, "uncertainty": 0.3},
    findings_count=5,
    unknowns_count=2
)

# Check if more loops needed
if tracker.should_continue():
    # Another loop needed
else:
    # Converged or max loops reached
```

**Convergence detection:**
- Tracks delta between PREFLIGHT and POSTFLIGHT vectors
- Converged when delta < threshold for N consecutive loops
- Prevents infinite investigation loops

---

### Persona Selection

#### DomainSignal
Signal from domain analysis for persona matching.

#### PersonaMatch
Result of persona selection with confidence score and rationale.

```python
match = PersonaMatch(
    persona_id="security_researcher",
    score=0.85,
    rationale="Task mentions security, authentication, vulnerabilities"
)
```

#### DecisionLogic
Selects appropriate personas for tasks using semantic matching.

```python
logic = DecisionLogic(qdrant_host="localhost", qdrant_port=6333)
matches = logic.select_personas(
    task="Review authentication implementation for security issues",
    max_personas=3,
    required_domains=["security"],
    excluded_personas=["junior_dev"]
)
```

---

### Orchestration

#### OrchestrationResult
Result of orchestrating a multi-agent task.

```python
result = OrchestrationResult(
    ok=True,
    task="Security review of auth module",
    personas_selected=[...],
    agents_spawned=["branch_abc", "branch_def"],
    aggregated_findings=["Finding 1", "Finding 2"],
    aggregated_unknowns=["Unknown 1"],
    merge_strategy=MergeStrategy.UNION,
    merged_vectors={"know": 0.75, "uncertainty": 0.25},
    compliance_check={"decision": "proceed", ...}
)
```

#### Sentinel
The main orchestrator class that ties everything together.

```python
sentinel = Sentinel(session_id="abc123")

# Load compliance profile
sentinel.load_domain_profile("healthcare")

# Initialize loop tracking
sentinel.init_loop_tracking(
    scope_breadth=0.6,
    scope_duration=0.5,
    mode=LoopMode.SENTINEL
)

# Orchestrate a task
result = sentinel.orchestrate(
    task="Review patient data handling",
    max_agents=3,
    merge_strategy=MergeStrategy.UNION
)

# Check compliance
compliance = sentinel.check_compliance(
    vectors={"know": 0.7, "uncertainty": 0.3},
    findings=["Data encrypted at rest"],
    unknowns=["Audit log retention unclear"],
    flags={"pii_detected": True}
)
```

---

## Integration with CASCADE

```
PREFLIGHT ──────────────────► CHECK ──────────────────► POSTFLIGHT ──────────────────► POST-TEST
    │                           │                           │                              │
    │                           │                           │                              │
    ▼                           ▼                           ▼                              ▼
sentinel.start_loop()    sentinel.check_compliance()   sentinel.complete_loop()    sentinel.verify_grounded()
                               │                                                   (Grounded Verification)
                               ▼
                        ┌──────────────┐
                        │ GateActions  │
                        │ ─────────────│
                        │ PROCEED      │──► Praxic phase
                        │ INVESTIGATE  │──► Back to Noetic
                        │ HALT_AUDIT   │──► Stop + log
                        │ REQUIRE_HUMAN│──► Pause
                        │ ESCALATE     │──► Higher authority
                        └──────────────┘
```

---

## Autonomy Calibration Loop

The Sentinel implements an adaptive transaction scope feedback loop:

### Three Touch Points

```
PREFLIGHT                    Sentinel (each PreToolUse)          POSTFLIGHT
    |                               |                                |
 Queries last 20             Increments tool_call_count        Records final count
 POSTFLIGHT reflex_data      Computes nudge ratio              in reflex_data
 → avg_turns                 = count / avg_turns               → closes feedback loop
```

### Implementation Details

**PREFLIGHT** (`workflow_commands.py`):
- Queries `json_extract(reflex_data, '$.tool_call_count')` from last 20 POSTFLIGHTs
- Computes mean → writes `avg_turns` to `active_transaction_{instance}.json`
- First transaction has no history → `avg_turns = 0` (nudging disabled)

**Sentinel** (`sentinel-gate.py`):
- `_try_increment_tool_count()`: Reads transaction file, increments `tool_call_count`, atomic write-back via tmpfile+rename
- `_compute_nudge()`: Computes `ratio = count / avg_turns`, returns threshold message
- Nudge appended to `permissionDecisionReason` on allow decisions
- `suppressOutput` set to `False` when nudge active (ensures user sees it)

**POSTFLIGHT** (`workflow_commands.py`):
- Reads `tool_call_count` and `avg_turns` from transaction file
- Stores both in `reflex_data` checkpoint metadata
- This data feeds the NEXT transaction's PREFLIGHT avg calculation

### Nudge Thresholds

| Ratio | Level | Behavior |
|-------|-------|----------|
| < 1.0x | None | Silent pass-through |
| >= 1.0x | Info | "Past average. Natural POSTFLIGHT point." |
| >= 1.5x | Warning | "Consider POSTFLIGHT soon." |
| >= 2.0x | Strong | "POSTFLIGHT strongly recommended." |

### Design Rationale

- **Informational, not forced:** The agent decides when to POSTFLIGHT. Forcing closure
  at arbitrary points produces incoherent transactions with meaningless deltas.
- **Adaptive to working style:** avg_turns converges to the agent's actual pattern.
  A thorough researcher will have higher avg_turns than a quick fixer.
- **Includes delegated work:** Subagent tool calls are added to parent's count,
  preventing budget evasion via delegation.

---

## Subagent CASCADE Exemption

Subagents (spawned via Claude Code's Task tool) are exempt from Sentinel gating.

### Detection Mechanism

```python
# In sentinel-gate.py main(), before main processing:
claude_session_id = hook_input.get('session_id')
for aw_file in (~/.empirica/).glob('active_work_*.json'):
    if aw_file.stem.replace('active_work_', '') == claude_session_id:
        is_known_session = True
        break
if not is_known_session:
    respond("allow", "Subagent exemption")
    sys.exit(0)
```

**Logic:** Parent sessions always have an `active_work_{session_id}.json` file
(written by session-init hook). Subagents have different Claude session IDs
and no active_work file → they're detected and exempted.

### Rationale

1. The parent's CHECK already authorized the spawn
2. Subagents don't have their own PREFLIGHT/CHECK state
3. Double-gating would block all subagent tool use
4. Subagents are bounded by `maxTurns` and attention budget instead

### Tool Count Isolation

Subagent tool calls are NOT counted in the Sentinel's per-PreToolUse increment.
Instead, SubagentStop aggregates the count post-hoc and adds it to the parent's
`delegated_tool_calls`. This prevents double-counting.

---

## Claude Code Hook Integration

The Sentinel also integrates with Claude Code via a PreToolUse hook that gates praxic tools (Edit, Write, certain Bash commands).

### Hook: sentinel-gate.py

Location: `plugins/claude-code-integration/hooks/sentinel-gate.py`

**Behavior:**
1. Intercepts Edit, Write, and non-read-only Bash commands
2. Checks for valid CHECK with `decision="proceed"`
3. Blocks if no CHECK or CHECK returned "investigate"
4. Validates vector thresholds (know >= 0.70, uncertainty <= 0.35)

**Environment Variables:**

| Variable | Default | Description |
|----------|---------|-------------|
| `EMPIRICA_SENTINEL_LOOPING` | `true` | Set to `false` to disable Sentinel gating entirely (env var fallback) |
| `EMPIRICA_SENTINEL_CHECK_EXPIRY` | `false` | Set to `true` to enable 30-minute CHECK expiry |

**File-based control (preferred):** `~/.empirica/sentinel_enabled` — write `true` or `false`.
Takes priority over the env var and is dynamically settable without session restart.
The AI can toggle this at runtime; env vars require restarting the terminal/session.

**Note on CHECK Expiry:** The age-based expiry is disabled by default because users may pause work and resume later. Wall-clock time doesn't reflect actual session activity. Enable with caution.

### hooks.json Configuration

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/sentinel-gate.py",
          "timeout": 10
        }]
      }
    ]
  }
}
```

---

## Source Files

- `empirica/core/sentinel/orchestrator.py` - Main Sentinel class
- `empirica/core/sentinel/decision_logic.py` - Persona selection logic
- `plugins/claude-code-integration/hooks/sentinel-gate.py` - Claude Code hook
