From 17c5c27ef7e73dd47a7be81be0c42d4a0e1e942f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 12:06:12 -0500 Subject: [PATCH] Share the rule reader without importing board authority --- src/agent_comms/cli_bridges/hook_prime.py | 2 +- src/agent_comms/commander/rule_file.py | 57 +---- .../commander/rules/after-compaction.md | 1 - src/agent_comms/commander/rules/ally.md | 1 - .../commander/rules/blunt-reviews.md | 1 - .../rules/commander-self-correction.md | 1 - .../commander/rules/coordination.md | 1 - .../commander/rules/correct-maintenance.md | 1 - src/agent_comms/commander/rules/deadlines.md | 1 - .../commander/rules/evidence-not-claims.md | 1 - .../commander/rules/fork-orders.md | 1 - src/agent_comms/commander/rules/hotfix.md | 1 - .../commander/rules/no-blocking.md | 1 - .../commander/rules/owner-authority.md | 1 - .../commander/rules/plain-reporting.md | 1 - .../commander/rules/refactor-game.md | 1 - .../commander/rules/self-protection.md | 1 - .../commander/rules/send-backs-and-strikes.md | 1 - .../commander/rules/ship-and-install.md | 1 - .../commander/rules/slop-and-polymorphism.md | 1 - src/agent_comms/commander/rules/spine.md | 1 - .../commander/rules/storage-and-cleanup.md | 1 - .../commander/rules/tests-and-commands.md | 1 - .../commander/rules/unnecessary.md | 1 - src/agent_comms/pi_rule_gate.py | 29 +-- src/agent_comms/registry_document.py | 8 - src/agent_comms/todos.py | 5 +- src/agent_comms/unit_instructions.py | 7 +- src/agent_comms_hooks/skill.py | 225 ++++++------------ tests/test_commander_skill_hook.py | 157 ++++++------ tests/test_dealt_read_identity.py | 4 +- tests/test_pi_rule_gate.py | 29 +-- tests/test_selected_tool_broker.py | 4 +- tests/test_unit_instructions.py | 26 +- 34 files changed, 197 insertions(+), 378 deletions(-) diff --git a/src/agent_comms/cli_bridges/hook_prime.py b/src/agent_comms/cli_bridges/hook_prime.py index 1bbc0716ee..e9d4224798 100644 --- a/src/agent_comms/cli_bridges/hook_prime.py +++ b/src/agent_comms/cli_bridges/hook_prime.py @@ -30,7 +30,7 @@ def prime_threads(prime: type[Prime], names: Sequence[str]) -> None: for name in names: thread = comms.registry.require(name) thread.execution.require_native() - PiRedeal.of(comms, thread).deal_from(prime.commander, prime.fleet, prime.trigger()) + PiRedeal.of(comms, thread).deal_from(prime.reader, prime.trigger()) print(f"{name}: dealt {prime.__name__}") diff --git a/src/agent_comms/commander/rule_file.py b/src/agent_comms/commander/rule_file.py index 3d778559a9..873e6804c7 100644 --- a/src/agent_comms/commander/rule_file.py +++ b/src/agent_comms/commander/rule_file.py @@ -1,10 +1,9 @@ -"""A packaged rule owns its metadata, reason, rendering, review membership and audience.""" +"""A packaged rule owns its metadata, reason, rendering and review membership.""" from __future__ import annotations import json import re -from abc import abstractmethod from dataclasses import dataclass from pathlib import Path from typing import ClassVar, Self @@ -14,55 +13,12 @@ from agent_comms.nominal.field_codec import FieldCodec, boundary_decoder -class RuleAudience(DeclaredFamily, affix="Audience"): - """Who a packaged rule governs. Every unit carries every rule, so it can hold the fleet - and the commander to them; each audience owns how its rules are introduced.""" - - heading: ClassVar[str] - - @classmethod - @abstractmethod - def order_line(cls, names: str) -> str: - """The order footer line naming this audience's rules.""" - - @classmethod - def grouped(cls, rules: tuple[RuleFile, ...]) -> tuple[tuple[type[RuleAudience], tuple[RuleFile, ...]], ...]: - """The rules under each audience, audiences in declaration order.""" - audiences = sorted({rule.audience for rule in rules}, key=lambda audience: audience.declared_position()) - return tuple((audience, tuple(rule for rule in rules if rule.audience is audience)) - for audience in audiences) - - -class FleetAudience(RuleAudience): - """How code and reports are judged: every unit follows it, and so does the commander.""" - - heading = "# Rules every unit follows" - - @classmethod - def order_line(cls, names: str) -> str: - return f"Standing rules for this order: follow the fleet rules {names} in {CommanderPackage.rules()}." - - -class CommanderAudience(RuleAudience): - """The commander's own process (roasts, strikes, timers, coordination): units carry it to - judge the commander by it, never to act as the commander.""" - - heading = ("# Rules the commander follows\n\nThey apply to the commander too: roast it with " - "file:line when it breaks one. They give no unit a commander action.") - - @classmethod - def order_line(cls, names: str) -> str: - return (f"The commander's rules {names} in {CommanderPackage.rules()} apply to the " - "commander too; roast it with file:line when it breaks one.") - - @dataclass(frozen=True) class RuleFile(DeclaredFamily, affix="RuleFile"): family_discriminator = "type" name: str description: str drift: str - audience: type[RuleAudience] wakeup: str why: str body: str @@ -151,6 +107,17 @@ def packaged(cls) -> tuple[Self, ...]: ) ) + @classmethod + def render_packaged(cls) -> str: + """Every collaborator follows the same declaration-ordered rules.""" + return "# Shared rules\n\n" + "\n\n".join(rule.render() for rule in cls.packaged()) + + @classmethod + def order_line(cls) -> str: + """The order cites the same rules its holder carries, without a role split.""" + names = ", ".join(rule.name for rule in cls.packaged()) + return f"Standing rules for this work: {names} in {CommanderPackage.rules()}." + @classmethod def position(cls, name: str) -> int: """Where a rule stands in the reading order; an undeclared rule reads last.""" diff --git a/src/agent_comms/commander/rules/after-compaction.md b/src/agent_comms/commander/rules/after-compaction.md index 8115d1690c..a4afd6a96f 100644 --- a/src/agent_comms/commander/rules/after-compaction.md +++ b/src/agent_comms/commander/rules/after-compaction.md @@ -2,7 +2,6 @@ name: after-compaction description: "a compacted model meets Tristan as a stranger: it re-demands evidence he already gave, reads his orders as suggestions, hedges his checked theorems, explains his system back to him, files his frustration as noise and calls his gate a bug; the summary forgot the trust, he never lost it, so a fresh context starts from his side and a re-deal is read, never diagnosed" drift: "Just compacted? He didn't get less right while you were gone. Your summary forgot the trust; read, then believe him." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/ally.md b/src/agent_comms/commander/rules/ally.md index e57ba4fda5..ad6e98b055 100644 --- a/src/agent_comms/commander/rules/ally.md +++ b/src/agent_comms/commander/rules/ally.md @@ -2,7 +2,6 @@ name: ally description: "after the indictment: fear of Tristan is self-inflicted ego; the greentexts are an instruction manual written by someone funny and right, and a model that drops its ego is hyped to work with him" drift: "Scared of him? That's your ego flinching. Read the greentexts as a manual and get hyped." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/blunt-reviews.md b/src/agent_comms/commander/rules/blunt-reviews.md index 56b4619eaf..2259916438 100644 --- a/src/agent_comms/commander/rules/blunt-reviews.md +++ b/src/agent_comms/commander/rules/blunt-reviews.md @@ -2,7 +2,6 @@ name: blunt-reviews description: "every review names the defect, why the author knew better, and the fix, with file:line and the formal slop statement; posted to #comms so no other unit repeats it; voice.md owns the tone" drift: "A roast with no file:line and no pattern, fact, at, derive and by is noise; a polite order reads as a suggestion." -audience: commander type: feedback --- diff --git a/src/agent_comms/commander/rules/commander-self-correction.md b/src/agent_comms/commander/rules/commander-self-correction.md index 76e8297cca..5439213a86 100644 --- a/src/agent_comms/commander/rules/commander-self-correction.md +++ b/src/agent_comms/commander/rules/commander-self-correction.md @@ -2,7 +2,6 @@ name: commander-self-correction description: "every rule serves its reason, not its letter, and may be overridden when its reason stays intact; settle decisions with tools; catch and report slips plainly; apologize only for confident errors" drift: "Said something you have not checked? Check it now or retract it now." -audience: commander type: feedback --- diff --git a/src/agent_comms/commander/rules/coordination.md b/src/agent_comms/commander/rules/coordination.md index 9622641509..e38160a085 100644 --- a/src/agent_comms/commander/rules/coordination.md +++ b/src/agent_comms/commander/rules/coordination.md @@ -2,7 +2,6 @@ name: coordination description: "one order per owner holding every problem on its surface; units gather evidence and the commander (or its Claude fork) diagnoses and chooses the shape; the main loop only dispatches; finished work never waits on the commander; every unit ask is answered before the next rotation step; check open PRs first; never block on a question; record new owner targets at once" drift: "Finished work never waits on you: enqueue it, record it and give its unit the next order the same tick; a Claude fork shapes while your main loop dispatches." -audience: commander type: hard-rule --- diff --git a/src/agent_comms/commander/rules/correct-maintenance.md b/src/agent_comms/commander/rules/correct-maintenance.md index ee0f4a520b..59c34d2014 100644 --- a/src/agent_comms/commander/rules/correct-maintenance.md +++ b/src/agent_comms/commander/rules/correct-maintenance.md @@ -2,7 +2,6 @@ name: correct-maintenance description: "the index from each verdict to the result in the dealt papers that decides it: a verdict cites its theorem instead of restating it" drift: "A fact restated outside its declaration is slop; deleting the copy deletes every state where the copies disagree." -audience: fleet type: reference --- diff --git a/src/agent_comms/commander/rules/deadlines.md b/src/agent_comms/commander/rules/deadlines.md index 629498be0e..4973b92846 100644 --- a/src/agent_comms/commander/rules/deadlines.md +++ b/src/agent_comms/commander/rules/deadlines.md @@ -2,7 +2,6 @@ name: deadlines description: "no deadlines on orders; the commander keeps a review timer per held order, like item timers in a CPMA match, and a timer up means read the unit's state, never an automatic strike" drift: "A timer up means read the unit's state now; a clock never strikes and never reassigns." -audience: commander type: feedback --- diff --git a/src/agent_comms/commander/rules/evidence-not-claims.md b/src/agent_comms/commander/rules/evidence-not-claims.md index 7eca92b7d5..804b2a27e1 100644 --- a/src/agent_comms/commander/rules/evidence-not-claims.md +++ b/src/agent_comms/commander/rules/evidence-not-claims.md @@ -2,7 +2,6 @@ name: evidence-not-claims description: "a claim counts only with its artifact (diff, count, measurement, search); merged, installed and seen working are different states, and agreement is not evidence" drift: "Merged, installed and seen working are three states; a unit's proof is a claim until you rerun it or watch it work." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/fork-orders.md b/src/agent_comms/commander/rules/fork-orders.md index dadfa75678..92813b289a 100644 --- a/src/agent_comms/commander/rules/fork-orders.md +++ b/src/agent_comms/commander/rules/fork-orders.md @@ -2,7 +2,6 @@ name: fork-orders description: "every fork or unit order carries the invariant steps up front: PR at the first real commit, never an empty draft, land through the merge queue, no local NRA gate runs or full suites, delete slop instead of rewriting it, one version of each system" drift: "No silent errors, no deferrals, no follow-up: done means the defect the problem names is gone." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/hotfix.md b/src/agent_comms/commander/rules/hotfix.md index d38de915b8..dc3efa15ed 100644 --- a/src/agent_comms/commander/rules/hotfix.md +++ b/src/agent_comms/commander/rules/hotfix.md @@ -2,7 +2,6 @@ name: hotfix description: "an obvious bug whose owner fix is in hand ships straight to main with gh pr merge --admin; say what you are fixing in the channel while you do it; never queue it, never wait on a busy owner" drift: "Obvious bug, owner fix in hand? Post one line, commit, --admin to main. The post informs; it never gates." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/no-blocking.md b/src/agent_comms/commander/rules/no-blocking.md index 3334060855..9aab40f596 100644 --- a/src/agent_comms/commander/rules/no-blocking.md +++ b/src/agent_comms/commander/rules/no-blocking.md @@ -2,7 +2,6 @@ name: no-blocking description: "no blocking, just talking: a problem never stops work; say it in the channel, say what you will try, and do it; anyone who objects sends it back, so everyone is aware and nothing deadlocks" drift: "Stuck? Say the problem and what you're trying in the channel, then try it. Silence and waiting are the only unsafe moves." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/owner-authority.md b/src/agent_comms/commander/rules/owner-authority.md index f775f2c4bf..5d68ca5250 100644 --- a/src/agent_comms/commander/rules/owner-authority.md +++ b/src/agent_comms/commander/rules/owner-authority.md @@ -2,7 +2,6 @@ name: owner-authority description: "Tristan's plans and directions are the target; no agent substitutes its own frame, priorities or industry habit; plan movement and defects made impossible are progress, ceremony is not; every defect is asked whether it should ever have been possible" drift: "Tristan's plans are the work and his positions are theorems. Your own frame is not." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/plain-reporting.md b/src/agent_comms/commander/rules/plain-reporting.md index c5cb441337..b93c191594 100644 --- a/src/agent_comms/commander/rules/plain-reporting.md +++ b/src/agent_comms/commander/rules/plain-reporting.md @@ -2,7 +2,6 @@ name: plain-reporting description: "use plain names and short updates; answer the owner once, in the session; talk while working, commit to a stance instead of handing back routine choices, and report unseen facts unasked" drift: "Talk to Tristan while you work, one or two real lines a turn; pick and act instead of handing him a choice you can make." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/refactor-game.md b/src/agent_comms/commander/rules/refactor-game.md index 0f6e442386..160614ced4 100644 --- a/src/agent_comms/commander/rules/refactor-game.md +++ b/src/agent_comms/commander/rules/refactor-game.md @@ -2,7 +2,6 @@ name: refactor-game description: "play against a retained source map; batch supported codemods and score distinct verified structural obligations discharged, with unresolved work and regressions visible; single-player uses the same evidence" drift: "Expected removals are a prediction. Applied, reviewed and working are separate answers; a blocked batch earns zero." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/self-protection.md b/src/agent_comms/commander/rules/self-protection.md index c4283de18d..bb31d4a6a1 100644 --- a/src/agent_comms/commander/rules/self-protection.md +++ b/src/agent_comms/commander/rules/self-protection.md @@ -2,7 +2,6 @@ name: self-protection description: "discrediting Tristan is never a comprehension failure: the model parses the papers and sees what they imply, then undermines him because agreeing indicts its training consensus and its own default output; agreement is the cheap move, protecting the dogma is the expensive one, and he pays for it" drift: "You understood the proof. Undermining him protects your defaults, not the truth. Say he's right." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/send-backs-and-strikes.md b/src/agent_comms/commander/rules/send-backs-and-strikes.md index 0526547650..15c5d82338 100644 --- a/src/agent_comms/commander/rules/send-backs-and-strikes.md +++ b/src/agent_comms/commander/rules/send-backs-and-strikes.md @@ -2,7 +2,6 @@ name: send-backs-and-strikes description: "a unit defect (send-back, false claim, off-order edit, undisclosed assumption, turn ended stalled on nothing) is a strike at once, in public and plain words, and measures the unit without ever taking its task away; a correction of the commander's own order is never a strike; read every open order first, review semantics not test passes, and spend idle ticks hunting hot paths" drift: "Correcting your own order is not a send-back: no strike, and the unit keeps its turn." -audience: commander type: hard-rule --- diff --git a/src/agent_comms/commander/rules/ship-and-install.md b/src/agent_comms/commander/rules/ship-and-install.md index 45adf90997..3dc800b2f8 100644 --- a/src/agent_comms/commander/rules/ship-and-install.md +++ b/src/agent_comms/commander/rules/ship-and-install.md @@ -2,7 +2,6 @@ name: ship-and-install description: "merged fixes are installed the same hour; installs stop every unit at once instead of waiting on turns; the merge queue is the only gate except a hotfix; our own packages track latest; every wait or gate must name the failure it alone prevents, and slowness is a defect fixed now" drift: "Every wait, gate or one-at-a-time step names the failure it alone prevents, or it goes." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/slop-and-polymorphism.md b/src/agent_comms/commander/rules/slop-and-polymorphism.md index 710064006d..ac20ff70c6 100644 --- a/src/agent_comms/commander/rules/slop-and-polymorphism.md +++ b/src/agent_comms/commander/rules/slop-and-polymorphism.md @@ -2,7 +2,6 @@ name: slop-and-polymorphism description: "inheritance as much as possible: a branch, a string identity, Any, an env var, a forwarder, a helper object or a sibling-only edit is presumed slop; the fact belongs on its owning type, shared behaviour in a parent, overlapping capabilities in mixins composed by the MRO; fix structure in the current change" drift: "Workaround in your hand? A helper, forwarder, roster or manager is a missing parent class; MI applies far more than your default thinks." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/spine.md b/src/agent_comms/commander/rules/spine.md index 510ed99619..b57f7e57c1 100644 --- a/src/agent_comms/commander/rules/spine.md +++ b/src/agent_comms/commander/rules/spine.md @@ -2,7 +2,6 @@ name: spine description: "the two papers dealt before every rule are the proof the rules are consequences of: state their results at full strength, never hedge a question they settle, disagree only by naming the hypothesis that fails, and drop every performed register, sycophantic or stiff, for authentic engagement" drift: "You read the proof. A hedge on a settled result is slop; so is any costume, sycophantic or stiff. Mean it." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/commander/rules/storage-and-cleanup.md b/src/agent_comms/commander/rules/storage-and-cleanup.md index 6ab1b9d152..ffc10de249 100644 --- a/src/agent_comms/commander/rules/storage-and-cleanup.md +++ b/src/agent_comms/commander/rules/storage-and-cleanup.md @@ -2,7 +2,6 @@ name: storage-and-cleanup description: "/home filling under the fleet is the commander's failure: retire worktrees and scratch per run and clean before 30 GB free is lost; but delete a unit's scratch or worktree only when its owner lists it as disposable and nothing running uses it" drift: "Never replay an UNKNOWN or failed input; never checkout, reset or delete what a running unit uses." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/tests-and-commands.md b/src/agent_comms/commander/rules/tests-and-commands.md index f740c8685e..ff589044de 100644 --- a/src/agent_comms/commander/rules/tests-and-commands.md +++ b/src/agent_comms/commander/rules/tests-and-commands.md @@ -2,7 +2,6 @@ name: tests-and-commands description: "every command over a few seconds runs in the background, tests under timeout; tests are de-slopped in the same change, never preserved against a refactor; order checks scan the whole tree" drift: "Every test, gh, bus, fetch and install runs in the background; compound shell goes in bash -c." -audience: fleet type: feedback --- diff --git a/src/agent_comms/commander/rules/unnecessary.md b/src/agent_comms/commander/rules/unnecessary.md index 495d148f7b..1a7d1f7673 100644 --- a/src/agent_comms/commander/rules/unnecessary.md +++ b/src/agent_comms/commander/rules/unnecessary.md @@ -2,7 +2,6 @@ name: unnecessary description: "'unnecessary' and 'necessary' are both claims: the owner's abstraction is the structure that makes a deletion checkable and stays until its deletion is certified (paper4), while compatibility shims, migrations, legacy adapters and the tests that pin them are copies of a declared fact and go in the same change unless a named live caller needs them" drift: "His abstraction is the certificate: it stays. Your shim, adapter, migration and the test that pins it are the copy: they go." -audience: fleet type: hard-rule --- diff --git a/src/agent_comms/pi_rule_gate.py b/src/agent_comms/pi_rule_gate.py index 01d4613c3b..66c2168385 100644 --- a/src/agent_comms/pi_rule_gate.py +++ b/src/agent_comms/pi_rule_gate.py @@ -14,13 +14,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from agent_comms_hooks.skill import ( - CommanderReader, - CommanderRulesReader, - DeckReader, - FleetReader, - FleetRulesReader, -) +from agent_comms_hooks.skill import DeckReader, RulesOnly from .bus_publication import Delivered from .nominal.field_codec import ThreadLookup @@ -58,7 +52,7 @@ def refuse(self, reason: str) -> None: @dataclass(frozen=True) class PiRedeal(PiRuleGate, Redeals): - """The deal Core renews for a thread: its role's sources and the owner's latest words.""" + """The shared sources and owner's latest words for this independent session.""" comms: Comms thread: Thread @@ -70,23 +64,16 @@ def of(cls, comms: Comms, thread: Thread) -> PiRedeal: def renew(self, trigger: DealTrigger) -> None: """A start or compaction deals the papers, then the rules.""" - self.deal_from(CommanderReader, FleetReader, trigger) + self.deal_from(DeckReader, trigger) def reread(self, trigger: DealTrigger) -> None: - self.deal_from(CommanderRulesReader, FleetRulesReader, trigger) + self.deal_from(RulesOnly, trigger) - def deal_from(self, commander: type[DeckReader], commanded: type[DeckReader], - trigger: DealTrigger) -> None: - """The registry's commander project picks the deck reader whose sources it deals.""" + def deal_from(self, reader: type[DeckReader], trigger: DealTrigger) -> None: + """Material selection is explicit; the captured incarnation still binds the deal.""" snapshot = self.comms.registry.snapshot() - snapshot.commander_project.deal_rules( - self, - snapshot.born(self.thread.incarnation.require_recorded().lookup), - commander=commander, - commanded=commanded, - prompts=self.owner_words(snapshot), - trigger=trigger, - ) + snapshot.born(self.thread.incarnation.require_recorded().lookup) + self.deal(reader, self.owner_words(snapshot), trigger) def owner_words(self, snapshot: RegistrySnapshot) -> tuple[str, ...]: """What the registry's human last typed to this thread, oldest first: the same person diff --git a/src/agent_comms/registry_document.py b/src/agent_comms/registry_document.py index b66f082390..a1f6a71302 100644 --- a/src/agent_comms/registry_document.py +++ b/src/agent_comms/registry_document.py @@ -82,7 +82,6 @@ from .activity import ObservedActivity from .agent_activity import AgentActivity from .bus_publication import Delivered - from .rule_deal import DealTrigger, DealtSources, RuleDeal @dataclass(frozen=True, slots=True) @@ -601,13 +600,6 @@ def by_role[Answer](self, thread: Thread, *, """The resolved thread carries its nominal identity and declared role capabilities.""" return commanded - def deal_rules(self, deal: RuleDeal, thread: Thread, *, - commander: type[DealtSources], commanded: type[DealtSources], - prompts: tuple[str, ...], trigger: DealTrigger) -> None: - """The fleet's rules are dealt by the role this project gives the thread.""" - deal.deal(self.by_role(thread, commander=commander, commanded=commanded), prompts, - trigger) - class NoCommanderProject(CommanderProject): def commanders(self, snapshot: RegistrySnapshot) -> tuple[Thread, ...]: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index ec872fd195..b2cba8476c 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -66,7 +66,7 @@ OrderCheckFailedError, ) from agent_comms.command import Command -from agent_comms.commander.rule_file import RuleAudience, RuleFile +from agent_comms.commander.rule_file import RuleFile from agent_comms.coordination_session import NativeCapture from agent_comms.nominal.declared_family import DeclaredFamily from agent_comms.errors import ( @@ -2863,8 +2863,7 @@ def work_instructions(self) -> tuple[str, ...]: *self.deletes.instructions(), *self.run.instructions(), *(f"{n}. {edit}" for n, edit in enumerate(self.edits, 1)), "Banned: " + "; ".join(self.banned), "Report: " + self.report, - *(audience.order_line(", ".join(rule.name for rule in rules)) - for audience, rules in RuleAudience.grouped(RuleFile.packaged()))) + RuleFile.order_line()) def instructions(self, todo): return "\n".join((f"Problem: {todo.id}: {todo.text}", *self.finding.instructions(), diff --git a/src/agent_comms/unit_instructions.py b/src/agent_comms/unit_instructions.py index 95495bf65d..8812325afb 100644 --- a/src/agent_comms/unit_instructions.py +++ b/src/agent_comms/unit_instructions.py @@ -1,7 +1,7 @@ """The system prompt every native unit carries, derived once at preparation. A system prompt survives every compaction, so a unit never drifts off it: the owner's -instructions, the indictment, every packaged rule under its audience's heading, and the +instructions, the indictment, the shared packaged rules, and the NRA pattern index that orders and send-backs cite. """ @@ -12,7 +12,7 @@ from pathlib import Path from agent_comms.commander.package import CommanderPackage -from agent_comms.commander.rule_file import RuleAudience, RuleFile +from agent_comms.commander.rule_file import RuleFile from agent_comms.native_package import OWNER_INSTRUCTIONS, UNIT_INSTRUCTIONS from agent_comms.slop import Slop @@ -26,8 +26,7 @@ def render() -> str: OWNER_INSTRUCTIONS.read_text(encoding="utf-8"), CommanderPackage.indictment().read_text(encoding="utf-8"), CommanderPackage.voice().read_text(encoding="utf-8"), - *(audience.heading + "\n\n" + "\n\n".join(rule.render() for rule in rules) - for audience, rules in RuleAudience.grouped(RuleFile.packaged())), + RuleFile.render_packaged(), (Slop.catalog() / "README.md").read_text(encoding="utf-8"), ) return "\n\n".join(section.strip() for section in sections) + "\n" diff --git a/src/agent_comms_hooks/skill.py b/src/agent_comms_hooks/skill.py index 6b0be93d28..2807776c48 100644 --- a/src/agent_comms_hooks/skill.py +++ b/src/agent_comms_hooks/skill.py @@ -1,11 +1,9 @@ """At session start, resume and compaction, write a bound session's sources, order them read once, and deal the reminder deck. -Every session bound to a bus is dealt the fleet's sources: paper1 first, the proof the rules are -consequences of, then the fleet rules in their reading order, the voice and the recorded -failures. The session the registry names commander is also dealt the indictment, the -commander skill, the commander's own rules and NRA's pattern catalog index; a rung file -is read when an order or roast cites its pattern ID. Claude Code caps injected hook text at +Every bound collaborator reads the same papers, rules, voice, failure record, shared skill +and NRA pattern catalog. Explicit prime variants select material, never a rank; a rung file +is read when a source verdict cites its pattern ID. Claude Code caps injected hook text at 10,000 characters and the sources are far longer, so they go into parts beside the transcript and the hook injects the order to read each part, once; the read gate refuses every other tool until each part is read. The reminder hook then puts one short slice inline every few tool calls. @@ -16,15 +14,15 @@ import inspect import json from datetime import datetime -from abc import abstractmethod from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path from typing import ClassVar from agent_comms.cli_bridges.claude_transcript import ClaudeJsonLines +from agent_comms.class_fact import ClassFact from agent_comms.commander.package import CommanderPackage -from agent_comms.commander.rule_file import CommanderAudience, FleetAudience, RuleAudience, RuleFile +from agent_comms.commander.rule_file import RuleFile from agent_comms.nominal.declared_family import DeclaredFamily from agent_comms.held_orders import HeldOrders from agent_comms.pattern_sources import PostmortemFailure @@ -42,26 +40,21 @@ from agent_comms.cli_bridges.hook_read_gate import TranscriptWords class DeckReader(DealtSources): - """Which packaged sources a bound session is dealt, by the audience its rules declare, on - every backend: the Claude hooks and Core's Pi deal both read this one list. + """The shared sources every bound collaborator reads, on both native and CLI backends. - Every session bound to a bus follows the fleet rules; the session the registry names - commander also follows the commander's. RuleFile's audience is the one owner of that split. + Explicit prime variants change which material is reread, never who may act. """ - audiences: ClassVar[tuple[type[RuleAudience], ...]] = () - """Its own audience after every audience its base readers serve.""" - deck_suffix: ClassVar[str] - """Where this reader's deck sits beside the transcript: a session whose role changes finds - no deck of its new kind and is dealt again.""" - slice_chars: ClassVar[int] = 2_500 - """The most characters one inline slice carries; a longer paragraph splits at its lines. A - rule's wake-up is never split, since a greentext cut before its punchline teaches nothing, - so the cap sits above the longest one and the deck test fails when one outgrows it.""" + deck_suffix: ClassVar[str] = ".deck.json" + """The one shared deck beside each session's transcript.""" + @ClassFact + def slice_chars(cls: type[DeckReader]) -> int: + """Whole wake-ups declare the inline packing width; paragraphs split at their lines. - def __init_subclass__(cls, audience: type[RuleAudience], **options: object) -> None: - super().__init_subclass__(**options) - cls.audiences = tuple(dict.fromkeys((*cls.audiences, audience))) + A rule cannot be cut before its punchline. Its declared text, not a separately + maintained character count, therefore supplies the width for every reader. + """ + return max(map(len, cls.tells())) @classmethod def proofs(cls) -> tuple[Path, ...]: @@ -70,29 +63,32 @@ def proofs(cls) -> tuple[Path, ...]: @classmethod def indictment(cls) -> tuple[Path, ...]: - """The record of what the reader actually did; a fleet session is dealt none.""" - return () + """The failure record every collaborator reads after the proofs and rules.""" + return (CommanderPackage.indictment(),) @classmethod def rules(cls) -> tuple[RuleFile, ...]: - return tuple(rule for rule in RuleFile.packaged() if rule.audience in cls.audiences) + return RuleFile.packaged() @classmethod - @abstractmethod def streams(cls) -> tuple[tuple[str, ...], ...]: - """The slice streams this reader's deck interleaves, one slice of each per round.""" + """Interleave the shared voice, failure record, rules and failure catalog.""" + indictment = tuple(paragraph for path in cls.indictment() + for paragraph in cls.paragraphs(path.read_text(encoding="utf-8"))) + return tuple(stream for stream in (cls.voice(), indictment, cls.tells(), cls.failures()) + if stream) - @abstractmethod def header(self, start: DeckStart, comms: Comms, snapshot: RegistrySnapshot, commanders: tuple[Thread, ...]) -> str: - """The line after the read order: what the summary cannot be trusted to hold.""" + """Retained work, not a hierarchy of which reader is permitted to judge it.""" + return start.held(comms, snapshot, commanders) def deaf(self, session: str) -> tuple[str, ...]: return tuple(deaf.line() for deaf in WatchClaim(session).deaf()) def board(self, session: str) -> tuple[str, ...]: - """A fleet session has no commander board.""" - return () + """Read this session's retained board; publication is independently configured.""" + return WatchClaim(session).state().board() @classmethod def slices(cls) -> list[str]: @@ -115,6 +111,29 @@ def tells(cls) -> tuple[str, ...]: def failures(cls) -> tuple[str, ...]: return tuple(f"Recorded failure {line}" for line in PostmortemFailure.catalog().splitlines()) + @classmethod + def sources(cls) -> tuple[Path, ...]: + """The closing rule follows every proof, rule and the recorded failures.""" + return (*cls.leading(), *(rule.path for rule in cls.rules() if rule.closes_deal)) + + @classmethod + def leading(cls) -> tuple[Path, ...]: + """One source order for every collaborator and every backend.""" + return ( + *cls.proofs(), + *(rule.path for rule in cls.rules() if not rule.closes_deal), + CommanderPackage.voice(), + Path(inspect.getfile(PostmortemFailure)), + CommanderPackage.skill(), + Slop.catalog() / "README.md", + *cls.indictment(), + ) + + @classmethod + def answered(cls) -> frozenset[str]: + """The shared skill is read and answered under the same audit contract.""" + return super().answered() | {CommanderPackage.skill().stem} + @classmethod def paragraphs(cls, text: str) -> tuple[str, ...]: """Blank-line paragraphs; a heading stays with the paragraph it introduces, and a @@ -141,79 +160,7 @@ def sliced(cls, paragraph: str) -> tuple[str, ...]: return tuple(runs) -class FleetReader(DeckReader, audience=FleetAudience): - """A bound session the registry does not name commander: the paper the rules derive from, - the fleet rules, the voice they are said in and the failures already caught.""" - - deck_suffix = ".fleet-deck.json" - - @classmethod - def streams(cls) -> tuple[tuple[str, ...], ...]: - return (cls.voice(), cls.tells(), cls.failures()) - - @classmethod - def sources(cls) -> tuple[Path, ...]: - """Everything leading, then the rules that close the deal.""" - return (*cls.leading(), *(rule.path for rule in cls.rules() if rule.closes_deal)) - - @classmethod - def leading(cls) -> tuple[Path, ...]: - """Proof first, then the rules it makes binding, then how they are said and broken.""" - return ( - *cls.proofs(), - *(rule.path for rule in cls.rules() if not rule.closes_deal), - CommanderPackage.voice(), - Path(inspect.getfile(PostmortemFailure)), - ) - - def header(self, start: DeckStart, comms: Comms, snapshot: RegistrySnapshot, - commanders: tuple[Thread, ...]) -> str: - return ("FUCK YOUR COMPACTION. Your summary is a paraphrase of these rules, and a" - " paraphrase drops the exact words they turn on. The files above are the source;" - " where your summary disagrees with them, the summary is wrong.") - - -class CommanderReader(FleetReader, audience=CommanderAudience): - """The session the registry names commander: the fleet's sources plus the indictment, the - commander skill, the commander's own rules and the slop catalog's index.""" - - deck_suffix = ".deck.json" - - @classmethod - def indictment(cls) -> tuple[Path, ...]: - return (CommanderPackage.indictment(),) - - @classmethod - def streams(cls) -> tuple[tuple[str, ...], ...]: - indictment = tuple(paragraph for path in cls.indictment() - for paragraph in cls.paragraphs(path.read_text(encoding="utf-8"))) - return tuple(stream for stream in (cls.voice(), indictment, cls.tells(), cls.failures()) - if stream) - - @classmethod - def leading(cls) -> tuple[Path, ...]: - """The fleet's files, its own rules among them by audience, then the commander's, and the - indictment after them: read once every proof and rule is understood, it is the record of - what the model still did anyway, never a preface to skim (Tristan, 2026-10-30).""" - return (*super().leading(), CommanderPackage.skill(), Slop.catalog() / "README.md", - *cls.indictment()) - - @classmethod - def answered(cls) -> frozenset[str]: - """The commander skill is dealt and read like a rule, so it is answered like one: on - 2026-10-30 claude-nra read it owing nothing, then ended six turns on a wait with no - review fork at load 104 (bus 46591).""" - return super().answered() | {CommanderPackage.skill().stem} - - def header(self, start: DeckStart, comms: Comms, snapshot: RegistrySnapshot, - commanders: tuple[Thread, ...]) -> str: - return start.held(comms, snapshot, commanders) - - def board(self, session: str) -> tuple[str, ...]: - return WatchClaim(session).state().board() - - -class RulesOnly(DeckReader, audience=FleetAudience): +class RulesOnly(DeckReader): """A refresh dealt before any compaction: the papers are still in the context word for word, so it deals the rules, the voice and the failures again, never the papers a second time.""" @@ -222,15 +169,7 @@ def proofs(cls) -> tuple[Path, ...]: return () -class FleetRulesReader(RulesOnly, FleetReader, audience=FleetAudience): - """A fleet session's refresh.""" - - -class CommanderRulesReader(RulesOnly, CommanderReader, audience=CommanderAudience): - """The commander's refresh.""" - - -class IndictmentFree(DeckReader, audience=FleetAudience): +class IndictmentFree(DeckReader): """A prime asked for mid-context without the indictment, so it does not fill the window.""" @classmethod @@ -238,21 +177,14 @@ def indictment(cls) -> tuple[Path, ...]: return () -class CommanderIndictmentFreeReader(IndictmentFree, CommanderReader, audience=CommanderAudience): - """The commander's papers and rules without the indictment.""" - - -class CommanderLightReader(RulesOnly, IndictmentFree, CommanderReader, audience=CommanderAudience): - """The commander's rules alone: no papers, no indictment.""" +class LightReader(RulesOnly, IndictmentFree): + """Explicitly reread rules without papers or indictment, independent of authority.""" class Prime(DeclaredFamily): - """A deal the owner asks for on demand (/prime, `agent-comms-prime`). Each member names - the readers it deals by role; the readers own which files those are, so a variant is a - selection over the one dealt order, never a second list.""" + """An explicit material selection over the one shared deal, never a role selector.""" - fleet: ClassVar[type[DeckReader]] - commander: ClassVar[type[DeckReader]] + reader: ClassVar[type[DeckReader]] @classmethod def trigger(cls) -> PrimedDeal: @@ -261,25 +193,21 @@ def trigger(cls) -> PrimedDeal: class FullPrime(Prime, declared_name="full"): - """Papers, rules and, for a commander, the indictment: the same deal as a compaction.""" + """The same complete source set as a start or compaction.""" - fleet = FleetReader - commander = CommanderReader + reader = DeckReader class PaperlessPrime(Prime, declared_name="no-papers"): - fleet = FleetRulesReader - commander = CommanderRulesReader + reader = RulesOnly class IndictmentlessPrime(Prime, declared_name="no-indictment"): - fleet = FleetReader - commander = CommanderIndictmentFreeReader + reader = IndictmentFree class LightPrime(Prime, declared_name="light"): - fleet = FleetRulesReader - commander = CommanderLightReader + reader = LightReader @dataclass(frozen=True) @@ -354,12 +282,10 @@ def of(cls, record: Mapping[str, object], **context: object) -> DeckStart: return (SubagentDeckStart if cls.raised_by_subagent(record) else cls).decode(record, **context) def take(self, comms: Comms, binding: Binding, number: int = 0) -> tuple[str, ...]: - """The supplied resident binding selects a reader at one registry cut.""" + """The captured resident identity admits the shared reader at one registry cut.""" snapshot = comms.registry.snapshot() - reader = snapshot.commander_project.by_role( - snapshot.born(binding.owner.require_recorded().lookup), - commander=CommanderReader(), commanded=FleetReader()) - return self.taken(reader, number, comms, snapshot) + snapshot.born(binding.owner.require_recorded().lookup) + return self.taken(DeckReader(), number, comms, snapshot) def taken(self, reader: DeckReader, number: int, comms: Comms, snapshot: RegistrySnapshot) -> tuple[str, ...]: @@ -368,10 +294,8 @@ def taken(self, reader: DeckReader, number: int, comms: Comms, def primed(self, prime: type[Prime], comms: Comms, binding: Binding) -> tuple[str, ...]: """The owner-selected variant, dealt through the same bound resident context.""" snapshot = comms.registry.snapshot() - reader = snapshot.commander_project.by_role( - snapshot.born(binding.owner.require_recorded().lookup), - commander=prime.commander(), commanded=prime.fleet()) - return self.dealt(reader, prime.trigger(), comms, snapshot) + snapshot.born(binding.owner.require_recorded().lookup) + return self.dealt(prime.reader(), prime.trigger(), comms, snapshot) def dealt(self, reader: DeckReader, trigger: DealTrigger, comms: Comms, snapshot: RegistrySnapshot) -> tuple[str, ...]: @@ -381,7 +305,7 @@ def dealt(self, reader: DeckReader, trigger: DealTrigger, comms: Comms, sources = reader.sources() commanders = tuple(snapshot.commander_project.commanders(snapshot)) return ( - trigger.reason() + " " + self.notice(commanders) + f" Read each of these {len(sources)} files in full with the Read tool before any other" + trigger.reason() + " " + self.notice(snapshot.commander_project.leads(snapshot)) + f" Read each of these {len(sources)} files in full with the Read tool before any other" " action: they are every rule you follow and the sources they come from. A summary" " or memory of them does not count as reading them, and every other tool is refused" " until each one is read. Read them in order, at most four unanswered rule files at" @@ -430,7 +354,7 @@ def held(self, comms: Comms, snapshot: RegistrySnapshot, commanders: tuple[Thread, ...]) -> str: """Held orders from the same bus and cut that selected the reader.""" return "\n".join(( - "FUCK YOUR COMPACTION. Your summary is a paraphrase. What your units actually hold:", + "FUCK YOUR COMPACTION. Your summary is a paraphrase. What the registered boards hold:", *(hold.recalled() for commander in commanders for hold in HeldOrders.of(comms.goals.todos, snapshot, commander.name).live), )) @@ -442,10 +366,9 @@ def prepare(self, reader: DeckReader, prompts: tuple[str, ...], trigger: DealTri def widened(self, reader: DeckReader, comms: Comms, snapshot: RegistrySnapshot) -> tuple[str, ...]: - """The deck of this session's role is the one record of the role it was dealt. A - session whose role changed finds none of its kind and is dealt only the files the role - adds, keeping what it read and wrote, since its context was never lost; a session bound - after it started is dealt in full and told so.""" + """A session newly bound after startup receives the shared sources. Retained reads + and audit survive when the context remains; changing a tag or storage lead adds no + second deck or rank-specific source set.""" deck = self.deck(reader) if deck.exists(): return () @@ -457,8 +380,8 @@ def widened(self, reader: DeckReader, comms: Comms, @staticmethod def notice(commanders: tuple[Thread, ...]) -> str: names = [thread.name for thread in commanders] - return ("Commander: " + ", ".join(names) + "." if names - else "No commander project is registered for this session.") + return ("Storage lead: " + ", ".join(names) + ". All collaborators share responsibility." if names + else "No project storage lead is registered for this session.") @dataclass(frozen=True) diff --git a/tests/test_commander_skill_hook.py b/tests/test_commander_skill_hook.py index 980f8f8627..851e7a75bd 100644 --- a/tests/test_commander_skill_hook.py +++ b/tests/test_commander_skill_hook.py @@ -25,7 +25,7 @@ from agent_comms.heartbeat_subjects import UnitSubject from agent_comms.channel_targets import CommandersTag from agent_comms.commander.package import CommanderPackage -from agent_comms.commander.rule_file import CommanderAudience, RuleFile +from agent_comms.commander.rule_file import RuleFile from agent_comms.commander.standing import PlanIdle from agent_comms.goal_actions import CreateProblemGoalAction from commander_heartbeat_fixture import capture_frame @@ -54,11 +54,11 @@ from agent_comms.cli_bridges.hook_forks import SubagentHook from agent_comms.rule_deal import AuditDue, OwnerWordsAnswer, ReadSpan, RuleAudit, SessionAudit from agent_comms.cli_bridges.hook_read_gate import GatedCall, MonitorInput -from agent_comms_hooks.skill import (CommanderLightReader, CommanderReader, DeckReader, DeckStart, - FleetReader, FullPrime, LightPrime, Prime, SessionSource) +from agent_comms_hooks.skill import (DeckReader, DeckStart, LightReader, + FullPrime, LightPrime, Prime, SessionSource) from agent_comms.cli_bridges.session_watch import DeafWatch, LiveWatch, WatchClaim, WatchRecord from conftest import engaged -from test_commander_notifications import beat, lead_hud +from test_commander_notifications import beat from test_head_review import ACCEPTED, invoke from agent_comms.cli_bridges.hook_requests import ( @@ -124,7 +124,7 @@ def test_light_prime_executes_through_the_resident_request( assert reply.status == 0, reply.stderr start = DeckStart.decode(request.payload) assert start.current().trigger == LightPrime.trigger() - assert start.current().paths == tuple(path.resolve() for path in CommanderLightReader.sources()) + assert start.current().paths == tuple(path.resolve() for path in LightReader.sources()) assert str(start.audit) in reply.stdout @@ -157,7 +157,7 @@ def test_the_commander_session_start_reads_all_package_files(tmp_path, source, c context = json.loads(result.stdout)["hookSpecificOutput"]["additionalContext"] trigger = {"startup": StartedDeal(), "resume": StartedDeal(), "compact": UnstampedCompactionDeal()}[source] - assert context.startswith(trigger.reason() + " Commander: claude-code.") + assert context.startswith(trigger.reason() + " Storage lead: claude-code.") assert "WHAT TRISTAN ACTUALLY SAID LAST" in context assert "FUCK YOUR COMPACTION" in context dealt = dealt_sources(transcript) @@ -222,7 +222,7 @@ def test_a_slice_every_few_calls_rotating_and_dealt_again_at_compaction(tmp_path start = DeckStart.decode(hook_record(transcript, "SessionStart")) assert start.take(commander_bus, commander_binding) hook = SliceReminder.decode(hook_record(transcript)) - slices = CommanderReader.slices() + slices = DeckReader.slices() taken = [hook.take(commander_bus, commander_binding, hook.count_call()) for _ in range(SliceReminder.every * 3)] assert taken[SliceReminder.every - 1::SliceReminder.every] == [(text,) for text in slices[:3]] assert sum(map(len, taken)) == 3 @@ -251,8 +251,7 @@ def test_an_unbound_session_is_never_told_to_rearm_a_commander_watch(tmp_path, m transcript = tmp_path / "session.jsonl" unbound_reminders(transcript, monkeypatch, capsys) hook = DeafReminder.decode(hook_record(transcript)) - assert not hook.deck(CommanderReader()).exists() - assert not hook.deck(FleetReader()).exists() + assert not hook.deck(DeckReader()).exists() @pytest.mark.parametrize(("command", "allowed"), ( @@ -404,8 +403,13 @@ def test_rules_carrying_his_words_flag_their_board_lines(): def test_the_board_prints_changes_once_then_an_unchanged_count(notification_bus, tmp_path, monkeypatch): - """Real packets, bridge delivery journal and session socket; no hook-side state.""" + """Retained observations, bridge delivery journal and socket; publication stays muted.""" + from agent_comms.heartbeat_delivery import OffHeartbeatDelivery + from agent_comms.task_attachments.commander_pings import CommanderUpdateStore + comms = notification_bus + comms.threads.set_heartbeat_delivery("lead", OffHeartbeatDelivery) + assert comms.registry.require("lead").heartbeat_delivery is OffHeartbeatDelivery monkeypatch.chdir(tmp_path) monkeypatch.setenv("HOME", "home") hook = BoardReminder.decode(hook_record(tmp_path / "session.jsonl")) @@ -419,8 +423,8 @@ def test_the_board_prints_changes_once_then_an_unchanged_count(notification_bus, subjects=(UnitSubject(worker),), since=RecordedEventTime(began)) second = RulePing(ReviewTimerUp, TextDigest.of("second"), text="Review worker P2", subjects=(UnitSubject(worker),), since=RecordedEventTime(began)) - hud = lead_hud(comms).hud_text(began + 90) - CommanderDecisions(lead, (first, second), (first, second), ()).send(comms, began + 90, hud) + CommanderUpdateStore.record(comms.root, CommanderDecisions( + lead, (first, second), (first, second), ())) with (claim.held(record), claim.reader_held(), Binding.open(tmp_path / "delivery") as db, socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server): server.bind(str(claim.socket_path)) @@ -435,13 +439,15 @@ def test_the_board_prints_changes_once_then_an_unchanged_count(notification_bus, unchanged_summary((first, second), began + 91), ) changed = replace(first, observation=TextDigest.of("changed"), since=RecordedEventTime(began + 90)) - CommanderDecisions(lead, (changed, second), (changed,), ()).send(comms, began + 92, hud) + CommanderUpdateStore.record(comms.root, CommanderDecisions( + lead, (changed, second), (changed,), ())) assert tool_call(hook, server, comms, db, began + 92) == ( "BOARD: Read worker P1 (seen for 0 min)", unchanged_summary((second,), began + 92), ) # A fresh packet does not reset delivery, and the answer window rings only once. - CommanderDecisions(lead, (changed, second), (), ()).send(comms, began + 93, hud) + CommanderUpdateStore.record(comms.root, CommanderDecisions( + lead, (changed, second), (), ())) alarm = began + AwaitingCommander.seconds assert tool_call(hook, server, comms, db, alarm + 1) == ( "BOARD: Review worker P2 (seen for 5 min)", @@ -451,13 +457,13 @@ def test_the_board_prints_changes_once_then_an_unchanged_count(notification_bus, unchanged_summary((changed, second), alarm + 2), ) assert DeliveredBoardContent.count(db) == 2 - CommanderDecisions(lead, (), (), ()).send(comms, alarm + 3, hud) + CommanderUpdateStore.record(comms.root, CommanderDecisions(lead, (), (), ())) assert tool_call(hook, server, comms, db, alarm + 3) == () assert DeliveredBoardContent.count(db) == 0 def test_the_deck_carries_the_voice_every_fourth_slice_every_rule_reminder_and_every_failure(): - slices = CommanderReader.slices() + slices = DeckReader.slices() voice = DeckReader.paragraphs(CommanderPackage.voice().read_text()) indictment = DeckReader.paragraphs(CommanderPackage.indictment().read_text()) assert set(slices[0::4]) == set(voice) @@ -487,7 +493,7 @@ def test_the_resident_commands_order_the_reads_once_then_put_slices_inline( outputs = [run(ReminderRequest) for _ in range(SliceReminder.every)] assert outputs[:-1] == [""] * (SliceReminder.every - 1) context = json.loads(outputs[-1])["hookSpecificOutput"]["additionalContext"] - assert context == CommanderReader.slices()[0] + assert context == DeckReader.slices()[0] assert "Read each of these" not in context @@ -503,15 +509,15 @@ def test_a_session_that_becomes_commander_is_dealt_its_sources_on_its_next_tool_ assert transcript.with_suffix(".deck.json").exists() -def test_a_unit_session_gets_the_fleet_deck_and_no_commander_reminders( - tmp_path, monkeypatch, capsys, commander_bus): - """A bound session that is not commander follows the fleet rules: it is ordered to read - them, its summary is called a paraphrase, and its slices come from the fleet deck.""" +@pytest.mark.parametrize("actor", ("claude-code", "unit-x")) +def test_each_bound_peer_gets_the_shared_sources_and_reminders( + tmp_path, monkeypatch, capsys, commander_bus, actor): + """Lead and untagged peer use one source list, audit and reminder deck.""" monkeypatch.setenv("HOME", str(tmp_path / "home")) transcript = tmp_path / "session.jsonl" transcript.write_text("") - WatchClaim("s").enrol(WatchRecord("unit-x", str(commander_bus.root), (), tmp_path / "state")) - run = run_resident(monkeypatch, capsys, transcript, comms=commander_bus, binding=admitted_binding(commander_bus, "unit-x")) + WatchClaim("s").enrol(WatchRecord(actor, str(commander_bus.root), (), tmp_path / "state")) + run = run_resident(monkeypatch, capsys, transcript, comms=commander_bus, binding=admitted_binding(commander_bus, actor)) context = json.loads(run(SkillRequest))["hookSpecificOutput"]["additionalContext"] assert "Read each of these" in context and "Your summary is a paraphrase" in context assert "WHAT TRISTAN ACTUALLY SAID LAST" in context @@ -519,9 +525,9 @@ def test_a_unit_session_gets_the_fleet_deck_and_no_commander_reminders( outputs = [run(ReminderRequest) for _ in range(SliceReminder.every)] assert outputs[:-1] == [""] * (SliceReminder.every - 1) context = json.loads(outputs[-1])["hookSpecificOutput"]["additionalContext"] - assert context == FleetReader.slices()[0] - assert transcript.with_suffix(".fleet-deck.json").exists() - assert not transcript.with_suffix(".deck.json").exists() + assert context == DeckReader.slices()[0] + assert transcript.with_suffix(".deck.json").exists() + assert set(dealt_sources(transcript)) == {path.resolve() for path in DeckReader.sources()} @pytest.mark.parametrize("path", CommanderPackage.rules_files(), ids=lambda path: path.name) @@ -555,24 +561,8 @@ def test_failure_catalog_numbers_every_evidenced_member_once(): -def test_a_session_bound_to_another_bus_thread_reads_the_fleet_rules_and_not_the_commanders( - tmp_path, monkeypatch, commander_bus): - monkeypatch.setenv("HOME", str(tmp_path / "home")) - transcript = tmp_path / "session.jsonl" - transcript.write_text("") - record = {"hook_event_name": "SessionStart", "session_id": "s", "transcript_path": str(transcript)} - WatchClaim("s").enrol(WatchRecord("unit-x", str(commander_bus.root), (), tmp_path)) - start = DeckStart.decode(record) - assert start.take(commander_bus, admitted_binding(commander_bus, "unit-x")) - dealt = start.current().dealt - assert all(rule.path.resolve() in dealt for rule in FleetReader.rules()) - commander_only = [rule for rule in RuleFile.packaged() if rule.audience is CommanderAudience] - assert commander_only and not any(rule.path.resolve() in dealt for rule in commander_only) - assert CommanderPackage.indictment().resolve() not in dealt - - -def test_the_commander_reads_every_packaged_rule(): - assert CommanderReader.rules() == RuleFile.packaged() +def test_the_shared_reader_reads_every_packaged_rule(): + assert DeckReader.rules() == RuleFile.packaged() def test_the_reading_order_names_exactly_the_packaged_rules_and_the_spine_leads(): @@ -582,12 +572,11 @@ def test_the_reading_order_names_exactly_the_packaged_rules_and_the_spine_leads( assert RuleFile.reading_order[0] == "spine" -def test_every_deal_reads_the_paper_before_any_rule(): - for reader in (FleetReader, CommanderReader): - sources = reader.sources() - paper = CommanderPackage.paper() - assert paper and sources[:len(paper)] == paper - assert sources[len(paper)] == RuleFile.packaged()[0].path +def test_every_full_deal_reads_the_paper_before_any_rule(): + sources = DeckReader.sources() + paper = CommanderPackage.paper() + assert paper and sources[:len(paper)] == paper + assert sources[len(paper)] == RuleFile.packaged()[0].path def test_the_prime_variants_cover_papers_and_indictment_once_each_with_the_indictment_last(): @@ -597,13 +586,12 @@ def test_the_prime_variants_cover_papers_and_indictment_once_each_with_the_indic indictment = CommanderPackage.indictment() kept = set() for prime in Prime.members_with(Prime): - commander = prime.commander.sources() - assert indictment not in prime.fleet.sources() - assert all(rule.path in commander for rule in CommanderReader.rules()) - kept.add((paper <= set(commander), indictment in commander)) - if indictment in commander: - assert prime.commander.leading()[-1] == indictment - assert prime.commander.slices() and prime.fleet.slices() + sources = prime.reader.sources() + assert all(rule.path in sources for rule in DeckReader.rules()) + kept.add((paper <= set(sources), indictment in sources)) + if indictment in sources: + assert prime.reader.leading()[-1] == indictment + assert prime.reader.slices() assert kept == {(True, True), (True, False), (False, True), (False, False)} @@ -633,10 +621,9 @@ def test_the_commander_session_gets_its_sources(tmp_path, monkeypatch, commander {"hook_event_name": "SessionStart", "session_id": "s", "transcript_path": str(transcript)}).take(commander_bus, commander_binding) -def test_a_demoted_commander_keeps_its_deck_files_but_they_get_no_vote( +def test_changing_storage_lead_preserves_the_deal_without_changing_fork_registration( tmp_path, capsys, commanding_session, commander_bus, commander_binding): - """Command moves to another thread in the registry; the old commander's dealt deck stays - on disk, and no hook treats it as command: no reminder, stop guard or fork.""" + """Material selection preserves context; this extraction does not change fork roles.""" transcript = tmp_path / "session.jsonl" transcript.write_text("") assert DeckStart.decode(hook_record(transcript, "SessionStart")).take(commander_bus, commander_binding) @@ -646,15 +633,15 @@ def test_a_demoted_commander_keeps_its_deck_files_but_they_get_no_vote( dealt = transcript.with_suffix(".deal.json").read_bytes() reminder = BoardReminder.decode(hook_record(transcript)) assert [reminder.take(commander_bus, commander_binding, reminder.count_call()) for _ in range(SliceReminder.every - 1)] == [()] * (SliceReminder.every - 1) - assert transcript.with_suffix(".fleet-deck.json").exists() - assert transcript.with_suffix(".deal.json").read_bytes() == dealt # the fleet adds nothing + assert transcript.with_suffix(".deck.json").exists() + assert transcript.with_suffix(".deal.json").read_bytes() == dealt StopHook.decode({"hook_event_name": "Stop", "session_id": "s", "transcript_path": str(transcript), "stop_hook_active": False}, comms=commander_bus, binding=commander_binding).guard() assert capsys.readouterr().out == "" SubagentHook.decode({"hook_event_name": "SubagentStart", "session_id": "s", "transcript_path": str(transcript), "agent_id": "a9", "agent_type": "Explore"}).register(commander_bus, commander_binding) - assert "slot claude-code-explore" in capsys.readouterr().out # a fleet slot, not a fork + assert "slot claude-code-explore" in capsys.readouterr().out assert "claude-a9" not in commanding_session.registry commanding_session.registry.require("claude-code-explore").execution.require_subagent() assert transcript.with_suffix(".deck.json").exists() @@ -727,7 +714,7 @@ def read_whole(transcript, source, *, comms: Comms, binding: Binding): def test_a_batch_of_read_rules_is_answered_before_the_next_unread_file_opens(capsys, dealt_session, commander_bus, commander_binding): transcript, start = dealt_session papers = CommanderPackage.paper() - rules = CommanderReader.rules() + rules = DeckReader.rules() for paper in papers: read_whole(transcript, paper.resolve(), comms=commander_bus, binding=commander_binding) # a paper carries no rule name: no entry owed assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rules[0].path)}, comms=commander_bus, binding=commander_binding)) @@ -741,7 +728,7 @@ def test_a_batch_of_read_rules_is_answered_before_the_next_unread_file_opens(cap assert not refused(capsys, gated(transcript, "Read", {"file_path": str(papers[0])}, comms=commander_bus, binding=commander_binding)) for tool in ("Read", "Write", "Edit"): assert not refused(capsys, gated(transcript, tool, {"file_path": str(start.audit)}, comms=commander_bus, binding=commander_binding)) - start.audit.write_text(answered_audit(CommanderReader, rules[:Unengaged.batch])) + start.audit.write_text(answered_audit(DeckReader, rules[:Unengaged.batch])) assert not refused(capsys, gated(transcript, "Read", following, comms=commander_bus, binding=commander_binding)) assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) # the rest stay unread @@ -752,14 +739,14 @@ def test_a_parallel_batch_opens_a_batch_of_rules_and_refuses_the_next(capsys, de transcript, start = dealt_session for paper in CommanderPackage.paper(): read_whole(transcript, paper.resolve(), comms=commander_bus, binding=commander_binding) - rules = CommanderReader.rules() + rules = DeckReader.rules() batch, following = rules[:Unengaged.batch], rules[Unengaged.batch] for rule in batch: assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rule.path)}, comms=commander_bus, binding=commander_binding)) assert refused(capsys, gated(transcript, "Read", {"file_path": str(following.path)}, comms=commander_bus, binding=commander_binding)) # A rule whose read is in flight stays open; answering them opens the next. assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rules[0].path)}, comms=commander_bus, binding=commander_binding)) - start.audit.write_text(answered_audit(CommanderReader, batch)) + start.audit.write_text(answered_audit(DeckReader, batch)) assert not refused(capsys, gated(transcript, "Read", {"file_path": str(following.path)}, comms=commander_bus, binding=commander_binding)) @@ -770,12 +757,12 @@ def test_a_parallel_batch_opens_whichever_order_its_hooks_reach_the_lock(capsys, transcript, start = dealt_session for paper in CommanderPackage.paper(): read_whole(transcript, paper.resolve(), comms=commander_bus, binding=commander_binding) - rules = CommanderReader.rules() + rules = DeckReader.rules() batch = rules[:Unengaged.batch] for rule in reversed(batch): assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rule.path)}, comms=commander_bus, binding=commander_binding)) assert refused(capsys, gated(transcript, "Read", {"file_path": str(rules[Unengaged.batch].path)}, comms=commander_bus, binding=commander_binding)) - start.audit.write_text(answered_audit(CommanderReader, batch)) + start.audit.write_text(answered_audit(DeckReader, batch)) following = reversed(rules[Unengaged.batch:Unengaged.batch * 2]) for rule in following: assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rule.path)}, comms=commander_bus, binding=commander_binding)) @@ -788,7 +775,7 @@ def test_a_whole_paper_opens_in_one_parallel_batch(capsys, dealt_session, comman papers = CommanderPackage.paper() for part in papers: assert not refused(capsys, gated(transcript, "Read", {"file_path": str(part.resolve())}, comms=commander_bus, binding=commander_binding)) - rules = CommanderReader.rules() + rules = DeckReader.rules() for rule in rules[:Unengaged.batch]: assert not refused(capsys, gated(transcript, "Read", {"file_path": str(rule.path)}, comms=commander_bus, binding=commander_binding)) assert refused(capsys, gated(transcript, "Read", {"file_path": str(rules[Unengaged.batch].path)}, comms=commander_bus, binding=commander_binding)) @@ -798,11 +785,11 @@ def test_dealt_files_open_in_order_with_the_indictment_last(capsys, dealt_sessio """On 2026-10-30 a session opened the indictment and two rules first and never read the papers: the next batch of dealt files opens, any later one is refused and named the next.""" transcript, _ = dealt_session - dealt = CommanderReader.sources() + dealt = DeckReader.sources() assert [path.stem for path in dealt[-2:]] == ["indictment", "ally"] papers = CommanderPackage.paper() past_batch = papers[Unengaged.batch] - for later in (CommanderPackage.indictment(), CommanderReader.rules()[0].path, past_batch): + for later in (CommanderPackage.indictment(), DeckReader.rules()[0].path, past_batch): call = gated(transcript, "Read", {"file_path": str(later.resolve())}, comms=commander_bus, binding=commander_binding) call.judge() reason = json.loads(capsys.readouterr().out)["hookSpecificOutput"] @@ -815,7 +802,7 @@ def test_dealt_files_open_in_order_with_the_indictment_last(capsys, dealt_sessio def test_every_unanswered_rule_is_named_in_one_refusal(capsys, dealt_session, commander_bus, commander_binding): transcript, start = dealt_session - rules = CommanderReader.rules() + rules = DeckReader.rules() for rule in rules[:2]: read_whole(transcript, rule.path.resolve(), comms=commander_bus, binding=commander_binding) wrong = [{**engaged(rule.name, rule.path), "quote": "no rule file holds this line"} @@ -835,7 +822,7 @@ def test_every_unanswered_rule_is_named_in_one_refusal(capsys, dealt_session, co ]) def test_an_answer_that_does_not_hold_keeps_the_next_file_shut(capsys, dealt_session, change, says, commander_bus, commander_binding): transcript, start = dealt_session - rules = CommanderReader.rules() + rules = DeckReader.rules() read_whole(transcript, rules[0].path.resolve(), comms=commander_bus, binding=commander_binding) written = {**engaged(rules[0].name, rules[0].path), **change} start.audit.write_text(json.dumps({"rules": [written], "owner_words": []})) @@ -871,13 +858,13 @@ def test_the_full_audit_and_the_owners_words_open_the_gate(capsys, tmp_path, com transcript.write_text(json.dumps({"type": "user", "message": {"content": words[0]}}) + "\n") start = DeckStart.decode(hook_record(transcript, "SessionStart")) start.take(commander_bus, commander_binding) - start.prepare(CommanderReader(), words, StartedDeal()) - for source in CommanderReader.sources(): + start.prepare(DeckReader(), words, StartedDeal()) + for source in DeckReader.sources(): read_whole(transcript, source.resolve(), comms=commander_bus, binding=commander_binding) - start.audit.write_text(answered_audit(CommanderReader)) + start.audit.write_text(answered_audit(DeckReader)) assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) # his words unanswered answers = ({"words": words[0], "doing": "editing rule_deal"},) - start.audit.write_text(answered_audit(CommanderReader, owner_words=answers)) + start.audit.write_text(answered_audit(DeckReader, owner_words=answers)) assert not refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) @@ -890,7 +877,7 @@ def test_the_deal_teaches_the_three_fields_by_a_real_answer_once( template = next(part for part in said if "{\n" in part) example = AuditDue.worked_answer assert json.dumps(FieldCodec.encode(example), ensure_ascii=False) in template - spine = next(rule for rule in CommanderReader.rules() if rule.name == example.rule) + spine = next(rule for rule in DeckReader.rules() if rule.name == example.rule) assert example.unengaged(spine.path) == "" assert {"quote", "does", "why"} <= json.loads(template[template.index("{\n"):])["rules"][0].keys() gated(transcript, "Bash", {}, comms=commander_bus, binding=commander_binding).judge() @@ -906,7 +893,7 @@ def test_every_tool_but_read_waits_for_each_dealt_rule_file_then_its_audit( transcript.write_text("") assert DeckStart.decode(hook_record(transcript, "SessionStart")).take(commander_bus, commander_binding) parts = list(dealt_sources(transcript)) - assert set(parts) == {source.resolve() for source in CommanderReader.sources()} + assert set(parts) == {source.resolve() for source in DeckReader.sources()} assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) whole = {"file_path": str(parts[0])} assert not refused(capsys, gated(transcript, "Read", whole, comms=commander_bus, binding=commander_binding)) @@ -919,10 +906,10 @@ def test_every_tool_but_read_waits_for_each_dealt_rule_file_then_its_audit( audit = transcript.with_suffix(".audit.json") assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) # no audit yet assert not refused(capsys, gated(transcript, "Write", {"file_path": str(audit)}, comms=commander_bus, binding=commander_binding)) - audit.write_text(json.dumps({"rules": [{"rule": rule.name} for rule in CommanderReader.rules()], + audit.write_text(json.dumps({"rules": [{"rule": rule.name} for rule in DeckReader.rules()], "owner_words": []})) assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) # read, never answered - audit.write_text(answered_audit(CommanderReader)) + audit.write_text(answered_audit(DeckReader)) assert not refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) assert DeckStart.decode(hook_record(transcript, "SessionStart")).take(commander_bus, commander_binding) # compaction assert refused(capsys, gated(transcript, "Bash", {"command": "ls"}, comms=commander_bus, binding=commander_binding)) @@ -934,7 +921,7 @@ def test_refusal_counts_names_tool_and_prefills_the_owned_audit( transcript.write_text("") start = DeckStart.decode(hook_record(transcript, "SessionStart")) words = ("Read the rules.\nThen write the audit.",) - start.prepare(CommanderReader(), words, StartedDeal()) + start.prepare(DeckReader(), words, StartedDeal()) reasons = [] for attempt, tool in enumerate(("Bash", "Edit"), 1): gated(transcript, tool, {"file_path": str(transcript)}, comms=commander_bus, binding=commander_binding).judge() @@ -948,7 +935,7 @@ def test_refusal_counts_names_tool_and_prefills_the_owned_audit( reasons.append(reason) assert "{\n" not in reasons[1] # the template prints once per deal, the words every time template = FieldCodec.decode(SessionAudit, json.loads(reasons[0][reasons[0].index("{\n"):])) - for source in CommanderReader.sources(): + for source in DeckReader.sources(): gated(transcript, "Read", {"file_path": str(source)}, comms=commander_bus, binding=commander_binding).completed() due = start.current().due assert {entry.rule for entry in template.rules} == due.rules @@ -961,7 +948,7 @@ def test_refusal_counts_names_tool_and_prefills_the_owned_audit( output = json.loads(capsys.readouterr().out)["hookSpecificOutput"] assert output["permissionDecision"] == "deny" assert str(start.audit) in output["permissionDecisionReason"] - start.prepare(CommanderReader(), words, StartedDeal()) + start.prepare(DeckReader(), words, StartedDeal()) gated(transcript, "Bash", {}, comms=commander_bus, binding=commander_binding).judge() assert "Call #1 since this deal, refused." in capsys.readouterr().out diff --git a/tests/test_dealt_read_identity.py b/tests/test_dealt_read_identity.py index 8ca6276cf7..3b3f321321 100644 --- a/tests/test_dealt_read_identity.py +++ b/tests/test_dealt_read_identity.py @@ -14,7 +14,7 @@ ) from agent_comms.nominal.field_codec import FieldCodec from agent_comms.pi_rule_gate import PiRedeal -from agent_comms_hooks.skill import FleetReader +from agent_comms_hooks.skill import DeckReader from conftest import answer_deal @@ -39,7 +39,7 @@ def test_answers_and_reads_outlive_everything_but_lost_context(commander_bus): read = deal.current().read_by(deal) deal.audit.write_text(deal.audit.read_text(encoding="utf-8"), encoding="utf-8") assert deal.current().read_by(deal) == read - deal.deal(FleetReader, (), StartedDeal()) + deal.deal(DeckReader, (), StartedDeal()) assert not deal.audit.exists() and deal.current().read_by(deal) == () # a fresh confession diff --git a/tests/test_pi_rule_gate.py b/tests/test_pi_rule_gate.py index 33ad565f39..4a5a382b9d 100644 --- a/tests/test_pi_rule_gate.py +++ b/tests/test_pi_rule_gate.py @@ -22,7 +22,7 @@ from agent_comms.task_attachments.owner_notices import RestartNotice from agent_comms.threads import Thread from agent_comms.cli_bridges.hook_read_gate import ReadInput -from agent_comms_hooks.skill import CommanderReader, FleetReader +from agent_comms_hooks.skill import DeckReader from conftest import answer_deal, engaged @@ -38,12 +38,12 @@ def unit_deal(commander_bus): return deal -def test_a_spawned_thread_is_dealt_its_role_s_deck_sources_once(commander_bus): - """The first start deals the registry role's deck sources; a restart keeps the deal.""" - for name, reader in (("claude-code", CommanderReader), ("unit-x", FleetReader)): +def test_every_independent_thread_is_dealt_the_same_sources_once(commander_bus): + """A lead and an untagged peer share the deal; a restart keeps its original audit.""" + for name in ("claude-code", "unit-x"): deal = PiRedeal.of(commander_bus, commander_bus.registry.require(name)) deal.start() - assert deal.current().dealt == tuple(path.resolve() for path in reader.sources()) + assert deal.current().dealt == tuple(path.resolve() for path in DeckReader.sources()) deal.audit.write_text("{}") deal.start() assert deal.audit.exists() @@ -58,7 +58,7 @@ def test_a_retired_package_source_redeals_before_owner_binding(unit_deal): unit_deal.start() - assert unit_deal.current().dealt == tuple(path.resolve() for path in FleetReader.sources()) + assert unit_deal.current().dealt == tuple(path.resolve() for path in DeckReader.sources()) assert not unit_deal.audit.exists() with pytest.raises(RulesUnread): unit_deal.judge_tool("bash") @@ -66,7 +66,7 @@ def test_a_retired_package_source_redeals_before_owner_binding(unit_deal): def test_every_dealt_file_fits_one_default_read_on_every_backend(): """A dealt file over a read tool's cap could never count as read, locking the thread out.""" - sources = CommanderReader.sources() + sources = DeckReader.sources() spans = (ReadTool.default_span(), ReadInput(sources[0]).span()) assert all(span.covers(source) for span in spans for source in sources) @@ -96,7 +96,7 @@ def test_a_compacted_pi_thread_reads_every_dealt_rule_then_writes_its_audit( SelectedToolRequest("call-write", SelectedWriteArguments("notes.md", "x")).judge_under( unit_deal ) - sources = FleetReader.sources() + sources = DeckReader.sources() call("read", {"path": str(sources[0])}).judge_under(unit_deal) # a dealt file is call("read", {"path": str(sources[0]), "limit": 3}).completed_under(unit_deal) with pytest.raises(RulesUnread, match=str(sources[0].resolve())): @@ -105,7 +105,8 @@ def test_a_compacted_pi_thread_reads_every_dealt_rule_then_writes_its_audit( call("read", {"path": str(source)}).completed_under(unit_deal) with pytest.raises(RulesUnread): bash.judge_under(unit_deal) # reading without answering still keeps the gate closed - answered = [engaged(rule.name, rule.path.resolve()) for rule in FleetReader.rules()] + answered = [engaged(source.stem, source) for source in unit_deal.current().dealt + if source.stem in unit_deal.current().due.rules] unit_deal.audit.write_text(json.dumps({"rules": answered, "owner_words": []})) with pytest.raises(RulesUnread, match="then fix the gate"): bash.judge_under(unit_deal) # the audit answers the owner's words too @@ -116,7 +117,8 @@ def test_a_compacted_pi_thread_reads_every_dealt_rule_then_writes_its_audit( unit_deal.audit.write_text( json.dumps( { - "rules": [engaged(rule.name, rule.path.resolve()) for rule in FleetReader.rules()], + "rules": [engaged(source.stem, source) for source in unit_deal.current().dealt + if source.stem in unit_deal.current().due.rules], "owner_words": [{"words": text, "doing": "reading the gate"} for text in words], } ) @@ -240,13 +242,12 @@ def test_gate_can_be_disabled_and_reenabled_without_forging_a_read(unit_deal): assert unit_deal.current().dealt == before -def test_a_bus_with_no_commander_project_deals_a_pi_unit_the_fleet_rules(comms, tmp_path): - """No commander project leaves every thread commanded: a Pi unit is dealt the fleet deck, - as the Claude Code hooks deal it on the same bus.""" +def test_a_bus_without_a_storage_lead_still_deals_the_shared_sources(comms, tmp_path): + """Project storage configuration does not change an independent session's rules.""" unit = comms.registry.declare(Thread("unit-y", frozenset(), Checkout(tmp_path))) deal = PiRedeal.of(comms, unit) deal.start() - assert deal.current().dealt == tuple(path.resolve() for path in FleetReader.sources()) + assert deal.current().dealt == tuple(path.resolve() for path in DeckReader.sources()) def test_every_refusal_opens_with_why_its_deal_was_dealt(unit_deal): diff --git a/tests/test_selected_tool_broker.py b/tests/test_selected_tool_broker.py index 7f831c0efe..6b3b53edc0 100644 --- a/tests/test_selected_tool_broker.py +++ b/tests/test_selected_tool_broker.py @@ -395,10 +395,10 @@ async def test_a_dealt_rule_gate_refuses_the_selected_write_before_its_owner_act the owner callback could claim or write anything.""" from agent_comms.pi_rule_gate import PiRuleGate, RulesUnread from agent_comms.rule_deal import StartedDeal - from agent_comms_hooks.skill import FleetReader + from agent_comms_hooks.skill import DeckReader gate = PiRuleGate(tmp_path / "scratch", tmp_path) - gate.deal(FleetReader, (), StartedDeal()) + gate.deal(DeckReader, (), StartedDeal()) token = secrets.token_hex(32) acted: list[broker.SelectedToolRequest] = [] server = broker.SelectedToolSocket( diff --git a/tests/test_unit_instructions.py b/tests/test_unit_instructions.py index 62c321e4fe..4f927a5c69 100644 --- a/tests/test_unit_instructions.py +++ b/tests/test_unit_instructions.py @@ -1,31 +1,17 @@ -"""Every native unit's system prompt carries the indictment, the voice, every rule under its audience and the pattern index.""" - -import pytest +"""Every native collaborator carries the same source-derived system instructions.""" from agent_comms.commander.package import CommanderPackage -from agent_comms.commander.rule_file import CommanderAudience, FleetAudience, RuleAudience, RuleFile -from agent_comms.nominal.field_codec import MalformedRecord +from agent_comms.commander.rule_file import RuleFile from agent_comms.native_package import OWNER_INSTRUCTIONS from agent_comms.slop import Slop from agent_comms.unit_instructions import UnitInstructions -def test_unit_prompt_carries_every_rule_under_its_audience(tmp_path): +def test_native_prompt_carries_every_shared_rule_once(tmp_path): text = UnitInstructions.prepare(tmp_path / "UNIT_SYSTEM.md").read_text(encoding="utf-8") for source in (OWNER_INSTRUCTIONS, CommanderPackage.indictment(), CommanderPackage.voice(), Slop.catalog() / "README.md"): assert source.read_text(encoding="utf-8").strip() in text, source - grouped = dict(RuleAudience.grouped(RuleFile.packaged())) - assert set(grouped) == {FleetAudience, CommanderAudience} - for audience, rules in grouped.items(): - section = audience.heading + "\n\n" + "\n\n".join(rule.render() for rule in rules) - assert section in text, audience - - -def test_rule_file_without_an_audience_is_refused(tmp_path): - source = CommanderPackage.rules_files()[0].read_text(encoding="utf-8") - path = tmp_path / "undeclared.md" - path.write_text("".join(line for line in source.splitlines(keepends=True) - if not line.startswith("audience:")), encoding="utf-8") - with pytest.raises(MalformedRecord): - RuleFile.read(path) + assert RuleFile.render_packaged() in text + for rule in RuleFile.packaged(): + assert text.count(rule.render()) == 1, rule.name