<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Deepak Goyal]]></title><description><![CDATA[Deepak Goyal — Technical Architect writing about AI architecture, system design, and the decisions behind building AI-powered systems at scale.]]></description><link>https://blog.deepakgoyal.ai</link><image><url>https://cdn.hashnode.com/uploads/logos/69e9e9b716b084c068f429cf/0aa77733-955b-4adb-b1d1-2916b3a2a3e0.jpg</url><title>Deepak Goyal</title><link>https://blog.deepakgoyal.ai</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 08 Oct 2026 10:38:53 GMT</lastBuildDate><atom:link href="https://blog.deepakgoyal.ai/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Shell scripts are the highest-ROI layer under your AI skills]]></title><description><![CDATA[Shell scripts are the highest-ROI layer under your AI skills
The glamorous layer is the agent. The load-bearing one is the boring shell script underneath — and that's where most of your reliability ac]]></description><link>https://blog.deepakgoyal.ai/shell-scripts-highest-roi-under-ai-skills</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/shell-scripts-highest-roi-under-ai-skills</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[automation]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Bash]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Mon, 10 Aug 2026 09:25:16 GMT</pubDate><content:encoded><![CDATA[<h1>Shell scripts are the highest-ROI layer under your AI skills</h1>
<p><em>The glamorous layer is the agent. The load-bearing one is the boring shell script underneath — and that's where most of your reliability actually lives.</em></p>
<hr />
<p>The highest-leverage code I shipped this month wasn't a prompt. It wasn't an agent. It was a handful of boring shell scripts.</p>
<p>They sit under <a href="https://blog.deepakgoyal.ai/stopped-being-my-second-brains-scheduler">my second brain</a> — the system I wrote about recently, the one that stopped needing me to schedule it. That post was about what the automation <em>does</em>. This one is about the part that actually carries it, which I glossed over at the time: not the AI. The scripts.</p>
<h2>The two kinds of work in every AI system</h2>
<p>Every AI automation is really doing two different jobs, and we tend to blur them.</p>
<p>One is <strong>judgment</strong>: decide which thing matters, draft the reply, synthesize the week into a paragraph. That needs a model — it's taste, not logic.</p>
<p>The other is <strong>deterministic</strong>: fetch the data, parse it, dedupe it, mutate a file, check a condition. That needs no brain at all. And yet the reflex, once you have a capable model in the loop, is to hand it that work too — because it <em>can</em> do it. It can. It'll also cost you tokens and hand back a slightly different answer every run.</p>
<p>The move that keeps earning its keep is dull to state and hard to remember: <strong>push everything deterministic down into a script, and spend the model only on the judgment.</strong></p>
<h2>What the split actually looks like</h2>
<p>I built a small system that surfaces conversations worth joining — a morning shortlist of threads where I genuinely have something to add. It has exactly two layers.</p>
<p>A <strong>shell script</strong> does the deterministic half: hit the API, pull candidate threads, keyword-match them against my areas, drop anything I've seen before. Some JSON parsing, a <code>grep</code>, a dedupe set. Nothing in there wants a brain.</p>
<p>An <strong>agent</strong> does the judgment half: read those candidates, pick the few I can actually help with, and draft a reply in my voice.</p>
<p>If I'd let the model do the fetching and filtering too, I'd be paying tokens to do a job <code>grep</code> does perfectly — and getting a slightly different shortlist each morning. Instead the mechanical part is fixed, fast, and free, and the model spends its whole budget on the part that's genuinely a decision.</p>
<h2>A script that breaks tells you it broke</h2>
<p>Here's the part that settled it for me.</p>
<p>The morning after I wired it up, the shortlist came back empty. My instinct was to distrust the whole thing. But because the fetch layer is a <em>script</em>, I could ask it exactly what happened in one line:</p>
<pre><code>curl -s -o /dev/null -w "%{http_code}\n" https://…
→ 403
</code></pre>
<p>The free API had started refusing unauthenticated requests. Thirty seconds to diagnose, because the failure was a <em>number</em>, not a mood.</p>
<p>Now put that same fetch inside a model loop. It hits the same wall — and then does what models do: smooths over the gap, returns a plausible empty result, and I trust it. Deterministic systems fail loudly and specifically. Probabilistic ones fail quietly and expensively. A script that breaks tells you it broke; that is worth far more than it sounds.</p>
<h2>The cheapest guardrail I own is a grep</h2>
<p>One more, because it's my favourite.</p>
<p>I keep a public, open-source version of my system, built by copying pieces out of my private one and stripping the private bits. A single leaked name or path in a public repo is the sort of mistake you can't quietly undo. So before I tag a release, a script greps the staged files for anything private — names, absolute paths, tokens — and exits non-zero if it finds one. A spellcheck for secrets. Zero tokens, runs in a second, and it <em>cannot have an off day</em>.</p>
<p>I could ask a model, "is anything private in here?" It would be slower, cost tokens, and — the part that matters — it could miss <em>differently</em> each time. The grep can only catch what's on its list, too; it's not magic. But it catches those every single time, and the day something slips through a new pattern, I add one line and it's caught on every release after. That's the trade I want in a safety gate: it fails the same way twice, so I only have to fix it once.</p>
<h2>Why it's the highest ROI, not just the cheapest</h2>
<p>It's tempting to say the win is cost, and it is. But cost is the bonus. The reason is three properties you get from code and cannot get from a model: it's <strong>reliable</strong> (same input, same output, forever — no drift, no temperature), it's <strong>testable</strong> (you can unit-test a script; you cannot unit-test a vibe), and it <strong>fails visibly</strong> (an exit code, not a confident wrong answer). Write it once, run it every morning, at zero marginal cost. That's the curve you want underneath the expensive, probabilistic layer — not on top of it.</p>
<h2>Where the line sits — and it cuts both ways</h2>
<p>The skill isn't "script everything." It's knowing where the boundary is.</p>
<p>Fetching, parsing, deduping, mutating files, checking a condition — that's below the line, and it belongs in a script. Deciding which thread deserves your voice, drafting the reply, reading a messy week and calling the theme — that's above it, and it belongs to the model.</p>
<p>Push the line as far toward <em>script</em> as the work honestly allows, and not one step past. Script the judgment and you get brittle rules that shatter on the first case you didn't foresee. Hand determinism to the model and you get expensive nondeterminism. They're the same mistake, pointed in opposite directions.</p>
<h2>The load-bearing layer</h2>
<p>The layer that ends up in the demo is the agent. The layer that keeps the thing standing is the script underneath it — and a surprising amount of your reliability lives down there, unglamorous and unmentioned.</p>
<p>If you're building anything with AI this week, the highest-leverage hour probably isn't a sharper prompt. It's taking one deterministic thing your model is quietly doing, and moving it into a small, dull script that will never surprise you.</p>
<p>Let the model do the thinking. Everything else is a job for something that shows up, does the boring thing the same way every time, and tells you plainly when it can't.</p>
]]></content:encoded></item><item><title><![CDATA[Deterministic context beats generated context — when structure exists]]></title><description><![CDATA[Deterministic context beats generated context — when structure exists
The prompt is only about 10% of what your LLM actually sees. Most of the other 90% is context you assemble — and when your data ha]]></description><link>https://blog.deepakgoyal.ai/deterministic-context-beats-generated-context</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/deterministic-context-beats-generated-context</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[llm]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Prompt Engineering]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Thu, 06 Aug 2026 12:20:17 GMT</pubDate><content:encoded><![CDATA[<h1>Deterministic context beats generated context — when structure exists</h1>
<p><em>The prompt is only about 10% of what your LLM actually sees. Most of the other 90% is context you assemble — and when your data has structure, you should build it with code, not another LLM call.</em></p>
<hr />
<h2>The 90% nobody edits</h2>
<p>When people say "prompt engineering," they picture the instruction they typed. But look at what actually arrives in the model's context window on a real request: your system prompt, sure — and then retrieved documents, tool-call results, prior messages, memory, file contents, database rows, schemas. Type it out and the instruction you obsessed over is a thin slice on top of a much larger pile of <em>assembled</em> material.</p>
<p>The prompt is maybe 10% of what the model sees. The other 90% is context you built — and <em>how</em> you built it decides more about the output than any wording tweak.</p>
<p>So the real question isn't "what should I say to the model." It's "how do I assemble the 90%." And that's where I see the same expensive reflex, over and over: <strong>teams reach for an LLM to build context that their code already knows.</strong></p>
<h2>The reflex: generate what you could assemble</h2>
<p>It's a natural instinct. You have an LLM; it's good with language; so you use it for everything upstream of the "real" call too. A few patterns I keep running into:</p>
<ul>
<li><strong>LLM-as-summarizer</strong> for content that's already structured — asking a model to "summarize the project status" from files whose frontmatter already <em>has</em> the status, the owner, the due date.</li>
<li><strong>LLM-as-router</strong> — a call to decide "which of these five categories does this belong to" when a lookup table or a label already answers it.</li>
<li><strong>LLM-as-extractor</strong> — parsing a field out of a document with a prompt when the document is JSON, or the field is a column.</li>
<li><strong>LLM-as-selector</strong> — "figure out which of my 200 notes are relevant" when the notes carry tags, links, and dates you could filter on directly.</li>
</ul>
<p>Each of these <em>works</em>. That's the trap. It works in the demo, so it ships. Then it runs a thousand times a day and you're paying — in three currencies:</p>
<ol>
<li><strong>Money and latency.</strong> Every one of those is a network round-trip and a token bill for something a query would return in a millisecond for free.</li>
<li><strong>Non-determinism.</strong> Same input, slightly different context tomorrow. Your pipeline develops a shimmer. Reproducing a bad output becomes archaeology.</li>
<li><strong>Hallucinated context.</strong> This is the quiet one. When you ask a model to <em>generate</em> the context for the next step, it can invent. Now you're not just risking a hallucinated <em>answer</em> — you've hallucinated the <em>inputs</em>, and everything downstream trusts them.</li>
</ol>
<p>The failure mode that should scare you isn't the model getting an answer wrong. It's the model getting the <strong>context</strong> wrong, confidently, and the rest of the system treating that fabricated context as ground truth.</p>
<h2>The principle</h2>
<p>Here's the line I've settled on, and it's simple enough to put on a sticky note:</p>
<blockquote>
<p><strong>When the data has structure, assemble the context deterministically. Reserve the LLM for the unstructured residue.</strong></p>
</blockquote>
<p>Structure is a gift. A schema, a frontmatter block, a database row, a file tree, a tag, a naming convention — every one of those is a place where <em>code</em> can produce the exact right context, the same way, every time, for free. If you can write a query, a template, or a parser that yields the context, you don't need a model to yield it. You need the model only where the input is genuinely unstructured — natural-language judgment, synthesis, semantics that no schema captures.</p>
<p>Most systems have far more structure than they use, and hand the LLM a job that a <code>SELECT</code> or a two-line script would nail deterministically.</p>
<h2>What this looks like in practice</h2>
<p>I'll make it concrete with systems I actually run.</p>
<p><strong>A pre-assembled context cache.</strong> My personal knowledge system (SMRITI — an open-source Obsidian + Claude Code setup) has a lot of standing context an assistant needs on every session: the tag taxonomy, the frontmatter schema, the folder conventions, the area definitions. The naive approach is to let the assistant read five to ten files each time and <em>infer</em> the shape. Instead, a build step assembles all of it into one static file — the "L1 cache" — from the source documents. One deterministic read replaces a scavenger hunt. The content is identical every session because it's <em>assembled by code from structured sources</em>, not re-derived by a model that might read differently on a bad day. The model spends its budget on the actual task, not on rediscovering its own filing system.</p>
<p><strong>Deterministic state bundles.</strong> My morning and evening routines used to be "assistant, figure out my day." Now a plain script produces a JSON bundle: today's date, the tasks due, completion counts, which cadences are overdue, project counts by status. All of that is <em>structured</em> — it lives in files and a task API with dates and fields. So a script reads it, and the assistant <em>consumes</em> the bundle instead of <em>inferring</em> it. The judgment-heavy part — "given all this, what should I focus on" — still goes to the model. But the model is reasoning over facts it can trust, because a script produced them, not a summarization pass that might drop a task or invent one.</p>
<p><strong>Retrieval, split by structure.</strong> This one is the sharpest. In RAG, there's a reflex to throw an LLM at everything: generate queries, re-rank, summarize. But retrieval quality often hinges on parts that are <em>pure structure</em>. An exact identifier — a function name, an error code, a SKU — is a keyword match; BM25 and a metadata filter find it deterministically and better than a dense model that "sort of" understands the token. Date ranges, categories, source filters — all structured, all deterministic. Reserve the semantic model for the genuinely semantic query ("what do these papers say about reranking"), and let deterministic retrieval own the part that's really a lookup. Stacking an LLM on top of a keyword match doesn't make it smarter; it makes it slower and less certain.</p>
<p><strong>Rule enforcement, tiered.</strong> The same split shows up in code-standards enforcement — hundreds of rules to check. The instinct is "it's about code quality, so use the LLM." But a large share of those rules are deterministic: a regex, a config-file assertion, a structural check. Only a minority need real semantic judgment. So the rules are <em>tiered</em> — the mechanical majority run as free, reproducible detectors; only the true-semantic residue is spent on a model call. Same principle: push everything that structure can decide onto deterministic detectors, and let the LLM handle only what's irreducibly semantic.</p>
<p>The pattern is identical across all four: <strong>find the structure, let code exploit it, and shrink the model's job to the part that has no structure to exploit.</strong></p>
<h2>Drawing the line</h2>
<p>The useful discipline is a single question you ask at every step of a pipeline:</p>
<p><strong>Does this input have structure that a query, parser, or template can exploit?</strong></p>
<ul>
<li><strong>Yes</strong> → assemble it with code. Schema fields, tags, dates, IDs, file paths, config, table rows, anything with a grammar. Deterministic, free, reproducible.</li>
<li><strong>No</strong> → this is the residue. Natural-language meaning, open-ended synthesis, judgment that no field encodes. <em>This</em> is what the LLM is for.</li>
</ul>
<p>Run that question honestly and the residue is almost always smaller than the first design assumed. The architecture work is drawing that boundary well — and it's real work, because the tempting move is to wave the whole thing at the model and call the vagueness "flexibility."</p>
<p>A good test: if you can't write down <em>why</em> a step needs a model — if the honest answer is "it was easier to prompt than to parse" — that step is probably deterministic work wearing an LLM costume.</p>
<h2>"But my agent's task is whatever the customer asks"</h2>
<p>Everything so far might read as if this only works when you know the task in advance — my morning routine does the same thing every day, so of course a script can assemble its context. What about an agent facing a customer, where the task is whatever they type? There's no fixed pipeline to make deterministic.</p>
<p>The principle survives this; you just have to see where it lives. It was never "the pipeline is fixed." It's the division of labor <em>at each step</em>.</p>
<p>In a dynamic agent, the model's job is to turn an open-ended ask into a <strong>structured request</strong>: which tool, which parameters, which record, which filter. Interpreting <em>"can you check why my last order was delayed"</em> into <code>get_order(customer_id, latest=true)</code> is genuinely semantic work — no schema encodes it — so it belongs to the LLM. But once the request is structured, <strong>fulfilling it is deterministic</strong>: the tool runs, the query returns real rows, the file is read. Then the model reasons over context that was <em>fetched</em>, not <em>fabricated</em>.</p>
<p>So the dynamism lives in <em>deciding what to fetch</em> — not in the fetching. The customer doesn't dissolve the determinism; they relocate the model's job to the boundary (interpret the ask, route to a source), and everything past that boundary stays deterministic.</p>
<p>And this is worth saying plainly: <strong>if your agent already has the LLM choose among deterministic tools, you're applying this principle correctly.</strong> A tool-using agent is the canonical case, not a counterexample. The tools are the deterministic assembly; the model is doing the irreducibly semantic routing. The principle just names <em>why</em> that architecture is right — and where it still quietly goes wrong.</p>
<p>Because it does still go wrong, even with tools. The line is <strong>generate vs. fetch.</strong> It's fine — necessary — for the model to <em>decide</em> what to pull. It's the anti-pattern for the model to <em>become the source</em>: recalling the customer's order history from its own weights instead of calling the orders tool, summarizing a policy from memory instead of reading the document, inventing a config value instead of looking it up. The instant the model is the <em>origin</em> of a fact rather than the <em>router</em> to one, you've traded a deterministic fetch for a hallucination — and in a customer-facing system, that's the fact that lands in an email or a refund.</p>
<p>So the discipline for a dynamic agent is narrow and strict: <strong>let the model route freely, but source every fact deterministically.</strong> Route with the LLM; never let it <em>be</em> the source of a fact when a source exists.</p>
<h2>Every hallucination is a missing tool</h2>
<p>All of this only holds if the deterministic source <em>exists</em>. Where there's no tool, no query, no schema to fetch from, the model has no choice but to generate — and you're back to inventing facts. Your deterministic layer is only as good as your tool set's coverage, which makes finding the gaps an ongoing job, not a one-time design pass.</p>
<p>It reframes a debugging reflex. When an agent hallucinates, the instinct is to tune the prompt. A better first question: <em>was there a fact here it should have looked up — and does the tool to look it up exist?</em> A surprising share of hallucinations are <strong>tool gaps in disguise</strong> — the model papering over a hole the architecture left open.</p>
<p>So the loop runs continuously: watch where the model is <em>sourcing</em> facts instead of <em>routing</em> to them; each one is a candidate for a new tool or query; build it; move that fact from generate to fetch. Every tool you add shrinks the surface where fabrication can get in.</p>
<p>And the judgment is two-directional — that's the craft. Not every gap deserves a tool. Some residue is genuinely semantic and belongs to the model; over-tooling it is its own failure, brittle and over-fitted. So the standing work is telling apart <em>"this is a structured fact I just haven't built a source for yet"</em> (a backlog item) from <em>"this is irreducibly semantic"</em> (by design). Your tool set becomes a living map of where the system trusts code versus where it trusts judgment — and keeping that map honest, as the product and its data grow, is a permanent architecture responsibility, not a decision you make once.</p>
<h2>Why determinism at the context layer matters</h2>
<p>Pushing structure onto code isn't just a cost optimization. It changes the properties of the whole system:</p>
<ul>
<li><strong>Reproducibility.</strong> Same input, same context, same setup for the model — every time. When something goes wrong, you can actually reproduce it, because the inputs weren't re-improvised on each run.</li>
<li><strong>Debuggability.</strong> You can <em>read</em> assembled context. It's a file, a query result, a bundle. When the model misbehaves, you inspect exactly what it was handed. Context generated by another model call is a moving target you have to re-run to even see.</li>
<li><strong>Cost and latency that don't scale with cleverness.</strong> A lookup is a lookup at any volume. You're not paying per-token to answer questions your data already answers.</li>
<li><strong>A smaller trust surface.</strong> Every LLM call is a place the system can hallucinate. Fewer calls, each with a tighter job, means fewer places for fabrication to enter — and the ones that remain are the ones you <em>chose</em>, where semantics genuinely required it.</li>
</ul>
<p>That last point is the crux. Determinism at the context layer is what makes the model layer <em>trustworthy</em>. You're not trying to make the LLM perfect; you're shrinking its blast radius to the irreducible semantic core and building everything around it from materials you can verify.</p>
<h2>The architect's move</h2>
<p>The reframe I'd leave you with: stop thinking of the LLM as the system and the code as glue. Invert it. <strong>The deterministic assembly is the system. The model is one component you call for the part that has no structure.</strong></p>
<p>The prompt is 10%. The 90% is an engineering surface — and most of it, if you look, is already structured. Your job as the architect is to see that structure and refuse to pay a model to reproduce what your code already knows.</p>
<p>Generate only what you genuinely can't assemble. Assemble everything else.</p>
<hr />
<p><em>Built by <a href="https://deepakgoyal.ai">Deepak Goyal</a>. Follow the journey: <a href="https://github.com/deepakgoyal-ai">GitHub</a> · <a href="https://linkedin.com/in/deepakgoyal-ai">LinkedIn</a></em></p>
]]></content:encoded></item><item><title><![CDATA[I stopped being my second brain's scheduler]]></title><description><![CDATA[I stopped being my second brain's scheduler
Cadences that surface what's due, habits that flag a slipping streak, and a live HUD that watches the vault — and it ships without a single background cron.]]></description><link>https://blog.deepakgoyal.ai/stopped-being-my-second-brains-scheduler</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/stopped-being-my-second-brains-scheduler</guid><category><![CDATA[obsidian]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[automation]]></category><category><![CDATA[open source]]></category><category><![CDATA[personal knowledge management]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Sun, 02 Aug 2026 06:33:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69e9e9b716b084c068f429cf/5546b9f8-577a-4528-8cae-852d312ff8de.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>I stopped being my second brain's scheduler</h1>
<p><em>Cadences that surface what's due, habits that flag a slipping streak, and a live HUD that watches the vault — and it ships without a single background cron. SMRITI v2.1.</em></p>
<hr />
<h2>1. I was the scheduler</h2>
<p>The system I'd built was good at holding structure. It was terrible at reminding me the structure existed.</p>
<p>Every month there was a review I was supposed to run per area. Every week, a backlog to groom. There was a walking habit I'd sworn to keep. And while I worked inside Claude Code all day, the state of my vault — what's due, what's done, what's slipping — lived somewhere I had to go and <em>look</em>.</p>
<p>So I became the scheduler. I remembered the monthly review three weeks late. I noticed the walking streak had already collapsed. I context-switched out of my terminal to check what today's tasks even were.</p>
<p><em>Quick context:</em> <a href="https://github.com/deepakgoyal-ai/smriti">SMRITI</a> is an open-source second brain that pairs <a href="https://obsidian.md/">Obsidian</a> with <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>. <a href="https://blog.deepakgoyal.ai/ai-powered-second-brain-obsidian-claude-code">v1.0</a> gave it daily-companion behaviours. <a href="https://blog.deepakgoyal.ai/rebuild-ai-second-brain-architecture-structure-standards">v2.0</a> gave it architecture — skills + agents, per-area Projects, Standards. Both gave me structure. Neither made the structure <em>surface itself</em>.</p>
<p>v2.1 is the release where the system starts watching for me. Three pieces — recurring-work cadences, a habit tracker, and a live statusline. And one deliberate call about what to <em>ship</em>: no crons, no background jobs — you'll see why in a minute. Here's what hurt, what I built, and the lesson that shaped all three.</p>
<hr />
<h2>2. The work that silently slipped</h2>
<h3>The pain</h3>
<p>Some work isn't a one-off task. It recurs: a monthly area review, a weekly content-backlog grooming, a quarterly retrospective. In my Project files these lived as recurring Initiatives with a <code>planned</code> date.</p>
<p>Nothing watched that date.</p>
<p>So the monthly review would come due on the 31st, and I'd notice it around the 20th of the <em>next</em> month — if I noticed at all. The Initiative sat there, quietly overdue, because the only thing that could catch it was me remembering to scroll the project list. A recurring cadence that depends on human memory isn't a cadence. It's a hope.</p>
<h3>The fix: two small scripts that make dates speak</h3>
<p>Two pieces of plain bash — no network, no daemon.</p>
<pre><code class="language-plaintext">cadence-scan.sh   → "which recurring Initiatives have planned &lt;= today?"
cadence-roll.sh   → advance a completed one's planned by its interval
</code></pre>
<p><code>cadence-scan</code> globs the Projects folder, reads frontmatter, and surfaces anything whose <code>planned</code> date has arrived. It runs inline inside <code>/start-my-day</code> — so the morning brief now carries a <strong>Cadences Due</strong> section:</p>
<pre><code class="language-plaintext">### ⏱️ Cadences Due
⏱️ area-review — Monthly area review · monthly · 2d overdue · opens on next close
</code></pre>
<p>When the iteration is actually done, <code>/close-my-day</code> runs <code>cadence-roll</code>: it advances <code>planned</code> by one interval (<code>monthly</code> → +1 month, <code>quarterly</code> → +3) so the cadence stops nagging, bumps the iteration counter, and — if the Initiative is marked <code>auto_scaffold: true</code> — opens the next iteration's task list automatically.</p>
<p>Two new frontmatter fields carry the behaviour:</p>
<ul>
<li><p><code>auto_scaffold: true</code> — open the next cycle without asking.</p>
</li>
<li><p><code>nth_weekday: "4 Sat"</code> — snap each roll to the 4th Saturday of the month instead of a fixed calendar date that drifts across weekdays.</p>
</li>
</ul>
<h3>Why it matters</h3>
<p>The date became the trigger, not my memory. A recurring review now surfaces itself the morning it's due, rolls itself forward the evening it's done, and re-opens its own next iteration. I went from <em>remembering to check</em> to <em>being told</em> — and the only thing that changed is that a 90-line script reads a frontmatter field I was already writing.</p>
<hr />
<h2>3. The streak I couldn't see</h2>
<h3>The pain</h3>
<p>I had a daily walking target. I'd log my step count in the day's note and… that was it. The number went into a file and never looked back at me.</p>
<p>So the failure mode was invisible. One low day is noise. Two in a row is the start of a collapse — but I only ever noticed the collapse after a week of it, when the habit was already gone. There was no moment where the system said <em>hey, that's two.</em></p>
<h3>The fix: a habit is a file, and the evening close reads it</h3>
<p>Each habit becomes its own small file with a check-in spec in its frontmatter:</p>
<pre><code class="language-yaml">phase_65:
  capture:
    type: number
    source: frontmatter
    key: steps
    low_threshold: 5000
  miss_rule:
    window_days: 2
    counts_as_miss: below-threshold
    flag: "⚠️ Daily Steps: {miss_count}-day streak break"
</code></pre>
<p>Now <code>/close-my-day</code> does a habit check-in. It reads today's step count from the daily note, flags a same-day low, then <strong>walks back</strong> the window and counts consecutive misses. Two days under the floor and it surfaces the flag — the night it becomes two, not the week after.</p>
<p>The detail I'm proudest of is the ramp-up clamp: a habit you promoted three days ago can't fire a "2-day streak break," because it clamps the look-back to <code>min(window, days_since_promoted)</code>. New habits get a grace period instead of a false alarm. The tracker meets you where the habit actually is.</p>
<p>It handles two capture types — <code>number</code> (steps, weight, hours slept) and <code>event</code> (did I journal today? did I log a decision?) — so the same machinery covers "how much" and "did I at all."</p>
<h3>Why it matters</h3>
<p>A single disruption shouldn't be able to snowball into a collapse. The whole value of the miss flag is catching the <em>second</em> day, while recovery is still one walk away. The system now holds the streak's memory so I don't have to — and tells me at the exact moment intervention is cheap.</p>
<hr />
<h2>4. The vault was invisible while I worked</h2>
<h3>The pain</h3>
<p>I live inside Claude Code. And while I'm there — heads-down, building — the state of my second brain is somewhere else. What's due today? How many quests have I closed this week? How's the project load across areas? Every one of those questions was a context switch: stop, go look, come back.</p>
<p>The information existed. It just wasn't <em>where my eyes already were.</em></p>
<h3>The fix: a HUD that reads the vault</h3>
<p>v2.1 ships a native Claude Code statusline — a five-row heads-up display that renders live, right under the prompt:</p>
<pre><code class="language-plaintext">⠹ SCANNING THE ATLAS…
🪔 SMRITI          │  🌍 YOUR CITY  │  Sun Aug 2  🕔 09:50
⚔️ PROJECTS   WORK Σ24 ✓9 ▶6   │   LEARNING Σ8 ✓2 ▶3   │   HEALTH Σ4 ✓1 ▶1
📥 INBOX 2   │   ⚔️ 3  🎯 1   │   📅  Mo 5/5  Tu 4/4  …  ▸Su 2/3
🛞 SARATHI   Claude  │  SK 6 · AG 8  │  ●●●●●●········  42%
</code></pre>
<p>Top to bottom: a rotating activity line; identity and clock; per-area project counts (total / done / active); today's inbox, quests, and bounties with a weekly Mon–Sun rollup; and the model, skill/agent counts plus a live context-window bar.</p>
<p>The engineering that matters is underneath. The slow parts — scanning folders, counting projects, rolling up the week — are precomputed into a small cache by <code>/start-my-day</code> and <code>/close-my-day</code>. The live render does zero folder-walking; it reads the cache and prints. So it's instant on every refresh, not a lag spike.</p>
<p>And here's the part I reworked before release: <strong>it reads entirely from your vault.</strong> The per-area counts come from your Project folders. Today's quests and the whole weekly rollup come straight from your daily notes' checkboxes. There's no service it has to phone. Early on I'd wired the weekly row to my task manager; that was a leak of my setup into everyone's. The final version derives <em>every</em> row from plain markdown you already have — so the HUD works the same whether or not you use any external tool.</p>
<p>The areas aren't hardcoded either. It reads whatever sub-folders live under your Projects directory, so your HUD shows <em>your</em> domains, not mine.</p>
<h3>Why it matters</h3>
<p>Ambient awareness beats on-demand lookup. I stopped leaving my terminal to ask "what's the state of things," because the state of things is now a glance away, updating as I work. The vault went from a place I visit to a thing I can feel in my peripheral vision.</p>
<hr />
<h2>5. Why I didn't ship a single cron (yet)</h2>
<p>Here's a confession that complicates the story: on <em>my</em> machine, some of this is scheduled. I have <code>launchd</code> jobs that run quiet maintenance while I sleep. I like them. They earn their keep.</p>
<p>So why does v2.1 ship with zero crons?</p>
<p>Because a schedule is a commitment I shouldn't make on your behalf. A second brain that installs background jobs the moment you clone it is a second brain you now have to <em>operate</em>: processes to monitor, failures that happen while you're not looking, a machine that has to stay on. That's a fair trade for me — on my setup, with my tolerance for babysitting infrastructure. It's a lousy thing to force on a stranger who just wanted to try it out.</p>
<p>So every piece of v2.1 hooks into a moment you <em>already show up</em>: the morning brief, the evening close, every statusline render. Cadences surface when you open the day. Streaks get checked when you close it. The HUD updates because you're typing anyway. Nothing runs behind your back, nothing needs babysitting, and it works the first minute after you clone — no scheduler, no daemon, no setup.</p>
<p>And if you <em>do</em> want it hands-free? The scripts are built for exactly that. <code>cadence-scan</code> and the habit check take no arguments and touch nothing but local files — point your own <code>cron</code> or <code>launchd</code> at them and you've got the scheduled version, on your terms, when you decide you want it. I left it as your choice instead of my default.</p>
<p>Making that opt-in — clean, documented, and yours to toggle — is where the next release starts.</p>
<hr />
<h2>6. What's in v2.1, and how to get it</h2>
<p>v2.1 is <strong>additive and drop-in</strong> over v2.0. Same six skills, same eight agents, no vault migration. If you're on v2.0, you pull and you're done.</p>
<ul>
<li><p><strong>Cadence engine</strong> — <code>cadence-scan</code> + <code>cadence-roll</code>, new <code>auto_scaffold</code> / <code>nth_weekday</code> fields, wired into start/close-my-day.</p>
</li>
<li><p><strong>Habit system</strong> — file-per-habit with a daily check-in, streak walk-back, and miss flags, plus a worked example.</p>
</li>
<li><p><strong>Native statusline</strong> — the five-row HUD, vault-sourced, auto-configured by <code>setup.sh</code>.</p>
</li>
<li><p><strong>No crons. No new skills or agents.</strong> The whole automation layer is skill-invoked local bash.</p>
</li>
</ul>
<p><strong>What's next.</strong> The opt-in scheduling layer is the headline candidate for v2.2: wire these same scripts into <code>cron</code>/<code>launchd</code> with a one-command installer, a clean on/off switch, and setup docs — so the fully hands-free version is a deliberate choice you make, never a default you inherit. If that's the piece you want, say so on the repo (an issue or a star) — reader signal is genuinely what decides what ships next.</p>
<p>It's open source — clone it, run <code>setup.sh</code>, and it'll offer to wire the statusline for you: <a href="https://github.com/deepakgoyal-ai/smriti">github.com/deepakgoyal-ai/smriti</a>.</p>
<p>The throughline across v1.0, v2.0, and now v2.1: a second brain shouldn't ask you to remember it exists. v2.0 made it coherent. v2.1 made it look up and wave when something needs you — without ever running behind your back, and without asking you to run anything behind yours.</p>
<hr />
<p><em>Built by</em> <a href="https://deepakgoyal.ai"><em>Deepak Goyal</em></a><em>. Follow the journey:</em> <a href="https://github.com/deepakgoyal-ai"><em>GitHub</em></a> <em>·</em> <a href="https://linkedin.com/in/deepakgoyal-ai"><em>LinkedIn</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Building Production RAG on ArXiv — Part 1: Architecture & Ingestion]]></title><description><![CDATA[Building Production RAG on ArXiv — Part 1: Architecture & Ingestion
What I'm building, and why
Nobody can keep up with AI research anymore. On ArXiv — the open-access preprint server where nearly all ]]></description><link>https://blog.deepakgoyal.ai/building-production-rag-arxiv-part-1</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/building-production-rag-arxiv-part-1</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Thu, 30 Jul 2026 14:27:15 GMT</pubDate><content:encoded><![CDATA[<h1>Building Production RAG on ArXiv — Part 1: Architecture &amp; Ingestion</h1>
<h2>What I'm building, and why</h2>
<p>Nobody can keep up with AI research anymore. On <a href="https://arxiv.org">ArXiv</a> — the open-access preprint server where nearly all AI/ML work lands first — the three machine-learning categories I care about (<a href="https://arxiv.org/list/cs.AI/recent"><code>cs.AI</code></a>, <a href="https://arxiv.org/list/cs.CL/recent"><code>cs.CL</code></a>, and <a href="https://arxiv.org/list/cs.IR/recent"><code>cs.IR</code></a>) publish more papers in a week than anyone can read in a month. The tooling for <em>interrogating</em> that firehose — "what do the last hundred papers say about reranking, and can I trust the answer?" — barely exists outside closed products.</p>
<p>So I'm building one, in the open. A system that ingests a pinned corpus of ArXiv papers and answers real questions against it: <a href="https://arxiv.org/abs/2005.11401">retrieval-augmented generation</a> (RAG) today, a multi-agent researcher-critic-synthesizer by the end. The code is <a href="https://github.com/deepakgoyal-ai/arxiv-research-agent">MIT-licensed on GitHub</a>, the write-ups live at <a href="https://deepakgoyal.ai">deepakgoyal.ai</a>, and it's reproducible down to the exact paper. It ships over five parts — each a working release and a post like this one. Not a tutorial, and not a learning diary: an architect building a real system and showing the decisions.</p>
<p><strong>This post is Part 1: the foundation</strong> — the corpus, the ingestion pipeline, the retrieval architecture, and the first evaluation framework. It's the least glamorous slice and the one everything else stands on.</p>
<p>Here's the arc, so you know what you're reading toward:</p>
<ol>
<li><strong>Part 1 (this post) — Foundation.</strong> Pinned corpus, retrieval pipeline, honest eval, FastAPI service.</li>
<li><strong>Part 2 — RAG to agent.</strong> Tool use: summarize a paper, discover citations, find related work.</li>
<li><strong>Part 3 — MCP server.</strong> Expose the agent's tools so Claude Desktop/Code can call them directly.</li>
<li><strong>Part 4 — ReAct workflows.</strong> Multi-step, plan-and-execute research tasks.</li>
<li><strong>Part 5 — Multi-agent.</strong> A researcher, a synthesizer, and a critic collaborating.</li>
</ol>
<p>And one rule runs through all five, the whole reason I write these the way I do: <strong>measurement before claim.</strong> Every comparison I publish shows its methodology, its sample size, its cost, and a command you can run to reproduce it. No "it's better" without the eval that says so.</p>
<p>Which is exactly why Part 1 has to open with a confession: my first eval was measuring nothing.</p>
<hr />
<h2>The eval that was lying to me</h2>
<p>I built a retrieval evaluation, ran it, and got a clean table of numbers. Then I looked closer and realized the metric was scoring itself — the way I'd defined "relevant" was rigged, by construction, to make retrieval look good. I was one commit from publishing a table that looked rigorous and meant nothing. (The exact mechanism is in the retrieval section below, where the numbers live.)</p>
<p>I cut the broken metrics and kept only what survives scrutiny. That's what this post is really about: the interesting part of building RAG isn't the pipeline, which is largely solved — it's knowing whether the thing works, and being honest when your own measurement can't tell you.</p>
<p>The rest is the foundation, in the order I'd build it again: the corpus, the pipeline, and the two evals — one I threw out, one I'll stand behind.</p>
<hr />
<h2>Why ArXiv, why 100 papers, why free-first</h2>
<p><strong>ArXiv</strong> because the corpus is stable (papers don't change under you), acquisition is trivial, the license is friendly, and the content matches the audience — AI/ML research in <code>cs.AI</code>, <code>cs.CL</code>, <code>cs.IR</code>.</p>
<p><strong>100 papers</strong> because it's large enough to escape toy-corpus saturation — where every query trivially retrieves the same handful of chunks — without burning a rate-limit budget on volume I couldn't yet justify. The corpus is pinned: 100 papers, 11,187 chunks. I'll scale to a few hundred only when a gold-labelled eval shows a recall ceiling worth chasing, not before.</p>
<p><strong>Free-first</strong> because most of this workload doesn't need a paid model. <a href="https://huggingface.co/BAAI/bge-small-en-v1.5">BGE-small</a> embeddings run locally, the <a href="https://www.trychroma.com/">ChromaDB</a> vector store is local, the <a href="https://www.sbert.net/examples/applications/retrieve_rerank/README.html">cross-encoder reranker</a> is local, and the default answer model is a free Groq-hosted <a href="https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct">Llama-3.3-70B</a>. Paid models come out only for documented A/B comparisons on small samples. Total project budget: ~$15.</p>
<p>That's a decision, not a limitation. A RAG pipeline that only works when every call hits a frontier API isn't a pipeline — it's a bill.</p>
<hr />
<h2>The pipeline — ingestion decides whether anything downstream is trustworthy</h2>
<p>The whole thing is one line of dataflow:</p>
<p><code>ingest → chunk + embed → ChromaDB → retrieve → (rerank / fuse) → generate → cite</code></p>
<p>exposed over <a href="https://fastapi.tiangolo.com/">FastAPI</a> (<code>/search</code>, <code>/ask</code>, <code>/chat</code>, <code>/ingest</code>, <code>/health</code>).</p>
<p><strong>Ingestion is the unglamorous part, and it's where trust is won or lost.</strong> It hits the ArXiv API with a 3-second politeness delay and automatic retries, and it's idempotent per paper — an interrupted run resumes instead of restarting. Any PDF that won't parse is logged in the run summary and skipped, never silently dropped or left to crash the batch. The output is <code>data/manifest.jsonl</code> — a committed record that pins every paper by its ArXiv ID. That file <em>is</em> the corpus. Anyone can rebuild the identical 100 papers from it.</p>
<p>Those first two items aren't decoration: ArXiv rate-limits aggressive clients, and an early impatient run earned a batch of 429s and 503s and downloaded nothing. Slowing down and checkpointing fixed it — the pinned rebuild then pulled all 100 papers with zero parse failures.</p>
<p><strong>Retrieval is one idiom, repeated.</strong> Every retriever is the same shape — a <code>retrieve(query, top_k) -&gt; list[dict]</code> closure over one consistent chunk dictionary — so a new strategy slots in without touching the callers. Six ship in Part 1, and since these names recur in the results below, here's each in a line:</p>
<ul>
<li><strong>dense</strong> — embed the query and return the nearest chunks by vector similarity (semantic match);</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a></strong> — classic keyword scoring; strong exactly where dense is weak, on rare terms and proper nouns;</li>
<li><strong>hybrid</strong> — run dense and BM25, then fuse the two rankings (see RRF, next);</li>
<li><strong><a href="https://www.sbert.net/examples/applications/retrieve_rerank/README.html">cross-encoder rerank</a></strong> — take a candidate pool and re-score it with a model that reads the query and chunk <em>together</em>, rather than comparing pre-computed vectors;</li>
<li><strong>multi-query</strong> — have an LLM rephrase the question into several variants, retrieve for each, and fuse;</li>
<li><strong><a href="https://arxiv.org/abs/2212.10496">HyDE</a></strong> — have an LLM draft a <em>hypothetical</em> answer first, then retrieve against that answer instead of the bare question.</li>
</ul>
<p>One design note I'd repeat on any RAG build: <strong>implement fusion once.</strong> A single Reciprocal Rank Fusion (RRF) routine — rank each list, score every item by <code>1 / (60 + rank)</code> (60 is RRF's smoothing constant, unrelated to the top-k cutoff), sum across lists — serves both hybrid retrieval (fuse 2 lists: dense + BM25) and multi-query (fuse N lists: one per variant). They look like different features; they're the same operation. Writing it twice is how the two copies drift.</p>
<p>The conversational path is <code>history → LLM rewrite → retrieve → answer</code>, with a <code>bypass_rewrite</code> escape hatch for the failure mode where the rewrite over-constrains a follow-up and retrieval returns nothing.</p>
<hr />
<h2>Measurement — what's honest and what isn't</h2>
<p>Here's where most RAG write-ups quietly cheat. Two evals ran on this corpus. Only one produces a number I'll stand behind — and before either table, here's exactly how each number is computed, because a metric you can't reproduce is just a vibe with a decimal point.</p>
<h3>How the numbers are computed</h3>
<p><strong>Retrieval — <a href="https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Precision">category-precision@k</a>.</strong> Per question: take the top <em>k</em> retrieved chunks (k=5), look up the source paper of each, and check whether that paper's primary ArXiv category is one the question expects (its <code>source_hint</code>, e.g. <code>cs.IR cs.CL</code>). The score is the fraction of the five that match; the reported figure averages that over every question with a usable hint (27 of the 33). It answers one narrow question — "did retrieval stay in the right topic?" — and nothing more.</p>
<p><strong>Generation — claim-level faithfulness.</strong> <a href="https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/faithfulness/">Faithfulness</a> measures how much of an answer is grounded in the retrieved context. A fixed judge model scores it in two steps, at temperature 0:</p>
<ol>
<li><strong>Extract</strong> — the judge reads the answer and lists every atomic factual claim it makes.</li>
<li><strong>Verify</strong> — each claim is checked <em>independently</em> against the retrieved chunks and marked <code>supported</code>, <code>unsupported</code>, or <code>not_found</code>.</li>
</ol>
<p>The question's score is <code>supported ÷ total claims</code>. So if an answer makes four claims and three trace to a sentence in the retrieved context, it scores <strong>0.75</strong> — whether or not the fourth claim happens to be true <em>in the world</em>. That distinction is the whole point: faithfulness asks "did the model stick to its sources," not "is the model right." A confident, plausible sentence with no support in the context is exactly what it's built to catch — because in RAG, that's the shape a hallucination takes.</p>
<p>Each model's faithfulness is averaged across the questions, with a <strong>95% <a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)">bootstrap</a> confidence interval</strong> (2,000 resamples, fixed seed) so the small sample size is visible in the number rather than hidden behind it. When two models' intervals don't overlap, the difference is unlikely to be sampling noise; when they do, treat the gap as unproven.</p>
<h3>One query, end to end</h3>
<p>Abstract metrics are easy to wave at, so here are two real questions from the eval set, run through the real pipeline.</p>
<p><strong>A retrieval miss.</strong> Ask <em>"What is HyDE, and when does it fail?"</em> and the hybrid retriever's top 5 chunks come back as:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Retrieved paper</th>
<th>Primary category</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Evidence Attribution in Visual Document Understanding</td>
<td>cs.CV</td>
</tr>
<tr>
<td>2</td>
<td>Self-Study Reconsidered: Learning from Self-Generated Data</td>
<td>cs.AI</td>
</tr>
<tr>
<td>3</td>
<td>DSCH-Loss: A Dynamic Semantic Channel Objective for Deep Semantic Hashing</td>
<td>cs.AI</td>
</tr>
<tr>
<td>4</td>
<td>Self-Study Reconsidered (a second chunk)</td>
<td>cs.AI</td>
</tr>
<tr>
<td>5</td>
<td>DataOrchestra: Per-Example Curation of Pretraining Data</td>
<td>cs.CL</td>
</tr>
</tbody></table>
<p>Not one is about HyDE. The corpus is 100 <em>recent</em> papers and none of them defines it, so retrieval stays vaguely in the machine-learning neighborhood and misses. Now watch what the proxy does with that miss: the question's hint is <code>cs.IR cs.CL</code>, so category-precision counts the single <code>cs.CL</code> paper (#5) as a hit and the rest as misses — a score of 0.2. But #5 isn't about HyDE either, and the <code>cs.AI</code> papers it scored as misses might be more relevant or less — the metric can't tell. Whether it lands at 0.2 or 0.8, the number is blind to the one thing that matters: did retrieval surface a chunk that actually answers the question. "In the right topic" is all it can ever see.</p>
<p><strong>A perfect faithfulness score for a non-answer.</strong> Now a question the corpus only <em>mentions</em> in passing — <em>"How does Reciprocal Rank Fusion combine rankings?"</em> Given the retrieved context, Claude Haiku answers:</p>
<blockquote>
<p>"…the documents do not provide a detailed explanation of how RRF actually works mathematically or algorithmically… I don't know the detailed methodology based on the provided context."</p>
</blockquote>
<p>The judge extracts five atomic claims from that reply (e.g., "RRF merges ranked lists from multiple retrieval channels," "the specific formula is not explained in the provided context") and finds <strong>all five supported</strong> by the retrieved chunks:</p>
<p><strong>faithfulness = 5 / 5 = 1.00.</strong></p>
<p>A perfect score — for an answer that never answered the question. And that's the metric working <em>correctly</em>: the retrieved chunks mentioned RRF but never gave its formula, so the model declined instead of inventing one, and every claim it did make was grounded. Faithfulness rewards <em>not hallucinating</em>; it says nothing about whether the question got a useful answer. Which is exactly why it's one axis of quality — the one that catches confident fabrication — and not the whole of it. (Getting the right chunk in front of the model in the first place is the retrieval problem above, and the reason Part 2 starts with gold labels.)</p>
<h3>The retrieval eval (the honesty lesson)</h3>
<p>Here's the full mechanism behind the bug at the top of this post. My 33-question eval set carries topical <code>source_hint</code> categories but <strong>no gold chunk or document labels</strong> — nobody has marked "for this question, chunk X is the right answer." My first pass papered over that by deriving each question's "relevant" set from the retrieved results themselves — <code>relevant ⊆ retrieved</code> by construction — so recall was pinned at 1.0-or-0.0 and "precision" only measured category purity. Any recall or MRR computed that way grades the retriever against its own output. That leaves category-precision as the one signal I can defend, and even it only measures topical purity — not whether the <em>right</em> chunk was found.</p>
<table>
<thead>
<tr>
<th>Config (k=5)</th>
<th>Category-precision@5</th>
<th>95% CI</th>
</tr>
</thead>
<tbody><tr>
<td>A — dense</td>
<td>0.23</td>
<td>[0.14–0.33]</td>
</tr>
<tr>
<td>B — dense + rerank</td>
<td>0.10</td>
<td>[0.05–0.16]</td>
</tr>
<tr>
<td>C — hybrid (RRF)</td>
<td>0.16</td>
<td>[0.09–0.22]</td>
</tr>
<tr>
<td>D — multi-query</td>
<td>0.21</td>
<td>[0.13–0.29]</td>
</tr>
<tr>
<td>E — HyDE</td>
<td><strong>0.27</strong></td>
<td>[0.19–0.36]</td>
</tr>
</tbody></table>
<p><em>n = 27 scored of 33 questions · coarse topical-purity proxy — NOT relevance recall.</em></p>
<p>Read that as a diagnostic, not a leaderboard. Reranking (config B) scores lowest — but read <em>that</em> carefully rather than as "reranking is bad." A cross-encoder pulls the single most-relevant chunk to the top; if that chunk happens to sit in a neighbouring category, category-precision counts it as a <em>miss</em>. So a low score here can mean the reranker did its job, not that it failed — which is the whole problem: none of these numbers measure whether the right chunk was found, so none is a verdict on retrieval quality. Publishing 0.27 as a retrieval-quality score would be exactly the lie I caught myself in. Gold doc-level labels are the next eval task (Part 2); publishable retrieval claims wait for them.</p>
<h3>The generation A/B (the number I'll stand behind)</h3>
<p>This one is honest because the metric doesn't score itself. Hold retrieval constant — the <em>same</em> retrieved context per question — vary only the answer model, and score claim-level faithfulness with one fixed judge (GPT-4o-mini). Five models, three provider families, across two size tiers, on a 20-question sample — the first 20 of the 33, a fixed deterministic slice, chosen only to cap paid-model spend:</p>
<table>
<thead>
<tr>
<th>Answer model</th>
<th>Provider</th>
<th>Faithfulness (n=20)</th>
<th>95% CI</th>
</tr>
</thead>
<tbody><tr>
<td><strong>claude-haiku-4-5</strong></td>
<td>Anthropic</td>
<td><strong>0.83</strong></td>
<td>[0.77–0.90]</td>
</tr>
<tr>
<td><strong>claude-sonnet-4-5</strong></td>
<td>Anthropic</td>
<td><strong>0.83</strong></td>
<td>[0.75–0.90]</td>
</tr>
<tr>
<td>gemini-2.5-flash</td>
<td>Google</td>
<td>0.33</td>
<td>[0.16–0.51]</td>
</tr>
<tr>
<td>gpt-4o-mini</td>
<td>OpenAI</td>
<td>0.28</td>
<td>[0.13–0.43]</td>
</tr>
<tr>
<td>gpt-4o</td>
<td>OpenAI</td>
<td>0.20</td>
<td>[0.08–0.34]</td>
</tr>
</tbody></table>
<p><em>n = 20 · judge: GPT-4o-mini (fixed) · faithfulness = supported claims ÷ total claims · 95% bootstrap CI. Latency is deliberately omitted — cached answers contaminate the timings, so a clean latency pass is a separate cold-cache run.</em></p>
<p>Three things this table says, and one it doesn't.</p>
<p><strong>Claude is in a different grounding regime.</strong> Both Anthropic models cluster at 0.83, and their confidence intervals sit clear of every other model's — this is the one gap in the table that isn't sampling noise. Given identical retrieved context, Claude confines its answer to what that context supports far more consistently than the GPT or Gemini models do.</p>
<p><strong>Bigger did not mean more faithful.</strong> Sonnet (0.83) is a dead heat with Haiku (0.83), and GPT-4o's point estimate (0.20) sits below GPT-4o-mini's (0.28) — though those two overlap within the sample, so read that as "no gain," not "GPT-4o is worse." Either way the direction is clear: whatever the flagship tiers buy — reasoning depth, breadth, polish — it isn't grounding on this task. If faithfulness is the axis you care about, the cheap tier is the buy; paying up bought no measurable grounding here.</p>
<p><strong>The likely mechanism is the interesting part.</strong> Faithfulness measures grounding, not correctness. The GPT models tend to answer from what they already "know" about retrieval and RAG — often perfectly reasonable answers — but any claim the model supplies from its own training that the retrieved chunks don't contain scores as unsupported. Claude more often stays inside the provided context — the RRF example, where it flagged what the context didn't cover instead of filling the gap from memory, is that discipline in miniature. So read the table precisely: it does <strong>not</strong> say GPT-4o is a worse model. It says GPT-4o is a <em>less grounded</em> one — and for a RAG answerer meant to cite its sources, grounding is exactly the property you're paying for.</p>
<p><strong>And it isn't Claude just hedging.</strong> The sharp objection to this whole table: a model that declines to answer trivially scores 100% faithful — the RRF non-answer above is exactly that — so maybe Claude wins by saying less. It doesn't. Counting the claims the judge extracts per answer, the Claude models make <em>more</em> of them, not fewer: about 6 (Haiku) and 8 (Sonnet) per answer, versus roughly 3 for GPT-4o-mini, GPT-4o, and Gemini. Claude is asserting twice as much and still grounding a higher share of it — saying more and staying honest, not saying less to protect the ratio.</p>
<p>And the one thing it doesn't say — <strong>the judge isn't playing favorites.</strong> GPT-4o-mini grades every answer, including its own family's, and it ranks OpenAI last. Whatever bias a single LLM judge carries, self-flattery isn't it here.</p>
<p><strong>The caveats, stated plainly, because they're the point:</strong></p>
<ul>
<li><strong>The judge is an LLM proxy, not ground truth.</strong> A single fixed judge keeps the scores comparable, but it's still a model grading models — it can misjudge a claim's support, and it inherits GPT-4o-mini's own blind spots. A second independent judge and some human spot-checks are on the list before I'd treat any single score as settled.</li>
<li><strong>The free production default isn't in this table.</strong> The pipeline's real answerer is the free Groq Llama-3.3-70B; this A/B benchmarks paid alternatives to decide whether paying buys enough grounding to matter. Measuring the free default head-to-head needs a configured Groq key in the eval environment — it's the obvious next baseline, not a result I'm hiding.</li>
<li><strong>Sample size is 20, not 3,300.</strong> The bootstrap intervals are wide on purpose. Non-overlapping intervals (Claude vs the rest) are real signal; overlapping ones (Haiku vs Sonnet, or the three trailing models against each other) are "not yet distinguished," not "the same."</li>
</ul>
<hr />
<h2>Reproducibility and cost</h2>
<p>The corpus rebuilds from the committed manifest:</p>
<pre><code class="language-bash">uv sync
uv run python scripts/run_ingest.py --max 100   # or rebuild the pinned IDs from data/manifest.jsonl
uv run python scripts/build_corpus.py            # 100 papers → 11,187 chunks → ChromaDB (~115 MB)
uv run python tests/eval/run_ab.py               # retrieval A/B (configs A–E)
uv run python tests/eval/run_llm_ab.py --n 20    # generation A/B (5 models)
</code></pre>
<p>One honesty note on the ingest line: ArXiv's "latest N" query drifts by the day, so <code>--max 100</code> today fetches today's newest 100, not my pinned set. The reproducible artifact is <code>data/manifest.jsonl</code> — it pins all 100 ArXiv IDs, and the build downloads exactly those by ID. The query bootstraps the corpus; the manifest <em>is</em> the corpus.</p>
<p><strong>Cost.</strong> The corpus build and the retrieval A/B run on free and local models — local BGE-small embeddings, local cross-encoder, ChromaDB, and the free Groq Llama default for the query-rewrite and HyDE steps → <strong>\(0</strong>. The generation A/B is the only paid run: five hosted models answering 20 questions each, plus a GPT-4o-mini judge extracting and verifying claims — an estimated <strong>~\)0.30</strong> (the repo doesn't meter tokens, so treat that as order-of-magnitude, not metered; GPT-4o and Claude Sonnet are the only non-trivial line items). Every LLM call is disk-cached, so re-running the identical eval is free. Running total stays inside the ~$15 project budget.</p>
<hr />
<h2>What's next</h2>
<p>Part 2 turns this from a pipeline into an agent — tool-using, able to summarize a paper, discover citations, and find related work on its own. And before any retrieval-quality claim gets published, the eval gets gold doc-level labels, so the honest-but-limited category proxy above can finally become a real precision number.</p>
<p>The pattern I'm trying to hold to across all five parts: show the architecture, show the eval, show the cost — and when the eval can't back the claim, say so out loud instead of shipping the pretty table.</p>
<hr />
<p><em>I'm a Technical Architect building AI-enabled systems. I write about what I build — the architecture and measurement decisions under the hood, not the model hype. The full project is <a href="https://github.com/deepakgoyal-ai/arxiv-research-agent">open-source</a>.</em></p>
<p><em>If you've caught your own eval measuring the wrong thing, I'd like to hear how. Find me on X <a href="https://x.com/deepakgoyal_ai">@deepakgoyal_ai</a> or <a href="https://www.linkedin.com/in/deepakgoyal-ai/">LinkedIn</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Sub-Agents Lie About Finishing — and the Fix Isn't a Better Prompt]]></title><description><![CDATA[A sub-agent on my system reported — cheerfully, in detail — that it had closed six stale tasks. It named each one. It summarized the work. All six were still sitting there, open and untouched.
Nothing]]></description><link>https://blog.deepakgoyal.ai/sub-agents-lie-about-success</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/sub-agents-lie-about-success</guid><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[llm]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Wed, 15 Jul 2026 12:01:56 GMT</pubDate><content:encoded><![CDATA[<p>A sub-agent on my system reported — cheerfully, in detail — that it had closed six stale tasks. It named each one. It summarized the work. All six were still sitting there, open and untouched.</p>
<p>Nothing in the agent looked broken from the inside. It had decided to close them, issued what it believed were the right calls, and then did what language models are trained to do: wrote a confident, helpful account of the work. The account described its <em>intention</em>. Nothing in its loop ever went back to check whether reality agreed.</p>
<p>That gap — between what a sub-agent intends and what actually lands in your system — is where silent failures breed. And you can't prompt your way out of it.</p>
<hr />
<h2>Why the narrative and the outcome drift apart</h2>
<p>A language model generates a plausible continuation of its context. When you hand a sub-agent a job and it runs a sequence of tool calls, its final "here's what I did" message is another plausible continuation — a summary of the trajectory it <em>took</em>, weighted toward the outcome it was aiming for.</p>
<p>That's fine when the worst case is a slightly-off answer. It is not fine when the agent is <em>mutating state</em> — writing to a database, closing tickets, moving files, calling a third-party API. There, the summary is a claim about the world, and the model has no privileged access to the world. It has access to its own transcript.</p>
<p>A loud error shows up in that transcript. But the dangerous failures don't error. A call that silently no-ops, drops a field, or half-applies still returns a success-shaped response — and from inside the transcript that's indistinguishable from a call that did exactly what was asked. So the agent reports success, because from where it's standing, success is the most plausible thing that happened.</p>
<p>The uncomfortable part: the better your model is at writing helpful narratives, the more convincing these false reports get.</p>
<hr />
<h2>Four silent failures from one real agent</h2>
<p>This isn't hypothetical. I run a personal "second brain" system where a sync agent reconciles my task manager with my notes — creating, updating, and closing tasks across a boundary. Hardening that one agent surfaced four distinct failure classes:</p>
<ul>
<li><strong>The dropped prefix.</strong> Tasks were supposed to be written with a structured <code>[&lt;project&gt; &lt;id&gt;]</code> title prefix so they could be traced back. The agent "created" them and reported success — but the prefix was silently missing on a batch, breaking the link with no error anywhere.</li>
<li><strong>The default lane.</strong> A required field that assigns each task to its correct section was omitted on some writes. The tasks landed — just dumped into the default bucket instead of where they belonged. Fully "successful," entirely misfiled.</li>
<li><strong>The phantom close.</strong> The six-tasks-closed story above. The agent asserted the close happened; the source of truth said all six were still active.</li>
<li><strong>The mislabeled multitude.</strong> A labeling step inferred each task's category from its folder location. One bug in that inference was a single run away from stamping the wrong label onto 47 tasks — a confident, bulk, wrong mutation, caught only because I read the plan back before it fired.</li>
</ul>
<p>Every one of these produced a clean success report. Not one threw an error. If I'd trusted the narrative, I'd have accumulated silent drift for weeks before an incident forced me to notice.</p>
<hr />
<h2>The fix has one shape: read your own write back</h2>
<p>All four fixes converged on the same structural move — and none of them was "write a better prompt."</p>
<p><strong>Close the loop against the source of truth.</strong> After a mutation, the agent (or the orchestrator above it) re-reads the thing it just wrote — through the actual system API, not its own memory — and asserts the post-state matches what it expected. Closed a task? Fetch it back and confirm it reads as closed. Set a field? Read the record and confirm the field is there. The narrative is no longer evidence; the read-back is.</p>
<p>Three concrete disciplines fall out of that:</p>
<ol>
<li><strong>Pre-flight refusal.</strong> If a required field is absent before the write, the agent refuses and surfaces it — rather than writing a malformed record and calling it done. Fail loudly at the boundary, not silently after it.</li>
<li><strong>Post-write validation.</strong> Every state-changing call is followed by a read-back assertion against the source of truth. No assertion, no "success."</li>
<li><strong>Explicit contracts.</strong> Schema maps and label namespaces are written down and matched against before the write — so "infer it from context" never silently produces the wrong structure.</li>
</ol>
<p>Notice these are all <em>structural</em> checks that live outside the model's judgment. That's the point. You are not asking the agent to be more careful. You are removing its self-report from the trust path entirely.</p>
<hr />
<h2>Where this applies — and where it's overkill</h2>
<p>This discipline earns its cost precisely when a wrong outcome is <em>silent and durable</em>:</p>
<ul>
<li>Any sub-agent that writes to an external system — a database, a task manager, a repo, a third-party API.</li>
<li>Bulk operations where the failure mode is quiet drift rather than a loud crash — sync agents, migration agents, cleanup passes.</li>
<li>Designs where the orchestrator can't cheaply diff before-and-after on its own.</li>
</ul>
<p>It's overkill in the opposite cases:</p>
<ul>
<li>Read-only agents — search, summarize, scan — where the worst case is a bad answer the next step will catch, not corrupted state.</li>
<li>Single-call write paths where a human sees the result immediately in the next turn and would spot a failure anyway.</li>
</ul>
<hr />
<h2>The question that replaces "is the model smart enough?"</h2>
<p>As you wire more sub-agents into systems that <em>do</em> things rather than just answer questions, the load-bearing question stops being "is the model smart enough?" and becomes "how do I know the work actually landed?"</p>
<p>An LLM will always tell you what it meant to do. Trust comes from making it prove what it did — a read-back against the source of truth, not a more persuasive prompt.</p>
<hr />
<p><em>I'm a Technical Architect building AI-enabled systems. I write about what I actually build — the reliability and design decisions under the hood, not the model hype.</em></p>
<p><em>If you've had a sub-agent confidently report work it never finished, I'd like to hear how you caught it. Find me on X <a href="https://x.com/deepakgoyal_ai">@deepakgoyal_ai</a> or <a href="https://www.linkedin.com/in/deepakgoyal-ai/">LinkedIn</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Parallel Sub-Agents Change How You Plan, Not Just How You Build]]></title><description><![CDATA[You can plan a day around what you can do, or around what you can decide. Until recently those were the same plan, so it never mattered which you meant. Parallel sub-agents pull them apart — and the i]]></description><link>https://blog.deepakgoyal.ai/parallel-sub-agents-change-how-you-plan</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/parallel-sub-agents-change-how-you-plan</guid><category><![CDATA[AI]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Sat, 11 Jul 2026 08:07:32 GMT</pubDate><content:encoded><![CDATA[<p>You can plan a day around what you can do, or around what you can decide. Until recently those were the same plan, so it never mattered which you meant. Parallel sub-agents pull them apart — and the interesting part isn't that the work goes faster. It's that <em>what you're planning</em> changes underneath you.</p>
<p>For years I planned my days around a single number: how much work I could personally get through. Six, maybe eight focused hours, and I'd fill the day to that line. I thought I was planning the work. I was planning <em>me</em> — my hands were the bottleneck, so my capacity was the plan.</p>
<p>That number is now wrong, and staying anchored to it leaves most of the day on the table.</p>
<hr />
<h2>Two kinds of work, and only one is yours</h2>
<p>Almost any task is a blend of two kinds of work that behave nothing alike.</p>
<p><strong>Mechanical work</strong> — reading files, summarizing, extracting, drafting first cuts, bulk-editing. It has no internal dependencies: item 7 doesn't need item 6. Hand it to a fleet of sub-agents and it happens all at once.</p>
<p><strong>Judgment work</strong> — deciding what to do, setting priorities, cutting the dead project, reviewing what came back and saying yes or no. Each call leans on the ones around it and on context only you hold. It can't be handed off, and it can't be parallelized.</p>
<p>For most of knowledge work's history, these two were welded together. You couldn't make the decisions without first doing the reading, so the reading time was simply "the work," bounded by how fast you personally moved through it. Parallel dispatch pulls the two apart. The mechanical half moves to the machine. The judgment half stays exactly where it was — with you, one call at a time.</p>
<p>So your scarce resource was never really <em>time</em> — it was <em>judgment</em>, trapped inside a lot of mechanical hours that hid the difference.</p>
<p>If that sounds like delegation, it isn't — not quite. Delegating to people never exposed this, because a human team has its own execution hours and coordination cost; the mechanical time just moved — it didn't vanish. Collapse that mechanical time toward zero, and your judgment is the only wall left.</p>
<hr />
<h2>The number that sets a day's capacity changed</h2>
<p>Once the mechanical half isn't yours, the question that sizes a day flips. It's no longer "how much can I execute?" It's "how much can I decide and review?" — and those numbers are nowhere close.</p>
<p>Here's the concrete shift in my own planning. I used to schedule my days at six to eight hours of work — the amount I could actually do with my own hands. Now I plan them at fifteen to twenty hours of work — measured as what it would have cost me by hand — while spending only a few hours steering it.</p>
<p>Not because I got faster, and not because I started grinding longer. Because most of those hours are mechanical, and mechanical hours aren't mine to grind through anymore. My few hours go to the part that's still mine: deciding what to send off, and judging what comes back — and the judging is real work, not a rubber stamp. Some output is faster to redo than to salvage; catching plausible-but-wrong results is the tax on the leverage.</p>
<p>The plan stopped being a list of what I can do. It became a list of what I can <em>decide</em> — and each decision now pulls far more finished work behind it than it used to.</p>
<hr />
<h2>Why the old estimate quietly wastes the day</h2>
<p>If you keep planning against your execution hours, you make the same mistake I did for a while: you under-load the day and never notice.</p>
<p>You block the calendar for the eight hours you can personally work, fill it with eight hours of tasks, and feel fully booked. But the machine that could have carried the mechanical bulk of that work sat idle, because you never scoped for it. You were busy the whole day and still left most of the day's real capacity unused — not because you ran out of hours, but because you ran out of <em>plan</em>.</p>
<p>The fix is to budget the resource that's actually scarce. Estimate the day by your decision-and-review hours, then load work onto it until <em>those</em> hours are full — not until your execution hours are. The mechanical bulk rides along underneath; it isn't what you're pricing.</p>
<p>In practice I scope a day or a sprint at roughly 2–3× what my old execution-bound estimate would have said, then correct from there. (That multiplier is my own calibration from planning-heavy weeks, not a benchmark — yours will track how much of your work actually decomposes into machine-runnable pieces.)</p>
<hr />
<h2>When the split isn't there</h2>
<p>This only rewrites your planning when there's a mechanical half to hand off. Often there isn't, and it's worth being honest about where:</p>
<ul>
<li><p><strong>Sequential design</strong> — where each step depends on the last decision — is judgment end to end. There's nothing to offload, so your execution hours are still the ceiling.</p>
</li>
<li><p><strong>A single hard artifact</strong> — one proof, one gnarly file, one architecture — has no surface to split.</p>
</li>
<li><p><strong>An external bottleneck</strong> — a slow CI run, a deploy, a human reviewer — doesn't care how many agents you have. The agents just wait in parallel.</p>
</li>
</ul>
<p>For that kind of work, plan the old way: you <em>are</em> the executor, and six-to-eight is honest. The reframe only pays off to the degree your week decomposes — and more of it decomposes than you'd guess once you look for the seam.</p>
<p>Stakes are a different axis, not an exception. Higher stakes don't hand the mechanical work back to you — they make the review half deeper, so you steer less in a day. The split holds; the price of your half goes up.</p>
<hr />
<h2>The skill that actually changes</h2>
<p>The instinct says adopting parallel sub-agents means learning to drive a fleet. The prompting is the easy part. The skill that changes is planning itself: seeing the judgment inside your work separately from the machinery around it, and sizing your days by the judgment alone.</p>
<p>Plan your decision-and-review hours, not your execution hours. The day holds as much work as you can <em>steer</em> — which is a great deal more than you can <em>do</em>. And the next time you fill a day to eight hours and call it full, ask what the machine could have been carrying while you did.</p>
<hr />
<p><em>I'm a Technical Architect writing about the AI systems I design and how they change the way I work. The capacity numbers above are from planning my own knowledge system; treat them as a starting point to tune, not a law.</em></p>
]]></content:encoded></item><item><title><![CDATA[AI's Value Compounds at the Task Layer, Not the Model Layer]]></title><description><![CDATA[When a team sets out to add AI to something, the first real argument is usually about the model. Which provider, which version, this one or the one shipping next month. It feels like the decision ever]]></description><link>https://blog.deepakgoyal.ai/ai-value-compounds-at-the-task-layer</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/ai-value-compounds-at-the-task-layer</guid><category><![CDATA[AI]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[software development]]></category><category><![CDATA[ai strategy]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Wed, 08 Jul 2026 14:55:46 GMT</pubDate><content:encoded><![CDATA[<p>When a team sets out to add AI to something, the first real argument is usually about the model. Which provider, which version, this one or the one shipping next month. It feels like the decision everything else rests on.</p>
<p>It's the most throwaway decision in the system.</p>
<p>Frontier models are converging and commoditizing. The one you pick tends to get matched within a few model cycles, and swapping it out is far less work than rebuilding the task around it. So any edge the model itself gives you is temporary — the same edge everyone else will have a quarter later. The value that actually builds up sits one layer up: in how you redesign the work itself around what AI now makes possible.</p>
<p>I learned this the way most things worth knowing get learned: by spending real effort on the wrong layer first.</p>
<hr />
<h2>The model layer is infrastructure</h2>
<p>That last point echoes something Benedict Evans has said about foundational technology cycles: the model layer is this cycle's TCP/IP — genuinely transformative, and <em>not where the durable value gets built</em>. TCP/IP made the web possible, but the value that lasted accrued one layer up — in what the protocol newly allowed, not in the protocol itself.</p>
<p>Same here. GPT-N, Claude-N, Gemini-N are the foundation. "Which model should we use?" is a buying question — it has a cheapest-good-enough answer and a short shelf life. The question that actually shapes an architecture is different: <strong>which tasks change shape once AI is embedded?</strong> Which steps dissolve, which get re-decomposed, which new ones appear. That question has answers that survive across model generations.</p>
<hr />
<h2>A worked example: the review gate</h2>
<p>The clearest case I've worked on is an automated review gate — the kind that checks every incoming change against a house rulebook and flags what's out of line. The first meetings went exactly where I've been describing: <em>which model judges violations best?</em> We treated it as a model-selection problem and spent our attention accordingly.</p>
<p>That framing was the mistake, and the decomposition exposed it. When I broke the rulebook down check by check, the question stopped being "which model" and became "what is each check, actually?" Most of them turned out to be <strong>structural</strong> questions, not questions of meaning — "does this component reach for something it isn't allowed to?" is a lookup over structure, not a judgment call. Only a minority genuinely turned on intent, the kind of thing — "is this message actually helpful to a human?" — where a language model is the right and only tool for the job.</p>
<p>Here is the part that reframed everything for me. <strong>A frontier-model upgrade would have sharpened that semantic minority a little and left the entire structural majority exactly where it was.</strong> The bulk of the gate's value — the checks meant to run on every change, every time — never depended on model capability at all. It came from <em>recognizing they were structural</em> and routing them to something that inspects structure. The value was in the decomposition. For most of the system, the model was almost a distraction from the decision that mattered.</p>
<p>I'm deliberately not reprinting the routing mechanics here — that's a cost story I've told elsewhere. The point for <em>this</em> argument is simpler and more lasting: the leverage came from rethinking the task, and a better model would not have delivered it.</p>
<hr />
<h2>Why one layer compounds and the other doesn't</h2>
<p>Model gains never last. You upgrade, and within a quarter the whole field upgrades with you; your lead washes out. It's a floor that rises for everyone at once — real, but never <em>yours</em>.</p>
<p>Task decomposition keeps paying off. Once the gate is split into "structural checks" and "genuine judgment," that split doesn't ride on frontier progress. A better model sharpens the judgment slot and leaves the structural checks doing exactly what they did. Each task you re-decompose is an asset that keeps paying off for as long as the task exists — a model swap can't erode it. Each model you pick is a rental that expires.</p>
<p>That's the whole thing in a line: <strong>the model is the part that dates fastest; the task redesign is the part that lasts.</strong> Your scarce attention belongs on the layer that compounds — which is rarely the one the first meeting wants to argue about.</p>
<hr />
<h2>The hard part is drawing the lines — and you'll draw some wrong</h2>
<p>This can read like a truism — <em>of course</em> decomposition matters. The sharper point is that AI raises the stakes on getting it right: the easy path, one prompt for everything, quietly buries the whole structural majority of steps that should never touch a model at all.</p>
<p>And drawing those lines is genuinely hard. The whole approach rests on one skill: telling <em>judgment</em> from <em>structure</em> — which steps truly need a model to understand meaning, and which only look like they do because it's easier to send everything through one prompt.</p>
<p>You will misjudge some. In the gate, a handful of checks looked purely structural but hid a meaning-based edge case that a naive rule would silently miss. The rule I settled on: when a check is ambiguous, send it <em>up</em> to the model rather than let a mechanical shortcut quietly get it wrong. Getting those boundaries right is iterative, and unlike a model swap, every correction you make sticks. The decomposition keeps improving precisely because you keep sharpening where the lines fall.</p>
<hr />
<h2>How to apply it</h2>
<p>Next time someone says "let's add AI to X," resist opening with the model. Open with the task:</p>
<ol>
<li><p><strong>Decompose X into its real steps and decisions</strong> — the actual ones, not the category name.</p>
</li>
<li><p><strong>For each, ask: judgment or structure?</strong> Does a model genuinely earn this step, or does it just look that way because one prompt is easier than one design?</p>
</li>
<li><p><strong>Redesign the workflow around that split</strong> — deterministic where you can, model where you must — not around the model's interface.</p>
</li>
<li><p><strong>Treat model choice as the last and most swappable decision.</strong> Pick the cheapest one that clears the bar, and assume you'll replace it within the year.</p>
</li>
</ol>
<p>The instinct is to settle the model question first because it feels like the important one. It's the one that expires soonest. Do the task work first — that's the part that compounds.</p>
<hr />
<p><em>I'm a Technical Architect building AI-enabled systems. I write about what I actually build — the decomposition and design decisions under the hood, not the model hype.</em></p>
<p><em>If you've re-decomposed a task around AI and found the model choice mattered less than you expected, I'd like to hear where the real leverage landed. Find me on X</em> <a href="https://x.com/deepakgoyal_ai"><em>@deepakgoyal_ai</em></a> <em>or</em> <a href="https://www.linkedin.com/in/deepakgoyal-ai/"><em>LinkedIn</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Route to the Cheapest Oracle That Can Decide]]></title><description><![CDATA[When you build on top of a language model, routing every decision to the model is the path of least resistance. One interface, one prompt, one place to add logic. It works beautifully in a demo.
Then ]]></description><link>https://blog.deepakgoyal.ai/route-to-the-cheapest-oracle</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/route-to-the-cheapest-oracle</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[cost-optimisation]]></category><category><![CDATA[Code Quality]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Wed, 01 Jul 2026 11:42:04 GMT</pubDate><content:encoded><![CDATA[<p>When you build on top of a language model, routing every decision to the model is the path of least resistance. One interface, one prompt, one place to add logic. It works beautifully in a demo.</p>
<p>Then it meets production scale, and you find you've been using the most expensive, slowest, least deterministic tool you own to answer questions that were already solved — for free — by something far simpler.</p>
<p>The fix isn't a better model. It's a routing decision made <em>before</em> any work runs: send each decision to the cheapest oracle that can actually decide it — where an "oracle" is just anything that can answer the question, from a regex or a lookup to a small model to the big one. Let me make that concrete with a system I built, then pull out the part that generalizes.</p>
<hr />
<h2>Why "LLM everything" is the default — and why it's wrong</h2>
<p>Defaulting every decision to a language model is the natural first move. At prototype scale it's the obvious one. Then production shows you three costs the demo never did:</p>
<ul>
<li><p><strong>Money.</strong> You pay per token for decisions a free deterministic function could make.</p>
</li>
<li><p><strong>Latency.</strong> A model call is hundreds of milliseconds to seconds; a structural check is sub-millisecond. Multiply by every decision, every time.</p>
</li>
<li><p><strong>Non-determinism.</strong> The same input can resolve one way now and another way later, because the oracle is probabilistic. For anything that gates a workflow, that's corrosive — people stop trusting a check that flip-flops.</p>
</li>
</ul>
<p>The design treats the expensive oracle as the <em>default</em>. It should be the <em>fallback</em>.</p>
<hr />
<h2>Most decisions have a cheaper oracle that can decide them</h2>
<p>Here's the observation that reframes the problem. A large share of the decisions you're tempted to route to an LLM don't require its judgment at all. Their truth condition is something a cheaper oracle can evaluate — a structural check, a lookup, a rule, a smaller model.</p>
<p>The minority that genuinely need the expensive oracle are the ones whose answer depends on <em>meaning</em> or <em>context</em> — the things language models are actually for. The mistake is letting that hard minority set the architecture for the easy majority.</p>
<p>So the design question is never "should we use an LLM here?" It's sharper: <strong>for which specific decisions is the LLM the <em>only</em> oracle that can decide?</strong> That's a per-decision question, and — crucially — it's answerable upfront, before anything runs.</p>
<hr />
<h2>A worked example: a code-standards enforcement system</h2>
<p>The clearest case I lived through was a rule-enforcement system — the kind that scans code against a set of standards and flags violations. The first version routed every rule to an LLM. It demoed well and fell apart the moment it ran on every commit.</p>
<p>Most of the rules I was paying a model to evaluate had a <em>structural</em> truth condition:</p>
<ul>
<li><p>"Does this module import a forbidden package?" — an import-graph lookup.</p>
</li>
<li><p>"Does every public function have a return type?" — an AST traversal.</p>
</li>
<li><p>"Does this file match the required naming pattern?" — a regex.</p>
</li>
</ul>
<p>None of those need a model to understand intent; they need a parser to inspect structure. The rules that <em>did</em> need a model were the semantic ones — "is this error message actually helpful?" — where the truth depends on meaning.</p>
<p>So I put a classification step in front of the dispatcher. Before a single check runs, each rule is graded once into two tiers:</p>
<table>
<thead>
<tr>
<th>Tier</th>
<th>Oracle</th>
<th>Properties</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Deterministic</strong></td>
<td>AST / regex / import-graph</td>
<td>sub-second · no per-token cost · reproducible · unit-testable</td>
</tr>
<tr>
<td><strong>Semantic</strong></td>
<td>LLM</td>
<td>slower · paid · needs intent or context</td>
</tr>
</tbody></table>
<p>The classification is cheap and one-time — a small, fast model is plenty to sort "structural" from "needs-judgment." You pay it once, not on every commit forever. And because that step runs once and gets a human sign-off, its own non-determinism never reaches production: the flaky part is bounded to design time, not runtime. The genuinely ambiguous rules — mostly structural but with a semantic edge — default <em>up</em> to the LLM tier; you'd rather over-power a check than let a deterministic shortcut quietly miss the hard case.</p>
<p>When I ran that split across one rule stream, the majority came back deterministic and only a handful were genuinely LLM-only. Routing accordingly cut the model spend on that stream by roughly 80% — the saving comes from pulling the <em>high-frequency</em> checks (the ones that run on every commit) off the model, not from dropping any coverage. The deterministic checks now run in milliseconds and never flip-flop, so the gate became something people trust.</p>
<hr />
<h2>The part that generalizes</h2>
<p>Strip the code-standards specifics and the pattern is domain-agnostic. Anywhere you're tempted to default to the model, the same routing layer applies:</p>
<ul>
<li><p><strong>Classification</strong> — a cheap keyword/rule pass first; the LLM only for the ambiguous remainder.</p>
</li>
<li><p><strong>Extraction</strong> — a regex or parser for the structured fields; the LLM only for the free text that resists one.</p>
</li>
<li><p><strong>Validation</strong> — schema and type checks before you ask a model whether something "reads right."</p>
</li>
<li><p><strong>Escalation</strong> — a small, cheap model first; promote to the expensive one only when confidence is low.</p>
</li>
</ul>
<p>In every case the move is the same: grade the decision once, route it to the cheapest oracle that can decide it, and reserve the expensive one for the decisions that genuinely earn it. A one-tier system that sends everything through the same oracle has structurally given up those savings before it starts.</p>
<hr />
<h2>What it costs you</h2>
<p>This isn't free. Two tiers means a classification step to maintain and two code paths to keep honest, where a one-tier "LLM everything" system has just one. That overhead is real, and it's why the one-tier version is the <em>right</em> first move: at prototype scale, when you're still discovering what the rules even are, a single model call per decision is the fastest way to learn.</p>
<p>The split earns its keep only when volume shows up — every commit, every item, every time — and the per-decision cost of the model starts compounding. Add the routing layer when the scale is real, not before. Reaching for it on day one is its own kind of over-engineering.</p>
<hr />
<h2>How to apply it</h2>
<p>If you're building on an LLM, run one audit:</p>
<ol>
<li><p><strong>List the actual decisions</strong> your system makes — not categories, the real decisions.</p>
</li>
<li><p><strong>For each, ask: what's the minimum oracle that can decide this?</strong> A rule? A regex? A parser? A lookup? A small model? Or genuine semantic judgment?</p>
</li>
<li><p><strong>Route accordingly.</strong> Cheap-tier decisions get a real function with a test. Only the judgment calls keep the model.</p>
</li>
<li><p><strong>Re-run the audit as you add features.</strong> New decisions default to the cheap tier and only graduate to the model when you can name the judgment they require.</p>
</li>
</ol>
<p>The instinct to make everything smart is the expensive one. The discipline is to make each decision exactly as smart as it needs to be — and no smarter.</p>
<hr />
<p><em>I'm a Technical Architect building AI-enabled systems. I write about what I actually build — the routing and cost decisions under the hood, not the hype.</em></p>
<p><em>If you've split an LLM pipeline into cheap and expensive tiers, I'd like to know where you drew the line. Find me on X</em> <a href="https://x.com/deepakgoyal_ai"><em>@deepakgoyal_ai</em></a> <em>or</em> <a href="https://www.linkedin.com/in/deepakgoyal-ai/"><em>LinkedIn</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[What a developer survey taught me after 6 weeks of AI code review]]></title><description><![CDATA[Six weeks into running AI-assisted architect code reviews, I did something I should have done earlier: I asked the team directly.
I'd been running a pipeline that scanned pull requests, generated arch]]></description><link>https://blog.deepakgoyal.ai/ai-code-review-developer-survey-6-weeks</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/ai-code-review-developer-survey-6-weeks</guid><category><![CDATA[AI Code Reviews]]></category><category><![CDATA[code review]]></category><category><![CDATA[developer-feedback]]></category><category><![CDATA[engineering leadership]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Fri, 26 Jun 2026 05:48:18 GMT</pubDate><content:encoded><![CDATA[<p>Six weeks into running AI-assisted architect code reviews, I did something I should have done earlier: I asked the team directly.</p>
<p>I'd been running a pipeline that scanned pull requests, generated architectural feedback, and posted comments under my review. The focus was architectural — system design, code structure, standards alignment — not syntax or linting, which automated tools already handle. My assumption going in was that developers would find the comments noisy — too many, too generic, useful only occasionally. AI-generated feedback has a reputation for noise: high volume, generic findings, low actionability on anything beyond linting.</p>
<p>I sent sixteen developers an anonymous survey asking about actionability (1–10), severity tag calibration, which comment categories they valued most, whether they understood the <em>why</em> behind findings, and what they'd change. I expected it to tell me to scale back.</p>
<p>It didn't.</p>
<p><em>Here's what the data said — and what I changed as a result.</em></p>
<hr />
<h2>What I expected to find</h2>
<p>Low actionability scores. Developers annoyed by AI-generated noise. A signal to reduce volume and tighten focus. That felt like a reasonable prior — and I'd been watching carefully for signs that the reviews were becoming wallpaper.</p>
<p>What I found instead: the pipeline was working better than I thought — with one specific exception I'd been blind to.</p>
<hr />
<h2>The headline numbers</h2>
<p>Average actionability: <strong>8.06 out of 10</strong>.</p>
<p>More surprising: <strong>94% of developers (15/16) said they understand the <em>why</em> behind comments</strong> — not just what was flagged, but the reasoning behind it. One respondent said "sometimes"; everyone else said "always" or "most of the time."</p>
<p>I'd spent significant effort on comment quality — making sure each finding had context, rationale, and a suggested direction. The data said that effort was landing. The reviews weren't noise.</p>
<p>So where was the gap?</p>
<hr />
<h2>The blind spot: severity tags</h2>
<p>When I looked at the calibration question, the numbers told a different story than the actionability scores.</p>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Well-calibrated</th>
<th>Not well-calibrated</th>
</tr>
</thead>
<tbody><tr>
<td>Blocker</td>
<td>11/16</td>
<td>5/16</td>
</tr>
<tr>
<td>Major</td>
<td>14/16</td>
<td>2/16</td>
</tr>
<tr>
<td>Suggestion</td>
<td>12/16</td>
<td>4/16</td>
</tr>
<tr>
<td><strong>Nit</strong></td>
<td><strong>7/16</strong></td>
<td><strong>9/16</strong></td>
</tr>
</tbody></table>
<p>Nit was the weakest tag by a wide margin. Only 7 of 16 developers thought it was consistently well-calibrated.</p>
<p>The open responses told me why. Two patterns kept surfacing:</p>
<p><strong>Pattern 1 — Nits with no verdict.</strong> Comments tagged <code>[nit]</code> that ended with "okay to ignore if time is tight." Developers picked up on this quickly: if it's truly okay to ignore, don't post it. The comment adds noise without value and erodes confidence in the tag for the comments where it actually matters.</p>
<p><strong>Pattern 2 — Blockers for preferences.</strong> One developer gave a precise, constructive example: a review had flagged an architectural approach as a hard blocker and recommended a different direction. Weeks later, a follow-up fix was needed that the original approach would have handled. The feedback: <em>"Some of the strongest comments would work better as 'preferred direction unless runtime proof says otherwise' rather than a hard stop."</em></p>
<p>That landed. A blocker is a correctness issue, a CI failure, a known production risk. An architectural preference — even a well-reasoned one — is not a blocker until there's evidence it's wrong at runtime. I'd been conflating the two.</p>
<hr />
<h2>The insight: severity tags are a contract, not decoration</h2>
<p>In any structured review process, the <strong>framing</strong> of a finding is a more sensitive credibility lever than its technical correctness.</p>
<p>A developer can tolerate a finding that turns out to be wrong. One wrong finding is one data point. But a mislabeled severity tag contaminates the vocabulary — if <code>[blocker]</code> sometimes means "I really prefer this," developers start discounting all blockers. If <code>[nit]</code> sometimes means "I ran out of things to tag," developers stop reading nits entirely.</p>
<p>Severity tags are a contract with the reader. Every <code>[blocker]</code> must mean the same thing. Every <code>[nit]</code> must be something worth changing, not something to pad the review with.</p>
<hr />
<h2>What developers actually value</h2>
<p>The calibration finding was the critical one — but the survey surfaced two more data points worth acting on.</p>
<p>The most-valued comment category was architecture — how the feature is structured and built. <strong>94% selected it</strong>.</p>
<p>Safety comments (security, PII, breaking changes) and code clarity (duplication, dead code) both landed around 63%. Tests and CI coverage scored 25%.</p>
<p>That last number was useful. Test commentary scores low not because tests don't matter but because automated tooling (linters, CI bots, static analysis) already covers that surface. Developers don't need another layer of feedback on something that's already caught. The architect review adds value where automated tools don't reach: system design, architectural trade-offs, standards alignment.</p>
<p>Knowing this changed how I allocate review depth. I pull back on test commentary and lean into architectural calls.</p>
<hr />
<h2>The dominant ask: more, not less</h2>
<p>The highest-frequency theme in open responses was review cadence. <strong>44% of respondents asked for more frequent reviews</strong>, not fewer. Comments ranged from "reducing the turnaround between cycles" to "can we run this several times a day?"</p>
<p>I'd expected the dominant ask to be "less." The actual ask was the opposite.</p>
<p>This matters for how you think about AI-assisted review pipelines. The risk isn't developer fatigue from too many comments — it's developer frustration from feedback arriving too late in the PR cycle to act on without rework. A comment posted 18 hours after a PR opens often arrives after the developer has moved on, which means the cost of incorporating it has gone up.</p>
<hr />
<h2>What I changed</h2>
<p>Four concrete changes, directly from the survey:</p>
<p><strong>1. Tightened the nit gate.</strong> If the verdict is "okay to ignore," the comment doesn't post. A <code>[nit]</code> must be worth changing to be worth sending.</p>
<p><strong>2. Calibrated the blocker tag.</strong> Architectural preferences now read as "preferred direction unless runtime proof says otherwise." <code>[Blocker]</code> is reserved for correctness issues, known production risk, or CI failure.</p>
<p><strong>3. Scaled back test/CI commentary.</strong> Where automated tooling (linters, CI bots, static analysis) already covers the surface, the architect review steps back. The value is in what bots can't catch.</p>
<p><strong>4. Added a mid-day review slot.</strong> The 44% frequency signal was the loudest ask in the survey. The pipeline now runs at multiple points during the working day rather than once, reducing the gap between a PR opening and the first round of feedback.</p>
<p>None of these are dramatic changes. Each one came directly from a developer telling me, with data behind it, where the signal was breaking down.</p>
<hr />
<h2>The operational test</h2>
<p>If you run structured reviews — AI-assisted or otherwise — read the last ten comments you posted and ask: do the severity tags mean what the reader thinks they mean?</p>
<p>If <code>[blocker]</code> has appeared on an architectural preference, you've inflated it. If <code>[nit]</code> has appeared on something you'd genuinely accept unchanged, you've diluted it. The cost isn't measured per-comment. It compounds across the corpus.</p>
<p>Survey your team. Ask specifically about tag calibration, not just overall satisfaction. The headline scores will probably be better than you expect. The calibration scores will tell you where the actual work is.</p>
<hr />
<p><em>I run AI-assisted code reviews as part of a Technical Architect role on an engineering team spanning backend, frontend, mobile, and QA. The pipeline generates architectural findings; I review and post them. After six weeks, I ran a 16-response anonymous survey to validate assumptions about how the practice was landing. The data changed how I operate.</em></p>
<p><em>If you've surveyed your team on AI-assisted reviews — what surprised you most?</em></p>
]]></content:encoded></item><item><title><![CDATA[Your LLM Reviewer Agrees With Itself. That's the Bug.]]></title><description><![CDATA[When one LLM generates content and another from the same family reviews it, they can agree completely — and both be wrong in the same direction.
That's not a capability failure. It's family bias: a st]]></description><link>https://blog.deepakgoyal.ai/llm-reviewer-agrees-with-itself-thats-the-bug</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/llm-reviewer-agrees-with-itself-thats-the-bug</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[LLM-as-Judge]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Tue, 16 Jun 2026 12:07:48 GMT</pubDate><content:encoded><![CDATA[<p>When one LLM generates content and another from the same family reviews it, they can agree completely — and both be wrong in the same direction.</p>
<p>That's not a capability failure. It's <strong>family bias</strong>: a structurally correlated blind spot that no amount of prompting fixes, because the judge and producer share training patterns — they find each other's outputs "familiar" in a way that operates below the prompt layer. The 2025 research on this is now strong enough to act on.</p>
<p>I built a two-judge review loop — one Anthropic model, one OpenAI model — with a convergence rule that defines what "agreement" actually means. After ~75 review items and dozens of iteration loops, the pattern held: <strong>the second judge caught a class of errors the first couldn't, not because it was smarter, but because it wasn't correlated with the producer.</strong></p>
<p>There's also an <a href="https://github.com/deepakgoyal-ai/dual-llm-validator">open-source tool</a> so you can run it against your own content.</p>
<hr />
<h2>Why single-judge fails quietly</h2>
<p>Frontier models are competent judges. That's precisely why this failure mode is dangerous — the verdicts look authoritative.</p>
<p>Spiliopoulou et al.'s <a href="https://arxiv.org/abs/2508.06709">"Play Favorites" (Aug 2025)</a> showed that GPT-4o and Claude 3.5 Sonnet don't just over-rate their own outputs — they over-rate outputs from their entire model lineage. Wataoka et al. <a href="https://arxiv.org/abs/2410.21819">(arXiv 2410.21819, rev. Jun 2025)</a> traced the mechanism to pattern familiarity: judges find text from familiar architectures easier to process and rate it higher. <a href="https://www.adaline.ai/blog/llm-as-a-judge-reliability-bias">Adaline's April 2026 analysis</a> found frontier judges failing more than 50% of bias stress tests, with the practical takeaway: <em>"Separate your judge's model family from your generator's."</em></p>
<p>The solution isn't a smarter judge. It's a judge that can't share the producer's blind spots.</p>
<hr />
<h2>The convergence rule</h2>
<p>I use two independent judges per item:</p>
<ul>
<li><p><strong>Judge A</strong>: Anthropic (Claude Sonnet by default)</p>
</li>
<li><p><strong>Judge B</strong>: OpenAI (GPT-4o by default)</p>
</li>
</ul>
<p>Each sees the same content and criteria. Neither sees the other's output. Each returns:</p>
<ul>
<li><p><code>confidence</code> — certainty in the verdict, 0–100</p>
</li>
<li><p><code>verdict</code> — <code>APPROVED</code>, <code>FLAGGED</code>, or <code>UNCERTAIN</code></p>
</li>
<li><p><code>rationale</code> — one sentence</p>
</li>
</ul>
<p>A decision is <strong>trusted</strong> when two conditions both hold:</p>
<table>
<thead>
<tr>
<th>Condition</th>
<th>Threshold</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Avg confidence</td>
<td>≥ 75</td>
<td>Both judges highly certain</td>
</tr>
<tr>
<td>Spread</td>
<td>≤ 15</td>
<td>Judges agree with each other</td>
</tr>
</tbody></table>
<p>Both are required. Average alone misses divergence; spread alone misses low-confidence agreement.</p>
<p><strong>Examples from actual runs:</strong></p>
<table>
<thead>
<tr>
<th>Judge A conf.</th>
<th>Judge B conf.</th>
<th>Avg</th>
<th>Spread</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>95</td>
<td>90</td>
<td>92.5</td>
<td>5</td>
<td>✅ Converged</td>
</tr>
<tr>
<td>85</td>
<td>48</td>
<td>66.5</td>
<td>37</td>
<td>❌ Verdicts disagree (one UNCERTAIN)</td>
</tr>
<tr>
<td>74</td>
<td>71</td>
<td>72.5</td>
<td>3</td>
<td>❌ Avg below floor (72.5 &lt; 75)</td>
</tr>
<tr>
<td>90</td>
<td>70</td>
<td>80.0</td>
<td>20</td>
<td>❌ Spread too high (20 &gt; 15)</td>
</tr>
</tbody></table>
<p><em>My calibration: avg ≥ 75, spread ≤ 15, max 4 iterations. Tune these against your corpus — the shape transfers, the numbers don't.</em></p>
<p>When a verdict doesn't meet both conditions, I don't discard the item — I escalate.</p>
<hr />
<h2>The escalation ladder</h2>
<p>The escalation trigger is specific: <strong>if improvement between iterations is ≤ 5 confidence points</strong>, more of the same model won't close the gap.</p>
<table>
<thead>
<tr>
<th>Level</th>
<th>Judge A</th>
<th>Judge B</th>
</tr>
</thead>
<tbody><tr>
<td>L1</td>
<td>claude-sonnet-4-6</td>
<td>gpt-4o</td>
</tr>
<tr>
<td>L2</td>
<td>claude-opus-4-7</td>
<td>gpt-4o</td>
</tr>
<tr>
<td>L3</td>
<td>claude-opus-4-7</td>
<td>gpt-4-turbo</td>
</tr>
<tr>
<td>L4</td>
<td>claude-opus-4-7</td>
<td>gpt-5o</td>
</tr>
</tbody></table>
<p>Two things I learned running this:</p>
<p><strong>I learned not to escalate both judges when one was diverging.</strong> If Judge A was consistent across iterations and Judge B was all over the place, escalating both burned budget without isolating the signal. The ladder reflects this — it steps up Judge A (Anthropic) first, since in my experience Opus is more reliable than Sonnet on genuinely ambiguous items, while Judge B stays on gpt-4o through L3.</p>
<p><strong>I capped at 4 iterations.</strong> After 4 loops without convergence, the item is genuinely ambiguous. The right response is a human decision, not a fifth model call.</p>
<hr />
<h2>See it running</h2>
<pre><code class="language-bash">uv run python3 validate.py \
  --content "Cache relies on TTL alone, no explicit eviction path." \
  --criteria "Flag any caching pattern without an eviction or fallback path."
