From e0ca0150042587e7494184b5ab2f6701f3487076 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 13:51:42 -0400 Subject: [PATCH 01/20] Record delivery integration on checked orders and decode named PR rows --- docs/evidence/cn-phase3-record.md | 52 +++++++++++ src/agent_comms/todos.py | 144 +++++++++++++++++++++++++++--- 2 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 docs/evidence/cn-phase3-record.md diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md new file mode 100644 index 0000000000..8bbc7a4dee --- /dev/null +++ b/docs/evidence/cn-phase3-record.md @@ -0,0 +1,52 @@ +# Commander record plane + +Owner: pi-cn-phase3. Branch: feat/cn-phase3-record. +Baseline: 5cdac561d498ece25142a8d2e5cf9b06002b6c8a. + +## Checklist + +- [x] Read the plan and inspect both repositories' open PR file lists. +- [x] Confirm existing verdict, strike limit, fire and void owners on main. +- [x] Add delivery integration to the canonical checked delivery. +- [x] Decode PR query rows into named records, not positional tuples. +- [ ] Confirm real scratch-bus order, verdict, strikes, fire and void through Core tools. +- [ ] Confirm delivery through the CLI and run bounded local checks. +- [ ] Publish a reviewed draft PR; do not merge or install. + +## Source relationships + +Before: `OrderDelivery.verified` ran the check and deletion counts; `CommitDelivery.require_source` +proved ancestry and publication only. Completion returned no integration fact. `PullRequests.read` +decoded a positional tuple of seven unrelated fields. `DeliveredProblemOrder.line` was a constant. + +After: completion passes its canonical Todo and registry to the delivery owner. A checked commit +records a `DeliveryMerge`: main, another PR target, or linked but unmerged source branches. +`DeliveredProblemOrder.line` derives its text from that delivery. `WorkBranches` still owns the +open-PR/open-issue branch-link rule. PR lookup still uses the immutable order reference, now decoded +through named query records including the target branch. No verdict, strike, delivery or branch store +was added. The integration is the observation at completion, not a later claim of installation. + +AST evidence used refactor-audit's `Package.load` with Python 3.14: 382 production modules parsed, +zero omissions. The only production `OrderDelivery.verified` caller is `DeliverTodo.prepare`; +creation is in `commander.order.done` and `CommsOrderDoneTool`. Goal summaries consume the order's +`line`. The installed Toad source has no order-delivery consumer; its unrelated input-delivery types +are not this family. A preliminary NRA run under its old Python 3.12 omitted Python 3.14 syntax; +it is not acceptance evidence. NRA under Python 3.14 is blocked by that checkout's incompatible +metaclass-registry/tree-sitter dependencies; the existing refactor-audit parser supplied the census. +Dynamic tool invocation is generated from ToolRequest declarations/FieldCodec, not statically +resolved by this census. + +## Overlaps + +P603 is merged (#1757). Open #1903 and #1873 do not edit these record owners. Recent #1756 edits +FairText, not the delivery family; #1925 removes DeadlineText. Recent #1878 changes TodoStore's +owner lookup, which this change leaves alone. Older #1737 changes claims and closing, not integration; +older #1733 adds printed check output and must compose that output with the checked delivery here. +#1912 moves the commander functions into an explicit Commander context, including `board` and `done`. +Those functions are left untouched pending the integration owner's conflict decision. + +## Resource boundary + +`tools/agent_resource_check.py --assert-headroom` warned: about 4 GiB available RAM and 3.8 GiB +free on /tmp. No parallel agents or large suite. The prescribed /home/ts/bin launcher is absent. +Owned verification state and logs will remain under this checkout's evidence directory, not /tmp. diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 2ac7f86eff..6e935866d1 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -38,10 +38,12 @@ from .standing_rules import UndatedStandingRule from .storage_rules import StorageRule from .plan_phases import OrderPhase, UnplannedPhase +from .work_branches import WorkBranches if TYPE_CHECKING: from .comms import Comms from .goal_management import Goals + from .registration import Registration @dataclass(frozen=True) @@ -81,9 +83,10 @@ async def run(self, check, *command): raise ValueError(result.stderr.decode(errors="replace") or "Delivery source check failed.") return result.stdout.decode(errors="replace") - async def verified(self, check: OrderCheck, deletes: OrderDeletion) -> OrderDelivery: + async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: """The order's check and every count its deletion declares run on the delivered source.""" - for each in (check, *deletes.checks): + check = todo.order.check + for each in (check, *todo.order.deletes.checks): await self.require_source(each) each.result.require(await self.checked(each)) return replace(self, at=await self.delivery_time(check)) @@ -170,6 +173,95 @@ async def require_source(self, check: OrderCheck) -> None: raise ValueError( f"Delivered commit {self.delivered} is on no origin branch; push it first.") + async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: + merge = await self.merge_state(todo, registry) + verified = await super().verified(todo, registry) + return CheckedCommitDelivery(commit=self.commit, delivered_commit=self.delivered, + at=verified.at, evidence=self.evidence, merge=merge) + + async def merge_state(self, todo: Todo, registry: Registration) -> DeliveryMerge: + """Observe integration, not merely publication, at the time of completion. + + Git proves ordinary merges to main. The order's merged PR also proves squash merges + and names a non-main target. Otherwise the pushed source must still have an open link, + whose owner is WorkBranches, not a second branch policy in the delivery tool. + """ + check = todo.order.check + branches = (await self.run( + check, "git", "for-each-ref", f"--contains={self.delivered}", + "--format=%(refname:strip=3)", "refs/remotes/origin/")).splitlines() + if WorkBranches.main in branches: + return MainDeliveryMerge() + requests = await asyncio.to_thread(PullRequests((todo,)).listed, + todo.repo, "merged", todo.order) + for request in requests: + if request.merge and await self.contains(check, request.head): + return (MainDeliveryMerge() if request.base == WorkBranches.main + else BranchDeliveryMerge(request.base, request.merge)) + linked = await asyncio.to_thread( + WorkBranches(Path(check.directory)).read, registry.snapshot()) + sources = tuple(sorted(branch.name for branch in linked + if branch.name in branches and branch.linked)) + if not sources: + raise ValueError( + f"Delivered commit {self.delivered} is not merged and has no branch linked " + "to an open PR or issue.") + return UnmergedDeliveryMerge(sources) + + async def contains(self, check: OrderCheck, head: str) -> bool: + if head == self.delivered: + return True + result = await BoundedRun.run( + ("git", "merge-base", "--is-ancestor", self.delivered, head), + cwd=check.directory, timeout=120) + if result.outcome not in (ExitedOutcome(0), ExitedOutcome(1)): + raise ValueError(result.stderr.decode(errors="replace") or + "Could not check the delivered commit against its PR head.") + return result.outcome == ExitedOutcome(0) + + +class DeliveryMerge(DeclaredFamily, affix="DeliveryMerge"): + """The integration observed when Core checked a commit delivery; never a claim of install.""" + + @property + @abstractmethod + def line(self) -> str: ... + + +@dataclass(frozen=True) +class MainDeliveryMerge(DeliveryMerge): + @property + def line(self) -> str: + return f"merged to {WorkBranches.main}" + + +@dataclass(frozen=True) +class BranchDeliveryMerge(DeliveryMerge): + branch: str + commit: str + + @property + def line(self) -> str: + return f"merged to {self.branch} as {self.commit}" + + +@dataclass(frozen=True) +class UnmergedDeliveryMerge(DeliveryMerge): + branches: tuple[str, ...] + + @property + def line(self) -> str: + return f"not merged; linked branches: {', '.join(self.branches)}" + + +@dataclass(frozen=True, kw_only=True) +class CheckedCommitDelivery(CommitDelivery): + merge: DeliveryMerge + + @property + def work(self) -> str: + return f"{super().work}; {self.merge.line}" + @dataclass(frozen=True, kw_only=True) class OperationDelivery(OrderDelivery): @@ -192,21 +284,46 @@ class PullRequest: merged_at: float | None = None updated_at: float = 0.0 orders: tuple[str, ...] = () + base: str = field(kw_only=True) @classmethod @boundary_decoder def listed(cls, number: int, branch: str, head: str, merge: str, merged: str, - updated: str, body: str) -> Self: + updated: str, body: str, base: str) -> Self: """One row of ``gh pr list``; GitHub leaves an unmerged PR's merge commit and time empty.""" return cls(int(number), branch, head, merge or None, _github_time(merged) if merged else None, _github_time(updated), - ProblemOrder.pull_request_ids(body)) + ProblemOrder.pull_request_ids(body), base=base) def _github_time(text: str) -> float: return calendar.timegm(time.strptime(text, "%Y-%m-%dT%H:%M:%SZ")) +@dataclass(frozen=True) +class PullRequestCommit: + """GitHub's optional mergeCommit object, decoded at the query boundary.""" + oid: str + + +@dataclass(frozen=True) +class ListedPullRequest: + """The external query row; conversion lives here, never in its consumers.""" + number: int + branch: str + head: str + merge: PullRequestCommit | None + merged: str | None + updated: str + body: str + base: str + + def parsed(self) -> PullRequest: + return PullRequest.listed( + self.number, self.branch, self.head, self.merge.oid if self.merge else "", + self.merged or "", self.updated, self.body, self.base) + + class PullRequests: """Pull requests queried by immutable order identity, cached within one board read.""" @@ -233,20 +350,19 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, f'"{candidate.pull_request_reference}" sort:updated-desc') selections.append( f'{alias}: search(query: {search}, type: ISSUE, first: 100) {{ nodes {{ ' - '... on PullRequest { number headRefName headRefOid body ' - 'mergeCommit { oid } mergedAt updatedAt } } }') + '... on PullRequest { number branch: headRefName head: headRefOid body ' + 'base: baseRefName merge: mergeCommit { oid } merged: mergedAt ' + 'updated: updatedAt } } }') listed = subprocess.run( ("gh", "api", "graphql", "-f", "query=query {" + " ".join(selections) + "}", - "--jq", '.data | map_values(.nodes | map([.number, .headRefName, .headRefOid,' - ' .mergeCommit.oid // "", .mergedAt // "", .updatedAt, .body]))'), + "--jq", '.data | map_values(.nodes)'), capture_output=True, text=True, timeout=60) if listed.returncode: raise ValueError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") - rows = FieldCodec.decode(dict[str, tuple[tuple[int, str, str, str, str, str, str], ...]], + rows = FieldCodec.decode(dict[str, tuple[ListedPullRequest, ...]], FieldCodec.parse(listed.stdout)) for alias, candidate in aliases.items(): - self._read[repo, state, candidate.id] = tuple( - PullRequest.listed(*row) for row in rows[alias]) + self._read[repo, state, candidate.id] = tuple(row.parsed() for row in rows[alias]) return self._read[repo, state, order.id] @@ -1297,7 +1413,9 @@ class DroppedProblemOrder(UnorderedProblemOrder, CheckedProblemOrder): class DeliveredProblemOrder(CheckedProblemOrder): delivery: OrderDelivery pending: ClassVar[bool] = False - line: ClassVar[str] = "Delivered, waiting on install" + @property + def line(self) -> str: + return f"Delivered: {self.delivery.work}, waiting on install" def require_transfer(self, todo: Todo) -> None: owner = (todo.assignment or todo.released[-1].previous).owner @@ -1885,7 +2003,7 @@ def prepare(self, goals, current): current.require_commander(self.actor) current.state.require_open(current) current.order.require_delivery(self.order_id) - verified = self.delivery.verified(current.order.check, current.order.deletes) + verified = self.delivery.verified(current, goals.registry) return replace(self, delivery=asyncio.run(verified)) def apply(self, current): From 31f8ca1cb1a3dd4cd626924b763d146ab6b95790 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 13:55:25 -0400 Subject: [PATCH 02/20] Add observed commander board with read-only progress and lazy cards --- docs/agents/commander-page.md | 31 ++++ src/agent_comms/presentation.py | 12 +- src/agent_comms/revision_views.py | 4 + src/agent_comms/todos.py | 30 +++- src/agent_comms/ui_model/commander.py | 195 ++++++++++++++++++++++++++ tests/test_commander_model.py | 54 +++++++ 6 files changed, 314 insertions(+), 12 deletions(-) create mode 100644 docs/agents/commander-page.md create mode 100644 src/agent_comms/ui_model/commander.py create mode 100644 tests/test_commander_model.py diff --git a/docs/agents/commander-page.md b/docs/agents/commander-page.md new file mode 100644 index 0000000000..2ef9107b05 --- /dev/null +++ b/docs/agents/commander-page.md @@ -0,0 +1,31 @@ +# Commander page, first slice + +Owner: pi-cn-phase6. Core base 011084fc5; Toad base 9f203ea1. Issue #1495. + +## Checklist + +- [x] Read phase 6, parity and dependencies; phase 5 is not a dependency. +- [x] List all open Core/Toad PR files; read #1912 and #1756 diffs. +- [x] Avoid the recent #1070 MainScreen changes: add a discovered slash command instead. +- [x] Read refactor-audit skill and catalog; run its AST overlay across both production roots (zero parse omissions). NRA is not installed on this machine. +- [ ] Implement observation, board, lazy details and existing delivery alarms. +- [ ] Exercise real scratch Core orders in Toad under isolated Xvfb/st. +- [ ] Run bounded checks in the background; publish paired draft PRs, do not merge. + +## Owners and consumers + +`TodoStore.for_commander` owns the indexed open-order selection, `Todo.held` owns assignment identity, `Todo.overdue` owns overdue meaning, `ProblemOrder.instructions` owns the full order text, `OrderProgress` owns git/PR state and stall policy, `TurnDigestRead` owns bounded newest-first turns, and delivery progress owns notification labels. The page must render these, not reinterpret strings or keep order state. + +`ProgressSource.readings` currently executes the order's acceptance check. A read-only commander page must not execute arbitrary checks. Keep one progress selection algorithm in ProgressSource, with a status-only source capability that does not request distance measurements. The existing native tool continues requesting its existing measured progress. + +`ObservationProcess` owns off-UI reads and lifetime; add a declared interest/result rather than a widget polling stores. `WireRevision` must include Todo main/WAL changes and the existing expiry tick. UI expansion belongs to the card; only expanded units request bounded turns. + +BOUND-2/BOUND-8: retain Todo, OrderProgress and ThreadIncarnation in typed models, not mapped records or redispatched kind strings. MEMB-1: the new slash command is discovered from its declaration; no command roster or MainScreen change. + +## Known gap + +Pre-send refusals currently escape as boundary exceptions; no retained indexed refusal feed was found. Existing failed delivery notifications are not the same fact and will be labelled honestly. Requested an owning-source decision from claude-code; continue the independent page work. + +## Disposable output + +Owned by pi-cn-phase6: `/home/ts/.cache/agent-scratch/pi-cn-phase6` for bounded test logs, one Xvfb/st run and screen captures. Source/handoffs remain in the two `/home/ts/wt/pi-cn-phase6--P*` worktrees. The standard resource-check executable is absent here; measured 3.1 GiB available RAM and 26 GiB disk. Run one UI, no parallel agent or large suite. diff --git a/src/agent_comms/presentation.py b/src/agent_comms/presentation.py index 0f367d291a..de3e3a5844 100644 --- a/src/agent_comms/presentation.py +++ b/src/agent_comms/presentation.py @@ -45,7 +45,7 @@ from .read_ledger import NoReadNotice, ReadLedger, ReadNotice from .recorded_bus import RecordedBusIdentity from .registry_document import DEFAULT_ROSTER, RosterVisibility -from .revision_views import (STATUS_SOURCE, BusSourceView, ExternalTranscriptsView, +from .revision_views import (STATUS_SOURCE, BusSourceView, CommanderView, ExternalTranscriptsView, NotificationsView, RegistrationsView, RevisionView, SidebarView, StoresView) from .runtime_info import RuntimeInfo @@ -771,7 +771,9 @@ class WireRevision(ReadState): """One observation of every Comms store a view derives from, by name.""" registry: tuple[FileRevision, ...] = revision_source(lambda views: (views.registry.store.path,), - *STATUS_SOURCE, RegistrationsView, NotificationsView) + *STATUS_SOURCE, RegistrationsView, NotificationsView, CommanderView) + todos: tuple[FileRevision, ...] = revision_source(lambda views: views.goals.todos.source_paths(), + StoresView, CommanderView) catalog: tuple[FileRevision, ...] = revision_source(lambda views: (views.channels.catalog.path,), *STATUS_SOURCE, RegistrationsView) bus_log: tuple[FileRevision, ...] = revision_source(lambda views: (views.bus.log.path,), @@ -793,12 +795,12 @@ class WireRevision(ReadState): relationships: tuple[FileRevision, ...] = revision_source(lambda views: (relationships.RelationshipStore.path_in(views.root),), *STATUS_SOURCE) notifications: tuple[FileRevision, ...] = revision_source(lambda views: NotificationAssignment.handling_paths(views.root), - *STATUS_SOURCE, NotificationsView) + *STATUS_SOURCE, NotificationsView, CommanderView) external_transcripts: tuple[FileRevision, ...] = revision_source(lambda views: ExternalTranscript.source_paths(views.root), - StoresView, ExternalTranscriptsView) + StoresView, ExternalTranscriptsView, CommanderView) # Notifications change once per expiry period, not per tick. expiry_tick: int = field(metadata={ - "views": (NotificationsView,), + "views": (NotificationsView, CommanderView), "differs": lambda before, now: ( before // WireRevision.expiry_seconds != now // WireRevision.expiry_seconds)}) diff --git a/src/agent_comms/revision_views.py b/src/agent_comms/revision_views.py index 29e6df6d9b..c5cb141606 100644 --- a/src/agent_comms/revision_views.py +++ b/src/agent_comms/revision_views.py @@ -36,6 +36,10 @@ class SidebarView(RevisionView): """Native unread progress updates counts without rebuilding thread presentations.""" +class CommanderView(RevisionView): + """Orders, retained deliveries, recent turns and time-derived commander attention.""" + + class ExternalTranscriptsView(RevisionView): """CLI drafts and finished items, independently of coordination status.""" diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 2ac7f86eff..1806cf371a 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -329,6 +329,15 @@ class ProgressSource: def since(self) -> float: return self.todo.order.at.recorded_at("An order") + def progress(self) -> OrderProgress: + """One selection algorithm for measured tools and read-only status views.""" + members = sorted(OrderProgress.members_with(FoundProgress), key=lambda member: member.precedence) + try: + return next((progress for member in members for progress in member.found(self)), + NoCommitProgress()) + except subprocess.TimeoutExpired as unread: + return UnreadProgress(str(unread)) + def requests(self, state: str) -> list[PullRequest]: """The order's explicit PR references survive checkout removal or branch reuse.""" return [pr for pr in self.pulls.listed(self.todo.repo, state, self.todo.order) @@ -372,6 +381,14 @@ def distance(self, base: str, sha: str) -> int: return check.result.distance(run) + self.todo.order.untouched(touched) +@dataclass(frozen=True) +class StatusProgressSource(ProgressSource): + """A display reads git/PR status, never executes the order's acceptance check.""" + + def readings(self) -> DistanceReadings: + return DistanceReadings() + + @dataclass(frozen=True) class OrderProgress(DeclaredFamily, affix="Progress"): """Where an order's work stands, derived on each read from its unit's checkout and PRs. @@ -383,13 +400,7 @@ def read(cls, todo: Todo, storage: StorageRule, pulls: PullRequests) -> OrderPro """The first found member by its declared precedence; with none found, no commit yet.""" owner = todo.holder assert owner, "Only a held order has progress." - source = ProgressSource(todo, storage.order_checkout(owner, todo.id), pulls) - members = sorted(cls.members_with(FoundProgress), key=lambda member: member.precedence) - try: - return next((progress for member in members for progress in member.found(source)), - NoCommitProgress()) - except subprocess.TimeoutExpired as unread: - return UnreadProgress(str(unread)) + return ProgressSource(todo, storage.order_checkout(owner, todo.id), pulls).progress() @property @abstractmethod @@ -2148,6 +2159,11 @@ class TodoStore(TypedStore): filename: ClassVar[str] = "todos.sqlite3" rows: ClassVar[tuple[type[TypedTable], ...]] = (Todo, HuntRecord) + def source_paths(self) -> tuple[Path, ...]: + """Both committed SQLite locations can change an observed order.""" + path = self.path + return path, path.with_name(path.name + "-wal") + def record_hunt(self, commander: ThreadIncarnation, hunt: Hunt) -> HuntRecord: hunt.require_commander(self, commander) recorded = HuntRecord(uuid4().hex, commander, hunt) diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py new file mode 100644 index 0000000000..6081b5d6e5 --- /dev/null +++ b/src/agent_comms/ui_model/commander.py @@ -0,0 +1,195 @@ +"""Read-only commander cards, derived from Core's order and transcript owners. + +The observer prepares these models. A frontend supplies only commander identity +and expansion interest; it never reads stores, runs checks or decides order state. +""" +from __future__ import annotations + +import time +from abc import abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +from agent_comms.declared_family import DeclaredFamily +from agent_comms.notification_progress import FailedProgress +from agent_comms.presentation import NoRevision, ReadState, WireRevision +from agent_comms.revision_views import CommanderView +from agent_comms.thread_identity import ThreadIncarnation +from agent_comms.todos import OrderProgress, PullRequests, StatusProgressSource, Todo, UnreadProgress +from agent_comms.turn_digests import LastTurns, ThreadTurns +from agent_comms.ui_model.observation import ( + InterestRequest, Observation, ObservationRequest, Observed, + StoredRead, UnreadableHistory, +) + +if TYPE_CHECKING: + from collections.abc import Iterator + from agent_comms.comms import Comms + from agent_comms.ui_model.observation import ObservationService, ObservationStep + + +@dataclass(frozen=True) +class CommanderOrder: + todo: Todo + progress: OrderProgress + overdue: bool + + @property + def line(self) -> str: + return (f"{'OVERDUE ' if self.overdue else ''}{self.todo.id}: " + f"{' '.join(self.todo.text.split())} | due {self.todo.deadline.clock()} | " + f"{self.todo.order.line}; {self.progress.state}") + + +class CardDetail: + @property + def text(self) -> str: + return "Expand to read this unit's orders and recent turns." + + +@dataclass(frozen=True) +class OpenCardDetail(CardDetail): + orders: tuple[CommanderOrder, ...] + turns: ThreadTurns + + @property + def text(self) -> str: + lines = [order.todo.order.instructions(order.todo) + "\nLatest work: " + order.progress.state + for order in self.orders] + lines.append("Recent turns (newest first):") + for turn in self.turns.turns: + excerpts = tuple(' '.join(imported.body.split())[:240] + for event in turn.events if (imported := event.imported()) is not None) + lines.append(f"{turn.turn_id}: " + " · ".join(excerpts)[:400]) + if not self.turns.turns: + lines.append("No recorded turns.") + return "\n\n".join(lines) + + +@dataclass(frozen=True) +class UnitCard: + unit: ThreadIncarnation + orders: tuple[CommanderOrder, ...] + detail: CardDetail + + @property + def line(self) -> str: + return self.unit.name + " | " + " ; ".join(order.line for order in self.orders) + + @property + def priority(self) -> tuple[bool, float, str]: + return (not any(order.overdue for order in self.orders), + min(order.todo.deadline.recorded_at("An order deadline") for order in self.orders), + self.unit.name.casefold()) + + +@dataclass(frozen=True) +class CommanderAlarm: + line: str + + +class CommanderAlarmSource(DeclaredFamily, affix="AlarmSource"): + """New retained alarm sources extend the model, never the page's rendering. + + The retained pre-send refusal owner can declare a source here when it lands. + Failed delivery is deliberately not described as a pre-send refusal. + """ + + @classmethod + @abstractmethod + def read(cls, comms: Comms, commander: ThreadIncarnation, + orders: tuple[CommanderOrder, ...], now: float) -> tuple[CommanderAlarm, ...]: ... + + +class StalledOrderAlarmSource(CommanderAlarmSource): + @classmethod + def read(cls, comms, commander, orders, now): + return tuple(CommanderAlarm( + f"STALLED {order.todo.held().owner} {order.todo.id}: {order.progress.state}") + for order in orders if order.todo.pending + and order.progress.stalled(order.todo.order.at.recorded_at("An order"), now)) + + +class FailedDeliveryAlarmSource(CommanderAlarmSource): + @classmethod + def read(cls, comms, commander, orders, now): + presentation = comms.views.thread_presentations((commander.name,)).require(commander.name) + return tuple(CommanderAlarm( + f"RECORDED FAILED DELIVERY to {notice.recipient}: {notice.label}") + for notice in presentation.sent_notifications if isinstance(notice.state, FailedProgress)) + + +@dataclass(frozen=True) +class CommanderModel: + commander: ThreadIncarnation + units: tuple[UnitCard, ...] + alarms: tuple[CommanderAlarm, ...] + + @classmethod + def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: + if Path(request.root).resolve() != comms.root.resolve(): + raise ValueError("The commander page belongs to a different Comms root; reopen it.") + commander = comms.registry.require(request.commander.name).incarnation + if commander != request.commander: + raise ValueError("The commander incarnation changed; reopen the page.") + todos = comms.goals.todos.held_for_commander(commander) + pulls, storage, now = PullRequests(todos), comms.registry.storage_rule, time.time() + grouped: dict[ThreadIncarnation, list[CommanderOrder]] = {} + for todo in todos: + unit = todo.held().owner_identity + try: + progress = StatusProgressSource( + todo, storage.order_checkout(unit.name, todo.id), pulls).progress() + except ValueError as error: + progress = UnreadProgress(str(error)) + grouped.setdefault(unit, []).append(CommanderOrder(todo, progress, todo.overdue(now))) + cards = [] + for unit, orders in grouped.items(): + if comms.registry.require(unit.name).incarnation != unit: + raise ValueError(f"Order holder {unit.name} changed incarnation; refresh its assignment.") + current = tuple(orders) + detail = (OpenCardDetail(current, comms.transcripts.turns(unit.name, LastTurns(3))) + if unit in request.expanded else CardDetail()) + cards.append(UnitCard(unit, current, detail)) + orders = tuple(order for card in cards for order in card.orders) + alarms = tuple(alarm for source in CommanderAlarmSource.members_with(CommanderAlarmSource) + for alarm in source.read(comms, commander, orders, now)) + return cls(commander, tuple(sorted(cards, key=lambda card: card.priority)), alarms) + + +@dataclass(frozen=True) +class ObserveCommander(InterestRequest): + root: str + commander: ThreadIncarnation + expanded: frozenset[ThreadIncarnation] = frozenset() + + def observation(self) -> CommanderReading: + return CommanderReading(self) + + +class CommanderReading(Observation[ObserveCommander]): + def __init__(self, request: ObserveCommander): + super().__init__(request) + self.revision: ReadState = NoRevision() + + def read(self, comms: Comms, revision: WireRevision) -> Iterator[ObservationStep]: + if revision.changed_since(CommanderView(), self.revision): + model = UnreadableHistory.outcome(lambda: CommanderModel.read(comms, self.request)) + # Contention is retried on the next observer turn, not published as an empty board. + ready = model.answer(lambda value: True, lambda: False, lambda error: True) + if ready: + self.revision = revision + yield CommanderObserved(revision, self.request, model) + + +@dataclass(frozen=True) +class CommanderObserved(Observed): + request: ObserveCommander + model: StoredRead[CommanderModel] + + +@dataclass(frozen=True) +class StopCommander(ObservationRequest): + def receive(self, service: ObservationService) -> None: + service.interest.remove(CommanderReading) diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py new file mode 100644 index 0000000000..f8092451b9 --- /dev/null +++ b/tests/test_commander_model.py @@ -0,0 +1,54 @@ +"""Real Core stores back commander models; the page never executes an order check.""" +from dataclasses import replace + +import pytest + +from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, OpenCardDetail +from agent_comms.todos import ProgressSource, PullRequests, StatusProgressSource +from test_commander_problem_orders import create + + +def test_board_uses_order_state_and_requests_details_only_when_opened(commander): + comms, caller = commander + lead = comms.registry.require(caller.name).incarnation + unit = comms.registry.require('worker').incarnation + create(commander, 'P1', deadline=1000) + create(commander, 'P2', deadline=2000) + request = ObserveCommander(str(comms.root), lead) + model = CommanderModel.read(comms, request) + assert len(model.units) == 1 + card, = model.units + assert card.unit == unit + assert 'P1' in card.line and 'P2' in card.line and 'OVERDUE' in card.line + assert 'printf' not in card.line and 'Repair the existing owner' not in card.detail.text + assert len(model.alarms) == 2 + opened = CommanderModel.read(comms, replace(request, expanded=frozenset((unit,)))) + assert isinstance(opened.units[0].detail, OpenCardDetail) + assert 'Repair the existing owner' in opened.units[0].detail.text + assert 'Latest work:' in opened.units[0].detail.text + assert 'Recent turns' in opened.units[0].detail.text + + +def test_status_source_never_requests_acceptance_measurement(commander, monkeypatch): + comms, caller = commander + todo = create(commander, 'P1') + def forbidden(self): + pytest.fail('A read-only commander page executed an acceptance measurement') + monkeypatch.setattr(ProgressSource, 'readings', forbidden) + # The read-only source owns the no-measurement capability, not each frontend. + source = StatusProgressSource(todo, comms.root, PullRequests((todo,))) + assert not source.readings().readings + model = CommanderModel.read(comms, ObserveCommander( + str(comms.root), comms.registry.require(caller.name).incarnation)) + assert model.units[0].orders[0].progress.state == 'no commit since the order' + + +def test_commander_read_rejects_root_and_incarnation_changes(commander): + comms, caller = commander + lead = comms.registry.require(caller.name).incarnation + request = ObserveCommander(str(comms.root / 'elsewhere'), lead) + with pytest.raises(ValueError, match='different Comms root'): + CommanderModel.read(comms, request) + with pytest.raises(ValueError, match='incarnation changed'): + CommanderModel.read(comms, replace(request, root=str(comms.root), + commander=replace(lead, created_at=lead.created_at + 1))) From 897909e9544ef55f6aafe7df88ad001dcf8bd68e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:04:32 -0400 Subject: [PATCH 03/20] Read actual checkout commits only for opened commander cards --- src/agent_comms/todos.py | 6 +- src/agent_comms/ui_model/commander.py | 45 ++++++--- tools/commander_page_journey.py | 135 ++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 tools/commander_page_journey.py diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 1806cf371a..579287f674 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -335,7 +335,7 @@ def progress(self) -> OrderProgress: try: return next((progress for member in members for progress in member.found(self)), NoCommitProgress()) - except subprocess.TimeoutExpired as unread: + except (ValueError, subprocess.TimeoutExpired) as unread: return UnreadProgress(str(unread)) def requests(self, state: str) -> list[PullRequest]: @@ -361,6 +361,10 @@ def git(self, *args: str) -> str: raise ValueError(run.stderr.strip() or f"git {args[0]} failed") return run.stdout + def latest_commit(self) -> str: + """The checkout's latest actual commit, even when it predates this order.""" + return self.git("log", "-1", "--format=%h %s").strip() + def readings(self) -> DistanceReadings: """The order's base, then each branch commit since the order; the last two are read.""" base = self.git("merge-base", "HEAD", "origin/HEAD").strip() diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 6081b5d6e5..f3d8f63c5f 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -16,15 +16,20 @@ from agent_comms.presentation import NoRevision, ReadState, WireRevision from agent_comms.revision_views import CommanderView from agent_comms.thread_identity import ThreadIncarnation -from agent_comms.todos import OrderProgress, PullRequests, StatusProgressSource, Todo, UnreadProgress +from agent_comms.todos import OrderProgress, PullRequests, StatusProgressSource, Todo from agent_comms.turn_digests import LastTurns, ThreadTurns from agent_comms.ui_model.observation import ( - InterestRequest, Observation, ObservationRequest, Observed, - StoredRead, UnreadableHistory, + InterestRequest, + Observation, + ObservationRequest, + Observed, + StoredRead, + UnreadableHistory, ) if TYPE_CHECKING: from collections.abc import Iterator + from agent_comms.comms import Comms from agent_comms.ui_model.observation import ObservationService, ObservationStep @@ -52,11 +57,13 @@ def text(self) -> str: class OpenCardDetail(CardDetail): orders: tuple[CommanderOrder, ...] turns: ThreadTurns + latest_commits: tuple[str, ...] @property def text(self) -> str: - lines = [order.todo.order.instructions(order.todo) + "\nLatest work: " + order.progress.state - for order in self.orders] + lines = [order.todo.order.instructions(order.todo) + + "\nLatest work: " + order.progress.state for order in self.orders] + lines.append("Latest checkout commits:\n" + "\n".join(self.latest_commits)) lines.append("Recent turns (newest first):") for turn in self.turns.turns: excerpts = tuple(' '.join(imported.body.split())[:240] @@ -117,7 +124,8 @@ def read(cls, comms, commander, orders, now): presentation = comms.views.thread_presentations((commander.name,)).require(commander.name) return tuple(CommanderAlarm( f"RECORDED FAILED DELIVERY to {notice.recipient}: {notice.label}") - for notice in presentation.sent_notifications if isinstance(notice.state, FailedProgress)) + for notice in presentation.sent_notifications + if isinstance(notice.state, FailedProgress)) @dataclass(frozen=True) @@ -138,19 +146,28 @@ def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: grouped: dict[ThreadIncarnation, list[CommanderOrder]] = {} for todo in todos: unit = todo.held().owner_identity - try: - progress = StatusProgressSource( - todo, storage.order_checkout(unit.name, todo.id), pulls).progress() - except ValueError as error: - progress = UnreadProgress(str(error)) + progress = StatusProgressSource( + todo, storage.order_checkout(unit.name, todo.id), pulls).progress() grouped.setdefault(unit, []).append(CommanderOrder(todo, progress, todo.overdue(now))) cards = [] for unit, orders in grouped.items(): if comms.registry.require(unit.name).incarnation != unit: - raise ValueError(f"Order holder {unit.name} changed incarnation; refresh its assignment.") + raise ValueError( + f"Order holder {unit.name} changed incarnation; refresh its assignment.") current = tuple(orders) - detail = (OpenCardDetail(current, comms.transcripts.turns(unit.name, LastTurns(3))) - if unit in request.expanded else CardDetail()) + detail: CardDetail = CardDetail() + if unit in request.expanded: + commits = [] + for order in current: + source = StatusProgressSource(order.todo, storage.order_checkout( + unit.name, order.todo.id), pulls) + try: + commit = source.latest_commit() + except ValueError as error: + commit = f"unavailable: {error}" + commits.append(f"{order.todo.id}: {commit}") + detail = OpenCardDetail(current, comms.transcripts.turns(unit.name, LastTurns(3)), + tuple(commits)) cards.append(UnitCard(unit, current, detail)) orders = tuple(order for card in cards for order in card.orders) alarms = tuple(alarm for source in CommanderAlarmSource.members_with(CommanderAlarmSource) diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py new file mode 100644 index 0000000000..412db1c0bd --- /dev/null +++ b/tools/commander_page_journey.py @@ -0,0 +1,135 @@ +"""Physical Toad journey on a scratch bus, using copies of real saved unit sessions. + +Run --prepare once, then run this file inside st on an isolated Xvfb display. +No provider is started, no synthetic transcript is written, and the original +sessions/registry are only read. Source and capture paths are recorded explicitly. +""" +from __future__ import annotations + +import argparse +import asyncio +import os +import subprocess +import time +from dataclasses import replace +from pathlib import Path + +from agent_comms.active_route import LocalRoute +from agent_comms.change_report import FileLine +from agent_comms.child_process import NoProcess +from agent_comms.comms import Comms +from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction +from agent_comms.goals import CountCheckResult, NoGoal, OrderCheck, RequestGoalOrder +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.thread_status import StoppedThreadStatus +from agent_comms.todos import MechanicalOrderRun, PendingProblemOrder +from agent_comms.tool_caller import ThreadCaller +from agent_comms.tools import CommsTodoCreateTool + +COMMANDER = "claude-code" +UNITS = ("pi-cn-phase2", "pi-cn-phase5") + + +def prepare(output: Path) -> None: + live = Comms(LocalRoute.default()) + root = output / "bus" + if root.exists(): + raise ValueError("Scratch bus already exists; preserve it and choose another output path.") + root.mkdir(parents=True) + comms = Comms(root) + receipts = [] + for name in (COMMANDER, *UNITS): + original = live.registry.require(name) + session = original.session_file + if session: + source = Path(session) + # Freeze a complete-line cut of the real saved session, not manufactured events. + with source.open("rb") as stream: + captured = stream.read(os.fstat(stream.fileno()).st_size) + copied = output / f"{name}.jsonl" + copied.write_bytes(captured.rsplit(b"\n", 1)[0] + b"\n") + copied.chmod(0o600) + session = str(copied) + receipts.append(f"{name}: {source} -> {copied} ({copied.stat().st_size} bytes)") + comms.registry.declare(replace(original, process_identity=NoProcess(), + session_file=session, goal=NoGoal()), StoppedThreadStatus()) + comms.goals.update_goal(COMMANDER, SetGoalAction( + text="Verify the commander page on copied real unit sessions", goal_id="page-request", + order=RequestGoalOrder())) + now = time.time() + for index, name in enumerate(UNITS): + comms.goals.update_goal(name, SetGoalAction(text="Copied unit for read-only page verification")) + order = PendingProblemOrder( + id=f"commander-page-{name}", owner=FileLine(__file__, 1), + check=OrderCheck(str(output), "printf 1", CountCheckResult(1)), + edits=("Render the canonical order owner without copying its state",), + banned=("Do not launch a provider or change the original saved session",), + report="Report the actual visible commander page and its expanded unit card", + at=RecordedEventTime(now - 1200 if not index else now), + first_call_deadline=RecordedEventTime(now + 120), run=MechanicalOrderRun()) + CommsTodoCreateTool(owner=name, order=CreateProblemGoalAction( + problem=f"P{index + 1}", order=order, request="page-request", + deadline=RecordedEventTime(now - 60 if not index else now + 1800), commander=COMMANDER, + repo="OpenHCSDev/agent-comms", text="Show the real order in the commander page", + )).apply(comms, ThreadCaller(COMMANDER)) + (output / "sources.txt").write_text("\n".join(receipts) + "\n") + + +async def journey(app, pilot, output: Path) -> None: + from textual.widgets import Static + from toad.navigation_target import FeedTarget + from toad.screens.commander import CommanderScreen, CommanderUnit + from toad.widgets.conversation import Conversation + + await pilot.pause(2) + # Open a real bus conversation and submit through its actual local-command boundary. + await app.screen.open_sidebar_target(FeedTarget()) + conversation = app.screen.query_one(Conversation) + await pilot.pause(1) + conversation.prompt.text = "/commander " + COMMANDER + conversation.prompt.focus() + await pilot.press("enter") + async with asyncio.timeout(90): + while not isinstance(app.screen, CommanderScreen) or len(app.screen.rows) != len(UNITS): + await pilot.pause(0.2) + screen = app.screen + await pilot.pause(1) + def capture(stage: str) -> None: + # The text is from the actual laid-out terminal strips, not the model. + text = "\n".join(screen.render_line(y).text for y in range(screen.size.height)) + (output / f"{stage}.txt").write_text(text + "\n") + subprocess.run(("import", "-window", "root", str(output / f"{stage}.png")), check=True) + capture("level-0") + card = next(row for row in screen.query(CommanderUnit) if row.unit.name == UNITS[0]) + await pilot.click(card.query_one("CollapsibleTitle")) + async with asyncio.timeout(90): + while "Latest checkout commits:" not in str(card.query_one(Static).content): + await pilot.pause(0.2) + await pilot.pause(1) + capture("level-1") + # Verify collapse/return does not replace the underlying conversation or its draft. + await pilot.press("escape") + assert app.screen.query_one(Conversation) is conversation + (output / "result.txt").write_text("PASS: normal Toad, real scratch orders, level 0 and expanded card.\n") + app.exit() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--prepare", action="store_true") + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + if args.prepare: + prepare(args.output) + return + os.environ["AGENT_COMMS_ROOT"] = str(args.output / "bus") + from toad.app import ToadApp + app = ToadApp(project_dir=str(args.output)) + app.run(auto_pilot=lambda pilot: journey(app, pilot, args.output)) + if app._exception: + raise app._exception + + +if __name__ == "__main__": + main() From ec5f94568f2c1dbff05b0e2eb0ac6ae63789f9b6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:08:21 -0400 Subject: [PATCH 04/20] Verify real order records and delivery states through the CLI --- docs/evidence/cn-phase3-record-results.jsonl | 12 ++ docs/evidence/cn-phase3-record.md | 73 +++++++++- src/agent_comms/todos.py | 25 ++-- tests/test_commander_problem_orders.py | 5 +- tests/test_delivery_merge.py | 30 ++++ tests/test_standing_rules.py | 6 +- tools/proofs/cn_phase3_record.py | 144 +++++++++++++++++++ 7 files changed, 277 insertions(+), 18 deletions(-) create mode 100644 docs/evidence/cn-phase3-record-results.jsonl create mode 100644 tests/test_delivery_merge.py create mode 100644 tools/proofs/cn_phase3_record.py diff --git a/docs/evidence/cn-phase3-record-results.jsonl b/docs/evidence/cn-phase3-record-results.jsonl new file mode 100644 index 0000000000..a525dd2535 --- /dev/null +++ b/docs/evidence/cn-phase3-record-results.jsonl @@ -0,0 +1,12 @@ +{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":2,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":2,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} +{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":0} +{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":3,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}}],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":1} +{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":4,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":2} +{"todos":[{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":5,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]}],"progress":{}} +{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":6,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[{"kind":"void_strike_todo","expected_revision":5,"actor":{"name":"record-lead","created_at":1792432385.1369143},"strike":{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}},"reason":"The second strike repeated the first finding rather than describing another failed change","at":{"kind":"recorded","at":1792432680.216849}}],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":0} +{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":3,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":3,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} +{"todo":{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792431763.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"5cdac561d498ece25142a8d2e5cf9b06002b6c8a","merge":{"kind":"main"}}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} +{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":4,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":4,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} +{"todo":{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792432302.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"e0ca01500","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} +{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":5,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":5,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P4","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P4","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433118.0605557},"first_call_deadline":{"kind":"recorded","at":1792433238.0605557},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436718.0605557},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} +{"error":"Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue.","type":"failed"} diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md index 8bbc7a4dee..f9f9efc930 100644 --- a/docs/evidence/cn-phase3-record.md +++ b/docs/evidence/cn-phase3-record.md @@ -9,9 +9,14 @@ Baseline: 5cdac561d498ece25142a8d2e5cf9b06002b6c8a. - [x] Confirm existing verdict, strike limit, fire and void owners on main. - [x] Add delivery integration to the canonical checked delivery. - [x] Decode PR query rows into named records, not positional tuples. -- [ ] Confirm real scratch-bus order, verdict, strikes, fire and void through Core tools. -- [ ] Confirm delivery through the CLI and run bounded local checks. -- [ ] Publish a reviewed draft PR; do not merge or install. +- [x] Confirm real scratch-bus order, verdict, strikes, explicit fire and void through Core tools. +- [x] Confirm main/unmerged delivery and unlinked refusal through the CLI. +- [x] Pass the two bounded record/codec sanity checks. Pytest is unavailable. +- [x] Publish draft PR #1935; do not merge or install. +- [ ] Automatic firing at the strike limit, with all strike sources in the same write. +- [ ] Retain original typed transitions and the state each operation replaces. +- [ ] Connect commander board/done rendering after the recent shared-function overlap is settled. +- [ ] Verify a real order merged to a non-main target (codec checked, not live-proven). ## Source relationships @@ -22,7 +27,8 @@ decoded a positional tuple of seven unrelated fields. `DeliveredProblemOrder.lin After: completion passes its canonical Todo and registry to the delivery owner. A checked commit records a `DeliveryMerge`: main, another PR target, or linked but unmerged source branches. `DeliveredProblemOrder.line` derives its text from that delivery. `WorkBranches` still owns the -open-PR/open-issue branch-link rule. PR lookup still uses the immutable order reference, now decoded +open-PR/open-issue branch-link rule. `PullRequests.listed` now owns exact order-reference matching; +`ProgressSource` no longer repeats that identity decision. PR lookup still uses the immutable order reference, now decoded through named query records including the target branch. No verdict, strike, delivery or branch store was added. The integration is the observation at completion, not a later claim of installation. @@ -49,4 +55,61 @@ Those functions are left untouched pending the integration owner's conflict deci `tools/agent_resource_check.py --assert-headroom` warned: about 4 GiB available RAM and 3.8 GiB free on /tmp. No parallel agents or large suite. The prescribed /home/ts/bin launcher is absent. -Owned verification state and logs will remain under this checkout's evidence directory, not /tmp. +Owned verification state is `/home/ts/wt/pi-cn-phase3--P1/.proof-record-plane` (real scratch bus, +copy of this unit's native transcript and temporary check trees). Logs are the checkout's `.proof-*.log`; +`.proof-test-state` is the empty intended pytest scratch directory. The check commands set TMPDIR +under the owned persistent root. These paths remain for the unfinished review; remove them after +acceptance, preserving the concise receipt below. The proof does not own other agents' /tmp worktrees. + +## Actual CLI proof + +Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (12 actual responses, +normalized only for whitespace). Driver: `tools/proofs/cn_phase3_record.py`. The driver initializes +real Core stores and calls `python -m agent_comms.cli tool --from ...`. It uses no fake Comms, +fixture events or model replies. Judgment names a real edit from this unit's native session, +read through `OrderEditsRead` after copying the session to the scratch root. No original session +was changed and no live unit received these deliberate scratch-only strikes. + +| Actual operation | P1 revision | verdicts | retained strikes | voids | assignment | +|---|---:|---:|---:|---:|---| +| create | 1 | 0 | 0 | 0 | record-worker | +| accepted verdict | 2 | 1 | 0 | 0 | record-worker | +| first strike | 3 | 1 | 1 | 0 | record-worker | +| second strike | 4 | 1 | 2 | 0 | record-worker | +| explicit comms_fire | 5 | 1 | 2 | 0 | null | +| void second strike | 6 | 1 | 2 | 1 | null | + +After voiding, Core's `live_strikes(record-worker)` returns one. A void does not restore a released +assignment. The tool's assignee-only `strike_count` is zero once there is no assignment; the original +strikes and void are still retained on the Todo. + +P2's real completion returned `todo.order.delivery.merge = {"kind":"main"}`. P3's real completion +on this published draft branch returned `{"kind":"unmerged","branches":["feat/cn-phase3-record"]}`. +P4 used an existing published unlinked commit, without editing or publishing that branch. Core refused: + +``` +Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue. +``` + +P4 remained pending at revision 1. The check can still leave its normal Git check note even when +completion is refused. These are source-CLI checks of the candidate, not an installed runtime claim. +Nothing was merged, published as a runtime, restarted or installed. + +## Local checks and precise blockers + +- The requested background `timeout 300 python -X faulthandler -m pytest ...` failed before + collection: the shared interpreter has no pytest. An isolated-venv attempt was refused by the + existing StorageRule; no alternate installation was attempted. A shared test interpreter with + pytest, or a reviewed dev-dependency installation by its owner, is needed for the suite. +- The two standalone record/codec sanity functions passed under the same bounded/background + command form. They check every DeliveryMerge member and named external non-main target decoding; + they are not substituted for the actual CLI proof. +- The real second-strike response still held P1. Main has a limit predicate and an explicit fire tool, + not automatic firing. The shared TodoStore/Goals write-path work overlaps recent #1878 and is raised + with claude-code. Source inspection also finds destructive order/assignment replacement, missing Core + recording timestamps and direct fold/correction writes. pi-cn-phase3 retains this implementation. +- pi-cn-phase5 owns SendBackVerdict/Goals.send_back publication, OrderPost rows/insertion and the + canonical order channel. Those changes must remain in the same transaction as retained operations; + this checkpoint does not edit their classes or TodoStore.rows/apply. Their exact published commit + is a dependency for that continuing write-path change. + diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 6e935866d1..f6aee47505 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -77,6 +77,10 @@ class OrderDelivery(DeclaredFamily, affix="Delivery"): def work(self) -> str: return "delivered work" + @property + def line(self) -> str: + return self.work + async def run(self, check, *command): result = await BoundedRun.run(tuple(command), cwd=check.directory, timeout=120) if result.outcome != ExitedOutcome(0): @@ -174,8 +178,8 @@ async def require_source(self, check: OrderCheck) -> None: f"Delivered commit {self.delivered} is on no origin branch; push it first.") async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: - merge = await self.merge_state(todo, registry) verified = await super().verified(todo, registry) + merge = await self.merge_state(todo, registry) return CheckedCommitDelivery(commit=self.commit, delivered_commit=self.delivered, at=verified.at, evidence=self.evidence, merge=merge) @@ -259,12 +263,16 @@ class CheckedCommitDelivery(CommitDelivery): merge: DeliveryMerge @property - def work(self) -> str: - return f"{super().work}; {self.merge.line}" + def line(self) -> str: + return f"{self.work}; {self.merge.line}" @dataclass(frozen=True, kw_only=True) class OperationDelivery(OrderDelivery): + @property + def line(self) -> str: + return "operation checked" + async def require_source(self, check): pass @@ -284,12 +292,12 @@ class PullRequest: merged_at: float | None = None updated_at: float = 0.0 orders: tuple[str, ...] = () - base: str = field(kw_only=True) + base: str = field(default=WorkBranches.main, kw_only=True) @classmethod @boundary_decoder def listed(cls, number: int, branch: str, head: str, merge: str, merged: str, - updated: str, body: str, base: str) -> Self: + updated: str, body: str, base: str = WorkBranches.main) -> Self: """One row of ``gh pr list``; GitHub leaves an unmerged PR's merge commit and time empty.""" return cls(int(number), branch, head, merge or None, _github_time(merged) if merged else None, _github_time(updated), @@ -338,7 +346,7 @@ def listed(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullReques key = (repo, state, order.id) if key not in self._read: self._read[key] = self.read(repo, state, order) - return self._read[key] + return tuple(request for request in self._read[key] if order.id in request.orders) def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, ...]: orders = self._orders.setdefault(repo, {}) @@ -448,7 +456,7 @@ def since(self) -> float: def requests(self, state: str) -> list[PullRequest]: """The order's explicit PR references survive checkout removal or branch reuse.""" return [pr for pr in self.pulls.listed(self.todo.repo, state, self.todo.order) - if self.todo.order.id in pr.orders and pr.updated_at >= self.since] + if pr.updated_at >= self.since] def commits(self, *excluded: str) -> Iterator[tuple[str, float]]: """The checkout's newest commit since the order that no ``excluded`` ref already holds.""" @@ -1413,9 +1421,10 @@ class DroppedProblemOrder(UnorderedProblemOrder, CheckedProblemOrder): class DeliveredProblemOrder(CheckedProblemOrder): delivery: OrderDelivery pending: ClassVar[bool] = False + @property def line(self) -> str: - return f"Delivered: {self.delivery.work}, waiting on install" + return f"Delivered: {self.delivery.line}, waiting on install" def require_transfer(self, todo: Todo) -> None: owner = (todo.assignment or todo.released[-1].previous).owner diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index f9cec65105..bc306d8d75 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -234,7 +234,7 @@ def git(*args): git('init', '-q', '-b', 'main') git('commit', '-q', '--allow-empty', '-m', 'Delivered fix') commit = git('rev-parse', 'HEAD') - git('update-ref', 'refs/remotes/origin/fix', commit) + git('update-ref', 'refs/remotes/origin/main', commit) marker = comms.root / 'check-calls' create(commander, 'P1', f"printf x >> '{marker}'; printf 1") create(commander, 'P2') @@ -288,6 +288,7 @@ def git(*args, at=None): with pytest.raises(ValueError, match='no origin branch'): CommsOrderDoneTool(problem='P1', order_id=comms.goals.todos.get('P1').order.id, delivery=CommitDelivery(commit=merge, delivered_commit=delivered), claims=working(comms)).apply(comms, caller) git('update-ref', 'refs/remotes/origin/work', delivered) + git('update-ref', 'refs/remotes/origin/main', merge) script.done('P1', delivered, seen=working(comms)[0].screenshot) met = comms.goals.todos.get('P1') assert (met.order.delivery.commit, met.order.delivery.delivered, met.order.delivery.at) == ( @@ -1296,7 +1297,7 @@ def test_a_problem_goal_shows_only_its_live_orders_through_convert_deliver_send_ todo.assignment, 'toolu_3', reason=StrikeReason(why))).apply(comms, caller) worker = comms.registry.require('worker') progress = GoalSegment.capture(worker, worker.goal, comms.goals).progress - assert progress.splitlines() == ['P1: Delivered, waiting on install', f'P2: Open · Sent back: {why}'] + assert progress.splitlines() == ['P1: Delivered: operation checked, waiting on install', f'P2: Open · Sent back: {why}'] assert (worker.goal.text, worker.goal.progress) == (ProblemGoalOrder.text, '') with pytest.raises(ValueError, match='block the problem.s Todo, or standby with wait_for'): comms.goals.update_goal('worker', BlockedGoalAction(block_reason='Waiting on lead.'), actor=ModelInvocable) diff --git a/tests/test_delivery_merge.py b/tests/test_delivery_merge.py new file mode 100644 index 0000000000..45f060de7c --- /dev/null +++ b/tests/test_delivery_merge.py @@ -0,0 +1,30 @@ +"""Small record/codec checks; the real CLI proof is recorded separately.""" +from agent_comms.field_codec import FieldCodec +from agent_comms.todos import ( + BranchDeliveryMerge, CheckedCommitDelivery, DeliveryMerge, ListedPullRequest, + MainDeliveryMerge, OrderDelivery, UnmergedDeliveryMerge, +) + + +def test_delivery_integration_members_keep_their_data_through_the_only_codec(): + for state in (MainDeliveryMerge(), BranchDeliveryMerge('release', 'b' * 40), + UnmergedDeliveryMerge(('fix/order',))): + delivery = CheckedCommitDelivery(commit='a' * 40, delivered_commit='a' * 40, merge=state) + restored = FieldCodec.decode(OrderDelivery, FieldCodec.encode(delivery)) + assert restored == delivery + assert FieldCodec.decode(DeliveryMerge, FieldCodec.encode(state)) == state + assert state.line in restored.line + assert restored.work == f'delivered commit {"a" * 40}' + + +def test_named_forge_row_preserves_the_actual_nonmain_merge_target(): + row = FieldCodec.decode(ListedPullRequest, FieldCodec.parse('''{ + "number": 42, "branch": "fix/order", "head": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "merge": {"oid": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, + "merged": "2026-10-19T12:00:00Z", "updated": "2026-10-19T12:00:01Z", + "body": "Agent-Comms-Order: proof-order", "base": "release" + }''')) + request = row.parsed() + assert request.base == 'release' + assert request.merge == 'b' * 40 + assert request.orders == ('proof-order',) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index d57aceed9d..976b55a916 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -57,11 +57,11 @@ def test_board_self_check_uses_order_progress_and_its_linked_merge( owned, pending, foreign = (create(commander, pid) for pid in ("P77", "P109", "P117")) stamp = "1970-01-01T00:08:20Z" merged = PullRequest.listed(1581, "docs/module-layers", "b" * 40, "d" * 40, - stamp, stamp, owned.order.pull_request_body("Module layer map")) + stamp, stamp, owned.order.pull_request_body("Module layer map"), 'main') opened = PullRequest.listed(1624, "ratchet/none-absence", "c" * 40, "", "", - stamp, pending.order.pull_request_body("Declared measure")) + stamp, pending.order.pull_request_body("Declared measure"), 'main') other = PullRequest.listed(1629, "ratchet/decoder-marker", "e" * 40, "f" * 40, - stamp, stamp, "Agent-Comms-Order: another-order\n") + stamp, stamp, "Agent-Comms-Order: another-order\n", 'main') def forge(self, repo, state, order): return (merged, other) if state == "merged" else (opened,) diff --git a/tools/proofs/cn_phase3_record.py b/tools/proofs/cn_phase3_record.py new file mode 100644 index 0000000000..d8577e3503 --- /dev/null +++ b/tools/proofs/cn_phase3_record.py @@ -0,0 +1,144 @@ +"""Exercise real Core records on an isolated bus; never start or replay a model turn. + +Run setup, perform a real tool edit in the owning native session, then run record with that +session path. All observations below are CLI tool results, not hand-constructed events. +""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import time + +from agent_comms.change_report import FileLine +from agent_comms.comms import Comms +from agent_comms.field_codec import FieldCodec +from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction +from agent_comms.goals import CountCheckResult, OrderCheck, RequestGoalOrder +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.tools import (CommsFireTool, CommsOrderDoneTool, CommsTodoCreateTool, + CommsTodoTool, CommsVerdictTool) +from agent_comms.todos import (AcceptedVerdict, CommitDelivery, MechanicalOrderRun, + PendingProblemOrder, StrikeReason, StrikeTodo, VoidStrikeTodo) +from agent_comms.progress_claims import MergedClaim + +CHECKOUT = Path(__file__).resolve().parents[2] +ROOT = CHECKOUT / ".proof-record-plane" +RECEIPT = CHECKOUT / "docs/evidence/cn-phase3-record-results.jsonl" + + +def cli(caller, tool): + arguments = FieldCodec.encode(tool) + del arguments[tool.family_discriminator] + command = [sys.executable, "-m", "agent_comms.cli", "--root", str(ROOT), "tool", + "--from", caller, "--tool", tool.declared_name, + "--arguments", json.dumps(arguments)] + result = subprocess.run(command, capture_output=True, text=True, timeout=180, + env=os.environ | {"PYTHONPATH": str(CHECKOUT / "src")}) + print(result.stdout, end="", flush=True) + if result.returncode: + raise RuntimeError(result.stderr or result.stdout) + with RECEIPT.open("a") as output: + output.write(result.stdout) + return json.loads(result.stdout) + + +def setup(): + if ROOT.exists(): + raise RuntimeError(f"Refusing to reuse an existing proof root: {ROOT}") + comms = Comms(ROOT) + comms.messaging.initialize_private_initial_protocol() + for name in ("record-lead", "record-worker"): + comms.registry.declare(comms.registry.new_thread( + name, frozenset(), str(CHECKOUT), created_at=time.time())) + comms.goals.update_goal("record-lead", SetGoalAction( + text="Confirm the order record through the actual Core tools on this isolated bus", + goal_id="record-request", order=RequestGoalOrder())) + comms.goals.update_goal("record-worker", SetGoalAction( + text="Complete the assigned record-plane proof without touching any live session", + goal_id="record-work")) + create_order() + + +def create_order(problem="P1"): + now = time.time() + cli("record-lead", CommsTodoCreateTool(owner="record-worker", order=CreateProblemGoalAction( + problem=problem, repo="OpenHCSDev/agent-comms", + text="Show the real verdict and strike records without a second source", + request="record-request", commander="record-lead", + deadline=RecordedEventTime(now + 3600), order=PendingProblemOrder( + id=f"record-order-{problem}", owner=FileLine(str(CHECKOUT / "src/agent_comms/todos.py"), 69), + check=OrderCheck(str(CHECKOUT), "printf 1", CountCheckResult(1)), + edits=("Decode pull request target branches through the existing record owner",), + banned=("No second order store",), report="Report the actual Core tool response", + run=MechanicalOrderRun(), at=RecordedEventTime(now), + first_call_deadline=RecordedEventTime(now + 120))))) + + +def record(session: Path): + from dataclasses import replace + from agent_comms.turn_digests import OrderEditsRead, TurnsSince + + retained = ROOT / "record-worker.jsonl" + shutil.copy2(session, retained) + comms = Comms(ROOT) + worker = comms.registry.require("record-worker") + comms.registry.register(replace(worker, session_file=str(retained)), + comms.registry.status(worker.name)) + todo = comms.goals.todos.get("P1") + since = todo.order.at.recorded_at("An order") + calls = OrderEditsRead(ROOT, comms.registry.require(worker.name), TurnsSince(since), todo).read() + if not calls: + raise RuntimeError("No actual native edit call followed the order; do not invent one") + judged = calls[0].id + cli("record-lead", CommsVerdictTool(id="P1", operation=AcceptedVerdict( + todo.revision, comms.registry.require("record-lead").incarnation, todo.held(), judged, + quote="Decode pull request target branches"))) + for reason in ("The assigned change left the target branch out of the saved order record", + "The second review found the target branch still missing from the saved record"): + todo = comms.goals.todos.get("P1") + cli("record-lead", CommsTodoTool(id="P1", operation=StrikeTodo( + todo.revision, comms.registry.require("record-lead").incarnation, todo.held(), reason))) + todo = comms.goals.todos.get("P1") + assert todo.struck_out(worker.name) + cli("record-lead", CommsFireTool(unit=worker.name, reason=StrikeReason( + "The unit left the required target branch absent after both recorded reviews"))) + todo = comms.goals.todos.get("P1") + cli("record-lead", CommsTodoTool(id="P1", operation=VoidStrikeTodo( + todo.revision, comms.registry.require("record-lead").incarnation, todo.strikes[-1], + "The second strike repeated the first finding rather than describing another failed change"))) + todo = comms.goals.todos.get("P1") + assert len(todo.strikes) == 2 and len(todo.voided) == 1 + assert len(todo.live_strikes(worker.name)) == 1 and todo.assignment is None + + +def deliver(problem: str, delivered: str, checked: str | None): + create_order(problem) + checked = checked or subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=CHECKOUT, text=True).strip() + cli("record-lead", CommsOrderDoneTool( + problem=problem, order_id=f"record-order-{problem}", + delivery=CommitDelivery(commit=checked, delivered_commit=delivered), + claims=(MergedClaim(delivered),))) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("step", choices=("setup", "order", "record", "deliver")) + parser.add_argument("--session", type=Path) + parser.add_argument("--problem") + parser.add_argument("--delivered") + parser.add_argument("--checked") + args = parser.parse_args() + if args.step == "setup": + setup() + elif args.step == "order": + create_order() + elif args.step == "deliver": + deliver(args.problem, args.delivered, args.checked) + else: + record(args.session) From c446fc345e3379908ee492de138845be13f79ea0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:10:23 -0400 Subject: [PATCH 05/20] Record final candidate checks and outstanding record-plane blockers --- docs/evidence/cn-phase3-record.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md index f9f9efc930..5bfcdef34f 100644 --- a/docs/evidence/cn-phase3-record.md +++ b/docs/evidence/cn-phase3-record.md @@ -32,8 +32,8 @@ open-PR/open-issue branch-link rule. `PullRequests.listed` now owns exact order- through named query records including the target branch. No verdict, strike, delivery or branch store was added. The integration is the observation at completion, not a later claim of installation. -AST evidence used refactor-audit's `Package.load` with Python 3.14: 382 production modules parsed, -zero omissions. The only production `OrderDelivery.verified` caller is `DeliverTodo.prepare`; +Before/after AST evidence used refactor-audit's `Package.load` with Python 3.14: 382 Core production +modules parsed, zero omissions. The final installed-Toad syntax pass parsed 283 modules, zero omissions. The only production `OrderDelivery.verified` caller is `DeliverTodo.prepare`; creation is in `commander.order.done` and `CommsOrderDoneTool`. Goal summaries consume the order's `line`. The installed Toad source has no order-delivery consumer; its unrelated input-delivery types are not this family. A preliminary NRA run under its old Python 3.12 omitted Python 3.14 syntax; @@ -101,7 +101,8 @@ Nothing was merged, published as a runtime, restarted or installed. collection: the shared interpreter has no pytest. An isolated-venv attempt was refused by the existing StorageRule; no alternate installation was attempted. A shared test interpreter with pytest, or a reviewed dev-dependency installation by its owner, is needed for the suite. -- The two standalone record/codec sanity functions passed under the same bounded/background +- Final checks on source checkpoint ec5f94568: Core/Toad AST passes above, the candidate CLI entrypoint, + and both standalone record/codec sanity functions passed under the same bounded/background command form. They check every DeliveryMerge member and named external non-main target decoding; they are not substituted for the actual CLI proof. - The real second-strike response still held P1. Main has a limit predicate and an explicit fire tool, @@ -112,4 +113,3 @@ Nothing was merged, published as a runtime, restarted or installed. canonical order channel. Those changes must remain in the same transaction as retained operations; this checkpoint does not edit their classes or TodoStore.rows/apply. Their exact published commit is a dependency for that continuing write-path change. - From 6ec698c49450eb46bb0144202a9af398a8e9cc7d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:15:04 -0400 Subject: [PATCH 06/20] Render retained refusals and bounded original turn summaries on commander cards --- src/agent_comms/transcript_events.py | 15 ++++++++ src/agent_comms/ui_model/commander.py | 27 +++++++++----- tests/test_commander_model.py | 2 +- tools/commander_page_journey.py | 51 ++++++++++++++++++++------- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/agent_comms/transcript_events.py b/src/agent_comms/transcript_events.py index 2f342e73fe..912870af62 100644 --- a/src/agent_comms/transcript_events.py +++ b/src/agent_comms/transcript_events.py @@ -101,6 +101,10 @@ def imported(self) -> ImportedMessage | None: """This event as portable text in an imported conversation; activity is not conversation.""" return None + def turn_line(self) -> str: + """A compact account of the unit's work; prompts and silent evidence add none.""" + return "" + def record_call(self, calls: dict[str, OrderCall], since: float) -> None: """Only tool events change a unit's calls.""" @@ -256,6 +260,9 @@ def imported(self) -> ImportedMessage: class AssistantTranscript(AgentTextTranscript): + def turn_line(self) -> str: + return " ".join(self.text.split())[:240] + @property def category(self) -> type[MessageCategory]: return OutboundCategory if self.routed else AgentCategory @@ -351,6 +358,9 @@ def kind(self) -> str: @dataclass(frozen=True, kw_only=True) class ToolStartTranscript(ToolTranscript): + def turn_line(self) -> str: + return self.title + def edit_calls(self, thread: str, problem: str, since: float) -> tuple[tool_declaration.EditCall, ...]: if self.before(since) or self.tool is None: return () @@ -440,6 +450,11 @@ class TurnDigest: turn_id: str events: tuple[TranscriptEvent, ...] + @property + def line(self) -> str: + work = tuple(line for event in self.events if (line := event.turn_line())) + return f"{self.turn_id}: " + (" · ".join(work[-3:])[:400] or "No work recorded yet.") + def before(self, at: float) -> bool: """The turn's last dated event precedes ``at``; an undated turn is never before.""" dated = [event for event in self.events if event.dated] diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index f3d8f63c5f..80538333a4 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import TYPE_CHECKING +from agent_comms.coordination_tables.refusals import SendRefusal from agent_comms.declared_family import DeclaredFamily from agent_comms.notification_progress import FailedProgress from agent_comms.presentation import NoRevision, ReadState, WireRevision @@ -52,6 +53,10 @@ class CardDetail: def text(self) -> str: return "Expand to read this unit's orders and recent turns." + @property + def turn_lines(self) -> tuple[str, ...]: + return () + @dataclass(frozen=True) class OpenCardDetail(CardDetail): @@ -65,14 +70,12 @@ def text(self) -> str: + "\nLatest work: " + order.progress.state for order in self.orders] lines.append("Latest checkout commits:\n" + "\n".join(self.latest_commits)) lines.append("Recent turns (newest first):") - for turn in self.turns.turns: - excerpts = tuple(' '.join(imported.body.split())[:240] - for event in turn.events if (imported := event.imported()) is not None) - lines.append(f"{turn.turn_id}: " + " · ".join(excerpts)[:400]) - if not self.turns.turns: - lines.append("No recorded turns.") return "\n\n".join(lines) + @property + def turn_lines(self) -> tuple[str, ...]: + return tuple(turn.line for turn in self.turns.turns) or ("No recorded turns.",) + @dataclass(frozen=True) class UnitCard: @@ -99,8 +102,8 @@ class CommanderAlarm: class CommanderAlarmSource(DeclaredFamily, affix="AlarmSource"): """New retained alarm sources extend the model, never the page's rendering. - The retained pre-send refusal owner can declare a source here when it lands. - Failed delivery is deliberately not described as a pre-send refusal. + Retained pre-send refusals and failed deliveries have different source owners. + Each source supplies its own lines; the frontend has no kind switches. """ @classmethod @@ -128,6 +131,14 @@ def read(cls, comms, commander, orders, now): if isinstance(notice.state, FailedProgress)) +class RefusedSendAlarmSource(CommanderAlarmSource): + @classmethod + def read(cls, comms, commander, orders, now): + return tuple(CommanderAlarm( + f"REFUSED SEND #{refusal.id} {refusal.sender} -> {refusal.target}: {refusal.reason}") + for refusal in SendRefusal.recent(comms.root, sender=commander.name)) + + @dataclass(frozen=True) class CommanderModel: commander: ThreadIncarnation diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index f8092451b9..edbf3479b2 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -3,8 +3,8 @@ import pytest -from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, OpenCardDetail from agent_comms.todos import ProgressSource, PullRequests, StatusProgressSource +from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, OpenCardDetail from test_commander_problem_orders import create diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index 412db1c0bd..06889b9258 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -18,13 +18,15 @@ from agent_comms.change_report import FileLine from agent_comms.child_process import NoProcess from agent_comms.comms import Comms +from agent_comms.errors import ToolRefusedError from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction from agent_comms.goals import CountCheckResult, NoGoal, OrderCheck, RequestGoalOrder +from agent_comms.progress_claims import WorkingClaim from agent_comms.recorded_time import RecordedEventTime from agent_comms.thread_status import StoppedThreadStatus -from agent_comms.todos import MechanicalOrderRun, PendingProblemOrder +from agent_comms.todos import MechanicalOrderRun, OperationDelivery, PendingProblemOrder from agent_comms.tool_caller import ThreadCaller -from agent_comms.tools import CommsTodoCreateTool +from agent_comms.tools import CommsOrderDoneTool, CommsSendTool, CommsTodoCreateTool COMMANDER = "claude-code" UNITS = ("pi-cn-phase2", "pi-cn-phase5") @@ -51,14 +53,16 @@ def prepare(output: Path) -> None: copied.chmod(0o600) session = str(copied) receipts.append(f"{name}: {source} -> {copied} ({copied.stat().st_size} bytes)") - comms.registry.declare(replace(original, process_identity=NoProcess(), - session_file=session, goal=NoGoal()), StoppedThreadStatus()) + comms.registry.declare( + replace(original, process_identity=NoProcess(), session_file=session, goal=NoGoal()), + StoppedThreadStatus()) comms.goals.update_goal(COMMANDER, SetGoalAction( text="Verify the commander page on copied real unit sessions", goal_id="page-request", order=RequestGoalOrder())) now = time.time() for index, name in enumerate(UNITS): - comms.goals.update_goal(name, SetGoalAction(text="Copied unit for read-only page verification")) + comms.goals.update_goal(name, SetGoalAction( + text="Copied unit for read-only page verification")) order = PendingProblemOrder( id=f"commander-page-{name}", owner=FileLine(__file__, 1), check=OrderCheck(str(output), "printf 1", CountCheckResult(1)), @@ -72,19 +76,26 @@ def prepare(output: Path) -> None: deadline=RecordedEventTime(now - 60 if not index else now + 1800), commander=COMMANDER, repo="OpenHCSDev/agent-comms", text="Show the real order in the commander page", )).apply(comms, ThreadCaller(COMMANDER)) + try: + CommsSendTool(sender=COMMANDER, target="unregistered-page-target", + body="Verify that the commander can read an actual refused send.").apply( + comms, ThreadCaller(COMMANDER)) + except ToolRefusedError as error: + receipts.append(f"Actual refused send: {error}") + else: + raise AssertionError("The deliberately absent target unexpectedly accepted a send.") (output / "sources.txt").write_text("\n".join(receipts) + "\n") async def journey(app, pilot, output: Path) -> None: - from textual.widgets import Static from toad.navigation_target import FeedTarget from toad.screens.commander import CommanderScreen, CommanderUnit from toad.widgets.conversation import Conversation await pilot.pause(2) # Open a real bus conversation and submit through its actual local-command boundary. - await app.screen.open_sidebar_target(FeedTarget()) - conversation = app.screen.query_one(Conversation) + await app.selected_session.open_sidebar_target(FeedTarget()) + conversation = app.selected_session.query_one(Conversation) await pilot.pause(1) conversation.prompt.text = "/commander " + COMMANDER conversation.prompt.focus() @@ -96,21 +107,37 @@ async def journey(app, pilot, output: Path) -> None: await pilot.pause(1) def capture(stage: str) -> None: # The text is from the actual laid-out terminal strips, not the model. - text = "\n".join(screen.render_line(y).text for y in range(screen.size.height)) + text = "\n".join(strip.text for strip in screen._compositor.render_strips()) (output / f"{stage}.txt").write_text(text + "\n") subprocess.run(("import", "-window", "root", str(output / f"{stage}.png")), check=True) capture("level-0") card = next(row for row in screen.query(CommanderUnit) if row.unit.name == UNITS[0]) await pilot.click(card.query_one("CollapsibleTitle")) async with asyncio.timeout(90): - while "Latest checkout commits:" not in str(card.query_one(Static).content): + while "Latest checkout commits:" not in str(card.detail_widget.content): await pilot.pause(0.2) await pilot.pause(1) + assert "pi-cn-phase2" in str(card.title) + assert screen.rows[next(unit for unit in screen.rows if unit.name == UNITS[1])].collapsed capture("level-1") + def deliver(): + comms = Comms(output / "bus") + todo = comms.goals.todos.get("P1") + CommsOrderDoneTool(problem=todo.id, order_id=todo.order.id, + delivery=OperationDelivery(), claims=(WorkingClaim(str(output / "level-0.png")),) + ).apply(comms, ThreadCaller(COMMANDER)) + await app.preparation.run_thread(deliver) + async with asyncio.timeout(90): + while "Delivered, waiting on install" not in str(card.title): + await pilot.pause(0.2) + await pilot.pause(0.5) + capture("live-delivery") # Verify collapse/return does not replace the underlying conversation or its draft. await pilot.press("escape") - assert app.screen.query_one(Conversation) is conversation - (output / "result.txt").write_text("PASS: normal Toad, real scratch orders, level 0 and expanded card.\n") + assert app.selected_session.query_one(Conversation) is conversation + (output / "result.txt").write_text( + "PASS: normal Toad, real scratch orders, level 0, expanded real turns, " + "and live delivery update without reopening.\n") app.exit() From fd2dd82b0c21d8f2dae28a37a4262aa2a25ff652 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:15:21 -0400 Subject: [PATCH 07/20] Decode named PR rows once and retain post-main CLI acceptance --- docs/evidence/cn-phase3-record-results.jsonl | 2 ++ docs/evidence/cn-phase3-record.md | 10 +++++++--- src/agent_comms/todos.py | 17 ++++------------- tests/test_delivery_merge.py | 1 + tests/test_order_progress.py | 4 ++-- tests/test_standing_rules.py | 13 ++++++------- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/docs/evidence/cn-phase3-record-results.jsonl b/docs/evidence/cn-phase3-record-results.jsonl index a525dd2535..bc9ea0c04d 100644 --- a/docs/evidence/cn-phase3-record-results.jsonl +++ b/docs/evidence/cn-phase3-record-results.jsonl @@ -10,3 +10,5 @@ {"todo":{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792432302.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"e0ca01500","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} {"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":5,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":5,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P4","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P4","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433118.0605557},"first_call_deadline":{"kind":"recorded","at":1792433238.0605557},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436718.0605557},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} {"error":"Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue.","type":"failed"} +{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":6,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":6,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} +{"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792433301.0},"commit":"256efcc8b6d632e224a3a4ffe7d5d1b49c4ae7e5","delivered_commit":"ec5f94568","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md index 5bfcdef34f..d06a2449de 100644 --- a/docs/evidence/cn-phase3-record.md +++ b/docs/evidence/cn-phase3-record.md @@ -29,7 +29,8 @@ records a `DeliveryMerge`: main, another PR target, or linked but unmerged sourc `DeliveredProblemOrder.line` derives its text from that delivery. `WorkBranches` still owns the open-PR/open-issue branch-link rule. `PullRequests.listed` now owns exact order-reference matching; `ProgressSource` no longer repeats that identity decision. PR lookup still uses the immutable order reference, now decoded -through named query records including the target branch. No verdict, strike, delivery or branch store +through named query records including the target branch. The old scalar `PullRequest.listed` +decoder is deleted; typed query rows are no longer decoded a second time. No verdict, strike, delivery or branch store was added. The integration is the observation at completion, not a later claim of installation. Before/after AST evidence used refactor-audit's `Package.load` with Python 3.14: 382 Core production @@ -63,7 +64,7 @@ acceptance, preserving the concise receipt below. The proof does not own other a ## Actual CLI proof -Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (12 actual responses, +Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (14 actual responses, normalized only for whitespace). Driver: `tools/proofs/cn_phase3_record.py`. The driver initializes real Core stores and calls `python -m agent_comms.cli tool --from ...`. It uses no fake Comms, fixture events or model replies. Judgment names a real edit from this unit's native session, @@ -93,7 +94,10 @@ Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has P4 remained pending at revision 1. The check can still leave its normal Git check note even when completion is refused. These are source-CLI checks of the candidate, not an installed runtime claim. -Nothing was merged, published as a runtime, restarted or installed. +No PR was merged, and nothing was published as a runtime, restarted or installed. Latest main +was merged normally into this branch at 256efcc8b. The two codec checks and CLI entrypoint passed +again there; a fresh P5 order then completed as unmerged through the actual CLI. The post-merge +Core AST pass parsed 384 modules with zero omissions. ## Local checks and precise blockers diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 36259c8c9a..68174ae0d5 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -28,7 +28,6 @@ from .errors import ToolRefusedError from .field_codec import ( FieldCodec, GitCommitText, MalformedRecord, NullMember, RecordMember, SubclassUnion, WireValue, - boundary_decoder, ) from .channel_targets import CommandTarget from .goals import CountCheckResult, OrderCheck, ProblemGoalOrder @@ -295,15 +294,6 @@ class PullRequest: orders: tuple[str, ...] = () base: str = field(default=WorkBranches.main, kw_only=True) - @classmethod - @boundary_decoder - def listed(cls, number: int, branch: str, head: str, merge: str, merged: str, - updated: str, body: str, base: str = WorkBranches.main) -> Self: - """One row of ``gh pr list``; GitHub leaves an unmerged PR's merge commit and time empty.""" - return cls(int(number), branch, head, merge or None, - _github_time(merged) if merged else None, _github_time(updated), - ProblemOrder.pull_request_ids(body), base=base) - def _github_time(text: str) -> float: return calendar.timegm(time.strptime(text, "%Y-%m-%dT%H:%M:%SZ")) @@ -328,9 +318,10 @@ class ListedPullRequest: base: str def parsed(self) -> PullRequest: - return PullRequest.listed( - self.number, self.branch, self.head, self.merge.oid if self.merge else "", - self.merged or "", self.updated, self.body, self.base) + return PullRequest( + self.number, self.branch, self.head, self.merge.oid if self.merge else None, + _github_time(self.merged) if self.merged else None, _github_time(self.updated), + ProblemOrder.pull_request_ids(self.body), base=self.base) class PullRequests: diff --git a/tests/test_delivery_merge.py b/tests/test_delivery_merge.py index 45f060de7c..af1d96d0b0 100644 --- a/tests/test_delivery_merge.py +++ b/tests/test_delivery_merge.py @@ -27,4 +27,5 @@ def test_named_forge_row_preserves_the_actual_nonmain_merge_target(): request = row.parsed() assert request.base == 'release' assert request.merge == 'b' * 40 + assert request.merged_at < request.updated_at assert request.orders == ('proof-order',) diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 9b42b53046..9beeff6ded 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -314,8 +314,8 @@ def forge(command, **arguments): monkeypatch.setattr(subprocess, "run", forge) script.PullRequestVerb(pid=pid, title=subject, body_file=str(body_file)).run() assert todo.order.pull_request_ids(bodies[0]) == (todo.order.id,) - stamp = "1970-01-01T00:08:20Z" - merged = PullRequest.listed(number, branch, "b" * 40, "d" * 40, stamp, stamp, bodies[0]) + merged = PullRequest(number, branch, "b" * 40, "d" * 40, + merged_at=500, updated_at=500, orders=todo.order.pull_request_ids(bodies[0])) foreign = replace(merged, number=number + 1, merge="f" * 40, orders=("another-order",)) monkeypatch.setattr(PullRequests, "read", lambda self, repo, state, order: (foreign, merged) if state == "merged" else ()) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index 976b55a916..d4e81414c8 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -55,13 +55,12 @@ def test_board_self_check_uses_order_progress_and_its_linked_merge( commander, standing, monkeypatch, reported, alarms): comms, caller = commander owned, pending, foreign = (create(commander, pid) for pid in ("P77", "P109", "P117")) - stamp = "1970-01-01T00:08:20Z" - merged = PullRequest.listed(1581, "docs/module-layers", "b" * 40, "d" * 40, - stamp, stamp, owned.order.pull_request_body("Module layer map"), 'main') - opened = PullRequest.listed(1624, "ratchet/none-absence", "c" * 40, "", "", - stamp, pending.order.pull_request_body("Declared measure"), 'main') - other = PullRequest.listed(1629, "ratchet/decoder-marker", "e" * 40, "f" * 40, - stamp, stamp, "Agent-Comms-Order: another-order\n", 'main') + merged = PullRequest(1581, "docs/module-layers", "b" * 40, "d" * 40, + merged_at=500, updated_at=500, orders=(owned.order.id,)) + opened = PullRequest(1624, "ratchet/none-absence", "c" * 40, + updated_at=500, orders=(pending.order.id,)) + other = PullRequest(1629, "ratchet/decoder-marker", "e" * 40, "f" * 40, + merged_at=500, updated_at=500, orders=("another-order",)) def forge(self, repo, state, order): return (merged, other) if state == "merged" else (opened,) From e248a6ace89c8becf99668ca126aab56ad183618 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:23:57 -0400 Subject: [PATCH 08/20] Resolve commander identity through the observation boundary and refresh turn fixtures --- src/agent_comms/ui_model/commander.py | 11 +++++++++++ src/agent_comms/ui_model/observation.py | 5 +++-- tests/test_commander_model.py | 19 ++++++++++++++++++- tests/test_turn_digests.py | 10 ++++++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 80538333a4..20a8560fd1 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -24,6 +24,7 @@ Observation, ObservationRequest, Observed, + ReadRequest, StoredRead, UnreadableHistory, ) @@ -196,6 +197,16 @@ def observation(self) -> CommanderReading: return CommanderReading(self) +@dataclass(frozen=True) +class ReadCommanderIdentity(ReadRequest): + """Resolve the external command spelling once, in the existing read process.""" + root: str + name: str + + def read(self, comms: Comms) -> ObserveCommander: + return ObserveCommander(self.root, comms.registry.require(self.name).incarnation) + + class CommanderReading(Observation[ObserveCommander]): def __init__(self, request: ObserveCommander): super().__init__(request) diff --git a/src/agent_comms/ui_model/observation.py b/src/agent_comms/ui_model/observation.py index 3ea0fa1c48..a4ab2d4c35 100644 --- a/src/agent_comms/ui_model/observation.py +++ b/src/agent_comms/ui_model/observation.py @@ -42,7 +42,7 @@ CoordinationReadUnavailable, StaleRevision, ) -from agent_comms.errors import RelationViolationError +from agent_comms.errors import RelationViolationError, ToolRefusedError from agent_comms.field_codec import FieldCodec from agent_comms.presentation import NoRevision, ReadState, WireRevision from agent_comms.registry_document import DEFAULT_ROSTER, RosterVisibility @@ -346,7 +346,8 @@ def answer(self, comms: Comms) -> ReadAnswered | ReadRefused | ReadBusy: except self.deferred_reads: return ReadBusy(self.request_id) return ReadAnswered(self.request_id, value) - except (CoordinationReadUnavailable, RelationViolationError, UnreadableHistory, StaleRevision) as error: + except (CoordinationReadUnavailable, RelationViolationError, UnreadableHistory, + StaleRevision, ToolRefusedError) as error: return self.refused(error) diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index edbf3479b2..b3b0efa0f8 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -4,7 +4,13 @@ import pytest from agent_comms.todos import ProgressSource, PullRequests, StatusProgressSource -from agent_comms.ui_model.commander import CommanderModel, ObserveCommander, OpenCardDetail +from agent_comms.ui_model.commander import ( + CommanderModel, + ObserveCommander, + OpenCardDetail, + ReadCommanderIdentity, +) +from agent_comms.ui_model.observation import ReadAnswered, ReadRefused from test_commander_problem_orders import create @@ -43,6 +49,17 @@ def forbidden(self): assert model.units[0].orders[0].progress.state == 'no commit since the order' +def test_commander_identity_is_resolved_or_refused_in_the_read_process(commander): + comms, caller = commander + answer = ReadCommanderIdentity(str(comms.root), caller.name).answer(comms) + assert isinstance(answer, ReadAnswered) + assert answer.value == ObserveCommander( + str(comms.root), comms.registry.require(caller.name).incarnation) + refusal = ReadCommanderIdentity(str(comms.root), 'absent-commander').answer(comms) + assert isinstance(refusal, ReadRefused) + assert 'absent-commander' in str(refusal.error) + + def test_commander_read_rejects_root_and_incarnation_changes(commander): comms, caller = commander lead = comms.registry.require(caller.name).incarnation diff --git a/tests/test_turn_digests.py b/tests/test_turn_digests.py index 105ca45654..d1c9a6f0be 100644 --- a/tests/test_turn_digests.py +++ b/tests/test_turn_digests.py @@ -39,7 +39,8 @@ def test_external_turns_preserve_interleaving_and_the_whole_selected_turn(tmp_pa source = comms.transcripts.external('cli') for turn, item, text in [('old', 'a', 'older'), ('one', 'b', 'first'), ('two', 'c', 'other'), ('one', 'd', 'continued')]: - source.finish(turn_id=turn, item_id=item, events=(AssistantTranscript(text),)) + source.finish(turn_id=turn, item_id=item, + events=(AssistantTranscript(text, position=item),)) answer = endpoint(comms, 'cli', 2) assert [turn['turn_id'] for turn in answer['turns']] == ['one', 'two'] assert [event['text'] for event in answer['turns'][0]['events']] == ['first', 'continued'] @@ -64,6 +65,8 @@ def test_native_turns_keep_original_calls_results_and_input_identity(tmp_path): dict(type='message', id='a2', parentId='result', message=dict( role='assistant', content=[dict(type='text', text='done')], stopReason='stop')), ] + for row in rows: + row['timestamp'] = '2026-10-16T12:00:00Z' source.write_text(''.join(json.dumps(row) + '\n' for row in rows)) source.chmod(0o600) comms.registry.declare(comms.registry.new_thread('native', frozenset(), str(tmp_path), session_file=str(source))) @@ -94,6 +97,8 @@ def test_claude_file_transport_reaches_the_same_cli_and_tool_answer(tmp_path, mo message=dict(content=[dict(type='tool_result', tool_use_id='read-1', content='contents')])) final = dict(type='assistant', uuid='final-record', timestamp='2026-10-16T12:00:03Z', message=dict(content=[dict(type='text', text='done')])) + final_position = len(''.join(json.dumps(row) + '\n' + for row in (prompt, call, result)).encode()) path.write_text(''.join(json.dumps(row) + '\n' for row in (prompt, call, result, final))) comms = prepared(tmp_path / 'wire') comms.registry.declare(comms.registry.new_thread('claude', frozenset(), str(tmp_path), execution=ExternalThreadExecution)) @@ -112,7 +117,8 @@ async def publish(): WatchSource.modified_file(item) for item in source.source_paths(comms.root)))) as changes: async for _ in changes: turns = await asyncio.to_thread(comms.transcripts.turns, 'claude', LastTurns()) - if any(event == AssistantTranscript('done', timestamp=datetime.fromisoformat(final['timestamp']).timestamp()) + if any(event == AssistantTranscript('done', position=final_position, + timestamp=datetime.fromisoformat(final['timestamp']).timestamp()) for turn in turns.turns for event in turn.events): break finally: From bcf11c840810c3f6dbedac264aeaa0a62e2bffe4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:26:36 -0400 Subject: [PATCH 09/20] Record physical commander acceptance, owner evidence and review limits --- docs/agents/commander-page.md | 93 ++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/docs/agents/commander-page.md b/docs/agents/commander-page.md index 2ef9107b05..b1a729a3ae 100644 --- a/docs/agents/commander-page.md +++ b/docs/agents/commander-page.md @@ -1,31 +1,86 @@ -# Commander page, first slice +# Commander page: delivered first slice -Owner: pi-cn-phase6. Core base 011084fc5; Toad base 9f203ea1. Issue #1495. +Issue #1495. Drafts: Core #1937 and Toad #1080. Owner: pi-cn-phase6. -## Checklist +## Delivered, not installed -- [x] Read phase 6, parity and dependencies; phase 5 is not a dependency. -- [x] List all open Core/Toad PR files; read #1912 and #1756 diffs. -- [x] Avoid the recent #1070 MainScreen changes: add a discovered slash command instead. -- [x] Read refactor-audit skill and catalog; run its AST overlay across both production roots (zero parse omissions). NRA is not installed on this machine. -- [ ] Implement observation, board, lazy details and existing delivery alarms. -- [ ] Exercise real scratch Core orders in Toad under isolated Xvfb/st. -- [ ] Run bounded checks in the background; publish paired draft PRs, do not merge. +`/commander ` opens the board in normal Toad. One collapsed line per held unit shows its problem number, plain problem text, deadline, recorded delivery state and native git/PR status. Overdue units come first. Click/Enter expands only that unit: full canonical order instructions, the actual latest checkout commit and up to three original recent turns, one line each. -## Owners and consumers +Alarms show native stalled orders, recorded failed deliveries and retained refused sends. Alarm sources declare their reads; Toad has no alarm-kind switch. Refusals come from pi-connector's `SendRefusal.recent`, not from a UI log. **Core #1937 locally integrates dependency #1922; land that dependency first.** No GitHub PR was merged. Nothing was installed, relinked or restarted in the normal launcher. -`TodoStore.for_commander` owns the indexed open-order selection, `Todo.held` owns assignment identity, `Todo.overdue` owns overdue meaning, `ProblemOrder.instructions` owns the full order text, `OrderProgress` owns git/PR state and stall policy, `TurnDigestRead` owns bounded newest-first turns, and delivery progress owns notification labels. The page must render these, not reinterpret strings or keep order state. +This is the requested first slice, not full phase 6: strikes, review queue, story, sidebar order decorations, turn/file drill-down and predictions remain outside this delivery. -`ProgressSource.readings` currently executes the order's acceptance check. A read-only commander page must not execute arbitrary checks. Keep one progress selection algorithm in ProgressSource, with a status-only source capability that does not request distance measurements. The existing native tool continues requesting its existing measured progress. +## Owners, consumers and deletions -`ObservationProcess` owns off-UI reads and lifetime; add a declared interest/result rather than a widget polling stores. `WireRevision` must include Todo main/WAL changes and the existing expiry tick. UI expansion belongs to the card; only expanded units request bounded turns. +- `TodoStore.held_for_commander` selects indexed held rows; `Todo.held().owner_identity` supplies nominal unit identity; `Todo.overdue` supplies overdue meaning. `CommanderModel` retains those types. It does not inspect kind/name strings to recover semantics. +- `ProblemOrder.instructions` supplies the complete order body. No second order renderer was written in Toad. +- `ProgressSource.progress` in `todos.py:333` now owns the sole progress selection algorithm, moved out of `OrderProgress.read`. The latter delegates to it. `StatusProgressSource` inherits that selection and suppresses acceptance-distance measurement: reading the page cannot execute an order's arbitrary acceptance check. Existing measured native-tool behavior remains on `ProgressSource`. +- `ProgressSource.latest_commit` reads one actual commit only for expanded cards. `OrderProgress.stalled` supplies stall policy. GitHub/read failures are `UnreadProgress`, not a false no-commit state. +- `TranscriptEvent.turn_line` and `TurnDigest.line` own recent-work summaries. Assistant and tool-start declarations contribute; prompts do not swamp the card with wire envelopes. The model's earlier generic imported-body rendering was deleted. +- `ReadCommanderIdentity` resolves the command spelling to `ThreadIncarnation` inside the existing observation process. Toad performs no registry or Todo read. Intentional typed read refusals return through the existing `ReadRefused` boundary instead of killing the observer. +- `ObserveCommander` and `CommanderReading` use the existing interest/process lifetime. `WireRevision` watches Todo main/WAL files plus relevant existing sources and the existing expiry tick. Closing the page removes its interest; there is no new polling timer or store. +- Toad's `CommanderCommand` is discovered from its declaration, with no command roster or MainScreen edit. `CommanderUnit` extends `Collapsible`; its existing title and explicit detail widgets are updated in place. A real run caught an incorrect broad `Static` lookup that updated the title; that lookup was deleted. -BOUND-2/BOUND-8: retain Todo, OrderProgress and ThreadIncarnation in typed models, not mapped records or redispatched kind strings. MEMB-1: the new slash command is discovered from its declaration; no command roster or MainScreen change. +One selection declaration, confirmed with: -## Known gap +``` +rg -n 'members = sorted\(OrderProgress.members_with' src/agent_comms +# todos.py:335 only +``` -Pre-send refusals currently escape as boundary exceptions; no retained indexed refusal feed was found. Existing failed delivery notifications are not the same fact and will be labelled honestly. Requested an owning-source decision from claude-code; continue the independent page work. +## Overlap and source evidence -## Disposable output +All open Core and Toad PR file lists were read before writing. #1903/#1873 concern launch/runtime classes not changed here. P603 #1757 was already merged. #1912 and #1756 diffs were read; their root-context and pattern-ID classes are not changed here. Same-file changes in `todos.py` are separate functions/classes. Recent Toad #1070 changes MainScreen and existing command classes; this slice adds a new command and screen instead. #1917 changes imported-message/UserTranscript behavior, not the event/turn-summary methods added here. `plan_phases.py` was not edited. -Owned by pi-cn-phase6: `/home/ts/.cache/agent-scratch/pi-cn-phase6` for bounded test logs, one Xvfb/st run and screen captures. Source/handoffs remain in the two `/home/ts/wt/pi-cn-phase6--P*` worktrees. The standard resource-check executable is absent here; measured 3.1 GiB available RAM and 26 GiB disk. Run one UI, no parallel agent or large suite. +Read the refactor-audit skill and pattern catalog. Reused its `Package` AST parser and overlay for both complete production roots, before/after source revisions, with zero parse omissions. Owners, imports, inheritance, direct consumers and function checks are recorded in `/home/ts/.cache/agent-scratch/pi-cn-phase6/owner-consumers.txt`. Dynamic family discovery, MRO dispatch and aliases were read semantically; the AST listing does not claim complete dynamic resolution. NRA is not installed on this machine. + +Relevant patterns: BOUND-2/BOUND-8 (retain Core's domain records rather than mapped shapes), MEMB-1 (derive command/alarm membership from declarations), IMPL-12 (one progress selector, inherited source capability). The final overlay adds no string/type dispatch switches or raw-record shapes. Its unused-module heuristic cannot see the pickled observation interest crossing the paired repository boundary. + +## Actual application acceptance + +Verified implementation pair: **Core e248a6ace / Toad 292bdaa5**, using the existing installed stack Python/dependencies with the two source checkouts on `PYTHONPATH`. This is a source-paired real Toad run, not a claim that the normal installed launcher contains the feature. + +Ran `tools/commander_page_journey.py` in **plain st on isolated Xvfb :186**, a real `ToadApp`, real observation child and scratch bus. It opened a real bus conversation and submitted `/commander claude-code` through the normal prompt/local-command boundary. It clicked a real Collapsible title, recorded delivery through the real order-completion tool, observed the page update without reopening, and returned to the identical underlying conversation. + +The scratch bus contains copies of actual registered units and complete-line cuts of their real saved session files, not fixture transcripts or hand-built events. Orders were created through `CommsTodoCreateTool`. The refusal was one actual `CommsSendTool` attempt to an absent target, retained by the send owner. No provider/agent was started and no original session, registry or uncertain input was modified. Snapshot provenance is in `physical-4/sources.txt`. + +Artifacts under `/home/ts/.cache/agent-scratch/pi-cn-phase6/physical-4/`: + +- `level-0.png`, `level-0.txt`: physically displayed collapsed board and alarms. +- `level-1.png`, `level-1.txt`: actual expanded card, commit and original turns; second unit still collapsed. +- `live-delivery.png`, `live-delivery.txt`: same open card changes to Delivered, OVERDUE disappears and its stall alarm clears; the refusal remains. +- `result.txt`: successful continuous journey receipt. + +Text below is excerpted from the actual composed terminal strips; the files contain the complete screen. + +### Level 0 + +``` +Commander claude-code | 2 units | Overdue first · Enter/click a unit to expand · Esc to return +▶ pi-cn-phase2 | OVERDUE P1: Show the real order in the commander page | due 14:23:01 | Open; no commit since the order +▶ pi-cn-phase5 | P2: Show the real order in the commander page | due 14:54:01 | Open; no commit since the order +Alarms +STALLED pi-cn-phase2 P1: no commit since the order +REFUSED SEND #1 claude-code -> unregistered-page-target: Target 'unregistered-page-target' is not a registered thread. +``` + +### Level 1, selected screen lines + +``` +▼ pi-cn-phase2 | OVERDUE P1: Show the real order in the commander page | due 14:23:01 | Open; no commit since the order + Problem: P1: Show the real order in the commander page + Order: commander-page-pi-cn-phase2 + 1. Render the canonical order owner without copying its state + Latest checkout commits: + P1: 69688af8d Merge remote-tracking branch 'origin/main' into feat/cn-phase2-design + Recent turns (newest first): + eb8bb409-f882-4e9b-80f2-efd20829b639: Run gh pr edit 1931 -R OpenHCSDev/agent-comms ... + 35561dbb-3227-4076-bda3-078c45dfdf7b: Run gh pr diff 1587 -R OpenHCSDev/agent-comms; ... +▶ pi-cn-phase5 | P2: Show the real order in the commander page | due 14:54:01 | Open; no commit since the order +``` + +## Checks and retained output + +Focused batch covers commander model, read-only progress, refusals and native/external turn readers. Three pre-existing turn-reader fixtures lacked required positions/timestamps; updated those fixtures without weakening their assertions. The final batch passed **40 checks in 31.90 seconds**, including the identity/refusal boundary check. Commands run in the background under `timeout 300 ... -X faulthandler`, with pytest default parallelism/whole-suite coverage disabled for the small selected batch. The environment lacks pytest-timeout, producing one configuration warning; the outer timeout remains enforced. New-file Ruff and both `git diff --check` checks pass. CI/full-suite acceptance was not requested or claimed. + +Owned disposable output: `/home/ts/.cache/agent-scratch/pi-cn-phase6`. Keep `physical-4` screen/source receipts while the drafts await review. Superseded physical runs and test databases were retired after checking actual borrowers; the isolated Xvfb/st processes were stopped. Source and handoffs stay in `/home/ts/wt/pi-cn-phase6--P1` and `--P2`. The standard resource-check executable is absent here; measured 3.1 GiB available RAM and 26 GiB disk before the bounded run. One physical UI was used; no parallel agent was launched. From 8f03b4b9f1f76d2cdf04ab40144a782f8f8a6833 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:30:17 -0400 Subject: [PATCH 10/20] Keep remote progress when the local checkout is absent --- docs/evidence/cn-phase3-record-results.jsonl | 1 + docs/evidence/cn-phase3-record.md | 56 ++++++++++++-- src/agent_comms/todos.py | 79 +++++++++++++------- tests/test_order_progress.py | 19 ++++- tools/proofs/cn_phase3_record.py | 2 +- 5 files changed, 118 insertions(+), 39 deletions(-) diff --git a/docs/evidence/cn-phase3-record-results.jsonl b/docs/evidence/cn-phase3-record-results.jsonl index bc9ea0c04d..6ad87cc68e 100644 --- a/docs/evidence/cn-phase3-record-results.jsonl +++ b/docs/evidence/cn-phase3-record-results.jsonl @@ -12,3 +12,4 @@ {"error":"Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue.","type":"failed"} {"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":6,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":6,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} {"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792433301.0},"commit":"256efcc8b6d632e224a3a4ffe7d5d1b49c4ae7e5","delivered_commit":"ec5f94568","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} +{"todos":[{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":6,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[{"kind":"void_strike_todo","expected_revision":5,"actor":{"name":"record-lead","created_at":1792432385.1369143},"strike":{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}},"reason":"The second strike repeated the first finding rather than describing another failed change","at":{"kind":"recorded","at":1792432680.216849}}],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792431763.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"5cdac561d498ece25142a8d2e5cf9b06002b6c8a","merge":{"kind":"main"}}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792432302.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"e0ca01500","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P4","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P4","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433118.0605557},"first_call_deadline":{"kind":"recorded","at":1792433238.0605557},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436718.0605557},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792433301.0},"commit":"256efcc8b6d632e224a3a4ffe7d5d1b49c4ae7e5","delivered_commit":"ec5f94568","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}],"progress":{"P2":{"kind":"no_commit"},"P3":{"kind":"no_commit"},"P4":{"kind":"no_commit"},"P5":{"kind":"open_pr","at":1792434165,"readings":{"readings":[]},"number":1935,"head":"fd2dd82b0"}}} diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md index d06a2449de..6c233b9b5b 100644 --- a/docs/evidence/cn-phase3-record.md +++ b/docs/evidence/cn-phase3-record.md @@ -11,7 +11,8 @@ Baseline: 5cdac561d498ece25142a8d2e5cf9b06002b6c8a. - [x] Decode PR query rows into named records, not positional tuples. - [x] Confirm real scratch-bus order, verdict, strikes, explicit fire and void through Core tools. - [x] Confirm main/unmerged delivery and unlinked refusal through the CLI. -- [x] Pass the two bounded record/codec sanity checks. Pytest is unavailable. +- [x] Pass the bounded record/codec checks and real missing-checkout board read. +- [ ] Finish the single-worker pytest run; the unrelated post-commit correction failure is assigned for overlap settlement. - [x] Publish draft PR #1935; do not merge or install. - [ ] Automatic firing at the strike limit, with all strike sources in the same write. - [ ] Retain original typed transitions and the state each operation replaces. @@ -64,7 +65,7 @@ acceptance, preserving the concise receipt below. The proof does not own other a ## Actual CLI proof -Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (14 actual responses, +Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (15 actual responses, normalized only for whitespace). Driver: `tools/proofs/cn_phase3_record.py`. The driver initializes real Core stores and calls `python -m agent_comms.cli tool --from ...`. It uses no fake Comms, fixture events or model replies. Judgment names a real edit from this unit's native session, @@ -101,10 +102,12 @@ Core AST pass parsed 384 modules with zero omissions. ## Local checks and precise blockers -- The requested background `timeout 300 python -X faulthandler -m pytest ...` failed before - collection: the shared interpreter has no pytest. An isolated-venv attempt was refused by the - existing StorageRule; no alternate installation was attempted. A shared test interpreter with - pytest, or a reviewed dev-dependency installation by its owner, is needed for the suite. +- The first interpreter had no pytest. StorageRule refused an isolated venv; no workaround + installation was attempted. pi-cn-phase7 supplied the existing + `/home/ts/code/projects/agent-comms/.venv/bin/python`. Bounded checks use `-o addopts=''` to avoid + the default eight workers and coverage gate, plus owned persistent basetemp/TMPDIR paths. + The first four-module run gave 125 passed and two failures. The GitHub-unread failure is repaired + at its typed remote-error boundary; the post-commit correction failure is described below. - Final checks on source checkpoint ec5f94568: Core/Toad AST passes above, the candidate CLI entrypoint, and both standalone record/codec sanity functions passed under the same bounded/background command form. They check every DeliveryMerge member and named external non-main target decoding; @@ -113,7 +116,48 @@ Core AST pass parsed 384 modules with zero omissions. not automatic firing. The shared TodoStore/Goals write-path work overlaps recent #1878 and is raised with claude-code. Source inspection also finds destructive order/assignment replacement, missing Core recording timestamps and direct fold/correction writes. pi-cn-phase3 retains this implementation. +- A real post-commit error remains: Messaging.send catches TodoConflict only, so an ordinary SQLite + correction error escapes after the message is committed and loses its receipt at the caller. + Reproducer: `test_committed_send_reports_correction_failure_to_tool_and_cli`. Recent #1878 + (toad-theme-2) changes that same method/class. claude-code has the exact overlap and the proposed + CorrectionResult-owned outcome capture; no Messaging code is edited here. +- New #1938 (P710) requires every delivered commit to be on origin/main. That conflicts with the + explicit delivered-but-not-merged workflow in phase 3. claude-code has this product decision; + the two branches must not silently overwrite each other's policy. - pi-cn-phase5 owns SendBackVerdict/Goals.send_back publication, OrderPost rows/insertion and the canonical order channel. Those changes must remain in the same transaction as retained operations; this checkpoint does not edit their classes or TodoStore.rows/apply. Their exact published commit is a dependency for that continuing write-path change. + +## Remote progress with no local checkout + +pi-cn-phase7 supplied a concrete rotation failure: all four +`test_board_self_check_uses_order_progress_and_its_linked_merge` cases reached +`OpenPrProgress.found -> source.readings -> git merge-base` on an absent checkout. +Remote PR evidence and local distance evidence were incorrectly compulsory together. + +The existing ProgressSource now owns remote facts and the absence of local evidence. +`ProgressSource.at` selects LocalProgressSource once at the filesystem boundary when the +checkout directory is available. The local subtype owns commits, Git and distance reads. +Its Git mechanism reuses CheckedCommit.git, rather than another subprocess/error path; +commits no longer suppress arbitrary Git errors. A broken existing checkout remains an +actual GitCommandFailed, not a fabricated no-commit or unread-PR result. + +ProgressPoint owns the log-row format and decode used by both commit and distance reads. +CommittedProgress and PushedProgress consume those named records; the raw sha/time tuples +and repeated split/float conversions are deleted. An order's baseline point still uses its +issue time; a Git commit point uses committer time. + +There are two production ProgressSource consumers: OrderProgress.read uses the availability +factory; TodoStore.board_disagreements intentionally uses the remote-only source to look for +missed merges. No TodoStore or phase-5 publication method was changed. PullRequests raises the +existing WorkBranchError for a forge refusal, which OrderProgress can report as UnreadProgress +without swallowing unrelated Git or malformed-record errors. + +The real proof temporarily added P5's actual immutable reference to this owned draft PR through +`todo.order.pull_request_body`. With `/home/ts/wt/record-worker--P5` absent, the actual CLI +`comms_todo_list` returned OpenPrProgress for PR #1935, with empty local distance readings. +The real TodoStore.board_disagreements read also passed. The temporary reference was removed +in finally while preserving other PR text. No fixture Comms, events or replacement Git runner +were used. Driver/logs remain in the owned proof root and `.proof-board-read.log`; the complete +CLI response is the fifteenth result in the retained receipt. diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 68174ae0d5..12b7b42390 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -30,7 +30,7 @@ FieldCodec, GitCommitText, MalformedRecord, NullMember, RecordMember, SubclassUnion, WireValue, ) from .channel_targets import CommandTarget -from .goals import CountCheckResult, OrderCheck, ProblemGoalOrder +from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime from .thread_identity import RecordedThreadIncarnation, ThreadIncarnation from .threads import Thread @@ -38,7 +38,7 @@ from .standing_rules import UndatedStandingRule from .storage_rules import StorageRule from .plan_phases import OrderPhase, UnplannedPhase -from .work_branches import WorkBranches +from .work_branches import WorkBranchError, WorkBranches if TYPE_CHECKING: from .comms import Comms @@ -358,7 +358,7 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, "--jq", '.data | map_values(.nodes)'), capture_output=True, text=True, timeout=60) if listed.returncode: - raise ValueError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") + raise WorkBranchError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") rows = FieldCodec.decode(dict[str, tuple[ListedPullRequest, ...]], FieldCodec.parse(listed.stdout)) for alias, candidate in aliases.items(): @@ -370,6 +370,19 @@ def _clock(at: float) -> str: return time.strftime("%H:%M", time.localtime(at)) +@dataclass(frozen=True) +class ProgressPoint: + """A revision and its time in the order's progress; Git rows use committer time.""" + sha: str + at: float + format: ClassVar[str] = "%H %ct" + + @classmethod + def listed(cls, line: str) -> ProgressPoint: + sha, at = line.split() + return cls(sha, float(at)) + + @dataclass(frozen=True) class DistanceReading: at: float @@ -436,11 +449,16 @@ def projection(self) -> str: @dataclass(frozen=True) class ProgressSource: - """What an order's progress is read from: its unit's checkout and the repository's PRs.""" + """The repository's PR facts, extended by local evidence only while a checkout is available.""" todo: Todo checkout: Path pulls: PullRequests + @staticmethod + def at(todo: Todo, checkout: Path, pulls: PullRequests) -> ProgressSource: + return (LocalProgressSource(todo, checkout, pulls) if checkout.is_dir() + else ProgressSource(todo, checkout, pulls)) + @property def since(self) -> float: return self.todo.order.at.recorded_at("An order") @@ -450,34 +468,37 @@ def requests(self, state: str) -> list[PullRequest]: return [pr for pr in self.pulls.listed(self.todo.repo, state, self.todo.order) if pr.updated_at >= self.since] - def commits(self, *excluded: str) -> Iterator[tuple[str, float]]: + def commits(self, *excluded: str) -> Iterator[ProgressPoint]: + """A remote source makes no claim about unpublished local commits.""" + return iter(()) + + def readings(self) -> DistanceReadings: + return DistanceReadings() + + +class LocalProgressSource(ProgressSource): + """An available checkout supplies the local commit and distance evidence.""" + + def commits(self, *excluded: str) -> Iterator[ProgressPoint]: """The checkout's newest commit since the order that no ``excluded`` ref already holds.""" after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) - log = subprocess.run( - ("git", "-C", str(self.checkout), "log", "-1", f"--since={after}", - "--format=%H %ct", "HEAD", "--not", *excluded), - capture_output=True, text=True, timeout=30) - for line in log.stdout.splitlines() if not log.returncode else (): - sha, at = line.split() - yield sha, float(at) + log = self.git("log", "-1", f"--since={after}", f"--format={ProgressPoint.format}", + "HEAD", "--not", *excluded) + return (ProgressPoint.listed(line) for line in log.splitlines()) def git(self, *args: str) -> str: - run = subprocess.run(("git", "-C", str(self.checkout), *args), - capture_output=True, text=True, timeout=60) - if run.returncode: - raise ValueError(run.stderr.strip() or f"git {args[0]} failed") - return run.stdout + return CheckedCommit.git(str(self.checkout), *args) def readings(self) -> DistanceReadings: """The order's base, then each branch commit since the order; the last two are read.""" base = self.git("merge-base", "HEAD", "origin/HEAD").strip() after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) - commits = [(sha, float(at)) for sha, at in (line.split() for line in reversed(self.git( - "log", "--first-parent", f"--since={after}", "--format=%H %ct", "HEAD", - "--not", base).splitlines()))] - points = [(base, self.since), *commits][-2:] - return DistanceReadings( - tuple(DistanceReading(at, self.distance(base, sha)) for sha, at in points)) + commits = [ProgressPoint.listed(line) for line in reversed(self.git( + "log", "--first-parent", f"--since={after}", f"--format={ProgressPoint.format}", + "HEAD", "--not", base).splitlines())] + points = [ProgressPoint(base, self.since), *commits][-2:] + return DistanceReadings(tuple( + DistanceReading(point.at, self.distance(base, point.sha)) for point in points)) def distance(self, base: str, sha: str) -> int: """The order's Check run on ``sha`` against its N, plus its numbered edits the diff since @@ -499,12 +520,12 @@ def read(cls, todo: Todo, storage: StorageRule, pulls: PullRequests) -> OrderPro """The first found member by its declared precedence; with none found, no commit yet.""" owner = todo.holder assert owner, "Only a held order has progress." - source = ProgressSource(todo, storage.order_checkout(owner, todo.id), pulls) + source = ProgressSource.at(todo, storage.order_checkout(owner, todo.id), pulls) members = sorted(cls.members_with(FoundProgress), key=lambda member: member.precedence) try: return next((progress for member in members for progress in member.found(source)), NoCommitProgress()) - except subprocess.TimeoutExpired as unread: + except (WorkBranchError, subprocess.TimeoutExpired) as unread: return UnreadProgress(str(unread)) @property @@ -606,8 +627,8 @@ class CommittedProgress(Moving, FoundProgress): @classmethod def found(cls, source: ProgressSource) -> Iterator[Self]: - return (cls(sha[:9], at=at, readings=source.readings()) - for sha, at in source.commits("--remotes")) + return (cls(commit.sha[:9], at=commit.at, readings=source.readings()) + for commit in source.commits("--remotes")) @property def state(self) -> str: @@ -621,8 +642,8 @@ class PushedProgress(CommittedProgress): @classmethod def found(cls, source: ProgressSource) -> Iterator[Self]: - return (cls(sha[:9], at=at, readings=source.readings()) - for sha, at in source.commits("origin/HEAD")) + return (cls(commit.sha[:9], at=commit.at, readings=source.readings()) + for commit in source.commits("origin/HEAD")) @property def state(self) -> str: diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 9beeff6ded..489e3bc72b 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -7,13 +7,16 @@ import pytest from agent_comms.recorded_time import RecordedEventTime +from agent_comms.goals import GitCommandFailed from agent_comms.tools import CommsOrderDoneTool from agent_comms.todos import (CommittedProgress, DistanceReadings, MergedProgress, NoCommitProgress, InstallDeadline, InstallDue, OpenPrProgress, OperationDelivery, OrderProgress, PlannedInstallDeadline, - PullRequest, PullRequests, PushedProgress, UnreadProgress) + LocalProgressSource, ProgressPoint, ProgressSource, PullRequest, PullRequests, + PushedProgress, UnreadProgress) from agent_comms.messages import InfoKind from agent_comms.storage_rules import StorageRule +from agent_comms.work_branches import WorkBranchError from test_commander_problem_orders import create, working @@ -116,11 +119,20 @@ def test_a_merged_order_still_awaits_its_record(checkout): def test_unread_github_is_explicit(checkout): pulls = Listed() - pulls.states = ValueError('gh: not logged in') + pulls.states = WorkBranchError('gh: not logged in') progress = read(checkout, pulls) assert progress == UnreadProgress('gh: not logged in') +def test_an_existing_broken_checkout_is_not_silently_treated_as_missing(checkout, tmp_path): + bad = tmp_path / 'not-a-repository' + bad.mkdir() + source = ProgressSource.at(checkout[0], bad, Listed()) + assert isinstance(source, LocalProgressSource) + with pytest.raises(GitCommandFailed): + source.readings() + + def test_board_and_overdue_name_a_merge_awaiting_its_record(commander, commander_script, monkeypatch, tmp_path): script, _ = commander_script # worker's P1 checkout, on the branch its merged PR came from. @@ -154,7 +166,8 @@ class HeldSource: since = 100.0 def commits(self, *excluded): - commits = {('--remotes',): ('a' * 40, 1000.0), ('origin/HEAD',): ('e' * 40, 1000.0)} + commits = {('--remotes',): ProgressPoint('a' * 40, 1000.0), + ('origin/HEAD',): ProgressPoint('e' * 40, 1000.0)} yield commits[excluded] def readings(self): diff --git a/tools/proofs/cn_phase3_record.py b/tools/proofs/cn_phase3_record.py index d8577e3503..e1d2ba9ab1 100644 --- a/tools/proofs/cn_phase3_record.py +++ b/tools/proofs/cn_phase3_record.py @@ -44,7 +44,7 @@ def cli(caller, tool): raise RuntimeError(result.stderr or result.stdout) with RECEIPT.open("a") as output: output.write(result.stdout) - return json.loads(result.stdout) + return result.stdout def setup(): From f9fc186f2148412763ed0052eb0e6353c794a43b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:41:24 -0400 Subject: [PATCH 11/20] Scope local progress to its checkout and bounded reading window --- docs/evidence/cn-phase3-record.md | 21 ++++++++++++++++----- src/agent_comms/todos.py | 14 ++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md index 6c233b9b5b..bdab4af770 100644 --- a/docs/evidence/cn-phase3-record.md +++ b/docs/evidence/cn-phase3-record.md @@ -12,7 +12,8 @@ Baseline: 5cdac561d498ece25142a8d2e5cf9b06002b6c8a. - [x] Confirm real scratch-bus order, verdict, strikes, explicit fire and void through Core tools. - [x] Confirm main/unmerged delivery and unlinked refusal through the CLI. - [x] Pass the bounded record/codec checks and real missing-checkout board read. -- [ ] Finish the single-worker pytest run; the unrelated post-commit correction failure is assigned for overlap settlement. +- [x] Pass 38 owned checks in one bounded worker, including the four missing-checkout cases. +- [ ] The broader post-commit correction failure remains with #1878; review comment posted as directed. - [x] Publish draft PR #1935; do not merge or install. - [ ] Automatic firing at the strike limit, with all strike sources in the same write. - [ ] Retain original typed transitions and the state each operation replaces. @@ -121,9 +122,10 @@ Core AST pass parsed 384 modules with zero omissions. Reproducer: `test_committed_send_reports_correction_failure_to_tool_and_cli`. Recent #1878 (toad-theme-2) changes that same method/class. claude-code has the exact overlap and the proposed CorrectionResult-owned outcome capture; no Messaging code is edited here. -- New #1938 (P710) requires every delivered commit to be on origin/main. That conflicts with the - explicit delivered-but-not-merged workflow in phase 3. claude-code has this product decision; - the two branches must not silently overwrite each other's policy. +- claude-code resolved the #1938 policy overlap: delivered, merged, installed and working remain + separate states; the desktop moves main ancestry onto the merged state. CommitDelivery here is + unchanged. The #1878 review comment is + https://github.com/OpenHCSDev/agent-comms/pull/1878#issuecomment-6274426175. - pi-cn-phase5 owns SendBackVerdict/Goals.send_back publication, OrderPost rows/insertion and the canonical order channel. Those changes must remain in the same transaction as retained operations; this checkpoint does not edit their classes or TodoStore.rows/apply. Their exact published commit @@ -141,7 +143,10 @@ The existing ProgressSource now owns remote facts and the absence of local evide checkout directory is available. The local subtype owns commits, Git and distance reads. Its Git mechanism reuses CheckedCommit.git, rather than another subprocess/error path; commits no longer suppress arbitrary Git errors. A broken existing checkout remains an -actual GitCommandFailed, not a fabricated no-commit or unread-PR result. +actual GitCommandFailed, not a fabricated no-commit or unread-PR result. Local Git is bound to +that checkout's own `.git`, so an empty directory cannot silently borrow a parent repository. +The real development worktree's gitfile was also exercised. DistanceReadings owns its two-point +window, which now bounds the Git log request instead of reading all commits and discarding them. ProgressPoint owns the log-row format and decode used by both commit and distance reads. CommittedProgress and PushedProgress consume those named records; the raw sha/time tuples @@ -161,3 +166,9 @@ The real TodoStore.board_disagreements read also passed. The temporary reference in finally while preserving other PR text. No fixture Comms, events or replacement Git runner were used. Driver/logs remain in the owned proof root and `.proof-board-read.log`; the complete CLI response is the fifteenth result in the retained receipt. + +Final owned check: `test_delivery_merge.py`, `test_order_progress.py` and the four standing-rule +missing-checkout cases: **38 passed**, single worker, 32.57 seconds, `OWNED_PYTEST_EXIT=0`. +The shared interpreter warns that pytest-timeout is absent; the outer timeout remains 300 seconds. +A broader intermediate run gave 126 passed and two failures: the remaining #1878 correction +failure, and the empty-directory/parent-repository defect that this final owned run confirms fixed. diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 12b7b42390..6b53e80b72 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -394,12 +394,13 @@ class DistanceReadings: """The order's last two distance readings, oldest first: its base at the order, then its newest commit. Fewer than two read, or a last reading at done, project nothing.""" readings: tuple[DistanceReading, ...] = () + window: ClassVar[int] = 2 @property def trend(self) -> DistanceTrend: """Velocity across the two readings: falling projects the finish, else it is flat.""" - last = self.readings[-2:] - if len(last) < 2 or not last[1].distance: + last = self.readings[-self.window:] + if len(last) < self.window or not last[1].distance: return UnreadTrend() first, final = last if final.distance >= first.distance: @@ -487,16 +488,17 @@ def commits(self, *excluded: str) -> Iterator[ProgressPoint]: return (ProgressPoint.listed(line) for line in log.splitlines()) def git(self, *args: str) -> str: - return CheckedCommit.git(str(self.checkout), *args) + return CheckedCommit.git( + str(self.checkout), f"--git-dir={(self.checkout / '.git').absolute()}", *args) def readings(self) -> DistanceReadings: """The order's base, then each branch commit since the order; the last two are read.""" base = self.git("merge-base", "HEAD", "origin/HEAD").strip() after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) commits = [ProgressPoint.listed(line) for line in reversed(self.git( - "log", "--first-parent", f"--since={after}", f"--format={ProgressPoint.format}", - "HEAD", "--not", base).splitlines())] - points = [ProgressPoint(base, self.since), *commits][-2:] + "log", f"--max-count={DistanceReadings.window}", "--first-parent", f"--since={after}", + f"--format={ProgressPoint.format}", "HEAD", "--not", base).splitlines())] + points = [ProgressPoint(base, self.since), *commits][-DistanceReadings.window:] return DistanceReadings(tuple( DistanceReading(point.at, self.distance(base, point.sha)) for point in points)) From 1b8bf1c05af62d7826b7ae51bcad7999523aa348 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:55:29 -0400 Subject: [PATCH 12/20] Read recorded check evidence without executing from progress --- src/agent_comms/goals.py | 13 +++--- src/agent_comms/todos.py | 14 +++--- tests/test_order_progress.py | 19 +++++++- tools/proofs/cn_progress_read_only.py | 65 +++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 tools/proofs/cn_progress_read_only.py diff --git a/src/agent_comms/goals.py b/src/agent_comms/goals.py index 05055b3114..e6863885c2 100644 --- a/src/agent_comms/goals.py +++ b/src/agent_comms/goals.py @@ -320,13 +320,16 @@ async def run(self) -> ChildResult: return await BoundedRun.run(("bash", "-c", self.command), cwd=self.directory, timeout=self.timeout) + def recorded(self, sha: str, repository: str) -> tuple[ChildResult, ...]: + """Read an explicit run's receipt; absent evidence never starts the check.""" + return tuple(checked.result for checked in CheckedCommit.stored(repository, sha) + if checked.command == self.command)[:1] + def at(self, revision: str, repository: str) -> ChildResult: - """The check on ``revision``: the result a git note on the commit records, or one run in - a throwaway detached checkout and recorded there for every later reader.""" + """The check on ``revision``: a recorded result, or an explicit run in a detached checkout.""" sha = CheckedCommit.git(repository, "rev-parse", f"{revision}^{{commit}}").strip() - for checked in CheckedCommit.stored(repository, sha): - if checked.command == self.command: - return checked.result + for result in self.recorded(sha, repository): + return result with tempfile.TemporaryDirectory(prefix="order-check-") as scratch: tree = str(Path(scratch) / "tree") CheckedCommit.git(repository, "worktree", "add", "--detach", tree, sha) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 6b53e80b72..fc917a76dc 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -500,15 +500,15 @@ def readings(self) -> DistanceReadings: f"--format={ProgressPoint.format}", "HEAD", "--not", base).splitlines())] points = [ProgressPoint(base, self.since), *commits][-DistanceReadings.window:] return DistanceReadings(tuple( - DistanceReading(point.at, self.distance(base, point.sha)) for point in points)) + reading for point in points for reading in self.recorded_distances(base, point))) - def distance(self, base: str, sha: str) -> int: - """The order's Check run on ``sha`` against its N, plus its numbered edits the diff since - ``base`` has not touched.""" + def recorded_distances(self, base: str, point: ProgressPoint) -> Iterator[DistanceReading]: + """Only explicit check receipts supply a distance; no receipt means unknown.""" check = self.todo.order.check - touched = self.git("diff", "--name-only", base, sha).split() - run = check.at(sha, str(self.checkout)) - return check.result.distance(run) + self.todo.order.untouched(touched) + for run in check.recorded(point.sha, str(self.checkout)): + touched = self.git("diff", "--name-only", base, point.sha).split() + yield DistanceReading(point.at, check.result.distance(run) + + self.todo.order.untouched(touched)) @dataclass(frozen=True) diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 489e3bc72b..8bacfa1a09 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -1,5 +1,6 @@ """An order's progress is read from its unit's checkout and PRs beside its deadline; merged work draws no strike.""" import subprocess +import shlex import time from dataclasses import replace @@ -7,7 +8,7 @@ import pytest from agent_comms.recorded_time import RecordedEventTime -from agent_comms.goals import GitCommandFailed +from agent_comms.goals import CountCheckResult, GitCommandFailed, OrderCheck from agent_comms.tools import CommsOrderDoneTool from agent_comms.todos import (CommittedProgress, DistanceReadings, MergedProgress, NoCommitProgress, InstallDeadline, InstallDue, OpenPrProgress, OperationDelivery, OrderProgress, PlannedInstallDeadline, @@ -124,6 +125,22 @@ def test_unread_github_is_explicit(checkout): assert progress == UnreadProgress('gh: not logged in') +def test_progress_reads_only_explicit_check_receipts(checkout, tmp_path): + todo, unit = checkout + marker = tmp_path / 'explicit-check-count' + check = OrderCheck(str(unit), f'printf x >> {shlex.quote(str(marker))}; printf 0', + CountCheckResult(0)) + todo = replace(todo, order=replace(todo.order, check=check)) + source = ProgressSource.at(todo, unit, Listed()) + assert source.readings() == DistanceReadings() + assert not marker.exists() + check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit)) + assert marker.read_text() == 'x' + assert len(source.readings().readings) == 1 + assert source.readings().readings[0].distance == todo.order.untouched(()) + assert marker.read_text() == 'x' + + def test_an_existing_broken_checkout_is_not_silently_treated_as_missing(checkout, tmp_path): bad = tmp_path / 'not-a-repository' bad.mkdir() diff --git a/tools/proofs/cn_progress_read_only.py b/tools/proofs/cn_progress_read_only.py new file mode 100644 index 0000000000..ccc2c78d98 --- /dev/null +++ b/tools/proofs/cn_progress_read_only.py @@ -0,0 +1,65 @@ +"""Real Core board reads consume explicit check receipts, never run the check.""" +import argparse +import shlex +import subprocess + +from agent_comms.comms import Comms +from agent_comms.field_codec import FieldCodec +from agent_comms.goals import CheckedCommit, CountCheckResult, OrderCheck +from agent_comms.todos import CorrectCheckTodo, PushedProgress +from agent_comms.tools import CommsTodoListTool, CommsTodoTool, TodoList +from tools.proofs.cn_phase3_record import CHECKOUT, ROOT, cli, create_order + +MARKER = ROOT / 'explicit-check-count' + + +def prepare(): + create_order('P6') + comms = Comms(ROOT) + todo = comms.goals.todos.get('P6') + check = OrderCheck(str(CHECKOUT), f'printf x >> {shlex.quote(str(MARKER))}; printf 0', + CountCheckResult(0)) + cli('record-lead', CommsTodoTool(id='P6', operation=CorrectCheckTodo( + todo.revision, comms.registry.require('record-lead').incarnation, + todo.order.check, check, 'Prove that reading the board cannot execute the check'))) + + +def board(): + listed = FieldCodec.decode(TodoList, FieldCodec.parse(cli('record-lead', CommsTodoListTool()))) + progress = listed.progress['P6'] + assert isinstance(progress, PushedProgress), progress + return progress.distance_readings() + + +def verify(): + comms = Comms(ROOT) + todo = comms.goals.todos.get('P6') + checkout = comms.registry.storage_rule.order_checkout(todo.held().owner, todo.id) + if checkout.exists() or MARKER.exists(): + raise RuntimeError('Proof resources already exist; inspect before another attempt') + subprocess.run(['git', 'worktree', 'add', '--detach', str(checkout), 'HEAD'], + cwd=CHECKOUT, check=True, timeout=60) + try: + assert not board().readings + assert not MARKER.exists() + sha = CheckedCommit.git(str(checkout), 'rev-parse', 'HEAD').strip() + assert not todo.order.check.recorded(sha, str(checkout)) + todo.order.check.at(sha, str(checkout)) # Deliberately explicit, never a board read. + assert MARKER.read_text() == 'x' + assert len(board().readings) == 1 + assert MARKER.read_text() == 'x' + for receipt in CheckedCommit.stored(str(checkout), sha): + if receipt.command == todo.order.check.command: + print(FieldCodec.encode(receipt), flush=True) + print('Actual CLI: unknown before explicit run, recorded afterward; exactly one execution.', + flush=True) + finally: + subprocess.run(['git', 'worktree', 'remove', str(checkout)], cwd=CHECKOUT, + check=True, timeout=60) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('step', choices=('prepare', 'verify')) + args = parser.parse_args() + prepare() if args.step == 'prepare' else verify() From 65fb631704f6c439ab8430bcac8a98863d64efee Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:21:41 -0400 Subject: [PATCH 13/20] Derive unit-card commit momentum from canonical order and branch evidence --- src/agent_comms/todos.py | 65 +++++++++++++++++++++++++-- src/agent_comms/ui_model/commander.py | 18 ++++++-- tests/test_commander_model.py | 40 ++++++++++++++++- tools/commander_page_journey.py | 31 ++++++++----- 4 files changed, 133 insertions(+), 21 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index ab0a048cfa..90d9aad8e5 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -383,6 +383,49 @@ def listed(cls, line: str) -> ProgressPoint: return cls(sha, float(at)) +@dataclass(frozen=True) +class CommitMomentum: + """A next-commit forecast from observed commits, not a completion estimate. + + The card derives this value on read; nothing is persisted. Without a known + amount of remaining work, commit cadence cannot predict order completion. + """ + todo: Todo + commits: tuple[ProgressPoint, ...] + observed_at: float + window: ClassVar[int] = 2 + + @property + def interval(self) -> float: + if len(self.commits) < self.window: + return 0 + return self.commits[-1].at - self.commits[0].at + + @property + def next_at(self) -> float: + return self.commits[-1].at + self.interval if self.interval > 0 else 0 + + @property + def deadline_margin(self) -> float: + return self.todo.deadline.recorded_at("An order deadline") - self.next_at + + @property + def line(self) -> str: + due = self.todo.deadline.recorded_at("An order deadline") - self.observed_at + deadline = f"due in {due / 60:.1f}m" if due >= 0 else f"due {-due / 60:.1f}m ago" + if not self.todo.pending: + return f"{self.todo.id}: momentum not applicable; order delivered" + if self.interval <= 0: + return (f"{self.todo.id}: cadence unknown (need two increasing commit times); " + f"{deadline}; completion unknown") + margin = self.deadline_margin + relation = "before" if margin >= 0 else "after" + late = self.observed_at - self.next_at + missed = f"; forecast missed by {late / 60:.1f}m" if late > 0 else "" + return (f"{self.todo.id}: {self.interval / 60:.1f}m/commit; next commit ~{_clock(self.next_at)} " + f"({abs(margin) / 60:.1f}m {relation} due); {deadline}{missed}; completion unknown") + + @dataclass(frozen=True) class DistanceReading: at: float @@ -488,6 +531,10 @@ def readings(self) -> DistanceReadings: def latest_commit(self) -> str: raise ValueError("The unit checkout is unavailable.") + def cadence_commits(self) -> tuple[ProgressPoint, ...]: + """No checkout means no local commit evidence, not zero velocity.""" + return () + class LocalProgressSource(ProgressSource): """An available checkout supplies the local commit and distance evidence.""" @@ -510,14 +557,24 @@ def latest_commit(self) -> str: def readings(self) -> DistanceReadings: """The order's base, then each branch commit since the order; the last two are read.""" base = self.git("merge-base", "HEAD", "origin/HEAD").strip() - after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) - commits = [ProgressPoint.listed(line) for line in reversed(self.git( - "log", f"--max-count={DistanceReadings.window}", "--first-parent", f"--since={after}", - f"--format={ProgressPoint.format}", "HEAD", "--not", base).splitlines())] + commits = self.branch_commits(base, DistanceReadings.window) points = [ProgressPoint(base, self.since), *commits][-DistanceReadings.window:] return DistanceReadings(tuple( reading for point in points for reading in self.recorded_distances(base, point))) + def cadence_commits(self) -> tuple[ProgressPoint, ...]: + """Bounded branch evidence since the order; never run its acceptance check.""" + base = self.git("merge-base", "HEAD", "origin/HEAD").strip() + return self.branch_commits(base, CommitMomentum.window) + + def branch_commits(self, base: str, limit: int) -> tuple[ProgressPoint, ...]: + """Decode the bounded order-time branch window once for all readers.""" + after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) + return tuple(point for line in reversed(self.git( + "log", f"--max-count={limit}", "--first-parent", f"--since={after}", + f"--format={ProgressPoint.format}", "HEAD", "--not", base).splitlines()) + if (point := ProgressPoint.listed(line)).at >= self.since) + def recorded_distances(self, base: str, point: ProgressPoint) -> Iterator[DistanceReading]: """Only explicit check receipts supply a distance; no receipt means unknown.""" check = self.todo.order.check diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 7f965eaeb6..633b1ea44f 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -17,7 +17,9 @@ from agent_comms.presentation import NoRevision, ReadState, WireRevision from agent_comms.revision_views import CommanderView from agent_comms.thread_identity import ThreadIncarnation -from agent_comms.todos import OrderProgress, ProgressSource, PullRequests, Todo +from agent_comms.todos import ( + CommitMomentum, OrderProgress, ProgressPoint, ProgressSource, PullRequests, Todo, +) from agent_comms.turn_digests import LastTurns, ThreadTurns from agent_comms.ui_model.observation import ( InterestRequest, @@ -41,6 +43,8 @@ class CommanderOrder: todo: Todo progress: OrderProgress overdue: bool + commits: tuple[ProgressPoint, ...] = () + observed_at: float = 0 @property def line(self) -> str: @@ -88,6 +92,12 @@ class UnitCard: def line(self) -> str: return self.unit.name + " | " + " ; ".join(order.line for order in self.orders) + @property + def momentum(self) -> tuple[CommitMomentum, ...]: + """Derived from order times and commit evidence; never a stored score.""" + return tuple(CommitMomentum(order.todo, order.commits, order.observed_at) + for order in self.orders) + @property def priority(self) -> tuple[bool, float, str]: return (not any(order.overdue for order in self.orders), @@ -158,9 +168,9 @@ def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: grouped: dict[ThreadIncarnation, list[CommanderOrder]] = {} for todo in todos: unit = todo.held().owner_identity - progress = ProgressSource.at( - todo, storage.order_checkout(unit.name, todo.id), pulls).progress() - grouped.setdefault(unit, []).append(CommanderOrder(todo, progress, todo.overdue(now))) + source = ProgressSource.at(todo, storage.order_checkout(unit.name, todo.id), pulls) + grouped.setdefault(unit, []).append(CommanderOrder( + todo, source.progress(), todo.overdue(now), source.cadence_commits(), now)) cards = [] for unit, orders in grouped.items(): if comms.registry.require(unit.name).incarnation != unit: diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index 46f1bac2d3..2482ca0ac6 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -3,12 +3,17 @@ import pytest -from agent_comms.goals import CheckedCommit +from agent_comms.goals import OrderCheck +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.todos import CommitMomentum, ProgressPoint, ProgressSource, PullRequests from agent_comms.ui_model.commander import ( CommanderModel, ObserveCommander, OpenCardDetail, ReadCommanderIdentity, + CommanderOrder, + CardDetail, + UnitCard, ) from agent_comms.ui_model.observation import ReadAnswered, ReadRefused from test_commander_problem_orders import create @@ -40,13 +45,44 @@ def test_status_source_never_requests_acceptance_measurement(commander, monkeypa create(commander, 'P1') def forbidden(*args, **kwargs): pytest.fail('A read-only commander page executed an acceptance measurement') - monkeypatch.setattr(CheckedCommit, 'run', forbidden) + monkeypatch.setattr(OrderCheck, 'at', forbidden) + monkeypatch.setattr(OrderCheck, 'run', forbidden) # Progress now consumes retained check receipts, never executes a measurement. model = CommanderModel.read(comms, ObserveCommander( str(comms.root), comms.registry.require(caller.name).incarnation)) assert model.units[0].orders[0].progress.state == 'no commit since the order' +def test_card_derives_commit_cadence_against_deadline_without_storing_momentum(commander): + comms, caller = commander + todo = replace(create(commander, 'P1'), deadline=RecordedEventTime(300)) + points = (ProgressPoint('first', 100), ProgressPoint('second', 160)) + order = CommanderOrder(todo, ProgressSource(todo, comms.root, PullRequests((todo,))).progress(), + False, points, 170) + card = UnitCard(comms.registry.require('worker').incarnation, (order,), CardDetail()) + momentum, = card.momentum + assert 'momentum' not in card.__dataclass_fields__ + assert momentum.interval == 60 + assert momentum.next_at == 220 + assert momentum.deadline_margin == 80 + assert '1.3m before due' in momentum.line + assert 'completion unknown' in momentum.line + late = replace(momentum, todo=replace(todo, deadline=RecordedEventTime(200)), observed_at=250) + assert '0.3m after due' in late.line + assert 'forecast missed by 0.5m' in late.line + + +@pytest.mark.parametrize('points', [(), (ProgressPoint('one', 100),), + (ProgressPoint('one', 100), ProgressPoint('two', 100)), + (ProgressPoint('one', 160), ProgressPoint('two', 100))]) +def test_unknown_commit_cadence_does_not_invent_forecast(commander, points): + todo = create(commander, 'P1') + momentum = CommitMomentum(todo, points, 170) + assert momentum.next_at == 0 + assert 'cadence unknown' in momentum.line + assert 'next commit ~' not in momentum.line + + def test_commander_identity_is_resolved_or_refused_in_the_read_process(commander): comms, caller = commander answer = ReadCommanderIdentity(str(comms.root), caller.name).answer(comms) diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index 06889b9258..3dc002e7ef 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -32,7 +32,7 @@ UNITS = ("pi-cn-phase2", "pi-cn-phase5") -def prepare(output: Path) -> None: +def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> None: live = Comms(LocalRoute.default()) root = output / "bus" if root.exists(): @@ -40,7 +40,7 @@ def prepare(output: Path) -> None: root.mkdir(parents=True) comms = Comms(root) receipts = [] - for name in (COMMANDER, *UNITS): + for name in (COMMANDER, *units): original = live.registry.require(name) session = original.session_file if session: @@ -60,7 +60,7 @@ def prepare(output: Path) -> None: text="Verify the commander page on copied real unit sessions", goal_id="page-request", order=RequestGoalOrder())) now = time.time() - for index, name in enumerate(UNITS): + for index, name in enumerate(units): comms.goals.update_goal(name, SetGoalAction( text="Copied unit for read-only page verification")) order = PendingProblemOrder( @@ -69,7 +69,8 @@ def prepare(output: Path) -> None: edits=("Render the canonical order owner without copying its state",), banned=("Do not launch a provider or change the original saved session",), report="Report the actual visible commander page and its expanded unit card", - at=RecordedEventTime(now - 1200 if not index else now), + at=RecordedEventTime(ordered_at if ordered_at is not None and not index + else now - 1200 if not index else now), first_call_deadline=RecordedEventTime(now + 120), run=MechanicalOrderRun()) CommsTodoCreateTool(owner=name, order=CreateProblemGoalAction( problem=f"P{index + 1}", order=order, request="page-request", @@ -87,7 +88,7 @@ def prepare(output: Path) -> None: (output / "sources.txt").write_text("\n".join(receipts) + "\n") -async def journey(app, pilot, output: Path) -> None: +async def journey(app, pilot, output: Path, units: tuple[str, ...], momentum: bool) -> None: from toad.navigation_target import FeedTarget from toad.screens.commander import CommanderScreen, CommanderUnit from toad.widgets.conversation import Conversation @@ -101,7 +102,7 @@ async def journey(app, pilot, output: Path) -> None: conversation.prompt.focus() await pilot.press("enter") async with asyncio.timeout(90): - while not isinstance(app.screen, CommanderScreen) or len(app.screen.rows) != len(UNITS): + while not isinstance(app.screen, CommanderScreen) or len(app.screen.rows) != len(units): await pilot.pause(0.2) screen = app.screen await pilot.pause(1) @@ -111,14 +112,17 @@ def capture(stage: str) -> None: (output / f"{stage}.txt").write_text(text + "\n") subprocess.run(("import", "-window", "root", str(output / f"{stage}.png")), check=True) capture("level-0") - card = next(row for row in screen.query(CommanderUnit) if row.unit.name == UNITS[0]) + card = next(row for row in screen.query(CommanderUnit) if row.unit.name == units[0]) + if momentum: + assert "m/commit; next commit" in str(card.momentum_widget.content) + assert "completion unknown" in str(card.momentum_widget.content) await pilot.click(card.query_one("CollapsibleTitle")) async with asyncio.timeout(90): while "Latest checkout commits:" not in str(card.detail_widget.content): await pilot.pause(0.2) await pilot.pause(1) - assert "pi-cn-phase2" in str(card.title) - assert screen.rows[next(unit for unit in screen.rows if unit.name == UNITS[1])].collapsed + assert units[0] in str(card.title) + assert screen.rows[next(unit for unit in screen.rows if unit.name == units[1])].collapsed capture("level-1") def deliver(): comms = Comms(output / "bus") @@ -131,6 +135,8 @@ def deliver(): while "Delivered, waiting on install" not in str(card.title): await pilot.pause(0.2) await pilot.pause(0.5) + if momentum: + assert "order delivered" in str(card.momentum_widget.content) capture("live-delivery") # Verify collapse/return does not replace the underlying conversation or its draft. await pilot.press("escape") @@ -145,15 +151,18 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--prepare", action="store_true") + parser.add_argument("--units", nargs=2, default=UNITS) + parser.add_argument("--ordered-at", type=float) + parser.add_argument("--momentum", action="store_true") args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) if args.prepare: - prepare(args.output) + prepare(args.output, tuple(args.units), args.ordered_at) return os.environ["AGENT_COMMS_ROOT"] = str(args.output / "bus") from toad.app import ToadApp app = ToadApp(project_dir=str(args.output)) - app.run(auto_pilot=lambda pilot: journey(app, pilot, args.output)) + app.run(auto_pilot=lambda pilot: journey(app, pilot, args.output, tuple(args.units), args.momentum)) if app._exception: raise app._exception From db685143f90b63f51e3f9ba9beb25b22ea98edaa Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:22:54 -0400 Subject: [PATCH 14/20] Revert "Integrate receipt-only progress source and delete obsolete status source" This reverts commit fcc5e75f3014ea55f38afffb452e4665c09d593e, reversing changes made to 6182e05fd59cbbe0f4d2b7636eee7036e745d4f3. --- docs/evidence/cn-phase3-record-results.jsonl | 15 -- docs/evidence/cn-phase3-record.md | 174 ------------- src/agent_comms/goals.py | 13 +- src/agent_comms/todos.py | 260 +++++-------------- src/agent_comms/ui_model/commander.py | 6 +- tests/test_commander_model.py | 13 +- tests/test_commander_problem_orders.py | 5 +- tests/test_delivery_merge.py | 31 --- tests/test_order_progress.py | 40 +-- tests/test_standing_rules.py | 13 +- tools/proofs/cn_phase3_record.py | 144 ---------- tools/proofs/cn_progress_read_only.py | 65 ----- 12 files changed, 93 insertions(+), 686 deletions(-) delete mode 100644 docs/evidence/cn-phase3-record-results.jsonl delete mode 100644 docs/evidence/cn-phase3-record.md delete mode 100644 tests/test_delivery_merge.py delete mode 100644 tools/proofs/cn_phase3_record.py delete mode 100644 tools/proofs/cn_progress_read_only.py diff --git a/docs/evidence/cn-phase3-record-results.jsonl b/docs/evidence/cn-phase3-record-results.jsonl deleted file mode 100644 index 6ad87cc68e..0000000000 --- a/docs/evidence/cn-phase3-record-results.jsonl +++ /dev/null @@ -1,15 +0,0 @@ -{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":2,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":2,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} -{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":0} -{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":3,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}}],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":1} -{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":4,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[],"released":[],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":2} -{"todos":[{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":5,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]}],"progress":{}} -{"todo":{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":6,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[{"kind":"void_strike_todo","expected_revision":5,"actor":{"name":"record-lead","created_at":1792432385.1369143},"strike":{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}},"reason":"The second strike repeated the first finding rather than describing another failed change","at":{"kind":"recorded","at":1792432680.216849}}],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},"strike_count":0} -{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":3,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":3,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} -{"todo":{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792431763.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"5cdac561d498ece25142a8d2e5cf9b06002b6c8a","merge":{"kind":"main"}}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} -{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":4,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":4,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} -{"todo":{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792432302.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"e0ca01500","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} -{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":5,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":5,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P4","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P4","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433118.0605557},"first_call_deadline":{"kind":"recorded","at":1792433238.0605557},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436718.0605557},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} -{"error":"Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue.","type":"failed"} -{"goal":{"text":"Complete the assigned problems.","id":"record-work","published_seq":0,"progress":"","revision":6,"reported_turn":null,"mention_source":{"goal_id":"record-work","text_revision":6,"text_digest":"816861a4f5b6cbf17b6bf4e4a3d9f87e1b8bdcd32bcf98c2b447f2a3a9be80b7","owner_name":"record-worker","owner_created_at":1792432385.2149153,"bindings":[]},"state":{"kind":"active"},"order":{"kind":"problem"}},"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}} -{"todo":{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792433301.0},"commit":"256efcc8b6d632e224a3a4ffe7d5d1b49c4ae7e5","delivered_commit":"ec5f94568","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},"strike_count":0} -{"todos":[{"row":"todo","id":"P1","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":6,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":null,"order":{"kind":"pending","id":"record-order-P1","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432466.497828},"first_call_deadline":{"kind":"recorded","at":1792432586.497828},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436066.497828},"strikes":[{"kind":"strike_todo","expected_revision":2,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The assigned change left the target branch out of the saved order record","at":{"kind":"recorded","at":1792432640.0348916}},{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}}],"voided":[{"kind":"void_strike_todo","expected_revision":5,"actor":{"name":"record-lead","created_at":1792432385.1369143},"strike":{"kind":"strike_todo","expected_revision":3,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The second review found the target branch still missing from the saved record","at":{"kind":"recorded","at":1792432652.476716}},"reason":"The second strike repeated the first finding rather than describing another failed change","at":{"kind":"recorded","at":1792432680.216849}}],"released":[{"kind":"release_todo","expected_revision":4,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"reason":"The unit left the required target branch absent after both recorded reviews","at":{"kind":"recorded","at":1792432675.6006632}}],"verdicts":[{"kind":"accepted_verdict","expected_revision":1,"actor":{"name":"record-lead","created_at":1792432385.1369143},"previous":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"tool_call_id":"call_a3e3f7350f4e44faa55a72bebf904fa6|fc_01744e18603d1183016ad659dcd6c081a3a769ec0e1880e266","at":{"kind":"recorded","at":1792432628.8483949},"quote":"Decode pull request target branches"}],"corrections":[],"messages":[]},{"row":"todo","id":"P2","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P2","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432735.6061842},"first_call_deadline":{"kind":"recorded","at":1792432855.6061842},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792431763.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"5cdac561d498ece25142a8d2e5cf9b06002b6c8a","merge":{"kind":"main"}}},"deadline":{"kind":"recorded","at":1792436335.6061842},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P3","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P3","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792432771.201293},"first_call_deadline":{"kind":"recorded","at":1792432891.201293},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792432302.0},"commit":"e0ca0150042587e7494184b5ab2f6701f3487076","delivered_commit":"e0ca01500","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792436371.201293},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P4","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":1,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"pending","id":"record-order-P4","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433118.0605557},"first_call_deadline":{"kind":"recorded","at":1792433238.0605557},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"}},"deadline":{"kind":"recorded","at":1792436718.0605557},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]},{"row":"todo","id":"P5","repo":"openhcsdev/agent-comms","text":"Show the real verdict and strike records without a second source","creator":"record-lead","creator_created":1792432385.1369143,"state":"open","revision":2,"goal":{"owner":"record-lead","owner_created":1792432385.1369143,"goal_id":"record-request"},"assignment":{"owner":"record-worker","owner_created":1792432385.2149153,"parent":"record-lead","parent_created":1792432385.1369143,"generation":"record-work"},"order":{"kind":"delivered","id":"record-order-P5","owner":"/home/ts/wt/pi-cn-phase3--P1/src/agent_comms/todos.py:69","check":{"directory":"/home/ts/wt/pi-cn-phase3--P1","command":"printf 1","result":{"kind":"count","expected":1}},"edits":["Decode pull request target branches through the existing record owner"],"banned":["No second order store"],"report":"Report the actual Core tool response","at":{"kind":"recorded","at":1792433505.0452404},"first_call_deadline":{"kind":"recorded","at":1792433625.0452404},"first_call":{"kind":"awaiting"},"run":{"kind":"mechanical"},"delivery":{"kind":"checked_commit","at":{"kind":"recorded","at":1792433301.0},"commit":"256efcc8b6d632e224a3a4ffe7d5d1b49c4ae7e5","delivered_commit":"ec5f94568","merge":{"kind":"unmerged","branches":["feat/cn-phase3-record"]}}},"deadline":{"kind":"recorded","at":1792437105.0452404},"strikes":[],"voided":[],"released":[],"verdicts":[],"corrections":[],"messages":[]}],"progress":{"P2":{"kind":"no_commit"},"P3":{"kind":"no_commit"},"P4":{"kind":"no_commit"},"P5":{"kind":"open_pr","at":1792434165,"readings":{"readings":[]},"number":1935,"head":"fd2dd82b0"}}} diff --git a/docs/evidence/cn-phase3-record.md b/docs/evidence/cn-phase3-record.md deleted file mode 100644 index bdab4af770..0000000000 --- a/docs/evidence/cn-phase3-record.md +++ /dev/null @@ -1,174 +0,0 @@ -# Commander record plane - -Owner: pi-cn-phase3. Branch: feat/cn-phase3-record. -Baseline: 5cdac561d498ece25142a8d2e5cf9b06002b6c8a. - -## Checklist - -- [x] Read the plan and inspect both repositories' open PR file lists. -- [x] Confirm existing verdict, strike limit, fire and void owners on main. -- [x] Add delivery integration to the canonical checked delivery. -- [x] Decode PR query rows into named records, not positional tuples. -- [x] Confirm real scratch-bus order, verdict, strikes, explicit fire and void through Core tools. -- [x] Confirm main/unmerged delivery and unlinked refusal through the CLI. -- [x] Pass the bounded record/codec checks and real missing-checkout board read. -- [x] Pass 38 owned checks in one bounded worker, including the four missing-checkout cases. -- [ ] The broader post-commit correction failure remains with #1878; review comment posted as directed. -- [x] Publish draft PR #1935; do not merge or install. -- [ ] Automatic firing at the strike limit, with all strike sources in the same write. -- [ ] Retain original typed transitions and the state each operation replaces. -- [ ] Connect commander board/done rendering after the recent shared-function overlap is settled. -- [ ] Verify a real order merged to a non-main target (codec checked, not live-proven). - -## Source relationships - -Before: `OrderDelivery.verified` ran the check and deletion counts; `CommitDelivery.require_source` -proved ancestry and publication only. Completion returned no integration fact. `PullRequests.read` -decoded a positional tuple of seven unrelated fields. `DeliveredProblemOrder.line` was a constant. - -After: completion passes its canonical Todo and registry to the delivery owner. A checked commit -records a `DeliveryMerge`: main, another PR target, or linked but unmerged source branches. -`DeliveredProblemOrder.line` derives its text from that delivery. `WorkBranches` still owns the -open-PR/open-issue branch-link rule. `PullRequests.listed` now owns exact order-reference matching; -`ProgressSource` no longer repeats that identity decision. PR lookup still uses the immutable order reference, now decoded -through named query records including the target branch. The old scalar `PullRequest.listed` -decoder is deleted; typed query rows are no longer decoded a second time. No verdict, strike, delivery or branch store -was added. The integration is the observation at completion, not a later claim of installation. - -Before/after AST evidence used refactor-audit's `Package.load` with Python 3.14: 382 Core production -modules parsed, zero omissions. The final installed-Toad syntax pass parsed 283 modules, zero omissions. The only production `OrderDelivery.verified` caller is `DeliverTodo.prepare`; -creation is in `commander.order.done` and `CommsOrderDoneTool`. Goal summaries consume the order's -`line`. The installed Toad source has no order-delivery consumer; its unrelated input-delivery types -are not this family. A preliminary NRA run under its old Python 3.12 omitted Python 3.14 syntax; -it is not acceptance evidence. NRA under Python 3.14 is blocked by that checkout's incompatible -metaclass-registry/tree-sitter dependencies; the existing refactor-audit parser supplied the census. -Dynamic tool invocation is generated from ToolRequest declarations/FieldCodec, not statically -resolved by this census. - -## Overlaps - -P603 is merged (#1757). Open #1903 and #1873 do not edit these record owners. Recent #1756 edits -FairText, not the delivery family; #1925 removes DeadlineText. Recent #1878 changes TodoStore's -owner lookup, which this change leaves alone. Older #1737 changes claims and closing, not integration; -older #1733 adds printed check output and must compose that output with the checked delivery here. -#1912 moves the commander functions into an explicit Commander context, including `board` and `done`. -Those functions are left untouched pending the integration owner's conflict decision. - -## Resource boundary - -`tools/agent_resource_check.py --assert-headroom` warned: about 4 GiB available RAM and 3.8 GiB -free on /tmp. No parallel agents or large suite. The prescribed /home/ts/bin launcher is absent. -Owned verification state is `/home/ts/wt/pi-cn-phase3--P1/.proof-record-plane` (real scratch bus, -copy of this unit's native transcript and temporary check trees). Logs are the checkout's `.proof-*.log`; -`.proof-test-state` is the empty intended pytest scratch directory. The check commands set TMPDIR -under the owned persistent root. These paths remain for the unfinished review; remove them after -acceptance, preserving the concise receipt below. The proof does not own other agents' /tmp worktrees. - -## Actual CLI proof - -Original response bodies: `docs/evidence/cn-phase3-record-results.jsonl` (15 actual responses, -normalized only for whitespace). Driver: `tools/proofs/cn_phase3_record.py`. The driver initializes -real Core stores and calls `python -m agent_comms.cli tool --from ...`. It uses no fake Comms, -fixture events or model replies. Judgment names a real edit from this unit's native session, -read through `OrderEditsRead` after copying the session to the scratch root. No original session -was changed and no live unit received these deliberate scratch-only strikes. - -| Actual operation | P1 revision | verdicts | retained strikes | voids | assignment | -|---|---:|---:|---:|---:|---| -| create | 1 | 0 | 0 | 0 | record-worker | -| accepted verdict | 2 | 1 | 0 | 0 | record-worker | -| first strike | 3 | 1 | 1 | 0 | record-worker | -| second strike | 4 | 1 | 2 | 0 | record-worker | -| explicit comms_fire | 5 | 1 | 2 | 0 | null | -| void second strike | 6 | 1 | 2 | 1 | null | - -After voiding, Core's `live_strikes(record-worker)` returns one. A void does not restore a released -assignment. The tool's assignee-only `strike_count` is zero once there is no assignment; the original -strikes and void are still retained on the Todo. - -P2's real completion returned `todo.order.delivery.merge = {"kind":"main"}`. P3's real completion -on this published draft branch returned `{"kind":"unmerged","branches":["feat/cn-phase3-record"]}`. -P4 used an existing published unlinked commit, without editing or publishing that branch. Core refused: - -``` -Delivered commit 2874a87ada914add901c7f318ea46c3c88aa555a is not merged and has no branch linked to an open PR or issue. -``` - -P4 remained pending at revision 1. The check can still leave its normal Git check note even when -completion is refused. These are source-CLI checks of the candidate, not an installed runtime claim. -No PR was merged, and nothing was published as a runtime, restarted or installed. Latest main -was merged normally into this branch at 256efcc8b. The two codec checks and CLI entrypoint passed -again there; a fresh P5 order then completed as unmerged through the actual CLI. The post-merge -Core AST pass parsed 384 modules with zero omissions. - -## Local checks and precise blockers - -- The first interpreter had no pytest. StorageRule refused an isolated venv; no workaround - installation was attempted. pi-cn-phase7 supplied the existing - `/home/ts/code/projects/agent-comms/.venv/bin/python`. Bounded checks use `-o addopts=''` to avoid - the default eight workers and coverage gate, plus owned persistent basetemp/TMPDIR paths. - The first four-module run gave 125 passed and two failures. The GitHub-unread failure is repaired - at its typed remote-error boundary; the post-commit correction failure is described below. -- Final checks on source checkpoint ec5f94568: Core/Toad AST passes above, the candidate CLI entrypoint, - and both standalone record/codec sanity functions passed under the same bounded/background - command form. They check every DeliveryMerge member and named external non-main target decoding; - they are not substituted for the actual CLI proof. -- The real second-strike response still held P1. Main has a limit predicate and an explicit fire tool, - not automatic firing. The shared TodoStore/Goals write-path work overlaps recent #1878 and is raised - with claude-code. Source inspection also finds destructive order/assignment replacement, missing Core - recording timestamps and direct fold/correction writes. pi-cn-phase3 retains this implementation. -- A real post-commit error remains: Messaging.send catches TodoConflict only, so an ordinary SQLite - correction error escapes after the message is committed and loses its receipt at the caller. - Reproducer: `test_committed_send_reports_correction_failure_to_tool_and_cli`. Recent #1878 - (toad-theme-2) changes that same method/class. claude-code has the exact overlap and the proposed - CorrectionResult-owned outcome capture; no Messaging code is edited here. -- claude-code resolved the #1938 policy overlap: delivered, merged, installed and working remain - separate states; the desktop moves main ancestry onto the merged state. CommitDelivery here is - unchanged. The #1878 review comment is - https://github.com/OpenHCSDev/agent-comms/pull/1878#issuecomment-6274426175. -- pi-cn-phase5 owns SendBackVerdict/Goals.send_back publication, OrderPost rows/insertion and the - canonical order channel. Those changes must remain in the same transaction as retained operations; - this checkpoint does not edit their classes or TodoStore.rows/apply. Their exact published commit - is a dependency for that continuing write-path change. - -## Remote progress with no local checkout - -pi-cn-phase7 supplied a concrete rotation failure: all four -`test_board_self_check_uses_order_progress_and_its_linked_merge` cases reached -`OpenPrProgress.found -> source.readings -> git merge-base` on an absent checkout. -Remote PR evidence and local distance evidence were incorrectly compulsory together. - -The existing ProgressSource now owns remote facts and the absence of local evidence. -`ProgressSource.at` selects LocalProgressSource once at the filesystem boundary when the -checkout directory is available. The local subtype owns commits, Git and distance reads. -Its Git mechanism reuses CheckedCommit.git, rather than another subprocess/error path; -commits no longer suppress arbitrary Git errors. A broken existing checkout remains an -actual GitCommandFailed, not a fabricated no-commit or unread-PR result. Local Git is bound to -that checkout's own `.git`, so an empty directory cannot silently borrow a parent repository. -The real development worktree's gitfile was also exercised. DistanceReadings owns its two-point -window, which now bounds the Git log request instead of reading all commits and discarding them. - -ProgressPoint owns the log-row format and decode used by both commit and distance reads. -CommittedProgress and PushedProgress consume those named records; the raw sha/time tuples -and repeated split/float conversions are deleted. An order's baseline point still uses its -issue time; a Git commit point uses committer time. - -There are two production ProgressSource consumers: OrderProgress.read uses the availability -factory; TodoStore.board_disagreements intentionally uses the remote-only source to look for -missed merges. No TodoStore or phase-5 publication method was changed. PullRequests raises the -existing WorkBranchError for a forge refusal, which OrderProgress can report as UnreadProgress -without swallowing unrelated Git or malformed-record errors. - -The real proof temporarily added P5's actual immutable reference to this owned draft PR through -`todo.order.pull_request_body`. With `/home/ts/wt/record-worker--P5` absent, the actual CLI -`comms_todo_list` returned OpenPrProgress for PR #1935, with empty local distance readings. -The real TodoStore.board_disagreements read also passed. The temporary reference was removed -in finally while preserving other PR text. No fixture Comms, events or replacement Git runner -were used. Driver/logs remain in the owned proof root and `.proof-board-read.log`; the complete -CLI response is the fifteenth result in the retained receipt. - -Final owned check: `test_delivery_merge.py`, `test_order_progress.py` and the four standing-rule -missing-checkout cases: **38 passed**, single worker, 32.57 seconds, `OWNED_PYTEST_EXIT=0`. -The shared interpreter warns that pytest-timeout is absent; the outer timeout remains 300 seconds. -A broader intermediate run gave 126 passed and two failures: the remaining #1878 correction -failure, and the empty-directory/parent-repository defect that this final owned run confirms fixed. diff --git a/src/agent_comms/goals.py b/src/agent_comms/goals.py index e6863885c2..05055b3114 100644 --- a/src/agent_comms/goals.py +++ b/src/agent_comms/goals.py @@ -320,16 +320,13 @@ async def run(self) -> ChildResult: return await BoundedRun.run(("bash", "-c", self.command), cwd=self.directory, timeout=self.timeout) - def recorded(self, sha: str, repository: str) -> tuple[ChildResult, ...]: - """Read an explicit run's receipt; absent evidence never starts the check.""" - return tuple(checked.result for checked in CheckedCommit.stored(repository, sha) - if checked.command == self.command)[:1] - def at(self, revision: str, repository: str) -> ChildResult: - """The check on ``revision``: a recorded result, or an explicit run in a detached checkout.""" + """The check on ``revision``: the result a git note on the commit records, or one run in + a throwaway detached checkout and recorded there for every later reader.""" sha = CheckedCommit.git(repository, "rev-parse", f"{revision}^{{commit}}").strip() - for result in self.recorded(sha, repository): - return result + for checked in CheckedCommit.stored(repository, sha): + if checked.command == self.command: + return checked.result with tempfile.TemporaryDirectory(prefix="order-check-") as scratch: tree = str(Path(scratch) / "tree") CheckedCommit.git(repository, "worktree", "add", "--detach", tree, sha) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 90d9aad8e5..a0c2f1dd5a 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -28,9 +28,10 @@ from .errors import ToolRefusedError from .field_codec import ( FieldCodec, GitCommitText, MalformedRecord, NullMember, RecordMember, SubclassUnion, WireValue, + boundary_decoder, ) from .channel_targets import CommandTarget -from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder +from .goals import CountCheckResult, OrderCheck, ProblemGoalOrder from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime from .thread_identity import RecordedThreadIncarnation, ThreadIncarnation from .threads import Thread @@ -38,12 +39,10 @@ from .standing_rules import UndatedStandingRule from .storage_rules import StorageRule from .plan_phases import OrderPhase, UnplannedPhase -from .work_branches import WorkBranchError, WorkBranches if TYPE_CHECKING: from .comms import Comms from .goal_management import Goals - from .registration import Registration @dataclass(frozen=True) @@ -77,20 +76,15 @@ class OrderDelivery(DeclaredFamily, affix="Delivery"): def work(self) -> str: return "delivered work" - @property - def line(self) -> str: - return self.work - async def run(self, check, *command): result = await BoundedRun.run(tuple(command), cwd=check.directory, timeout=120) if result.outcome != ExitedOutcome(0): raise ValueError(result.stderr.decode(errors="replace") or "Delivery source check failed.") return result.stdout.decode(errors="replace") - async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: + async def verified(self, check: OrderCheck, deletes: OrderDeletion) -> OrderDelivery: """The order's check and every count its deletion declares run on the delivered source.""" - check = todo.order.check - for each in (check, *todo.order.deletes.checks): + for each in (check, *deletes.checks): await self.require_source(each) each.result.require(await self.checked(each)) return replace(self, at=await self.delivery_time(check)) @@ -177,102 +171,9 @@ async def require_source(self, check: OrderCheck) -> None: raise ValueError( f"Delivered commit {self.delivered} is on no origin branch; push it first.") - async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: - verified = await super().verified(todo, registry) - merge = await self.merge_state(todo, registry) - return CheckedCommitDelivery(commit=self.commit, delivered_commit=self.delivered, - at=verified.at, evidence=self.evidence, merge=merge) - - async def merge_state(self, todo: Todo, registry: Registration) -> DeliveryMerge: - """Observe integration, not merely publication, at the time of completion. - - Git proves ordinary merges to main. The order's merged PR also proves squash merges - and names a non-main target. Otherwise the pushed source must still have an open link, - whose owner is WorkBranches, not a second branch policy in the delivery tool. - """ - check = todo.order.check - branches = (await self.run( - check, "git", "for-each-ref", f"--contains={self.delivered}", - "--format=%(refname:strip=3)", "refs/remotes/origin/")).splitlines() - if WorkBranches.main in branches: - return MainDeliveryMerge() - requests = await asyncio.to_thread(PullRequests((todo,)).listed, - todo.repo, "merged", todo.order) - for request in requests: - if request.merge and await self.contains(check, request.head): - return (MainDeliveryMerge() if request.base == WorkBranches.main - else BranchDeliveryMerge(request.base, request.merge)) - linked = await asyncio.to_thread( - WorkBranches(Path(check.directory)).read, registry.snapshot()) - sources = tuple(sorted(branch.name for branch in linked - if branch.name in branches and branch.linked)) - if not sources: - raise ValueError( - f"Delivered commit {self.delivered} is not merged and has no branch linked " - "to an open PR or issue.") - return UnmergedDeliveryMerge(sources) - - async def contains(self, check: OrderCheck, head: str) -> bool: - if head == self.delivered: - return True - result = await BoundedRun.run( - ("git", "merge-base", "--is-ancestor", self.delivered, head), - cwd=check.directory, timeout=120) - if result.outcome not in (ExitedOutcome(0), ExitedOutcome(1)): - raise ValueError(result.stderr.decode(errors="replace") or - "Could not check the delivered commit against its PR head.") - return result.outcome == ExitedOutcome(0) - - -class DeliveryMerge(DeclaredFamily, affix="DeliveryMerge"): - """The integration observed when Core checked a commit delivery; never a claim of install.""" - - @property - @abstractmethod - def line(self) -> str: ... - - -@dataclass(frozen=True) -class MainDeliveryMerge(DeliveryMerge): - @property - def line(self) -> str: - return f"merged to {WorkBranches.main}" - - -@dataclass(frozen=True) -class BranchDeliveryMerge(DeliveryMerge): - branch: str - commit: str - - @property - def line(self) -> str: - return f"merged to {self.branch} as {self.commit}" - - -@dataclass(frozen=True) -class UnmergedDeliveryMerge(DeliveryMerge): - branches: tuple[str, ...] - - @property - def line(self) -> str: - return f"not merged; linked branches: {', '.join(self.branches)}" - - -@dataclass(frozen=True, kw_only=True) -class CheckedCommitDelivery(CommitDelivery): - merge: DeliveryMerge - - @property - def line(self) -> str: - return f"{self.work}; {self.merge.line}" - @dataclass(frozen=True, kw_only=True) class OperationDelivery(OrderDelivery): - @property - def line(self) -> str: - return "operation checked" - async def require_source(self, check): pass @@ -292,38 +193,21 @@ class PullRequest: merged_at: float | None = None updated_at: float = 0.0 orders: tuple[str, ...] = () - base: str = field(default=WorkBranches.main, kw_only=True) + + @classmethod + @boundary_decoder + def listed(cls, number: int, branch: str, head: str, merge: str, merged: str, + updated: str, body: str) -> Self: + """One row of ``gh pr list``; GitHub leaves an unmerged PR's merge commit and time empty.""" + return cls(int(number), branch, head, merge or None, + _github_time(merged) if merged else None, _github_time(updated), + ProblemOrder.pull_request_ids(body)) def _github_time(text: str) -> float: return calendar.timegm(time.strptime(text, "%Y-%m-%dT%H:%M:%SZ")) -@dataclass(frozen=True) -class PullRequestCommit: - """GitHub's optional mergeCommit object, decoded at the query boundary.""" - oid: str - - -@dataclass(frozen=True) -class ListedPullRequest: - """The external query row; conversion lives here, never in its consumers.""" - number: int - branch: str - head: str - merge: PullRequestCommit | None - merged: str | None - updated: str - body: str - base: str - - def parsed(self) -> PullRequest: - return PullRequest( - self.number, self.branch, self.head, self.merge.oid if self.merge else None, - _github_time(self.merged) if self.merged else None, _github_time(self.updated), - ProblemOrder.pull_request_ids(self.body), base=self.base) - - class PullRequests: """Pull requests queried by immutable order identity, cached within one board read.""" @@ -338,7 +222,7 @@ def listed(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullReques key = (repo, state, order.id) if key not in self._read: self._read[key] = self.read(repo, state, order) - return tuple(request for request in self._read[key] if order.id in request.orders) + return self._read[key] def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, ...]: orders = self._orders.setdefault(repo, {}) @@ -350,19 +234,20 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, f'"{candidate.pull_request_reference}" sort:updated-desc') selections.append( f'{alias}: search(query: {search}, type: ISSUE, first: 100) {{ nodes {{ ' - '... on PullRequest { number branch: headRefName head: headRefOid body ' - 'base: baseRefName merge: mergeCommit { oid } merged: mergedAt ' - 'updated: updatedAt } } }') + '... on PullRequest { number headRefName headRefOid body ' + 'mergeCommit { oid } mergedAt updatedAt } } }') listed = subprocess.run( ("gh", "api", "graphql", "-f", "query=query {" + " ".join(selections) + "}", - "--jq", '.data | map_values(.nodes)'), + "--jq", '.data | map_values(.nodes | map([.number, .headRefName, .headRefOid,' + ' .mergeCommit.oid // "", .mergedAt // "", .updatedAt, .body]))'), capture_output=True, text=True, timeout=60) if listed.returncode: - raise WorkBranchError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") - rows = FieldCodec.decode(dict[str, tuple[ListedPullRequest, ...]], + raise ValueError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") + rows = FieldCodec.decode(dict[str, tuple[tuple[int, str, str, str, str, str, str], ...]], FieldCodec.parse(listed.stdout)) for alias, candidate in aliases.items(): - self._read[repo, state, candidate.id] = tuple(row.parsed() for row in rows[alias]) + self._read[repo, state, candidate.id] = tuple( + PullRequest.listed(*row) for row in rows[alias]) return self._read[repo, state, order.id] @@ -437,13 +322,12 @@ class DistanceReadings: """The order's last two distance readings, oldest first: its base at the order, then its newest commit. Fewer than two read, or a last reading at done, project nothing.""" readings: tuple[DistanceReading, ...] = () - window: ClassVar[int] = 2 @property def trend(self) -> DistanceTrend: """Velocity across the two readings: falling projects the finish, else it is flat.""" - last = self.readings[-self.window:] - if len(last) < self.window or not last[1].distance: + last = self.readings[-2:] + if len(last) < 2 or not last[1].distance: return UnreadTrend() first, final = last if final.distance >= first.distance: @@ -493,16 +377,11 @@ def projection(self) -> str: @dataclass(frozen=True) class ProgressSource: - """The repository's PR facts, extended by local evidence only while a checkout is available.""" + """What an order's progress is read from: its unit's checkout and the repository's PRs.""" todo: Todo checkout: Path pulls: PullRequests - @staticmethod - def at(todo: Todo, checkout: Path, pulls: PullRequests) -> ProgressSource: - return (LocalProgressSource(todo, checkout, pulls) if checkout.is_dir() - else ProgressSource(todo, checkout, pulls)) - @property def since(self) -> float: return self.todo.order.at.recorded_at("An order") @@ -510,45 +389,31 @@ def since(self) -> float: def progress(self) -> OrderProgress: """One selection algorithm for measured tools and read-only status views.""" members = sorted(OrderProgress.members_with(FoundProgress), key=lambda member: member.precedence) - try: - return next((progress for member in members for progress in member.found(self)), - NoCommitProgress()) - except (WorkBranchError, ValueError, subprocess.TimeoutExpired) as unread: - return UnreadProgress(str(unread)) + return next((progress for member in members for progress in member.found(self)), + NoCommitProgress()) def requests(self, state: str) -> list[PullRequest]: """The order's explicit PR references survive checkout removal or branch reuse.""" return [pr for pr in self.pulls.listed(self.todo.repo, state, self.todo.order) - if pr.updated_at >= self.since] - - def commits(self, *excluded: str) -> Iterator[ProgressPoint]: - """A remote source makes no claim about unpublished local commits.""" - return iter(()) + if self.todo.order.id in pr.orders and pr.updated_at >= self.since] - def readings(self) -> DistanceReadings: - return DistanceReadings() - - def latest_commit(self) -> str: - raise ValueError("The unit checkout is unavailable.") - - def cadence_commits(self) -> tuple[ProgressPoint, ...]: - """No checkout means no local commit evidence, not zero velocity.""" - return () - - -class LocalProgressSource(ProgressSource): - """An available checkout supplies the local commit and distance evidence.""" - - def commits(self, *excluded: str) -> Iterator[ProgressPoint]: + def commits(self, *excluded: str) -> Iterator[tuple[str, float]]: """The checkout's newest commit since the order that no ``excluded`` ref already holds.""" after = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(self.since)) - log = self.git("log", "-1", f"--since={after}", f"--format={ProgressPoint.format}", - "HEAD", "--not", *excluded) - return (ProgressPoint.listed(line) for line in log.splitlines()) + log = subprocess.run( + ("git", "-C", str(self.checkout), "log", "-1", f"--since={after}", + "--format=%H %ct", "HEAD", "--not", *excluded), + capture_output=True, text=True, timeout=30) + for line in log.stdout.splitlines() if not log.returncode else (): + sha, at = line.split() + yield sha, float(at) def git(self, *args: str) -> str: - return CheckedCommit.git( - str(self.checkout), f"--git-dir={(self.checkout / '.git').absolute()}", *args) + run = subprocess.run(("git", "-C", str(self.checkout), *args), + capture_output=True, text=True, timeout=60) + if run.returncode: + raise ValueError(run.stderr.strip() or f"git {args[0]} failed") + return run.stdout def latest_commit(self) -> str: """The checkout's latest actual commit, even when it predates this order.""" @@ -557,10 +422,10 @@ def latest_commit(self) -> str: def readings(self) -> DistanceReadings: """The order's base, then each branch commit since the order; the last two are read.""" base = self.git("merge-base", "HEAD", "origin/HEAD").strip() - commits = self.branch_commits(base, DistanceReadings.window) - points = [ProgressPoint(base, self.since), *commits][-DistanceReadings.window:] + commits = self.branch_commits(base, 2) + points = [ProgressPoint(base, self.since), *commits][-2:] return DistanceReadings(tuple( - reading for point in points for reading in self.recorded_distances(base, point))) + DistanceReading(point.at, self.distance(base, point.sha)) for point in points)) def cadence_commits(self) -> tuple[ProgressPoint, ...]: """Bounded branch evidence since the order; never run its acceptance check.""" @@ -575,13 +440,21 @@ def branch_commits(self, base: str, limit: int) -> tuple[ProgressPoint, ...]: f"--format={ProgressPoint.format}", "HEAD", "--not", base).splitlines()) if (point := ProgressPoint.listed(line)).at >= self.since) - def recorded_distances(self, base: str, point: ProgressPoint) -> Iterator[DistanceReading]: - """Only explicit check receipts supply a distance; no receipt means unknown.""" + def distance(self, base: str, sha: str) -> int: + """The order's Check run on ``sha`` against its N, plus its numbered edits the diff since + ``base`` has not touched.""" check = self.todo.order.check - for run in check.recorded(point.sha, str(self.checkout)): - touched = self.git("diff", "--name-only", base, point.sha).split() - yield DistanceReading(point.at, check.result.distance(run) + - self.todo.order.untouched(touched)) + touched = self.git("diff", "--name-only", base, sha).split() + run = check.at(sha, str(self.checkout)) + return check.result.distance(run) + self.todo.order.untouched(touched) + + +@dataclass(frozen=True) +class StatusProgressSource(ProgressSource): + """A display reads git/PR status, never executes the order's acceptance check.""" + + def readings(self) -> DistanceReadings: + return DistanceReadings() @dataclass(frozen=True) @@ -595,7 +468,7 @@ def read(cls, todo: Todo, storage: StorageRule, pulls: PullRequests) -> OrderPro """The first found member by its declared precedence; with none found, no commit yet.""" owner = todo.holder assert owner, "Only a held order has progress." - return ProgressSource.at(todo, storage.order_checkout(owner, todo.id), pulls).progress() + return ProgressSource(todo, storage.order_checkout(owner, todo.id), pulls).progress() @property @abstractmethod @@ -696,8 +569,8 @@ class CommittedProgress(Moving, FoundProgress): @classmethod def found(cls, source: ProgressSource) -> Iterator[Self]: - return (cls(commit.sha[:9], at=commit.at, readings=source.readings()) - for commit in source.commits("--remotes")) + return (cls(sha[:9], at=at, readings=source.readings()) + for sha, at in source.commits("--remotes")) @property def state(self) -> str: @@ -711,8 +584,8 @@ class PushedProgress(CommittedProgress): @classmethod def found(cls, source: ProgressSource) -> Iterator[Self]: - return (cls(commit.sha[:9], at=commit.at, readings=source.readings()) - for commit in source.commits("origin/HEAD")) + return (cls(sha[:9], at=at, readings=source.readings()) + for sha, at in source.commits("origin/HEAD")) @property def state(self) -> str: @@ -1495,10 +1368,7 @@ class DroppedProblemOrder(UnorderedProblemOrder, CheckedProblemOrder): class DeliveredProblemOrder(CheckedProblemOrder): delivery: OrderDelivery pending: ClassVar[bool] = False - - @property - def line(self) -> str: - return f"Delivered: {self.delivery.line}, waiting on install" + line: ClassVar[str] = "Delivered, waiting on install" def require_transfer(self, todo: Todo) -> None: owner = (todo.assignment or todo.released[-1].previous).owner @@ -2086,7 +1956,7 @@ def prepare(self, goals, current): current.require_commander(self.actor) current.state.require_open(current) current.order.require_delivery(self.order_id) - verified = self.delivery.verified(current, goals.registry) + verified = self.delivery.verified(current.order.check, current.order.deletes) return replace(self, delivery=asyncio.run(verified)) def apply(self, current): diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 633b1ea44f..b289e55650 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -18,7 +18,7 @@ from agent_comms.revision_views import CommanderView from agent_comms.thread_identity import ThreadIncarnation from agent_comms.todos import ( - CommitMomentum, OrderProgress, ProgressPoint, ProgressSource, PullRequests, Todo, + CommitMomentum, OrderProgress, ProgressPoint, StatusProgressSource, PullRequests, Todo, ) from agent_comms.turn_digests import LastTurns, ThreadTurns from agent_comms.ui_model.observation import ( @@ -168,7 +168,7 @@ def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: grouped: dict[ThreadIncarnation, list[CommanderOrder]] = {} for todo in todos: unit = todo.held().owner_identity - source = ProgressSource.at(todo, storage.order_checkout(unit.name, todo.id), pulls) + source = StatusProgressSource(todo, storage.order_checkout(unit.name, todo.id), pulls) grouped.setdefault(unit, []).append(CommanderOrder( todo, source.progress(), todo.overdue(now), source.cadence_commits(), now)) cards = [] @@ -181,7 +181,7 @@ def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: if unit in request.expanded: commits = [] for order in current: - source = ProgressSource.at(order.todo, storage.order_checkout( + source = StatusProgressSource(order.todo, storage.order_checkout( unit.name, order.todo.id), pulls) try: commit = source.latest_commit() diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index 2482ca0ac6..cfde318ebe 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -3,9 +3,8 @@ import pytest -from agent_comms.goals import OrderCheck from agent_comms.recorded_time import RecordedEventTime -from agent_comms.todos import CommitMomentum, ProgressPoint, ProgressSource, PullRequests +from agent_comms.todos import CommitMomentum, ProgressPoint, ProgressSource, PullRequests, StatusProgressSource from agent_comms.ui_model.commander import ( CommanderModel, ObserveCommander, @@ -42,12 +41,12 @@ def test_board_uses_order_state_and_requests_details_only_when_opened(commander) def test_status_source_never_requests_acceptance_measurement(commander, monkeypatch): comms, caller = commander - create(commander, 'P1') - def forbidden(*args, **kwargs): + todo = create(commander, 'P1') + def forbidden(self): pytest.fail('A read-only commander page executed an acceptance measurement') - monkeypatch.setattr(OrderCheck, 'at', forbidden) - monkeypatch.setattr(OrderCheck, 'run', forbidden) - # Progress now consumes retained check receipts, never executes a measurement. + monkeypatch.setattr(ProgressSource, 'readings', forbidden) + source = StatusProgressSource(todo, comms.root, PullRequests((todo,))) + assert not source.readings().readings model = CommanderModel.read(comms, ObserveCommander( str(comms.root), comms.registry.require(caller.name).incarnation)) assert model.units[0].orders[0].progress.state == 'no commit since the order' diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index bc306d8d75..f9cec65105 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -234,7 +234,7 @@ def git(*args): git('init', '-q', '-b', 'main') git('commit', '-q', '--allow-empty', '-m', 'Delivered fix') commit = git('rev-parse', 'HEAD') - git('update-ref', 'refs/remotes/origin/main', commit) + git('update-ref', 'refs/remotes/origin/fix', commit) marker = comms.root / 'check-calls' create(commander, 'P1', f"printf x >> '{marker}'; printf 1") create(commander, 'P2') @@ -288,7 +288,6 @@ def git(*args, at=None): with pytest.raises(ValueError, match='no origin branch'): CommsOrderDoneTool(problem='P1', order_id=comms.goals.todos.get('P1').order.id, delivery=CommitDelivery(commit=merge, delivered_commit=delivered), claims=working(comms)).apply(comms, caller) git('update-ref', 'refs/remotes/origin/work', delivered) - git('update-ref', 'refs/remotes/origin/main', merge) script.done('P1', delivered, seen=working(comms)[0].screenshot) met = comms.goals.todos.get('P1') assert (met.order.delivery.commit, met.order.delivery.delivered, met.order.delivery.at) == ( @@ -1297,7 +1296,7 @@ def test_a_problem_goal_shows_only_its_live_orders_through_convert_deliver_send_ todo.assignment, 'toolu_3', reason=StrikeReason(why))).apply(comms, caller) worker = comms.registry.require('worker') progress = GoalSegment.capture(worker, worker.goal, comms.goals).progress - assert progress.splitlines() == ['P1: Delivered: operation checked, waiting on install', f'P2: Open · Sent back: {why}'] + assert progress.splitlines() == ['P1: Delivered, waiting on install', f'P2: Open · Sent back: {why}'] assert (worker.goal.text, worker.goal.progress) == (ProblemGoalOrder.text, '') with pytest.raises(ValueError, match='block the problem.s Todo, or standby with wait_for'): comms.goals.update_goal('worker', BlockedGoalAction(block_reason='Waiting on lead.'), actor=ModelInvocable) diff --git a/tests/test_delivery_merge.py b/tests/test_delivery_merge.py deleted file mode 100644 index af1d96d0b0..0000000000 --- a/tests/test_delivery_merge.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Small record/codec checks; the real CLI proof is recorded separately.""" -from agent_comms.field_codec import FieldCodec -from agent_comms.todos import ( - BranchDeliveryMerge, CheckedCommitDelivery, DeliveryMerge, ListedPullRequest, - MainDeliveryMerge, OrderDelivery, UnmergedDeliveryMerge, -) - - -def test_delivery_integration_members_keep_their_data_through_the_only_codec(): - for state in (MainDeliveryMerge(), BranchDeliveryMerge('release', 'b' * 40), - UnmergedDeliveryMerge(('fix/order',))): - delivery = CheckedCommitDelivery(commit='a' * 40, delivered_commit='a' * 40, merge=state) - restored = FieldCodec.decode(OrderDelivery, FieldCodec.encode(delivery)) - assert restored == delivery - assert FieldCodec.decode(DeliveryMerge, FieldCodec.encode(state)) == state - assert state.line in restored.line - assert restored.work == f'delivered commit {"a" * 40}' - - -def test_named_forge_row_preserves_the_actual_nonmain_merge_target(): - row = FieldCodec.decode(ListedPullRequest, FieldCodec.parse('''{ - "number": 42, "branch": "fix/order", "head": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "merge": {"oid": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, - "merged": "2026-10-19T12:00:00Z", "updated": "2026-10-19T12:00:01Z", - "body": "Agent-Comms-Order: proof-order", "base": "release" - }''')) - request = row.parsed() - assert request.base == 'release' - assert request.merge == 'b' * 40 - assert request.merged_at < request.updated_at - assert request.orders == ('proof-order',) diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 8bacfa1a09..9b42b53046 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -1,6 +1,5 @@ """An order's progress is read from its unit's checkout and PRs beside its deadline; merged work draws no strike.""" import subprocess -import shlex import time from dataclasses import replace @@ -8,16 +7,13 @@ import pytest from agent_comms.recorded_time import RecordedEventTime -from agent_comms.goals import CountCheckResult, GitCommandFailed, OrderCheck from agent_comms.tools import CommsOrderDoneTool from agent_comms.todos import (CommittedProgress, DistanceReadings, MergedProgress, NoCommitProgress, InstallDeadline, InstallDue, OpenPrProgress, OperationDelivery, OrderProgress, PlannedInstallDeadline, - LocalProgressSource, ProgressPoint, ProgressSource, PullRequest, PullRequests, - PushedProgress, UnreadProgress) + PullRequest, PullRequests, PushedProgress, UnreadProgress) from agent_comms.messages import InfoKind from agent_comms.storage_rules import StorageRule -from agent_comms.work_branches import WorkBranchError from test_commander_problem_orders import create, working @@ -120,36 +116,11 @@ def test_a_merged_order_still_awaits_its_record(checkout): def test_unread_github_is_explicit(checkout): pulls = Listed() - pulls.states = WorkBranchError('gh: not logged in') + pulls.states = ValueError('gh: not logged in') progress = read(checkout, pulls) assert progress == UnreadProgress('gh: not logged in') -def test_progress_reads_only_explicit_check_receipts(checkout, tmp_path): - todo, unit = checkout - marker = tmp_path / 'explicit-check-count' - check = OrderCheck(str(unit), f'printf x >> {shlex.quote(str(marker))}; printf 0', - CountCheckResult(0)) - todo = replace(todo, order=replace(todo.order, check=check)) - source = ProgressSource.at(todo, unit, Listed()) - assert source.readings() == DistanceReadings() - assert not marker.exists() - check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit)) - assert marker.read_text() == 'x' - assert len(source.readings().readings) == 1 - assert source.readings().readings[0].distance == todo.order.untouched(()) - assert marker.read_text() == 'x' - - -def test_an_existing_broken_checkout_is_not_silently_treated_as_missing(checkout, tmp_path): - bad = tmp_path / 'not-a-repository' - bad.mkdir() - source = ProgressSource.at(checkout[0], bad, Listed()) - assert isinstance(source, LocalProgressSource) - with pytest.raises(GitCommandFailed): - source.readings() - - def test_board_and_overdue_name_a_merge_awaiting_its_record(commander, commander_script, monkeypatch, tmp_path): script, _ = commander_script # worker's P1 checkout, on the branch its merged PR came from. @@ -183,8 +154,7 @@ class HeldSource: since = 100.0 def commits(self, *excluded): - commits = {('--remotes',): ProgressPoint('a' * 40, 1000.0), - ('origin/HEAD',): ProgressPoint('e' * 40, 1000.0)} + commits = {('--remotes',): ('a' * 40, 1000.0), ('origin/HEAD',): ('e' * 40, 1000.0)} yield commits[excluded] def readings(self): @@ -344,8 +314,8 @@ def forge(command, **arguments): monkeypatch.setattr(subprocess, "run", forge) script.PullRequestVerb(pid=pid, title=subject, body_file=str(body_file)).run() assert todo.order.pull_request_ids(bodies[0]) == (todo.order.id,) - merged = PullRequest(number, branch, "b" * 40, "d" * 40, - merged_at=500, updated_at=500, orders=todo.order.pull_request_ids(bodies[0])) + stamp = "1970-01-01T00:08:20Z" + merged = PullRequest.listed(number, branch, "b" * 40, "d" * 40, stamp, stamp, bodies[0]) foreign = replace(merged, number=number + 1, merge="f" * 40, orders=("another-order",)) monkeypatch.setattr(PullRequests, "read", lambda self, repo, state, order: (foreign, merged) if state == "merged" else ()) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index d4e81414c8..d57aceed9d 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -55,12 +55,13 @@ def test_board_self_check_uses_order_progress_and_its_linked_merge( commander, standing, monkeypatch, reported, alarms): comms, caller = commander owned, pending, foreign = (create(commander, pid) for pid in ("P77", "P109", "P117")) - merged = PullRequest(1581, "docs/module-layers", "b" * 40, "d" * 40, - merged_at=500, updated_at=500, orders=(owned.order.id,)) - opened = PullRequest(1624, "ratchet/none-absence", "c" * 40, - updated_at=500, orders=(pending.order.id,)) - other = PullRequest(1629, "ratchet/decoder-marker", "e" * 40, "f" * 40, - merged_at=500, updated_at=500, orders=("another-order",)) + stamp = "1970-01-01T00:08:20Z" + merged = PullRequest.listed(1581, "docs/module-layers", "b" * 40, "d" * 40, + stamp, stamp, owned.order.pull_request_body("Module layer map")) + opened = PullRequest.listed(1624, "ratchet/none-absence", "c" * 40, "", "", + stamp, pending.order.pull_request_body("Declared measure")) + other = PullRequest.listed(1629, "ratchet/decoder-marker", "e" * 40, "f" * 40, + stamp, stamp, "Agent-Comms-Order: another-order\n") def forge(self, repo, state, order): return (merged, other) if state == "merged" else (opened,) diff --git a/tools/proofs/cn_phase3_record.py b/tools/proofs/cn_phase3_record.py deleted file mode 100644 index e1d2ba9ab1..0000000000 --- a/tools/proofs/cn_phase3_record.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Exercise real Core records on an isolated bus; never start or replay a model turn. - -Run setup, perform a real tool edit in the owning native session, then run record with that -session path. All observations below are CLI tool results, not hand-constructed events. -""" -from __future__ import annotations - -import argparse -import json -import os -from pathlib import Path -import shutil -import subprocess -import sys -import time - -from agent_comms.change_report import FileLine -from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec -from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction -from agent_comms.goals import CountCheckResult, OrderCheck, RequestGoalOrder -from agent_comms.recorded_time import RecordedEventTime -from agent_comms.tools import (CommsFireTool, CommsOrderDoneTool, CommsTodoCreateTool, - CommsTodoTool, CommsVerdictTool) -from agent_comms.todos import (AcceptedVerdict, CommitDelivery, MechanicalOrderRun, - PendingProblemOrder, StrikeReason, StrikeTodo, VoidStrikeTodo) -from agent_comms.progress_claims import MergedClaim - -CHECKOUT = Path(__file__).resolve().parents[2] -ROOT = CHECKOUT / ".proof-record-plane" -RECEIPT = CHECKOUT / "docs/evidence/cn-phase3-record-results.jsonl" - - -def cli(caller, tool): - arguments = FieldCodec.encode(tool) - del arguments[tool.family_discriminator] - command = [sys.executable, "-m", "agent_comms.cli", "--root", str(ROOT), "tool", - "--from", caller, "--tool", tool.declared_name, - "--arguments", json.dumps(arguments)] - result = subprocess.run(command, capture_output=True, text=True, timeout=180, - env=os.environ | {"PYTHONPATH": str(CHECKOUT / "src")}) - print(result.stdout, end="", flush=True) - if result.returncode: - raise RuntimeError(result.stderr or result.stdout) - with RECEIPT.open("a") as output: - output.write(result.stdout) - return result.stdout - - -def setup(): - if ROOT.exists(): - raise RuntimeError(f"Refusing to reuse an existing proof root: {ROOT}") - comms = Comms(ROOT) - comms.messaging.initialize_private_initial_protocol() - for name in ("record-lead", "record-worker"): - comms.registry.declare(comms.registry.new_thread( - name, frozenset(), str(CHECKOUT), created_at=time.time())) - comms.goals.update_goal("record-lead", SetGoalAction( - text="Confirm the order record through the actual Core tools on this isolated bus", - goal_id="record-request", order=RequestGoalOrder())) - comms.goals.update_goal("record-worker", SetGoalAction( - text="Complete the assigned record-plane proof without touching any live session", - goal_id="record-work")) - create_order() - - -def create_order(problem="P1"): - now = time.time() - cli("record-lead", CommsTodoCreateTool(owner="record-worker", order=CreateProblemGoalAction( - problem=problem, repo="OpenHCSDev/agent-comms", - text="Show the real verdict and strike records without a second source", - request="record-request", commander="record-lead", - deadline=RecordedEventTime(now + 3600), order=PendingProblemOrder( - id=f"record-order-{problem}", owner=FileLine(str(CHECKOUT / "src/agent_comms/todos.py"), 69), - check=OrderCheck(str(CHECKOUT), "printf 1", CountCheckResult(1)), - edits=("Decode pull request target branches through the existing record owner",), - banned=("No second order store",), report="Report the actual Core tool response", - run=MechanicalOrderRun(), at=RecordedEventTime(now), - first_call_deadline=RecordedEventTime(now + 120))))) - - -def record(session: Path): - from dataclasses import replace - from agent_comms.turn_digests import OrderEditsRead, TurnsSince - - retained = ROOT / "record-worker.jsonl" - shutil.copy2(session, retained) - comms = Comms(ROOT) - worker = comms.registry.require("record-worker") - comms.registry.register(replace(worker, session_file=str(retained)), - comms.registry.status(worker.name)) - todo = comms.goals.todos.get("P1") - since = todo.order.at.recorded_at("An order") - calls = OrderEditsRead(ROOT, comms.registry.require(worker.name), TurnsSince(since), todo).read() - if not calls: - raise RuntimeError("No actual native edit call followed the order; do not invent one") - judged = calls[0].id - cli("record-lead", CommsVerdictTool(id="P1", operation=AcceptedVerdict( - todo.revision, comms.registry.require("record-lead").incarnation, todo.held(), judged, - quote="Decode pull request target branches"))) - for reason in ("The assigned change left the target branch out of the saved order record", - "The second review found the target branch still missing from the saved record"): - todo = comms.goals.todos.get("P1") - cli("record-lead", CommsTodoTool(id="P1", operation=StrikeTodo( - todo.revision, comms.registry.require("record-lead").incarnation, todo.held(), reason))) - todo = comms.goals.todos.get("P1") - assert todo.struck_out(worker.name) - cli("record-lead", CommsFireTool(unit=worker.name, reason=StrikeReason( - "The unit left the required target branch absent after both recorded reviews"))) - todo = comms.goals.todos.get("P1") - cli("record-lead", CommsTodoTool(id="P1", operation=VoidStrikeTodo( - todo.revision, comms.registry.require("record-lead").incarnation, todo.strikes[-1], - "The second strike repeated the first finding rather than describing another failed change"))) - todo = comms.goals.todos.get("P1") - assert len(todo.strikes) == 2 and len(todo.voided) == 1 - assert len(todo.live_strikes(worker.name)) == 1 and todo.assignment is None - - -def deliver(problem: str, delivered: str, checked: str | None): - create_order(problem) - checked = checked or subprocess.check_output( - ["git", "rev-parse", "HEAD"], cwd=CHECKOUT, text=True).strip() - cli("record-lead", CommsOrderDoneTool( - problem=problem, order_id=f"record-order-{problem}", - delivery=CommitDelivery(commit=checked, delivered_commit=delivered), - claims=(MergedClaim(delivered),))) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("step", choices=("setup", "order", "record", "deliver")) - parser.add_argument("--session", type=Path) - parser.add_argument("--problem") - parser.add_argument("--delivered") - parser.add_argument("--checked") - args = parser.parse_args() - if args.step == "setup": - setup() - elif args.step == "order": - create_order() - elif args.step == "deliver": - deliver(args.problem, args.delivered, args.checked) - else: - record(args.session) diff --git a/tools/proofs/cn_progress_read_only.py b/tools/proofs/cn_progress_read_only.py deleted file mode 100644 index ccc2c78d98..0000000000 --- a/tools/proofs/cn_progress_read_only.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Real Core board reads consume explicit check receipts, never run the check.""" -import argparse -import shlex -import subprocess - -from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec -from agent_comms.goals import CheckedCommit, CountCheckResult, OrderCheck -from agent_comms.todos import CorrectCheckTodo, PushedProgress -from agent_comms.tools import CommsTodoListTool, CommsTodoTool, TodoList -from tools.proofs.cn_phase3_record import CHECKOUT, ROOT, cli, create_order - -MARKER = ROOT / 'explicit-check-count' - - -def prepare(): - create_order('P6') - comms = Comms(ROOT) - todo = comms.goals.todos.get('P6') - check = OrderCheck(str(CHECKOUT), f'printf x >> {shlex.quote(str(MARKER))}; printf 0', - CountCheckResult(0)) - cli('record-lead', CommsTodoTool(id='P6', operation=CorrectCheckTodo( - todo.revision, comms.registry.require('record-lead').incarnation, - todo.order.check, check, 'Prove that reading the board cannot execute the check'))) - - -def board(): - listed = FieldCodec.decode(TodoList, FieldCodec.parse(cli('record-lead', CommsTodoListTool()))) - progress = listed.progress['P6'] - assert isinstance(progress, PushedProgress), progress - return progress.distance_readings() - - -def verify(): - comms = Comms(ROOT) - todo = comms.goals.todos.get('P6') - checkout = comms.registry.storage_rule.order_checkout(todo.held().owner, todo.id) - if checkout.exists() or MARKER.exists(): - raise RuntimeError('Proof resources already exist; inspect before another attempt') - subprocess.run(['git', 'worktree', 'add', '--detach', str(checkout), 'HEAD'], - cwd=CHECKOUT, check=True, timeout=60) - try: - assert not board().readings - assert not MARKER.exists() - sha = CheckedCommit.git(str(checkout), 'rev-parse', 'HEAD').strip() - assert not todo.order.check.recorded(sha, str(checkout)) - todo.order.check.at(sha, str(checkout)) # Deliberately explicit, never a board read. - assert MARKER.read_text() == 'x' - assert len(board().readings) == 1 - assert MARKER.read_text() == 'x' - for receipt in CheckedCommit.stored(str(checkout), sha): - if receipt.command == todo.order.check.command: - print(FieldCodec.encode(receipt), flush=True) - print('Actual CLI: unknown before explicit run, recorded afterward; exactly one execution.', - flush=True) - finally: - subprocess.run(['git', 'worktree', 'remove', str(checkout)], cwd=CHECKOUT, - check=True, timeout=60) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('step', choices=('prepare', 'verify')) - args = parser.parse_args() - prepare() if args.step == 'prepare' else verify() From 7b1c81d207a14b64f649b54bcee9189b513b1234 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:25:51 -0400 Subject: [PATCH 15/20] Remove competing phase 3 source changes and repair page typing and error ownership --- src/agent_comms/history_views.py | 2 + src/agent_comms/presentation.py | 27 +++-- src/agent_comms/todos.py | 64 ++++++++---- src/agent_comms/ui_model/commander.py | 136 ++++++++++++++++++-------- tests/test_commander_model.py | 7 +- tools/commander_page_journey.py | 78 ++++++++++----- 6 files changed, 219 insertions(+), 95 deletions(-) diff --git a/src/agent_comms/history_views.py b/src/agent_comms/history_views.py index a4c261c79b..c284b76548 100644 --- a/src/agent_comms/history_views.py +++ b/src/agent_comms/history_views.py @@ -174,6 +174,8 @@ def require(self, name: str) -> ThreadPresentation: class HistoryViews: + goals: Goals + def __init__( self, root: Path, diff --git a/src/agent_comms/presentation.py b/src/agent_comms/presentation.py index b5af8d2c5e..c8b49a7eb5 100644 --- a/src/agent_comms/presentation.py +++ b/src/agent_comms/presentation.py @@ -20,10 +20,10 @@ from .channels import Channel, ChannelMemberIndex, Channels from .cli_result import CliResult from .coordination_errors import DMPaintedBoundChanged, StaleRevision +from .declared_family import DeclaredFamily from .display_order import ChannelSort, NameChannelSort from .display_references import DisplayReferences from .external_transcript import ExternalTranscript -from .declared_family import DeclaredFamily from .field_codec import FieldCodec, JsonShapeFamily, JsonShapeMember, projected from .goal_presentation import ThreadGoalExecution from .goal_waits import GoalWaits @@ -39,15 +39,27 @@ from .notification_progress import DeliveryProgress, HumanReadProgress, ReadDelivery from .pi_counts import PiCount from .private_path import FileRevision -from .ui_model.unread import UnreadPresentation, UnobservedUnread -from .read_basis import (AnonymousViewer, CapturedSelection, ChannelDisplayScope, DisplayViewer, - NamedViewer) +from .read_basis import ( + AnonymousViewer, + CapturedSelection, + ChannelDisplayScope, + DisplayViewer, + NamedViewer, +) from .read_ledger import NoReadNotice, ReadLedger, ReadNotice from .recorded_bus import RecordedBusIdentity from .registry_document import DEFAULT_ROSTER, RosterVisibility -from .revision_views import (STATUS_SOURCE, BusSourceView, CommanderView, ExternalTranscriptsView, - NotificationsView, RegistrationsView, RevisionView, - SidebarView, StoresView) +from .revision_views import ( + STATUS_SOURCE, + BusSourceView, + CommanderView, + ExternalTranscriptsView, + NotificationsView, + RegistrationsView, + RevisionView, + SidebarView, + StoresView, +) from .runtime_info import RuntimeInfo from .store_files import SharedLock, _store_lock from .thread_identity import ThreadIncarnation @@ -59,6 +71,7 @@ from .thread_status import ThreadStatus, ThreadStatusName from .threads import Thread from .tool_result import ToolResult +from .ui_model.unread import UnobservedUnread, UnreadPresentation from .wire_log import WireLog, WireSnapshot if TYPE_CHECKING: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index a0c2f1dd5a..ea96decffa 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -18,7 +18,7 @@ from collections.abc import Iterator, Sequence from dataclasses import dataclass, field, fields, replace from pathlib import Path -from typing import TYPE_CHECKING, ClassVar, Final, Self +from typing import TYPE_CHECKING, ClassVar, Self from uuid import uuid4 from .change_report import FileLine @@ -276,39 +276,60 @@ class CommitMomentum: amount of remaining work, commit cadence cannot predict order completion. """ todo: Todo - commits: tuple[ProgressPoint, ...] observed_at: float window: ClassVar[int] = 2 + @classmethod + def from_commits(cls, todo: Todo, commits: tuple[ProgressPoint, ...], + observed_at: float) -> CommitMomentum: + if len(commits) < cls.window or commits[-1].at <= commits[0].at: + return UnknownCommitMomentum(todo, observed_at) + return ForecastCommitMomentum(todo, observed_at, commits[0], commits[-1]) + + @property + @abstractmethod + def forecast(self) -> str: ... + + @property + def line(self) -> str: + if not self.todo.pending: + return f"{self.todo.id}: momentum not applicable; order delivered" + due = self.todo.deadline.recorded_at("An order deadline") - self.observed_at + deadline = f"due in {due / 60:.1f}m" if due >= 0 else f"due {-due / 60:.1f}m ago" + return f"{self.todo.id}: {self.forecast}; {deadline}; completion unknown" + + +class UnknownCommitMomentum(CommitMomentum): + @property + def forecast(self) -> str: + return "cadence unknown (need two increasing commit times)" + + +@dataclass(frozen=True) +class ForecastCommitMomentum(CommitMomentum): + first: ProgressPoint + last: ProgressPoint + @property def interval(self) -> float: - if len(self.commits) < self.window: - return 0 - return self.commits[-1].at - self.commits[0].at + return self.last.at - self.first.at @property def next_at(self) -> float: - return self.commits[-1].at + self.interval if self.interval > 0 else 0 + return self.last.at + self.interval @property def deadline_margin(self) -> float: return self.todo.deadline.recorded_at("An order deadline") - self.next_at @property - def line(self) -> str: - due = self.todo.deadline.recorded_at("An order deadline") - self.observed_at - deadline = f"due in {due / 60:.1f}m" if due >= 0 else f"due {-due / 60:.1f}m ago" - if not self.todo.pending: - return f"{self.todo.id}: momentum not applicable; order delivered" - if self.interval <= 0: - return (f"{self.todo.id}: cadence unknown (need two increasing commit times); " - f"{deadline}; completion unknown") + def forecast(self) -> str: margin = self.deadline_margin relation = "before" if margin >= 0 else "after" late = self.observed_at - self.next_at missed = f"; forecast missed by {late / 60:.1f}m" if late > 0 else "" - return (f"{self.todo.id}: {self.interval / 60:.1f}m/commit; next commit ~{_clock(self.next_at)} " - f"({abs(margin) / 60:.1f}m {relation} due); {deadline}{missed}; completion unknown") + return (f"{self.interval / 60:.1f}m/commit; next commit ~{_clock(self.next_at)} " + f"({abs(margin) / 60:.1f}m {relation} due){missed}") @dataclass(frozen=True) @@ -388,7 +409,8 @@ def since(self) -> float: def progress(self) -> OrderProgress: """One selection algorithm for measured tools and read-only status views.""" - members = sorted(OrderProgress.members_with(FoundProgress), key=lambda member: member.precedence) + members = sorted(OrderProgress.members_with(FoundProgress), + key=lambda member: member.precedence) return next((progress for member in members for progress in member.found(self)), NoCommitProgress()) @@ -1147,6 +1169,10 @@ def pull_request_ids(cls, body: str) -> tuple[str, ...]: return tuple(line.removeprefix(cls.pull_request_label).strip() for line in body.splitlines() if line.startswith(cls.pull_request_label)) + def instructions(self, todo: Todo) -> str: + return "\n".join((f"Problem: {todo.id}: {todo.text}", + *self.finding.instructions(), self.line)) + def require_transfer(self, todo: Todo) -> None: raise ValueError("This problem cannot receive another order.") @@ -1185,10 +1211,6 @@ class UnorderedProblemOrder(ProblemOrder): def require_transfer(self, todo: Todo) -> None: pass - def instructions(self, todo: Todo) -> str: - return "\n".join((f"Problem: {todo.id}: {todo.text}", - *self.finding.instructions(), "No order")) - def linked(self, order: PendingProblemOrder) -> PendingProblemOrder: return replace(order, finding=self.finding) diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index b289e55650..8775d2abf3 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -3,6 +3,7 @@ The observer prepares these models. A frontend supplies only commander identity and expansion interest; it never reads stores, runs checks or decides order state. """ + from __future__ import annotations import time @@ -18,7 +19,12 @@ from agent_comms.revision_views import CommanderView from agent_comms.thread_identity import ThreadIncarnation from agent_comms.todos import ( - CommitMomentum, OrderProgress, ProgressPoint, StatusProgressSource, PullRequests, Todo, + CommitMomentum, + OrderProgress, + ProgressPoint, + PullRequests, + StatusProgressSource, + Todo, ) from agent_comms.turn_digests import LastTurns, ThreadTurns from agent_comms.ui_model.observation import ( @@ -43,14 +49,16 @@ class CommanderOrder: todo: Todo progress: OrderProgress overdue: bool - commits: tuple[ProgressPoint, ...] = () - observed_at: float = 0 + commits: tuple[ProgressPoint, ...] + observed_at: float @property def line(self) -> str: - return (f"{'OVERDUE ' if self.overdue else ''}{self.todo.id}: " - f"{' '.join(self.todo.text.split())} | due {self.todo.deadline.clock()} | " - f"{self.todo.order.line}; {self.progress.state}") + return ( + f"{'OVERDUE ' if self.overdue else ''}{self.todo.id}: " + f"{' '.join(self.todo.text.split())} | due {self.todo.deadline.clock()} | " + f"{self.todo.order.line}; {self.progress.state}" + ) class CardDetail: @@ -71,8 +79,10 @@ class OpenCardDetail(CardDetail): @property def text(self) -> str: - lines = [order.todo.order.instructions(order.todo) - + "\nLatest work: " + order.progress.state for order in self.orders] + lines = [ + order.todo.order.instructions(order.todo) + "\nLatest work: " + order.progress.state + for order in self.orders + ] lines.append("Latest checkout commits:\n" + "\n".join(self.latest_commits)) lines.append("Recent turns (newest first):") return "\n\n".join(lines) @@ -95,14 +105,18 @@ def line(self) -> str: @property def momentum(self) -> tuple[CommitMomentum, ...]: """Derived from order times and commit evidence; never a stored score.""" - return tuple(CommitMomentum(order.todo, order.commits, order.observed_at) - for order in self.orders) + return tuple( + CommitMomentum.from_commits(order.todo, order.commits, order.observed_at) + for order in self.orders + ) @property def priority(self) -> tuple[bool, float, str]: - return (not any(order.overdue for order in self.orders), - min(order.todo.deadline.recorded_at("An order deadline") for order in self.orders), - self.unit.name.casefold()) + return ( + not any(order.overdue for order in self.orders), + min(order.todo.deadline.recorded_at("An order deadline") for order in self.orders), + self.unit.name.casefold(), + ) @dataclass(frozen=True) @@ -119,35 +133,66 @@ class CommanderAlarmSource(DeclaredFamily, affix="AlarmSource"): @classmethod @abstractmethod - def read(cls, comms: Comms, commander: ThreadIncarnation, - orders: tuple[CommanderOrder, ...], now: float) -> tuple[CommanderAlarm, ...]: ... + def read( + cls, + comms: Comms, + commander: ThreadIncarnation, + orders: tuple[CommanderOrder, ...], + now: float, + ) -> tuple[CommanderAlarm, ...]: ... class StalledOrderAlarmSource(CommanderAlarmSource): @classmethod - def read(cls, comms, commander, orders, now): - return tuple(CommanderAlarm( - f"STALLED {order.todo.held().owner} {order.todo.id}: {order.progress.state}") - for order in orders if order.todo.pending - and order.progress.stalled(order.todo.order.at.recorded_at("An order"), now)) + def read( + cls, + comms: Comms, + commander: ThreadIncarnation, + orders: tuple[CommanderOrder, ...], + now: float, + ) -> tuple[CommanderAlarm, ...]: + return tuple( + CommanderAlarm( + f"STALLED {order.todo.held().owner} {order.todo.id}: {order.progress.state}" + ) + for order in orders + if order.todo.pending + and order.progress.stalled(order.todo.order.at.recorded_at("An order"), now) + ) class FailedDeliveryAlarmSource(CommanderAlarmSource): @classmethod - def read(cls, comms, commander, orders, now): + def read( + cls, + comms: Comms, + commander: ThreadIncarnation, + orders: tuple[CommanderOrder, ...], + now: float, + ) -> tuple[CommanderAlarm, ...]: presentation = comms.views.thread_presentations((commander.name,)).require(commander.name) - return tuple(CommanderAlarm( - f"RECORDED FAILED DELIVERY to {notice.recipient}: {notice.label}") + return tuple( + CommanderAlarm(f"RECORDED FAILED DELIVERY to {notice.recipient}: {notice.label}") for notice in presentation.sent_notifications - if isinstance(notice.state, FailedProgress)) + if isinstance(notice.state, FailedProgress) + ) class RefusedSendAlarmSource(CommanderAlarmSource): @classmethod - def read(cls, comms, commander, orders, now): - return tuple(CommanderAlarm( - f"REFUSED SEND #{refusal.id} {refusal.sender} -> {refusal.target}: {refusal.reason}") - for refusal in SendRefusal.recent(comms.root, sender=commander.name)) + def read( + cls, + comms: Comms, + commander: ThreadIncarnation, + orders: tuple[CommanderOrder, ...], + now: float, + ) -> tuple[CommanderAlarm, ...]: + return tuple( + CommanderAlarm( + f"REFUSED SEND #{refusal.id} {refusal.sender} -> {refusal.target}: {refusal.reason}" + ) + for refusal in SendRefusal.recent(comms.root, sender=commander.name) + ) @dataclass(frozen=True) @@ -169,31 +214,37 @@ def read(cls, comms: Comms, request: ObserveCommander) -> CommanderModel: for todo in todos: unit = todo.held().owner_identity source = StatusProgressSource(todo, storage.order_checkout(unit.name, todo.id), pulls) - grouped.setdefault(unit, []).append(CommanderOrder( - todo, source.progress(), todo.overdue(now), source.cadence_commits(), now)) + grouped.setdefault(unit, []).append( + CommanderOrder( + todo, source.progress(), todo.overdue(now), source.cadence_commits(), now + ) + ) cards = [] for unit, orders in grouped.items(): if comms.registry.require(unit.name).incarnation != unit: raise ValueError( - f"Order holder {unit.name} changed incarnation; refresh its assignment.") + f"Order holder {unit.name} changed incarnation; refresh its assignment." + ) current = tuple(orders) detail: CardDetail = CardDetail() if unit in request.expanded: commits = [] for order in current: - source = StatusProgressSource(order.todo, storage.order_checkout( - unit.name, order.todo.id), pulls) - try: - commit = source.latest_commit() - except ValueError as error: - commit = f"unavailable: {error}" + source = StatusProgressSource( + order.todo, storage.order_checkout(unit.name, order.todo.id), pulls + ) + commit = source.latest_commit() commits.append(f"{order.todo.id}: {commit}") - detail = OpenCardDetail(current, comms.transcripts.turns(unit.name, LastTurns(3)), - tuple(commits)) + detail = OpenCardDetail( + current, comms.transcripts.turns(unit.name, LastTurns(3)), tuple(commits) + ) cards.append(UnitCard(unit, current, detail)) - orders = tuple(order for card in cards for order in card.orders) - alarms = tuple(alarm for source in CommanderAlarmSource.members_with(CommanderAlarmSource) - for alarm in source.read(comms, commander, orders, now)) + all_orders = tuple(order for card in cards for order in card.orders) + alarms = tuple( + alarm + for source in CommanderAlarmSource.members_with(CommanderAlarmSource) + for alarm in source.read(comms, commander, all_orders, now) + ) return cls(commander, tuple(sorted(cards, key=lambda card: card.priority)), alarms) @@ -210,6 +261,7 @@ def observation(self) -> CommanderReading: @dataclass(frozen=True) class ReadCommanderIdentity(ReadRequest): """Resolve the external command spelling once, in the existing read process.""" + root: str name: str diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index cfde318ebe..9eef3f3bef 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -4,7 +4,7 @@ import pytest from agent_comms.recorded_time import RecordedEventTime -from agent_comms.todos import CommitMomentum, ProgressPoint, ProgressSource, PullRequests, StatusProgressSource +from agent_comms.todos import CommitMomentum, ForecastCommitMomentum, UnknownCommitMomentum, ProgressPoint, ProgressSource, PullRequests, StatusProgressSource from agent_comms.ui_model.commander import ( CommanderModel, ObserveCommander, @@ -61,6 +61,7 @@ def test_card_derives_commit_cadence_against_deadline_without_storing_momentum(c card = UnitCard(comms.registry.require('worker').incarnation, (order,), CardDetail()) momentum, = card.momentum assert 'momentum' not in card.__dataclass_fields__ + assert isinstance(momentum, ForecastCommitMomentum) assert momentum.interval == 60 assert momentum.next_at == 220 assert momentum.deadline_margin == 80 @@ -76,8 +77,8 @@ def test_card_derives_commit_cadence_against_deadline_without_storing_momentum(c (ProgressPoint('one', 160), ProgressPoint('two', 100))]) def test_unknown_commit_cadence_does_not_invent_forecast(commander, points): todo = create(commander, 'P1') - momentum = CommitMomentum(todo, points, 170) - assert momentum.next_at == 0 + momentum = CommitMomentum.from_commits(todo, points, 170) + assert isinstance(momentum, UnknownCommitMomentum) assert 'cadence unknown' in momentum.line assert 'next commit ~' not in momentum.line diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index 3dc002e7ef..51f9f00f68 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -4,6 +4,7 @@ No provider is started, no synthetic transcript is written, and the original sessions/registry are only read. Source and capture paths are recorded explicitly. """ + from __future__ import annotations import argparse @@ -55,32 +56,54 @@ def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> N receipts.append(f"{name}: {source} -> {copied} ({copied.stat().st_size} bytes)") comms.registry.declare( replace(original, process_identity=NoProcess(), session_file=session, goal=NoGoal()), - StoppedThreadStatus()) - comms.goals.update_goal(COMMANDER, SetGoalAction( - text="Verify the commander page on copied real unit sessions", goal_id="page-request", - order=RequestGoalOrder())) + StoppedThreadStatus(), + ) + comms.goals.update_goal( + COMMANDER, + SetGoalAction( + text="Verify the commander page on copied real unit sessions", + goal_id="page-request", + order=RequestGoalOrder(), + ), + ) now = time.time() for index, name in enumerate(units): - comms.goals.update_goal(name, SetGoalAction( - text="Copied unit for read-only page verification")) + comms.goals.update_goal( + name, SetGoalAction(text="Copied unit for read-only page verification") + ) order = PendingProblemOrder( - id=f"commander-page-{name}", owner=FileLine(__file__, 1), + id=f"commander-page-{name}", + owner=FileLine(__file__, 1), check=OrderCheck(str(output), "printf 1", CountCheckResult(1)), edits=("Render the canonical order owner without copying its state",), banned=("Do not launch a provider or change the original saved session",), report="Report the actual visible commander page and its expanded unit card", - at=RecordedEventTime(ordered_at if ordered_at is not None and not index - else now - 1200 if not index else now), - first_call_deadline=RecordedEventTime(now + 120), run=MechanicalOrderRun()) - CommsTodoCreateTool(owner=name, order=CreateProblemGoalAction( - problem=f"P{index + 1}", order=order, request="page-request", - deadline=RecordedEventTime(now - 60 if not index else now + 1800), commander=COMMANDER, - repo="OpenHCSDev/agent-comms", text="Show the real order in the commander page", - )).apply(comms, ThreadCaller(COMMANDER)) + at=RecordedEventTime( + ordered_at + if ordered_at is not None and not index + else now - 1200 if not index else now + ), + first_call_deadline=RecordedEventTime(now + 120), + run=MechanicalOrderRun(), + ) + CommsTodoCreateTool( + owner=name, + order=CreateProblemGoalAction( + problem=f"P{index + 1}", + order=order, + request="page-request", + deadline=RecordedEventTime(now - 60 if not index else now + 1800), + commander=COMMANDER, + repo="OpenHCSDev/agent-comms", + text="Show the real order in the commander page", + ), + ).apply(comms, ThreadCaller(COMMANDER)) try: - CommsSendTool(sender=COMMANDER, target="unregistered-page-target", - body="Verify that the commander can read an actual refused send.").apply( - comms, ThreadCaller(COMMANDER)) + CommsSendTool( + sender=COMMANDER, + target="unregistered-page-target", + body="Verify that the commander can read an actual refused send.", + ).apply(comms, ThreadCaller(COMMANDER)) except ToolRefusedError as error: receipts.append(f"Actual refused send: {error}") else: @@ -106,11 +129,13 @@ async def journey(app, pilot, output: Path, units: tuple[str, ...], momentum: bo await pilot.pause(0.2) screen = app.screen await pilot.pause(1) + def capture(stage: str) -> None: # The text is from the actual laid-out terminal strips, not the model. text = "\n".join(strip.text for strip in screen._compositor.render_strips()) (output / f"{stage}.txt").write_text(text + "\n") subprocess.run(("import", "-window", "root", str(output / f"{stage}.png")), check=True) + capture("level-0") card = next(row for row in screen.query(CommanderUnit) if row.unit.name == units[0]) if momentum: @@ -124,12 +149,17 @@ def capture(stage: str) -> None: assert units[0] in str(card.title) assert screen.rows[next(unit for unit in screen.rows if unit.name == units[1])].collapsed capture("level-1") + def deliver(): comms = Comms(output / "bus") todo = comms.goals.todos.get("P1") - CommsOrderDoneTool(problem=todo.id, order_id=todo.order.id, - delivery=OperationDelivery(), claims=(WorkingClaim(str(output / "level-0.png")),) + CommsOrderDoneTool( + problem=todo.id, + order_id=todo.order.id, + delivery=OperationDelivery(), + claims=(WorkingClaim(str(output / "level-0.png")),), ).apply(comms, ThreadCaller(COMMANDER)) + await app.preparation.run_thread(deliver) async with asyncio.timeout(90): while "Delivered, waiting on install" not in str(card.title): @@ -143,7 +173,8 @@ def deliver(): assert app.selected_session.query_one(Conversation) is conversation (output / "result.txt").write_text( "PASS: normal Toad, real scratch orders, level 0, expanded real turns, " - "and live delivery update without reopening.\n") + "and live delivery update without reopening.\n" + ) app.exit() @@ -161,8 +192,11 @@ def main() -> None: return os.environ["AGENT_COMMS_ROOT"] = str(args.output / "bus") from toad.app import ToadApp + app = ToadApp(project_dir=str(args.output)) - app.run(auto_pilot=lambda pilot: journey(app, pilot, args.output, tuple(args.units), args.momentum)) + app.run( + auto_pilot=lambda pilot: journey(app, pilot, args.output, tuple(args.units), args.momentum) + ) if app._exception: raise app._exception From 68d7c93db041e00494eaa4e4ea8ee8e8700520d6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:47:34 -0400 Subject: [PATCH 16/20] Allow a focused single-unit physical commander journey --- tools/commander_page_journey.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index 51f9f00f68..1972929918 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -147,7 +147,8 @@ def capture(stage: str) -> None: await pilot.pause(0.2) await pilot.pause(1) assert units[0] in str(card.title) - assert screen.rows[next(unit for unit in screen.rows if unit.name == units[1])].collapsed + if len(units) > 1: + assert screen.rows[next(unit for unit in screen.rows if unit.name == units[1])].collapsed capture("level-1") def deliver(): @@ -182,7 +183,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--prepare", action="store_true") - parser.add_argument("--units", nargs=2, default=UNITS) + parser.add_argument("--units", nargs="+", default=UNITS) parser.add_argument("--ordered-at", type=float) parser.add_argument("--momentum", action="store_true") args = parser.parse_args() From 43dfbd2d249060a3f8a1a68e2653e7a8f78eae17 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 17:10:54 -0400 Subject: [PATCH 17/20] Represent actual GitHub process failures without swallowing Git and source errors --- src/agent_comms/todos.py | 12 +++++++---- tests/test_order_progress.py | 2 +- tools/commander_page_journey.py | 38 ++++++++++++++++----------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 3325d09c93..84227054c2 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -244,8 +244,7 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, "--jq", '.data | map_values(.nodes | map([.number, .headRefName, .headRefOid,' ' .mergeCommit.oid // "", .mergedAt // "", .updatedAt, .body]))'), capture_output=True, text=True, timeout=60) - if listed.returncode: - raise ValueError(listed.stderr.strip() or f"GitHub order lookup in {repo} failed.") + listed.check_returncode() rows = FieldCodec.decode(dict[str, tuple[tuple[int, str, str, str, str, str, str], ...]], FieldCodec.parse(listed.stdout)) for alias, candidate in aliases.items(): @@ -494,8 +493,13 @@ def progress(self) -> OrderProgress: """One selection algorithm for measured tools and read-only status views.""" members = sorted(OrderProgress.members_with(FoundProgress), key=lambda member: member.precedence) - return next((progress for member in members for progress in member.found(self)), - NoCommitProgress()) + try: + return next((progress for member in members for progress in member.found(self)), + NoCommitProgress()) + except subprocess.CalledProcessError as unread: + return UnreadProgress(unread.stderr.strip() or str(unread)) + except subprocess.TimeoutExpired as unread: + return UnreadProgress(str(unread)) def requests(self, state: str) -> list[PullRequest]: """Explicit PR references survive checkout removal or branch reuse.""" diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 7d8041a799..ebe0af8a95 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -135,7 +135,7 @@ def test_a_merged_order_still_awaits_its_record(checkout): def test_unread_github_is_explicit(checkout): pulls = Listed() - pulls.states = ValueError('gh: not logged in') + pulls.states = subprocess.CalledProcessError(1, 'gh api graphql', stderr='gh: not logged in') progress = read(checkout, pulls) assert progress == UnreadProgress('gh: not logged in') diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index 1972929918..b7fa4c4859 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -15,6 +15,12 @@ from dataclasses import replace from pathlib import Path +from textual.pilot import Pilot +from toad.app import ToadApp +from toad.navigation_target import FeedTarget +from toad.screens.commander import CommanderScreen, CommanderUnit +from toad.widgets.conversation import Conversation + from agent_comms.active_route import LocalRoute from agent_comms.change_report import FileLine from agent_comms.child_process import NoProcess @@ -33,14 +39,14 @@ UNITS = ("pi-cn-phase2", "pi-cn-phase5") -def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> None: +def prepare(output: Path, units: tuple[str, ...], ordered_at: float) -> None: live = Comms(LocalRoute.default()) root = output / "bus" if root.exists(): raise ValueError("Scratch bus already exists; preserve it and choose another output path.") root.mkdir(parents=True) comms = Comms(root) - receipts = [] + source_notes = [] for name in (COMMANDER, *units): original = live.registry.require(name) session = original.session_file @@ -53,7 +59,7 @@ def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> N copied.write_bytes(captured.rsplit(b"\n", 1)[0] + b"\n") copied.chmod(0o600) session = str(copied) - receipts.append(f"{name}: {source} -> {copied} ({copied.stat().st_size} bytes)") + source_notes.append(f"{name}: {source} -> {copied} ({copied.stat().st_size} bytes)") comms.registry.declare( replace(original, process_identity=NoProcess(), session_file=session, goal=NoGoal()), StoppedThreadStatus(), @@ -78,11 +84,7 @@ def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> N edits=("Render the canonical order owner without copying its state",), banned=("Do not launch a provider or change the original saved session",), report="Report the actual visible commander page and its expanded unit card", - at=RecordedEventTime( - ordered_at - if ordered_at is not None and not index - else now - 1200 if not index else now - ), + at=RecordedEventTime(ordered_at if not index else now), first_call_deadline=RecordedEventTime(now + 120), run=MechanicalOrderRun(), ) @@ -105,17 +107,14 @@ def prepare(output: Path, units: tuple[str, ...], ordered_at: float | None) -> N body="Verify that the commander can read an actual refused send.", ).apply(comms, ThreadCaller(COMMANDER)) except ToolRefusedError as error: - receipts.append(f"Actual refused send: {error}") + source_notes.append(f"Actual refused send: {error}") else: raise AssertionError("The deliberately absent target unexpectedly accepted a send.") - (output / "sources.txt").write_text("\n".join(receipts) + "\n") + (output / "sources.txt").write_text("\n".join(source_notes) + "\n") -async def journey(app, pilot, output: Path, units: tuple[str, ...], momentum: bool) -> None: - from toad.navigation_target import FeedTarget - from toad.screens.commander import CommanderScreen, CommanderUnit - from toad.widgets.conversation import Conversation - +async def journey(app: ToadApp, pilot: Pilot, output: Path, + units: tuple[str, ...], momentum: bool) -> None: await pilot.pause(2) # Open a real bus conversation and submit through its actual local-command boundary. await app.selected_session.open_sidebar_target(FeedTarget()) @@ -151,9 +150,10 @@ def capture(stage: str) -> None: assert screen.rows[next(unit for unit in screen.rows if unit.name == units[1])].collapsed capture("level-1") - def deliver(): + def deliver() -> None: comms = Comms(output / "bus") todo = comms.goals.todos.get("P1") + assert isinstance(todo.order, PendingProblemOrder) CommsOrderDoneTool( problem=todo.id, order_id=todo.order.id, @@ -184,16 +184,14 @@ def main() -> None: parser.add_argument("--output", type=Path, required=True) parser.add_argument("--prepare", action="store_true") parser.add_argument("--units", nargs="+", default=UNITS) - parser.add_argument("--ordered-at", type=float) + parser.add_argument("--ordered-at", type=float, default=time.time() - 1200) parser.add_argument("--momentum", action="store_true") args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) if args.prepare: prepare(args.output, tuple(args.units), args.ordered_at) return - os.environ["AGENT_COMMS_ROOT"] = str(args.output / "bus") - from toad.app import ToadApp - + os.environ.update(AGENT_COMMS_ROOT=str(args.output / "bus")) app = ToadApp(project_dir=str(args.output)) app.run( auto_pilot=lambda pilot: journey(app, pilot, args.output, tuple(args.units), args.momentum) From d07ee61b85e91a262ced9cae2c5fcd0b61f17260 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 17:14:25 -0400 Subject: [PATCH 18/20] Own recorded order-time normalization in checked orders and migrate numeric consumers --- src/agent_comms/commander/order.py | 4 ++-- src/agent_comms/external_transcript.py | 4 ++-- src/agent_comms/todos.py | 16 ++++++++++++---- src/agent_comms/tools.py | 4 ++-- src/agent_comms/turn_digests.py | 2 +- src/agent_comms/ui_model/commander.py | 3 +-- tools/commander_page_journey.py | 5 +++-- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index de6a972d2e..da4b514fdb 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -519,7 +519,7 @@ def first_calls(held, activity): unit, order = todo.assignment.owner, todo.order if not todo.pending: continue - acted = activity[unit].since(order.at.recorded_at("An order")) + acted = activity[unit].since(order.ordered_at) if not acted and time.time() < order.first_call_deadline.at: continue event = ObservedFirstCallEvent if acted else NotifiedFirstCallEvent @@ -590,7 +590,7 @@ def deadline_lines(pending: list[Todo], read: dict[str, OrderProgress], since: f now = time.time() late, stalled = [], [] for t in pending: - ordered, work = t.order.at.recorded_at('An order'), read[t.id] + ordered, work = t.order.ordered_at, read[t.id] if work.stalled_between(ordered, since, now): stalled.append(f'STALL {t.id} {t.holder}: {work.line} -> push it now or reassign') if not t.overdue(now): diff --git a/src/agent_comms/external_transcript.py b/src/agent_comms/external_transcript.py index d2eb3a25b0..f86b0b3af8 100644 --- a/src/agent_comms/external_transcript.py +++ b/src/agent_comms/external_transcript.py @@ -211,7 +211,7 @@ def unjudged_query(cls, thread, todo): tools = tuple(tool.declared_name for tool in ToolDeclaration.members_with(MutatingPathTool)) judged = tuple(verdict.tool_call_id for verdict in todo.verdicts) placeholders = lambda values: ",".join("?" for _ in values) - since = todo.order.at.recorded_at("An order") + since = todo.order.ordered_at # SQL NULL is the original undated fact, decoded through FieldCodec # as UnrecordedEventTime when a row is read. No time is substituted. table = cls.declared_name @@ -382,7 +382,7 @@ def unjudged(self, todo): for encoded, in rows: event = FieldCodec.decode(TranscriptEvent, FieldCodec.parse(encoded)) yield from event.edit_calls(todo.held().owner, todo.id, - todo.order.at.recorded_at("An order")) + todo.order.ordered_at) def unjudged_count(self, todo) -> int: where, parameters = ExternalEventRow.unjudged_query(self.transcript.lookup, todo) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 09b8ab993d..ece47c990d 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -508,7 +508,7 @@ def of(cls, todo: Todo, storage: StorageRule, pulls: PullRequests) -> Self: @property def since(self) -> float: - return self.todo.order.at.recorded_at("An order") + return self.todo.order.ordered_at def progress(self) -> OrderProgress: """One selection algorithm for measured tools and read-only status views.""" @@ -1241,6 +1241,10 @@ def pull_request_ids(cls, body: str) -> tuple[str, ...]: return tuple(line.removeprefix(cls.pull_request_label).strip() for line in body.splitlines() if line.startswith(cls.pull_request_label)) + @property + def ordered_at(self) -> float: + raise ValueError("This problem has no checked order time.") + def instructions(self, todo: Todo) -> str: return "\n".join((f"Problem: {todo.id}: {todo.text}", *self.finding.instructions(), self.line)) @@ -1318,6 +1322,10 @@ class CheckedProblemOrder(ProblemOrder, declared_name=None): default_factory=NoOrderDeletion, metadata={"wire_omit_default": True}) serves: type[OrderPhase] = field(default=UnplannedPhase, metadata={"wire_omit_default": True}) + @property + def ordered_at(self) -> float: + return self.at.recorded_at("An order") + def __post_init__(self): if not self.edits or not self.banned: raise ValueError("An order requires edits and banned actions.") @@ -1427,7 +1435,7 @@ def waiting_on_install(self, todo: Todo, read: dict[str, OrderProgress]) -> tupl class PendingProblemOrder(CheckedProblemOrder): def reissued(self, at: float) -> PendingProblemOrder: window = (self.first_call_deadline.recorded_at("A first-call deadline") - - self.at.recorded_at("An order")) + - self.ordered_at) return replace(self, id=uuid4().hex[:12], at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + window), first_call=AwaitingFirstCall()) @@ -2177,7 +2185,7 @@ def live_strikes(self, unit: str | None = None) -> tuple[StrikeTodo, ...]: def struck_on_order(self, unit: str) -> bool: """A live strike on ``unit`` recorded since the current order was sent; a send-back that replaces this order is that same miss, so it counts no second strike.""" - sent = self.order.at.recorded_at("An order") + sent = self.order.ordered_at return any(recorded.at.recorded_at("A strike") >= sent for recorded in self.live_strikes(unit)) @@ -2343,7 +2351,7 @@ def resolve_order(self, commander: ThreadIncarnation, unit: str, message: str) - def drop(self, commander: ThreadIncarnation, unit: str, message: str, by: str) -> Todo: old = self.resolve_order(commander, unit, message) newer = self.resolve_order(commander, unit, by) - if not old.pending or not newer.pending or newer.order.at.at <= old.order.at.at: + if not old.pending or not newer.pending or newer.order.ordered_at <= old.order.ordered_at: raise ValueError('Only a live newer order to the same unit replaces an old one.') with self.writing() as db: current = self._current(db, old.id) diff --git a/src/agent_comms/tools.py b/src/agent_comms/tools.py index f07fe9a1eb..1c8526c646 100644 --- a/src/agent_comms/tools.py +++ b/src/agent_comms/tools.py @@ -755,7 +755,7 @@ def readers(cls, comms: Comms, commander: str, read_type=OrderEditsRead): raise ToolRefusedError( f"Order {todo.id} holder incarnation no longer exists" ) - yield read_type(comms.root, holder, TurnsSince(todo.order.at.recorded_at("An order")), todo) + yield read_type(comms.root, holder, TurnsSince(todo.order.ordered_at), todo) @classmethod def count(cls, comms: Comms, commander: str) -> int: @@ -955,7 +955,7 @@ class CommsOrderWorkTool(ReadTool, ToolRequest): def apply(self, comms: Comms, caller: ToolCaller) -> OrderWork: todo = comms.goals.todos.get(self.problem) - since = todo.order.at.recorded_at("An order") + since = todo.order.ordered_at turns = comms.transcripts.turns(todo.held().owner, TurnsSince(since)) return OrderWork.read(self.problem, turns.thread, since, turns.turns) diff --git a/src/agent_comms/turn_digests.py b/src/agent_comms/turn_digests.py index 69ba2f14f7..ded5afa013 100644 --- a/src/agent_comms/turn_digests.py +++ b/src/agent_comms/turn_digests.py @@ -119,7 +119,7 @@ class OrderRead[Result](TranscriptRead[Result]): def edits_from_turns(self, turns: tuple[TurnDigest, ...]) -> tuple[EditCall, ...]: judged = {verdict.tool_call_id for verdict in self.todo.verdicts} - since = self.todo.order.at.recorded_at("An order") + since = self.todo.order.ordered_at edits = {edit.id: edit for turn in reversed(turns) for event in turn.events for edit in event.edit_calls(self.thread.name, self.todo.id, since) diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 2c569b40d8..2e7349cd36 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -156,8 +156,7 @@ def read( f"STALLED {order.todo.held().owner} {order.todo.id}: {order.progress.state}" ) for order in orders - if order.todo.pending - and order.progress.stalled(order.todo.order.at.recorded_at("An order"), now) + if order.todo.pending and order.progress.stalled(order.todo.order.ordered_at, now) ) diff --git a/tools/commander_page_journey.py b/tools/commander_page_journey.py index b7fa4c4859..268c48af83 100644 --- a/tools/commander_page_journey.py +++ b/tools/commander_page_journey.py @@ -113,8 +113,9 @@ def prepare(output: Path, units: tuple[str, ...], ordered_at: float) -> None: (output / "sources.txt").write_text("\n".join(source_notes) + "\n") -async def journey(app: ToadApp, pilot: Pilot, output: Path, - units: tuple[str, ...], momentum: bool) -> None: +async def journey( + app: ToadApp, pilot: Pilot, output: Path, units: tuple[str, ...], momentum: bool +) -> None: await pilot.pause(2) # Open a real bus conversation and submit through its actual local-command boundary. await app.selected_session.open_sidebar_target(FeedTarget()) From ca9c4f881cd3a4b1d15a3f36e143b4eb0253f3d1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:56:27 -0400 Subject: [PATCH 19/20] Let delivery-progress variants select their failed notifications --- src/agent_comms/notification_progress.py | 10 ++++++++++ src/agent_comms/ui_model/commander.py | 5 ++--- tests/test_commander_model.py | 12 ++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/agent_comms/notification_progress.py b/src/agent_comms/notification_progress.py index 52f7d9a9e4..36087c4922 100644 --- a/src/agent_comms/notification_progress.py +++ b/src/agent_comms/notification_progress.py @@ -22,6 +22,11 @@ class DeliveryProgress(ThreadPresentationStatus): def notification_label(self, notification: MessageNotification) -> str: return self.label + def failed_notifications( + self, notification: MessageNotification + ) -> tuple[MessageNotification, ...]: + return () + class PendingDelivery: delivered: ClassVar[bool] = False @@ -87,6 +92,11 @@ class InterruptedProgress(ContextualProgress): class FailedProgress(ContextualProgress): label = "Failed" + def failed_notifications( + self, notification: MessageNotification + ) -> tuple[MessageNotification, ...]: + return (notification,) + class EngagedProgress(ContextualProgress, WorkingThreadPresentationStatus): priority = 1 diff --git a/src/agent_comms/ui_model/commander.py b/src/agent_comms/ui_model/commander.py index 2e7349cd36..0a3396046b 100644 --- a/src/agent_comms/ui_model/commander.py +++ b/src/agent_comms/ui_model/commander.py @@ -14,7 +14,6 @@ from agent_comms.coordination_tables.refusals import SendRefusal from agent_comms.declared_family import DeclaredFamily -from agent_comms.notification_progress import FailedProgress from agent_comms.presentation import NoRevision, ReadState, WireRevision from agent_comms.revision_views import CommanderView from agent_comms.thread_identity import ThreadIncarnation @@ -171,9 +170,9 @@ def read( ) -> tuple[CommanderAlarm, ...]: presentation = comms.views.thread_presentations((commander.name,)).require(commander.name) return tuple( - CommanderAlarm(f"RECORDED FAILED DELIVERY to {notice.recipient}: {notice.label}") + CommanderAlarm(f"RECORDED FAILED DELIVERY to {failed.recipient}: {failed.label}") for notice in presentation.sent_notifications - if isinstance(notice.state, FailedProgress) + for failed in notice.state.failed_notifications(notice) ) diff --git a/tests/test_commander_model.py b/tests/test_commander_model.py index 04465bbb78..eed930994b 100644 --- a/tests/test_commander_model.py +++ b/tests/test_commander_model.py @@ -3,6 +3,9 @@ import pytest +from agent_comms.audience_manifest import FrozenRecipient +from agent_comms.notification_progress import FailedProgress, SentProgress +from agent_comms.presentation import MessageNotification from agent_comms.recorded_time import RecordedEventTime from agent_comms.todos import CommitMomentum, ForecastCommitMomentum, UnknownCommitMomentum, ProgressPoint, ProgressSource, PullRequests, StatusProgressSource from agent_comms.ui_model.commander import ( @@ -83,6 +86,15 @@ def test_unknown_commit_cadence_does_not_invent_forecast(commander, points): assert 'next commit ~' not in momentum.line +def test_delivery_progress_selects_its_original_failed_notification(): + sent = MessageNotification(FrozenRecipient('worker', 'worker'), 'lead', SentProgress(1)) + assert sent.state.failed_notifications(sent) == () + failed = replace(sent, state=FailedProgress(sent.state)) + selected, = failed.state.failed_notifications(failed) + assert selected is failed + assert selected.label == 'Failed' + + def test_commander_identity_is_resolved_or_refused_in_the_read_process(commander): comms, caller = commander answer = ReadCommanderIdentity(str(comms.root), caller.name).answer(comms) From 5bc9d1500eeb889946b59ce4949c9b6c5d661b71 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:59:26 -0400 Subject: [PATCH 20/20] Record cadence workflow acceptance and current review limits --- docs/agents/commander-page.md | 190 +++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 83 deletions(-) diff --git a/docs/agents/commander-page.md b/docs/agents/commander-page.md index b1a729a3ae..b12ac04114 100644 --- a/docs/agents/commander-page.md +++ b/docs/agents/commander-page.md @@ -1,86 +1,110 @@ -# Commander page: delivered first slice - -Issue #1495. Drafts: Core #1937 and Toad #1080. Owner: pi-cn-phase6. - -## Delivered, not installed - -`/commander ` opens the board in normal Toad. One collapsed line per held unit shows its problem number, plain problem text, deadline, recorded delivery state and native git/PR status. Overdue units come first. Click/Enter expands only that unit: full canonical order instructions, the actual latest checkout commit and up to three original recent turns, one line each. - -Alarms show native stalled orders, recorded failed deliveries and retained refused sends. Alarm sources declare their reads; Toad has no alarm-kind switch. Refusals come from pi-connector's `SendRefusal.recent`, not from a UI log. **Core #1937 locally integrates dependency #1922; land that dependency first.** No GitHub PR was merged. Nothing was installed, relinked or restarted in the normal launcher. - -This is the requested first slice, not full phase 6: strikes, review queue, story, sidebar order decorations, turn/file drill-down and predictions remain outside this delivery. - -## Owners, consumers and deletions - -- `TodoStore.held_for_commander` selects indexed held rows; `Todo.held().owner_identity` supplies nominal unit identity; `Todo.overdue` supplies overdue meaning. `CommanderModel` retains those types. It does not inspect kind/name strings to recover semantics. -- `ProblemOrder.instructions` supplies the complete order body. No second order renderer was written in Toad. -- `ProgressSource.progress` in `todos.py:333` now owns the sole progress selection algorithm, moved out of `OrderProgress.read`. The latter delegates to it. `StatusProgressSource` inherits that selection and suppresses acceptance-distance measurement: reading the page cannot execute an order's arbitrary acceptance check. Existing measured native-tool behavior remains on `ProgressSource`. -- `ProgressSource.latest_commit` reads one actual commit only for expanded cards. `OrderProgress.stalled` supplies stall policy. GitHub/read failures are `UnreadProgress`, not a false no-commit state. -- `TranscriptEvent.turn_line` and `TurnDigest.line` own recent-work summaries. Assistant and tool-start declarations contribute; prompts do not swamp the card with wire envelopes. The model's earlier generic imported-body rendering was deleted. -- `ReadCommanderIdentity` resolves the command spelling to `ThreadIncarnation` inside the existing observation process. Toad performs no registry or Todo read. Intentional typed read refusals return through the existing `ReadRefused` boundary instead of killing the observer. -- `ObserveCommander` and `CommanderReading` use the existing interest/process lifetime. `WireRevision` watches Todo main/WAL files plus relevant existing sources and the existing expiry tick. Closing the page removes its interest; there is no new polling timer or store. -- Toad's `CommanderCommand` is discovered from its declaration, with no command roster or MainScreen edit. `CommanderUnit` extends `Collapsible`; its existing title and explicit detail widgets are updated in place. A real run caught an incorrect broad `Static` lookup that updated the title; that lookup was deleted. - -One selection declaration, confirmed with: - -``` -rg -n 'members = sorted\(OrderProgress.members_with' src/agent_comms -# todos.py:335 only -``` - -## Overlap and source evidence - -All open Core and Toad PR file lists were read before writing. #1903/#1873 concern launch/runtime classes not changed here. P603 #1757 was already merged. #1912 and #1756 diffs were read; their root-context and pattern-ID classes are not changed here. Same-file changes in `todos.py` are separate functions/classes. Recent Toad #1070 changes MainScreen and existing command classes; this slice adds a new command and screen instead. #1917 changes imported-message/UserTranscript behavior, not the event/turn-summary methods added here. `plan_phases.py` was not edited. - -Read the refactor-audit skill and pattern catalog. Reused its `Package` AST parser and overlay for both complete production roots, before/after source revisions, with zero parse omissions. Owners, imports, inheritance, direct consumers and function checks are recorded in `/home/ts/.cache/agent-scratch/pi-cn-phase6/owner-consumers.txt`. Dynamic family discovery, MRO dispatch and aliases were read semantically; the AST listing does not claim complete dynamic resolution. NRA is not installed on this machine. - -Relevant patterns: BOUND-2/BOUND-8 (retain Core's domain records rather than mapped shapes), MEMB-1 (derive command/alarm membership from declarations), IMPL-12 (one progress selector, inherited source capability). The final overlay adds no string/type dispatch switches or raw-record shapes. Its unused-module heuristic cannot see the pickled observation interest crossing the paired repository boundary. - -## Actual application acceptance - -Verified implementation pair: **Core e248a6ace / Toad 292bdaa5**, using the existing installed stack Python/dependencies with the two source checkouts on `PYTHONPATH`. This is a source-paired real Toad run, not a claim that the normal installed launcher contains the feature. - -Ran `tools/commander_page_journey.py` in **plain st on isolated Xvfb :186**, a real `ToadApp`, real observation child and scratch bus. It opened a real bus conversation and submitted `/commander claude-code` through the normal prompt/local-command boundary. It clicked a real Collapsible title, recorded delivery through the real order-completion tool, observed the page update without reopening, and returned to the identical underlying conversation. - -The scratch bus contains copies of actual registered units and complete-line cuts of their real saved session files, not fixture transcripts or hand-built events. Orders were created through `CommsTodoCreateTool`. The refusal was one actual `CommsSendTool` attempt to an absent target, retained by the send owner. No provider/agent was started and no original session, registry or uncertain input was modified. Snapshot provenance is in `physical-4/sources.txt`. - -Artifacts under `/home/ts/.cache/agent-scratch/pi-cn-phase6/physical-4/`: - -- `level-0.png`, `level-0.txt`: physically displayed collapsed board and alarms. -- `level-1.png`, `level-1.txt`: actual expanded card, commit and original turns; second unit still collapsed. -- `live-delivery.png`, `live-delivery.txt`: same open card changes to Delivered, OVERDUE disappears and its stall alarm clears; the refusal remains. -- `result.txt`: successful continuous journey receipt. - -Text below is excerpted from the actual composed terminal strips; the files contain the complete screen. - -### Level 0 - -``` -Commander claude-code | 2 units | Overdue first · Enter/click a unit to expand · Esc to return -▶ pi-cn-phase2 | OVERDUE P1: Show the real order in the commander page | due 14:23:01 | Open; no commit since the order -▶ pi-cn-phase5 | P2: Show the real order in the commander page | due 14:54:01 | Open; no commit since the order -Alarms -STALLED pi-cn-phase2 P1: no commit since the order +# Commander board and derived commit cadence + +Issue #1495. Core #1937, paired Toad #1080. Owner: pi-cn-phase6. +These are source-paired changes, not an installation claim. + +## Working slice + +`/commander ` opens the ordinary Toad page. It shows held +orders, deadlines and delivery state, overdue first. Expanding one unit reads +its actual checkout commit and three original recent turns. Recorded failures, +stalled orders and retained refused sends appear in the alarms pane. + +The collapsed card also shows commit cadence, the forecast next-commit time, +its margin against the order deadline, and whether that forecast has already +been missed. Missing checkouts and insufficient/non-increasing samples remain +explicitly unknown. Delivery removes the forecast. + +**Interpretation:** commit cadence predicts the next commit, not completion. +There is no known amount of remaining work from which to calculate a completion +ETA. The page explicitly says `completion unknown`; no prediction is persisted. +Full row-27 prediction/outcome comparison remains unverified. pi-plan-claims +owns that row's declaration in #1992; no second parity registry was added here. +Strikes, review queue, story and sidebar decorations remain outside this slice. + +## Owners and deletions + +- `TodoStore.held_for_commander`, nominal holder identities and `Todo.overdue` + supply the canonical order selection and deadline meaning. +- `ProblemOrder.instructions` supplies order text. `CheckedProblemOrder.ordered_at` + owns recorded-time normalization; all numeric order-time consumers use it. +- `ProgressSource.progress` supplies one progress-selection algorithm. + `StatusProgressSource` inherits it and disables acceptance-check execution. +- Main's `ProgressCheckout` family owns available versus removed checkouts. + Its concrete classes supply commit details and bounded cadence evidence. + Both distance and cadence reads share `PresentCheckout.branch_commits` and + the `ProgressPoint` decoder. #1935's competing source changes were reverted. +- `UnitCard.momentum` derives `CommitMomentum` values from canonical orders and + observed commits. Unknown and forecast states are distinct types, not zero + timestamps standing in for missing observations. No new store or timer exists. +- `DeliveryProgress.failed_notifications` owns failure selection. `FailedProgress` + returns the original notification. The alarm reader's `isinstance` filter was + deleted; Toad has no failure-kind switch or notification copy. +- Actual GitHub command failures and timeouts become explicit unread progress. + Other Git, decode and source errors propagate; there is no ValueError catch-all. +- Existing observation interests, store/WAL revisions and expiry notifications + drive updates. Closing the page retires its interest. +- Toad renders the Core model through its existing Collapsible widgets. GoalBar + presence is derived from actual composition, not an optional cached child; + opening/clicking a bus conversation no longer raises `goal_bar` AttributeError. + +Used the existing refactor-audit Package parser across both production roots. +The declaration/consumer evidence reports zero parse omissions. Dynamic family +and MRO resolution was read semantically, not claimed as an AST behavioral proof. +Evidence is under `/home/ts/.cache/agent-scratch/pi-cn-phase6/`. + +## Physical application proof + +`physical-failure-owner/` contains the latest plain-st/Xvfb run, using normal +Toad, its real observation process, copied registered saved sessions, real +scratch-bus orders created through Core tools, and an actual retained refused +send. No provider was started, and no original session or uncertain input was +changed. `sources.txt` records provenance. + +The journey opened a real bus conversation, submitted `/commander claude-code`, +clicked one card, observed a tool-recorded delivery update without reopening, +and returned to the same conversation. Captures were inspected; no traceback +pane or unavailable-page error is present. + +- `level-0.txt/png`: collapsed cards, known and unknown cadence, alarms. +- `level-1.txt/png`: one expanded card and original turns; second card collapsed. +- `live-delivery.txt/png`: delivery updates the same open card and clears its stall. +- `result.txt`: successful continuous-journey result. + +Actual collapsed screen excerpt: + +```text +Commander claude-code | 2 units | Overdue first +P1: 3.0m/commit; next commit ~17:17 (132.1m before due); forecast missed by 133.2m; due 1.2m ago; completion unknown +P2: cadence unknown (need two increasing commit times); due in 29.8m; completion unknown +STALLED pi-cn-phase6 P1: pushed d07ee61b8 at 17:14, no PR REFUSED SEND #1 claude-code -> unregistered-page-target: Target 'unregistered-page-target' is not a registered thread. ``` -### Level 1, selected screen lines - -``` -▼ pi-cn-phase2 | OVERDUE P1: Show the real order in the commander page | due 14:23:01 | Open; no commit since the order - Problem: P1: Show the real order in the commander page - Order: commander-page-pi-cn-phase2 - 1. Render the canonical order owner without copying its state - Latest checkout commits: - P1: 69688af8d Merge remote-tracking branch 'origin/main' into feat/cn-phase2-design - Recent turns (newest first): - eb8bb409-f882-4e9b-80f2-efd20829b639: Run gh pr edit 1931 -R OpenHCSDev/agent-comms ... - 35561dbb-3227-4076-bda3-078c45dfdf7b: Run gh pr diff 1587 -R OpenHCSDev/agent-comms; ... -▶ pi-cn-phase5 | P2: Show the real order in the commander page | due 14:54:01 | Open; no commit since the order -``` - -## Checks and retained output - -Focused batch covers commander model, read-only progress, refusals and native/external turn readers. Three pre-existing turn-reader fixtures lacked required positions/timestamps; updated those fixtures without weakening their assertions. The final batch passed **40 checks in 31.90 seconds**, including the identity/refusal boundary check. Commands run in the background under `timeout 300 ... -X faulthandler`, with pytest default parallelism/whole-suite coverage disabled for the small selected batch. The environment lacks pytest-timeout, producing one configuration warning; the outer timeout remains enforced. New-file Ruff and both `git diff --check` checks pass. CI/full-suite acceptance was not requested or claimed. - -Owned disposable output: `/home/ts/.cache/agent-scratch/pi-cn-phase6`. Keep `physical-4` screen/source receipts while the drafts await review. Superseded physical runs and test databases were retired after checking actual borrowers; the isolated Xvfb/st processes were stopped. Source and handoffs stay in `/home/ts/wt/pi-cn-phase6--P1` and `--P2`. The standard resource-check executable is absent here; measured 3.1 GiB available RAM and 26 GiB disk before the bounded run. One physical UI was used; no parallel agent was launched. +## Checks and review limits + +The stable source checkpoint passed **142 tests in 72.67s**. After removing the +failure-family filter, all **10 commander-model checks passed in 1.88s**, including +selection of the original failed notification. Tests ran in the background under +`timeout 300 ... -X faulthandler`, with default parallelism/coverage disabled. +The environment warns about the unavailable pytest-timeout plugin; the outer +timeout was enforced. Ruff for the new model and both diff checks pass. + +The retained full ratchet reports at the stable checkpoint both exit **1**, solely +for `SourceLines` (+465 Core, +205 tools), with no introduced findings. Rechecking +`ca9c4f881` after deleting the failure-family filter gives **src 1/tools 1**, solely ++474/+205 SourceLines, again with introduced findings empty. No budget, +suppression or checker change was made. Under the commander's ruling in message +3617, SourceLines-only failures do not block review. Reports name their exact +base/head; they are not a claim about a fresh merger integration head. + +The checking interpreter is the shared QA Python, with the paired Toad source and +existing stack dependencies visible. Toad now publishes its inline declarations +through `py.typed`; no copied Toad schema or stub was introduced. The installed +stack interpreter lacks the checking modules and was not modified. + +The required `order pr P243` create command was attempted once. It failed on its +derived missing `pi-cn-phase6--P243` directory; this task's assigned checkout is +`pi-cn-phase6--P1`, and #1937 already exists. The existing PR is updated in place +with metadata generated by its canonical order owner. GitHub currently reports +a conflict with current main; no merge-main or unrequested rebase was performed.