Skip to content

Latest commit

 

History

History
163 lines (132 loc) · 6.19 KB

File metadata and controls

163 lines (132 loc) · 6.19 KB

Work Board Phase 0 Contract

Status: Phase 0. Scope: packages/core (contract) + packages/storage (store and migration).

Boundary

The Work Board is a user-owned, local-first surface for deferred work. It is not an execution authority:

  • no task_* tools, no task.ledger.query, and no workflow_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.

Item contract

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

Provenance is a discriminated union:

  • manual;
  • main_conversation (sessionId, messageId, optional runId / turnId, capturedAt, bounded excerpt; parentSessionId is rejected);
  • side_conversation (same fields plus required parentSessionId).

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.

Persistence and mutation

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;
  • expectedRevision provides 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.scope as corrupt_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.

Linked-session projection

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.

Tests

  • 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).