mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
## Thinking Path > - Paperclip manages AI agents and their task execution. > - Agents need a durable way to return to work after a delayed check. > - The issue monitor scheduler already provides a one-shot wake for an assignee. > - Native runners reject generic execution-policy writes and had no bound monitor tool. > - Scheduling alone is insufficient because native completion also needs to accept a timed wait. > - This pull request adds an authorized monitor tool and connects it to completion and the existing scheduler. > - An agent can now schedule its next check, end the run, and resume on the same task. ## Linked Issues or Issue Description **What happened?** A native runner could not set its own task monitor. `call_api` correctly rejected execution-policy writes, while `schedule_wake` had no production binding. `paperclip_finish` also rejected monitor waits. **Expected behavior** A standard native run can set a one-shot monitor on its current task or another accessible task assigned to the same agent. After a confirmed schedule on the current task, it can yield. The scheduler later delivers `issue_monitor_due`. **Steps to reproduce** 1. Start a standard native task. 2. Ask the agent to check the task again later and end its current run. 3. Inspect available tools and try the generic issue execution-policy update. 4. Observe the missing native tool and the lifecycle-write denial. Related PRs: #14680 concerns monitor notes in the shared wake prompt. #11919 changes attempt-limit scope. This PR adds native scheduling and completion authority and retains the existing cumulative attempt bounds. It does not depend on either PR. ## What Changed - Add provider-neutral `set_task_monitor` with a default current-task target, future timestamp, required notes, existing bounds, and explicit clearing. - Check company, task visibility, ownership, runtime permissions, work mode, and active-run authority. Preserve review-only restrictions. Reject the reserved server-owned quota-recovery name before saving or accepting a native wait. - Commit the monitor, audit event, and retry receipt together. Retry receipts survive a successor run without re-arming cleared or consumed timers. - Permit `paperclip_finish` to yield to a persisted monitor. Recheck ownership and the schedule when committing final disposition. Release execution without an immediate continuation. - Preserve due monitors during native execution. Fence wake admission and consumption against replacement, clearing, reassignment, and completion. Preserve unrelated review policy. - Expose scheduled and consumed monitor instructions in task context. Update provider schemas, Rust validation, generated contracts, and execution documentation. - Add an opt-in live Codex smoke script with isolated data and explicit run/session/runner/process evidence. ## Verification - Repository `pnpm -r typecheck` and `pnpm build` passed after rebase. Server typecheck passed again after review fixes. All CI test shards pass on `3def77b1b`, including runner TypeScript/Rust, server, serialized server, workspace, and browser tests. All CI gates are green, including the canary dry run. Greptile is 5/5 on the same commit with zero unresolved threads. - The local monolithic `pnpm test:run`, started before the rebase, was interrupted after current-head CI test coverage passed. It is not counted as a standalone full-suite pass; the focused local regression suites passed. - Targeted server tests cover scheduling, replacement, clearing, policy preservation, cumulative bounds, cross-run retries, permissions, provider-neutral discovery, review restrictions, completion authority, and scheduler/finalizer races. - Runner contract/catalog/semantic tests and Rust terminal-tool tests cover the new operation and monitor completion. - Live Codex test passed twice (latest live run on `e0bcd63e6`) in a temporary database and workspace, with a 300,000 ms warm window. First run `67bd7709-c089-4d4a-9d2b-0d6b618a34b0` yielded at `2026-10-07T13:15:03.274Z`. Second run `97c323f9-595a-4cc5-a007-db5a2fbb937c` started at `13:15:30.952Z`, received `issue_monitor_due`, and completed the same task. Exactly one monitor wake was recorded. - Both live runs used native session `7b1dd753-1c9b-4e7a-b22f-a125dbc3748c`, runner `e90d9a1b-3502-4ee3-b15e-edc024c555d4`, provider session `01a11680-6d01-70c0-9a55-db7246ed66c3`, and PID `64218` with the same process start time. This proves warm reuse for that local Codex test, not only successful scheduling. - Reproduce the paid live test with `node --import ./server/node_modules/tsx/dist/loader.mjs server/scripts/smoke-native-task-monitor.ts --run`, with the installed Codex binary on `PATH` and a valid local login. ## Risks - The scheduler now defers monitor dispatch while the task has an active native run. A stuck run still depends on the existing recovery lifecycle. - Idempotency uses the existing run ledger; no table or migration is added. - Other providers share the tested tool and completion contracts. Only Codex received a live model test. - Existing `call_api` lifecycle restrictions remain enforced. Monitor waits do not bypass task blockers, reviews, or approvals. ## Model Used OpenAI Codex, GPT-6 family, with tool use, code execution, and TypeScript/Rust editing. The session does not expose the exact deployed model ID or context-window size. ## 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>
719 lines
26 KiB
TypeScript
719 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("waits on persisted monitors without an immediate continuation, even with unfinished work", () => {
|
|
const pending = assessment({ reportedDisposition: "yielded", hasBlockingRemainingWork: true,
|
|
continuation: { kind: "monitor", summary: "Check CI", idempotencyKey: "ci" } });
|
|
for (const priorIssueStatus of ["in_progress", "in_review"] as const) {
|
|
expect(arbitrate({ assessment: pending, priorIssueStatus, monitorWaitAuthorized: true }))
|
|
.toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus,
|
|
reasonCode: "scheduled_monitor_waiting", effects: [{ kind: "release_checkout" }] });
|
|
}
|
|
expect(arbitrate({ assessment: pending })).toMatchObject({ reasonCode: "monitor_wait_authority_lost",
|
|
effects: [{ kind: "record_finalization_error" }] });
|
|
expect(arbitrate({ assessment: pending, monitorWaitAuthorized: true, hasUnresolvedIssueBlockers: true }).toStatus).toBe("blocked");
|
|
expect(arbitrate({ assessment: pending, monitorWaitAuthorized: true, governanceGate: { kind: "approval", id: "pending" } }).toStatus).toBe("in_review");
|
|
});
|
|
|
|
it("keeps a pending child result non-terminal without adding another continuation", () => {
|
|
expect(arbitrate({ hasPendingChildCompletion: true })).toMatchObject({
|
|
statusAction: "in_progress", toStatus: "in_progress", reasonCode: "native_child_completion_pending",
|
|
effects: [{ kind: "release_checkout" }],
|
|
});
|
|
expect(arbitrate({ hasPendingChildCompletion: false }).toStatus).toBe("done");
|
|
for (const priorIssueStatus of ["done", "cancelled"] as const) {
|
|
expect(arbitrate({ priorIssueStatus, hasPendingChildCompletion: true }).toStatus).toBe(priorIssueStatus);
|
|
}
|
|
expect(arbitrate({ hasPendingChildCompletion: true, hasUnresolvedIssueBlockers: true }).toStatus).toBe("blocked");
|
|
expect(arbitrate({ hasPendingChildCompletion: true, governanceGate: { kind: "approval", id: "approval" } }).toStatus)
|
|
.toBe("in_review");
|
|
expect(arbitrate({ hasPendingChildCompletion: true, workspaceFinalizeStatus: "failed" }).statusAction).toBe("preserve");
|
|
});
|
|
|
|
it("schedules a capacity retry while preserving partial work and review authority", () => {
|
|
for (const nativeReviewOutcome of [undefined, "pending"] as const) {
|
|
const decision = arbitrate({ terminalState: "failed", providerOverloaded: true, nativeReviewOutcome });
|
|
expect(decision).toMatchObject({ statusAction: "preserve", reasonCode: "native_provider_overloaded" });
|
|
expect(decision.effects).toContainEqual(expect.objectContaining({ kind: "schedule_retry", cause: "native_provider_overloaded" }));
|
|
}
|
|
expect(arbitrate({ terminalState: "failed", providerOverloaded: true, failureRetryCount: 2 }))
|
|
.toMatchObject({ statusAction: "blocked", reasonCode: "native_provider_overloaded_exhausted", effects: [{ kind: "bind_blocker", owner: "board", action: expect.stringContaining("Automatic retries exhausted") }] });
|
|
});
|
|
it.each([
|
|
{ hasActivePauseHold: true }, { hasUnresolvedIssueBlockers: true },
|
|
{ governanceGate: { kind: "approval" as const, id: "approval" } },
|
|
{ priorIssueStatus: "blocked" as const }, { priorIssueStatus: "done" as const },
|
|
{ workspaceFinalizeStatus: "failed" as const }, { nativeReviewOutcome: "stale" as const },
|
|
{ nativeReviewOutcome: "resolved" as const },
|
|
])("does not schedule capacity retries through existing authority or cleanup gates (%j)", (gate) => {
|
|
const decision = arbitrate({ terminalState: "failed", providerOverloaded: true, ...gate });
|
|
expect(decision.effects.some(effect => effect.kind === "schedule_retry")).toBe(false);
|
|
});
|
|
it.each(["in_progress", "in_review"] as const)("blocks a current worker's proven model rejection without a retry (%s)", (priorIssueStatus) => {
|
|
const decision = arbitrate({ priorIssueStatus, terminalState: "failed", providerModelRejected: true });
|
|
expect(decision).toMatchObject({ statusAction: "blocked", toStatus: "blocked", reasonCode: "native_provider_model_rejected", unblockDescriptor: { owner: "board" } });
|
|
expect(decision.effects).toEqual([{ kind: "bind_blocker", owner: "board", action: expect.any(String) }]);
|
|
expect(arbitrate({ terminalState: "failed", providerModelRejected: false }).effects).toContainEqual(expect.objectContaining({ kind: "schedule_retry" }));
|
|
expect(arbitrate({ terminalState: "succeeded", providerModelRejected: true }).reasonCode).not.toBe("native_provider_model_rejected");
|
|
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, workspaceFinalizeStatus: "failed" }).reasonCode).toBe("finalization_failed_claim_preserved");
|
|
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, priorIssueStatus: "done" }).toStatus).toBe("done");
|
|
for (const nativeReviewOutcome of ["stale", "resolved"] as const) {
|
|
expect(arbitrate({ terminalState: "failed", providerModelRejected: true, priorIssueStatus, nativeReviewOutcome }))
|
|
.toMatchObject({ statusAction: "preserve", toStatus: priorIssueStatus, reasonCode: "native_review_action_finished" });
|
|
}
|
|
});
|
|
it("preserves pending review authority without automatic recovery after model rejection", () => {
|
|
expect(arbitrate({ priorIssueStatus: "in_review", terminalState: "failed", providerModelRejected: true, nativeReviewOutcome: "pending" }))
|
|
.toMatchObject({ statusAction: "preserve", toStatus: "in_review", reasonCode: "native_provider_model_rejected", unblockDescriptor: null, effects: [{ kind: "release_checkout" }] });
|
|
});
|
|
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-v11",
|
|
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" }),
|
|
]);
|
|
});
|
|
|
|
});
|