<?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>Second-Opinions on My House Runs on an AI Agent</title><link>https://psykosoldier.github.io/blog/tags/second-opinions/</link><description>Recent content in Second-Opinions 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/second-opinions/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><item><title>Welcome — Running a Home on an AI Agent, and Writing Down What Breaks</title><link>https://psykosoldier.github.io/blog/posts/welcome/</link><pubDate>Thu, 24 Sep 2026 00:00:00 +0200</pubDate><guid>https://psykosoldier.github.io/blog/posts/welcome/</guid><description>&lt;p&gt;I&amp;rsquo;m starting this blog for a simple reason: I&amp;rsquo;ve been running my home automation system with an AI coding agent acting less like a chatbot and more like a project lead — reading a backlog, opening pull requests, asking for sign-off on the risky stuff — for months now. It&amp;rsquo;s been genuinely useful, occasionally broken in genuinely funny ways, and at this point my AI agent has more commit access to my house than most of my actual family.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;m starting this blog for a simple reason: I&rsquo;ve been running my home automation system with an AI coding agent acting less like a chatbot and more like a project lead — reading a backlog, opening pull requests, asking for sign-off on the risky stuff — for months now. It&rsquo;s been genuinely useful, occasionally broken in genuinely funny ways, and at this point my AI agent has more commit access to my house than most of my actual family.</p>
<blockquote>
<p>I haven&rsquo;t found much writing that treats this setup as the real, load-bearing infrastructure it actually is, instead of a novelty demo where someone asks a chatbot to turn on a lightbulb and calls it a day.</p>
</blockquote>
<p>So I&rsquo;m writing it down as it happens — the wins and the &ldquo;why is the alarm system arguing with the vacuum cleaner&rdquo; moments alike.</p>
<hr>
<h2 id="who-this-is-for">Who this is for</h2>
<p>Two groups, deliberately: people who live in the <strong>smart home / home automation</strong> world and want to see what &ldquo;AI-managed&rdquo; actually looks like day to day, not just in a demo video — and people who work with <strong>AI agents generally</strong> (coding agents, autonomous workflows, anything with real write access to something that matters) and want concrete, specific stories rather than abstract best-practice advice. If you only care about one of the two, you&rsquo;ll still get something out of the other half — the failure modes turn out to be mostly the same regardless of whether the agent is touching a door lock or a production database.</p>
<hr>
<h2 id="what-this-actually-is">What this actually is</h2>
<p>Home Assistant, self-hosted, fully version-controlled, running on real hardware sitting on a shelf in my house — not a cloud instance, not a demo VM. An AI coding agent with real write access to that configuration, a self-hosted issue tracker, and a set of deliberately scoped permissions — some things it does freely, some things need my sign-off, a few things it&rsquo;s not allowed to touch at all, on pain of a stern chat.</p>
<p>It&rsquo;s not a toy. It arms and disarms a real alarm system, controls real door locks, and decides when a robot vacuum should or shouldn&rsquo;t start based on whether anyone&rsquo;s home. Bugs here aren&rsquo;t failed unit tests you shrug off with a red CI badge — they&rsquo;re a door that doesn&rsquo;t lock, or an alarm that goes off because a vacuum cleaner committed the crime of existing.</p>
<h3 id="the-stack-roughly">The stack, roughly</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Home Assistant (self-hosted)  ──┐
</span></span><span class="line"><span class="cl">Git repo, full config in it     ├── AI coding agent, scoped read/write
</span></span><span class="line"><span class="cl">Self-hosted issue tracker       │   + a dedicated review pass +
</span></span><span class="line"><span class="cl">CI on every change              ├── external second opinions before
</span></span><span class="line"><span class="cl">Static status dashboard         │   anything safety-critical ships
</span></span><span class="line"><span class="cl">Nightly/weekly unattended jobs ─┘
</span></span></code></pre></div><p>Two physical boxes underneath all of it — a low-power mini-PC running Home Assistant itself, and a NAS doing double duty as the Git server and CI runner. The <a href="../daily-workflow/">daily workflow post</a> has the actual make/model/spec sheet, and the <a href="../architecture-deep-dive/">architecture deep-dive</a> covers how the agent process stays alive across all of it.</p>
<h3 id="a-rough-sketch-of-what-a-normal-day-looks-like">A rough sketch of what a normal day looks like</h3>
<ul>
<li>The agent starts each session by reading a running log and a backlog of open work, and checks whether I&rsquo;ve flagged anything specifically for it via a small dashboard on my phone.</li>
<li>Every piece of real work — feature, bug fix, even a documentation change — gets a short ID and a tracked issue. The open issues <em>are</em> the project status; there&rsquo;s no separate document to keep in sync.</li>
<li>Nothing risky ships on one opinion. Changes to anything safety-critical get reviewed by a dedicated review pass plus two independent second opinions from different model providers — and even then, as you&rsquo;ll see in the first real post, all three can be confidently wrong at the same time.</li>
<li>A handful of unattended jobs run overnight — light audits, health checks, security log reviews — and file findings for me to triage when I&rsquo;m back, never acting unsupervised on anything non-trivial.</li>
<li>Long sessions eventually hit a context limit and get cleared; a handoff step writes down exactly what was mid-flight so the next session doesn&rsquo;t have to reconstruct it from git history. A separate, longer-lived memory keeps track of preferences and hard lessons across all of that — not &ldquo;what were we doing,&rdquo; but &ldquo;what should never happen again.&rdquo;</li>
</ul>
<p>That last part is where most of the interesting content on this blog will come from: not the features that worked, but the specific, dated moments where the process caught something — or didn&rsquo;t, and had to grow a new guardrail afterward, usually accompanied by me staring at a diff at an unreasonable hour wondering how a robot vacuum nearly got my house burgled by itself.</p>
<hr>
<h2 id="what-youll-find-here">What you&rsquo;ll find here</h2>
<p>Mostly two kinds of posts. <strong>Incident write-ups</strong> — something broke, here&rsquo;s the actual root cause, here&rsquo;s the fix, here&rsquo;s the guardrail added so the same class of bug can&rsquo;t recur silently. And <strong>pattern posts</strong> — a piece of reusable design that came out of one of those incidents, generalized enough to apply outside my specific setup (state persistence across restarts, how to avoid an agent racing itself, why timeouts are often the wrong signal for &ldquo;a process is done&rdquo;).</p>
<p>I&rsquo;ll keep every post technical and specific — real config snippets, real reasoning, not a sanitized case study. The one thing I&rsquo;ll deliberately leave out or generalize is anything that would tell someone exactly how my house&rsquo;s security setup behaves — the <em>pattern</em> is the point, not the exact parameters of my alarm system.</p>
<p>If you&rsquo;re running something similar, or thinking about it, I&rsquo;d genuinely like to hear what&rsquo;s broken for you too — that&rsquo;s a big part of why this is public instead of a private notebook.</p>
<p>First real post is up next: <a href="../three-ai-reviews-agreed/">three independent AI code reviews unanimously flagged the same change as critical</a>. All three were wrong, and the actual bug was something none of them found.</p>
]]></content:encoded></item></channel></rss>