# AI Agents — Production Live-Testing Readiness (Q&A)

> Scope: **production only** (`dashboard.numuangels.net`). All commands below run
> from the project root on the production server (cPanel Terminal / SSH) using the
> server PHP binary `/opt/cpanel/ea-php82/root/usr/bin/php`.

---

## 1. Are the endpoints actually deployed, or only in the code?

**Deployed.** Confirmed on production via `route:list` — the agent block is live, including
the orchestration endpoint `agent-runs/{run}/decision` and `PATCH agents/{agent}`.

Verify the full set:
```bash
/opt/cpanel/ea-php82/root/usr/bin/php artisan route:list | grep -E "api/v1/ai/(agent|approval|action-polic|execution|handoff|testing)"
```
Expected groups (all under `/api/v1/ai`): `agent-runs*`, `agents*`, `approvals*`
(incl. `/approve`, `/reject`), `action-policies*`, `execution-requests` / `execution-results*`,
`handoffs*`, `testing/*`.

**Prerequisite — run the migrations** (the endpoints depend on columns added by them):
```bash
/opt/cpanel/ea-php82/root/usr/bin/php artisan migrate --force
/opt/cpanel/ea-php82/root/usr/bin/php artisan migrate:status | grep -Ei "agent_type|investor_subject|context_to_agent|decision_fields"
```
All four must show **Ran**. If `route:cache` is used, refresh it after deploys:
`php artisan route:clear && php artisan route:cache`.

---

## 2. Test base URL

```
https://dashboard.numuangels.net/api/v1/ai/...
```
This is the production API base. There is no separate test domain; for non-destructive
testing use the safe-record approach in §4 (testing endpoints with `revert`, or a throwaway record).

---

## 3. Authentication & final scope

- **Header:** `Authorization: Bearer <token>` · `Accept: application/json` · `Content-Type: application/json` (for writes).
- **Required scope/ability:** **`numu:full`** — the entire `/api/v1/ai` agent block is gated by `McpAuth:full`.
- **Mint a token:** Admin → **AI → Connectors** (`/admin/ai/connectors`) → create a **Full** connector
  → returns a Sanctum `naat_…` token carrying `numu:full`. (OAuth 2.1 is also supported via
  `/oauth/authorize` with scope `numu:full`.)
- Rate limit: 600 requests/min. Keep the token secret; rotate/revoke from the same Connectors screen.

---

## 4. Safe startup / investor IDs for testing

There are **no pre-defined safe IDs** on production — never run a live `always_allow` decision or an
approval **execution** against a real startup/investor without protection, because it changes business
state and fires real notifications. Use one of:

1. **Throwaway record:** create a dedicated test startup (and/or investor) in the dashboard, note its
   `id`, and run all scenarios against it.
2. **Testing endpoints with auto-revert:** `POST /api/v1/ai/testing/{startup|investor|approval|blocked}-scenario`
   with `"revert": true` restores the record after the run. These are **disabled on production by
   default**; enable temporarily by setting `AI_AGENT_TESTING=true` in `.env` (then
   `php artisan config:clear`), test, and set it back to `false`.
3. **Read-only / non-executing paths first:** exercise `POST /agent-runs` + a `needs_approval` or
   `blocked` decision (these create records but do **not** change the startup) before any `always_allow`.

Resolve current state safely with `GET /api/v1/ai/agent-runs/{run}` and
`GET /api/v1/ai/action-policies/{agentKey}/{actionKey}` (no side effects).

---

## 5. Official Handoff schema

`POST /api/v1/ai/handoffs` — **subject = exactly one of `startup_id | investor_id`.**

| Field | Required | Type / limit |
|---|---|---|
| `startup_id` \| `investor_id` | **one required** (`required_without` + `prohibits`) | integer (must exist) |
| `source_run_id` | ✅ | integer (exists `agent_runs`) |
| `from_agent` | ✅ | string ≤ 80 |
| `stage` | ✅ | string ≤ 80 |
| `schema_version` | ✅ | string ≤ 40 |
| `to_agent` | optional | string ≤ 80 |
| `current_group` / `current_status` | optional | string ≤ 120 |
| `proposed_action` | optional | string ≤ 120 |
| `resulting_group` / `resulting_status` | optional | string ≤ 120 |
| `facts` / `findings` / `risks` / `open_questions` / `documents_used` / `decision_summary` | optional | array (JSON) |
| `retention_class` | optional | string ≤ 40 (default `standard`) |

Server-assigned: `sequence_number` (monotonic **per subject**), `superseded_by` (set via
`POST /api/v1/ai/handoffs/{handoff}/supersede` — corrections create a new versioned row; rows are immutable).

