An agent patch should merge only when every strong property in its locality passes on sealed fixtures. A flaky-test freeze removes that test from the quorum. It does not convert the result to a pass. If open freezes exceed a debt cap, the agent queue stops for every patch, including patches that never touched the flaky code.
Weak checks still matter. They catch shape errors early. They are not a substitute for a strong property that failed, flaked, or was left out of the vote.
What the gate refuses to count as a pass
Agent diffs fail in three different ways. A single green job hides which one you hit.
- A weak property checks structure: required keys, types, non-empty bodies, stable status codes.
- A strong property checks an invariant the patch can actually break. Idempotence under replay, monotonic counters, and error responses that must not gain fields are strong. A key-presence check is not.
- A flake is a disagreement between two runs of the same strong property on the same sealed fixture. Disagreement is not a pass. It is also not, by itself, a reason to reject lines that the property does not observe.
The merge rule is a quorum, not a tally of green dots. Weak properties may be required. They never fill a gap left by a strong property that cannot vote.
Step 1: Catalog properties by locality and strength
Keep the catalog beside the suite. Each row names the predicate, the path prefixes it observes, the fixture directory it may read, and the strength.
# proposal catalog — not a record of a production run
properties:
- id: resp.no_secret_fields
strength: strong
locality: [api/handlers, api/serializers]
fixtures: fixtures/responses/
- id: resp.keys_present
strength: weak
locality: [api/serializers]
fixtures: fixtures/responses/
- id: counter.monotonic_under_replay
strength: strong
locality: [store/counters]
fixtures: fixtures/replays/
Locality is a path-prefix list, not a guess about intent. Intersect the diff's touched paths with those prefixes. Strong properties outside the intersection stay out of this patch's quorum. They still belong on a scheduled full pass that is not part of the agent merge button.
A patch that only touches store/counters cannot clear the gate with serializer key checks. The required vote is the strong set whose prefixes overlap the diff. If that overlap is empty, do not invent a locality to force a green result. Reject auto-promotion and hand the diff to a reviewer.
Step 2: Seal fixtures before the patch is applied
Fixtures are inputs. If the candidate diff can edit them, a failing predicate can be silenced by editing the data instead of the code.
Compute a tree digest in the scoring worktree before apply, then compare it with the digest committed in the repo. This seal is an allow-or-deny check on whether a freeze may be opened. It is not a score column, and it is not a replay log.
# unexecuted GNU findutils example — scoring worktree only
find fixtures -type f ! -name '*.seal' -print0 \
| sort -z \
| xargs -0 sha256sum \
| sha256sum > fixtures.seal
cmp -s fixtures.seal fixtures.seal.expected
cmp exits non-zero when the tree drifted. Call that a fixture fault. It is not a product failure, and it is not a flake. Do not open a freeze on a seal mismatch. Amend fixtures only in a separate reviewed change that updates fixtures.seal.expected in the same commit.
The agent job may read fixtures. It does not get a rewrite pass on that directory. A later audit can recompute the digest without replaying the draft.
Step 3: Classify two runs, then exclude freezes from the vote
Run each locality-matched strong property twice. Same command, same worktree, no fixture rewrite between runs. The sketch below is unexecuted. It states no timings and no flake rate.
# unexecuted proposal — stdlib only
import hashlib, subprocess
from pathlib import Path
DEBT_CAP = 5 # policy choice, not a measured default
def tree_seal(root: Path) -> str:
rows = []
for path in sorted(p for p in root.rglob("*") if p.is_file()):
if path.name.endswith(".seal"):
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
rows.append(f"{digest} {path.as_posix()}")
return hashlib.sha256("\n".join(rows).encode()).hexdigest()
def run_once(prop_id: str) -> int:
return subprocess.call(["python", "run_property.py", prop_id])
def classify(prop_id: str) -> str:
first, second = run_once(prop_id), run_once(prop_id)
if first == 0 and second == 0:
return "pass"
if first == second:
return "fail"
return "flake"
def quorum(results: dict, ledger: list) -> str:
frozen = {row["property_id"] for row in ledger if row["state"] == "open"}
if len(frozen) > DEBT_CAP:
return "stop_queue"
strong = [pid for pid, meta in results.items() if meta["strength"] == "strong"]
voting = [pid for pid in strong if pid not in frozen]
if not voting:
return "reject"
for pid in voting:
if results[pid]["state"] != "pass":
return "reject"
weak_failed = [
pid for pid, meta in results.items()
if meta["strength"] == "weak" and meta["state"] == "fail" and pid not in frozen
]
if weak_failed:
return "reject"
return "promote"
Normalize paths to repo-relative form before you hash if you implement tree_seal. The sketch uses as_posix() and does not strip the worktree prefix. Two machines can otherwise seal the same bytes differently.
Read classify literally. Two zero exits may vote yes. Two equal non-zero exits are a stable failure: reject the patch, and do not freeze it. Unequal exits, including unequal non-zero codes, are flakes in this sketch because the failure mode did not reproduce. Hold the patch. A human may open a ledger row only after the same split shows up on the unmodified tree with the same seal. Until that row exists, the property still votes, and a flake vote is a rejection.
An open freeze omits the property from voting. The report must say excluded. It must not say passed. If every matched strong property is frozen, voting is empty and quorum returns reject. An empty strong quorum is a rejection. The debt cap is a second brake for the rest of the queue, not a way to paint this patch green.
Store the freeze as data, not as a skip decorator in the test file.
{
"property_id": "counter.monotonic_under_replay",
"state": "open",
"seal": "<tree digest at open>",
"reason": "exit mismatch across two runs on sealed fixtures",
"opened_against_commit": "<git sha>",
"retire_by_commit_count": 30,
"owner": "unassigned"
}
retire_by_commit_count is a budget you choose. It is not evidence that the flake will stop. When the budget is spent, remove the row from the open set by fixing the property or by deleting it in a reviewed change. Do not let an agent job extend its own row. A spent budget can feed the same debt check as an extra open row. Expiry is only an input to that check. The rule this workflow adds is the vote: a live freeze cannot count as a pass, and too many live freezes halt patches that never touched the flaky property.
Step 4: Stop the agent queue when debt exceeds the cap
Per-patch exclusion is not enough. Open freezes can pile up until the strong set is mostly outside the vote. Weak checks then look like a suite.
Put the cap in gate config. The sketch uses five open rows. Set that number from your own tolerance for a halted queue. This article does not report a measured flake rate, and five is not a published default. If open rows exceed the cap, return stop_queue for every agent merge. A human override stays outside the agent path and should name the ledger ids it ignores.
Wire the job to one table. The table is the contract. The script is only an illustration.
| Two-run result | Seal | Ledger | Gate action |
|---|---|---|---|
| pass / pass | match | property not listed | vote yes |
| fail / fail | match | none | reject; do not freeze |
| split exits | match | none | hold; open a freeze only after a clean-tree split on the same seal |
| any | mismatch | any | fixture fault; no freeze |
| pass / pass | match | property open | exclude from the vote; not a pass |
| any | match | open count above cap | stop the agent queue |
| any strong set | match | all matched strong rows open | reject; empty quorum |
A freeze never appears in the vote-yes row. A stable failure never opens a ledger row. A seal mismatch never becomes a flake.
Where drafting and scoring should split
Drafting a patch and scoring it are different jobs. One process should not do both. The draft context will prefer a command that is easy to satisfy, and the ledger will then store a story instead of a vote.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode's free model access is a reasonable place to draft a candidate diff and a first cut of a predicate. MonkeyCode's free server option is a reasonable place to run the seal check and the double property run, apart from that draft session. No model name, quota, hardware shape, duration, or uptime figure is stated here, because none was supplied. The quorum rule does not need those facts.
If that server is unavailable, run the same seal and quorum steps on an isolated runner you already trust. Do not score inside the draft session. Keep the ledger format identical so promotion records stay comparable across runners.
Before a new strong property can vote, ask the draft path for a patch that deletes the invariant and changes nothing else. The double run should return fail on that patch and pass on the unmodified tree, both against the same seal. If the deleted invariant still passes, keep the predicate out of the catalog. That is a review procedure, not a performance result.
Limitations
These sketches are unexecuted proposals. They will not catch a property that is stably wrong, and they do not estimate how often a two-run split appears.
- Two runs miss rare flakes. A third run costs more and still fails to prove stability.
- Path prefixes miss cross-module effects through globals, plugins, or generated code. Keep a scheduled full strong pass outside the agent merge path.
- A debt cap blocks useful patches when nobody retires ledger rows. That block is intentional. Skip the cap if
ledger.jsonhas no owner. - A fixture seal does not pin clocks, network calls, or process-global randomness in product code. Control those in the runner before you trust a
pass. - A diff can add the only property it satisfies. Review new catalog rows with the patch. Do not auto-promote a change that introduces its own voting predicate.
-
tree_sealas written is sensitive to path spelling. Hash repo-relative paths, or two worktrees will disagree on an unchanged tree.
Who should skip this
Skip the quorum if the suite is still one end-to-end script. Extract one invariant before you build a catalog. Skip the debt cap if you cannot name an owner for freezes. A halt that is overridden every day is noisier than a documented exclusion. Skip hosted scoring if policy forbids sending the repository, fixtures included, to that host. The seal, the table, and the ledger still run in a local isolated worktree.
Leave the promotion bit in the ledger, not in the draft transcript. A predicate proposed on a free model path still has to survive the double run on sealed fixtures before it earns a vote.
Top comments (0)