CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m22s
Development Plan M0.1. Every section reference in code, tests, specs/, the example config, the tuning guide, and the CLAUDE.md and README files now cites docs/Architecture.md (P, L, D, O, X, Q) instead of the old spec's A-H, and docs/Architecture-Build-Spec.md is deleted (git keeps it). The plan's old-to-new table goes with it. Mostly mechanical, with a per-site choice where one old section split: A4 became L2 where it meant stop/abandon/the loop and L3 where it meant approvals; A10 became L6 in the context-budget code and L7 elsewhere; B2 became D4 for outbound Gitea and publishing and D5 for review intake; 'A3 step N' became the matching step of L8's boot sequence. Comments describing behaviour the architecture drops (planning always escalating, thresholds tuned at an old build step) now describe the current code and point at the Development Plan. docs/Architecture.md gains one line the rewrite missed: intake outages delay but never fail work (old B2), now in L5, where the poller and Matrix code cite it. Comments and docs only; no behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
"""Boot-time reconciliation (architecture L8): turn an ambiguous crash into a safe,
|
|
known state.
|
|
|
|
Every pending intent found at startup is checked against external truth by a
|
|
registered reconciler — an observable check (query Gitea, inspect the tree),
|
|
never model self-assessment. Anything unverifiable gates to AWAIT_INPUT;
|
|
the agent never blind-resumes.
|
|
"""
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
from nisse_dev.state import Intent, StateStore
|
|
|
|
# Returns True if the intent's external effect verifiably happened (do not
|
|
# repeat it), False if it verifiably never started (safe to discard), or
|
|
# None if it cannot be verified.
|
|
Reconciler = Callable[[Intent], bool | None]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BootReport:
|
|
action: Literal["resume", "await_input"]
|
|
notes: list[str]
|
|
|
|
|
|
def reconcile_on_boot(
|
|
store: StateStore, reconcilers: Mapping[str, Reconciler]
|
|
) -> BootReport:
|
|
notes: list[str] = []
|
|
unresolved = False
|
|
for intent in store.pending_intents():
|
|
label = f"intent {intent.id} ({intent.kind})"
|
|
reconciler = reconcilers.get(intent.kind)
|
|
if reconciler is None:
|
|
unresolved = True
|
|
notes.append(f"{label}: no reconciler registered")
|
|
continue
|
|
verdict = reconciler(intent)
|
|
if verdict is True:
|
|
store.commit_intent(
|
|
intent.id, cause="reconciliation verdict: effect verified"
|
|
)
|
|
notes.append(f"{label}: external effect verified, marked committed")
|
|
elif verdict is False:
|
|
store.abort_intent(
|
|
intent.id, cause="reconciliation verdict: never took effect"
|
|
)
|
|
notes.append(f"{label}: never took effect, marked aborted")
|
|
else:
|
|
unresolved = True
|
|
notes.append(f"{label}: unverifiable")
|
|
report = BootReport(
|
|
action="await_input" if unresolved else "resume",
|
|
notes=notes,
|
|
)
|
|
store.oplog.emit(
|
|
"boot.reconcile", cause="startup", action=report.action, verdicts=notes
|
|
)
|
|
return report
|