Slice 4 hardening: bounded resolver, terminal states, reconciliation (#21)
Fixes review findings before Slice 5. Resolver: resolveOutcome + WORK_STATUS_FOR_OUTCOME + lifecycle fixes. Execution: bounded finishAttempt retry, terminal Work status preservation, slice validation, bounded reconciliation with 30s cron. Planning: revision guards, durable submitQuestion, design-approval invalidation. Primitives 69 (+4), backend 22 (+10) tests; root typecheck/build/Ultracite pass.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Schema } from "effect";
|
||||
|
||||
import type { WorkStatus } from "./work-lifecycle";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
@@ -48,9 +50,6 @@ export const CodingKitV0 = Schema.Struct({
|
||||
});
|
||||
export type CodingKitV0 = typeof CodingKitV0.Type;
|
||||
|
||||
export const isTerminalClassification = (_: AttemptClassification): true =>
|
||||
true;
|
||||
|
||||
export const shouldRetry = (
|
||||
outcome: AttemptOutcome,
|
||||
attemptNumber: number,
|
||||
@@ -60,6 +59,39 @@ export const shouldRetry = (
|
||||
attemptNumber < policy.maxAttempts &&
|
||||
policy.retryableClassifications.includes(outcome.classification);
|
||||
|
||||
// Terminal Work status the resolver settles on once it will run no further
|
||||
// attempt. RetryableFailure lands here only after the retry budget is gone.
|
||||
export const WORK_STATUS_FOR_OUTCOME: Readonly<
|
||||
Record<AttemptClassification, WorkStatus>
|
||||
> = {
|
||||
Blocked: "blocked",
|
||||
BudgetExhausted: "failed",
|
||||
Cancelled: "ready",
|
||||
NeedsInput: "needs-input",
|
||||
PermanentFailure: "failed",
|
||||
RetryableFailure: "failed",
|
||||
Succeeded: "completed",
|
||||
VerificationFailed: "blocked",
|
||||
};
|
||||
|
||||
export type Resolution =
|
||||
| { readonly kind: "retry" }
|
||||
| { readonly kind: "terminal"; readonly workStatus: WorkStatus };
|
||||
|
||||
// Resolver decision after an attempt finishes: retry within policy, or settle
|
||||
// Work at the terminal status mapped from the attempt classification.
|
||||
export const resolveOutcome = (
|
||||
outcome: AttemptOutcome,
|
||||
attemptNumber: number,
|
||||
policy: RetryPolicy
|
||||
): Resolution =>
|
||||
shouldRetry(outcome, attemptNumber, policy)
|
||||
? { kind: "retry" }
|
||||
: {
|
||||
kind: "terminal",
|
||||
workStatus: WORK_STATUS_FOR_OUTCOME[outcome.classification],
|
||||
};
|
||||
|
||||
export const defaultCodingKitV0: CodingKitV0 = {
|
||||
harness: "fake",
|
||||
id: "coding-kit-v0",
|
||||
|
||||
@@ -20,14 +20,10 @@ export const WORK_TRANSITIONS: Readonly<
|
||||
Record<WorkStatus, readonly WorkStatus[]>
|
||||
> = {
|
||||
"awaiting-definition-approval": ["designing", "defining"],
|
||||
"awaiting-design-approval": [
|
||||
"ready",
|
||||
"designing",
|
||||
"awaiting-definition-approval",
|
||||
],
|
||||
"awaiting-design-approval": ["ready", "designing"],
|
||||
blocked: ["ready", "executing", "cancelled"],
|
||||
cancelled: ["ready", "proposed"],
|
||||
completed: ["proposed", "defining"],
|
||||
cancelled: ["ready"],
|
||||
completed: [],
|
||||
defining: ["awaiting-definition-approval", "proposed"],
|
||||
designing: ["awaiting-design-approval", "awaiting-definition-approval"],
|
||||
executing: [
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Effect } from "effect";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { FakeHarnessLive } from "./harness-runtime";
|
||||
import { defaultCodingKitV0, shouldRetry } from "./resolver";
|
||||
import { defaultCodingKitV0, resolveOutcome, shouldRetry } from "./resolver";
|
||||
import { validateDefinition } from "./work-definition";
|
||||
import { validateDesignPacket } from "./work-design";
|
||||
import { canTransitionWork } from "./work-lifecycle";
|
||||
@@ -30,6 +30,11 @@ const definition = {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
const probeOutcome = (
|
||||
classification: Parameters<typeof resolveOutcome>[0]["classification"],
|
||||
retryable = false
|
||||
) => ({ classification, retryable, summary: "probe" });
|
||||
|
||||
describe("work resolution contracts", () => {
|
||||
test("high-impact open questions block definition approval", async () => {
|
||||
await expect(
|
||||
@@ -103,3 +108,53 @@ describe("work resolution contracts", () => {
|
||||
expect(canTransitionWork("ready", "executing")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolver decisions", () => {
|
||||
const policy = defaultCodingKitV0.retryPolicy;
|
||||
|
||||
test("retries a retryable failure while the budget remains", () => {
|
||||
expect(
|
||||
resolveOutcome(probeOutcome("RetryableFailure", true), 1, policy)
|
||||
).toEqual({ kind: "retry" });
|
||||
});
|
||||
|
||||
test("settles RetryableFailure as failed once the budget is exhausted", () => {
|
||||
expect(
|
||||
resolveOutcome(probeOutcome("RetryableFailure", true), 3, policy)
|
||||
).toEqual({
|
||||
kind: "terminal",
|
||||
workStatus: "failed",
|
||||
});
|
||||
});
|
||||
|
||||
test("maps each terminal classification to its Work status", () => {
|
||||
expect(resolveOutcome(probeOutcome("Succeeded"), 1, policy)).toEqual({
|
||||
kind: "terminal",
|
||||
workStatus: "completed",
|
||||
});
|
||||
expect(resolveOutcome(probeOutcome("PermanentFailure"), 1, policy)).toEqual(
|
||||
{
|
||||
kind: "terminal",
|
||||
workStatus: "failed",
|
||||
}
|
||||
);
|
||||
expect(resolveOutcome(probeOutcome("NeedsInput"), 1, policy)).toEqual({
|
||||
kind: "terminal",
|
||||
workStatus: "needs-input",
|
||||
});
|
||||
expect(resolveOutcome(probeOutcome("Blocked"), 1, policy)).toEqual({
|
||||
kind: "terminal",
|
||||
workStatus: "blocked",
|
||||
});
|
||||
expect(resolveOutcome(probeOutcome("Cancelled"), 1, policy)).toEqual({
|
||||
kind: "terminal",
|
||||
workStatus: "ready",
|
||||
});
|
||||
});
|
||||
|
||||
test("completed and cancelled are not silently reopened", () => {
|
||||
expect(canTransitionWork("completed", "defining")).toBe(false);
|
||||
expect(canTransitionWork("cancelled", "proposed")).toBe(false);
|
||||
expect(canTransitionWork("ready", "executing")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user