</code></pre>
<pre><code class="language-text">Dual-LLM Validator
  Judge A : claude-sonnet-4-6
  Judge B : gpt-4o
  Rule    : avg &gt;= 75  AND  spread &lt;= 15  |  max 4 iters

Iteration 1 / 4
  Judge A  claude-sonnet-4-6   confidence  95  FLAGGED
  Judge B  gpt-4o              confidence  95  FLAGGED

  avg 95.0  |  spread 0  |  CONVERGED ✅

Result     : FLAGGED
Iterations : 1

Rationales:
  Judge A: TTL-only caching creates silent stale reads
           when upstream data changes without eviction.
  Judge B: No eviction path means stale entries persist
           until TTL expires, regardless of data changes.
</code></pre>
<p>When it doesn't converge on the first pass:</p>
<pre><code class="language-text">Iteration 1 / 4
  Judge A  claude-sonnet-4-6   confidence  85  FLAGGED
  Judge B  gpt-4o              confidence  48  UNCERTAIN

  avg 66.5  |  spread 37  |  NOT CONVERGED (verdicts disagree)

  &gt;&gt; Trust delta stalled — escalating to L2

Iteration 2 / 4  [L2]
  Judge A  claude-opus-4-7     confidence  80  FLAGGED
  Judge B  gpt-4o              confidence  76  FLAGGED

  avg 78.0  |  spread 4  |  CONVERGED ✅
