Files
zopu-code/packages/primitives/src/work.ts
T
-Puter 005b26fa32 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.
2026-07-27 22:52:14 +05:30

250 lines
6.5 KiB
TypeScript

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 WorkDraft = Schema.Struct({
objective: MeaningfulString,
title: MeaningfulString,
});
export type WorkDraft = typeof WorkDraft.Type;
export const SignalProblem = Schema.Struct({
desiredOutcome: MeaningfulString,
summary: MeaningfulString,
title: MeaningfulString,
});
export const WorkCommandReason = Schema.Literals([
"InvalidInput",
"ProjectMismatch",
"TitleTooLong",
"ObjectiveTooLong",
]);
export type WorkCommandReason = typeof WorkCommandReason.Type;
export class WorkCommandError extends Schema.TaggedErrorClass<WorkCommandError>()(
"WorkCommandError",
{
message: Schema.String,
reason: WorkCommandReason,
}
) {}
export const workDraftFromSignal = Effect.fn("Work.draftFromSignal")(
function* workDraftFromSignal(input: unknown) {
const problem = yield* Schema.decodeUnknownEffect(SignalProblem)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Signal is missing a valid problem statement",
reason: "InvalidInput",
})
)
);
const title = problem.title.trim();
const objective = problem.desiredOutcome.trim();
if (title.length > 160) {
return yield* new WorkCommandError({
message: "Work title must be 160 characters or fewer",
reason: "TitleTooLong",
});
}
if (objective.length > 2000) {
return yield* new WorkCommandError({
message: "Work objective must be 2000 characters or fewer",
reason: "ObjectiveTooLong",
});
}
return { objective, title } satisfies WorkDraft;
}
);
const WorkIdentity = Schema.Struct({
organizationId: MeaningfulString,
projectId: MeaningfulString,
workId: MeaningfulString,
});
const SignalIdentity = Schema.Struct({
organizationId: MeaningfulString,
projectId: MeaningfulString,
signalId: MeaningfulString,
title: MeaningfulString,
});
const WorkAttachmentInput = Schema.Struct({
signal: SignalIdentity,
work: WorkIdentity,
});
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;
export const WorkEventDraft = Schema.Struct({
data: Schema.Struct({
signalId: MeaningfulString,
title: MeaningfulString,
}),
idempotencyKey: MeaningfulString,
kind: WorkEventKind,
});
export type WorkEventDraft = typeof WorkEventDraft.Type;
export const workProposedEventFromSignal = Effect.fn(
"Work.proposedEventFromSignal"
)(function* workProposedEventFromSignal(input: unknown) {
const signal = yield* Schema.decodeUnknownEffect(SignalIdentity)(input).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Signal identity is invalid",
reason: "InvalidInput",
})
)
);
return {
data: { signalId: signal.signalId, title: signal.title.trim() },
idempotencyKey: `proposed:${signal.signalId}`,
kind: "work.proposed",
} satisfies WorkEventDraft;
});
export const signalAttachedEvent = Effect.fn("Work.signalAttachedEvent")(
function* signalAttachedEvent(input: unknown) {
const attachment = yield* Schema.decodeUnknownEffect(WorkAttachmentInput)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Work attachment input is invalid",
reason: "InvalidInput",
})
)
);
if (
attachment.signal.organizationId !== attachment.work.organizationId ||
attachment.signal.projectId !== attachment.work.projectId
) {
return yield* new WorkCommandError({
message: "Signal and Work must belong to the same project",
reason: "ProjectMismatch",
});
}
return {
data: {
signalId: attachment.signal.signalId,
title: attachment.signal.title.trim(),
},
idempotencyKey: `signal:${attachment.signal.signalId}`,
kind: "signal.attached",
} satisfies WorkEventDraft;
}
);
const WorkNoticeInput = Schema.Array(
Schema.Struct({
_id: MeaningfulString,
events: Schema.Array(
Schema.Struct({
_id: MeaningfulString,
createdAt: Schema.Number,
kind: WorkEventKind,
})
),
signals: Schema.Array(
Schema.Struct({
sources: Schema.Array(
Schema.Struct({ messageId: MeaningfulString, rawText: Schema.String })
),
})
),
})
);
export interface WorkNotice {
readonly createdAt: number;
readonly eventId: string;
readonly sourceMessageIds: readonly string[];
readonly sourceTexts: readonly string[];
readonly workId: string;
}
const projectWorkNoticesEffect = Effect.fn("Work.projectNotices")(
function* projectWorkNoticesEffect(input: unknown) {
const works = yield* Schema.decodeUnknownEffect(WorkNoticeInput)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Work notice data is invalid",
reason: "InvalidInput",
})
)
);
const notices = works.flatMap((work) => {
const sourceMessageIds = work.signals.flatMap((signal) =>
signal.sources.map((source) => source.messageId)
);
const sourceTexts = work.signals.flatMap((signal) =>
signal.sources.map((source) => source.rawText)
);
return work.events
.filter((event) => event.kind === "work.proposed")
.map(
(event): WorkNotice => ({
createdAt: event.createdAt,
eventId: event._id,
sourceMessageIds,
sourceTexts,
workId: work._id,
})
);
});
return EffectArray.sortWith(
notices,
(notice) => notice.createdAt,
Order.Number
);
}
);
export const projectWorkNotices = (input: unknown): readonly WorkNotice[] =>
Effect.runSync(projectWorkNoticesEffect(input));