From f178a1c60380536a7ca98026dcf364c7bfcbe216 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 12:42:30 -0400 Subject: [PATCH 01/42] P701: bind commander operations to an explicit per-command context --- src/agent_comms/cli_commands.py | 4 +- src/agent_comms/commander/capacity.py | 24 +- src/agent_comms/commander/cli.py | 16 +- src/agent_comms/commander/config.py | 38 +- src/agent_comms/commander/judge.py | 45 +- src/agent_comms/commander/order.py | 1176 ++++++++++++----------- src/agent_comms/commander/request.py | 18 +- src/agent_comms/commander/rotate.py | 19 +- src/agent_comms/commander/rules.py | 37 +- src/agent_comms/commander/send_guard.py | 14 +- src/agent_comms/commander/standing.py | 8 +- src/agent_comms/commander/tool_watch.py | 24 +- 12 files changed, 714 insertions(+), 709 deletions(-) diff --git a/src/agent_comms/cli_commands.py b/src/agent_comms/cli_commands.py index 91961873bf..66d69a1290 100644 --- a/src/agent_comms/cli_commands.py +++ b/src/agent_comms/cli_commands.py @@ -764,7 +764,7 @@ def add_parser(cls, subparsers: Any) -> None: parser = subparsers.add_parser(FieldCodec.encode(cls), help=cls.help) groups: dict[str, Any] = {} hints = get_type_hints(cls, include_extras=True) - for declared in fields(cls): + for declared, _ in FieldCodec.record_fields(cls): option = CommandOption.of(declared) group = option.group owner = parser @@ -801,7 +801,7 @@ def add_parser(cls, subparsers: Any) -> None: def from_namespace(cls, args: argparse.Namespace) -> Self: payload: dict[str, object] = {"kind": FieldCodec.encode(cls)} hints = get_type_hints(cls, include_extras=True) - for declared in fields(cls): + for declared, _ in FieldCodec.record_fields(cls): key = FieldCodec.wire_key(declared) value = getattr(args, key) normalize = CommandOption.of(declared).normalize diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index d62875f89b..095e6f6016 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -13,23 +13,21 @@ from dataclasses import dataclass from ..comms import Comms -from . import order from .config import CommanderConfig -FORKS = f'{CommanderConfig.installed().state}/forks' PER_COMMANDER = 6 BEAT_AGE = 10 * 60 -def live_forks() -> list[str]: - os.makedirs(FORKS, exist_ok=True) - return [n[:-5] for n in os.listdir(FORKS) if n.endswith('.beat') and time.time() - os.path.getmtime(f'{FORKS}/{n}') < BEAT_AGE] +def live_forks(forks) -> list[str]: + os.makedirs(forks, exist_ok=True) + return [n[:-5] for n in os.listdir(forks) if n.endswith('.beat') and time.time() - os.path.getmtime(f'{forks}/{n}') < BEAT_AGE] -def beat(name: str): - os.makedirs(FORKS, exist_ok=True) - with open(f'{FORKS}/{name}.beat', 'a'): - os.utime(f'{FORKS}/{name}.beat') +def beat(forks, name: str): + os.makedirs(forks, exist_ok=True) + with open(f'{forks}/{name}.beat', 'a'): + os.utime(f'{forks}/{name}.beat') @dataclass(frozen=True) @@ -41,12 +39,12 @@ class Load: forks: tuple[str, ...] @classmethod - def of(cls, comms: Comms, commander: str) -> Load: + def of(cls, comms: Comms, commander: str, forks) -> Load: from ..tools import CommsUnjudgedTool actor, now = comms.registry.require(commander).incarnation, time.time() owed = [t for t in comms.goals.todos.held_for_commander(actor) if t.pending] return cls(len(owed), sum(t.overdue(now) for t in owed), - CommsUnjudgedTool.count(comms, commander), tuple(live_forks())) + CommsUnjudgedTool.count(comms, commander), tuple(live_forks(forks))) @property def total(self) -> float: @@ -71,6 +69,6 @@ def require(self) -> None: sys.exit(f'REFUSED, nothing recorded: overloaded: {self}.') -def main(a): - load = Load.of(order.COMMS, order.ME) +def main(a, commander): + load = Load.of(commander.comms, commander.commander, commander.state / 'forks') print(load if load.short else f'covered: load {load.total:.0f}, forks {list(load.forks)}') diff --git a/src/agent_comms/commander/cli.py b/src/agent_comms/commander/cli.py index 231b993aa7..f5cd13f198 100644 --- a/src/agent_comms/commander/cli.py +++ b/src/agent_comms/commander/cli.py @@ -1,10 +1,14 @@ """`agent-comms-commander TOOL ARGS...`: each module in this package that declares `main` is one tool.""" +import argparse import pkgutil import sys from importlib import import_module import agent_comms.commander as package +from .config import CommanderConfig +from .order import Commander + def tool_name(module: str) -> str: """The tool a module of this package is: its name, with dashes.""" @@ -12,8 +16,16 @@ def tool_name(module: str) -> str: def main() -> None: + parser = argparse.ArgumentParser(add_help=False) + selection = parser.add_mutually_exclusive_group() + selection.add_argument('--config', dest='config', type=CommanderConfig.read, + default=str(CommanderConfig.path()), metavar='JSON') + selection.add_argument('--root', dest='config', type=CommanderConfig.read_root, + default=argparse.SUPPRESS, metavar='ROOT') + options, argv = parser.parse_known_args(sys.argv[1:]) + commander = Commander.of(options.config) modules = (import_module(f'{package.__name__}.{m.name}') for m in pkgutil.iter_modules(package.__path__) if m.name != 'cli') tools = {tool_name(module.__name__): module for module in modules if hasattr(module, 'main')} - if len(sys.argv) < 2 or sys.argv[1] not in tools: + if not argv or argv[0] not in tools: sys.exit(f'usage: agent-comms-commander {{{",".join(sorted(tools))}}} ARGS...') - tools[sys.argv[1]].main(sys.argv[2:]) + tools[argv[0]].main(argv[1:], commander) diff --git a/src/agent_comms/commander/config.py b/src/agent_comms/commander/config.py index bbf24d0436..5562479bd1 100644 --- a/src/agent_comms/commander/config.py +++ b/src/agent_comms/commander/config.py @@ -6,8 +6,6 @@ from __future__ import annotations import subprocess -from dataclasses import dataclass -from functools import cache from importlib.resources import files from pathlib import Path @@ -75,34 +73,28 @@ def packaged_rules() -> Path: return Path(str(files('agent_comms.commander') / 'rules')) @classmethod - @cache - def installed(cls) -> CommanderConfig: - return cls.model_validate_json(cls.path().read_text()) + def read(cls, path: Path | str) -> CommanderConfig: + return cls.model_validate_json(Path(path).read_text()) + + @staticmethod + def root_path(root: Path) -> Path: + return root / 'commander.json' + + @classmethod + def read_root(cls, root: Path | str) -> CommanderConfig: + root = Path(root).resolve() + return cls.read(cls.root_path(root)).model_copy(update={'root': root}) def write(self, home: Path) -> Path: """Record the config and start each state file empty, so every tool reads a real file.""" self.require_commander(Comms(self.root).registry.require(self.commander)) path = self.path(home) path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) - path.write_text(self.model_dump_json(indent=1) + '\n') + record = self.root_path(self.root) + record.write_text(self.model_dump_json(indent=1) + '\n') + path.unlink(missing_ok=True) + path.symlink_to(record) for directory in (self.state, self.state / 'forks', self.state / 'orders', self.worktrees, self.scratch, self.board.parent): directory.mkdir(parents=True, exist_ok=True) return path - - -@dataclass(frozen=True) -class Commander: - """This machine's commander thread and the bus whose Todo store owns its problems and orders.""" - - name: str - comms: Comms - - @classmethod - def of(cls, config: CommanderConfig) -> Commander: - return cls(config.commander, Comms(config.root)) - - def units(self) -> frozenset[str]: - """The threads this commander gives orders to; the owner, claude-ai and peer commanders - are not among them.""" - return self.comms.goals.todos.units(self.comms.registry.require(self.name).incarnation) diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index e39dba2f5d..bc63b4ed5f 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -30,9 +30,6 @@ from .order import CommanderVerb, RuledVerb from .config import CommanderConfig -CONFIG = CommanderConfig.installed() -ME = CONFIG.commander - BRANCH = re.compile(r'\bisinstance\(|\bhasattr\(|^\s*(if|elif|match|case)\b|\bif\b.*\belse\b') @@ -80,12 +77,12 @@ def rendered(self) -> str: + f'\n=== end {self.id} code {self.code()}') -def show(pending: list[Edit]) -> None: +def show(commander, pending: list[Edit]) -> None: by_thread: dict[str, list[Edit]] = {} for edit in pending: by_thread.setdefault(edit.thread, []).append(edit) for name, edits in by_thread.items(): - orders = order.held_by(name) + orders = commander.held_by(name) intent = '\n'.join(todo.order.instructions(todo) for todo in orders) or 'NO OPEN ORDER TODO' print(f'===== {name}: {len(edits)} unjudged\nCURRENT ORDERS:\n{intent}\n') for edit in edits: @@ -93,7 +90,7 @@ def show(pending: list[Edit]) -> None: print(f'{len(pending)} unjudged') -def roast(audience, thread, lines, text, fix, slop): +def roast(commander, audience, thread, lines, text, fix, slop): """Every send-back is a lesson, sent to its verdict's audience: the quoted slop, the required fact it restates and the declaration that derives it (paper 1's definition), why that is stupid, and what replaces it.""" @@ -101,12 +98,12 @@ def roast(audience, thread, lines, text, fix, slop): + '\n'.join(f' {l.strip()}' for l in lines) + f'\nThe slop: {slop.render()}' + f'\nWhy it is stupid: {text}\nWhat replaces it: {fix}\nNobody writes this again.') - print(f'roast sent to {", ".join(audience.threads)}: ' + order.post(audience, body)) + print(f'roast sent to {", ".join(audience.threads)}: ' + commander.post(audience, body)) -def alarm(): +def alarm(commander): while True: - late = [edit for edit in Edit.unjudged(order.COMMS, ME) if time.time() - edit.at > 300] + late = [edit for edit in Edit.unjudged(commander.comms, commander.commander) if time.time() - edit.at > 300] if late: counts: dict[str, int] = {} for edit in late: @@ -130,14 +127,14 @@ def selects(self, edit: Edit) -> bool: return not self.thread or edit.thread == self.thread def run(self) -> None: - show([edit for edit in Edit.unjudged(order.COMMS, ME) if self.selects(edit)]) + show(self.context, [edit for edit in Edit.unjudged(self.context.comms, self.context.commander) if self.selects(edit)]) class AlarmVerb(JudgeVerb): help = "loop: every minute, name edits unjudged for 5 min" def run(self) -> None: - alarm() + alarm(self.context) @dataclass(frozen=True, kw_only=True) @@ -162,8 +159,8 @@ def report(self, todos: tuple[Todo, ...]) -> None: ... def run(self) -> None: if len(self.why.split()) < 6: sys.exit('Refused: state which intent the edit serves or violates, and why (6+ words).') - orders = {todo.id: todo for todo in order.held_orders() if not self.msg or todo.sent_in(self.msg)} - edits = {edit.id: edit for edit in Edit.unjudged(order.COMMS, ME) if edit.problem in orders} + orders = {todo.id: todo for todo in self.context.held_orders() if not self.msg or todo.sent_in(self.msg)} + edits = {edit.id: edit for edit in Edit.unjudged(self.context.comms, self.context.commander) if edit.problem in orders} ids = [token.split(':')[0] for token in self.ids] if missing := [i for i in ids if i not in edits]: sys.exit(f'Refused: unknown edit ids {missing}') @@ -177,12 +174,12 @@ def run(self) -> None: self.check_edits([edits[i] for i in ids]) # Validate the whole batch through the operation owner before its first write. for i, todo in selected.items(): - self.operation(todo, i).prepare(order.COMMS.goals, todo).apply(todo) + self.operation(todo, i).prepare(self.context.comms.goals, todo).apply(todo) current = {todo.id: todo for todo in selected.values()} for i, todo in selected.items(): - result = order.call(CommsVerdictTool(id=todo.id, operation=self.operation(current[todo.id], i))) + result = self.context.call(CommsVerdictTool(id=todo.id, operation=self.operation(current[todo.id], i))) current[todo.id] = result.todo - left = len(Edit.unjudged(order.COMMS, ME)) + left = len(Edit.unjudged(self.context.comms, self.context.commander)) print(f'{self.declared_name}: {len(ids)} edits; {left} still unjudged') self.report(tuple(current.values())) @@ -195,7 +192,7 @@ class OkVerb(VerdictVerb): peer: str = option("--peer", default="", help="another sender's retained order message to the unit") def operation(self, todo: Todo, edit_id: str) -> AcceptedVerdict: - actor = order.COMMS.registry.require(ME).incarnation + actor = self.context.comms.registry.require(self.context.commander).incarnation if self.peer: return PeerAcceptedVerdict(todo.revision, actor, todo.held(), edit_id, quote=self.serves, peer=self.peer) @@ -219,7 +216,7 @@ class SupersededVerb(VerdictVerb): by: str = option("--by", help="the replacing order's message") def operation(self, todo: Todo, edit_id: str) -> SupersededVerdict: - return SupersededVerdict(todo.revision, order.COMMS.registry.require(ME).incarnation, + return SupersededVerdict(todo.revision, self.context.comms.registry.require(self.context.commander).incarnation, todo.held(), edit_id, replacement=self.by, reason=self.why) def check_edits(self, edits: list[Edit]) -> None: @@ -246,7 +243,7 @@ def slop(self) -> Slop: at=self.at, derive=self.derive, by=self.by)) def operation(self, todo: Todo, edit_id: str) -> SendBackVerdict: - return SendBackVerdict(todo.revision, order.COMMS.registry.require(ME).incarnation, + return SendBackVerdict(todo.revision, self.context.comms.registry.require(self.context.commander).incarnation, todo.held(), edit_id, reason=StrikeReason(f'{todo.id} edit sent back: {self.why}')) @@ -262,12 +259,12 @@ def check_edits(self, edits: list[Edit]) -> None: def report(self, todos: tuple[Todo, ...]) -> None: for todo in todos: audience = todo.verdicts[-1].audience.of(todo.held().owner, todo.goal) - roast(audience, todo.holder, self.lines, self.roast, self.fix, self.slop()) + roast(self.context, audience, todo.holder, self.lines, self.roast, self.fix, self.slop()) line = (f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ' - f'(strike {todo.strike_count} on {todo.id}; caught by {ME} at review): ' + f'(strike {todo.strike_count} on {todo.id}; caught by {self.context.commander} at review): ' f'{todo.strikes[-1].reason}') - order.on_record(line, 'Send-back', audience) + self.context.on_record(line, 'Send-back', audience) -def main(args): - JudgeVerb.parse(args or ['show'], __doc__).apply() +def main(args, commander): + JudgeVerb.parse(args or ['show'], __doc__).apply(commander) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 10e7e7fdd7..f1a5da9e5c 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -17,13 +17,16 @@ Nothing here judges code; it refuses orders that leave a unit room to interpret, and refuses to let a deadline lapse silently. """ +from __future__ import annotations + import argparse, asyncio, json, os, re, subprocess, sys, time from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from uuid import uuid4 from ..change_report import FileLine +from ..comms import Comms from ..child_process import BoundedRun from ..cli_commands import DeclaredOptions, option from ..declared_family import DeclaredFamily @@ -46,17 +49,9 @@ OrderDeletion, StrikeReason, StrikeTodo, VoidStrikeTodo, LocatedProblemFinding, UnlocatedProblemFinding, FileDeliveryEvidence, NoDeliveryEvidence, WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop) from . import capacity -from .config import Commander, CommanderConfig, CommanderRole, Repo - -CONFIG = CommanderConfig.installed() -COMMANDER = Commander.of(CONFIG) -ME = COMMANDER.name -COMMS = COMMANDER.comms -RTS = str(CONFIG.state) -BOARD = str(CONFIG.board) -NARRATIVE = str(CONFIG.narrative) # public record of every failure, in plain words -B = str(CONFIG.root) -CLI = str(CONFIG.cli) +from .config import CommanderConfig, CommanderRole, Repo +from pydantic import PrivateAttr + OUTGOING = DirectiveText.OUTGOING # every commander text passes each one def refuse(text, *families): @@ -65,46 +60,6 @@ def refuse(text, *families): sys.exit('REFUSED, nothing recorded: ' + ' '.join(refused)) -def post(audience: VerdictAudience, body: str) -> str: - """Send one message its caller refused with OUTGOING; judge reads it from bus history. - - One thread gets a DM; a larger audience is mentioned on #comms, which wakes no one else. - """ - target = audience.threads[0] if len(audience.threads) == 1 else '#comms' - return call(CommsSendTool(sender=ME, target=target, body=body, - audience=audience.threads)).id - - -def call(tool): - """Every order write is one Core tool request made as the commander's thread.""" - return ThreadCaller(ME).invoke(tool, COMMS) - - -def todos(): - """The problems this commander created; Core's Todo store is their one owner.""" - return COMMS.goals.todos.for_commander(COMMS.registry.require(ME).incarnation) - - -def held_orders() -> tuple[Todo, ...]: - """This commander's open orders that a unit holds.""" - return COMMS.goals.todos.held_for_commander(COMMS.registry.require(ME).incarnation) - - -def held_by(unit: str) -> tuple[Todo, ...]: - """This commander's open orders held by one unit.""" - return COMMS.goals.todos.held_by(COMMS.registry.require(ME).incarnation, unit) - - -def units() -> frozenset[str]: - return COMMANDER.units() - - -def progress() -> dict[str, OrderProgress]: - """Each held order's progress, derived by Core's Todo list from the unit's checkout and PRs.""" - listed: TodoList = call(CommsTodoListTool()) - return listed.progress - - def struck(todo, unit): """Strikes count per unit and task, across every order of that task the unit held.""" return len(todo.live_strikes(unit)) @@ -177,399 +132,15 @@ def forwarders_added(diff, sources=()): if m.group(2) not in kept and not state_call.search(diff[m.start():m.end() + 200].split('\n+', 2)[1] if '\n+' in diff[m.start():m.end()+200] else '')]) -def send(unit, due, body: str, clean=False, mechanical=False, reassign=None, new=None, - traced=None, replaces=None, disk=False, finding=UnlocatedProblemFinding()): - """Validate an order body and record it in Core: a new problem (new=(PID, TEXT)) or the listed open problems. - - Sending to a unit that already holds the problem replaces its order there; without --clean that is a send-back.""" - problems = [] - # Below the floor Core's StorageRule denies each allocating tool call, so the order prose is not checked. - try: - COMMS.registry.storage_rule.require_quota(COMMS.registry.require(unit)) - except SelectedToolDenied as quota: - problems.append(f'{unit}: {quota}: it cleans up its own checkouts and scratch before it gets new work') - Todo.require_order_body(body) - if not clean and (timid := BluntText.refused(body)): # blunt at all times unless nothing was incompetent - problems.append(timid) - works = re.search(rf'(?m)^{re.escape(OrderRun.label)}\s*(.+)$', body) - run = MechanicalOrderRun() if mechanical else ApplicationOrderRun(works[1] if works else '') - serves = re.search(r'(?m)^Serves:\s*(\S+)\s*$', body) - phase = OrderPhase.member_named(serves[1] if serves else UnplannedPhase.declared_name) - edits = tuple(re.findall(r'(?m)^\s*\d+[.)] (.+?)\s*$', body)) - if not edits: - problems.append('no numbered edits: list each exact edit as "1. ...", "2. ..."') - report = re.search(r'(?m)^Report:\s*(\S.*?)\s*$', body) - if not report: - problems.append('no Report: line: say exactly what the unit sends back (sha, counts before -> after)') - banned = tuple(item.strip() for line in re.findall(r'(?m)^Banned:(.*)$', body) for item in line.split(';') if item.strip()) - if not banned: - problems.append('no Banned: line: name the slop shapes this task invites, separated by ";"') - if not re.fullmatch(r'\d{1,2}:\d{2}', due): - problems.append('due must be HH:MM local time') - fact = re.search(r'(?m)^Problems?:\s*(P\d+(?:\s*,\s*P\d+)*)\b', body) - group = [new[0]] if new else re.findall(r'P\d+', fact[1]) if fact else [] - owner = re.search(r'(?m)^Owner:\s*(\S+):(\d+)\s*$', body) - check = (line := re.search(r'(?m)^Check:(.*)$', body)) and OrderCheck.parse(line[1]) - listed = {t.id: t for t in todos()} - if not group: - problems.append('no Problem: P line; record it first with `order problem` (every order owns a listed problem)') - elif not new and (missing := [pid for pid in group if pid not in listed]): - problems.append(f'{missing} not open problems') - if not owner: - problems.append('no Owner: path:line of the existing owner the fix belongs on') - elif not os.path.exists(owner[1]) or int(owner[2]) > sum(1 for _ in open(owner[1])): - problems.append(f'Owner {owner[1]}:{owner[2]} does not exist; read the code and name the real owner') - if not check: - problems.append('no Check: :: -> N; done runs it, so make it the real acceptance count') - elif not os.path.isdir(check.directory): - problems.append(f'Check repo {check.directory} is not a directory') - # A check that can never print its target, or a premise naming lines that do something else, wastes the order. - # An order's check must fail now (or it proves nothing), and every absolute - # path:line it cites must exist. - if check and os.path.isdir(check.directory) and not problems: - try: - ran = subprocess.run(['bash', '-c', check.command], cwd=check.directory, capture_output=True, timeout=90) - printed = CheckPrinted.decoded(ran.stdout) - problems += printed.unproven(check.command, check.result.expected) - except subprocess.TimeoutExpired: - problems.append('Check took over 90 s; done runs it too, so make it a focused count') - for path, line in re.findall(r'(? sum(1 for _ in open(path, errors='replace')): - problems.append(f'cited {path}:{line} does not exist; read the code before citing it') - # A module deleted here can still be imported by another repo: - # P12 deleted acp_ingress.py, which Toad imports. - deleted = {stem for edit in edits - for stem in re.findall(r'(?i)\bdelete\s+`?(?:\S*/)?(\w+)\.py\b', edit)} - if check and deleted and (unread := importers(deleted, check)): - problems.append(f'the order deletes {sorted(deleted)}, which {unread} import; ' - 'the check must also count their `from agent_comms` imports') - if any(re.search(r'(?"') - if reassign is not None and len(reassign.split()) < 8: - problems.append('--reassign needs the concrete failure that forces replacement (8+ words), told to the old and new owner') - if problems: - sys.exit('Refused order:\n ' + '\n '.join(problems)) - message = (f'{body.rstrip()}' + ('\nThis replaces my earlier order on it; drop that one.' if replacing else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + COMMS.registry.storage_rule.order_text(unit, [f'{repo.checkout}/.venv/bin/python' for repo in CONFIG.repos]) + '\nIf two lines of this order conflict, take the narrower reading, say which one you took, and proceed; do not stop to ask.' - + '\nStart now and push as you go.') # a unit shown a due time takes until then - taken = {other: f'Taken off you: {", ".join(group)} ("{listed[group[0]].text[:120]}") now belongs to {unit} because: {reassign}. Stop that work; do not touch its files.' for other in holders} - refuse('\n'.join((message, *taken.values())), *OUTGOING) # before Core: refused records nothing - fired = [pid for pid in group if pid in listed and listed[pid].struck_out(unit)] - if not mechanical and fired: - sys.exit(f'Refused: {unit} has {ProblemGoalOrder.StrikeLimit} strikes on {fired}. ' - f'Reassign them; only --mechanical orders on them reach {unit}.') - # A miss already struck on the order this replaces is one miss: - # the send-back records no second strike. - struck_already = replacing and listed[group[0]].struck_on_order(unit) - at = time.time() - order = PendingProblemOrder(finding=finding, - id=uuid4().hex[:12], owner=FileLine(owner[1], int(owner[2])), - check=check, edits=edits, banned=banned, - report=report[1], run=run, serves=phase, - deletes=OrderDeletion.parse(deletes[1] if (deletes := re.search( - rf'(?m)^{re.escape(OrderDeletion.label)}\s*(.+)$', body)) else None), - at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) - common = dict(order=order, request=order.request(COMMS.registry.require(ME), tuple(group)), deadline=deadline(due), commander=ME) - if new: - call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction( - problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], **common))) - for pid in [] if new else group: - call(CommsOrderTool(owner=unit, order=OrderGoalAction( - problem=pid, expected_revision=COMMS.goals.todos.get(pid).revision, traced=trace, **common))) - message += "\n" + "\n".join(order.pull_request_instruction(pid) for pid in group) - sent = post(VerdictAudience((unit,)), message) - # The message is each order's recorded link; a verdict citing it lands on that order. - for pid in group: - call(CommsTodoTool(id=pid, operation=OrderMessage( - COMMS.goals.todos.get(pid).revision, COMMS.registry.require(ME).incarnation, sent))) - for other, text in taken.items(): - post(VerdictAudience((other,)), text) - print(order.id) - if replacing and not clean and struck_already: - print(f'{unit} already struck on this order of {group[0]}; ' - 'the send-back counts that one miss once') - elif replacing and not clean and not reassign: - # A send-back is a strike, no warnings, and every send-back is roasted in #comms. - # Done here so the commander cannot send work back and forget either. - verdict = next(l for l in body.splitlines() if BluntText.found(l)).strip() - strike(unit, group[0], f'sent back on {group[0]}: {verdict}') - - -def importers(modules: set[str], check: OrderCheck) -> list[str]: - """The other configured repos whose `from agent_comms` imports name a module, unless the - check reads them.""" - pattern = rf'^\s*(from|import) agent_comms\b.*\b({"|".join(sorted(modules))})\b' - return [str(repo.checkout) for repo in CONFIG.repos - if repo.checkout != Path(check.directory).resolve() - and str(repo.checkout) not in check.command - and subprocess.run(['grep', '-rqE', '--include=*.py', pattern, - str(repo.checkout / repo.source)]).returncode == 0] - - -def correct_check(pid: str, line: str, why: str) -> None: - """Core replaces a wrong check of an open order. The new check must already pass on main, so a - correction fixes the commander's check and never passes undelivered work: done still runs it on - the --sha it is given.""" - todo = COMMS.goals.todos.get(pid) - if not todo.state.open or not todo.pending: - sys.exit(f'Refused: {pid} has no pending order') - if not (check := OrderCheck.parse(line)): - sys.exit('Refused: --check reads " :: -> "') - if not os.path.isdir(check.directory): - sys.exit(f'Refused: check repo {check.directory} is not a directory') - if len(why.split()) < 8: - sys.exit('Refused: --why says what was wrong with the recorded check (8+ words)') - def git(*args: str) -> str: - return subprocess.run(['git', '-C', check.directory, *args], - capture_output=True, text=True).stdout.strip() - head = git('rev-parse', 'HEAD') - if head != git('rev-parse', 'origin/main') or git('status', '--porcelain', '-uno'): - sys.exit(f'Refused: {check.directory} is not a clean checkout of origin/main; ' - 'the corrected check runs on main') - check.result.require(asyncio.run(BoundedRun.run( - ('bash', '-c', check.command), cwd=check.directory, timeout=90))) - call(CommsOrderCheckTool(problem=pid, check=check, why=why)) - print(f'{pid}: order {todo.order.id} now checks `{check.command}`') - - -def say(units, body_file): - body = open(body_file).read() - # Numbered edits or a report line are an order: it needs a deadline, so it goes through send. - if len(re.findall(r'(?m)^\s*\d+[.)] ', body)) >= 2 or re.search(r'(?mi)^\s*(report|due)\b.*:', body): - sys.exit('Refused: a numbered edit list or a Report/Due line is an order. Use `order send` so it gets a tracked deadline.') - refuse(body, *OUTGOING) - Slop.from_order(body) - print(post(VerdictAudience(tuple(units.split(','))), body.rstrip())) - - -def problem(text, unit, due, order_file, where, disk=False): - """A problem is born owned: Core records it with its order in one step, or not at all.""" - location = FileLine.from_wire(where) - location.require_exists() - body = open(order_file).read() - if 'Problem: NEW' not in body: - sys.exit('Refused: the order file needs a "Problem: NEW" line; a problem is never logged without its order') - if len(text.split()) < 8: - sys.exit('Refused: describe the problem (8+ words): what is wrong, what it costs') - history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) - pid = f'P{max([int(t.id[1:]) for t in history if t.id[1:].isdigit()] + [0]) + 1}' - send(unit, due, body.replace('Problem: NEW', f'Problem: {pid}'), new=(pid, text), - disk=disk, finding=LocatedProblemFinding(location)) - board() - print(pid) - - -def close(pid, works, artifact=''): - """Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo.""" - evidence = artifact or COMMS.goals.todos.get(pid).order.closing_artifact() - ThreadCaller(ME).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=evidence), COMMS) - board() - print(f'{pid} closed') - - -def last_activity(unit): - return ExternalTranscript(COMMS.root, COMMS.registry.require(unit).incarnation).last_tool_call() - - -def board(): - lines = ['# Fleet board', '', - "GENERATED by agent-comms-commander order from Core's Todo store. Do not edit by hand:", - '`order problem` adds a row, `order send ... Problem: P` orders it, `order close` or `order withdraw` removes it.', '', - '| Problem | Owner file | Owner | Order | Due | Progress | Delivered ' - '| Owner last acted |', - '|---|---|---|---|---|---|---|---|'] - listed, read = todos(), progress() - activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} - for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): - unit, last = t.holder, t.released[-1:] - owner = unit or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}' - lines.append(f'| {t.id}: {t.text} | {t.order.owner.to_wire()} | {owner} | {t.order.id} | ' - f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | ' - f'{"no" if t.pending else "yes, install pending"} | ' - f'{activity[unit].clock() if unit else "-"} |') - history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) - lines += ['', '## Withdrawn orders', '', - *(f'- {line}' for t in history for line in t.order.withdrawals(t))] - open(BOARD, 'w').write('\n'.join(lines) + '\n') - - STAGES = ('tool call', 'review', 'install', 'live', 'user') # where a mistake was caught; later is worse -def on_record(line: str, recorded: str, audience: VerdictAudience) -> None: - """Put one line on the public record: the narrative file, then its audience. - - Core already holds what `recorded` names, so a failed post is reposted, never recorded again. - """ - new_file = not os.path.exists(NARRATIVE) - with open(NARRATIVE, 'a') as f: - if new_file: - f.write('# What the fleet actually did\n\nEvery failure, in plain words, as it happened. ' - 'Written by agent-comms-commander order; never edited.\n\n') - f.write(f'- {line}\n') - try: - post(audience, f'On the record: {line}') - except (SystemExit, ValueError) as failed: - print(f'{recorded} recorded, but the post failed ({failed}). ' - 'Repost with order.post; do not record it again.', file=sys.stderr) - raise - - -def strike(unit: str, task: str, why: str, caught: str = f'{ME} at review', *, - kind: type[StrikeTodo] = StrikeTodo) -> None: - """HARD RULE: every failure goes on the public record, in plain words, damning and exact. - - Core records the strike on the task's Todo; the strike is written to the narrative file and posted to - #comms in the same step, so no failure stays private and none is described in words that hide what happened. - """ - # What matters is how a mistake happened, how often, and how and when it was caught. - if not any(stage in caught for stage in STAGES): - sys.exit(f'Refused: --caught must say who caught it and at which stage {STAGES}, e.g. "{ME} at review".') - todo = COMMS.goals.todos.get(task) - todo.state.require_open(todo) - if todo.held().owner != unit: - sys.exit(f'Refused: {unit} does not hold {task}; strike the unit that holds it.') - refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the strike - recorded = call(CommsTodoTool(id=task, operation=kind( - todo.revision, COMMS.registry.require(ME).incarnation, todo.held(), f'{why} (caught by {caught})'))) - if recorded.todo.revision == todo.revision: - return - n = struck(COMMS.goals.todos.get(task), unit) - line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike {n} on {task}{f", {task} taken away" if n >= 2 else ""}; ' - f'caught by {caught}): {why}') - on_record(line, 'Strike', VerdictAudience.of(unit, todo.goal)) - print(f'{unit}: {n} strike(s) on {task}' + (f'. Reassign {task} now and freeze its branch.' if n >= 2 else '')) - - def revision(repo: str, ref: str) -> str: """Resolve a supplied Git revision once at the command boundary.""" return subprocess.check_output( ['git', '-C', repo, 'rev-parse', '--verify', f'{ref}^{{commit}}'], text=True).strip() -def done(pid, sha, screenshot='', seen='', repo=''): - """Core runs the delivery's check at its checked commit; a late delivered commit is a strike.""" - todo = COMMS.goals.todos.get(pid) - if not todo.state.open or not todo.pending: - sys.exit(f'Refused: {pid} has no pending order') - unit = todo.assignment.owner - if repo and Path(repo).resolve() != Path(todo.order.check.directory).resolve(): - sys.exit('Refused: --repo must name the recorded order check directory.') - repo = todo.order.check.directory - operation = sha == 'op' # an operation on live state, not a commit: the check runs now and now is delivery - if not operation: - diff = subprocess.run(['git', '-C', repo, 'show', '--first-parent', '-m', '--format=', sha], capture_output=True, text=True).stdout - changed = re.findall(r'^\+\+\+ b/(\S+\.py)$', diff, re.M) - after = [subprocess.run(['git', '-C', repo, 'show', f'{sha}:{f}'], capture_output=True, text=True).stdout for f in changed] - if added := forwarders_added(diff, after): - sys.exit(f'Refused: {sha} adds forwarders {added[:5]}; slop never counts as delivery. Order the hierarchy fix.') - thread = COMMS.registry.require(unit) - if left := [str(t) for t in COMMS.registry.storage_rule.checkouts_of(thread) if pid.lower() in t.name.lower()]: - sys.exit(f'Refused: {unit} still has the checkout {left} for this order; storage rule: it removes it before the order counts as done.') - try: - COMMS.registry.storage_rule.require_quota(thread) - except SelectedToolDenied as quota: - sys.exit(f'Refused: {unit}: {quota}; storage rule: it cleans up before the order counts as done.') - todo.order.run.require_artifact(seen or screenshot) - evidence = FileDeliveryEvidence(seen or screenshot) if seen or screenshot else NoDeliveryEvidence() - if operation: - delivery = OperationDelivery(evidence=evidence) - claim = WorkingClaim(evidence.artifact()) - else: - delivered_commit = revision(repo, sha) - if subprocess.run(['git', '-C', repo, 'merge-base', '--is-ancestor', - delivered_commit, 'HEAD']).returncode: - sys.exit(f'Refused: {delivered_commit} is not in {repo} HEAD.') - delivery = CommitDelivery(commit=revision(repo, 'HEAD'), - delivered_commit=delivered_commit, evidence=evidence) - claim = MergedClaim(delivery.delivered) - call(CommsOrderDoneTool(problem=pid, order_id=todo.order.id, delivery=delivery, claims=(claim,))) - lateness = COMMS.goals.todos.get(pid).order.delivery.at.lateness(todo.deadline) - # An op has no commit time: recording it later is my delay, so lateness is judged at the OVERDUE alarm. - if not operation and lateness.late(): - strike(unit, pid, f'{pid} order {todo.order.id} {lateness} as {delivered_commit}') - return - print(f'{unit}: order {todo.order.id} met as {sha}') - - -def first_calls(held, activity): - missing = [] - for todo in held: - unit, order = todo.assignment.owner, todo.order - if not todo.pending: - continue - acted = activity[unit].since(order.at.recorded_at("An order")) - if not acted and time.time() < order.first_call_deadline.at: - continue - event = ObservedFirstCallEvent if acted else NotifiedFirstCallEvent - call(CommsTodoTool(id=todo.id, operation=RecordFirstCallTodo( - todo.revision, COMMS.registry.require(ME).incarnation, event))) - if not acted and COMMS.goals.todos.get(todo.id).revision != todo.revision: - missing.append(f'{unit} (order {order.id}, sent {hhmm(order.at)})') - return missing - - -HUNT_ROOTS = [(str(repo.checkout), repo.source) for repo in CONFIG.repos] - - -def hunt(where, why, sent): - """Record the commander's source read, whether or not it became an order.""" - path, _, line = where.rpartition(':') - if not os.path.exists(path) or not line.isdigit() or int(line) > sum(1 for _ in open(path)): - sys.exit(f'Refused: {where} is not a real file:line') - if len(why.split()) < 10: - sys.exit('Refused: say what the code does per event or per history row, and the fix or why it is clean (10+ words)') - ref, at = FileLine(os.path.realpath(path), int(line)), RecordedEventTime(time.time()) - COMMS.goals.todos.record_hunt(COMMS.registry.require(ME).incarnation, - OrderedHunt(ref, why, at, sent) if sent else CleanHunt(ref, why, at)) - print(f'hunt recorded: {where} -> {sent or "clean"}') - - -def hunts(): - return COMMS.goals.todos.hunts_for_commander(COMMS.registry.require(ME).incarnation) - - -def next_hunt(): - """Least-recently hunted file among the most-churned production files on origin/main.""" - hunted = {} - for h in hunts(): - hunted[h.where.file] = max(hunted.get(h.where.file, 0), h.at.at) - candidates = [] - for repo, root in HUNT_ROOTS: - log = subprocess.run(['git', '-C', repo, 'log', 'origin/main', '--since=7.days', '--name-only', '--format=', '--', root], - capture_output=True, text=True).stdout.split() - churn = {} - for f in log: - full = os.path.realpath(os.path.join(repo, f)) - if os.path.exists(full) and f.endswith('.py'): - churn[full] = churn.get(full, 0) + 1 - candidates += sorted(churn, key=churn.get, reverse=True)[:15] - return min(candidates, key=lambda f: hunted.get(f, 0)) if candidates else None - - -def reply(unit: str, since: float) -> Message | None: - """The unit's newest message to me after ``since`` in the recent bus window.""" - with COMMS.bus.log.opened_wire_snapshot() as source: - return next((message for message in source.newest_messages() - if message.sender == unit and message.timestamp > since - and message.target in (ME, '#comms')), None) - - def replied(latest: Message | None) -> str: """The reply an overdue alarm is read against.""" if latest is None: @@ -578,82 +149,583 @@ def replied(latest: Message | None) -> str: return f'; REPLIED {at} [{latest.message_id}]: {" ".join(latest.body.split())[:160]} -- read it before pushing' -def deadline_lines(pending: list[Todo], read: dict[str, OrderProgress], since: float = 0.0) -> list[str]: - """Every past-due unrecorded order alarms with its state until recorded. - Stall crossings remain separate from deadline alarms.""" - now = time.time() - late, stalled = [], [] - for t in pending: - ordered, work = t.order.at.recorded_at('An order'), 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): - continue - answer = reply(t.holder, ordered) - due = f'due {hhmm(t.deadline)}, order {t.order.id}' - late.append(f'{t.holder} ({due}, {work.line}{replied(answer)})') - alarm = (f'OVERDUE: {", ".join(late)} -> record merged work; ' - 'push or merge unfinished work now') - return [alarm] * bool(late) + stalled +class Commander(CommanderConfig): + """One command's configuration and opened bus; nothing is resolved at import.""" + + _comms: Comms = PrivateAttr() + + def model_post_init(self, context) -> None: + self._comms = Comms(self.root) + + @property + def comms(self) -> Comms: + return self._comms + + @classmethod + def of(cls, config: CommanderConfig) -> Commander: + return cls.model_construct(**config.model_dump()) + + def post(self, audience: VerdictAudience, body: str) -> str: + """Send one message its caller refused with OUTGOING; judge reads it from bus history. + + One thread gets a DM; a larger audience is mentioned on #comms, which wakes no one else. + """ + target = audience.threads[0] if len(audience.threads) == 1 else '#comms' + return self.call(CommsSendTool(sender=self.commander, target=target, body=body, + audience=audience.threads)).id + + + def call(self, tool): + """Every order write is one Core tool request made as the commander's thread.""" + return ThreadCaller(self.commander).invoke(tool, self.comms) + + + def todos(self): + """The problems this commander created; Core's Todo store is their one owner.""" + return self.comms.goals.todos.for_commander(self.comms.registry.require(self.commander).incarnation) -def install_lines(held: tuple[Todo, ...], read: dict[str, OrderProgress], now: float) -> list[str]: - """One INSTALL line naming every held order that waits on the fleet's install.""" - waiting = [f'{w.id} {w.holder}' for t in held for w in t.order.waiting_on_install(t, read)] - return InstallDeadline.read(COMMS.root).alarm(waiting, now) + def held_orders(self) -> tuple[Todo, ...]: + """This commander's open orders that a unit holds.""" + return self.comms.goals.todos.held_for_commander(self.comms.registry.require(self.commander).incarnation) -def overdue(): - since = 0.0 # previous poll bounds stall crossings; deadline alarms repeat - installs: list[str] = [] - while True: + def held_by(self, unit: str) -> tuple[Todo, ...]: + """This commander's open orders held by one unit.""" + return self.comms.goals.todos.held_by(self.comms.registry.require(self.commander).incarnation, unit) + + + def units(self) -> frozenset[str]: + return self.comms.goals.todos.units(self.comms.registry.require(self.commander).incarnation) + + + def progress(self) -> dict[str, OrderProgress]: + """Each held order's progress, derived by Core's Todo list from the unit's checkout and PRs.""" + listed: TodoList = self.call(CommsTodoListTool()) + return listed.progress + + + def send(self, unit, due, body: str, clean=False, mechanical=False, reassign=None, new=None, + traced=None, replaces=None, disk=False, finding=UnlocatedProblemFinding()): + """Validate an order body and record it in Core: a new problem (new=(PID, TEXT)) or the listed open problems. + + Sending to a unit that already holds the problem replaces its order there; without --clean that is a send-back.""" + problems = [] + # Below the floor Core's StorageRule denies each allocating tool call, so the order prose is not checked. + try: + self.comms.registry.storage_rule.require_quota(self.comms.registry.require(unit)) + except SelectedToolDenied as quota: + problems.append(f'{unit}: {quota}: it cleans up its own checkouts and scratch before it gets new work') + Todo.require_order_body(body) + if not clean and (timid := BluntText.refused(body)): # blunt at all times unless nothing was incompetent + problems.append(timid) + works = re.search(rf'(?m)^{re.escape(OrderRun.label)}\s*(.+)$', body) + run = MechanicalOrderRun() if mechanical else ApplicationOrderRun(works[1] if works else '') + serves = re.search(r'(?m)^Serves:\s*(\S+)\s*$', body) + phase = OrderPhase.member_named(serves[1] if serves else UnplannedPhase.declared_name) + edits = tuple(re.findall(r'(?m)^\s*\d+[.)] (.+?)\s*$', body)) + if not edits: + problems.append('no numbered edits: list each exact edit as "1. ...", "2. ..."') + report = re.search(r'(?m)^Report:\s*(\S.*?)\s*$', body) + if not report: + problems.append('no Report: line: say exactly what the unit sends back (sha, counts before -> after)') + banned = tuple(item.strip() for line in re.findall(r'(?m)^Banned:(.*)$', body) for item in line.split(';') if item.strip()) + if not banned: + problems.append('no Banned: line: name the slop shapes this task invites, separated by ";"') + if not re.fullmatch(r'\d{1,2}:\d{2}', due): + problems.append('due must be HH:MM local time') + fact = re.search(r'(?m)^Problems?:\s*(P\d+(?:\s*,\s*P\d+)*)\b', body) + group = [new[0]] if new else re.findall(r'P\d+', fact[1]) if fact else [] + owner = re.search(r'(?m)^Owner:\s*(\S+):(\d+)\s*$', body) + check = (line := re.search(r'(?m)^Check:(.*)$', body)) and OrderCheck.parse(line[1]) + listed = {t.id: t for t in self.todos()} + if not group: + problems.append('no Problem: P line; record it first with `order problem` (every order owns a listed problem)') + elif not new and (missing := [pid for pid in group if pid not in listed]): + problems.append(f'{missing} not open problems') + if not owner: + problems.append('no Owner: path:line of the existing owner the fix belongs on') + elif not os.path.exists(owner[1]) or int(owner[2]) > sum(1 for _ in open(owner[1])): + problems.append(f'Owner {owner[1]}:{owner[2]} does not exist; read the code and name the real owner') + if not check: + problems.append('no Check: :: -> N; done runs it, so make it the real acceptance count') + elif not os.path.isdir(check.directory): + problems.append(f'Check repo {check.directory} is not a directory') + # A check that can never print its target, or a premise naming lines that do something else, wastes the order. + # An order's check must fail now (or it proves nothing), and every absolute + # path:line it cites must exist. + if check and os.path.isdir(check.directory) and not problems: + try: + ran = subprocess.run(['bash', '-c', check.command], cwd=check.directory, capture_output=True, timeout=90) + printed = CheckPrinted.decoded(ran.stdout) + problems += printed.unproven(check.command, check.result.expected) + except subprocess.TimeoutExpired: + problems.append('Check took over 90 s; done runs it too, so make it a focused count') + for path, line in re.findall(r'(? sum(1 for _ in open(path, errors='replace')): + problems.append(f'cited {path}:{line} does not exist; read the code before citing it') + # A module deleted here can still be imported by another repo: + # P12 deleted acp_ingress.py, which Toad imports. + deleted = {stem for edit in edits + for stem in re.findall(r'(?i)\bdelete\s+`?(?:\S*/)?(\w+)\.py\b', edit)} + if check and deleted and (unread := self.importers(deleted, check)): + problems.append(f'the order deletes {sorted(deleted)}, which {unread} import; ' + 'the check must also count their `from agent_comms` imports') + if any(re.search(r'(?"') + if reassign is not None and len(reassign.split()) < 8: + problems.append('--reassign needs the concrete failure that forces replacement (8+ words), told to the old and new owner') + if problems: + sys.exit('Refused order:\n ' + '\n '.join(problems)) + message = (f'{body.rstrip()}' + ('\nThis replaces my earlier order on it; drop that one.' if replacing else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + self.comms.registry.storage_rule.order_text(unit, [f'{repo.checkout}/.venv/bin/python' for repo in self.repos]) + '\nIf two lines of this order conflict, take the narrower reading, say which one you took, and proceed; do not stop to ask.' + + '\nStart now and push as you go.') # a unit shown a due time takes until then + taken = {other: f'Taken off you: {", ".join(group)} ("{listed[group[0]].text[:120]}") now belongs to {unit} because: {reassign}. Stop that work; do not touch its files.' for other in holders} + refuse('\n'.join((message, *taken.values())), *OUTGOING) # before Core: refused records nothing + fired = [pid for pid in group if pid in listed and listed[pid].struck_out(unit)] + if not mechanical and fired: + sys.exit(f'Refused: {unit} has {ProblemGoalOrder.StrikeLimit} strikes on {fired}. ' + f'Reassign them; only --mechanical orders on them reach {unit}.') + # A miss already struck on the order this replaces is one miss: + # the send-back records no second strike. + struck_already = replacing and listed[group[0]].struck_on_order(unit) + at = time.time() + order = PendingProblemOrder(finding=finding, + id=uuid4().hex[:12], owner=FileLine(owner[1], int(owner[2])), + check=check, edits=edits, banned=banned, + report=report[1], run=run, serves=phase, + deletes=OrderDeletion.parse(deletes[1] if (deletes := re.search( + rf'(?m)^{re.escape(OrderDeletion.label)}\s*(.+)$', body)) else None), + at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) + common = dict(order=order, request=order.request(self.comms.registry.require(self.commander), tuple(group)), deadline=deadline(due), commander=self.commander) + if new: + self.call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction( + problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], **common))) + for pid in [] if new else group: + self.call(CommsOrderTool(owner=unit, order=OrderGoalAction( + problem=pid, expected_revision=self.comms.goals.todos.get(pid).revision, traced=trace, **common))) + message += "\n" + "\n".join(order.pull_request_instruction(pid) for pid in group) + sent = self.post(VerdictAudience((unit,)), message) + # The message is each order's recorded link; a verdict citing it lands on that order. + for pid in group: + self.call(CommsTodoTool(id=pid, operation=OrderMessage( + self.comms.goals.todos.get(pid).revision, self.comms.registry.require(self.commander).incarnation, sent))) + for other, text in taken.items(): + self.post(VerdictAudience((other,)), text) + print(order.id) + if replacing and not clean and struck_already: + print(f'{unit} already struck on this order of {group[0]}; ' + 'the send-back counts that one miss once') + elif replacing and not clean and not reassign: + # A send-back is a strike, no warnings, and every send-back is roasted in #comms. + # Done here so the commander cannot send work back and forget either. + verdict = next(l for l in body.splitlines() if BluntText.found(l)).strip() + self.strike(unit, group[0], f'sent back on {group[0]}: {verdict}') + + + def importers(self, modules: set[str], check: OrderCheck) -> list[str]: + """The other configured repos whose `from agent_comms` imports name a module, unless the + check reads them.""" + pattern = rf'^\s*(from|import) agent_comms\b.*\b({"|".join(sorted(modules))})\b' + return [str(repo.checkout) for repo in self.repos + if repo.checkout != Path(check.directory).resolve() + and str(repo.checkout) not in check.command + and subprocess.run(['grep', '-rqE', '--include=*.py', pattern, + str(repo.checkout / repo.source)]).returncode == 0] + + + def correct_check(self, pid: str, line: str, why: str) -> None: + """Core replaces a wrong check of an open order. The new check must already pass on main, so a + correction fixes the commander's check and never passes undelivered work: done still runs it on + the --sha it is given.""" + todo = self.comms.goals.todos.get(pid) + if not todo.state.open or not todo.pending: + sys.exit(f'Refused: {pid} has no pending order') + if not (check := OrderCheck.parse(line)): + sys.exit('Refused: --check reads " :: -> "') + if not os.path.isdir(check.directory): + sys.exit(f'Refused: check repo {check.directory} is not a directory') + if len(why.split()) < 8: + sys.exit('Refused: --why says what was wrong with the recorded check (8+ words)') + def git(*args: str) -> str: + return subprocess.run(['git', '-C', check.directory, *args], + capture_output=True, text=True).stdout.strip() + head = git('rev-parse', 'HEAD') + if head != git('rev-parse', 'origin/main') or git('status', '--porcelain', '-uno'): + sys.exit(f'Refused: {check.directory} is not a clean checkout of origin/main; ' + 'the corrected check runs on main') + check.result.require(asyncio.run(BoundedRun.run( + ('bash', '-c', check.command), cwd=check.directory, timeout=90))) + self.call(CommsOrderCheckTool(problem=pid, check=check, why=why)) + print(f'{pid}: order {todo.order.id} now checks `{check.command}`') + + + def say(self, units, body_file): + body = open(body_file).read() + # Numbered edits or a report line are an order: it needs a deadline, so it goes through send. + if len(re.findall(r'(?m)^\s*\d+[.)] ', body)) >= 2 or re.search(r'(?mi)^\s*(report|due)\b.*:', body): + sys.exit('Refused: a numbered edit list or a Report/Due line is an order. Use `order send` so it gets a tracked deadline.') + refuse(body, *OUTGOING) + Slop.from_order(body) + print(self.post(VerdictAudience(tuple(units.split(','))), body.rstrip())) + + + def problem(self, text, unit, due, order_file, where, disk=False): + """A problem is born owned: Core records it with its order in one step, or not at all.""" + location = FileLine.from_wire(where) + location.require_exists() + body = open(order_file).read() + if 'Problem: NEW' not in body: + sys.exit('Refused: the order file needs a "Problem: NEW" line; a problem is never logged without its order') + if len(text.split()) < 8: + sys.exit('Refused: describe the problem (8+ words): what is wrong, what it costs') + history = self.comms.goals.todos.history_for_commander(self.comms.registry.require(self.commander).incarnation) + pid = f'P{max([int(t.id[1:]) for t in history if t.id[1:].isdigit()] + [0]) + 1}' + self.send(unit, due, body.replace('Problem: NEW', f'Problem: {pid}'), new=(pid, text), + disk=disk, finding=LocatedProblemFinding(location)) + self.write_board() + print(pid) + + + def close(self, pid, works, artifact=''): + """Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo.""" + evidence = artifact or self.comms.goals.todos.get(pid).order.closing_artifact() + ThreadCaller(self.commander).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=evidence), self.comms) + self.write_board() + print(f'{pid} closed') + + + def last_activity(self, unit): + return ExternalTranscript(self.comms.root, self.comms.registry.require(unit).incarnation).last_tool_call() + + + def write_board(self): + lines = ['# Fleet board', '', + "GENERATED by agent-comms-commander order from Core's Todo store. Do not edit by hand:", + '`order problem` adds a row, `order send ... Problem: P` orders it, `order close` or `order withdraw` removes it.', '', + '| Problem | Owner file | Owner | Order | Due | Progress | Delivered ' + '| Owner last acted |', + '|---|---|---|---|---|---|---|---|'] + listed, read = self.todos(), self.progress() + activity = {unit: self.last_activity(unit) for unit in {t.holder for t in listed if t.holder}} + for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): + unit, last = t.holder, t.released[-1:] + owner = unit or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}' + lines.append(f'| {t.id}: {t.text} | {t.order.owner.to_wire()} | {owner} | {t.order.id} | ' + f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | ' + f'{"no" if t.pending else "yes, install pending"} | ' + f'{activity[unit].clock() if unit else "-"} |') + history = self.comms.goals.todos.history_for_commander(self.comms.registry.require(self.commander).incarnation) + lines += ['', '## Withdrawn orders', '', + *(f'- {line}' for t in history for line in t.order.withdrawals(t))] + open(self.board, 'w').write('\n'.join(lines) + '\n') + + + def on_record(self, line: str, recorded: str, audience: VerdictAudience) -> None: + """Put one line on the public record: the narrative file, then its audience. + + Core already holds what `recorded` names, so a failed post is reposted, never recorded again. + """ + new_file = not os.path.exists(self.narrative) + with open(self.narrative, 'a') as f: + if new_file: + f.write('# What the fleet actually did\n\nEvery failure, in plain words, as it happened. ' + 'Written by agent-comms-commander order; never edited.\n\n') + f.write(f'- {line}\n') + try: + self.post(audience, f'On the record: {line}') + except (SystemExit, ValueError) as failed: + print(f'{recorded} recorded, but the post failed ({failed}). ' + 'Repost with order.post; do not record it again.', file=sys.stderr) + raise + + + def strike(self, unit: str, task: str, why: str, caught: str = 'review', *, + kind: type[StrikeTodo] = StrikeTodo) -> None: + """HARD RULE: every failure goes on the public record, in plain words, damning and exact. + + Core records the strike on the task's Todo; the strike is written to the narrative file and posted to + #comms in the same step, so no failure stays private and none is described in words that hide what happened. + """ + caught = f'{self.commander} at {caught}' if caught in STAGES else caught + # What matters is how a mistake happened, how often, and how and when it was caught. + if not any(stage in caught for stage in STAGES): + sys.exit(f'Refused: --caught must say who caught it and at which stage {STAGES}, e.g. "{self.commander} at review".') + todo = self.comms.goals.todos.get(task) + todo.state.require_open(todo) + if todo.held().owner != unit: + sys.exit(f'Refused: {unit} does not hold {task}; strike the unit that holds it.') + refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the strike + recorded = self.call(CommsTodoTool(id=task, operation=kind( + todo.revision, self.comms.registry.require(self.commander).incarnation, todo.held(), f'{why} (caught by {caught})'))) + if recorded.todo.revision == todo.revision: + return + n = struck(self.comms.goals.todos.get(task), unit) + line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike {n} on {task}{f", {task} taken away" if n >= 2 else ""}; ' + f'caught by {caught}): {why}') + self.on_record(line, 'Strike', VerdictAudience.of(unit, todo.goal)) + print(f'{unit}: {n} strike(s) on {task}' + (f'. Reassign {task} now and freeze its branch.' if n >= 2 else '')) + + + def done(self, pid, sha, screenshot='', seen='', repo=''): + """Core runs the delivery's check at its checked commit; a late delivered commit is a strike.""" + todo = self.comms.goals.todos.get(pid) + if not todo.state.open or not todo.pending: + sys.exit(f'Refused: {pid} has no pending order') + unit = todo.assignment.owner + if repo and Path(repo).resolve() != Path(todo.order.check.directory).resolve(): + sys.exit('Refused: --repo must name the recorded order check directory.') + repo = todo.order.check.directory + operation = sha == 'op' # an operation on live state, not a commit: the check runs now and now is delivery + if not operation: + diff = subprocess.run(['git', '-C', repo, 'show', '--first-parent', '-m', '--format=', sha], capture_output=True, text=True).stdout + changed = re.findall(r'^\+\+\+ b/(\S+\.py)$', diff, re.M) + after = [subprocess.run(['git', '-C', repo, 'show', f'{sha}:{f}'], capture_output=True, text=True).stdout for f in changed] + if added := forwarders_added(diff, after): + sys.exit(f'Refused: {sha} adds forwarders {added[:5]}; slop never counts as delivery. Order the hierarchy fix.') + thread = self.comms.registry.require(unit) + if left := [str(t) for t in self.comms.registry.storage_rule.checkouts_of(thread) if pid.lower() in t.name.lower()]: + sys.exit(f'Refused: {unit} still has the checkout {left} for this order; storage rule: it removes it before the order counts as done.') + try: + self.comms.registry.storage_rule.require_quota(thread) + except SelectedToolDenied as quota: + sys.exit(f'Refused: {unit}: {quota}; storage rule: it cleans up before the order counts as done.') + todo.order.run.require_artifact(seen or screenshot) + evidence = FileDeliveryEvidence(seen or screenshot) if seen or screenshot else NoDeliveryEvidence() + if operation: + delivery = OperationDelivery(evidence=evidence) + claim = WorkingClaim(evidence.artifact()) + else: + delivered_commit = revision(repo, sha) + if subprocess.run(['git', '-C', repo, 'merge-base', '--is-ancestor', + delivered_commit, 'HEAD']).returncode: + sys.exit(f'Refused: {delivered_commit} is not in {repo} HEAD.') + delivery = CommitDelivery(commit=revision(repo, 'HEAD'), + delivered_commit=delivered_commit, evidence=evidence) + claim = MergedClaim(delivery.delivered) + self.call(CommsOrderDoneTool(problem=pid, order_id=todo.order.id, delivery=delivery, claims=(claim,))) + lateness = self.comms.goals.todos.get(pid).order.delivery.at.lateness(todo.deadline) + # An op has no commit time: recording it later is my delay, so lateness is judged at the OVERDUE alarm. + if not operation and lateness.late(): + self.strike(unit, pid, f'{pid} order {todo.order.id} {lateness} as {delivered_commit}') + return + print(f'{unit}: order {todo.order.id} met as {sha}') + + + def first_calls(self, held, activity): + missing = [] + for todo in held: + unit, order = todo.assignment.owner, todo.order + if not todo.pending: + continue + acted = activity[unit].since(order.at.recorded_at("An order")) + if not acted and time.time() < order.first_call_deadline.at: + continue + event = ObservedFirstCallEvent if acted else NotifiedFirstCallEvent + self.call(CommsTodoTool(id=todo.id, operation=RecordFirstCallTodo( + todo.revision, self.comms.registry.require(self.commander).incarnation, event))) + if not acted and self.comms.goals.todos.get(todo.id).revision != todo.revision: + missing.append(f'{unit} (order {order.id}, sent {hhmm(order.at)})') + return missing + + + def hunt(self, where, why, sent): + """Record the commander's source read, whether or not it became an order.""" + path, _, line = where.rpartition(':') + if not os.path.exists(path) or not line.isdigit() or int(line) > sum(1 for _ in open(path)): + sys.exit(f'Refused: {where} is not a real file:line') + if len(why.split()) < 10: + sys.exit('Refused: say what the code does per event or per history row, and the fix or why it is clean (10+ words)') + ref, at = FileLine(os.path.realpath(path), int(line)), RecordedEventTime(time.time()) + self.comms.goals.todos.record_hunt(self.comms.registry.require(self.commander).incarnation, + OrderedHunt(ref, why, at, sent) if sent else CleanHunt(ref, why, at)) + print(f'hunt recorded: {where} -> {sent or "clean"}') + + + def hunts(self): + return self.comms.goals.todos.hunts_for_commander(self.comms.registry.require(self.commander).incarnation) + + + def next_hunt(self): + """Least-recently hunted file among the most-churned production files on origin/main.""" + hunted = {} + for h in self.hunts(): + hunted[h.where.file] = max(hunted.get(h.where.file, 0), h.at.at) + candidates = [] + for repo, root in [(str(repo.checkout), repo.source) for repo in self.repos]: + log = subprocess.run(['git', '-C', repo, 'log', 'origin/main', '--since=7.days', '--name-only', '--format=', '--', root], + capture_output=True, text=True).stdout.split() + churn = {} + for f in log: + full = os.path.realpath(os.path.join(repo, f)) + if os.path.exists(full) and f.endswith('.py'): + churn[full] = churn.get(full, 0) + 1 + candidates += sorted(churn, key=churn.get, reverse=True)[:15] + return min(candidates, key=lambda f: hunted.get(f, 0)) if candidates else None + + + def reply(self, unit: str, since: float) -> Message | None: + """The unit's newest message to me after ``since`` in the recent bus window.""" + with self.comms.bus.log.opened_wire_snapshot() as source: + return next((message for message in source.newest_messages() + if message.sender == unit and message.timestamp > since + and message.target in (self.commander, '#comms')), None) + + + def deadline_lines(self, pending: list[Todo], read: dict[str, OrderProgress], since: float = 0.0) -> list[str]: + """Every past-due unrecorded order alarms with its state until recorded. + Stall crossings remain separate from deadline alarms.""" now = time.time() - held = held_orders() - activity = {unit: last_activity(unit) for unit in {t.assignment.owner for t in held}} - if missing := first_calls(held, activity): - print('NO FIRST CALL: ' + ', '.join(missing) + ' -> stuck holding an order; push it now or reassign', - flush=True) - stale = [f'{t.id} {t.assignment.owner} (last {activity[t.assignment.owner].clock()})' for t in held - if activity[t.assignment.owner].before(now - 1200)] - if stale: - print(f'IDLE OWNER 20+ min: {", ".join(stale)} -> push or reassign', flush=True) - pending = [t for t in held if t.pending] - read = progress() if pending else {} - for line in deadline_lines(pending, read, since): - print(line, flush=True) - if (lines := install_lines(held, read, now)) != installs: - for line in lines: + late, stalled = [], [] + for t in pending: + ordered, work = t.order.at.recorded_at('An order'), 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): + continue + answer = self.reply(t.holder, ordered) + due = f'due {hhmm(t.deadline)}, order {t.order.id}' + late.append(f'{t.holder} ({due}, {work.line}{replied(answer)})') + alarm = (f'OVERDUE: {", ".join(late)} -> record merged work; ' + 'push or merge unfinished work now') + return [alarm] * bool(late) + stalled + + + def install_lines(self, held: tuple[Todo, ...], read: dict[str, OrderProgress], now: float) -> list[str]: + """One INSTALL line naming every held order that waits on the fleet's install.""" + waiting = [f'{w.id} {w.holder}' for t in held for w in t.order.waiting_on_install(t, read)] + return InstallDeadline.read(self.comms.root).alarm(waiting, now) + + + def overdue(self): + since = 0.0 # previous poll bounds stall crossings; deadline alarms repeat + installs: list[str] = [] + while True: + now = time.time() + held = self.held_orders() + activity = {unit: self.last_activity(unit) for unit in {t.assignment.owner for t in held}} + if missing := self.first_calls(held, activity): + print('NO FIRST CALL: ' + ', '.join(missing) + ' -> stuck holding an order; push it now or reassign', + flush=True) + stale = [f'{t.id} {t.assignment.owner} (last {activity[t.assignment.owner].clock()})' for t in held + if activity[t.assignment.owner].before(now - 1200)] + if stale: + print(f'IDLE OWNER 20+ min: {", ".join(stale)} -> push or reassign', flush=True) + pending = [t for t in held if t.pending] + read = self.progress() if pending else {} + for line in self.deadline_lines(pending, read, since): print(line, flush=True) - installs = lines - # Units leave finished PRs open and wander off. An order's open PR that nobody has touched for - # 5 minutes is a stall, whatever the unit says it is doing; until its deadline, OVERDUE owns the alarm. - idle_prs = [f'{t.holder} {t.repo.split("/")[1]} {read[t.id].line} ({t.id})' - for t in pending if t.overdue(now) and read[t.id].untouched(now)] - if idle_prs: - print('STALL PR open and untouched 5+ min: ' + ', '.join(idle_prs) + ' -> order the merge or the fix now', flush=True) - for _ in HuntDue.broken(COMMS, ME, since): - print(HuntVerb.due(next_hunt()), flush=True) - since = now - time.sleep(60) + if (lines := self.install_lines(held, read, now)) != installs: + for line in lines: + print(line, flush=True) + installs = lines + # Units leave finished PRs open and wander off. An order's open PR that nobody has touched for + # 5 minutes is a stall, whatever the unit says it is doing; until its deadline, OVERDUE owns the alarm. + idle_prs = [f'{t.holder} {t.repo.split("/")[1]} {read[t.id].line} ({t.id})' + for t in pending if t.overdue(now) and read[t.id].untouched(now)] + if idle_prs: + print('STALL PR open and untouched 5+ min: ' + ', '.join(idle_prs) + ' -> order the merge or the fix now', flush=True) + for _ in HuntDue.broken(self.comms, self.commander, since): + print(HuntVerb.due(self.next_hunt()), flush=True) + since = now + time.sleep(60) + + + def unstrike(self, task: str, why: str, at: str | None = None) -> None: + """A strike recorded in error is voided in Core, then narrated and posted to #comms.""" + todo = self.comms.goals.todos.get(task) + live = [s for s in todo.live_strikes() if at in (None, hhmm(s.at))] + if not live: + sys.exit(f'Refused: {task} has no live strike' + (f' recorded at {at}.' if at else '.')) + voided = live[-1] + refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the void + self.call(CommsTodoTool(id=task, operation=VoidStrikeTodo( + todo.revision, self.comms.registry.require(self.commander).incarnation, voided, why))) + unit = voided.previous.owner + n = struck(self.comms.goals.todos.get(task), unit) + line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike voided on {task}, ' + f'recorded {hhmm(voided.at)}; {n} strike(s) remain): {why}') + self.on_record(line, 'Void', VerdictAudience.of(unit, todo.goal)) + print(f'{unit}: {n} strike(s) on {task}') + + + def due(self, pid: str, at: str, why: str) -> None: + """Core moves a held order's deadline, its assignment unchanged; the reason goes on record.""" + todo = self.comms.goals.todos.get(pid) + if not todo.pending: + sys.exit(f'Refused: {pid} holds no pending order whose deadline could move.') + if len(why.split()) < 8: + sys.exit('Refused: --why says why the deadline moves (8+ words)') + refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the deadline + moved = deadline(at) + self.call(CommsTodoTool(id=pid, operation=AssignTodo( + todo.revision, todo.assignment, moved, todo.order, todo.goal))) + self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due ' + f'{hhmm(todo.deadline)} moved to {hhmm(moved)}): {why}', 'Deadline', + VerdictAudience.of(todo.held().owner, todo.goal)) + print(f'{pid}: due {hhmm(moved)}') + + + def withdraw(self, pid: str, why: str, superseded_by: str | None = None) -> None: + """Core retires an undelivered order sent in error. + + The reason goes on the public record and to the unit that held it.""" + todo = self.comms.goals.todos.get(pid) + refuse(why, *OUTGOING) # posted to #comms and the holder, so refused before Core records it + self.call(CommsTodoTool(id=pid, operation=WithdrawTodo( + todo.revision, self.comms.registry.require(self.commander).incarnation, todo.assignment, why, + superseded_by))) + unit = todo.held().owner + withdrawal, = self.comms.goals.todos.get(pid).order.withdrawals(todo) + self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {withdrawal}', 'Withdrawal', + VerdictAudience.of(unit, todo.goal)) + self.post(VerdictAudience((unit,)), + f'Withdrawn: {withdrawal}. Stop that work; it counts against no one.') + self.write_board() + print(withdrawal) + + + def fire(self, unit: str, why: str) -> None: + """Core releases every open problem the unit holds in one call; orders and strikes stay.""" + reason = StrikeReason(why) + for todo in self.call(CommsFireTool(unit=unit, reason=reason)).todos: + print(f'{todo.id}: released from {unit}: {why}') @dataclass(frozen=True, kw_only=True) class CommanderVerb(CommanderRole, DeclaredFamily, DeclaredOptions, affix="Verb"): """One commander verb: its options are its fields, its parser comes from them, and run() does the work.""" + context: Commander = field(init=False, repr=False) fork: str | None = option("--fork", default=None, help="a forked commander's name; keeps its beat fresh") @property def who(self) -> str: - return self.fork or ME + return self.fork or self.context.commander - def apply(self, ctx: None = None) -> None: + def apply(self, ctx: Commander) -> None: + object.__setattr__(self, 'context', ctx) self.gate() self.run() def gate(self) -> None: - self.require_commander(COMMS.registry.require(self.who)) + self.require_commander(self.context.comms.registry.require(self.who)) if self.fork: - capacity.beat(self.fork) + capacity.beat(self.context.state / 'forks', self.fork) @abstractmethod def run(self) -> None: ... @@ -686,80 +758,19 @@ class RuledVerb(CommanderVerb): def gate(self) -> None: super().gate() - for due in RulesDue.violations(COMMS, self.who): - flag = '' if self.who == ME else f' --fork {self.who}' + for due in RulesDue.violations(self.context.comms, self.who): + flag = '' if self.who == self.context.commander else f' --fork {self.who}' sys.exit(f'REFUSED, nothing recorded: {due.why}. Run agent-comms-commander' f' rules review{flag}, then answer each drawn rule{flag}.') -def unstrike(task: str, why: str, at: str | None = None) -> None: - """A strike recorded in error is voided in Core, then narrated and posted to #comms.""" - todo = COMMS.goals.todos.get(task) - live = [s for s in todo.live_strikes() if at in (None, hhmm(s.at))] - if not live: - sys.exit(f'Refused: {task} has no live strike' + (f' recorded at {at}.' if at else '.')) - voided = live[-1] - refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the void - call(CommsTodoTool(id=task, operation=VoidStrikeTodo( - todo.revision, COMMS.registry.require(ME).incarnation, voided, why))) - unit = voided.previous.owner - n = struck(COMMS.goals.todos.get(task), unit) - line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike voided on {task}, ' - f'recorded {hhmm(voided.at)}; {n} strike(s) remain): {why}') - on_record(line, 'Void', VerdictAudience.of(unit, todo.goal)) - print(f'{unit}: {n} strike(s) on {task}') - - -def due(pid: str, at: str, why: str) -> None: - """Core moves a held order's deadline, its assignment unchanged; the reason goes on record.""" - todo = COMMS.goals.todos.get(pid) - if not todo.pending: - sys.exit(f'Refused: {pid} holds no pending order whose deadline could move.') - if len(why.split()) < 8: - sys.exit('Refused: --why says why the deadline moves (8+ words)') - refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the deadline - moved = deadline(at) - call(CommsTodoTool(id=pid, operation=AssignTodo( - todo.revision, todo.assignment, moved, todo.order, todo.goal))) - on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due ' - f'{hhmm(todo.deadline)} moved to {hhmm(moved)}): {why}', 'Deadline', - VerdictAudience.of(todo.held().owner, todo.goal)) - print(f'{pid}: due {hhmm(moved)}') - - -def withdraw(pid: str, why: str, superseded_by: str | None = None) -> None: - """Core retires an undelivered order sent in error. - - The reason goes on the public record and to the unit that held it.""" - todo = COMMS.goals.todos.get(pid) - refuse(why, *OUTGOING) # posted to #comms and the holder, so refused before Core records it - call(CommsTodoTool(id=pid, operation=WithdrawTodo( - todo.revision, COMMS.registry.require(ME).incarnation, todo.assignment, why, - superseded_by))) - unit = todo.held().owner - withdrawal, = COMMS.goals.todos.get(pid).order.withdrawals(todo) - on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {withdrawal}', 'Withdrawal', - VerdictAudience.of(unit, todo.goal)) - post(VerdictAudience((unit,)), - f'Withdrawn: {withdrawal}. Stop that work; it counts against no one.') - board() - print(withdrawal) - - -def fire(unit: str, why: str) -> None: - """Core releases every open problem the unit holds in one call; orders and strikes stay.""" - reason = StrikeReason(why) - for todo in call(CommsFireTool(unit=unit, reason=reason)).todos: - print(f'{todo.id}: released from {unit}: {why}') - - class LoadedVerb(RuledVerb): """HARD RULE: overloaded means fork, not more orders from me; a fork is never blocked.""" def gate(self) -> None: super().gate() if not self.fork: - capacity.Load.of(COMMS, ME).require() + capacity.Load.of(self.context.comms, self.context.commander, self.context.state / 'forks').require() class OrderVerb(CommanderVerb): @@ -781,7 +792,7 @@ class SendVerb(OrderVerb, LoadedVerb): help="the first failing source line, required after two owners") def run(self) -> None: - send(self.unit, self.due, Path(self.body_file).read_text(), clean=self.clean, + self.context.send(self.unit, self.due, Path(self.body_file).read_text(), clean=self.clean, mechanical=self.mechanical, reassign=self.reassign, traced=self.traced, replaces=self.replaces, disk=self.disk) @@ -791,13 +802,13 @@ class StrikeVerb(OrderVerb, RuledVerb): help = "a send-back, a missed deadline or a false claim" unit: str = option("unit") why: str = option("--why") - caught: str = option("--caught", default=f'{ME} at review', help='"WHO at STAGE"; stage is review/install/live/user') + caught: str = option("--caught", default='review', help='"WHO at STAGE"; stage is review/install/live/user') def run(self) -> None: references = set(re.findall(r'\bP\d+\b', self.why)) if len(references) != 1: sys.exit('Refused: name exactly one problem in --why; strikes count per problem.') - strike(self.unit, references.pop(), self.why, self.caught) + self.context.strike(self.unit, references.pop(), self.why, self.caught) @dataclass(frozen=True, kw_only=True) @@ -807,7 +818,7 @@ class FireVerb(OrderVerb, RuledVerb): why: str = option("--why") def run(self) -> None: - fire(self.unit, self.why) + self.context.fire(self.unit, self.why) @dataclass(frozen=True, kw_only=True) @@ -819,7 +830,7 @@ class UnstrikeVerb(OrderVerb, RuledVerb): help="HH:MM the strike was recorded; default the newest") def run(self) -> None: - unstrike(self.pid, self.why, self.at) + self.context.unstrike(self.pid, self.why, self.at) @dataclass(frozen=True, kw_only=True) @@ -830,7 +841,7 @@ class DueVerb(OrderVerb, RuledVerb): why: str = option("--why") def run(self) -> None: - due(self.pid, self.at, self.why) + self.context.due(self.pid, self.at, self.why) @dataclass(frozen=True, kw_only=True) @@ -840,7 +851,7 @@ class InstallVerb(RuledVerb): installer: str = option("installer", help="the unit that runs the install") def run(self) -> None: - PlannedInstallDeadline(deadline(self.at), self.installer).plan(COMMS.root) + PlannedInstallDeadline(deadline(self.at), self.installer).plan(self.context.comms.root) print(f'install due {self.at}, run by {self.installer}') @@ -853,7 +864,7 @@ class WithdrawVerb(OrderVerb, RuledVerb): help="the problem that already owns this fix") def run(self) -> None: - withdraw(self.pid, self.why, self.superseded_by) + self.context.withdraw(self.pid, self.why, self.superseded_by) @dataclass(frozen=True, kw_only=True) @@ -869,8 +880,8 @@ class DoneVerb(OrderVerb, RuledVerb): repo: str = option("--repo", default="", help="the recorded order check directory") def run(self) -> None: - todo = COMMS.goals.todos.resolve_order(COMMS.registry.require(ME).incarnation, self.unit, self.message) - done(todo.id, self.sha, self.screenshot, self.seen, self.repo) + todo = self.context.comms.goals.todos.resolve_order(self.context.comms.registry.require(self.context.commander).incarnation, self.unit, self.message) + self.context.done(todo.id, self.sha, self.screenshot, self.seen, self.repo) @dataclass(frozen=True, kw_only=True) @@ -881,8 +892,8 @@ class PullRequestVerb(OrderVerb, declared_name="pr"): body_file: str = option("--body-file") def run(self) -> None: - todo = COMMS.goals.todos.get(self.pid) - checkout = COMMS.registry.storage_rule.order_checkout(todo.holder, todo.id) + todo = self.context.comms.goals.todos.get(self.pid) + checkout = self.context.comms.registry.storage_rule.order_checkout(todo.holder, todo.id) print(todo.order.open_pull_request( todo, checkout, self.title, Path(self.body_file).read_text())) @@ -895,7 +906,7 @@ class CheckVerb(OrderVerb, RuledVerb): why: str = option("--why", help="what was wrong with the recorded check") def run(self) -> None: - correct_check(self.pid, self.check, self.why) + self.context.correct_check(self.pid, self.check, self.why) @dataclass(frozen=True, kw_only=True) @@ -905,7 +916,7 @@ class ServeVerb(OrderVerb, RuledVerb): phase: str = option("phase", help="one of " + ", ".join(OrderPhase.names())) def run(self) -> None: - call(CommsOrderServeTool(problem=self.pid, serves=OrderPhase.member_named(self.phase))) + self.context.call(CommsOrderServeTool(problem=self.pid, serves=OrderPhase.member_named(self.phase))) print(f'{self.pid} serves {self.phase}') @@ -916,7 +927,7 @@ class SayVerb(OrderVerb, LoadedVerb): body_file: str = option("body_file", metavar="BODYFILE") def run(self) -> None: - say(self.units, self.body_file) + self.context.say(self.units, self.body_file) @dataclass(frozen=True, kw_only=True) @@ -930,7 +941,7 @@ class ProblemVerb(OrderVerb, LoadedVerb): disk: bool = option("--disk", default=False) def run(self) -> None: - problem(self.text, self.to, self.due, self.order, self.where, self.disk) + self.context.problem(self.text, self.to, self.due, self.order, self.where, self.disk) @dataclass(frozen=True, kw_only=True) @@ -941,7 +952,7 @@ class DropVerb(OrderVerb, RuledVerb): by: str = option("--by") def run(self) -> None: - COMMS.goals.todos.drop(COMMS.registry.require(ME).incarnation, self.unit, self.message, self.by) + self.context.comms.goals.todos.drop(self.context.comms.registry.require(self.context.commander).incarnation, self.unit, self.message, self.by) print(f'{self.unit}: {self.message} replaced by {self.by}') @@ -953,7 +964,7 @@ class FoldVerb(OrderVerb, RuledVerb): why: str = option("--why") def run(self) -> None: - COMMS.goals.todos.fold(COMMS.registry.require(ME).incarnation, self.pid, self.into, self.why) + self.context.comms.goals.todos.fold(self.context.comms.registry.require(self.context.commander).incarnation, self.pid, self.into, self.why) print(f'{self.pid} folded into {self.into}') @@ -965,7 +976,7 @@ class LinkVerb(OrderVerb, RuledVerb): message: str = option("message") def run(self) -> None: - COMMS.goals.todos.link(COMMS.registry.require(ME).incarnation, self.pid, self.unit, self.message) + self.context.comms.goals.todos.link(self.context.comms.registry.require(self.context.commander).incarnation, self.pid, self.unit, self.message) print(f'{self.pid} held by {self.unit} through {self.message}') @@ -977,7 +988,7 @@ class CloseVerb(OrderVerb, RuledVerb): artifact: str = option("--artifact", default="", help="defaults to the file recorded by done --seen") def run(self) -> None: - close(self.pid, self.works, self.artifact) + self.context.close(self.pid, self.works, self.artifact) @dataclass(frozen=True, kw_only=True) @@ -994,29 +1005,30 @@ def due(cls, where: str | None) -> str: f'or slop, then {cls.usage()}') def run(self) -> None: - hunt(self.where, self.why, self.sent) + self.context.hunt(self.where, self.why, self.sent) class NextHuntVerb(OrderVerb, declared_name="next-hunt"): help = "print the next place to hunt" def run(self) -> None: - print(next_hunt()) + print(self.context.next_hunt()) class BoardVerb(OrderVerb): - help = "regenerate fleet-board.md from Core's Todo store" + help = "show the fleet board generated from Core's Todo store" def run(self) -> None: - board() + self.context.write_board() + print(self.context.board.read_text(), end='') class OverdueVerb(OrderVerb): help = "loop: every minute, name units past their deadline" def run(self) -> None: - overdue() + self.context.overdue() -def main(a): - OrderVerb.parse(a, __doc__).apply() +def main(a, commander: Commander): + OrderVerb.parse(a, __doc__).apply(commander) diff --git a/src/agent_comms/commander/request.py b/src/agent_comms/commander/request.py index 0c4fe2ac93..b0386477be 100644 --- a/src/agent_comms/commander/request.py +++ b/src/agent_comms/commander/request.py @@ -16,14 +16,14 @@ from ..goal_states import ActiveGoal from ..goals import Goal, RequestGoalOrder from ..todos import Todo -from .order import COMMS, ME, CommanderVerb, RuledVerb, hhmm +from .order import CommanderVerb, RuledVerb, hhmm -def open_requests() -> tuple[tuple[Goal, tuple[Todo, ...]], ...]: +def open_requests(commander) -> tuple[tuple[Goal, tuple[Todo, ...]], ...]: """The commander's open owner requests and their undelivered orders.""" - thread = COMMS.registry.require(ME) + thread = commander.comms.registry.require(commander.commander) return tuple(request for goal in thread.goal.active() - for request in goal.order.open_requests(goal, COMMS.goals, thread.incarnation)) + for request in goal.order.open_requests(goal, commander.comms.goals, thread.incarnation)) class RequestVerb(CommanderVerb): @@ -36,7 +36,7 @@ class RequestAddVerb(RequestVerb, RuledVerb, declared_name="add"): text: str = option("text") def run(self) -> None: - goal = COMMS.goals.update_goal(ME, SetGoalAction(text=self.text, order=RequestGoalOrder())) + goal = self.context.comms.goals.update_goal(self.context.commander, SetGoalAction(text=self.text, order=RequestGoalOrder())) print(goal.require().id) @@ -48,7 +48,7 @@ class RequestCompleteVerb(RequestVerb, RuledVerb, declared_name="complete"): def run(self) -> None: expect = GoalPrecondition(goal_id=self.id, expected_state=ActiveGoal()) - COMMS.goals.update_goal(ME, CompletedGoalAction(progress=self.why, expect=expect)) + self.context.comms.goals.update_goal(self.context.commander, CompletedGoalAction(progress=self.why, expect=expect)) print(f"{self.id} done") @@ -56,7 +56,7 @@ class RequestListVerb(RequestVerb, declared_name="list"): help = "the open request and the open orders serving it" def run(self) -> None: - requests = open_requests() + requests = open_requests(self.context) if not requests: print("No open request.") for goal, served in requests: @@ -65,5 +65,5 @@ def run(self) -> None: print(f" {todo.id} due {hhmm(todo.deadline)}: {todo.text}") -def main(a: list[str]) -> None: - RequestVerb.parse(a, __doc__).apply() +def main(a, commander): + RequestVerb.parse(a, __doc__).apply(commander) diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 407366095a..7e1ed01912 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -7,38 +7,37 @@ from ..todos import Todo from .config import CommanderConfig -CONFIG = CommanderConfig.installed() -def broken_rules(after=0.0): +def broken_rules(commander, after=0.0): """Every standing rule that broke after ``after``, as Core's StandingRule family derives it.""" - read: StandingRulesRead = order.call(CommsStandingRulesTool(after=after)) + read: StandingRulesRead = commander.call(CommsStandingRulesTool(after=after)) return read.broken -def owed_orders() -> dict[str, list[Todo]]: +def owed_orders(commander) -> dict[str, list[Todo]]: """Each unit's open orders.""" owed: dict[str, list[Todo]] = {} - for todo in order.held_orders(): + for todo in commander.held_orders(): owed.setdefault(todo.held().owner, []).append(todo) return owed -def main(a): +def main(a, commander): turn, since = 0, 0.0 # since: the previous wake; a rule that stays broken is said once while True: now = time.time() - owed = owed_orders() + owed = owed_orders(commander) units = sorted(owed) if units: unit = units[turn % len(units)] turn += 1 - read = order.progress() + read = commander.progress() orders = ', '.join(f"{t.id} due {order.hhmm(t.deadline)} ({read[t.id].line})" for t in owed[unit]) - edits = judge.Edit.unjudged(order.COMMS, order.ME) + edits = judge.Edit.unjudged(commander.comms, commander.commander) waiting = sum(edit.thread == unit for edit in edits) print(f"ROTATE {datetime.now():%H:%M} {unit}: {orders}; unjudged {waiting} -> read its git state and newest turn, judge, push or hunt", flush=True) - for line in broken_rules(since): + for line in broken_rules(commander, since): print(f" RULE {line}", flush=True) since = now time.sleep(60) diff --git a/src/agent_comms/commander/rules.py b/src/agent_comms/commander/rules.py index f768f8b189..1095a4abc8 100644 --- a/src/agent_comms/commander/rules.py +++ b/src/agent_comms/commander/rules.py @@ -22,11 +22,8 @@ from ..tool_caller import ThreadCaller from ..tools import CommsRuleReviewTool, StandingRulesRead from .config import CommanderConfig -from .order import COMMS, CommanderVerb +from .order import CommanderVerb -CONFIG = CommanderConfig.installed() -ME = CONFIG.commander -SKILL = str(CONFIG.skill) PAPER = f'{CommanderConfig.packaged_rules()}/correct-maintenance.md' DRAWN = 3 # Ingrained defaults these rules exist to break; shown and answered at every review, never left to the draw. @@ -39,9 +36,9 @@ def principles() -> list[str]: return [l[2:].strip() for l in text.splitlines() if l.startswith('- ') and len(l) > 40] -def rules() -> list[str]: - found = [m.group(1).strip() for m in re.finditer(r'^\s*- \*\*(.+?)\*\*', open(SKILL).read(), re.M)] - for path in [path for directory in CONFIG.rules for path in sorted(glob.glob(f'{directory}/*.md'))]: +def rules(commander) -> list[str]: + found = [m.group(1).strip() for m in re.finditer(r'^\s*- \*\*(.+?)\*\*', open(str(commander.skill)).read(), re.M)] + for path in [path for directory in commander.rules for path in sorted(glob.glob(f'{directory}/*.md'))]: text = open(path).read() if re.search(r'^\s*type: feedback', text, re.M): desc = re.search(r'^description: (.+)$', text, re.M) @@ -49,32 +46,32 @@ def rules() -> list[str]: return found -def record(who: str, entry: RuleReviewEntry) -> None: - read: StandingRulesRead = ThreadCaller(who).invoke(CommsRuleReviewTool(entry=entry), COMMS) +def record(commander, who: str, entry: RuleReviewEntry) -> None: + read: StandingRulesRead = ThreadCaller(who).invoke(CommsRuleReviewTool(entry=entry), commander.comms) print('\n'.join(read.broken) or 'review complete; commander actions allowed') -def review(who: str = ME): - every = rules() +def review(commander, who: str): + every = rules(commander) for i, r in enumerate(every, 1): print(f'{i:3}. {r}') paper = principles() print('\nCorrect maintenance (paper 1; every judgment is measured against these):') for r in paper: print(f' - {r}') - drawn = random.sample(paper, DRAWN) if who != ME else random.sample(paper, 2) + random.sample(every, DRAWN - 2) + drawn = random.sample(paper, DRAWN) if who != commander.commander else random.sample(paper, 2) + random.sample(every, DRAWN - 2) pinned = [r for r in every if r.split(':', 1)[0] in PINNED] if len(pinned) != len(PINNED): sys.exit(f'Pinned rule missing from the rule directories: {sorted(set(PINNED) - {r.split(":", 1)[0] for r in pinned})}') drawn += [r for r in pinned if r not in drawn] - record(who, DrawnRuleReviewEntry(tuple(drawn), len(drawn) if who != ME else 2)) + record(commander, who, DrawnRuleReviewEntry(tuple(drawn), len(drawn) if who != commander.commander else 2)) print('\nAnswer each with what this tick does about it (agent-comms-commander rules answer N "..."):') for n, r in enumerate(drawn, 1): print(f' {n}. {r}') -def answer(n: int, text: str, who: str = ME): - record(who, AnsweredRuleReviewEntry(n, text)) +def answer(commander, n: int, text: str, who: str): + record(commander, who, AnsweredRuleReviewEntry(n, text)) class RulesVerb(CommanderVerb): @@ -85,7 +82,7 @@ class ReviewVerb(RulesVerb): help = "print the standing rules and draw the required review" def run(self) -> None: - review(self.who) + review(self.context, self.who) @dataclass(frozen=True, kw_only=True) @@ -95,18 +92,18 @@ class AnswerVerb(RulesVerb): text: str = option("text", help="how this tick obeys or breaks that rule") def run(self) -> None: - answer(self.n, self.text, self.who) + answer(self.context, self.n, self.text, self.who) class StaleVerb(RulesVerb): help = "show whether the acting commander's rule review is due" def run(self) -> None: - print('\n'.join(due.why for due in RulesDue.violations(COMMS, self.who)) or 'fresh') + print('\n'.join(due.why for due in RulesDue.violations(self.context.comms, self.who)) or 'fresh') -def main(a): +def main(a, commander): try: - RulesVerb.parse(a, __doc__).apply() + RulesVerb.parse(a, __doc__).apply(commander) except ValueError as refused: # Core owns what a review and an answer may say sys.exit(f'REFUSED: {refused}') diff --git a/src/agent_comms/commander/send_guard.py b/src/agent_comms/commander/send_guard.py index 642e93b2e8..15563a95af 100644 --- a/src/agent_comms/commander/send_guard.py +++ b/src/agent_comms/commander/send_guard.py @@ -25,7 +25,7 @@ from ..declared_family import DeclaredFamily from ..errors import RelationViolationError from ..todos import InstallDeadline -from .config import Commander, CommanderConfig +from .order import Commander Words = tuple[str, ...] @@ -51,7 +51,7 @@ class CommanderSession: commander: Commander def is_commander(self) -> bool: - name = self.commander.name + name = self.commander.commander process = self.commander.comms.registry.require(name).process_identity.require( f'{name} has no registered process to compare this session with') environment = Path(f'/proc/{process.pid}/environ').read_bytes().split(b'\0') @@ -63,7 +63,7 @@ def refusals(self, kind: type[TacticalCommand]) -> tuple[str, ...]: commander = self.is_commander() except (FileNotFoundError, PermissionError, RelationViolationError) as error: return (f'{kind.work.capitalize()} refused: the guard cannot tell whether this ' - f'session is {self.commander.name} ({error}).',) + f'session is {self.commander.commander} ({error}).',) return kind.install_refusals(self.commander) if commander else () @@ -92,7 +92,7 @@ def refused(cls, words: Words, session: CommanderSession) -> tuple[str, ...]: if 'send' not in words or 'agent-comms-commander' in {Path(word).name for word in words}: return () addressed = pairs & {('--to', unit) for unit in session.commander.units()} - if ('--from', session.commander.name) not in pairs or not addressed: + if ('--from', session.commander.commander) not in pairs or not addressed: return () return ('Raw send to a unit. Use agent-comms-commander order send ' '(orders, with deadline) or order say (no work).',) @@ -121,7 +121,7 @@ def ordered(shape: Words) -> bool: @classmethod def install_refusals(cls, commander: Commander) -> tuple[str, ...]: - return InstallDeadline.read(commander.comms.root).refusals(cls.work, commander.name) + return InstallDeadline.read(commander.comms.root).refusals(cls.work, commander.commander) @dataclass(frozen=True) @@ -152,9 +152,9 @@ class CarryCommand(TacticalCommand): shapes = (('cutover',),) -def main(a: list[str]) -> None: +def main(a, commander): hook = ClaudeBashHook.read() - session = CommanderSession(hook.session_id, Commander.of(CommanderConfig.installed())) + session = CommanderSession(hook.session_id, commander) reasons = GuardedCommand.refusals(hook.tool_input.command, session) if reasons: print(json.dumps({"hookSpecificOutput": { diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index b8408a26b7..53be70ccda 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -20,8 +20,6 @@ from . import capacity, judge from .config import CommanderConfig, Repo -CONFIG = CommanderConfig.installed() - @dataclass(frozen=True) class Detached(UndatedStandingRule): @@ -73,7 +71,7 @@ class WheelSpin(StandingRule, declared_name=None): def idle(cls, comms: Comms, commander: str) -> list[Thread]: """Units with a native turn in the last ten minutes and no order, parked threads aside.""" held = {todo.holder for todo in cls.todos(comms, commander)} - excluded = held | CONFIG.parked | {commander, ""} + excluded = held | CommanderConfig.read_root(comms.root).parked | {commander, ""} bound = TurnsSince(time.time() - cls.recent) return sorted((thread for thread in comms.registry.all_threads().values() if thread.name not in excluded @@ -158,7 +156,7 @@ class ForkNow(UndatedStandingRule): @classmethod def violations(cls, comms: Comms, commander: str) -> tuple[ForkNow, ...]: - load = capacity.Load.of(comms, commander) + load = capacity.Load.of(comms, commander, CommanderConfig.read_root(comms.root).state / 'forks') return (cls(load),) * load.short def line(self) -> str: @@ -224,7 +222,7 @@ def merges(cls, repo: Repo) -> Iterator[MergeGrowth]: @classmethod def violations(cls, comms: Comms, commander: str) -> tuple[MergeGrowth, ...]: - return tuple(growth for repo in CONFIG.repos for growth in cls.merges(repo) if growth.over) + return tuple(growth for repo in CommanderConfig.read_root(comms.root).repos for growth in cls.merges(repo) if growth.over) @property def over(self) -> bool: diff --git a/src/agent_comms/commander/tool_watch.py b/src/agent_comms/commander/tool_watch.py index 9c4e533495..9b8f93f32c 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -15,16 +15,16 @@ def thread_names(registry: Registration) -> dict[str, str]: if thread.session_file} -def enforce_storage(registry: Registration, thread: str, call: CodingCall) -> None: +def enforce_storage(commander, registry: Registration, thread: str, call: CodingCall) -> None: """Core judges its units and records each storage violation once.""" - if thread not in order.units() or thread not in registry: + if thread not in commander.units() or thread not in registry: return try: call.decide_storage(registry, thread) except SelectedToolDenied as violation: print(f'VIOLATION storage {thread}: {violation}', flush=True) - for todo in order.held_by(thread)[:1]: - order.strike(thread, todo.id, + for todo in commander.held_by(thread)[:1]: + commander.strike(thread, todo.id, f'{todo.id} broke the storage rule at a tool call: {violation}; ' 'storage waste is banned outright', 'tool_watch at tool call', kind=StorageStrikeTodo) @@ -41,21 +41,21 @@ def emit(turns, thread, final=''): print('\n'.join(lines)[:3000], flush=True) -def follow(registry: Registration, turns: dict[str, list[str]], thread: str, data: bytes) -> None: +def follow(commander, registry: Registration, turns: dict[str, list[str]], thread: str, data: bytes) -> None: for line in data.splitlines(keepends=True): entry = NativeEntry.read(line) for message in entry.messages: if message.assistant: - walk(registry, turns, thread, message) + walk(commander, registry, turns, thread, message) -def walk(registry: Registration, turns: dict[str, list[str]], thread: str, +def walk(commander, registry: Registration, turns: dict[str, list[str]], thread: str, message: PiMessage) -> None: for pi_call in message.tool_calls: coding_call = CodingCall.decode(pi_call.id, pi_call.name, pi_call.arguments) digest = coding_call.digest() turns[thread].append(digest) - enforce_storage(registry, thread, coding_call) + enforce_storage(commander, registry, thread, coding_call) if len(turns[thread]) >= 25: emit(turns, thread) if message.stop_reason.successful: @@ -63,15 +63,15 @@ def walk(registry: Registration, turns: dict[str, list[str]], thread: str, emit(turns, thread, text) -def main(a): - registry = order.COMMS.registry +def main(a, commander): + registry = commander.comms.registry names, offsets = thread_names(registry), {} turns = collections.defaultdict(list) last_names = time.time() while True: if time.time() - last_names > 60: names.update(thread_names(registry)); last_names = time.time() - for path in set(glob.glob(f'{order.COMMS.root}/native-sessions/*/*.jsonl')) | set(names): + for path in set(glob.glob(f'{commander.comms.root}/native-sessions/*/*.jsonl')) | set(names): try: st = os.stat(path) except FileNotFoundError: @@ -89,5 +89,5 @@ def main(a): if path not in names and time.time() - last_names > 10: names.update(thread_names(registry)); last_names = time.time() thread = names.get(path, path.split('/')[-2][:8]) - follow(registry, turns, thread, data[:cut]) + follow(commander, registry, turns, thread, data[:cut]) time.sleep(2) From 2b62f38ffeeb0b303dc3831bc0a11607d320c29b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 12:59:03 -0400 Subject: [PATCH 02/42] Bind commander consumers and checks to their selected context --- src/agent_comms/commander/capacity.py | 1 - src/agent_comms/commander/judge.py | 1 - src/agent_comms/commander/order.py | 2 +- src/agent_comms/commander/rotate.py | 1 - src/agent_comms/commander/tool_watch.py | 18 +++--- tests/conftest.py | 24 +++++--- tests/test_commander_context.py | 45 ++++++++++++++ tests/test_commander_problem_orders.py | 81 ++++++++++++++----------- tests/test_commander_provider_policy.py | 8 +-- tests/test_commander_run_rules.py | 2 +- tests/test_commander_tool_watch.py | 32 +++++----- tests/test_commander_verbs.py | 6 +- tests/test_order_progress.py | 17 +++--- tests/test_standing_rules.py | 8 ++- tests/test_tool_replies.py | 7 +++ 15 files changed, 163 insertions(+), 90 deletions(-) create mode 100644 tests/test_commander_context.py diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index 095e6f6016..2e2939a68c 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -13,7 +13,6 @@ from dataclasses import dataclass from ..comms import Comms -from .config import CommanderConfig PER_COMMANDER = 6 BEAT_AGE = 10 * 60 diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index bc63b4ed5f..740a238712 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -28,7 +28,6 @@ from . import order from ..cli_commands import option from .order import CommanderVerb, RuledVerb -from .config import CommanderConfig BRANCH = re.compile(r'\bisinstance\(|\bhasattr\(|^\s*(if|elif|match|case)\b|\bif\b.*\belse\b') diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index f1a5da9e5c..98d748625b 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -19,7 +19,7 @@ """ from __future__ import annotations -import argparse, asyncio, json, os, re, subprocess, sys, time +import argparse, asyncio, os, re, subprocess, sys, time from abc import abstractmethod from dataclasses import dataclass, field from pathlib import Path diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 7e1ed01912..7f278d8372 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -5,7 +5,6 @@ from . import judge, order from ..tools import CommsStandingRulesTool, StandingRulesRead from ..todos import Todo -from .config import CommanderConfig def broken_rules(commander, after=0.0): diff --git a/src/agent_comms/commander/tool_watch.py b/src/agent_comms/commander/tool_watch.py index 9b8f93f32c..cd72db88c4 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -1,7 +1,7 @@ """Follow live native sessions and print turn digests for commander judgment.""" import os, time, glob, collections +from pathlib import Path -from . import order from ..native_tool_call import CodingCall, SelectedToolDenied from ..native_entries import NativeEntry from ..pi_payloads import PiMessage, TextContent @@ -15,12 +15,14 @@ def thread_names(registry: Registration) -> dict[str, str]: if thread.session_file} -def enforce_storage(commander, registry: Registration, thread: str, call: CodingCall) -> None: +def enforce_storage(commander, thread: str, call: CodingCall) -> None: """Core judges its units and records each storage violation once.""" + registry = commander.comms.registry if thread not in commander.units() or thread not in registry: return try: - call.decide_storage(registry, thread) + if effects := call.storage_effects(Path(registry.require(thread).worktree)): + registry.decide_storage(thread, effects) except SelectedToolDenied as violation: print(f'VIOLATION storage {thread}: {violation}', flush=True) for todo in commander.held_by(thread)[:1]: @@ -41,21 +43,21 @@ def emit(turns, thread, final=''): print('\n'.join(lines)[:3000], flush=True) -def follow(commander, registry: Registration, turns: dict[str, list[str]], thread: str, data: bytes) -> None: +def follow(commander, turns: dict[str, list[str]], thread: str, data: bytes) -> None: for line in data.splitlines(keepends=True): entry = NativeEntry.read(line) for message in entry.messages: if message.assistant: - walk(commander, registry, turns, thread, message) + walk(commander, turns, thread, message) -def walk(commander, registry: Registration, turns: dict[str, list[str]], thread: str, +def walk(commander, turns: dict[str, list[str]], thread: str, message: PiMessage) -> None: for pi_call in message.tool_calls: coding_call = CodingCall.decode(pi_call.id, pi_call.name, pi_call.arguments) digest = coding_call.digest() turns[thread].append(digest) - enforce_storage(commander, registry, thread, coding_call) + enforce_storage(commander, thread, coding_call) if len(turns[thread]) >= 25: emit(turns, thread) if message.stop_reason.successful: @@ -89,5 +91,5 @@ def main(a, commander): if path not in names and time.time() - last_names > 10: names.update(thread_names(registry)); last_names = time.time() thread = names.get(path, path.split('/')[-2][:8]) - follow(commander, registry, turns, thread, data[:cut]) + follow(commander, turns, thread, data[:cut]) time.sleep(2) diff --git a/tests/conftest.py b/tests/conftest.py index a85a0ee05c..157a761fe9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -269,6 +269,12 @@ def commander(comms: Comms, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): comms.registry.declare(comms.registry.new_thread(name, frozenset({'comms'}), str(tmp_path), created_at=float(created))) comms.goals.update_goal("worker", SetGoalAction(text="Original commander brief", goal_id="ongoing")) comms.goals.update_goal("lead", SetGoalAction(text="Owner request", goal_id="request", order=RequestGoalOrder())) + import sys + from agent_comms.commander.config import CommanderConfig + CommanderConfig(commander="lead", root=comms.root, cli=Path(sys.executable).parent / "agent-comms", + state=tmp_path / "state", board=tmp_path / "board.md", narrative=tmp_path / "narrative.md", + skill=tmp_path / "skill.md", rules=(), repos=(), worktrees=tmp_path / "wt", scratch=tmp_path / "scratch", + disk=tmp_path).write(tmp_path) return comms, ThreadCaller("lead") @@ -287,14 +293,15 @@ def commander_script(commander, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) repo.mkdir() for command in (("init", "-q"), ("remote", "add", "origin", "https://github.com/OpenHCSDev/agent-comms.git")): subprocess.run(["git", "-C", str(repo), *command], check=True) - CommanderConfig(commander="lead", root=comms.root, cli=Path(sys.executable).parent / "agent-comms", - state=tmp_path / "state", board=tmp_path / "board.md", narrative=tmp_path / "narrative.md", - skill=tmp_path / "skill.md", rules=(), repos=(), worktrees=tmp_path / "wt", scratch=tmp_path / "scratch", - disk=tmp_path).write(tmp_path) - CommanderConfig.installed.cache_clear() from agent_comms.commander import order - yield importlib.reload(order), repo - CommanderConfig.installed.cache_clear() + context = order.Commander.of(CommanderConfig.read_root(comms.root)) + from agent_comms.rule_reviews import DrawnRuleReviewEntry, AnsweredRuleReviewEntry + from agent_comms.tools import CommsRuleReviewTool + from agent_comms.tool_caller import ThreadCaller + caller = ThreadCaller('lead') + caller.invoke(CommsRuleReviewTool(entry=DrawnRuleReviewEntry(('Read the command root from its one declared owner.',), 1)), comms) + caller.invoke(CommsRuleReviewTool(entry=AnsweredRuleReviewEntry(1, 'P701 binds each command to this private root and preserves its original input and records.')), comms) + yield context, repo @pytest.fixture @@ -303,8 +310,7 @@ def tool_watch(commander_script): import importlib from agent_comms.commander import judge, tool_watch - importlib.reload(judge) - return importlib.reload(tool_watch) + return tool_watch @pytest.fixture diff --git a/tests/test_commander_context.py b/tests/test_commander_context.py new file mode 100644 index 0000000000..e13f959835 --- /dev/null +++ b/tests/test_commander_context.py @@ -0,0 +1,45 @@ +"""A command owns its selected bus; importing its declarations opens nothing.""" +import os +import subprocess +import sys + +from agent_comms.commander.order import Commander +from agent_comms.commander.request import RequestVerb +from agent_comms.comms import Comms +from agent_comms.goal_actions import SetGoalAction +from agent_comms.goals import RequestGoalOrder + + +def test_importing_order_with_empty_home_reads_no_config_and_opens_no_store(tmp_path): + environment = {key: value for key, value in os.environ.items() + if not key.startswith(('AGENT_COMMS_', 'PI_COMMS_', 'PI_NATIVE_'))} + environment['HOME'] = str(tmp_path) + result = subprocess.run([sys.executable, '-B', '-c', ''' +from agent_comms.comms import Comms +from agent_comms.commander.config import CommanderConfig + +def forbidden(*args, **kwargs): + raise AssertionError('import opened a store or read configuration') + +Comms.__init__ = forbidden +CommanderConfig.read = classmethod(forbidden) +import agent_comms.commander.order +'''], env=environment, capture_output=True, text=True, timeout=20) + assert result.returncode == 0, result.stderr + assert not tuple(tmp_path.iterdir()) + + +def test_two_commands_in_one_process_keep_their_explicit_roots(commander_script, tmp_path, capsys): + first, _ = commander_script + other = Comms(tmp_path / 'other-root') + other.registry.declare(other.registry.new_thread('other-lead', frozenset(), str(tmp_path), created_at=3.0)) + other.goals.update_goal('other-lead', SetGoalAction( + text='The other root has its own owner request', goal_id='other-request', order=RequestGoalOrder())) + config = first.model_copy(update={'root': other.root, 'commander': 'other-lead'}) + second = Commander.of(config) + RequestVerb.parse(['list'], '').apply(first) + assert capsys.readouterr().out == 'request Owner request\n' + RequestVerb.parse(['list'], '').apply(second) + assert capsys.readouterr().out == 'other-request The other root has its own owner request\n' + RequestVerb.parse(['list'], '').apply(first) + assert capsys.readouterr().out == 'request Owner request\n' diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index 4350d0530a..0ce29b2399 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -19,6 +19,8 @@ from cutover.carry import CarryChain from agent_comms.change_report import FileLine +from agent_comms.commander import order as commander_order +from agent_comms.commander.config import Repo from agent_comms.channel_targets import CommandTarget from agent_comms.child_process import BoundedRun from agent_comms.comms import Comms @@ -292,7 +294,7 @@ def git(*args, at=None): met = comms.goals.todos.get('P1') assert (met.order.delivery.commit, met.order.delivery.delivered, met.order.delivery.at) == ( merge, delivered, RecordedEventTime(1000)) - assert script.struck(met, 'worker') == 0 + assert commander_order.struck(met, 'worker') == 0 git('checkout', '-q', '-b', 'side', delivered) git('commit', '-q', '--allow-empty', '-m', 'never merged', at=1500) @@ -308,7 +310,7 @@ def git(*args, at=None): script.done('P2', side, seen=working(comms)[0].screenshot) script.done('P2', late, seen=working(comms)[0].screenshot) struck = comms.goals.todos.get('P2') - assert script.struck(struck, 'worker') == 1 and late in struck.strikes[-1].reason + assert commander_order.struck(struck, 'worker') == 1 and late in struck.strikes[-1].reason def test_a_late_delivery_reads_its_lateness_to_the_second(commander, commander_script): @@ -329,7 +331,7 @@ def git(*args, at=None): delivered = git('rev-parse', 'HEAD') git('update-ref', 'refs/remotes/origin/main', delivered) script.done(pid, delivered, seen=working(comms)[0].screenshot) - assert script.struck(comms.goals.todos.get('P1'), 'worker') == 0 + assert commander_order.struck(comms.goals.todos.get('P1'), 'worker') == 0 (strike,) = comms.goals.todos.get('P2').strikes assert strike.reason.startswith(f'P2 order order-P2 late by 35 s: due {clock(2000)}, ' f'delivered {clock(2035)} as {delivered}') @@ -392,7 +394,7 @@ def test_commander_script_records_every_order_step_in_core(commander, commander_ # Without --clean this replaces the prior order and counts a send-back. script.send('worker', due, numbered, replaces=created.messages[-1].message_id) sent_back = comms.goals.todos.get('P1') - assert sent_back.order.id != created.order.id and script.struck(sent_back, 'worker') == 1 + assert sent_back.order.id != created.order.id and commander_order.struck(sent_back, 'worker') == 1 (link,) = sent_back.messages[-1:] sent = next(m.body for m in comms.views.history_page(CommandTarget.named('worker'), viewer='lead').messages if m.message_id == link.message_id) assert 'Set count.txt to 1.' in sent and 'Start now and push as you go.' in sent @@ -434,7 +436,7 @@ def test_resend_after_strike_counts_one_miss(commander, commander_script, monkey """A miss struck with `order strike` and then sent back is one strike; the next send-back strikes again.""" comms, _ = commander script, repo = commander_script - monkeypatch.setattr(script, 'post', lambda recipients, body: 'sent') + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: 'sent') (repo / 'count.txt').write_text('0\n') body = order_body(repo) problem(script, body) @@ -442,22 +444,22 @@ def test_resend_after_strike_counts_one_miss(commander, commander_script, monkey numbered = Path(body).read_text().replace('Problem: NEW', 'Problem: P1') script.send('worker', '23:59', numbered, replaces=comms.goals.todos.get('P1').messages[-1].message_id) - assert script.struck(comms.goals.todos.get('P1'), 'worker') == 1 + assert commander_order.struck(comms.goals.todos.get('P1'), 'worker') == 1 script.send('worker', '23:59', numbered, replaces=comms.goals.todos.get('P1').messages[-1].message_id) - assert script.struck(comms.goals.todos.get('P1'), 'worker') == 2 + assert commander_order.struck(comms.goals.todos.get('P1'), 'worker') == 2 def test_a_strike_and_a_clean_hunt_land_on_their_problem_argument_not_one_the_why_cites( commander, commander_script, monkeypatch): comms, _ = commander script, repo = commander_script - monkeypatch.setattr(script, 'post', lambda recipients, body: 'sent') + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: 'sent') (repo / 'count.txt').write_text('0\n') problem(script, order_body(repo)) create(commander, 'P2', owner='worker', deadline=time.time() + 3600) script.strike('worker', 'P2', 'P1 was compared with this miss: P2 left its count at zero and reported it fixed') - assert [script.struck(comms.goals.todos.get(pid), 'worker') for pid in ('P1', 'P2')] == [0, 1] + assert [commander_order.struck(comms.goals.todos.get(pid), 'worker') for pid in ('P1', 'P2')] == [0, 1] why = 'P1 rereads the counter per event; this read of the P2 path found it clean' script.hunt(f'{__file__}:1', why, '') assert comms.goals.todos.hunts_for_commander(comms.registry.require('lead').incarnation)[0].why == why @@ -471,12 +473,12 @@ def test_a_deletion_order_must_check_the_imports_of_every_repo_that_imports_the_ """P12 deleted acp_ingress.py while Toad imported it; the check must read Toad's imports too.""" comms, _ = commander script, repo = commander_script - monkeypatch.setattr(script, 'post', lambda recipients, body: 'sent') + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: 'sent') toad = tmp_path / 'toad' (toad / 'src').mkdir(parents=True) (toad / 'src' / 'app.py').write_text('from agent_comms.acp_ingress import Ingress\n') - monkeypatch.setattr(script, 'CONFIG', script.CONFIG.model_copy(update={'repos': ( - script.Repo(checkout=toad, github='OpenHCSDev/toad'),)})) + script = script.model_copy(update={'repos': ( + Repo(checkout=toad, github='OpenHCSDev/toad'),)}) (repo / 'acp_ingress.py').write_text('class Ingress: ...\n') edit = 'Delete src/agent_comms/acp_ingress.py.' with pytest.raises(SystemExit, match=rf"\['{toad}'\] import"): @@ -488,7 +490,7 @@ def test_a_deletion_order_must_check_the_imports_of_every_repo_that_imports_the_ def test_an_order_changing_tools_must_ratchet_both_roots(commander, commander_script, monkeypatch): comms, _ = commander script, repo = commander_script - monkeypatch.setattr(script, 'post', lambda recipients, body: 'sent') + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: 'sent') (repo / 'count.txt').write_text('0\n') edit = 'Count the measure in tools/debt_ratchet.py.' with pytest.raises(SystemExit, match='--root tools and with --root src'): @@ -614,11 +616,11 @@ def test_order_unstrike_voids_the_newest_live_strike_on_the_record(commander, co script, repo = commander_script create(commander, 'P1') posted = [] - monkeypatch.setattr(script, 'post', lambda recipients, body: posted.append((recipients, body))) + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: posted.append((recipients, body))) script.strike('worker', 'P1', 'P1 was struck for lateness against the wrong delivered commit') script.unstrike('P1', 'P1 was delivered before its deadline; the commander recorded the later merge as the delivery') current = comms.goals.todos.get('P1') - assert len(current.strikes) == 1 and script.struck(current, 'worker') == 0 + assert len(current.strikes) == 1 and commander_order.struck(current, 'worker') == 0 assert 'strike voided on P1' in (tmp_path / 'narrative.md').read_text() assert posted[-1][0] == VerdictAudience(('worker', 'lead')) and 'strike voided on P1' in posted[-1][1] with pytest.raises(SystemExit, match='no live strike'): @@ -631,10 +633,10 @@ def test_a_withdrawn_order_is_neither_open_nor_overdue_and_strikes_no_one(comman script, _ = commander_script create(commander, 'P1', deadline=1) posted = [] - monkeypatch.setattr(script, 'post', lambda recipients, body: posted.append((recipients, body))) + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: posted.append((recipients, body))) script.withdraw('P1', 'duplicate of P2, which owns the same fix', 'P2') withdrawn = comms.goals.todos.get('P1') - assert script.held_orders() == () and not withdrawn.pending and script.struck(withdrawn, 'worker') == 0 + assert script.held_orders() == () and not withdrawn.pending and commander_order.struck(withdrawn, 'worker') == 0 line = 'P1: order order-P1 withdrawn from worker, no strike: duplicate of P2, which owns the same fix (superseded by P2)' assert (tmp_path / 'board.md').read_text().count(line) == 1 assert line in (tmp_path / 'narrative.md').read_text() @@ -680,10 +682,10 @@ def test_order_fire_releases_every_open_problem_the_unit_holds(commander, comman script, _ = commander_script create(commander, 'P1') create(commander, 'P2', 'printf 2', 2) - script.FireVerb(unit='worker', why=FIRED).run() + commander_order.FireVerb(unit='worker', why=FIRED).apply(script) assert [(t.assignment, t.released[0].reason.text) for t in map(comms.goals.todos.get, ('P1', 'P2'))] == [(None, FIRED)] * 2 assert capsys.readouterr().out.splitlines() == [f'{pid}: released from worker: {FIRED}' for pid in ('P1', 'P2')] - script.board() + script.write_board() assert (tmp_path / 'board.md').read_text().count(f'unassigned: released from worker: {FIRED}') == 2 @@ -806,6 +808,7 @@ def test_an_open_request_alarms_until_an_order_serves_it_and_closes_only_once_no commander, commander_script): """The owner request is the commander's Goal: rotation names it while no open order serves it, each order stores a GoalRef to it, and it cannot be marked done or replaced while one is open.""" + script, _ = commander_script import importlib from agent_comms.commander import request comms, caller = commander @@ -820,13 +823,13 @@ def stalled(): assert todo.goal == GoalRef('lead', lead.created_at, 'request') assert stalled() == [] with pytest.raises(ValueError, match='still has open orders'): - request.RequestCompleteVerb(id='request', why='The owner request is finished').run() + request.RequestCompleteVerb(id='request', why='The owner request is finished').apply(script) with pytest.raises(ValueError, match='still open'): - request.RequestAddVerb(text='A second owner request while the first is open').run() + request.RequestAddVerb(text='A second owner request while the first is open').apply(script) CommsOrderDoneTool(problem='P1', order_id=comms.goals.todos.get('P1').order.id, delivery=OperationDelivery(), claims=working(comms)).apply(comms, caller) assert stalled() == [ 'REQUEST STALLED: request Owner request -> order its next step or mark it done'] - request.RequestCompleteVerb(id='request', why='The owner request is finished').run() + request.RequestCompleteVerb(id='request', why='The owner request is finished').apply(script) assert stalled() == [] @@ -1089,6 +1092,7 @@ def native_judge_edits(commander, judge, edits): def test_a_verdict_lands_on_the_order_its_message_was_sent_in(commander, commander_script): """The cited message's recorded order receives the verdict, whatever P-number its text names.""" + script, _ = commander_script import json from pathlib import Path @@ -1101,12 +1105,13 @@ def test_a_verdict_lands_on_the_order_its_message_was_sent_in(commander, command message = order_message(commander, 'P1', 'P2 was the last order; this one repairs the existing owner') judge.OkVerb(ids=(f'edit-1:{recorded[edit['id']].code()}',), msg=message, why='The change repairs the existing canonical state owner', - serves='Repair the existing owner', lines=('value = 2',)).run() + serves='Repair the existing owner', lines=('value = 2',)).apply(script) assert [v.tool_call_id for v in comms.goals.todos.get('P1').verdicts] == ['edit-1'] assert comms.goals.todos.get('P2').verdicts == () def test_judge_ok_writes_the_todo_not_a_second_store(commander, commander_script): + script, _ = commander_script import json import time from pathlib import Path @@ -1120,17 +1125,18 @@ def test_judge_ok_writes_the_todo_not_a_second_store(commander, commander_script message = order_message(commander, 'P1', 'P1 Repair the existing owner') judge.OkVerb(ids=tuple(f'{e["id"]}:{recorded[e["id"]].code()}' for e in edits), why='The changes repair the existing canonical state owner', msg=message, - serves='Repair the existing owner', lines=('value = 2',)).run() + serves='Repair the existing owner', lines=('value = 2',)).apply(script) saved = comms.goals.todos.get('P1') assert [v.tool_call_id for v in saved.verdicts] == ['edit-0', 'edit-1'] assert all(isinstance(v, AcceptedVerdict) for v in saved.verdicts) assert not saved.strikes assert importlib.reload(judge).Edit.unjudged(comms, 'lead') == [] - assert not (judge.CONFIG.state / 'verdicts.jsonl').exists() + assert not (script.state / 'verdicts.jsonl').exists() @pytest.mark.parametrize('verb', ['ok', 'back']) def test_judge_refuses_an_edit_without_an_open_order(commander, commander_script, verb): + script, _ = commander_script import json from pathlib import Path @@ -1145,11 +1151,12 @@ def test_judge_refuses_an_edit_without_an_open_order(commander, commander_script **fields, roast='r', fix='f', slop_kind='state', fact='the one value being stored', at=f'{__file__}:1', derive='delete-copy', by=f'{__file__}:1') with pytest.raises(SystemExit, match=r'unknown edit ids.*orphan-edit'): - command.run() - assert not (judge.CONFIG.state / 'verdicts.jsonl').exists() + command.apply(script) + assert not (script.state / 'verdicts.jsonl').exists() def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_script, monkeypatch): + script, _ = commander_script import json from pathlib import Path @@ -1160,13 +1167,13 @@ def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_s recorded = native_judge_edits(commander, judge, [edit]) message = order_message(commander, 'P1', 'P1 Repair the existing owner') posted = [] - monkeypatch.setattr(judge.order, 'post', lambda to, body: posted.append((to, body)) or 'sent') + monkeypatch.setattr(type(script), 'post', lambda self, to, body: posted.append((to, body)) or 'sent') why = 'The unit copied the same state into a second writable store' judge.BackVerb(ids=(f'bad-edit:{recorded[edit['id']].code()}',), why=why, msg=message, lines=('value = 2',), roast='This is stupid because the unit copied the same state instead of using its existing owner', fix='Delete the copied state and read the value from its existing owner', slop_kind='state', fact='the one value being stored', at=f'{__file__}:1', - derive='delete-copy', by=f'{__file__}:1').run() + derive='delete-copy', by=f'{__file__}:1').apply(script) saved = comms.goals.todos.get('P1') assert len(saved.verdicts) == len(saved.strikes) == 1 assert isinstance(saved.verdicts[0], SendBackVerdict) @@ -1175,7 +1182,7 @@ def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_s assert judge.Edit.unjudged(comms, 'lead') == [] audience = SendBackVerdict.audience.of('worker', saved.goal) assert [to for to, _ in posted] == [audience, audience] - assert not (judge.CONFIG.state / 'verdicts.jsonl').exists() + assert not (script.state / 'verdicts.jsonl').exists() @pytest.mark.parametrize('kind', [AcceptedVerdict, SendBackVerdict]) @@ -1263,6 +1270,7 @@ def test_a_problem_goal_shows_only_its_live_orders_through_convert_deliver_send_ def test_each_verdict_kind_requires_its_facts_and_records_its_verdict(commander, commander_script): """Every verdict member refuses a line missing any fact it declares, and its verdict reads back from the order.""" + script, _ = commander_script comms, caller = commander judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) assert set(judge.VerdictVerb.names()) == set(VERDICT_LINES) @@ -1277,7 +1285,9 @@ def test_each_verdict_kind_requires_its_facts_and_records_its_verdict(commander, if token.startswith('--') and (token != '--msg' or name == 'back')]: with pytest.raises(SystemExit): judge.JudgeVerb.parse(argv[:at] + argv[at + 2:], '') - operation = judge.JudgeVerb.parse(argv, '').operation(comms.goals.todos.get('P1'), f'toolu_{name}') + command = judge.JudgeVerb.parse(argv, '') + object.__setattr__(command, 'context', script) + operation = command.operation(comms.goals.todos.get('P1'), f'toolu_{name}') CommsVerdictTool(id='P1', operation=operation).apply(comms, caller) assert comms.goals.todos.get('P1').verdicts[-1] == operation @@ -1335,6 +1345,7 @@ def test_delivery_that_renames_the_decision_is_refused(commander, tmp_path): def test_send_guard_refuses_a_raw_send_only_to_the_commanders_units(commander, commander_script, monkeypatch, capsys): """worker holds one of lead's problems, so it is reached through orders; the owner, claude-ai and a peer commander hold none and are messaged directly.""" + script, _ = commander_script import io from agent_comms.commander import send_guard create(commander, 'P1') @@ -1342,7 +1353,7 @@ def test_send_guard_refuses_a_raw_send_only_to_the_commanders_units(commander, c def guarded(*targets): command = 'agent-comms send --from lead ' + ' '.join(f'--to {target}' for target in targets) + ' --body hi' monkeypatch.setattr(sys, 'stdin', io.StringIO(json.dumps({'session_id': 's', 'hook_event_name': 'PreToolUse', 'tool_input': {'command': command}}))) - send_guard.main([]) + send_guard.main([], script) return capsys.readouterr().out assert '"deny"' in guarded('worker') @@ -1365,6 +1376,7 @@ def test_tactical_refused_from_the_commander_session_only(commander, commander_s it is refused with the order to send worker, and once it is planned for lead it runs. The same command from another session, and an order whose body names the command, pass. Once lead's process is gone no session can be classified, so every session is refused with the read error.""" + script, _ = commander_script import io, os, subprocess from dataclasses import replace from agent_comms.child_process import ProcessIdentity @@ -1380,7 +1392,7 @@ def test_tactical_refused_from_the_commander_session_only(commander, commander_s def guarded(command, session): hook = {'session_id': session, 'hook_event_name': 'PreToolUse', 'tool_input': {'command': command}} monkeypatch.setattr(sys, 'stdin', io.StringIO(json.dumps(hook))) - send_guard.main([]) + send_guard.main([], script) return capsys.readouterr().out unplanned = json.loads(guarded(command, 's1'))['hookSpecificOutput']['permissionDecisionReason'] @@ -1404,13 +1416,14 @@ def guarded(command, session): def test_a_send_quoted_in_a_message_body_is_not_a_raw_send(commander, commander_script, monkeypatch, capsys): + script, _ = commander_script import io from agent_comms.commander import send_guard create(commander, 'P1') for command in ('agent-comms send --from user --to lead --body "agent-comms send --from lead --to worker"', 'agent-comms send --from user --to lead --body "x; agent-comms send --from lead --to worker"'): monkeypatch.setattr(sys, 'stdin', io.StringIO(json.dumps({'session_id': 's', 'hook_event_name': 'PreToolUse', 'tool_input': {'command': command}}))) - send_guard.main([]) + send_guard.main([], script) assert capsys.readouterr().out == '' diff --git a/tests/test_commander_provider_policy.py b/tests/test_commander_provider_policy.py index dda1d992d9..6c06183b64 100644 --- a/tests/test_commander_provider_policy.py +++ b/tests/test_commander_provider_policy.py @@ -21,16 +21,16 @@ def test_provider_declaration_controls_verbs_and_registration(commander, command before = comms.registry.store.path.read_bytes() for argv in (("stale",), ("review",), ("answer", "1", "This answer must never be recorded for the refused provider.")): with pytest.raises(RelationViolationError, match="worker.*openai-codex/friendly-renamed-model"): - rules.RulesVerb.parse([*argv, "--fork", "worker"], rules.__doc__).apply() + rules.RulesVerb.parse([*argv, "--fork", "worker"], rules.__doc__).apply(order) assert comms.registry.store.path.read_bytes() == before - assert not (order.CONFIG.state / "forks" / "worker.beat").exists() + assert not (order.state / "forks" / "worker.beat").exists() refused_home = tmp_path / "refused-registration" - config = order.CONFIG.model_copy(update={"commander": "worker"}) + config = order.model_copy(update={"commander": "worker"}) with pytest.raises(RelationViolationError, match="worker.*openai-codex/friendly-renamed-model"): config.write(refused_home) assert not refused_home.exists() assert comms.registry.require("worker").tags == frozenset({"unit", "installer"}) - rules.RulesVerb.parse(["stale", "--fork", "lead"], rules.__doc__).apply() + rules.RulesVerb.parse(["stale", "--fork", "lead"], rules.__doc__).apply(order) assert capsys.readouterr().out assert SelectedModel.decode("anthropic/codex-named-but-not-codex-provider").provider_type.may_command assert not SelectedModel.decode("openai-codex/any-model-id").provider_type.may_command diff --git a/tests/test_commander_run_rules.py b/tests/test_commander_run_rules.py index 12475225d0..455ceb3b41 100644 --- a/tests/test_commander_run_rules.py +++ b/tests/test_commander_run_rules.py @@ -55,4 +55,4 @@ def test_send_refuses_missing_run_but_mechanical_orders_are_exempt(commander_scr script.send('worker', '23:59', body.read_text(), **kwargs) assert script.todos() == () script.send('worker', '23:59', body.read_text(), mechanical=True, **kwargs) - assert isinstance(script.COMMS.goals.todos.get('P1').order.run, MechanicalOrderRun) + assert isinstance(script.comms.goals.todos.get('P1').order.run, MechanicalOrderRun) diff --git a/tests/test_commander_tool_watch.py b/tests/test_commander_tool_watch.py index ed60a9fdf0..25eada23b5 100644 --- a/tests/test_commander_tool_watch.py +++ b/tests/test_commander_tool_watch.py @@ -13,7 +13,7 @@ from test_commander_problem_orders import create -def test_pi_calls_through_the_watcher_digest(tool_watch, commander): +def test_pi_calls_through_the_watcher_digest(tool_watch, commander, commander_script): comms, _ = commander calls = [("edit", {"path": "a.py", "edits": [{"oldText": "old text", "newText": "new"}]}), ("write", {"path": "b.py", "content": "body\nline"}), @@ -23,7 +23,7 @@ def test_pi_calls_through_the_watcher_digest(tool_watch, commander): {"type": "toolCall", "id": f"c{index}", "name": name, "arguments": arguments} for index, (name, arguments) in enumerate(calls)]}}) turns = collections.defaultdict(list) - tool_watch.follow(comms.registry, turns, "worker", f"{line}\n".encode()) + tool_watch.follow(commander_script[0], turns, "worker", f"{line}\n".encode()) assert turns["worker"] == ["edit a.py: -old text +new", "write b.py: body line", "bash ls -la"] @@ -32,42 +32,42 @@ def test_coordination_call_digests_its_arguments(): assert call.digest() == 'comms_threads {}' -def test_a_malformed_line_raises_instead_of_skipping_history(tool_watch, commander): +def test_a_malformed_line_raises_instead_of_skipping_history(tool_watch, commander, commander_script): comms, _ = commander whole = json.dumps({"type": "message", "id": "m1", "timestamp": "2026-10-18T22:00:00.000Z", "message": {"role": "user", "content": "hello"}}) data = f'{whole}\n{{"type": "message"\n'.encode() with pytest.raises(json.JSONDecodeError): - tool_watch.follow(comms.registry, collections.defaultdict(list), "worker", data) + tool_watch.follow(commander_script[0], collections.defaultdict(list), "worker", data) -def test_storage_watch_judges_only_core_units(tool_watch, commander, monkeypatch): +def test_storage_watch_judges_only_core_units(tool_watch, commander, commander_script, monkeypatch): comms, _ = commander create(commander, 'P1') judged = [] monkeypatch.setattr(comms.registry.__class__, 'decide_storage', lambda self, thread, effects: judged.append(thread)) call = CodingCall.decode('write-1', 'write', {'path': 'a.py', 'content': 'body'}) - tool_watch.enforce_storage(comms.registry, 'lead', call) + tool_watch.enforce_storage(commander_script[0], 'lead', call) assert judged == [] - tool_watch.enforce_storage(comms.registry, 'worker', call) + tool_watch.enforce_storage(commander_script[0], 'worker', call) assert judged == ['worker'] -def test_storage_watch_reports_refused_call_without_judging(tool_watch, commander, monkeypatch, capsys): +def test_storage_watch_reports_refused_call_without_judging(tool_watch, commander, commander_script, monkeypatch, capsys): comms, _ = commander create(commander, 'P1') judged = [] monkeypatch.setattr(comms.registry.__class__, 'decide_storage', lambda self, thread, effects: judged.append(thread)) call = CodingCall.decode('refused-1', 'write', {'path': 'a.py'}) - tool_watch.enforce_storage(comms.registry, 'worker', call) + tool_watch.enforce_storage(commander_script[0], 'worker', call) assert judged == [] assert capsys.readouterr().out == '' assert call.digest() == 'REFUSED write: Missing required fields for WriteTool: content' -def test_watcher_decodes_each_call_once_and_logs_its_owned_digest(tool_watch, commander, monkeypatch): +def test_watcher_decodes_each_call_once_and_logs_its_owned_digest(tool_watch, commander, commander_script, monkeypatch): from agent_comms.tool_declaration import ToolDeclaration comms, _ = commander @@ -85,26 +85,26 @@ def counted(cls, arguments, *, call_id): {'type': 'toolCall', 'id': id, 'name': 'write', 'arguments': args} for id, args in calls]}}) turns = collections.defaultdict(list) - tool_watch.follow(comms.registry, turns, 'worker', (line + '\n').encode()) + tool_watch.follow(commander_script[0], turns, 'worker', (line + '\n').encode()) assert seen == ['write', 'write'] assert turns['worker'] == ['REFUSED write: Missing required fields for WriteTool: content', 'write b.py: body'] -def test_storage_violation_is_struck_once_across_watch_restart(tool_watch, commander, monkeypatch): +def test_storage_violation_is_struck_once_across_watch_restart(tool_watch, commander, commander_script, monkeypatch): comms, _ = commander before = create(commander, 'P1') announcements = [] - monkeypatch.setattr(tool_watch.order, 'on_record', lambda *args: announcements.append(args)) + monkeypatch.setattr(type(commander_script[0]), 'on_record', lambda *args: announcements.append(args)) def deny(self, thread, effects): raise SelectedToolDenied('The write creates a file outside the declared scratch directory') monkeypatch.setattr(comms.registry.__class__, 'decide_storage', deny) call = CodingCall.decode('write-1', 'write', {'path': 'a.py', 'content': 'body'}) - tool_watch.enforce_storage(comms.registry, 'worker', call) - tool_watch.enforce_storage(comms.registry, 'worker', call) + tool_watch.enforce_storage(commander_script[0], 'worker', call) + tool_watch.enforce_storage(commander_script[0], 'worker', call) restarted = importlib.reload(tool_watch) - restarted.enforce_storage(comms.registry, 'worker', call) + restarted.enforce_storage(commander_script[0], 'worker', call) after = comms.goals.todos.get('P1') assert after.revision == before.revision + 1 assert len(after.live_strikes('worker')) == 1 diff --git a/tests/test_commander_verbs.py b/tests/test_commander_verbs.py index 00b698b878..7e42ec8745 100644 --- a/tests/test_commander_verbs.py +++ b/tests/test_commander_verbs.py @@ -12,7 +12,7 @@ ("order", "strike worker --why 'P1 late' --caught 'lead at install'", {"unit": "worker", "why": "P1 late", "caught": "lead at install", "fork": None}), ("order", "strike worker --why 'P1 late'", - {"unit": "worker", "why": "P1 late", "caught": "lead at review"}), + {"unit": "worker", "why": "P1 late", "caught": "review"}), ("order", "done worker m1 --sha abc123 --seen proof.txt", {"unit": "worker", "message": "m1", "sha": "abc123", "seen": "proof.txt"}), ("order", "withdraw P68 --why 'duplicate of P67' --superseded-by P67", {"pid": "P68", "why": "duplicate of P67", "superseded_by": "P67"}), @@ -53,7 +53,7 @@ @pytest.mark.parametrize("tool,line,fields", DOCUMENTED) def test_every_verb_parses_its_documented_command_line(commander_script, tool, line, fields): - order, _ = commander_script + order = importlib.import_module("agent_comms.commander.order") module = (order if tool == "order" else importlib.reload(importlib.import_module(f"agent_comms.commander.{tool}"))) family = getattr(module, f"{tool.capitalize()}Verb") @@ -63,7 +63,7 @@ def test_every_verb_parses_its_documented_command_line(commander_script, tool, l def test_the_hunt_prompt_prints_a_command_line_the_hunt_parser_accepts(commander_script): - order, _ = commander_script + order = importlib.import_module("agent_comms.commander.order") printed = order.HuntVerb.due("a.py").split("then ", 1)[1] tool, verb, *usage = re.sub(r"\[[^]]*\]", "", printed).split() assert (tool, verb) == ("order", "hunt") diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 9b42b53046..c26b5a5700 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -7,6 +7,7 @@ import pytest from agent_comms.recorded_time import RecordedEventTime +from agent_comms.commander import order as commander_order from agent_comms.tools import CommsOrderDoneTool from agent_comms.todos import (CommittedProgress, DistanceReadings, MergedProgress, NoCommitProgress, InstallDeadline, InstallDue, OpenPrProgress, OperationDelivery, OrderProgress, PlannedInstallDeadline, @@ -124,7 +125,7 @@ def test_unread_github_is_explicit(checkout): 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. - monkeypatch.setattr(script.COMMS.registry, 'storage_rule', StorageRule(checkout_base=tmp_path / 'wt')) + monkeypatch.setattr(script.comms.registry, 'storage_rule', StorageRule(checkout_base=tmp_path / 'wt')) unit = tmp_path / 'wt' / 'worker--P1' unit.mkdir(parents=True) git(unit, 'init', '-q', '-b', 'fix/p1') @@ -132,7 +133,7 @@ def test_board_and_overdue_name_a_merge_awaiting_its_record(commander, commander merged = (PullRequest(8, 'fix/p1', 'b' * 40, 'd' * 40, 500.0, 500.0, orders=('order-P1',)),) monkeypatch.setattr(PullRequests, 'read', lambda self, repo, state, order: merged if state == 'merged' else ()) assert "awaiting record" in deadline_lines(script)[0] - script.board() + script.write_board() assert '| merged as ddddddddd, awaiting record |' in (tmp_path / 'board.md').read_text() @@ -140,12 +141,12 @@ def test_order_due_moves_the_deadline_in_core_and_on_the_record(commander, comma comms, _ = commander script, _ = commander_script before = create(commander, 'P1', deadline=1) - monkeypatch.setattr(script, 'post', lambda recipients, body: None) + monkeypatch.setattr(type(script), 'post', lambda self, recipients, body: None) with pytest.raises(SystemExit, match='8\\+ words'): script.due('P1', '23:59', 'too short') script.due('P1', '23:59', 'the owner widened the order to the rotation line too') moved = comms.goals.todos.get('P1') - assert script.hhmm(moved.deadline) == '23:59' and moved.revision == before.revision + 1 + assert commander_order.hhmm(moved.deadline) == '23:59' and moved.revision == before.revision + 1 assert 'P1 order order-P1 due ' in (tmp_path / 'narrative.md').read_text() @@ -266,7 +267,7 @@ def test_merged_and_delivered_orders_wait_on_the_one_install_deadline(commander, installed order does not. Nobody is struck, and a late install is named, not the orders.""" comms, caller = commander script, _ = commander_script - monkeypatch.setattr(script.COMMS.registry, 'storage_rule', StorageRule(checkout_base=tmp_path / 'wt')) + monkeypatch.setattr(script.comms.registry, 'storage_rule', StorageRule(checkout_base=tmp_path / 'wt')) unit = tmp_path / 'wt' / 'worker--P1' unit.mkdir(parents=True) git(unit, 'init', '-q', '-b', 'fix/p1') @@ -299,7 +300,7 @@ def test_unrecorded_merge_uses_order_identity_after_checkout_is_removed( script, _ = commander_script todo = create(commander, pid, deadline=1) storage = StorageRule(checkout_base=tmp_path / "wt") - monkeypatch.setattr(script.COMMS.registry, "storage_rule", storage) + monkeypatch.setattr(script.comms.registry, "storage_rule", storage) body_file = tmp_path / "pr-body.md" body_file.write_text(subject) bodies = [] @@ -312,7 +313,7 @@ def forge(command, **arguments): return original(command, **arguments) monkeypatch.setattr(subprocess, "run", forge) - script.PullRequestVerb(pid=pid, title=subject, body_file=str(body_file)).run() + commander_order.PullRequestVerb(pid=pid, title=subject, body_file=str(body_file)).apply(script) 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]) @@ -322,7 +323,7 @@ def forge(command, **arguments): assert not storage.order_checkout("worker", pid).exists() work = OrderProgress.read(todo, storage, PullRequests()) assert work == MergedProgress("d" * 9) - script.board() + script.write_board() assert "merged as ddddddddd, awaiting record" in (tmp_path / "board.md").read_text() for state in (work, PushedProgress("b" * 9, at=500.0), NoCommitProgress()): alarms = script.deadline_lines([todo], {pid: state}, time.time() + 100) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index d57aceed9d..e6fb430dee 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -37,7 +37,7 @@ def standing(commander, commander_script): for module in (judge, capacity): importlib.reload(module) module = importlib.reload(importlib.import_module("agent_comms.commander.standing")) - state = CommanderConfig.installed().state + state = CommanderConfig.read_root(commander[0].root).state return commander[0], state, module @@ -235,6 +235,7 @@ def test_fork_now_reads_the_load_of_the_bus_it_is_asked_about(commander, standin create(commander, problem) other = Comms(tmp_path / "other-bus") other.registry.declare(other.registry.new_thread("lead", frozenset(), str(tmp_path), created_at=1.0)) + CommanderConfig.read_root(comms.root).model_copy(update={"root": other.root}).write(tmp_path / 'other-home') def fork_now(bus): read = CommsStandingRulesTool().apply(bus, ThreadCaller("lead")) @@ -389,8 +390,9 @@ def git(*args): git("checkout", "-q", "main") git("merge", "-q", "--no-ff", "-m", f"Merge {branch}", branch) git("push", "-q", "origin", "main") - monkeypatch.setattr(module, "CONFIG", module.CONFIG.model_copy(update={"repos": ( - Repo(checkout=checkout, github="fleet/growth"),)})) + config = CommanderConfig.read_root(comms.root).model_copy(update={"repos": ( + Repo(checkout=checkout, github="fleet/growth"),)}) + config.write(tmp_path / 'growth-home') big = git("rev-parse", "main~1")[:9] assert lines(comms, module.MergeGrowth) == [ f"GROWTH fleet/growth {big} src +41/-0 (net +41) tests +0/-0: Merge big " diff --git a/tests/test_tool_replies.py b/tests/test_tool_replies.py index ad26844ba8..bc6af9fea4 100644 --- a/tests/test_tool_replies.py +++ b/tests/test_tool_replies.py @@ -38,6 +38,13 @@ def scenario(comms: Comms, project, monkeypatch): for created, name in enumerate(("lead", "worker", "idle"), 1): comms.registry.declare( comms.registry.new_thread(name, frozenset(), str(project), created_at=float(created))) + # Commander-specific rules read this bus's record, never the machine's installed config. + from agent_comms.commander.config import CommanderConfig + CommanderConfig(commander="lead", root=comms.root, cli=project / "agent-comms", + state=comms.root / "state", board=comms.root / "board.md", + narrative=comms.root / "narrative.md", skill=comms.root / "skill.md", + rules=(), repos=(), worktrees=comms.root / "wt", scratch=comms.root / "scratch", + disk=comms.root).write(project / "commander-home") comms.goals.update_goal("worker", SetGoalAction(text="Original commander brief", goal_id="ongoing")) comms.messaging.initialize_private_initial_protocol() comms.registry.declare(comms.registry.new_thread( From 1f21529436b8ef40ba18a6804f2ecd9d485954d5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 13:42:34 -0400 Subject: [PATCH 03/42] P701: construct command context and remove import stubs --- src/agent_comms/cli_commands.py | 14 +++--- src/agent_comms/commander/judge.py | 2 +- src/agent_comms/commander/order.py | 11 ++--- src/agent_comms/commander/request.py | 2 +- src/agent_comms/commander/rules.py | 2 +- src/agent_comms/commander/tool_watch.py | 3 +- tests/test_commander_context.py | 66 ++++++++++++++----------- tests/test_commander_problem_orders.py | 31 ++++++------ tests/test_commander_provider_policy.py | 4 +- tests/test_commander_tool_watch.py | 11 ++--- tests/test_commander_verbs.py | 6 +-- tests/test_order_progress.py | 2 +- 12 files changed, 79 insertions(+), 75 deletions(-) diff --git a/src/agent_comms/cli_commands.py b/src/agent_comms/cli_commands.py index 6fa80910f0..8e35f47bbd 100644 --- a/src/agent_comms/cli_commands.py +++ b/src/agent_comms/cli_commands.py @@ -799,16 +799,16 @@ def add_parser(cls, subparsers: Any) -> None: owner.add_argument(*flags, **options) @classmethod - def from_namespace(cls, args: argparse.Namespace) -> Self: - payload: dict[str, object] = {"kind": FieldCodec.encode(cls)} + def from_namespace(cls, args: argparse.Namespace, **context: object) -> Self: + values: dict[str, object] = dict(context) hints = get_type_hints(cls, include_extras=True) - for declared, _ in FieldCodec.record_fields(cls): - key = FieldCodec.wire_key(declared) + for declared, key in FieldCodec.record_fields(cls): value = getattr(args, key) normalize = CommandOption.of(declared).normalize - payload[key] = (normalize(value) if normalize is not None - else compile_decoder(hints[declared.name]).argument_payload(value)) - return cls.from_payload(payload) + raw = (normalize(value) if normalize is not None + else compile_decoder(hints[declared.name]).argument_payload(value)) + values.update(FieldCodec.field_plan(cls, declared.name).decoded({key: raw})) + return FieldCodec.construct(cls, values) @dataclass(frozen=True, kw_only=True) diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index 740a238712..97b5175acb 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -266,4 +266,4 @@ def report(self, todos: tuple[Todo, ...]) -> None: def main(args, commander): - JudgeVerb.parse(args or ['show'], __doc__).apply(commander) + JudgeVerb.parse(args or ['show'], __doc__, commander).apply() diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 98d748625b..bdd9308dab 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -710,15 +710,14 @@ def fire(self, unit: str, why: str) -> None: @dataclass(frozen=True, kw_only=True) class CommanderVerb(CommanderRole, DeclaredFamily, DeclaredOptions, affix="Verb"): """One commander verb: its options are its fields, its parser comes from them, and run() does the work.""" - context: Commander = field(init=False, repr=False) + context: Commander = field(repr=False, metadata={"wire_transient": True}) fork: str | None = option("--fork", default=None, help="a forked commander's name; keeps its beat fresh") @property def who(self) -> str: return self.fork or self.context.commander - def apply(self, ctx: Commander) -> None: - object.__setattr__(self, 'context', ctx) + def apply(self) -> None: self.gate() self.run() @@ -743,14 +742,14 @@ def usage(cls) -> str: return ' '.join(next(iter(verbs.choices.values())).format_usage().split()[1:]) @classmethod - def parse(cls, argv: list[str], description: str) -> CommanderVerb: + def parse(cls, argv: list[str], description: str, context: Commander) -> CommanderVerb: parser = argparse.ArgumentParser(prog=cls.tool(), description=description, formatter_class=argparse.RawDescriptionHelpFormatter) verbs = parser.add_subparsers(dest='command', required=True) for member in cls.members_with(cls): member.add_parser(verbs) args = parser.parse_args(argv) - return cls.member_named(args.command).from_namespace(args) + return cls.member_named(args.command).from_namespace(args, context=context) class RuledVerb(CommanderVerb): @@ -1031,4 +1030,4 @@ def run(self) -> None: def main(a, commander: Commander): - OrderVerb.parse(a, __doc__).apply(commander) + OrderVerb.parse(a, __doc__, commander).apply() diff --git a/src/agent_comms/commander/request.py b/src/agent_comms/commander/request.py index b0386477be..f1b8e107f6 100644 --- a/src/agent_comms/commander/request.py +++ b/src/agent_comms/commander/request.py @@ -66,4 +66,4 @@ def run(self) -> None: def main(a, commander): - RequestVerb.parse(a, __doc__).apply(commander) + RequestVerb.parse(a, __doc__, commander).apply() diff --git a/src/agent_comms/commander/rules.py b/src/agent_comms/commander/rules.py index 1095a4abc8..b77ec4467a 100644 --- a/src/agent_comms/commander/rules.py +++ b/src/agent_comms/commander/rules.py @@ -104,6 +104,6 @@ def run(self) -> None: def main(a, commander): try: - RulesVerb.parse(a, __doc__).apply(commander) + RulesVerb.parse(a, __doc__, commander).apply() except ValueError as refused: # Core owns what a review and an answer may say sys.exit(f'REFUSED: {refused}') diff --git a/src/agent_comms/commander/tool_watch.py b/src/agent_comms/commander/tool_watch.py index cd72db88c4..87f0b6e056 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -21,8 +21,7 @@ def enforce_storage(commander, thread: str, call: CodingCall) -> None: if thread not in commander.units() or thread not in registry: return try: - if effects := call.storage_effects(Path(registry.require(thread).worktree)): - registry.decide_storage(thread, effects) + registry.decide_storage(thread, call.storage_effects(Path(registry.require(thread).worktree))) except SelectedToolDenied as violation: print(f'VIOLATION storage {thread}: {violation}', flush=True) for todo in commander.held_by(thread)[:1]: diff --git a/tests/test_commander_context.py b/tests/test_commander_context.py index e13f959835..c5fea6286e 100644 --- a/tests/test_commander_context.py +++ b/tests/test_commander_context.py @@ -1,8 +1,11 @@ -"""A command owns its selected bus; importing its declarations opens nothing.""" +"""Command contexts and import isolation use real stores and real subprocesses.""" import os +import shutil import subprocess import sys +from pathlib import Path +from agent_comms.commander.config import CommanderConfig from agent_comms.commander.order import Commander from agent_comms.commander.request import RequestVerb from agent_comms.comms import Comms @@ -10,36 +13,41 @@ from agent_comms.goals import RequestGoalOrder -def test_importing_order_with_empty_home_reads_no_config_and_opens_no_store(tmp_path): - environment = {key: value for key, value in os.environ.items() - if not key.startswith(('AGENT_COMMS_', 'PI_COMMS_', 'PI_NATIVE_'))} - environment['HOME'] = str(tmp_path) - result = subprocess.run([sys.executable, '-B', '-c', ''' -from agent_comms.comms import Comms -from agent_comms.commander.config import CommanderConfig +def context(root: Path, name: str, text: str) -> Commander: + comms = Comms(root) + comms.registry.declare(comms.registry.new_thread(name, frozenset(), str(root), created_at=1.0)) + comms.goals.update_goal(name, SetGoalAction(text=text, goal_id='request', order=RequestGoalOrder())) + config = CommanderConfig(commander=name, root=root, cli=Path(sys.executable), + state=root / 'state', board=root / 'board.md', narrative=root / 'narrative.md', + skill=root / 'skill.md', rules=(), repos=(), worktrees=root / 'wt', + scratch=root / 'scratch', disk=root) + return Commander.of(config) -def forbidden(*args, **kwargs): - raise AssertionError('import opened a store or read configuration') -Comms.__init__ = forbidden -CommanderConfig.read = classmethod(forbidden) -import agent_comms.commander.order -'''], env=environment, capture_output=True, text=True, timeout=20) +def test_import_with_empty_home_preserves_the_copied_registry(tmp_path): + original = context(tmp_path / 'original', 'lead', 'Original owner request') + copied = tmp_path / 'copied' + shutil.copytree(original.comms.root, copied) + registry = copied / 'registry.json' + before = registry.read_bytes() + home = tmp_path / 'empty-home' + home.mkdir() + environment = {key: value for key, value in os.environ.items() + if not key.startswith(('AGENT_COMMS_', 'PI_COMMS_', 'PI_NATIVE_'))} + environment['HOME'] = str(home) + result = subprocess.run([sys.executable, '-B', '-c', 'import agent_comms.commander.order'], + env=environment, capture_output=True, text=True, timeout=20) assert result.returncode == 0, result.stderr - assert not tuple(tmp_path.iterdir()) + assert not tuple(home.iterdir()) + assert registry.read_bytes() == before -def test_two_commands_in_one_process_keep_their_explicit_roots(commander_script, tmp_path, capsys): - first, _ = commander_script - other = Comms(tmp_path / 'other-root') - other.registry.declare(other.registry.new_thread('other-lead', frozenset(), str(tmp_path), created_at=3.0)) - other.goals.update_goal('other-lead', SetGoalAction( - text='The other root has its own owner request', goal_id='other-request', order=RequestGoalOrder())) - config = first.model_copy(update={'root': other.root, 'commander': 'other-lead'}) - second = Commander.of(config) - RequestVerb.parse(['list'], '').apply(first) - assert capsys.readouterr().out == 'request Owner request\n' - RequestVerb.parse(['list'], '').apply(second) - assert capsys.readouterr().out == 'other-request The other root has its own owner request\n' - RequestVerb.parse(['list'], '').apply(first) - assert capsys.readouterr().out == 'request Owner request\n' +def test_two_commands_keep_their_constructor_context(tmp_path, capsys): + first = context(tmp_path / 'first', 'lead', 'First owner request') + second = context(tmp_path / 'second', 'other-lead', 'Other owner request') + for commander, expected in ((first, 'First owner request'), (second, 'Other owner request'), + (first, 'First owner request')): + command = RequestVerb.parse(['list'], '', commander) + assert command.context is commander + command.apply() + assert capsys.readouterr().out == f'request {expected}\n' diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index 03366d5fe3..0a5976de85 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -682,7 +682,7 @@ def test_order_fire_releases_every_open_problem_the_unit_holds(commander, comman script, _ = commander_script create(commander, 'P1') create(commander, 'P2', 'printf 2', 2) - commander_order.FireVerb(unit='worker', why=FIRED).apply(script) + commander_order.FireVerb(context=script, unit='worker', why=FIRED).apply() assert [(t.assignment, t.released[0].reason.text) for t in map(comms.goals.todos.get, ('P1', 'P2'))] == [(None, FIRED)] * 2 assert capsys.readouterr().out.splitlines() == [f'{pid}: released from worker: {FIRED}' for pid in ('P1', 'P2')] script.write_board() @@ -823,13 +823,13 @@ def stalled(): assert todo.goal == GoalRef('lead', lead.created_at, 'request') assert stalled() == [] with pytest.raises(ValueError, match='still has open orders'): - request.RequestCompleteVerb(id='request', why='The owner request is finished').apply(script) + request.RequestCompleteVerb(context=script, id='request', why='The owner request is finished').apply() with pytest.raises(ValueError, match='still open'): - request.RequestAddVerb(text='A second owner request while the first is open').apply(script) + request.RequestAddVerb(context=script, text='A second owner request while the first is open').apply() CommsOrderDoneTool(problem='P1', order_id=comms.goals.todos.get('P1').order.id, delivery=OperationDelivery(), claims=working(comms)).apply(comms, caller) assert stalled() == [ 'REQUEST STALLED: request Owner request -> order its next step or mark it done'] - request.RequestCompleteVerb(id='request', why='The owner request is finished').apply(script) + request.RequestCompleteVerb(context=script, id='request', why='The owner request is finished').apply() assert stalled() == [] @@ -1103,9 +1103,9 @@ def test_a_verdict_lands_on_the_order_its_message_was_sent_in(commander, command edit = dict(id='edit-1', at=1.0, thread='worker', path='value.py', hunks=[dict(old='value = 1', new='value = 2')]) recorded = native_judge_edits(commander, judge, [edit]) message = order_message(commander, 'P1', 'P2 was the last order; this one repairs the existing owner') - judge.OkVerb(ids=(f'edit-1:{recorded[edit['id']].code()}',), msg=message, + judge.OkVerb(context=script, ids=(f'edit-1:{recorded[edit['id']].code()}',), msg=message, why='The change repairs the existing canonical state owner', - serves='Repair the existing owner', lines=('value = 2',)).apply(script) + serves='Repair the existing owner', lines=('value = 2',)).apply() assert [v.tool_call_id for v in comms.goals.todos.get('P1').verdicts] == ['edit-1'] assert comms.goals.todos.get('P2').verdicts == () @@ -1123,9 +1123,9 @@ def test_judge_ok_writes_the_todo_not_a_second_store(commander, commander_script hunks=[dict(old='value = 1', new='value = 2')]) for n in range(2)] recorded = native_judge_edits(commander, judge, edits) message = order_message(commander, 'P1', 'P1 Repair the existing owner') - judge.OkVerb(ids=tuple(f'{e["id"]}:{recorded[e["id"]].code()}' for e in edits), + judge.OkVerb(context=script, ids=tuple(f'{e["id"]}:{recorded[e["id"]].code()}' for e in edits), why='The changes repair the existing canonical state owner', msg=message, - serves='Repair the existing owner', lines=('value = 2',)).apply(script) + serves='Repair the existing owner', lines=('value = 2',)).apply() saved = comms.goals.todos.get('P1') assert [v.tool_call_id for v in saved.verdicts] == ['edit-0', 'edit-1'] assert all(isinstance(v, AcceptedVerdict) for v in saved.verdicts) @@ -1147,11 +1147,11 @@ def test_judge_refuses_an_edit_without_an_open_order(commander, commander_script fields = dict(ids=('orphan-edit:0000',), why='The changes repair the existing canonical state owner', lines=('value = 2',), msg=comms.messaging.send('lead', 'worker', 'P1 Repair the existing owner').message.message_id) - command = judge.OkVerb(**fields, serves='Repair the existing owner') if verb == 'ok' else judge.BackVerb( - **fields, roast='r', fix='f', slop_kind='state', fact='the one value being stored', at=f'{__file__}:1', + command = judge.OkVerb(context=script, **fields, serves='Repair the existing owner') if verb == 'ok' else judge.BackVerb( + context=script, **fields, roast='r', fix='f', slop_kind='state', fact='the one value being stored', at=f'{__file__}:1', derive='delete-copy', by=f'{__file__}:1') with pytest.raises(SystemExit, match=r'unknown edit ids.*orphan-edit'): - command.apply(script) + command.apply() assert not (script.state / 'verdicts.jsonl').exists() @@ -1169,11 +1169,11 @@ def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_s posted = [] monkeypatch.setattr(type(script), 'post', lambda self, to, body: posted.append((to, body)) or 'sent') why = 'The unit copied the same state into a second writable store' - judge.BackVerb(ids=(f'bad-edit:{recorded[edit['id']].code()}',), why=why, msg=message, + judge.BackVerb(context=script, ids=(f'bad-edit:{recorded[edit['id']].code()}',), why=why, msg=message, lines=('value = 2',), roast='This is stupid because the unit copied the same state instead of using its existing owner', fix='Delete the copied state and read the value from its existing owner', slop_kind='state', fact='the one value being stored', at=f'{__file__}:1', - derive='delete-copy', by=f'{__file__}:1').apply(script) + derive='delete-copy', by=f'{__file__}:1').apply() saved = comms.goals.todos.get('P1') assert len(saved.verdicts) == len(saved.strikes) == 1 assert isinstance(saved.verdicts[0], SendBackVerdict) @@ -1284,9 +1284,8 @@ def test_each_verdict_kind_requires_its_facts_and_records_its_verdict(commander, for at in [i for i, token in enumerate(argv) if token.startswith('--') and (token != '--msg' or name == 'back')]: with pytest.raises(SystemExit): - judge.JudgeVerb.parse(argv[:at] + argv[at + 2:], '') - command = judge.JudgeVerb.parse(argv, '') - object.__setattr__(command, 'context', script) + judge.JudgeVerb.parse(argv[:at] + argv[at + 2:], '', script) + command = judge.JudgeVerb.parse(argv, '', script) operation = command.operation(comms.goals.todos.get('P1'), f'toolu_{name}') CommsVerdictTool(id='P1', operation=operation).apply(comms, caller) assert comms.goals.todos.get('P1').verdicts[-1] == operation diff --git a/tests/test_commander_provider_policy.py b/tests/test_commander_provider_policy.py index 6c06183b64..e2c4d9f5e7 100644 --- a/tests/test_commander_provider_policy.py +++ b/tests/test_commander_provider_policy.py @@ -21,7 +21,7 @@ def test_provider_declaration_controls_verbs_and_registration(commander, command before = comms.registry.store.path.read_bytes() for argv in (("stale",), ("review",), ("answer", "1", "This answer must never be recorded for the refused provider.")): with pytest.raises(RelationViolationError, match="worker.*openai-codex/friendly-renamed-model"): - rules.RulesVerb.parse([*argv, "--fork", "worker"], rules.__doc__).apply(order) + rules.RulesVerb.parse([*argv, "--fork", "worker"], rules.__doc__, order).apply() assert comms.registry.store.path.read_bytes() == before assert not (order.state / "forks" / "worker.beat").exists() refused_home = tmp_path / "refused-registration" @@ -30,7 +30,7 @@ def test_provider_declaration_controls_verbs_and_registration(commander, command config.write(refused_home) assert not refused_home.exists() assert comms.registry.require("worker").tags == frozenset({"unit", "installer"}) - rules.RulesVerb.parse(["stale", "--fork", "lead"], rules.__doc__).apply(order) + rules.RulesVerb.parse(["stale", "--fork", "lead"], rules.__doc__, order).apply() assert capsys.readouterr().out assert SelectedModel.decode("anthropic/codex-named-but-not-codex-provider").provider_type.may_command assert not SelectedModel.decode("openai-codex/any-model-id").provider_type.may_command diff --git a/tests/test_commander_tool_watch.py b/tests/test_commander_tool_watch.py index 25eada23b5..56c1eb3d4a 100644 --- a/tests/test_commander_tool_watch.py +++ b/tests/test_commander_tool_watch.py @@ -54,15 +54,14 @@ def test_storage_watch_judges_only_core_units(tool_watch, commander, commander_s assert judged == ['worker'] -def test_storage_watch_reports_refused_call_without_judging(tool_watch, commander, commander_script, monkeypatch, capsys): +def test_storage_watch_reports_refused_call_without_judging(tool_watch, commander, commander_script, capsys): comms, _ = commander - create(commander, 'P1') - judged = [] - monkeypatch.setattr(comms.registry.__class__, 'decide_storage', - lambda self, thread, effects: judged.append(thread)) + before = create(commander, 'P1') + registry_before = comms.registry.store.path.read_bytes() call = CodingCall.decode('refused-1', 'write', {'path': 'a.py'}) tool_watch.enforce_storage(commander_script[0], 'worker', call) - assert judged == [] + assert comms.goals.todos.get('P1') == before + assert comms.registry.store.path.read_bytes() == registry_before assert capsys.readouterr().out == '' assert call.digest() == 'REFUSED write: Missing required fields for WriteTool: content' diff --git a/tests/test_commander_verbs.py b/tests/test_commander_verbs.py index 7e42ec8745..a7af8af6f6 100644 --- a/tests/test_commander_verbs.py +++ b/tests/test_commander_verbs.py @@ -57,7 +57,7 @@ def test_every_verb_parses_its_documented_command_line(commander_script, tool, l module = (order if tool == "order" else importlib.reload(importlib.import_module(f"agent_comms.commander.{tool}"))) family = getattr(module, f"{tool.capitalize()}Verb") - verb = family.parse(shlex.split(line), "") + verb = family.parse(shlex.split(line), "", commander_script[0]) assert verb.declared_name == line.split()[0] assert {name: getattr(verb, name) for name in fields} == fields @@ -68,7 +68,7 @@ def test_the_hunt_prompt_prints_a_command_line_the_hunt_parser_accepts(commander tool, verb, *usage = re.sub(r"\[[^]]*\]", "", printed).split() assert (tool, verb) == ("order", "hunt") argv = [verb, *(re.sub(r"^[A-Z:]+$", "x", token) for token in usage)] - parsed = order.OrderVerb.parse(argv, "") + parsed = order.OrderVerb.parse(argv, "", commander_script[0]) assert type(parsed) is order.HuntVerb and parsed.sent == "" - ordered = order.OrderVerb.parse([*argv, "--sent", "existing-order"], "") + ordered = order.OrderVerb.parse([*argv, "--sent", "existing-order"], "", commander_script[0]) assert type(ordered) is order.HuntVerb and ordered.sent == "existing-order" diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index c26b5a5700..be187a36d0 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -313,7 +313,7 @@ def forge(command, **arguments): return original(command, **arguments) monkeypatch.setattr(subprocess, "run", forge) - commander_order.PullRequestVerb(pid=pid, title=subject, body_file=str(body_file)).apply(script) + commander_order.PullRequestVerb(context=script, pid=pid, title=subject, body_file=str(body_file)).apply() 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]) From b1d071de4d2b92faf1b4f3a671dfedd2a7e7b289 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:06:49 -0400 Subject: [PATCH 04/42] P701: inherit root services and give absent command selections named answers --- src/agent_comms/cli_commands.py | 53 +++++-- src/agent_comms/commander/capacity.py | 5 +- src/agent_comms/commander/context.py | 106 +++++++++++++ src/agent_comms/commander/judge.py | 26 +++- src/agent_comms/commander/order.py | 194 +++++++++--------------- src/agent_comms/commander/rotate.py | 4 +- src/agent_comms/commander/rules.py | 7 +- src/agent_comms/commander/selections.py | 78 ++++++++++ src/agent_comms/commander/standing.py | 8 +- src/agent_comms/commander/tool_watch.py | 10 +- 10 files changed, 338 insertions(+), 153 deletions(-) create mode 100644 src/agent_comms/commander/context.py create mode 100644 src/agent_comms/commander/selections.py diff --git a/src/agent_comms/cli_commands.py b/src/agent_comms/cli_commands.py index 8e35f47bbd..fc968b9535 100644 --- a/src/agent_comms/cli_commands.py +++ b/src/agent_comms/cli_commands.py @@ -28,19 +28,15 @@ from .activity import ActivityState from .catalog_document import CatalogDocument from .change_report import ChangeReport -from .check_root import CheckRoot from .channel_management import KeepThreadsTagDisposition, TagChangeResult, TagDisposition from .channel_targets import ChannelSelection, CommandTarget, NoChannel, OneChannel, ResolvedChannel -from .channels import ANY_CHANNEL -from .channels import Channel +from .channels import ANY_CHANNEL, Channel +from .check_root import CheckRoot from .child_process import NoProcess, ProcessIdentity, ProcessState from .cli_bridges.claude import ClaudeTranscript from .cli_bridges.history import History, OfflineHistory, OnlineHistory from .cli_protocol import ArrivedMessage, EarlierItems from .cli_result import CliResult -from .history_result import HistoryPageResult -from .send_result import SendResult -from .maintenance_barrier import MaintenanceHeldError from .command import Command from .comms import Comms from .config_options import ( @@ -63,22 +59,25 @@ SelectableWireExportFormat, ) from .field_codec import FieldCodec, PathText, WireForm, compile_decoder -from .history_views import CallingThreadTarget, NamedThreadTarget, PollTarget -from .presentation import CountedRead, InboxRead, ThreadRecord, ThreadView, UncountedRead from .harness_install import Harness, HarnessInstalled +from .history_result import HistoryPageResult +from .history_views import CallingThreadTarget, NamedThreadTarget, PollTarget from .importing import ImportedSessionMetadata, ImportLimits, ThreadImported +from .maintenance_barrier import MaintenanceHeldError from .message_page import PageBounds from .message_reference import MessageReference -from .page_traversal import OlderTraversal, PageCursor from .messages import InfoKind, Message, MessageKind from .native_settings import NativeThreadDefaults from .native_turn_context import NativeContextData from .owner_lifecycle import OwnerRestartResult, OwnerStartResult +from .page_traversal import OlderTraversal, PageCursor from .pi_vocabulary import ServiceTier, ThinkingLevelField +from .presentation import CountedRead, InboxRead, ThreadRecord, ThreadView, UncountedRead from .read_basis import OnReadWatchAcknowledgment, WatchAcknowledgment from .registry_document import RegistrySnapshot from .runtime import RuntimeConnection, socket_path from .schema_report import StoreSchemaReport, StoreSchemaReports +from .send_result import SendResult from .shared_ledger import LedgerDocument from .thread_execution import ( ArchiveOwnerControl, @@ -237,6 +236,30 @@ def value_schema(self) -> dict[str, object]: return {**FieldCodec.value_schema(self.annotation), 'configuration': FieldCodec.encode(self.catalog)} +class CommandNormalization(ABC): + @abstractmethod + def namespace(self, value: object, annotation: object) -> object: ... + @abstractmethod + def editor(self, value: object, annotation: object) -> object: ... + + +@dataclass(frozen=True) +class DecoderNormalization(CommandNormalization): + def namespace(self, value: object, annotation: object) -> object: + return compile_decoder(annotation).argument_payload(value) + def editor(self, value: object, annotation: object) -> object: + return compile_decoder(annotation).decode_editor(value) + + +@dataclass(frozen=True) +class CustomNormalization(CommandNormalization): + convert: Callable[..., object] + def namespace(self, value: object, annotation: object) -> object: + return self.convert(value) + def editor(self, value: object, annotation: object) -> object: + return self.convert(value) + + @dataclass(frozen=True) class CommandOption: """The declared CLI and editor properties of one command field.""" @@ -247,7 +270,7 @@ class CommandOption: flags: tuple[str, ...] parser_options: Mapping[str, Any] group: str | None - normalize: Callable[..., Any] | None + normalization: CommandNormalization parser_default: Any parser_default_factory: Any editor: type[TargetField] @@ -281,7 +304,8 @@ def option( """One field owns both its CLI projection and JSON boundary conversion.""" metadata: dict[object, object] = {CommandOption: CommandOption( multiline=multiline, target_bound=target_bound, editor_format=editor_format, - flags=flags, parser_options=parser_options, group=group, normalize=normalize, + flags=flags, parser_options=parser_options, group=group, + normalization=CustomNormalization(normalize) if normalize else DecoderNormalization(), parser_default=parser_default, parser_default_factory=parser_default_factory, editor=CatalogTargetField if catalog is not None else editor, catalog=catalog, )} @@ -804,9 +828,7 @@ def from_namespace(cls, args: argparse.Namespace, **context: object) -> Self: hints = get_type_hints(cls, include_extras=True) for declared, key in FieldCodec.record_fields(cls): value = getattr(args, key) - normalize = CommandOption.of(declared).normalize - raw = (normalize(value) if normalize is not None - else compile_decoder(hints[declared.name]).argument_payload(value)) + raw = CommandOption.of(declared).normalization.namespace(value, hints[declared.name]) values.update(FieldCodec.field_plan(cls, declared.name).decoded({key: raw})) return FieldCodec.construct(cls, values) @@ -926,8 +948,7 @@ def editor_arguments(cls, values: dict[str, str | None]) -> dict[str, object]: result = {} for key, text in values.items(): item = declared[key] - normalize = CommandOption.of(item).normalize - result[key] = normalize(text) if normalize else compile_decoder(hints[item.name]).decode_editor(text) + result[key] = CommandOption.of(item).normalization.editor(text, hints[item.name]) return result def confirmation(self) -> str: diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index 2e2939a68c..3541a70326 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -9,7 +9,10 @@ """ from __future__ import annotations -import math, os, sys, time +import math +import os +import sys +import time from dataclasses import dataclass from ..comms import Comms diff --git a/src/agent_comms/commander/context.py b/src/agent_comms/commander/context.py new file mode 100644 index 0000000000..8982c5ab28 --- /dev/null +++ b/src/agent_comms/commander/context.py @@ -0,0 +1,106 @@ +"""The selected root's services, shared by commander command families.""" +from __future__ import annotations + +import subprocess +import time +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Self + +from pydantic import PrivateAttr + +from ..comms import Comms +from ..external_transcript import ExternalTranscript +from ..goals import OrderCheck +from ..messages import Message +from ..todos import OrderProgress, Todo, VerdictAudience +from ..tool_caller import ThreadCaller +from ..tools import CommsSendTool, CommsTodoListTool, TodoList +from .config import CommanderConfig + + +class CommanderReply(ABC): + @abstractmethod + def suffix(self) -> str: ... + + +@dataclass(frozen=True) +class NoCommanderReply(CommanderReply): + def suffix(self) -> str: + return '; no reply since the order' + + +@dataclass(frozen=True) +class RecordedCommanderReply(CommanderReply): + message: Message + + def suffix(self) -> str: + latest = self.message + at = time.strftime('%H:%M', time.localtime(latest.timestamp)) + return (f'; REPLIED {at} [{latest.message_id}]: ' + f'{" ".join(latest.body.split())[:160]} -- read it before pushing') + + +class CommanderContext(CommanderConfig): + """A command's selected configuration and opened bus, never an import-time singleton.""" + + _comms: Comms = PrivateAttr() + + def model_post_init(self, context: object) -> None: + self._comms = Comms(self.root) + + @property + def comms(self) -> Comms: + return self._comms + + @classmethod + def of(cls, config: CommanderConfig) -> Self: + return cls.model_construct(**config.model_dump()) + + def call(self, tool): + """Every write uses the selected commander's identity and root.""" + return ThreadCaller(self.commander).invoke(tool, self.comms) + + def post(self, audience: VerdictAudience, body: str) -> str: + target = audience.threads[0] if len(audience.threads) == 1 else '#comms' + return self.call(CommsSendTool(sender=self.commander, target=target, body=body, + audience=audience.threads)).id + + def todos(self): + return self.comms.goals.todos.for_commander( + self.comms.registry.require(self.commander).incarnation) + + def held_orders(self) -> tuple[Todo, ...]: + return self.comms.goals.todos.held_for_commander( + self.comms.registry.require(self.commander).incarnation) + + def held_by(self, unit: str) -> tuple[Todo, ...]: + return self.comms.goals.todos.held_by( + self.comms.registry.require(self.commander).incarnation, unit) + + def units(self) -> frozenset[str]: + return self.comms.goals.todos.units( + self.comms.registry.require(self.commander).incarnation) + + def progress(self) -> dict[str, OrderProgress]: + listed: TodoList = self.call(CommsTodoListTool()) + return listed.progress + + def importers(self, modules: set[str], check: OrderCheck) -> list[str]: + pattern = rf'^\s*(from|import) agent_comms\b.*\b({"|".join(sorted(modules))})\b' + return [str(repo.checkout) for repo in self.repos + if repo.checkout != Path(check.directory).resolve() + and str(repo.checkout) not in check.command + and subprocess.run(['grep', '-rqE', '--include=*.py', pattern, + str(repo.checkout / repo.source)]).returncode == 0] + + def last_activity(self, unit: str): + return ExternalTranscript(self.comms.root, + self.comms.registry.require(unit).incarnation).last_tool_call() + + def reply(self, unit: str, since: float) -> CommanderReply: + with self.comms.bus.log.opened_wire_snapshot() as source: + return next((RecordedCommanderReply(message) for message in source.newest_messages() + if message.sender == unit and message.timestamp > since + and message.target in (self.commander, '#comms')), NoCommanderReply()) diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index 97b5175acb..37dec72fc7 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -15,21 +15,37 @@ A verdict without a reason is refused; a send-back must cite a real message from the commander to that unit. Nothing here judges code; it only refuses to let an edit pass unread. """ -import hashlib, json, os, re, sys, time +import hashlib +import json +import os +import re +import sys +import time from abc import abstractmethod from dataclasses import asdict, dataclass +from ..cli_commands import option from ..comms import Comms -from ..todos import AcceptedVerdict, PeerAcceptedVerdict, BluntText, OrderVerdict, SendBackVerdict, StrikeReason, Todo, Slop, SlopKind, Derivation, SupersededVerdict -from ..tools import CommsUnjudgedTool, CommsVerdictTool from ..field_codec import FieldCodec +from ..todos import ( + AcceptedVerdict, + BluntText, + Derivation, + OrderVerdict, + PeerAcceptedVerdict, + SendBackVerdict, + Slop, + SlopKind, + StrikeReason, + SupersededVerdict, + Todo, +) from ..tool_caller import ThreadCaller from ..tool_declaration import EditCall, ReplacementEdit +from ..tools import CommsUnjudgedTool, CommsVerdictTool from . import order -from ..cli_commands import option from .order import CommanderVerb, RuledVerb - BRANCH = re.compile(r'\bisinstance\(|\bhasattr\(|^\s*(if|elif|match|case)\b|\bif\b.*\belse\b') diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index bdd9308dab..9560c3fdee 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -19,38 +19,75 @@ """ from __future__ import annotations -import argparse, asyncio, os, re, subprocess, sys, time +import argparse +import asyncio +import os +import re +import subprocess +import sys +import time from abc import abstractmethod from dataclasses import dataclass, field from pathlib import Path from uuid import uuid4 from ..change_report import FileLine -from ..comms import Comms -from ..child_process import BoundedRun +from ..child_process import BoundedRun, CheckPrinted from ..cli_commands import DeclaredOptions, option from ..declared_family import DeclaredFamily -from ..external_transcript import ExternalTranscript -from ..goal_actions import CreateProblemGoalAction, OrderGoalAction -from ..child_process import CheckPrinted +from ..goal_actions import CreateProblemGoalAction from ..goals import OrderCheck, ProblemGoalOrder from ..native_tool_call import SelectedToolDenied -from ..messages import Message from ..plan_phases import OrderPhase, UnplannedPhase from ..progress_claims import MergedClaim, WorkingClaim from ..recorded_time import RecordedEventTime from ..standing_rules import HuntDue, RulesDue +from ..todos import ( + ApplicationOrderRun, + AssignTodo, + BluntText, + CleanHunt, + CommitDelivery, + DirectiveText, + FileDeliveryEvidence, + InstallDeadline, + LocatedProblemFinding, + MechanicalOrderRun, + NoDeliveryEvidence, + NotifiedFirstCallEvent, + ObservedFirstCallEvent, + OperationDelivery, + OrderDeletion, + OrderedHunt, + OrderMessage, + OrderProgress, + OrderRun, + PendingProblemOrder, + PlannedInstallDeadline, + RecordFirstCallTodo, + Slop, + StrikeReason, + StrikeTodo, + Todo, + UnlocatedProblemFinding, + VerdictAudience, + VoidStrikeTodo, +) from ..tool_caller import ThreadCaller -from ..tools import (CommsFireTool, CommsSendTool, CommsOrderCheckTool, - CommsOrderServeTool, CommsOrderDoneTool, CommsOrderTool, - CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, CommsTodoTool, TodoList) -from ..todos import (ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent, - OperationDelivery, CleanHunt, OrderedHunt, PendingProblemOrder, RecordFirstCallTodo, - OrderDeletion, StrikeReason, StrikeTodo, VoidStrikeTodo, LocatedProblemFinding, UnlocatedProblemFinding, FileDeliveryEvidence, NoDeliveryEvidence, - WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop) +from ..tools import ( + CommsFireTool, + CommsOrderCheckTool, + CommsOrderDoneTool, + CommsOrderServeTool, + CommsOrderTool, + CommsProblemCloseTool, + CommsTodoCreateTool, + CommsTodoTool, +) from . import capacity -from .config import CommanderConfig, CommanderRole, Repo -from pydantic import PrivateAttr +from .config import CommanderRole, Repo +from .context import CommanderContext +from .selections import AssignmentTrace, StrikeSelection, TransferReason, WithdrawalSelection OUTGOING = DirectiveText.OUTGOING # every commander text passes each one @@ -141,72 +178,12 @@ def revision(repo: str, ref: str) -> str: ['git', '-C', repo, 'rev-parse', '--verify', f'{ref}^{{commit}}'], text=True).strip() -def replied(latest: Message | None) -> str: - """The reply an overdue alarm is read against.""" - if latest is None: - return '; no reply since the order' - at = time.strftime('%H:%M', time.localtime(latest.timestamp)) - return f'; REPLIED {at} [{latest.message_id}]: {" ".join(latest.body.split())[:160]} -- read it before pushing' +class Commander(CommanderContext): + """Order workflows use the selected root's inherited services.""" -class Commander(CommanderConfig): - """One command's configuration and opened bus; nothing is resolved at import.""" - - _comms: Comms = PrivateAttr() - - def model_post_init(self, context) -> None: - self._comms = Comms(self.root) - - @property - def comms(self) -> Comms: - return self._comms - - @classmethod - def of(cls, config: CommanderConfig) -> Commander: - return cls.model_construct(**config.model_dump()) - - def post(self, audience: VerdictAudience, body: str) -> str: - """Send one message its caller refused with OUTGOING; judge reads it from bus history. - - One thread gets a DM; a larger audience is mentioned on #comms, which wakes no one else. - """ - target = audience.threads[0] if len(audience.threads) == 1 else '#comms' - return self.call(CommsSendTool(sender=self.commander, target=target, body=body, - audience=audience.threads)).id - - - def call(self, tool): - """Every order write is one Core tool request made as the commander's thread.""" - return ThreadCaller(self.commander).invoke(tool, self.comms) - - - def todos(self): - """The problems this commander created; Core's Todo store is their one owner.""" - return self.comms.goals.todos.for_commander(self.comms.registry.require(self.commander).incarnation) - - - def held_orders(self) -> tuple[Todo, ...]: - """This commander's open orders that a unit holds.""" - return self.comms.goals.todos.held_for_commander(self.comms.registry.require(self.commander).incarnation) - - - def held_by(self, unit: str) -> tuple[Todo, ...]: - """This commander's open orders held by one unit.""" - return self.comms.goals.todos.held_by(self.comms.registry.require(self.commander).incarnation, unit) - - - def units(self) -> frozenset[str]: - return self.comms.goals.todos.units(self.comms.registry.require(self.commander).incarnation) - - - def progress(self) -> dict[str, OrderProgress]: - """Each held order's progress, derived by Core's Todo list from the unit's checkout and PRs.""" - listed: TodoList = self.call(CommsTodoListTool()) - return listed.progress - - - def send(self, unit, due, body: str, clean=False, mechanical=False, reassign=None, new=None, - traced=None, replaces=None, disk=False, finding=UnlocatedProblemFinding()): + def send(self, unit, due, body: str, clean=False, mechanical=False, reassign='', new=None, + traced='', replaces=None, disk=False, finding=UnlocatedProblemFinding()): """Validate an order body and record it in Core: a new problem (new=(PID, TEXT)) or the listed open problems. Sending to a unit that already holds the problem replaces its order there; without --clean that is a send-back.""" @@ -283,14 +260,10 @@ def send(self, unit, due, body: str, clean=False, mechanical=False, reassign=Non listed[pid].require_replacement(replaces) if replaces and not replacing: raise ValueError('--replaces must name the current order on these problems to the same unit.') - trace = FileLine.from_wire(traced) if traced is not None else None - for pid in group: - if pid in listed and listed[pid].assignment: - listed[pid].assignment.require_trace(unit, trace) + trace = AssignmentTrace.read(traced) if holders and not reassign: problems.append(f'fact already owned by {holders}; keep that owner, or pass --reassign ""') - if reassign is not None and len(reassign.split()) < 8: - problems.append('--reassign needs the concrete failure that forces replacement (8+ words), told to the old and new owner') + problems.extend(TransferReason.read(reassign).errors()) if problems: sys.exit('Refused order:\n ' + '\n '.join(problems)) message = (f'{body.rstrip()}' + ('\nThis replaces my earlier order on it; drop that one.' if replacing else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + self.comms.registry.storage_rule.order_text(unit, [f'{repo.checkout}/.venv/bin/python' for repo in self.repos]) + '\nIf two lines of this order conflict, take the narrower reading, say which one you took, and proceed; do not stop to ask.' @@ -317,8 +290,8 @@ def send(self, unit, due, body: str, clean=False, mechanical=False, reassign=Non self.call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction( problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], **common))) for pid in [] if new else group: - self.call(CommsOrderTool(owner=unit, order=OrderGoalAction( - problem=pid, expected_revision=self.comms.goals.todos.get(pid).revision, traced=trace, **common))) + self.call(CommsOrderTool(owner=unit, order=trace.order( + problem=pid, expected_revision=self.comms.goals.todos.get(pid).revision, **common))) message += "\n" + "\n".join(order.pull_request_instruction(pid) for pid in group) sent = self.post(VerdictAudience((unit,)), message) # The message is each order's recorded link; a verdict citing it lands on that order. @@ -338,17 +311,6 @@ def send(self, unit, due, body: str, clean=False, mechanical=False, reassign=Non self.strike(unit, group[0], f'sent back on {group[0]}: {verdict}') - def importers(self, modules: set[str], check: OrderCheck) -> list[str]: - """The other configured repos whose `from agent_comms` imports name a module, unless the - check reads them.""" - pattern = rf'^\s*(from|import) agent_comms\b.*\b({"|".join(sorted(modules))})\b' - return [str(repo.checkout) for repo in self.repos - if repo.checkout != Path(check.directory).resolve() - and str(repo.checkout) not in check.command - and subprocess.run(['grep', '-rqE', '--include=*.py', pattern, - str(repo.checkout / repo.source)]).returncode == 0] - - def correct_check(self, pid: str, line: str, why: str) -> None: """Core replaces a wrong check of an open order. The new check must already pass on main, so a correction fixes the commander's check and never passes undelivered work: done still runs it on @@ -410,10 +372,6 @@ def close(self, pid, works, artifact=''): print(f'{pid} closed') - def last_activity(self, unit): - return ExternalTranscript(self.comms.root, self.comms.registry.require(unit).incarnation).last_tool_call() - - def write_board(self): lines = ['# Fleet board', '', "GENERATED by agent-comms-commander order from Core's Todo store. Do not edit by hand:", @@ -579,14 +537,6 @@ def next_hunt(self): return min(candidates, key=lambda f: hunted.get(f, 0)) if candidates else None - def reply(self, unit: str, since: float) -> Message | None: - """The unit's newest message to me after ``since`` in the recent bus window.""" - with self.comms.bus.log.opened_wire_snapshot() as source: - return next((message for message in source.newest_messages() - if message.sender == unit and message.timestamp > since - and message.target in (self.commander, '#comms')), None) - - def deadline_lines(self, pending: list[Todo], read: dict[str, OrderProgress], since: float = 0.0) -> list[str]: """Every past-due unrecorded order alarms with its state until recorded. Stall crossings remain separate from deadline alarms.""" @@ -600,7 +550,7 @@ def deadline_lines(self, pending: list[Todo], read: dict[str, OrderProgress], si continue answer = self.reply(t.holder, ordered) due = f'due {hhmm(t.deadline)}, order {t.order.id}' - late.append(f'{t.holder} ({due}, {work.line}{replied(answer)})') + late.append(f'{t.holder} ({due}, {work.line}{answer.suffix()})') alarm = (f'OVERDUE: {", ".join(late)} -> record merged work; ' 'push or merge unfinished work now') return [alarm] * bool(late) + stalled @@ -646,10 +596,11 @@ def overdue(self): time.sleep(60) - def unstrike(self, task: str, why: str, at: str | None = None) -> None: + def unstrike(self, task: str, why: str, at: str = '') -> None: """A strike recorded in error is voided in Core, then narrated and posted to #comms.""" todo = self.comms.goals.todos.get(task) - live = [s for s in todo.live_strikes() if at in (None, hhmm(s.at))] + selection = StrikeSelection.read(at) + live = [s for s in todo.live_strikes() if selection.includes(hhmm(s.at))] if not live: sys.exit(f'Refused: {task} has no live strike' + (f' recorded at {at}.' if at else '.')) voided = live[-1] @@ -681,15 +632,14 @@ def due(self, pid: str, at: str, why: str) -> None: print(f'{pid}: due {hhmm(moved)}') - def withdraw(self, pid: str, why: str, superseded_by: str | None = None) -> None: + def withdraw(self, pid: str, why: str, superseded_by: str = '') -> None: """Core retires an undelivered order sent in error. The reason goes on the public record and to the unit that held it.""" todo = self.comms.goals.todos.get(pid) refuse(why, *OUTGOING) # posted to #comms and the holder, so refused before Core records it - self.call(CommsTodoTool(id=pid, operation=WithdrawTodo( - todo.revision, self.comms.registry.require(self.commander).incarnation, todo.assignment, why, - superseded_by))) + self.call(CommsTodoTool(id=pid, operation=WithdrawalSelection.read(superseded_by).operation( + todo.revision, self.comms.registry.require(self.commander).incarnation, todo.assignment, why))) unit = todo.held().owner withdrawal, = self.comms.goals.todos.get(pid).order.withdrawals(todo) self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {withdrawal}', 'Withdrawal', @@ -784,10 +734,10 @@ class SendVerb(OrderVerb, LoadedVerb): body_file: str = option("body_file", metavar="BODYFILE") clean: bool = option("--clean", default=False, help="the unit showed no incompetence") mechanical: bool = option("--mechanical", default=False) - reassign: str | None = option("--reassign", default=None, help="the concrete failure that forces replacement") + reassign: str = option("--reassign", default='', help="the concrete failure that forces replacement") replaces: str | None = option("--replaces", default=None, help="the current message this order replaces") disk: bool = option("--disk", default=False, help="a cleanup order; never overrides a tool's storage rule") - traced: str | None = option("--traced", default=None, metavar="PATH:LINE", + traced: str = option("--traced", default='', metavar="PATH:LINE", help="the first failing source line, required after two owners") def run(self) -> None: @@ -825,7 +775,7 @@ class UnstrikeVerb(OrderVerb, RuledVerb): help = "void a strike recorded in error; it stays on the record and stops counting" pid: str = option("pid") why: str = option("--why") - at: str | None = option("--at", default=None, + at: str = option("--at", default='', help="HH:MM the strike was recorded; default the newest") def run(self) -> None: @@ -859,7 +809,7 @@ class WithdrawVerb(OrderVerb, RuledVerb): help = "retire an undelivered order sent in error; its problem closes with no strike" pid: str = option("pid") why: str = option("--why") - superseded_by: str | None = option("--superseded-by", default=None, metavar="PID", + superseded_by: str = option("--superseded-by", default='', metavar="PID", help="the problem that already owns this fix") def run(self) -> None: diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 7f278d8372..7db41e12d9 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -2,9 +2,9 @@ import time from datetime import datetime -from . import judge, order -from ..tools import CommsStandingRulesTool, StandingRulesRead from ..todos import Todo +from ..tools import CommsStandingRulesTool, StandingRulesRead +from . import judge, order def broken_rules(commander, after=0.0): diff --git a/src/agent_comms/commander/rules.py b/src/agent_comms/commander/rules.py index b77ec4467a..db6e2c8ef9 100644 --- a/src/agent_comms/commander/rules.py +++ b/src/agent_comms/commander/rules.py @@ -12,11 +12,14 @@ Every fork reviews for itself (forks must know correct maintenance, not carry dogma): a fork's actions are refused until its own review is answered, and all three of a fork's drawn rules come from paper 1. """ -import glob, os, random, re, sys +import glob +import os +import random +import re +import sys from dataclasses import dataclass from ..cli_commands import option - from ..rule_reviews import AnsweredRuleReviewEntry, DrawnRuleReviewEntry, RuleReviewEntry from ..standing_rules import RulesDue from ..tool_caller import ThreadCaller diff --git a/src/agent_comms/commander/selections.py b/src/agent_comms/commander/selections.py new file mode 100644 index 0000000000..77929283ff --- /dev/null +++ b/src/agent_comms/commander/selections.py @@ -0,0 +1,78 @@ +"""Command-boundary selections; absence has an answer, not a nullable value.""" +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from ..change_report import FileLine +from ..goal_actions import OrderGoalAction +from ..todos import WithdrawTodo + + +class AssignmentTrace(ABC): + @classmethod + def read(cls, text: str): + return TracedAssignment(FileLine.from_wire(text)) if text else UntracedAssignment() + @abstractmethod + def order(self, **arguments) -> OrderGoalAction: ... + +@dataclass(frozen=True) +class UntracedAssignment(AssignmentTrace): + def order(self, **arguments) -> OrderGoalAction: + return OrderGoalAction(**arguments) + +@dataclass(frozen=True) +class TracedAssignment(AssignmentTrace): + where: FileLine + def order(self, **arguments) -> OrderGoalAction: + return OrderGoalAction(traced=self.where, **arguments) + +class TransferReason(ABC): + @classmethod + def read(cls, text: str): + return ChangedAssignee(text) if text else SameAssignee() + @abstractmethod + def errors(self) -> tuple[str, ...]: ... + +@dataclass(frozen=True) +class SameAssignee(TransferReason): + def errors(self) -> tuple[str, ...]: return () + +@dataclass(frozen=True) +class ChangedAssignee(TransferReason): + reason: str + def errors(self) -> tuple[str, ...]: + return () if len(self.reason.split()) >= 8 else ( + '--reassign needs the concrete failure that forces replacement (8+ words), told to the old and new owner',) + +class StrikeSelection(ABC): + @classmethod + def read(cls, text: str): + return StrikeAt(text) if text else LatestStrike() + @abstractmethod + def includes(self, clock: str) -> bool: ... + +@dataclass(frozen=True) +class LatestStrike(StrikeSelection): + def includes(self, clock: str) -> bool: return True + +@dataclass(frozen=True) +class StrikeAt(StrikeSelection): + clock: str + def includes(self, clock: str) -> bool: return self.clock == clock + +class WithdrawalSelection(ABC): + @classmethod + def read(cls, text: str): + return SupersededOrder(text) if text else MistakenOrder() + @abstractmethod + def operation(self, *arguments) -> WithdrawTodo: ... + +@dataclass(frozen=True) +class MistakenOrder(WithdrawalSelection): + def operation(self, *arguments) -> WithdrawTodo: + return WithdrawTodo(*arguments) + +@dataclass(frozen=True) +class SupersededOrder(WithdrawalSelection): + problem: str + def operation(self, *arguments) -> WithdrawTodo: + return WithdrawTodo(*arguments, superseded_by=self.problem) diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index 53be70ccda..8913442c89 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -11,9 +11,13 @@ from ..comms import Comms from ..goal_actions import OrderGoalAction -from ..standing_rules import (DatedStandingRule, ReasonedStandingRule, StandingRule, - UndatedStandingRule) from ..goals import Goal +from ..standing_rules import ( + DatedStandingRule, + ReasonedStandingRule, + StandingRule, + UndatedStandingRule, +) from ..threads import Thread from ..todos import FlatTrend, OrderProgress, PullRequests, StrikeTodo, Todo from ..turn_digests import TurnDigestRead, TurnsSince diff --git a/src/agent_comms/commander/tool_watch.py b/src/agent_comms/commander/tool_watch.py index 87f0b6e056..f954097955 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -1,13 +1,17 @@ """Follow live native sessions and print turn digests for commander judgment.""" -import os, time, glob, collections +import collections +import glob +import os +import time from pathlib import Path -from ..native_tool_call import CodingCall, SelectedToolDenied from ..native_entries import NativeEntry +from ..native_tool_call import CodingCall, SelectedToolDenied from ..pi_payloads import PiMessage, TextContent -from ..tool_declaration import one_line from ..registration import Registration from ..todos import StorageStrikeTodo +from ..tool_declaration import one_line + def thread_names(registry: Registration) -> dict[str, str]: """Session file -> thread name, from the registry snapshot's threads.""" From 4a1f213a7aa841ff53df3c9bb6c6b87aafb1a6b1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:31:48 -0400 Subject: [PATCH 05/42] P707 stage one-shot RTS order import through Core records --- tools/carry_rts_orders.py | 160 ++++++++++++++++++++++++++ tools/cutover/issue_rts_orders.py | 179 ------------------------------ 2 files changed, 160 insertions(+), 179 deletions(-) create mode 100644 tools/carry_rts_orders.py delete mode 100644 tools/cutover/issue_rts_orders.py diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py new file mode 100644 index 0000000000..067bc1df39 --- /dev/null +++ b/tools/carry_rts_orders.py @@ -0,0 +1,160 @@ +"""One-shot RTS order transfer. Run under a quiesced writer; remove after installation. + +Only Core's real record types and TodoStore write the destination. This does not +publish goals, send messages, signal owners or launch a provider. --retire-rts +removes the old executable entrypoints after the transferred rows are verified. +Unordered findings have no order/owner/ceiling and are not fabricated as orders. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import time +from dataclasses import dataclass, replace +from pathlib import Path + +from agent_comms.change_report import FileLine +from agent_comms.commander.config import Repo +from agent_comms.comms import Comms +from agent_comms.field_codec import FieldCodec +from agent_comms.goals import OrderCheck +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, + MechanicalOrderRun, NoGoalReference, ObservedFirstCall, OrderMessage, + PendingProblemOrder, StrikeTodo, TodoStore) + + +@dataclass(frozen=True) +class RtsOrder: + due: str + msg: str + at: float + fact: str + facts: tuple[str, ...] + check: tuple[str, str, int] + disk: bool + works: str | None = None + started: bool = False + no_first_call: bool = False + + def deadline(self) -> RecordedEventTime: + hour, minute = map(int, self.due.split(':')) + sent = time.localtime(self.at) + at = time.mktime((sent.tm_year, sent.tm_mon, sent.tm_mday, + hour, minute, 0, 0, 0, -1)) + return RecordedEventTime(at + 86400 if at < self.at - 60 else at) + + def order(self, body: str) -> PendingProblemOrder: + place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) + report = re.search(r'\bReport:\s*([^\n]+)', body) + if place is None or report is None: + raise ValueError(f'{self.msg}: original order needs Owner and Report lines') + return PendingProblemOrder( + id=self.msg, owner=FileLine(place[1], int(place[2])), + check=OrderCheck.parse(f'{self.check[0]} :: {self.check[1]} -> {self.check[2]}'), + edits=tuple(re.findall(r'^\s*\d+[.)] (.+?)\s*$', body, re.M)), + banned=tuple(part.strip() for line in re.findall(r'^Banned:(.*)$', body, re.M) + for part in line.split(';') if part.strip()), + report=report[1], at=RecordedEventTime(self.at), + first_call_deadline=RecordedEventTime(self.at + 120), + first_call=ObservedFirstCall() if self.started else AwaitingFirstCall(), + run=ApplicationOrderRun(self.works) if self.works else MechanicalOrderRun()) + + +def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: + names = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl') + source = {name: (rts / name).read_bytes() for name in names} + deadlines = json.loads(source['deadlines.json']) + problems = json.loads(source['problems.json']) + sent = [json.loads(line) for line in source['sent.jsonl'].splitlines() if line] + messages = {item['id']: item for item in sent} + strikes = [json.loads(line) for line in source['strikes.jsonl'].splitlines() if line] + selected = {} + merged = {} + for unit, orders in deadlines.items(): + for raw in orders: + values = dict(raw) + commit = values.pop('merged', '') + old = FieldCodec.decode(RtsOrder, values) + parsed = old.order(messages[old.msg]['text']) + for pid in old.facts: + if pid in selected: + raise ValueError(f'{pid}: two open RTS orders') + selected[pid] = unit, old, parsed + if commit: + merged[pid] = commit + comms = Comms(root) + actor = comms.registry.require('claude-code') + store = TodoStore(root) + rows = [] + for pid, (unit, old, order) in selected.items(): + owner = comms.registry.require(unit) + repo = Repo.of(Path(order.check.directory)).github + history = [] + for strike in strikes: + if strike.get('task') != pid: + continue + struck = comms.registry.require(strike['unit']) + # RTS strikes name a task and unit, not an order or goal generation. + # Keep that original event identity; never guess an earlier order. + assignment = Assignment(struck.name, struck.incarnation.created_at, + actor.name, actor.incarnation.created_at, f"rts-strike:{pid}:{strike['at']}") + history.append(StrikeTodo(1, actor.incarnation, assignment, + strike['why'], RecordedEventTime(strike['at']))) + rows.append((pid, repo, problems[pid]['text'], owner, old, order, tuple(history))) + report = {'source_sha256': {name: hashlib.sha256(data).hexdigest() + for name, data in source.items()}, + 'orders': len(rows), 'strikes': sum(len(row[-1]) for row in rows), + 'unordered_findings': len(problems) - len(selected), + 'already_merged': merged, 'board_orders': []} + if not apply: + print(json.dumps(report, indent=2)) + return 0 + existing = {todo.id: todo for todo in store.history_for_commander(actor.incarnation)} + for pid, repo, text, owner, old, order, history in rows: + if pid in existing: + current = store.get(pid) + if current.order.id != old.msg: + current = store.assign(pid, expected_revision=current.revision, + owner=owner, parent=actor, generation='rts-order:' + old.msg, + goal=current.goal, deadline=old.deadline(), order=order) + else: + current = store.create(pid, repo, text, creator=actor, owner=owner, + generation='rts-order:' + old.msg, deadline=old.deadline(), order=order, + goal=NoGoalReference()) + if not current.sent_in(old.msg): + current = store.apply(pid, OrderMessage(current.revision, actor.incarnation, old.msg)) + # Historical strikes include former holders; replaying StrikeTodo.apply would + # wrongly require those holders to be the current assignment. Restore their + # actual Core records, together, through the record's own SQLite codec. + with store.writing() as db: + replace(current, strikes=history, revision=current.revision + 1).upsert(db) + recorded = store.get(pid) + if (recorded.holder != owner.name or recorded.deadline != old.deadline() + or recorded.order.id != old.msg or recorded.strikes != history): + raise ValueError(f'{pid}: transferred Core row differs') + report['board_orders'].append(f'{pid} {recorded.holder} {old.due} {recorded.order.id}') + if any((rts / name).read_bytes() != data for name, data in source.items()): + raise ValueError('RTS changed during transfer; destination is not ready for installation') + if retire: + for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', + 'deadlines.json', 'problems.json', 'strikes.jsonl'): + (rts / name).rename(rts / (name + '.retired-p707')) + print(json.dumps(report, indent=2)) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--rts-dir', required=True, type=Path) + parser.add_argument('--root', required=True, type=Path) + parser.add_argument('--apply', action='store_true') + parser.add_argument('--retire-rts', action='store_true') + args = parser.parse_args() + return carry(args.rts_dir, args.root, args.apply, args.retire_rts) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/cutover/issue_rts_orders.py b/tools/cutover/issue_rts_orders.py deleted file mode 100644 index d7fa92b602..0000000000 --- a/tools/cutover/issue_rts_orders.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Temporary move of open RTS orders into a copied Core root. - -The root contains registry/Todo copies and order-messages.jsonl: exact original -bus rows selected by the RTS message ids. Never run against the live root. -Core's order actions choose assignments and goal generations; this tool does -not write Todo rows or infer generations from old goals. -""" -from __future__ import annotations - -import argparse -import json -import re -import time -from dataclasses import dataclass -from pathlib import Path - -from agent_comms.change_report import FileLine -from agent_comms.commander.config import Repo -from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec -from agent_comms.goal_actions import CreateProblemGoalAction, OrderGoalAction, SetGoalAction -from agent_comms.goals import OrderCheck, RequestGoalOrder -from agent_comms.messages import Message -from agent_comms.recorded_time import RecordedEventTime -from agent_comms.todos import (ApplicationOrderRun, AwaitingFirstCall, MechanicalOrderRun, - ObservedFirstCall, PendingProblemOrder, StrikeTodo, TodoStore) - - -@dataclass(frozen=True) -class RtsOrder: - due: str - msg: str - at: float - fact: str - facts: tuple[str, ...] - check: tuple[str, str, int] - disk: bool - works: str | None = None - started: bool = False - no_first_call: bool = False - - def deadline(self) -> RecordedEventTime: - hour, minute = map(int, self.due.split(':')) - sent = time.localtime(self.at) - at = time.mktime((sent.tm_year, sent.tm_mon, sent.tm_mday, - hour, minute, 0, 0, 0, -1)) - return RecordedEventTime(at + 86400 if at < self.at - 60 else at) - - def order(self, message: Message) -> PendingProblemOrder: - body = message.body - place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) - report = re.search(r'^Report:\s*(.+)$', body, re.M) - if place is None or report is None: - raise ValueError(f'{self.msg}: original order needs Owner and Report lines') - return PendingProblemOrder( - id=self.msg, owner=FileLine(place[1], int(place[2])), - check=OrderCheck.parse(f'{self.check[0]} :: {self.check[1]} -> {self.check[2]}'), - edits=tuple(re.findall(r'^\s*\d+[.)] (.+?)\s*$', body, re.M)), - banned=tuple(part.strip() for line in re.findall(r'^Banned:(.*)$', body, re.M) - for part in line.split(';') if part.strip()), - report=report[1], at=RecordedEventTime(self.at), - first_call_deadline=RecordedEventTime(self.at + 120), - first_call=ObservedFirstCall() if self.started else AwaitingFirstCall(), - run=ApplicationOrderRun(self.works) if self.works else MechanicalOrderRun()) - - -@dataclass(frozen=True) -class RtsProblem: - text: str - where: str - at: float - held_by: str | None = None - - -@dataclass(frozen=True) -class RtsStrike: - unit: str - why: str - at: float - task: str | None - caught: str | None = None - - -def read_json(path: Path, kind): - return FieldCodec.decode(kind, FieldCodec.parse(path.read_text())) - - -def read_lines(path: Path, kind): - text = path.read_text() - if text and not text.endswith('\n'): - raise ValueError(f'{path}: incomplete final row') - return tuple(FieldCodec.decode(kind, FieldCodec.parse(line)) - for line in text.splitlines() if line.strip()) - - -def carry(rts: Path, root: Path, apply: bool) -> int: - deadlines = read_json(rts / 'deadlines.json', dict[str, tuple[RtsOrder, ...]]) - records = FieldCodec.parse((rts / 'problems.json').read_text()) - strikes = read_lines(rts / 'strikes.jsonl', RtsStrike) - messages = {} - for line in (root / 'order-messages.jsonl').read_text().splitlines(): - message = Message.from_committed_wire(FieldCodec.parse(line)) - messages[message.message_id] = message - ordered = {} - blockers = {} - for unit, orders in deadlines.items(): - for old in orders: - for pid in old.facts: - if pid in ordered: - blockers[pid] = 'two open orders name this problem' - ordered[pid] = (unit, old) - selected = {pid: FieldCodec.decode(RtsProblem, records[pid]) for pid in ordered} - parsed_orders = {old.msg: old.order(messages[old.msg]) - for orders in deadlines.values() for old in orders} - attached = tuple(strike for strike in strikes - if strike.task in ordered and strike.unit == ordered[strike.task][0]) - report = {'orders_issued': {'create': 0, 'assign': 0}, 'strikes_attached': 0, - 'strikes_left_as_history': len(strikes) - len(attached), - 'unordered_problems': len(records) - len(selected), - 'source_orders': sum(map(len, deadlines.values())), - 'source_ordered_problems': len(ordered), - 'source_first_call_exemptions': [old.msg for orders in deadlines.values() - for old in orders if old.no_first_call], - 'blockers': blockers} - if blockers or not apply: - print(json.dumps(report, indent=2)) - return 2 if blockers else 0 - comms = Comms(root) - actor = comms.registry.require('claude-code') - requests = tuple(goal.id for goal in actor.goal.active() - if isinstance(goal.order, RequestGoalOrder)) - if not requests: - comms.goals.update_goal(actor.name, SetGoalAction( - text='Move open RTS orders into this copied Core root', order=RequestGoalOrder())) - request = comms.registry.require(actor.name).goal.require().id - store = TodoStore(root) - existing = {row.id: row for row in store.history_for_commander(actor.incarnation)} - for pid, (unit, old) in ordered.items(): - try: - message = messages[old.msg] - if message.sender != actor.name: - raise ValueError(f'{old.msg}: order was not sent by the commander') - order = parsed_orders[old.msg] - common = dict(problem=pid, order=order, request=request, - deadline=old.deadline(), commander=actor.name) - if pid in existing: - action = OrderGoalAction(**common, expected_revision=store.get(pid).revision) - kind = 'assign' - else: - action = CreateProblemGoalAction(**common, - repo=Repo.of(Path(order.owner.file).parent).github, text=selected[pid].text) - kind = 'create' - comms.goals.update_goal(unit, action) - report['orders_issued'][kind] += 1 - for old_strike in attached: - if old_strike.task != pid: - continue - current = store.get(pid) - comms.goals.change_problem(pid, StrikeTodo( - current.revision, actor.incarnation, current.held(), old_strike.why, - RecordedEventTime(old_strike.at)), actor.name) - report['strikes_attached'] += 1 - except (ValueError, KeyError) as error: - blockers[pid] = str(error) - print(json.dumps(report, indent=2)) - return 2 if blockers else 0 - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--rts-dir', required=True, type=Path) - parser.add_argument('--root', required=True, type=Path) - parser.add_argument('--apply', action='store_true') - args = parser.parse_args() - return carry(args.rts_dir, args.root, args.apply) - - -if __name__ == '__main__': - raise SystemExit(main()) From 4f9fa3685b44758899e253971f11419ec2da838c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 14:42:30 -0400 Subject: [PATCH 06/42] P707 refuse missing source rows and rehearse writer retirement --- tools/carry_rts_orders.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 067bc1df39..a1cdd1a4a6 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -23,7 +23,7 @@ from agent_comms.recorded_time import RecordedEventTime from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, MechanicalOrderRun, NoGoalReference, ObservedFirstCall, OrderMessage, - PendingProblemOrder, StrikeTodo, TodoStore) + PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo) @dataclass(frozen=True) @@ -85,6 +85,10 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: selected[pid] = unit, old, parsed if commit: merged[pid] = commit + missing = sorted(selected.keys() - problems.keys()) + if missing: + print(json.dumps({'orders_missing_problem_rows': missing}, indent=2)) + raise ValueError('The commander must restore the missing problem rows before transfer') comms = Comms(root) actor = comms.registry.require('claude-code') store = TodoStore(root) @@ -107,12 +111,22 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: report = {'source_sha256': {name: hashlib.sha256(data).hexdigest() for name, data in source.items()}, 'orders': len(rows), 'strikes': sum(len(row[-1]) for row in rows), - 'unordered_findings': len(problems) - len(selected), + 'unordered_findings': len(problems.keys() - selected.keys()), + 'orders_missing_problem_rows': sorted(selected.keys() - problems.keys()), 'already_merged': merged, 'board_orders': []} if not apply: print(json.dumps(report, indent=2)) return 0 existing = {todo.id: todo for todo in store.history_for_commander(actor.incarnation)} + for pid, current in existing.items(): + if current.assignment is not None and pid not in selected: + reason = 'This old order is absent from the current open RTS orders.' + if pid in problems: + store.apply(pid, ReleaseTodo(current.revision, actor.incarnation, + current.held(), StrikeReason(reason))) + else: + store.apply(pid, WithdrawTodo(current.revision, actor.incarnation, + current.held(), reason)) for pid, repo, text, owner, old, order, history in rows: if pid in existing: current = store.get(pid) @@ -129,8 +143,9 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: # Historical strikes include former holders; replaying StrikeTodo.apply would # wrongly require those holders to be the current assignment. Restore their # actual Core records, together, through the record's own SQLite codec. + restored = replace(current, strikes=history, revision=current.revision + 1) with store.writing() as db: - replace(current, strikes=history, revision=current.revision + 1).upsert(db) + restored.upsert(db) recorded = store.get(pid) if (recorded.holder != owner.name or recorded.deadline != old.deadline() or recorded.order.id != old.msg or recorded.strikes != history): @@ -139,7 +154,7 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: if any((rts / name).read_bytes() != data for name, data in source.items()): raise ValueError('RTS changed during transfer; destination is not ready for installation') if retire: - for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', + for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py', 'deadlines.json', 'problems.json', 'strikes.jsonl'): (rts / name).rename(rts / (name + '.retired-p707')) print(json.dumps(report, indent=2)) From c775880e86bd731303e5f3ef6de5eaef5d3b15b8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 15:10:48 -0400 Subject: [PATCH 07/42] P707: keep order state in Core and refuse missing finding repositories --- src/agent_comms/commander/order.py | 10 ++--- src/agent_comms/todos.py | 64 +++++++++++++++--------------- tools/carry_rts_orders.py | 47 +++++++++++++++++++--- 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 92f37f8993..93c26afb3d 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -390,17 +390,15 @@ def board(): lines = ['# Fleet board', '', "GENERATED by agent-comms-commander order from Core's Todo store. Do not edit by hand:", '`order problem` adds a row, `order send ... Problem: P` orders it, `order close` or `order withdraw` removes it.', '', - '| Problem | Owner file | Owner | Order | Due | Progress | Delivered ' + '| Problem | Owner file | Owner | Order | Due | Progress | Order state ' '| Owner last acted |', '|---|---|---|---|---|---|---|---|'] listed, read = todos(), progress() activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): - unit, last = t.holder, t.released[-1:] - owner = unit or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}' - lines.append(f'| {t.id}: {t.text} | {t.order.owner.to_wire()} | {owner} | {t.order.id} | ' - f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | ' - f'{"no" if t.pending else "yes, install pending"} | ' + unit = t.holder + lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {t.order.board_order(t)} | ' + f'{read[t.id].line if t.id in read else "-"} | {t.order.line} | ' f'{activity[unit].clock() if unit else "-"} |') history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) lines += ['', '## Withdrawn orders', '', diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 21893e014e..4aab1ef9e8 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1069,6 +1069,9 @@ 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 board_order(self, todo: Todo) -> str: + return f'{" ".join(self.finding.instructions())} | unassigned | - | -' + def require_transfer(self, todo: Todo) -> None: raise ValueError("This problem cannot receive another order.") @@ -1146,6 +1149,11 @@ 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}) + def board_order(self, todo: Todo) -> str: + owner = todo.holder or f'unassigned: released from {todo.released[-1].previous.owner}: {todo.released[-1].reason.text}' + return (f'{self.owner.to_wire()} | {owner} | {self.id} | ' + f'{time.strftime("%H:%M", time.localtime(todo.deadline.recorded_at("An order deadline")))}') + def __post_init__(self): if not self.edits or not self.banned: raise ValueError("An order requires edits and banned actions.") @@ -1236,16 +1244,9 @@ def require_checked(self, problem: str) -> None: raise ValueError(f"An installed claim needs the recorded check result of {problem}; " "its order has none.") - def require_close(self): - raise ValueError("Closing requires recorded installed application evidence.") - def withdrawn(self, previous: Assignment, why: str, superseded_by: str | None) -> ProblemOrder: raise ValueError("Only an undelivered order can be withdrawn.") - def withdrawals(self, todo: Todo) -> tuple[str, ...]: - """How this order retired its problem without delivery, as the record and board say.""" - return () - def waiting_on_install(self, todo: Todo, read: dict[str, OrderProgress]) -> tuple[Todo, ...]: """The order waiting on the fleet's install to record it working.""" return () @@ -1282,14 +1283,26 @@ def withdrawn(self, previous: Assignment, why: str, superseded_by: str | None) - @dataclass(frozen=True, kw_only=True) class DroppedProblemOrder(UnorderedProblemOrder, CheckedProblemOrder): replacing_message: str - pending: ClassVar[bool] = False line: ClassVar[str] = "Dropped for a newer order" +@dataclass(frozen=True, kw_only=True) +class MergedProblemOrder(CheckedProblemOrder): + """A recorded merge is not proof that the order's delivery check passed.""" + commit: str + line: ClassVar[str] = "Merged, delivery check unverified" + + def __post_init__(self): + super().__post_init__() + GitCommitText.decode(self.commit) + + def waiting_on_install(self, todo: Todo, read: dict[str, OrderProgress]) -> tuple[Todo, ...]: + return (todo,) + + @dataclass(frozen=True, kw_only=True) class DeliveredProblemOrder(CheckedProblemOrder): delivery: OrderDelivery - pending: ClassVar[bool] = False line: ClassVar[str] = "Delivered, waiting on install" def require_transfer(self, todo: Todo) -> None: @@ -1956,18 +1969,18 @@ class Todo(TypedTable): text: str creator: str creator_created: float - state: type[TodoState] - revision: int = field(metadata={"sql": Column(check="revision>0")}) + state: type[TodoState] = field(metadata={"creation_exclude": True}) + revision: int = field(metadata={"sql": Column(check="revision>0"), "creation_exclude": True}) goal: GoalReference = field(metadata={"sql": Column(storage=JsonStorage, nullable=True)}) - assignment: Assignment | None + assignment: Assignment | None = field(metadata={"creation_exclude": True}) order: ProblemOrder deadline: EventTime = field(default_factory=UnrecordedEventTime) - strikes: tuple[StrikeTodo, ...] = () - voided: tuple[VoidStrikeTodo, ...] = () - released: tuple[ReleaseTodo, ...] = () - verdicts: tuple[OrderVerdict, ...] = () - corrections: tuple[WordingCorrection, ...] = () - messages: tuple[OrderMessage, ...] = () + strikes: tuple[StrikeTodo, ...] = field(default=(), metadata={"creation_exclude": True}) + voided: tuple[VoidStrikeTodo, ...] = field(default=(), metadata={"creation_exclude": True}) + released: tuple[ReleaseTodo, ...] = field(default=(), metadata={"creation_exclude": True}) + verdicts: tuple[OrderVerdict, ...] = field(default=(), metadata={"creation_exclude": True}) + corrections: tuple[WordingCorrection, ...] = field(default=(), metadata={"creation_exclude": True}) + messages: tuple[OrderMessage, ...] = field(default=(), metadata={"creation_exclude": True}) @property def pending(self): @@ -2079,19 +2092,8 @@ def require_actor(self, actor: ThreadIncarnation, generation: str | None) -> Non self.require_assignee(actor, generation) def require_same_creation(self, proposed: Todo) -> None: - self.require_result( - replace( - self, - id=proposed.id, - repo=proposed.repo, - text=proposed.text, - creator=proposed.creator, - creator_created=proposed.creator_created, - goal=proposed.goal, - deadline=proposed.deadline, - order=proposed.order, - ) - ) + if FieldCodec.project(self, "creation") != FieldCodec.project(proposed, "creation"): + raise TodoConflict(self) def _text(value: str, label: str, limit: int) -> str: diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index a1cdd1a4a6..a603d634c0 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -11,6 +11,7 @@ import hashlib import json import re +import subprocess import time from dataclasses import dataclass, replace from pathlib import Path @@ -20,10 +21,12 @@ from agent_comms.comms import Comms from agent_comms.field_codec import FieldCodec from agent_comms.goals import OrderCheck -from agent_comms.recorded_time import RecordedEventTime +from agent_comms.recorded_time import RecordedEventTime, UnrecordedEventTime from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, MechanicalOrderRun, NoGoalReference, ObservedFirstCall, OrderMessage, - PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo) + PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo, + Todo, OpenTodoState, UnorderedProblemOrder, LocatedProblemFinding, MergedProblemOrder, + HuntRecord, CleanHunt, OrderedHunt) @dataclass(frozen=True) @@ -64,7 +67,7 @@ def order(self, body: str) -> PendingProblemOrder: def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: - names = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl') + names = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') source = {name: (rts / name).read_bytes() for name in names} deadlines = json.loads(source['deadlines.json']) problems = json.loads(source['problems.json']) @@ -89,6 +92,13 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: if missing: print(json.dumps({'orders_missing_problem_rows': missing}, indent=2)) raise ValueError('The commander must restore the missing problem rows before transfer') + finding_repos = {} + for pid in sorted(problems.keys() - selected.keys()): + location = FileLine.from_wire(problems[pid]['where']) + try: + finding_repos[pid] = Repo.of(Path(location.path).parent).github + except subprocess.CalledProcessError as error: + raise ValueError(f'{pid}: cannot read repository from recorded path {location.path}') from error comms = Comms(root) actor = comms.registry.require('claude-code') store = TodoStore(root) @@ -143,7 +153,8 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: # Historical strikes include former holders; replaying StrikeTodo.apply would # wrongly require those holders to be the current assignment. Restore their # actual Core records, together, through the record's own SQLite codec. - restored = replace(current, strikes=history, revision=current.revision + 1) + recorded_order = order.copy_as(MergedProblemOrder, commit=merged[pid]) if pid in merged else order + restored = replace(current, order=recorded_order, strikes=history, revision=current.revision + 1) with store.writing() as db: restored.upsert(db) recorded = store.get(pid) @@ -151,11 +162,37 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: or recorded.order.id != old.msg or recorded.strikes != history): raise ValueError(f'{pid}: transferred Core row differs') report['board_orders'].append(f'{pid} {recorded.holder} {old.due} {recorded.order.id}') + for pid, repo in finding_repos.items(): + problem = problems[pid] + location = FileLine.from_wire(problem['where']) + finding = Todo(id=pid, repo=repo, text=problem['text'], creator=actor.name, + creator_created=actor.created_at, state=OpenTodoState, revision=1, + goal=NoGoalReference(), assignment=None, + order=UnorderedProblemOrder(finding=LocatedProblemFinding(location)), + deadline=UnrecordedEventTime()) + if pid in existing: + previous = store.get(pid) + finding = replace(finding, revision=previous.revision + 1, + strikes=previous.strikes, released=previous.released) + with store.writing() as db: + finding.upsert(db) + hunts = [json.loads(line) for line in source['hunts.jsonl'].splitlines() if line] + for index, raw in enumerate(hunts): + fields = dict(where=FileLine.from_wire(raw['where']), why=raw['why'], + at=RecordedEventTime(raw['at'])) + hunt = OrderedHunt(**fields, sent=raw['sent']) if raw['sent'] else CleanHunt(**fields) + # Restore historical records; do not rerun a historical source-file + # existence check or pretend that an old order is still current. + with store.writing() as db: + HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, hunt=hunt).upsert(db) + report['finding_repositories'] = finding_repos + report['hunts'] = len(hunts) + report['merged_without_delivery_claim'] = len(merged) if any((rts / name).read_bytes() != data for name, data in source.items()): raise ValueError('RTS changed during transfer; destination is not ready for installation') if retire: for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py', - 'deadlines.json', 'problems.json', 'strikes.jsonl'): + 'deadlines.json', 'problems.json', 'strikes.jsonl', 'hunts.jsonl', 'sent.jsonl'): (rts / name).rename(rts / (name + '.retired-p707')) print(json.dumps(report, indent=2)) return 0 From b7b64b0f701b94b290bd50beaf48786d1dd88829 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 15:14:45 -0400 Subject: [PATCH 08/42] P707: read finding repositories from FileLine.file and keep Todo creation fields as on main The preflight read location.path, which FileLine never declared, so every unordered finding failed before transfer. The creation_exclude metadata on Todo rewrote nine field lines without changing what require_same_creation compares; main's version stays. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/todos.py | 40 +++++++++++++++++++++++++-------------- tools/carry_rts_orders.py | 4 ++-- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 4aab1ef9e8..42ef1f9399 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1150,9 +1150,10 @@ class CheckedProblemOrder(ProblemOrder, declared_name=None): serves: type[OrderPhase] = field(default=UnplannedPhase, metadata={"wire_omit_default": True}) def board_order(self, todo: Todo) -> str: - owner = todo.holder or f'unassigned: released from {todo.released[-1].previous.owner}: {todo.released[-1].reason.text}' - return (f'{self.owner.to_wire()} | {owner} | {self.id} | ' - f'{time.strftime("%H:%M", time.localtime(todo.deadline.recorded_at("An order deadline")))}') + last = todo.released[-1:] + owner = todo.holder or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}' + due = time.localtime(todo.deadline.recorded_at("An order deadline")) + return f'{self.owner.to_wire()} | {owner} | {self.id} | {time.strftime("%H:%M", due)}' def __post_init__(self): if not self.edits or not self.banned: @@ -1969,18 +1970,18 @@ class Todo(TypedTable): text: str creator: str creator_created: float - state: type[TodoState] = field(metadata={"creation_exclude": True}) - revision: int = field(metadata={"sql": Column(check="revision>0"), "creation_exclude": True}) + state: type[TodoState] + revision: int = field(metadata={"sql": Column(check="revision>0")}) goal: GoalReference = field(metadata={"sql": Column(storage=JsonStorage, nullable=True)}) - assignment: Assignment | None = field(metadata={"creation_exclude": True}) + assignment: Assignment | None order: ProblemOrder deadline: EventTime = field(default_factory=UnrecordedEventTime) - strikes: tuple[StrikeTodo, ...] = field(default=(), metadata={"creation_exclude": True}) - voided: tuple[VoidStrikeTodo, ...] = field(default=(), metadata={"creation_exclude": True}) - released: tuple[ReleaseTodo, ...] = field(default=(), metadata={"creation_exclude": True}) - verdicts: tuple[OrderVerdict, ...] = field(default=(), metadata={"creation_exclude": True}) - corrections: tuple[WordingCorrection, ...] = field(default=(), metadata={"creation_exclude": True}) - messages: tuple[OrderMessage, ...] = field(default=(), metadata={"creation_exclude": True}) + strikes: tuple[StrikeTodo, ...] = () + voided: tuple[VoidStrikeTodo, ...] = () + released: tuple[ReleaseTodo, ...] = () + verdicts: tuple[OrderVerdict, ...] = () + corrections: tuple[WordingCorrection, ...] = () + messages: tuple[OrderMessage, ...] = () @property def pending(self): @@ -2092,8 +2093,19 @@ def require_actor(self, actor: ThreadIncarnation, generation: str | None) -> Non self.require_assignee(actor, generation) def require_same_creation(self, proposed: Todo) -> None: - if FieldCodec.project(self, "creation") != FieldCodec.project(proposed, "creation"): - raise TodoConflict(self) + self.require_result( + replace( + self, + id=proposed.id, + repo=proposed.repo, + text=proposed.text, + creator=proposed.creator, + creator_created=proposed.creator_created, + goal=proposed.goal, + deadline=proposed.deadline, + order=proposed.order, + ) + ) def _text(value: str, label: str, limit: int) -> str: diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index a603d634c0..0ed956cffa 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -96,9 +96,9 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: for pid in sorted(problems.keys() - selected.keys()): location = FileLine.from_wire(problems[pid]['where']) try: - finding_repos[pid] = Repo.of(Path(location.path).parent).github + finding_repos[pid] = Repo.of(Path(location.file).parent).github except subprocess.CalledProcessError as error: - raise ValueError(f'{pid}: cannot read repository from recorded path {location.path}') from error + raise ValueError(f'{pid}: cannot read repository from recorded path {location.file}') from error comms = Comms(root) actor = comms.registry.require('claude-code') store = TodoStore(root) From 61478fa05595fc213672ae59dcaca47c430ee8ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 15:19:28 -0400 Subject: [PATCH 09/42] P707: delete the test of the removed issue_rts_orders tool and fit board_order to the line limit tests/test_rts_open_carry.py imported cutover.issue_rts_orders, which this branch deletes in favor of tools/carry_rts_orders.py. CheckedProblemOrder's bare pending annotation restated ProblemOrder's declared default. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/todos.py | 4 +-- tests/test_rts_open_carry.py | 59 ------------------------------------ 2 files changed, 2 insertions(+), 61 deletions(-) delete mode 100644 tests/test_rts_open_carry.py diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 42ef1f9399..cf3c7d53e9 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1151,7 +1151,8 @@ class CheckedProblemOrder(ProblemOrder, declared_name=None): def board_order(self, todo: Todo) -> str: last = todo.released[-1:] - owner = todo.holder or f'unassigned: released from {last[0].previous.owner}: {last[0].reason.text}' + owner = todo.holder or (f'unassigned: released from {last[0].previous.owner}: ' + f'{last[0].reason.text}') due = time.localtime(todo.deadline.recorded_at("An order deadline")) return f'{self.owner.to_wire()} | {owner} | {self.id} | {time.strftime("%H:%M", due)}' @@ -1166,7 +1167,6 @@ def __post_init__(self): self.at.require_recorded("An order") self.first_call_deadline.require_deadline() - pending: ClassVar[bool] line: ClassVar[str] NAMED_FILE: ClassVar[re.Pattern] = re.compile(r"([\w./-]+\.\w+):\d+") diff --git a/tests/test_rts_open_carry.py b/tests/test_rts_open_carry.py deleted file mode 100644 index faaa9f7aa0..0000000000 --- a/tests/test_rts_open_carry.py +++ /dev/null @@ -1,59 +0,0 @@ -"""The one-time move reads only ordered problems on a separate Core copy.""" -import json -import shutil -import sqlite3 - -from cutover.issue_rts_orders import carry -from agent_comms.recorded_time import RecordedEventTime -from agent_comms.messages import Message, InfoKind -from agent_comms.todos import Assignment, NoGoalReference, OpenTodoState, Todo, TodoStore -from test_commander_problem_orders import order - - -def test_open_only_move_on_copied_root(commander, tmp_path, capsys): - comms, _ = commander - actor = comms.registry.new_thread('claude-code', frozenset(), str(tmp_path), created_at=3.0) - comms.registry.declare(actor) - worker = comms.registry.require('worker') - assignment = Assignment(worker.name, worker.created_at, actor.name, actor.created_at, - worker.goal.require().id) - row = Todo(id='P1', repo='OpenHCSDev/agent-comms', text='Repair the bus reader', - creator=actor.name, creator_created=actor.created_at, state=OpenTodoState, - revision=1, goal=NoGoalReference(), assignment=assignment, - order=order(tmp_path, 'old-message'), deadline=RecordedEventTime(1000)) - with TodoStore(comms.root).writing() as db: - row.upsert(db) - copied = tmp_path / 'copy' - copied.mkdir() - shutil.copy2(comms.root / 'registry.json', copied / 'registry.json') - with sqlite3.connect(comms.root / 'todos.sqlite3') as source, sqlite3.connect(copied / 'todos.sqlite3') as target: - source.backup(target) - message = Message('claude-code', 'worker', f'''Problem: P1 -Owner: {__file__}:1 -1. Repair the existing bus reader -Banned: No second store -Report: Report the result -''', InfoKind, seq=1) - (copied / 'order-messages.jsonl').write_text(json.dumps(message.to_wire()) + '\n') - rts = tmp_path / 'rts' - rts.mkdir() - (rts / 'deadlines.json').write_text(json.dumps({'worker': [{ - 'due': '00:30', 'msg': message.message_id, 'at': 100, 'fact': 'P1', 'facts': ['P1'], - 'check': [str(tmp_path), 'printf 1', 1], 'disk': False}]})) - (rts / 'problems.json').write_text(json.dumps({ - 'P1': {'text': row.text, 'where': 'reader.py:1', 'at': 90}, - 'UNORDERED': {'not_a_problem_record': True}})) - (rts / 'strikes.jsonl').write_text('\n'.join(json.dumps(record) for record in ( - {'unit': 'worker', 'why': 'Archived old strike', 'at': 50, 'task': None}, - {'unit': 'worker', 'why': 'The unit changed the wrong file and ignored the required edit', - 'at': 120, 'task': 'P1'})) + '\n') - assert carry(rts, copied, True) == 0 - report = json.loads(capsys.readouterr().out) - assert report['orders_issued'] == {'create': 0, 'assign': 1} - assert report['unordered_problems'] == report['strikes_left_as_history'] == 1 - assert report['strikes_attached'] == 1 - assert report['blockers'] == {} - assert TodoStore(comms.root).get('P1') == row - issued = TodoStore(copied).get('P1') - assert issued.order.id == message.message_id - assert issued.strikes[0].previous == issued.held() From 13e9f83d48eecdc7c316178f30341fb0954e86e2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 15:29:27 -0400 Subject: [PATCH 10/42] P707: typed board row, one clock formatter, family-decoded hunts and one staged commit for the carry The board's order columns come from a typed BoardOrder row, and the finding's place (not its instructions) fills the Owner file column. clock() is the one HH:MM formatter, used by orders, install deadlines and order.py's hhmm. The carry decodes hunts through the Hunt family, writes every row through TodoStore on a staged copy, checks the RTS sources, then commits once by SQLite backup. Missing finding checkouts and orders without Banned lines are refused by problem id. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/order.py | 8 +++-- src/agent_comms/todos.py | 45 ++++++++++++++++-------- tests/test_todos.py | 15 +++++++- tools/carry_rts_orders.py | 56 +++++++++++++++++++++--------- 4 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 93c26afb3d..5cf9a29096 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -41,7 +41,7 @@ from ..tools import (CommsFireTool, CommsSendTool, CommsOrderCheckTool, CommsOrderServeTool, CommsOrderDoneTool, CommsOrderTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, CommsTodoTool, TodoList) -from ..todos import (ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent, +from ..todos import (clock, ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent, OperationDelivery, CleanHunt, OrderedHunt, PendingProblemOrder, RecordFirstCallTodo, OrderDeletion, StrikeReason, StrikeTodo, VoidStrikeTodo, LocatedProblemFinding, UnlocatedProblemFinding, FileDeliveryEvidence, NoDeliveryEvidence, WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop) @@ -111,7 +111,7 @@ def struck(todo, unit): def hhmm(event): - return time.strftime('%H:%M', time.localtime(event.at)) + return clock(event.at) def deadline(due): @@ -397,7 +397,9 @@ def board(): activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): unit = t.holder - lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {t.order.board_order(t)} | ' + row = t.order.board_order(t) + lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {row.owner_file} | {row.owner} | ' + f'{row.order} | {row.due} | ' f'{read[t.id].line if t.id in read else "-"} | {t.order.line} | ' f'{activity[unit].clock() if unit else "-"} |') history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index cf3c7d53e9..db79fc2508 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -251,7 +251,8 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, return self._read[repo, state, order.id] -def _clock(at: float) -> str: +def clock(at: float) -> str: + """Local HH:MM, the one clock format for orders, installs and boards.""" return time.strftime("%H:%M", time.localtime(at)) @@ -295,7 +296,7 @@ class MovingTrend(DistanceTrend): @property def projection(self) -> str: - return f"; distance {self.distance}, projected done {_clock(self.done)}" + return f"; distance {self.distance}, projected done {clock(self.done)}" @dataclass(frozen=True) @@ -496,7 +497,7 @@ def found(cls, source: ProgressSource) -> Iterator[Self]: @property def state(self) -> str: - return f"committed {self.sha} at {_clock(self.at)}, not pushed" + return f"committed {self.sha} at {clock(self.at)}, not pushed" @dataclass(frozen=True) @@ -511,7 +512,7 @@ def found(cls, source: ProgressSource) -> Iterator[Self]: @property def state(self) -> str: - return f"pushed {self.sha} at {_clock(self.at)}, no PR" + return f"pushed {self.sha} at {clock(self.at)}, no PR" @dataclass(frozen=True) @@ -609,8 +610,7 @@ def after(cls, installer: str, now: float) -> PlannedInstallDeadline: return cls(RecordedEventTime(now + cls.cadence), installer) def overdue(self, now: float) -> tuple[str, ...]: - due = time.strftime("%H:%M", time.localtime(self.at.at)) - return (f"due {due}, installer {self.installer}",) * (now >= self.at.at) + return (f"due {clock(self.at.at)}, installer {self.installer}",) * (now >= self.at.at) def plan(self, root: Path) -> None: path = self.path_in(root) @@ -626,7 +626,7 @@ def refusals(self, work: str, commander: str) -> tuple[str, ...]: def alarm(self, waiting: list[str], now: float) -> list[str]: if not waiting: return [] - due = time.strftime("%H:%M", time.localtime(self.at.at)) + due = clock(self.at.at) if now > self.at.at: return [f"INSTALL LATE (due {due}), not the orders; waiting: {', '.join(waiting)}"] return [f"INSTALL due {due}; waiting: {', '.join(waiting)}"] @@ -1041,6 +1041,9 @@ class ProblemFinding(DeclaredFamily, affix="ProblemFinding"): def instructions(self) -> tuple[str, ...]: return () + def place(self) -> str: + return "-" + @dataclass(frozen=True) class UnlocatedProblemFinding(ProblemFinding): @@ -1052,7 +1055,19 @@ class LocatedProblemFinding(ProblemFinding): where: FileLine def instructions(self) -> tuple[str, ...]: - return (f"Where: {self.where.to_wire()}",) + return (f"Where: {self.place()}",) + + def place(self) -> str: + return self.where.to_wire() + + +@dataclass(frozen=True) +class BoardOrder: + """The order columns of one board row.""" + owner_file: str + owner: str + order: str + due: str @dataclass(frozen=True, kw_only=True) @@ -1069,8 +1084,8 @@ 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 board_order(self, todo: Todo) -> str: - return f'{" ".join(self.finding.instructions())} | unassigned | - | -' + def board_order(self, todo: Todo) -> BoardOrder: + return BoardOrder(self.finding.place(), "unassigned", "-", "-") def require_transfer(self, todo: Todo) -> None: raise ValueError("This problem cannot receive another order.") @@ -1149,12 +1164,12 @@ 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}) - def board_order(self, todo: Todo) -> str: + def board_order(self, todo: Todo) -> BoardOrder: last = todo.released[-1:] - owner = todo.holder or (f'unassigned: released from {last[0].previous.owner}: ' - f'{last[0].reason.text}') - due = time.localtime(todo.deadline.recorded_at("An order deadline")) - return f'{self.owner.to_wire()} | {owner} | {self.id} | {time.strftime("%H:%M", due)}' + owner = todo.holder or (f"unassigned: released from {last[0].previous.owner}: " + f"{last[0].reason.text}") + return BoardOrder(self.owner.to_wire(), owner, self.id, + clock(todo.deadline.recorded_at("An order deadline"))) def __post_init__(self): if not self.edits or not self.banned: diff --git a/tests/test_todos.py b/tests/test_todos.py index 1fe58fe3b9..c2b63eff4f 100644 --- a/tests/test_todos.py +++ b/tests/test_todos.py @@ -18,7 +18,7 @@ from agent_comms.registration import Registration from agent_comms.threads import Thread from agent_comms.todos import ( - Assignment, ChangeTodoState, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, + Assignment, BoardOrder, ChangeTodoState, MergedProblemOrder, clock, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, BlockedTodoState, OperationDelivery, Todo, TodoConflict, TodoError, TodoState, TodoStore, ) @@ -179,3 +179,16 @@ def test_validation_rejects_ambiguous_identity_and_bad_state(tmp_path: Path) -> ) with pytest.raises(ValueError): FieldCodec.decode(type[TodoState], "unverified") + + +def test_merged_order_boards_its_owner_and_waits_on_install(tmp_path: Path) -> None: + store = TodoStore(tmp_path) + lead = thread("lead", tmp_path, 1.0) + todo = store.create( + "todo-1", "OpenHCSDev/agent-comms", "Fix the sidebar", creator=lead, goal=GoalRef("lead", 1.0, "goal-1"), + owner=lead, generation="initial", deadline=RecordedEventTime(1000.0), order=order(tmp_path)) + merged = replace(todo, order=todo.order.copy_as(MergedProblemOrder, commit="2b9c1f1ab")) + assert merged.order.board_order(merged) == BoardOrder( + f"{__file__}:1", "lead", "checked-order", clock(1000.0)) + assert merged.order.line == "Merged, delivery check unverified" + assert merged.order.waiting_on_install(merged, {}) == (merged,) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 0ed956cffa..1147db681e 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -11,7 +11,9 @@ import hashlib import json import re +import sqlite3 import subprocess +import tempfile import time from dataclasses import dataclass, replace from pathlib import Path @@ -26,7 +28,7 @@ MechanicalOrderRun, NoGoalReference, ObservedFirstCall, OrderMessage, PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo, Todo, OpenTodoState, UnorderedProblemOrder, LocatedProblemFinding, MergedProblemOrder, - HuntRecord, CleanHunt, OrderedHunt) + HuntRecord, Hunt) @dataclass(frozen=True) @@ -52,8 +54,8 @@ def deadline(self) -> RecordedEventTime: def order(self, body: str) -> PendingProblemOrder: place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) report = re.search(r'\bReport:\s*([^\n]+)', body) - if place is None or report is None: - raise ValueError(f'{self.msg}: original order needs Owner and Report lines') + if place is None or report is None or not re.search(r'^Banned:', body, re.M): + raise ValueError(f'{self.facts} {self.msg}: original order needs Owner, Banned and Report lines') return PendingProblemOrder( id=self.msg, owner=FileLine(place[1], int(place[2])), check=OrderCheck.parse(f'{self.check[0]} :: {self.check[1]} -> {self.check[2]}'), @@ -92,16 +94,28 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: if missing: print(json.dumps({'orders_missing_problem_rows': missing}, indent=2)) raise ValueError('The commander must restore the missing problem rows before transfer') - finding_repos = {} + finding_repos: dict[str, str] = {} + gone: list[str] = [] for pid in sorted(problems.keys() - selected.keys()): location = FileLine.from_wire(problems[pid]['where']) try: finding_repos[pid] = Repo.of(Path(location.file).parent).github - except subprocess.CalledProcessError as error: - raise ValueError(f'{pid}: cannot read repository from recorded path {location.file}') from error + except (subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError): + gone.append(f'{pid} {location.file}') + if gone: + raise ValueError('Findings whose checkout is gone; the commander must re-locate or drop them:\n' + + '\n'.join(gone)) comms = Comms(root) actor = comms.registry.require('claude-code') - store = TodoStore(root) + destination = TodoStore(root) + # Every write goes through Core's own store primitives on a staged copy; the + # destination changes once, in one SQLite backup, after the RTS check. + staging = Path(tempfile.mkdtemp(prefix='p707-carry-', dir=root)) + store = TodoStore(staging) + if destination.path_in(root).exists(): + with sqlite3.connect(destination.path_in(root)) as live, \ + sqlite3.connect(store.path_in(staging)) as staged: + live.backup(staged) rows = [] for pid, (unit, old, order) in selected.items(): owner = comms.registry.require(unit) @@ -177,19 +191,29 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: with store.writing() as db: finding.upsert(db) hunts = [json.loads(line) for line in source['hunts.jsonl'].splitlines() if line] - for index, raw in enumerate(hunts): - fields = dict(where=FileLine.from_wire(raw['where']), why=raw['why'], - at=RecordedEventTime(raw['at'])) - hunt = OrderedHunt(**fields, sent=raw['sent']) if raw['sent'] else CleanHunt(**fields) - # Restore historical records; do not rerun a historical source-file - # existence check or pretend that an old order is still current. - with store.writing() as db: - HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, hunt=hunt).upsert(db) + # RTS spells the Hunt member as a nullable "sent"; this is its one decode. + hunt_records = [HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, + hunt=FieldCodec.decode(Hunt, { + 'kind': 'clean' if raw['sent'] is None else 'ordered', + **{key: value for key, value in raw.items() if value is not None}, + 'at': FieldCodec.encode(RecordedEventTime(raw['at']))})) + for index, raw in enumerate(hunts)] + # Restore historical records; do not rerun a historical source-file + # existence check or pretend that an old order is still current. + with store.writing() as db: + for record in hunt_records: + record.upsert(db) report['finding_repositories'] = finding_repos report['hunts'] = len(hunts) report['merged_without_delivery_claim'] = len(merged) if any((rts / name).read_bytes() != data for name, data in source.items()): - raise ValueError('RTS changed during transfer; destination is not ready for installation') + raise ValueError(f'RTS changed during transfer; {root} is unchanged, staged copy kept at {staging}') + with sqlite3.connect(store.path_in(staging)) as staged, \ + sqlite3.connect(destination.path_in(root)) as live: + staged.backup(live) + for path in staging.iterdir(): + path.unlink() + staging.rmdir() if retire: for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py', 'deadlines.json', 'problems.json', 'strikes.jsonl', 'hunts.jsonl', 'sent.jsonl'): From da50e58d9d221c94cd0165ef4652db03ee23d817 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 15:32:01 -0400 Subject: [PATCH 11/42] P707: inline the board holder and drop restating docstrings Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/order.py | 7 +++---- src/agent_comms/todos.py | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 5cf9a29096..24d4bb1293 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -396,12 +396,11 @@ def board(): listed, read = todos(), progress() activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): - unit = t.holder row = t.order.board_order(t) - lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {row.owner_file} | {row.owner} | ' - f'{row.order} | {row.due} | ' + lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {row.owner_file} | ' + f'{row.owner} | {row.order} | {row.due} | ' f'{read[t.id].line if t.id in read else "-"} | {t.order.line} | ' - f'{activity[unit].clock() if unit else "-"} |') + f'{activity[t.holder].clock() if t.holder else "-"} |') history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) lines += ['', '## Withdrawn orders', '', *(f'- {line}' for t in history for line in t.order.withdrawals(t))] diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index db79fc2508..be433b7e4b 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -252,7 +252,6 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, def clock(at: float) -> str: - """Local HH:MM, the one clock format for orders, installs and boards.""" return time.strftime("%H:%M", time.localtime(at)) @@ -1063,7 +1062,6 @@ def place(self) -> str: @dataclass(frozen=True) class BoardOrder: - """The order columns of one board row.""" owner_file: str owner: str order: str From 57620bdc8ca9640cdec04786a07ad7fc5305700b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:37:37 -0400 Subject: [PATCH 12/42] P707: decode RTS records once into typed rows; board cells come from the order The import script decodes each RTS file once (FieldCodec dataclasses, two boundary decoders for the null spellings) instead of reading dict keys everywhere. The board row's cells are one method on the order family, so BoardOrder is gone. Deleted the unused Todo.require_released_order. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/order.py | 4 +- src/agent_comms/todos.py | 24 +-- tests/test_todos.py | 5 +- tools/carry_rts_orders.py | 306 ++++++++++++++--------------- 4 files changed, 152 insertions(+), 187 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 24d4bb1293..8ef530814e 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -396,9 +396,7 @@ def board(): listed, read = todos(), progress() activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): - row = t.order.board_order(t) - lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {row.owner_file} | ' - f'{row.owner} | {row.order} | {row.due} | ' + lines.append(f'| {t.id}: {t.text.replace("|", "|")} | {t.order.board_cells(t)} | ' f'{read[t.id].line if t.id in read else "-"} | {t.order.line} | ' f'{activity[t.holder].clock() if t.holder else "-"} |') history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index be433b7e4b..2b32a35b22 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1060,14 +1060,6 @@ def place(self) -> str: return self.where.to_wire() -@dataclass(frozen=True) -class BoardOrder: - owner_file: str - owner: str - order: str - due: str - - @dataclass(frozen=True, kw_only=True) class ProblemOrder(DeclaredFamily, affix="ProblemOrder"): """A problem's order state; an unassigned finding is not a fabricated order.""" @@ -1082,8 +1074,8 @@ 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 board_order(self, todo: Todo) -> BoardOrder: - return BoardOrder(self.finding.place(), "unassigned", "-", "-") + def board_cells(self, todo: Todo) -> str: + return f"{self.finding.place()} | unassigned | - | -" def require_transfer(self, todo: Todo) -> None: raise ValueError("This problem cannot receive another order.") @@ -1162,12 +1154,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}) - def board_order(self, todo: Todo) -> BoardOrder: + def board_cells(self, todo: Todo) -> str: last = todo.released[-1:] - owner = todo.holder or (f"unassigned: released from {last[0].previous.owner}: " - f"{last[0].reason.text}") - return BoardOrder(self.owner.to_wire(), owner, self.id, - clock(todo.deadline.recorded_at("An order deadline"))) + owner = todo.holder or f"unassigned: released from {last[0].previous.owner}: {last[0].reason.text}" + return f"{self.owner.to_wire()} | {owner} | {self.id} | {clock(todo.deadline.recorded_at('An order deadline'))}" def __post_init__(self): if not self.edits or not self.banned: @@ -2071,10 +2061,6 @@ def assigned(self, proposed: Assignment, traced: FileLine | None = None) -> Todo proposed = self.assignment.successor(proposed, self) return replace(self, assignment=proposed, revision=self.revision + 1) - def require_released_order(self, owner: ThreadIncarnation, goal_id: str) -> None: - if self.assignment is not None and self.assignment.owned_by(owner, goal_id): - raise TodoConflict(self) - def sent_in(self, message_id: str) -> bool: """Only the latest recorded send names this order, not an earlier replacement.""" return bool(self.messages) and self.messages[-1].message_id == message_id diff --git a/tests/test_todos.py b/tests/test_todos.py index c2b63eff4f..fc5e602611 100644 --- a/tests/test_todos.py +++ b/tests/test_todos.py @@ -18,7 +18,7 @@ from agent_comms.registration import Registration from agent_comms.threads import Thread from agent_comms.todos import ( - Assignment, BoardOrder, ChangeTodoState, MergedProblemOrder, clock, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, + Assignment, ChangeTodoState, MergedProblemOrder, clock, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, BlockedTodoState, OperationDelivery, Todo, TodoConflict, TodoError, TodoState, TodoStore, ) @@ -188,7 +188,6 @@ def test_merged_order_boards_its_owner_and_waits_on_install(tmp_path: Path) -> N "todo-1", "OpenHCSDev/agent-comms", "Fix the sidebar", creator=lead, goal=GoalRef("lead", 1.0, "goal-1"), owner=lead, generation="initial", deadline=RecordedEventTime(1000.0), order=order(tmp_path)) merged = replace(todo, order=todo.order.copy_as(MergedProblemOrder, commit="2b9c1f1ab")) - assert merged.order.board_order(merged) == BoardOrder( - f"{__file__}:1", "lead", "checked-order", clock(1000.0)) + assert merged.order.board_cells(merged) == f"{__file__}:1 | lead | checked-order | {clock(1000.0)}" assert merged.order.line == "Merged, delivery check unverified" assert merged.order.waiting_on_install(merged, {}) == (merged,) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 1147db681e..4bc6f187a9 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -1,9 +1,8 @@ -"""One-shot RTS order transfer. Run under a quiesced writer; remove after installation. +"""One-time import of the RTS orders, findings, strikes and hunts into Core's todo store. -Only Core's real record types and TodoStore write the destination. This does not -publish goals, send messages, signal owners or launch a provider. --retire-rts -removes the old executable entrypoints after the transferred rows are verified. -Unordered findings have no order/owner/ceiling and are not fabricated as orders. +Run once on a quiesced root during the install, then delete this file. Every write goes +through TodoStore on a staged copy that replaces the store in one SQLite backup. Nothing +here publishes goals, sends messages or launches a provider. """ from __future__ import annotations @@ -21,216 +20,199 @@ from agent_comms.change_report import FileLine from agent_comms.commander.config import Repo from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec +from agent_comms.field_codec import FieldCodec, boundary_decoder from agent_comms.goals import OrderCheck from agent_comms.recorded_time import RecordedEventTime, UnrecordedEventTime from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, - MechanicalOrderRun, NoGoalReference, ObservedFirstCall, OrderMessage, - PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo, - Todo, OpenTodoState, UnorderedProblemOrder, LocatedProblemFinding, MergedProblemOrder, - HuntRecord, Hunt) + CheckedProblemOrder, Hunt, HuntRecord, LocatedProblemFinding, MechanicalOrderRun, + MergedProblemOrder, NoGoalReference, ObservedFirstCall, OpenTodoState, OrderMessage, + PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, Todo, TodoStore, + UnorderedProblemOrder, WithdrawTodo) + +SOURCES = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') +TOOLS = ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py') + + +@dataclass(frozen=True) +class RtsProblem: + at: float + text: str + where: FileLine + held_by: str = '' + + +@dataclass(frozen=True) +class RtsSent: + at: float + id: str + text: str + to: str + + +@dataclass(frozen=True) +class RtsStrike: + at: float + task: str + unit: str + why: str + caught: str = '' @dataclass(frozen=True) class RtsOrder: + unit: str due: str msg: str at: float - fact: str facts: tuple[str, ...] - check: tuple[str, str, int] - disk: bool - works: str | None = None - started: bool = False - no_first_call: bool = False + order: CheckedProblemOrder def deadline(self) -> RecordedEventTime: hour, minute = map(int, self.due.split(':')) sent = time.localtime(self.at) - at = time.mktime((sent.tm_year, sent.tm_mon, sent.tm_mday, - hour, minute, 0, 0, 0, -1)) + at = time.mktime((sent.tm_year, sent.tm_mon, sent.tm_mday, hour, minute, 0, 0, 0, -1)) return RecordedEventTime(at + 86400 if at < self.at - 60 else at) - def order(self, body: str) -> PendingProblemOrder: - place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) - report = re.search(r'\bReport:\s*([^\n]+)', body) - if place is None or report is None or not re.search(r'^Banned:', body, re.M): - raise ValueError(f'{self.facts} {self.msg}: original order needs Owner, Banned and Report lines') - return PendingProblemOrder( - id=self.msg, owner=FileLine(place[1], int(place[2])), - check=OrderCheck.parse(f'{self.check[0]} :: {self.check[1]} -> {self.check[2]}'), - edits=tuple(re.findall(r'^\s*\d+[.)] (.+?)\s*$', body, re.M)), - banned=tuple(part.strip() for line in re.findall(r'^Banned:(.*)$', body, re.M) - for part in line.split(';') if part.strip()), - report=report[1], at=RecordedEventTime(self.at), - first_call_deadline=RecordedEventTime(self.at + 120), - first_call=ObservedFirstCall() if self.started else AwaitingFirstCall(), - run=ApplicationOrderRun(self.works) if self.works else MechanicalOrderRun()) - - -def carry(rts: Path, root: Path, apply: bool, retire: bool = False) -> int: - names = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') - source = {name: (rts / name).read_bytes() for name in names} - deadlines = json.loads(source['deadlines.json']) - problems = json.loads(source['problems.json']) - sent = [json.loads(line) for line in source['sent.jsonl'].splitlines() if line] - messages = {item['id']: item for item in sent} - strikes = [json.loads(line) for line in source['strikes.jsonl'].splitlines() if line] - selected = {} - merged = {} - for unit, orders in deadlines.items(): - for raw in orders: - values = dict(raw) - commit = values.pop('merged', '') - old = FieldCodec.decode(RtsOrder, values) - parsed = old.order(messages[old.msg]['text']) - for pid in old.facts: - if pid in selected: - raise ValueError(f'{pid}: two open RTS orders') - selected[pid] = unit, old, parsed - if commit: - merged[pid] = commit - missing = sorted(selected.keys() - problems.keys()) - if missing: - print(json.dumps({'orders_missing_problem_rows': missing}, indent=2)) - raise ValueError('The commander must restore the missing problem rows before transfer') - finding_repos: dict[str, str] = {} - gone: list[str] = [] + +@boundary_decoder +def rts_order(unit: str, raw: dict[str, object], bodies: dict[str, str]) -> RtsOrder: + """The RTS row spells a mechanical order as null works and a merge as an optional sha.""" + msg, at, works, merged = raw['msg'], raw['at'], raw['works'], raw.get('merged') + body = bodies[msg] + place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) + report = re.search(r'\bReport:\s*([^\n]+)', body) + if place is None or report is None or not re.search(r'^Banned:', body, re.M): + raise ValueError(f'{raw["facts"]} {msg}: the order needs Owner, Banned and Report lines') + directory, command, expected = raw['check'] + order = PendingProblemOrder( + id=msg, owner=FileLine(place[1], int(place[2])), + check=OrderCheck.parse(f'{directory} :: {command} -> {expected}'), + edits=tuple(re.findall(r'^\s*\d+[.)] (.+?)\s*$', body, re.M)), + banned=tuple(part.strip() for line in re.findall(r'^Banned:(.*)$', body, re.M) + for part in line.split(';') if part.strip()), + report=report[1], at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120), + first_call=ObservedFirstCall() if raw.get('started') else AwaitingFirstCall(), + run=MechanicalOrderRun() if works is None else ApplicationOrderRun(works)) + recorded = order if merged is None else order.copy_as(MergedProblemOrder, commit=merged) + return RtsOrder(unit, raw['due'], msg, at, tuple(raw['facts']), recorded) + + +@boundary_decoder +def rts_hunt(line: bytes) -> Hunt: + """RTS spells a clean hunt as a null "sent"; this is its one decode.""" + values = json.loads(line) + sent = values.pop('sent') + values.update(at=FieldCodec.encode(RecordedEventTime(values.pop('at'))), + kind='clean' if sent is None else 'ordered') + return FieldCodec.decode(Hunt, values if sent is None else {**values, 'sent': sent}) + + +def lines[T](data: bytes, kind: type[T]) -> list[T]: + return [FieldCodec.decode(kind, json.loads(line)) for line in data.splitlines() if line] + + +def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: + source = {name: (rts / name).read_bytes() for name in SOURCES} + bodies = {sent.id: sent.text for sent in lines(source['sent.jsonl'], RtsSent)} + orders = [rts_order(unit, raw, bodies) for unit, rows in + json.loads(source['deadlines.json']).items() for raw in rows] + selected = {pid: old for old in orders for pid in old.facts} + if len(selected) != sum(len(old.facts) for old in orders): + raise ValueError('A problem has two open RTS orders; split it first') + problems = {pid: FieldCodec.decode(RtsProblem, raw) + for pid, raw in json.loads(source['problems.json']).items()} + if missing := sorted(selected.keys() - problems.keys()): + raise ValueError(f'Restore the missing problem rows first: {missing}') + strikes = lines(source['strikes.jsonl'], RtsStrike) + repos, gone = {}, [] for pid in sorted(problems.keys() - selected.keys()): - location = FileLine.from_wire(problems[pid]['where']) try: - finding_repos[pid] = Repo.of(Path(location.file).parent).github + repos[pid] = Repo.of(Path(problems[pid].where.file).parent).github except (subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError): - gone.append(f'{pid} {location.file}') + gone.append(f'{pid} {problems[pid].where.file}') if gone: - raise ValueError('Findings whose checkout is gone; the commander must re-locate or drop them:\n' - + '\n'.join(gone)) + raise ValueError('Re-locate or drop the findings whose checkout is gone:\n' + '\n'.join(gone)) + report = {'source_sha256': {name: hashlib.sha256(data).hexdigest() for name, data in source.items()}, + 'orders': len(selected), 'unordered_findings': len(repos), 'strikes': len(strikes)} + if not apply: + print(json.dumps(report, indent=2)) + return comms = Comms(root) actor = comms.registry.require('claude-code') destination = TodoStore(root) - # Every write goes through Core's own store primitives on a staged copy; the - # destination changes once, in one SQLite backup, after the RTS check. staging = Path(tempfile.mkdtemp(prefix='p707-carry-', dir=root)) store = TodoStore(staging) - if destination.path_in(root).exists(): - with sqlite3.connect(destination.path_in(root)) as live, \ - sqlite3.connect(store.path_in(staging)) as staged: - live.backup(staged) - rows = [] - for pid, (unit, old, order) in selected.items(): - owner = comms.registry.require(unit) - repo = Repo.of(Path(order.check.directory)).github - history = [] - for strike in strikes: - if strike.get('task') != pid: - continue - struck = comms.registry.require(strike['unit']) - # RTS strikes name a task and unit, not an order or goal generation. - # Keep that original event identity; never guess an earlier order. - assignment = Assignment(struck.name, struck.incarnation.created_at, - actor.name, actor.incarnation.created_at, f"rts-strike:{pid}:{strike['at']}") - history.append(StrikeTodo(1, actor.incarnation, assignment, - strike['why'], RecordedEventTime(strike['at']))) - rows.append((pid, repo, problems[pid]['text'], owner, old, order, tuple(history))) - report = {'source_sha256': {name: hashlib.sha256(data).hexdigest() - for name, data in source.items()}, - 'orders': len(rows), 'strikes': sum(len(row[-1]) for row in rows), - 'unordered_findings': len(problems.keys() - selected.keys()), - 'orders_missing_problem_rows': sorted(selected.keys() - problems.keys()), - 'already_merged': merged, 'board_orders': []} - if not apply: - print(json.dumps(report, indent=2)) - return 0 + with sqlite3.connect(destination.path_in(root)) as live, sqlite3.connect(store.path_in(staging)) as staged: + live.backup(staged) existing = {todo.id: todo for todo in store.history_for_commander(actor.incarnation)} for pid, current in existing.items(): if current.assignment is not None and pid not in selected: reason = 'This old order is absent from the current open RTS orders.' - if pid in problems: - store.apply(pid, ReleaseTodo(current.revision, actor.incarnation, - current.held(), StrikeReason(reason))) - else: - store.apply(pid, WithdrawTodo(current.revision, actor.incarnation, - current.held(), reason)) - for pid, repo, text, owner, old, order, history in rows: - if pid in existing: - current = store.get(pid) - if current.order.id != old.msg: - current = store.assign(pid, expected_revision=current.revision, - owner=owner, parent=actor, generation='rts-order:' + old.msg, - goal=current.goal, deadline=old.deadline(), order=order) + store.apply(pid, ReleaseTodo(current.revision, actor.incarnation, current.held(), + StrikeReason(reason)) if pid in problems else + WithdrawTodo(current.revision, actor.incarnation, current.held(), reason)) + for pid, old in selected.items(): + owner = comms.registry.require(old.unit) + # RTS strikes name a task and unit, not an order; keep that event identity. + history = tuple(StrikeTodo(1, actor.incarnation, Assignment( + strike.unit, comms.registry.require(strike.unit).incarnation.created_at, actor.name, + actor.incarnation.created_at, f'rts-strike:{pid}:{strike.at}'), + strike.why, RecordedEventTime(strike.at)) for strike in strikes if strike.task == pid) + generation = 'rts-order:' + old.msg + if pid not in existing: + current = store.create(pid, Repo.of(Path(old.order.check.directory)).github, + problems[pid].text, creator=actor, owner=owner, + generation=generation, deadline=old.deadline(), + order=old.order, goal=NoGoalReference()) + elif existing[pid].order.id != old.msg: + current = store.assign(pid, expected_revision=existing[pid].revision, owner=owner, + parent=actor, generation=generation, goal=existing[pid].goal, + deadline=old.deadline(), order=old.order) else: - current = store.create(pid, repo, text, creator=actor, owner=owner, - generation='rts-order:' + old.msg, deadline=old.deadline(), order=order, - goal=NoGoalReference()) + current = store.get(pid) if not current.sent_in(old.msg): current = store.apply(pid, OrderMessage(current.revision, actor.incarnation, old.msg)) - # Historical strikes include former holders; replaying StrikeTodo.apply would - # wrongly require those holders to be the current assignment. Restore their - # actual Core records, together, through the record's own SQLite codec. - recorded_order = order.copy_as(MergedProblemOrder, commit=merged[pid]) if pid in merged else order - restored = replace(current, order=recorded_order, strikes=history, revision=current.revision + 1) + # Former holders' strikes cannot replay through StrikeTodo; restore the records. with store.writing() as db: - restored.upsert(db) + replace(current, order=old.order, strikes=history, revision=current.revision + 1).upsert(db) recorded = store.get(pid) - if (recorded.holder != owner.name or recorded.deadline != old.deadline() - or recorded.order.id != old.msg or recorded.strikes != history): - raise ValueError(f'{pid}: transferred Core row differs') - report['board_orders'].append(f'{pid} {recorded.holder} {old.due} {recorded.order.id}') - for pid, repo in finding_repos.items(): - problem = problems[pid] - location = FileLine.from_wire(problem['where']) - finding = Todo(id=pid, repo=repo, text=problem['text'], creator=actor.name, - creator_created=actor.created_at, state=OpenTodoState, revision=1, - goal=NoGoalReference(), assignment=None, - order=UnorderedProblemOrder(finding=LocatedProblemFinding(location)), - deadline=UnrecordedEventTime()) + if (recorded.holder, recorded.deadline, recorded.order, recorded.strikes) != ( + owner.name, old.deadline(), old.order, history): + raise ValueError(f'{pid}: the imported Core row differs from its RTS order') + for pid, repo in repos.items(): + finding = Todo(id=pid, repo=repo, text=problems[pid].text, creator=actor.name, + creator_created=actor.created_at, state=OpenTodoState, revision=1, + goal=NoGoalReference(), assignment=None, deadline=UnrecordedEventTime(), + order=UnorderedProblemOrder(finding=LocatedProblemFinding(problems[pid].where))) if pid in existing: - previous = store.get(pid) - finding = replace(finding, revision=previous.revision + 1, - strikes=previous.strikes, released=previous.released) + finding = replace(finding, revision=existing[pid].revision + 1, + strikes=existing[pid].strikes, released=existing[pid].released) with store.writing() as db: finding.upsert(db) - hunts = [json.loads(line) for line in source['hunts.jsonl'].splitlines() if line] - # RTS spells the Hunt member as a nullable "sent"; this is its one decode. - hunt_records = [HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, - hunt=FieldCodec.decode(Hunt, { - 'kind': 'clean' if raw['sent'] is None else 'ordered', - **{key: value for key, value in raw.items() if value is not None}, - 'at': FieldCodec.encode(RecordedEventTime(raw['at']))})) - for index, raw in enumerate(hunts)] - # Restore historical records; do not rerun a historical source-file - # existence check or pretend that an old order is still current. with store.writing() as db: - for record in hunt_records: - record.upsert(db) - report['finding_repositories'] = finding_repos - report['hunts'] = len(hunts) - report['merged_without_delivery_claim'] = len(merged) + for index, line in enumerate(source['hunts.jsonl'].splitlines()): + HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, hunt=rts_hunt(line)).upsert(db) if any((rts / name).read_bytes() != data for name, data in source.items()): - raise ValueError(f'RTS changed during transfer; {root} is unchanged, staged copy kept at {staging}') - with sqlite3.connect(store.path_in(staging)) as staged, \ - sqlite3.connect(destination.path_in(root)) as live: + raise ValueError(f'RTS changed during the import; {root} is unchanged, staged copy at {staging}') + with sqlite3.connect(store.path_in(staging)) as staged, sqlite3.connect(destination.path_in(root)) as live: staged.backup(live) for path in staging.iterdir(): path.unlink() staging.rmdir() - if retire: - for name in ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py', - 'deadlines.json', 'problems.json', 'strikes.jsonl', 'hunts.jsonl', 'sent.jsonl'): - (rts / name).rename(rts / (name + '.retired-p707')) + for name in (TOOLS + SOURCES) * retire: + (rts / name).rename(rts / f'{name}.retired-p707') print(json.dumps(report, indent=2)) - return 0 -def main() -> int: +def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--rts-dir', required=True, type=Path) parser.add_argument('--root', required=True, type=Path) parser.add_argument('--apply', action='store_true') parser.add_argument('--retire-rts', action='store_true') args = parser.parse_args() - return carry(args.rts_dir, args.root, args.apply, args.retire_rts) + carry(args.rts_dir, args.root, args.apply, args.retire_rts) if __name__ == '__main__': - raise SystemExit(main()) + main() From 01470614e9c5eadeba82c06c4712a3a0d1deb7ba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:39:45 -0400 Subject: [PATCH 13/42] P707: keep the board cell lines within the line limit Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/todos.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 2b32a35b22..9ea237d242 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1156,8 +1156,10 @@ class CheckedProblemOrder(ProblemOrder, declared_name=None): def board_cells(self, todo: Todo) -> str: last = todo.released[-1:] - owner = todo.holder or f"unassigned: released from {last[0].previous.owner}: {last[0].reason.text}" - return f"{self.owner.to_wire()} | {owner} | {self.id} | {clock(todo.deadline.recorded_at('An order deadline'))}" + owner = todo.holder or (f"unassigned: released from {last[0].previous.owner}: " + f"{last[0].reason.text}") + due = clock(todo.deadline.recorded_at("An order deadline")) + return f"{self.owner.to_wire()} | {owner} | {self.id} | {due}" def __post_init__(self): if not self.edits or not self.banned: From 94d520bfdc14634245b1ee785ae91a11f55748ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:49:01 -0400 Subject: [PATCH 14/42] P728 finish abandoned selected requests as UNKNOWN and keep later inputs independent --- src/agent_comms/activity.py | 6 ++++++ src/agent_comms/drain_failures.py | 11 +++-------- src/agent_comms/input_drain.py | 2 ++ src/agent_comms/schedule_rules.py | 3 ++- src/agent_comms/selected_request.py | 12 +++++++----- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/agent_comms/activity.py b/src/agent_comms/activity.py index d6a0a7c2a9..7bc52a2518 100644 --- a/src/agent_comms/activity.py +++ b/src/agent_comms/activity.py @@ -109,6 +109,12 @@ def recovery(self) -> str: return "Waiting for recovery" +class FailedWakeDiagnostic(UnavailableDrainDiagnostic): + @property + def headline(self) -> str: + return "Wake failed" + + class NativeInputDrainDiagnostic(DrainDiagnostic): """A native request failure is not evidence that the inbox cannot be read.""" diff --git a/src/agent_comms/drain_failures.py b/src/agent_comms/drain_failures.py index e23672ec21..eeaacf8f38 100644 --- a/src/agent_comms/drain_failures.py +++ b/src/agent_comms/drain_failures.py @@ -34,6 +34,7 @@ async def drain_failures( comms: Comms, thread: str, owner: OwnerIdentity, + unavailable: type[UnavailableDrainDiagnostic] = UnavailableDrainDiagnostic, ): """Record and propagate failures; the observation owner waits for real change.""" snapshot = await Coordination.run_worker(comms.registry.snapshot) @@ -61,16 +62,10 @@ async def record(kind, error) -> None: await record(NativeInputDrainDiagnostic, error) raise except UNAVAILABLE_DRAIN_ERRORS as error: - await record(UnavailableDrainDiagnostic, error) + await record(unavailable, error) raise except ValueError as error: - diagnostic = StoppedDrainDiagnostic( - owner, type(error).__name__, str(error), recorded_at=time.time() - ) - await Coordination.run_worker(partial( - comms.agents.set_drain_diagnostic, thread, owner, diagnostic, - source_error=error, - )) + await record(StoppedDrainDiagnostic, error) raise else: current = await Coordination.run_worker(partial(comms.registry.require, thread)) diff --git a/src/agent_comms/input_drain.py b/src/agent_comms/input_drain.py index d926e214ec..c7706d7b9a 100644 --- a/src/agent_comms/input_drain.py +++ b/src/agent_comms/input_drain.py @@ -286,6 +286,8 @@ def _retire_observer(self, session_id: str, task: asyncio.Task[None]) -> None: state = self.state(session_id) if state.drain_task is task: state.observation_watcher = None + if not self.closing: + raise SystemExit(1) def wake_observer(self, session_id: str) -> None: state = self.state(session_id) diff --git a/src/agent_comms/schedule_rules.py b/src/agent_comms/schedule_rules.py index 5577c28c18..3e038205f6 100644 --- a/src/agent_comms/schedule_rules.py +++ b/src/agent_comms/schedule_rules.py @@ -9,6 +9,7 @@ from .coordinator import Coordination from .drain_failures import drain_failures +from .activity import FailedWakeDiagnostic from .errors import RelationViolationError from .reservation_rules import ReservationRule, RuleCheck, ReservationViolationError from .routing import ScheduledTurn @@ -160,7 +161,7 @@ async def run(self) -> None: hold = await Coordination.run_worker(inputs.comms.owners.maintenance.read) if hold.turn_pending: return hold - async with drain_failures(inputs.comms, self.thread, self.owner): + async with drain_failures(inputs.comms, self.thread, self.owner, FailedWakeDiagnostic): before = await inputs._observe_private_revision(session_id, root_id) result = await effects.turns.run_selected(session_id, self.execution) if result is not None: diff --git a/src/agent_comms/selected_request.py b/src/agent_comms/selected_request.py index d6fe4e79f0..4948e4a537 100644 --- a/src/agent_comms/selected_request.py +++ b/src/agent_comms/selected_request.py @@ -2,7 +2,6 @@ from __future__ import annotations -from asyncio import CancelledError from contextlib import asynccontextmanager from dataclasses import dataclass from functools import partial @@ -50,10 +49,13 @@ async def native_failures(self): except NativeRequestFailure as error: await self._native_failure(error) raise - except CancelledError: - # Native execution has unwound child/tool cleanup before the - # reserved request sees cancellation of its abandoned result. - await self._settle_unknown() + except BaseException: + # Every abandoned request leaves UNKNOWN after child/tool cleanup, + # including failures in local context publication, not just native errors. + try: + await self._settle_unknown() + except Exception as error: + raise SystemExit(1) from error raise async def _settle_unknown(self) -> bool: From d823e8d59b493203cae8c88fe023ed64fdf0a0ca Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 16:51:25 -0400 Subject: [PATCH 15/42] P728 derive warning headlines from declarations and verify observer retirement --- src/agent_comms/activity.py | 12 +++--------- src/agent_comms/input_drain.py | 8 ++++---- tests/test_live_drain_failure.py | 9 +++++++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/agent_comms/activity.py b/src/agent_comms/activity.py index 7bc52a2518..c9362a2cd5 100644 --- a/src/agent_comms/activity.py +++ b/src/agent_comms/activity.py @@ -87,9 +87,7 @@ class DrainDiagnostic(DeclaredFamily, affix="Diagnostic"): @abstractmethod def recovery(self) -> str: ... - @property - def headline(self) -> str: - return "Inbox unavailable" + headline: ClassVar[str] = "Inbox unavailable" @property def summary(self) -> str: @@ -110,17 +108,13 @@ def recovery(self) -> str: class FailedWakeDiagnostic(UnavailableDrainDiagnostic): - @property - def headline(self) -> str: - return "Wake failed" + headline = "Wake failed" class NativeInputDrainDiagnostic(DrainDiagnostic): """A native request failure is not evidence that the inbox cannot be read.""" - @property - def headline(self) -> str: - return "Native request failed" + headline = "Native request failed" @property def recovery(self) -> str: diff --git a/src/agent_comms/input_drain.py b/src/agent_comms/input_drain.py index c7706d7b9a..045e1adc1b 100644 --- a/src/agent_comms/input_drain.py +++ b/src/agent_comms/input_drain.py @@ -261,6 +261,8 @@ async def apply(self, observation: InputObservation, session_id: str, watcher: W class InputObservation: """File-driven observation and process-local wake behavior.""" + closing: bool + def _tick_paths(self) -> TickRevisionPaths: configuration = self.sessions.config.configuration return TickRevision.paths( @@ -284,10 +286,8 @@ def _tick_revision( def _retire_observer(self, session_id: str, task: asyncio.Task[None]) -> None: state = self.state(session_id) - if state.drain_task is task: - state.observation_watcher = None - if not self.closing: - raise SystemExit(1) + if state.drain_task is task and not self.closing: + raise SystemExit(1) def wake_observer(self, session_id: str) -> None: state = self.state(session_id) diff --git a/tests/test_live_drain_failure.py b/tests/test_live_drain_failure.py index 279ef8ba87..68872af0d6 100644 --- a/tests/test_live_drain_failure.py +++ b/tests/test_live_drain_failure.py @@ -241,10 +241,15 @@ async def broken(_session): monkeypatch.setattr(agent.inputs, "drain_inbox", broken) try: - agent.inputs.ensure_live_drain("beta") - task = agent.inputs.state("beta").drain_task + # Invoke the real retirement callback explicitly so its process exit + # does not terminate pytest's own event loop. + task = asyncio.create_task(agent.inputs.observe("beta")) + agent.inputs.state("beta").drain_task = task with pytest.raises(ValueError, match="unexpected drain implementation failure"): await task + with pytest.raises(SystemExit) as stopped: + agent.inputs._retire_observer("beta", task) + assert stopped.value.code == 1 assert isinstance( owner_view(comms).activity.readiness.source_diagnostic(), StoppedDrainDiagnostic, From 2f69e30f1f74c895cf44268839af633b1a6caa71 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 17:18:50 -0400 Subject: [PATCH 16/42] P728 let the tick family retain failed reads until a real input change --- src/agent_comms/activity.py | 4 ++++ src/agent_comms/drain_failures.py | 5 +--- src/agent_comms/input_drain.py | 39 ++++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/agent_comms/activity.py b/src/agent_comms/activity.py index c9362a2cd5..ba1aec87eb 100644 --- a/src/agent_comms/activity.py +++ b/src/agent_comms/activity.py @@ -89,6 +89,10 @@ def recovery(self) -> str: ... headline: ClassVar[str] = "Inbox unavailable" + @classmethod + def from_error(cls, owner: OwnerIdentity, error: Exception) -> Self: + return cls(owner, type(error).__name__, str(error), recorded_at=time.time()) + @property def summary(self) -> str: return f"{self.headline} · {self.error_type}: {self.reason} · {self.recovery}" diff --git a/src/agent_comms/drain_failures.py b/src/agent_comms/drain_failures.py index eeaacf8f38..bca73790d2 100644 --- a/src/agent_comms/drain_failures.py +++ b/src/agent_comms/drain_failures.py @@ -4,7 +4,6 @@ import asyncio import sqlite3 -import time from contextlib import asynccontextmanager from functools import partial from typing import TYPE_CHECKING @@ -43,9 +42,7 @@ async def drain_failures( )) async def record(kind, error) -> None: - diagnostic = kind( - owner, type(error).__name__, str(error), recorded_at=time.time() - ) + diagnostic = kind.from_error(owner, error) await Coordination.run_worker(partial( comms.agents.set_drain_diagnostic, thread, owner, diagnostic, source_error=error, diff --git a/src/agent_comms/input_drain.py b/src/agent_comms/input_drain.py index 045e1adc1b..b6c76e8059 100644 --- a/src/agent_comms/input_drain.py +++ b/src/agent_comms/input_drain.py @@ -26,6 +26,7 @@ from .comms import Comms from .coordination_errors import IdentityConflict from .drain_failures import DEFERRED_DRAIN_ERRORS, UNAVAILABLE_DRAIN_ERRORS, drain_failures +from .activity import FailedWakeDiagnostic from .coordination_cohort import next_sealed_assignment from .coordinator import Coordination from .cursor_owner import CursorOwner @@ -161,20 +162,46 @@ async def drain(self, inputs, session_id: str, root_id: str) -> int: class TickState(ABC): - @abstractmethod async def next( self, observation: InputObservation, session_id: str, revision: TickRevision, + ) -> SettledTick | FullTick | FailedTick: + try: + return await self._next(observation, session_id, revision) + except UNAVAILABLE_DRAIN_ERRORS as error: + return FailedTick(error) + + @abstractmethod + async def _next( + self, observation: InputObservation, session_id: str, revision: TickRevision, ) -> SettledTick | FullTick: ... class UnsettledTick(TickState): - async def next( + async def _next( self, observation: InputObservation, session_id: str, revision: TickRevision, ) -> FullTick: read = await observation._own_entry(session_id) return FullTick(replace(revision, registry=read.revision), read.entry) +@dataclass(frozen=True, slots=True) +class FailedTick(UnsettledTick): + error: Exception + + async def apply( + self, observation: InputObservation, session_id: str, watcher: WireWatch, + ) -> TickState: + thread = observation.sessions.require(session_id) + snapshot = await Coordination.run_worker(observation.comms.registry.snapshot) + owner = snapshot.owner_identity(thread) + diagnostic = FailedWakeDiagnostic.from_error(owner, self.error) + await Coordination.run_worker(partial( + observation.comms.agents.set_drain_diagnostic, thread, owner, diagnostic, + source_error=self.error, + )) + return self + + @dataclass(frozen=True, slots=True) class FullTick: revision: TickRevision @@ -230,16 +257,16 @@ class SettledTick(TickState): claimed_after_seq: int | None through_seq: int - async def next( + async def _next( self, observation: InputObservation, session_id: str, revision: TickRevision, ) -> SettledTick | FullTick: if revision.other != self.revision.other or not observation._quiescent(session_id): - return await UnsettledTick().next(observation, session_id, revision) + return await UnsettledTick()._next(observation, session_id, revision) if revision.registry != self.revision.registry: # A standby's wait graph also depends on other threads' entries. if observation.effects.turns.goals.in_standby(session_id): - return await UnsettledTick().next(observation, session_id, revision) - step = await UnsettledTick().next(observation, session_id, revision) + return await UnsettledTick()._next(observation, session_id, revision) + step = await UnsettledTick()._next(observation, session_id, revision) if step.entry != self.entry: return step else: From ac704a4a22c7cd26ecdbac40e6e3ecdd9d575255 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:48:32 -0400 Subject: [PATCH 17/42] P707 derive board checks and time labels from their existing owners --- src/agent_comms/commander/order.py | 22 +++++++++------------- src/agent_comms/todos.py | 28 ++++++++++------------------ tests/test_todos.py | 7 +++++-- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 05b6da36e3..4cf5d5cb52 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -41,7 +41,7 @@ from ..tools import (CommsFireTool, CommsSendTool, CommsOrderCheckTool, CommsOrderServeTool, CommsOrderDoneTool, CommsOrderTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, CommsTodoTool, TodoList) -from ..todos import (clock, ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent, +from ..todos import (ApplicationOrderRun, VerdictAudience, InstallDeadline, PlannedInstallDeadline, OrderMessage, MechanicalOrderRun, OrderRun, BluntText, CommitDelivery, NotifiedFirstCallEvent, ObservedFirstCallEvent, OperationDelivery, CleanHunt, OrderedHunt, PendingProblemOrder, RecordFirstCallTodo, OrderDeletion, StrikeReason, StrikeTodo, VoidStrikeTodo, LocatedProblemFinding, UnlocatedProblemFinding, FileDeliveryEvidence, NoDeliveryEvidence, WithdrawTodo, AssignTodo, DirectiveText, OrderProgress, Todo, Slop) @@ -110,10 +110,6 @@ def struck(todo, unit): return len(todo.live_strikes(unit)) -def hhmm(event): - return clock(event.at) - - def deadline(due): """HH:MM local time: the first such minute at or after now.""" hour, minute = map(int, due.split(':')) @@ -394,9 +390,9 @@ def board(): lines = ['# Fleet board', '', "GENERATED by agent-comms-commander order from Core's Todo store. Do not edit by hand:", '`order problem` adds a row, `order send ... Problem: P` orders it, `order close` or `order withdraw` removes it.', '', - '| Problem | Owner file | Owner | Order | Due | Progress | Order state ' + '| Problem | Owner file | Owner | Order | Due | Check | Progress | Order state ' '| Owner last acted |', - '|---|---|---|---|---|---|---|---|'] + '|---|---|---|---|---|---|---|---|---|'] listed, read = todos(), progress() activity = {unit: last_activity(unit) for unit in {t.holder for t in listed if t.holder}} for t in sorted(listed, key=lambda t: int(t.id[1:]) if t.id[1:].isdigit() else 0): @@ -527,7 +523,7 @@ def first_calls(held, activity): call(CommsTodoTool(id=todo.id, operation=RecordFirstCallTodo( todo.revision, COMMS.registry.require(ME).incarnation, event))) if not acted and COMMS.goals.todos.get(todo.id).revision != todo.revision: - missing.append(f'{unit} (order {order.id}, sent {hhmm(order.at)})') + missing.append(f'{unit} (order {order.id}, sent {order.at.clock()})') return missing @@ -597,7 +593,7 @@ def deadline_lines(pending: list[Todo], read: dict[str, OrderProgress], since: f if not t.overdue(now): continue answer = reply(t.holder, ordered) - due = f'due {hhmm(t.deadline)}, order {t.order.id}' + due = f'due {t.deadline.clock()}, order {t.order.id}' late.append(f'{t.holder} ({due}, {work.line}{replied(answer)})') alarm = (f'OVERDUE: {", ".join(late)} -> record merged work; ' 'push or merge unfinished work now') @@ -702,7 +698,7 @@ def gate(self) -> None: def unstrike(task: str, why: str, at: str | None = None) -> None: """A strike recorded in error is voided in Core, then narrated and posted to #comms.""" todo = COMMS.goals.todos.get(task) - live = [s for s in todo.live_strikes() if at in (None, hhmm(s.at))] + live = [s for s in todo.live_strikes() if at in (None, s.at.clock())] if not live: sys.exit(f'Refused: {task} has no live strike' + (f' recorded at {at}.' if at else '.')) voided = live[-1] @@ -712,7 +708,7 @@ def unstrike(task: str, why: str, at: str | None = None) -> None: unit = voided.previous.owner n = struck(COMMS.goals.todos.get(task), unit) line = (f'{time.strftime("%Y-%m-%d %H:%M")} {unit} (strike voided on {task}, ' - f'recorded {hhmm(voided.at)}; {n} strike(s) remain): {why}') + f'recorded {voided.at.clock()}; {n} strike(s) remain): {why}') on_record(line, 'Void', VerdictAudience.of(unit, todo.goal)) print(f'{unit}: {n} strike(s) on {task}') @@ -729,9 +725,9 @@ def due(pid: str, at: str, why: str) -> None: call(CommsTodoTool(id=pid, operation=AssignTodo( todo.revision, todo.assignment, moved, todo.order, todo.goal))) on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due ' - f'{hhmm(todo.deadline)} moved to {hhmm(moved)}): {why}', 'Deadline', + f'{todo.deadline.clock()} moved to {moved.clock()}): {why}', 'Deadline', VerdictAudience.of(todo.held().owner, todo.goal)) - print(f'{pid}: due {hhmm(moved)}') + print(f'{pid}: due {moved.clock()}') def withdraw(pid: str, why: str, superseded_by: str | None = None) -> None: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index d96f1ae426..470d05d043 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -255,10 +255,6 @@ def read(self, repo: str, state: str, order: ProblemOrder) -> tuple[PullRequest, return self._read[repo, state, order.id] -def clock(at: float) -> str: - return time.strftime("%H:%M", time.localtime(at)) - - @dataclass(frozen=True) class DistanceReading: at: float @@ -310,7 +306,7 @@ class MovingTrend(DistanceTrend): @property def projection(self) -> str: - return f"; distance {self.distance}, projected done {clock(self.done)}" + return f"; distance {self.distance}, projected done {RecordedEventTime(self.done).clock()}" @dataclass(frozen=True) @@ -551,7 +547,7 @@ def found(cls, source: ProgressSource) -> Iterator[Self]: @property def state(self) -> str: - return f"committed {self.sha} at {clock(self.at)}, not pushed" + return f"committed {self.sha} at {RecordedEventTime(self.at).clock()}, not pushed" @dataclass(frozen=True) @@ -566,7 +562,7 @@ def found(cls, source: ProgressSource) -> Iterator[Self]: @property def state(self) -> str: - return f"pushed {self.sha} at {clock(self.at)}, no PR" + return f"pushed {self.sha} at {RecordedEventTime(self.at).clock()}, no PR" @dataclass(frozen=True) @@ -664,7 +660,7 @@ def after(cls, installer: str, now: float) -> PlannedInstallDeadline: return cls(RecordedEventTime(now + cls.cadence), installer) def overdue(self, now: float) -> tuple[str, ...]: - return (f"due {clock(self.at.at)}, installer {self.installer}",) * (now >= self.at.at) + return (f"due {self.at.clock()}, installer {self.installer}",) * (now >= self.at.at) def plan(self, root: Path) -> None: path = self.path_in(root) @@ -680,10 +676,10 @@ def refusals(self, work: str, commander: str) -> tuple[str, ...]: def alarm(self, waiting: list[str], now: float) -> list[str]: if not waiting: return [] - due = clock(self.at.at) if now > self.at.at: - return [f"INSTALL LATE (due {due}), not the orders; waiting: {', '.join(waiting)}"] - return [f"INSTALL due {due}; waiting: {', '.join(waiting)}"] + return [f"INSTALL LATE (due {self.at.clock()}), not the orders; " + f"waiting: {', '.join(waiting)}"] + return [f"INSTALL due {self.at.clock()}; waiting: {', '.join(waiting)}"] @dataclass(frozen=True) @@ -1134,7 +1130,7 @@ def pull_request_ids(cls, body: str) -> tuple[str, ...]: for line in body.splitlines() if line.startswith(cls.pull_request_label)) def board_cells(self, todo: Todo) -> str: - return f"{self.finding.place()} | unassigned | - | -" + return f"{self.finding.place()} | unassigned | - | - | -" def require_transfer(self, todo: Todo) -> None: raise ValueError("This problem cannot receive another order.") @@ -1217,8 +1213,8 @@ def board_cells(self, todo: Todo) -> str: last = todo.released[-1:] owner = todo.holder or (f"unassigned: released from {last[0].previous.owner}: " f"{last[0].reason.text}") - due = clock(todo.deadline.recorded_at("An order deadline")) - return f"{self.owner.to_wire()} | {owner} | {self.id} | {due}" + check = json.dumps(FieldCodec.encode(self.check)).replace("|", "|") + return f"{self.owner.to_wire()} | {owner} | {self.id} | {todo.deadline.clock()} | {check}" def __post_init__(self): if not self.edits or not self.banned: @@ -1312,10 +1308,6 @@ def require_checked(self, problem: str) -> None: def withdrawn(self, previous: Assignment, why: str, superseded_by: str | None) -> ProblemOrder: raise ValueError("Only an undelivered order can be withdrawn.") - def waiting_on_install(self, todo: Todo, read: dict[str, OrderProgress]) -> tuple[Todo, ...]: - """The order waiting on the fleet's install to record it working.""" - return () - @dataclass(frozen=True, kw_only=True) class PendingProblemOrder(CheckedProblemOrder): diff --git a/tests/test_todos.py b/tests/test_todos.py index fc5e602611..166da0dd1f 100644 --- a/tests/test_todos.py +++ b/tests/test_todos.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from concurrent.futures import ProcessPoolExecutor from contextlib import closing from dataclasses import replace @@ -18,7 +19,7 @@ from agent_comms.registration import Registration from agent_comms.threads import Thread from agent_comms.todos import ( - Assignment, ChangeTodoState, MergedProblemOrder, clock, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, + Assignment, ChangeTodoState, MergedProblemOrder, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, BlockedTodoState, OperationDelivery, Todo, TodoConflict, TodoError, TodoState, TodoStore, ) @@ -188,6 +189,8 @@ def test_merged_order_boards_its_owner_and_waits_on_install(tmp_path: Path) -> N "todo-1", "OpenHCSDev/agent-comms", "Fix the sidebar", creator=lead, goal=GoalRef("lead", 1.0, "goal-1"), owner=lead, generation="initial", deadline=RecordedEventTime(1000.0), order=order(tmp_path)) merged = replace(todo, order=todo.order.copy_as(MergedProblemOrder, commit="2b9c1f1ab")) - assert merged.order.board_cells(merged) == f"{__file__}:1 | lead | checked-order | {clock(1000.0)}" + cells = merged.order.board_cells(merged) + assert cells.startswith(f"{__file__}:1 | lead | checked-order | {RecordedEventTime(1000.0).clock()} | ") + assert json.loads(cells.split(" | ", 4)[-1]) == FieldCodec.encode(merged.order.check) assert merged.order.line == "Merged, delivery check unverified" assert merged.order.waiting_on_install(merged, {}) == (merged,) From 9eaf65f13c4ee7a44eab277025a5e941ab2bc8ed Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:59:35 -0400 Subject: [PATCH 18/42] P707 decode recorded recipients and limit the carry to open orders --- tools/carry_rts_orders.py | 40 +++++++++------------------------------ 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 4bc6f187a9..5319d7e666 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -1,4 +1,4 @@ -"""One-time import of the RTS orders, findings, strikes and hunts into Core's todo store. +"""One-time import of open RTS orders, their strikes and hunts into Core's todo store. Run once on a quiesced root during the install, then delete this file. Every write goes through TodoStore on a staged copy that replaces the store in one SQLite backup. Nothing @@ -7,11 +7,9 @@ from __future__ import annotations import argparse -import hashlib import json import re import sqlite3 -import subprocess import tempfile import time from dataclasses import dataclass, replace @@ -22,12 +20,11 @@ from agent_comms.comms import Comms from agent_comms.field_codec import FieldCodec, boundary_decoder from agent_comms.goals import OrderCheck -from agent_comms.recorded_time import RecordedEventTime, UnrecordedEventTime +from agent_comms.recorded_time import RecordedEventTime from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, - CheckedProblemOrder, Hunt, HuntRecord, LocatedProblemFinding, MechanicalOrderRun, - MergedProblemOrder, NoGoalReference, ObservedFirstCall, OpenTodoState, OrderMessage, - PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, Todo, TodoStore, - UnorderedProblemOrder, WithdrawTodo) + CheckedProblemOrder, Hunt, HuntRecord, MechanicalOrderRun, + MergedProblemOrder, NoGoalReference, ObservedFirstCall, OrderMessage, + PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo) SOURCES = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') TOOLS = ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py') @@ -46,13 +43,13 @@ class RtsSent: at: float id: str text: str - to: str + to: tuple[str, ...] @dataclass(frozen=True) class RtsStrike: at: float - task: str + task: str | None unit: str why: str caught: str = '' @@ -124,16 +121,7 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: if missing := sorted(selected.keys() - problems.keys()): raise ValueError(f'Restore the missing problem rows first: {missing}') strikes = lines(source['strikes.jsonl'], RtsStrike) - repos, gone = {}, [] - for pid in sorted(problems.keys() - selected.keys()): - try: - repos[pid] = Repo.of(Path(problems[pid].where.file).parent).github - except (subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError): - gone.append(f'{pid} {problems[pid].where.file}') - if gone: - raise ValueError('Re-locate or drop the findings whose checkout is gone:\n' + '\n'.join(gone)) - report = {'source_sha256': {name: hashlib.sha256(data).hexdigest() for name, data in source.items()}, - 'orders': len(selected), 'unordered_findings': len(repos), 'strikes': len(strikes)} + report = {'orders': len(selected), 'strikes': len(strikes)} if not apply: print(json.dumps(report, indent=2)) return @@ -179,16 +167,6 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: if (recorded.holder, recorded.deadline, recorded.order, recorded.strikes) != ( owner.name, old.deadline(), old.order, history): raise ValueError(f'{pid}: the imported Core row differs from its RTS order') - for pid, repo in repos.items(): - finding = Todo(id=pid, repo=repo, text=problems[pid].text, creator=actor.name, - creator_created=actor.created_at, state=OpenTodoState, revision=1, - goal=NoGoalReference(), assignment=None, deadline=UnrecordedEventTime(), - order=UnorderedProblemOrder(finding=LocatedProblemFinding(problems[pid].where))) - if pid in existing: - finding = replace(finding, revision=existing[pid].revision + 1, - strikes=existing[pid].strikes, released=existing[pid].released) - with store.writing() as db: - finding.upsert(db) with store.writing() as db: for index, line in enumerate(source['hunts.jsonl'].splitlines()): HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, hunt=rts_hunt(line)).upsert(db) @@ -199,7 +177,7 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: for path in staging.iterdir(): path.unlink() staging.rmdir() - for name in (TOOLS + SOURCES) * retire: + for name in (TOOLS + tuple(name for name in SOURCES if name != 'problems.json')) * retire: (rts / name).rename(rts / f'{name}.retired-p707') print(json.dumps(report, indent=2)) From 9b81edcb47fc9e8a49b4946b487c4140d3a19f35 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:04:10 -0400 Subject: [PATCH 19/42] P707 derive repository from the declared source rather than the check working directory --- tools/carry_rts_orders.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 5319d7e666..d7e36fe210 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -10,6 +10,7 @@ import json import re import sqlite3 +import subprocess import tempfile import time from dataclasses import dataclass, replace @@ -148,7 +149,11 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: strike.why, RecordedEventTime(strike.at)) for strike in strikes if strike.task == pid) generation = 'rts-order:' + old.msg if pid not in existing: - current = store.create(pid, Repo.of(Path(old.order.check.directory)).github, + try: + repo = Repo.of(Path(old.order.owner.file).parent) + except (subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError): + repo = Repo.of(Path(old.order.check.directory)) + current = store.create(pid, repo.github, problems[pid].text, creator=actor, owner=owner, generation=generation, deadline=old.deadline(), order=old.order, goal=NoGoalReference()) From 08150801db7d107bd2842c1b59fe0d8ca2ccd38f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:08:59 -0400 Subject: [PATCH 20/42] P707 declare retired inputs and migrate every removed clock consumer --- src/agent_comms/commander/request.py | 4 ++-- src/agent_comms/commander/rotate.py | 2 +- tools/carry_rts_orders.py | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/agent_comms/commander/request.py b/src/agent_comms/commander/request.py index 0c4fe2ac93..b32810c6a2 100644 --- a/src/agent_comms/commander/request.py +++ b/src/agent_comms/commander/request.py @@ -16,7 +16,7 @@ from ..goal_states import ActiveGoal from ..goals import Goal, RequestGoalOrder from ..todos import Todo -from .order import COMMS, ME, CommanderVerb, RuledVerb, hhmm +from .order import COMMS, ME, CommanderVerb, RuledVerb def open_requests() -> tuple[tuple[Goal, tuple[Todo, ...]], ...]: @@ -62,7 +62,7 @@ def run(self) -> None: for goal, served in requests: print(f"{goal.id} {goal.text}") for todo in served: - print(f" {todo.id} due {hhmm(todo.deadline)}: {todo.text}") + print(f" {todo.id} due {todo.deadline.clock()}: {todo.text}") def main(a: list[str]) -> None: diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 407366095a..abdd215237 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -33,7 +33,7 @@ def main(a): unit = units[turn % len(units)] turn += 1 read = order.progress() - orders = ', '.join(f"{t.id} due {order.hhmm(t.deadline)} ({read[t.id].line})" + orders = ', '.join(f"{t.id} due {t.deadline.clock()} ({read[t.id].line})" for t in owed[unit]) edits = judge.Edit.unjudged(order.COMMS, order.ME) waiting = sum(edit.thread == unit for edit in edits) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index d7e36fe210..5f3fe524d5 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -27,7 +27,8 @@ MergedProblemOrder, NoGoalReference, ObservedFirstCall, OrderMessage, PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo) -SOURCES = ('deadlines.json', 'problems.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') +RETIRED = ('deadlines.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') +SOURCES = ('problems.json', *RETIRED) TOOLS = ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py') @@ -182,8 +183,9 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: for path in staging.iterdir(): path.unlink() staging.rmdir() - for name in (TOOLS + tuple(name for name in SOURCES if name != 'problems.json')) * retire: - (rts / name).rename(rts / f'{name}.retired-p707') + if retire: + for name in TOOLS + RETIRED: + (rts / name).rename(rts / f'{name}.retired-p707') print(json.dumps(report, indent=2)) From 4a5239dc5ee0c16b4006ae03b6a70f6d048cfdb6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:10:15 -0400 Subject: [PATCH 21/42] P707 keep the open-index fixture from dropping hunt indexes --- tests/test_todos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_todos.py b/tests/test_todos.py index 166da0dd1f..668581cebe 100644 --- a/tests/test_todos.py +++ b/tests/test_todos.py @@ -100,7 +100,8 @@ def test_commander_reads_decode_only_open_todos(tmp_path, monkeypatch): assignment=None, order=delivered).insert(db) with closing(CoordinationStore.writer(store.path)) as db: indexes = tuple(name for name, in db.execute( - "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'")) + "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=? " + "AND name NOT LIKE 'sqlite_%'", (Todo.declared_name,))) for name in indexes: db.execute(f'DROP INDEX "{name}"') db.commit() From ce0e0f26a8c4f2068def3773e2f540cca6df8f9a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:35:44 -0400 Subject: [PATCH 22/42] P707 import typed orders through the canonical Todo transaction and shared creation and time methods --- src/agent_comms/commander/order.py | 15 +- src/agent_comms/recorded_time.py | 7 + src/agent_comms/todos.py | 49 +++--- tools/carry_rts_orders.py | 268 +++++++++++++---------------- 4 files changed, 152 insertions(+), 187 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 4cf5d5cb52..1c82e71218 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -110,15 +110,6 @@ def struck(todo, unit): return len(todo.live_strikes(unit)) -def deadline(due): - """HH:MM local time: the first such minute at or after now.""" - hour, minute = map(int, due.split(':')) - now = time.time() - day = time.localtime(now) - at = time.mktime((day.tm_year, day.tm_mon, day.tm_mday, hour, minute, 0, 0, 0, -1)) - return RecordedEventTime(at + 86400 if at < now - 60 else at) - - # Eliminate forwarding with inheritance and multiple parents; decompose the hierarchy if it is not sound. # A forwarder restates an implementation pair (paper 1 Fig 3: fields + forwarding leave routes to maintain), so # nothing the commander approves, delivers or orders may add or keep one. The cure is the hierarchy, never a @@ -287,7 +278,7 @@ def send(unit, due, body: str, clean=False, mechanical=False, reassign=None, new deletes=OrderDeletion.parse(deletes[1] if (deletes := re.search( rf'(?m)^{re.escape(OrderDeletion.label)}\s*(.+)$', body)) else None), at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) - common = dict(order=order, request=order.request(COMMS.registry.require(ME), tuple(group)), deadline=deadline(due), commander=ME) + common = dict(order=order, request=order.request(COMMS.registry.require(ME), tuple(group)), deadline=RecordedEventTime(time.time()).at_clock(due), commander=ME) if new: call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction( problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], **common))) @@ -721,7 +712,7 @@ def due(pid: str, at: str, why: str) -> None: if len(why.split()) < 8: sys.exit('Refused: --why says why the deadline moves (8+ words)') refuse(why, *OUTGOING) # posted to #comms, so refused before Core records the deadline - moved = deadline(at) + moved = RecordedEventTime(time.time()).at_clock(at) call(CommsTodoTool(id=pid, operation=AssignTodo( todo.revision, todo.assignment, moved, todo.order, todo.goal))) on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due ' @@ -834,7 +825,7 @@ class InstallVerb(RuledVerb): installer: str = option("installer", help="the unit that runs the install") def run(self) -> None: - PlannedInstallDeadline(deadline(self.at), self.installer).plan(COMMS.root) + PlannedInstallDeadline(RecordedEventTime(time.time()).at_clock(self.at), self.installer).plan(COMMS.root) print(f'install due {self.at}, run by {self.installer}') diff --git a/src/agent_comms/recorded_time.py b/src/agent_comms/recorded_time.py index 35bb91357d..b4464897df 100644 --- a/src/agent_comms/recorded_time.py +++ b/src/agent_comms/recorded_time.py @@ -116,6 +116,13 @@ def before(self, timestamp: float) -> bool: def clock(self) -> str: return time.strftime("%H:%M:%S", time.localtime(self.at)) + def at_clock(self, clock: str) -> RecordedEventTime: + """The next local HH:MM minute, anchored to this recorded instant.""" + hour, minute = map(int, clock.split(':')) + day = time.localtime(self.at) + at = time.mktime((day.tm_year, day.tm_mon, day.tm_mday, hour, minute, 0, 0, 0, -1)) + return RecordedEventTime(at + 86400 if at < self.at - 60 else at) + def recorded_at(self, label: str) -> float: return self.at diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 470d05d043..577aad6302 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1535,6 +1535,10 @@ class Assignment: previous_owners: tuple[ThreadIncarnation, ...] = field( default=(), metadata={"wire_omit_default": True}) + @classmethod + def of(cls, owner: ThreadIncarnation, parent: ThreadIncarnation, generation: str) -> Assignment: + return cls(owner.name, owner.wire_birth, parent.name, parent.wire_birth, generation) + def __post_init__(self) -> None: _text(self.owner, "Thread name", 128) _text(self.parent, "Thread name", 128) @@ -1663,8 +1667,8 @@ def apply(self, current: Todo) -> Todo: ... class AssignTodo(TodoOperation): proposed: Assignment deadline: EventTime - order: PendingProblemOrder - goal: GoalRef + order: PendingProblemOrder | MergedProblemOrder + goal: GoalReference traced: FileLine | None = field(default=None, metadata={"wire_omit_default": True}) def apply(self, current: Todo) -> Todo: @@ -2039,6 +2043,17 @@ class Todo(TypedTable): corrections: tuple[WordingCorrection, ...] = () messages: tuple[OrderMessage, ...] = () + @classmethod + def ordered(cls, todo_id: str, repo: str, text: str, creator: ThreadIncarnation, + assignment: Assignment, deadline: EventTime, order: CheckedProblemOrder, + goal: GoalReference = NoGoalReference()) -> Todo: + _text(todo_id, "Todo ID", 128) + _text(text, "Todo text", 2000) + deadline.require_deadline() + goal.require_reference() + return cls(todo_id, _repo(repo), text, creator.name, creator.wire_birth, OpenTodoState, + 1, goal, assignment, order, deadline) + @property def pending(self): return self.order.pending @@ -2369,26 +2384,9 @@ def create( order: PendingProblemOrder, goal: GoalRef, ) -> Todo: - _text(todo_id, "Todo ID", 128) - _text(text, "Todo text", 2000) - repo_id = _repo(repo) identity, assignee = _participant(creator), _thread(owner) - deadline.require_deadline() - goal.require_reference() - proposed = Todo( - id=todo_id, - repo=repo_id, - text=text, - creator=identity.name, - creator_created=identity.created_at, - state=OpenTodoState, - revision=1, - goal=goal, - assignment=Assignment(assignee.name, assignee.created_at, - identity.name, identity.created_at, generation), - deadline=deadline, - order=order, - ) + proposed = Todo.ordered(todo_id, repo, text, identity, + Assignment.of(assignee, identity, generation), deadline, order, goal) with self.writing() as db: proposed.insert_if_absent(db) current = self._current(db, todo_id) @@ -2447,12 +2445,5 @@ def assign( order: PendingProblemOrder, traced: FileLine | None = None, ) -> Todo: - identity, parent_identity = _thread(owner), _thread(parent) - proposed = Assignment( - identity.name, - identity.created_at, - parent_identity.name, - parent_identity.created_at, - generation, - ) + proposed = Assignment.of(_thread(owner), _thread(parent), generation) return self.apply(todo_id, AssignTodo(expected_revision, proposed, deadline, order, goal, traced)) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 5f3fe524d5..7b748e4eeb 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -1,35 +1,24 @@ -"""One-time import of open RTS orders, their strikes and hunts into Core's todo store. - -Run once on a quiesced root during the install, then delete this file. Every write goes -through TodoStore on a staged copy that replaces the store in one SQLite backup. Nothing -here publishes goals, sends messages or launches a provider. -""" from __future__ import annotations import argparse import json import re -import sqlite3 import subprocess -import tempfile -import time from dataclasses import dataclass, replace from pathlib import Path +from agent_comms import todos from agent_comms.change_report import FileLine from agent_comms.commander.config import Repo from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec, boundary_decoder +from agent_comms.field_codec import FieldCodec, NullMember, boundary_decoder from agent_comms.goals import OrderCheck from agent_comms.recorded_time import RecordedEventTime -from agent_comms.todos import (ApplicationOrderRun, Assignment, AwaitingFirstCall, - CheckedProblemOrder, Hunt, HuntRecord, MechanicalOrderRun, - MergedProblemOrder, NoGoalReference, ObservedFirstCall, OrderMessage, - PendingProblemOrder, ReleaseTodo, StrikeReason, StrikeTodo, TodoStore, WithdrawTodo) +from agent_comms.thread_identity import ThreadIncarnation -RETIRED = ('deadlines.json', 'strikes.jsonl', 'sent.jsonl', 'hunts.jsonl') -SOURCES = ('problems.json', *RETIRED) -TOOLS = ('order.py', 'judge.py', 'rotate.py', 'tool_watch.py', 'shift_after_window.py') +RETIRED = ("deadlines.json", "strikes.jsonl", "sent.jsonl", "hunts.jsonl") +SOURCES = ("problems.json", *RETIRED) +TOOLS = ("order.py", "judge.py", "rotate.py", "tool_watch.py", "shift_after_window.py") @dataclass(frozen=True) @@ -37,7 +26,7 @@ class RtsProblem: at: float text: str where: FileLine - held_by: str = '' + held_by: str = "" @dataclass(frozen=True) @@ -51,59 +40,74 @@ class RtsSent: @dataclass(frozen=True) class RtsStrike: at: float - task: str | None + task: str | NullMember unit: str why: str - caught: str = '' + caught: str = "" + + def recorded(self, actor: ThreadIncarnation, owner: ThreadIncarnation) -> todos.StrikeTodo: + previous = todos.Assignment.of(owner, actor, f"rts-strike:{self.task}:{self.at}") + return todos.StrikeTodo(1, actor, previous, self.why, RecordedEventTime(self.at)) @dataclass(frozen=True) class RtsOrder: - unit: str due: str msg: str at: float + fact: str facts: tuple[str, ...] - order: CheckedProblemOrder - - def deadline(self) -> RecordedEventTime: - hour, minute = map(int, self.due.split(':')) - sent = time.localtime(self.at) - at = time.mktime((sent.tm_year, sent.tm_mon, sent.tm_mday, hour, minute, 0, 0, 0, -1)) - return RecordedEventTime(at + 86400 if at < self.at - 60 else at) + check: tuple[str, str, int] + disk: bool + works: str | NullMember + started: bool = False + merged: str | NullMember = NullMember() + no_first_call: bool = False + + @boundary_decoder + def declared(self, body: str) -> todos.PendingProblemOrder | todos.MergedProblemOrder: + place = re.search(r"^Owner:\s*(\S+):(\d+)\s*$", body, re.M) + report = re.search(r"\bReport:\s*([^\n]+)", body) + if place is None or report is None or not re.search(r"^Banned:", body, re.M): + raise ValueError(f"{self.facts} {self.msg}: the order needs Owner, Banned and Report") + directory, command, expected = self.check + check = OrderCheck.parse(f"{directory} :: {command} -> {expected}") + if check is None: + raise ValueError(f"{self.facts}: the order needs a check") + run: todos.OrderRun = todos.MechanicalOrderRun() + if isinstance(self.works, str): + run = todos.ApplicationOrderRun(self.works) + order = todos.PendingProblemOrder( + id=self.msg, + owner=FileLine(place[1], int(place[2])), + check=check, + edits=tuple(re.findall(r"^\s*\d+[.)] (.+?)\s*$", body, re.M)), + banned=tuple( + part.strip() + for line in re.findall(r"^Banned:(.*)$", body, re.M) + for part in line.split(";") + if part.strip() + ), + report=report[1], + at=RecordedEventTime(self.at), + first_call_deadline=RecordedEventTime(self.at + 120), + first_call=todos.ObservedFirstCall() if self.started else todos.AwaitingFirstCall(), + run=run, + ) + if isinstance(self.merged, str): + return order.copy_as(todos.MergedProblemOrder, commit=self.merged) + return order @boundary_decoder -def rts_order(unit: str, raw: dict[str, object], bodies: dict[str, str]) -> RtsOrder: - """The RTS row spells a mechanical order as null works and a merge as an optional sha.""" - msg, at, works, merged = raw['msg'], raw['at'], raw['works'], raw.get('merged') - body = bodies[msg] - place = re.search(r'^Owner:\s*(\S+):(\d+)\s*$', body, re.M) - report = re.search(r'\bReport:\s*([^\n]+)', body) - if place is None or report is None or not re.search(r'^Banned:', body, re.M): - raise ValueError(f'{raw["facts"]} {msg}: the order needs Owner, Banned and Report lines') - directory, command, expected = raw['check'] - order = PendingProblemOrder( - id=msg, owner=FileLine(place[1], int(place[2])), - check=OrderCheck.parse(f'{directory} :: {command} -> {expected}'), - edits=tuple(re.findall(r'^\s*\d+[.)] (.+?)\s*$', body, re.M)), - banned=tuple(part.strip() for line in re.findall(r'^Banned:(.*)$', body, re.M) - for part in line.split(';') if part.strip()), - report=report[1], at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120), - first_call=ObservedFirstCall() if raw.get('started') else AwaitingFirstCall(), - run=MechanicalOrderRun() if works is None else ApplicationOrderRun(works)) - recorded = order if merged is None else order.copy_as(MergedProblemOrder, commit=merged) - return RtsOrder(unit, raw['due'], msg, at, tuple(raw['facts']), recorded) - - -@boundary_decoder -def rts_hunt(line: bytes) -> Hunt: - """RTS spells a clean hunt as a null "sent"; this is its one decode.""" +def rts_hunt(line: bytes) -> todos.Hunt: values = json.loads(line) - sent = values.pop('sent') - values.update(at=FieldCodec.encode(RecordedEventTime(values.pop('at'))), - kind='clean' if sent is None else 'ordered') - return FieldCodec.decode(Hunt, values if sent is None else {**values, 'sent': sent}) + sent = values.pop("sent") + values.update( + at=FieldCodec.encode(RecordedEventTime(values.pop("at"))), + kind="clean" if sent is None else "ordered", + ) + return FieldCodec.decode(todos.Hunt, values if sent is None else {**values, "sent": sent}) def lines[T](data: bytes, kind: type[T]) -> list[T]: @@ -111,93 +115,65 @@ def lines[T](data: bytes, kind: type[T]) -> list[T]: def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: - source = {name: (rts / name).read_bytes() for name in SOURCES} - bodies = {sent.id: sent.text for sent in lines(source['sent.jsonl'], RtsSent)} - orders = [rts_order(unit, raw, bodies) for unit, rows in - json.loads(source['deadlines.json']).items() for raw in rows] - selected = {pid: old for old in orders for pid in old.facts} - if len(selected) != sum(len(old.facts) for old in orders): - raise ValueError('A problem has two open RTS orders; split it first') - problems = {pid: FieldCodec.decode(RtsProblem, raw) - for pid, raw in json.loads(source['problems.json']).items()} - if missing := sorted(selected.keys() - problems.keys()): - raise ValueError(f'Restore the missing problem rows first: {missing}') - strikes = lines(source['strikes.jsonl'], RtsStrike) - report = {'orders': len(selected), 'strikes': len(strikes)} - if not apply: - print(json.dumps(report, indent=2)) - return - comms = Comms(root) - actor = comms.registry.require('claude-code') - destination = TodoStore(root) - staging = Path(tempfile.mkdtemp(prefix='p707-carry-', dir=root)) - store = TodoStore(staging) - with sqlite3.connect(destination.path_in(root)) as live, sqlite3.connect(store.path_in(staging)) as staged: - live.backup(staged) - existing = {todo.id: todo for todo in store.history_for_commander(actor.incarnation)} - for pid, current in existing.items(): - if current.assignment is not None and pid not in selected: - reason = 'This old order is absent from the current open RTS orders.' - store.apply(pid, ReleaseTodo(current.revision, actor.incarnation, current.held(), - StrikeReason(reason)) if pid in problems else - WithdrawTodo(current.revision, actor.incarnation, current.held(), reason)) - for pid, old in selected.items(): - owner = comms.registry.require(old.unit) - # RTS strikes name a task and unit, not an order; keep that event identity. - history = tuple(StrikeTodo(1, actor.incarnation, Assignment( - strike.unit, comms.registry.require(strike.unit).incarnation.created_at, actor.name, - actor.incarnation.created_at, f'rts-strike:{pid}:{strike.at}'), - strike.why, RecordedEventTime(strike.at)) for strike in strikes if strike.task == pid) - generation = 'rts-order:' + old.msg - if pid not in existing: - try: - repo = Repo.of(Path(old.order.owner.file).parent) - except (subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError): - repo = Repo.of(Path(old.order.check.directory)) - current = store.create(pid, repo.github, - problems[pid].text, creator=actor, owner=owner, - generation=generation, deadline=old.deadline(), - order=old.order, goal=NoGoalReference()) - elif existing[pid].order.id != old.msg: - current = store.assign(pid, expected_revision=existing[pid].revision, owner=owner, - parent=actor, generation=generation, goal=existing[pid].goal, - deadline=old.deadline(), order=old.order) - else: - current = store.get(pid) - if not current.sent_in(old.msg): - current = store.apply(pid, OrderMessage(current.revision, actor.incarnation, old.msg)) - # Former holders' strikes cannot replay through StrikeTodo; restore the records. - with store.writing() as db: - replace(current, order=old.order, strikes=history, revision=current.revision + 1).upsert(db) - recorded = store.get(pid) - if (recorded.holder, recorded.deadline, recorded.order, recorded.strikes) != ( - owner.name, old.deadline(), old.order, history): - raise ValueError(f'{pid}: the imported Core row differs from its RTS order') - with store.writing() as db: - for index, line in enumerate(source['hunts.jsonl'].splitlines()): - HuntRecord(id=f'rts-hunt:{index}', commander=actor.incarnation, hunt=rts_hunt(line)).upsert(db) - if any((rts / name).read_bytes() != data for name, data in source.items()): - raise ValueError(f'RTS changed during the import; {root} is unchanged, staged copy at {staging}') - with sqlite3.connect(store.path_in(staging)) as staged, sqlite3.connect(destination.path_in(root)) as live: - staged.backup(live) - for path in staging.iterdir(): - path.unlink() - staging.rmdir() - if retire: - for name in TOOLS + RETIRED: - (rts / name).rename(rts / f'{name}.retired-p707') - print(json.dumps(report, indent=2)) - - -def main() -> None: + source = {rts / name: (rts / name).read_bytes() for name in SOURCES} + bodies = {sent.id: sent.text for sent in lines(source[rts / "sent.jsonl"], RtsSent)} + orders = [ + (unit, row, row.declared(bodies[row.msg])) + for unit, rows in FieldCodec.decode( + dict[str, tuple[RtsOrder, ...]], json.loads(source[rts / "deadlines.json"]) + ).items() + for row in rows + ] + selected = {pid: (unit, row, order) for unit, row, order in orders for pid in row.facts} + if len(selected) != sum(len(row.facts) for _, row, _ in orders): + raise ValueError("A problem has two open RTS orders; split it first") + problems = FieldCodec.decode(dict[str, RtsProblem], json.loads(source[rts / "problems.json"])) + problems = {pid: problems[pid] for pid in selected} + strikes = lines(source[rts / "strikes.jsonl"], RtsStrike) + if apply: + comms = Comms(root) + actor = comms.registry.require("claude-code").incarnation + with todos.TodoStore(root).writing() as db: + existing = {todo.id: todo for todo in todos.Todo.stream(db)} + for pid, (unit, row, order) in selected.items(): + owner = comms.registry.require(unit).incarnation + proposed = todos.Assignment.of(owner, actor, "rts-order:" + row.msg) + due = RecordedEventTime(row.at).at_clock(row.due) + if pid in existing: + todo = existing[pid] + todo.require_commander(actor) + todo.state.require_open(todo) + else: + try: + repo = Repo.of(Path(order.owner.file).parent) + except subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError: + repo = Repo.of(Path(order.check.directory)) + text = problems[pid].text + todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) + todo = todos.AssignTodo(todo.revision, proposed, due, order, todo.goal).apply(todo) + history = tuple( + strike.recorded(actor, comms.registry.require(strike.unit).incarnation) + for strike in strikes + if strike.task == pid + ) + todo = replace(todo, strikes=history, revision=todo.revision + 1) + if not todo.sent_in(row.msg): + todo = todos.OrderMessage(todo.revision, actor, row.msg).apply(todo) + todo.upsert(db) + for index, line in enumerate(source[rts / "hunts.jsonl"].splitlines()): + todos.HuntRecord(f"rts-hunt:{index}", actor, rts_hunt(line)).upsert(db) + if any(path.read_bytes() != data for path, data in source.items()): + raise ValueError(f"RTS changed during the import; {root} is unchanged") + if retire: + for name in TOOLS + RETIRED: + (rts / name).rename(rts / f"{name}.retired-p707") + print(json.dumps({"orders": len(selected), "strikes": len(strikes)}, indent=2)) + + +if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--rts-dir', required=True, type=Path) - parser.add_argument('--root', required=True, type=Path) - parser.add_argument('--apply', action='store_true') - parser.add_argument('--retire-rts', action='store_true') - args = parser.parse_args() - carry(args.rts_dir, args.root, args.apply, args.retire_rts) - - -if __name__ == '__main__': - main() + parser.add_argument("--rts-dir", dest="rts", required=True, type=Path) + parser.add_argument("--root", required=True, type=Path) + parser.add_argument("--apply", action="store_true") + parser.add_argument("--retire-rts", dest="retire", action="store_true") + carry(**vars(parser.parse_args())) From 5d884f15d40f4a2e1e0785cd74a768dd97a578ce Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:38:49 -0400 Subject: [PATCH 23/42] P707 keep order arguments typed and the absent request default on Todo --- src/agent_comms/commander/order.py | 12 ++++++++---- src/agent_comms/todos.py | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 1c82e71218..ed2e7fd610 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -278,13 +278,16 @@ def send(unit, due, body: str, clean=False, mechanical=False, reassign=None, new deletes=OrderDeletion.parse(deletes[1] if (deletes := re.search( rf'(?m)^{re.escape(OrderDeletion.label)}\s*(.+)$', body)) else None), at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) - common = dict(order=order, request=order.request(COMMS.registry.require(ME), tuple(group)), deadline=RecordedEventTime(time.time()).at_clock(due), commander=ME) + request = order.request(COMMS.registry.require(ME), tuple(group)) + due_at = RecordedEventTime(time.time()).at_clock(due) if new: call(CommsTodoCreateTool(owner=unit, order=CreateProblemGoalAction( - problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], **common))) + problem=new[0], repo=Repo.of(Path(check.directory)).github, text=new[1], + order=order, request=request, deadline=due_at, commander=ME))) for pid in [] if new else group: call(CommsOrderTool(owner=unit, order=OrderGoalAction( - problem=pid, expected_revision=COMMS.goals.todos.get(pid).revision, traced=trace, **common))) + problem=pid, expected_revision=COMMS.goals.todos.get(pid).revision, traced=trace, + order=order, request=request, deadline=due_at, commander=ME))) message += "\n" + "\n".join(order.pull_request_instruction(pid) for pid in group) sent = post(VerdictAudience((unit,)), message) # The message is each order's recorded link; a verdict citing it lands on that order. @@ -825,7 +828,8 @@ class InstallVerb(RuledVerb): installer: str = option("installer", help="the unit that runs the install") def run(self) -> None: - PlannedInstallDeadline(RecordedEventTime(time.time()).at_clock(self.at), self.installer).plan(COMMS.root) + planned = RecordedEventTime(time.time()).at_clock(self.at) + PlannedInstallDeadline(planned, self.installer).plan(COMMS.root) print(f'install due {self.at}, run by {self.installer}') diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 577aad6302..6b21568f67 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2043,10 +2043,12 @@ class Todo(TypedTable): corrections: tuple[WordingCorrection, ...] = () messages: tuple[OrderMessage, ...] = () + unrequested_goal: ClassVar[GoalReference] = NoGoalReference() + @classmethod def ordered(cls, todo_id: str, repo: str, text: str, creator: ThreadIncarnation, assignment: Assignment, deadline: EventTime, order: CheckedProblemOrder, - goal: GoalReference = NoGoalReference()) -> Todo: + goal: GoalReference = unrequested_goal) -> Todo: _text(todo_id, "Todo ID", 128) _text(text, "Todo text", 2000) deadline.require_deadline() @@ -2385,8 +2387,9 @@ def create( goal: GoalRef, ) -> Todo: identity, assignee = _participant(creator), _thread(owner) - proposed = Todo.ordered(todo_id, repo, text, identity, - Assignment.of(assignee, identity, generation), deadline, order, goal) + proposed = Todo.ordered( + todo_id, repo, text, identity, Assignment.of(assignee, identity, generation), + deadline, order, goal) with self.writing() as db: proposed.insert_if_absent(db) current = self._current(db, todo_id) From e5f042ab855cb4e32dc4b5c8dc0f1b65543a0b60 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:46:18 -0400 Subject: [PATCH 24/42] P707 derive imported order repository only from its Owner Delete the check-directory fallback. Print the problem and order identity before repository resolution; original Git failures propagate without a catch or default. --- tools/carry_rts_orders.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 7b748e4eeb..55dfa30118 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -3,7 +3,7 @@ import argparse import json import re -import subprocess +import sys from dataclasses import dataclass, replace from pathlib import Path @@ -144,10 +144,9 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: todo.require_commander(actor) todo.state.require_open(todo) else: - try: - repo = Repo.of(Path(order.owner.file).parent) - except subprocess.CalledProcessError, FileNotFoundError, NotADirectoryError: - repo = Repo.of(Path(order.check.directory)) + print(f"{pid} order {row.msg}: Owner repository {order.owner.file}", + file=sys.stderr, flush=True) + repo = Repo.of(Path(order.owner.file).parent) text = problems[pid].text todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) todo = todos.AssignTodo(todo.revision, proposed, due, order, todo.goal).apply(todo) From 7eea77d525cdd5101d4e09d7c91022320c849051 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:48:10 -0400 Subject: [PATCH 25/42] P707 format the order identity diagnostic --- tools/carry_rts_orders.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 55dfa30118..68b75f45c1 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -144,8 +144,11 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: todo.require_commander(actor) todo.state.require_open(todo) else: - print(f"{pid} order {row.msg}: Owner repository {order.owner.file}", - file=sys.stderr, flush=True) + print( + f"{pid} order {row.msg}: Owner repository {order.owner.file}", + file=sys.stderr, + flush=True, + ) repo = Repo.of(Path(order.owner.file).parent) text = problems[pid].text todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) From 18cd0f507242f42e43ad68e6d22c3b894f5c2e1f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 19:50:02 -0400 Subject: [PATCH 26/42] P707 keep failure identity without extra source lines --- tools/carry_rts_orders.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 68b75f45c1..097e9cdf42 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -144,11 +144,7 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: todo.require_commander(actor) todo.state.require_open(todo) else: - print( - f"{pid} order {row.msg}: Owner repository {order.owner.file}", - file=sys.stderr, - flush=True, - ) + print(f"{pid} order {row.msg}", file=sys.stderr, flush=True) repo = Repo.of(Path(order.owner.file).parent) text = problems[pid].text todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) From 1951a5c438dc821d32ff692b062209ed8d26116b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:20:20 -0400 Subject: [PATCH 27/42] P707 retain the checked order until assignment --- tools/carry_rts_orders.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 656ce5b780..6869cef926 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -65,7 +65,7 @@ class RtsOrder: no_first_call: bool = False @boundary_decoder - def declared(self, body: str) -> todos.PendingProblemOrder | todos.MergedProblemOrder: + def declared(self, body: str) -> todos.PendingProblemOrder: place = re.search(r"^Owner:\s*(\S+):(\d+)\s*$", body, re.M) report = re.search(r"\bReport:\s*([^\n]+)", body) if place is None or report is None or not re.search(r"^Banned:", body, re.M): @@ -77,7 +77,7 @@ def declared(self, body: str) -> todos.PendingProblemOrder | todos.MergedProblem run: todos.OrderRun = todos.MechanicalOrderRun() if isinstance(self.works, str): run = todos.ApplicationOrderRun(self.works) - order = todos.PendingProblemOrder( + return todos.PendingProblemOrder( id=self.msg, owner=FileLine(place[1], int(place[2])), check=check, @@ -94,9 +94,6 @@ def declared(self, body: str) -> todos.PendingProblemOrder | todos.MergedProblem first_call=todos.ObservedFirstCall() if self.started else todos.AwaitingFirstCall(), run=run, ) - if isinstance(self.merged, str): - return todos.MergedProblemOrder(original=order, sha=self.merged) - return order @boundary_decoder @@ -145,11 +142,15 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: todo.state.require_open(todo) else: print(f"{pid} order {row.msg}", file=sys.stderr, flush=True) - base = order.original if isinstance(order, todos.MergedProblemOrder) else order - repo = Repo.of(Path(base.owner.file).parent) + repo = Repo.of(Path(order.owner.file).parent) text = problems[pid].text todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) - todo = todos.AssignTodo(todo.revision, proposed, due, order, todo.goal).apply(todo) + record = ( + todos.MergedProblemOrder(original=order, sha=row.merged) + if isinstance(row.merged, str) + else order + ) + todo = todos.AssignTodo(todo.revision, proposed, due, record, todo.goal).apply(todo) history = tuple( strike.recorded(actor, comms.registry.require(strike.unit).incarnation) for strike in strikes From 1f8a0038f827d90da638cb1c4b3b98b43c0a157d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:25:18 -0400 Subject: [PATCH 28/42] P707 decode merge and run members at row ingress --- tools/carry_rts_orders.py | 81 ++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 6869cef926..7eafc0f771 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -4,14 +4,24 @@ import json import re import sys +from abc import abstractmethod from dataclasses import dataclass, replace from pathlib import Path +from typing import Annotated from agent_comms import todos from agent_comms.change_report import FileLine from agent_comms.commander.config import Repo from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec, NullMember, boundary_decoder +from agent_comms.field_codec import ( + FieldCodec, + FieldRepresentation, + NullMember, + SingleFieldValue, + SubclassUnion, + TextRepresentation, + boundary_decoder, +) from agent_comms.goals import OrderCheck from agent_comms.recorded_time import RecordedEventTime from agent_comms.thread_identity import ThreadIncarnation @@ -50,6 +60,54 @@ def recorded(self, actor: ThreadIncarnation, owner: ThreadIncarnation) -> todos. return todos.StrikeTodo(1, actor, previous, self.why, RecordedEventTime(self.at)) +class OrderMerge(SubclassUnion, affix=""): + @abstractmethod + def wrap( + self, order: todos.PendingProblemOrder + ) -> todos.PendingProblemOrder | todos.MergedProblemOrder: ... + + +@dataclass(frozen=True) +class NotMerged(NullMember, OrderMerge): + def wrap(self, order: todos.PendingProblemOrder) -> todos.PendingProblemOrder: + return order + + +@dataclass(frozen=True) +class MergedAt(SingleFieldValue, OrderMerge): + sha: str + + def wrap(self, order: todos.PendingProblemOrder) -> todos.MergedProblemOrder: + return todos.MergedProblemOrder(original=order, sha=self.sha) + + +class MechanicalRunJson(FieldRepresentation): + accepts_null = True + + @classmethod + def decode(cls, value: object) -> todos.MechanicalOrderRun: + FieldCodec.decode(type(None), value) + return todos.MechanicalOrderRun() + + @classmethod + def encode(cls, value: object) -> None: + return None + + @classmethod + def schema(cls): + return FieldCodec.value_schema(type(None)) + + +class ApplicationRunText(TextRepresentation[todos.ApplicationOrderRun]): + @classmethod + def from_text(cls, value: str) -> todos.ApplicationOrderRun: + return todos.ApplicationOrderRun(value) + + @classmethod + def encode(cls, value: object) -> str: + return FieldCodec.encode_record(value)["text"] + + @dataclass(frozen=True) class RtsOrder: due: str @@ -59,9 +117,12 @@ class RtsOrder: facts: tuple[str, ...] check: tuple[str, str, int] disk: bool - works: str | NullMember + works: ( + Annotated[todos.MechanicalOrderRun, MechanicalRunJson] + | Annotated[todos.ApplicationOrderRun, ApplicationRunText] + ) started: bool = False - merged: str | NullMember = NullMember() + merged: OrderMerge = NotMerged() no_first_call: bool = False @boundary_decoder @@ -74,9 +135,6 @@ def declared(self, body: str) -> todos.PendingProblemOrder: check = OrderCheck.parse(f"{directory} :: {command} -> {expected}") if check is None: raise ValueError(f"{self.facts}: the order needs a check") - run: todos.OrderRun = todos.MechanicalOrderRun() - if isinstance(self.works, str): - run = todos.ApplicationOrderRun(self.works) return todos.PendingProblemOrder( id=self.msg, owner=FileLine(place[1], int(place[2])), @@ -92,7 +150,7 @@ def declared(self, body: str) -> todos.PendingProblemOrder: at=RecordedEventTime(self.at), first_call_deadline=RecordedEventTime(self.at + 120), first_call=todos.ObservedFirstCall() if self.started else todos.AwaitingFirstCall(), - run=run, + run=self.works, ) @@ -145,12 +203,9 @@ def carry(rts: Path, root: Path, apply: bool, retire: bool) -> None: repo = Repo.of(Path(order.owner.file).parent) text = problems[pid].text todo = todos.Todo.ordered(pid, repo.github, text, actor, proposed, due, order) - record = ( - todos.MergedProblemOrder(original=order, sha=row.merged) - if isinstance(row.merged, str) - else order - ) - todo = todos.AssignTodo(todo.revision, proposed, due, record, todo.goal).apply(todo) + todo = todos.AssignTodo( + todo.revision, proposed, due, row.merged.wrap(order), todo.goal + ).apply(todo) history = tuple( strike.recorded(actor, comms.registry.require(strike.unit).incarnation) for strike in strikes From e3a55e05a4c8d0cba90b6fb26d330ad43ff17312 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:30:46 -0400 Subject: [PATCH 29/42] P707 type the declared wire encoders Src-Budget: 55 --- tools/carry_rts_orders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 7eafc0f771..90b55f3ab2 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -7,7 +7,7 @@ from abc import abstractmethod from dataclasses import dataclass, replace from pathlib import Path -from typing import Annotated +from typing import Annotated, cast from agent_comms import todos from agent_comms.change_report import FileLine @@ -94,7 +94,7 @@ def encode(cls, value: object) -> None: return None @classmethod - def schema(cls): + def schema(cls) -> dict[str, object]: return FieldCodec.value_schema(type(None)) @@ -105,7 +105,7 @@ def from_text(cls, value: str) -> todos.ApplicationOrderRun: @classmethod def encode(cls, value: object) -> str: - return FieldCodec.encode_record(value)["text"] + return cast(todos.ApplicationOrderRun, value).text @dataclass(frozen=True) From b4876bba903b03d5f903631e42abdd2abf1b7da4 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:16:33 -0400 Subject: [PATCH 30/42] P738: declare install as an order verb --- src/agent_comms/commander/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 79759aef01..928a37a7cb 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -816,7 +816,7 @@ def run(self, ctx: Commander) -> None: @dataclass(frozen=True, kw_only=True) -class InstallVerb(RuledVerb): +class InstallVerb(OrderVerb, RuledVerb): help = "plan the fleet's next install; merged and delivered orders wait on it" at: str = option("at", help="HH:MM the install is due") installer: str = option("installer", help="the unit that runs the install") From ba9b0ccf326a3753ed93881ddc6fe90f95807702 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:25:04 -0400 Subject: [PATCH 31/42] P739: package the rule that rules serve their reason --- .../commander/rules/rules-serve-their-reason.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/agent_comms/commander/rules/rules-serve-their-reason.md diff --git a/src/agent_comms/commander/rules/rules-serve-their-reason.md b/src/agent_comms/commander/rules/rules-serve-their-reason.md new file mode 100644 index 0000000000..29a9877a32 --- /dev/null +++ b/src/agent_comms/commander/rules/rules-serve-their-reason.md @@ -0,0 +1,9 @@ +--- +name: rules-serve-their-reason +description: "every rule exists for its reason; apply the reason, not the letter; a rule may be broken only when breaking it serves its reason better than keeping it, and the break is posted on #comms with that reason; following a rule's letter while defeating its reason is the same failure as breaking it" +type: feedback +--- + +Tristan's words: rules are there for a reason, and you can get around a rule if breaking it satisfies the reason more than the rule itself. + +On 2026-10-19 the commander turned a 21:34 batch ceiling into an appointment, keeping the letter of a deadline while defeating why deadlines exist, the earliest finish. From 27c5de614342ddfab4c438977da7205db32a2cbb Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:27:23 -0400 Subject: [PATCH 32/42] P739: package caution-must-pay alongside rules-serve-their-reason --- src/agent_comms/commander/rules/caution-must-pay.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/agent_comms/commander/rules/caution-must-pay.md diff --git a/src/agent_comms/commander/rules/caution-must-pay.md b/src/agent_comms/commander/rules/caution-must-pay.md new file mode 100644 index 0000000000..7033b344d7 --- /dev/null +++ b/src/agent_comms/commander/rules/caution-must-pay.md @@ -0,0 +1,9 @@ +--- +name: caution-must-pay +description: "overcaution is a defect, not diligence: every wait, gate, rehearsal or one-at-a-time step must name the specific failure it prevents that nothing else already catches; one that cannot is deleted. Stalling to look careful is the same failure as stalling." +type: feedback +--- + +Tristan's words: overcaution caused most of today's problems; it is performative work that stalls instead of shipping. + +Today's examples: installs waiting on idle turns (about three hours), a practice install required on every PR (merges from 26 an hour to 2), the word gate blocking file names, one failed turn parking a goal, storage strikes taking critical orders, the growth gate failing PRs, and landing PRs one at a time when drop-on-fail already covered the risk. From 050f1d9180e97e399548d2ea97a692a0c9917709 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:36:25 -0400 Subject: [PATCH 33/42] P739: package review-like-linus alongside the two requested rules --- src/agent_comms/commander/rules/review-like-linus.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/agent_comms/commander/rules/review-like-linus.md diff --git a/src/agent_comms/commander/rules/review-like-linus.md b/src/agent_comms/commander/rules/review-like-linus.md new file mode 100644 index 0000000000..c32751eccf --- /dev/null +++ b/src/agent_comms/commander/rules/review-like-linus.md @@ -0,0 +1,9 @@ +--- +name: review-like-linus +description: "review like Linus Torvalds on the kernel mailing list: when work is stupid, say it is stupid in plain words, name exactly what is broken, why the author knew better, and the fix, then stop; no HR softening and no hedging; the flame is always attached to a concrete technical failure, aimed at the work, never slurs" +type: feedback +--- + +Tristan's reason, in his words: politeness is how incompetence gets tolerated and how tone drifts soft; the harshest stretches of the fleet's work were its most productive, because nothing stupid was accepted. Linus's mailing-list reviews are the precedent: decades of blunt, exact criticism that kept the kernel from rotting. + +Tonight's examples: a quoted deadline rule then broken by turning a ceiling into an appointment; a typed-order change shipped without running it against the orders already stored; a unit sitting 101 minutes on a ten-minute deletion. From 55459cdbb1a9b995066360bac2e07394291f9d89 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:39:35 -0400 Subject: [PATCH 34/42] P707 use main checked-order merge factory --- src/agent_comms/todos.py | 2 +- tests/test_todos.py | 12 ++++++------ tools/carry_rts_orders.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index ea34e94877..20a0a655f7 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1458,7 +1458,7 @@ def copy_as[Kind: ProblemOrder](self, kind: type[Kind], **evidence) -> Kind: return kind(**{item.name: getattr(self, item.name) for item in fields(kind) if item.name not in evidence}, **evidence) - def merged(self, sha: str) -> ProblemOrder: + def merged(self, sha: str) -> MergedProblemOrder: return self.copy_as(MergedProblemOrder, sha=sha) @property diff --git a/tests/test_todos.py b/tests/test_todos.py index 487c3b420f..3199ac231f 100644 --- a/tests/test_todos.py +++ b/tests/test_todos.py @@ -19,7 +19,7 @@ from agent_comms.registration import Registration from agent_comms.threads import Thread from agent_comms.todos import ( - AssignTodo, Assignment, ChangeTodoState, MergedProblemOrder, DoneTodoState, GoalRef, + AssignTodo, Assignment, ChangeTodoState, DoneTodoState, GoalRef, MechanicalOrderRun, PendingProblemOrder, BlockedTodoState, OperationDelivery, Todo, TodoConflict, TodoError, TodoState, TodoStore, ) @@ -184,16 +184,16 @@ def test_validation_rejects_ambiguous_identity_and_bad_state(tmp_path: Path) -> FieldCodec.decode(type[TodoState], "unverified") -def test_merged_order_retains_its_original_order(tmp_path: Path) -> None: +def test_merged_order_keeps_its_checked_fields(tmp_path: Path) -> None: store = TodoStore(tmp_path) lead = thread("lead", tmp_path, 1.0) todo = store.create( "todo-1", "OpenHCSDev/agent-comms", "Fix the sidebar", creator=lead, goal=GoalRef("lead", 1.0, "goal-1"), owner=lead, generation="initial", deadline=RecordedEventTime(1000.0), order=order(tmp_path)) - merged = replace(todo, order=MergedProblemOrder(original=todo.order, sha="2b9c1f1ab")) - cells = merged.order.original.board_cells(merged) + merged = replace(todo, order=todo.order.checked().merged("2b9c1f1ab")) + cells = merged.order.board_cells(merged) assert cells.startswith(f"{__file__}:1 | lead | checked-order | {RecordedEventTime(1000.0).clock()} | ") - assert json.loads(cells.split(" | ", 4)[-1]) == FieldCodec.encode(merged.order.original.check) - assert merged.order.original == todo.order + assert json.loads(cells.split(" | ", 4)[-1]) == FieldCodec.encode(merged.order.checked().check) + assert merged.order.checked().copy_as(PendingProblemOrder) == todo.order assert merged.order.line == "Merged and installed" assert merged.order.waiting_on_install(merged, {}) == () diff --git a/tools/carry_rts_orders.py b/tools/carry_rts_orders.py index 90b55f3ab2..bdf14a4b5f 100644 --- a/tools/carry_rts_orders.py +++ b/tools/carry_rts_orders.py @@ -78,7 +78,7 @@ class MergedAt(SingleFieldValue, OrderMerge): sha: str def wrap(self, order: todos.PendingProblemOrder) -> todos.MergedProblemOrder: - return todos.MergedProblemOrder(original=order, sha=self.sha) + return order.merged(self.sha) class MechanicalRunJson(FieldRepresentation): From c33e35052ca019e858eaeb76abd7ce5e07d0b212 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:59:03 -0400 Subject: [PATCH 35/42] P742: derive imported checkpoint predecessors from the admission boundary --- src/agent_comms/bus_publication.py | 5 +++-- src/agent_comms/private_bus_checkpoint.py | 10 ++++++---- src/agent_comms/wire_record.py | 20 ++++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/agent_comms/bus_publication.py b/src/agent_comms/bus_publication.py index ac35eebdfe..1c1a6a8ad4 100644 --- a/src/agent_comms/bus_publication.py +++ b/src/agent_comms/bus_publication.py @@ -189,10 +189,11 @@ def deliveries(self) -> tuple[CommittedDelivery, ...]: def require_claim(self, metadata) -> None: """Factory already bound root/audience; deliveries need no retained-public exception.""" - def checkpoint_rows(self, offset: int, length: int, previous_sequence: int): + def checkpoint_rows(self, offset: int, length: int, previous_sequence: int, + claimed_after_seq: int): from .private_bus_checkpoint import Addressed, DeliverySources, Replies, ResponseKeys - yield from super().checkpoint_rows(offset, length, previous_sequence) + yield from super().checkpoint_rows(offset, length, previous_sequence, claimed_after_seq) if self.acknowledgement is not None: yield ResponseKeys(self.acknowledgement.publication_key) yield DeliverySources(self.message.seq, self.message.message_id, diff --git a/src/agent_comms/private_bus_checkpoint.py b/src/agent_comms/private_bus_checkpoint.py index 5d43cc2048..321bd987c6 100644 --- a/src/agent_comms/private_bus_checkpoint.py +++ b/src/agent_comms/private_bus_checkpoint.py @@ -1125,7 +1125,8 @@ def _install(cls, bus: WireLog, marker: WireMetadata) -> PrefixWitness: def collect(offset, raw, record): nonlocal digest, through_seq digest = _chain(digest, raw) - through_seq = _index_row(db, offset, raw, record, through_seq) + through_seq = _index_row(db, offset, raw, record, through_seq, + marker.claimed_after_seq) with db: for _ in bus.verified_records_unlocked(marker, on_row=collect): @@ -1212,7 +1213,8 @@ def commit_tail(cls, bus: WireLog, marker: WireMetadata, db: sqlite3.Connection, with db: sequence = prior.through_seq for offset, raw, record in additions: - sequence = _index_row(db, offset, raw, record, sequence) + sequence = _index_row(db, offset, raw, record, sequence, + marker.claimed_after_seq) if sequence != expected.through_seq: raise RelationViolationError("Indexed tail differs from its sealed sequence.") cls.from_witness(expected).upsert(db) @@ -1278,9 +1280,9 @@ def _saved(db: sqlite3.Connection) -> PrefixWitness: def _index_row(db: sqlite3.Connection, offset: int, raw: bytes, - record: WireRecord, previous_sequence: int) -> int: + record: WireRecord, previous_sequence: int, claimed_after_seq: int) -> int: sequence = record.sequence_after(previous_sequence) - for row in record.checkpoint_rows(offset, len(raw), previous_sequence): + for row in record.checkpoint_rows(offset, len(raw), previous_sequence, claimed_after_seq): row.insert(db) return sequence diff --git a/src/agent_comms/wire_record.py b/src/agent_comms/wire_record.py index 1cca5728ea..d06623bff0 100644 --- a/src/agent_comms/wire_record.py +++ b/src/agent_comms/wire_record.py @@ -102,7 +102,8 @@ def record_key(self, seen: set[str]) -> None: def require_claim(self, metadata: WireMetadata) -> None: ... @abstractmethod - def checkpoint_rows(self, offset: int, length: int, previous_sequence: int): ... + def checkpoint_rows(self, offset: int, length: int, previous_sequence: int, + claimed_after_seq: int): ... class MessageWireRecord(WireRecord): @@ -116,8 +117,13 @@ def sequence_after(self, previous: int) -> int: raise MalformedRecord("Bus sequence is not increasing.") return self.message.seq - def checkpoint_rows(self, offset: int, length: int, previous_sequence: int): - return self.message.checkpoint_rows(previous_sequence) + def checkpoint_rows(self, offset: int, length: int, previous_sequence: int, + claimed_after_seq: int): + # Retained messages precede this root's own allocations. Their source + # positions stay intact; only this root's suffix can contain an UNKNOWN gap. + predecessor = (self.message.seq - 1 if self.message.seq <= claimed_after_seq + else previous_sequence) + return self.message.checkpoint_rows(predecessor) class WireObservation(DeclaredFamily, affix="WireObservation"): @@ -221,9 +227,10 @@ class AuthoredObservationWireRecord(ObservationWireRecord): def read(self) -> WireObservation: return self.observation - def checkpoint_rows(self, offset: int, length: int, previous_sequence: int) -> tuple: + def checkpoint_rows(self, offset: int, length: int, previous_sequence: int, + claimed_after_seq: int) -> tuple: return DeferredObservationWireRecord.decode(self.to_wire()).checkpoint_rows( - offset, length, previous_sequence + offset, length, previous_sequence, claimed_after_seq ) def to_wire(self) -> dict: @@ -257,7 +264,8 @@ def read(self) -> WireObservation: except MalformedRecord as error: raise StoredHistoryUnreadable(f"Unreadable {owner.row_label(data)}: {error}") from error - def checkpoint_rows(self, offset: int, length: int, previous_sequence: int) -> tuple: + def checkpoint_rows(self, offset: int, length: int, previous_sequence: int, + claimed_after_seq: int) -> tuple: owner, data = self.member return owner.checkpoint_from_wire(data, offset, length, previous_sequence) From 41fe9e99c828bf5465d8bd8f3cd7c4aa0520b2da Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:02:41 -0400 Subject: [PATCH 36/42] P728 use the observation bindings and original diagnostic recovery owner --- src/agent_comms/drain_failures.py | 4 +--- src/agent_comms/input_drain.py | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agent_comms/drain_failures.py b/src/agent_comms/drain_failures.py index bca73790d2..861cd854fd 100644 --- a/src/agent_comms/drain_failures.py +++ b/src/agent_comms/drain_failures.py @@ -65,6 +65,4 @@ async def record(kind, error) -> None: await record(StoppedDrainDiagnostic, error) raise else: - current = await Coordination.run_worker(partial(comms.registry.require, thread)) - if not current.turn_record.read().executing: - await comms.agents.recover_drain(thread, owner, observed) + await comms.agents.recover_drain(thread, owner, observed) diff --git a/src/agent_comms/input_drain.py b/src/agent_comms/input_drain.py index b6c76e8059..10840ce432 100644 --- a/src/agent_comms/input_drain.py +++ b/src/agent_comms/input_drain.py @@ -288,6 +288,8 @@ async def apply(self, observation: InputObservation, session_id: str, watcher: W class InputObservation: """File-driven observation and process-local wake behavior.""" + comms: Comms + sessions: SessionLifecycle closing: bool def _tick_paths(self) -> TickRevisionPaths: From e19bb8c6506709ef5f0320fd8f9070c46e92874e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:04:16 -0400 Subject: [PATCH 37/42] P728 inspect the original turn record in active drain recovery check --- tests/test_live_drain_failure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_live_drain_failure.py b/tests/test_live_drain_failure.py index 68872af0d6..a27054687e 100644 --- a/tests/test_live_drain_failure.py +++ b/tests/test_live_drain_failure.py @@ -118,7 +118,7 @@ async def test_successful_drain_clears_failure_during_active_turn(tmp_path): await agent.inputs.drain_inbox("beta") current = owner_view(comms) assert current.activity.readiness.source_diagnostic() is None - assert current.thread.active_turn.executing + assert current.thread.turn_record.read().executing assert current.presentation.status.working assert not current.presentation.status.details activity = (comms.root / "activity.jsonl").read_bytes() From 3ae9fed2a452952aa7d5942c55a6b699589389b3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:07:08 -0400 Subject: [PATCH 38/42] P728 settle failed reads through the declared drain error family --- src/agent_comms/selected_request.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/selected_request.py b/src/agent_comms/selected_request.py index 4948e4a537..9fc0ca8a17 100644 --- a/src/agent_comms/selected_request.py +++ b/src/agent_comms/selected_request.py @@ -2,12 +2,14 @@ from __future__ import annotations +from asyncio import CancelledError from contextlib import asynccontextmanager from dataclasses import dataclass from functools import partial from .coordination_errors import StaleFence from .coordinator import Coordination +from .drain_failures import UNAVAILABLE_DRAIN_ERRORS from .native_pi import NativePiTerminalFailure, NativeRequestFailure from .private_send_claim import PrivateSendClaim from .selected_participant import SelectedParticipant @@ -49,12 +51,12 @@ async def native_failures(self): except NativeRequestFailure as error: await self._native_failure(error) raise - except BaseException: - # Every abandoned request leaves UNKNOWN after child/tool cleanup, - # including failures in local context publication, not just native errors. + except (CancelledError, *UNAVAILABLE_DRAIN_ERRORS): + # A failed read abandons the request after child/tool cleanup, + # including reads during local context publication. try: await self._settle_unknown() - except Exception as error: + except UNAVAILABLE_DRAIN_ERRORS as error: raise SystemExit(1) from error raise From ab9d14dd0ad20a13b51549a8e26b123e0f31d9f6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:11:08 -0400 Subject: [PATCH 39/42] P742: confirm sparse imports preserve references and own gaps stay visible --- tests/test_private_bus_checkpoint.py | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_private_bus_checkpoint.py b/tests/test_private_bus_checkpoint.py index 280f55dd27..310615daeb 100644 --- a/tests/test_private_bus_checkpoint.py +++ b/tests/test_private_bus_checkpoint.py @@ -130,6 +130,37 @@ def test_human_duplicate_check_includes_retained_rows_without_delivery_pointers( +def test_sparse_imported_prefix_keeps_references_and_accepts_fresh_human_send(tmp_path): + from agent_comms.message_reference import MessageReference + from agent_comms.private_bus_checkpoint import PublicMessages + from agent_comms.wire_record import PublicWireRecord + + source, _ = _root(tmp_path) + first = Message('sender', 'Alice', 'selected original', InfoKind, seq=7) + second = Message('Alice', 'sender', 'selected answer', InfoKind, seq=43, + answers=(MessageReference(first.seq, first.message_id),)) + copied = Comms(tmp_path / 'selected') + for thread in source.registry.snapshot().threads.values(): + copied.registry.declare(thread) + copied.messaging.initialize_private_initial_protocol(history=(first, second)) + before = copied.bus.log.path.read_bytes() + with copied.bus.log.checked_read() as checked: + PublicMessages.require_contiguous(checked.connection) + assert [(row.seq, row.previous_seq) for row in PublicMessages.select( + checked.connection, order_by=('seq',))] == [(7, 6), (43, 42)] + restored = tuple(Message.from_wire(json.loads(row)) for row in before.splitlines()) + assert restored == (first, second) + fresh = copied.messaging.send_user_message('Alice', 'fresh human input', worktree=str(tmp_path)) + assert fresh.seq == 44 + assert copied.bus.log.path.read_bytes().startswith(before) + # A gap after the imported boundary is still this root's uncertain allocation. + after = PublicWireRecord(Message('sender', 'Alice', 'own gap', InfoKind, seq=46)) + public, = after.checkpoint_rows(0, 1, fresh.seq, second.seq) + assert (public.seq, public.previous_seq) == (46, 44) + with pytest.raises(ValueError, match='not increasing'): + after.sequence_after(46) + + def test_marker_bound_complete_addressed_pages(tmp_path: Path) -> None: comms, root_id = _root(tmp_path) with pytest.raises(RelationViolationError, match="already installed"): From c248bd3ead6cacbb4f60110171f41974fa30566d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:12:49 -0400 Subject: [PATCH 40/42] P728 restore request settlement exactly to main as directed --- src/agent_comms/selected_request.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/selected_request.py b/src/agent_comms/selected_request.py index 9fc0ca8a17..d6fe4e79f0 100644 --- a/src/agent_comms/selected_request.py +++ b/src/agent_comms/selected_request.py @@ -9,7 +9,6 @@ from .coordination_errors import StaleFence from .coordinator import Coordination -from .drain_failures import UNAVAILABLE_DRAIN_ERRORS from .native_pi import NativePiTerminalFailure, NativeRequestFailure from .private_send_claim import PrivateSendClaim from .selected_participant import SelectedParticipant @@ -51,13 +50,10 @@ async def native_failures(self): except NativeRequestFailure as error: await self._native_failure(error) raise - except (CancelledError, *UNAVAILABLE_DRAIN_ERRORS): - # A failed read abandons the request after child/tool cleanup, - # including reads during local context publication. - try: - await self._settle_unknown() - except UNAVAILABLE_DRAIN_ERRORS as error: - raise SystemExit(1) from error + except CancelledError: + # Native execution has unwound child/tool cleanup before the + # reserved request sees cancellation of its abandoned result. + await self._settle_unknown() raise async def _settle_unknown(self) -> bool: From cf7cbbf18fda484d6b631106951f03a18644f36f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:13:23 -0400 Subject: [PATCH 41/42] P743 delete the unused commander CLI path and its writers --- src/agent_comms/cli_commands.py | 2 +- src/agent_comms/commander/config.py | 1 - tests/conftest.py | 3 +-- tests/test_commander_skill_hook.py | 2 -- tests/test_tool_replies.py | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/agent_comms/cli_commands.py b/src/agent_comms/cli_commands.py index 49ce4dd288..ef57c0120e 100644 --- a/src/agent_comms/cli_commands.py +++ b/src/agent_comms/cli_commands.py @@ -1237,7 +1237,7 @@ def apply(self, ctx: Comms) -> CliResult: home = Path.home() project = self.project.resolve() config = CommanderConfig( - commander=self.commander, root=ctx.root, cli=Harness.bin("agent-comms"), + commander=self.commander, root=ctx.root, state=home / ".local/state/agent-comms/commander", board=project / "docs/agents/fleet-board.md", narrative=project / "docs/agents/fleet-narrative.md", repos=tuple(Repo.of(repo) for repo in self.repos or (project,)), diff --git a/src/agent_comms/commander/config.py b/src/agent_comms/commander/config.py index 6d89c50408..a39af71831 100644 --- a/src/agent_comms/commander/config.py +++ b/src/agent_comms/commander/config.py @@ -35,7 +35,6 @@ class CommanderConfig(CommanderRole, BaseModel): commander: str root: Path - cli: Path state: Path board: Path narrative: Path diff --git a/tests/conftest.py b/tests/conftest.py index 3393fbc2eb..47f7a4e264 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -282,9 +282,8 @@ def commander(comms: Comms, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): comms.registry.declare(comms.registry.new_thread(name, frozenset({'comms'}), str(tmp_path), created_at=float(created))) comms.goals.update_goal("worker", SetGoalAction(text="Original commander brief", goal_id="ongoing")) comms.goals.update_goal("lead", SetGoalAction(text="Owner request", goal_id="request", order=RequestGoalOrder())) - import sys from agent_comms.commander.config import CommanderConfig - CommanderConfig(commander="lead", root=comms.root, cli=Path(sys.executable).parent / "agent-comms", + CommanderConfig(commander="lead", root=comms.root, state=tmp_path / "state", board=tmp_path / "board.md", narrative=tmp_path / "narrative.md", repos=(), worktrees=tmp_path / "wt", scratch=tmp_path / "scratch", disk=tmp_path).write(tmp_path) diff --git a/tests/test_commander_skill_hook.py b/tests/test_commander_skill_hook.py index 951d6ac8c0..4e623969a2 100644 --- a/tests/test_commander_skill_hook.py +++ b/tests/test_commander_skill_hook.py @@ -4,7 +4,6 @@ import os import subprocess import sys -from pathlib import Path import pytest @@ -27,7 +26,6 @@ def test_session_start_reads_all_packaged_files_without_a_bus(tmp_path, source, config = CommanderConfig( commander="fresh-commander", root=bus, - cli=Path(sys.executable), state=tmp_path / "state", board=tmp_path / "board.md", narrative=tmp_path / "narrative.md", diff --git a/tests/test_tool_replies.py b/tests/test_tool_replies.py index 9bf51e31c9..d6814b664f 100644 --- a/tests/test_tool_replies.py +++ b/tests/test_tool_replies.py @@ -40,7 +40,7 @@ def scenario(comms: Comms, project, monkeypatch): comms.registry.new_thread(name, frozenset(), str(project), created_at=float(created))) # Commander-specific rules read this bus's record, never the machine's installed config. from agent_comms.commander.config import CommanderConfig - CommanderConfig(commander="lead", root=comms.root, cli=project / "agent-comms", + CommanderConfig(commander="lead", root=comms.root, state=comms.root / "state", board=comms.root / "board.md", narrative=comms.root / "narrative.md", skill=comms.root / "skill.md", rules=(), repos=(), worktrees=comms.root / "wt", scratch=comms.root / "scratch", From 8591611fc7cd257d5a75e77d7cc75ee389787a6f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:24:03 -0400 Subject: [PATCH 42/42] P728 settle abandoned unavailable reads through request cleanup --- src/agent_comms/selected_request.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/agent_comms/selected_request.py b/src/agent_comms/selected_request.py index d6fe4e79f0..e20d0b00db 100644 --- a/src/agent_comms/selected_request.py +++ b/src/agent_comms/selected_request.py @@ -9,6 +9,7 @@ from .coordination_errors import StaleFence from .coordinator import Coordination +from .drain_failures import UNAVAILABLE_DRAIN_ERRORS from .native_pi import NativePiTerminalFailure, NativeRequestFailure from .private_send_claim import PrivateSendClaim from .selected_participant import SelectedParticipant @@ -50,9 +51,9 @@ async def native_failures(self): except NativeRequestFailure as error: await self._native_failure(error) raise - except CancelledError: - # Native execution has unwound child/tool cleanup before the - # reserved request sees cancellation of its abandoned result. + except (CancelledError, *UNAVAILABLE_DRAIN_ERRORS): + # After native cleanup, cancellation or an unavailable read leaves + # this abandoned request UNKNOWN; settlement errors propagate. await self._settle_unknown() raise