Skip to main content

State

ASF50 has two related but distinct notions of "state":

  • Canonical state — the Git-tracked source of truth. Contracts, evidence, frozen receipts.
  • Runtime state — the SQLite-backed state owned by RuntimeStore. Goals, phases, tasks, runs, gates, evidence records, worktrees.

The two must agree. When they disagree, the durable answer is the canonical state; the runtime is reconciled to it via the registry-reconciliation path.

Canonical vs runtime

ConcernCanonicalRuntime
Goal contractgoals/<GOAL_ID>/GOAL_RECORD.yamlgoals row
Task listtasks: in the contracttasks rows
Phase statedeclared by the contractphases rows
Receiptsevidence/<GOAL_ID>/evidence_records table
Gatesagent-harness/governance/allowed_signers and per-goal owner_gatesowner_gates table
PRsGitHub-sidepull_requests table
Worktrees.git/worktrees/worktrees table

RuntimeStore

scripts/goal-runner/runtime_store.py defines the RuntimeStore API. All persistent mutations go through it. The store is constructed with a runtime_root and opens the SQLite at <runtime_root>/goal-state.sqlite3.

The store's API is narrow on purpose: insert/get/list with predicate parameters. There is no general "execute SQL" method.

Reconciliation

When a goal is materialized, GoalRegistry.reconcile_all() runs:

  1. Walk all goals/* directories.
  2. For each contract, upsert the goals row.
  3. For each declared task, upsert the tasks row.
  4. Leave runtime-derived state (runs, sessions, locks) alone.

Reconciliation is idempotent and is the only safe way to recover a corrupted runtime DB.

Why direct SQLite mutation is forbidden

Three reasons:

  1. Auditability — every change must flow through a function with a receipt.
  2. Schema versioning — the schema is migrated by RuntimeStore's migrate() step. A direct SQL UPDATE may write values that the migration expects to derive.
  3. Reconciliation — the next materialization will overwrite any hand-mutated rows back to the canonical value, silently losing the change.

If you need to fix a state bug, file a repair goal (goal_type: REPAIR_GOAL) and let the orchestrator do it.

Backup and recovery

The runtime DB can be backed up with sqlite3 .backup while ASF50 is idle. Recovery is via GoalRegistry.reconcile_all() after restoring the backup. A canonical-state-only recovery is possible by deleting goal-state.sqlite3 and re-materializing every goal — the registry will rebuild the runtime from the Git-tracked contracts.