From 496127897a343063b99551d55e5da7e3901ee5ab Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:53:33 -0400 Subject: [PATCH 1/9] Derive the commander matrix and red cutover gate from row declarations --- plans/commander-native-plan.md | 71 ++--- src/agent_comms/commander_parity/__init__.py | 81 +++++ src/agent_comms/commander_parity/__main__.py | 17 ++ src/agent_comms/commander_parity/rows.py | 296 +++++++++++++++++++ src/agent_comms/plan_phases.py | 13 +- tests/test_commander_parity.py | 61 ++++ tests/test_plan_phases.py | 3 +- 7 files changed, 503 insertions(+), 39 deletions(-) create mode 100644 src/agent_comms/commander_parity/__init__.py create mode 100644 src/agent_comms/commander_parity/__main__.py create mode 100644 src/agent_comms/commander_parity/rows.py create mode 100644 tests/test_commander_parity.py diff --git a/plans/commander-native-plan.md b/plans/commander-native-plan.md index 888180377f..091a13a675 100644 --- a/plans/commander-native-plan.md +++ b/plans/commander-native-plan.md @@ -28,42 +28,43 @@ ## Parity matrix -| # | Capability | Script source today | Native owner | Phase | + +| # | Capability | Current commander | Native owner | Native phases | |---|---|---|---|---| -| 1 | Create a problem with owner, order and deadline | `order.py problem`; `problems.json` | `CommsTodoCreateTool` (exists) | 1 | -| 2 | Assign, reassign, drop an order | `order.py send`, `--reassign`, `drop`; `deadlines.json` | `CommsOrderTool`, `CommsTodoTool` assignment and release (exist) | 1 | -| 3 | Complete only on the delivered commit | `order.py done` | `CommsOrderDoneTool` (exists), returning a delivery state: merged to `main`, merged to another branch, not merged | 1, 3 | -| 4 | Close with installed evidence | `order.py close`; `closed.jsonl` | `CommsProblemCloseTool` (exists) | 1 | -| 5 | Board, overdue, silent, spinning | `order.py board`, `overdue`; `fleet-board.md` | `CommsTodoListTool` and the fleet tool, derived (exist) | 1 | -| 6 | Branch hygiene | `branches-*.txt` by hand | `CommsWorkBranchesTool` (exists) | done | -| 7 | Strikes and the strike limit | `order.py strike`; `strikes.jsonl` | `Strike` rows on the order; `StrikeLimit` declared on the commanded author; one `comms_fire` | 3 | -| 8 | Verdicts on a unit's tool calls | `judge.py ok`, `back`; `verdicts.jsonl` | `Verdict` on the order; a send-back revises the order Goal and records the strike in one write | 3 | -| 9 | Plain words in verdicts and strikes | judge and order checks | `PlainText` value type | 3 | -| 10 | Refuse soft or vague orders | `order.py` | `OrderText` value type on every order action | 3 | -| 11 | What a unit did during its order | `tool_watch.py`; `edits.jsonl`; `judge.py show` | `comms_order_work`, read from both transcript families by `tool_call_id` | 4 | -| 12 | What the commander has not judged yet | `judge.py unjudged`; `vibe_watch.py` | `comms_unjudged`: tool calls and unit messages after the last verdict, derived | 4 | -| 13 | Public story and roasts | `fleet-narrative.md`; `judge.py roast` | `comms_order_story`, `comms_fleet_story`; each send-back verdict posts its plain reason to the order's channel | 5 | -| 14 | Heartbeat: next unit every minute | `rotate.py` (60 s loop) | a scheduled commander wake owned by Core | 7 | -| 15 | Standing rules broken right now | `rotate.py broken_rules` | a `StandingRule` family, each member deriving its violations on read | 7 | -| 16 | Rule review | `rules.py`; `rules.jsonl`; a maximum age | the rules as instruction segments in the commander's turn context, with provenance | 7 | -| 17 | Load and forking | `capacity.py`: 6 units per commander, 10-minute beat age; `forks/` | `CommanderLoad` derived from open orders and the unjudged queue; forks are registered threads | 7 | -| 18 | Growth alarm | `growth_watch.py`: merges growing `src` by more than 40 net lines | a growth check derived on read from git per configured repository | 7 | -| 19 | Storage hygiene | `storage.py`: worktree limit per unit, cleanup due under 30 GB free, nothing allocates under 5 GB | `UnitStorage` derived on read from worktree paths and free disk | 7 | -| 20 | Commander reaches units only through orders | `send_guard.py` (PreToolUse hook) | stays a hook, at the harness boundary, checking against Core's order tools | 7 | -| 21 | Paths, repositories, commander identity | `CommanderConfig` from `commander.json` | Core's project and registry configuration | 8 | -| 33 | Several commanders on one machine and bus, one per project | none (one `commander.json`; a second install overwrites the first) | each commander is a registered thread that owns one project. Orders, strikes, load, board, alarms and forks are keyed by that commander, and the commander for a call comes from its `--from` thread. The send guard refuses a raw send from any registered commander. Two commanders for one project are refused when the second registers | 1, 8 | -| 22 | Seeing the fleet: board, strikes, queue, story, alarms | `fleet-board.md`, `order.py board`, the terminal output of `rotate.py` and `judge.py` | a commander page in Toad, rendering Core's answers through `ui_model` | 6 | -| 23 | An order states the user-visible run that closes it | scratch `rts/order.py send` (`Works when:`) | `OrderText` requires it; `CommsProblemCloseTool.works` is checked against it | 3 | -| 24 | Reassigning past two owners needs the commander's trace | scratch `rts/order.py send --reassign --traced` | the transfer operation reads owners from the Todo's assignment history and refuses without a traced source location | 3 | -| 25 | Unit wording gets a correction, not a strike | scratch `vibe_watch.py` | `PlainText` applies to the commander's own verdicts and strikes; unit wording never becomes a strike | 3 | -| 26 | Alerts carry only decisions | none (today every line interrupts) | the commander's wake receives decision events only, each once | 7 | -| 27 | Momentum prediction per unit, compared with the outcome | none | a prediction record on the order, shown on the commander page beside what happened | 6 | -| 28 | Measured load and self-report | `capacity.py` unit count | `CommanderLoad`: look debt, judge debt, inflow, calibration, context, plus a one-word self-report calibrated against them | 7 | -| 29 | No handoff of finished, checked work | none (scratch `order.py send` moved P507 after its fix was pushed and checked) | the transfer operation refuses an order whose current owner has a delivered commit with its check passing; that owner merges it, and the check's recorded run is reused, never rerun by another unit | 3 | -| 30 | A proof is bound to the build it ran on | none (screenshots and logs are bare file paths) | the run record written by the build check (`tools/build_runtime.py`) carries runtime ID and Core/Toad revisions; `CommsProblemCloseTool` refuses when that runtime does not contain the order's delivered commit | 3 | -| 31 | An order states its intent as a deletion target | none (orders name a line, so units fix the line: P520 swapped `is_set()` for `except ValueError` around every caller) | the order declares the decision family being deleted, its owner, and a family-wide count from the existing NRA/refactor-audit scanners; completion runs that count on the delivered commit and refuses unless it is zero with no new site of the same kind | 3 | -| 32 | Where units' time goes, classified | none (`activity.log` has call times but no durations or turn boundaries) | Core's turn records carry each call's start and end and each turn's start reason; `UnitTime` derives per unit and fleet the minutes in model, tool runs (build, check, test, shell), waiting on another owner, waiting on the commander, and idle; the commander marks each large wait necessary or self-inflicted | 6, 7 | -| 33 | Order progress beside its deadline; the unit never sees a due time | `order.py board`, `overdue`, `due`, `send`; `rotate.py` (before: a due time and "Delivered" only, so merged work drew the strike threat) | `OrderProgress` in Core (`todos.py`), derived on read from the unit's checkout and the repository's PRs, never stored: no commit, committed not pushed, pushed with no PR, open PR, merged; `CommsTodoListTool` returns it per held order. Merged work draws no strike; no first commit within 10 minutes, or no new commit, push or PR change for 15 minutes, is a STALL with the progress line. `CommsOrderTool` sends the unit no due time: the deadline stays the commander's fire line | 1, 7 | +| 1 | Create a problem with owner, order and deadline | `order.py problem`; `problems.json` | `CommsTodoCreateTool` | `one_order_owner` | +| 2 | Assign, reassign, drop an order | `order.py send`, `--reassign`, `drop`; `deadlines.json` | `CommsOrderTool`, `CommsTodoTool` assignment and release | `one_order_owner` | +| 3 | Complete only on the delivered commit | `order.py done` | `CommsOrderDoneTool`: merged to main, another branch, or not merged | `one_order_owner`, `record_plane` | +| 4 | Close with installed evidence | `order.py close`; `closed.jsonl` | `CommsProblemCloseTool` | `one_order_owner` | +| 5 | Board, overdue, silent, spinning | `order.py board`, `overdue`; `fleet-board.md` | `CommsTodoListTool` and the fleet tool, derived | `one_order_owner` | +| 6 | Branch hygiene | `branches-*.txt` by hand | `CommsWorkBranchesTool` | `one_order_owner` | +| 7 | Strikes and the strike limit | `order.py strike`; `strikes.jsonl` | `Strike` rows, author-declared `StrikeLimit`, one `comms_fire` | `record_plane` | +| 8 | Verdicts on a unit's tool calls | `judge.py ok`, `back`; `verdicts.jsonl` | Order `Verdict`; send-back revises the Goal and records its strike atomically | `record_plane` | +| 9 | Plain words in verdicts and strikes | judge and order checks | `PlainText` | `record_plane` | +| 10 | Refuse soft or vague orders | `order.py` | `OrderText` on every order action | `record_plane` | +| 11 | What a unit did during its order | `tool_watch.py`; `edits.jsonl`; `judge.py show` | `comms_order_work`: both transcript families by `tool_call_id` | `review_queue` | +| 12 | What the commander has not judged yet | `judge.py unjudged`; `vibe_watch.py` | `comms_unjudged`: calls and unit messages after the last verdict | `review_queue` | +| 13 | Public story and roasts | `fleet-narrative.md`; `judge.py roast` | `comms_order_story`, `comms_fleet_story`; send-back reason posted once | `story` | +| 14 | Heartbeat: next unit every minute | `rotate.py` (60 s loop) | Core-owned scheduled commander wake | `control_plane` | +| 15 | Standing rules broken right now | `rotate.py broken_rules` | `StandingRule` members derive violations on read | `control_plane` | +| 16 | Rule review | `rules.py`; `rules.jsonl`; maximum age | Rule instruction segments in the commander's turn context, with provenance | `control_plane` | +| 17 | Load and forking | `capacity.py`: six units, ten-minute beat age; `forks/` | `commander_load.CommanderLoad.of`; `commander_measurements.ForkNow` | `control_plane` | +| 18 | Growth alarm | `growth_watch.py`: merges growing src by more than 40 net lines | `commander_measurements.MergeGrowth`: configured repositories and merged PRs | `control_plane` | +| 19 | Storage hygiene | `storage.py`: checkout limit, cleanup under 30 GB, allocation floor 5 GB | `UnitStorage`, derived from worktree paths and free disk | `control_plane` | +| 20 | Commander reaches units only through orders | `send_guard.py` (PreToolUse hook) | Harness hook checking Core order tools | `control_plane` | +| 21 | Paths, repositories, commander identity | `CommanderConfig` from `commander.json` | Core project and registry configuration | `cutover` | +| 22 | Seeing the fleet: board, strikes, queue, story, alarms | `fleet-board.md`, `order.py board`, `rotate.py`, `judge.py` | Toad commander page rendering Core through `ui_model` | `commander_page` | +| 23 | An order states the user-visible run that closes it | scratch `rts/order.py send` (`Works when:`) | `OrderText` requires the run; `CommsProblemCloseTool.works` checks it | `record_plane` | +| 24 | Reassigning past two owners needs the commander's trace | scratch `order.py send --reassign --traced` | Transfer reads Todo owner history and requires traced source location | `record_plane` | +| 25 | Unit wording gets a correction, not a strike | scratch `vibe_watch.py` | `PlainText` for commander verdicts/strikes; unit wording gets correction | `record_plane` | +| 26 | Alerts carry only decisions | none; every line interrupts | `messages.AlertKind.require_publishable`, `tools.CommsDecisionTool` | `control_plane` | +| 27 | Momentum prediction per unit, compared with the outcome | none | Prediction record and later outcome on commander page; `CommitMomentum` is a partial candidate | `commander_page` | +| 28 | Measured load and self-report | `capacity.py` unit count | `commander_load.CommanderLoad.of`: debts, inflow, calibration and context | `control_plane` | +| 29 | No handoff of finished, checked work | none; scratch transfer moved P507 after its fix was pushed and checked | Transfer refuses delivered checked work; that owner merges; check is reused | `record_plane` | +| 30 | A proof is bound to the build it ran on | none; screenshots and logs are bare paths | Build run records runtime and Core/Toad revisions; close requires delivered commit | `record_plane` | +| 31 | An order states its intent as a deletion target | none; P520 renamed a decision rather than deleting it | Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions | `record_plane` | +| 32 | Where units' time goes, classified | none; activity.log lacks durations and turn boundaries | `UnitTime`: model, build/check/test/shell, dependency/commander waits and idle; classify large waits | `commander_page`, `control_plane` | +| 33 | Order progress beside its deadline; the unit never sees a due time | `order.py board`, `overdue`, `due`, `send`; `rotate.py` | Derived `OrderProgress`; merged work draws no strike; first commit 10 min, later progress 15 min STALL; units see no due time | `one_order_owner`, `control_plane` | +| 34 | Several commanders on one machine and bus, one per project | none; a second commander.json install overwrites the first | Registry commander/project ownership; caller from --from; scoped orders, strikes, load, board, alarms and forks; raw-send guard; duplicate project commander refused | `one_order_owner`, `cutover` | **Dropped deliberately:** old verdicts and `edits.jsonl`. They judged copies of tool calls, and any old call can be judged again from the transcripts. diff --git a/src/agent_comms/commander_parity/__init__.py b/src/agent_comms/commander_parity/__init__.py new file mode 100644 index 0000000000..8d26fcbc6f --- /dev/null +++ b/src/agent_comms/commander_parity/__init__.py @@ -0,0 +1,81 @@ +"""Parity declarations own matrix generation and the cutover decision.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Iterable +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, ClassVar + +from ..declared_family import DeclaredFamily + +if TYPE_CHECKING: + from ..plan_phases import PlanPhase + from ..todos import Todo + + +class ParityComparison(ABC): + @property + @abstractmethod + def passed(self) -> bool: ... + + +@dataclass(frozen=True) +class UnverifiedParity(ParityComparison): + """Missing behavior has no evidence from which to claim a pass.""" + + reason: str = "The native/current workflow comparison has not run." + passed: ClassVar[bool] = False + + +class ParityRow( + DeclaredFamily, affix="ParityRow", discovery_package="agent_comms.commander_parity" +): + number: ClassVar[int] + capability: ClassVar[str] + current: ClassVar[str] + native: ClassVar[str] + phases: ClassVar[tuple[type[PlanPhase], ...]] + + @classmethod + def compare(cls, todos: Iterable[Todo]) -> ParityComparison: + return UnverifiedParity() + + @classmethod + def rows(cls) -> tuple[type[ParityRow], ...]: + rows = tuple(sorted(cls.members_with(cls), key=lambda row: row.number)) + numbers = tuple(row.number for row in rows) + if not numbers or min(numbers) < 1 or len(set(numbers)) != len(numbers): + raise ValueError("Parity declarations require unique positive row numbers") + return rows + + @classmethod + def done(cls, todos: Iterable[Todo]) -> bool: + history = tuple(todos) + return all(row.compare(history).passed for row in cls.rows()) + + @classmethod + def matrix(cls) -> str: + """Render declared facts only; no mutable completion column lives here.""" + lines = [ + "", + "| # | Capability | Current commander | Native owner | Native phases |", + "|---|---|---|---|---|", + ] + for row in cls.rows(): + owners = ", ".join(f"`{phase.declared_name}`" for phase in row.phases) + lines.append( + f"| {row.number} | {row.capability} | {row.current} | {row.native} | {owners} |" + ) + return "\n".join(lines) + "\n" + + @classmethod + def render_plan(cls, path: Path) -> str: + """Replace a generated section, never decode prose as executable identity.""" + text = path.read_text() + before, marker, remainder = text.partition("## Parity matrix\n") + _, end, after = remainder.partition("\n**Dropped deliberately:**") + if not marker or not end: + raise ValueError("Plan has no bounded parity matrix section") + return before + marker + "\n" + cls.matrix() + end + after diff --git a/src/agent_comms/commander_parity/__main__.py b/src/agent_comms/commander_parity/__main__.py new file mode 100644 index 0000000000..3bbfceb6ea --- /dev/null +++ b/src/agent_comms/commander_parity/__main__.py @@ -0,0 +1,17 @@ +"""Render the commander matrix from its owning declarations.""" + +import argparse +from pathlib import Path + +from . import ParityRow + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--plan", type=Path, help="Render the full plan instead of only the table") + args = parser.parse_args() + print(ParityRow.render_plan(args.plan) if args.plan else ParityRow.matrix(), end="") + + +if __name__ == "__main__": + main() diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py new file mode 100644 index 0000000000..b79577498a --- /dev/null +++ b/src/agent_comms/commander_parity/rows.py @@ -0,0 +1,296 @@ +"""Each native/current workflow declares its identity once.""" + +from ..plan_phases import ( + CommanderPagePhase, + ControlPlanePhase, + CutoverPhase, + OneOrderOwnerPhase, + RecordPlanePhase, + ReviewQueuePhase, + StoryPhase, +) +from . import ParityRow + + +class CreateProblemParityRow(ParityRow): + number = 1 + capability = "Create a problem with owner, order and deadline" + current = "`order.py problem`; `problems.json`" + native = "`CommsTodoCreateTool`" + phases = (OneOrderOwnerPhase,) + + +class AssignOrderParityRow(ParityRow): + number = 2 + capability = "Assign, reassign, drop an order" + current = "`order.py send`, `--reassign`, `drop`; `deadlines.json`" + native = "`CommsOrderTool`, `CommsTodoTool` assignment and release" + phases = (OneOrderOwnerPhase,) + + +class DeliveredCommitParityRow(ParityRow): + number = 3 + capability = "Complete only on the delivered commit" + current = "`order.py done`" + native = "`CommsOrderDoneTool`: merged to main, another branch, or not merged" + phases = (OneOrderOwnerPhase, RecordPlanePhase) + + +class InstalledEvidenceParityRow(ParityRow): + number = 4 + capability = "Close with installed evidence" + current = "`order.py close`; `closed.jsonl`" + native = "`CommsProblemCloseTool`" + phases = (OneOrderOwnerPhase,) + + +class BoardParityRow(ParityRow): + number = 5 + capability = "Board, overdue, silent, spinning" + current = "`order.py board`, `overdue`; `fleet-board.md`" + native = "`CommsTodoListTool` and the fleet tool, derived" + phases = (OneOrderOwnerPhase,) + + +class BranchHygieneParityRow(ParityRow): + number = 6 + capability = "Branch hygiene" + current = "`branches-*.txt` by hand" + native = "`CommsWorkBranchesTool`" + phases = (OneOrderOwnerPhase,) + + +class StrikeLimitParityRow(ParityRow): + number = 7 + capability = "Strikes and the strike limit" + current = "`order.py strike`; `strikes.jsonl`" + native = "`Strike` rows, author-declared `StrikeLimit`, one `comms_fire`" + phases = (RecordPlanePhase,) + + +class VerdictParityRow(ParityRow): + number = 8 + capability = "Verdicts on a unit's tool calls" + current = "`judge.py ok`, `back`; `verdicts.jsonl`" + native = "Order `Verdict`; send-back revises the Goal and records its strike atomically" + phases = (RecordPlanePhase,) + + +class PlainWordsParityRow(ParityRow): + number = 9 + capability = "Plain words in verdicts and strikes" + current = "judge and order checks" + native = "`PlainText`" + phases = (RecordPlanePhase,) + + +class OrderWordingParityRow(ParityRow): + number = 10 + capability = "Refuse soft or vague orders" + current = "`order.py`" + native = "`OrderText` on every order action" + phases = (RecordPlanePhase,) + + +class OrderWorkParityRow(ParityRow): + number = 11 + capability = "What a unit did during its order" + current = "`tool_watch.py`; `edits.jsonl`; `judge.py show`" + native = "`comms_order_work`: both transcript families by `tool_call_id`" + phases = (ReviewQueuePhase,) + + +class UnjudgedParityRow(ParityRow): + number = 12 + capability = "What the commander has not judged yet" + current = "`judge.py unjudged`; `vibe_watch.py`" + native = "`comms_unjudged`: calls and unit messages after the last verdict" + phases = (ReviewQueuePhase,) + + +class PublicStoryParityRow(ParityRow): + number = 13 + capability = "Public story and roasts" + current = "`fleet-narrative.md`; `judge.py roast`" + native = "`comms_order_story`, `comms_fleet_story`; send-back reason posted once" + phases = (StoryPhase,) + + +class ScheduledWakeParityRow(ParityRow): + number = 14 + capability = "Heartbeat: next unit every minute" + current = "`rotate.py` (60 s loop)" + native = "Core-owned scheduled commander wake" + phases = (ControlPlanePhase,) + + +class StandingRulesParityRow(ParityRow): + number = 15 + capability = "Standing rules broken right now" + current = "`rotate.py broken_rules`" + native = "`StandingRule` members derive violations on read" + phases = (ControlPlanePhase,) + + +class RuleReviewParityRow(ParityRow): + number = 16 + capability = "Rule review" + current = "`rules.py`; `rules.jsonl`; maximum age" + native = "Rule instruction segments in the commander's turn context, with provenance" + phases = (ControlPlanePhase,) + + +class LoadForkingParityRow(ParityRow): + number = 17 + capability = "Load and forking" + current = "`capacity.py`: six units, ten-minute beat age; `forks/`" + native = "`commander_load.CommanderLoad.of`; `commander_measurements.ForkNow`" + phases = (ControlPlanePhase,) + + +class GrowthAlarmParityRow(ParityRow): + number = 18 + capability = "Growth alarm" + current = "`growth_watch.py`: merges growing src by more than 40 net lines" + native = "`commander_measurements.MergeGrowth`: configured repositories and merged PRs" + phases = (ControlPlanePhase,) + + +class StorageHygieneParityRow(ParityRow): + number = 19 + capability = "Storage hygiene" + current = "`storage.py`: checkout limit, cleanup under 30 GB, allocation floor 5 GB" + native = "`UnitStorage`, derived from worktree paths and free disk" + phases = (ControlPlanePhase,) + + +class OrderOnlyContactParityRow(ParityRow): + number = 20 + capability = "Commander reaches units only through orders" + current = "`send_guard.py` (PreToolUse hook)" + native = "Harness hook checking Core order tools" + phases = (ControlPlanePhase,) + + +class ConfigurationParityRow(ParityRow): + number = 21 + capability = "Paths, repositories, commander identity" + current = "`CommanderConfig` from `commander.json`" + native = "Core project and registry configuration" + phases = (CutoverPhase,) + + +class CommanderPageParityRow(ParityRow): + number = 22 + capability = "Seeing the fleet: board, strikes, queue, story, alarms" + current = "`fleet-board.md`, `order.py board`, `rotate.py`, `judge.py`" + native = "Toad commander page rendering Core through `ui_model`" + phases = (CommanderPagePhase,) + + +class ApplicationRunParityRow(ParityRow): + number = 23 + capability = "An order states the user-visible run that closes it" + current = "scratch `rts/order.py send` (`Works when:`)" + native = "`OrderText` requires the run; `CommsProblemCloseTool.works` checks it" + phases = (RecordPlanePhase,) + + +class TracedTransferParityRow(ParityRow): + number = 24 + capability = "Reassigning past two owners needs the commander's trace" + current = "scratch `order.py send --reassign --traced`" + native = "Transfer reads Todo owner history and requires traced source location" + phases = (RecordPlanePhase,) + + +class UnitWordingParityRow(ParityRow): + number = 25 + capability = "Unit wording gets a correction, not a strike" + current = "scratch `vibe_watch.py`" + native = "`PlainText` for commander verdicts/strikes; unit wording gets correction" + phases = (RecordPlanePhase,) + + +class DecisionAlertsParityRow(ParityRow): + number = 26 + capability = "Alerts carry only decisions" + current = "none; every line interrupts" + native = "`messages.AlertKind.require_publishable`, `tools.CommsDecisionTool`" + phases = (ControlPlanePhase,) + + +class MomentumParityRow(ParityRow): + number = 27 + capability = "Momentum prediction per unit, compared with the outcome" + current = "none" + native = ( + "Prediction record and later outcome on commander page; " + "`CommitMomentum` is a partial candidate" + ) + phases = (CommanderPagePhase,) + + +class MeasuredLoadParityRow(ParityRow): + number = 28 + capability = "Measured load and self-report" + current = "`capacity.py` unit count" + native = "`commander_load.CommanderLoad.of`: debts, inflow, calibration and context" + phases = (ControlPlanePhase,) + + +class FinishedWorkTransferParityRow(ParityRow): + number = 29 + capability = "No handoff of finished, checked work" + current = "none; scratch transfer moved P507 after its fix was pushed and checked" + native = "Transfer refuses delivered checked work; that owner merges; check is reused" + phases = (RecordPlanePhase,) + + +class BuildBoundProofParityRow(ParityRow): + number = 30 + capability = "A proof is bound to the build it ran on" + current = "none; screenshots and logs are bare paths" + native = "Build run records runtime and Core/Toad revisions; close requires delivered commit" + phases = (RecordPlanePhase,) + + +class DeletionTargetParityRow(ParityRow): + number = 31 + capability = "An order states its intent as a deletion target" + current = "none; P520 renamed a decision rather than deleting it" + native = "Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions" + phases = (RecordPlanePhase,) + + +class UnitTimeParityRow(ParityRow): + number = 32 + capability = "Where units' time goes, classified" + current = "none; activity.log lacks durations and turn boundaries" + native = ( + "`UnitTime`: model, build/check/test/shell, dependency/commander waits and idle; " + "classify large waits" + ) + phases = (CommanderPagePhase, ControlPlanePhase) + + +class OrderProgressParityRow(ParityRow): + number = 33 + capability = "Order progress beside its deadline; the unit never sees a due time" + current = "`order.py board`, `overdue`, `due`, `send`; `rotate.py`" + native = ( + "Derived `OrderProgress`; merged work draws no strike; first commit 10 min, " + "later progress 15 min STALL; units see no due time" + ) + phases = (OneOrderOwnerPhase, ControlPlanePhase) + + +class MultipleCommandersParityRow(ParityRow): + number = 34 + capability = "Several commanders on one machine and bus, one per project" + current = "none; a second commander.json install overwrites the first" + native = ( + "Registry commander/project ownership; caller from --from; scoped orders, strikes, " + "load, board, alarms and forks; raw-send guard; duplicate project commander refused" + ) + phases = (OneOrderOwnerPhase, CutoverPhase) diff --git a/src/agent_comms/plan_phases.py b/src/agent_comms/plan_phases.py index c42d40d3c8..04865c669b 100644 --- a/src/agent_comms/plan_phases.py +++ b/src/agent_comms/plan_phases.py @@ -12,6 +12,7 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, ClassVar +from .commander_parity import ParityRow from .declared_family import DeclaredFamily from .phase_tasks import PhaseTask, UnspecifiedPhaseTask @@ -89,11 +90,17 @@ class ControlPlanePhase(PlanPhase): after = (RecordPlanePhase,) -class CutoverPhase(PlanPhase): - # The parity gate in the Markdown plan has no executable declaration yet. - # These dependencies do not establish parity or authorize deleting scripts. +class ParityPhase(PlanPhase): after = (RecordPlanePhase, ReviewQueuePhase, ControlPlanePhase) + @classmethod + def done(cls, todos: Iterable[Todo]) -> bool: + return ParityRow.done(todos) + + +class CutoverPhase(PlanPhase): + after = (RecordPlanePhase, ReviewQueuePhase, ControlPlanePhase, ParityPhase) + class StoryPhase(PlanPhase): """A view of the native records, not a prerequisite for their cutover.""" diff --git a/tests/test_commander_parity.py b/tests/test_commander_parity.py new file mode 100644 index 0000000000..a034cd029e --- /dev/null +++ b/tests/test_commander_parity.py @@ -0,0 +1,61 @@ +from pathlib import Path + +from agent_comms.commander_parity import ParityComparison, ParityRow, UnverifiedParity +from agent_comms.plan_phases import ( + CommanderPagePhase, + ControlPlanePhase, + CutoverPhase, + ParityPhase, + PlanPhase, + RecordPlanePhase, + ReviewQueuePhase, + StoryPhase, +) + +PLAN = Path(__file__).resolve().parents[1] / "plans/commander-native-plan.md" + + +class MatchingComparison(ParityComparison): + passed = True + + +def test_generated_matrix_is_a_derived_output(): + assert ParityRow.render_plan(PLAN) == PLAN.read_text() + rows = ParityRow.rows() + assert len(rows) == len({row.number for row in rows}) == 34 + assert all(row.capability and row.current and row.native and row.phases for row in rows) + assert all(not row.compare(()).passed for row in rows) + + +def test_cutover_gate_is_computed_from_every_comparison(monkeypatch): + assert CutoverPhase.after == ( + RecordPlanePhase, + ReviewQueuePhase, + ControlPlanePhase, + ParityPhase, + ) + assert not ParityPhase.done(()) + for row in ParityRow.rows(): + monkeypatch.setattr(row, "compare", classmethod(lambda cls, history: MatchingComparison())) + assert ParityPhase.done(()) + last = ParityRow.rows()[-1] + monkeypatch.setattr(last, "compare", classmethod(lambda cls, history: UnverifiedParity())) + assert not ParityPhase.done(()) + assert not any( + dependency in (StoryPhase, CommanderPagePhase) + for phase in PlanPhase.members_with(PlanPhase) + for dependency in phase.after + ) + + +def test_generated_prose_is_not_input_to_the_gate(tmp_path): + plan = tmp_path / "plan.md" + plan.write_text( + "Before\n## Parity matrix\n\nHand edited, invalid prose.\n" + "**Dropped deliberately:** historical text\nAfter\n" + ) + rendered = ParityRow.render_plan(plan) + assert rendered.startswith("Before\n## Parity matrix\n\n") + assert rendered.endswith("**Dropped deliberately:** historical text\nAfter\n") + assert "Hand edited" not in rendered + assert not ParityPhase.done(()) diff --git a/tests/test_plan_phases.py b/tests/test_plan_phases.py index 669ccccd83..82dfc41aeb 100644 --- a/tests/test_plan_phases.py +++ b/tests/test_plan_phases.py @@ -68,7 +68,8 @@ def test_views_wait_for_native_work_but_never_block_cutover(commander, monkeypat accepted(todo) if todo.order.serves in required else todo for todo in todos ) - assert CutoverPhase.ready(native_done) + # Native source completion alone cannot bypass the independent parity gate. + assert not CutoverPhase.ready(native_done) assert StoryPhase.ready(native_done) assert CommanderPagePhase.ready(native_done) assert not SkillPhase.ready(native_done) From c46b8636c02ab6d516da30f714903ad90e0966f0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 17:21:03 -0400 Subject: [PATCH 2/9] Track reporting-only load and first-parent growth owners in parity declarations --- plans/commander-native-plan.md | 6 +++--- src/agent_comms/commander_parity/rows.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plans/commander-native-plan.md b/plans/commander-native-plan.md index 091a13a675..c01a447ed7 100644 --- a/plans/commander-native-plan.md +++ b/plans/commander-native-plan.md @@ -47,8 +47,8 @@ | 14 | Heartbeat: next unit every minute | `rotate.py` (60 s loop) | Core-owned scheduled commander wake | `control_plane` | | 15 | Standing rules broken right now | `rotate.py broken_rules` | `StandingRule` members derive violations on read | `control_plane` | | 16 | Rule review | `rules.py`; `rules.jsonl`; maximum age | Rule instruction segments in the commander's turn context, with provenance | `control_plane` | -| 17 | Load and forking | `capacity.py`: six units, ten-minute beat age; `forks/` | `commander_load.CommanderLoad.of`; `commander_measurements.ForkNow` | `control_plane` | -| 18 | Growth alarm | `growth_watch.py`: merges growing src by more than 40 net lines | `commander_measurements.MergeGrowth`: configured repositories and merged PRs | `control_plane` | +| 17 | Load and forking | `capacity.py`: six units, ten-minute beat age; `forks/` | Reporting-only `commander_load.CommanderLoad.of`; P253 removes forking policy | `control_plane` | +| 18 | Growth alarm | `growth_watch.py`: merges growing src by more than 40 net lines | `commander.config.Repo.main_changes` → `commander_measurements.MergeGrowth.read` | `control_plane` | | 19 | Storage hygiene | `storage.py`: checkout limit, cleanup under 30 GB, allocation floor 5 GB | `UnitStorage`, derived from worktree paths and free disk | `control_plane` | | 20 | Commander reaches units only through orders | `send_guard.py` (PreToolUse hook) | Harness hook checking Core order tools | `control_plane` | | 21 | Paths, repositories, commander identity | `CommanderConfig` from `commander.json` | Core project and registry configuration | `cutover` | @@ -58,7 +58,7 @@ | 25 | Unit wording gets a correction, not a strike | scratch `vibe_watch.py` | `PlainText` for commander verdicts/strikes; unit wording gets correction | `record_plane` | | 26 | Alerts carry only decisions | none; every line interrupts | `messages.AlertKind.require_publishable`, `tools.CommsDecisionTool` | `control_plane` | | 27 | Momentum prediction per unit, compared with the outcome | none | Prediction record and later outcome on commander page; `CommitMomentum` is a partial candidate | `commander_page` | -| 28 | Measured load and self-report | `capacity.py` unit count | `commander_load.CommanderLoad.of`: debts, inflow, calibration and context | `control_plane` | +| 28 | Measured load and self-report | `capacity.py` unit count | Reporting-only `commander_load.CommanderLoad.of`: orders, overdue, edits and live forks | `control_plane` | | 29 | No handoff of finished, checked work | none; scratch transfer moved P507 after its fix was pushed and checked | Transfer refuses delivered checked work; that owner merges; check is reused | `record_plane` | | 30 | A proof is bound to the build it ran on | none; screenshots and logs are bare paths | Build run records runtime and Core/Toad revisions; close requires delivered commit | `record_plane` | | 31 | An order states its intent as a deletion target | none; P520 renamed a decision rather than deleting it | Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions | `record_plane` | diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index b79577498a..227cfcb03c 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -144,7 +144,7 @@ class LoadForkingParityRow(ParityRow): number = 17 capability = "Load and forking" current = "`capacity.py`: six units, ten-minute beat age; `forks/`" - native = "`commander_load.CommanderLoad.of`; `commander_measurements.ForkNow`" + native = "Reporting-only `commander_load.CommanderLoad.of`; P253 removes forking policy" phases = (ControlPlanePhase,) @@ -152,7 +152,7 @@ class GrowthAlarmParityRow(ParityRow): number = 18 capability = "Growth alarm" current = "`growth_watch.py`: merges growing src by more than 40 net lines" - native = "`commander_measurements.MergeGrowth`: configured repositories and merged PRs" + native = "`commander.config.Repo.main_changes` → `commander_measurements.MergeGrowth.read`" phases = (ControlPlanePhase,) @@ -235,7 +235,7 @@ class MeasuredLoadParityRow(ParityRow): number = 28 capability = "Measured load and self-report" current = "`capacity.py` unit count" - native = "`commander_load.CommanderLoad.of`: debts, inflow, calibration and context" + native = "Reporting-only `commander_load.CommanderLoad.of`: orders, overdue, edits and live forks" phases = (ControlPlanePhase,) From 50524650aeb94263f7030eaab39984b77306db21 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:37:50 -0400 Subject: [PATCH 3/9] Verify live commander load reporting through its actual native measurement --- plans/commander-native-plan.md | 4 +- src/agent_comms/commander_parity/__init__.py | 11 ++ src/agent_comms/commander_parity/__main__.py | 25 +++++ src/agent_comms/commander_parity/load.py | 106 +++++++++++++++++++ src/agent_comms/commander_parity/rows.py | 15 ++- tests/test_commander_parity_load.py | 25 +++++ 6 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 src/agent_comms/commander_parity/load.py create mode 100644 tests/test_commander_parity_load.py diff --git a/plans/commander-native-plan.md b/plans/commander-native-plan.md index c01a447ed7..5daaf8adaa 100644 --- a/plans/commander-native-plan.md +++ b/plans/commander-native-plan.md @@ -47,7 +47,7 @@ | 14 | Heartbeat: next unit every minute | `rotate.py` (60 s loop) | Core-owned scheduled commander wake | `control_plane` | | 15 | Standing rules broken right now | `rotate.py broken_rules` | `StandingRule` members derive violations on read | `control_plane` | | 16 | Rule review | `rules.py`; `rules.jsonl`; maximum age | Rule instruction segments in the commander's turn context, with provenance | `control_plane` | -| 17 | Load and forking | `capacity.py`: six units, ten-minute beat age; `forks/` | Reporting-only `commander_load.CommanderLoad.of`; P253 removes forking policy | `control_plane` | +| 17 | Load and forking | `agent-comms-commander capacity`: load and live fork reports; ten-minute beat age | Reporting-only `commander.capacity.Load.of`; no capacity refusal or automatic fork policy | `control_plane` | | 18 | Growth alarm | `growth_watch.py`: merges growing src by more than 40 net lines | `commander.config.Repo.main_changes` → `commander_measurements.MergeGrowth.read` | `control_plane` | | 19 | Storage hygiene | `storage.py`: checkout limit, cleanup under 30 GB, allocation floor 5 GB | `UnitStorage`, derived from worktree paths and free disk | `control_plane` | | 20 | Commander reaches units only through orders | `send_guard.py` (PreToolUse hook) | Harness hook checking Core order tools | `control_plane` | @@ -58,7 +58,7 @@ | 25 | Unit wording gets a correction, not a strike | scratch `vibe_watch.py` | `PlainText` for commander verdicts/strikes; unit wording gets correction | `record_plane` | | 26 | Alerts carry only decisions | none; every line interrupts | `messages.AlertKind.require_publishable`, `tools.CommsDecisionTool` | `control_plane` | | 27 | Momentum prediction per unit, compared with the outcome | none | Prediction record and later outcome on commander page; `CommitMomentum` is a partial candidate | `commander_page` | -| 28 | Measured load and self-report | `capacity.py` unit count | Reporting-only `commander_load.CommanderLoad.of`: orders, overdue, edits and live forks | `control_plane` | +| 28 | Measured load and self-report | `capacity.py` unit count | `commander.capacity.Load.of` reports orders, overdue, edits and forks; self-report comparison unverified | `control_plane` | | 29 | No handoff of finished, checked work | none; scratch transfer moved P507 after its fix was pushed and checked | Transfer refuses delivered checked work; that owner merges; check is reused | `record_plane` | | 30 | A proof is bound to the build it ran on | none; screenshots and logs are bare paths | Build run records runtime and Core/Toad revisions; close requires delivered commit | `record_plane` | | 31 | An order states its intent as a deletion target | none; P520 renamed a decision rather than deleting it | Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions | `record_plane` | diff --git a/src/agent_comms/commander_parity/__init__.py b/src/agent_comms/commander_parity/__init__.py index 8d26fcbc6f..22073f86a4 100644 --- a/src/agent_comms/commander_parity/__init__.py +++ b/src/agent_comms/commander_parity/__init__.py @@ -20,6 +20,9 @@ class ParityComparison(ABC): @abstractmethod def passed(self) -> bool: ... + def describe(self) -> str: + return repr(self) + @dataclass(frozen=True) class UnverifiedParity(ParityComparison): @@ -28,6 +31,9 @@ class UnverifiedParity(ParityComparison): reason: str = "The native/current workflow comparison has not run." passed: ClassVar[bool] = False + def describe(self) -> str: + return self.reason + class ParityRow( DeclaredFamily, affix="ParityRow", discovery_package="agent_comms.commander_parity" @@ -42,6 +48,11 @@ class ParityRow( def compare(cls, todos: Iterable[Todo]) -> ParityComparison: return UnverifiedParity() + @classmethod + def verify(cls) -> ParityComparison: + """Run this declared workflow now; a run is not durable cutover authority.""" + return UnverifiedParity() + @classmethod def rows(cls) -> tuple[type[ParityRow], ...]: rows = tuple(sorted(cls.members_with(cls), key=lambda row: row.number)) diff --git a/src/agent_comms/commander_parity/__main__.py b/src/agent_comms/commander_parity/__main__.py index 3bbfceb6ea..ee378b8178 100644 --- a/src/agent_comms/commander_parity/__main__.py +++ b/src/agent_comms/commander_parity/__main__.py @@ -1,6 +1,7 @@ """Render the commander matrix from its owning declarations.""" import argparse +import json from pathlib import Path from . import ParityRow @@ -9,7 +10,31 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--plan", type=Path, help="Render the full plan instead of only the table") + parser.add_argument( + "--verify", action="store_true", help="Run live comparisons; do not grant cutover" + ) args = parser.parse_args() + if args.verify: + comparisons = tuple((row, row.verify()) for row in ParityRow.rows()) + print( + json.dumps( + { + "verified": sum(result.passed for _, result in comparisons), + "total": len(comparisons), + "rows": [ + dict( + number=row.number, + capability=row.capability, + passed=result.passed, + result=result.describe(), + ) + for row, result in comparisons + ], + }, + indent=2, + ) + ) + raise SystemExit(0 if all(result.passed for _, result in comparisons) else 1) print(ParityRow.render_plan(args.plan) if args.plan else ParityRow.matrix(), end="") diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py new file mode 100644 index 0000000000..18d2cbce98 --- /dev/null +++ b/src/agent_comms/commander_parity/load.py @@ -0,0 +1,106 @@ +"""Observe the native load measurement used by the installed capacity command.""" + +from __future__ import annotations + +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from ..commander.capacity import Load +from ..commander.config import CommanderConfig +from ..field_codec import FieldCodec +from ..private_path import FileRevision +from . import ParityComparison, UnverifiedParity + + +# Profile the real return, never replace the measurement, command, bus or reply. +# Comparing independent reads of a running fleet confuses new edits with drift. +OBSERVE_CAPACITY = """ +import contextlib, io, json, sys +import agent_comms.commander.capacity as capacity +from agent_comms.commander.cli import main +from agent_comms.commander.config import CommanderConfig +from agent_comms.field_codec import FieldCodec +measured = [] +def observe(frame, event, value): + if event == 'return' and frame.f_code is capacity.Load.of.__func__.__code__: + measured.append(value) +output = io.StringIO() +sys.setprofile(observe) +try: + with contextlib.redirect_stdout(output): + main() +finally: + sys.setprofile(None) +if len(measured) != 1: + raise ValueError('Capacity must use exactly one native load measurement') +config = CommanderConfig.installed() +print(json.dumps(dict(current=output.getvalue(), load=FieldCodec.encode(measured[0]), + source=capacity.__file__, root=str(config.root), + commander=config.commander))) +""" + + +def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: + return tuple( + (str(path.relative_to(directory)), FileRevision.capture(path)) + for path in sorted(directory.rglob("*")) + if path.is_file() + ) + + +@dataclass(frozen=True) +class MeasuredOutput: + current: str + load: Load + source: str + root: str + commander: str + + +@dataclass(frozen=True) +class LoadComparison(ParityComparison): + observation: MeasuredOutput + state_before: tuple[tuple[str, FileRevision], ...] + state_after: tuple[tuple[str, FileRevision], ...] + + @property + def passed(self) -> bool: + return ( + self.observation.current.strip() == str(self.observation.load) + and self.state_before == self.state_after + ) + + def describe(self) -> str: + return ( + f"current={self.observation.current.strip()!r}; " + f"native={str(self.observation.load)!r}; owner={self.observation.source}; " + f"commander={self.observation.commander}; root={self.observation.root}; " + f"private_state_unchanged={self.state_before == self.state_after}" + ) + + @classmethod + def read(cls) -> ParityComparison: + config = CommanderConfig.installed() + before = state_files(config.state) + # -I uses installed imports even when this candidate runs with PYTHONPATH=src. + # HOME and the environment stay unchanged; the actual CLI main runs. + current = subprocess.run( + (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), + capture_output=True, + text=True, + timeout=30, + ) + if current.returncode: + return UnverifiedParity(current.stderr) + observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) + source = Path(observed.source) + if ( + observed.root != str(config.root) + or observed.commander != config.commander + or not any(source.is_relative_to(repo.checkout) for repo in config.repos) + ): + return UnverifiedParity( + "The installed command used a different configuration or source" + ) + return cls(observed, before, state_files(config.state)) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 227cfcb03c..8eb2cffc73 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -9,7 +9,8 @@ ReviewQueuePhase, StoryPhase, ) -from . import ParityRow +from . import ParityComparison, ParityRow +from .load import LoadComparison class CreateProblemParityRow(ParityRow): @@ -143,10 +144,16 @@ class RuleReviewParityRow(ParityRow): class LoadForkingParityRow(ParityRow): number = 17 capability = "Load and forking" - current = "`capacity.py`: six units, ten-minute beat age; `forks/`" - native = "Reporting-only `commander_load.CommanderLoad.of`; P253 removes forking policy" + current = "`agent-comms-commander capacity`: load and live fork reports; ten-minute beat age" + native = ( + "Reporting-only `commander.capacity.Load.of`; no capacity refusal or automatic fork policy" + ) phases = (ControlPlanePhase,) + @classmethod + def verify(cls) -> ParityComparison: + return LoadComparison.read() + class GrowthAlarmParityRow(ParityRow): number = 18 @@ -235,7 +242,7 @@ class MeasuredLoadParityRow(ParityRow): number = 28 capability = "Measured load and self-report" current = "`capacity.py` unit count" - native = "Reporting-only `commander_load.CommanderLoad.of`: orders, overdue, edits and live forks" + native = "`commander.capacity.Load.of` reports orders, overdue, edits and forks; self-report comparison unverified" phases = (ControlPlanePhase,) diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py new file mode 100644 index 0000000000..3d81ad518b --- /dev/null +++ b/tests/test_commander_parity_load.py @@ -0,0 +1,25 @@ +from dataclasses import replace + +from agent_comms.commander.capacity import Load +from agent_comms.commander_parity.load import LoadComparison, MeasuredOutput +from agent_comms.private_path import FileRevision + + +def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_path): + load = Load(37, 8, 1692, ("merger",)) + observed = MeasuredOutput(str(load) + "\n", load, "capacity.py", "/bus", "lead") + comparison = LoadComparison(observed, (), ()) + assert comparison.passed + for different in ( + replace(load, owed=38), + replace(load, overdue=9), + replace(load, unjudged=1693), + replace(load, forks=()), + ): + assert not replace(comparison, observation=replace(observed, load=different)).passed + path = tmp_path / "old-state" + path.write_text("before") + before = ((path.name, FileRevision.capture(path)),) + path.write_text("after") + after = ((path.name, FileRevision.capture(path)),) + assert not LoadComparison(observed, before, after).passed From ca1cbf9198f9efee4ad291fb83141b0b4a662a92 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:40:30 -0400 Subject: [PATCH 4/9] Keep live load observation independent of commander CLI imports --- src/agent_comms/commander_parity/load.py | 10 ++++------ tests/test_commander_parity_load.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index 18d2cbce98..173be0e177 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from pathlib import Path -from ..commander.capacity import Load from ..commander.config import CommanderConfig from ..field_codec import FieldCodec from ..private_path import FileRevision @@ -20,7 +19,6 @@ import agent_comms.commander.capacity as capacity from agent_comms.commander.cli import main from agent_comms.commander.config import CommanderConfig -from agent_comms.field_codec import FieldCodec measured = [] def observe(frame, event, value): if event == 'return' and frame.f_code is capacity.Load.of.__func__.__code__: @@ -35,7 +33,7 @@ def observe(frame, event, value): if len(measured) != 1: raise ValueError('Capacity must use exactly one native load measurement') config = CommanderConfig.installed() -print(json.dumps(dict(current=output.getvalue(), load=FieldCodec.encode(measured[0]), +print(json.dumps(dict(current=output.getvalue(), native=str(measured[0]), source=capacity.__file__, root=str(config.root), commander=config.commander))) """ @@ -52,7 +50,7 @@ def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: @dataclass(frozen=True) class MeasuredOutput: current: str - load: Load + native: str source: str root: str commander: str @@ -67,14 +65,14 @@ class LoadComparison(ParityComparison): @property def passed(self) -> bool: return ( - self.observation.current.strip() == str(self.observation.load) + self.observation.current.strip() == self.observation.native and self.state_before == self.state_after ) def describe(self) -> str: return ( f"current={self.observation.current.strip()!r}; " - f"native={str(self.observation.load)!r}; owner={self.observation.source}; " + f"native={self.observation.native!r}; owner={self.observation.source}; " f"commander={self.observation.commander}; root={self.observation.root}; " f"private_state_unchanged={self.state_before == self.state_after}" ) diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index 3d81ad518b..b21c3e3c8c 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -7,7 +7,7 @@ def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_path): load = Load(37, 8, 1692, ("merger",)) - observed = MeasuredOutput(str(load) + "\n", load, "capacity.py", "/bus", "lead") + observed = MeasuredOutput(str(load) + "\n", str(load), "capacity.py", "/bus", "lead") comparison = LoadComparison(observed, (), ()) assert comparison.passed for different in ( @@ -16,7 +16,7 @@ def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_ replace(load, unjudged=1693), replace(load, forks=()), ): - assert not replace(comparison, observation=replace(observed, load=different)).passed + assert not replace(comparison, observation=replace(observed, native=str(different))).passed path = tmp_path / "old-state" path.write_text("before") before = ((path.name, FileRevision.capture(path)),) From d14d55f2f362c7b311141093aab6dee1d396ee15 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:00:20 -0400 Subject: [PATCH 5/9] Observe the actual selected commander context after P701 --- src/agent_comms/commander_parity/load.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index 173be0e177..f2aeac797f 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -18,11 +18,13 @@ import contextlib, io, json, sys import agent_comms.commander.capacity as capacity from agent_comms.commander.cli import main -from agent_comms.commander.config import CommanderConfig measured = [] +contexts = [] def observe(frame, event, value): if event == 'return' and frame.f_code is capacity.Load.of.__func__.__code__: measured.append(value) + if event == 'call' and frame.f_code is capacity.main.__code__: + contexts.append(frame.f_locals['ctx']) output = io.StringIO() sys.setprofile(observe) try: @@ -30,9 +32,9 @@ def observe(frame, event, value): main() finally: sys.setprofile(None) -if len(measured) != 1: - raise ValueError('Capacity must use exactly one native load measurement') -config = CommanderConfig.installed() +if len(measured) != 1 or len(contexts) != 1: + raise ValueError('Capacity must use exactly one native measurement and selected context') +config = contexts[0] print(json.dumps(dict(current=output.getvalue(), native=str(measured[0]), source=capacity.__file__, root=str(config.root), commander=config.commander))) From f7eaedb053968eaa9deff62712985bd75196b973 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:06:57 -0400 Subject: [PATCH 6/9] Read configured commander facts through their current owner --- src/agent_comms/commander_parity/load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index f2aeac797f..d23ddc877a 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -81,7 +81,7 @@ def describe(self) -> str: @classmethod def read(cls) -> ParityComparison: - config = CommanderConfig.installed() + config = CommanderConfig.read(CommanderConfig.path()) before = state_files(config.state) # -I uses installed imports even when this candidate runs with PYTHONPATH=src. # HOME and the environment stay unchanged; the actual CLI main runs. From 7fc73ab3e134d5b4288358a40664ef8ab5b6b7a9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:14:54 -0400 Subject: [PATCH 7/9] Complete P701 integration of parity observation and checked dispatch --- src/agent_comms/commander_parity/load.py | 7 +++---- src/agent_comms/commander_parity/rows.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index d23ddc877a..7750b343e9 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -11,7 +11,6 @@ from ..private_path import FileRevision from . import ParityComparison, UnverifiedParity - # Profile the real return, never replace the measurement, command, bus or reply. # Comparing independent reads of a running fleet confuses new edits with drift. OBSERVE_CAPACITY = """ @@ -41,7 +40,7 @@ def observe(frame, event, value): """ -def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: +def state_files(directory: Path) -> tuple[tuple[str, FileRevision | None], ...]: return tuple( (str(path.relative_to(directory)), FileRevision.capture(path)) for path in sorted(directory.rglob("*")) @@ -61,8 +60,8 @@ class MeasuredOutput: @dataclass(frozen=True) class LoadComparison(ParityComparison): observation: MeasuredOutput - state_before: tuple[tuple[str, FileRevision], ...] - state_after: tuple[tuple[str, FileRevision], ...] + state_before: tuple[tuple[str, FileRevision | None], ...] + state_after: tuple[tuple[str, FileRevision | None], ...] @property def passed(self) -> bool: diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 8eb2cffc73..0c64e8c69a 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -242,7 +242,10 @@ class MeasuredLoadParityRow(ParityRow): number = 28 capability = "Measured load and self-report" current = "`capacity.py` unit count" - native = "`commander.capacity.Load.of` reports orders, overdue, edits and forks; self-report comparison unverified" + native = ( + "`commander.capacity.Load.of` reports orders, overdue, edits and forks; " + "self-report comparison unverified" + ) phases = (ControlPlanePhase,) From 4a84825e9b19ae2e766cb0a60005902d1a99f935 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:03:04 -0400 Subject: [PATCH 8/9] Require real file revisions and fail closed on observation errors --- src/agent_comms/commander_parity/load.py | 55 +++++++++++++----------- tests/test_commander_parity_load.py | 7 ++- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index 7750b343e9..91b9bdb17e 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -40,9 +40,9 @@ def observe(frame, event, value): """ -def state_files(directory: Path) -> tuple[tuple[str, FileRevision | None], ...]: +def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: return tuple( - (str(path.relative_to(directory)), FileRevision.capture(path)) + (str(path.relative_to(directory)), FileRevision.from_stat(path.stat())) for path in sorted(directory.rglob("*")) if path.is_file() ) @@ -60,8 +60,8 @@ class MeasuredOutput: @dataclass(frozen=True) class LoadComparison(ParityComparison): observation: MeasuredOutput - state_before: tuple[tuple[str, FileRevision | None], ...] - state_after: tuple[tuple[str, FileRevision | None], ...] + state_before: tuple[tuple[str, FileRevision], ...] + state_after: tuple[tuple[str, FileRevision], ...] @property def passed(self) -> bool: @@ -80,26 +80,29 @@ def describe(self) -> str: @classmethod def read(cls) -> ParityComparison: - config = CommanderConfig.read(CommanderConfig.path()) - before = state_files(config.state) - # -I uses installed imports even when this candidate runs with PYTHONPATH=src. - # HOME and the environment stay unchanged; the actual CLI main runs. - current = subprocess.run( - (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), - capture_output=True, - text=True, - timeout=30, - ) - if current.returncode: - return UnverifiedParity(current.stderr) - observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) - source = Path(observed.source) - if ( - observed.root != str(config.root) - or observed.commander != config.commander - or not any(source.is_relative_to(repo.checkout) for repo in config.repos) - ): - return UnverifiedParity( - "The installed command used a different configuration or source" + try: + config = CommanderConfig.read(CommanderConfig.path()) + before = state_files(config.state) + # -I uses installed imports even when this candidate runs with PYTHONPATH=src. + # HOME and the environment stay unchanged; the actual CLI main runs. + current = subprocess.run( + (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), + capture_output=True, + text=True, + timeout=30, ) - return cls(observed, before, state_files(config.state)) + if current.returncode: + return UnverifiedParity(current.stderr) + observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) + source = Path(observed.source) + if ( + observed.root != str(config.root) + or observed.commander != config.commander + or not any(source.is_relative_to(repo.checkout) for repo in config.repos) + ): + return UnverifiedParity( + "The installed command used a different configuration or source" + ) + return cls(observed, before, state_files(config.state)) + except (OSError, subprocess.TimeoutExpired) as error: + return UnverifiedParity(str(error)) diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index b21c3e3c8c..6e26d798fa 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -1,8 +1,7 @@ from dataclasses import replace from agent_comms.commander.capacity import Load -from agent_comms.commander_parity.load import LoadComparison, MeasuredOutput -from agent_comms.private_path import FileRevision +from agent_comms.commander_parity.load import LoadComparison, MeasuredOutput, state_files def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_path): @@ -19,7 +18,7 @@ def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_ assert not replace(comparison, observation=replace(observed, native=str(different))).passed path = tmp_path / "old-state" path.write_text("before") - before = ((path.name, FileRevision.capture(path)),) + before = state_files(tmp_path) path.write_text("after") - after = ((path.name, FileRevision.capture(path)),) + after = state_files(tmp_path) assert not LoadComparison(observed, before, after).passed From 6ee1708a57345642c7aabc018e5b64be1176f001 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:07:59 -0400 Subject: [PATCH 9/9] Surface observation failures instead of swallowing them --- src/agent_comms/commander_parity/load.py | 47 +++++++++++------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index 91b9bdb17e..66ec536cef 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -80,29 +80,26 @@ def describe(self) -> str: @classmethod def read(cls) -> ParityComparison: - try: - config = CommanderConfig.read(CommanderConfig.path()) - before = state_files(config.state) - # -I uses installed imports even when this candidate runs with PYTHONPATH=src. - # HOME and the environment stay unchanged; the actual CLI main runs. - current = subprocess.run( - (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), - capture_output=True, - text=True, - timeout=30, + config = CommanderConfig.read(CommanderConfig.path()) + before = state_files(config.state) + # -I uses installed imports even when this candidate runs with PYTHONPATH=src. + # HOME and the environment stay unchanged; the actual CLI main runs. + current = subprocess.run( + (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), + capture_output=True, + text=True, + timeout=30, + ) + if current.returncode: + return UnverifiedParity(current.stderr) + observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) + source = Path(observed.source) + if ( + observed.root != str(config.root) + or observed.commander != config.commander + or not any(source.is_relative_to(repo.checkout) for repo in config.repos) + ): + return UnverifiedParity( + "The installed command used a different configuration or source" ) - if current.returncode: - return UnverifiedParity(current.stderr) - observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) - source = Path(observed.source) - if ( - observed.root != str(config.root) - or observed.commander != config.commander - or not any(source.is_relative_to(repo.checkout) for repo in config.repos) - ): - return UnverifiedParity( - "The installed command used a different configuration or source" - ) - return cls(observed, before, state_files(config.state)) - except (OSError, subprocess.TimeoutExpired) as error: - return UnverifiedParity(str(error)) + return cls(observed, before, state_files(config.state))