Skip to content

feat(loop): tri agree - two implementations of one rule, asked the same question - #475

Open
gHashTag wants to merge 10 commits into
loop/rejudgefrom
loop/agree
Open

gHashTag wants to merge 10 commits into
loop/rejudgefrom
loop/agree

Conversation

@gHashTag

@gHashTag gHashTag commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Stacked on #473#471. Merge in order; bases retarget automatically.

The defect class this repository keeps paying for

Every expensive failure here has been one rule written twice, drifting, with nothing comparing them:

  • the boundary rule in three copies — only two knew ## Границы, so seven bees were accused of straying
  • can_start_another in five
  • five SR-00 rules retyped into TypeScript — three disagreed in production
  • eleven more from the rings audit

Yesterday it cost 153 dispatches (#472). The thing that hid it was a comment asserting the two functions could not disagree.

A comment claiming two functions agree is a test that has not been written. This is that test. It does not read the comment and does not reason about the code — it runs both implementations over the rows the system actually holds.

The precedent is already in the tree: t27-parity.mjs does exactly this across languages, 460 cases against the deployed twin. This is the same instrument pointed at two functions in one file, which is where drift is cheapest to create and hardest to see.

The first run corrected the round that motivated it

pair "answered-criteria" - which promised criteria did the bee answer?
  A: unjudgedCriteria (matches by text)
  B: missingVerdictSlots (matches by slot number)

358 row(s) compared, 24 skipped
  agree : 11
  DIFFER: 347

A missed something B found on 196 row(s); B missed something A found on 151.
BOTH directions are populated, so neither side is the rule and picking
one would trade this defect for its mirror image.

    76  sendBack: A=4 B=5 of 5
    44  accept:   A=4 B=0 of 4
    34  accept:   A=5 B=0 of 5

In #472 I wrote that missingVerdictSlots was "the right answer the repository already had." It is not. 156 blocks carry no numbering at all, and slot matching finds nothing covered on every one of those 156 — wiring it would have marked every criterion in 44% of the board unanswered. Corrected at the call site and on that PR.

That is why divergence is reported with its direction. A bare "347 differ" invites picking a winner, and here neither side is the rule.

What it does not claim

Agreement is not correctness. Two implementations can be wrong together and this cannot see that — the same limit rejudge carries and states in its own output. What it rules out is the other case: a divergence sitting in the open behind a sentence saying it cannot happen.

It runs the deployed module, never a copy. Copying the rule into this file would make it the third implementation — the defect it was written to catch.

Calibration

Three cases, each proving a negative:

  • a row neither side could answer is skipped, not agreement — counting it as agreement is how a gate reports health for rows it never looked at
  • an unreadable row accuses neither side
  • a one-way divergence must not claim "both directions", or the sentence stops meaning anything when it does appear
  • agreement is never rendered as proof

node .trinity/loop/selftest.mjs260 passed, 0 failed. Report-only, in the heal chain, exit 2 on divergence.

🤖 Generated with Claude Code

…me question

This repository's most expensive defects are all one shape: a rule written
twice, drifting, with nothing comparing them. The boundary rule had three
copies and only two knew `## Границы`, so seven bees were accused of straying.
`can_start_another` has five. Five SR-00 rules were retyped into TypeScript and
three disagreed in production. The rings audit found eleven more.

Yesterday it cost 153 dispatches, and the sentence that hid it was a comment
asserting the two functions could not disagree. A comment claiming two
functions agree is a test that has not been written. This is that test: it does
not read the comment and does not reason about the code, it runs both
implementations over the rows the system actually holds.

The precedent is already here - `t27-parity.mjs` does exactly this across
languages, 460 cases against the deployed twin. This is the same instrument
pointed at two functions in one file, which is where drift is cheapest to
create and hardest to see.

FIRST RUN, AND IT CORRECTED THE ROUND THAT MOTIVATED IT. 358 rows compared, 11
agree, 347 differ - and the divergence runs BOTH ways: A missed something B
found on 196 rows, B missed something A found on 151. I had written that
`missingVerdictSlots` was "the right answer the repository already had". It is
not: 156 blocks carry no numbering at all and slot matching finds NOTHING
covered on every one of them, so wiring it would have marked every criterion in
44% of the board unanswered. Corrected at the call site and on the PR.

So divergence is reported with its DIRECTION. A bare "347 differ" invites
picking a winner, and here neither side is the rule.

AGREEMENT IS NOT CORRECTNESS - two implementations can be wrong together and
this cannot see that, the same limit `rejudge` carries and states. What it
rules out is the other case: a divergence sitting in the open behind a sentence
saying it cannot happen.

Three calibration cases, each proving the negative: a row neither side could
answer is not agreement, an unreadable row accuses nobody, a one-way divergence
must not claim both directions, and agreement is never reported as proof.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gHashTag and others added 2 commits September 6, 2026 12:49
`tri agree` measures it; the box the operator reads is rendered by snapshot.mjs
from its own list, so without this the number lived in a file nobody opens.

The row does NOT fall to zero when one side is fixed. `unjudgedCriteria` and
`missingVerdictSlots` will keep disagreeing on the 156 unnumbered blocks until
one of them is deleted or wired properly - which is exactly the point. A rule
implemented twice is a liability for as long as it exists, not only on the day
it bites, and a metric that vanished on the fix would say the opposite.

Read from the recorded reading rather than recomputed, the rule `provenCounts`
already follows: the comparison asks production and takes about a minute, and
snapshot runs on the tick path. An absent reading renders "not measured", never
zero, and does not write the anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns at once

`tri agree` had one pair and one shape - two functions in one TypeScript file.
A gate that only works on the case it was written for is not a gate. This adds
the hardest shape available: one rule, three languages, three processes.

"Which paths may this bee touch?" is answered in the loop's own JavaScript, in
the deployed TypeScript, and in the SR-00 Swift ring. A FOURTH copy sits in
`queen-brief-shape.ts`, whose comment justifies itself by saying the other
TypeScript copy "is module-private" - that copy is exported now. The stated
reason expired and nobody noticed, which is the argument for testing the claim
rather than reading it.

This rule has already cost real work: two copies knew `## Границы` and one did
not, and seven bees were accused of straying outside a boundary they had
honoured. The heading was added everywhere. Nothing was added that would notice
the next divergence.

THE ANSWER IS AGREEMENT, and that is worth reporting plainly. All three return
identical path lists on all 521 issue bodies that carry a boundary section, and
on 18 adversarial shapes besides - backticked paths followed by commas, nested
brackets, tab-separated markers, markdown emphasis, two paths on one line. The
Swift comment predicts that sequential stripping mishandles a backtick before a
comma; it does not, because the TypeScript trailing class contains the backtick.
So this pair is a regression guard rather than a find, and it says so.

A MISSING COMPILER IS NOT AGREEMENT. With `swiftc` unavailable the run reports
521 rows unreadable, names the side that would not build, claims nothing, and
exits 3 - not 0. A pair that compared zero rows no longer counts as a pair that
ran, because "0 diverging rows" must never be able to mean "we could not look".

The TypeScript side is EXTRACTED from the shipping ref rather than
reimplemented. A reimplementation would be the fifth copy of this rule, in the
file whose whole purpose is to complain about there being four.

One build per script, shared by both boundary pairs: building the ring twice to
answer two questions about the same rows could produce two different answers
for one run, which is the confusion this tool exists to remove.

Two calibration cases: every declared pair is runnable for its kind and no two
share a name, and a side that could not be built classifies as unknown rather
than agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag

gHashTag commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

Second and third pairs: one rule, three languages, three processes

A gate that only works on the case it was written for is not a gate. This adds the hardest shape available.

"Which paths may this bee touch?" is answered in the loop's own JavaScript, in the deployed TypeScript, and in the SR-00 Swift ring. A fourth copy sits in queen-brief-shape.ts, whose comment justifies itself with "that copy is module-private" — it is exported now. The stated reason expired and nobody noticed, which is the argument for testing the claim instead of reading it.

This rule has already cost real work: two copies knew ## Границы and one did not, and seven bees were accused of straying outside a boundary they had honoured. The heading was added everywhere; nothing was added that would notice the next divergence.

The answer is agreement, reported plainly

pair "boundary-js-vs-ts"      521 compared   agree 521   DIFFER 0
pair "boundary-ts-vs-swift"   521 compared   agree 521   DIFFER 0

All three return identical path lists on all 521 issue bodies with a boundary section, and on 18 adversarial shapes besides — backticked paths before commas, nested brackets, tab-separated markers, markdown emphasis, two paths on one line. The Swift comment predicts sequential stripping mishandles a backtick before a comma; it does not, because the TypeScript trailing class contains the backtick.

So these are regression guards, not finds — and the output says so rather than letting a clean line read as a discovery.

A missing compiler is not agreement

With swiftc unavailable:

0 row(s) compared
  unreadable, so NOT counted against either side: 521
  NOT BUILT, so nothing is claimed about it: swift
NOTHING was compared. An unbuildable side is not an agreeing one.
exit=3

A pair that compared zero rows no longer counts as a pair that ran — "0 diverging rows" must never be able to mean "we could not look".

Notes

  • The TypeScript side is extracted from the shipping ref, not reimplemented. A reimplementation would be the fifth copy of this rule, in the file whose purpose is to complain about there being four.
  • One build per script, shared by both boundary pairs: building the ring twice for two questions about the same rows could yield two answers for one run.
  • Two new calibration cases: every declared pair is runnable for its kind with no duplicate names, and an unbuilt side classifies unknown.

selftest 262 passed, 0 failed.

The operator's golden rule is that the bees work, and start again the moment
they finish. Twenty instruments in this directory count issues, verdicts,
briefs, branches and disagreements. Not one of them measured whether the swarm
was WORKING, and it had been false for a long time:

  minutes with ZERO bees working : 356 of 721  (49%)
  minutes with every bee working : 268         (37%)
  median gap between bursts      : 18.4 min against a 5-minute tick
  gaps under a minute            : 1 of 18

WHY THE DASHBOARD COULD NOT SHOW IT. `bees running (of 4)` has been on the
board for weeks. It is an INSTANT - one sample per iteration - of a quantity
that turns out to be bimodal, and it read 4 as often as 0. A rate needs a
window; an instant needs none, which is exactly why the instant is the one that
gets measured.

It also names what stopped the rounds, from the service log: 135 of 136 recent
round failures were `GitHub returned 403`, and one was `deadlock detected` -
the second of which nothing else in this repository has ever mentioned.

IT RESTARTS NOTHING. Idleness has several causes - a failing round, an empty
backlog, a full volume, a refused key - and the cure differs for each, so this
names the cause and stops.

Three calibration cases, and one of them caught a real defect in this file
before it shipped: a twenty-minute window sampled inclusively holds twenty-ONE
one-minute buckets, so a half-idle window measured 52%. The loop is `t < to`
now. That is what those cases are for.

The other two pin the parts that would otherwise flatter: overlapping bees are
one burst so no gap is invented between them, and gaps longer than the tick are
reported as "rounds are running and starting nothing" - a different fault from
"rounds are rare", with a branch that refutes it when gaps are within cadence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gHashTag

gHashTag commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

tri idle — the one number nobody was measuring

The operator's golden rule is that the bees work, and start again the moment they finish. Twenty instruments here count issues, verdicts, briefs, branches and disagreements. None measured whether the swarm was working — and it had been false for a long time:

minutes with ZERO bees working : 356 of 721  (49%)
minutes with every bee working : 268         (37%)
median gap between bursts      : 18.4 min against a 5-minute tick
gaps under a minute            : 1 of 18      <- the golden rule lives here

136 round(s) FAILED in the log window:
   135  GitHub returned 403
     1  deadlock detected

The cause is fixed in #476. This is the instrument that would have surfaced it on day one.

Why the dashboard could not show it

bees running (of 4) has been on the board for weeks. It is an instant — one sample per iteration — of a quantity that turns out to be bimodal, and it read 4 as often as 0.

A rate needs a window; an instant needs none, which is exactly why the instant is the one that gets measured.

It restarts nothing

Idleness has several causes — a failing round, an empty backlog, a full volume, a refused key — and the cure differs for each. It names the cause and stops.

A calibration case caught a real defect before it shipped

A twenty-minute window sampled inclusively holds twenty-one one-minute buckets, so a half-idle window measured 52%. The loop is t < to now. That is what those cases are for.

The other two pin the parts that would otherwise flatter: overlapping bees are one burst so no gap is invented between them, and gaps longer than the tick are reported as "rounds are running and starting nothing" — a different fault from "rounds are rare" — with a branch that refutes it when gaps are within cadence.

selftest 265 passed, 0 failed.

gHashTag and others added 3 commits September 6, 2026 15:32
`make queen-core-sync` compares the eleven policy files that exist twice - the
ring the app compiles and the copy the Docker build compiles for Linux - byte
for byte. Its own comment states the stakes: "a policy that differs between them
is two arbiters of the same rule."

It appeared in no workflow, and one of the thirteen files had already drifted.

Writing a check is the easy half. The half that decides whether it protects
anything is whether something runs it without being reminded, and nothing in
this repository was asking that question. Sixty-eight targets, eleven invoked by
a workflow.

A LIST OF FIFTY-SEVEN WOULD BE IGNORED BY WEEK TWO, correctly: most of them
should be local. `make` builds an app, `run` launches it, `relaunch` needs a
window server. So this asks two questions instead of one - does the name promise
to refuse something, and could a runner execute it at all - and reports nine
rather than fifty-seven.

THE TOOL'S OWN OUTPUT CAUGHT A DEFECT IN IT before it shipped. `check` and
`verify` appeared in the portable group, which is wrong: both have NO recipe at
all - they are a list of other targets - and reading only the recipe found no
`swiftc` to object to. Following prerequisites moves them where they belong, and
it is why the count went from eleven to nine. Read your own report against two
entries you already know the answer for.

It reads the SHIPPING REF, not this checkout, which is hundreds of commits
behind: a gate wired upstream would otherwise read as unwired here, an
accusation produced entirely by standing in the wrong place.

Three calibration cases: a target with no recipe inherits what its prerequisites
reach, a cycle does not hang the audit, and a plain name is not reported as a
gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more misclassifications of the same family, both caught by reading the tool's output against targets whose answer I already knew:

check-bypass came out portable. It is one line - `$(MAKE) check` - and following only prerequisites saw an empty recipe. Its own echo says 'never for CI', which is how it was caught: the tool disagreed with the target's description of itself. Sub-make calls are followed now.

drift-guard came out portable and then ran for four minutes without finishing. It is `bash run_chat_sse_e2e.sh` with an env var, and the Swift compiler is inside the script. A scan of make recipes sees one layer; a script is the second.

So there is a third answer now - opaque - rather than a guess. A gate that shells out is reported as unjudgeable from here, which is what it is. The portable list went 11 -> 9 -> 6 -> 3 as each layer was followed, and every step of that shrinking was a false accusation withdrawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sub-make call reaches its target; a target that shells out to a script is opaque and names the script rather than guessing. Both cases fail against the version that shipped an hour ago.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gHashTag and others added 3 commits September 6, 2026 15:57
The same question asked of npm scripts found the t27.ai dashboard: trios/apps/website carries a 194-check review-lifecycle contract that imports the real page module and pins the states against what the server publishes, and the string 'apps/website' appeared in NO workflow.

MOST UNWIRED SCRIPTS ARE LEGITIMATELY UNWIRED, and that shaped the classes. A flat first pass listed nineteen and eighteen were fine: aggregates CI deliberately splits into per-group jobs, an alias for a wired script, a helper other scripts call, a mutating lint:fix, an interactive watch. Reported flat, the one row worth reading was buried.

And the one it cannot decide, said out loud: a script whose WORK is wired under a different command. `lint` is `biome check` while CI runs `biome ci .` - the linting happens, the name never appears. Same shape as drift-guard, whose script the macOS job already runs. Named rather than silently dropped, because guessing either way has been wrong.

test:* is deliberately outside the word list: including it buries the report, and that coverage already has a better guard in run-test-group.test.ts. Stated in the output so the count cannot imply completeness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ately blind

Six cases. Two of them assert not-a-gate for test:* - the word is outside the gate list on purpose - and the case that first claimed 'interactive' for test:watch was wrong, not the code. The classifier never sees a test:* script at all, and the audit says so in its own output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`make check` names its own gate suite in one line of prerequisites. Reading it corrected this file badly: of thirteen unwired gates the repository declares, ELEVEN were classified not-a-gate here, because they are named after their SUBJECT and not their function - t27-rings, type-floor, recipe-backticks, variant-fence, vendor-step, skill-frontmatter.

t27-rings runs the 460-case parity between the generated ring and the policy binary, plus a Verilog simulation, and this file called it not a gate.

The word list stays as the fallback for targets outside the suite, where nothing has declared anything. The report now prints the declaration first: 24 declared, 8 in a workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant