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
| Concern | Canonical | Runtime |
|---|---|---|
| Goal contract | goals/<GOAL_ID>/GOAL_RECORD.yaml | goals row |
| Task list | tasks: in the contract | tasks rows |
| Phase state | declared by the contract | phases rows |
| Receipts | evidence/<GOAL_ID>/ | evidence_records table |
| Gates | agent-harness/governance/allowed_signers and per-goal owner_gates | owner_gates table |
| PRs | GitHub-side | pull_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:
- Walk all
goals/*directories. - For each contract, upsert the
goalsrow. - For each declared task, upsert the
tasksrow. - 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:
- Auditability — every change must flow through a function with a receipt.
- Schema versioning — the schema is migrated by
RuntimeStore'smigrate()step. A direct SQLUPDATEmay write values that the migration expects to derive. - 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.