mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A task's Properties panel shows the assignee agent's context window
(paperclipai/paperclip#19273)
> - The board can see that the context is nearly full, but it cannot act
on it
> - A full context makes the agent compact at an unplanned time, often
in the middle of work
> - This pull request adds "Compact now" to the Context hover card
> - The benefit is that the board can compact the agent's context
between runs, at a time it chooses
## Linked Issues or Issue Description
Refs paperclipai/paperclip#19273
**Problem**
The Context row shows how full the agent's context window is. The board
cannot ask the agent to compact it.
**Proposed solution**
A "Compact now" button in the Context hover card. It runs the harness's
own compaction: Codex `thread/compact/start`, and Claude's `/compact`
command through ACP.
**Alternatives considered**
A separate PRP `session.compact` command. Both harnesses run compaction
as a turn, so a compaction turn keeps the existing durable turn binding,
interruption, and recovery. A separate command needs a second turn
lifecycle.
## What Changed
- UI: the Context hover card shows Compact now with three states:
available, Compacting…, and unavailable with a reason (agent mid-run,
harness unsupported, task closed). The Storybook story shows each state.
- Server: `POST /api/issues/:id/context/compact` (board only,
`decideIssueAccess` `issue:mutate`, activity log
`issue.context_compaction_requested`). It queues a short run marked by
`contextSnapshot.contextCompactionRequestId`. `GET
/issues/:id/context-usage` returns the `compaction` state.
- Server: a compaction run does no task work. The native status arbiter
preserves the task status. The lifecycle skips handoff, liveness
continuation, review-path disposition and the issue comment, as it does
for goal-control runs.
- Runner TS: `startTurn({ compact: true })`. The Codex harness session
sends `thread/compact/start`. The runnerd transport maps it to PRP
`turn.start` with `compact: true` (Codex) or `text: "/compact"`
(Claude/ACPX). The native runtime option `sessionCompaction` requires a
resumed session and settles with a synthesized yielded result.
- Runner Rust: `turn.start` accepts `compact: true` for Codex.
`CodexProvider::start_compaction_turn` sends `thread/compact/start` and
binds the turn from the `turn/started` that follows the empty response.
- Fix (from #19273): Codex emitted `context.compacted` on both the start
and the end of one compaction item. It now emits it once, and the count
is distinct by `compactionId`.
- Fix (from #19273): the conversation event view dropped
`context.compacted`, so a finished run never showed the divider. The
view now keeps compactions, plus context readings for runs that
compacted. A finished run's compactions show as thread dividers.
- Fix: the Codex harness stamps before/after sizes on the compaction
event. Codex reports the compacted size before the compaction item ends,
so the old neighbour-based guess showed the wrong size.
## Verification
- `pnpm -r typecheck`, `pnpm build`, `pnpm check:token-gates`, `node
scripts/storybook-catalog.mjs`: pass.
- New tests: `server/src/__tests__/context-compaction.test.ts`,
`status-arbiter.test.ts`, `issue-context-usage.test.ts` (duplicate
compaction counts once), `ui/.../context-usage-row.test.tsx`,
`native-session-runtime.test.ts` (context compaction), Codex driver and
runnerd transport tests, Rust
`codex_compaction_turn_binds_the_turn_reported_after_its_empty_response`.
Rust `codex_provider` suite: 115 passed.
- `pnpm test:run` locally: one failure in
`chat-channels.integration.test.ts`. It reads
`.claude/skills/slack/SKILL.md` from the main checkout, which is not
present in this worktree. It is not related to this change.
- Live, local worktree instance, Codex agent on the Paperclip runner:
clicked Compact now. The run succeeded. Context went from 35k to 9.4k
tokens, and later 9.4k to 9.2k. The task stayed In Review. The thread
shows "Context compacted · 9.4k → 9.2k tokens" and the meter dropped.
- Live Claude proof: pending. The local Claude agent needs an Anthropic
API key. claude-agent-acp handles `/compact` as an echo-less turn, and
the runner already maps its "Compacting completed." notice to
`context.compacted`.
## Risks
- A compaction run resumes the agent's session. If the session cannot
resume, the run fails with `native_context_compaction_unavailable` and
the task is unchanged.
- Compact now is not available while the agent runs on the task, because
compaction interrupts the active turn.
- The conversation view now returns context readings for runs that
compacted. Runs that never compacted return the same events as before.
- An old runner binary ignores `compact` and rejects the `turn.start`
because it has no text. It does not start an ordinary turn.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, through Claude Code
with tool use and code execution.
## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
725 lines
26 KiB
TypeScript
725 lines
26 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("cannot complete an explicitly required quality review from claims or file evidence", () => {
|
|
expect(arbitrate({ completionClaimPolicyAccepted: true, requiredQualityReviewSatisfied: false }))
|
|
.toMatchObject({ toStatus: "in_progress", reasonCode: "required_quality_review_incomplete" });
|
|
expect(arbitrate({ completionClaimPolicyAccepted: false, requiredQualityReviewSatisfied: false }))
|
|
.toMatchObject({ toStatus: "in_progress", reasonCode: "required_quality_review_incomplete" });
|
|
expect(arbitrate({ requiredQualityReviewSatisfied: true })).toMatchObject({ toStatus: "done" });
|
|
expect(arbitrate()).toMatchObject({ toStatus: "done" });
|
|
expect(arbitrate({ requiredQualityReviewSatisfied: false, governanceGate: { kind: "interaction", id: "quality" } }))
|
|
.toMatchObject({ toStatus: "in_review", reasonCode: "governed_gate_pending" });
|
|
});
|
|
it("waits on a published external review without creating human attention", () => {
|
|
const wait = assessment({ reportedDisposition: "yielded",
|
|
continuation: { kind: "response_wake", summary: "Wait for the PR update", idempotencyKey: "pr-wait" } });
|
|
expect(arbitrate({ assessment: wait, publishedExternalReview: true, externalChatResponseWaitAuthorization: "authorized" }))
|
|
.toMatchObject({ toStatus: "in_review", reasonCode: "external_chat_response_waiting", effects: [] });
|
|
expect(arbitrate({ assessment: wait, publishedExternalReview: true, externalChatResponseWaitAuthorization: "revoked" }))
|
|
.toMatchObject({ statusAction: "preserve", effects: [] });
|
|
expect(arbitrate({ assessment: wait, publishedExternalReview: true,
|
|
externalChatResponseWaitAuthorization: "authorized", governanceGate: { kind: "approval", id: "access" } }))
|
|
.toMatchObject({ reasonCode: "governed_response_waiting", effects: [{ kind: "create_interaction" }] });
|
|
const monitor = { ...wait, continuation: { ...wait.continuation!, kind: "monitor" as const } };
|
|
expect(arbitrate({ assessment: monitor, publishedExternalReview: true, monitorWaitAuthorized: true }))
|
|
.toMatchObject({ toStatus: "in_review", reasonCode: "scheduled_monitor_waiting", effects: [{ kind: "release_checkout" }] });
|
|
expect(arbitrate({ assessment: monitor, publishedExternalReview: true }))
|
|
.toMatchObject({ statusAction: "preserve", reasonCode: "monitor_wait_authority_lost" });
|
|
});
|
|
it("preserves an open task after an accepted case handoff even if the departing model claims done", () => {
|
|
expect(arbitrate({ caseHandoffAuthorized: true })).toMatchObject({
|
|
statusAction: "preserve", toStatus: "in_progress", reasonCode: "case_handoff_waiting_for_release",
|
|
effects: [{ kind: "release_checkout" }],
|
|
});
|
|
});
|
|
|
|
it("preserves the task status after a context compaction run", () => {
|
|
for (const priorIssueStatus of ["in_progress", "in_review", "blocked", "todo"] as const) {
|
|
expect(arbitrate({
|
|
contextCompactionRun: true,
|
|
priorIssueStatus,
|
|
assessment: assessment({ reportedDisposition: "yielded", objectiveSatisfied: false, allCriteriaSatisfied: false }),
|
|
})).toMatchObject({
|
|
statusAction: "preserve", toStatus: priorIssueStatus, reasonCode: "context_compaction_status_preserved", effects: [],
|
|
});
|
|
}
|
|
});
|
|
|
|
it("keeps workspace repair ahead of an accepted case handoff", () => {
|
|
expect(arbitrate({ caseHandoffAuthorized: true, workspaceFinalizeStatus: "failed" })).toMatchObject({
|
|
statusAction: "preserve", reasonCode: "finalization_failed_claim_preserved",
|
|
});
|
|
});
|
|
it("keeps a verified accepted Cursor plan passive without completing or replaying work", () => {
|
|
const passive = assessment({ reportedDisposition: "yielded", objectiveSatisfied: false,
|
|
allCriteriaSatisfied: false, hasBlockingRemainingWork: true,
|
|
continuation: { kind: "response_wake", summary: "Explicit continuation needed", idempotencyKey: "cursor-plan-wait:event" } });
|
|
expect(arbitrate({ assessment: passive, planWaitAuthorized: true })).toMatchObject({
|
|
toStatus: "in_progress", reasonCode: "native_plan_accepted_waiting_for_continuation", effects: [],
|
|
});
|
|
expect(arbitrate({ assessment: passive })).toMatchObject({
|
|
toStatus: "in_progress",
|
|
reasonCode: "completion_evidence_incomplete",
|
|
effects: [expect.objectContaining({
|
|
kind: "enqueue_continuation",
|
|
continuationKind: "same_agent",
|
|
idempotencyKey: "native-completion-incomplete",
|
|
})],
|
|
});
|
|
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, terminalState: "failed" }).reasonCode).not.toBe("native_plan_accepted_waiting_for_continuation");
|
|
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, priorIssueStatus: "cancelled" }).toStatus).toBe("cancelled");
|
|
expect(arbitrate({ assessment: passive, planWaitAuthorized: true, governanceGate: { kind: "interaction", id: "pending" } }).toStatus).toBe("in_review");
|
|
});
|
|
|
|
it("a reviewer finishes its decision without completing rejected or still-reviewed work", () => {
|
|
for (const priorIssueStatus of ["in_progress", "in_review"] as const) {
|
|
const decision = arbitrate({ priorIssueStatus, nativeReviewOutcome: "resolved" });
|
|
expect(decision).toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus });
|
|
expect(decision.effects).not.toContainEqual(expect.objectContaining({ kind: "enqueue_continuation" }));
|
|
}
|
|
});
|
|
|
|
it("routes an unfinished reviewer action to bounded recovery instead of retrying the worker task", () => {
|
|
const decision = arbitrate({ priorIssueStatus: "in_review", nativeReviewOutcome: "pending" });
|
|
expect(decision).toMatchObject({ statusAction: "preserve", toStatus: "in_review" });
|
|
expect(decision.effects).toContainEqual(expect.objectContaining({
|
|
kind: "record_recovery", cause: "native_review_unresolved", agentId: "agent",
|
|
}));
|
|
expect(decision.effects.some((effect) => ["enqueue_continuation", "schedule_retry"].includes(effect.kind))).toBe(false);
|
|
});
|
|
|
|
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"] as const) {
|
|
expect(
|
|
arbitrate({
|
|
assessment: {
|
|
...passive,
|
|
continuation: { ...passive.continuation!, kind },
|
|
},
|
|
boardResponseWaitAuthorized: true,
|
|
}),
|
|
).toMatchObject({
|
|
reasonCode: "live_continuation_registered",
|
|
effects: [expect.objectContaining({ continuationKind: kind })],
|
|
});
|
|
}
|
|
});
|
|
it("repairs unfinished response waits once using the server continuation budget", () => {
|
|
const unfinished = assessment({
|
|
reportedDisposition: "yielded", hasBlockingRemainingWork: true,
|
|
continuation: { kind: "response_wake", summary: "Answered status; await next message", idempotencyKey: "model-key" },
|
|
});
|
|
for (const boardResponseWaitAuthorized of [true, false]) {
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitAuthorized })).toMatchObject({
|
|
reasonCode: "completion_evidence_incomplete",
|
|
toStatus: "in_progress",
|
|
effects: [{ kind: "enqueue_continuation", continuationKind: "same_agent", idempotencyKey: "native-completion-incomplete" }],
|
|
});
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitAuthorized, allowIncompleteContinuation: false })).toMatchObject({
|
|
reasonCode: "prior_status_preserved_no_live_path",
|
|
effects: [{ kind: "record_finalization_error", cause: "completion_evidence_incomplete" }],
|
|
});
|
|
}
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitOrigin: true, boardResponseWaitAuthorized: true }))
|
|
.toMatchObject({ reasonCode: "completion_evidence_incomplete" });
|
|
expect(arbitrate({ assessment: unfinished, boardResponseWaitOrigin: true, boardResponseWaitAuthorized: false }))
|
|
.toMatchObject({ reasonCode: "board_response_wait_superseded", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, hasActivePauseHold: true }))
|
|
.toMatchObject({ reasonCode: "response_wait_pause_preserved", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, hasUnresolvedIssueBlockers: true }))
|
|
.toMatchObject({ toStatus: "blocked", reasonCode: "durable_dependency_blocker_bound", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, governanceGate: { kind: "interaction", id: "question" } }))
|
|
.toMatchObject({ reasonCode: "governed_response_waiting", effects: [{ kind: "create_interaction" }] });
|
|
expect(arbitrate({ assessment: unfinished, externalChatResponseWaitAuthorization: "authorized" }))
|
|
.toMatchObject({ reasonCode: "external_chat_response_waiting", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, externalChatResponseWaitAuthorization: "revoked" }))
|
|
.toMatchObject({ reasonCode: "external_chat_response_wait_authorization_lost", effects: [] });
|
|
expect(arbitrate({ assessment: unfinished, isConversation: true, boardResponseWaitAuthorized: true }))
|
|
.toMatchObject({ reasonCode: "board_response_waiting", effects: [] });
|
|
});
|
|
|
|
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-v13",
|
|
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" }),
|
|
]);
|
|
});
|
|
|
|
});
|