Saltar al contenido

Docs / Reproducción de política

Policy replay

Re-decide a recorded session against a candidate maintainer.yml and get back the decisions that would change. Executes nothing, stores nothing.

Policy replay answers one question before you commit a policy change: would this maintainer.yml have decided differently on a session that already happened? You hand it a session id and a candidate policy. It re-runs the policy decisions that session recorded in its audit trail against the candidate, and returns the decisions diff.

It is a dry run. It executes nothing, re-triggers no GitHub event, and changes neither the session nor your stored policy.

What it re-decides

A session's audit trail records two kinds of decision with most of their inputs stored beside them, so replay can decide them again:

Recorded event What was decided What replay answers
policy.gate.refused The policy refused the agent a capability (for example, a tool it was not allowed to use). allowed or refused under the candidate.
escalation.fired An escalation was routed to a channel by its category. The channel under the candidate: email-immediate or digest, with the routing reason in detail.

A session.escalated row is the session's outcome rather than a decision with inputs, so it comes back as context, always unchanged.

Some limits follow from what the trail stores:

  • The original policy is not rebuilt. Only its hash was recorded, so "before" is the outcome the session actually recorded, and "after" is the candidate's answer.
  • An agent's own request for an immediate email is not replayed. When the agent asked for an escalation to be emailed straight away rather than held for the digest, replay decides it without that request, so it can come back as digest and changed: true even under a candidate identical to your current policy. This is a known gap.
  • The model is not re-run. Replay tells you which decisions flip. It cannot tell you what the agent would have done next if one had.
  • security, license and cla always route to email-immediate. No candidate changes that, so those rows never show as changed.
  • Decisions made outside the session are out of reach: CI policy decisions, triage verdicts, dispatch escalations, hand-offs and CodeRabbit enqueue decisions. If the session's trail carries any, their kinds are listed in unsupported_kinds instead of being guessed at.

Replay reads whatever the session has recorded when you call it, so replay a session that has finished to see all of its decisions. Find session ids in the dashboard's Audit log, from GET /v1/sessions, or with the MCP session list action. They look like ses_….

Where to run it

All three doors run the same check, so a candidate each of them accepts gets the same diff from each. They differ only in how long a candidate may be: 65,536 characters over REST and the dashboard, 64,000 over MCP.

Dashboard

Open the Audit log, open a session, and choose Replay with new policy. Paste the candidate maintainer.yml and choose Run replay. The panel lists every replayed decision marked Changed or Unchanged, with a count of the decisions that would change and the candidate's policy hash.

REST

POST /v1/sessions/:id/replay with a JSON body holding the candidate as text. The candidate is a whole maintainer.yml, not a fragment: without version: 1 it fails to parse. This one keeps direct_mention in escalate.always and drops hostile_tone:

# candidate.yml
version: 1
escalate:
  always: [direct_mention]
curl -X POST "https://api.developerz.ai/v1/sessions/$SESSION_ID/replay" \
  -H "Authorization: Bearer $DEVELOPERZ_PAT" \
  -H "Content-Type: application/json" \
  --data "$(jq -n --rawfile yml candidate.yml '{yml: $yml}')"

$DEVELOPERZ_PAT is a personal access token (dev_pat_…) that holds read:sessions. jq --rawfile sends the file as one JSON string. The body accepts exactly one field, yml: non-empty, at most 65,536 characters. Any other field is refused.

MCP

The session tool, action replay, with the session id and the candidate:

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "id": 1,
  "params": {
    "name": "session",
    "arguments": {
      "action": "replay",
      "session_id": "ses_…",
      "yml": "version: 1\nescalate:\n  always: [direct_mention]\n"
    }
  }
}

Over MCP, yml is capped at 64,000 characters, slightly below the REST limit.

Scope

read:sessions, on both REST and MCP. REST serves replay as a POST because it takes a body, but it is a read: it changes nothing, so it needs no write scope. See Scopes.

What it returns

{
  "session_id": "ses_…",
  "policy_hash": "…",
  "decisions": [
    {
      "audit_event_id": "…",
      "kind": "escalation.fired",
      "at": "2026-10-01T09:14:03.000Z",
      "category": "hostile_tone",
      "changed": true,
      "original": "email-immediate",
      "replayed": "digest",
      "detail": "routine"
    }
  ],
  "changed_count": 1,
  "unsupported_kinds": []
}
Field Meaning
session_id The session you replayed.
policy_hash A hash of the candidate policy, so a diff is tied to the exact input that produced it.
decisions One entry per replayed decision, in the order the session recorded them.
decisions[].kind policy.gate.refused, escalation.fired or session.escalated.
decisions[].at When the original decision was recorded.
decisions[].capability The refused capability, on policy.gate.refused rows.
decisions[].category The escalation category, on escalation.fired rows.
decisions[].original The outcome the session recorded.
decisions[].replayed The outcome under the candidate.
decisions[].changed true when the two differ.
decisions[].detail Extra context, such as the reason an escalation was routed where it was.
changed_count How many decisions would change.
unsupported_kinds Decision kinds found in the trail that replay cannot re-decide. Normally empty.

The MCP action returns the same object.

When it refuses

The candidate is validated before the session is looked up, with the same parse and semantic checks as POST /v1/repos/:owner/:repo/validate.

REST MCP Why
400, detail: "invalid .maintainer.yml: parse failed" or "… semantic validation failed", with the validator's messages in errors an error result carrying the same error sentence and errors The candidate does not parse, or parses into a policy that validation refuses. Fix the YAML.
400, detail: "invalid replay body" the call's input is refused as invalid REST: the body is missing yml, carries another field, or yml is over 65,536 characters. MCP: yml is missing or over 64,000 characters, or session_id is not a well-formed ses_… id.
404 an error result naming the session No such session on your account. Another account's session gets the same answer, so a session you cannot read is never confirmed to exist. Over REST a malformed id gets this answer too; over MCP it is refused as invalid input (the row above).

Errors are problem documents, like every other REST error.