Skip to content

feat(tri): CLI wave — mutate, pr ready --wait/--merge, synth/sweep area - #2223

Merged
gHashTag merged 19 commits into
masterfrom
feat/tri-cli-wave
Aug 19, 2026
Merged

feat(tri): CLI wave — mutate, pr ready --wait/--merge, synth/sweep area#2223
gHashTag merged 19 commits into
masterfrom
feat/tri-cli-wave

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Rebuilt from PR #2177 with L1-traceable commits (each carries Closes #2222).

Commands: tri mutate (negative-control mutation), tri pr ready (merge gate:
baseline-aware verdict, --wait/--poll/--merge), tri synth area (last-stat-block
yosys extraction — the 3x inflation guard), tri sweep area, tri red, tri gates,
plus rtl-check build hardening. All developed and battle-tested in the trinity-fpga
measurement campaign.

Closes #2222

🤖 Generated with Claude Code

…Refs #2176)

cargo build -p tri failed on main: dlc10 embeds a bitstream with
include_bytes! that was committed to a feature branch and never to main.
Nothing caught it because none of this repository's 35 workflows built
the crate, while an autonomous loop kept committing into cli/tri/src/.

tri rtl check runs the five structural checks t27.ai offers, locally. It
names the yosys version beside the cell count, because that number is
version-dependent: 0.33 reports 45 cells where 0.65 reports 49 on the
same design, while wires and flip-flops agree exactly.

Closes #2222
)

Eighteen active workflows across three repositories have never produced a
green run, consuming 8182 runs between them. The largest are 1705, 1541 and
1224 runs with zero successes.

This was a hand-run loop of gh api calls three times before it became a
command. It reports and disables nothing: choosing between fix,
workflow_dispatch-only and delete belongs to whoever owns the workflow.

Verified against t27: 4 workflows, 1565 runs, matching the manual count.

Closes #2222
)

Written because of a specific failure of mine, not a hypothetical. The
publisher for t27.ai failed six consecutive times, the site served
hours-old content, and site-live-gate.yml caught it correctly and went red
five times on my own commits. I read it zero times and found the outage by
accident when a page I had just published returned 404.

Detection was never the problem. Reading it was.

Measured on trinity: 27 workflows red on main, several since February.
A saturated streak prints as '30+' rather than '30', because the count is
bounded by the page size — the same silent truncation this command exists
to surface, which appeared in the command itself first.

Closes #2222
Two vacuous checks turned up within an hour of each other. A workflow
step read an 8-digit date out of `yosys -V`, which contains no date, so
the guard skipped and the step passed without comparing anything. A
verifier had a lookup table on both sides of the identity it tested, so
flipping an entry cancelled with itself and left every claim green.

Neither was found by reading. Both were found by changing a constant and
noticing nothing went red. This automates that: perturb one numeric
literal at a time, re-run the checker, report the ones it did not notice.

Refuses to run on a file with uncommitted changes -- it rewrites the file
and restores it, and an interrupted run must be recoverable with git.

Dogfooded on the verifier that prompted it: 28 survivors, all but one in
prose. Masking comments and strings cut that to 17, and the one real
finding underneath was a quantiser compared only against itself, which
would have made the whole inertness claim pass vacuously.

Columns are reported because two identical literals on one line are
otherwise indistinguishable; I read such a report, hand-checked the wrong
one, and wrongly concluded the tool was lying.

Closes #2222
A measurement taken against a file this command left perturbed is not a
measurement. That is not hypothetical: a perturbed constant survived a
hand-run mutation on this machine, was read back as if it were the real
value, and produced a written-up finding that did not exist -- an
'unreachable top binade' that the clean file does not have.

Each mutant now re-reads the file after restoring it and fails loudly
with the git command to recover, rather than continuing quietly.

Closes #2222
The clean-tree requirement was safe and was the wrong trade. Every
mutation run on work-in-progress needed a throwaway commit first, and a
throwaway commit is how a 'wip-for-mutation' subject reached a
repository whose format gate rejects it -- twice in two iterations, the
second time an hour after I wrote the lesson down.

A sibling backup gives the same recovery guarantee without asking the
caller to commit anything. Git cleanliness is still reported, because
git checkout is the nicer recovery path when it is available, and the
backup is removed on success.

Verified on a deliberately dirty file: the run completes, the file comes
back byte-for-byte, and no backup is left behind.

Closes #2222
Verifying the source came back byte-for-byte was not enough. Most
mutations here preserve the file's LENGTH -- 5 becomes 6, 16 becomes 17
-- and Python decides a .pyc is current by comparing the source's
(mtime, size). Restore inside the same filesystem second and both match,
so the interpreter serves bytecode compiled from the mutant.

Measured, not theorised: a benchmark's format table read back as e5m11
and then e6m10 on consecutive runs while the file on disk said e5m10
both times. Clearing __pycache__ made every assertion pass.

Second contamination of this kind in two iterations -- first the file
itself, now its cache. The restore has to reach derived artefacts or the
next measurement in that session is against a mutant nobody can see.

Closes #2222
Written after merging a pull request whose language audit was red. The
failure was in the list and I read a summary line I had written myself
instead of the list. The gate was correct; I was not.

The judgement that matters is not 'is anything failing' -- in these
repositories something always is -- but 'is anything failing HERE that
is not already failing everywhere else'. So it classifies every failure
against the default branch and the last few merged PRs, and ends with
one unambiguous verdict line.

Pending checks produce WAIT rather than a verdict: a verdict computed
from a partial list reads exactly like a complete one.

