Files
nisse/nisse-dev/src/nisse_dev/loop/items.py
T
dongarandClaude Opus 5.5 21744efac1
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m48s
Milestones and review in the new loop (M2.1, PR C)
A work item starts from a fresh workspace: fetch, set aside (never
discard) leftover changes, branches and unpushed commits, a clean default
branch, and a branch of its own (D3, #62). start_milestone cuts the
milestone's branch; finish_milestone commits only the work, pushes and
opens a PR that closes the issue, under an intent, adopting an open PR and
treating a closed one as the operator's answer (D4). Between runs the
loop watches each item's PR: operator comments and inline review notes
(#69) reach the item, a merge or close hands back to the driver. A review
fix is checked after it's pushed and goes out as a new PR if the PR merged
first (D5). At boot an interrupted publish is settled against Gitea (L8).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-02 23:53:02 +00:00

78 lines
2.6 KiB
Python

"""A work item's place in the loop (L1), kept in its row in the state store.
The row's ``position`` is the item's state; what the loop needs besides
(the thread its conversation lives in, the revision of what is put up for
agreement, what it is waiting for) is in ``data["loop"]``.
"""
from dataclasses import asdict, dataclass
from typing import Any
from nisse_dev.state import StateStore, WorkItem
READY = "ready"
RUNNING = "running" # a driver run is under way; on boot it means interrupted
WAITING = "waiting"
CLOSED = "closed"
# What a waiting item waits for.
GO_AHEAD = "go-ahead" # a go on the restatement up (L3, D1)
ANSWER = "answer" # an answer to a question
MERGE = "merge" # the PR's review and merge (D4, D5)
@dataclass(frozen=True)
class Loop:
thread: str = "" # the root of the item's Matrix thread (L5)
issue: int = 0 # its record issue (D1)
wait: str = "" # GO_AHEAD / ANSWER, when waiting
rev: int = 0 # the latest proposal put up (L3)
started: bool = False # scope agreed: a go-ahead was given (D1)
note: str = "" # what a waiting item said it is waiting on
prepared: bool = False # its fresh workspace was made (D3)
milestone: int = 0 # the current milestone (D4)
branch: str = "" # the current milestone's branch, while one is open
pr: int = 0 # the current milestone's PR, once published
pr_url: str = ""
comments: int = 0 # the last PR conversation comment taken (L8)
notes: tuple[str, ...] = () # review notes taken (#69)
fixes: int = 0 # review fixes pushed, for follow-up branch names
def to_data(self) -> dict[str, Any]:
return asdict(self)
def loop_of(item: WorkItem) -> Loop:
data = dict(item.data.get("loop", {}))
data["notes"] = tuple(data.get("notes", ()))
return Loop(**data)
def save(
store: StateStore, item_id: str, state: str, loop: Loop, *, cause: str
) -> None:
item = store.work_item(item_id)
assert item is not None
store.update_work_item(
item_id,
position=state,
data={**item.data, "loop": loop.to_data()},
cause=cause,
)
def item_for_thread(store: StateStore, thread: str) -> WorkItem | None:
for item in store.work_items("open"):
if loop_of(item).thread == thread:
return item
return None
def item_for_issue(store: StateStore, issue: int) -> WorkItem | None:
"""Any item, open or closed, recorded by this issue: intake never takes
an issue twice (D1)."""
for item in store.work_items():
if loop_of(item).issue == issue:
return item
return None