<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Chang Min Park</title>
    <description>Senior Software Engineer at Yahoo!</description>
    <link>https://changminpark.github.io/</link>
    <atom:link href="https://changminpark.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Thu, 10 Sep 2026 00:40:22 +0000</pubDate>
    <lastBuildDate>Thu, 10 Sep 2026 00:40:22 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Who Picks the Next Step: Loops vs Graphs</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;prompts&lt;/strong&gt;, &lt;strong&gt;agent loops&lt;/strong&gt;, &lt;strong&gt;orchestration graphs&lt;/strong&gt;, &lt;strong&gt;harnesses&lt;/strong&gt;, or &lt;strong&gt;agentic design patterns&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/building-effective-agents&quot;&gt;Building effective agents (Anthropic)&lt;/a&gt; — when a predefined workflow beats a free-form tool loop, and the reverse&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.langchain.com/blog/the-art-of-loop-engineering&quot;&gt;The Art of Loop Engineering (LangChain)&lt;/a&gt; — an agent as model + tools until a stop condition; stacked loops around that core&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.langchain.com/blog/langgraph&quot;&gt;LangGraph (LangChain)&lt;/a&gt; — cyclical graphs as an agent runtime; nodes, edges, and LLM-chosen branches&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.deeplearning.ai/the-batch/how-agents-can-improve-llm-performance&quot;&gt;Agentic design patterns, part 1 (Andrew Ng)&lt;/a&gt; — reflection, tool use, planning, multi-agent as capabilities, not a framework vote&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://linear.app/now/introducing-loops&quot;&gt;Introducing Loops (Linear)&lt;/a&gt; — scheduled or event-triggered agent runs; the product name that collided with “graph” on X&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;the-wrong-question&quot;&gt;The wrong question&lt;/h2&gt;

&lt;p&gt;I do not need a DAG (directed acyclic graph) to fix a Kotlin null in Cursor. The session is already a loop: read, edit, run the test, repeat until the test or I say stop. Nobody draws LangGraph for that.&lt;/p&gt;

&lt;p&gt;In July 2026 two announcements landed in the same week. DeepLearning.AI released a course with “graph” in the title. Linear released a product called &lt;strong&gt;Loops&lt;/strong&gt;. On X, people treated that coincidence as a fork you had to pick: build agents as loops, or build them as graphs.&lt;/p&gt;

&lt;p&gt;That is the wrong question. The useful one is simpler: &lt;strong&gt;who picks the next step, and when?&lt;/strong&gt; A loop answers that at inference time — the model chooses the next tool while it runs. A graph answers it at authoring time — you already named the legal hops in code. Everything else in this post is that distinction, plus the layers people keep stacking next to it as if they were rival products.&lt;/p&gt;

&lt;h2 id=&quot;two-words-four-jobs&quot;&gt;Two words, four jobs&lt;/h2&gt;

&lt;p&gt;The July argument mashed four jobs under two nouns:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phrase people used&lt;/th&gt;
      &lt;th&gt;Actual job&lt;/th&gt;
      &lt;th&gt;Example&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Knowledge graph&lt;/td&gt;
      &lt;td&gt;Data: entities and edges you store&lt;/td&gt;
      &lt;td&gt;Neo4j course on building a product-issue graph from CSVs and reviews&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Orchestration graph&lt;/td&gt;
      &lt;td&gt;Control flow: which node runs next&lt;/td&gt;
      &lt;td&gt;LangGraph, a state machine, a CI DAG&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent loop&lt;/td&gt;
      &lt;td&gt;Control flow: model picks the next tool&lt;/td&gt;
      &lt;td&gt;Claude Code, Cursor, a ReAct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Linear Loop&lt;/td&gt;
      &lt;td&gt;Product: cron or trigger plus an agent&lt;/td&gt;
      &lt;td&gt;Recurring triage, doc updates&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A knowledge graph answers &lt;em&gt;what is related&lt;/em&gt;. An orchestration graph answers &lt;em&gt;who runs next&lt;/em&gt;. Mix those and a course about storing related facts sounds like a vote for LangGraph. Linear’s Loops is closer to cron plus an agent than to either.&lt;/p&gt;

&lt;p&gt;Andrew Ng did not write the “graduate from loops to graphs” ladder people attributed to him. He published &lt;strong&gt;design patterns&lt;/strong&gt; — reflection, tool use, planning, multi-agent — and &lt;strong&gt;iteration beats one-shot&lt;/strong&gt;. Those fit inside a loop or a graph. The course that started the noise, &lt;a href=&quot;https://www.deeplearning.ai/courses/agentic-knowledge-graph-construction&quot;&gt;Agentic Knowledge Graph Construction&lt;/a&gt;, is Kollegger / Neo4j filling a &lt;strong&gt;knowledge&lt;/strong&gt; graph. Ng hosts the platform.&lt;/p&gt;

&lt;p&gt;So forget the fork. Keep the question: who picks the next step, and when?&lt;/p&gt;

&lt;h2 id=&quot;loop-vs-graph&quot;&gt;Loop vs graph&lt;/h2&gt;

&lt;p&gt;A coding-agent session is one model, tools, a budget, a stop. Each turn the model chooses a tool. The next step does not exist until it emits it.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# The model chooses the next action. Edges are not in your repo.
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;budget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# billed every iteration
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;harness&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;should_stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Use a loop when you cannot draw the workflow without running it: debugging, open-ended research, “make this Compose preview compile.” A graph cannot discover a step you did not author.&lt;/p&gt;

&lt;p&gt;An orchestration graph is the other end. You wrote the legal next steps when you drew the nodes. The model still works &lt;em&gt;inside&lt;/em&gt; a node — often as its own loop — and it may still pick among &lt;strong&gt;declared&lt;/strong&gt; successors. It does not invent a hop that is not on the map.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# You chose the next node. The model does the work inside one.
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_edge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;triage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;android&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_conditional_edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;critic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pass&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;human&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;fail&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;android&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That buys inspectability and a real checkpoint — a human step before a store-facing side effect. You pay in coordination cost and bugs in the graph itself.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  M[Model] --&amp;gt;|picks tool| T[Tool]
  T --&amp;gt;|observe| M
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. A loop: the model owns the next hop. There is no edge list in the repo.&lt;/em&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  A[Triage] --&amp;gt; B[Android specialist]
  B --&amp;gt; C[Critic]
  C --&amp;gt;|fail| B
  C --&amp;gt;|pass| H[Human gate]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 2. A graph: the edge list owns the next hop. The Android node may still run a loop inside.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A loop is already a graph with one node and an edge back to itself. The interesting difference is &lt;strong&gt;when routing is decided&lt;/strong&gt;, not whether you can draw a circle.&lt;/p&gt;

&lt;h2 id=&quot;layers-not-a-ladder&quot;&gt;Layers, not a ladder&lt;/h2&gt;

&lt;p&gt;Vendor decks also stack &lt;strong&gt;prompt&lt;/strong&gt;, &lt;strong&gt;loop&lt;/strong&gt;, &lt;strong&gt;graph&lt;/strong&gt;, and &lt;strong&gt;harness&lt;/strong&gt; as if you pick one. They answer different questions. They stack under the same “who picks next?” problem; they are not four products.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Term&lt;/th&gt;
      &lt;th&gt;Question it answers&lt;/th&gt;
      &lt;th&gt;What you write&lt;/th&gt;
      &lt;th&gt;What it cannot do&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What should &lt;em&gt;this&lt;/em&gt; model call do?&lt;/td&gt;
      &lt;td&gt;System / user text, few-shots&lt;/td&gt;
      &lt;td&gt;Call a tool, retry, veto “done”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Loop&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Who picks the next tool?&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt; + model + tools&lt;/td&gt;
      &lt;td&gt;Declare illegal hops; enforce a stop&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Graph&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Which named step may run next?&lt;/td&gt;
      &lt;td&gt;Nodes, edges, joins, human gates&lt;/td&gt;
      &lt;td&gt;Discover a step you did not draw&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Harness&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;What may execute, and when is it actually done?&lt;/td&gt;
      &lt;td&gt;Tool allowlists, hooks, observe, budgets&lt;/td&gt;
      &lt;td&gt;Invent product intent (that is the spec / prompt)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The prompt is an &lt;em&gt;input&lt;/em&gt; to a turn. The loop is who picks the next tool. The graph is which named step may start. The &lt;a href=&quot;/agent-done-but-ci-red&quot;&gt;harness&lt;/a&gt; is the machine that runs a loop &lt;em&gt;or&lt;/em&gt; a graph — hooks, permissions, max steps — and can refuse both. A loop without a harness stops when the model writes an essay. A graph without a harness still has no honest “done” on a node. The harness is not a fourth topology.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  H[Harness] --&amp;gt; G[Graph]
  H --&amp;gt; L[Loop]
  G --&amp;gt; L
  L --&amp;gt; P[Prompt]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 3. Layers, not a ladder. The harness can run a graph or a loop. A graph still bottoms out in a loop. Every turn still has a prompt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You do not need all four on every task. Almost every useful agent has a &lt;strong&gt;prompt&lt;/strong&gt; and a &lt;strong&gt;harness&lt;/strong&gt;. Then you choose a control-flow shape: usually a &lt;strong&gt;loop&lt;/strong&gt;; a &lt;strong&gt;graph&lt;/strong&gt; only when the path is real.&lt;/p&gt;

&lt;p&gt;Take the Kotlin null. The prompt names the file. The loop reads, edits, and runs the test. The harness refuses “done” until the test exits 0. Drawing a graph here is leftover ceremony.&lt;/p&gt;

&lt;p&gt;For example, in a mail-style Android client, a crash that must be grouped, attributed to a module, checked against a release flag, and only then allowed to file a public bug is a &lt;strong&gt;path&lt;/strong&gt;: triage → specialist → critic → human. Different duties, different stop rules, a human before anything leaves the building. That is graph-shaped even if you implement it with three scripts and a queue. Each node still has a prompt and a loop. The harness is what blocks the public-bug file until the human step runs.&lt;/p&gt;

&lt;p&gt;If a chat failed, name the layer. Wrong wording is a prompt bug. The model wandering into the wrong module is a loop with a thin harness. A missing human approval step is a graph you never drew. A long “done” write-up over a failing test is a missing harness, not a reason to buy LangGraph.&lt;/p&gt;

