# 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)` reference `task_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_counter` is bigint; locked atomic increment/RETURNING creates a decimal-string lease token. Lease/checkpoint composites bind exact workspace+task+assignment/session+fence. - `task_execution_states` durably records retry/quarantine/exhaustion. - Tags are normalized; legacy `tasks.tags` remains 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=`, `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=`. 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 1. **Pre-expand:** N-1 patch stops `mission_tasks.status` as write source; inventory writers; backup/checksum. 2. **Expand:** add enums/tables and nullable-first columns; retain legacy declarations/uniques; emit no v1-only status. 3. **Backfill:** bootstrap workspace; bounded idempotent cursor/checksum batches; quarantine ambiguous rows. 4. **Validate:** no null tenant, cross-project link, ambiguous owner; status/tag/date/config retention; then constraints/NOT NULL. 5. **Compatibility:** N-1 reads legacy; same-DB transaction mirrors only unavoidable fields; never file/Valkey dual write. 6. **Switch:** stop N-1 writers; Gateway sole command boundary; enable canonical statuses. 7. **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: 1. create `task_events` and its unique `(workspace_id, id)` key; 2. create `change_proposals` with nullable acceptance-event ID and required submission-event ID; 3. add `change_proposals_workspace_submitted_event_fk` from `(workspace_id, submitted_audit_event_id)` to `task_events(workspace_id, id)` with `ON DELETE RESTRICT`; 4. add `change_proposals_workspace_accepted_command_event_fk` from `(workspace_id, accepted_command_audit_event_id)` to the same composite key with `ON DELETE RESTRICT`; 5. 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. ```ts interface SuccessEnvelopeV1 { contractVersion: '1.0.0'; data: T; aggregateRevision: string; correlationId: string; } interface ListEnvelopeV1 extends SuccessEnvelopeV1 { 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.ts` - `mcp.controller.ts` - `mcp.service.ts` - `mcp.module.ts` - `mcp.tokens.ts` - `mcp.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.