**Read:** `GET /api/v1/ai/handoffs/latest?startup_id=|investor_id=&from_agent=&schema_version=` ·
`GET /api/v1/ai/handoffs?startup_id=|investor_id=` · `GET /api/v1/ai/handoffs/{handoff}`.

### Request example
```json
POST /api/v1/ai/handoffs
{
  "startup_id": 0,
  "source_run_id": 0,
  "from_agent": "intake_triage",
  "to_agent": "prescreen",
  "stage": "prescreen",
  "schema_version": "v1",
  "current_group": "New",
  "current_status": "prescreen",
  "facts": { "summary": "…" },
  "findings": [],
  "risks": []
}
```

### Success response — `201 Created`
Envelope is `{ "data": <handoff>, "meta": {…} }`. The handoff is **immutable** (it has
`created_at` only — no `updated_at`). Server-assigned fields: `id`, `sequence_number`
(monotonic per subject), `superseded_by` (null until a correction). The exact field values
can be confirmed live later against a test Startup record.
```json
{
  "data": {
    "id": 123,
    "startup_id": 0,
    "investor_id": null,
    "source_run_id": 0,
    "from_agent": "intake_triage",
    "to_agent": "prescreen",
    "stage": "prescreen",
    "schema_version": "v1",
    "sequence_number": 1,
    "current_group": "New",
    "current_status": "prescreen",
    "proposed_action": null,
    "resulting_group": null,
    "resulting_status": null,
    "facts": { "summary": "…" },
    "findings": [],
    "risks": [],
    "open_questions": [],
    "documents_used": [],
    "decision_summary": null,
    "superseded_by": null,
    "retention_class": "standard",
    "created_at": "2026-06-29T12:00:00.000000Z"
  },
  "meta": {
    "request_id": "req_…",
    "correlation_id": "…",
    "actor": { "id": 1, "email": "admin@numuangels.net" }
  }
}
```
`POST /handoffs/{handoff}/supersede` returns the **new** handoff the same way (`201`), with an
incremented `sequence_number`; the old row's `superseded_by` is set to the new `id`.

### Read responses
- `GET /handoffs/latest?startup_id=…` → `{ "data": <handoff>, "meta": {…} }`, or `404` `{ "error": { "code": "not_found", "message": "No matching non-superseded handoff." } }`.
- `GET /handoffs?startup_id=…` → `{ "data": [ <handoff>, … ], "meta": {…} }` (newest `sequence_number` first).

### Error response — `422` (validation, e.g. neither subject sent)
```json
{ "error": { "code": "validation_failed", "message": "…", "details": { "startup_id": ["…"], "investor_id": ["…"] } } }
```

---

## 6. Is there a standalone endpoint to update Agent Run status?

**No standalone status endpoint** (there is no generic `PATCH /agent-runs/{run}/status`). An Agent
Run's status changes through **four** routes only — by design, to keep the decision layer and the
execution layer separate (decision fields are immutable once analysis is terminal):

| Endpoint | Changes |
|---|---|
| `PATCH /api/v1/ai/agent-runs/{run}/analysis-result` | `analysis_status` (`queued`/`running`/`completed`/`failed`/`cancelled`) + decision fields |
| `POST /api/v1/ai/agent-runs/{run}/decision` | **Orchestrated** — drives both tracks via the pipeline (policy → pause → expected-state → execute / create approval / block) |
| `POST /api/v1/ai/agent-runs/{run}/execution-results` | `execution_status` (`executing`/`executed`/`blocked`/`cancelled`/`failed`) |
| `POST /api/v1/ai/agent-runs/{run}/cancel` | cancels the run (analysis + execution → `cancelled`) |

For the final architecture, the orchestration layer (`/decision`) is the primary path — Numu moves
the status internally. `analysis-result` and `execution-results` exist for cases where an externally
produced result is recorded; they are not the main path.

---

## Quick start (once §1–§3 are confirmed)
```bash
# 1) create a run for a throwaway test startup
POST https://dashboard.numuangels.net/api/v1/ai/agent-runs
Authorization: Bearer naat_…
{ "agent_key": "intake_triage", "startup_id": <test_startup_id> }
# → take data.id  →  RUN

# 2) submit the decision
POST https://dashboard.numuangels.net/api/v1/ai/agent-runs/RUN/decision
{ "action_slug": "move_to_review", "confidence": 0.9 }
# intake_triage → move_to_review is always_allow → executes; reject is needs_approval → creates an approval.
```
