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
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""Plan externalisation and Gitea-backed approvals (architecture L3, D2).
|
|
|
|
Plans are posted as comments on a per-project Gitea issue. The approval
|
|
comment — its author, timestamp, and revision tag — is the durable off-box
|
|
record, authenticated by Gitea's own auth. The state store keeps only a
|
|
pointer to it, so boot reconciliation can re-derive gate state from Gitea
|
|
even after the state volume is lost: external truth wins.
|
|
|
|
Approvals arriving here bind to a revision exactly as they do over transport
|
|
(L3). The first valid approval on any channel wins; the orchestrator echoes
|
|
the outcome to the other channel so the records converge.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from nisse_dev.gitea import Comment, GiteaClient, GiteaError
|
|
|
|
APPROVAL_HINT = (
|
|
"Reply `approve {tag}` to approve this revision, or comment with feedback "
|
|
"to reject it."
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PlanPost:
|
|
issue: int
|
|
comment_id: int
|
|
|
|
|
|
class PlanBoard:
|
|
"""The Gitea side of plan gating: one issue per project, one comment per
|
|
plan revision."""
|
|
|
|
def __init__(self, client: GiteaClient, repo: str) -> None:
|
|
self._client = client
|
|
self._repo = repo
|
|
|
|
@property
|
|
def repo(self) -> str:
|
|
return self._repo
|
|
|
|
def open_project_issue(self, project: str, task_id: str) -> int:
|
|
return self._client.create_issue(
|
|
self._repo,
|
|
title=f"nisse-dev: {project}",
|
|
body=(
|
|
f"Plan gate for project `{project}` (task `{task_id}`).\n\n"
|
|
"Each plan revision is posted here as a comment. Approving a "
|
|
"revision by comment is equivalent to approving it in Matrix; "
|
|
"the first valid approval on either channel wins."
|
|
),
|
|
)
|
|
|
|
def post_plan(self, issue: int, gate: str, rev: int, plan: str) -> PlanPost:
|
|
tag = f"rev-{rev}"
|
|
body = "\n".join(
|
|
[
|
|
f"## Plan for `{gate}` ({tag})",
|
|
"",
|
|
plan,
|
|
"",
|
|
"---",
|
|
APPROVAL_HINT.format(tag=tag),
|
|
]
|
|
)
|
|
comment = self._client.post_comment(self._repo, issue, body)
|
|
return PlanPost(issue=issue, comment_id=comment.id)
|
|
|
|
def echo(self, issue: int, text: str) -> None:
|
|
"""Mirror an outcome decided on another channel, so the Gitea record
|
|
does not silently disagree with Matrix."""
|
|
try:
|
|
self._client.post_comment(self._repo, issue, text)
|
|
except GiteaError:
|
|
pass
|
|
|
|
def close(self, issue: int, note: str) -> None:
|
|
"""Close the plan issue with a note saying why (e.g. abandoned).
|
|
Unlike echo, failures raise: the caller reports them."""
|
|
self._client.post_comment(self._repo, issue, note)
|
|
self._client.close_issue(self._repo, issue)
|
|
|
|
def comments_since(self, issue: int, after_id: int) -> list[Comment]:
|
|
return [
|
|
c for c in self._client.list_comments(self._repo, issue) if c.id > after_id
|
|
]
|