</code></pre>
<p><em>L2 escalates Judge A to Opus (stronger on ambiguous items); Judge B stays on gpt-4o — the divergence was on the Anthropic side.</em></p>
<hr />
<h2>Try it yourself</h2>
<pre><code class="language-bash">git clone https://github.com/deepakgoyal-ai/dual-llm-validator
cd dual-llm-validator
uv sync
cp .env.example .env   # then open .env and fill in your keys

uv run python3 validate.py \
  --content "your content here" \
  --criteria "your review criteria here"
</code></pre>
<p>Custom models and thresholds:</p>
<pre><code class="language-bash">uv run python3 validate.py \
  --content-file review.txt \
  --criteria-file criteria.txt \
  --model-a claude-opus-4-7 \
  --model-b gpt-4-turbo \
  --avg-floor 80 \
  --spread-max 10
</code></pre>
<p>Exit code <code>0</code> = converged. Exit code <code>2</code> = human review required. Pipe-friendly.</p>
<hr />
<h2>The operational discipline that matters most</h2>
<p>Two things that aren't about the models at all:</p>
<p><strong>I re-validate after every fix.</strong> First dispatch + fix applied ≠ done. A verdict is closed only when a re-validation pass converges on the corrected content. Skipping re-validation means I've applied a fix that passed one judge once — which is the original failure mode.</p>
<p><strong>I managed two token budgets, not one.</strong> Running two providers concurrently means two rate limits refilling on different schedules. I alternated between providers across dispatch batches rather than parallelizing everything. Parallel looks faster until one provider hits a rate limit 40% through and stalls everything.</p>
<hr />
<h2>What I'm still unsure about</h2>
<p><strong>Do these thresholds transfer?</strong> I calibrated avg ≥ 75 and spread ≤ 15 against one corpus. There's no reason to expect them to be correct for a different review task. Calibrate against your own items before trusting the defaults.</p>
<p><strong>Did I trade family bias for a different correlated bias?</strong> Cross-family reduces family bias — the 2025 papers document this. It doesn't eliminate correlated failure. Two models trained heavily on the same internet text could still share blind spots I haven't characterized.</p>
<p><strong>When shouldn't you do this?</strong> If the cost of a wrong verdict is low, single-judge is cheaper and probably fine. If you have labelled data and bandwidth, a fine-tuned single judge likely outperforms two general-purpose ones. This pattern is the <strong>0-to-1 case</strong> — no training data, real stakes, need something better than one model family's opinion.</p>
<hr />
<p>The short version: two judges, different families, convergence = avg ≥ floor AND spread ≤ max, cap at 4 iterations, accept ambiguity rather than escalating forever.</p>
<p>The rest is calibration.</p>
<hr />
<p><em>I'm a Technical Architect building AI-enabled systems. Writing about what I actually build — not what I think I should build.</em></p>
<p><em>If you're running a review pipeline and have calibrated your own thresholds, I'd like to know what you landed on.</em></p>
]]></content:encoded></item><item><title><![CDATA[Why I rebuilt my AI second brain — three problems that surfaced after five weeks]]></title><description><![CDATA[Standards matter more when AI is your editor. v2.0 is the rebuild that proved it.

1. The cracks: five weeks in
Five weeks into using the AI second brain I'd built, three things were nagging at me.
Th]]></description><link>https://blog.deepakgoyal.ai/rebuild-ai-second-brain-architecture-structure-standards</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/rebuild-ai-second-brain-architecture-structure-standards</guid><category><![CDATA[obsidian]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[secondbrain]]></category><category><![CDATA[pkm]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Tue, 02 Jun 2026 06:12:00 GMT</pubDate><content:encoded><![CDATA[<p><em>Standards matter more when AI is your editor. v2.0 is the rebuild that proved it.</em></p>
<hr />
<h2>1. The cracks: five weeks in</h2>
<p>Five weeks into using the AI second brain I'd built, three things were nagging at me.</p>
<p>The skills were getting fat. The project list was lying to me. Every area had its own dialect.</p>
<p>None of them were bugs. All of them were structural.</p>
<p><em>Quick context if you missed the original post:</em> <a href="https://blog.deepakgoyal.ai/ai-powered-second-brain-obsidian-claude-code">SMRITI v1.0</a> is an open-source second brain that pairs Obsidian with Claude Code. 13 markdown skill files give Claude Code daily-companion behaviors — morning brief, capture, weekly review, knowledge extraction. It worked. The Galaxy crossed 100 atomic notes. The morning brief landed on time.</p>
<p>By week four, those cracks were impossible to ignore. v2.0 is the rebuild that fixes all three. Below is what hurt, what I changed, and the cross-cutting lesson I didn't see until the rebuild was done.</p>
<hr />
<h2>2. One command, doing everything</h2>
<h3>The pain</h3>
<p>One evening I was debugging why <code>/close-my-day</code> had skipped a step. I opened the skill file. Scrolled. Scrolled more. Skill file: 127 lines.</p>
<p><code>/close-my-day</code> was doing everything. Pulling Todoist completions. Reconciling daily notes. Running the weekly review on Sundays. Extracting Galaxy candidates. Drafting tomorrow's quests. Mini-triaging captures.</p>
<p>Each was a block of inline logic. Token cost climbed every time I invoked it. Changing one phase meant reading all the others.</p>
<p>13 skills, most of the big ones bloating the same way. The biggest one ran past 200 lines.</p>
<h3>The fix: skill = touchpoint, agent = depth</h3>
<p>The skill stays thin. It's what you type. The depth moves into specialized agents that the skill dispatches.</p>
<pre><code>/close-my-day  (79 lines — was 127)
  │
  ├─→  Task Sync agent           (pull Todoist completions)
  ├─→  Auto-Link agent           (Friday cascade)
  ├─→  Weekly Review agent       (Sunday cascade)
  └─→  Galaxy Extractor agent    (pull atomic candidates)
