Article V The schema
5.1
One file
All state lives in a single SQLite file, opened once with WAL journaling, a five-second busy
timeout and foreign keys enabled. There is no ORM and no migration framework: schema creation is one
idempotent function of CREATE TABLE IF NOT EXISTS statements, and every later addition is
a guarded ALTER TABLE … ADD COLUMN whose duplicate-column error is caught and swallowed.
try { db.prepare(`ALTER TABLE operator_progress ADD COLUMN ${col[0]} ${col[1]}`).run(); } catch (e) { if (!/duplicate column name/i.test(String(e && e.message))) throw e; }
Twenty-four distinct tables are created across the codebase; fifteen of them by the core schema function, six by the runner library’s additive bootstrap, and the rest by the advisor and the voice arbiter.5 Uniqueness is used structurally rather than defensively: several partial unique indexes make an invalid state a hard insert error instead of a condition to be checked for.
5.2
The work model
Intent flows downward — from a confirmed goal model, through goals and projects, to the tasks of a particular day, and from there to a forward plan the loop advances one step at a time. Every arrow in the diagram below is a real column or index in the schema.
serves_goal holds the goal title, “NO invented ids”. Resolution of a
title to a goal goes through the conservative matcher, which refuses ambiguity.5.3
The priority function
Urgency is computed by a pure kernel function the model cannot reach. It is deliberately separate
from — and never overloaded onto — the legacy hand-set priority column, which runs in the
opposite direction.
// urgent = deadline proximity (no deadline -> 0.5 neutral; monotone-decreasing in days-left) // important = goalImportance (0..1) of task.serves_goal; default mid 0.5 when absent/unknown // ready = (status !== 'blocked' && !blocker) ? 1 : 0 -> a blocked task sinks to 0 urgent = daysLeft <= 0 ? 1 : clamp01(1 / (1 + daysLeft)); const computed_priority = Number((urgent * important * ready).toFixed(4));
Each computation also writes a human-readable explanation of itself into
priority_explain — for instance
urgent=0.33(deadline 2.0d)×important=0.80(goal:…)×ready=1 ⇒ 0.264 — so the ranking is
auditable without re-deriving it. A blocked task multiplies to zero rather than being filtered out,
which keeps it visible in the ordering while sinking it.
5.4
The state hash, and what is kept out of it
A short SHA-1 over the salient board state lets a tick tell whether anything actually moved since
the last one. It is computed over exactly four projections: each task as
id:status:priority:blocker, each decision as id:status, each intent as
id:status:policy_decision, and the identifier of the most recent instruction.
What is excluded matters more than what is included. Two categories are deliberately kept out, each with a comment explaining the failure that inclusion would cause:
- computed_priorityRecomputed on every read. Hashing it “would forge per-tick board churn into the no_progress_streak / board_unchanged_streak signal and disarm the stall monitor.”
- observations, salienceThe perceiving layer is volatile by construction; it is loaded after the hash is computed, so it can never enter it.
The same discipline governs the outcome vocabulary. An intent that the gate allowed but that
nothing has executed records the outcome staged, which is classified non-advancing on
purpose: “an ‘allowed’ intent that nothing fires must NOT reset the no-progress streak, else the loop
looks productive while the board never moves.”
5.5
Core tables
| Table | Holds | Notable constraint |
|---|---|---|
| operator_briefings | One row per operator day: title, summary, meeting and execution status. | day is the primary key |
| operator_tasks | The board: title, lane, owner, status, blocker, deadline, goal served. | UNIQUE(day, slug) |
| operator_decisions | Decisions awaiting or carrying a verdict, including approvals. | UNIQUE(day, slug); carries intent_id and action_hash |
| operator_comments | Instructions and comments, with the author’s subject. | indexed by day |
| operator_audit | Every consequential event: intents, approvals, gate passes, executor moves. | append-only in practice |
| operator_runs | Autonomous run records with heartbeat, owner process and host. | partial unique index: one running run per day |
| operator_action_intents | The gate’s ledger. | partial unique index on (day, idempotency_key) |
| operator_progress | What each tick did, plus the verifier’s separate verdict columns. | indexed by day and by task |
| operator_learnings | Distilled candidate lessons, never auto-promoted. | bi-temporal; deduplicated by content hash |
| operator_reflections | Short self-critiques after a failed tick. | ephemeral; pruned to the last three per goal |
| operator_observations | Cheap world and self deltas from the perceiving tick. | untrusted DEFAULT 1; UNIQUE(day, source, delta_hash) |
| operator_feedback_signals | Implicit human facts: a revert, a re-ask, a dismissal, an approval latency. | never read by the model |
| operator_interrupt_budget | The persisted token bucket for unsolicited contact. | day is the primary key |
| user_world_model_snapshots | Reviewable versions of the model of the person. | partial unique index: one active snapshot |
| user_world_model_beliefs | Typed, evidence-carrying beliefs attached to a snapshot. | UNIQUE(snapshot_version, belief_key) |
5.6
Bi-temporality, and what may be deleted
Durable rows are never destructively rewritten. A superseded goal model, an outdated belief, a
retired lesson: each is closed by stamping valid_until, so history is preserved and every
change is reversible. The world-model documentation states the resulting rule plainly — older active
snapshots are “marked superseded, never deleted”, and “draft imports do not change the
active model. Activation is explicit.”
Bulk deletion exists, but only for high-volume ephemeral logs, and it is fenced by three hard floors: never prune inside the retention window; always keep at least the most recent N progress rows regardless of age, because the cross-day streak logic reads that window and “must NEVER be starved by pruning”; and never touch the durable tables at all. The durable exclusion list is enumerated in the source: goal models, confidence, decisions, action intents, learnings, plans, memory and the state of play.
5.7
The day, and where it comes from
Almost every table is keyed by an operator day rather than a timestamp, and that day is derived from the profile’s timezone — not from the host’s clock, and not from UTC. The runner library and the state layer resolve it through the same profile-timezone source, and a cross-cutting test asserts the two agree at a clock reading near the UTC boundary, so a tick started late at night cannot land on a different day from the gate that judged it.
The same concern shapes the executor: it scans a rolling trailing window of days rather than only today, “so a cross-midnight approval/claim is never invisible.” Three days is the shipped default, and it is environment-overridable.
Notes
- The section header in the source reads
// --- schema (8 tables) ---; the function beneath it now issues fifteenCREATE TABLEstatements. The comment is stale, not the code. ↩