<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>False-Positives on My House Runs on an AI Agent</title><link>https://psykosoldier.github.io/blog/tags/false-positives/</link><description>Recent content in False-Positives on My House Runs on an AI Agent</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 24 Sep 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://psykosoldier.github.io/blog/tags/false-positives/index.xml" rel="self" type="application/rss+xml"/><item><title>Three AI Code Reviews Said "Critical Bug." All Three Were Wrong.</title><link>https://psykosoldier.github.io/blog/posts/three-ai-reviews-agreed/</link><pubDate>Thu, 24 Sep 2026 00:00:00 +0200</pubDate><guid>https://psykosoldier.github.io/blog/posts/three-ai-reviews-agreed/</guid><description>&lt;p&gt;I recently made a small change to my home automation setup: a robot vacuum that used to start on a rigid 10-minute &amp;ldquo;away&amp;rdquo; timer should instead wait until a larger geofence is actually crossed — no more triggering just because someone stepped out to the mailbox. A tiny feature. It should have been a ten-minute edit. It was not, because in this house even the vacuum cleaner has opinions about the security system.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I recently made a small change to my home automation setup: a robot vacuum that used to start on a rigid 10-minute &ldquo;away&rdquo; timer should instead wait until a larger geofence is actually crossed — no more triggering just because someone stepped out to the mailbox. A tiny feature. It should have been a ten-minute edit. It was not, because in this house even the vacuum cleaner has opinions about the security system.</p>
<h2 id="the-warning-before-writing-a-single-line">The warning before writing a single line</h2>
<p>Before touching any code, I noticed something: right next to the automation that starts the vacuum sits a second one — the security alarm. And the two are coupled through a duplicated piece of logic. The alarm sequence contains a variable that <em>predicts</em> whether the vacuum is about to start, in order to decide: arm upstairs immediately, or wait until the vacuum finishes (a motion sensor would otherwise flag the moving robot as an intruder)?</p>
<p><img alt="one trigger, two coupled systems that must agree" loading="lazy" src="/blog/posts/three-ai-reviews-agreed/diagram-1-coupling.png"></p>
<p>Two independent conditions, deliberately kept identical, deciding two different things.</p>
<blockquote>
<p>That&rsquo;s exactly the kind of coupling that had already caused two real false alarms in the past.</p>
</blockquote>
<p>If I extended the vacuum&rsquo;s start condition with the new geofence logic without updating the alarm&rsquo;s copy of that logic in lockstep, I&rsquo;d introduce a fresh race condition in the single most safety-critical corner of the config. So: flagged the risk, got the go-ahead (&ldquo;do it, but be extra careful&rdquo;), and synchronized both conditions word-for-word:</p>
<h3 id="the-synced-condition">The synced condition</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Both the vacuum-start condition AND the alarm&#39;s prediction variable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># use this exact same expression:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">value_template</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">  {{ (trigger.id == &#39;home_away&#39; and is_state(&#39;person.dennis&#39;, &#39;not_home&#39;)
</span></span></span><span class="line"><span class="cl"><span class="sd">  and is_state(&#39;person.anni&#39;, &#39;not_home&#39;)) or trigger.id ==
</span></span></span><span class="line"><span class="cl"><span class="sd">  &#39;home_away_sauger_fallback&#39; }}</span><span class="w">
</span></span></span></code></pre></div><hr>
<h2 id="escalating-to-three-opinions-instead-of-one">Escalating to three opinions instead of one</h2>
<p>Changes to safety-critical areas don&rsquo;t just get a single code review in my setup. There&rsquo;s a standing rule: a dedicated review agent, plus two independent opinions from <em>different</em> model providers — genuine diversity, not the same blind spots twice.</p>
<p>Roughly, invoking the second and third opinions looks like this — one default call, one escalated to a stronger model on the same run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">scripts/external_review.py <span class="s2">&#34;geofence change to vacuum-start condition&#34;</span>
</span></span><span class="line"><span class="cl">scripts/external_review.py --model &lt;stronger-model&gt; <span class="s2">&#34;same change, second pass&#34;</span>
</span></span></code></pre></div><p>All three came back with the same verdict: <strong>critical.</strong></p>
<ul>
<li>Model one claimed the prediction formula in the alarm sequence and the actual vacuum-start condition weren&rsquo;t identical.</li>
<li>Model two made the same claim, backed by a different (also wrong) detail.</li>
<li>The internal review agent claimed something else entirely: that the zone check only recognized the small 75-meter zone, not the new, larger one — and separately, that a safety property of the automation (<code>mode: parallel</code>, allowing two trigger instances to run concurrently without blocking each other) didn&rsquo;t actually exist in the code.</li>
</ul>
<p>Three independent sources, one converging &ldquo;critical&rdquo; alarm.</p>
<blockquote>
<p>Three different companies, three different training runs, one shared hallucination — either impressively bad luck or a surprisingly efficient way to be wrong in stereo.</p>
</blockquote>
<p>That&rsquo;s exactly the moment where it&rsquo;s tempting to either trust blindly (&ldquo;three out of three can&rsquo;t all be wrong&rdquo;) or dismiss blindly (&ldquo;the AI is exaggerating again, back to my coffee&rdquo;).</p>
<hr>
<h2 id="checking-instead-of-believing">Checking instead of believing</h2>
<p>Neither. Line-by-line comparison instead.</p>
<h3 id="what-each-claim-actually-turned-out-to-be">What each claim actually turned out to be</h3>
<table>
	<thead>
			<tr>
					<th>Claim</th>
					<th>Verdict</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Prediction formula ≠ vacuum condition</td>
					<td>False — byte-for-byte identical in the same commit</td>
			</tr>
			<tr>
					<td>Zone check only sees the small 75m radius</td>
					<td>False — documented HA behavior: smaller radius wins on concentric zones, working as intended</td>
			</tr>
			<tr>
					<td><code>mode: parallel</code> missing</td>
					<td>False — two lines below where the reviewer apparently stopped reading</td>
			</tr>
	</tbody>
</table>
<p>All three &ldquo;critical&rdquo; findings: false positives. A clean sweep, in the wrong direction.</p>
<hr>
<h2 id="the-one-real-finding">The one real finding</h2>
<p>But while doing that line-by-line comparison myself, I noticed something none of the three AI opinions had flagged: a comment directly above the automation&rsquo;s triggers claimed the alarm sequence was &ldquo;exclusively gated to the 10-minute trigger&rdquo; — the <em>opposite</em> of what was actually intended and implemented (the alarm sequence has to react to both triggers, or it reintroduces the exact race condition I&rsquo;d warned about in the first place). A stale, misleading comment, not a logic bug — but exactly the kind of detail that misleads whoever (human or AI) trusts the comment over the code six months from now.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Before (misleading):</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Light/Cover/Lock stay exclusively gated on home_away (10 min).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Alarm too.  &lt;-- this part was simply no longer true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># After:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Light/Cover/Lock stay exclusively gated on home_away (10 min).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># The alarm sequence is deliberately NOT gated — it must react to</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># BOTH trigger IDs (see the synced prediction formula above) to</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># correctly decide immediate-arm vs. wait-for-vacuum.</span><span class="w">
</span></span></span></code></pre></div><hr>
<h2 id="what-i-took-away-from-it">What I took away from it</h2>
<p>Three AI reviews, three false &ldquo;critical&rdquo; alarms, one real (if minor) finding that none of them caught. That&rsquo;s not an argument against getting second opinions — it&rsquo;s an argument for treating them as what they are: extra scrutiny, not proof. The value wasn&rsquo;t in trusting the three opinions; it was in the process forcing me to verify every single claim against the actual code — and it was precisely <em>while</em> disproving the false positives that the real issue surfaced, not because any review tool found it.</p>
<p>The lesson I&rsquo;m keeping: for safety-critical code, verifying against ground truth — actual code, actual documented system behavior — always pays off, independent of how many AI voices agree on something. Trust doesn&rsquo;t substitute for checking, no matter how many opinions you stack.</p>
<p>If you want the backstory on why changes like this get three opinions instead of one in the first place, <a href="../welcome/">the introduction post</a> has the full picture. Next up: <a href="../subagent-pushed-to-main/">the time a subagent I explicitly told to stay read-only pushed a commit to main anyway</a>.</p>
]]></content:encoded></item></channel></rss>