From prose to assertions: making 'done' checkable when an AI agent writes the code A developer built DwGuard-Dsh-Hmos, a plugin for DeepSeek Harness that orchestrates HarmonyOS app delivery by freezing a four-dimension acceptance pack, running an adversarial pre-review, dispatching work to DevEco Code in Goal mode, and auditing delivery with independently gathered evidence. The plugin adds optional machine-checkable assertions to acceptance criteria, using eight matchers (equals, contains, regex, range, gte/lte, oneOf, present), so the audit gate recomputes verdicts from reported actual values instead of trusting free-text evidence. The gate errors when a passing check lacks an actual value or contradicts its assertion, warns on other mismatches, and refuses to freeze specs containing invalid assertions. 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 https://github.com/Dogwind221/dwguard-dsh-hmos , a plugin for DeepSeek Harness https://github.com/deepseek-ai/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 no actual | error — you cannot pass an assertion you never ran | | criterion has an assertion, check says pass , assertion recomputes to fail | error — audit refused; this is the self-contradiction case | | criterion has an assertion, check says fail , assertion recomputes to pass | warning — something else may have failed it; ask for review | | criterion has an assertion, check says unverified without actual | 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 with E 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-