Status: Phase 0. Scope: packages/core (contract) + packages/storage (store and migration).
The Work Board is a user-owned, local-first surface for deferred work. It is not an execution authority:
- no
task_*tools, notask.ledger.query, and noworkflow_task_ledger_*reads/writes; - no model-visible tools or automatic prompt injection;
- no Goal, AgentRun, RuntimeEvent, or Agent Graph writes;
- execution state is projected at read time, never copied into board storage.
interface WorkBoardItem {
schemaVersion: 1;
id: string; // durable UUID, never rewritten
revision: number; // monotonic per item, incremented on every effective mutation
scope: WorkBoardScope; // inbox | { kind: 'project'; projectId }
title: string;
notes?: string;
state: 'todo' | 'in_progress' | 'done';
creator: { kind: 'user' } | { kind: 'agent_suggestion'; confirmedAt: number };
provenance: WorkBoardProvenance;
createdAt: number;
updatedAt: number;
// Active items never carry `archivedAt`; archived items always carry it:
// { archived: false } | { archived: true; archivedAt: number }
archived: boolean;
archivedAt?: number;
}linkedSessions is deferred to Phase 3: Phase 0 has no consumer and no mutation
path that writes it, so the Phase 0 contract rejects the field in create input
and in stored records. It will be added together with the canonical continuity
adapter when "start as task" lands.
Inbox is a scope, not a status. State transitions are user-confirmed only:
todo <-> in_progress
todo -> done
in_progress -> done
done -> todo | in_progress
done is user intent. It is never derived from a Session or AgentRun outcome.
Provenance is a discriminated union:
manual;main_conversation(sessionId,messageId, optionalrunId/turnId,capturedAt, boundedexcerpt;parentSessionIdis rejected);side_conversation(same fields plus requiredparentSessionId).
The bounded excerpt is snapshotted at capture time so a side-chat item survives
the temporary fork's deletion. Typed refs are best-effort links, not hard
dependencies.
Agent suggestions require confirmedAt and are only created by explicit user
action or an unambiguous instruction. The board never writes itself.
WorkBoardStore in packages/storage owns the workflow_work_board_items table
in runtime.sqlite (operational-state database), added by the additive workflow
schema 8 → 9 migration:
CREATE TABLE IF NOT EXISTS workflow_work_board_items (
item_id TEXT PRIMARY KEY,
revision INTEGER NOT NULL CHECK (revision >= 1),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('inbox', 'project')),
project_id TEXT,
archived INTEGER NOT NULL CHECK (archived IN (0, 1)),
record_json TEXT NOT NULL,
CHECK (
(scope_kind = 'inbox' AND project_id IS NULL)
OR
(scope_kind = 'project' AND project_id IS NOT NULL)
)
);Writes:
- mutations are semantic patches (
title/notes/scope/state), never full-record replacement. In this patch contract,undefined(or an omitted field) means "not provided" and keeps the stored value;notes: null, an empty string, or a whitespace-only string is the explicit clear signal. This is a contract decision, not a JavaScript object-equality rule; - all mutations are serialized on one write queue and committed in a transaction;
- every effective mutation increments
revision; expectedRevisionprovides optimistic concurrency (CAS);- mutation timestamps are clamped to the stored
updatedAt, so keyset ordering stays monotonic even if the wall clock moves backwards; - permanent deletion requires an archived item;
- reads reject disagreement between the indexed columns and
record_json.scopeascorrupt_record.
There is no total item cap. List/query size is bounded by pagination (default 50,
max 100) with an opaque keyset cursor. Cursors are bound to the normalized
scope / includeArchived filters; reusing a cursor with different filters is
rejected as invalid input.
The default archived = 0 list queries are covered by partial indexes on active
rows only:
CREATE INDEX workflow_work_board_items_active_scope_order
ON workflow_work_board_items(scope_kind, project_id, updated_at DESC, item_id DESC)
WHERE archived = 0;
CREATE INDEX workflow_work_board_items_active_order
ON workflow_work_board_items(updated_at DESC, item_id DESC)
WHERE archived = 0;This keeps an archive-heavy database from walking most of the ordering index to produce one page of active items.
Deferred. Phase 0 does not ship a projection function: there is no production
consumer yet, and the minimal DTO would have been a parallel contract instead
of the canonical SessionContinuitySnapshot / TurnSnapshot used by the
Runtime Host. It will be implemented beside the real continuity adapter when
"start as task" lands in Phase 3, using only facts those authorities directly
expose.
- contract: decode, provenance invariants, state transitions, archive invariant, bounds, patch/CAS semantics;
- storage: migration, reopen persistence, pagination/filtering, archive/delete, archive-heavy index coverage, concurrent mutations, corrupt-record rejection, backup/restore;
- projection: deferred to Phase 3 (no Phase 0 module or tests).