32 KiB
Native Kanban/SOT — Remediated Shared Contract v1
Status: INDEPENDENT REVIEW GO; freezes as v1 when issue #751 canon merges to main
Version: 1.0.0-rc.3
Date: 2026-07-14
Change authority: Mosaic control plane/Jason only
1. Authority
Concrete contracts are the four contracts/*.v1.ts files. PostgreSQL/current-main Drizzle is the sole writable SOT. Public health, Valkey, files, exports, providers, browser state, and outage notes cannot authorize/reconstruct writes. Mechanical Coordinator is non-LLM with no scope/gate/certification/merge authority. Certifier is final independent gate with no merge authority. No feature lane starts until this canon merges and the KBN-010/KBN-105 prerequisites are satisfied.
2. Health proof and exact failures
KanbanHealthResponseV1 is a discriminated union:
| State | read | write | Capability |
|---|---|---|---|
healthy |
true | true | reads; public state still cannot authorize mutation |
read-only-degraded |
true | false | reads only |
write-unavailable |
false | false | diagnostics only |
Every response has checkedAt, validUntil, policyRevision; contradictory booleans fail validation.
For a mutation, Gateway opens the PostgreSQL transaction, executes the live write probe on that transaction/connection, mints the internal branded PostgresWriteHealthProofV1, and revalidates time/policy/transaction identity immediately before mutation. Public REST/MCP/CLI DTOs never accept health/proof fields. Valkey/caller assertions cannot mint proof. Pure Coordinator takes KanbanEvaluationContextV1; persistence takes InternalKanbanMutationContextV1 or probes internally.
| Case | HTTP | Frozen result | Retry |
|---|---|---|---|
| degraded write | 503 | KANBAN_WRITE_HEALTH_UNPROVEN, read-only-degraded, not_applied |
false |
| write unavailable | 503 | KANBAN_WRITE_UNAVAILABLE, write-unavailable, not_applied |
false |
| version conflict | 409 | AGGREGATE_VERSION_CONFLICT, actual version, not_applied |
false |
| timeout/unreachable | timeout/502/504 | retryable_transport_error, unknown |
same idempotency key |
| stale fence/session/approval | coordinator rejection union | not_applied |
false |
Required negatives: contradictory state, expired/policy-mismatched/wrong-transaction proof, Valkey-only health, forged healthy, and exhaustive non-cross-mapping of 503 vs 502/504/timeout vs 409.
3. Canonical schema invariants
Complete declaration: contracts/kanban-schema.v1.ts.
- Tables: tenant/identity (
workspaces, members, teams/members, agents/sessions); planning (projects,milestones, current-milestone join,missions, mission-milestones,tasks, normalized tags, dependencies); orchestration (task_assignments, durable execution state, leases, checkpoints/evidence); governance (change_proposals, immutable artifacts/evidence, events, approvals, outbox, external links). - Task statuses:
backlog | ready | in_progress | blocked | in_review | done | cancelled. - Assignment states everywhere:
awaiting_approval | policy_pre_authorized | approved | rejected | leased | released | expired | superseded. - Specialist roles everywhere:
planning | enhance | coder | review | security-review | pr-monitor | certifier. - Owner uses exactly-one user/team; assignment principal exactly-one user/team/agent; users require active membership; agent/session and all evidence are workspace-bound.
- Task→mission/milestone/parent, mission→milestone, and project→current-milestone are project-congruent composite relations.
- Dependency identity is workspace+predecessor+successor independent of type.
- Approval evidence and checkpoint evidence are workspace-scoped joins to immutable artifacts, never JSON ID arrays.
- Proposal audit links are composite relations:
(workspace_id, submitted_audit_event_id)and(workspace_id, accepted_command_audit_event_id)referencetask_events(workspace_id, id)with RESTRICT deletion. - Assignment is persisted with task/version, exact target/session, expiry/state/policy/proposer/reason. Approval relates to assignment. Lease acquisition accepts IDs, then reloads/locks and validates every relation.
tasks.fencing_counteris bigint; locked atomic increment/RETURNING creates a decimal-string lease token. Lease/checkpoint composites bind exact workspace+task+assignment/session+fence.task_execution_statesdurably records retry/quarantine/exhaustion.- Tags are normalized; legacy
tasks.tagsremains through N-1. Archive is explicit actor/reason/time and does not change lifecycle. - Canonical parents use RESTRICT. Events/checkpoints/artifacts/evidence are INSERT/SELECT-only for application roles. Normal flow archives/cancels; purge is audited break-glass retention work.
4. Outage proposal contract
change_proposals stores workspace, active-member proposer, source-note digest, target/version, typed command/payload, idempotency, lifecycle, decision actor/reason/time, proposal version, and submit/accepted event IDs. Both event IDs are workspace-aware composite foreign keys to task_events(workspace_id, id); a bare UUID is never sufficient.
Submission preallocates the proposal ID. One transaction inserts change_proposal.submitted with the proposal workspace, aggregate_type='change_proposal', aggregate_id=<new proposal ID>, previous_version=NULL, and new_version=1, then inserts the proposal referencing that event. Missing, foreign-workspace, wrong-type, or unrelated-proposal events abort the transaction.
Submit/list/get/accept/reject are explicit Gateway commands. Pending/rejected proposals are inert: no scheduling, dependency/gate satisfaction, or direct target mutation. Acceptance locks proposal+target, obtains fresh transaction-local proof, verifies pending/expected version, invokes the normal command handler, and atomically stores the emitted normal-command event ID. That event must share the proposal workspace, match target_aggregate_type and target_aggregate_id, use causation_id=submitted_audit_event_id, and carry payload.changeProposalId=<locked proposal ID>. Missing, foreign-workspace, unrelated-target, unrelated-proposal, or unrelated-command events abort acceptance.
5. Concrete current-main N-1 migration delta
Inspected: origin/main:packages/db/src/schema.ts at e72388b2cbfe400842fe940fa6cabf984ed43711 (2026-07-13). It has global teams/no workspace keys, legacy project/mission/task statuses, nullable task project/mission, tasks.assignee/tags/due_date, mission JSON/config, duplicated mission_tasks.status, legacy agent fields, and separate fleet backlog claims.
Legacy columns remain declared in unified schema.ts for expand + full N-1/rollback window. Generation must not infer early drops.
5.1 Ordered phases
- Pre-expand: N-1 patch stops
mission_tasks.statusas write source; inventory writers; backup/checksum. - Expand: add enums/tables and nullable-first columns; retain legacy declarations/uniques; emit no v1-only status.
- Backfill: bootstrap workspace; bounded idempotent cursor/checksum batches; quarantine ambiguous rows.
- Validate: no null tenant, cross-project link, ambiguous owner; status/tag/date/config retention; then constraints/NOT NULL.
- Compatibility: N-1 reads legacy; same-DB transaction mirrors only unavoidable fields; never file/Valkey dual write.
- Switch: stop N-1 writers; Gateway sole command boundary; enable canonical statuses.
- Contract release: later release after rollback/N-1; remove compatibility/global uniques/legacy fields.
5.2 New audit/proposal DDL order
KBN-100 migration DDL must execute in this order:
- create
task_eventsand its unique(workspace_id, id)key; - create
change_proposalswith nullable acceptance-event ID and required submission-event ID; - add
change_proposals_workspace_submitted_event_fkfrom(workspace_id, submitted_audit_event_id)totask_events(workspace_id, id)withON DELETE RESTRICT; - add
change_proposals_workspace_accepted_command_event_fkfrom(workspace_id, accepted_command_audit_event_id)to the same composite key withON DELETE RESTRICT; - install application-role immutability privileges and same-transaction semantic validation before enabling proposal commands.
The submission transaction inserts the event first using a preallocated proposal UUID, then the proposal. Acceptance inserts the normal command event before updating the locked proposal. Neither FK is omitted or replaced by a bare UUID/index check.
5.3 Field map
| Current | Expand/backfill | N-1 compatibility | Switch/contract |
|---|---|---|---|
global teams, team_members |
add workspace nullable; bootstrap; validate active owners | retain global slug/FKs | workspace composites; global unique contracts later |
projects.status |
add canonical_status; map active/paused/completed/archived |
mirror representable values; no planning |
canonical authority; legacy contracts later |
project owner_id/team_id/owner_type |
add exact accountable user/team; deterministic map or quarantine | preserve old reads and compare drift | canonical exact-one; remove legacy after parity |
| current milestone | create milestones then join table (no circular DDL) | absent to N-1 | join is authority |
nullable missions.project_id |
derive workspace/project; null/orphan exception, never guess | keep nullable legacy read | canonical required; validate/set NOT NULL later |
mission description |
add objective; preserve description; reviewed nonblank mapping | N-1 description | objective authority; retain until signed review |
missions.status |
add canonical; planning→draft, active/paused/completed/failed same | no new-only statuses emitted | canonical authority |
mission milestones JSON |
normalize with source digest; preserve malformed/original | N-1 reads JSON; no reverse sync | normalized authority; JSON removed after checksum sign-off |
| mission config/metadata/phase/user | retain all; map known typed policy only | all remain declared | remove only by signed consumer inventory |
nullable tasks.project_id |
derive explicit/mission project; orphan quarantine | retain nullable read/write during compatibility | canonical required; NOT NULL later |
tasks.mission_id |
add project-congruent composite | old relation readable | composite authority |
tasks.status |
canonical: not-started→backlog, in-progress→in_progress, others same | no ready/in_review emission | canonical authority |
tasks.assignee |
deterministic active user/team/agent assignment; raw value preserved if ambiguous | mirror text only if unambiguous | canonical owner/assignment; remove after no-loss sign-off |
tasks.tags JSON |
normalize trim/case/dedupe with original digest | transactionally mirror normalized rows | normalized authority; JSON later removed |
tasks.due_date |
copy exactly to due_at |
mirror | due_at authority; legacy later |
| task common fields | preserve metadata byte-for-byte; add criteria/rank/retry/archive/version/fence | old reads valid | new fields canonical |
mission_tasks.status |
keep; prohibit as write source; linked status ignored; unlinked becomes task or reject | read-only compatibility value | membership uses task mission; status dropped after no readers |
| mission-task notes/PR/user | map to metadata/artifact/event/link/attribution; preserve | read-only | remove after parity |
agents.status |
add workspace/lifecycle/runtime/roles; status remains presence | retain all legacy fields | lifecycle/roles authority; status may remain telemetry |
| agent project/owner/prompt/tools/skills/config | preserve; validate tenant; derive typed capabilities without loss | N-1 reads | removal only by separate inventory |
fleet backlog |
map to designated-project tasks; edges; claimed rows quarantine | freeze claims before switch; read-only compare | task/lease authority; retire after stabilization |
5.4 Required migration tests
Empty DB; exact production-shape snapshot; crash/resume; rollback before switch; N-1 startup/read/write; workspace/member negatives; status-shadow/no premature new status; mission_tasks.status write prohibition; tags/assignee/date/mission JSON/config/description/agent checksum; project congruence/current-milestone order; backlog freeze/no dispatch; and proof legacy declarations persist until contract release.
Proposal-specific negatives must attempt: missing submission event, foreign-workspace submission event, foreign-workspace acceptance event, same-workspace event for another proposal, event for another target aggregate, and unrelated normal-command event. Every attempt must fail atomically with no accepted proposal and no target mutation.
6. Ownership and Coordinator split
coder2 solely owns packages/db/src/schema.ts, packages/db/drizzle/**, journal/metadata, and migration tests. No other lane generates migrations. Expand is additive; no drop/rename/narrow; constraints validate before NOT NULL; compatibility is same-DB only; contract is later.
KBN-200/coder4 owns pure MechanicalCoordinatorDecisionEngineV1: complete immutable snapshots in, deterministic eligibility/proposal/retry decisions out; no ID loading, SQL, Gateway, Valkey, proof, persistence, restart I/O, or LLM.
KBN-210/coder3 owns MechanicalCoordinatorServicePortV1: ID loading, locks, fresh proof, assignment/approval persistence, atomic fencing, lease/checkpoint/outbox, Valkey wakes, durable retry/quarantine, and recoverFromPostgres. Cycle: load snapshots → pure decision → persist assignment → authoritative approval/policy → acquire by IDs/locks → increment fence → lease → ack/heartbeat/checkpoint → submit to review or durable retry/quarantine. No completion/certification/merge method exists.
7. Exact Gateway/DTO freeze for KBN-105
7.1 Common wire rules
Base is /api/v1/workspaces/:workspaceId. Mutations require header Idempotency-Key (1–128 chars). Existing-aggregate mutations also require If-Match-Version (positive integer); create and privileged assignment-cycle requests are the only exceptions, while proposal submission carries expectedTargetVersion in its body. Body workspace fields are forbidden. Tenant denial follows one 404/403 policy without foreign existence detail.
interface SuccessEnvelopeV1<T> {
contractVersion: '1.0.0';
data: T;
aggregateRevision: string;
correlationId: string;
}
interface ListEnvelopeV1<T> extends SuccessEnvelopeV1<T[]> {
page: { cursor: string | null; nextCursor: string | null; limit: number };
}
Errors are the exact health/transport/version unions in §2 plus validation/auth/not-found. Public DTOs never expose/accept internal write proof.
7.2 Exact route registry
| Method/path | Request body/query | Success data |
|---|---|---|
GET /kanban-health |
none | KanbanHealthResponseV1 |
GET /projects |
status,ownerUserId,ownerTeamId,cursor,limit |
project list |
POST /projects |
name,key,description,status,priority,ownerUserId XOR ownerTeamId,metadata |
project |
GET /projects/:projectId |
none | project |
PATCH /projects/:projectId |
editable create fields + expected header | project |
POST /projects/:projectId/archive |
reason |
project |
GET /tasks |
projectId,missionId,milestoneId,status,priority,ownerUserId,ownerTeamId,specialistRole,tag,dueState,archived,cursor,limit |
task summary list |
POST /tasks |
projectId,missionId?,milestoneId?,parentTaskId?,title,description?,acceptanceCriteria[],status,priority,rank,ownerUserId XOR ownerTeamId,specialistRole?,dueAt?,notBeforeAt?,estimateMinutes?,retryPolicy?,tagIds[],metadata |
task detail |
GET /tasks/:taskId |
none | task detail including readiness/dependencies/assignment/lease/events |
PATCH /tasks/:taskId |
editable non-transition fields | task detail |
POST /tasks/:taskId/transition |
toStatus,reason? |
task detail |
POST /tasks/:taskId/move |
toStatus?,beforeTaskId?,afterTaskId? |
task detail with persisted rank |
POST /tasks/:taskId/archive |
reason |
task detail |
PUT /tasks/:taskId/tags |
tagIds[] |
task detail |
POST /tasks/:taskId/dependencies |
predecessorTaskId,type |
dependency |
DELETE /tasks/:taskId/dependencies/:predecessorTaskId |
no body | deleted dependency ID |
GET /tasks/:taskId/events |
cursor,limit |
event list |
GET /tags |
query,cursor,limit |
tag list |
POST /tags |
name,color? |
tag |
GET /change-proposals |
state,targetType,targetId,cursor,limit |
proposal list |
POST /change-proposals |
sourceNoteDigest,targetType,targetId,expectedTargetVersion,commandType,commandPayload |
inert proposal |
GET /change-proposals/:proposalId |
none | proposal |
POST /change-proposals/:proposalId/accept |
reason |
proposal + normal command result |
POST /change-proposals/:proposalId/reject |
reason |
proposal |
GET /coordinator/eligibility |
projectId?,missionId?,cursor,limit |
EligibilityDecisionV1[] |
POST /coordinator/assignment-cycles |
limit |
assignment proposals; privileged internal |
POST /coordinator/assignments/:assignmentId/approve |
decision,reason,policyRevision,artifactIds[] |
approval decision |
POST /coordinator/leases/acquire |
taskId,assignmentId,approvalDecisionId,targetSessionId,leaseTtlSeconds |
lease with decimal-string fence |
POST /coordinator/leases/:leaseId/ack |
taskId,sessionId,fencingToken |
lease |
POST /coordinator/leases/:leaseId/heartbeat |
taskId,sessionId,fencingToken,extendSeconds |
lease |
POST /coordinator/leases/:leaseId/checkpoints |
taskId,sessionId,fencingToken,sequence,resumableSummary,artifactIds[],contextUsagePercent |
checkpoint |
POST /coordinator/leases/:leaseId/submit-review |
taskId,sessionId,fencingToken,artifactIds[],summary |
task in in_review |
All Coordinator mutations except human approval are service-identity-only. Generic task PATCH cannot perform claim/heartbeat/checkpoint/review/certification/completion shortcuts. Completion after certification uses a separately gated lifecycle command owned by the Portfolio/Sub-Orchestrator flow, not the Coordinator.
7.3 DTO invariants
Task summary/detail use exact schema vocabularies, owner union, version: number, fencingCounter: string, explicit archivedAt/by/reason, normalized tags, computed readiness, and separate assignment/lease. Assignment DTO includes one persisted ID, task/version, exact principal/agent/session, role, state, expiry, policy, proposer/reason. Lease/checkpoint DTOs serialize every fence as decimal string. Proposal DTO exposes no hidden write authority.
7.4 MCP ownership and mapping
coder3 exclusively owns:
apps/gateway/src/mcp/mcp.dto.tsmcp.controller.tsmcp.service.tsmcp.module.tsmcp.tokens.tsmcp.service.spec.ts
MCP tools are thin maps: mosaic_projects_{list,get,create,update,archive}, mosaic_tasks_{list,get,create,update,transition,move,archive,set_tags,add_dependency,remove_dependency}, and mosaic_change_proposals_{list,get,submit,accept,reject} to the exact routes above. coder4 owns CLI/projection clients only and must not edit Gateway MCP files.
KBN-105 publishes route+DTO fixture digest before KBN-110/120/130. Every web/CLI/MCP call must match this registry and the generated client.
8. Recovery contract and bounded delivery slice
Runtime must invoke normative validateRecoveryPostureV1; JSON Schema alone is insufficient. It rejects unknown fields, PITR/WAL mismatch, RPO better than mechanism, unsafe storage, and weakened High-assurance. High-assurance is RPO 15m/RTO 4h, WAL ≤5m, PITR ≥35d, base ≤24h, restore test ≤30d, break-glass ≤90d, encrypted separate-failure-domain storage.
KBN-115/coder2 owns packages/config/src/recovery-posture.ts, tests, and recovery runbook. It wires parser/refinement, override audit, mechanism assertions, restore test, and break-glass evidence. Any deployment manifest is separately enumerated and Mos-serialized. Recovery config has no SOT/gate/Coordinator authority fields.
9. Integration, security, and hold
Required release evidence includes empty/prod/partial/rollback/N-1 migration tests; cross-workspace and same-workspace wrong-project negatives; active-membership owners/principals; proposal inertness/normal acceptance; exact failure mapping; concurrent monotonic bigint fences; relational lease/checkpoint/evidence mismatch; immutability privileges/RESTRICT; recovery validation/mechanism evidence; endpoint registry alignment; accessible web journeys; author≠reviewer; mandatory SecReview; final Certifier pass/no merge authority.
The build hold remains active until independent re-review reports GO for KCR-001–016. Mos alone releases waves and serializes integration roots.