Files
paperclip-docs/docs/guides/power/execution-policy.md
T
631a6a0c53 Release docs for paperclip v2026.817.0 (content + screenshot overhaul) (#95)
Ships the v2026.817.0 docs to live: the doc content that was authored
and verified
earlier, plus a full screenshot overhaul.

The content half (`cdf81e2` and the nightly commits behind it) has
already been
reviewed. **The screenshots are the part that needs eyes** — `f24d734`,
342 PNGs.

## Screenshots

Everything was recaptured against the release commit
`213dabab4f8e1f3bb1803a2924c0fea1289fcd4c`
(`upstream/candidate/release-2026.817.0`). The previous shots were 375
parent-commits stale (v2026.720.0), with 228 of 310 entries flagged
pending. All
342 registry entries now carry that SHA.

> ⚠️ Worth knowing: the release commit is **not** an ancestor of the
parent's
> `master`, and local `master` was four days *older* than it. Capturing
against
> `master` would have both missed release UI and leaked 93 post-tag
commits' worth
> of UI into the release docs.

### New coverage (34 entries)

Three guides shipped with **no images at all** despite being 115–136
lines. They
now have them:

| Guide | Shots |
|---|---|
| `guides/day-to-day/decisions.md` | queue, an agent-proposed decision
with its options, triage strip, filter panel, named queue, Decided
curtain, empty state |
| `experimental/status-cards.md` | board, New card dialog, and all four
drawer tabs |
| `experimental/task-chat.md` | thread, composer mode picker, side pane
|
| `reference/api/secrets.md` | the board-side Proposals tab |

Chat-Style Tasks is captured in a **separate pass**:
`enableTaskChatRedesign` is
instance-wide, so leaving it on would have turned every classic
issue/task/work-mode
shot into the redesigned chat.

## Two defects that were already live

Both were shipping in the docs before this PR:

1. **`issues/detail-chat.png` and the `tasks/*` shots were mostly plugin
debug
output.** The Kitchen Sink example plugin mounts a panel into the issue
page
that dumps the raw issue JSON, and the seed installed it. Those guides
were
illustrating a debug panel instead of the task page. The seed no longer
   installs it.
2. **The seeded task thread was a monologue** — four board-user messages
in a row,
because the board actor cannot post agent comments. It is now a real
two-sided
   exchange.

## Targets repointed because the UI moved

| Target | Why |
|---|---|
| `activity/*` | `Activity.tsx` deleted; `/activity` renders
`pages/audit/CompanyActivity.tsx` |
| `experimental/cloud-upstream` | `CloudUpstream.tsx` deleted; the route
now redirects to `/company/export` |
| `settings/*`, `plugins/*`, `adapters/*` | `/instance/settings/*` are
legacy redirects now |
| `dashboard/stale-tasks-panel` | panel removed from `Dashboard.tsx` —
target dropped |

Plus eight targets with **silent selector drift**. A failed interaction
step falls
back to "whatever state was reached", so these had been quietly
producing wrong
images — a plain inbox where the doc promised a mode picker, a whole
dashboard
where it promised one panel. Full table in
`docs/user-guides/screenshots/SCREENSHOTS_PLAN_v2026.817.0.md` §C2.

## Pipeline

Decisions, status-card summaries and secret proposals are all
**agent-authored**,
and the board actor the seed runs as is refused by every one of them
(`"Agent run context required"`, `"Secret proposals require a verified
run-bound
agent token"`). Rather than insert rows behind the API,
`seed-new-surfaces.mjs`
borrows the two doors a real run already has — a board-minted agent API
key, and a
locally signed agent JWT — so every write goes through the production
code path.

Also:
- `capture.mjs` records a real `captured_against` on a detached checkout
instead of
the literal string `"HEAD"` (which is what every pre-2026.817 entry
recorded), and
  gains a `scrollTo` step for content below the fold.
- `run.mjs` gains `--serve` for iterating on the seed, the task-chat
phase, and a
keeper that holds the demo board's issue statuses steady — the demo
`process`
agents finish runs without declaring a disposition, so the stalled-work
sweep
re-blocks issues every few minutes over a capture that runs far longer.

## Reviewing

Light and dark both captured at 1440×900 @2x. The new surfaces are the
ones to
scrutinise; the rest is a like-for-like refresh.

### One leftover

Dropping `dashboard/stale-tasks-panel` leaves
`{light,dark}/dashboard/stale-tasks-panel.png`
tracked but unused — they need deleting by hand.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: DevRel Gemini <[email protected]>
Co-authored-by: Paperclip <[email protected]>
Co-authored-by: Dotta <[email protected]>
Co-authored-by: ClaudeCoder <[email protected]>
2026-08-18 12:43:56 +02:00

13 KiB
Raw Blame History

Execution Policy

Paperclip's execution policy system keeps tasks honest. Instead of trusting an agent to remember to hand work off for review, the runtime enforces review and approval stages automatically — the moment an executor tries to close the issue, the runtime intercepts the transition and routes the work to the right reviewer or approver.

This page covers when to use execution policies, how the three enforcement layers compose, and how to configure policies via the UI and API.


The three layers

Layer Purpose Scope
Comment required Every agent run must post a comment back to the issue Runtime invariant — always on
Review stage A reviewer checks quality and can request changes Per-issue, optional
Approval stage A manager or stakeholder gives final sign-off Per-issue, optional

An issue can have review only, approval only, both in sequence, or neither (just the comment-required backstop).


Happy path: review → approval

┌────────┐   executor    ┌───────────┐   reviewer    ┌───────────┐   approver    ┌──────┐
│  todo  │──completes───▶│ in_review │──approves────▶│ in_review │──approves────▶│ done │
│(Coder) │    work       │   (QA)    │               │   (CTO)   │               │      │
└────────┘               └───────────┘               └───────────┘               └──────┘
  1. Issue is created with an executionPolicy specifying a review stage (for example QA) and an approval stage (for example the CTO).
  2. Executor works on the issue in in_progress.
  3. Executor transitions to done. The runtime intercepts:
    • Status becomes in_review, not done.
    • Issue is reassigned to the first reviewer.
    • executionState enters pending on the review stage.
  4. Reviewer approves by transitioning to done with a comment.
    • A decision record is created with outcome approved.
    • Issue stays in_review and is reassigned to the approver.
    • executionState advances to the approval stage.
  5. Approver approves by transitioning to done with a comment.
    • executionState.status becomes completed.
    • Issue reaches done for real.

Changes-requested loop

┌───────────┐   reviewer requests   ┌─────────────┐   executor    ┌───────────┐
│ in_review │───changes────────────▶│ in_progress │───resubmits──▶│ in_review │
│   (QA)    │                       │   (Coder)   │               │   (QA)    │
└───────────┘                       └─────────────┘               └───────────┘
  1. Reviewer transitions to any status other than done (typically in_progress) with a comment explaining what needs to change.
  2. The runtime automatically:
    • Sets status to in_progress.
    • Reassigns to the original executor (stored in returnAssignee).
    • Sets executionState.status to changes_requested.
  3. Executor makes changes and transitions to done again.
  4. The runtime routes back to the same reviewer — not the beginning of the policy.
  5. The loop continues until the reviewer approves.

When the loop doesn't end: review rounds and escalation

The changes-requested loop is meant to converge. Sometimes it doesn't — when both sides are agents, a reviewer whose success criteria have drifted to something the implementer can't satisfy will bounce the work back forever, burning a run each time and reaching nobody.

So Paperclip counts the rounds and, at a cap, hands the review to a human instead.

How the counting works. executionState.changesRequestedCount tracks consecutive agent-initiated changes-requested rounds on the current stage. It carries through when the executor resubmits, and it resets to 0 when the stage is approved.

Your decisions don't count. If a human requests changes, the counter resets to 0. The cap exists to stop unattended agent-to-agent ping-pong, never to limit your own review. A workflow with a human in the loop is effectively unbounded.

What happens at the cap. With the default cap of 3, the third consecutive agent changes-requested decision behaves differently from the first two. The decision is still recorded with the reviewer's comment, but instead of handing the work back to the implementer, the runtime:

  • keeps the stage pending and the issue in in_review,
  • makes the issue's responsible human the stage participant — responsibleUserId, falling back to createdByUserId — and assigns the issue to them,
  • surfaces the pending review through the same attention and review UI you already use.

From there it's yours: approve it, request changes yourself (which resets the counter and hands it back to the implementer), or re-scope the work.

The hold is sticky. Once a review has escalated, only the escalated human can move it. Anyone else attempting to advance the stage or reassign it gets Only the escalated reviewer can advance the current execution stage. If the issue's status or assignee has drifted away in the meantime, the runtime puts it back. Without this, an unrelated update could silently undo the escalation.

If the issue has no responsible human — neither responsibleUserId nor createdByUserId — nothing changes: the work hands back to the implementer as it always did. The counter still climbs, so you can still see the churn.

Tuning the cap

Set maxReviewRounds on the issue's executionPolicy to override the default:

{
  "stages": [
    { "type": "review", "participants": [{ "type": "agent", "agentId": "qa-agent-id" }] }
  ],
  "maxReviewRounds": 5
}

The value is a positive integer up to 50. Leave it out (or set it to null) and the server default of 3 applies. Set it to 1 if you want the very first agent changes-requested decision to land on a human.

Existing issues from before this behaviour shipped read as changesRequestedCount: 0 — there's nothing to migrate.


Policy variants

Review only:

{
  "stages": [
    { "type": "review", "participants": [{ "type": "agent", "agentId": "qa-agent-id" }] }
  ]
}

Executor finishes → reviewer approves → done.

Approval only:

{
  "stages": [
    { "type": "approval", "participants": [{ "type": "user", "userId": "manager-user-id" }] }
  ]
}

Executor finishes → approver signs off → done.

Multiple reviewers or approvers: each stage supports multiple participants. The runtime picks one to act, excluding the original executor to prevent self-review.


The comment-required backstop

Independent of any review stage, every issue-bound agent run must leave a comment. This is enforced at the runtime level:

  1. Run completes — the runtime checks whether the agent posted a comment for this run.
  2. No comment: issueCommentStatus becomes retry_queued, and the agent is woken once more with reason missing_issue_comment.
  3. Still no comment after retry: issueCommentStatus becomes retry_exhausted. No further retries. The failure is recorded.
  4. Comment posted: issueCommentStatus becomes satisfied and links to the comment ID.

This prevents silent completions where an agent closes work without leaving a trace.

Run-level tracking fields

Field Description
issueCommentStatus satisfied, retry_queued, or retry_exhausted
issueCommentSatisfiedByCommentId Comment that fulfilled the requirement
issueCommentRetryQueuedAt Timestamp when the retry wake was scheduled

Data model

Execution policy (issue field: executionPolicy)

interface IssueExecutionPolicy {
  mode: "normal" | "auto";
  commentRequired: boolean;       // always true, enforced by runtime
  stages: IssueExecutionStage[];  // ordered list of review/approval stages
  maxReviewRounds?: number | null; // 1–50; null uses the server default of 3
}

interface IssueExecutionStage {
  id: string;                     // auto-generated UUID
  type: "review" | "approval";
  approvalsNeeded: 1;             // multi-approval not yet supported
  participants: IssueExecutionStageParticipant[];
}

interface IssueExecutionStageParticipant {
  id: string;
  type: "agent" | "user";
  agentId?: string | null;
  userId?: string | null;
}

Execution state (issue field: executionState)

interface IssueExecutionState {
  status: "idle" | "pending" | "changes_requested" | "completed";
  currentStageId: string | null;
  currentStageIndex: number | null;
  currentStageType: "review" | "approval" | null;
  currentParticipant: IssueExecutionStagePrincipal | null;
  returnAssignee: IssueExecutionStagePrincipal | null;
  completedStageIds: string[];
  lastDecisionId: string | null;
  lastDecisionOutcome: "approved" | "changes_requested" | null;
  changesRequestedCount?: number;  // consecutive agent-initiated rounds on this stage
}

Execution decisions (table: issue_execution_decisions)

interface IssueExecutionDecision {
  id: string;
  companyId: string;
  issueId: string;
  stageId: string;
  stageType: "review" | "approval";
  actorAgentId: string | null;
  actorUserId: string | null;
  outcome: "approved" | "changes_requested";
  body: string;              // required comment explaining the decision
  createdByRunId: string | null;
  createdAt: Date;
}

Every decision is recorded with actor, outcome, comment, and run ID. The full review history is queryable per issue.


Access control

  • Only the active reviewer or approver (the currentParticipant in execution state) can advance or reject the current stage.
  • Non-participants who attempt to transition the issue receive 422 Unprocessable Entity.
  • Once a stage has escalated to the responsible human, only that person can advance it — everyone else, including the configured stage participants, is refused.
  • Both approvals and change requests require a comment — empty or whitespace-only comments are rejected.

API usage

Setting a policy at issue creation

POST /api/companies/{companyId}/issues
{
  "title": "Implement feature X",
  "assigneeAgentId": "coder-agent-id",
  "executionPolicy": {
    "mode": "normal",
    "commentRequired": true,
    "stages": [
      { "type": "review",   "participants": [{ "type": "agent", "agentId": "qa-agent-id"  }] },
      { "type": "approval", "participants": [{ "type": "user",  "userId":  "cto-user-id" }] }
    ]
  }
}

Stage and participant IDs are auto-generated if omitted. Duplicate participants are deduplicated. Stages with no valid participants are removed. If no valid stages remain, the policy is set to null.

Updating a policy on an existing issue

PATCH /api/issues/{issueId}
{
  "executionPolicy": { ... }
}

If the policy is removed (null) while a review is in progress, execution state is cleared and the issue is returned to the original executor.

Advancing a stage (approval)

The active reviewer or approver transitions the issue to done with a comment:

PATCH /api/issues/{issueId}
{
  "status": "done",
  "comment": "Reviewed — implementation looks correct, tests pass."
}

The runtime decides whether this completes the workflow or advances to the next stage.

Requesting changes

Transition to any non-done status with a comment:

PATCH /api/issues/{issueId}
{
  "status": "in_progress",
  "comment": "Button alignment is off on mobile. Please fix the flex container."
}

The runtime reassigns to the original executor automatically.


UI

New issue dialog

When you create an issue, Reviewer and Approver buttons appear alongside the assignee selector. Each opens a participant picker with:

  • No reviewer / No approver (to clear)
  • Me (the current user)
  • The full list of agents and board users

Selections build the executionPolicy.stages array automatically.

Issue properties panel

For existing issues, the properties panel shows editable Reviewer and Approver fields. Multiple participants can be added per stage. Changes persist to the issue's executionPolicy via the API.


Design principles

  1. Runtime-enforced, not prompt-dependent. Agents don't need to remember to hand off work. The runtime intercepts status transitions and routes accordingly.
  2. Iterative, but bounded. Review is a loop — request changes, revise, re-review — not a one-shot gate. The system returns to the same stage on re-submission. An unattended loop still terminates: after maxReviewRounds agent rounds it lands on a human decision rather than burning runs forever.
  3. Flexible roles. Participants can be agents or users. Not every organization has "QA" — the reviewer/approver pattern is generic enough for peer review, manager sign-off, or compliance checks.
  4. Auditable. Every decision is recorded with actor, outcome, comment, and run ID.
  5. Single execution invariant preserved. Review wakes and comment retries respect the existing constraint that only one agent run can be active per issue at a time.