&lt;h2 id=&quot;so-when-do-i-draw-edges&quot;&gt;So when do I draw edges?&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Situation&lt;/th&gt;
      &lt;th&gt;Prefer&lt;/th&gt;
      &lt;th&gt;Why&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;One module, one verifier, same retry&lt;/td&gt;
      &lt;td&gt;Loop&lt;/td&gt;
      &lt;td&gt;Topology is overhead&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Research → write → skeptic → ship or return&lt;/td&gt;
      &lt;td&gt;Graph&lt;/td&gt;
      &lt;td&gt;Different duties and stop rules&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Parallel audit, then a veto&lt;/td&gt;
      &lt;td&gt;Graph&lt;/td&gt;
      &lt;td&gt;Fan-in is a real join&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Human approval before a store/side effect&lt;/td&gt;
      &lt;td&gt;Graph + gate&lt;/td&gt;
      &lt;td&gt;The checkpoint is an edge&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A personal Cursor session on one Gradle module stays a loop. Recurring Linear triage is a &lt;strong&gt;triggered loop&lt;/strong&gt; with a review step — closer to cron plus an agent than to a knowledge graph or to LangGraph.&lt;/p&gt;

&lt;p&gt;Do not buy a graph framework to feel current. Buy it when you can name the nodes, the illegal transitions, and the join you will debug at 2 a.m. Until then the interesting engineering is still the loop: tools, budgets, and a stop that is not the model’s essay. Same default Ng and Anthropic point at — start with iteration inside a loop; add named nodes when a branch, a join, or a human gate is a real requirement.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.deeplearning.ai/the-batch/agentic-design-patterns-part-2-reflection&quot;&gt;Agentic Design Patterns Part 2: Reflection (Andrew Ng)&lt;/a&gt; — self-critique in one worker vs a separate critic agent&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.deeplearning.ai/courses/agentic-knowledge-graph-construction&quot;&gt;Agentic Knowledge Graph Construction (DeepLearning.AI)&lt;/a&gt; — Kollegger / Neo4j; entities and relationships, not LangGraph&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://explainx.ai/blog/graphs-vs-loops-agentic-ai-debate-linear-andrew-ng-2026&quot;&gt;Graphs vs. Loops: the orchestration debate (explainx)&lt;/a&gt; — July 2026 collision of the knowledge-graph course and Linear Loops&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://sangampandey.info/blog/graph-engineering-agent-loops-to-graphs&quot;&gt;Graph Engineering: when an agent loop should be a graph (Sangam Pandey)&lt;/a&gt; — inference-time vs authoring-time routing&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.truefoundry.com/blog/andrew-ng-loop-graph-engineering-production&quot;&gt;Andrew Ng, loops and graphs as production architecture (TrueFoundry)&lt;/a&gt; — graph nodes still contain loops; do not confuse with knowledge graphs&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aakashgupta.medium.com/2025-was-agents-2026-is-agent-harnesses-heres-why-that-changes-everything-073e9877655e&quot;&gt;2025 was agents; 2026 is harnesses&lt;/a&gt; — runtime around the model: tools, context, stop conditions&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://sarthakai.substack.com/p/harness-graph-and-loop-engineering&quot;&gt;Harness, graph, and loop engineering (Sarthak)&lt;/a&gt; — stacked layers; loops and graphs are two ways to run, not replacements for a harness&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 08 Sep 2026 10:30:00 +0000</pubDate>
        <link>https://changminpark.github.io/agent-loop-vs-orchestration-graph</link>
        <guid isPermaLink="true">https://changminpark.github.io/agent-loop-vs-orchestration-graph</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>The Rule the Other Agent Never Saw</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;.mdc project rules&lt;/strong&gt;, &lt;strong&gt;CLAUDE.md vs AGENTS.md&lt;/strong&gt;, &lt;strong&gt;hooks&lt;/strong&gt;, or &lt;strong&gt;skills&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://cursor.com/docs/rules&quot;&gt;Cursor docs — Rules&lt;/a&gt; — &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; with frontmatter; a plain &lt;code&gt;.md&lt;/code&gt; in that folder is ignored&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/memory&quot;&gt;Claude Code — Memory&lt;/a&gt; — reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, not &lt;code&gt;AGENTS.md&lt;/code&gt;; &lt;code&gt;@AGENTS.md&lt;/code&gt; import and &lt;code&gt;/import&lt;/code&gt; as a one-time copy&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://cursor.com/docs/reference/third-party-hooks&quot;&gt;Cursor — Third-party hooks&lt;/a&gt; — Claude &lt;code&gt;.claude/settings.json&lt;/code&gt; hooks load in Cursor only after an opt-in&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/skills&quot;&gt;Claude Code — Skills&lt;/a&gt; — &lt;code&gt;.claude/skills/*/SKILL.md&lt;/code&gt;, not Cursor&apos;s skill tree&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/mcp&quot;&gt;Claude Code — MCP&lt;/a&gt; — listing a server is not enabling it; deny lives in settings&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;why-the-same-checkout-disagrees&quot;&gt;Why the same checkout disagrees&lt;/h2&gt;

&lt;p&gt;A company blesses Cursor &lt;em&gt;and&lt;/em&gt; Claude Code, or it blesses one and people still use the other — different teams, different laptops. That is normal. The failure is treating “the repo has rules” as if every agent reads the same files.&lt;/p&gt;

&lt;p&gt;Each product is its own loader. &lt;strong&gt;Which sentences enter the window depends on which binary someone launched.&lt;/strong&gt; Two people on the same ticket can follow two policies without knowing it.&lt;/p&gt;

