420 lines
12 KiB
TypeScript
420 lines
12 KiB
TypeScript
/**
|
|
* Mosaic Native Kanban — frozen Mechanical Coordinator contracts v1.
|
|
*
|
|
* The pure decision engine and persistence/orchestration service are separate.
|
|
* Neither surface can create scope, edit acceptance, waive gates, certify,
|
|
* merge, release a deployment, or close a provider issue.
|
|
*/
|
|
|
|
import type {
|
|
DeliberateWriteDenialV1,
|
|
InternalKanbanMutationContextV1,
|
|
KanbanEvaluationContextV1,
|
|
KanbanMutationFailureV1,
|
|
RetryableTransportErrorV1,
|
|
VersionConflictV1,
|
|
} from './health-state.v1.js';
|
|
|
|
export const COORDINATOR_CONTRACT_VERSION = '1.0.0' as const;
|
|
export type Uuid = string;
|
|
export type IsoTimestamp = string;
|
|
/** PostgreSQL bigint-safe decimal string; never a JavaScript number. */
|
|
export type FencingTokenV1 = string;
|
|
|
|
export const specialistRoles = [
|
|
'planning',
|
|
'enhance',
|
|
'coder',
|
|
'review',
|
|
'security-review',
|
|
'pr-monitor',
|
|
'certifier',
|
|
] as const;
|
|
export type SpecialistRole = (typeof specialistRoles)[number];
|
|
|
|
/** One vocabulary shared with task_assignment_state_v1 in the Drizzle schema. */
|
|
export const assignmentStates = [
|
|
'awaiting_approval',
|
|
'policy_pre_authorized',
|
|
'approved',
|
|
'rejected',
|
|
'leased',
|
|
'released',
|
|
'expired',
|
|
'superseded',
|
|
] as const;
|
|
export type AssignmentStateV1 = (typeof assignmentStates)[number];
|
|
|
|
export const readinessStates = [
|
|
'dependency-gated',
|
|
'schedule-gated',
|
|
'policy-gated',
|
|
'lease-available',
|
|
'leased',
|
|
'retry-delayed',
|
|
'exhausted',
|
|
'quarantined',
|
|
] as const;
|
|
export type ReadinessState = (typeof readinessStates)[number];
|
|
|
|
export interface RetryStateSnapshotV1 {
|
|
disposition: 'available' | 'retry_delayed' | 'quarantined' | 'exhausted';
|
|
attemptCount: number;
|
|
maxAttempts: number;
|
|
nextEligibleAt: IsoTimestamp | null;
|
|
idempotent: boolean;
|
|
terminalReason: string | null;
|
|
version: number;
|
|
}
|
|
|
|
export interface TaskEligibilitySnapshotV1 {
|
|
workspaceId: Uuid;
|
|
taskId: Uuid;
|
|
taskVersion: number;
|
|
projectId: Uuid;
|
|
projectActive: boolean;
|
|
missionId: Uuid | null;
|
|
missionActive: boolean;
|
|
status: 'ready';
|
|
priority: 'critical' | 'high' | 'medium' | 'low';
|
|
boardRank: string;
|
|
dueAt: IsoTimestamp | null;
|
|
notBeforeAt: IsoTimestamp | null;
|
|
createdAt: IsoTimestamp;
|
|
requiredRole: SpecialistRole;
|
|
requiredCapabilities: readonly string[];
|
|
blockingDependencies: readonly {
|
|
taskId: Uuid;
|
|
done: boolean;
|
|
completionConditionSatisfied: boolean;
|
|
}[];
|
|
releaseApproval: {
|
|
decisionId: Uuid;
|
|
approved: boolean;
|
|
policyRevision: string;
|
|
} | null;
|
|
activeLeaseId: Uuid | null;
|
|
retry: RetryStateSnapshotV1;
|
|
}
|
|
|
|
export interface AgentSessionSnapshotV1 {
|
|
workspaceId: Uuid;
|
|
agentId: Uuid;
|
|
sessionId: Uuid;
|
|
state: 'available' | 'busy';
|
|
roles: readonly SpecialistRole[];
|
|
capabilities: readonly string[];
|
|
capacity: number;
|
|
activeLeaseCount: number;
|
|
heartbeatAt: IsoTimestamp;
|
|
}
|
|
|
|
export interface EligibilityExplanationV1 {
|
|
taskId: Uuid;
|
|
eligible: boolean;
|
|
readiness: ReadinessState;
|
|
reasons: readonly {
|
|
gate:
|
|
| 'status'
|
|
| 'project'
|
|
| 'mission'
|
|
| 'dependency'
|
|
| 'schedule'
|
|
| 'retry'
|
|
| 'approval'
|
|
| 'lease'
|
|
| 'capability'
|
|
| 'capacity'
|
|
| 'health';
|
|
satisfied: boolean;
|
|
code: string;
|
|
detail: string;
|
|
}[];
|
|
policyRevision: string;
|
|
evaluatedAt: IsoTimestamp;
|
|
}
|
|
|
|
export interface AssignmentProposalDecisionV1 {
|
|
workspaceId: Uuid;
|
|
taskId: Uuid;
|
|
taskVersion: number;
|
|
targetAgentId: Uuid;
|
|
targetSessionId: Uuid;
|
|
specialistRole: SpecialistRole;
|
|
initialState: 'awaiting_approval' | 'policy_pre_authorized';
|
|
policyRevision: string;
|
|
explanation: EligibilityExplanationV1;
|
|
expiresAt: IsoTimestamp;
|
|
}
|
|
|
|
export interface AssignmentCycleSnapshotV1 {
|
|
context: KanbanEvaluationContextV1;
|
|
tasks: readonly TaskEligibilitySnapshotV1[];
|
|
sessions: readonly AgentSessionSnapshotV1[];
|
|
workspaceFairness: Readonly<Record<Uuid, number>>;
|
|
limit: number;
|
|
}
|
|
|
|
export interface AssignmentCycleDecisionV1 {
|
|
evaluatedTaskCount: number;
|
|
proposals: readonly AssignmentProposalDecisionV1[];
|
|
explanations: readonly EligibilityExplanationV1[];
|
|
}
|
|
|
|
export interface LeaseExpirySnapshotV1 {
|
|
workspaceId: Uuid;
|
|
taskId: Uuid;
|
|
taskVersion: number;
|
|
leaseId: Uuid;
|
|
assignmentId: Uuid;
|
|
sessionId: Uuid;
|
|
fencingToken: FencingTokenV1;
|
|
state: 'pending_ack' | 'active';
|
|
acknowledgeBy: IsoTimestamp;
|
|
expiresAt: IsoTimestamp;
|
|
lastHeartbeatAt: IsoTimestamp | null;
|
|
retry: RetryStateSnapshotV1;
|
|
}
|
|
|
|
export interface LeaseExpiryDecisionV1 {
|
|
leaseId: Uuid;
|
|
action: 'retain' | 'release' | 'retry' | 'quarantine' | 'exhaust';
|
|
reason: string;
|
|
nextEligibleAt: IsoTimestamp | null;
|
|
}
|
|
|
|
/** Pure package owned by KBN-200. It receives complete immutable snapshots. */
|
|
export interface MechanicalCoordinatorDecisionEngineV1 {
|
|
evaluateAssignmentCycle(snapshot: AssignmentCycleSnapshotV1): AssignmentCycleDecisionV1;
|
|
explainEligibility(
|
|
context: KanbanEvaluationContextV1,
|
|
task: TaskEligibilitySnapshotV1,
|
|
sessions: readonly AgentSessionSnapshotV1[],
|
|
): EligibilityExplanationV1;
|
|
decideLeaseExpiry(
|
|
context: KanbanEvaluationContextV1,
|
|
lease: LeaseExpirySnapshotV1,
|
|
): LeaseExpiryDecisionV1;
|
|
}
|
|
|
|
export interface PersistedAssignmentV1 {
|
|
assignmentId: Uuid;
|
|
workspaceId: Uuid;
|
|
taskId: Uuid;
|
|
taskVersion: number;
|
|
targetAgentId: Uuid;
|
|
targetSessionId: Uuid;
|
|
specialistRole: SpecialistRole;
|
|
state: AssignmentStateV1;
|
|
policyRevision: string;
|
|
proposedBy: { kind: 'user' | 'agent'; id: Uuid };
|
|
reason: string;
|
|
createdAt: IsoTimestamp;
|
|
expiresAt: IsoTimestamp;
|
|
}
|
|
|
|
export interface TaskLeaseV1 {
|
|
leaseId: Uuid;
|
|
workspaceId: Uuid;
|
|
taskId: Uuid;
|
|
taskVersion: number;
|
|
assignmentId: Uuid;
|
|
agentId: Uuid;
|
|
sessionId: Uuid;
|
|
state: 'pending_ack' | 'active';
|
|
fencingToken: FencingTokenV1;
|
|
attempt: number;
|
|
acquiredAt: IsoTimestamp;
|
|
acknowledgeBy: IsoTimestamp;
|
|
lastHeartbeatAt: IsoTimestamp | null;
|
|
expiresAt: IsoTimestamp;
|
|
}
|
|
|
|
interface ServiceCommandBaseV1 {
|
|
context: InternalKanbanMutationContextV1;
|
|
taskId: Uuid;
|
|
expectedTaskVersion: number;
|
|
}
|
|
|
|
export interface AcquireApprovedLeaseCommandV1 extends ServiceCommandBaseV1 {
|
|
assignmentId: Uuid;
|
|
approvalDecisionId: Uuid;
|
|
targetSessionId: Uuid;
|
|
leaseTtlSeconds: number;
|
|
}
|
|
|
|
export interface LeaseCommandV1 extends ServiceCommandBaseV1 {
|
|
leaseId: Uuid;
|
|
sessionId: Uuid;
|
|
fencingToken: FencingTokenV1;
|
|
}
|
|
|
|
export interface HeartbeatLeaseCommandV1 extends LeaseCommandV1 {
|
|
extendSeconds: number;
|
|
}
|
|
|
|
export interface CheckpointCommandV1 extends LeaseCommandV1 {
|
|
sequence: number;
|
|
resumableSummary: string;
|
|
artifactIds: readonly Uuid[];
|
|
contextUsagePercent: number;
|
|
}
|
|
|
|
export interface SubmitForReviewCommandV1 extends LeaseCommandV1 {
|
|
artifactIds: readonly Uuid[];
|
|
summary: string;
|
|
}
|
|
|
|
export interface ReleaseLeaseCommandV1 extends LeaseCommandV1 {
|
|
reason:
|
|
| 'worker_requested'
|
|
| 'ack_timeout'
|
|
| 'heartbeat_timeout'
|
|
| 'task_submitted'
|
|
| 'policy_revoked'
|
|
| 'shutdown';
|
|
}
|
|
|
|
export interface AssignmentCycleCommandV1 {
|
|
context: InternalKanbanMutationContextV1;
|
|
limit: number;
|
|
}
|
|
|
|
export interface ExpirySweepCommandV1 {
|
|
context: InternalKanbanMutationContextV1;
|
|
limit: number;
|
|
}
|
|
|
|
export interface RecoverCoordinatorCommandV1 {
|
|
context: InternalKanbanMutationContextV1;
|
|
}
|
|
|
|
interface CoordinatorRejectionBaseV1 {
|
|
kind: 'coordinator_rejection';
|
|
retryable: false;
|
|
requestOutcome: 'not_applied';
|
|
correlationId: Uuid;
|
|
idempotencyKey: string;
|
|
message: string;
|
|
}
|
|
|
|
export type CoordinatorPolicyRejectionV1 =
|
|
| (CoordinatorRejectionBaseV1 & { code: 'WORKSPACE_MISMATCH' })
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'TASK_NOT_ELIGIBLE';
|
|
explanation: EligibilityExplanationV1;
|
|
})
|
|
| (CoordinatorRejectionBaseV1 & { code: 'APPROVAL_REQUIRED' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'APPROVAL_STALE' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'ASSIGNMENT_STALE' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'ASSIGNMENT_TARGET_MISMATCH' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'POLICY_REVISION_MISMATCH' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'ARTIFACT_WORKSPACE_MISMATCH' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'LEASE_ALREADY_ACTIVE' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'LEASE_NOT_FOUND' })
|
|
| (CoordinatorRejectionBaseV1 & { code: 'LEASE_NOT_ACTIVE' })
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'ACK_DEADLINE_EXPIRED';
|
|
expiredAt: IsoTimestamp;
|
|
})
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'FENCING_TOKEN_STALE';
|
|
currentFencingToken: FencingTokenV1;
|
|
})
|
|
| (CoordinatorRejectionBaseV1 & { code: 'SESSION_MISMATCH' })
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'HEARTBEAT_EXPIRED';
|
|
expiredAt: IsoTimestamp;
|
|
})
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'CHECKPOINT_SEQUENCE_CONFLICT';
|
|
currentSequence: number;
|
|
})
|
|
| (CoordinatorRejectionBaseV1 & { code: 'RETRY_EXHAUSTED' })
|
|
| (CoordinatorRejectionBaseV1 & {
|
|
code: 'NON_IDEMPOTENT_RETRY_REQUIRES_ORCHESTRATOR';
|
|
});
|
|
|
|
/** Explicit mapping to the Gateway mutation failure union; no arbitrary booleans. */
|
|
export type CoordinatorFailureV1 =
|
|
| DeliberateWriteDenialV1
|
|
| VersionConflictV1
|
|
| RetryableTransportErrorV1
|
|
| CoordinatorPolicyRejectionV1;
|
|
|
|
export interface CoordinatorSuccessV1<T> {
|
|
ok: true;
|
|
value: T;
|
|
correlationId: Uuid;
|
|
}
|
|
export interface CoordinatorFailureResultV1 {
|
|
ok: false;
|
|
failure: CoordinatorFailureV1;
|
|
}
|
|
export type CoordinatorResultV1<T> = CoordinatorSuccessV1<T> | CoordinatorFailureResultV1;
|
|
|
|
export interface ExpirySweepResultV1 {
|
|
examined: number;
|
|
released: readonly Uuid[];
|
|
retryScheduled: readonly Uuid[];
|
|
quarantined: readonly Uuid[];
|
|
exhausted: readonly Uuid[];
|
|
}
|
|
|
|
export interface RestartRecoveryResultV1 {
|
|
activeLeaseIds: readonly Uuid[];
|
|
expiredLeaseIds: readonly Uuid[];
|
|
pendingAssignmentIds: readonly Uuid[];
|
|
pendingOutboxEventIds: readonly Uuid[];
|
|
}
|
|
|
|
/** Persistence/Gateway adapter owned by KBN-210. */
|
|
export interface MechanicalCoordinatorServicePortV1 {
|
|
/** Loads immutable snapshots, invokes pure engine, and persists proposals atomically. */
|
|
runAssignmentCycle(
|
|
command: AssignmentCycleCommandV1,
|
|
): Promise<CoordinatorResultV1<{ assignments: readonly PersistedAssignmentV1[] }>>;
|
|
|
|
/** Query path loads by ID; public health observation cannot authorize mutation. */
|
|
getEligibilityExplanation(
|
|
context: KanbanEvaluationContextV1,
|
|
taskId: Uuid,
|
|
): Promise<CoordinatorResultV1<EligibilityExplanationV1>>;
|
|
|
|
/**
|
|
* Accepts IDs only. Implementation reloads and locks assignment + approval +
|
|
* task + target session in PostgreSQL, then verifies workspace, task version,
|
|
* target agent/session, state, expiry, policy revision, and current approval.
|
|
*/
|
|
acquireApprovedLease(
|
|
command: AcquireApprovedLeaseCommandV1,
|
|
): Promise<CoordinatorResultV1<TaskLeaseV1>>;
|
|
|
|
acknowledgeLease(command: LeaseCommandV1): Promise<CoordinatorResultV1<TaskLeaseV1>>;
|
|
heartbeatLease(command: HeartbeatLeaseCommandV1): Promise<CoordinatorResultV1<TaskLeaseV1>>;
|
|
appendCheckpoint(
|
|
command: CheckpointCommandV1,
|
|
): Promise<CoordinatorResultV1<{ checkpointId: Uuid }>>;
|
|
submitForReview(
|
|
command: SubmitForReviewCommandV1,
|
|
): Promise<CoordinatorResultV1<{ taskVersion: number; status: 'in_review' }>>;
|
|
releaseLease(command: ReleaseLeaseCommandV1): Promise<CoordinatorResultV1<{ released: true }>>;
|
|
expireAndRecover(
|
|
command: ExpirySweepCommandV1,
|
|
): Promise<CoordinatorResultV1<ExpirySweepResultV1>>;
|
|
recoverFromPostgres(
|
|
command: RecoverCoordinatorCommandV1,
|
|
): Promise<CoordinatorResultV1<RestartRecoveryResultV1>>;
|
|
}
|
|
|
|
/** Compile-time mapping guarantee: Coordinator Gateway failures are Kanban failures or exact policy rejections. */
|
|
export function isKanbanMutationFailureV1(
|
|
failure: CoordinatorFailureV1,
|
|
): failure is KanbanMutationFailureV1 {
|
|
return (
|
|
failure.kind === 'deliberate_fail_closed_denial' ||
|
|
failure.kind === 'retryable_transport_error' ||
|
|
failure.kind === 'version_conflict'
|
|
);
|
|
}
|