<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>Nick Blokhin — Notes</title>
		<link>https://blokhin.us/notes/</link>
		<description>Short technical notes on software engineering, tools, and AI.</description>
		<language>en</language>
		<atom:link href="https://blokhin.us/rss.xml" rel="self" type="application/rss+xml"/>
		<lastBuildDate>Sun, 02 Aug 2026 00:00:00 +0000</lastBuildDate>
		<item>
			<title>How I Split Work with Agentic Coding Tools: Architect, Reviewer, Agent</title>
			<link>https://blokhin.us/notes/agentic-coding-workflow/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/agentic-coding-workflow/</guid>
			<pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate>
			<description>An AI model test became a shipped app. How I split work with coding agents: trust boundaries, written guardrails, and what I never delegate.</description>
			<content:encoded><![CDATA[<p><a href="/texodus/">Texodus</a> started as a model test. My <a href="https://github.com/w512/Prompt-Vault">Prompt Vault</a> repository holds a spec called &quot;Markdown Editor — Tauri 2 Desktop App&quot; in the Hard category: forty-four lines of requirements I use to see what models do with a non-trivial task. One of those runs produced code good enough to be worth finishing. Today it is an editor for macOS, Windows, and Linux that has been through four major versions and <a href="/notes/tauri-three-platforms/">a Linux packaging saga</a> with nothing whatsoever to do with AI.</p>
<p>The question I get asked most often is not which tool I use. It is what this looks like over the long run. Here is the answer.</p>
<h2>Architect: the spec that is not in the prompt</h2>
<p>The public spec is forty-four lines with no section numbers anywhere. Yet the <a href="https://github.com/w512/Texodus">Texodus sources</a> still carry references like these:</p>
<pre><code class="language-ts">// ── Window close confirmation (§4.4) + Tauri drag-drop (§4.1) ──
/* Implements the 3-button flow required by spec §4.4 (Save / Don&#39;t Save / Cancel) */
// Debounce rendering (§6.2)
</code></pre>
<p>They point at a detailed, numbered specification — it runs to §12.8 — that I wrote myself, using that public prompt as the starting point. This is probably the clearest answer to what the human actually does here: not &quot;ask for a Markdown editor&quot;, but turn a page of requirements into a document the code can cite and that you can later check implementation against, line by line.</p>
<p>Those markers are still in the sources four major versions later, and a rule in the agent instructions requires keeping them: &quot;<code>§N.N</code> comments reference the original product spec — keep them intact&quot;. A <code>§4.4</code> next to the unsaved-changes dialog is not decoration. It is an address: it takes a second to find how the behavior was meant to work, instead of reconstructing the intent from the code.</p>
<h2>The boundary follows the cost of a mistake</h2>
<p>The agent does not get the same freedom everywhere in the codebase. The line is drawn not by type of task but by what an error costs. The Texodus rules mark it in plain text:</p>
<blockquote>
<p><code>services/markdownSanitizer.ts</code> — <strong>single source of <code>marked</code> options + the DOMPurify whitelist</strong>, shared by live preview and export so sanitization can&#39;t drift. A regression here = XSS.</p>
</blockquote>
<blockquote>
<p>The cold-start race (macOS <code>Opened</code> fires before <code>main</code> exists) is handled in <code>main_window_is_empty</code> — read its comments before touching.</p>
</blockquote>
<p>Where a mistake is cheap — layout, refactoring, routine changes — the agent works on its own. Where it is expensive — the sanitizer, cryptography, saving files — the freedom ends.</p>
<h2>The rules file is an archive of mistakes already made</h2>
<p>The most useful thing I have learned from all of this: agent instructions consist almost entirely of prohibitions, and every prohibition is the trace of something that once broke. Not an abstract style guide — a list of expensive lessons written in the imperative.</p>
<p>From <a href="https://github.com/w512/Kivarion">Kivarion</a>, my password manager:</p>
<blockquote>
<p><code>onDragOver</code> must decide with <code>isEntryDrag</code>, <strong>never</strong> with <code>getData</code>: the drag data store is in protected mode for the whole drag, so <code>getData</code> returns <code>&#39;&#39;</code> there — which is exactly how entry→group drag came to be silently dead.</p>
</blockquote>
<blockquote>
<p>Do not move it back into the webview, and do not reintroduce <code>argon2-browser</code>.</p>
</blockquote>
<p>The second rule comes with a full paragraph of reasoning: the WASM call is synchronous no matter what the promise looks like, and kdbxweb re-derives the key on every save — so the interface froze for the length of the KDF on every auto-save.</p>
<p>From <a href="https://github.com/w512/Texodus">Texodus</a>:</p>
<blockquote>
<p><code>AppState.pending_files</code> is a <strong>queue per window label</strong> — several &quot;Open With&quot; files can target one window before the frontend drains; don&#39;t collapse it back to a single slot.</p>
</blockquote>
<p>Every one of these lines was written after something went wrong. Their value is that they stop anyone from returning to the &quot;simpler&quot; solution that already turned out to be the wrong one — the agent, and me six months later.</p>
<h2>Reviewer: I look at everything, I read selectively</h2>
<p>I go through every diff. But I do not read all of it closely — only the parts that touch critical and important code. That is the only honest way to work at the speed agents give you: reading every line would eat the entire gain, and reading nothing means eventually shipping a regression in the sanitizer.</p>
<p>So the other half of review is automated. The rules contain a one-line gate:</p>
<blockquote>
<p>Run typecheck + test + lint before declaring frontend work done.</p>
</blockquote>
<p>Work is not done until those three commands pass. The tests cover exactly the dangerous places — the sanitizer is tested for stripping XSS, and the rules even record why the test environment is jsdom rather than happy-dom: happy-dom&#39;s parser mishandles siblings after a <code>&lt;script&gt;</code>, which would have made the test useless.</p>
<h2>This is a system, not one project&#39;s setup</h2>
<p><code>CLAUDE.md</code> lives in many of my repositories. In <a href="https://github.com/w512/Texodus">Texodus</a> and <a href="https://github.com/w512/Kivarion">Kivarion</a> an <code>AGENTS.md</code> sits next to it — I work with two agents in parallel, and each one needs its own file carrying the same knowledge about the project. Those files never reach the public repositories: they are in <code>.gitignore</code> and stay local.</p>
<p>Rules pay off best where things break quietly. For example, the <code>CLAUDE.md</code> for this site records that routes are linked by <code>name</code>, so renaming one silently breaks both the links and the active-menu highlight, and that the sitemap generator parses the routes file with a regex — which is why the one-line route format must not be touched. Neither tests nor reading a diff catches that.</p>
<h2>Where it breaks: the line between JS and Rust</h2>
<p>The most expensive problem I have hit was not code quality. It was that the agent has no memory between sessions.</p>
<p>In a Tauri app, almost every task has two places it could be solved: the JavaScript frontend or the Rust backend. The agent kept getting this wrong — and it decided differently in different sessions and different tasks. Not because the choices were bad: each individual one was defensible. The problem is that every new session starts from zero, so any decision not written down in the project gets made again, differently. Architecture does not drift because of one big mistake. It drifts because of a dozen small ones, each perfectly reasonable on its own.</p>
<p>There is exactly one cure: draw the boundary explicitly, in writing. In Texodus the rules list all five Rust commands and state what belongs to the backend and what to the frontend. In Kivarion the line is drawn harder, with the price attached:</p>
<blockquote>
<p>KDBX 4&#39;s KDF runs in the <strong>backend</strong>, not the webview. […] Do not move it back into the webview.</p>
</blockquote>
<blockquote>
<p>The frontend has <strong>no direct filesystem access</strong>; <code>saveDatabase</code> serializes the db and calls the Rust <code>save_database</code> command.</p>
</blockquote>
<p>Which closes the loop on the archive of mistakes: the rules I quoted earlier exist because of exactly this problem. The agent does not remember architectural decisions. The repository does.</p>
<h2>What it comes down to</h2>
<p>The split is simple. I own the spec, the boundaries — both trust and architectural — and the critical parts of the code. The agent owns the volume of work inside those boundaries. Tests and the linter make sure I never have to take its word for it.</p>
<p>All of it reduces to one principle. An agent has no memory between sessions, so the project&#39;s memory has to live in the project: a numbered spec the code can cite, and a rules file that grows out of every expensive mistake. Whatever is not written down will be decided again — and differently next time.</p>
<p>How I pick models for this kind of work, and why I do not trust leaderboards, is on <a href="/ai/">the AI page</a> and in <a href="https://github.com/w512/Prompt-Vault">Prompt Vault</a>.</p>
]]></content:encoded>
		</item>
		<item>
			<title>&quot;Cross-Platform&quot; Means Three Separate Platforms: Shipping a Tauri 2 App</title>
			<link>https://blokhin.us/notes/tauri-three-platforms/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/tauri-three-platforms/</guid>
			<pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate>
			<description>My Tauri 2 AppImage died with EGL_BAD_PARAMETER on recent Mesa: bundled graphics libraries. The fix lives in the release pipeline — and wasn't the only one.</description>
			<content:encoded><![CDATA[<p><a href="/texodus/">Texodus</a> is a Markdown editor built with Tauri 2 and Vue 3, running from one codebase on macOS, Windows, and Linux. The cross-platform promise holds perfectly — right up to the point where the code is written. Then comes delivery, and there is no shared platform there at all: there are three independent sets of problems, each with its own way of breaking on a user&#39;s machine rather than on mine.</p>
<p>Here is what shipping Texodus taught me about the distance between &quot;it builds&quot; and &quot;they downloaded it and it started&quot;.</p>
<h2>Linux: it did not start for everyone</h2>
<p>I did not find this one myself. It arrived as a bug report on GitHub: the AppImage would not launch at all, dying on graphics initialization. On my machine, and on plenty of distributions, everything worked — exactly the kind of symptom you cannot reproduce where you develop.</p>
<pre><code>Could not create default EGL display: EGL_BAD_PARAMETER
</code></pre>
<p>The cause was not in the application code but in the packaging. <code>linuxdeploy</code> faithfully bundles the graphics client libraries of the machine that built the app — in my case Ubuntu 22.04 on GitHub Actions. At launch they are loaded ahead of the host&#39;s own copies, and then talk to the <em>host&#39;s</em> Mesa driver. As long as the versions stay close, this works. On recent Mesa — the report came from Arch with RDNA4 — WebKitGTK cannot initialize EGL through that mismatched pair, and the window simply never opens.</p>
<p>The reasoning is the same as behind AppImage&#39;s standard excludelist: the graphics stack has to come from the host, not from the bundle. Which means those libraries have to leave the bundle.</p>
<h2>The fix belongs in the pipeline</h2>
<p>Fixing this by hand once would be useless — the next release would rebuild it exactly the same way. So the GitHub Actions workflow got a step that runs after the build: unpack the AppImage, delete what should not be there, and pack it back up.</p>
<pre><code class="language-bash">find squashfs-root \( \
  -name &#39;libwayland-*.so*&#39; -o \
  -name &#39;libgbm.so*&#39; -o \
  -name &#39;libdrm*.so*&#39; -o \
  -name &#39;libEGL.so*&#39; -o \
  -name &#39;libGL.so*&#39; -o \
  -name &#39;libGLX*.so*&#39; -o \
  -name &#39;libGLdispatch.so*&#39; \
\) -print -delete
</code></pre>
<p>Then came three details, each of which cost its own round trip:</p>
<ul>
<li><strong>No FUSE on the runner.</strong> Extraction goes through <code>--appimage-extract</code>, and <code>appimagetool</code> itself runs with <code>--appimage-extract-and-run</code>.</li>
<li><strong>The <code>appimagetool</code> download was dead.</strong> Assets on the old AppImageKit releases return 404, so the step pulls from the maintained <code>AppImage/appimagetool</code> repository instead, at a pinned version.</li>
<li><strong>The asset was already uploaded.</strong> <code>tauri-action</code> publishes the AppImage before this step runs, so the finished file has to be swapped through the API: delete the old release asset, upload the repacked one.</li>
</ul>
<p>None of this follows from the Tauri documentation. All three showed up on live releases.</p>
<h2>macOS: not even in CI</h2>
<p>The Texodus build matrix has two runners: <code>ubuntu-22.04</code> and <code>windows-latest</code>. macOS is not there at all — those builds run locally, from a separate script.</p>
<p>The reason is signing. A shippable DMG needs a Developer ID certificate from the local keychain and notarization with Apple, which wants an app-specific password and a team ID. On top of that, a universal binary means two Rust targets (<code>x86_64-apple-darwin</code> and <code>aarch64-apple-darwin</code>) built and merged. All of this can be moved into CI — at the price of spreading certificates and secrets across someone else&#39;s infrastructure. For a project I run alone, a local script turned out to be the honest trade.</p>
<p>The result is one application with two different release paths: Linux and Windows build in the cloud at the push of a button, macOS builds on my Mac.</p>
<h2>One feature, three mechanisms</h2>
<p>The most underrated part of cross-platform work is not packaging. It is that identical behavior has to be implemented differently.</p>
<p>Take opening a file by double-clicking it in the file manager. On macOS the path arrives as a <code>RunEvent::Opened</code> event. On Windows and Linux it arrives as a command-line argument, plus a single-instance plugin so that a second launch hands the file to the running process instead of starting a second application.</p>
<p>The macOS branch also has a race that does not physically exist on the others: <code>Opened</code> can fire before the main window has been created. Until that was handled, double-clicking a <code>.md</code> file while the editor was closed opened two windows — one empty, one with the file. The routing logic saw no window and read that as a signal to spawn one.</p>
<p>The same category includes features that simply do not exist elsewhere. In <a href="/kivarion/">Kivarion</a>, Touch ID unlock is macOS-only, and reporting an honest &quot;not supported&quot; on Windows and Linux is as much a part of cross-platform work as the feature itself.</p>
<h2>What I took away</h2>
<ul>
<li><strong>Test the release artifact, not the build.</strong> The EGL problem lived neither in the code nor in a dev build — only in the packaged AppImage, on someone else&#39;s system. If you do not test that, a user will, and then they will write to you about it.</li>
<li><strong>&quot;Works on my machine&quot; means nothing on Linux.</strong> Distributions differ in graphics stack versions more than you expect, and it is the newest environment that breaks, not the oldest.</li>
<li><strong>A platform fix has to be a pipeline step.</strong> Anything repaired by hand after the build is lost on the next release.</li>
<li><strong>Plan for different release paths.</strong> Apple&#39;s signing requirements differ enough that a single pipeline for all three platforms stops being an obvious goal.</li>
</ul>
<p>&quot;Cross-platform&quot; is true about the code. About delivery, it is three separate projects that happen to share a repository.</p>
<p>Texodus is open source: <a href="https://github.com/w512/Texodus">repository</a> and <a href="https://github.com/w512/Texodus/releases">releases</a>.</p>
]]></content:encoded>
		</item>
		<item>
			<title>FAANG Experience Is No Longer an Elite Engineer Badge</title>
			<link>https://blokhin.us/notes/faang-experience/</link>
			<guid isPermaLink="true">https://blokhin.us/notes/faang-experience/</guid>
			<pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
			<description>Narrow roles, closed internal stacks, and golden handcuffs: why a Big Tech line on a résumé stopped being an automatic quality signal for hiring managers.</description>
			<content:encoded><![CDATA[<p>Ten years ago, a line saying Google, Meta, or Apple on a résumé was an absolute quality signal — a free pass into almost any company in the world. Today, when I see that line as the person doing the hiring, what I feel is more complicated. Judging by conversations with other hiring managers, I am not alone.</p>
<p>Let me be upfront about where I am standing. I have never worked at a FAANG company myself. But in twenty-plus years in this industry I have worked alongside plenty of people who did, a good number of my friends and former colleagues built careers there, and I have interviewed and hired candidates coming out of those companies. This is the view from the other side of the table — the side where the brand on the résumé stopped answering the only question that matters: what can this engineer actually build?</p>
<h2>The role narrows to a single component</h2>
<p>At a company with hundreds of thousands of employees, work has to be split into very small pieces. That is not a flaw in anyone&#39;s character — it is what scale demands. But instead of designing a system or taking a service from zero to production, an engineer can spend years maintaining one narrow component of one internal platform. Depth in that component grows. The view of the system as a whole quietly atrophies.</p>
<p>The result for a career is the same either way: after a few years you know something brilliantly, and that something exists only inside one company.</p>
<h2>A closed stack does not transfer</h2>
<p>The largest companies have always built their own ecosystems: their own frameworks, build systems, version control, deployment tooling. Inside, these are often excellent — years ahead of what the rest of the market has. The problem is not quality. It is portability. Expertise in an internal platform is worth very little the moment you walk off the campus.</p>
<p>There is an honest complication here worth stating. A significant part of today&#39;s open stack consists of public descendants of exactly those internal systems: Kubernetes grew out of the experience of Borg, Bazel is the open version of Blaze. The argument is not that Big Tech has bad tools. The argument is that the market runs on the public versions, and an engineer who spent four or five years on the internal ones tends to discover that the ecosystem moved on without them — from cloud providers to orchestrators to frameworks.</p>
<h2>Golden handcuffs</h2>
<p>High salaries and equity grants build a standard of living that is hard to maintain anywhere else. The price for that comfort is often routine work wrapped in process. Colleagues describe simple changes spending weeks in approvals that a small team would not need at all. Engineering initiative does not die suddenly in that environment. It just stops paying off.</p>
<h2>What Big Tech still teaches better than anyone</h2>
<p>I am not interested in building a straw man, so let me be clear about the other side. Some things scale teaches better than any startup can: operating systems where a failure reaches millions of people, a real code review culture and the discipline that comes with it, the skill of changing a running system without knocking it over. And for researchers and R&amp;D engineers pushing fundamental work, those labs are still the best places on the planet.</p>
<p>Choosing stability is a legitimate choice too. Trading some speed of professional growth for comfort and predictability is an adult decision, not a mistake — as long as it is actually a decision. The failure mode is the cargo cult: treating &quot;getting into the corporate elite&quot; as the goal itself.</p>
<h2>What to do about it</h2>
<p>If you are an engineer inside a big company, do not let the internal platform become the only thing you know. Side projects, open source, a public trail — anything that lives outside the campus and can be checked from outside it.</p>
<p>If you are hiring, look past the brand at what the person has built and what you can verify: shipped products, breadth of stack, a visible trail in open code. The brand on a résumé answers the question of where someone was once hired. It stopped answering the question of what they can build a while ago.</p>
]]></content:encoded>
		</item>
	</channel>
</rss>
