{
  "name": "HumanApproval",
  "summary": "An agent stops and asks before doing something consequential, and a person approves, rejects or edits it.",
  "useWhen": [
    "the action spends money, sends something to a third party, deletes data, or changes access",
    "the action is irreversible by the person who would have to undo it",
    "a regulation or an internal policy requires a human decision on the record — the EU AI Act's human-oversight duty is the common case"
  ],
  "composes": [
    "ApprovalGate",
    "AgentActivity",
    "AgentActivityStep",
    "AgentStop",
    "Alert",
    "List"
  ],
  "drivenBy": {
    "hook": "useAgentRun",
    "note": "`approvalProps` carries the gate's `status`, `isSubmitting` and `resolution`, and `onApprove`/`onReject` when `onDecision` is given. A decision that throws stays pending with `state.approval.error` set — show it, because a decision the server never received must not look made. A stop or an expiry while pending resolves the gate as `expired`, never as approved."
  },
  "structure": [
    "The gate appears in place, where the run reached it — inside the step log, not in a dialog that covers it.",
    "The AgentActivityStep that is waiting stays `active`; the ApprovalGate is its `detail`.",
    "An ApprovalGate whose `title` names the action and its object, with the specifics — amount, recipient, record count — as `children`.",
    "`risk` set from the consequence, not from the agent's confidence.",
    "After the decision, `status` becomes `approved`, `rejected` or `expired` and `resolution` records who decided and when.",
    "Where several runs are waiting, the queue is a List named by its heading: a row per waiting run, labelled with the gate's `title` — the action and its object — with the agent, the task and how long it has waited as its `description`, the gate's `risk` as a Badge in `meta`, and an `href` to the run where the gate is. The decision is made on the run, beside its evidence — never from the queue row."
  ],
  "states": {
    "loading": {
      "must": "While the decision is being submitted, set `isSubmitting` — the gate keeps its buttons in place and disables them. Do not remove the gate or advance the step log until the server confirms.",
      "why": "an optimistically-advanced run tells the user the action was approved before anything approved it, which is the one thing this pattern exists to prevent"
    },
    "empty": {
      "must": "Render no ApprovalGate when nothing is waiting. A queue view with no pending items says so in words — the List's `renderEmptyState` — and does not render an empty gate shell.",
      "why": "a gate with no action in it still shows Approve and Reject buttons, and a user who presses one has approved nothing while believing they approved something"
    },
    "error": {
      "must": "If the decision fails to submit, keep `status=\"pending\"`, clear `isSubmitting`, and show an Alert with `intent=\"error\"` inside the gate. The action must not proceed.",
      "why": "a failed approval that looks like a successful one is an unapproved action running with a human's name on it"
    },
    "expired": {
      "must": "Set `status=\"expired\"` when the window closes, state what happened by default in `resolution`, and remove the decision buttons.",
      "why": "a gate that sits pending forever hides that the agent already gave up, and buttons that no longer do anything are worse than no buttons"
    }
  },
  "propsUsed": {
    "ApprovalGate": [
      "title",
      "risk",
      "status",
      "onApprove",
      "onReject",
      "onEdit",
      "approveLabel",
      "rejectLabel",
      "isSubmitting",
      "resolution"
    ],
    "AgentActivity": [
      "announceActive"
    ],
    "AgentActivityStep": [
      "status",
      "detail"
    ],
    "AgentStop": [
      "onStop",
      "label",
      "size"
    ],
    "Alert": [
      "intent",
      "title"
    ],
    "List": [
      "items",
      "aria-labelledby",
      "renderEmptyState"
    ]
  },
  "variantsUsed": {
    "ApprovalGate": {
      "risk": [
        "low",
        "medium",
        "high"
      ],
      "status": [
        "pending",
        "approved",
        "rejected",
        "expired"
      ]
    },
    "AgentActivityStep": {
      "status": [
        "active",
        "done",
        "skipped"
      ]
    },
    "Alert": {
      "intent": [
        "error",
        "warning"
      ]
    },
    "AgentStop": {
      "size": [
        "sm"
      ]
    },
    "List": {
      "selectionMode": [
        "none"
      ]
    }
  },
  "a11y": {
    "requires": [
      "a `title` that names the action and the object — it is the gate's accessible name",
      "`risk` set from consequence, so the level is the same for the same action every time",
      "reject reachable before approve by keyboard, which the component's DOM order already guarantees"
    ],
    "notes": [
      "ApprovalGate autofocuses nothing, deliberately. A dialog that focuses Approve turns Enter on an unread screen into consent.",
      "Do not put the gate in a Modal that traps focus and hides the run behind it. The evidence for the decision is the step log, and a person cannot judge an action they can no longer see.",
      "`risk` is a label on the consequence, not a colour scheme. Using `high` for emphasis on a routine action is how a team learns to ignore it."
    ]
  },
  "antiPatterns": [
    {
      "dont": "\"The agent wants to continue. Approve?\"",
      "why": "it names neither the action nor its object, so the only available answer is a reflex — and a reflex is not oversight"
    },
    {
      "dont": "an approval that also acts as the progress indicator",
      "do": "the AgentRun pattern",
      "why": "conflating \"I am working\" with \"I need you\" teaches the user to dismiss both"
    },
    {
      "dont": "approve-all across a queue of unrelated actions — a queue List with `selectionMode=\"multiple\"` and an Approve batch action",
      "do": "a queue List whose rows open each run, where its gate is decided on its own evidence",
      "why": "one click covering many consequences is indistinguishable from no gate at all, and it is the failure regulators look for"
    },
    {
      "dont": "a countdown that approves on expiry",
      "why": "silence is not consent; an expired gate must default to the safe outcome, which is not doing the thing"
    },
    {
      "dont": "hiding the gate's earlier steps once a decision is made",
      "why": "the record of what was approved, on what evidence, is the artefact an audit asks for"
    }
  ]
}
