# Numu AI Operating System — Complete Lifecycle

> End-to-end reference for AI Agents, Actions, Policies, Approvals, Executions,
> Handoffs, Audits and the Make integration — as **actually implemented**. Numu is
> the authoritative owner of policy, validation, approvals, execution and audit;
> Make is orchestration + AI only. **Status: implemented, not yet deployed** (branch `main`, uncommitted).

---

## 1. High-level architecture (as implemented)

```
Make (orchestration + AI)                                   OWNER
   │  POST /agent-runs                                      Make → Numu
   ▼
Numu Agent Runs ........... agent_runs ................... Numu  (AgentRunService)
   │  POST /agent-runs/{run}/decision
   ▼
[1b] Pause/Disable gate ... agent_runtime_config / agents . Numu  (AgentRuntimeService → AgentDecisionService)
   ▼
Policy Engine ............. agent_action_policies,         Numu  (ActionPolicyService)
   │                        policy_runtime_overrides
   ▼
   ├── always_allow ─────────────────────────────────┐
   ├── needs_approval → Approval Engine ........ agent_approval_requests   Numu (ApprovalService) → human → ApprovalExecutionService
   └── blocked / default → stop                       │
   ▼ (always_allow, or after approve)                 │
Expected-State Validation . (reads subject group/status) Numu (ExpectedStateValidator)
   │  match → continue · mismatch → reject              │
   ▼                                                   │
Execution claim ........... execution_requests ......... Numu (ExecutionRequestService, idempotency+lock)
   ▼
Native Action Execution ... startups/investors ......... Numu (Startup/InvestorLabelActionService::apply)
   │   fires StartupLabelOptionChanged | InvestorLabelOptionChanged
   ▼
Notifications ............. (queued listeners) .......... Numu (Dispatch{Startup|Investor}LabelNotifications — ShouldQueue)
Automations / Group move .. groups / observers .......... Numu (MoveStartupToGroupOnActionChange / InvestorWorkflowObserver, CancelSubjectMeetingsOnLabelAction)
Audit ..................... activity_logs (module=agents) Numu (AgentAuditLogger → ActivityLogger)
```

**The 8 agent tables:** `agents`, `agent_runtime_config`, `agent_runs`, `handoffs`,
`agent_action_policies`, `policy_runtime_overrides`, `execution_requests`,
`agent_approval_requests` (+ business tables `startups`, `investors`, `groups`,
`labels`, `label_options`, `activity_logs`).

**Services (`app/Services/Agents/`):** `AgentRegistryService`, `AgentRuntimeService`,
`AgentRunService`, `AgentDecisionService`, `ActionPolicyService`, `PolicyOverrideService`,
`ApprovalService`, `ApprovalExecutionService`, `ExecutionRequestService`, `AgentActionExecutor`,
`ExpectedStateValidator`, `HandoffService`, `AgentAuditLogger`. **Native exec:**
`app/Services/Actions/StartupLabelActionService` + `InvestorLabelActionService` (extend `AbstractLabelActionService`).

**Events:** `StartupLabelOptionChanged` → DispatchStartupLabelNotifications,
MoveStartupToGroupOnActionChange, CancelSubjectMeetingsOnLabelAction.
`InvestorLabelOptionChanged` → DispatchInvestorLabelNotifications, CancelSubjectMeetingsOnLabelAction
(+ investor group move via `InvestorWorkflowObserver`).

---

## 2. AI Agent lifecycle

### Creation
| Path | How |
|---|---|
| **UI** | top-right user-menu → **AI Agents** → 🚀 Startup / 👥 Investor tab → **Register Agent** (Agent Type radio). `registered_by=admin_ui`. |
| **API** | `POST /api/v1/ai/agents` `{agent_key*, agent_type?, name*, environment?, agent_version?, is_active?}` → `registered_by=make_api`. |
| **Seeder** | `AgentsSeeder` → 7 startup agents, `registered_by=seed`. |