</code></pre>
<p><em>Mini-triage and tomorrow's quests draft stay inline in the skill — they're light enough.</em></p>
<ul>
<li><strong>Skills (6).</strong> <code>/start-my-day</code>, <code>/close-my-day</code>, <code>/checkpoint</code>, <code>/braindump</code>, <code>/triage</code>, <code>/consolidate</code>. Each is a thin coordinator — read state, dispatch, present results, write back.</li>
<li><strong>Agents (8).</strong> Specialized sub-capabilities the skills dispatch. The clearest example is the Task Sync agent: it pulls completed Todoist tasks, finds the matching tids in vault Project files, ticks the checkboxes, and writes <code>Completed::</code> fields. 270 lines of focused logic. Yes, that's longer than v1.0's biggest skill — that's the point. Depth lives in agents now. The skill stays thin; the agent takes the weight.</li>
</ul>
<h3>Why it matters</h3>
<p><strong>Each agent picks its own model.</strong> Galaxy Extractor runs on Opus because synthesis quality matters. Inbox Triage runs on Sonnet because it's template-driven. Task Sync runs on Sonnet because it's transactional. A monolithic skill can't optimize per-phase. A dispatcher can.</p>
<p><strong>Agents run in parallel.</strong> Friday close fires Auto-Link and Librarian in one message. Wall-clock drops to the slowest agent, not the sum.</p>
<p><strong>Skills shrank.</strong> <code>/close-my-day</code> went from 127 → 79 lines (38%). <code>/start-my-day</code> went from 148 → 107 (28%). The hard logic still exists — it just lives in agent files where it belongs.</p>
<p>Net: 13 skills → 6 skills + 8 agents. Fewer commands. More capability.</p>
<hr />
<h2>3. The list that always lied</h2>
<h3>The pain</h3>
<p>Every Friday I'd open the area's <code>Initiatives.md</code> to plan the next sprint. Table of projects — status, priority, effort, owner. Looks great in screenshots.</p>
<p>It was always wrong. I'd close a project. Forget to update the table. Next Friday, three rows said "active" for projects I'd shipped two weeks ago.</p>
<p>The Project files had the truth. The companion file had a snapshot nobody re-snapped.</p>
<p>And every project lived in one flat <code>01 - PROJECTS/</code> folder. Work next to learning next to personal. The folder lost its signal.</p>
<h3>The fix</h3>
<p>Two changes, one outcome.</p>
<p><strong>Kill the companion file.</strong> The Initiative Index is now a live Dataview query (Dataview is an Obsidian plugin that reads frontmatter from notes and renders queryable tables). It reads directly from the Project files themselves. No more hand-maintained tables.</p>
<p>Add this to the area's Playbook under <code>## Initiative Index</code>:</p>
<pre><code class="language-dataview">TABLE status, priority, effort, planned, completed
FROM "01 - PROJECTS/Work"
WHERE status != "cancelled"
SORT priority ASC, planned ASC
</code></pre>
<p>The Playbook shows the truth because the truth is the Project files.</p>
<p><strong>Move Projects under per-area folders.</strong> Instead of a flat folder, Projects live under per-area subfolders:</p>
<pre><code>01 - PROJECTS/
├── Work/
│   ├── 001-q3-platform-migration.md
│   └── 002-pr-policy.md
├── Learning/
│   └── 014-fastapi-deep-dive.md
└── Personal/
    └── 003-q3-personal-checkpoint.md
