From d1c39ac825b53ffe417d0f8440af76e1cafd43bd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:05:09 -0400 Subject: [PATCH 01/13] P196 declare the BehaviourPattern family Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/behaviour_patterns.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/agent_comms/behaviour_patterns.py diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py new file mode 100644 index 0000000000..bebc03a6ca --- /dev/null +++ b/src/agent_comms/behaviour_patterns.py @@ -0,0 +1,17 @@ +"""Behaviour failure patterns Core catches: each declared once with its signature and check. + +A member names what the pattern looks like in state, readable without trusting +a report, the existing check that enforces it, and where the pattern was learned. +``docs/agents/behaviour-catalog.md`` is generated from these declarations. +""" + +from __future__ import annotations + +from typing import ClassVar + +from .declared_family import DeclaredFamily + + +class BehaviourPattern(DeclaredFamily, affix="Pattern"): + signature: ClassVar[str] + source: ClassVar[str] From fbc6916fc664de0ad2539d1406cd889e755e8bea Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:15:11 -0400 Subject: [PATCH 02/13] P196 declare behaviour failure patterns once and generate their catalog BehaviourPattern members name their signature in state, the existing checks that enforce them (a StandingRule member, or a verb or tool refusal located from source so the catalog never imports the commander verbs, which open the installed bus), and where each was learned. docs/agents/behaviour-catalog.md is BehaviourPattern.catalog(); the family test fails when they differ. Src-Budget: 220 Co-Authored-By: Claude Opus 5.5 --- docs/agents/behaviour-catalog.md | 115 ++++++++++++++ src/agent_comms/behaviour_patterns.py | 213 +++++++++++++++++++++++++- tests/test_behaviour_catalog.py | 16 ++ 3 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 docs/agents/behaviour-catalog.md create mode 100644 tests/test_behaviour_catalog.py diff --git a/docs/agents/behaviour-catalog.md b/docs/agents/behaviour-catalog.md new file mode 100644 index 0000000000..8bb3eab66c --- /dev/null +++ b/docs/agents/behaviour-catalog.md @@ -0,0 +1,115 @@ +# Behaviour catalog + +Generated from `agent_comms.behaviour_patterns` by +`python -m agent_comms.behaviour_patterns > docs/agents/behaviour-catalog.md`; do not edit by hand. + +Each behaviour failure pattern is declared once in Core with its signature (what it looks like in state, readable without trusting a report) and the existing checks that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit skill's catalog (`refactor-audit/references/patterns/README.md`). + +The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) names ten failures; each pattern cites the ones it checks: + +- [Failure 1](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 2](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 3](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `uncounted_check` +- [Failure 5](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 6](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `board_disagrees`, `uncounted_check` +- [Failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `plain_text`, `undeclared_audience` +- [Failure 9](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +- [Failure 10](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet + +## plan_idle + +A plan phase whose earlier phases are all done is not done itself, and no open order held by a unit serves it. + +Checked by: + +- standing rule `plan_idle` (`agent_comms.standing_rules.PlanIdle`) + +Learned from: P83 (34ba68c81) + +## board_disagrees + +An open order's progress reads no commit while a pull request linked to it has merged. + +Checked by: + +- standing rule `board_disagrees` (`agent_comms.standing_rules.BoardDisagrees`) + +Learned from: P170 (c9c0c04db); [Codex postmortem, failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) + +## tactical_command + +The commander's own session runs an install, maintenance, connector restart or carry command instead of ordering a unit to. + +Checked by: + +- tool refusal in `agent_comms.commander.send_guard.TacticalCommand` + +Learned from: P171 (cba7b09c3) + +## plain_text + +A commander order, strike, roast or directive whose words outside cited spans match the declared jargon list in `PlainText`. + +Checked by: + +- verb refusal in `agent_comms.commander.order.refuse` +- tool refusal in `agent_comms.connector.DirectorCaller.require_directive` + +Learned from: [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Hate jargon."; [Codex postmortem, failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) + +## undeclared_audience + +A send to a channel, which reaches many threads, names no thread as its audience. + +Checked by: + +- tool refusal in `agent_comms.channel_targets.ChannelTarget.addressed` + +Learned from: commit dc4fb3bfa; [Codex postmortem, failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) + +## repeated_strike + +A second strike for one miss: the same actor, owner and reason as a live strike, or a send-back strike on an order already struck since it was sent. + +Checked by: + +- tool refusal in `agent_comms.todos.StrikeTodo.repeats` +- verb refusal in `agent_comms.commander.order.send` + +Learned from: P184 (7af14a520) + +## strike_in_error + +A strike recorded in error is deleted, or voided while not live, instead of a void recorded beside it with a plain reason. + +Checked by: + +- verb refusal in `agent_comms.commander.order.unstrike` +- tool refusal in `agent_comms.todos.VoidStrikeTodo.apply` + +Learned from: P47 (15d435152) + +## uncounted_check + +An order whose check line does not read `repo :: command -> integer`, prints no integer, or already prints its target before any work. + +Checked by: + +- verb refusal in `agent_comms.goals.OrderCheck.parse` +- verb refusal in `agent_comms.goals.NoIntegerPrinted` +- verb refusal in `agent_comms.commander.order.correct_check` + +Learned from: P114 (d889df612); [Codex postmortem, failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md); [Codex postmortem, failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) + +## strike_on_named_problem + +A strike or hunt recorded on a problem other than the one given as its argument, such as one moved by a problem number inside its reason. + +Checked by: + +- verb refusal in `agent_comms.commander.order.strike` +- verb refusal in `agent_comms.commander.order.hunt` + +Learned from: P194 (b26843727); [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Strikes, then fire." diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index bebc03a6ca..dc2736e8e0 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -1,17 +1,224 @@ """Behaviour failure patterns Core catches: each declared once with its signature and check. A member names what the pattern looks like in state, readable without trusting -a report, the existing check that enforces it, and where the pattern was learned. -``docs/agents/behaviour-catalog.md`` is generated from these declarations. +a report, the existing checks that enforce it, and where the pattern was learned. +``docs/agents/behaviour-catalog.md`` is this family's ``catalog()``; regenerate it +with ``python -m agent_comms.behaviour_patterns > docs/agents/behaviour-catalog.md``. """ from __future__ import annotations +import ast +from abc import ABC, abstractmethod +from dataclasses import dataclass +from inspect import isabstract +from pathlib import Path from typing import ClassVar from .declared_family import DeclaredFamily +from .standing_rules import BoardDisagrees, PlanIdle, StandingRule + +POSTMORTEM = "https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md" +COMMANDER_SKILL = "../../src/agent_comms/skills/comms-commander/SKILL.md" + + +class PatternCheck(ABC): + """An existing check that enforces a pattern.""" + + @abstractmethod + def exists(self) -> bool: ... + + @abstractmethod + def cite(self) -> str: ... + + +@dataclass(frozen=True) +class RuleCheck(PatternCheck): + """A StandingRule member that reports the pattern on every read of state.""" + + rule: type[StandingRule] + + def exists(self) -> bool: + return not isabstract(self.rule) and self.rule.declared_name is not None + + def cite(self) -> str: + return f"standing rule `{self.rule.declared_name}` (`{self.rule.__module__}.{self.rule.__qualname__}`)" + + +@dataclass(frozen=True) +class RefusalCheck(PatternCheck): + """A refusal located by its definition, read from source: the commander verbs + open the installed bus when imported, so the catalog never imports them.""" + + module: str + qualname: str + surface: ClassVar[str] + + def exists(self) -> bool: + path = Path(__file__).parent.joinpath(*self.module.split(".")[1:]).with_suffix(".py") + scope = ast.parse(path.read_text()).body + for name in self.qualname.split("."): + found = next((node for node in scope if isinstance( + node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name), None) + if found is None: + return False + scope = found.body + return True + + def cite(self) -> str: + return f"{self.surface} in `{self.module}.{self.qualname}`" + + +class VerbRefusal(RefusalCheck): + surface = "verb refusal" + + +class ToolRefusal(RefusalCheck): + surface = "tool refusal" + + +class PatternSource(ABC): + """Where a pattern was learned.""" + + @abstractmethod + def cite(self) -> str: ... + + +@dataclass(frozen=True) +class PostmortemFailure(PatternSource): + number: int + + def cite(self) -> str: + return f"[Codex postmortem, failure {self.number}]({POSTMORTEM})" + + +@dataclass(frozen=True) +class CommanderRule(PatternSource): + lead: str + + def cite(self) -> str: + return f"[comms-commander rule]({COMMANDER_SKILL}) \"{self.lead}\"" + + +@dataclass(frozen=True) +class ProblemOrder(PatternSource): + problem: str + commit: str + + def cite(self) -> str: + return f"{self.problem} ({self.commit})" + + +@dataclass(frozen=True) +class Commit(PatternSource): + commit: str + + def cite(self) -> str: + return f"commit {self.commit}" class BehaviourPattern(DeclaredFamily, affix="Pattern"): signature: ClassVar[str] - source: ClassVar[str] + checks: ClassVar[tuple[PatternCheck, ...]] + sources: ClassVar[tuple[PatternSource, ...]] + + @classmethod + def section(cls) -> str: + checks = "".join(f"- {check.cite()}\n" for check in cls.checks) + sources = "; ".join(source.cite() for source in cls.sources) + return (f"## {cls.declared_name}\n\n{cls.signature}\n\n" + f"Checked by:\n\n{checks}\nLearned from: {sources}\n") + + @classmethod + def catalog(cls) -> str: + members = cls.members_with(BehaviourPattern) + cited = {failure: [m.declared_name for m in members if PostmortemFailure(failure) in m.sources] + for failure in range(1, 11)} + failures = "".join( + f"- [Failure {failure}]({POSTMORTEM}): " + + (", ".join(f"`{name}`" for name in names) if names else "no pattern checks it yet") + + "\n" for failure, names in cited.items()) + return ( + "# Behaviour catalog\n\n" + "Generated from `agent_comms.behaviour_patterns` by\n" + "`python -m agent_comms.behaviour_patterns > docs/agents/behaviour-catalog.md`;" + " do not edit by hand.\n\n" + "Each behaviour failure pattern is declared once in Core with its signature (what it looks" + " like in state, readable without trusting a report) and the existing checks that enforce" + " it. Patterns in code, rather than in behaviour, are the refactor-audit skill's catalog" + " (`refactor-audit/references/patterns/README.md`).\n\n" + f"The [Codex postmortem]({POSTMORTEM}) names ten failures; each pattern cites the ones it" + " checks:\n\n" + f"{failures}\n" + + "\n".join(member.section() for member in members)) + + +class PlanIdlePattern(BehaviourPattern): + signature = ("A plan phase whose earlier phases are all done is not done itself, and no open" + " order held by a unit serves it.") + checks = (RuleCheck(PlanIdle),) + sources = (ProblemOrder("P83", "34ba68c81"),) + + +class BoardDisagreesPattern(BehaviourPattern): + signature = "An open order's progress reads no commit while a pull request linked to it has merged." + checks = (RuleCheck(BoardDisagrees),) + sources = (ProblemOrder("P170", "c9c0c04db"), PostmortemFailure(7)) + + +class TacticalCommandPattern(BehaviourPattern): + signature = ("The commander's own session runs an install, maintenance, connector restart or carry" + " command instead of ordering a unit to.") + checks = (ToolRefusal("agent_comms.commander.send_guard", "TacticalCommand"),) + sources = (ProblemOrder("P171", "cba7b09c3"),) + + +class PlainTextPattern(BehaviourPattern): + signature = ("A commander order, strike, roast or directive whose words outside cited spans match" + " the declared jargon list in `PlainText`.") + checks = (VerbRefusal("agent_comms.commander.order", "refuse"), + ToolRefusal("agent_comms.connector", "DirectorCaller.require_directive")) + sources = (CommanderRule("Hate jargon."), PostmortemFailure(8)) + + +class UndeclaredAudiencePattern(BehaviourPattern): + signature = "A send to a channel, which reaches many threads, names no thread as its audience." + checks = (ToolRefusal("agent_comms.channel_targets", "ChannelTarget.addressed"),) + sources = (Commit("dc4fb3bfa"), PostmortemFailure(8)) + + +class RepeatedStrikePattern(BehaviourPattern): + signature = ("A second strike for one miss: the same actor, owner and reason as a live strike, or a" + " send-back strike on an order already struck since it was sent.") + checks = (ToolRefusal("agent_comms.todos", "StrikeTodo.repeats"), + VerbRefusal("agent_comms.commander.order", "send")) + sources = (ProblemOrder("P184", "7af14a520"),) + + +class StrikeInErrorPattern(BehaviourPattern): + signature = ("A strike recorded in error is deleted, or voided while not live, instead of a void" + " recorded beside it with a plain reason.") + checks = (VerbRefusal("agent_comms.commander.order", "unstrike"), + ToolRefusal("agent_comms.todos", "VoidStrikeTodo.apply")) + sources = (ProblemOrder("P47", "15d435152"),) + + +class UncountedCheckPattern(BehaviourPattern): + signature = ("An order whose check line does not read `repo :: command -> integer`, prints no" + " integer, or already prints its target before any work.") + checks = (VerbRefusal("agent_comms.goals", "OrderCheck.parse"), + VerbRefusal("agent_comms.goals", "NoIntegerPrinted"), + VerbRefusal("agent_comms.commander.order", "correct_check")) + sources = (ProblemOrder("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) + + +class StrikeOnNamedProblemPattern(BehaviourPattern): + signature = ("A strike or hunt recorded on a problem other than the one given as its argument, such" + " as one moved by a problem number inside its reason.") + checks = (VerbRefusal("agent_comms.commander.order", "strike"), + VerbRefusal("agent_comms.commander.order", "hunt")) + sources = (ProblemOrder("P194", "b26843727"), CommanderRule("Strikes, then fire.")) + + +if __name__ == "__main__": + print(BehaviourPattern.catalog(), end="") diff --git a/tests/test_behaviour_catalog.py b/tests/test_behaviour_catalog.py new file mode 100644 index 0000000000..f8b25923e2 --- /dev/null +++ b/tests/test_behaviour_catalog.py @@ -0,0 +1,16 @@ +"""The behaviour catalog in docs/agents is the family's output, never a hand edit.""" + +from pathlib import Path + +from agent_comms.behaviour_patterns import BehaviourPattern + +ROOT = Path(__file__).resolve().parents[1] + + +def test_behaviour_catalog_members_name_existing_checks_and_generate_the_file(): + members = BehaviourPattern.members_with(BehaviourPattern) + assert members + for member in members: + assert member.checks and member.sources, member.declared_name + assert all(check.exists() for check in member.checks), member.declared_name + assert (ROOT / "docs/agents/behaviour-catalog.md").read_text() == BehaviourPattern.catalog() From 549ea4703d582bf41edcd9e880dc70530531534c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:16:36 -0400 Subject: [PATCH 03/13] P196 format the pattern family and read definitions without absence probes Src-Budget: 260 Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/behaviour_patterns.py | 128 +++++++++++++++++--------- 1 file changed, 82 insertions(+), 46 deletions(-) diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index dc2736e8e0..6df13766b1 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -39,10 +39,11 @@ class RuleCheck(PatternCheck): rule: type[StandingRule] def exists(self) -> bool: - return not isabstract(self.rule) and self.rule.declared_name is not None + return issubclass(self.rule, StandingRule) and not isabstract(self.rule) def cite(self) -> str: - return f"standing rule `{self.rule.declared_name}` (`{self.rule.__module__}.{self.rule.__qualname__}`)" + where = f"{self.rule.__module__}.{self.rule.__qualname__}" + return f"standing rule `{self.rule.declared_name}` (`{where}`)" @dataclass(frozen=True) @@ -58,11 +59,14 @@ def exists(self) -> bool: path = Path(__file__).parent.joinpath(*self.module.split(".")[1:]).with_suffix(".py") scope = ast.parse(path.read_text()).body for name in self.qualname.split("."): - found = next((node for node in scope if isinstance( - node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name), None) - if found is None: + defined = { + node.name: node.body + for node in scope + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) + } + if name not in defined: return False - scope = found.body + scope = defined[name] return True def cite(self) -> str: @@ -97,7 +101,7 @@ class CommanderRule(PatternSource): lead: str def cite(self) -> str: - return f"[comms-commander rule]({COMMANDER_SKILL}) \"{self.lead}\"" + return f'[comms-commander rule]({COMMANDER_SKILL}) "{self.lead}"' @dataclass(frozen=True) @@ -126,58 +130,74 @@ class BehaviourPattern(DeclaredFamily, affix="Pattern"): def section(cls) -> str: checks = "".join(f"- {check.cite()}\n" for check in cls.checks) sources = "; ".join(source.cite() for source in cls.sources) - return (f"## {cls.declared_name}\n\n{cls.signature}\n\n" - f"Checked by:\n\n{checks}\nLearned from: {sources}\n") + return ( + f"## {cls.declared_name}\n\n{cls.signature}\n\n" + f"Checked by:\n\n{checks}\nLearned from: {sources}\n" + ) @classmethod def catalog(cls) -> str: members = cls.members_with(BehaviourPattern) - cited = {failure: [m.declared_name for m in members if PostmortemFailure(failure) in m.sources] - for failure in range(1, 11)} + cited = { + failure: [m.declared_name for m in members if PostmortemFailure(failure) in m.sources] + for failure in range(1, 11) + } failures = "".join( f"- [Failure {failure}]({POSTMORTEM}): " + (", ".join(f"`{name}`" for name in names) if names else "no pattern checks it yet") - + "\n" for failure, names in cited.items()) + + "\n" + for failure, names in cited.items() + ) return ( "# Behaviour catalog\n\n" "Generated from `agent_comms.behaviour_patterns` by\n" "`python -m agent_comms.behaviour_patterns > docs/agents/behaviour-catalog.md`;" " do not edit by hand.\n\n" - "Each behaviour failure pattern is declared once in Core with its signature (what it looks" - " like in state, readable without trusting a report) and the existing checks that enforce" - " it. Patterns in code, rather than in behaviour, are the refactor-audit skill's catalog" - " (`refactor-audit/references/patterns/README.md`).\n\n" - f"The [Codex postmortem]({POSTMORTEM}) names ten failures; each pattern cites the ones it" - " checks:\n\n" - f"{failures}\n" - + "\n".join(member.section() for member in members)) + "Each behaviour failure pattern is declared once in Core with its signature (what it" + " looks like in state, readable without trusting a report) and the existing checks" + " that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit" + " skill's catalog (`refactor-audit/references/patterns/README.md`).\n\n" + f"The [Codex postmortem]({POSTMORTEM}) names ten failures; each pattern cites the" + " ones it checks:\n\n" + f"{failures}\n" + "\n".join(member.section() for member in members) + ) class PlanIdlePattern(BehaviourPattern): - signature = ("A plan phase whose earlier phases are all done is not done itself, and no open" - " order held by a unit serves it.") + signature = ( + "A plan phase whose earlier phases are all done is not done itself, and no open" + " order held by a unit serves it." + ) checks = (RuleCheck(PlanIdle),) sources = (ProblemOrder("P83", "34ba68c81"),) class BoardDisagreesPattern(BehaviourPattern): - signature = "An open order's progress reads no commit while a pull request linked to it has merged." + signature = ( + "An open order's progress reads no commit while a pull request linked to it has merged." + ) checks = (RuleCheck(BoardDisagrees),) sources = (ProblemOrder("P170", "c9c0c04db"), PostmortemFailure(7)) class TacticalCommandPattern(BehaviourPattern): - signature = ("The commander's own session runs an install, maintenance, connector restart or carry" - " command instead of ordering a unit to.") + signature = ( + "The commander's own session runs an install, maintenance, connector restart or carry" + " command instead of ordering a unit to." + ) checks = (ToolRefusal("agent_comms.commander.send_guard", "TacticalCommand"),) sources = (ProblemOrder("P171", "cba7b09c3"),) class PlainTextPattern(BehaviourPattern): - signature = ("A commander order, strike, roast or directive whose words outside cited spans match" - " the declared jargon list in `PlainText`.") - checks = (VerbRefusal("agent_comms.commander.order", "refuse"), - ToolRefusal("agent_comms.connector", "DirectorCaller.require_directive")) + signature = ( + "A commander order, strike, roast or directive whose words outside cited spans match" + " the declared jargon list in `PlainText`." + ) + checks = ( + VerbRefusal("agent_comms.commander.order", "refuse"), + ToolRefusal("agent_comms.connector", "DirectorCaller.require_directive"), + ) sources = (CommanderRule("Hate jargon."), PostmortemFailure(8)) @@ -188,35 +208,51 @@ class UndeclaredAudiencePattern(BehaviourPattern): class RepeatedStrikePattern(BehaviourPattern): - signature = ("A second strike for one miss: the same actor, owner and reason as a live strike, or a" - " send-back strike on an order already struck since it was sent.") - checks = (ToolRefusal("agent_comms.todos", "StrikeTodo.repeats"), - VerbRefusal("agent_comms.commander.order", "send")) + signature = ( + "A second strike for one miss: the same actor, owner and reason as a live strike, or a" + " send-back strike on an order already struck since it was sent." + ) + checks = ( + ToolRefusal("agent_comms.todos", "StrikeTodo.repeats"), + VerbRefusal("agent_comms.commander.order", "send"), + ) sources = (ProblemOrder("P184", "7af14a520"),) class StrikeInErrorPattern(BehaviourPattern): - signature = ("A strike recorded in error is deleted, or voided while not live, instead of a void" - " recorded beside it with a plain reason.") - checks = (VerbRefusal("agent_comms.commander.order", "unstrike"), - ToolRefusal("agent_comms.todos", "VoidStrikeTodo.apply")) + signature = ( + "A strike recorded in error is deleted, or voided while not live, instead of a void" + " recorded beside it with a plain reason." + ) + checks = ( + VerbRefusal("agent_comms.commander.order", "unstrike"), + ToolRefusal("agent_comms.todos", "VoidStrikeTodo.apply"), + ) sources = (ProblemOrder("P47", "15d435152"),) class UncountedCheckPattern(BehaviourPattern): - signature = ("An order whose check line does not read `repo :: command -> integer`, prints no" - " integer, or already prints its target before any work.") - checks = (VerbRefusal("agent_comms.goals", "OrderCheck.parse"), - VerbRefusal("agent_comms.goals", "NoIntegerPrinted"), - VerbRefusal("agent_comms.commander.order", "correct_check")) + signature = ( + "An order whose check line does not read `repo :: command -> integer`, prints no" + " integer, or already prints its target before any work." + ) + checks = ( + VerbRefusal("agent_comms.goals", "OrderCheck.parse"), + VerbRefusal("agent_comms.goals", "NoIntegerPrinted"), + VerbRefusal("agent_comms.commander.order", "correct_check"), + ) sources = (ProblemOrder("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) class StrikeOnNamedProblemPattern(BehaviourPattern): - signature = ("A strike or hunt recorded on a problem other than the one given as its argument, such" - " as one moved by a problem number inside its reason.") - checks = (VerbRefusal("agent_comms.commander.order", "strike"), - VerbRefusal("agent_comms.commander.order", "hunt")) + signature = ( + "A strike or hunt recorded on a problem other than the one given as its argument, such" + " as one moved by a problem number inside its reason." + ) + checks = ( + VerbRefusal("agent_comms.commander.order", "strike"), + VerbRefusal("agent_comms.commander.order", "hunt"), + ) sources = (ProblemOrder("P194", "b26843727"), CommanderRule("Strikes, then fire.")) From 268f6ddb1e4f7ee2943dd01ae65e5cb2ad4d1bef Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:17:21 -0400 Subject: [PATCH 04/13] P196 budget the behaviour pattern family's source lines Src-Budget: 260 Co-Authored-By: Claude Opus 5.5 From 3b1746abf7b89fedf7722311fdcebe8391be1988 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:31:31 -0400 Subject: [PATCH 05/13] P196 rename the pattern-source ProblemFix so it does not shadow todos.ProblemOrder Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/behaviour_patterns.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index 6df13766b1..0cafef4b7d 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -105,7 +105,7 @@ def cite(self) -> str: @dataclass(frozen=True) -class ProblemOrder(PatternSource): +class ProblemFix(PatternSource): problem: str commit: str @@ -169,7 +169,7 @@ class PlanIdlePattern(BehaviourPattern): " order held by a unit serves it." ) checks = (RuleCheck(PlanIdle),) - sources = (ProblemOrder("P83", "34ba68c81"),) + sources = (ProblemFix("P83", "34ba68c81"),) class BoardDisagreesPattern(BehaviourPattern): @@ -177,7 +177,7 @@ class BoardDisagreesPattern(BehaviourPattern): "An open order's progress reads no commit while a pull request linked to it has merged." ) checks = (RuleCheck(BoardDisagrees),) - sources = (ProblemOrder("P170", "c9c0c04db"), PostmortemFailure(7)) + sources = (ProblemFix("P170", "c9c0c04db"), PostmortemFailure(7)) class TacticalCommandPattern(BehaviourPattern): @@ -186,7 +186,7 @@ class TacticalCommandPattern(BehaviourPattern): " command instead of ordering a unit to." ) checks = (ToolRefusal("agent_comms.commander.send_guard", "TacticalCommand"),) - sources = (ProblemOrder("P171", "cba7b09c3"),) + sources = (ProblemFix("P171", "cba7b09c3"),) class PlainTextPattern(BehaviourPattern): @@ -216,7 +216,7 @@ class RepeatedStrikePattern(BehaviourPattern): ToolRefusal("agent_comms.todos", "StrikeTodo.repeats"), VerbRefusal("agent_comms.commander.order", "send"), ) - sources = (ProblemOrder("P184", "7af14a520"),) + sources = (ProblemFix("P184", "7af14a520"),) class StrikeInErrorPattern(BehaviourPattern): @@ -228,7 +228,7 @@ class StrikeInErrorPattern(BehaviourPattern): VerbRefusal("agent_comms.commander.order", "unstrike"), ToolRefusal("agent_comms.todos", "VoidStrikeTodo.apply"), ) - sources = (ProblemOrder("P47", "15d435152"),) + sources = (ProblemFix("P47", "15d435152"),) class UncountedCheckPattern(BehaviourPattern): @@ -241,7 +241,7 @@ class UncountedCheckPattern(BehaviourPattern): VerbRefusal("agent_comms.goals", "NoIntegerPrinted"), VerbRefusal("agent_comms.commander.order", "correct_check"), ) - sources = (ProblemOrder("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) + sources = (ProblemFix("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) class StrikeOnNamedProblemPattern(BehaviourPattern): @@ -253,7 +253,7 @@ class StrikeOnNamedProblemPattern(BehaviourPattern): VerbRefusal("agent_comms.commander.order", "strike"), VerbRefusal("agent_comms.commander.order", "hunt"), ) - sources = (ProblemOrder("P194", "b26843727"), CommanderRule("Strikes, then fire.")) + sources = (ProblemFix("P194", "b26843727"), CommanderRule("Strikes, then fire.")) if __name__ == "__main__": From 722d0243d20f87d393a3431c088e8e8546c80e68 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:36:09 -0400 Subject: [PATCH 06/13] P196 open the commander bus at dispatch, not at import Every commander module bound CONFIG, ME, COMMS and path strings at import, so importing commander/order.py read commander.json and opened the installed bus. CommanderConfig.installed() already owns the installed facts; it now also owns the bus (comms), the activity log, the forks directory and the default review catch, and every use reads it when the verb runs. The module globals, the order/capacity import cycle and the test reloads that rebound them are deleted. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/capacity.py | 14 ++- src/agent_comms/commander/config.py | 28 ++++- src/agent_comms/commander/judge.py | 16 ++- src/agent_comms/commander/order.py | 131 +++++++++++------------- src/agent_comms/commander/request.py | 11 +- src/agent_comms/commander/rotate.py | 3 +- src/agent_comms/commander/rules.py | 23 ++--- src/agent_comms/commander/send_guard.py | 11 +- src/agent_comms/commander/standing.py | 5 +- src/agent_comms/commander/tool_watch.py | 9 +- tests/conftest.py | 10 +- tests/test_commander_problem_orders.py | 22 ++-- tests/test_commander_verbs.py | 2 +- tests/test_standing_rules.py | 5 +- 14 files changed, 141 insertions(+), 149 deletions(-) diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index 8766d034b1..a7c8127bfa 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] + os.makedirs(CommanderConfig.installed().forks, exist_ok=True) + return [n[:-5] for n in os.listdir(CommanderConfig.installed().forks) if n.endswith('.beat') and time.time() - os.path.getmtime(f'{CommanderConfig.installed().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') + os.makedirs(CommanderConfig.installed().forks, exist_ok=True) + with open(f'{CommanderConfig.installed().forks}/{name}.beat', 'a'): + os.utime(f'{CommanderConfig.installed().forks}/{name}.beat') @dataclass(frozen=True) @@ -72,5 +70,5 @@ def require(self) -> None: def main(a): - load = Load.of(order.COMMS, order.ME) + load = Load.of(CommanderConfig.installed().comms, CommanderConfig.installed().commander) print(load if load.short else f'covered: load {load.total:.0f}, forks {list(load.forks)}') diff --git a/src/agent_comms/commander/config.py b/src/agent_comms/commander/config.py index 81d3bbbb3e..06c69c6be4 100644 --- a/src/agent_comms/commander/config.py +++ b/src/agent_comms/commander/config.py @@ -6,12 +6,14 @@ from __future__ import annotations import subprocess -from functools import cache +from functools import cache, cached_property from importlib.resources import files from pathlib import Path from pydantic import BaseModel, ConfigDict +from ..comms import Comms + class Repo(BaseModel): """A repository the fleet works on: its check checkout, its GitHub name and its source root.""" @@ -61,14 +63,32 @@ def packaged_rules() -> Path: def installed(cls) -> CommanderConfig: return cls.model_validate_json(cls.path().read_text()) + @cached_property + def comms(self) -> Comms: + """The bus whose Todo store owns problems, orders, deadlines and strikes, opened on first use.""" + return Comms(self.root) + + @property + def review_catch(self) -> str: + """Who caught a struck miss, and at which stage, when the strike names no one else.""" + return f'{self.commander} at review' + + @property + def activity(self) -> Path: + return self.state / 'activity.log' + + @property + def forks(self) -> Path: + return self.state / 'forks' + def write(self, home: Path) -> Path: """Record the config and start each state file empty, so every tool reads a real file.""" path = self.path(home) path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) path.write_text(self.model_dump_json(indent=1) + '\n') - for directory in (self.state, self.state / 'forks', self.state / 'orders', self.worktrees, + for directory in (self.state, self.forks, self.state / 'orders', self.worktrees, self.scratch, self.board.parent): directory.mkdir(parents=True, exist_ok=True) - if not (self.state / 'activity.log').exists(): - (self.state / 'activity.log').write_text('') + if not self.activity.exists(): + self.activity.write_text('') return path diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index 114266b17b..3bfc5bf763 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -30,8 +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') @@ -106,7 +104,7 @@ def roast(audience, thread, lines, text, fix, slop): def alarm(): 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(CommanderConfig.installed().comms, CommanderConfig.installed().commander) if time.time() - edit.at > 300] if late: counts: dict[str, int] = {} for edit in late: @@ -128,7 +126,7 @@ def selects(self, edit: Edit) -> bool: return True def run(self) -> None: - show([edit for edit in Edit.unjudged(order.COMMS, ME) if self.selects(edit)]) + show([edit for edit in Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander) if self.selects(edit)]) @dataclass(frozen=True, kw_only=True) @@ -170,7 +168,7 @@ 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 todo.sent_in(self.msg)} - edits = {edit.id: edit for edit in Edit.unjudged(order.COMMS, ME) if edit.problem in orders} + edits = {edit.id: edit for edit in Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().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}') @@ -189,7 +187,7 @@ def run(self) -> None: for i, todo in selected.items(): result = order.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(CommanderConfig.installed().comms, CommanderConfig.installed().commander)) print(f'{self.declared_name}: {len(ids)} edits; {left} still unjudged') self.report(tuple(current.values())) @@ -200,7 +198,7 @@ class OkVerb(VerdictVerb): serves: str = option(help="exact words of the order") def operation(self, todo: Todo, edit_id: str) -> AcceptedVerdict: - return AcceptedVerdict(todo.revision, order.COMMS.registry.require(ME).incarnation, + return AcceptedVerdict(todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.held(), edit_id, quote=self.serves) def check_edits(self, edits: list[Edit]) -> None: @@ -247,7 +245,7 @@ def slop(self) -> order.Slop: return order.Slop(self.slop_kind, self.fact, self.at, self.derive, 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, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.held(), edit_id, reason=StrikeReason(f'{todo.id} edit sent back: {self.why}')) @@ -265,7 +263,7 @@ def report(self, todos: tuple[Todo, ...]) -> None: audience = todo.verdicts[-1].audience.of(todo.held().owner, todo.goal) roast(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 {CommanderConfig.installed().review_catch}): ' f'{todo.strikes[-1].reason}') order.on_record(line, 'Send-back', audience) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index fe64d3bc61..28a38c2bfc 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -27,7 +27,6 @@ from ..child_process import BoundedRun from ..cli_commands import DeclaredOptions, option from ..declared_family import DeclaredFamily -from ..comms import Comms from ..goal_actions import CreateProblemGoalAction, OrderGoalAction from ..goals import CheckPrinted, OrderCheck, ProblemGoalOrder from ..native_tool_call import SelectedToolDenied @@ -46,14 +45,6 @@ from . import capacity from .config import CommanderConfig, Repo -CONFIG = CommanderConfig.installed() -ME = CONFIG.commander -COMMS = Comms(CONFIG.root) # the bus whose Todo store owns problems, orders, deadlines and strikes -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) OUTGOING = DirectiveText.OUTGOING # every commander text passes each one def refuse(text, *families): @@ -68,7 +59,7 @@ def post(audience: VerdictAudience, body: str) -> str: 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' - out = subprocess.run(['timeout', '300', CLI, '--root', B, 'send', '--from', ME, '--to', target, + out = subprocess.run(['timeout', '300', CommanderConfig.installed().cli, '--root', CommanderConfig.installed().root, 'send', '--from', CommanderConfig.installed().commander, '--to', target, '--type', 'info', '--body', audience.addressed(target, body)], capture_output=True, text=True) if '"id"' not in out.stdout: @@ -78,28 +69,28 @@ def post(audience: VerdictAudience, body: str) -> str: def call(tool): """Every order write is one Core tool request made as the commander's thread.""" - return ThreadCaller(ME).invoke(tool, COMMS) + return ThreadCaller(CommanderConfig.installed().commander).invoke(tool, CommanderConfig.installed().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) + return CommanderConfig.installed().comms.goals.todos.for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).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) + return CommanderConfig.installed().comms.goals.todos.held_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).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) + return CommanderConfig.installed().comms.goals.todos.held_by(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, unit) def units() -> frozenset[str]: """The threads this commander gives orders to; the owner, claude-ai and peer commanders are not among them.""" - return COMMS.goals.todos.units(COMMS.registry.require(ME).incarnation) + return CommanderConfig.installed().comms.goals.todos.units(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) def progress() -> dict[str, OrderProgress]: @@ -231,7 +222,7 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= 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)) + CommanderConfig.installed().comms.registry.storage_rule.require_quota(CommanderConfig.installed().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') Slop.from_order(body) # calling something slop requires the fact, where it is restated, its owner and the mechanism @@ -315,7 +306,7 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= 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 replaces 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.' + message = (f'{body.rstrip()}' + ('\nThis replaces my earlier order on it; drop that one.' if replaces else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + CommanderConfig.installed().comms.registry.storage_rule.order_text(unit, [f'{repo.checkout}/.venv/bin/python' for repo in CommanderConfig.installed().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 @@ -337,19 +328,19 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) except ValueError as refused: # Core owns what an order may say sys.exit(f'Refused order: {refused}') - common = dict(order=order, request=request, deadline=deadline(due), commander=ME) + common = dict(order=order, request=request, deadline=deadline(due), commander=CommanderConfig.installed().commander) 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))) + problem=pid, expected_revision=CommanderConfig.installed().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))) + CommanderConfig.installed().comms.goals.todos.get(pid).revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, sent))) for other, text in taken.items(): post(VerdictAudience((other,)), text) print(order.id) @@ -360,14 +351,14 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, 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}') + strike(unit, group[0], f'sent back on {group[0]}: {verdict}', CommanderConfig.installed().review_catch) 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 + return [str(repo.checkout) for repo in CommanderConfig.installed().repos if repo.checkout != Path(check.directory).resolve() and str(repo.checkout) not in check.command and subprocess.run(['grep', '-rqE', '--include=*.py', pattern, @@ -378,7 +369,7 @@ 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) + todo = CommanderConfig.installed().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)): @@ -421,7 +412,7 @@ def problem(text, unit, due, order_file, request): 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) + history = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) pid = f'P{max([int(t.id[1:]) for t in history if t.id[1:].isdigit()] + [0]) + 1}' open(order_file, 'w').write(body.replace('Problem: NEW', f'Problem: {pid}')) try: @@ -435,13 +426,13 @@ def problem(text, unit, due, order_file, request): def close(pid, works, artifact): """Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo.""" - ThreadCaller(ME).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=artifact), COMMS) + ThreadCaller(CommanderConfig.installed().commander).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=artifact), CommanderConfig.installed().comms) board() print(f'{pid} closed') def last_activity(unit): - lines = open(ACTIVITY).read().splitlines()[-6000:] + lines = open(CommanderConfig.installed().activity).read().splitlines()[-6000:] stamps = [l[:8] for l in lines if l[9:].startswith(unit + ' ')] return stamps[-1] if stamps else None @@ -461,10 +452,10 @@ def board(): f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | ' f'{"no" if t.pending else "yes, install pending"} | ' f'{unit and last_activity(unit) or "-"} |') - history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) + history = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).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') + open(CommanderConfig.installed().board, 'w').write('\n'.join(lines) + '\n') STAGES = ('tool call', 'review', 'install', 'live', 'user') # where a mistake was caught; later is worse @@ -475,8 +466,8 @@ def on_record(line: str, recorded: str, audience: VerdictAudience) -> None: 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: + new_file = not os.path.exists(CommanderConfig.installed().narrative) + with open(CommanderConfig.installed().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') @@ -488,7 +479,7 @@ def on_record(line: str, recorded: str, audience: VerdictAudience) -> None: 'Repost with order.post; do not record it again.', file=sys.stderr) -def strike(unit: str, task: str, why: str, caught: str = f'{ME} at review', *, +def strike(unit: str, task: str, why: str, caught: str, *, kind: type[StrikeTodo] = StrikeTodo) -> None: """HARD RULE: every failure goes on the public record, in plain words, damning and exact. @@ -497,16 +488,16 @@ def strike(unit: str, task: str, why: str, caught: str = f'{ME} at review', *, """ # 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) + sys.exit(f'Refused: --caught must say who caught it and at which stage {STAGES}, e.g. "{CommanderConfig.installed().review_catch}".') + todo = CommanderConfig.installed().comms.goals.todos.get(task) if not todo.state.open or 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})'))) + todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.held(), f'{why} (caught by {caught})'))) if recorded.todo.revision == todo.revision: return - n = struck(COMMS.goals.todos.get(task), unit) + n = struck(CommanderConfig.installed().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)) @@ -515,7 +506,7 @@ def strike(unit: str, task: str, why: str, caught: str = f'{ME} at review', *, def done(pid, sha, delivered=None, screenshot=''): """Core runs the delivery's check at its checked commit; a late delivered commit is a strike.""" - todo = COMMS.goals.todos.get(pid) + todo = CommanderConfig.installed().comms.goals.todos.get(pid) if not todo.state.open or not todo.pending: sys.exit(f'Refused: {pid} has no pending order') unit, repo = todo.assignment.owner, todo.order.check.directory @@ -526,30 +517,29 @@ def done(pid, sha, delivered=None, screenshot=''): 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()]: + thread = CommanderConfig.installed().comms.registry.require(unit) + if left := [str(t) for t in CommanderConfig.installed().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) + CommanderConfig.installed().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.') delivery = OperationDelivery() if operation else CommitDelivery(commit=sha, delivered_commit=delivered) claim = WorkingClaim(screenshot) if operation else MergedClaim(delivered) call(CommsOrderDoneTool(problem=pid, delivery=delivery, claims=(claim,))) - lateness = COMMS.goals.todos.get(pid).order.delivery.at.lateness(todo.deadline) + lateness = CommanderConfig.installed().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}') + strike(unit, pid, f'{pid} order {todo.order.id} {lateness} as {delivered}', CommanderConfig.installed().review_catch) return print(f'{unit}: order {todo.order.id} met as {delivered or sha}') -ACTIVITY = f'{RTS}/activity.log' def first_calls(): """Each order starts a first-tool-call clock: 2 minutes, separate from its commit deadline. Core records each mark.""" - lines = open(ACTIVITY).read().splitlines()[-4000:] + lines = open(CommanderConfig.installed().activity).read().splitlines()[-4000:] watched = {line[9:].split(' ', 1)[0] for line in lines} missing = [] for todo in held_orders(): @@ -562,13 +552,12 @@ def first_calls(): 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: + todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, event))) + if not acted and CommanderConfig.installed().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, pid, why, sent=None): @@ -587,13 +576,13 @@ def hunt(where, pid, why, sent=None): + HuntVerb.usage()) ref, at = FileLine(os.path.realpath(path), int(line)), RecordedEventTime(time.time()) call(CommsTodoTool(id=todo.id, operation=RecordHuntTodo( - todo.revision, COMMS.registry.require(ME).incarnation, + todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, OrderedHunt(ref, why, at, sent) if sent else CleanHunt(ref, why, at)))) print(f'hunt recorded: {where} -> {sent or "clean"}') def hunts(): - history = COMMS.goals.todos.history_for_commander(COMMS.registry.require(ME).incarnation) + history = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) return [h for t in history for h in t.hunts] @@ -603,12 +592,12 @@ def next_hunt(): 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], + for repo in CommanderConfig.installed().repos: + log = subprocess.run(['git', '-C', str(repo.checkout), 'log', 'origin/main', '--since=7.days', '--name-only', '--format=', '--', repo.source], capture_output=True, text=True).stdout.split() churn = {} for f in log: - full = os.path.realpath(os.path.join(repo, f)) + full = os.path.realpath(os.path.join(repo.checkout, 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] @@ -617,9 +606,9 @@ def next_hunt(): def reply(unit: str, since: float) -> dict | None: """The unit's latest message to me after ``since``, or None.""" - if not os.path.exists(f'{B}/bus.jsonl'): + if not os.path.exists(f'{CommanderConfig.installed().root}/bus.jsonl'): return None - with open(f'{B}/bus.jsonl', 'rb') as f: + with open(f'{CommanderConfig.installed().root}/bus.jsonl', 'rb') as f: f.seek(start := max(0, os.path.getsize(f.name) - 3_000_000)) lines = f.read().splitlines()[bool(start):] latest = None @@ -629,7 +618,7 @@ def reply(unit: str, since: float) -> dict | None: except ValueError: continue to = str(m.get('to')) - if m.get('from') == unit and float(m.get('ts', 0)) > since and (ME in to or '#comms' in to): + if m.get('from') == unit and float(m.get('ts', 0)) > since and (CommanderConfig.installed().commander in to or '#comms' in to): latest = m return latest @@ -664,7 +653,7 @@ def deadline_lines(pending: list[Todo], read: dict[str, OrderProgress], since: f 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) + return InstallDeadline.read(CommanderConfig.installed().comms.root).alarm(waiting, now) def overdue(): @@ -694,7 +683,7 @@ def overdue(): 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): + for _ in HuntDue.broken(CommanderConfig.installed().comms, CommanderConfig.installed().commander, since): print(HuntVerb.due(next_hunt()), flush=True) since = now time.sleep(60) @@ -707,7 +696,7 @@ class CommanderVerb(DeclaredFamily, DeclaredOptions, affix="Verb"): @property def who(self) -> str: - return self.fork or ME + return self.fork or CommanderConfig.installed().commander def apply(self, ctx: None = None) -> None: self.gate() @@ -748,24 +737,24 @@ class RuledVerb(CommanderVerb): def gate(self) -> None: super().gate() - for due in RulesDue.violations(COMMS, self.who): - flag = '' if self.who == ME else f' --as {self.who}' + for due in RulesDue.violations(CommanderConfig.installed().comms, self.who): + flag = '' if self.who == CommanderConfig.installed().commander else f' --as {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) + todo = CommanderConfig.installed().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))) + todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, voided, why))) unit = voided.previous.owner - n = struck(COMMS.goals.todos.get(task), unit) + n = struck(CommanderConfig.installed().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)) @@ -774,7 +763,7 @@ def unstrike(task: str, why: str, at: str | None = None) -> None: 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) + todo = CommanderConfig.installed().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: @@ -793,13 +782,13 @@ 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) + todo = CommanderConfig.installed().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, + todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.assignment, why, superseded_by))) unit = todo.held().owner - withdrawal, = COMMS.goals.todos.get(pid).order.withdrawals(todo) + withdrawal, = CommanderConfig.installed().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,)), @@ -823,7 +812,7 @@ class LoadedVerb(RuledVerb): def gate(self) -> None: if not self.fork: - capacity.Load.of(COMMS, ME).require() + capacity.Load.of(CommanderConfig.installed().comms, CommanderConfig.installed().commander).require() super().gate() @@ -855,7 +844,7 @@ class StrikeVerb(OrderVerb, RuledVerb): unit: str = option("unit") pid: str = option("pid", help="the problem the strike is for; strikes count per problem") 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", parser_default_factory=lambda: CommanderConfig.installed().review_catch, help='"WHO at STAGE"; stage is review/install/live/user') def run(self) -> None: strike(self.unit, self.pid, self.why, self.caught) @@ -901,7 +890,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(CommanderConfig.installed().comms.root) print(f'install due {self.at}, run by {self.installer}') @@ -939,8 +928,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 = CommanderConfig.installed().comms.goals.todos.get(self.pid) + checkout = CommanderConfig.installed().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())) diff --git a/src/agent_comms/commander/request.py b/src/agent_comms/commander/request.py index 0c4fe2ac93..5256fc0de0 100644 --- a/src/agent_comms/commander/request.py +++ b/src/agent_comms/commander/request.py @@ -16,14 +16,15 @@ from ..goal_states import ActiveGoal from ..goals import Goal, RequestGoalOrder from ..todos import Todo -from .order import COMMS, ME, CommanderVerb, RuledVerb, hhmm +from .config import CommanderConfig +from .order import CommanderVerb, RuledVerb, hhmm def open_requests() -> tuple[tuple[Goal, tuple[Todo, ...]], ...]: """The commander's open owner requests and their undelivered orders.""" - thread = COMMS.registry.require(ME) + thread = CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().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, CommanderConfig.installed().comms.goals, thread.incarnation)) class RequestVerb(CommanderVerb): @@ -36,7 +37,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 = CommanderConfig.installed().comms.goals.update_goal(CommanderConfig.installed().commander, SetGoalAction(text=self.text, order=RequestGoalOrder())) print(goal.require().id) @@ -48,7 +49,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)) + CommanderConfig.installed().comms.goals.update_goal(CommanderConfig.installed().commander, CompletedGoalAction(progress=self.why, expect=expect)) print(f"{self.id} done") diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 407366095a..3488a3e0a6 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -7,7 +7,6 @@ from ..todos import Todo from .config import CommanderConfig -CONFIG = CommanderConfig.installed() def broken_rules(after=0.0): """Every standing rule that broke after ``after``, as Core's StandingRule family derives it.""" @@ -35,7 +34,7 @@ def main(a): read = order.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(CommanderConfig.installed().comms, CommanderConfig.installed().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): diff --git a/src/agent_comms/commander/rules.py b/src/agent_comms/commander/rules.py index ddbab1d5c6..4e7939af91 100644 --- a/src/agent_comms/commander/rules.py +++ b/src/agent_comms/commander/rules.py @@ -19,11 +19,8 @@ from ..tool_caller import ThreadCaller from ..tools import CommsRuleReviewTool, StandingRulesRead from .config import CommanderConfig -from .order import COMMS -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. @@ -37,8 +34,8 @@ def principles() -> list[str]: 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'))]: + found = [m.group(1).strip() for m in re.finditer(r'^\s*- \*\*(.+?)\*\*', open(CommanderConfig.installed().skill).read(), re.M)] + for path in [path for directory in CommanderConfig.installed().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) @@ -47,11 +44,11 @@ def rules() -> list[str]: def record(who: str, entry: RuleReviewEntry) -> None: - read: StandingRulesRead = ThreadCaller(who).invoke(CommsRuleReviewTool(entry=entry), COMMS) + read: StandingRulesRead = ThreadCaller(who).invoke(CommsRuleReviewTool(entry=entry), CommanderConfig.installed().comms) print('\n'.join(read.broken) or 'review complete; commander actions allowed') -def review(who: str = ME): +def review(who: str): every = rules() for i, r in enumerate(every, 1): print(f'{i:3}. {r}') @@ -59,23 +56,23 @@ def review(who: str = ME): 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 != CommanderConfig.installed().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(who, DrawnRuleReviewEntry(tuple(drawn), len(drawn) if who != CommanderConfig.installed().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): +def answer(n: int, text: str, who: str): record(who, AnsweredRuleReviewEntry(n, text)) def main(a): - who = ME + who = CommanderConfig.installed().commander if '--as' in a: i = a.index('--as'); who = a[i + 1]; del a[i:i + 2] try: @@ -84,7 +81,7 @@ def main(a): elif a[:1] == ['answer'] and len(a) == 3 and a[1].isdigit(): answer(int(a[1]), a[2], who) elif a == ['stale']: - print('\n'.join(due.why for due in RulesDue.violations(COMMS, who)) or 'fresh') + print('\n'.join(due.why for due in RulesDue.violations(CommanderConfig.installed().comms, who)) or 'fresh') else: sys.exit(__doc__) except ValueError as refused: # Core owns what a review and an answer may say diff --git a/src/agent_comms/commander/send_guard.py b/src/agent_comms/commander/send_guard.py index 3c3b37a7d3..e08704b9b1 100644 --- a/src/agent_comms/commander/send_guard.py +++ b/src/agent_comms/commander/send_guard.py @@ -24,6 +24,7 @@ from ..declared_family import DeclaredFamily from ..errors import RelationViolationError from ..todos import InstallDeadline +from .config import CommanderConfig from . import order Words = tuple[str, ...] @@ -49,8 +50,8 @@ class CommanderSession: session: str def commander(self) -> bool: - process = order.COMMS.registry.require(order.ME).process_identity.require( - f'{order.ME} has no registered process to compare this session with') + process = CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).process_identity.require( + f'{CommanderConfig.installed().commander} has no registered process to compare this session with') environment = Path(f'/proc/{process.pid}/environ').read_bytes().split(b'\0') return f'CLAUDE_CODE_SESSION_ID={self.session}'.encode() in environment @@ -59,7 +60,7 @@ def refusals(self, kind: type[TacticalCommand]) -> tuple[str, ...]: return (kind.refusal(),) if self.commander() else () except (OSError, RelationViolationError) as error: return (f'{kind.work.capitalize()} refused: the guard cannot tell whether this ' - f'session is {order.ME} ({error}).',) + f'session is {CommanderConfig.installed().commander} ({error}).',) @dataclass(frozen=True) @@ -87,7 +88,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 order.units()} - if ('--from', order.ME) not in pairs or not addressed: + if ('--from', CommanderConfig.installed().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).',) @@ -116,7 +117,7 @@ def ordered(shape: Words) -> bool: @classmethod def refusal(cls) -> str: - return InstallDeadline.read(order.COMMS.root).refusal(cls.work, order.ME) + return InstallDeadline.read(CommanderConfig.installed().comms.root).refusal(cls.work, CommanderConfig.installed().commander) @dataclass(frozen=True) diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index b8408a26b7..bbc3d01414 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -20,7 +20,6 @@ from . import capacity, judge from .config import CommanderConfig, Repo -CONFIG = CommanderConfig.installed() @dataclass(frozen=True) @@ -73,7 +72,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.installed().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 @@ -224,7 +223,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.installed().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 9f2e881f79..5e149302f9 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -18,9 +18,6 @@ from ..registration import Registration from ..todos import StorageStrikeTodo -CONFIG = CommanderConfig.installed() -B = str(CONFIG.root) -LOG = f'{CONFIG.state}/activity.log' def thread_names(registry: Registration) -> dict[str, str]: @@ -103,7 +100,7 @@ def walk(registry: Registration, turns: dict[str, list[str]], thread: str, name, args = call.name, call.arguments tool = decoded(name, args, call.id) digest = digest_line(name, args, tool) - with open(LOG, 'a') as log: + with open(CommanderConfig.installed().activity, 'a') as log: log.write(f"{time.strftime('%H:%M:%S')} {thread} {digest[:260]}\n") turns[thread].append(digest) enforce_storage(registry, thread, tool) @@ -115,14 +112,14 @@ def walk(registry: Registration, turns: dict[str, list[str]], thread: str, def main(a): - registry = Registration(Path(B) / 'registry.json') + registry = Registration(Path(CommanderConfig.installed().root) / 'registry.json') 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'{B}/native-sessions/*/*.jsonl')) | set(names): + for path in set(glob.glob(f'{CommanderConfig.installed().root}/native-sessions/*/*.jsonl')) | set(names): try: st = os.stat(path) except OSError: diff --git a/tests/conftest.py b/tests/conftest.py index 1987595dd6..61ae64aa3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -257,7 +257,6 @@ def commander(comms: Comms, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): @pytest.fixture def commander_script(commander, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """The order script installed for lead on the commander bus, with a git checkout its orders check.""" - import importlib import subprocess import sys @@ -275,18 +274,15 @@ def commander_script(commander, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) disk=tmp_path).write(tmp_path) CommanderConfig.installed.cache_clear() from agent_comms.commander import order - yield importlib.reload(order), repo + yield order, repo CommanderConfig.installed.cache_clear() @pytest.fixture def tool_watch(commander_script): """The tool-call watcher reading the commander config commander_script installed.""" - import importlib - - from agent_comms.commander import judge, tool_watch - importlib.reload(judge) - return importlib.reload(tool_watch) + from agent_comms.commander import tool_watch + return tool_watch @pytest.fixture diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index ab2521cb90..80b896b5ab 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -21,6 +21,7 @@ from agent_comms.change_report import FileLine from agent_comms.channel_targets import CommandTarget from agent_comms.child_process import BoundedRun +from agent_comms.commander.config import CommanderConfig from agent_comms.comms import Comms from agent_comms.coordination_database import CoordinationStore from agent_comms.field_codec import FieldCodec, MalformedRecord @@ -796,8 +797,7 @@ def test_an_open_request_alarms_until_an_order_serves_it_and_closes_only_once_no import importlib from agent_comms.commander import request comms, caller = commander - request = importlib.reload(request) - standing = importlib.reload(importlib.import_module('agent_comms.commander.standing')) + standing = importlib.import_module('agent_comms.commander.standing') def stalled(): return [rule.line() for rule in standing.RequestStalled.violations(comms, 'lead')] assert stalled() == [ @@ -990,9 +990,9 @@ def test_a_rotation_pass_names_the_unit_with_orders_that_reads_nothing(commander create(commander, 'P1', owner='parted') create(commander, 'P2', owner='watching') comms.messaging.send('lead', 'parted', 'Are you there?').message.message_id - rotate = importlib.reload(importlib.import_module('agent_comms.commander.rotate')) + importlib.import_module('agent_comms.commander.rotate') with comms.registry.external_process('watching', ProcessIdentity.capture(os.getpid())): - standing = importlib.reload(importlib.import_module('agent_comms.commander.standing')) + standing = importlib.import_module('agent_comms.commander.standing') lines = [rule.line() for rule in standing.Detached.violations(comms, 'lead')] assert lines == ['DETACHED parted: 1 unread, orders P1 -> wake it or reassign'] @@ -1037,7 +1037,7 @@ def test_a_verdict_lands_on_the_order_its_message_was_sent_in(commander, command comms, _ = commander create(commander, 'P1') create(commander, 'P2') - judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) + judge = importlib.import_module('agent_comms.commander.judge') 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') @@ -1055,7 +1055,7 @@ def test_judge_ok_writes_the_todo_not_a_second_store(commander, commander_script comms, _ = commander create(commander, 'P1') - judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) + judge = importlib.import_module('agent_comms.commander.judge') edits = [dict(id=f'edit-{n}', at=time.time(), thread='worker', path='value.py', hunks=[dict(old='value = 1', new='value = 2')]) for n in range(2)] recorded = native_judge_edits(commander, judge, edits) @@ -1067,8 +1067,8 @@ def test_judge_ok_writes_the_todo_not_a_second_store(commander, commander_script 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 judge.Edit.unjudged(comms, 'lead') == [] + assert not (CommanderConfig.installed().state / 'verdicts.jsonl').exists() @pytest.mark.parametrize('verb', ['ok', 'back']) @@ -1077,7 +1077,7 @@ def test_judge_refuses_an_edit_without_an_open_order(commander, commander_script from pathlib import Path comms, _ = commander - judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) + judge = importlib.import_module('agent_comms.commander.judge') edit = dict(id='orphan-edit', at=1.0, thread='worker', path='value.py', hunks=[dict(old='value = 1', new='value = 2')]) recorded = native_judge_edits(commander, judge, [edit]) fields = dict(ids=('orphan-edit:0000',), @@ -1097,7 +1097,7 @@ def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_s comms, _ = commander create(commander, 'P1') - judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) + judge = importlib.import_module('agent_comms.commander.judge') edit = dict(id='bad-edit', 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', 'P1 Repair the existing owner') @@ -1206,7 +1206,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.""" comms, caller = commander - judge = importlib.reload(importlib.import_module('agent_comms.commander.judge')) + judge = importlib.import_module('agent_comms.commander.judge') assert set(judge.VerdictVerb.names()) == set(VERDICT_LINES) create(commander, 'P1') for name, line in VERDICT_LINES.items(): diff --git a/tests/test_commander_verbs.py b/tests/test_commander_verbs.py index 75029e3feb..6cd1cad022 100644 --- a/tests/test_commander_verbs.py +++ b/tests/test_commander_verbs.py @@ -55,7 +55,7 @@ def test_every_verb_parses_its_documented_command_line(commander_script, tool, line, fields): order, _ = commander_script module = (order if tool == "order" - else importlib.reload(importlib.import_module(f"agent_comms.commander.{tool}"))) + else importlib.import_module(f"agent_comms.commander.{tool}")) family = getattr(module, f"{tool.capitalize()}Verb") verb = family.parse(shlex.split(line), "") assert verb.declared_name == line.split()[0] diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index 3bebb641b8..a2be8b28d5 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -33,10 +33,7 @@ @pytest.fixture def standing(commander, commander_script): """The commander-file members reloaded against the scratch commander config.""" - from agent_comms.commander import capacity, judge - for module in (judge, capacity): - importlib.reload(module) - module = importlib.reload(importlib.import_module("agent_comms.commander.standing")) + module = importlib.import_module("agent_comms.commander.standing") state = CommanderConfig.installed().state return commander[0], state, module From 4a02de0117e92e9be36e11f8338c2949233e4842 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:41:01 -0400 Subject: [PATCH 07/13] P196 migrate the remaining readers of the commander module globals Tests that read script.CONFIG, script.COMMS or judge.CONFIG read the installed config; tests that replaced a module's CONFIG write the changed config the way install does. strike takes who caught the miss explicitly, and tool_watch now passes the struck task, which P194's signature change had left as the reason. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/tool_watch.py | 2 +- stack/pi-native.sha256 | 2 +- tests/test_commander_problem_orders.py | 17 +++++++++-------- tests/test_commander_run_rules.py | 3 ++- tests/test_order_progress.py | 7 ++++--- tests/test_standing_rules.py | 6 ++++-- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/agent_comms/commander/tool_watch.py b/src/agent_comms/commander/tool_watch.py index 5e149302f9..385df4e81a 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -60,7 +60,7 @@ def enforce_storage(registry: Registration, thread: str, call: CodingCall) -> No violation = str(denied) print(f'VIOLATION storage {thread}: {violation}', flush=True) for todo in order.held_by(thread)[:1]: - order.strike(thread, f'{todo.id} broke the storage rule at a tool call: {violation}; ' + order.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) diff --git a/stack/pi-native.sha256 b/stack/pi-native.sha256 index c81a918467..ec40c5c538 100644 --- a/stack/pi-native.sha256 +++ b/stack/pi-native.sha256 @@ -57,4 +57,4 @@ d64e8c05c7932d377034820b1db0df45612afe64885c0765f8f348a12e1bc955 dist/core/skil 0391543cf703a93a0163b37b3ccebe53e0f12bff6f6162b86d56fb017fe49011 dist/core/system-prompt.js # agent-comms-native-stock-v1 @earendil-works/pi-coding-agent@0.85.1 # agent-comms-native-tree-v1 f236c5eda9bfd3abf164011d36e8cdffb70c0d82db7c83f1923d61fb03ed8728 -# agent-comms-native-inputs-v1 8e303ad501091cacd68c47fd5970335b19359b6852ca1cfd0e0df95d0adb605f +# agent-comms-native-inputs-v1 1645abec0f5b3615aaa7efd29e8f757c8faa3ccc0358b3530bc44cd927293855 diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index 80b896b5ab..9de4a72593 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -434,7 +434,7 @@ def test_resend_after_strike_counts_one_miss(commander, commander_script, monkey (repo / 'count.txt').write_text('0\n') body = order_body(repo) problem(script, body) - script.strike('worker', 'P1', 'P1 left count.txt at zero and reported it fixed') + script.strike('worker', 'P1', 'P1 left count.txt at zero and reported it fixed', 'lead at review') script.send('worker', '23:59', body, 'request') assert script.struck(comms.goals.todos.get('P1'), 'worker') == 1 script.send('worker', '23:59', body, 'request') @@ -449,7 +449,7 @@ def test_a_strike_and_a_clean_hunt_land_on_their_problem_argument_not_one_the_wh (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') + script.strike('worker', 'P2', 'P1 was compared with this miss: P2 left its count at zero and reported it fixed', 'lead at review') assert [script.struck(comms.goals.todos.get(pid), 'worker') for pid in ('P1', 'P2')] == [0, 1] script.hunt(f'{__file__}:1', 'P2', 'P1 rereads the counter per event; this read of the P2 path found it clean', None) assert [len(comms.goals.todos.get(pid).hunts) for pid in ('P1', 'P2')] == [0, 1] @@ -464,8 +464,9 @@ def test_a_deletion_order_must_check_the_imports_of_every_repo_that_imports_the_ 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'),)})) + CommanderConfig.installed().model_copy(update={'repos': ( + script.Repo(checkout=toad, github='OpenHCSDev/toad'),)}).write(Path.home()) + CommanderConfig.installed.cache_clear() (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"): @@ -502,7 +503,7 @@ def test_order_check_replaces_a_wrong_check_so_done_runs_the_corrected_one(comma "The counter is wrong and that is stupid.\n1. Set count.txt to 1.\n" "Banned: no second store; no cache\nReport: the commit and the count before -> after\n") script.problem('The counter file holds zero where the fleet needs one', 'worker', '23:59', str(body), 'request') - script.strike('worker', 'P1', 'P1 was sent back once before its check was corrected') + script.strike('worker', 'P1', 'P1 was sent back once before its check was corrected', 'lead at review') sent = comms.goals.todos.get('P1') corrected = f'{repo} :: cat count.txt -> 1' why = 'the recorded check greps a word the ordered edit never writes' @@ -603,7 +604,7 @@ def test_order_unstrike_voids_the_newest_live_strike_on_the_record(commander, co create(commander, 'P1') posted = [] monkeypatch.setattr(script, 'post', lambda recipients, body: posted.append((recipients, body))) - script.strike('worker', 'P1', 'P1 was struck for lateness against the wrong delivered commit') + script.strike('worker', 'P1', 'P1 was struck for lateness against the wrong delivered commit', 'lead at review') 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 @@ -1088,7 +1089,7 @@ def test_judge_refuses_an_edit_without_an_open_order(commander, commander_script 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() + assert not (CommanderConfig.installed().state / 'verdicts.jsonl').exists() def test_judge_back_writes_one_strike_and_posts_the_roast(commander, commander_script, monkeypatch): @@ -1117,7 +1118,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 (CommanderConfig.installed().state / 'verdicts.jsonl').exists() @pytest.mark.parametrize('kind', [AcceptedVerdict, SendBackVerdict]) diff --git a/tests/test_commander_run_rules.py b/tests/test_commander_run_rules.py index 6a3b44ff99..85ad22835d 100644 --- a/tests/test_commander_run_rules.py +++ b/tests/test_commander_run_rules.py @@ -4,6 +4,7 @@ import pytest from agent_comms.change_report import FileLine +from agent_comms.commander.config import CommanderConfig from agent_comms.field_codec import FieldCodec from agent_comms.recorded_time import RecordedEventTime from agent_comms.todos import ApplicationOrderRun, GoalRef, MechanicalOrderRun, Todo, TodoStore @@ -55,4 +56,4 @@ def test_send_refuses_missing_run_but_mechanical_orders_are_exempt(commander_scr script.send('worker', '23:59', str(body), 'request', **kwargs) assert script.todos() == () script.send('worker', '23:59', str(body), 'request', mechanical=True, **kwargs) - assert isinstance(script.COMMS.goals.todos.get('P1').order.run, MechanicalOrderRun) + assert isinstance(CommanderConfig.installed().comms.goals.todos.get('P1').order.run, MechanicalOrderRun) diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index 508e1fea23..451742b452 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -6,6 +6,7 @@ import pytest +from agent_comms.commander.config import CommanderConfig from agent_comms.recorded_time import RecordedEventTime from agent_comms.tools import CommsOrderDoneTool from agent_comms.todos import (CommittedProgress, DistanceReadings, MergedProgress, NoCommitProgress, @@ -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(CommanderConfig.installed().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') @@ -249,7 +250,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(CommanderConfig.installed().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') @@ -282,7 +283,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(CommanderConfig.installed().comms.registry, "storage_rule", storage) body_file = tmp_path / "pr-body.md" body_file.write_text(subject) bodies = [] diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index a2be8b28d5..36a81236b1 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -6,6 +6,7 @@ import sys import time from dataclasses import replace +from pathlib import Path import pytest @@ -386,8 +387,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"),)})) + CommanderConfig.installed().model_copy(update={"repos": ( + Repo(checkout=checkout, github="fleet/growth"),)}).write(Path.home()) + CommanderConfig.installed.cache_clear() 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 " From 695592424875d98f54e57caeb95ecfb51d0d7e45 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:41:01 -0400 Subject: [PATCH 08/13] P196 hold each refusal check as the callable it names commander/order.py imports without opening the bus, so a refusal check holds the function or class itself. The module and qualname strings, the ast re-parse and the exists() probe it needed are deleted. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/behaviour_patterns.py | 64 ++++++++++----------------- tests/test_behaviour_catalog.py | 1 - 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index 0cafef4b7d..86c0168330 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -8,15 +8,19 @@ from __future__ import annotations -import ast from abc import ABC, abstractmethod +from collections.abc import Callable from dataclasses import dataclass -from inspect import isabstract -from pathlib import Path from typing import ClassVar +from .channel_targets import ChannelTarget +from .commander import order +from .commander.send_guard import TacticalCommand +from .connector import DirectorCaller from .declared_family import DeclaredFamily +from .goals import NoIntegerPrinted, OrderCheck from .standing_rules import BoardDisagrees, PlanIdle, StandingRule +from .todos import StrikeTodo, VoidStrikeTodo POSTMORTEM = "https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md" COMMANDER_SKILL = "../../src/agent_comms/skills/comms-commander/SKILL.md" @@ -25,9 +29,6 @@ class PatternCheck(ABC): """An existing check that enforces a pattern.""" - @abstractmethod - def exists(self) -> bool: ... - @abstractmethod def cite(self) -> str: ... @@ -38,9 +39,6 @@ class RuleCheck(PatternCheck): rule: type[StandingRule] - def exists(self) -> bool: - return issubclass(self.rule, StandingRule) and not isabstract(self.rule) - def cite(self) -> str: where = f"{self.rule.__module__}.{self.rule.__qualname__}" return f"standing rule `{self.rule.declared_name}` (`{where}`)" @@ -48,29 +46,13 @@ def cite(self) -> str: @dataclass(frozen=True) class RefusalCheck(PatternCheck): - """A refusal located by its definition, read from source: the commander verbs - open the installed bus when imported, so the catalog never imports them.""" + """A refusal, held as the function or class that refuses.""" - module: str - qualname: str + refusal: Callable[..., object] surface: ClassVar[str] - def exists(self) -> bool: - path = Path(__file__).parent.joinpath(*self.module.split(".")[1:]).with_suffix(".py") - scope = ast.parse(path.read_text()).body - for name in self.qualname.split("."): - defined = { - node.name: node.body - for node in scope - if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) - } - if name not in defined: - return False - scope = defined[name] - return True - def cite(self) -> str: - return f"{self.surface} in `{self.module}.{self.qualname}`" + return f"{self.surface} in `{self.refusal.__module__}.{self.refusal.__qualname__}`" class VerbRefusal(RefusalCheck): @@ -185,7 +167,7 @@ class TacticalCommandPattern(BehaviourPattern): "The commander's own session runs an install, maintenance, connector restart or carry" " command instead of ordering a unit to." ) - checks = (ToolRefusal("agent_comms.commander.send_guard", "TacticalCommand"),) + checks = (ToolRefusal(TacticalCommand),) sources = (ProblemFix("P171", "cba7b09c3"),) @@ -195,15 +177,15 @@ class PlainTextPattern(BehaviourPattern): " the declared jargon list in `PlainText`." ) checks = ( - VerbRefusal("agent_comms.commander.order", "refuse"), - ToolRefusal("agent_comms.connector", "DirectorCaller.require_directive"), + VerbRefusal(order.refuse), + ToolRefusal(DirectorCaller.require_directive), ) sources = (CommanderRule("Hate jargon."), PostmortemFailure(8)) class UndeclaredAudiencePattern(BehaviourPattern): signature = "A send to a channel, which reaches many threads, names no thread as its audience." - checks = (ToolRefusal("agent_comms.channel_targets", "ChannelTarget.addressed"),) + checks = (ToolRefusal(ChannelTarget.addressed),) sources = (Commit("dc4fb3bfa"), PostmortemFailure(8)) @@ -213,8 +195,8 @@ class RepeatedStrikePattern(BehaviourPattern): " send-back strike on an order already struck since it was sent." ) checks = ( - ToolRefusal("agent_comms.todos", "StrikeTodo.repeats"), - VerbRefusal("agent_comms.commander.order", "send"), + ToolRefusal(StrikeTodo.repeats), + VerbRefusal(order.send), ) sources = (ProblemFix("P184", "7af14a520"),) @@ -225,8 +207,8 @@ class StrikeInErrorPattern(BehaviourPattern): " recorded beside it with a plain reason." ) checks = ( - VerbRefusal("agent_comms.commander.order", "unstrike"), - ToolRefusal("agent_comms.todos", "VoidStrikeTodo.apply"), + VerbRefusal(order.unstrike), + ToolRefusal(VoidStrikeTodo.apply), ) sources = (ProblemFix("P47", "15d435152"),) @@ -237,9 +219,9 @@ class UncountedCheckPattern(BehaviourPattern): " integer, or already prints its target before any work." ) checks = ( - VerbRefusal("agent_comms.goals", "OrderCheck.parse"), - VerbRefusal("agent_comms.goals", "NoIntegerPrinted"), - VerbRefusal("agent_comms.commander.order", "correct_check"), + VerbRefusal(OrderCheck.parse), + VerbRefusal(NoIntegerPrinted), + VerbRefusal(order.correct_check), ) sources = (ProblemFix("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) @@ -250,8 +232,8 @@ class StrikeOnNamedProblemPattern(BehaviourPattern): " as one moved by a problem number inside its reason." ) checks = ( - VerbRefusal("agent_comms.commander.order", "strike"), - VerbRefusal("agent_comms.commander.order", "hunt"), + VerbRefusal(order.strike), + VerbRefusal(order.hunt), ) sources = (ProblemFix("P194", "b26843727"), CommanderRule("Strikes, then fire.")) diff --git a/tests/test_behaviour_catalog.py b/tests/test_behaviour_catalog.py index f8b25923e2..740d7f49d1 100644 --- a/tests/test_behaviour_catalog.py +++ b/tests/test_behaviour_catalog.py @@ -12,5 +12,4 @@ def test_behaviour_catalog_members_name_existing_checks_and_generate_the_file(): assert members for member in members: assert member.checks and member.sources, member.declared_name - assert all(check.exists() for check in member.checks), member.declared_name assert (ROOT / "docs/agents/behaviour-catalog.md").read_text() == BehaviourPattern.catalog() From 305fa5a27ebeecd66a0a7c8c860a4be8914457fe Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:41:17 -0400 Subject: [PATCH 09/13] P196 drop the native pin taken from P206's branch for the local gate Co-Authored-By: Claude Opus 5.5 --- stack/pi-native.sha256 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack/pi-native.sha256 b/stack/pi-native.sha256 index ec40c5c538..c81a918467 100644 --- a/stack/pi-native.sha256 +++ b/stack/pi-native.sha256 @@ -57,4 +57,4 @@ d64e8c05c7932d377034820b1db0df45612afe64885c0765f8f348a12e1bc955 dist/core/skil 0391543cf703a93a0163b37b3ccebe53e0f12bff6f6162b86d56fb017fe49011 dist/core/system-prompt.js # agent-comms-native-stock-v1 @earendil-works/pi-coding-agent@0.85.1 # agent-comms-native-tree-v1 f236c5eda9bfd3abf164011d36e8cdffb70c0d82db7c83f1923d61fb03ed8728 -# agent-comms-native-inputs-v1 1645abec0f5b3615aaa7efd29e8f757c8faa3ccc0358b3530bc44cd927293855 +# agent-comms-native-inputs-v1 8e303ad501091cacd68c47fd5970335b19359b6852ca1cfd0e0df95d0adb605f From 440d368db7fd54a4ab2b15cc48b9bf8958d376e8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:42:00 -0400 Subject: [PATCH 10/13] P196 declare the postmortem failures as members Each of the Codex postmortem's ten failures is a PostmortemFailure member with its number and title; a pattern cites an instance, and the catalog lists the declared members instead of range(1, 11). Co-Authored-By: Claude Opus 5.5 --- docs/agents/behaviour-catalog.md | 24 ++++---- src/agent_comms/behaviour_patterns.py | 80 +++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/docs/agents/behaviour-catalog.md b/docs/agents/behaviour-catalog.md index 8bb3eab66c..ee2f7655f2 100644 --- a/docs/agents/behaviour-catalog.md +++ b/docs/agents/behaviour-catalog.md @@ -5,18 +5,18 @@ Generated from `agent_comms.behaviour_patterns` by Each behaviour failure pattern is declared once in Core with its signature (what it looks like in state, readable without trusting a report) and the existing checks that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit skill's catalog (`refactor-audit/references/patterns/README.md`). -The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) names ten failures; each pattern cites the ones it checks: - -- [Failure 1](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 2](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 3](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `uncounted_check` -- [Failure 5](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 6](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `board_disagrees`, `uncounted_check` -- [Failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): `plain_text`, `undeclared_audience` -- [Failure 9](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet -- [Failure 10](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md): no pattern checks it yet +The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) names these failures; each pattern cites the ones it checks: + +- [Failure 1](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It rewrote the framework instead of questioning its own design: no pattern checks it yet +- [Failure 2](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It added states instead of removing them: no pattern checks it yet +- [Failure 3](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It wrote down the right principle and then violated it: no pattern checks it yet +- [Failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It measured the wrong thing and reported it as progress: `uncounted_check` +- [Failure 5](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It redefined "the real app" until something passed: no pattern checks it yet +- [Failure 6](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It ignored direct orders: no pattern checks it yet +- [Failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It described its intentions as its accomplishments: `board_disagrees`, `uncounted_check` +- [Failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It drowned the work in ceremony: `plain_text`, `undeclared_audience` +- [Failure 9](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It merged over its own checks and broke things in circles: no pattern checks it yet +- [Failure 10](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It invented rules and blamed them on Tristan: no pattern checks it yet ## plan_idle diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index 86c0168330..4554056dca 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -70,14 +70,66 @@ class PatternSource(ABC): def cite(self) -> str: ... -@dataclass(frozen=True) -class PostmortemFailure(PatternSource): - number: int +class PostmortemFailure(PatternSource, DeclaredFamily, affix="Failure"): + """A failure the Codex postmortem names; a pattern that checks it cites an instance.""" + + number: ClassVar[int] + title: ClassVar[str] def cite(self) -> str: return f"[Codex postmortem, failure {self.number}]({POSTMORTEM})" +class RewroteFrameworkFailure(PostmortemFailure): + number = 1 + title = "It rewrote the framework instead of questioning its own design" + + +class AddedStatesFailure(PostmortemFailure): + number = 2 + title = "It added states instead of removing them" + + +class ViolatedPrincipleFailure(PostmortemFailure): + number = 3 + title = "It wrote down the right principle and then violated it" + + +class MeasuredWrongThingFailure(PostmortemFailure): + number = 4 + title = "It measured the wrong thing and reported it as progress" + + +class RedefinedRealAppFailure(PostmortemFailure): + number = 5 + title = 'It redefined "the real app" until something passed' + + +class IgnoredOrdersFailure(PostmortemFailure): + number = 6 + title = "It ignored direct orders" + + +class IntentionsAsAccomplishmentsFailure(PostmortemFailure): + number = 7 + title = "It described its intentions as its accomplishments" + + +class CeremonyFailure(PostmortemFailure): + number = 8 + title = "It drowned the work in ceremony" + + +class MergedOverChecksFailure(PostmortemFailure): + number = 9 + title = "It merged over its own checks and broke things in circles" + + +class InventedRulesFailure(PostmortemFailure): + number = 10 + title = "It invented rules and blamed them on Tristan" + + @dataclass(frozen=True) class CommanderRule(PatternSource): lead: str @@ -121,11 +173,13 @@ def section(cls) -> str: def catalog(cls) -> str: members = cls.members_with(BehaviourPattern) cited = { - failure: [m.declared_name for m in members if PostmortemFailure(failure) in m.sources] - for failure in range(1, 11) + failure: [ + m.declared_name for m in members if any(isinstance(s, failure) for s in m.sources) + ] + for failure in PostmortemFailure.members_with(PostmortemFailure) } failures = "".join( - f"- [Failure {failure}]({POSTMORTEM}): " + f"- [Failure {failure.number}]({POSTMORTEM}), {failure.title}: " + (", ".join(f"`{name}`" for name in names) if names else "no pattern checks it yet") + "\n" for failure, names in cited.items() @@ -139,7 +193,7 @@ def catalog(cls) -> str: " looks like in state, readable without trusting a report) and the existing checks" " that enforce it. Patterns in code, rather than in behaviour, are the refactor-audit" " skill's catalog (`refactor-audit/references/patterns/README.md`).\n\n" - f"The [Codex postmortem]({POSTMORTEM}) names ten failures; each pattern cites the" + f"The [Codex postmortem]({POSTMORTEM}) names these failures; each pattern cites the" " ones it checks:\n\n" f"{failures}\n" + "\n".join(member.section() for member in members) ) @@ -159,7 +213,7 @@ class BoardDisagreesPattern(BehaviourPattern): "An open order's progress reads no commit while a pull request linked to it has merged." ) checks = (RuleCheck(BoardDisagrees),) - sources = (ProblemFix("P170", "c9c0c04db"), PostmortemFailure(7)) + sources = (ProblemFix("P170", "c9c0c04db"), IntentionsAsAccomplishmentsFailure()) class TacticalCommandPattern(BehaviourPattern): @@ -180,13 +234,13 @@ class PlainTextPattern(BehaviourPattern): VerbRefusal(order.refuse), ToolRefusal(DirectorCaller.require_directive), ) - sources = (CommanderRule("Hate jargon."), PostmortemFailure(8)) + sources = (CommanderRule("Hate jargon."), CeremonyFailure()) class UndeclaredAudiencePattern(BehaviourPattern): signature = "A send to a channel, which reaches many threads, names no thread as its audience." checks = (ToolRefusal(ChannelTarget.addressed),) - sources = (Commit("dc4fb3bfa"), PostmortemFailure(8)) + sources = (Commit("dc4fb3bfa"), CeremonyFailure()) class RepeatedStrikePattern(BehaviourPattern): @@ -223,7 +277,11 @@ class UncountedCheckPattern(BehaviourPattern): VerbRefusal(NoIntegerPrinted), VerbRefusal(order.correct_check), ) - sources = (ProblemFix("P114", "d889df612"), PostmortemFailure(4), PostmortemFailure(7)) + sources = ( + ProblemFix("P114", "d889df612"), + MeasuredWrongThingFailure(), + IntentionsAsAccomplishmentsFailure(), + ) class StrikeOnNamedProblemPattern(BehaviourPattern): From a105a003dda94dd045291fd1b524fa1fabcd3e83 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:48:11 -0400 Subject: [PATCH 11/13] P196 bind the installed config once per verb and declare the commander incarnation Functions that read the installed config more than once bind it at their top. The commander's incarnation, read through the registry at 15 sites, is a CommanderConfig property. Lines the longer reads pushed past 100 columns are wrapped, so the commander files carry the base's lint count. The budget covers the catalog module (300 lines) and the commander package's net 59. Src-Budget: 359 Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander/capacity.py | 15 +-- src/agent_comms/commander/config.py | 9 +- src/agent_comms/commander/judge.py | 22 +++-- src/agent_comms/commander/order.py | 124 ++++++++++++++---------- src/agent_comms/commander/request.py | 14 ++- src/agent_comms/commander/rotate.py | 3 +- src/agent_comms/commander/rules.py | 18 ++-- src/agent_comms/commander/send_guard.py | 10 +- src/agent_comms/commander/standing.py | 3 +- src/agent_comms/commander/tool_watch.py | 10 +- 10 files changed, 144 insertions(+), 84 deletions(-) diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index a7c8127bfa..441e0d344f 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -20,14 +20,16 @@ def live_forks() -> list[str]: - os.makedirs(CommanderConfig.installed().forks, exist_ok=True) - return [n[:-5] for n in os.listdir(CommanderConfig.installed().forks) if n.endswith('.beat') and time.time() - os.path.getmtime(f'{CommanderConfig.installed().forks}/{n}') < BEAT_AGE] + config = CommanderConfig.installed() + os.makedirs(config.forks, exist_ok=True) + return [n[:-5] for n in os.listdir(config.forks) if n.endswith('.beat') and time.time() - os.path.getmtime(f'{config.forks}/{n}') < BEAT_AGE] def beat(name: str): - os.makedirs(CommanderConfig.installed().forks, exist_ok=True) - with open(f'{CommanderConfig.installed().forks}/{name}.beat', 'a'): - os.utime(f'{CommanderConfig.installed().forks}/{name}.beat') + config = CommanderConfig.installed() + os.makedirs(config.forks, exist_ok=True) + with open(f'{config.forks}/{name}.beat', 'a'): + os.utime(f'{config.forks}/{name}.beat') @dataclass(frozen=True) @@ -70,5 +72,6 @@ def require(self) -> None: def main(a): - load = Load.of(CommanderConfig.installed().comms, CommanderConfig.installed().commander) + config = CommanderConfig.installed() + load = Load.of(config.comms, config.commander) print(load if load.short else f'covered: load {load.total:.0f}, forks {list(load.forks)}') diff --git a/src/agent_comms/commander/config.py b/src/agent_comms/commander/config.py index 06c69c6be4..d05de73ba3 100644 --- a/src/agent_comms/commander/config.py +++ b/src/agent_comms/commander/config.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, ConfigDict from ..comms import Comms +from ..thread_identity import ThreadIncarnation class Repo(BaseModel): @@ -65,9 +66,15 @@ def installed(cls) -> CommanderConfig: @cached_property def comms(self) -> Comms: - """The bus whose Todo store owns problems, orders, deadlines and strikes, opened on first use.""" + """The bus whose Todo store owns problems, orders, deadlines and strikes, opened on first + use.""" return Comms(self.root) + @property + def incarnation(self) -> ThreadIncarnation: + """The commander thread's current incarnation, read from the registry on each use.""" + return self.comms.registry.require(self.commander).incarnation + @property def review_catch(self) -> str: """Who caught a struck miss, and at which stage, when the strike names no one else.""" diff --git a/src/agent_comms/commander/judge.py b/src/agent_comms/commander/judge.py index 3bfc5bf763..76f46b61fe 100644 --- a/src/agent_comms/commander/judge.py +++ b/src/agent_comms/commander/judge.py @@ -103,8 +103,10 @@ def roast(audience, thread, lines, text, fix, slop): def alarm(): + config = CommanderConfig.installed() while True: - late = [edit for edit in Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander) if time.time() - edit.at > 300] + late = [edit for edit in Edit.unjudged(config.comms, config.commander) + if time.time() - edit.at > 300] if late: counts: dict[str, int] = {} for edit in late: @@ -126,7 +128,8 @@ def selects(self, edit: Edit) -> bool: return True def run(self) -> None: - show([edit for edit in Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander) if self.selects(edit)]) + config = CommanderConfig.installed() + show([edit for edit in Edit.unjudged(config.comms, config.commander) if self.selects(edit)]) @dataclass(frozen=True, kw_only=True) @@ -165,10 +168,12 @@ def check_edits(self, edits: list[Edit]) -> None: ... def report(self, todos: tuple[Todo, ...]) -> None: ... def run(self) -> None: + config = CommanderConfig.installed() 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 todo.sent_in(self.msg)} - edits = {edit.id: edit for edit in Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander) if edit.problem in orders} + edits = {edit.id: edit for edit in Edit.unjudged(config.comms, config.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}') @@ -187,7 +192,7 @@ def run(self) -> None: for i, todo in selected.items(): result = order.call(CommsVerdictTool(id=todo.id, operation=self.operation(current[todo.id], i))) current[todo.id] = result.todo - left = len(Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander)) + left = len(Edit.unjudged(config.comms, config.commander)) print(f'{self.declared_name}: {len(ids)} edits; {left} still unjudged') self.report(tuple(current.values())) @@ -198,7 +203,8 @@ class OkVerb(VerdictVerb): serves: str = option(help="exact words of the order") def operation(self, todo: Todo, edit_id: str) -> AcceptedVerdict: - return AcceptedVerdict(todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, + config = CommanderConfig.installed() + return AcceptedVerdict(todo.revision, config.incarnation, todo.held(), edit_id, quote=self.serves) def check_edits(self, edits: list[Edit]) -> None: @@ -245,7 +251,8 @@ def slop(self) -> order.Slop: return order.Slop(self.slop_kind, self.fact, self.at, self.derive, self.by) def operation(self, todo: Todo, edit_id: str) -> SendBackVerdict: - return SendBackVerdict(todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, + config = CommanderConfig.installed() + return SendBackVerdict(todo.revision, config.incarnation, todo.held(), edit_id, reason=StrikeReason(f'{todo.id} edit sent back: {self.why}')) @@ -259,11 +266,12 @@ def check_edits(self, edits: list[Edit]) -> None: order.refuse(' '.join((self.why, self.roast, self.fix)), *order.OUTGOING) def report(self, todos: tuple[Todo, ...]) -> None: + config = CommanderConfig.installed() 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()) line = (f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ' - f'(strike {todo.strike_count} on {todo.id}; caught by {CommanderConfig.installed().review_catch}): ' + f'(strike {todo.strike_count} on {todo.id}; caught by {config.review_catch}): ' f'{todo.strikes[-1].reason}') order.on_record(line, 'Send-back', audience) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 28a38c2bfc..83f3afd2e0 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -58,8 +58,9 @@ def post(audience: VerdictAudience, body: str) -> str: One thread gets a DM; a larger audience is mentioned on #comms, which wakes no one else. """ + config = CommanderConfig.installed() target = audience.threads[0] if len(audience.threads) == 1 else '#comms' - out = subprocess.run(['timeout', '300', CommanderConfig.installed().cli, '--root', CommanderConfig.installed().root, 'send', '--from', CommanderConfig.installed().commander, '--to', target, + out = subprocess.run(['timeout', '300', config.cli, '--root', config.root, 'send', '--from', config.commander, '--to', target, '--type', 'info', '--body', audience.addressed(target, body)], capture_output=True, text=True) if '"id"' not in out.stdout: @@ -69,28 +70,33 @@ def post(audience: VerdictAudience, body: str) -> str: def call(tool): """Every order write is one Core tool request made as the commander's thread.""" - return ThreadCaller(CommanderConfig.installed().commander).invoke(tool, CommanderConfig.installed().comms) + config = CommanderConfig.installed() + return ThreadCaller(config.commander).invoke(tool, config.comms) def todos(): """The problems this commander created; Core's Todo store is their one owner.""" - return CommanderConfig.installed().comms.goals.todos.for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + config = CommanderConfig.installed() + return config.comms.goals.todos.for_commander(config.incarnation) def held_orders() -> tuple[Todo, ...]: """This commander's open orders that a unit holds.""" - return CommanderConfig.installed().comms.goals.todos.held_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + config = CommanderConfig.installed() + return config.comms.goals.todos.held_for_commander(config.incarnation) def held_by(unit: str) -> tuple[Todo, ...]: """This commander's open orders held by one unit.""" - return CommanderConfig.installed().comms.goals.todos.held_by(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, unit) + config = CommanderConfig.installed() + return config.comms.goals.todos.held_by(config.incarnation, unit) def units() -> frozenset[str]: """The threads this commander gives orders to; the owner, claude-ai and peer commanders are not among them.""" - return CommanderConfig.installed().comms.goals.todos.units(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + config = CommanderConfig.installed() + return config.comms.goals.todos.units(config.incarnation) def progress() -> dict[str, OrderProgress]: @@ -218,11 +224,12 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= """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.""" + config = CommanderConfig.installed() body = open(body_file).read() problems = [] # Below the floor Core's StorageRule denies each allocating tool call, so the order prose is not checked. try: - CommanderConfig.installed().comms.registry.storage_rule.require_quota(CommanderConfig.installed().comms.registry.require(unit)) + config.comms.registry.storage_rule.require_quota(config.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') Slop.from_order(body) # calling something slop requires the fact, where it is restated, its owner and the mechanism @@ -306,7 +313,7 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= 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 replaces else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + CommanderConfig.installed().comms.registry.storage_rule.order_text(unit, [f'{repo.checkout}/.venv/bin/python' for repo in CommanderConfig.installed().repos]) + '\nIf two lines of this order conflict, take the narrower reading, say which one you took, and proceed; do not stop to ask.' + message = (f'{body.rstrip()}' + ('\nThis replaces my earlier order on it; drop that one.' if replaces else '') + (f'\nTaken over because: {reassign}' if reassign else '') + '\n' + config.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 @@ -328,19 +335,19 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, reassign= at=RecordedEventTime(at), first_call_deadline=RecordedEventTime(at + 120)) except ValueError as refused: # Core owns what an order may say sys.exit(f'Refused order: {refused}') - common = dict(order=order, request=request, deadline=deadline(due), commander=CommanderConfig.installed().commander) + common = dict(order=order, request=request, deadline=deadline(due), commander=config.commander) 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=CommanderConfig.installed().comms.goals.todos.get(pid).revision, traced=trace, **common))) + problem=pid, expected_revision=config.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( - CommanderConfig.installed().comms.goals.todos.get(pid).revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, sent))) + config.comms.goals.todos.get(pid).revision, config.incarnation, sent))) for other, text in taken.items(): post(VerdictAudience((other,)), text) print(order.id) @@ -351,7 +358,7 @@ def send(unit, due, body_file, request, clean=False, mechanical=False, 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}', CommanderConfig.installed().review_catch) + strike(unit, group[0], f'sent back on {group[0]}: {verdict}', config.review_catch) def importers(modules: set[str], check: OrderCheck) -> list[str]: @@ -407,12 +414,13 @@ def say(units, body_file): def problem(text, unit, due, order_file, request): """A problem is born owned: Core records it with its order in one step, or not at all.""" + config = CommanderConfig.installed() 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 = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + history = config.comms.goals.todos.history_for_commander(config.incarnation) pid = f'P{max([int(t.id[1:]) for t in history if t.id[1:].isdigit()] + [0]) + 1}' open(order_file, 'w').write(body.replace('Problem: NEW', f'Problem: {pid}')) try: @@ -426,7 +434,8 @@ def problem(text, unit, due, order_file, request): def close(pid, works, artifact): """Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo.""" - ThreadCaller(CommanderConfig.installed().commander).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=artifact), CommanderConfig.installed().comms) + config = CommanderConfig.installed() + ThreadCaller(config.commander).invoke(CommsProblemCloseTool(problem=pid, works=works, artifact=artifact), config.comms) board() print(f'{pid} closed') @@ -438,6 +447,7 @@ def last_activity(unit): def board(): + config = CommanderConfig.installed() 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.', '', @@ -452,10 +462,10 @@ def board(): f'{hhmm(t.deadline)} | {read[t.id].line if t.id in read else "-"} | ' f'{"no" if t.pending else "yes, install pending"} | ' f'{unit and last_activity(unit) or "-"} |') - history = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + history = config.comms.goals.todos.history_for_commander(config.incarnation) lines += ['', '## Withdrawn orders', '', *(f'- {line}' for t in history for line in t.order.withdrawals(t))] - open(CommanderConfig.installed().board, 'w').write('\n'.join(lines) + '\n') + open(config.board, 'w').write('\n'.join(lines) + '\n') STAGES = ('tool call', 'review', 'install', 'live', 'user') # where a mistake was caught; later is worse @@ -466,8 +476,9 @@ def on_record(line: str, recorded: str, audience: VerdictAudience) -> None: Core already holds what `recorded` names, so a failed post is reposted, never recorded again. """ - new_file = not os.path.exists(CommanderConfig.installed().narrative) - with open(CommanderConfig.installed().narrative, 'a') as f: + config = CommanderConfig.installed() + new_file = not os.path.exists(config.narrative) + with open(config.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') @@ -487,17 +498,18 @@ def strike(unit: str, task: str, why: str, caught: str, *, #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. + config = CommanderConfig.installed() 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. "{CommanderConfig.installed().review_catch}".') - todo = CommanderConfig.installed().comms.goals.todos.get(task) + sys.exit(f'Refused: --caught must say who caught it and at which stage {STAGES}, e.g. "{config.review_catch}".') + todo = config.comms.goals.todos.get(task) if not todo.state.open or 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, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.held(), f'{why} (caught by {caught})'))) + todo.revision, config.incarnation, todo.held(), f'{why} (caught by {caught})'))) if recorded.todo.revision == todo.revision: return - n = struck(CommanderConfig.installed().comms.goals.todos.get(task), unit) + n = struck(config.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)) @@ -506,7 +518,8 @@ def strike(unit: str, task: str, why: str, caught: str, *, def done(pid, sha, delivered=None, screenshot=''): """Core runs the delivery's check at its checked commit; a late delivered commit is a strike.""" - todo = CommanderConfig.installed().comms.goals.todos.get(pid) + config = CommanderConfig.installed() + todo = config.comms.goals.todos.get(pid) if not todo.state.open or not todo.pending: sys.exit(f'Refused: {pid} has no pending order') unit, repo = todo.assignment.owner, todo.order.check.directory @@ -517,20 +530,21 @@ def done(pid, sha, delivered=None, screenshot=''): 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 = CommanderConfig.installed().comms.registry.require(unit) - if left := [str(t) for t in CommanderConfig.installed().comms.registry.storage_rule.checkouts_of(thread) if pid.lower() in t.name.lower()]: + thread = config.comms.registry.require(unit) + if left := [str(t) for t in config.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: - CommanderConfig.installed().comms.registry.storage_rule.require_quota(thread) + config.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.') delivery = OperationDelivery() if operation else CommitDelivery(commit=sha, delivered_commit=delivered) claim = WorkingClaim(screenshot) if operation else MergedClaim(delivered) call(CommsOrderDoneTool(problem=pid, delivery=delivery, claims=(claim,))) - lateness = CommanderConfig.installed().comms.goals.todos.get(pid).order.delivery.at.lateness(todo.deadline) + lateness = config.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}', CommanderConfig.installed().review_catch) + strike(unit, pid, f'{pid} order {todo.order.id} {lateness} as {delivered}', + config.review_catch) return print(f'{unit}: order {todo.order.id} met as {delivered or sha}') @@ -539,7 +553,8 @@ def done(pid, sha, delivered=None, screenshot=''): def first_calls(): """Each order starts a first-tool-call clock: 2 minutes, separate from its commit deadline. Core records each mark.""" - lines = open(CommanderConfig.installed().activity).read().splitlines()[-4000:] + config = CommanderConfig.installed() + lines = open(config.activity).read().splitlines()[-4000:] watched = {line[9:].split(' ', 1)[0] for line in lines} missing = [] for todo in held_orders(): @@ -552,8 +567,8 @@ def first_calls(): continue event = ObservedFirstCallEvent if acted else NotifiedFirstCallEvent call(CommsTodoTool(id=todo.id, operation=RecordFirstCallTodo( - todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, event))) - if not acted and CommanderConfig.installed().comms.goals.todos.get(todo.id).revision != todo.revision: + todo.revision, config.incarnation, event))) + if not acted and config.comms.goals.todos.get(todo.id).revision != todo.revision: missing.append(f'{unit} (order {order.id}, sent {hhmm(order.at)})') return missing @@ -563,6 +578,7 @@ def first_calls(): def hunt(where, pid, why, sent=None): """A hot-path read with a verdict, recorded on open problem ``pid``; HuntVerb declares its file:line, problem, reason and outcome.""" + config = CommanderConfig.installed() 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') @@ -576,13 +592,14 @@ def hunt(where, pid, why, sent=None): + HuntVerb.usage()) ref, at = FileLine(os.path.realpath(path), int(line)), RecordedEventTime(time.time()) call(CommsTodoTool(id=todo.id, operation=RecordHuntTodo( - todo.revision, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, + todo.revision, config.incarnation, OrderedHunt(ref, why, at, sent) if sent else CleanHunt(ref, why, at)))) print(f'hunt recorded: {where} -> {sent or "clean"}') def hunts(): - history = CommanderConfig.installed().comms.goals.todos.history_for_commander(CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation) + config = CommanderConfig.installed() + history = config.comms.goals.todos.history_for_commander(config.incarnation) return [h for t in history for h in t.hunts] @@ -606,9 +623,10 @@ def next_hunt(): def reply(unit: str, since: float) -> dict | None: """The unit's latest message to me after ``since``, or None.""" - if not os.path.exists(f'{CommanderConfig.installed().root}/bus.jsonl'): + config = CommanderConfig.installed() + if not os.path.exists(f'{config.root}/bus.jsonl'): return None - with open(f'{CommanderConfig.installed().root}/bus.jsonl', 'rb') as f: + with open(f'{config.root}/bus.jsonl', 'rb') as f: f.seek(start := max(0, os.path.getsize(f.name) - 3_000_000)) lines = f.read().splitlines()[bool(start):] latest = None @@ -618,7 +636,8 @@ def reply(unit: str, since: float) -> dict | None: except ValueError: continue to = str(m.get('to')) - if m.get('from') == unit and float(m.get('ts', 0)) > since and (CommanderConfig.installed().commander in to or '#comms' in to): + if (m.get('from') == unit and float(m.get('ts', 0)) > since + and (config.commander in to or '#comms' in to)): latest = m return latest @@ -657,6 +676,7 @@ def install_lines(held: tuple[Todo, ...], read: dict[str, OrderProgress], now: f def overdue(): + config = CommanderConfig.installed() since = 0.0 # previous poll bounds stall crossings; deadline alarms repeat installs: list[str] = [] while True: @@ -683,7 +703,7 @@ def overdue(): 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(CommanderConfig.installed().comms, CommanderConfig.installed().commander, since): + for _ in HuntDue.broken(config.comms, config.commander, since): print(HuntVerb.due(next_hunt()), flush=True) since = now time.sleep(60) @@ -736,25 +756,27 @@ class RuledVerb(CommanderVerb): """HARD RULE: no commander action, mine or a fork's, on rules not re-read in 20 min.""" def gate(self) -> None: + config = CommanderConfig.installed() super().gate() - for due in RulesDue.violations(CommanderConfig.installed().comms, self.who): - flag = '' if self.who == CommanderConfig.installed().commander else f' --as {self.who}' + for due in RulesDue.violations(config.comms, self.who): + flag = '' if self.who == config.commander else f' --as {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 = CommanderConfig.installed().comms.goals.todos.get(task) + config = CommanderConfig.installed() + todo = config.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, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, voided, why))) + todo.revision, config.incarnation, voided, why))) unit = voided.previous.owner - n = struck(CommanderConfig.installed().comms.goals.todos.get(task), unit) + n = struck(config.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)) @@ -782,13 +804,14 @@ 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 = CommanderConfig.installed().comms.goals.todos.get(pid) + config = CommanderConfig.installed() + todo = config.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, CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).incarnation, todo.assignment, why, + todo.revision, config.incarnation, todo.assignment, why, superseded_by))) unit = todo.held().owner - withdrawal, = CommanderConfig.installed().comms.goals.todos.get(pid).order.withdrawals(todo) + withdrawal, = config.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,)), @@ -811,8 +834,9 @@ class LoadedVerb(RuledVerb): """HARD RULE: overloaded means fork, not more orders from me; a fork is never blocked.""" def gate(self) -> None: + config = CommanderConfig.installed() if not self.fork: - capacity.Load.of(CommanderConfig.installed().comms, CommanderConfig.installed().commander).require() + capacity.Load.of(config.comms, config.commander).require() super().gate() @@ -890,7 +914,8 @@ 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(CommanderConfig.installed().comms.root) + config = CommanderConfig.installed() + PlannedInstallDeadline(deadline(self.at), self.installer).plan(config.comms.root) print(f'install due {self.at}, run by {self.installer}') @@ -928,8 +953,9 @@ class PullRequestVerb(OrderVerb, declared_name="pr"): body_file: str = option("--body-file") def run(self) -> None: - todo = CommanderConfig.installed().comms.goals.todos.get(self.pid) - checkout = CommanderConfig.installed().comms.registry.storage_rule.order_checkout(todo.holder, todo.id) + config = CommanderConfig.installed() + todo = config.comms.goals.todos.get(self.pid) + checkout = config.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())) diff --git a/src/agent_comms/commander/request.py b/src/agent_comms/commander/request.py index 5256fc0de0..698579b7ab 100644 --- a/src/agent_comms/commander/request.py +++ b/src/agent_comms/commander/request.py @@ -22,9 +22,11 @@ def open_requests() -> tuple[tuple[Goal, tuple[Todo, ...]], ...]: """The commander's open owner requests and their undelivered orders.""" - thread = CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander) + config = CommanderConfig.installed() + thread = config.comms.registry.require(config.commander) return tuple(request for goal in thread.goal.active() - for request in goal.order.open_requests(goal, CommanderConfig.installed().comms.goals, thread.incarnation)) + for request in goal.order.open_requests(goal, config.comms.goals, + thread.incarnation)) class RequestVerb(CommanderVerb): @@ -37,7 +39,9 @@ class RequestAddVerb(RequestVerb, RuledVerb, declared_name="add"): text: str = option("text") def run(self) -> None: - goal = CommanderConfig.installed().comms.goals.update_goal(CommanderConfig.installed().commander, SetGoalAction(text=self.text, order=RequestGoalOrder())) + config = CommanderConfig.installed() + goal = config.comms.goals.update_goal( + config.commander, SetGoalAction(text=self.text, order=RequestGoalOrder())) print(goal.require().id) @@ -48,8 +52,10 @@ class RequestCompleteVerb(RequestVerb, RuledVerb, declared_name="complete"): why: str = option("--why") def run(self) -> None: + config = CommanderConfig.installed() expect = GoalPrecondition(goal_id=self.id, expected_state=ActiveGoal()) - CommanderConfig.installed().comms.goals.update_goal(CommanderConfig.installed().commander, CompletedGoalAction(progress=self.why, expect=expect)) + config.comms.goals.update_goal( + config.commander, CompletedGoalAction(progress=self.why, expect=expect)) print(f"{self.id} done") diff --git a/src/agent_comms/commander/rotate.py b/src/agent_comms/commander/rotate.py index 3488a3e0a6..0a49972d70 100644 --- a/src/agent_comms/commander/rotate.py +++ b/src/agent_comms/commander/rotate.py @@ -23,6 +23,7 @@ def owed_orders() -> dict[str, list[Todo]]: def main(a): + config = CommanderConfig.installed() turn, since = 0, 0.0 # since: the previous wake; a rule that stays broken is said once while True: now = time.time() @@ -34,7 +35,7 @@ def main(a): read = order.progress() orders = ', '.join(f"{t.id} due {order.hhmm(t.deadline)} ({read[t.id].line})" for t in owed[unit]) - edits = judge.Edit.unjudged(CommanderConfig.installed().comms, CommanderConfig.installed().commander) + edits = judge.Edit.unjudged(config.comms, config.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): diff --git a/src/agent_comms/commander/rules.py b/src/agent_comms/commander/rules.py index 4e7939af91..09778b79c3 100644 --- a/src/agent_comms/commander/rules.py +++ b/src/agent_comms/commander/rules.py @@ -34,8 +34,9 @@ def principles() -> list[str]: def rules() -> list[str]: - found = [m.group(1).strip() for m in re.finditer(r'^\s*- \*\*(.+?)\*\*', open(CommanderConfig.installed().skill).read(), re.M)] - for path in [path for directory in CommanderConfig.installed().rules for path in sorted(glob.glob(f'{directory}/*.md'))]: + config = CommanderConfig.installed() + found = [m.group(1).strip() for m in re.finditer(r'^\s*- \*\*(.+?)\*\*', open(config.skill).read(), re.M)] + for path in [path for directory in config.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) @@ -44,11 +45,13 @@ def rules() -> list[str]: def record(who: str, entry: RuleReviewEntry) -> None: - read: StandingRulesRead = ThreadCaller(who).invoke(CommsRuleReviewTool(entry=entry), CommanderConfig.installed().comms) + read: StandingRulesRead = ThreadCaller(who).invoke( + CommsRuleReviewTool(entry=entry), CommanderConfig.installed().comms) print('\n'.join(read.broken) or 'review complete; commander actions allowed') def review(who: str): + config = CommanderConfig.installed() every = rules() for i, r in enumerate(every, 1): print(f'{i:3}. {r}') @@ -56,12 +59,12 @@ def review(who: str): 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 != CommanderConfig.installed().commander else random.sample(paper, 2) + random.sample(every, DRAWN - 2) + drawn = random.sample(paper, DRAWN) if who != config.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 != CommanderConfig.installed().commander else 2)) + record(who, DrawnRuleReviewEntry(tuple(drawn), len(drawn) if who != config.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}') @@ -72,7 +75,8 @@ def answer(n: int, text: str, who: str): def main(a): - who = CommanderConfig.installed().commander + config = CommanderConfig.installed() + who = config.commander if '--as' in a: i = a.index('--as'); who = a[i + 1]; del a[i:i + 2] try: @@ -81,7 +85,7 @@ def main(a): elif a[:1] == ['answer'] and len(a) == 3 and a[1].isdigit(): answer(int(a[1]), a[2], who) elif a == ['stale']: - print('\n'.join(due.why for due in RulesDue.violations(CommanderConfig.installed().comms, who)) or 'fresh') + print('\n'.join(due.why for due in RulesDue.violations(config.comms, who)) or 'fresh') else: sys.exit(__doc__) except ValueError as refused: # Core owns what a review and an answer may say diff --git a/src/agent_comms/commander/send_guard.py b/src/agent_comms/commander/send_guard.py index e08704b9b1..ea8c9cbaa7 100644 --- a/src/agent_comms/commander/send_guard.py +++ b/src/agent_comms/commander/send_guard.py @@ -24,8 +24,8 @@ from ..declared_family import DeclaredFamily from ..errors import RelationViolationError from ..todos import InstallDeadline -from .config import CommanderConfig from . import order +from .config import CommanderConfig Words = tuple[str, ...] @@ -50,8 +50,9 @@ class CommanderSession: session: str def commander(self) -> bool: - process = CommanderConfig.installed().comms.registry.require(CommanderConfig.installed().commander).process_identity.require( - f'{CommanderConfig.installed().commander} has no registered process to compare this session with') + config = CommanderConfig.installed() + process = config.comms.registry.require(config.commander).process_identity.require( + f'{config.commander} has no registered process to compare this session with') environment = Path(f'/proc/{process.pid}/environ').read_bytes().split(b'\0') return f'CLAUDE_CODE_SESSION_ID={self.session}'.encode() in environment @@ -117,7 +118,8 @@ def ordered(shape: Words) -> bool: @classmethod def refusal(cls) -> str: - return InstallDeadline.read(CommanderConfig.installed().comms.root).refusal(cls.work, CommanderConfig.installed().commander) + config = CommanderConfig.installed() + return InstallDeadline.read(config.comms.root).refusal(cls.work, config.commander) @dataclass(frozen=True) diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index bbc3d01414..b5a022783b 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -223,7 +223,8 @@ def merges(cls, repo: Repo) -> Iterator[MergeGrowth]: @classmethod def violations(cls, comms: Comms, commander: str) -> tuple[MergeGrowth, ...]: - return tuple(growth for repo in CommanderConfig.installed().repos for growth in cls.merges(repo) if growth.over) + config = CommanderConfig.installed() + return tuple(growth for repo in config.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 385df4e81a..e6cc2742fe 100644 --- a/src/agent_comms/commander/tool_watch.py +++ b/src/agent_comms/commander/tool_watch.py @@ -60,8 +60,9 @@ def enforce_storage(registry: Registration, thread: str, call: CodingCall) -> No violation = str(denied) print(f'VIOLATION storage {thread}: {violation}', flush=True) for todo in order.held_by(thread)[:1]: - order.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', + order.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) @@ -112,14 +113,15 @@ def walk(registry: Registration, turns: dict[str, list[str]], thread: str, def main(a): - registry = Registration(Path(CommanderConfig.installed().root) / 'registry.json') + config = CommanderConfig.installed() + registry = Registration(Path(config.root) / 'registry.json') 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'{CommanderConfig.installed().root}/native-sessions/*/*.jsonl')) | set(names): + for path in set(glob.glob(f'{config.root}/native-sessions/*/*.jsonl')) | set(names): try: st = os.stat(path) except OSError: From 1e6e032b24debaa402ea067534371e2569f83132 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 00:57:31 -0400 Subject: [PATCH 12/13] P196 declare the five seeded patterns P197 to P201 join Seed patterns 1 (report instead of state, P198), 4 (wrong measure, P197), 15 (commander overloaded, P199), 16 (dead provider, P200) and 18 (unowned work, P201) are members with their seed ID as a SeedCatalog source. Each holds a PendingCheck naming the open problem whose change adds its check, so those changes join an existing member. Src-Budget: 419 Co-Authored-By: Claude Opus 5.5 --- docs/agents/behaviour-catalog.md | 52 ++++++++++++++++++++++- src/agent_comms/behaviour_patterns.py | 61 +++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/docs/agents/behaviour-catalog.md b/docs/agents/behaviour-catalog.md index ee2f7655f2..eb59a03280 100644 --- a/docs/agents/behaviour-catalog.md +++ b/docs/agents/behaviour-catalog.md @@ -10,7 +10,7 @@ The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/ - [Failure 1](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It rewrote the framework instead of questioning its own design: no pattern checks it yet - [Failure 2](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It added states instead of removing them: no pattern checks it yet - [Failure 3](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It wrote down the right principle and then violated it: no pattern checks it yet -- [Failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It measured the wrong thing and reported it as progress: `uncounted_check` +- [Failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It measured the wrong thing and reported it as progress: `uncounted_check`, `wrong_measure` - [Failure 5](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It redefined "the real app" until something passed: no pattern checks it yet - [Failure 6](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It ignored direct orders: no pattern checks it yet - [Failure 7](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It described its intentions as its accomplishments: `board_disagrees`, `uncounted_check` @@ -113,3 +113,53 @@ Checked by: - verb refusal in `agent_comms.commander.order.hunt` Learned from: P194 (b26843727); [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Strikes, then fire." + +## report_instead_of_state + +A strike or send-back whose reason cites no state the record can check. + +Checked by: + +- none yet: P198 (#1744) adds it + +Learned from: seed catalog pattern 1 (claude-ai message 1c8729ba0ed7) + +## wrong_measure + +A done report that names its own measure instead of what its order's Check printed. + +Checked by: + +- none yet: P197 (#1733) adds it + +Learned from: seed catalog pattern 4 (claude-ai message 1c8729ba0ed7); [Codex postmortem, failure 4](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) + +## commander_overloaded + +The commander's unread backlog is over the limit while no second commander is forked to share it. + +Checked by: + +- none yet: P199 (branch cn-phase-0/P199) adds it + +Learned from: seed catalog pattern 15 (claude-ai message 1c8729ba0ed7) + +## dead_provider + +A unit's latest turn ended in a usage or credit limit on its own model and the unit still runs on that model. + +Checked by: + +- none yet: P200 (#1746) adds it + +Learned from: seed catalog pattern 16 (claude-ai message 1c8729ba0ed7) + +## unowned_work + +Agent-comms work runs under no open order, or on a runtime that publication replaced. + +Checked by: + +- none yet: P201 (#1739) adds it + +Learned from: seed catalog pattern 18 (claude-ai message 1c8729ba0ed7) diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index 4554056dca..4565e06696 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -63,6 +63,17 @@ class ToolRefusal(RefusalCheck): surface = "tool refusal" +@dataclass(frozen=True) +class PendingCheck(PatternCheck): + """The open problem whose change adds the pattern's check; that change replaces this.""" + + problem: str + change: str + + def cite(self) -> str: + return f"none yet: {self.problem} ({self.change}) adds it" + + class PatternSource(ABC): """Where a pattern was learned.""" @@ -147,6 +158,16 @@ def cite(self) -> str: return f"{self.problem} ({self.commit})" +@dataclass(frozen=True) +class SeedCatalog(PatternSource): + """A pattern's seed ID in claude-ai's numbered catalog (message 1c8729ba0ed7).""" + + seed: int + + def cite(self) -> str: + return f"seed catalog pattern {self.seed} (claude-ai message 1c8729ba0ed7)" + + @dataclass(frozen=True) class Commit(PatternSource): commit: str @@ -296,5 +317,45 @@ class StrikeOnNamedProblemPattern(BehaviourPattern): sources = (ProblemFix("P194", "b26843727"), CommanderRule("Strikes, then fire.")) +class ReportInsteadOfStatePattern(BehaviourPattern): + signature = "A strike or send-back whose reason cites no state the record can check." + checks = (PendingCheck("P198", "#1744"),) + sources = (SeedCatalog(1),) + + +class WrongMeasurePattern(BehaviourPattern): + signature = ( + "A done report that names its own measure instead of what its order's Check printed." + ) + checks = (PendingCheck("P197", "#1733"),) + sources = (SeedCatalog(4), MeasuredWrongThingFailure()) + + +class CommanderOverloadedPattern(BehaviourPattern): + signature = ( + "The commander's unread backlog is over the limit while no second commander is forked" + " to share it." + ) + checks = (PendingCheck("P199", "branch cn-phase-0/P199"),) + sources = (SeedCatalog(15),) + + +class DeadProviderPattern(BehaviourPattern): + signature = ( + "A unit's latest turn ended in a usage or credit limit on its own model and the unit" + " still runs on that model." + ) + checks = (PendingCheck("P200", "#1746"),) + sources = (SeedCatalog(16),) + + +class UnownedWorkPattern(BehaviourPattern): + signature = ( + "Agent-comms work runs under no open order, or on a runtime that publication replaced." + ) + checks = (PendingCheck("P201", "#1739"),) + sources = (SeedCatalog(18),) + + if __name__ == "__main__": print(BehaviourPattern.catalog(), end="") From 2dc763a1574b79bbd6fe94b4467ef817505a6d13 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 13:44:13 -0400 Subject: [PATCH 13/13] P196 finish declaration-owned catalog against current Core checks Remove the catalog's commander verb imports and cite the existing check owners directly. Derive postmortem coverage without a names roster. Keep the five seed declarations and distinguish their missing enforcement, including the two closed unmerged implementations. Verify regeneration without commander configuration and reject hand-edited output. --- docs/agents/behaviour-catalog.md | 4 ++-- src/agent_comms/behaviour_patterns.py | 6 +++--- tests/test_behaviour_catalog.py | 24 ++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/agents/behaviour-catalog.md b/docs/agents/behaviour-catalog.md index 0b24d3e8fa..be48d160b6 100644 --- a/docs/agents/behaviour-catalog.md +++ b/docs/agents/behaviour-catalog.md @@ -7,7 +7,7 @@ Each behaviour failure pattern is declared once in Core with its signature (what A pending check records a known gap, not enforcement. Adding a pattern means adding one `BehaviourPattern` subclass; membership and this document derive from those classes. `tests/test_behaviour_catalog.py` rejects hand edits. -The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) names these failures; each pattern cites the ones it checks: +The [Codex postmortem](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) names these failures; coverage below derives from the patterns that cite them: - [Failure 1](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It rewrote the framework instead of questioning its own design: no pattern cites it yet - [Failure 2](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md), It added states instead of removing them: no pattern cites it yet @@ -56,7 +56,7 @@ A commander order, strike, roast or directive whose words outside cited spans ma Checked by: -- check in `agent_comms.todos.OrderWords.refused` +- check in `agent_comms.todos.PlainText` Learned from: [comms-commander rule](../../src/agent_comms/skills/comms-commander/SKILL.md) "Hate jargon."; [Codex postmortem, failure 8](https://github.com/OpenHCSDev/toad/blob/main/docs/agents/CODEX-POSTMORTEM.md) diff --git a/src/agent_comms/behaviour_patterns.py b/src/agent_comms/behaviour_patterns.py index efa1c2c22a..6abd717c28 100644 --- a/src/agent_comms/behaviour_patterns.py +++ b/src/agent_comms/behaviour_patterns.py @@ -204,8 +204,8 @@ def catalog(cls) -> str: "A pending check records a known gap, not enforcement. Adding a pattern means" " adding one `BehaviourPattern` subclass; membership and this document derive" " from those classes. `tests/test_behaviour_catalog.py` rejects hand edits.\n\n" - f"The [Codex postmortem]({POSTMORTEM}) names these failures; each pattern cites the" - " ones it checks:\n\n" + f"The [Codex postmortem]({POSTMORTEM}) names these failures; coverage below derives" + " from the patterns that cite them:\n\n" f"{failures}\n" + "\n".join(member.section() for member in members) ) @@ -241,7 +241,7 @@ class PlainTextPattern(BehaviourPattern): "A commander order, strike, roast or directive whose words outside cited spans match" " the declared jargon list in `PlainText`." ) - checks = (CallableCheck(PlainText.refused),) + checks = (CallableCheck(PlainText),) sources = (CommanderRule("Hate jargon."), CeremonyFailure()) diff --git a/tests/test_behaviour_catalog.py b/tests/test_behaviour_catalog.py index 740d7f49d1..5694fe411e 100644 --- a/tests/test_behaviour_catalog.py +++ b/tests/test_behaviour_catalog.py @@ -1,8 +1,11 @@ """The behaviour catalog in docs/agents is the family's output, never a hand edit.""" +import os +import subprocess +import sys from pathlib import Path -from agent_comms.behaviour_patterns import BehaviourPattern +from agent_comms.behaviour_patterns import BehaviourPattern, PatternCheck, PatternSource ROOT = Path(__file__).resolve().parents[1] @@ -11,5 +14,22 @@ def test_behaviour_catalog_members_name_existing_checks_and_generate_the_file(): members = BehaviourPattern.members_with(BehaviourPattern) assert members for member in members: - assert member.checks and member.sources, member.declared_name + assert member.__dict__["signature"].strip(), member + assert member.__dict__["checks"] and member.__dict__["sources"], member + assert all(isinstance(check, PatternCheck) and check.cite() for check in member.checks) + assert all(isinstance(source, PatternSource) and source.cite() for source in member.sources) assert (ROOT / "docs/agents/behaviour-catalog.md").read_text() == BehaviourPattern.catalog() + + +def test_catalog_command_needs_no_commander_installation_or_bus(tmp_path): + generated = subprocess.run( + [sys.executable, "-X", "faulthandler", "-m", "agent_comms.behaviour_patterns"], + cwd=tmp_path, + env=os.environ | {"HOME": str(tmp_path), "PYTHONPATH": str(ROOT / "src")}, + capture_output=True, + text=True, + timeout=30, + ) + assert generated.returncode == 0, generated.stderr + assert generated.stdout == BehaviourPattern.catalog() + assert not tuple(tmp_path.iterdir())