Slice 4 control-plane repairs, lint/catalog cleanup, Flue adapter fixes
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
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"./signal": "./src/signal.ts",
|
||||
"./smoke": "./src/smoke.ts",
|
||||
"./work": "./src/work.ts",
|
||||
"./work-artifact": "./src/work-artifact.ts",
|
||||
"./work-definition": "./src/work-definition.ts",
|
||||
"./work-design": "./src/work-design.ts",
|
||||
"./work-lifecycle": "./src/work-lifecycle.ts",
|
||||
@@ -35,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@code/config": "workspace:*",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@code/primitives",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./agent-os": "./src/agent-os.ts",
|
||||
"./git": "./src/git.ts",
|
||||
"./project": "./src/project.ts",
|
||||
"./project-issue": "./src/project-issue.ts",
|
||||
"./project-workspace": "./src/project-workspace.ts",
|
||||
"./project-work": "./src/project-work.ts",
|
||||
"./signal": "./src/signal.ts",
|
||||
"./smoke": "./src/smoke.ts"
|
||||
},
|
||||
@@ -10,6 +10,7 @@ export * from "./project-work";
|
||||
export * from "./signal";
|
||||
export * from "./smoke";
|
||||
export * from "./work";
|
||||
export * from "./work-artifact";
|
||||
export * from "./work-definition";
|
||||
export * from "./work-design";
|
||||
export * from "./work-lifecycle";
|
||||
|
||||
@@ -8,7 +8,12 @@ const Text = Schema.String.check(
|
||||
})
|
||||
);
|
||||
|
||||
export const RunStatus = Schema.Literals(["ready", "running", "terminal"]);
|
||||
export const RunStatus = Schema.Literals([
|
||||
"ready",
|
||||
"running",
|
||||
"terminal",
|
||||
"cancelled",
|
||||
]);
|
||||
export type RunStatus = typeof RunStatus.Type;
|
||||
export const AttemptStatus = Schema.Literals([
|
||||
"queued",
|
||||
|
||||
58
packages/primitives/src/work-artifact.test.ts
Normal file
58
packages/primitives/src/work-artifact.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Effect } from "effect";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
decodeWorkArtifactDraft,
|
||||
decodeWorkDeliveryDraft,
|
||||
} from "./work-artifact";
|
||||
|
||||
describe("Work artifact contracts", () => {
|
||||
test("accepts traceable artifact and delivery metadata", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
decodeWorkArtifactDraft({
|
||||
idempotencyKey: "attempt:1:test-report",
|
||||
kind: "test-report",
|
||||
metadataJson: "{}",
|
||||
producer: "fake-harness",
|
||||
provenanceJson: "{}",
|
||||
sourceRevision: "abc123",
|
||||
title: "Focused test report",
|
||||
verificationStatus: "verified",
|
||||
})
|
||||
)
|
||||
).resolves.toMatchObject({ kind: "test-report" });
|
||||
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
decodeWorkDeliveryDraft({
|
||||
externalId: "42",
|
||||
idempotencyKey: "delivery:pr:42",
|
||||
kind: "pull-request",
|
||||
metadataJson: "{}",
|
||||
provider: "gitea",
|
||||
sourceRevision: "abc123",
|
||||
status: "ready",
|
||||
target: "main",
|
||||
url: "https://git.example/pulls/42",
|
||||
})
|
||||
)
|
||||
).resolves.toMatchObject({ kind: "pull-request" });
|
||||
});
|
||||
|
||||
test("rejects unverifiable evidence metadata", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
decodeWorkArtifactDraft({
|
||||
idempotencyKey: "attempt:1:test-report",
|
||||
kind: "test-report",
|
||||
metadataJson: "not-json",
|
||||
producer: "fake-harness",
|
||||
provenanceJson: "{}",
|
||||
title: "Focused test report",
|
||||
verificationStatus: "verified",
|
||||
})
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
141
packages/primitives/src/work-artifact.ts
Normal file
141
packages/primitives/src/work-artifact.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Effect, Schema } from "effect";
|
||||
|
||||
const Text = Schema.String.check(
|
||||
Schema.makeFilter((value) => value.trim().length > 0, {
|
||||
expected: "a non-empty string",
|
||||
})
|
||||
);
|
||||
const JsonText = Schema.String.check(
|
||||
Schema.makeFilter(
|
||||
(value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ expected: "valid JSON text" }
|
||||
)
|
||||
);
|
||||
|
||||
export const WorkArtifactKind = Schema.Literals([
|
||||
"definition",
|
||||
"design",
|
||||
"diff",
|
||||
"test-report",
|
||||
"verification-report",
|
||||
"runtime-log",
|
||||
"screenshot",
|
||||
"video",
|
||||
"commit",
|
||||
"branch",
|
||||
"pull-request",
|
||||
"preview",
|
||||
"deployment",
|
||||
"other",
|
||||
]);
|
||||
export type WorkArtifactKind = typeof WorkArtifactKind.Type;
|
||||
|
||||
export const ArtifactVerificationStatus = Schema.Literals([
|
||||
"unverified",
|
||||
"verified",
|
||||
"rejected",
|
||||
]);
|
||||
export type ArtifactVerificationStatus = typeof ArtifactVerificationStatus.Type;
|
||||
|
||||
export const WorkArtifactDraft = Schema.Struct({
|
||||
contentHash: Schema.optional(Text),
|
||||
environmentId: Schema.optional(Text),
|
||||
idempotencyKey: Text,
|
||||
kind: WorkArtifactKind,
|
||||
metadataJson: JsonText,
|
||||
producer: Text,
|
||||
provenanceJson: JsonText,
|
||||
sourceRevision: Schema.optional(Text),
|
||||
title: Text,
|
||||
uri: Schema.optional(Text),
|
||||
verificationStatus: ArtifactVerificationStatus,
|
||||
});
|
||||
export type WorkArtifactDraft = typeof WorkArtifactDraft.Type;
|
||||
|
||||
export const WorkDeliveryKind = Schema.Literals([
|
||||
"branch",
|
||||
"commit",
|
||||
"pull-request",
|
||||
"preview",
|
||||
"deployment",
|
||||
]);
|
||||
export type WorkDeliveryKind = typeof WorkDeliveryKind.Type;
|
||||
|
||||
export const WorkDeliveryStatus = Schema.Literals([
|
||||
"recorded",
|
||||
"ready",
|
||||
"approved",
|
||||
"delivered",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]);
|
||||
export type WorkDeliveryStatus = typeof WorkDeliveryStatus.Type;
|
||||
|
||||
export const WorkDeliveryDraft = Schema.Struct({
|
||||
externalId: Text,
|
||||
idempotencyKey: Text,
|
||||
kind: WorkDeliveryKind,
|
||||
metadataJson: JsonText,
|
||||
provider: Text,
|
||||
sourceRevision: Text,
|
||||
status: WorkDeliveryStatus,
|
||||
target: Text,
|
||||
url: Schema.optional(Text),
|
||||
});
|
||||
export type WorkDeliveryDraft = typeof WorkDeliveryDraft.Type;
|
||||
|
||||
export class WorkArtifactError extends Schema.TaggedErrorClass<WorkArtifactError>()(
|
||||
"WorkArtifactError",
|
||||
{ message: Schema.String }
|
||||
) {}
|
||||
|
||||
const decode = <A, I, R>(schema: Schema.Codec<A, I, R>, input: unknown) =>
|
||||
Schema.decodeUnknownEffect(schema)(input).pipe(
|
||||
Effect.mapError(
|
||||
(cause) => new WorkArtifactError({ message: cause.message })
|
||||
)
|
||||
);
|
||||
|
||||
export const decodeWorkArtifactDraft = Effect.fn("Work.decodeArtifactDraft")(
|
||||
function* decodeArtifactDraft(input: unknown) {
|
||||
const draft = yield* decode(WorkArtifactDraft, input);
|
||||
if (
|
||||
draft.verificationStatus === "verified" &&
|
||||
draft.sourceRevision === undefined
|
||||
) {
|
||||
return yield* Effect.fail(
|
||||
new WorkArtifactError({
|
||||
message: "Verified artifacts must bind an exact source revision",
|
||||
})
|
||||
);
|
||||
}
|
||||
return draft;
|
||||
}
|
||||
);
|
||||
|
||||
export const decodeWorkDeliveryDraft = Effect.fn("Work.decodeDeliveryDraft")(
|
||||
(input: unknown) => decode(WorkDeliveryDraft, input)
|
||||
);
|
||||
|
||||
const DELIVERY_TRANSITIONS: Readonly<
|
||||
Record<WorkDeliveryStatus, readonly WorkDeliveryStatus[]>
|
||||
> = {
|
||||
approved: ["delivered", "failed", "cancelled"],
|
||||
cancelled: [],
|
||||
delivered: [],
|
||||
failed: [],
|
||||
ready: ["approved", "delivered", "failed", "cancelled"],
|
||||
recorded: ["ready", "failed", "cancelled"],
|
||||
};
|
||||
|
||||
export const canTransitionWorkDelivery = (
|
||||
from: WorkDeliveryStatus,
|
||||
to: WorkDeliveryStatus
|
||||
): boolean => DELIVERY_TRANSITIONS[from].includes(to);
|
||||
@@ -59,8 +59,8 @@ export class DefinitionError extends Schema.TaggedErrorClass<DefinitionError>()(
|
||||
}
|
||||
) {}
|
||||
|
||||
export const validateDefinition = Effect.fn("Work.validateDefinition")(
|
||||
function* validateDefinition(input: unknown) {
|
||||
export const decodeDefinition = Effect.fn("Work.decodeDefinition")(
|
||||
function* decodeDefinition(input: unknown) {
|
||||
const definition = yield* Schema.decodeUnknownEffect(WorkDefinition)(
|
||||
input
|
||||
).pipe(
|
||||
@@ -69,6 +69,25 @@ export const validateDefinition = Effect.fn("Work.validateDefinition")(
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -59,6 +59,7 @@ export class DesignError extends Schema.TaggedErrorClass<DesignError>()(
|
||||
"Invalid",
|
||||
"NoObservableSlices",
|
||||
"DefinitionMismatch",
|
||||
"InvalidSliceGraph",
|
||||
]),
|
||||
}
|
||||
) {}
|
||||
@@ -73,8 +74,10 @@ export const validateDesignPacket = Effect.fn("Work.validateDesignPacket")(
|
||||
);
|
||||
if (
|
||||
design.slices.length === 0 ||
|
||||
design.slices.length > 4 ||
|
||||
design.slices.some(
|
||||
(slice) =>
|
||||
slice.codeBoundaries.length === 0 ||
|
||||
slice.observableBehavior.trim().length === 0 ||
|
||||
slice.evidenceRequirements.length === 0 ||
|
||||
slice.verification.length === 0
|
||||
@@ -88,6 +91,24 @@ export const validateDesignPacket = Effect.fn("Work.validateDesignPacket")(
|
||||
})
|
||||
);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const slice of design.slices) {
|
||||
if (
|
||||
seen.has(slice.id) ||
|
||||
slice.dependsOn.some(
|
||||
(dependency) => dependency === slice.id || !seen.has(dependency)
|
||||
)
|
||||
) {
|
||||
return yield* Effect.fail(
|
||||
new DesignError({
|
||||
message:
|
||||
"Slice IDs must be unique and dependencies must reference earlier slices",
|
||||
reason: "InvalidSliceGraph",
|
||||
})
|
||||
);
|
||||
}
|
||||
seen.add(slice.id);
|
||||
}
|
||||
return design;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
|
||||
|
||||
import { FakeHarnessLive } from "./harness-runtime";
|
||||
import { defaultCodingKitV0, resolveOutcome, shouldRetry } from "./resolver";
|
||||
import { validateDefinition } from "./work-definition";
|
||||
import { decodeDefinition, validateDefinition } from "./work-definition";
|
||||
import { validateDesignPacket } from "./work-design";
|
||||
import { canTransitionWork } from "./work-lifecycle";
|
||||
|
||||
@@ -37,11 +37,25 @@ const probeOutcome = (
|
||||
|
||||
describe("work resolution contracts", () => {
|
||||
test("high-impact open questions block definition approval", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(decodeDefinition(definition))
|
||||
).resolves.toEqual(definition);
|
||||
await expect(
|
||||
Effect.runPromise(validateDefinition(definition))
|
||||
).rejects.toThrow(/High-impact/u);
|
||||
});
|
||||
|
||||
test("question identities are unique within a Definition version", async () => {
|
||||
await expect(
|
||||
Effect.runPromise(
|
||||
decodeDefinition({
|
||||
...definition,
|
||||
questions: [definition.questions[0], definition.questions[0]],
|
||||
})
|
||||
)
|
||||
).rejects.toThrow(/Question ID must be unique/u);
|
||||
});
|
||||
|
||||
test("observable slices and terminal fake outcomes are enforced", async () => {
|
||||
const approved = await Effect.runPromise(
|
||||
validateDefinition({ ...definition, questions: [] })
|
||||
|
||||
@@ -96,6 +96,7 @@ export const WorkEventKind = Schema.Literals([
|
||||
"definition.revised",
|
||||
"definition.approved",
|
||||
"definition.invalidated",
|
||||
"question.created",
|
||||
"question.answered",
|
||||
"question.withdrawn",
|
||||
"design.requested",
|
||||
@@ -103,12 +104,21 @@ export const WorkEventKind = Schema.Literals([
|
||||
"design.revised",
|
||||
"design.approved",
|
||||
"design.invalidated",
|
||||
"planner.failed",
|
||||
"slice.started",
|
||||
"slice.completed",
|
||||
"slice.ready",
|
||||
"run.started",
|
||||
"run.completed",
|
||||
"run.cancelled",
|
||||
"attempt.claimed",
|
||||
"attempt.event",
|
||||
"attempt.completed",
|
||||
"attempt.reconciled",
|
||||
"resolver.decided",
|
||||
"artifact.recorded",
|
||||
"delivery.recorded",
|
||||
"delivery.updated",
|
||||
]);
|
||||
export type WorkEventKind = typeof WorkEventKind.Type;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user