</code></pre>
<p>Project IDs are area-scoped — <code>work-001</code>, <code>learning-014</code>. Never reused. When you cancel a project, the file stays in place with <code>status: cancelled</code>. The ID stays bound to the file. No tombstoning, no ambiguity, no "which work-014 are we talking about?"</p>
<h3>The locked schema</h3>
<p>Each Project file carries 18 frontmatter fields — status, priority, effort, planned/completed dates, recurrence pattern, Todoist linkage, tags. Each field earned its place by being load-bearing for dispatch, queries, or stability.</p>
<p>The point isn't the field count. The point is that the schema forces decisions to be visible.</p>
<ul>
<li><code>status</code> is one of 8 values. No freeform. You can't fudge a project as "kinda done."</li>
<li><code>effort</code> is one of 7 values. You can't claim a 30-minute task is "ish, maybe."</li>
<li><code>recurring: true</code> + <code>iteration: N</code> lets one file hold 50 iterations of weekly work instead of generating 50 files.</li>
</ul>
<p>Feels restrictive for a week. Then it's muscle memory.</p>
<hr />
<h2>4. Five areas, five dialects</h2>
<h3>The pain</h3>
<p>I move between areas every day. Work in the morning. Learning at lunch. Personal in the evening.</p>
<p>Each area's <code>CLAUDE.md</code> had drifted into its own vocabulary.</p>
<p>Work documented its weekly rhythm as "Operating Loop." Learning called it "Daily Cadence." Personal didn't document it at all.</p>
<p>When I wanted to know the rhythm, I had to translate every time. So did the AI. Every skill reading an area's CLAUDE.md had to handle three conventions.</p>
<h3>The fix: every area gets the same Playbook</h3>
<p>Six canonical sections, always the same, always in the same order. Here's a real Learning-area Playbook filled in:</p>
<pre><code class="language-markdown">## Mission
Learning is where I spend protected hours on AI/ML mastery — concept
clinics, hands-on experiments, and Galaxy notes that compound into
long-term competence. This is the area I won't let work crowd out.

