Convex control plane (Slices 1-4 prerequisites for Slice 5): - Versioned Definition/Design persistence: revisions preserve history instead of deleting prior slices/approvals - Fenced conversation turn queue: attempt-numbered leases prevent stale worker overwrites; expired turns reconciled by cron - Fenced Run/Attempt claiming: only queued attempts from running Runs can be claimed; lease expiry checks on every finish/checkpoint - Multi-slice progression: successful slice marks next slice ready instead of completing the whole Work; completed Runs blocked from retry - Typed AttemptClassification in schema and resolver decisions table - Durable resolver decisions for normal outcomes, cancellation, and lease-expiry recovery - Persistent artifacts and delivery metadata with provenance, source revision, verification status, and controlled delivery transitions - High-impact open questions block definition approval - Question submission gated to defining/awaiting-approval states only - Delivery status updates validate JSON metadata before persisting - Planner failures recorded as durable blocked events - Approval identity uses canonical tokenIdentifier Primitives: - decodeDefinition separates decode from validate - Design validation enforces 1-4 slices with unique IDs and valid deps - Artifact and delivery draft schemas with verified-revision binding - Delivery status transition table Package management: - Consolidated shared deps into single catalog (hono, valibot, streamdown, @types/react, @types/node, @tailwindcss/*, react-native) - Removed cross-version Hono boundary: flue() exported as Fetchable - Deleted bun.lock and regenerated from clean catalog resolution Lint/format: - Removed .eslintignore; oxc uses native ignorePatterns in oxlint.config.ts - Deleted redundant packages/backend/.oxlintrc.json - Added repos/** and scripts/** to ignore patterns - Convex-specific rule overrides for ES2022 target constraints - Fixed all oxc errors in fluePersistence.ts and agents/src/db.ts - Fixed all formatting across changed files
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
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 decodeDefinition = Effect.fn("Work.decodeDefinition")(
|
|
function* decodeDefinition(input: unknown) {
|
|
const definition = yield* Schema.decodeUnknownEffect(WorkDefinition)(
|
|
input
|
|
).pipe(
|
|
Effect.mapError(
|
|
(cause) =>
|
|
new DefinitionError({ message: cause.message, reason: "Invalid" })
|
|
)
|
|
);
|
|
const questionIds = new Set<string>();
|
|
for (const question of definition.questions) {
|
|
if (questionIds.has(question.id)) {
|
|
return yield* Effect.fail(
|
|
new DefinitionError({
|
|
message: `Question ID must be unique: ${question.id}`,
|
|
reason: "Invalid",
|
|
})
|
|
);
|
|
}
|
|
questionIds.add(question.id);
|
|
}
|
|
return definition;
|
|
}
|
|
);
|
|
|
|
export const validateDefinition = Effect.fn("Work.validateDefinition")(
|
|
function* validateDefinition(input: unknown) {
|
|
const definition = yield* decodeDefinition(input);
|
|
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"
|
|
);
|