**Table:** `agents` (+ a row in `agent_runtime_config` created enabled). **Required:** `agent_key`
(`^[a-z0-9_\-]+$`), `name`; `agent_type` defaults `startup`. **Startup vs Investor:** only
`agent_type` differs — it filters the UI tabs and scopes which action catalog the agent's actions
come from. Example: `agent_key=committee_analysis, agent_type=startup`.

### Runtime (pause/enable) — TD-003, enforced
- **Enable/Pause:** UI buttons or `POST /agents/{agentKey}/enable|pause`; global `POST /agents/pause-all|enable-all`. State in `agent_runtime_config` (versioned, 409 on stale version).
- **Effective status:** `AgentRuntimeService::effectiveStatus()` → global `*` pause wins, else the agent row, else enabled.
- **Enforcement:** `AgentDecisionService` calls `pauseBlock()` **before** acting — a paused agent OR a disabled agent (`agents.is_active=false`) → no resolution, no approval, no execution; run `execution_status=blocked`, `error_code=agent_paused|agent_disabled`, audit `agent.blocked_agent_paused|agent.blocked_agent_disabled`. Make cannot bypass it.

---

## 3. Actions lifecycle

Actions **are label options** internally (no separate "actions" table) — they reuse the existing
Labels/Options engine. An action = a `label_options` row under the `action` label of the entity type.

| | Startup | Investor |
|---|---|---|
| Label | `labels` where `key=action, type=startup` | `key=action, type=investor` |
| Options (slugs) | move_to_review, reject, request_more_info, request_meeting, move_to_initial_dd, move_to_committee, move_to_prelist, move_to_listing, … | approve, approve_w, reject, move_to_archive, create_deals, default, new |

**Create/manage (UI):** sidebar **Core → Actions** → Startups/Investors tab → table (drag-order,
inline AR/EN name edit, Notify, Enabled, Policy summary, View 👁, 🤖 Agents, Delete) + **＋ New Action**
modal (AR/EN/slug/enabled/notify). **APIs (reuse Labels):** `POST /admin/labels/{label}/options`,
`PATCH /admin/labels/options/{option}`, `POST /admin/labels/options/reorder`, `DELETE …`. **No DB
schema change** — Actions are a focused UI over `label_options`.

---

## 4. Agent ↔ Action assignment (the core)

**Source of truth = `agent_action_policies`** (one row per agent×action×environment, versioned).
The **AI Agents → Agent Details → Assigned Actions** screen is the primary authoring surface; the
**Action Agent Policies** screen (🤖 on the Actions table) and the Actions Policy column are
read/edit views over the same rows. Assigning an action to an agent **is** creating a policy row.

```
Committee Analysis Agent ──(assign)──▶ move_to_prelist ──(policy)──▶ needs_approval
                                       row: agent_action_policies(agent_key=committee_analysis,
                                            action_key=move_to_prelist, policy=needs_approval, version=N)
```

| Operation | UI | API | DB effect |
|---|---|---|---|
| Add action to agent | Agent Details → pick action + policy → Assign | `POST /admin/actions/policy` (admin) / `PUT /api/v1/ai/action-policies/{agent}/{action}` (Make) | new versioned row (supersedes prior; old gets `effective_to`) |
| Change policy | same dropdown | same | new version, old superseded |
| Remove action | set policy → "Not assigned" | `policy=none` (admin endpoint) → `effective_to=now` | active row closed |
| Many agents ↔ one action | each agent has its own policy row for the same `action_key` | — | N rows, distinct `agent_key` |
| Many actions ↔ one agent | one row per action | — | N rows, distinct `action_key` |

**History:** never hard-deleted; every change writes a new row with `version`, `effective_from`,
`effective_to`, `reason`, `created_by/updated_by`. Audit `agent.policy_set`.
**Resolution input** (§6) reads only the **active** row (`effective_to IS NULL`).

---

## 5. Agent Run lifecycle