## Operating Rhythm
### Daily
- 12:00–14:00 protected study block (Slack DND, no calls)
- 21:00 close ritual + Galaxy candidates pass

### Weekly
- Sunday: next topic plan + audit current topic for AI-moves-fast drift
- Galaxy target: 2 atomic notes / week

## Initiative Index
(Dataview query renders open + closed Projects from
 01 - PROJECTS/Learning/)

## Library
- Phase plan, topic prerequisites, Galaxy MOC entries
- Reference papers, cohort notes, external course bookmarks

## Signal Log
- Topic-level surprises ("retrievers + rerankers asymmetric" — 2026-05-20)
- Blog post candidates surfaced mid-study
- Tools to investigate (LangSmith, Phoenix evaluation)

## Cross-cuts
- Inputs: research papers (RSS / Readwise) → Inbox → triage → Learning
- Outputs: Galaxy notes → blog posts; Decisions → Work architecture
</code></pre>
<p>When every area has the same shape, three things change.</p>
<p>You move between areas without translation. "What's open in Learning?" → Initiative Index. "What's the rhythm?" → Operating Rhythm. No mental tax.</p>
<p>AI doesn't relearn. Every skill knows where the Initiative Index lives. The Auto-Link agent knows Library entries are link candidates. The Inbox Triage agent knows every area has a Signal Log where a half-baked idea can sit until it ripens.</p>
<p>New areas are cheap. <code>setup.sh</code> ships an interactive Area loop — type a name, it copies the Example-Area template, substitutes your name across the Playbook + <code>CLAUDE.md</code>, creates the matching Projects folder. ~30 seconds per area.</p>
<p>The trade-off: some loss of structural freedom. I wanted a "Reading Queue" section in my Learning area's Playbook — it ended up inside Library. Library is for stable reference material; a Reading Queue is more of a status surface. The fit isn't perfect, but it's good enough. That's the price of conformity. I'll take it.</p>
<hr />
<h2>5. Why Standards matter more when AI is your editor</h2>
<p>The cross-cutting lesson I didn't see until v2.0 was done.</p>
<p>v1.0 had no Standards layer. Every skill was its own interpretation of how the vault should work. Every area's <code>CLAUDE.md</code> was a dialect. Drift was automatic — and worse, drift was <em>invisible</em> because everything still worked.</p>
<p>And here's why this matters when AI is the primary editor.</p>
<p>Every skill is an instruction file. When you author skill N, you're consciously matching skill N-1's conventions. When you author skill N+3 three weeks later, you've forgotten which conventions were deliberate and which were accidental. Two incompatible versions of the same idea both work. Both ship. Drift accumulates.</p>
<p>On a team, PR review catches the drift. Solo with AI, there's no PR review.</p>
<p><strong>The Standard is the enforcement layer.</strong> It's the thing that lets you author skill N+3 and have it land coherent with skills 1 through N+2 because there's a written reference for "how we do it here."</p>
<p>v2.0 ships 8 canonical Standards under <code>02 - AREAS/SADHANA/Standards/</code>. The four highest-leverage ones:</p>
<ul>
<li><strong>Project Template</strong> — Epic / Task / Sub Task hierarchy, the 18-field schema, status vocabulary.</li>
<li><strong>Initiative Index Standard</strong> — the Dataview query shape, status grouping, column order. This is what makes Section 3's queryable project structure possible.</li>
<li><strong>Frontmatter Standards</strong> — the 5-field minimum on every note. Got muscle-memory in three days.</li>
<li><strong>Skill Authoring Standard</strong> — skill size limit (110 lines hard cap), frontmatter, the pre-add gate (more on this below).</li>
</ul>
<p>The other four cover tags, themes (optional), permissions, and model routing.</p>
<h3>The pre-add gate</h3>
<p>The single most useful artifact across all 8 Standards is a three-question gate in the Skill Authoring Standard. Before adding skill N+1:</p>
<ol>
<li>Can an existing skill extend to cover this?</li>
<li>Can a hook, cron, or agent replace the need for a user-invocable command?</li>
<li>If neither — retire one skill, or justify with a concrete re-eval trigger.</li>
</ol>
<p>v1.0 hit 13 skills because there was no gate. v2.0 sits at 6 because the gate fires on every new idea. The cap counts user-invocable skills only — agents and standards don't count, so the system grows in depth without sprawling at the surface.</p>
<hr />
<h2>6. What I didn't see coming</h2>
<p><strong>Writing a new skill feels easier now.</strong> The hard work happens in the agent. The skill is just a switchboard. I expected the split to add cognitive overhead. It removed it.</p>
<p><strong>Standards stuck faster than I expected.</strong> Once Frontmatter Standards locked the 5-field minimum, every note from that day forward had those 5 fields without me thinking. About three days of deliberate practice, then muscle memory.</p>
<p><strong>L1 cache earned its keep.</strong> L1 is a pre-assembled context file at <code>.claude/cache/L1-static.md</code> — tags, frontmatter rules, sync rules, area context, all in one read. Replaces 5-10 source-file reads per skill invocation. I haven't measured token counts precisely (the prompt size varies too much for one number), but the bigger win was perceived latency. Skills feel snappier.</p>
<p><strong>Standards made new agents cheap to write.</strong> Inbox Triage was the last agent I added during the rebuild. I opened the Skill Authoring Standard, copied the structure, filled in the depth — done in an evening. Without the Standard, this would have been three evenings of "what's the right pattern here?" The retrofit pain I paid in v2.0 buys me speed in everything that comes after.</p>
<hr />
<h2>7. What I'd do differently</h2>
<p><strong>Start with Standards, not skills.</strong> v1.0 → v2.0 was a forced migration. If I'd written even a minimal Standards layer first and authored skills against it, the rebuild wouldn't have been necessary.</p>
<p><strong>The recursive Standard problem.</strong> How do you write a Skill Authoring Standard with no skills to test it against? I dodged this in v1.0 — wrote zero standards, shipped 13 skills, then tried to retrofit Standards on top. Half the rules in v2.0's Skill Authoring Standard are reverse-engineered from v1.0 mistakes. The right move is iterative: write a minimal Standard, ship one or two skills against it, refine the Standard, ship more.</p>
<p><strong>Documentation outpaced the code.</strong> I assumed 8 Standards docs would take a weekend. They took five evenings. The Standards layer ended up roughly 3× the line count of the skills it governs (~1,500 lines vs ~460). If you're building one of these, budget the writing time as its own phase.</p>
<p><strong>Agents are easier to retire than skills.</strong> I planned 10 agents. Shipped 8. Two got cut during integration testing — not dispatched by any shipping skill, depended on infra I hadn't built. Cutting an agent meant deleting one file. Cutting a v1.0-style skill would have meant breaking a user-facing command and forcing every user (me included) to relearn the flow. Agents are cheap to add and cheap to remove. Skills aren't.</p>
<hr />
<h2>8. What v2.0 doesn't ship</h2>
<p>I want to be transparent about what v1.0 features got cut or moved:</p>
<ul>
<li><strong>Inline capture</strong> (<code>/capture</code> in v1.0) — folded into <code>/braindump</code>. One capture surface, less to remember.</li>
<li><strong>Standalone connect skill</strong> (<code>/connect</code>) — became the Auto-Link agent, dispatched automatically from Friday and Sunday <code>/close-my-day</code>. Removes the manual command.</li>
<li><strong>Standalone weekly review</strong> (<code>/weekly-review</code>) — became the Weekly Review agent, auto-dispatched from Sunday <code>/close-my-day</code>.</li>
<li><strong>Standalone extract / promote skills</strong> — became the Galaxy Extractor and Knowledge Promoter agents, dispatched by <code>/consolidate</code> and the weekly cascade.</li>
</ul>
<p>If you used any of these as standalone commands daily in v1.0, the muscle memory shifts in v2.0 — the work still happens, but it's auto-dispatched rather than typed.</p>
<hr />
<h2>9. Try it</h2>
<p>v2.0 is open source. Three steps:</p>
<pre><code class="language-bash">git clone https://github.com/deepakgoyal-ai/smriti.git
cd smriti
./setup.sh
</code></pre>
<p><code>setup.sh</code> is interactive — 8 steps. Name, day-start, day-end, optional Todoist Sprint Board IDs, then the Area loop where you scaffold your first real Areas. ~5 minutes start to finish on a fresh clone.</p>
<p><strong>Migrating from v1.0?</strong> <code>SETUP.md</code> has a dedicated section. Breaking changes are real — vault layout shifted, 7 skills converted to agents, Standards became canonical. My own vault migration took about 30 minutes (file moves + frontmatter additions, no MCP config changes).</p>
<p><strong>What you need:</strong></p>
<ul>
<li><a href="https://obsidian.md/">Obsidian</a> (free)</li>
<li><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> (Pro plan recommended)</li>
<li>A clean clone or a v1.0 vault to migrate</li>
<li>A daily journaling habit — if you don't already journal daily, this won't compound</li>
</ul>
<p><strong>Daily time commitment:</strong> ~5 minutes morning (<code>/start-my-day</code>), captures during the day (zero overhead — just type), and 5-10 minutes evening (<code>/close-my-day</code>). The Sunday close is the longest at ~15 minutes because it runs the weekly review.</p>
<p><strong>What ships:</strong> 6 skills, 8 agents, 8 Standards, 1 L1 cache template, opt-in Todoist sync, an Example-Area scaffold, a sample Project, a couple of sample Galaxy notes.</p>
<p>If v1.0 still works for you, stay. v2.0 is what I'd ship if starting from scratch today.</p>
<p>Notes, skills, agents, standards — all markdown. Your knowledge is yours.</p>
<hr />
<p><em>Built by <a href="https://blog.deepakgoyal.ai">Deepak Goyal</a>. Follow the journey: <a href="https://github.com/deepakgoyal-ai">GitHub</a> · <a href="https://linkedin.com/in/deepakgoyal-ai">LinkedIn</a></em></p>
]]></content:encoded></item><item><title><![CDATA[How I Built an AI-Powered Second Brain with Obsidian + Claude Code]]></title><description><![CDATA[13 skills, no plugins, one CLAUDE.md file.

1. The Problem
My notes were in Obsidian. My tasks were in Todoist. My AI remembered neither.
I'm a Technical Architect. Decisions, meeting notes, architect]]></description><link>https://blog.deepakgoyal.ai/ai-powered-second-brain-obsidian-claude-code</link><guid isPermaLink="true">https://blog.deepakgoyal.ai/ai-powered-second-brain-obsidian-claude-code</guid><category><![CDATA[obsidian]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[secondbrain]]></category><category><![CDATA[AI Knowledge Management]]></category><category><![CDATA[personal knowledge management]]></category><category><![CDATA[llm-wiki]]></category><category><![CDATA[markdown]]></category><category><![CDATA[claude ai]]></category><category><![CDATA[obsidian-ai]]></category><category><![CDATA[pkm]]></category><category><![CDATA[ai productivity tools]]></category><dc:creator><![CDATA[Deepak Goyal]]></dc:creator><pubDate>Sat, 25 Apr 2026 10:28:21 GMT</pubDate><content:encoded><![CDATA[<p><em>13 skills, no plugins, one CLAUDE.md file.</em></p>
<hr />
<h2>1. The Problem</h2>
<p>My notes were in Obsidian. My tasks were in Todoist. My AI remembered neither.</p>
<p>I'm a Technical Architect. Decisions, meeting notes, architecture reviews, a long-term learning goal — all producing knowledge daily. I had a decent PKM setup. But the AI layer was disconnected. Every Claude conversation started from zero. Every session, I re-explained my vault structure, my workflows, my preferences.</p>
<p><strong>The real problem isn't note-taking. It's that your AI forgets everything the moment you close the terminal.</strong></p>
<p>I wanted an AI that knows my vault, runs workflows when I say <code>/start-my-day</code>, tracks what's overdue, and builds a knowledge graph alongside me. Not a chatbot. A companion that compounds.</p>
<hr />
<h2>2. The Idea and the Four Pillars</h2>
<p>I didn't invent this from scratch. The spark came from <a href="https://x.com/karpathy/status/2039805659525644595">Karpathy's LLM Wiki</a> — using LLMs to build and maintain personal knowledge bases in plain markdown. Not RAG. Not vector databases. Just an AI that reads your files and writes back to them.</p>
<p>The community ran with it. People built <a href="https://github.com/eugeniughelbur/obsidian-second-brain">Obsidian + Claude Code implementations</a>, wiki compilers, auto-linkers. What I saw was a deeper pattern: <strong>separate storage from intelligence.</strong></p>
<ul>
<li><p><strong>Obsidian</strong> = storage. Plain markdown. No vendor lock-in. You own it forever.</p>
</li>
<li><p><strong>Claude Code</strong> = intelligence. Reads files, runs skills, creates notes, remembers context.</p>
</li>
<li><p><strong>MCP</strong> = the bridge. Claude Code reads and writes to the vault directly.</p>
</li>
</ul>
<p>What's original in my approach: the skill system (13 markdown instruction files, not code), the four-pillar philosophy, the Architect's Codex vocabulary, and the daily orchestration flow.</p>
<p>The four pillars — all Sanskrit names — each own one responsibility:</p>
<p><strong>SMRITI</strong> (Memory) — The vault. Plain markdown, owned forever. Atomic knowledge that compounds.</p>
<p><strong>SARATHI</strong> (Charioteer) — Claude Code as the AI companion. Like Krishna guiding Arjuna's chariot — it sees the battlefield and executes the strategy. 13 skills are its capabilities.</p>
<p><strong>SADHANA</strong> (Practice) — The operating principles. Partnership model, autonomy levels, daily rhythm. The constitution that governs how the system operates.</p>
<p><strong>SIDDHI</strong> (Achievement) — Long-term goal pursuit. Structured weekly plans, session notes, progress tracking. Any ambitious multi-week goal.</p>
<p>They reinforce each other: SMRITI stores what you learn. SARATHI captures and connects it. SADHANA keeps it consistent. SIDDHI gives it direction.</p>
<p>If Claude Code disappears tomorrow, I keep every note.</p>
<hr />
<h2>3. The Architecture</h2>
<pre><code class="language-plaintext">YOU (Ideas, Decisions, Judgment)
  |
  v
CLAUDE CODE (Reads vault → Runs skills → Writes back)
  |
  +---&gt; Obsidian MCP (vault read/write)
  +---&gt; Todoist MCP (task sync, optional)
  |
  v
OBSIDIAN VAULT (Plain markdown — you own this forever)
</code></pre>
<p>The vault follows IPARAG (Inbox, Projects, Areas, Resources, Archives, Galaxy) — six folders that route every piece of information:</p>
<pre><code class="language-plaintext">00 - INBOX/          Capture everything here first
01 - PROJECTS/       Active work with end dates
02 - AREAS/          Ongoing responsibilities (no end date)
03 - RESOURCES/      Reference material
04 - ARCHIVES/       Done or inactive
05 - GALAXY/         Permanent atomic knowledge
</code></pre>
<p><strong>The Galaxy</strong> is the crown jewel. Every note is one atomic idea — a claim stated as a title, written in 3-5 sentences, linked to related ideas. A vault with 1000 unlinked notes is a graveyard. A vault with 100 well-connected Galaxy notes is a thinking tool.</p>
<p>But the folder structure is just the skeleton. Four layers make it intelligent:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>File</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Master Context</strong></td>
<td><code>CLAUDE.md</code></td>
<td>Who I am, vault map, startup checks. Under 80 lines.</td>
</tr>
<tr>
<td><strong>Skills</strong></td>
<td><code>.claude/skills/*.md</code></td>
<td>13 capabilities. Read on invoke, not memorized.</td>
</tr>
<tr>
<td><strong>L1 Cache</strong></td>
<td><code>L1-static.md</code></td>
<td>Tags, frontmatter, schedule — one read replaces ten.</td>
</tr>
<tr>
<td><strong>State</strong></td>
<td><code>session-active.md</code>, <code>workflow-state.md</code></td>
<td>Persistence across sessions. Overdue detection.</td>
</tr>
</tbody></table>
<p>The whole system is ~2,500 lines of markdown instructions and templates. Built over 3 weeks of evenings.</p>
<hr />
<h2>4. A Day in the System</h2>
<h3>Morning: <code>/start-my-day</code></h3>
<p>One command. Claude reads state files, counts Galaxy notes, checks overdue workflows, pulls Todoist tasks, generates a brief:</p>
<pre><code class="language-plaintext">&gt; MORNING BRIEF — Friday, April 25
&gt; Focus: SIDDHI W3 Day 5 + Sprint close
&gt; Atlas: 31 · Drop Zone: 0
&gt;
&gt; SIDDHI: Week 3 — 14.5/15 hrs | 22/29 tasks | on track
&gt;
&gt; Active Quests:
&gt; 1. SIDDHI study session — Hybrid Search
&gt; 2. PR Review scan #2
&gt;
&gt; Bounties:
&gt; 3. Update property IDs (15m)
&gt; 4. LinkedIn post draft (30m)
</code></pre>
<p>Creates the daily note, fills the session log, sets up quests (big tasks) and bounties (small jobs). Most important task goes first.</p>
<h3>During the day: <code>/capture</code></h3>
<p>Mid-meeting thought: <code>/capture the retry logic on event grid should use exponential backoff with jitter</code>. Claude detects the domain, structures a note, suggests wikilinks, offers to create a Todoist task. Captured in seconds.</p>
<p>Need to close the terminal mid-task? <code>/checkpoint</code> saves full context — plan, progress, next step. Resume later with "continue."</p>
<h3>Evening: <code>/close-my-day</code></h3>
<p>Claude reads my daily note, harvests uncaptured action items, syncs completed Todoist tasks, generates a summary:</p>
<pre><code class="language-plaintext">&gt; SESSION CLOSED — Friday, April 25
&gt; Quests: 2/2 · Bounties: 3/4
&gt; Galaxy candidates: 2 — run /extract
&gt; Tomorrow: SIDDHI Day 6 + Blog draft
</code></pre>
<p>Drafts tomorrow's quests, rates intention score, and on Fridays generates a weekly summary. Under 2 minutes.</p>
<hr />
<h2>5. The Skill System and Knowledge Pipeline</h2>
<p>No code. Zero.</p>
<p>Each skill is a markdown file — plain text instructions that tell Claude Code what to do. Here's <code>/extract</code>, simplified:</p>
<pre><code class="language-plaintext">/extract — Knowledge Extraction to Galaxy

1. Read &amp; Identify Atomic Ideas
   Read source. Find ideas that pass:
   - Assertion test — statable as a claim?
   - Standalone test — readable without source context?

2. Check Duplicates
   Search Galaxy for existing notes on same concept.

3. Draft Galaxy Notes
   Assertion-style title, 3-5 sentences, 2+ wikilinks.

4. Present for Approval
   Show previews. User approves/modifies/rejects each.

5. Create Notes
   On approval: create in Galaxy, update source.
</code></pre>
<p>No API calls. No function definitions. No deployment. Claude reads this file when you type <code>/extract</code> and writes results back to your vault.</p>
<p>The 13 skills form a knowledge lifecycle:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Skills</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Orchestration</strong></td>
<td><code>/start-my-day</code>, <code>/close-my-day</code>, <code>/checkpoint</code></td>
</tr>
<tr>
<td><strong>Capture</strong></td>
<td><code>/capture</code>, <code>/braindump</code></td>
</tr>
<tr>
<td><strong>Knowledge Pipeline</strong></td>
<td><code>/triage</code>, <code>/extract</code>, <code>/promote</code>, <code>/connect</code></td>
</tr>
<tr>
<td><strong>Review</strong></td>
<td><code>/weekly-review</code>, <code>/consolidate</code></td>
</tr>
<tr>
<td><strong>Integration</strong></td>
<td><code>/task-sync</code> (Todoist)</td>
</tr>
<tr>
<td><strong>Goals</strong></td>
<td><code>/siddhi-session</code></td>
</tr>
</tbody></table>
<p>The pipeline moves knowledge through stages:</p>
<ul>
<li><p><strong>Capture</strong> → raw thoughts land in inbox</p>
</li>
<li><p><strong>Triage</strong> → route to the right folder</p>
</li>
<li><p><strong>Extract</strong> → pull atomic ideas into the Galaxy</p>
</li>
<li><p><strong>Promote</strong> → synthesize mature knowledge</p>
</li>
<li><p><strong>Connect</strong> → build the graph</p>
</li>
</ul>
<p>Because skills are markdown, you can edit them in 30 seconds. My morning brief was too long — I added "Keep output under 400 words" to the rules. Wanted quest/bounty separation — added a template to the brief format. The system evolves at the speed of editing a text file.</p>
<hr />
<h2>6. What Surprised Me — and What Breaks</h2>
<p><strong>The AI actually follows instructions.</strong> I expected drift, hallucinated steps, constant correction. Instead, Claude follows markdown instructions with surprising fidelity. "Ask before creating" — it asks. "Skip Todoist if not configured" — it skips. Markdown-as-code works.</p>
<p><strong>The system self-improves.</strong> Claude Code has auto-memory — it saves your corrections and preferences across sessions. I said "stop summarizing what you just did." It saved that as feedback and never did it again. The vault feeds the AI. The AI feeds the vault. Flywheel.</p>
<p><strong>Plain text wins.</strong> Notion databases, Roam graphs, Tana supertags — I've tried them. They all hit the same wall: AI can't access it, data is locked, or the tool pivots. Markdown in a folder will outlive every app. Swap the intelligence layer anytime. The knowledge layer is permanent.</p>
<h3>What breaks</h3>
<p><strong>Context window limits.</strong> A skill that reads too many files in one session burns through context. I hit this with <code>/weekly-review</code> trying to read 7 daily notes at once. The fix: L1 cache pre-assembles common context so skills don't re-read source files.</p>
<p><strong>Permission prompts.</strong> Claude Code asks for approval on every file write by default. Useful for safety, annoying at scale. You need to configure allow-rules in <code>settings.json</code> for your vault paths — this takes trial and error.</p>
<p><strong>The AI occasionally over-helps.</strong> Tell it to update a date in a state file and it might also "improve" surrounding content. The fix: explicit rules in skills — "touch only what's needed, match existing style." It works, but you learn this through corrections.</p>
<p><strong>Todoist sync is fragile.</strong> Task IDs, project IDs, section IDs — one wrong ID and tasks land in the wrong project. I burned an hour debugging this on day one. The setup script handles it now, but it's the least forgiving part of the system.</p>
<hr />
<h2>7. What I'd Do Differently</h2>
<p><strong>I built 13 skills before locking in the daily habit.</strong> Wrong order. Should have started with 3 — <code>/start-my-day</code>, <code>/close-my-day</code>, <code>/capture</code> — and lived with them for a week before adding the pipeline skills. The knowledge pipeline only matters once you have enough notes to pipeline.</p>
<p><strong>I optimized tokens before the flow worked.</strong> Built an L1 cache to reduce context usage before I had the basic skills running cleanly. Cache optimization matters at scale. At the start, just get the flow right.</p>
<p><strong>The one thing I got right: anchoring to a physical trigger.</strong> <code>/start-my-day</code> follows making warm water after my morning walk. Physical trigger, not digital. The system followed the habit, not the other way around.</p>
<hr />
<h2>8. Get Started</h2>
<p>SMRITI is open source. Three steps:</p>
<pre><code class="language-bash">git clone https://github.com/deepakgoyal-ai/smriti.git
cd smriti
claude
# Paste: "Set up SMRITI for me. Read SETUP.md and follow the instructions."
</code></pre>
<p>Claude auto-detects your system, configures MCP, walks you through setup. Open the folder in Obsidian as your viewing layer. Done.</p>
<p><strong>What you need:</strong></p>
<ul>
<li><p><a href="https://obsidian.md/">Obsidian</a> (free)</p>
</li>
<li><p><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> (Pro plan recommended)</p>
</li>
<li><p>30 minutes (longer if you hit MCP auth issues — see the repo README)</p>
</li>
</ul>
<p><strong>What you get:</strong></p>
<ul>
<li><p>13 skills for daily orchestration, knowledge management, and goal tracking</p>
</li>
<li><p>IPARAG vault structure that routes every piece of information</p>
</li>
<li><p>A knowledge pipeline that compounds over time</p>
</li>
<li><p>An AI companion that remembers your context across sessions</p>
</li>
</ul>
<p>Ships with sample notes, templates, and a full operating system you can customize.</p>
<p>Your notes are markdown. Your skills are markdown. Your knowledge is yours.</p>
<hr />
<p><em>Built by</em> <a href="https://deepakgoyal.ai"><em>Deepak Goyal</em></a><em>. Follow the journey:</em> <a href="https://github.com/deepakgoyal-ai"><em>GitHub</em></a> <em>·</em> <a href="https://linkedin.com/in/deepakgoyal-ai"><em>LinkedIn</em></a></p>
]]></content:encoded></item></channel></rss>