"""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_core.state import WorkItem from nisse_dev.state import StateStore READY = "ready" RUNNING = "running" # a driver run is under way; on boot it means interrupted WAITING = "waiting" BLOCKED = "blocked" # waits for its recorded dependencies to close (L1) 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) CHAT = "conversation" # a conversation waits for the operator to say more (L5) PROVISION = "provisioning" # a new project waits for what it needs (D1, D8) @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 / MERGE, 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 blocked_by: tuple[str, ...] = () # open dependencies, "owner/repo#N" (L1) first: int = 0 # the operator's "do #N next": higher runs sooner labels: tuple[str, ...] = () # the issue's labels at intake: they steer (D1) grace_until: float = 0.0 # a `bug` restatement proceeds then, unanswered talked: bool = False # the operator has spoken in the item's thread # A conversation (`general`, L1): when it last heard or said anything, # the conversation it continues, the work it turned into, and the work # proposed for a go-ahead. idle_since: float = 0.0 follows: str = "" became: tuple[str, ...] = () proposal_project: str = "" proposal_title: str = "" proposal_kind: str = "" # "work" on a project, or a new "project" new_project: str = "" # a new project being provisioned (D1) design_merged: bool = False # a `design` item's design PR has merged (D1, D2) helps: int = 0 # delegations so far, for their branch names (L9) helped_at: int = 0 # the milestone a helper was last called in for, unasked # A question from the operator is answered without changing the item # (L5): the wait it interrupted, to go back to. question: bool = False back_to: str = "" back_note: str = "" # Where the operator last spoke (D5: answer where asked): "" for the # Matrix thread, or the PR it came from, with the inline location and a # little of what they said, for quoting. asked_on_pr: int = 0 asked_where: str = "" asked_text: str = "" # A dangerous action put to the operator (P3): what `allow` would let # run, and what it did let run, once, in the next run (L3: bound to # exactly that action, by its key). pending_action: str = "" allowed: str = "" # The trust dial (M3.3): why an approved PR was last left unmerged, so # it's said once per commit and reason. merge_held: str = "" # The toolchain as the harness last checked it (M4.1): name, version # command, version. toolchain: tuple[tuple[str, str, str], ...] = () # L6: the agent's memory index was put into the transcript (once). memory_shown: bool = False def to_data(self) -> dict[str, Any]: return asdict(self) def loop_of(item: WorkItem) -> Loop: data = dict(item.data.get("loop", {})) # Retired: a changed host key now blocks the build VM (#137). data.pop("host_key_changed", None) data["notes"] = tuple(data.get("notes", ())) data["blocked_by"] = tuple(data.get("blocked_by", ())) data["labels"] = tuple(data.get("labels", ())) data["became"] = tuple(data.get("became", ())) data["toolchain"] = tuple(tuple(t) for t in data.get("toolchain", ())) 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, status: str | None = "open" ) -> WorkItem | None: """The item whose conversation is ``thread``; the latest, when a closed conversation's thread was picked up again.""" found = [i for i in store.work_items(status) if loop_of(i).thread == thread] return found[-1] if found else None def item_for_issue( store: StateStore, issue: int, project: str | None = None ) -> WorkItem | None: """The latest item, open or closed, recorded by this issue (in ``project``, when given): intake takes an issue once, and again only on a fresh assignment after its item closed (D1).""" found = [ item for item in store.work_items() if loop_of(item).issue == issue and project in (None, item.scope) ] return found[-1] if found else None