Files
PaperClipAI/server/src/services/native-runtime/status-arbiter.test.ts
T
DottaandPaperclip c9021c6721 fix: require explicit native completion reviews (#13314)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native runs report their outcome through paperclip_finish.
> - The server previously turned incomplete reports into human approval
requests.
> - Those requests could block a later successful run, even when no
person had requested review.
> - This pull request creates review cards only for explicit attention
requests and withdraws proven old fallback cards.
> - Agents receive useful completion feedback, while explicit approval
gates and task state protections remain in force.

## Linked Issues or Issue Description

Related: #13266 removed reviews caused by policy upgrades. This change
removes the separate completion fallback.

**What happened?**

An agent reported needs_review while waiting for checks without
requesting a human decision. Paperclip created a generic Native
completion review. A later successful report could not complete the task
because that old card remained pending.

**Expected behavior**

Ordinary low-risk work completes after a valid done report, a successful
run, and workspace finalization. Incomplete work stays with the agent.
Explicit approval requests remain visible and must be resolved.

**Steps to reproduce**

1. Complete a native run with needs_review and no attention requests.
2. Continue the task and submit a successful done report.
3. Observe that the old implementation leaves the task in review behind
a generic confirmation card.

## What Changed

- Require explicit attention requests to create native review cards.
Route each request independently and preserve pending or declined
decisions.
- Withdraw only pending system cards with matching old decision,
assessment, effect, contract, and prompt provenance. Preserve history
and explicit or answered requests.
- Reassess an affected current result without overwriting later task
edits, runs, contracts, or workspace failures.
- Return pending approval links and required actions through the
completion tool. Reject contradictory done reports and empty review
requests before accepting a result.
- Allow one corrective continuation for incomplete results, then expose
a recovery action.
- Update status fixtures, database regressions, runner tests, and the
completion contract documentation.

## Verification

- `pnpm -r typecheck` passed after merging current master.
- `pnpm build` passed after merging current master.
- The combined branch passed 89 completion and Agent Chat tests. Other
targeted tests passed: 170 external-chat and reconciliation tests; 50
runner-resume and control-plane tests; 13 arbiter tests; 7 chat delivery
tests; 21 runner completion and runtime-context tests.
- The full local test attempt exposed old review fixtures and a missing
fake-provider binary. The fixtures are fixed and the helper is built.
All affected suites pass in fresh reruns. The timing-sensitive Discord
test also passed on rerun.
- All latest-head CI checks passed, including build, typecheck, general
and serialized tests, runner verification, browser tests, and canary dry
run. Greptile is 5/5 with zero unresolved comments.

## Risks

- Cleanup changes existing pending cards. It requires exact system
provenance and only applies to low-risk agent-claim contracts. It does
not delete history or dismiss explicit requests.
- Status still commits after the turn and workspace finalization.
Completion feedback reports current constraints and does not claim an
early status commit.
- Incomplete reports now request a bounded corrective run instead of an
automatic approval. Repeated failures expose recovery.

## Model Used

OpenAI Codex, GPT-6, with repository inspection, code execution, and
test tools. The exact deployment identifier and context window size are
not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-12 13:00:04 -05:00

603 lines
17 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { NativeEvidenceAssessment } from "./evidence-classifier.js";
import { arbitrateNativeStatus } from "./status-arbiter.js";
function assessment(
overrides: Partial<NativeEvidenceAssessment> = {},
): NativeEvidenceAssessment {
return {
objectiveClaimSatisfied: true,
objectiveSatisfied: true,
allCriteriaSatisfied: true,
verificationPassed: true,
hasFailedVerification: false,
hasBlockingRemainingWork: false,
reportedDisposition: "done",
summary: "Complete",
contractRevisionMatches: true,
criterionAssessments: [],
verificationAssessments: [],
verificationCaveats: [],
acceptedEvidenceRefs: ["event:2"],
missingRequirements: [],
rejectedEvidence: [],
unverifiableEvidence: [],
blocker: null,
continuation: null,
attentionRequests: [],
ignoredAttentionRequests: [],
...overrides,
};
}
function arbitrate(
overrides: Partial<Parameters<typeof arbitrateNativeStatus>[0]> = {},
) {
return arbitrateNativeStatus({
assessment: assessment(),
terminalState: "succeeded",
workspaceFinalizeStatus: "succeeded",
agentId: "agent",
priorIssueStatus: "in_progress",
...overrides,
});
}
describe("native status authority", () => {
it("treats only the authorized Board response_wake as passive and preserves governance", () => {
const passive = assessment({
reportedDisposition: "yielded",
continuation: {
kind: "response_wake",
summary: "Wait for the next request",
idempotencyKey: "board-wait",
},
});
expect(
arbitrate({ assessment: passive, boardResponseWaitAuthorized: true }),
).toMatchObject({
reasonCode: "board_response_waiting",
toStatus: "in_progress",
effects: [],
});
expect(arbitrate({ assessment: passive })).toMatchObject({
reasonCode: "live_continuation_registered",
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
});
expect(
arbitrate({ assessment: passive, boardResponseWaitOrigin: true }),
).toMatchObject({
reasonCode: "board_response_wait_superseded",
statusAction: "preserve",
effects: [],
});
expect(
arbitrate({
assessment: passive,
boardResponseWaitAuthorized: true,
governanceGate: { kind: "approval", id: "approval" },
}),
).toMatchObject({
reasonCode: "governed_response_waiting",
toStatus: "in_review",
effects: [expect.objectContaining({ kind: "create_interaction" })],
});
for (const kind of ["same_agent", "retry", "monitor"] as const) {
expect(
arbitrate({
assessment: {
...passive,
continuation: { ...passive.continuation!, kind },
},
boardResponseWaitAuthorized: true,
}),
).toMatchObject({
reasonCode: "live_continuation_registered",
effects: [expect.objectContaining({ continuationKind: kind })],
});
}
});
it("marks done only from successful finalization and complete durable evidence", () => {
expect(arbitrate()).toEqual(
expect.objectContaining({
statusAction: "done",
toStatus: "done",
reasonCode: "completion_contract_satisfied",
effects: [{ kind: "release_checkout" }],
}),
);
expect(arbitrate({ completionClaimPolicyAccepted: true })).toEqual(
expect.objectContaining({
statusAction: "done",
reasonCode: "completion_claim_policy_accepted",
}),
);
expect(
arbitrate({
assessment: assessment({
verificationPassed: false,
missingRequirements: ["test"],
}),
}),
).toEqual(
expect.objectContaining({
statusAction: "in_progress",
toStatus: "in_progress",
reasonCode: "completion_evidence_incomplete",
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
}),
);
const claimOnly = assessment({
objectiveSatisfied: false,
allCriteriaSatisfied: false,
verificationPassed: false,
criterionAssessments: [
{
criterionId: "objective",
claimStatus: "satisfied",
outcome: "missing",
evidenceRefs: [],
reasonCode: "criterion_evidence_missing",
},
],
verificationAssessments: [
{
commandOrCheck: "Answered the question",
claimStatus: "passed",
outcome: "unverifiable",
evidenceRef: null,
reasonCode: "verification_has_no_durable_reference",
reportedReasonCode: null,
detail: null,
},
],
acceptedEvidenceRefs: [],
missingRequirements: ["objective"],
});
expect(arbitrate({ assessment: claimOnly })).toEqual(
expect.objectContaining({
toStatus: "in_progress",
reasonCode: "completion_evidence_incomplete",
}),
);
expect(
arbitrate({ assessment: claimOnly, completionClaimPolicyAccepted: true }),
).toEqual(
expect.objectContaining({
toStatus: "done",
reasonCode: "completion_claim_policy_accepted",
}),
);
});
it("creates explicit liveness paths for review, continuation, cancellation, and governance", () => {
expect(
arbitrate({
assessment: assessment({ reportedDisposition: "needs_review" }),
}),
).toEqual(
expect.objectContaining({
toStatus: "in_progress",
effects: [expect.objectContaining({ kind: "enqueue_continuation" })],
}),
);
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "yielded",
continuation: {
kind: "retry",
summary: "Retry the task",
idempotencyKey: "retry-task",
},
}),
}),
).toEqual(
expect.objectContaining({
toStatus: "in_progress",
effects: [
expect.objectContaining({
kind: "enqueue_continuation",
continuationKind: "retry",
}),
],
}),
);
const externalChatWait = assessment({
reportedDisposition: "yielded",
continuation: {
kind: "response_wake",
summary: "Wait for the next authorized provider message",
idempotencyKey: "external-chat-wait",
},
});
expect(
arbitrate({
assessment: externalChatWait,
externalChatResponseWaitAuthorization: "authorized",
}),
).toEqual(
expect.objectContaining({
statusAction: "in_progress",
toStatus: "in_progress",
reasonCode: "external_chat_response_waiting",
effects: [],
}),
);
expect(
arbitrate({
assessment: externalChatWait,
externalChatResponseWaitAuthorization: "revoked",
}),
).toEqual(
expect.objectContaining({
statusAction: "preserve",
toStatus: "in_progress",
reasonCode: "external_chat_response_wait_authorization_lost",
effects: [],
}),
);
expect(
arbitrate({
assessment: externalChatWait,
externalChatResponseWaitAuthorization: "not_applicable",
}),
).toEqual(
expect.objectContaining({
reasonCode: "live_continuation_registered",
effects: [
expect.objectContaining({
kind: "enqueue_continuation",
continuationKind: "response_wake",
}),
],
}),
);
expect(arbitrate({ terminalState: "cancelled" })).toEqual(
expect.objectContaining({
toStatus: "in_progress",
effects: [expect.objectContaining({ kind: "release_run_resources" })],
}),
);
expect(
arbitrate({ governanceGate: { kind: "interaction", id: "interaction" } }),
).toEqual(
expect.objectContaining({
toStatus: "in_review",
reasonCode: "governed_gate_pending",
effects: [
{
kind: "create_interaction",
gate: { kind: "interaction", id: "interaction" },
},
{
kind: "notify_owner",
agentId: "agent",
reason: "governed_gate_pending",
},
],
}),
);
expect(
arbitrate({
governanceGate: { kind: "interaction", id: "interaction" },
assessment: assessment({
reportedDisposition: "yielded",
continuation: {
kind: "response_wake",
summary: "Resume from the response",
idempotencyKey: "interaction-response:interaction",
},
}),
}),
).toEqual(
expect.objectContaining({
toStatus: "in_review",
reasonCode: "governed_response_waiting",
effects: [
{
kind: "create_interaction",
gate: { kind: "interaction", id: "interaction" },
},
],
}),
);
});
it("accepts low-risk completion claims with unrun verification caveats", () => {
const withCaveat = assessment({
verificationPassed: false,
criterionAssessments: [
{
criterionId: "objective",
claimStatus: "satisfied",
outcome: "missing",
evidenceRefs: [],
reasonCode: "criterion_evidence_missing",
},
],
verificationAssessments: [
{
commandOrCheck: "Run npm test",
claimStatus: "not_run",
outcome: "missing",
evidenceRef: null,
reasonCode: "verification_not_run",
reportedReasonCode: "tool_unavailable",
detail: "Node and npm are unavailable in this environment.",
},
],
verificationCaveats: [
{
commandOrCheck: "Run npm test",
reasonCode: "tool_unavailable",
detail: "Node and npm are unavailable in this environment.",
},
],
});
expect(
arbitrate({
assessment: withCaveat,
completionClaimPolicyAccepted: true,
}),
).toEqual(
expect.objectContaining({
toStatus: "done",
reasonCode: "completion_claim_policy_accepted",
}),
);
});
it("keeps failed verification with the agent and routes explicit attention to its owner", () => {
const failed = assessment({
verificationPassed: false,
hasFailedVerification: true,
verificationAssessments: [
{
commandOrCheck: "Run npm test",
claimStatus: "failed",
outcome: "rejected",
evidenceRef: null,
reasonCode: "verification_reported_failed",
reportedReasonCode: null,
detail: "One test failed.",
},
],
});
expect(
arbitrate({
assessment: failed,
completionClaimPolicyAccepted: true,
reviewOwnerUserId: "user-1",
}),
).toEqual(
expect.objectContaining({
toStatus: "in_progress",
reasonCode: "completion_evidence_incomplete",
effects: [
expect.objectContaining({
kind: "enqueue_continuation",
agentId: "agent",
}),
],
}),
);
const withAttention = assessment({
attentionRequests: [
{
kind: "approval",
summary: "Approve publication",
ownerClass: "human",
targetAgentId: null,
sourceIndex: 0,
sourceKind: "approval",
legacy: false,
},
],
});
expect(
arbitrate({
assessment: withAttention,
completionClaimPolicyAccepted: true,
}),
).toEqual(
expect.objectContaining({
toStatus: "in_review",
reasonCode: "actionable_attention_pending",
}),
);
});
it("blocks only for a task-wide blocker with a named owner and action", () => {
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "blocked",
blocker: {
boardOwned: true,
scope: "task_wide",
unblockAction: "Approve access",
},
}),
}),
).toEqual(
expect.objectContaining({
toStatus: "blocked",
unblockDescriptor: { owner: "board", action: "Approve access" },
effects: [
{ kind: "bind_blocker", owner: "board", action: "Approve access" },
{
kind: "notify_owner",
agentId: "agent",
reason: "task_wide_blocker_bound",
},
],
}),
);
});
it("waits for an explicit unblock instead of inventing another productive track", () => {
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "blocked",
objectiveClaimSatisfied: false,
objectiveSatisfied: false,
allCriteriaSatisfied: false,
verificationPassed: false,
hasBlockingRemainingWork: true,
blocker: {
boardOwned: false,
scope: "current_track",
unblockAction:
"Grant access to the current Board comment attachment, then explicitly retry.",
},
}),
}),
).toEqual(
expect.objectContaining({
statusAction: "blocked",
toStatus: "blocked",
policyVersion: "phase6-v5",
reasonCode: "current_track_blocker_waiting",
unblockDescriptor: {
owner: "board",
action:
"Grant access to the current Board comment attachment, then explicitly retry.",
},
effects: [
{
kind: "bind_blocker",
owner: "board",
action:
"Grant access to the current Board comment attachment, then explicitly retry.",
},
],
}),
);
});
it("blocks a current-track result when a durable dependency already gates the issue", () => {
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "blocked",
blocker: {
boardOwned: false,
scope: "current_track",
unblockAction: "Wait for child task DOT-52",
},
}),
hasUnresolvedIssueBlockers: true,
}),
).toEqual(
expect.objectContaining({
statusAction: "blocked",
toStatus: "blocked",
reasonCode: "durable_dependency_blocker_bound",
unblockDescriptor: {
owner: { agentId: "agent" },
action: "Wait for child task DOT-52",
},
effects: [],
}),
);
});
it("lets a durable dependency override an optimistic done result", () => {
expect(
arbitrate({
assessment: assessment({ reportedDisposition: "done", blocker: null }),
completionClaimPolicyAccepted: true,
hasUnresolvedIssueBlockers: true,
}),
).toEqual(
expect.objectContaining({
statusAction: "blocked",
toStatus: "blocked",
reasonCode: "durable_dependency_blocker_bound",
effects: [],
}),
);
});
it("does not create a duplicate review after this run's review was already accepted", () => {
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "needs_review",
attentionRequests: [
{
kind: "review",
summary: "Accept the plan",
ownerClass: "human",
targetAgentId: null,
sourceIndex: 0,
sourceKind: "review",
legacy: false,
},
],
}),
governanceResolvedForRun: true,
}),
).toEqual(
expect.objectContaining({
statusAction: "in_review",
reasonCode: "governance_response_continuation_queued",
effects: [],
}),
);
});
it("does not create a duplicate review after this run's planning confirmation was already accepted", () => {
expect(
arbitrate({
assessment: assessment({
reportedDisposition: "blocked",
attentionRequests: [
{
kind: "approval",
summary: "Accept the pinned plan revision.",
ownerClass: "human",
targetAgentId: null,
sourceIndex: 0,
sourceKind: "approval",
legacy: false,
},
],
}),
governanceResolvedForRun: true,
}),
).toEqual(
expect.objectContaining({
statusAction: "in_review",
reasonCode: "governance_response_continuation_queued",
effects: [],
}),
);
});
it("preserves authoritative terminal statuses", () => {
expect(arbitrate({ priorIssueStatus: "done" })).toEqual(
expect.objectContaining({
toStatus: "done",
reasonCode: "terminal_status_preserved",
effects: [],
}),
);
});
it("routes each explicit request to its own reviewer", () => {
const decision = arbitrate({ assessment: assessment({ attentionRequests: [
{ kind: "approval", summary: "Approve release", ownerClass: "human", targetAgentId: null, sourceIndex: 0, sourceKind: "approval", legacy: false },
{ kind: "review", summary: "Review code", ownerClass: "agent", targetAgentId: "review-agent", sourceIndex: 1, sourceKind: "review", legacy: false },
] }), reviewOwnerUserId: "release-owner" });
expect(decision.effects).toEqual([
expect.objectContaining({ kind: "bind_reviewer", requestKey: "attention-0", prompt: "Approve release", ownerUserId: "release-owner", ownerAgentId: null }),
expect.objectContaining({ kind: "bind_reviewer", requestKey: "attention-1", prompt: "Review code", ownerUserId: null, ownerAgentId: "review-agent" }),
]);
});
});