```
Make → POST /agent-runs {agent_key, startup_id|investor_id, context?} → agent_runs(analysis=queued, execution=not_started)   ▶ agent.run_created
Make → POST /agent-runs/{run}/decision {action_slug, confidence?, reason?, payload?}
        AgentRunService.recordAnalysisResult → analysis=completed, proposed_action set (immutable after)                    ▶ agent.run_analysis
        AgentDecisionService: pause gate → policy resolve                                                                    ▶ agent.decision
        branch …
```

**Status fields (two independent tracks on `agent_runs`):**
- `analysis_status`: `queued → running → completed | failed | cancelled` (frozen once terminal).
- `execution_status`: `not_started → executing → executed | blocked | cancelled | failed`.

**Outcome mapping by branch:**
| Branch | analysis | execution | other rows |
|---|---|---|---|
| always_allow (state ok) | completed | executed | execution_requests(succeeded) |
| always_allow (state mismatch) | completed | failed (`expected_state_mismatch`) | execution_requests(failed) |
| needs_approval | completed | not_started → (on approve) executed/failed | agent_approval_requests(pending→approved) |
| blocked / default | completed | blocked | — |
| paused/disabled | completed | blocked (`agent_paused`/`agent_disabled`) | — |

**Idempotency:** `agent_runs.idempotency_key` unique → duplicate `POST /agent-runs` returns the
original (200). **Immutability:** decision fields can't change after analysis terminal (`409`).
`GET /agent-runs`, `GET /agent-runs/{run}`, `POST /agent-runs/{run}/cancel` (`agent.run_cancelled`).

---

## 6. Policy resolution lifecycle

**Service:** `ActionPolicyService::resolve(agentKey, actionKey, environment, scope)`.
**Order:** active **override** (`policy_runtime_overrides`, scoped startup/environment/test_fixture)
→ active **designed** policy (`agent_action_policies`) → **default `blocked`**.

```
resolve() returns { policy: always_allow|needs_approval|blocked, source: override|designed|default, version? }
```
**Tables queried:** `policy_runtime_overrides` then `agent_action_policies`. **Fallback:** unknown
or unassigned agent+action → `blocked` (`source=default`) — agents can only do what they're
explicitly authorized for. **Audit:** `agent.decision` (records action + resolved policy + source);
setting a policy = `agent.policy_set`; overrides = `agent.override_created` / `agent.override_revoked`.

---

## 7. Expected-state validation lifecycle (TD-005, enforced)

**Stored:** the agent records the state it observed — on the **run** as `expected_group` /
`expected_status` (set by Make at `POST /agent-runs`), and on the **approval** as `current_group` /
`current_status` (captured when the approval is created).
**Validated by:** `ExpectedStateValidator` — compares the subject's **current** `group` (name/id)
and `status` (status_option value/name), plus optional `updated_at` staleness. Only provided
expectations are checked.
**Enforced at:** `AgentDecisionService` (always_allow, before claim/execute) **and**
`ApprovalExecutionService` (approve, before execute).

```
expected_status = prescreen, action = move_to_committee
   actual still prescreen → pass → execute
   actual = committee (a human moved it) → MISMATCH → reject:
       response: { decision: "state_mismatch", executed: false, mismatches: [{field:"status",expected:"prescreen",current:"committee"}], current_state:{…} }
       agent_runs: execution_status=failed, error_code=expected_state_mismatch
       execution_requests: status=failed
       audit: agent.expected_state_mismatch
```

---

## 8. Approval lifecycle

