Slices 2-4: Work becomes executable
Domain primitives: versioned WorkDefinition/DesignPacket with risk, questions, and approval gates; lifecycle transition table with invalidation rules; Resolver (Run/Attempt/outcomes/CodingKitV0); provider-neutral HarnessRuntime with normalized events and a deterministic FakeHarnessLive that always reaches a terminal classification and never claims implementation. Convex durable model: workDefinitions, workQuestions, workApprovals, designPackets, workSlices, workRuns, workAttempts, workAttemptEvents tables; broadened works.status union; generalized workEvents with optional signalId, referenceId, and payloadJson. Authenticated commands for definition request/ save/revise/approve, question answer/withdraw, design save/revise/approve, and simulated execution start/cancel/retry with leased attempts, checkpointed events, and expired-lease reconciliation. Private work-planner FLUE agent with proposal-only tools (definition, design, question). Convex validates and stores every proposal; FLUE never approves or advances Work. Expanded Work card with Outcome, Design, and Build sections wired to the new mutations. Slice 1 conversation and provenance preserved.
This commit is contained in:
@@ -15,7 +15,12 @@
|
||||
"./project-workspace": "./src/project-workspace.ts",
|
||||
"./signal": "./src/signal.ts",
|
||||
"./smoke": "./src/smoke.ts",
|
||||
"./work": "./src/work.ts"
|
||||
"./work": "./src/work.ts",
|
||||
"./work-definition": "./src/work-definition.ts",
|
||||
"./work-design": "./src/work-design.ts",
|
||||
"./work-lifecycle": "./src/work-lifecycle.ts",
|
||||
"./resolver": "./src/resolver.ts",
|
||||
"./harness-runtime": "./src/harness-runtime.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"check-types": "tsc --noEmit",
|
||||
|
||||
167
packages/primitives/src/harness-runtime.ts
Normal file
167
packages/primitives/src/harness-runtime.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Effect, Schema } from "effect";
|
||||
|
||||
import type { AttemptOutcome } from "./resolver";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
export const HarnessEventKind = Schema.Literals([
|
||||
"started",
|
||||
"progress",
|
||||
"question",
|
||||
"log",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]);
|
||||
export type HarnessEventKind = typeof HarnessEventKind.Type;
|
||||
|
||||
export const HarnessEvent = Schema.Struct({
|
||||
kind: HarnessEventKind,
|
||||
message: Text,
|
||||
metadata: Schema.Record(Schema.String, Schema.String),
|
||||
occurredAt: Schema.Number,
|
||||
sequence: Schema.Int,
|
||||
});
|
||||
export type HarnessEvent = typeof HarnessEvent.Type;
|
||||
|
||||
export const FakeScenario = Schema.Literals([
|
||||
"success",
|
||||
"transient-failure-then-success",
|
||||
"needs-input",
|
||||
"permanent-failure",
|
||||
"cancelled",
|
||||
]);
|
||||
export type FakeScenario = typeof FakeScenario.Type;
|
||||
|
||||
export interface HarnessRuntime {
|
||||
readonly name: string;
|
||||
readonly run: (input: {
|
||||
readonly attemptNumber?: number;
|
||||
readonly scenario: FakeScenario;
|
||||
readonly signal?: AbortSignal;
|
||||
}) => Effect.Effect<readonly [readonly HarnessEvent[], AttemptOutcome]>;
|
||||
}
|
||||
|
||||
const event = (
|
||||
sequence: number,
|
||||
kind: HarnessEventKind,
|
||||
message: string,
|
||||
occurredAt: number
|
||||
): HarnessEvent => ({ kind, message, metadata: {}, occurredAt, sequence });
|
||||
|
||||
export const FakeHarnessLive = (
|
||||
now: () => number = Date.now
|
||||
): HarnessRuntime => ({
|
||||
name: "fake",
|
||||
run: ({ attemptNumber = 1, scenario, signal }) =>
|
||||
Effect.sync(() => {
|
||||
const started = now();
|
||||
const events: HarnessEvent[] = [
|
||||
event(0, "started", `fake harness started: ${scenario}`, started),
|
||||
];
|
||||
if (signal?.aborted || scenario === "cancelled") {
|
||||
events.push(event(1, "cancelled", "simulation cancelled", now()));
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "Cancelled",
|
||||
retryable: false,
|
||||
summary: "Simulation cancelled",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
events.push(
|
||||
event(1, "progress", "validated approved Definition and Design", now())
|
||||
);
|
||||
switch (scenario) {
|
||||
case "success": {
|
||||
events.push(
|
||||
event(
|
||||
2,
|
||||
"completed",
|
||||
"fake implementation evidence recorded",
|
||||
now()
|
||||
)
|
||||
);
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "Succeeded",
|
||||
retryable: false,
|
||||
summary: "Simulation succeeded; no implementation was claimed",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
case "transient-failure-then-success": {
|
||||
if (attemptNumber > 1) {
|
||||
events.push(
|
||||
event(2, "completed", "transient failure recovered", now())
|
||||
);
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "Succeeded",
|
||||
retryable: false,
|
||||
summary:
|
||||
"Simulation recovered after a transient failure; no implementation was claimed",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
events.push(event(2, "failed", "transient provider failure", now()));
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "RetryableFailure",
|
||||
retryable: true,
|
||||
summary: "Transient simulation failure",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
case "needs-input": {
|
||||
events.push(
|
||||
event(2, "question", "simulation needs a human decision", now())
|
||||
);
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "NeedsInput",
|
||||
retryable: false,
|
||||
summary: "Simulation needs input",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
case "permanent-failure": {
|
||||
events.push(
|
||||
event(
|
||||
2,
|
||||
"failed",
|
||||
"deterministic permanent simulation failure",
|
||||
now()
|
||||
)
|
||||
);
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "PermanentFailure",
|
||||
retryable: false,
|
||||
summary: "Permanent simulation failure",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
default: {
|
||||
return [
|
||||
events,
|
||||
{
|
||||
classification: "PermanentFailure",
|
||||
retryable: false,
|
||||
summary: "Unknown simulation scenario",
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -10,3 +10,8 @@ export * from "./project-work";
|
||||
export * from "./signal";
|
||||
export * from "./smoke";
|
||||
export * from "./work";
|
||||
export * from "./work-definition";
|
||||
export * from "./work-design";
|
||||
export * from "./work-lifecycle";
|
||||
export * from "./resolver";
|
||||
export * from "./harness-runtime";
|
||||
|
||||
71
packages/primitives/src/resolver.ts
Normal file
71
packages/primitives/src/resolver.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Schema } from "effect";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
export const RunStatus = Schema.Literals(["ready", "running", "terminal"]);
|
||||
export type RunStatus = typeof RunStatus.Type;
|
||||
export const AttemptStatus = Schema.Literals([
|
||||
"queued",
|
||||
"claimed",
|
||||
"running",
|
||||
"terminal",
|
||||
]);
|
||||
export type AttemptStatus = typeof AttemptStatus.Type;
|
||||
export const AttemptClassification = Schema.Literals([
|
||||
"Succeeded",
|
||||
"RetryableFailure",
|
||||
"NeedsInput",
|
||||
"Blocked",
|
||||
"VerificationFailed",
|
||||
"BudgetExhausted",
|
||||
"Cancelled",
|
||||
"PermanentFailure",
|
||||
]);
|
||||
export type AttemptClassification = typeof AttemptClassification.Type;
|
||||
|
||||
export const AttemptOutcome = Schema.Struct({
|
||||
classification: AttemptClassification,
|
||||
retryable: Schema.Boolean,
|
||||
summary: Text,
|
||||
});
|
||||
export type AttemptOutcome = typeof AttemptOutcome.Type;
|
||||
|
||||
export const RetryPolicy = Schema.Struct({
|
||||
maxAttempts: Schema.Int,
|
||||
retryableClassifications: Schema.Array(AttemptClassification),
|
||||
});
|
||||
export type RetryPolicy = typeof RetryPolicy.Type;
|
||||
|
||||
export const CodingKitV0 = Schema.Struct({
|
||||
harness: Schema.Literal("fake"),
|
||||
id: Schema.Literal("coding-kit-v0"),
|
||||
retryPolicy: RetryPolicy,
|
||||
version: Schema.Literal("0.1.0"),
|
||||
});
|
||||
export type CodingKitV0 = typeof CodingKitV0.Type;
|
||||
|
||||
export const isTerminalClassification = (_: AttemptClassification): true =>
|
||||
true;
|
||||
|
||||
export const shouldRetry = (
|
||||
outcome: AttemptOutcome,
|
||||
attemptNumber: number,
|
||||
policy: RetryPolicy
|
||||
): boolean =>
|
||||
outcome.retryable &&
|
||||
attemptNumber < policy.maxAttempts &&
|
||||
policy.retryableClassifications.includes(outcome.classification);
|
||||
|
||||
export const defaultCodingKitV0: CodingKitV0 = {
|
||||
harness: "fake",
|
||||
id: "coding-kit-v0",
|
||||
retryPolicy: {
|
||||
maxAttempts: 3,
|
||||
retryableClassifications: ["RetryableFailure"],
|
||||
},
|
||||
version: "0.1.0",
|
||||
};
|
||||
91
packages/primitives/src/work-definition.ts
Normal file
91
packages/primitives/src/work-definition.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Effect, Schema } from "effect";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
export const DefinitionRisk = Schema.Literals(["low", "medium", "high"]);
|
||||
export type DefinitionRisk = typeof DefinitionRisk.Type;
|
||||
|
||||
export const QuestionImpact = Schema.Literals(["low", "medium", "high"]);
|
||||
export type QuestionImpact = typeof QuestionImpact.Type;
|
||||
|
||||
export const WorkQuestion = Schema.Struct({
|
||||
alternatives: Schema.Array(Text),
|
||||
answer: Schema.optional(Text),
|
||||
id: Text,
|
||||
impact: QuestionImpact,
|
||||
prompt: Text,
|
||||
recommendation: Schema.optional(Text),
|
||||
status: Schema.Literals(["open", "answered", "withdrawn"]),
|
||||
});
|
||||
export type WorkQuestion = typeof WorkQuestion.Type;
|
||||
|
||||
export const WorkDefinition = Schema.Struct({
|
||||
acceptanceCriteria: Schema.NonEmptyArray(Text),
|
||||
affectedUsers: Schema.Array(Text),
|
||||
assumptions: Schema.Array(Text),
|
||||
constraints: Schema.Array(Text),
|
||||
desiredOutcome: Text,
|
||||
inScope: Schema.Array(Text),
|
||||
outOfScope: Schema.Array(Text),
|
||||
problem: Text,
|
||||
questions: Schema.Array(WorkQuestion),
|
||||
requiredArtifacts: Schema.Array(Text),
|
||||
risk: DefinitionRisk,
|
||||
rollback: Schema.optional(Text),
|
||||
rollout: Schema.optional(Text),
|
||||
version: Schema.Int,
|
||||
});
|
||||
export type WorkDefinition = typeof WorkDefinition.Type;
|
||||
|
||||
export const DefinitionApproval = Schema.Struct({
|
||||
approvedAt: Schema.Number,
|
||||
approvedBy: Text,
|
||||
definitionVersion: Schema.Int,
|
||||
});
|
||||
export type DefinitionApproval = typeof DefinitionApproval.Type;
|
||||
|
||||
export const DefinitionProposal = WorkDefinition;
|
||||
export type DefinitionProposal = typeof DefinitionProposal.Type;
|
||||
|
||||
export class DefinitionError extends Schema.TaggedErrorClass<DefinitionError>()(
|
||||
"DefinitionError",
|
||||
{
|
||||
message: Schema.String,
|
||||
reason: Schema.Literals(["Invalid", "OpenHighImpactQuestion"]),
|
||||
}
|
||||
) {}
|
||||
|
||||
export const validateDefinition = Effect.fn("Work.validateDefinition")(
|
||||
function* validateDefinition(input: unknown) {
|
||||
const definition = yield* Schema.decodeUnknownEffect(WorkDefinition)(
|
||||
input
|
||||
).pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new DefinitionError({ message: cause.message, reason: "Invalid" })
|
||||
)
|
||||
);
|
||||
const blocked = definition.questions.some(
|
||||
(question) => question.status === "open" && question.impact === "high"
|
||||
);
|
||||
if (blocked) {
|
||||
return yield* Effect.fail(
|
||||
new DefinitionError({
|
||||
message:
|
||||
"High-impact open questions must be resolved before approval",
|
||||
reason: "OpenHighImpactQuestion",
|
||||
})
|
||||
);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
);
|
||||
|
||||
export const canApproveDefinition = (definition: WorkDefinition): boolean =>
|
||||
definition.questions.every(
|
||||
(question) => question.status !== "open" || question.impact !== "high"
|
||||
);
|
||||
98
packages/primitives/src/work-design.ts
Normal file
98
packages/primitives/src/work-design.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Effect, Schema } from "effect";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
export const ImpactMap = Schema.Struct({
|
||||
files: Schema.Array(Text),
|
||||
modules: Schema.Array(Text),
|
||||
risks: Schema.Array(Text),
|
||||
summary: Text,
|
||||
});
|
||||
export type ImpactMap = typeof ImpactMap.Type;
|
||||
|
||||
export const VerticalSlice = Schema.Struct({
|
||||
codeBoundaries: Schema.Array(Text),
|
||||
dependsOn: Schema.Array(Text),
|
||||
evidenceRequirements: Schema.NonEmptyArray(Text),
|
||||
id: Text,
|
||||
objective: Text,
|
||||
observableBehavior: Text,
|
||||
reviewRequired: Schema.Boolean,
|
||||
title: Text,
|
||||
verification: Schema.NonEmptyArray(Text),
|
||||
});
|
||||
export type VerticalSlice = typeof VerticalSlice.Type;
|
||||
|
||||
export const DesignPacket = Schema.Struct({
|
||||
architectureSummary: Text,
|
||||
callFlowDelta: Schema.Array(Text),
|
||||
concerns: Schema.Array(Text),
|
||||
definitionVersion: Schema.Int,
|
||||
evidenceRequirements: Schema.NonEmptyArray(Text),
|
||||
fileTreeDelta: Schema.Array(Text),
|
||||
impactMap: ImpactMap,
|
||||
invariants: Schema.NonEmptyArray(Text),
|
||||
keyInterfaces: Schema.Array(Text),
|
||||
slices: Schema.Array(VerticalSlice),
|
||||
tradeoffs: Schema.Array(Text),
|
||||
version: Schema.Int,
|
||||
});
|
||||
export type DesignPacket = typeof DesignPacket.Type;
|
||||
|
||||
export const DesignApproval = Schema.Struct({
|
||||
approvedAt: Schema.Number,
|
||||
approvedBy: Text,
|
||||
definitionVersion: Schema.Int,
|
||||
designVersion: Schema.Int,
|
||||
});
|
||||
export type DesignApproval = typeof DesignApproval.Type;
|
||||
|
||||
export class DesignError extends Schema.TaggedErrorClass<DesignError>()(
|
||||
"DesignError",
|
||||
{
|
||||
message: Schema.String,
|
||||
reason: Schema.Literals([
|
||||
"Invalid",
|
||||
"NoObservableSlices",
|
||||
"DefinitionMismatch",
|
||||
]),
|
||||
}
|
||||
) {}
|
||||
|
||||
export const validateDesignPacket = Effect.fn("Work.validateDesignPacket")(
|
||||
function* validateDesignPacket(input: unknown) {
|
||||
const design = yield* Schema.decodeUnknownEffect(DesignPacket)(input).pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
new DesignError({ message: cause.message, reason: "Invalid" })
|
||||
)
|
||||
);
|
||||
if (
|
||||
design.slices.length === 0 ||
|
||||
design.slices.some(
|
||||
(slice) =>
|
||||
slice.observableBehavior.trim().length === 0 ||
|
||||
slice.evidenceRequirements.length === 0 ||
|
||||
slice.verification.length === 0
|
||||
)
|
||||
) {
|
||||
return yield* Effect.fail(
|
||||
new DesignError({
|
||||
message:
|
||||
"Every Design must contain observable, independently verifiable slices",
|
||||
reason: "NoObservableSlices",
|
||||
})
|
||||
);
|
||||
}
|
||||
return design;
|
||||
}
|
||||
);
|
||||
|
||||
export const isObservableSlice = (slice: VerticalSlice): boolean =>
|
||||
slice.observableBehavior.trim().length > 0 &&
|
||||
slice.evidenceRequirements.length > 0 &&
|
||||
slice.verification.length > 0;
|
||||
69
packages/primitives/src/work-lifecycle.ts
Normal file
69
packages/primitives/src/work-lifecycle.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Schema } from "effect";
|
||||
|
||||
export const WorkStatus = Schema.Literals([
|
||||
"proposed",
|
||||
"defining",
|
||||
"awaiting-definition-approval",
|
||||
"designing",
|
||||
"awaiting-design-approval",
|
||||
"ready",
|
||||
"executing",
|
||||
"needs-input",
|
||||
"blocked",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]);
|
||||
export type WorkStatus = typeof WorkStatus.Type;
|
||||
|
||||
export const WORK_TRANSITIONS: Readonly<
|
||||
Record<WorkStatus, readonly WorkStatus[]>
|
||||
> = {
|
||||
"awaiting-definition-approval": ["designing", "defining"],
|
||||
"awaiting-design-approval": [
|
||||
"ready",
|
||||
"designing",
|
||||
"awaiting-definition-approval",
|
||||
],
|
||||
blocked: ["ready", "executing", "cancelled"],
|
||||
cancelled: ["ready", "proposed"],
|
||||
completed: ["proposed", "defining"],
|
||||
defining: ["awaiting-definition-approval", "proposed"],
|
||||
designing: ["awaiting-design-approval", "awaiting-definition-approval"],
|
||||
executing: [
|
||||
"ready",
|
||||
"needs-input",
|
||||
"blocked",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
],
|
||||
failed: ["ready", "executing", "cancelled"],
|
||||
"needs-input": ["ready", "executing", "cancelled"],
|
||||
proposed: ["defining"],
|
||||
ready: ["executing", "designing", "cancelled"],
|
||||
};
|
||||
|
||||
export const canTransitionWork = (from: WorkStatus, to: WorkStatus): boolean =>
|
||||
WORK_TRANSITIONS[from].includes(to);
|
||||
|
||||
export const assertWorkTransition = (
|
||||
from: WorkStatus,
|
||||
to: WorkStatus
|
||||
): void => {
|
||||
if (!canTransitionWork(from, to)) {
|
||||
throw new Error(`Invalid Work transition: ${from} -> ${to}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const definitionRevisionInvalidation = {
|
||||
definitionApproval: "invalidate" as const,
|
||||
dependentDesign: "invalidate" as const,
|
||||
designApproval: "invalidate" as const,
|
||||
workStatus: "defining" as const,
|
||||
};
|
||||
|
||||
export const designRevisionInvalidation = {
|
||||
designApproval: "invalidate" as const,
|
||||
workStatus: "designing" as const,
|
||||
};
|
||||
105
packages/primitives/src/work-resolution.test.ts
Normal file
105
packages/primitives/src/work-resolution.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Effect } from "effect";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { FakeHarnessLive } from "./harness-runtime";
|
||||
import { defaultCodingKitV0, shouldRetry } from "./resolver";
|
||||
import { validateDefinition } from "./work-definition";
|
||||
import { validateDesignPacket } from "./work-design";
|
||||
import { canTransitionWork } from "./work-lifecycle";
|
||||
|
||||
const definition = {
|
||||
acceptanceCriteria: ["terminal outcome"],
|
||||
affectedUsers: ["founders"],
|
||||
assumptions: [],
|
||||
constraints: [],
|
||||
desiredOutcome: "A visible terminal simulation exists",
|
||||
inScope: ["fake execution"],
|
||||
outOfScope: ["real sandboxing"],
|
||||
problem: "A deterministic build path is needed",
|
||||
questions: [
|
||||
{
|
||||
alternatives: [],
|
||||
id: "q1",
|
||||
impact: "high",
|
||||
prompt: "Which runtime?",
|
||||
status: "open",
|
||||
},
|
||||
],
|
||||
requiredArtifacts: ["events"],
|
||||
risk: "medium",
|
||||
version: 1,
|
||||
};
|
||||
|
||||
describe("work resolution contracts", () => {
|
||||
test("high-impact open questions block definition approval", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(validateDefinition(definition))
|
||||
).rejects.toThrow(/High-impact/u);
|
||||
});
|
||||
|
||||
test("observable slices and terminal fake outcomes are enforced", async () => {
|
||||
const approved = await Effect.runPromise(
|
||||
validateDefinition({ ...definition, questions: [] })
|
||||
);
|
||||
const design = await Effect.runPromise(
|
||||
validateDesignPacket({
|
||||
architectureSummary: "small",
|
||||
callFlowDelta: [],
|
||||
concerns: [],
|
||||
definitionVersion: approved.version,
|
||||
evidenceRequirements: ["event"],
|
||||
fileTreeDelta: [],
|
||||
impactMap: { files: [], modules: [], risks: [], summary: "small" },
|
||||
invariants: ["terminal"],
|
||||
keyInterfaces: [],
|
||||
slices: [
|
||||
{
|
||||
codeBoundaries: ["runtime"],
|
||||
dependsOn: [],
|
||||
evidenceRequirements: ["event"],
|
||||
id: "s1",
|
||||
objective: "one",
|
||||
observableBehavior: "visible",
|
||||
reviewRequired: false,
|
||||
title: "one",
|
||||
verification: ["assert"],
|
||||
},
|
||||
],
|
||||
tradeoffs: [],
|
||||
version: 1,
|
||||
})
|
||||
);
|
||||
expect(design.slices).toHaveLength(1);
|
||||
const [, outcome] = await Effect.runPromise(
|
||||
FakeHarnessLive(() => 1).run({ scenario: "success" })
|
||||
);
|
||||
expect(outcome.classification).toBe("Succeeded");
|
||||
expect(outcome.summary).toMatch(/no implementation/u);
|
||||
const [, firstTransient] = await Effect.runPromise(
|
||||
FakeHarnessLive(() => 1).run({
|
||||
attemptNumber: 1,
|
||||
scenario: "transient-failure-then-success",
|
||||
})
|
||||
);
|
||||
const [, recoveredTransient] = await Effect.runPromise(
|
||||
FakeHarnessLive(() => 1).run({
|
||||
attemptNumber: 2,
|
||||
scenario: "transient-failure-then-success",
|
||||
})
|
||||
);
|
||||
expect(firstTransient.classification).toBe("RetryableFailure");
|
||||
expect(recoveredTransient.classification).toBe("Succeeded");
|
||||
expect(
|
||||
shouldRetry(
|
||||
{
|
||||
classification: "RetryableFailure",
|
||||
retryable: true,
|
||||
summary: "retry",
|
||||
},
|
||||
1,
|
||||
defaultCodingKitV0.retryPolicy
|
||||
)
|
||||
).toBe(true);
|
||||
expect(canTransitionWork("ready", "executing")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Array as EffectArray, Effect, Order, Schema } from "effect";
|
||||
|
||||
export { WorkStatus } from "./work-lifecycle";
|
||||
export type { WorkStatus as WorkLifecycleStatus } from "./work-lifecycle";
|
||||
|
||||
const MeaningfulString = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
|
||||
export const WorkStatus = Schema.Literal("proposed");
|
||||
export type WorkStatus = typeof WorkStatus.Type;
|
||||
|
||||
export const WorkDraft = Schema.Struct({
|
||||
objective: MeaningfulString,
|
||||
title: MeaningfulString,
|
||||
@@ -91,6 +91,24 @@ const WorkAttachmentInput = Schema.Struct({
|
||||
export const WorkEventKind = Schema.Literals([
|
||||
"work.proposed",
|
||||
"signal.attached",
|
||||
"definition.requested",
|
||||
"definition.saved",
|
||||
"definition.revised",
|
||||
"definition.approved",
|
||||
"definition.invalidated",
|
||||
"question.answered",
|
||||
"question.withdrawn",
|
||||
"design.requested",
|
||||
"design.saved",
|
||||
"design.revised",
|
||||
"design.approved",
|
||||
"design.invalidated",
|
||||
"run.started",
|
||||
"run.cancelled",
|
||||
"attempt.claimed",
|
||||
"attempt.event",
|
||||
"attempt.completed",
|
||||
"attempt.reconciled",
|
||||
]);
|
||||
export type WorkEventKind = typeof WorkEventKind.Type;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user