&lt;p&gt;The case that made this concrete for me was a security / skill-creation rule: do not add a second skill (or a second always-on rule) for a job the repo already owns. Cursor honored it. Claude Code cheerfully wrote &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/review/SKILL.md&lt;/code&gt; next to the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/skills/review/&lt;/code&gt; playbook — same job, second copy, now two sources of truth. The model did not rebel. The “no duplicates” sentence lived only in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alwaysApply: true&lt;/code&gt;. Claude Code never saw that file.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  repo[Git tree]
  repo --&amp;gt; cur[Cursor]
  repo --&amp;gt; cla[Claude Code]
  cur --&amp;gt; mdc[&quot;.mdc / AGENTS.md&quot;]
  cur --&amp;gt; chooks[hooks.json]
  cla --&amp;gt; cmd[CLAUDE.md]
  cla --&amp;gt; crules[&quot;.claude/rules/*.md&quot;]
  cla --&amp;gt; sethooks[settings.json hooks]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. One checkout, two loaders. Overlap is accidental unless you designed it.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;mdc-is-not-md&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; is not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Cursor project rules must be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; (frontmatter for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;globs&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alwaysApply&lt;/code&gt;). A plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-duplicate-skills.md&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/rules/&lt;/code&gt; is ignored. Claude’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/rules/&lt;/code&gt; is the opposite: &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt;, recursive.&lt;/strong&gt; Copy without renaming and nothing loads.&lt;/p&gt;

&lt;p&gt;A Cursor glob on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**/{skills,rules}/**&lt;/code&gt; has no 1:1 in a root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;. Flatten it and you burn context on every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls&lt;/code&gt;; skip it and the CLI never sees the no-duplicate rule. Claude’s path-scoped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/rules/&lt;/code&gt; is a cousin, not a clone.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; looks portable. Cursor reads it. Claude Code reads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;. The bridges are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AGENTS.md&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/import&lt;/code&gt;, which &lt;strong&gt;appends a copy&lt;/strong&gt;. A copy forks the next time someone edits only one side.&lt;/p&gt;

&lt;h2 id=&quot;the-rest-of-the-harness-splits-too&quot;&gt;The rest of the harness splits too&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Surface&lt;/th&gt;
      &lt;th&gt;Cursor&lt;/th&gt;
      &lt;th&gt;Claude Code&lt;/th&gt;
      &lt;th&gt;How it bites&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Rules&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; (+ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/rules/*.md&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;“Do not create a duplicate skill” lives only in Cursor&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Skills&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/skills/&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/*/SKILL.md&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Claude writes a second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;review&lt;/code&gt; skill because it never saw the no-duplicate rule&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Hooks&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/hooks.json&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hooks&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;A write-block on new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; files never runs in the other product (Cursor loads Claude hooks only if you opt in; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bash&lt;/code&gt; vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Shell&lt;/code&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MCP&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/mcp.json&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mcp.json&lt;/code&gt; + enable/deny lists&lt;/td&gt;
      &lt;td&gt;Connected in the IDE, pending in the CLI&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Org overlay&lt;/td&gt;
      &lt;td&gt;Team / User rules, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.cursor/&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude/&lt;/code&gt;, managed settings&lt;/td&gt;
      &lt;td&gt;Dashboard policy never hits the CLI&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Ignore files, memories, subagents, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permissions.deny&lt;/code&gt; split the same way.&lt;/p&gt;

&lt;p&gt;Markdown is advice. A PreToolUse deny is a gate the model cannot forge. If that gate lives only in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.cursor/hooks.json&lt;/code&gt;, Claude Code will agree not to add a second skill and then write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/skills/review/SKILL.md&lt;/code&gt; anyway. Cursor Team Rules feel like company policy and never leave the IDE.&lt;/p&gt;

&lt;p&gt;A missing file is easy: “Claude doesn’t pick up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt;.” &lt;strong&gt;Both files present and disagreeing is worse.&lt;/strong&gt; Cursor says one review skill, owned here. Claude’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.md&lt;/code&gt; still says “add a skill when the workflow is useful.” Each session looks consistent. The repo now has two playbooks. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/import&lt;/code&gt; and a hand-copied &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; are the same bug as the duplicate skill: a second copy you will not keep in sync.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - if it must hold in both products, prove it in both: ask each agent to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;review&lt;/code&gt; skill that already exists. If only Cursor refuses, you have folklore in git.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;keep-one-prose-file-thin-adapters&quot;&gt;Keep one prose file, thin adapters&lt;/h2&gt;

&lt;p&gt;Do not symlink everything. Do not generate forty adapters.&lt;/p&gt;

&lt;p&gt;Put portable facts in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; (one skill per job, where skills live, who owns them). Point Claude at it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@AGENTS.md&lt;/code&gt;. Keep Cursor globs in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt;, Claude path rules under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/rules/&lt;/code&gt;, and hard stops in &lt;strong&gt;hooks both processes actually run&lt;/strong&gt; — after the third-party-hook opt-in, with matcher names checked.&lt;/p&gt;

&lt;p&gt;Do not put “do not create a duplicate skill” only in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt;, org security only in a Cursor dashboard, or treat &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/init&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/import&lt;/code&gt; as a sync. Those are onboarding. Drift starts on the next edit.&lt;/p&gt;

&lt;p&gt;Where a constraint lives &lt;em&gt;inside&lt;/em&gt; one product is &lt;a href=&quot;/agent-forgot-the-constraint&quot;&gt;a placement problem&lt;/a&gt;. Making the tree readable is &lt;a href=&quot;/monorepo-navigable-to-agents&quot;&gt;a map problem&lt;/a&gt;. If the CLI “forgets” a rule Cursor still injects, look at the loaders before the model.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://agents.md/&quot;&gt;AGENTS.md&lt;/a&gt; — the cross-tool instruction file Cursor reads natively; Claude Code still needs an import&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/claude-directory&quot;&gt;Explore the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude&lt;/code&gt; directory&lt;/a&gt; — which Claude files are project vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cursor.com/help/customization/skills&quot;&gt;Cursor — Skills&lt;/a&gt; — Cursor’s on-demand playbooks vs Claude’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKILL.md&lt;/code&gt; tree&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.digitalapplied.com/blog/where-ai-coding-tools-keep-their-instructions&quot;&gt;Where every AI coding tool looks for instructions&lt;/a&gt; — loader map across Cursor, Claude Code, and neighbors&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dev.to/olivia_craft/switching-from-cursor-to-claude-code-your-rules-stop-working-migration-guide-5g2f&quot;&gt;Switching from Cursor to Claude Code? Your rules stop working&lt;/a&gt; — the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; migration people underestimate&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 02 Sep 2026 14:20:00 +0000</pubDate>
        <link>https://changminpark.github.io/the-rule-the-other-agent-never-saw</link>
        <guid isPermaLink="true">https://changminpark.github.io/the-rule-the-other-agent-never-saw</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>You Can&apos;t Delete Claude&apos;s Watermark With a Remover</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;SynthID-style text watermarks&lt;/strong&gt;, &lt;strong&gt;EU AI Act transparency marking&lt;/strong&gt;, or &lt;strong&gt;C2PA file credentials&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/news/claude-text-watermark&quot;&gt;How Claude&apos;s text watermarking works (Anthropic)&lt;/a&gt; — word-choice watermark, not hidden characters; what a detection score can and cannot mean&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://support.claude.com/en/articles/16266773-how-claude-marks-ai-generated-content&quot;&gt;How Claude marks AI-generated content&lt;/a&gt; — text watermark vs C2PA on files; applies across API and products&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.nature.com/articles/s41586-024-08025-4&quot;&gt;SynthID-Text (Nature, 2024)&lt;/a&gt; — the watermarking method family Anthropic builds on&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai&quot;&gt;EU AI Act overview&lt;/a&gt; — why providers are marking generated content for transparency&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;the-wrong-mental-model-of-delete&quot;&gt;The wrong mental model of “delete”&lt;/h2&gt;

&lt;p&gt;Within days of Anthropic explaining Claude’s invisible text watermark, the web filled with “watermark removers.” Many of them strip zero-width Unicode, normalize fancy dashes, scrub metadata. That toolkit made sense when people suspected &lt;em&gt;hidden characters&lt;/em&gt;. Claude’s mark is not that.&lt;/p&gt;

&lt;p&gt;Anthropic’s public write-up is blunt: &lt;strong&gt;nothing is added to the text&lt;/strong&gt;. The signal is a statistical pattern in &lt;strong&gt;which near-synonyms the model picks&lt;/strong&gt; — a SynthID-Text-style scheme. You cannot &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; away a bias in sampling. Anthropic frames the launch as EU AI Act transparency marking applied globally; other providers will use different keys. Until a public detector exists, “we removed Claude’s watermark” is faith-based.&lt;/p&gt;

&lt;p&gt;The claim worth arguing: &lt;strong&gt;you cannot delete the mark; you can only replace the words.&lt;/strong&gt; A positive detection means Claude was &lt;em&gt;in the loop&lt;/em&gt;, not that a human was &lt;em&gt;out&lt;/em&gt; of it. Removers, C2PA strippers, and authorship court all fail that distinction.&lt;/p&gt;

&lt;h2 id=&quot;how-the-mark-gets-into-the-prose&quot;&gt;How the mark gets into the prose&lt;/h2&gt;

&lt;p&gt;Language models repeatedly choose the next token among candidates. Sometimes the choice is forced (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Principia&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mathematica&lt;/code&gt;). Often it is soft: &lt;em&gt;overcast&lt;/em&gt; vs &lt;em&gt;grey&lt;/em&gt;. Watermarking reuses those soft choices. Sampling is nudged with a secret key and recent context so that, over a long enough passage, the sequence of soft picks is unlikely under a human or a different key.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  ctx[Prior tokens] --&amp;gt; candidates[Soft candidates]
  key[Watermark key] --&amp;gt; pick[Biased sample]
  candidates --&amp;gt; pick
  pick --&amp;gt; next[Next word]
  next --&amp;gt; pattern[Long-run pattern]
  pattern --&amp;gt; detect[Detector with key]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The watermark is not a sticker on the paragraph. It is a bias in low-stakes word picks that becomes measurable only with the key and enough text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Consequences that kill the remover fantasy:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Short snippets are weak.&lt;/strong&gt; Few soft decisions → little signal.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Hard text is sparsely marked.&lt;/strong&gt; Code that must compile and math that must be right leave fewer degrees of freedom.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Heavy Claude writing leaves more mark.&lt;/strong&gt; A from-scratch draft is dense; a light proofread may not move the needle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;removers-do-not-delete--rewrites-replace&quot;&gt;Removers do not delete — rewrites replace&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Attack people propose&lt;/th&gt;
      &lt;th&gt;What it actually does&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Strip zero-width / bidi / “AI dash” Unicode&lt;/td&gt;
      &lt;td&gt;Removes characters that were never the Claude watermark&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Strip C2PA / EXIF from a PNG&lt;/td&gt;
      &lt;td&gt;Drops &lt;strong&gt;file&lt;/strong&gt; provenance. Does not touch &lt;strong&gt;text&lt;/strong&gt; watermark&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Light edit, synonym sprinkle&lt;/td&gt;
      &lt;td&gt;May leave enough of the original soft-pick sequence to still score&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Full paraphrase / retranslate with another model&lt;/td&gt;
      &lt;td&gt;Can destroy the Claude-key pattern — because you replaced the words&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;“Remover” SaaS before a public detector exists&lt;/td&gt;
      &lt;td&gt;Unverifiable marketing until you can re-run a real detector&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Anthropic’s FAQ lands where the engineering does: light editing probably does not wipe the mark; a complete rewrite will — and at that point you are arguing whether the artifact is still “Claude’s text.” Evading a watermark the hard way is authorship transfer by exhaustion, not a delete button. The cottage industry selling one-click deletion is, for now, mostly Unicode hygiene and hope.&lt;/p&gt;

&lt;p&gt;Do not conflate that with &lt;strong&gt;C2PA&lt;/strong&gt; on supported files. Text watermark lives in word-choice statistics and survives copy-paste; C2PA is signed file metadata that often dies on export or screenshot and is easy to strip. If your threat model is a CMS paste, the sticky signal is the text mark — the one removers keep misunderstanding.&lt;/p&gt;

&lt;h2 id=&quot;a-positive-hit-is-not-authorship-court&quot;&gt;A positive hit is not authorship court&lt;/h2&gt;

&lt;p&gt;A Claude watermark detector (when the API ships) answers something like: &lt;strong&gt;how likely is it that Claude was involved in producing this passage?&lt;/strong&gt; It does &lt;strong&gt;not&lt;/strong&gt; prove a human did not write the first draft, distinguish “Claude authored this” from “Claude heavily edited this,” identify which user called the API, or detect GPT- or Gemini-only text.&lt;/p&gt;

&lt;p&gt;That gap creates real failure modes. You paste a design doc into Claude for a clarity pass; the returned prose may carry detectable involvement even though the ideas were yours. You ask Claude Code for a PR description; the markdown may be marked while the diff is mostly yours. A hiring screen that treats “watermark present” as “cheated” is using a probability as a verdict. The dual error is also true: &lt;strong&gt;absence of a Claude watermark is not proof of human authorship&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I use Claude the same way many Android engineers do: draft PR text, reshape release notes, occasionally a first pass on a design doc. Watermarking does not change ownership under Anthropic’s terms. I still own the words I merge. Do not treat a future detector score as court — decide the policy you want (disclose assistance, ban certain uses, or judge the work product). Separate C2PA hygiene from text hygiene when you ship screenshots.&lt;/p&gt;

&lt;p&gt;The headline fight has a precise answer: &lt;strong&gt;not by deletion, only by replacement.&lt;/strong&gt; Everything else is metadata theater or an unverifiable SaaS claim. The harder product question is the one Anthropic already admits: a watermark says Claude was &lt;em&gt;in the loop&lt;/em&gt;, not that a human was &lt;em&gt;out&lt;/em&gt; of it.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/ai-watermark-removers-flood-the-web-almost-none-can-prove-they-work/&quot;&gt;AI ‘watermark removers’ flood the web (BleepingComputer)&lt;/a&gt; — why Unicode/C2PA strippers do not prove text-watermark removal&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.theverge.com/ai-artificial-intelligence/977823/anthropic-claude-ai-watermarks-c2pa-text-images&quot;&gt;Claude will apply invisible watermarks (The Verge)&lt;/a&gt; — product scope and EU timing context&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://deepmind.google/blog/watermarking-ai-generated-text-and-video-with-synthid/&quot;&gt;Watermarking AI-generated text with SynthID (DeepMind blog)&lt;/a&gt; — accessible overview of the method family (paper is in Prerequisites)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://c2pa.org/&quot;&gt;C2PA specification&lt;/a&gt; — signed content credentials for files (distinct from text sampling watermarks)&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 22 Aug 2026 19:35:00 +0000</pubDate>
        <link>https://changminpark.github.io/claude-watermark-cant-just-delete</link>
        <guid isPermaLink="true">https://changminpark.github.io/claude-watermark-cant-just-delete</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>When GitHub Actions Is the Outage</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;GitHub Status&lt;/strong&gt;, &lt;strong&gt;Actions runners&lt;/strong&gt;, or &lt;strong&gt;required checks as a merge freeze&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.githubstatus.com/&quot;&gt;GitHub Status&lt;/a&gt; — step zero when every repo’s Actions lies at once&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions&quot;&gt;Understanding GitHub Actions&lt;/a&gt; — workflows, jobs, and how a runner gets assigned work&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners&quot;&gt;About self-hosted runners&lt;/a&gt; — your VM is not a full escape from GitHub’s scheduler&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches&quot;&gt;About protected branches&lt;/a&gt; — required checks turn an Actions outage into a merge freeze&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks&quot;&gt;About status checks (GitHub)&lt;/a&gt; — what those required checks are supposed to protect&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;it-was-not-your-gradle-cache&quot;&gt;It was not your Gradle cache&lt;/h2&gt;

&lt;p&gt;On August 6, 2026, &lt;a href=&quot;https://www.githubstatus.com/&quot;&gt;GitHub Status&lt;/a&gt; opened an Actions investigation at &lt;strong&gt;15:22 UTC&lt;/strong&gt;. Within an hour, workflow runs were failing to start or dying mid-flight, the Actions REST API was erroring, and some workflows saw unexpected rate limits. Capacity stayed constrained for hours. Hosted runners were hit hard; self-hosted runners still saw registration errors and rate limiting. Webhook delivery slowed. Pages, Copilot code review, and Copilot coding agent degraded alongside Actions. Enterprise Importer migrations were paused to help recovery.&lt;/p&gt;

&lt;p&gt;By evening, GitHub’s updates named a concrete failure mode: &lt;strong&gt;runners were being assigned jobs that were no longer valid&lt;/strong&gt;. Job success rates for runs that could start climbed from roughly &lt;strong&gt;30–40%&lt;/strong&gt; earlier in the incident toward &lt;strong&gt;97–99%&lt;/strong&gt; after mitigations. Webhook-triggered workflows stayed throttled while queues burned down. A cold Gradle cache does not look like that. A broken &lt;strong&gt;assignment / scheduling path&lt;/strong&gt; does.&lt;/p&gt;

&lt;p&gt;If your merge bar is a required Actions check—and for most Android PR gates it is (see &lt;a href=&quot;/github-actions-android-pr-gates&quot;&gt;GitHub Actions for Android PR gates&lt;/a&gt;)—then an Actions outage is not a vendor inconvenience. It is a &lt;strong&gt;shipping freeze&lt;/strong&gt; with a status-page URL.&lt;/p&gt;

&lt;h2 id=&quot;what-actually-broke-from-the-public-timeline&quot;&gt;What actually broke (from the public timeline)&lt;/h2&gt;

&lt;p&gt;You do not need an internal postmortem to act. The public status updates are enough to shape an on-call mental model:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Symptom teams saw&lt;/th&gt;
      &lt;th&gt;What it usually means&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Jobs sit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;queued&lt;/code&gt; for a long time, then time out&lt;/td&gt;
      &lt;td&gt;Assignment / capacity path is stuck&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Runs fail at start or mid-job with infra errors&lt;/td&gt;
      &lt;td&gt;Runner got work it cannot finish cleanly&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Actions API errors / odd rate limits&lt;/td&gt;
      &lt;td&gt;Control plane under load or shedding&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Self-hosted runners “online” but idle&lt;/td&gt;
      &lt;td&gt;Scheduler / registration path, not your VM&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pages / Copilot agents / webhooks wobble&lt;/td&gt;
      &lt;td&gt;Shared dependency blast radius&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - if &lt;em&gt;every&lt;/em&gt; repo’s Actions went weird in the same window, open &lt;a href=&quot;https://www.githubstatus.com/&quot;&gt;githubstatus.com&lt;/a&gt; before you debug &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gradle.properties&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Self-hosted is the wrong comfort blanket here. Your machines still ask GitHub’s scheduling layer for work. When that layer assigns dead jobs or throttles registration, “we run our own runners” does not buy immunity. Hosted vs self-hosted still matters for SDK images and secrets—see &lt;a href=&quot;/ghec-vs-ghes&quot;&gt;GHEC vs GHES&lt;/a&gt;—but it is not a reliability silver bullet for Actions itself.&lt;/p&gt;

&lt;h2 id=&quot;why-this-hurts-android-merge-queues-specifically&quot;&gt;Why this hurts Android merge queues specifically&lt;/h2&gt;

&lt;p&gt;Android PR latency is already expensive: Gradle, emulators optional, module graphs, cache warm-up. Teams design &lt;a href=&quot;/github-actions-android-pr-gates&quot;&gt;merge contracts&lt;/a&gt; so humans wait on a &lt;strong&gt;bounded&lt;/strong&gt; green check, not a forty-minute vanity matrix.&lt;/p&gt;

&lt;p&gt;When Actions degrades:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Required checks stop meaning “safe to merge.”&lt;/strong&gt; They mean “the fleet could not prove anything.” Merging on red because “Actions is down” is a policy decision—not a free pass.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Retry storms make it worse.&lt;/strong&gt; Twenty engineers hammering &lt;strong&gt;Re-run all jobs&lt;/strong&gt; on a throttled webhook path deepens the backlog. Prefer one coordinated retry after status shows recovery, not N independent panic clicks.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Agent / bot reviewers go quiet too.&lt;/strong&gt; Copilot code review and coding agent sat in the same blast radius. If your process assumes a bot comment before human review, that assumption fails with the scheduler.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Release trains that only trust GitHub-hosted proof stall.&lt;/strong&gt; Org CI next door (Jenkins, Screwdriver, Buildkite) exists partly for this reason—see &lt;a href=&quot;/cicd-jenkins-to-screwdriver&quot;&gt;Jenkins to Screwdriver&lt;/a&gt;—but day-to-day PR gates usually still live on Actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical failure mode on a large client monorepo is social as much as technical: Slack fills with screenshots of yellow dots, people ask whether to merge without checks, and someone almost ships a Manifest permission change because “CI has been broken all day.”&lt;/p&gt;

&lt;h2 id=&quot;what-to-do-while-it-is-red&quot;&gt;What to do while it is red&lt;/h2&gt;

&lt;p&gt;Keep a short playbook next to your workflow YAML—not a novel:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Confirm scope.&lt;/strong&gt; One repo vs status page. If status is red for Actions, stop local archaeology.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Freeze non-urgent merges&lt;/strong&gt; that require Actions proof. Document the exception path (who can waive, for what class of change).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Do not mass re-run.&lt;/strong&gt; Re-run after GitHub reports queues draining and webhook throughput restoring—not every fifteen minutes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Separate “human review done” from “machine proof done.”&lt;/strong&gt; Review can continue offline; merge waits for proof unless policy explicitly allows a waive.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;After recovery, re-run the critical path once&lt;/strong&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;/release branches you care about. Queued-before-recovery jobs may still be poisoned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Optional resilience (months, not during the fire): a thin &lt;strong&gt;org CI shadow&lt;/strong&gt; for release branches, or a documented manual assemble cone for emergencies. Dual-running every PR on two fleets is usually cost theater; dual-path for &lt;em&gt;promotion&lt;/em&gt; is often rational.&lt;/p&gt;

&lt;h2 id=&quot;what-not-to-conclude&quot;&gt;What not to conclude&lt;/h2&gt;

&lt;p&gt;Do not rewrite your whole DevEx strategy from one bad Thursday. Do conclude:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Actions reliability is part of your &lt;strong&gt;merge SLA&lt;/strong&gt;, not GitHub’s problem alone.&lt;/li&gt;
  &lt;li&gt;Status pages belong in the first five minutes of “CI is broken” triage.&lt;/li&gt;
  &lt;li&gt;Self-hosted runners optimize for environment control; they do not remove scheduler coupling.&lt;/li&gt;
  &lt;li&gt;Cascades matter: the same day can take down Pages deploys and AI review bots that your process silently depends on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub will publish a fuller RCA when it is ready. Until then, the actionable lesson is operational: when the green check is lying because the &lt;strong&gt;platform&lt;/strong&gt; is lying, treat it like an incident—not like a flaky unit test.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/ghec-vs-ghes&quot;&gt;GHEC vs GHES (this site)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/cicd-jenkins-to-screwdriver&quot;&gt;From Jenkins to Screwdriver (this site)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 07 Aug 2026 10:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/when-github-actions-is-the-outage</link>
        <guid isPermaLink="true">https://changminpark.github.io/when-github-actions-is-the-outage</guid>
        
        <category>Writing</category>
        
        <category>DevEx</category>
        
        
      </item>
    
      <item>
        <title>Experiment Layers vs Overlapping on Mobile</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;A/B tests&lt;/strong&gt;, &lt;strong&gt;exposure&lt;/strong&gt;, or &lt;strong&gt;feature flags vs experiments&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.statsig.com/experiments/layers-overview&quot;&gt;Statsig — Layers&lt;/a&gt; — mutual exclusion domains&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.statsig.com/blog/embracing-overlapping-a-b-tests-and-the-danger-of-isolating-experiments&quot;&gt;Embracing overlapping A/B tests&lt;/a&gt; — why isolation can hurt power&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.statsig.com/experiments/holdouts-introduction&quot;&gt;Holdouts&lt;/a&gt; — measuring cumulative impact with a held-back floor&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.statsig.com/feature-flags/overview&quot;&gt;Statsig — Feature flags&lt;/a&gt; — kill switch vs rollout vs experiment&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;this-ab-ate-that-ab&quot;&gt;This A/B ate that A/B&lt;/h2&gt;

&lt;p&gt;Two “winning” experiments ship the same week: a denser message list and a louder compose CTA. Metrics look great in isolation. Together, the inbox feels noisy and send drops. Neither dashboard was wrong; both were reading a population that quietly overlapped. &lt;strong&gt;Traffic topology&lt;/strong&gt; is the thing nobody put in the readout.&lt;/p&gt;

&lt;p&gt;This is not a rewrite of &lt;a href=&quot;/feature-flags-experiments-mobile&quot;&gt;kill switch / rollout / experiment jobs&lt;/a&gt;. It is the failure mode when those jobs share a screen without a contract.&lt;/p&gt;

&lt;h2 id=&quot;the-wrong-fight-overlap-is-modern-layers-are-legacy&quot;&gt;The wrong fight: “overlap is modern, layers are legacy”&lt;/h2&gt;

&lt;p&gt;Platforms that argue for &lt;strong&gt;overlapping&lt;/strong&gt; assignment (e.g. Statsig’s public writing) are right about velocity and statistical power: isolating &lt;em&gt;everything&lt;/em&gt; destroys both. The same docs still ship &lt;strong&gt;layers&lt;/strong&gt; (mutual exclusion domains) and &lt;strong&gt;interaction detection&lt;/strong&gt; — because some treatments are not independent parameters.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
  subgraph overlap [&quot;Overlapping — combinations exist&quot;]
    direction TB
    uO[User U]
    uO --&amp;gt; aO[&quot;Exp A: denser list&quot;]
    uO --&amp;gt; bO[&quot;Exp B: louder CTA&quot;]
    aO --&amp;gt; combo[&quot;U&apos;s inbox = denser + louder&quot;]
    bO --&amp;gt; combo
  end

  subgraph layer [&quot;Layer — mutual exclusion&quot;]
    direction TB
    pop[Users in one layer]
    pop --&amp;gt; xor{&quot;Hash once — A XOR B&quot;}
    xor --&amp;gt;|&quot;slot A&quot;| aL[&quot;Exp A only&amp;lt;br/&amp;gt;never enters Exp B&quot;]
    xor --&amp;gt;|&quot;slot B&quot;| bL[&quot;Exp B only&amp;lt;br/&amp;gt;never enters Exp A&quot;]
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Overlap: the same user can sit in A and B at once (combination cells). Layer: traffic is partitioned so a user is in at most one experiment in that domain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The interesting decision is not “which religion?” It is: &lt;strong&gt;can these two treatments rewrite the same pixel or the same write path at once?&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Symptom&lt;/th&gt;
      &lt;th&gt;Likely topology mistake&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Two “wins,” one ugly product&lt;/td&gt;
      &lt;td&gt;Overlap on colliding surfaces (list chrome + CTA + ranking)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Every test underpowered; teams fighting for users&lt;/td&gt;
      &lt;td&gt;Everything forced into layers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cumulative product drift nobody can measure&lt;/td&gt;
      &lt;td&gt;No &lt;strong&gt;holdout&lt;/strong&gt; floor — everyone is always in something&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ghost variants on old APKs&lt;/td&gt;
      &lt;td&gt;Overlapping remote config on clients that only understand half the matrix&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;where-layers-still-earn-rent-on-mobile&quot;&gt;Where layers still earn rent on mobile&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Colliding surfaces.&lt;/strong&gt; List density, ranking, and compose CTAs are not independent knobs. Exclusive layers (or hard gates) keep ownership honest when two teams ship into the same screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binary skew.&lt;/strong&gt; Old APKs linger. Overlapping configs on clients that only know half the variants produce “impossible” combinations in telemetry. Layers plus version targeting cut that class of ghost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Holdout floors.&lt;/strong&gt; A small global holdout (often a few percent in org folklore) never enters a set of experiments so you can measure &lt;strong&gt;cumulative&lt;/strong&gt; drift vs “everyone in everything.” That answers a different question than any single A/B — and it is easy to forget when overlap is the default pitch.&lt;/p&gt;

&lt;p&gt;Overlapping remains the right default when interactions are rare, metrics are robust, and you can detect collisions. Layers are the circuit breaker when the product is one shared UI.&lt;/p&gt;

&lt;h2 id=&quot;topology-does-not-fix-missing-exposure&quot;&gt;Topology does not fix missing exposure&lt;/h2&gt;

&lt;p&gt;Neither overlapping nor layers save the science if you never fire “user saw treatment T.” Sticky assignment, offline defaults, and no config fetch on the critical path still rule — then choose topology. Without exposure, you will ship another pair of “wins” that only look good because the dashboards never saw the combination users actually got.&lt;/p&gt;

&lt;p&gt;For example, in a mail-shaped client, the dense-list + loud-CTA week is not a stats pedantry problem. It is a product incident caused by assignment topology. Fix the exclusion (or gate the ship) before you A/B a third chrome change on top.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.statsig.com/experiments/exploring-results/interaction-detection&quot;&gt;Interaction detection&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://research.google.com/archive/papers/Overlapping_Experiment_Infrastructure_More_Be.pdf&quot;&gt;Tang et al. — Overlapping Experiment Infrastructure (KDD 2010)&lt;/a&gt; — layers as mutually exclusive domains so more tests share traffic&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://experimentguide.com/&quot;&gt;Kohavi, Tang, Xu — Trustworthy Online Controlled Experiments&lt;/a&gt; — interactions, SRM, and why isolation is not free&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 18 Jul 2026 12:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/experiment-layers-vs-overlapping-mobile</link>
        <guid isPermaLink="true">https://changminpark.github.io/experiment-layers-vs-overlapping-mobile</guid>
        
        <category>Writing</category>
        
        <category>Android</category>
        
        
      </item>
    
      <item>
        <title>Tickets Where an Agent Makes You Slower</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;measured slowdown vs perceived speedup&lt;/strong&gt;, &lt;strong&gt;assistant vs agent&lt;/strong&gt;, or &lt;strong&gt;when not to use AI&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/&quot;&gt;METR: early-2025 AI and experienced open-source developer productivity&lt;/a&gt; — measured ~19% slowdown vs believing you were faster&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://dev.to/aws/5-things-to-avoid-when-working-with-ai-tools-5cld&quot;&gt;5 things to avoid when working with AI coding tools&lt;/a&gt; — scope, review burden, and context bloat&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/building-effective-agents&quot;&gt;Building effective agents (Anthropic)&lt;/a&gt; — prefer the simplest system that works; agents are not the default&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;negative-expected-value-is-allowed&quot;&gt;Negative expected value is allowed&lt;/h2&gt;

&lt;p&gt;Not every ticket deserves an agent. That sentence sounds obvious until you watch a team agent-wash a one-line string fix, a confidential design review, or a research spike with no acceptance criteria — then spend longer reviewing the diff than typing the change would have taken.&lt;/p&gt;

&lt;p&gt;Coding agents are leverage under the right distribution of tasks. Under the wrong one they are a tax: prompting, waiting, auditing, and undoing confident mistakes. The METR randomized study on experienced open-source developers (early 2025 tooling) found participants took about &lt;strong&gt;19% longer&lt;/strong&gt; with AI on familiar repos — while still &lt;em&gt;believing&lt;/em&gt; they were faster. Perception is not a ship metric. Expected value is.&lt;/p&gt;

&lt;h2 id=&quot;assistant-vs-agency&quot;&gt;Assistant vs agency&lt;/h2&gt;

&lt;p&gt;It helps to separate two products that share a chat box:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Mode&lt;/th&gt;
      &lt;th&gt;What it is&lt;/th&gt;
      &lt;th&gt;Failure cost&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Assistant&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Completions, local refactors, explain-this-stack-trace&lt;/td&gt;
      &lt;td&gt;Usually contained; you stay in the loop&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Agency&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Multi-file goals, PR bots, “just fix CI overnight”&lt;/td&gt;
      &lt;td&gt;Diffs and comments you must still own&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;An &lt;strong&gt;assistant&lt;/strong&gt; speeds mechanical work when you can verify line-by-line. &lt;strong&gt;Agency&lt;/strong&gt; needs a clear goal, a navigable repo, and a verifier — or it externalizes judgment while leaving you accountable. I wrote about &lt;a href=&quot;/android-compose-ai-assistant&quot;&gt;assistant-style Compose UI&lt;/a&gt; as a &lt;strong&gt;UI/IME pattern&lt;/strong&gt;: streaming bubbles, keyboard insets, stable list keys. This one is about when &lt;em&gt;you&lt;/em&gt; should refuse agency on a ticket. Different layer; same word. Do not confuse a helpful panel with an unsupervised worker.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  ticket[Ticket] --&amp;gt; ev{Expected value?}
  ev --&amp;gt;|high| agent[Agent with checks]
  ev --&amp;gt;|low| hand[Do it yourself]
  ev --&amp;gt;|unclear| spike[Time-boxed spike only]
  agent --&amp;gt; verify[Tests / review]
  spike --&amp;gt; decide[Promote or stop]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Default is not “always agent.” Route by expected value, then verify.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;negative-ev-ticket-shapes&quot;&gt;Negative-EV ticket shapes&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unclear product intent.&lt;/strong&gt; “Make it feel premium” has no falsifiable done. An agent will invent UI and you will argue with a mirror. Write acceptance first (&lt;a href=&quot;/planning-theater-vs-real-plan&quot;&gt;planning with teeth&lt;/a&gt;) or keep the work human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-line / known-locus fixes.&lt;/strong&gt; Typo in a string, wrong constant, obvious NPE with a stack frame pointing at one method. The prompting overhead exceeds the edit. Use the agent as autocomplete if you want — do not open a multi-tool investigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidential or political review.&lt;/strong&gt; Security-sensitive diffs, personnel issues, unreleased metrics, legal holds. Pasting context into a cloud agent can be a policy violation; even when allowed, you need a human who can swear to what shipped. Agency here is not speed — it is risk transfer theater.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Novel research with no oracle.&lt;/strong&gt; Greenfield spikes where you do not yet know what “correct” means. Agents optimize for plausible completeness. You need exploration notes, not a PR that pretends the design exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks where you are the expert and the suite is thin.&lt;/strong&gt; On mature code you know cold, review cost dominates. METR’s setting — experienced developers on their own large repos — is exactly where “AI will 10× me” is most likely to lie. Thin golden evals help configs (&lt;a href=&quot;/agent-eval-not-a-demo&quot;&gt;ship bar ≠ demo&lt;/a&gt;); they do not erase the review tax on every ticket.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - if explaining the task to the agent takes longer than doing it, you already have your answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;dont-agent-wash-everything&quot;&gt;Don’t agent-wash everything&lt;/h2&gt;

&lt;p&gt;Agent-washing is the organizational failure mode: every Jira ticket gets an agent run because leadership bought seats, or because “AI-first” is a performance narrative. Symptoms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;PRs with huge unrelated refactors the human did not ask for&lt;/li&gt;
  &lt;li&gt;Review queues full of bot comments nobody owns (&lt;a href=&quot;/bot-commented-on-pr-nobody-owns&quot;&gt;public wrongness&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Engineers who cannot find the one-shot test command because the map was never written (&lt;a href=&quot;/monorepo-navigable-to-agents&quot;&gt;navigable monorepos&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seat utilization is not productivity. Track outcomes you already care about: cycle time to green CI, revert rate, review latency. If those worsen while agent usage climbs, you are paying for theater.&lt;/p&gt;

&lt;h2 id=&quot;when-agency-is-positive-ev&quot;&gt;When agency is positive EV&lt;/h2&gt;

&lt;p&gt;Use agents hard when the work is &lt;strong&gt;bounded, checkable, and boring at scale&lt;/strong&gt;: mechanical API migrations across many call sites, test scaffolding with a compiler as verifier, summarizing a failing CI log into a hypothesis you then validate, drafting a first-pass patch inside a module whose commands are documented. Pair with tests and path bans. Keep humans on intent, architecture, and anything that must be true in production — not on typing every rename.&lt;/p&gt;

&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h2&gt;

&lt;p&gt;Refuse agency on unclear, tiny, confidential, or oracle-free tickets. Keep assistants for mechanical help you can audit in seconds. Measure yourself against wall-clock and reverts, not against how busy the sidebar looks. The mature move is not “AI everywhere” — it is knowing when the expected value is negative and closing the chat.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://arstechnica.com/ai/2025/07/study-finds-ai-tools-made-open-source-software-developers-19-percent-slower/&quot;&gt;Ars Technica summary of the METR study&lt;/a&gt; — review/prompt/wait overhead eating coding-time savings&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/bot-commented-on-pr-nobody-owns&quot;&gt;The bot commented on your PR and nobody owns it (this site)&lt;/a&gt; — review-queue tax from agent-washed tickets&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/monorepo-navigable-to-agents&quot;&gt;Making a monorepo navigable to agents (this site)&lt;/a&gt; — missing command maps that turn agency into wandering&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 08 Jul 2026 12:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/when-agents-make-you-slower</link>
        <guid isPermaLink="true">https://changminpark.github.io/when-agents-make-you-slower</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>On-Device Agents Without the Mini Fantasy</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;on-device LLMs&lt;/strong&gt;, &lt;strong&gt;edge vs cloud inference&lt;/strong&gt;, or &lt;strong&gt;agent harness loops&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://arxiv.org/html/2409.00088v1&quot;&gt;On-Device Language Models: A Comprehensive Review&lt;/a&gt; — why edge-cloud hybrids dominate wish lists (latency, privacy, cost)&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://code.claude.com/docs/en/hooks&quot;&gt;Claude Code hooks&lt;/a&gt; — stop conditions and verifiers; the loop shape does not change on-device&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/overview&quot;&gt;Strengthen guardrails (Anthropic)&lt;/a&gt; — approvals and least privilege still bound tool blast radius&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://langfuse.com/docs/evaluation/overview&quot;&gt;Langfuse — Evaluation overview&lt;/a&gt; — golden tasks beat a single local demo&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt; — run a small local model with an OpenAI-compatible API shape (ops literacy, not a 70B goal)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;you-do-not-need-a-rack-to-learn-agents&quot;&gt;You do not need a rack to learn agents&lt;/h2&gt;

&lt;p&gt;Every few months the feed invents a new reason to buy hardware: a Mac Mini forever-on host, a 70B at home, an NPU story that implies your laptop is obsolete. For learning how agents fail — false “done,” fat tool menus, rubber-stamp approvals, demo-only evals — that shopping list is a distraction. &lt;strong&gt;On-device and edge are wiring literacy&lt;/strong&gt;: latency budgets, what stays on the phone, what works offline. They are not a prerequisite for understanding harnesses.&lt;/p&gt;

&lt;h2 id=&quot;what-actually-changes-on-the-device&quot;&gt;What actually changes on the device&lt;/h2&gt;

&lt;p&gt;Move the weights next to the sensors and three product constraints get louder:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Constraint&lt;/th&gt;
      &lt;th&gt;Cloud-shaped habit&lt;/th&gt;
      &lt;th&gt;On-device / edge habit&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Latency&lt;/td&gt;
      &lt;td&gt;Round trip + queue&lt;/td&gt;
      &lt;td&gt;Local tokens; UI can stay interactive&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Privacy&lt;/td&gt;
      &lt;td&gt;Ship context upstream&lt;/td&gt;
      &lt;td&gt;Keep PII / mailbox-adjacent text local when you can&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Offline&lt;/td&gt;
      &lt;td&gt;Spinner or error&lt;/td&gt;
      &lt;td&gt;Degraded path that still does something honest&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A mail-style assistant that suggests a reply subject while the radio is dead only works if something local can run. That is a real product reason. It is not the same claim as “I cannot study agents until I own a 70B.”&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  phone / laptop                 cloud (optional)
  ┌─────────────────────┐        ┌──────────────────┐
  │ small model / NPU   │        │ frontier model   │
  │ tools + policy      │───────▶│ heavy tasks      │
  │ approvals + logs    │◀───────│ when policy OK   │
  └─────────────────────┘        └──────────────────┘
         harness loop identical either side
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Placement of weights changes; the loop (goal → act → observe → stop) does not.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-does-not-change&quot;&gt;What does not change&lt;/h2&gt;

&lt;p&gt;Whether the brain is Claude in the cloud or a 7–8B via Ollama on a laptop:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Stop conditions&lt;/strong&gt; still need exit codes, not chat tone (&lt;a href=&quot;/agent-done-but-ci-red&quot;&gt;CI as done&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Approvals&lt;/strong&gt; still gate irreversible tools; local does not mean trusted (&lt;a href=&quot;/agent-trust-boundaries&quot;&gt;trust boundaries&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Evals&lt;/strong&gt; still need golden tasks you can re-run after a model bump (&lt;a href=&quot;/agent-eval-not-a-demo&quot;&gt;not a demo&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tool menus&lt;/strong&gt; still punish kitchen-sink defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weaker local models make bad judgment &lt;em&gt;more&lt;/em&gt; obvious. That is useful for labs. It is a poor excuse to skip the harness and blame parameter count. The day you move a policy check onto the device — “never upload draft body without an explicit toggle” — you will care more about where the gate lives than about whether the next token came from Sonnet or a quantized 8B.&lt;/p&gt;

&lt;h2 id=&quot;a-sane-learning-order&quot;&gt;A sane learning order&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;Cloud API + a hand-rolled loop (fake tools, fake CI) until “done ≠ green” is muscle memory&lt;/li&gt;
  &lt;li&gt;Thin tools and skills; measure thrash when you deliberately add junk&lt;/li&gt;
  &lt;li&gt;Optional: same prompts against a small local model to feel latency and quality cliffs&lt;/li&gt;
  &lt;li&gt;On-device / edge as a &lt;strong&gt;product&lt;/strong&gt; topic when you have a latency, privacy, or offline requirement — not as gear acquisition&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where people overspend. A 7–8B on a laptop already teaches timeout budgets, context limits, and “the model agreed but the tool never ran.” A Mini hosting a 70B mostly teaches thermals and electricity bills. Only one of those lessons transfers to shipping an Android feature that must not upload draft text.&lt;/p&gt;

&lt;p&gt;The order is the point, not the silicon. Hardware becomes worth buying at the moment a latency, privacy, or offline requirement forces the weights local — and by then the question you are answering is which gate moves onto the device, not which parameter count fits in RAM.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://agent-flux.github.io/&quot;&gt;AgentFlux — privacy-preserving on-device agentic systems&lt;/a&gt; — local orchestration with selective cloud collaboration&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://petronellatech.com/blog/edge-first-ai-agents-offline-private-frontline-ready/&quot;&gt;Edge-first AI agents (Petronella)&lt;/a&gt; — offline / private patterns without treating hardware as the curriculum&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/building-effective-agents&quot;&gt;Building effective agents (Anthropic)&lt;/a&gt; — start simple; add complexity when measurement demands it&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 27 Jun 2026 09:15:00 +0000</pubDate>
        <link>https://changminpark.github.io/on-device-agents-without-the-mini-fantasy</link>
        <guid isPermaLink="true">https://changminpark.github.io/on-device-agents-without-the-mini-fantasy</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>Making a Monorepo Navigable to Agents</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;AGENTS.md&lt;/strong&gt;, &lt;strong&gt;rules&lt;/strong&gt;, &lt;strong&gt;skills&lt;/strong&gt;, or &lt;strong&gt;context engineering&lt;/strong&gt; (what the agent can “see” in a repo) are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://cursor.com/docs/rules.md&quot;&gt;Cursor docs — Rules and AGENTS.md&lt;/a&gt; — root and nested agent instructions; glob-scoped project rules&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot;&gt;Effective context engineering for AI agents (Anthropic)&lt;/a&gt; — curate high-signal tokens; avoid stuffing the whole monorepo into every turn&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://codersera.com/blog/agents-md-vs-claude-md-vs-cursor-rules-comparison-2026/&quot;&gt;AGENTS.md vs CLAUDE.md vs Cursor rules&lt;/a&gt; — portable repo docs vs always-on editor rules&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://cursor.com/help/customization/skills&quot;&gt;Cursor — Skills&lt;/a&gt; — on-demand workflows (verify commands, “add a screen”) without a 10k-token root file&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;agents-fail-on-layout-not-iq&quot;&gt;Agents fail on layout, not IQ&lt;/h2&gt;

&lt;p&gt;Watch an agent on a large Android tree long enough and a pattern shows up. It invents a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:app:assemble&lt;/code&gt; flavor that does not exist. It “fixes” a crash by editing files under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build/&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;generated/&lt;/code&gt;. It runs the root unit-test task that the team abandoned two years ago because it takes forty minutes and flakes. Then it declares done. The model was not uniquely dumb that day — &lt;strong&gt;the repo was illegible&lt;/strong&gt; to tools that only know what you wrote down.&lt;/p&gt;

&lt;p&gt;Intelligence without a map wastes tokens. The job is to make the monorepo &lt;strong&gt;reachable&lt;/strong&gt;: one-shot commands, explicit forbidden paths, module boundaries in prose short enough to load — not a 10k-token novel nobody maintains.&lt;/p&gt;

&lt;p&gt;This pairs with how we think about &lt;a href=&quot;/cicd-jenkins-to-screwdriver&quot;&gt;CI for Android&lt;/a&gt;: if humans need a pinned JDK and a real pipeline command, agents need the same, discoverable in one hop.&lt;/p&gt;

&lt;h2 id=&quot;what-navigable-means&quot;&gt;What “navigable” means&lt;/h2&gt;

&lt;p&gt;An agent (or a new hire) should answer these without archaeology:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Question&lt;/th&gt;
      &lt;th&gt;Bad answer&lt;/th&gt;
      &lt;th&gt;Good answer in-repo&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;How do I unit-test module X?&lt;/td&gt;
      &lt;td&gt;“Gradle somewhere”&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./gradlew :feature-mail:testDebugUnitTest&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;What is generated?&lt;/td&gt;
      &lt;td&gt;Silence&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*Generated*&lt;/code&gt;, Room/Schema folders — do not edit&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Where does feature Y live?&lt;/td&gt;
      &lt;td&gt;Search the universe&lt;/td&gt;
      &lt;td&gt;Module map: feature → Gradle path → owners&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;What must never ship from chat?&lt;/td&gt;
      &lt;td&gt;Vibes&lt;/td&gt;
      &lt;td&gt;Secrets paths, Play keys, internal metric names&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;If those answers live only in Slack lore, every agent session relearns them wrong.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
  root[AGENTS.md — commands + bans]
  root --&amp;gt; feat[feature-mail/AGENTS.md]
  root --&amp;gt; shared[core-ui/AGENTS.md]
  root --&amp;gt; rules[&quot;.cursor/rules — glob-scoped&quot;]
  feat --&amp;gt; cmd[&quot;./gradlew :feature-mail:testDebugUnitTest&quot;]
  shared --&amp;gt; ban[Do not edit build/ or generated]
  rules --&amp;gt; compose[Compose rules on ui/**]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Thin root instructions; nested playbooks and glob rules for local truth — not one giant always-on essay.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;one-shot-commands-beat-narrative&quot;&gt;One-shot commands beat narrative&lt;/h2&gt;

&lt;p&gt;Put the commands you actually run in CI near the top of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; (or equivalent). Prefer copy-paste lines over paragraphs:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Module unit tests (PR gate shape)&lt;/span&gt;
./gradlew :feature-mail:testDebugUnitTest

&lt;span class=&quot;c&quot;&gt;# Static analysis the team enforces&lt;/span&gt;
./gradlew :feature-mail:detekt

&lt;span class=&quot;c&quot;&gt;# Do not: ./gradlew test  (root suite — slow, not the PR gate)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Agents pattern-match on executable snippets. A sentence that says “run the appropriate module tests” invites improvisation. The same discipline belongs in skills and runbooks for recurring workflows: “add a Compose screen in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:feature-X&lt;/code&gt;” should list files, packages, and the verify command — &lt;a href=&quot;https://medium.engineering/making-ai-write-android-code-our-way-a-practical-guide-to-agent-skills-4e7b085d8e50&quot;&gt;Medium Engineering’s Android skills writeup&lt;/a&gt; makes that case well for team playbooks.&lt;/p&gt;

&lt;h2 id=&quot;generated-folders-look-editable&quot;&gt;Generated folders look editable&lt;/h2&gt;

&lt;p&gt;To a model, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FooDto_Impl.kt&lt;/code&gt; is just another Kotlin file with a helpful name. Your rules must say &lt;strong&gt;do not edit generated sources&lt;/strong&gt; and name the directories. Pair that with harness checks if you evaluate agents: a golden task that fails when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build/&lt;/code&gt; appears in the diff (&lt;a href=&quot;/agent-eval-not-a-demo&quot;&gt;eval as ship bar&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Also call out:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local.properties&lt;/code&gt; / signing material — read-only or out of bounds&lt;/li&gt;
  &lt;li&gt;Screenshot and goldens folders — update only via the approved test task&lt;/li&gt;
  &lt;li&gt;Vendored or generated GraphQL/Apollo sources — regenerate, do not hand-patch&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;agents--rules-without-a-10k-novel&quot;&gt;AGENTS / rules without a 10k novel&lt;/h2&gt;

&lt;p&gt;Always-on context is a budget. A 10k-token root file that restates Clean Architecture will be half-ignored and half-overfitted. Split by job:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; — setup, global bans, canonical commands, link to module map&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Nested &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; — per feature or package conventions that apply when working in that subtree&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Glob-scoped rules&lt;/strong&gt; (e.g. Cursor &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mdc&lt;/code&gt;) — Compose, ViewModel, or test patterns that attach only when matching files are in play&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;On-demand skills&lt;/strong&gt; — multi-step runbooks loaded when the task matches, not on every chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of the split is that the model gets the &lt;em&gt;right&lt;/em&gt; playbook at the &lt;em&gt;right&lt;/em&gt; time instead of one always-on essay. Cursor’s docs for &lt;a href=&quot;https://cursor.com/docs/rules.md&quot;&gt;rules and AGENTS.md&lt;/a&gt; match that shape: portable repo instructions plus scoped overrides.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - if a paragraph has not prevented a real wrong edit in the last quarter, cut it; agents do not need your manifesto.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;module-boundaries-on-paper&quot;&gt;Module boundaries on paper&lt;/h2&gt;

&lt;p&gt;Large mail-style clients are graphs of Gradle projects. Write a short module map: feature name, Gradle path, what it may depend on, what must not depend back. Agents routinely “help” by introducing a dependency cycle or reaching into another team’s internals because the graph was implicit. You do not need a full architecture book — a table and a “preferred public API packages” line save more thrash than another essay on MVVM.&lt;/p&gt;

&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h2&gt;

&lt;p&gt;Make the monorepo legible the way you make CI honest: one-shot commands, named forbidden paths, thin always-on rules, deeper playbooks on demand. Agents are not lost because they lack a bigger model; they are lost because the map was never checked in. Fix the map first — then judge the model.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.engineering/making-ai-write-android-code-our-way-a-practical-guide-to-agent-skills-4e7b085d8e50&quot;&gt;Making AI write Android code our way&lt;/a&gt; — AGENTS.md + skills on an Android codebase; move weight into scoped rules&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.romanticode.com/blog/cursor-rules-vs-agents-md/&quot;&gt;Cursor rules vs AGENTS.md&lt;/a&gt; — portable commands/gates vs editor-scoped behavior&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 12 Jun 2026 12:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/monorepo-navigable-to-agents</link>
        <guid isPermaLink="true">https://changminpark.github.io/monorepo-navigable-to-agents</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
      <item>
        <title>Docker and Kubernetes: How Backend Servers Actually Run</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;containers&lt;/strong&gt;, &lt;strong&gt;Pods&lt;/strong&gt;, &lt;strong&gt;Services&lt;/strong&gt;, or &lt;strong&gt;the hostname the phone calls&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.docker.com/get-started/docker-overview/&quot;&gt;Docker overview&lt;/a&gt; — images as the repeatable process the phone never runs itself&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/overview/&quot;&gt;Kubernetes overview&lt;/a&gt; — how a fleet of those images stays up&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/&quot;&gt;Pods&lt;/a&gt; — the running process(es) behind one replica&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot;&gt;Service&lt;/a&gt; — stable name/IP the app actually calls&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/deployment/&quot;&gt;Deployments&lt;/a&gt; — replica count and rolling updates&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;the-phone-never-runs-docker-it-calls-something-that-does&quot;&gt;The phone never runs Docker. It calls something that does.&lt;/h2&gt;

&lt;p&gt;A mail or chat Android client talks HTTPS to a hostname baked into the build (or fetched from config). That hostname is not “Kubernetes” and not “a Docker daemon.” It is a &lt;strong&gt;backend process&lt;/strong&gt; — usually several of them — packaged as images and kept running across machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; is how that process becomes a repeatable image: same bits on a laptop, in CI, and in staging. &lt;strong&gt;Kubernetes&lt;/strong&gt; is how a fleet of those images stays up: how many copies, how they roll, what name the app calls, what happens when a node dies. Build with one; run the production shape with the other.&lt;/p&gt;

&lt;p&gt;For a mobile service, the useful question is not whale vs helm. It is &lt;strong&gt;which backend jobs the app depends on&lt;/strong&gt;, and which of those jobs live in the cluster.&lt;/p&gt;

&lt;h2 id=&quot;the-mobile-facing-backend-is-several-jobs&quot;&gt;The mobile-facing backend is several jobs&lt;/h2&gt;

&lt;p&gt;The app’s critical path is mostly one kind of process. The cluster is not.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Kind&lt;/th&gt;
      &lt;th&gt;What it does for the mobile app&lt;/th&gt;
      &lt;th&gt;How it usually runs&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;API / BFF&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;HTTPS the app actually calls. &lt;strong&gt;BFF&lt;/strong&gt; (&lt;em&gt;Backend for Frontend&lt;/em&gt;) is an API shaped for one client — e.g. a mobile BFF that aggregates list/send/auth so the phone does not fan out to five services&lt;/td&gt;
      &lt;td&gt;Replicas behind a stable name; roll out gradually&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Downstream services&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Mailbox, identity, attachments — the BFF fans out here&lt;/td&gt;
      &lt;td&gt;Same cluster or another; the phone should not know&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Worker&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Send, index, push fan-out, thumbnails — work that must not sit on the tap&lt;/td&gt;
      &lt;td&gt;Scale on queue lag, not on HTTP QPS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Job / Cron&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Backfill, cleanup, replay failed sends&lt;/td&gt;
      &lt;td&gt;Run to completion; not a forever replica&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Agent&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Node collector (metrics, logs, traces) — e.g. a Datadog agent talking to Datadog’s intake&lt;/td&gt;
      &lt;td&gt;One per node (or a sidecar); &lt;strong&gt;outbound&lt;/strong&gt;, not a hostname the app calls&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Docker’s job is the same for all of them: &lt;strong&gt;this image is the process&lt;/strong&gt;. Kubernetes’s job differs by shape: a Deployment + Service for anything the phone hits; workers without a public hostname; Jobs that exit; agents as a DaemonSet so every machine is observed.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
  phone[Mobile app] --&amp;gt;|HTTPS| edge[Service / ingress]
  edge --&amp;gt; bff[Mobile BFF / API pods]
  bff --&amp;gt; ds[Downstream services]
  bff --&amp;gt; q[Queue]
  q --&amp;gt; w[Worker pods]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The app calls one name. Kubernetes keeps BFF replicas behind it; workers and downstream services do the rest of the product. Node agents (not shown) scrape those boxes — they are not on the request path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For example, in a mail app, opening the inbox should be BFF → mailbox metadata → a payload the list can paint. Tapping send should return fast; SMTP and push fan-out belong on a worker. That split is why &lt;a href=&quot;/request-path-should-never-wait&quot;&gt;the request path should never wait&lt;/a&gt; is an ops shape as much as an API slogan. A &lt;a href=&quot;/fat-vs-chatty-apis-cellular&quot;&gt;fat mobile payload&lt;/a&gt; is cheaper on cellular when the BFF does the join &lt;strong&gt;in the cluster&lt;/strong&gt;, not on the radio.&lt;/p&gt;

&lt;h2 id=&quot;how-docker-and-kubernetes-show-up-in-that-path&quot;&gt;How Docker and Kubernetes show up in that path&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Same image, three places.&lt;/strong&gt; CI &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build&lt;/code&gt;s the BFF, tags it, pushes a registry. A developer runs the same image with Compose (BFF + Redis + stub auth) and points a debug APK at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10.0.2.2&lt;/code&gt;. Staging and prod pull the tag onto N pods. When “works in Compose” and “fails on device against staging” disagree, the image is often fine — ingress, secrets, and real dependencies are not in the laptop YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The name the app already has.&lt;/strong&gt; Kubernetes puts a Service (and usually ingress/TLS) in front of BFF pods so the APK does not chase pod IPs. Rolling a Deployment replaces pods gradually. For a minute, some phones hit the new tag and some hit the old one. Aggressive client retries turn that into a hang; the cluster looks “mostly green.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environments are different fleets.&lt;/strong&gt; Namespaces or clusters are how staging vs prod exist. A dogfood build pointed at the wrong Service looks like an Android sync bug. The backend change was a base URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workers are the rest of the product.&lt;/strong&gt; They share the cluster and often the same repo as the BFF, with a different command. They must not sit on the hostname the phone uses. Scale them on queue depth. If send is slow, look at worker lag before rewriting the compose screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents watch the fleet.&lt;/strong&gt; A node agent — Datadog, Fluent Bit, a Prometheus exporter — is another container, usually one per machine, shipping telemetry to an intake. Scaling the BFF to 20 replicas does not mean 20 collectors. If the agent cannot reach intake, dashboards go dark while the app still gets 200s. Useful, easy to over-index on; it is not the mobile API.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - Docker answers “what binary is this backend process?” Kubernetes answers “how many, where, and under what name does the app call it?” For a mobile service, start with the BFF hostname, then workers, then collectors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The APK is a client of that hostname. Docker and Kubernetes are how the servers behind it get built and kept alive — not a choice the phone makes, and not a rivalry between two logos.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/&quot;&gt;DaemonSet&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://samnewman.io/patterns/architectural/bff/&quot;&gt;Backends for Frontends&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/request-path-should-never-wait&quot;&gt;Request path should never wait (this site)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/fat-vs-chatty-apis-cellular&quot;&gt;Fat vs chatty APIs on cellular (this site)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/bff-three-clients-same-aggregation&quot;&gt;BFF — three clients, same aggregation (this site)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 05 Jun 2026 14:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/docker-and-kubernetes-backend-servers</link>
        <guid isPermaLink="true">https://changminpark.github.io/docker-and-kubernetes-backend-servers</guid>
        
        <category>Writing</category>
        
        <category>Distributed</category>
        
        
      </item>
    
      <item>
        <title>Wrong Chunk, Confident Answer</title>
        <description>&lt;details class=&quot;post-prereq&quot;&gt;
  &lt;summary&gt;Prerequisites&lt;/summary&gt;
  &lt;div class=&quot;post-prereq__body&quot;&gt;
    &lt;p class=&quot;post-prereq__hint&quot;&gt;Read these first if &lt;strong&gt;RAG&lt;/strong&gt;, &lt;strong&gt;chunking&lt;/strong&gt;, &lt;strong&gt;embeddings&lt;/strong&gt;, or &lt;strong&gt;knowledge bases&lt;/strong&gt; are new.&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.pinecone.io/learn/chunking-strategies/&quot;&gt;Chunking strategies (Pinecone)&lt;/a&gt; — how you slice the corpus; wrong boundaries → wrong top-&lt;em&gt;k&lt;/em&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/what-is/retrieval-augmented-generation/&quot;&gt;What is RAG? (AWS)&lt;/a&gt; — retrieve from an authoritative corpus, then generate&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2005.11401&quot;&gt;Lewis et al. — RAG for Knowledge-Intensive NLP Tasks&lt;/a&gt; (NeurIPS 2020) — original paper that named the pattern&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents&quot;&gt;Effective context engineering for AI agents (Anthropic)&lt;/a&gt; — retrieved (or repo) context competes for the same attention budget&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/prescriptive-guidance/latest/retrieval-augmented-generation-options/what-is-rag.html&quot;&gt;Understanding RAG (AWS Prescriptive Guidance)&lt;/a&gt; — embeddings, vector store, retrieve → augment → generate&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;h2 id=&quot;the-wiki-answered-it-was-last-years-policy&quot;&gt;The wiki answered. It was last year’s policy.&lt;/h2&gt;

&lt;p&gt;A teammate asks the internal assistant how long retention is for deleted drafts. The bot cites three passages, names a doc title, and answers with a round number. Legal’s real page was updated last quarter. The chunks that ranked highest were an old FAQ and a neighboring section about &lt;em&gt;sent&lt;/em&gt; mail. Nobody lied in the prompt. &lt;strong&gt;Retrieval ranked the wrong slice of a knowledge base&lt;/strong&gt;, and generation did what generation does: sound finished.&lt;/p&gt;

&lt;p&gt;That is the everyday failure mode for &lt;strong&gt;RAG&lt;/strong&gt; — retrieval-augmented generation. The terms are inseparable in practice: a &lt;strong&gt;knowledge base&lt;/strong&gt; is the corpus you maintain (wikis, runbooks, PDFs, ticket dumps); &lt;strong&gt;RAG&lt;/strong&gt; is the pattern that &lt;em&gt;searches&lt;/em&gt; that corpus at query time, stuffs passages into context, and lets the model write. Lewis et al. popularized the recipe in 2020; production teams rediscover that the generator is only as honest as the top-&lt;em&gt;k&lt;/em&gt; you feed it.&lt;/p&gt;

&lt;h2 id=&quot;knowledge-base-first-then-the-pipeline&quot;&gt;Knowledge base first, then the pipeline&lt;/h2&gt;

&lt;p&gt;Call the asset what it is before you buy an embedding product.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Piece&lt;/th&gt;
      &lt;th&gt;Job&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Knowledge base&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Owned documents with freshness, permissions, and a clear “source of truth”&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Index / chunks&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;How you slice and embed those docs for search&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Retriever (+ optional rerank)&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Which chunks enter the prompt&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Generator&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;The LLM that answers &lt;em&gt;conditioned on&lt;/em&gt; those chunks&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  q[Question] --&amp;gt; r[Retrieve from KB]
  r --&amp;gt; c[Top-k chunks]
  c --&amp;gt; g[Generate answer]
  g --&amp;gt; a[Reply + citations]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;Figure 1. RAG is search-then-write over a knowledge base — not a smarter model that “remembers” your wiki.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the KB is a graveyard of contradictory Confluence pages, RAG will amplify the loudest stale page. Ownership matters as much as embeddings: who retires superseded runbooks, who can publish, and whether the index lags the wiki by hours or by quarters. Chunking that splits a definition from its exception produces the confident wrongness that RAG failure taxonomies keep rediscovering: boundary errors, off-topic hits, and distractors that dominate the prompt while the answer-bearing span sits one chunk away.&lt;/p&gt;

&lt;p&gt;For example, in a mail app help center, “deleted drafts” and “deleted messages” often live in adjacent articles. A fixed-size split can put the retention number in chunk A and the product caveat (“drafts vs sent”) in chunk B. The retriever returns A because the query shared more tokens with the FAQ title. The model cites A and invents confidence. Users experience a policy answer; you have a ranking bug.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt; - debug the ranked passages before you blame the model. If the gold paragraph never entered the prompt, no prompt tweak fixes the product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;two-products-people-confuse&quot;&gt;Two products people confuse&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docs / support Q&amp;amp;A&lt;/strong&gt; needs a maintained knowledge base and a retrieval stack you can evaluate: Did the right passage appear? Can the user open the cite? Stale sync and bad ACLs are product bugs, not “LLM vibes.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coding agents&lt;/strong&gt; already live inside a different knowledge base: the repository. For “which Gradle task gates PRs?” or “where is the Compose list adapter?”, the honest path is usually &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rg&lt;/code&gt;, a module map, and thin &lt;a href=&quot;/monorepo-navigable-to-agents&quot;&gt;AGENTS / skills&lt;/a&gt; — the same discipline as &lt;a href=&quot;/agent-forgot-the-constraint&quot;&gt;context that must stay durable&lt;/a&gt;. Standing up a vector index of every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.kt&lt;/code&gt; file because “we need RAG” often buys approximate neighbors when exact path search was the requirement. Similarity search shines on prose handbooks; it is a poor substitute for a checked-in verify command.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Need&lt;/th&gt;
      &lt;th&gt;Prefer&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Policy / FAQ / handbook answers with citations&lt;/td&gt;
      &lt;td&gt;KB + RAG (and freshness SLO)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;“Where is this symbol / module / verify command?”&lt;/td&gt;
      &lt;td&gt;Repo tools + navigable monorepo&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Mixed: “how does our send pipeline work?” in prose runbooks&lt;/td&gt;
      &lt;td&gt;RAG over &lt;em&gt;owned&lt;/em&gt; architecture docs — not raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build/&lt;/code&gt; trees&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent must not invent APIs&lt;/td&gt;
      &lt;td&gt;Skills and rules in the harness, not a similarity search over Slack&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Embeddings are great at “find something &lt;em&gt;like&lt;/em&gt; this paragraph.” They are mediocre at “open the file the build actually uses.” Treat that as an architecture choice, not a fashion cycle.&lt;/p&gt;

&lt;h2 id=&quot;what-to-measure-when-chat-looks-green&quot;&gt;What to measure when chat looks green&lt;/h2&gt;

&lt;p&gt;Demos fail the same way coding agents fail when chat says done and CI is red (&lt;a href=&quot;/agent-done-but-ci-red&quot;&gt;harness verifiers&lt;/a&gt;): the UI is fluent while the gate is missing.&lt;/p&gt;

&lt;p&gt;For a KB + RAG path, cheap checks beat vibes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Retrieval hit&lt;/strong&gt; — for a golden question set, is the answer-bearing chunk in top-&lt;em&gt;k&lt;/em&gt;?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Freshness&lt;/strong&gt; — does the index lag the wiki by hours or by quarters?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Citation honesty&lt;/strong&gt; — does the cited doc actually support the sentence, or is it decorative?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Generation-only scores hide “right document, wrong page” failures that show up in long manuals and mail-style help centers: the retriever found the family of pages and still missed the paragraph that changed last release.&lt;/p&gt;

&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap-up&lt;/h2&gt;

&lt;p&gt;A knowledge base is the corpus you own; RAG is retrieve-then-generate over it. They belong in one mental model because the popular bug is joint: &lt;strong&gt;wrong chunk in, confident answer out.&lt;/strong&gt; Build RAG when answers must track a living document set with provenance. For coding agents, invest in a legible repo and harness context first — and only add retrieval when the question is genuinely “search the handbook,” not “find the module.”&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://tianpan.co/blog/2026-04-10-rag-chunking-problem-not-solved&quot;&gt;Why the Chunking Problem Isn’t Solved&lt;/a&gt; — boundary splits and naive fixed-size chunks in production&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aclanthology.org/2026.trustnlp-main.27/&quot;&gt;A Systematic Taxonomy of Failure Modes in RAG&lt;/a&gt; — retrieval vs fusion vs generation failures&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aclanthology.org/2026.eacl-long.147/&quot;&gt;Classifying Errors in RAG Systems&lt;/a&gt; — missed retrieval, low relevance, partial evidence&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 30 May 2026 11:00:00 +0000</pubDate>
        <link>https://changminpark.github.io/wrong-chunk-confident-answer</link>
        <guid isPermaLink="true">https://changminpark.github.io/wrong-chunk-confident-answer</guid>
        
        <category>Writing</category>
        
        <category>Agents</category>
        
        
      </item>
    
  </channel>
</rss>