```
AI decision (needs_approval) → ApprovalService.createFromRun → agent_approval_requests(pending,
     confidence_score, agent_payload, policy_snapshot, current_group/status)                 ▶ agent.approval_created
Admin opens /admin/approvals/{id} → viewed_at stamped (timeline)
Admin Approve  → ApprovalExecutionService.approve → decide(approved) ▶ agent.approval_approve
     → expected-state re-validation → claim execution_requests → Native Action → results       ▶ agent.approval_executed + agent.run_execution
Admin Reject   → decide(rejected) ▶ agent.approval_reject → run execution_status=blocked; nothing executes
Admin Cancel   → decide(cancelled) ▶ agent.approval_cancel
Scheduler      → approvals:expire → pending past expires_at → expired
```
**States:** `pending → approved | rejected | cancelled | expired`. **One-shot** (a settled approval
re-decided → `409`). **APIs:** `GET /api/v1/ai/approvals`, `GET …/{id}`, `POST …/{id}/approve`,
`POST …/{id}/reject`, `POST …/{id}/decide`; admin web `POST /admin/approvals/{id}/decide`.
**On approve Numu executes** (not Make) — idempotent by `approval-{id}:{slug}`.

---

## 9. Native action execution lifecycle

**Single execution path** (the same one the dashboard/REST/Claude use):
`AgentActionExecutor::executeForRun()` resolves the action **option** from its slug and calls:
- **Startup:** `StartupLabelActionService::apply($startup, $optionId, $opts, $request, source='agent')`.
- **Investor:** `InvestorLabelActionService::apply($investor, …)`.

`apply()` sets `action_option_id` (txn + audit) then dispatches the event **after commit**:
- `StartupLabelOptionChanged` → **DispatchStartupLabelNotifications** (queued, per option `methods`)
  + **MoveStartupToGroupOnActionChange** (linked-group move) + **CancelSubjectMeetingsOnLabelAction**.
- `InvestorLabelOptionChanged` → **DispatchInvestorLabelNotifications** (queued) +
  **CancelSubjectMeetingsOnLabelAction** + investor group move via `InvestorWorkflowObserver`.

Notifications fire on the channels armed for that option (email/sms/whatsapp/push/internal); a
no-method option fires no send. Examples: `move_to_committee`, `request_meeting`, `reject`,
`move_to_listing` (startup); `approve`, `create_deals`, `move_to_archive` (investor).

---

## 10. Handoff lifecycle (TD-002, startup + investor)

**Storage:** `handoffs` (subject = exactly one of `startup_id|investor_id`; `sequence_number`
monotonic **per subject**; immutable — corrections create a new row + `superseded_by`).
**Owner:** Numu authoritative; Make authors the content (`schema_version`, payload, from/to agent).
**Service:** `HandoffService::create / supersede / latest('startup_id'|'investor_id', id, …)`.
**APIs:** `POST /handoffs`, `GET /handoffs/latest`, `GET /handoffs`, `GET /handoffs/{handoff}`,
`POST /handoffs/{handoff}/supersede` (each takes one subject). **Audit:** `agent.handoff_created`,
`agent.handoff_superseded`.

---

## 11. Audit lifecycle

All agent audit flows through `AgentAuditLogger` → `ActivityLogger` → **`activity_logs`** table
(`module='agents'`, `source='api'`, actor = token/admin user, `subject_type`+`subject_id` = the
agent record, `action` = the event name, `description` = context).

| Stage | Audit event(s) |
|---|---|
| Agent assignment (policy set) | `agent.policy_set` (+ `agent.override_created` / `agent.override_revoked`) |
| Run created | `agent.run_created` |
| Decision recorded | `agent.run_analysis` |
| Policy resolved | `agent.decision` |
| Execution claimed | `agent.execution_claimed` |
| Execution result | `agent.execution_result`, `agent.run_execution` |
| Approval created | `agent.approval_created` |
| Approval decided | `agent.approval_approve` / `agent.approval_reject` / `agent.approval_cancel` |
| Approval executed | `agent.approval_executed` |
| Native action | the existing label `activity_logs` rows (action_changed, notifications) written by `apply()` |
| Pause/disable block | `agent.blocked_agent_paused` / `agent.blocked_agent_disabled` |
| Expected-state mismatch | `agent.expected_state_mismatch` |
| Handoff | `agent.handoff_created` / `agent.handoff_superseded` |
| Run cancelled | `agent.run_cancelled` |

