The hardest part of an AI coding loop is not getting code written. It is the moment after the agent says "done".
When one agent writes the code and another agent reviews it, you have not removed the trust problem — you have just moved it. The reviewer reads the diff, reads the tests, reads the ticket, and produces a verdict. If the ticket says "spacing should be 12vp" and the screenshot looks about right, the verdict is "pass". Two auditors can look at the same delivery and disagree, and nothing in the tooling forces that disagreement to resolve.
That is the gap I tried to close in DwGuard-Dsh-Hmos, a plugin for DeepSeek Harness that orchestrates HarmonyOS app delivery: DSH freezes a four-dimension acceptance pack (function / aesthetic / performance / accessibility), runs an adversarial pre-review before dispatch, hands the work to DevEco Code in Goal mode, then audits the delivery with independently gathered evidence.
The change I want to write about is small in surface area and large in consequence: acceptance criteria can carry a machine-checkable assertion, and the audit gate recomputes the verdict instead of trusting prose.
My criteria used to be a sentence plus a verification method:
statement: 冷启动到首帧 ≤ 800ms (cold start to first frame ≤ 800ms)
verify: Profiler 启动分析毫秒数 (milliseconds from the profiler)
And an audit check was a result plus free-text evidence:
result: pass
evidence: 3 runs, 700/720/760ms, screenshot artifacts/perf.png
Three problems showed up immediately:
evidence is free text. No tool can tell whether "1200ms" satisfies "≤ 800ms" without a human eyeballing it.
So the audit was still a person voting, with extra steps.
The fix is an optional assertion on each criterion:
{
"id": "P-01",
"dimension": "performance",
"statement": "Cold start to first frame ≤ 800ms",
"verify": "ms reported by the startup profiler",
"weight": "must",
"ruleId": "DW-PERF-01",
"assertion": {
"matcher": "lte",
"expected": 800,
"unit": "ms",
"source": "startup profiler"
}
}
Eight matchers cover the criteria I actually write:
| matcher | meaning |
|---|---|
equals |
numeric comparison when both sides are numbers, exact string otherwise |
contains |
substring, case-insensitive |
regex |
pattern (unanchored — add ^$ yourself) |
range |
within [min, max] , inclusive |
gte /lte |
numeric greater/less or equal |
oneOf |
equals one of a list |
present |
non-empty |
The audit side then must report an actual value next to the verdict:
{
"criterionId": "P-01",
"result": "pass",
"evidence": "three cold starts: 700/720/760ms; screenshot artifacts/perf.png",
"actual": "720ms"
}
One detail matters more than it looks: numeric matchers take the first number inside actual. That means measured values can keep their units — "12vp" parses as 12, "720ms" as 720, "4.9:1" as 4.9. The assertion compares numbers; the report stays human-readable.
An optional field that nobody enforces is decoration. So the gate has opinions:
| situation | result |
|---|---|
criterion has an assertion, check says pass , but noactual |
error — you cannot pass an assertion you never ran |
criterion has an assertion, check says pass , assertion recomputes tofail |
error — audit refused; this is the self-contradiction case |
criterion has an assertion, check says fail , assertion recomputes topass |
warning — something else may have failed it; ask for review |
criterion has an assertion, check says unverified withoutactual |
warning (the existing must-gate already errors) |
| criterion has no assertion | unchanged behaviour: human verdict + evidence |
the assertion itself is invalid (unknown matcher, range with no bounds, bad regex) |
the spec refuses to freeze withE_CRITERION_ASSERTION_INVALID |
Plus a nudge at authoring time: a must criterion whose statement contains numbers but carries no assertion gets W_CRITERION_NO_ASSERTION, because that is exactly where two auditors will disagree later.
The subtle rule is the third one. Saying "fail" when the number passes is not always wrong — a screenshot can show a layout bug that a timing assertion cannot see. But the gate should make that mismatch visible instead of silently accepting whatever the reviewer wrote.
Because the verdict is now recomputed, the reports gain a column each:
spec.md and the Goal brief gain an audit-<n>.md gains And audit-<n>.json now contains checks[].assertionVerdict, which means CI can fail a build on verdict: fail without parsing markdown. That was the whole point of making the report consumable.
I want to be careful here, because "machine-checkable acceptance" is easy to oversell.
dumpsys reading, a layout tree. Evidence files get copied, hashed, and recorded, but the assertion only decides pass/fail from the number.actual is the same process being audited, an agent can satisfy the letter. That is why the audit playbook insists on externally observable sources — hidumper render stats and hitrace for performance, layout bounds for spacing — and treats in-app timing instrumentation as auxiliary evidence, never the sole basis.
The change shipped as 0.5.0 and the suite went from 49 to 55 tests, all green (six new ones specifically about assertions: accepting valid, rejecting invalid, recomputing, refusing contradictions, refusing a pass without actual, and keeping old packs unchanged).
The real payoff is not "the AI is now trustworthy". It is that disagreement became resolvable. When two auditors disagree now, there is a file with a number and a rule that says whether it passes. Either the number is wrong, or the threshold is wrong, or the measurement is wrong — and all three are fixable. Before, the disagreement was just two paragraphs.
If you are building an agent loop where one model checks another, the question to ask is not "is the reviewer good". It is: can the reviewer's verdict be recomputed by something that did not write it?
Plugin: https://github.com/Dogwind221/dwguard-dsh-hmos · npm: https://www.npmjs.com/package/dwguard-dsh-hmos · listed in awesome-dsh-plugin.