Checked against the two real cases: #588 (safe) and #832, the one I
merged red -- it names `checks` as the single failure unique to that PR
and says DO NOT MERGE.

Closes #2222
Written after getting the same task wrong twice in one session, both
times producing a plausible number instead of an error.

First: the stat parser matched `Number of cells:`, which yosys 0.33
prints and 0.65 does not. It found nothing and reported zero, and zero
looks exactly like a small design.

Second: the yosys output was captured into a shell variable and printed
with `echo`. zsh's echo interprets backslash escapes and yosys writes
identifiers with a leading backslash, so `\tern_node` became a tab and a
`\c` truncated the stream. Every count came back zero again, from data
that was correct on disk.

Now the counts are parsed by cell NAME (which did not change between
versions), both streams are read, the yosys banner is printed beside the
numbers, and a run that never reaches a stat block is an error carrying
yosys's own message rather than a row of zeros.

Reproduces the hand-measured layer numbers exactly: 4299/0/254/220 and
3906/3/123/204.

It also states what it does not measure. `ltp` is not a frequency
substitute: it counts topological hops in netlists whose structure
differs between designs, so it does not compare across them -- measured
today, where it reported 213 hops for a one-adder scale path and 20 for
a 32x16 multiplier.

Closes #2222
I merged a pull request while this command's own output said WAIT -- ten
checks still running -- because the merge was in the same batch as the
readiness check, so the verdict gated nothing.

The polling loop that fed it had its own bug: it counted rows matching
'pending' and exited on zero. Zero rows also means the checks have not
STARTED, which is exactly the state it hit.

So: --wait blocks until the list is both non-empty and quiet, and an
empty list is treated as 'not started' for four rounds before being
believed. The verdict can no longer be computed against a list that has
not appeared yet.

Nothing broke on main this time. That was luck, not process.

Closes #2222
The combined form
  [[.check_runs[]|select(.status!="completed")]|length, [...]|length]|@TSV
failed with 'expected an object but got: array' the first time --wait
ran, on the very PR it was added to gate. A wait loop that errors out is
worse than no wait loop: it turns a gate into an interruption.

Two plain queries instead. Verified against gHashTag/trinity-fpga#592.

Closes #2222
Written after two folds in one afternoon, both of which reported a
number rather than an error.

A layer whose operand memory had no write port: yosys propagated the
never-written memory as constant and pruned the design. 12 logic cells
for a 64-tap multiplier layer, and area flat across fan-in.

The same layer with one memory location per lane: reading N per cycle is
N read ports, so muxes rather than block RAM. 23052 cells on a
7680-cell part, zero RAM inferred.

Neither is an error and both were caught only because a human found the
number implausible. This checks the shape: sweep the parameter and say
plainly when the area does not respond to it, or responds far too
steeply. Both real cases are unit tests, with their real numbers.

It stays quiet on proportional growth, because a detector that fires on
the ordinary case stops being read.

Closes #2222
The first time --wait met a TLS handshake timeout it propagated the
error, the caller's merge ran anyway, and the gate protected nothing --
the third time in this project a verdict has failed to gate.

Now a failed poll is retried up to five times before giving up, and a
failed FINAL read makes the verdict WAIT rather than an invented clean
list. Unknown is not zero.

Closes #2222
Inserting a new variant used `Red {` as the anchor, which put the new
command BETWEEN Red's doc comment and Red itself. Clap then read that
comment as the new command's description and left Red with none:

  mutate   What is failing on the default branch right now, and since
           when. Find the constants in a checker that nothing checks
  red      (blank)

Four commands were added against that same anchor this session, so the
help text has been wrong since the first of them. Nothing behaved
differently -- it just described itself incorrectly, which for a set of
commands whose whole point is being readable is not a small thing.

Closes #2222
The verdict cannot gate anything if the caller puts `gh pr merge` in the
same batch as this command. It prints WAIT, the merge runs anyway, and
nobody reads the line.

That happened four times in one session -- including on the pull request
that added the waiting mode, and again on the one that made the wait
survive network failures. Each time the fix addressed the tool and the
next failure came from the same place: the human batching past it.

So the action moves inside the verdict. With --merge, the command merges
on 'safe to merge' and refuses on WAIT or DO NOT MERGE, telling the
caller to re-run with --wait. There is no longer a gap to batch through.

Closes #2222
@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-19 12:57:33 UTC

Summary

Status Count
Total Open PRs 29
PRs with Failing Checks 12
PRs with All Checks Green 17
READY 10
FAILING 12
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=a8a5da547cb3 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-19 14:32:56 UTC

Summary

Status Count
Total Open PRs 29
PRs with Failing Checks 11
PRs with All Checks Green 18
READY 9
FAILING 11
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=54d19991b0c2 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

@github-actions

Copy link
Copy Markdown
Contributor

PR Dashboard

Generated at: 2026-08-19 19:08:48 UTC

Summary

Status Count
Total Open PRs 31
PRs with Failing Checks 14
PRs with All Checks Green 17
READY 9
FAILING 14
PENDING 0

Seal Status

  • ⚠️ STALE -- sha256(compiler.rs)=54d19991b0c2 != manifest seal=87e5cbd3ad94.
    The committed NMSE numbers were certified against an older compiler.rs.
    Run scripts/reseal-check.sh locally for the two-step reseal command (advisory; not a merge gate).

@github-actions

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tri CLI: mutate, pr ready --wait/--merge, synth area, sweep area, rtl-check hardening

1 participant