Example row: `{ action:"agent.expected_state_mismatch", module:"agents", subject_type:"App\\Models\\AgentRun", subject_id:42, description:"mismatch=[{...}]", source:"api" }`.

---

## 12. Postman testing lifecycle

> Auth `Authorization: Bearer <numu:full token>`, `Accept: application/json`. Easiest path: the
> one-call **/testing/** endpoints (gated by `AI_AGENT_TESTING`, on in non-prod, auto-revert). Or the
> raw flow: `PUT policy` → `POST /agent-runs` → `POST /agent-runs/{run}/decision` → (approve).

| # | Scenario | One-call request | Expected |
|---|---|---|---|
| 1 | **always_allow** | `POST /testing/startup-scenario {startup_id,agent_key:"intake_triage",action_slug:"move_to_review",policy:"always_allow",revert:true}` | `decision=executed, executed:true`; run executed; exec-request succeeded; **no approval** |
| 2 | **needs_approval** | `POST /testing/approval-scenario {…action_slug:"request_more_info",auto_approve:false}` | `policy=needs_approval, executed:false, approval_id`; approval PENDING; startup unchanged |
| 3 | **blocked** | `POST /testing/blocked-scenario {…action_slug:"reject"}` | `policy=blocked, executed:false`; run blocked; no approval |
| 4 | **expected_state_mismatch** | raw: `POST /agent-runs {…,"expected_status":"prescreen"}` after the startup moved, then `POST /agent-runs/{run}/decision {action_slug:"move_to_committee"}` with policy always_allow | `decision=state_mismatch, executed:false, mismatches:[…]`; run failed `expected_state_mismatch`; audit |
| 5 | **paused_agent** | `POST /agents/{agentKey}/pause`, then `POST /agent-runs` + `/decision` | `decision=agent_paused, executed:false`; run blocked; audit `agent.blocked_agent_paused` |

Each returns the full trace (run, decision, approval, before/after, reverted). Folders **8/9** in the
Postman collection cover scenarios 1–7 + investor.

---

## 13. Make integration lifecycle (orchestration-only)

```
External trigger → Make → AI Agent (recommendation)
   → POST /api/v1/ai/agent-runs        { agent_key, startup_id|investor_id, context, expected_group?, expected_status? }
   → POST /api/v1/ai/agent-runs/{run}/decision { action_slug, confidence, reason, payload }
   → Numu: pause gate → policy → (expected-state) → execute | create approval | block
   → for needs_approval: human approves in Numu → Numu executes
```

| Make **calls** | Make **never calls** (Numu owns) |
|---|---|
| `POST /agent-runs`, `POST …/decision` | the Native Action / `apply()` |
| `POST /handoffs`, `GET /handoffs/latest` | policy resolution / `agent_action_policies` writes from logic |
| `GET /approvals` (optional poll) | approval execution (Numu executes on approve) |
| `GET /agents/{key}/runtime-status` (optional pre-check) | expected-state validation, pause enforcement |
| `POST /agents` (register, optional) | duplicate-prevention internals, audit |

**Make owns:** the trigger, the LLM recommendation, external comms, and sending the right
`agent_key`/subject/`action_slug` + optional `expected_*`. **Numu owns:** pause enforcement, policy,
expected-state validation, approvals, execution, notifications/automations, audit. Make duplicates
**none** of this.

---

## Deliverables index
- **Diagrams + sequences:** §1, §5, §7, §8, §13 above (+ `TD_001_005_VERIFICATION_PACKAGE.md` runtime matrix).
- **Tables / services / APIs / audit events:** §1, §6, §11 + `AI_AGENTS_API_CONTRACT.md` (conventions, schemas, status codes, enums).
- **Postman + examples (startup & investor):** `docs/postman/numu-ai-agents.postman_collection.json`; investor end-to-end in `AI_AGENTS_INVESTOR_PARITY.md`.
- **Verification status (not deployed):** `TD_001_005_VERIFICATION_PACKAGE.md`.
