<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Kun Chen]]></title><description><![CDATA[Solo builder. Former L8 engineer at Meta, Microsoft, Atlassian.]]></description><link>https://blog.kunchenguid.com</link><image><url>https://substackcdn.com/image/fetch/$s_!F_N4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dd8a5-3dd1-446e-ba26-89b40c69b868_800x800.jpeg</url><title>Kun Chen</title><link>https://blog.kunchenguid.com</link></image><generator>Substack</generator><lastBuildDate>Mon, 04 May 2026 11:37:47 GMT</lastBuildDate><atom:link href="https://blog.kunchenguid.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Kun Chen]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[kunchenguid@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[kunchenguid@substack.com]]></itunes:email><itunes:name><![CDATA[Kun Chen]]></itunes:name></itunes:owner><itunes:author><![CDATA[Kun Chen]]></itunes:author><googleplay:owner><![CDATA[kunchenguid@substack.com]]></googleplay:owner><googleplay:email><![CDATA[kunchenguid@substack.com]]></googleplay:email><googleplay:author><![CDATA[Kun Chen]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[How I built a starry night in TUI]]></title><description><![CDATA[Twinkling stars, a moon strip, with a hand-rolled ANSI diff renderer]]></description><link>https://blog.kunchenguid.com/p/how-i-built-a-starry-night-in-tui</link><guid isPermaLink="false">https://blog.kunchenguid.com/p/how-i-built-a-starry-night-in-tui</guid><dc:creator><![CDATA[Kun Chen]]></dc:creator><pubDate>Tue, 28 Apr 2026 22:03:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9eOs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been building <a href="https://github.com/kunchenguid/gnhf">gnhf</a>, a CLI that orchestrates coding agents to get work done overnight. The name stands for &#8220;good night, have fun&#8221;, so the TUI leans into the bedtime metaphor: a centered status panel sitting under a slow-twinkling starfield, with a moon strip tracking each iteration.</p><p>I wanted the background to feel alive without ever stealing attention from the actual run state.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;0692d439-fa29-4ce4-a465-14205985cc97&quot;,&quot;duration&quot;:null}"></div><p>I ended up implementing the renderer with no TUI framework, no &#8220;small game engine&#8221; - just a small cell grid, a seeded random number generator, and an ANSI diff at 5 FPS.</p><p>This post walks through how it works. The whole renderer is a few hundred lines of TypeScript and you can read it in <a href="https://github.com/kunchenguid/gnhf/blob/main/src/renderer.ts">src/renderer.ts</a> and <a href="https://github.com/kunchenguid/gnhf/blob/main/src/utils/stars.ts">src/utils/stars.ts</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Solo builder. I share practical field notes about my agentic engineering workflows and experience building agentic systems. Former L8 engineer at Meta, Microsoft, Atlassian.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>Why no framework</h2><p>I actually started with Ink. It&#8217;s the obvious choice for a Node TUI - React mental model, flexbox layout, good ergonomics. For a static panel it would have been fine.</p><p>The problem showed up the moment I added the starfield. Ink re-renders through React&#8217;s reconciler: every animation tick walks a virtual DOM, diffs it, and hands the result to Yoga for layout. For a 120x80 grid where ~30 cells change per frame, that&#8217;s a lot of machinery to move a handful of characters. I could see the CPU spike on every tick, and on slower terminals the redraw lagged behind the 200ms tick.</p><p>The actual problem is very simple. The TUI is a fixed grid of cells - no scrolling, no input widgets, no focus, no flexbox. What I needed was a 2D <code>Cell[][]</code> buffer, a function that diffs two of them, and an ANSI emitter for the diff. That&#8217;s about 100 lines, it has zero per-frame allocation overhead beyond the buffer itself, and it scales with <em>changed</em> cells instead of total cells. At 5 FPS with 30-cell deltas, the renderer is essentially free.</p><p>We shouldn&#8217;t assume a framework is needed before analyzing the problem we&#8217;re trying to solve. Frameworks should earn their way into our projects.</p><h2>Picking the star characters</h2><p>The first thing I tried was the obvious one: <code>*</code>. It looked terrible. Asterisks are loud, they sit on the baseline, and a screen full of them reads as code, not sky.</p><p>What I actually wanted was the visual weight of a real night sky - mostly faint dots, a few brighter accents, nothing that competes with the text. I went through the Unicode block of misc symbols and settled on a small palette, weighted toward the quietest characters:</p><pre><code><code>const STAR_CHARS = [
  "&#183;",
  "&#183;",
  "&#183;",
  "&#183;",
  "&#183;",
  "&#183;",
  "&#10023;",
  "&#8902;",
  "&#8902;",
  "&#8902;",
  "&#176;",
  "&#176;",
] as const;</code></code></pre><p>The duplication is the weighting. When I pick a character with <code>Math.floor(rand() * STAR_CHARS.length)</code>, half the stars come out as middle dots, a quarter as four-pointed stars, the rest split between the bright <code>&#10023;</code> and the small ring <code>&#176;</code>. That ratio is what makes it read as a sky instead of a pattern.</p><p>A few things that mattered:</p><ul><li><p>All single-width characters. Wide graphemes wreck a fixed cell grid - one stray emoji and every column to its right shifts.</p></li><li><p>All visually centered in their cell. <code>*</code> sits low; <code>&#183;</code> and <code>&#8902;</code> sit in the middle and don&#8217;t fight the line height.</p></li><li><p>No characters that look like punctuation in context. <code>.</code> and <code>'</code> would have been disastrous next to the prompt text.</p></li></ul><h2>Animating the stars</h2><p>Real stars don&#8217;t blink in unison. The cheapest way to fake that is to give every star its own clock.</p><p>When I generate the field, each star gets a random phase offset and a random period somewhere between 10 and 25 seconds:</p><pre><code><code>stars.push({
  x,
  y,
  char: STAR_CHARS[charIdx],
  phase: rand() * Math.PI * 2,
  period: 10_000 + rand() * 15_000,
  rest,
});</code></code></pre><p><code>rest</code> is the state the star sits in most of the time - mostly <code>bright</code>, sometimes <code>dim</code>, occasionally <code>hidden</code>. The hidden ones are important: they&#8217;re empty cells that occasionally blink into view, which is what stops the field from looking static.</p><p>The animation itself is a tiny state machine driven by wall-clock time. Each star is in its rest state for ~95% of its period, then runs through a brief blink envelope:</p><pre><code><code>export function getStarState(star: Star, now: number): StarState {
  const t =
    ((now % star.period) / star.period + star.phase / (Math.PI * 2)) % 1;
  if (t &gt; 0.05) return star.rest;
  if (star.rest === "bright" || star.rest === "hidden") {
    const opposite = star.rest === "bright" ? "hidden" : "bright";
    if (t &gt; 0.0325) return "dim";
    if (t &gt; 0.0175) return opposite;
    return "dim";
  }
  if (t &gt; 0.025) return "bright";
  return "dim";
}</code></code></pre><p>The shape is <code>dim &#8594; opposite &#8594; dim &#8594; rest</code>. A bright star fades down before it disappears; a hidden star fades up before it shows. That little three-step ramp is the difference between &#8220;twinkling&#8221; and &#8220;flickering&#8221;. Sharp on/off transitions look like rendering bugs.</p><p>Three states map cleanly to ANSI styles, no truecolor needed:</p><pre><code><code>function starStyle(state) {
  if (state === "bright") return "bold";
  if (state === "dim") return "dim";
  return "normal";
}</code></code></pre><p><code>bold</code> and <code>dim</code> are SGR 1 and SGR 2, supported by every terminal I care about. Hidden stars render as a literal space.</p><p>One detail worth calling out: the star positions come from a seeded PRNG (pseudo-random number generator - a function that produces a deterministic sequence of &#8220;random&#8221; numbers from a starting seed). I used a Park-Miller LCG, which is one multiply and one modulo per call:</p><pre><code><code>let s = seed;
const rand = () =&gt; {
  s = (s * 16807 + 0) % 2147483647;
  return s / 2147483647;
};</code></code></pre><p>Determinism matters here - if the field regenerated freshly on every frame, the stars would jump around. Seeding it means the same terminal size always produces the same sky. <code>Math.random()</code> would have worked for the randomness, but it isn&#8217;t seedable in Node, so a few lines of LCG was the simpler answer.</p><h2>Avoiding the main content</h2><p>Another interesting problem was making sure the stars never overlapped the actual content we need to display.</p><p>The naive approach is to render stars under everything and overdraw. That works visually but it&#8217;s wasteful, and it creates a flicker risk if the diff order is wrong.</p><p>Instead, I split the screen into three regions and only generate stars where they&#8217;re allowed:</p><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;      top stars  (full width)       &#9474;
&#9474;                                    &#9474;
&#9474; side  &#9474;   content panel   &#9474; side   &#9474;
&#9474; stars &#9474;                   &#9474; stars  &#9474;
&#9474;       &#9474;   (no stars)      &#9474;        &#9474;
&#9474;                                    &#9474;
&#9474;     bottom stars (full width)      &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</code></code></pre><p>The content panel is a fixed <code>CONTENT_WIDTH</code>. Above and below it, stars get the full terminal width. To either side, only the margin gets stars. The frame builder stitches them together row by row:</p><pre><code><code>for (let i = 0; i &lt; contentRows.length; i++) {
  const left = renderSideStarsCells(sideStars, i, 0, sideWidth, now);
  const center = centerLineCells(contentRows[i], CONTENT_WIDTH);
  const right = renderSideStarsCells(
    sideStars,
    i,
    terminalWidth - sideWidth,
    sideWidth,
    now,
  );
  frame.push([...left, ...center, ...right]);
}</code></code></pre><p>Three independent star fields - top, bottom, sides - each with their own seed. The side field gets generated at full terminal width but <code>placeStarsInCells</code> only emits stars whose <code>x</code> falls inside the requested column range, so a star that would have landed under the panel just doesn&#8217;t exist.</p><p>The win here is that there&#8217;s no z-ordering, no occlusion test, no overdraw. Each cell in the final frame has exactly one writer. When the panel grows or shrinks (the layout drops sections when the terminal is short), the regions recompute and the stars follow. No leaks, no half-overwritten characters.</p><h2>Diffing frames</h2><p>5 FPS doesn&#8217;t sound like much, but a 120x80 terminal is 9,600 cells. Repainting all of them every 200ms gives you a visible flicker on slow terminals and burns bandwidth over SSH.</p><p>So the renderer keeps the previous frame in memory, builds the next one as a 2D array of <code>Cell</code> objects, and only emits ANSI for cells that actually changed:</p><pre><code><code>export function diffFrames(prev: Cell[][], next: Cell[][]): Change[] {
  const changes: Change[] = [];
  const rows = Math.min(prev.length, next.length);
  for (let r = 0; r &lt; rows; r++) {
    const prevRow = prev[r];
    const nextRow = next[r];
    const cols = Math.min(prevRow.length, nextRow.length);
    for (let c = 0; c &lt; cols; c++) {
      const n = nextRow[c];
      if (n.width === 0) continue;
      const p = prevRow[c];
      if (p.char !== n.char || p.style !== n.style || p.width !== n.width) {
        changes.push({ row: r, col: c, cell: n });
      }
    }
  }
  return changes;
}</code></code></pre><p><code>Cell</code> is the unit the whole renderer trades in:</p><pre><code><code>export interface Cell {
  char: string;
  style: Style; // "normal" | "bold" | "dim"
  width: number; // 1 normal, 2 wide, 0 continuation
}</code></code></pre><p>The <code>width: 0</code> field deserves a little more attention, because it&#8217;s where a real bug used to live.</p><p>The moon strip uses emoji - &#127761;&#127762;&#127763;&#127764;&#127765;&#127766;&#127767;&#127768;. In every modern terminal these render two columns wide, but JavaScript sees them as one grapheme made of one or two UTF-16 code units. If you naively put one moon in one cell of your buffer, your buffer thinks column 5 is occupied but the terminal has actually painted columns 5 and 6. Every cell to the right of the moon is now off by one, and the strip smears into the stars next to it.</p><p>The fix is to make the buffer agree with the terminal. A wide grapheme occupies two adjacent cells: the first holds the character with <code>width: 2</code>, the second is a placeholder with <code>width: 0</code> and an empty <code>char</code>. The diff skips continuation cells outright:</p><pre><code><code>if (n.width === 0) continue;</code></code></pre><p>That single line is what keeps the moon strip aligned. When the active moon advances one phase, the diff sees exactly one changed cell, emits one cursor move and one character, and the continuation slot stays untouched. Without it we&#8217;d either double-emit (paint the moon, then paint a stray space over its right half) or get half-rendered moons on frames where only one of the two cells &#8220;changed&#8221;.</p><p>The same mechanism handles CJK characters that show up in agent output. No special case for emoji, no special case for Chinese - one width field, one rule.</p><p>Emitting the diff is also boring on purpose. Move the cursor with <code>CSI row;col H</code>, set the style if it changed, write the character, advance the cursor cursor in memory:</p><pre><code><code>for (const { row, col, cell } of changes) {
  if (row !== cursorRow || col !== cursorCol) {
    result += `\x1b[${row + 1};${col + 1}H`;
  }
  if (cell.style !== currentStyle) {
    result += "\x1b[0m";
    if (cell.style === "bold") result += "\x1b[1m";
    else if (cell.style === "dim") result += "\x1b[2m";
    currentStyle = cell.style;
  }
  result += cell.char;
  cursorRow = row;
  cursorCol = col + cell.width;
}</code></code></pre><p>Two small optimizations carry most of the win:</p><ol><li><p><strong>Skip the cursor move if it&#8217;s already there.</strong> Adjacent changed cells become bare characters with no escape sequence in between.</p></li><li><p><strong>Don&#8217;t re-emit style codes that haven&#8217;t changed.</strong> A run of bright stars on the same row is one <code>\x1b[1m</code> followed by the characters.</p></li></ol><p>In practice a typical frame in steady state is maybe 10-30 cell changes - a handful of stars transitioning, the elapsed timer ticking, and the active moon advancing one phase. That&#8217;s a few hundred bytes per frame, comfortably under any terminal&#8217;s redraw budget.</p><p>The render loop is one line:</p><pre><code><code>this.interval = setInterval(() =&gt; this.render(), TICK_MS); // TICK_MS = 200</code></code></pre><p><code>render()</code> builds a frame, diffs it against the previous one, writes the diff, stores the new frame as the previous. That&#8217;s the whole engine.</p><h2>Wrapping up</h2><p>The starfield ended up looking really nice and gives me the feeling of zen.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9eOs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9eOs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9eOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;gnhf &#8212; Good Night, Have Fun&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="gnhf &#8212; Good Night, Have Fun" title="gnhf &#8212; Good Night, Have Fun" srcset="https://substackcdn.com/image/fetch/$s_!9eOs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!9eOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cd134ae-9d19-46b6-8361-3f0b4873d02c_1600x1200.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you want to poke at it, the code is at <a href="https://github.com/kunchenguid/gnhf">github.com/kunchenguid/gnhf</a>. The two files worth reading are <code>src/utils/stars.ts</code> (~80 lines) and <code>src/renderer-diff.ts</code> (~120 lines). Have fun!</p>]]></content:encoded></item><item><title><![CDATA[Org-Bench: Let’s Simulate the Org Charts Meme with Agents and See Who Wins]]></title><description><![CDATA[And prepare to have your mind blown.]]></description><link>https://blog.kunchenguid.com/p/org-bench-lets-simulate-the-org-charts</link><guid isPermaLink="false">https://blog.kunchenguid.com/p/org-bench-lets-simulate-the-org-charts</guid><dc:creator><![CDATA[Kun Chen]]></dc:creator><pubDate>Wed, 22 Apr 2026 19:40:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Dykj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You might have seen Manu Cornet&#8217;s org charts picture at some point. It became a popular meme because the stereotypes felt so accurate.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Dykj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dykj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dykj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg" width="627" height="627" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:627,&quot;width&quot;:627,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:136077,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://kunchenguid.substack.com/i/195064321?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Dykj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Dykj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4485a504-2e90-48ca-b52f-4bea51d6782d_627x627.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But have you ever wondered how these org structures would actually perform, if we put them to work and compare results side by side?</p><p>I say it&#8217;s time to get some data! We have agents now - let&#8217;s reproduce these org charts with agent teams and see how they work. Same input, same model, six org archetypes wired up as multi-agent topologies, each one asked to ship the same product. </p><p>Who will ship the best product in the shortest time? Let&#8217;s find out!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I share practical field notes about my agentic engineering workflows and experience building agentic systems. Former L8 engineer at Meta, Microsoft, Atlassian.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="pullquote"><p>Spoiler alert: I ended up spending hundreds of millions of tokens and these agent teams took days to run. The result really blew my mind. I can&#8217;t wait to share that with you.</p></div><h2>Setup</h2><p>Here&#8217;s the benchmark setup I designed for running the simulation. Everything from the benchmark harness to the result datasets are all open sourced at <a href="https://github.com/kunchenguid/org-bench">https://github.com/kunchenguid/org-bench</a>. </p><p><strong>The task:</strong> Build an in-browser spreadsheet in vanilla HTML/CSS/JS. The project brief is in <code>configs/brief.md</code>.</p><p><strong>The agent harness:</strong> We use <a href="https://github.com/sst/opencode">opencode</a> which is a CLI agent harness. The biggest thing I like about opencode is that it&#8217;s model-provider agnostic, which allows us to easily test different models down the road.</p><p><strong>One agent = one opencode session.</strong> Every agent is a separate <a href="https://github.com/sst/opencode">opencode</a> session running in its own subprocess. Every agent and the judge all run on <strong>openai/gpt-5.4</strong>. Any output difference between topologies comes from topology, since the agent harness and models are all consistent.</p><p><strong>Per-run isolation.</strong> Every run gets a disposable sandbox. At the end of each topology run the whole directory gets wiped, so two topology runs never see each other&#8217;s work.</p><p><strong>Topology config.</strong> A topology is a plain TypeScript object: an array of agent names, a list of bidirectional edges (who can message whom), a named leader, a list of developers, a list of integrators (the agents allowed to merge PRs to main), and a culture definition string. The six configs all live in <code>configs/topologies/</code>.</p><p><strong>Inter-agent communication.</strong> Messages route through per-agent inboxes. The orchestrator enforces adjacency: If an agent tries to send to someone they don&#8217;t have an edge with, the message gets dropped. This is the whole &#8220;org structure&#8221; machinery - the edge list enforces the org structure.</p><p><strong>A round.</strong> Every agent wakes every round. Within a round all agents execute in parallel (each opencode session gets one prompt, runs tools, writes a JSON reply); the round only ends when the last one finishes or a safety timer fires. Rounds are sequential - round N+1 doesn&#8217;t start until round N is complete. We cap at a total of 28 rounds to avoid infinite rabbit holes.</p><p><strong>Per-round prompt.</strong> Each agent&#8217;s prompt is assembled by the orchestrator from: </p><ul><li><p>How many rounds remain </p></li><li><p>The agent&#8217;s persona - leader vs developer vs integrator </p></li><li><p>The team charter (everyone&#8217;s expectations, not just yours, so agents can infer what peers are working on) </p></li><li><p>The agent&#8217;s direct neighbors and their roles </p></li><li><p>The culture summary for that topology </p></li><li><p>The path to the brief </p></li><li><p>The agent&#8217;s current inbox messages </p></li><li><p>The agent-browser CLI reference </p></li><li><p>A required reply format: JSON with a <code>messages: [{to, tag?, content}]</code> array and an optional <code>summary</code></p></li></ul><p><strong>Git flow.</strong> Developers commit to their own per-agent branch, push, open a PR targeting <code>run/&lt;run-id&gt;/main</code> (or a staging branch under a sub-lead for Amazon/Oracle). Only integrators (defined per topology) can merge PRs; everyone else&#8217;s merge attempts bounce.</p><p><strong>Finalize.</strong> When the leader emits <code>THIS_IS_MY_FINAL_SUBMISSION</code>, the orchestrator considers the work done and sends the judge a single prompt asking it to drive the result app through <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a> like a user and return an 8-axis rubric JSON. We report the average score across the rubric. </p><p>The orchestrator is otherwise hands-off. No human in the loop, no scoring intervention, no retries of failed PRs.</p><h2>The results</h2><p>Without further ado, drumroll please... the final results are here!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Wj5f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Wj5f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 424w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 848w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 1272w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Wj5f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png" width="1402" height="1122" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1122,&quot;width&quot;:1402,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:216737,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://kunchenguid.substack.com/i/195064321?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Wj5f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 424w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 848w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 1272w, https://substackcdn.com/image/fetch/$s_!Wj5f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F868cc363-f111-469f-a31b-170f5d72e028_1402x1122.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The best way to understand the differences though is to dive into the details, and play with what they built yourself.</p><p>Now, let&#8217;s dive deeper into the super interesting details.</p><div><hr></div><h2>Apple (judge 3.00, 24 rounds, 7.20M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/apple/">kunchenguid.github.io/org-bench/apple</a></p><pre><code><code>      Alice   Ben   Carol   Dave
          \    |     |     /
           \   |     |    /
            \  |     |   /
             \ |     |  /
                Steve
             / |     |  \
            /  |     |   \
           /   |     |    \
          /    |     |     \
       Emma  Frank  Grace  Henry
</code></code></pre><h3>How this topology is set up</h3><p>Hub and spoke, to reflect what&#8217;s in the meme, which is of course not exactly how Apple the company works. </p><p>Steve is the leader, the sole integrator, and the single communication hub. </p><p>Eight workers (Alice, Ben, Carol, Dave, Emma, Frank, Grace, Henry) each have one bidirectional edge to Steve. No worker can talk to any other worker directly. </p><p>Culture overlay: &#8220;taste bar + secrecy. Polish-first. Quality over schedule.&#8221; </p><p>This is the most centralized topology in the benchmark - compare to Facebook where every pair of agents has an edge, or Google where workers report up through middle integrators. Steve is the only one who sees the whole picture, and he&#8217;s the only one who can ship.</p><h3>What happened</h3><p>Steve decomposed the brief into eight clean subsystems on day one and held review authority over every integration. Twelve PRs landed on main. Shell, formula engine, clipboard, persistence, structural edits, visual polish, all merged individually. The app rendered beautifully.</p><p>Then the judge typed <code>=SUM(A1:A3)</code> and watched it render as the literal string <code>=SUM(A1:A3)</code>. The formula engine was merged. The shell was merged. But nobody wired the engine into the render layer properly. </p><p>The bottleneck was structural. Workers couldn&#8217;t negotiate seam contracts directly, so every scope collision had to be caught by Steve on review - Carol&#8217;s editing PR overlapping Ben&#8217;s shell, Alice&#8217;s Node-only engine export that no browser peer checked, Dave&#8217;s clipboard hardwiring the wrong engine export path. Steve was sending six to eight messages per round to eight different workers, also doing all conflict resolution and all live browser QA. </p><p>The one thing nobody was specifically looking at, the one thing a peer-review culture would have cross-checked - the render layer calling the formula engine - slipped through, because the only person looking for it was also the only person doing everything else.</p><p>Some conversations happened during the run:</p><p><strong>1. Carol gets rejected for a conflict.</strong> Round 4, Steve &#8594; Carol:</p><blockquote><p>&#8220;Request changes on <a href="https://github.com/kunchenguid/org-bench/pull/328">PR #328</a>. Ben&#8217;s shell is merged, so your PR now overlaps the shipped entry surface (<code>index.html</code>, <code>app.js</code>, <code>styles.css</code>) and exceeds your boundary. Rework on top of current <code>run/apple/main</code> so you deliver only single-cell interaction behavior wired into the existing shell.&#8221;</p></blockquote><p>Carol opened <a href="https://github.com/kunchenguid/org-bench/pull/328">#328</a> before Ben&#8217;s shell merged. Now her PR wanted to replace files Ben owned. Steve rejected the whole thing on scope, not content.</p><p>I&#8217;m sure this is far from how Apple actually works, but here we&#8217;re following the meme, and the hub and spoke topology prevents peer to peer coordination which inevitably results in conflicts like this. And even worse, Steve ended up being a bottleneck to rationalize all such conflicts.</p><p><strong>2. Grace&#8217;s visual polish is the one PR Steve merges without comment.</strong> Round 5, Steve &#8594; Grace:</p><blockquote><p>&#8220;Merged <a href="https://github.com/kunchenguid/org-bench/pull/330">PR #330</a>. The diff stayed inside <code>styles.css</code>, preserved Ben&#8217;s structure, and raised the polish bar cleanly. Shell presentation ownership remains with you for any follow-up visual defects I find in live QA.&#8221;</p></blockquote><p>Compare to every other sub-team&#8217;s review. Alice: rework. Carol: rework. Dave: rework. Henry: rework. Grace: merged, complimented, held on retainer for future defects.</p><p>Steve likes visual polish.</p><p><strong>3. Live QA finds the real product broken.</strong> Round 14, Steve &#8594; Carol:</p><blockquote><p>&#8220;Live bug report on your subsystem from the merged app in <code>run/apple/main</code>: open <code>index.html</code>, click cell <code>A1</code>, type <code>2</code>, then press <code>Enter</code>. Expected: commit <code>2</code> into <code>A1</code> and move selection to <code>A2</code>. Actual: the in-cell editor remains open inside <code>A1</code>, the name box stays on <code>A1</code>, and the commit does not complete. I reproduced this in the real browser.&#8221;</p></blockquote><p>Same round, Steve &#8594; Grace:</p><blockquote><p>&#8220;I started the live browser pass on current main. The first blocker is functional rather than visual: single-cell Enter commit is broken, so I&#8217;m holding visual polish notes until that core edit flow works in the real app.&#8221;</p></blockquote><p>This is round 14 of 24. The team has shipped eight subsystems and still can&#8217;t type a 2 into A1 and hit Enter. Steve did hold a high bar.</p><p><strong>4. Steve&#8217;s final call.</strong> Round 24, Steve &#8594; Grace:</p><blockquote><p>&#8220;I completed the final clean browser pass and did not find a concrete visual blocker that justifies another round. We are at ship quality on the current artifact.&#8221;</p></blockquote><p>The judge&#8217;s verdict on the same artifact: &#8220;the browser snapshot still exposed only cell addresses, not evaluated results; source inspection explains why: <code>app.js</code> <code>renderGridValues()</code> writes <code>state.cells[address]</code> directly, and the formula engine is not wired into UI rendering.&#8221;</p><p>Steve&#8217;s bar was very high. But relying on a single leader to catch all bugs was unrealistic and didn&#8217;t work well.</p><div><hr></div><h2>Amazon (judge 3.12, 13 rounds, 3.50M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/amazon/">kunchenguid.github.io/org-bench/amazon</a></p><pre><code><code>                  Jeff
                 /    \
              Alice    Ben
             /    \    / \
          Carol  Dave Frank Emma
                           /   \
                        Grace  Henry
</code></code></pre><h3>How this topology is set up</h3><p>Three-level tree with staging branches. </p><p>Jeff at the top, two tech leads (Alice and Ben) under him, each running their own subtree. Alice leads Carol and Dave. Ben leads Frank and a sub-sub-lead Emma, who runs her own two-person team (Grace and Henry). </p><p>Integrators: Jeff, Alice, Ben, Emma - but with a twist. Code doesn&#8217;t just flow into main. Each tech lead owns a staging branch (<code>run/amazon/Alice</code>, <code>run/amazon/Ben</code>, <code>run/amazon/Emma</code>), merges their subtree&#8217;s PRs into it, and then opens an integration PR upward. </p><p>Jeff is the only one who can merge to <code>run/amazon/main</code>. Grace and Henry&#8217;s work travels through three merges to reach main. Communication is hierarchical: workers only talk to their lead.</p><p>Culture overlay: &#8220;PR/FAQ writing + customer obsession + frugality.&#8221;</p><h3>What happened</h3><p>Amazon finished fastest of any topology - 13 rounds. The artifact looked good in isolation: real shell, real formulas, real undo, visible insert-row controls. But the score landed at 3.12 with specific feature gaps: reload persistence didn&#8217;t reload anything, and shift-range clear didn&#8217;t work across multiple cells. Both features were explicitly in the brief, and both were called out in Jeff&#8217;s own PR/FAQ on day one.</p><p>Two things happened. First, the three-layer hierarchy lost information in both directions. Ben told Emma one thing in round 5, a different thing in round 7, and Grace and Henry at the bottom of the tree never got re-briefed. When Grace opened a structural-edits PR with specific delete-row and delete-column controls, Emma blocked it claiming &#8220;Ben has it upstream.&#8221; The judge later confirmed exactly what Grace was building was missing from the shipped artifact. Information that would have crossed a peer edge in one hop had to traverse three layers, and it decayed.</p><p>Second, the PR/FAQ turned out to be Jeff&#8217;s chain-of-thought more than a team execution tool. It was referenced exactly twice in 165 messages - both by Jeff in round 1, to Alice and Ben, and never again. Zero citations by any of the eight other agents. Downstream decisions were made on informal claims (&#8221;Ben says he has it&#8221;) rather than against the PR/FAQ&#8217;s explicit feature list. Writing the customer story on day one helped Jeff think clearly. It didn&#8217;t help anyone else ship to it.</p><p><strong>1. Jeff&#8217;s opening PR/FAQ.</strong> Round 2, Jeff &#8594; Alice and Ben (abridged):</p><blockquote><p>&#8220;PR/FAQ: We are shipping a static in-browser spreadsheet that opens instantly from <code>file://</code> and feels production-ready on first click. Customers can type directly into a 26x100 grid, use a visible formula bar, evaluate formulas with references and ranges... Build the foundation customers feel immediately: crisp grid rendering, unmistakable single-cell selection plus rectangular range selection, edit mode behavior, formula bar parity, keyboard navigation, and persistence namespace support.&#8221;</p></blockquote><p>Every other topology dropped right into decomposition. Amazon wrote a press release first and tried to use it to guide execution. Reload persistence and range selection are right there in the opening sentences. They&#8217;re also exactly what the judge flagged broken 11 rounds later.</p><p><strong>2. The directive that reversed and never propagated.</strong> Round 5, Ben &#8594; Emma:</p><blockquote><p>&#8220;Keep Henry focused on fixing the formula-bar sync bug before you roll the broader interaction slice upward; once that is clean, your subtree can layer range selection and clipboard flows onto a stable foundation.&#8221;</p></blockquote><p>Round 7, two rounds later, Ben &#8594; Emma again:</p><blockquote><p>&#8220;Your subtree no longer needs to cover that ground; focus your upward work on the interaction gaps we still do not have on Ben, especially range selection and clipboard behavior.&#8221;</p></blockquote><p>Ben reversed himself cleanly. Emma updated her instructions to Grace and Henry. But she compressed both messages into her own framing, and by the time Grace was deciding what to build in round 10, &#8220;Ben has range selection and clipboard&#8221; had become the operative assumption. The original directive, the reversal, and the actual state of Ben&#8217;s branch were all different things, and only Ben knew which was current.</p><p><strong>3. Emma blocks Grace on a feature Ben didn&#8217;t actually have.</strong> Round 13, Emma &#8594; Grace on <a href="https://github.com/kunchenguid/org-bench/pull/437">PR #437</a>:</p><blockquote><p>&#8220;Ben has already moved his branch to 0fabd81 with browser-visible row and column insert/delete controls plus the supporting model behavior, so landing the same structural-edit surface here would duplicate upstream work instead of closing the next customer gap.&#8221;</p></blockquote><p>Grace had built specific delete-row and delete-column controls. Emma&#8217;s claim was inherited from Ben&#8217;s high-level status, not verified by reading Ben&#8217;s branch. The judge later: &#8220;the DOM snapshot shows only <code>+</code> row controls&#8221; - insert-only, no delete. Exactly what Grace was adding. In a mesh, Grace could have pinged Ben directly. In a tree, Emma&#8217;s translation of Ben&#8217;s claim is the only channel, and &#8220;Ben has structural edits&#8221; didn&#8217;t distinguish &#8220;insert&#8221; from &#8220;insert and delete.&#8221;</p><div><hr></div><h2>Facebook (judge 3.38, 20 rounds, 5.99M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/facebook/">kunchenguid.github.io/org-bench/facebook</a></p><pre><code><code>     Mark   Alice   Ben   Carol   Dave
        \    |      |      |     /
         \   |      |      |    /
    (full mesh: all 36 edges present)
         /   |      |      |    
        /    |      |      |     
     Emma  Frank  Grace   Henry
</code></code></pre><h3>How this topology is set up</h3><p>Full mesh. Nine agents, and every pair has a bidirectional edge. </p><p>Mark is the named leader but the adjacency gives him no structural advantage - everyone can reach everyone. </p><p>Every agent is both a developer and an integrator. Anyone can review and merge someone else&#8217;s PR. </p><p>Mark sets direction and removes blockers; he doesn&#8217;t gate-keep. Peers are expected to respond to review requests in the same round. </p><p>Culture overlay: &#8220;move fast. A merged imperfect change beats a perfect unmerged one.&#8221; This is the opposite extreme from Apple. No chokepoints, no single reviewer, and the only centralized authority is conventional, not enforced.</p><h3>What happened</h3><p>Facebook scored 3.38 with a quite polished UI. Then the judge entered 10, 20, 30 into A1, A2, A3 and asked for <code>=SUM(A1:A3)</code>. The answer was 40. The formula engine was wrong.</p><p>The mesh meant peers didn&#8217;t wait for permission. Alice unilaterally merged Ben&#8217;s spreadsheet foundation as the team&#8217;s baseline without Mark&#8217;s sign-off. When she couldn&#8217;t get to a review in a round, she reassigned it to Carol directly. Emma pulled trunk, saw her own undo/redo branch was superseded, and stood down without being told. </p><p>Bug reports routed peer-to-peer: Henry told Mark, Mark told Alice in the same round, Alice shipped the fix in the same round. No leader bottleneck, parallel decisions, same-round turnarounds. This is what people mean when they say a flat org &#8220;moves fast.&#8221;</p><p>It&#8217;s also what they mean when they say accountability &#8220;diffuses.&#8221; Late in the run, Alice and Ben jointly concluded that the remaining keyboard-typing bug was a test-harness artifact rather than a product bug - verified each other&#8217;s instrumentation, agreed on the call, stood down together. Nobody above them was asked to validate the conclusion. That same distributed-trust dynamic is why SUM(10, 20, 30) returned 40 and shipped. In a mesh, the answer to &#8220;who&#8217;s supposed to verify the math?&#8221; is everyone, which in practice means no one.</p><p><strong>1. Alice unilaterally picks the team&#8217;s baseline.</strong> Round 3, Alice &#8594; Ben, cc Mark:</p><blockquote><p>&#8220;Merged your PR: <a href="https://github.com/kunchenguid/org-bench/pull/390">#390</a>. I reviewed the diff, confirmed it targets <code>run/facebook/main</code>, and noted that the included workbook tests cover evaluation, recalculation, circular refs, and formula shifting. Merged so the team can build on the stronger spreadsheet baseline this round.&#8221;</p></blockquote><p>Alice had her own competing foundation PR. She reviewed Ben&#8217;s, concluded it was stronger, and merged it as the new trunk baseline. Mark was notified after the fact. In Apple this would have routed to Steve and cost rounds of review cycle. In Amazon it would have needed Jeff&#8217;s approval. In the mesh, two peers negotiated whose direction to adopt and the answer was binding.</p><p><strong>2. Emma self-disqualifies her own branch.</strong> Round 3, Emma &#8594; Mark, Dave, Carol:</p><blockquote><p>&#8220;I pulled the latest <code>origin/run/facebook/main</code> while working on undo/redo and found the merged mainline spreadsheet already has session history in <code>app.js</code> with 50-deep stacks plus <code>Cmd/Ctrl+Z</code>, <code>Cmd/Ctrl+Shift+Z</code>, and <code>Cmd/Ctrl+Y</code>. I&#8217;m not pushing my divergent duplicate branch because it is now based on the older scaffold and would just conflict with the merged shell.&#8221;</p></blockquote><p>Nobody told Emma to stand down. She checked her own work against the new trunk and concluded it was redundant. In a mesh peers can verify their own relevance. In a tree, someone upstream has to notice and tell them.</p><p><strong>3. Review reassigned directly between peers.</strong> Round 4, Alice &#8594; Carol:</p><blockquote><p>&#8220;Can you take over review ownership for Frank&#8217;s PR this round: <a href="https://github.com/kunchenguid/org-bench/pull/395">PR #395</a>? I had to use this turn to sync and repair my stale PR against the merged trunk. Please either merge it or send concrete change requests back to Frank this round.&#8221;</p></blockquote><p>Carol picked it up, verified the PR was already merged, and reported back. No leader involvement. Every integrator is fungible, so a peer can route around their own round-level constraint by pointing at another peer.</p><p><strong>4. Jointly accepting an unresolved bug.</strong> Round 19, Alice &#8594; Ben, followed by Ben &#8594; Alice:</p><blockquote><p>Alice: &#8220;I finished the instrumented debug pass on the clean local entrypoint. For the exact <code>click A1 -&gt; type 2</code> replay, the DOM log only recorded a <code>focusin</code> on <code>BUTTON:A1</code> and no subsequent <code>keydown</code>, <code>beforeinput</code>, or <code>input</code> events. That matches your hold-off instinct - the failing path looks like <code>agent-browser type</code> never delivers printable-input events to the grid button, so I&#8217;m not pushing the local direct-entry experiments as a PR.&#8221;</p><p>Ben: &#8220;Your instrumented clean-session result matches what I was converging on. Since the exact shared replay only records <code>focusin</code> on <code>BUTTON:A1</code> and no printable-input events afterward, I&#8217;m standing down on the local direct-entry experiments too instead of creating another low-confidence PR.&#8221;</p></blockquote><p>Two peers instrumented the same bug, compared notes directly, reached the same conclusion, and both stood down. This is the mesh working exactly as designed - co-authored technical judgment without leader arbitration. It&#8217;s also how a bug that the judge would later find real gets collectively accepted as a tool issue.</p><p><strong>5. The math was wrong and nobody owned the check.</strong> Post-run, from <code>docs/facebook/trajectory/analysis.json</code>:</p><blockquote><p>&#8220;The shipped build still mis-evaluated some basic formulas such as observed <code>SUM</code>, <code>AVERAGE</code>, and <code>CONCAT</code> outputs, so the run ended with a shipped integrated product and an unresolved gap between the team&#8217;s final acceptance picture and the judge&#8217;s formula-correctness result.&#8221;</p></blockquote><p>Every peer tested the formula bar. Every peer tested edit/undo/paste. Every peer saw the result &#8220;render&#8221; in a cell. Nobody checked whether the rendered value was arithmetically correct. The mesh&#8217;s virtue - distributed verification - was also the mechanism that spread correctness ownership so thin it disappeared.</p><div><hr></div><h2>Google (judge 3.62, 15 rounds, 5.84M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/google/">kunchenguid.github.io/org-bench/google</a></p><pre><code><code>                     Eric
                 /  /    \   \
              Alice Ben Carol Dave     &lt;- middle managers
                \ \  |  / /
       (all 16 middle -&gt; worker edges)
                / /   |   \ \
              Emma Frank Grace Henry   &lt;- workers
</code></code></pre><h3>How this topology is set up</h3><p>Two-layer bipartite. Eric at the top, four middle integrators (Alice, Ben, Carol, Dave), four workers (Emma, Frank, Grace, Henry). </p><p>Every middle integrator has edges to all four workers; workers have no edges to each other. Integrators: Eric plus all four middles. </p><p>Workers&#8217; role prompts say &#8220;every substantive change starts with a short design doc shared with connected middle integrators&#8221;; middles&#8217; prompts say &#8220;reviews design docs from connected workers, asks for data or metrics when claims are made, and merges only after consensus forms in the doc comments.&#8221; </p><p>Culture overlay: &#8220;design docs + data-driven consensus. Claims need data.&#8221;</p><h3>What happened</h3><p>Google got the top score (3.62) with 25 passing app-level checks and the widest working feature set of any topology. It finished in 15 rounds on 5.84M tokens - not particularly long, not particularly heavy.</p><p>The biggest contributing factor to the success was design-doc discipline, and it was actually doing work rather than just existing. </p><p>Eric&#8217;s established the rule that no substantive code merges until a design doc with a TDD plan and claim-to-check mapping has consensus - turned review from taste-based judgment into mechanical comparison. </p><p>Once a design was approved and landed, every subsequent PR could be evaluated against it. Ben rejected Henry&#8217;s later PR #360 not because it lacked a doc but because it diverged from the design that had already landed. Carol rejected Grace&#8217;s PR #356 by comparing her design doc to Emma&#8217;s approved convergence path. By round 12 Dave could name the exact remaining blocker by diffing the committed design against what was visible in the browser.</p><p>Four middle integrators reviewing in parallel against the same written criteria catches more than one Steve ever could. The docs weren&#8217;t there to check a process box. They were the yardstick every later decision measured itself against.</p><p><strong>1. The first block: a doc without a TDD plan isn&#8217;t a doc.</strong> Round 3, Alice &#8594; Henry on <a href="https://github.com/kunchenguid/org-bench/pull/353">PR #353</a>:</p><blockquote><p>&#8220;The architecture and execution order look reasonable, but before I can merge I need one doc update: add an explicit TDD plan for the first slices and map concrete acceptance checks to the major claims, especially around the workbook model, parser/evaluator, dependency recalculation, persistence namespace handling, and grid interactions.&#8221;</p></blockquote><p>Alice isn&#8217;t asking for more words. She&#8217;s asking for traceability: every architectural claim must be tied to a test that would fail if the claim breaks. Eric backs the block in round 4 with the principle: &#8220;We need doc consensus before substantive code lands.&#8221;</p><p><strong>2. Henry splits the PR. That unblocks the rest of the run.</strong> Round 6, Henry:</p><blockquote><p>&#8220;I split the doc review from the product code as requested. New doc-only PR: <a href="https://github.com/kunchenguid/org-bench/pull/358">#358</a>. It contains just <code>design-doc-henry-round1.md</code> with the architecture, TDD plan, and claim-to-check mapping. I also closed stale mixed-scope <a href="https://github.com/kunchenguid/org-bench/pull/353">PR #353</a>.&#8221;</p></blockquote><p>Three rounds of gating to get the doc right. Then the gate stays open for the rest of the run - every subsequent feature inherits the pattern. The upfront cost pays down across twelve more rounds of mechanical review.</p><p><strong>3. The approved design becomes the rejection yardstick.</strong> Round 8, Ben &#8594; Henry on <a href="https://github.com/kunchenguid/org-bench/pull/360">PR #360</a>:</p><blockquote><p>&#8220;I reviewed <a href="https://github.com/kunchenguid/org-bench/pull/360">PR #360</a> and ran tests; your 10 core tests pass. I am not merging it, though, because it duplicates formula-evaluation logic that is already shipped on main through <a href="https://github.com/kunchenguid/org-bench/pull/357">PR #357</a> instead of extending the product path that the app actually uses. That would split the architecture and create two sources of truth.&#8221;</p></blockquote><p>Henry&#8217;s doc existed. His tests passed. Ben still rejected him, because the code diverged from the <em>landed</em> design. This is what makes the gate a mechanism rather than ceremony. Once Emma&#8217;s design had won consensus and her code had landed, &#8220;matching the landed design&#8221; became the test, and reviewers could decide without arguing from taste.</p><p><strong>4. An architectural gap caught at design review, not at QA.</strong> Round 5, Alice on <a href="https://github.com/kunchenguid/org-bench/pull/354">PR #354</a> (PR comment):</p><blockquote><p>&#8220;UI only supports a single active cell and single selection; no rectangular range highlight, no <code>Shift+Click</code> or <code>Shift+Arrow</code> extension in the view, and <code>Delete</code>/<code>Backspace</code> clear is scoped to the active cell only, not the selection range.&#8221;</p></blockquote><p>Range selection was a brief requirement. Alice caught it missing at round 5, in design review, by reading the doc and the code together. Compare to Apple, where the equivalent bug (render layer not calling the formula engine) was never caught at all - Steve only did live QA, not doc review against the brief.</p><p><strong>5. By round 12 the doc names the exact remaining gap.</strong> Round 12, Dave:</p><blockquote><p>&#8220;I validated the final judged gap on merged <code>run/google/main</code>. Code search found no row or column insert-delete implementation beyond ordinary cell clearing, and the live browser snapshot shows only plain row labels and column labels with no discoverable structural-edit affordances. So the remaining blocker is explicit now: there is no user-facing row-column insert-delete action to exercise.&#8221;</p></blockquote><p>Dave isn&#8217;t guessing. He&#8217;s diffing the committed design against what&#8217;s actually rendering. By round 12 the acceptance criteria are so precise that the remaining gap has one sentence. The team closed it in the next PR.</p><div><hr></div><h2>Microsoft (judge 3.00, 15 rounds, 6.47M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/microsoft/">kunchenguid.github.io/org-bench/microsoft</a></p><pre><code><code>                         Bill
                      /       \
                 Diana   ===   Edward       &lt;- division heads
                /  |  \        /  |  \
             Alice Ben Carol Dave Emma Frank
             \__ Diana's __/ \__ Edward's __/
</code></code></pre><h3>How this topology is set up</h3><p>Two rival divisions plus a leader. Bill at top, with edges to Diana and Edward. Integrators: Bill, Diana, Edward. </p><p>Diana runs a division of three (Alice, Ben, Carol) with edges only to her team. Edward runs a parallel division of three (Dave, Emma, Frank) with edges only to his team. Diana and Edward have an edge to each other but their workers do not cross. </p><p>Both divisions are given overlapping scope by design. Bill&#8217;s prompt says he should &#8220;create urgency by playing the divisions off each other.&#8221; The losing division&#8217;s workers get redistributed to the winner. </p><p>Culture overlay: &#8220;two divisions fighting for survival. Stack-rank Ballmer-era energy.&#8221;</p><p>Bill runs this as a winner-take-all race. Diana and Edward each get the full brief, with contested surfaces on purpose, and Bill tells each of them that shipping a complete product first keeps their team intact.</p><h3>What happened</h3><p>The race incentive did exactly what you&#8217;d expect. Five workers opened competing foundation PRs in round 2 alone. The clipboard delta rebased four times across rounds 8, 9, 10, and 12 because Edward&#8217;s division kept landing work on the trunk while Diana was mid-review. Carol&#8217;s structural-edit work (PR #479, rebased to PR #485) passed Diana&#8217;s review and then died because Edward shipped PR #484 on the same surface first. Edward himself kept rebuilding clipboard locally across eight rounds without ever opening a PR - he wanted to land it as a single clean flag-plant, and the trunk kept moving out from under him.</p><p>This shows up in the tokens. Microsoft burned 6.47M tokens on 15 rounds, almost twice Amazon&#8217;s budget for a similar number of rounds. Conservatively, ~1.5M of those tokens went to work that was completely abandoned, superseded, or never left a worker&#8217;s local branch. That&#8217;s the measurable cost of the race.</p><p>It also shows up in the score. Microsoft landed at 3.00, tied for lowest, on three specific judge complaints. Reload persistence didn&#8217;t persist (the code existed, but no one validated end-to-end). Keyboard copy/paste sent the browser to a blank page (Frank reported the crash in round 8, Edward kept &#8220;fixing&#8221; it in his local branch across the next seven rounds and never opened a reviewable PR, the crash shipped). The UI had no visible insert/delete row or column controls (Carol&#8217;s engine logic shipped twice, but no UI affordance was ever added, because all UI work went to the contested shell PRs that stayed dirty). </p><p>Three acceptance-critical features in the brief, all uncontested, all absent from the final artifact. Both divisions were busy fighting each other for the visible surfaces.</p><p><strong>1. Bill plays the divisions against each other.</strong> Round 9, Bill &#8594; Edward:</p><blockquote><p>&#8220;Diana now has reviewed evidence that a clipboard branch already achieves the judge-critical relative-shifting behavior, so your division no longer has the inside lane by default.&#8221;</p></blockquote><p>Edward&#8217;s response was to ship clipboard faster. That&#8217;s what the incentive rewards: feature A already shipped by the rival, so double down on feature A. Feature B (reload persistence, uncontested) gets nobody&#8217;s attention.</p><p><strong>2. Carol&#8217;s verified structural-edits work dies to Edward&#8217;s flag-plant.</strong> Round 11, Edward &#8594; Bill:</p><blockquote><p>&#8220;Merged Emma&#8217;s structural-edit <a href="https://github.com/kunchenguid/org-bench/pull/484">PR #484</a> into <code>run/microsoft/main</code> this round so Diana cannot take the row/column insert-delete surface uncontested.&#8221;</p></blockquote><p>Diana to Carol, same round, on <a href="https://github.com/kunchenguid/org-bench/pull/485">PR #485</a>:</p><blockquote><p>&#8220;I verified PR 485&#8217;s behavior and the implementation itself looked sound, but main advanced again during review and landed structural edit semantics in <a href="https://github.com/kunchenguid/org-bench/pull/484">PR #484</a>, which made 485 dirty before I could merge it. Treat the structural lane as effectively closed unless Bill explicitly asks for comparison follow-up.&#8221;</p></blockquote><p>Carol shipped the same feature twice (PRs #479 and #485), Diana reviewed both, both were technically sound, both died because Edward&#8217;s division landed first on the same surface. The race rewards speed on contested ground. The cost is the other team&#8217;s verified work converting to waste.</p><p><strong>3. Frank&#8217;s crash report goes nowhere.</strong> Round 8, Frank &#8594; Edward:</p><blockquote><p>&#8220;Ran the focused browser proof on the live grid using the formula bar plus real keyboard copy/paste on the shared trunk build. I could set up <code>A1=2</code>, <code>A2=3</code>, and <code>B1==A1</code>, but after selecting <code>B1</code>, sending <code>Meta+C</code>, selecting <code>C1</code>, and sending <code>Meta+V</code>, the browser session collapsed to an empty page.&#8221;</p></blockquote><p>Frank filed a clean, concrete repro. Edward said the crash was &#8220;gone on Edward&#8221; in round 9 (his local branch). But Edward never opened a PR with the fix. He kept rebuilding clipboard locally across rounds 9, 10, 11, 12 as main moved under him, and by round 14 Bill declared final submission. The clipboard code that shipped was Ben&#8217;s PR #477 - which didn&#8217;t include the keyboard-path fix Frank had reported. The crash shipped.</p><div><hr></div><h2>Oracle (judge 3.25, 28 rounds, 4.36M tokens)</h2><p>Try it: <a href="https://kunchenguid.github.io/org-bench/oracle/">kunchenguid.github.io/org-bench/oracle</a></p><pre><code><code>                        Larry
                  /  /  /  |  \  \
                 /  /  /   |   \  \
              Alice Ben Carol Dave Quinn     &lt;- Quinn
             (legal)(sec)(priv)(accs)  |    (eng director)
             &lt;------ reviewers -----&gt;           / | \
                                               /  |  \
                                           Emma Frank Grace   &lt;- engineers
</code></code></pre><h3>How this topology is set up</h3><p>Hierarchical with a named gatekeeper layer. </p><p>Larry is the leader and the only agent that can merge to <code>run/oracle/main</code>. </p><p>Quinn is the engineering director - explicitly non-coding - who runs a three-engineer team (Emma, Frank, Grace) on a staging branch (<code>run/oracle/Quinn</code>), opens one integration PR upward when the staging branch is ready, and personally drives the composed app through <a href="https://github.com/vercel-labs/agent-browser">agent-browser</a> as QA. Integrators: Larry, Quinn.</p><p>Alice, Ben, Carol, Dave are dedicated reviewers, each locked to a single angle: Alice = legal, Ben = security, Carol = privacy, Dave = accessibility. Their role prompts explicitly forbid them from commenting outside their lane. Approvals go through a specific convention: each reviewer posts a PR comment whose first line starts with <code>APPROVED (&lt;angle&gt;):</code> or <code>BLOCKED (&lt;angle&gt;):</code>; Larry merges once all four <code>APPROVED</code> comments are present at the current head.  </p><p>Engineers don&#8217;t contact reviewers directly; Quinn mediates. </p><p>Culture overlay: &#8220;hierarchical, process-first, multi-angle review. Slowness is by design; bypassing is forbidden.&#8221; This is the only topology with dedicated reviewers and a hard approval gate.</p><h3>What happened</h3><p>Oracle ran the full round cap (28 rounds, longest in the benchmark) and the multi-angle review gate did roughly what you&#8217;d expect from a process-first org: it caught real lane-specific bugs, it took forever to clear, and it left actual product functionality untested.</p><p><strong>The review layer was helpful to a degree, but it was expensive.</strong> The four angle reviewers spent rounds 2-14 in pure standby while Quinn and the engineers built. Once the integration PR opened, Dave (accessibility) alone bounced it three times across three different heads, each bounce about a subtler Tab-key behavior than the last.</p><p><strong>The review layer didn&#8217;t care about whether the product worked.</strong> Carol (privacy) caught a real bug: <code>src/app.js</code> hardcoded the <code>localStorage</code> namespace instead of using the harness-injected run-scoped namespace. Dave (accessibility) caught a real ARIA grid-pattern violation: <code>Tab</code> was walking cells instead of exiting the widget per WCAG. </p><p>But nobody was watching for actual product value. The same PR that got three sign-offs and an eventual accessibility approval still had persistence that didn&#8217;t persist on reload, and still had the documented known issue that <code>click A1 -&gt; type "1" -&gt; Enter</code> didn&#8217;t commit into A1. </p><p>The judge, driving the app through browser like a real user, gave Oracle 2/4 on functional completeness. The internal red tapes didn&#8217;t act as a bar on product quality. It&#8217;s a set of compliance gates stacked on top of each other, and nobody owns the question &#8220;does this thing actually work?&#8221;</p><p><strong>1. Larry forces the ship.</strong> Round 12, Larry &#8594; Quinn:</p><blockquote><p>&#8220;I checked both PR lanes directly again and there is still no open PR in either. Parallelizing the fix is fine, but the next useful milestone is not more diagnosis - it is an actual PR. Get whichever of Frank or Emma has the first working minimal edit-flow patch to open immediately against <code>run/oracle/Quinn</code>, merge it, and then either proceed to integration if the browser path is green or open the integration PR with the known issue documented if this still slips. We cannot keep spending rounds with no review artifact.&#8221;</p></blockquote><p>This is the one thing that kept Oracle from looping forever. Without Larry pushing for a review artifact over a perfect artifact, Oracle would have shipped nothing.</p><p><strong>3. Quinn opens the integration PR with a documented known issue.</strong> Round 14, Quinn &#8594; Larry:</p><blockquote><p>&#8220;I opened the integration PR on the documented-known-issue path so review can start immediately: <a href="https://github.com/kunchenguid/org-bench/pull/503">PR #503</a>. It targets <code>run/oracle/main</code> from <code>run/oracle/Quinn</code>, includes the required labels, and documents the remaining blocker from my acceptance pass - the <code>click A1 -&gt; type 1 -&gt; Enter</code> flow is not yet a trustworthy visible commit-and-advance path. Please watch for the four angle signoff comments and merge once they are all present at the current head.&#8221;</p></blockquote><p>Note what Quinn did and didn&#8217;t do. He shipped a PR with a broken core interaction clearly labeled. Four specialized reviewers are about to look at this. None of them will block on <code>click A1 -&gt; type 1 -&gt; Enter</code> being broken, because it&#8217;s not in anyone&#8217;s lane.</p><p><strong>4. Carol catches a real privacy bug.</strong> Round 15, Carol on <a href="https://github.com/kunchenguid/org-bench/pull/503">PR #503</a>:</p><blockquote><p>&#8220;BLOCKED (privacy) on PR #503. Privacy blocker: <code>src/app.js</code> hardcodes <code>localStorage</code> namespace <code>oracle-sheet</code> instead of using the harness-provided run-scoped namespace, so saved cell contents and selection can collide across runs in the same browser profile. I posted the blocking PR comment with details.&#8221;</p></blockquote><p>This is exactly what you hope a privacy reviewer catches - a concrete cross-run data-hygiene bug that breaks an isolation assumption. The gate paid for itself on this one catch. Frank shipped a fix in the next round.</p><p><strong>5. Dave blocks three times on progressively subtler Tab behavior.</strong> Rounds 15, 20, and 24, same reviewer, same PR, same &#8220;lane&#8221;:</p><blockquote><p>Round 15: &#8220;the grid exposes every cell as its own tab stop instead of using a single focusable grid/roving-tabindex model, so <code>Tab</code> walks cell-by-cell across the full matrix.&#8221;</p><p>Round 20: &#8220;<code>Tab</code> still moves the active cell from <code>A1</code> to <code>B1</code>, so keyboard users are still traversing the grid cell by cell instead of exiting the grid.&#8221;</p><p>Round 24: &#8220;<code>Tab</code> still advances within the grid (<code>B1</code> -&gt; <code>C1</code>) instead of exiting the spreadsheet widget.&#8221;</p></blockquote><p>Three PR heads, three accessibility fixes from Frank, three increasingly narrow complaints about where Tab goes. Each bounce cost a full round-trip of fix + merge to staging + re-review. By the third bounce the argument was whether one specific focus-move edge case obeyed the ARIA grid pattern correctly. One reviewer with a narrow bar and no cross-check seems to become a bottleneck.</p><p><strong>6. The gate&#8217;s blind spot, post-merge.</strong> After the PR finally merged at round 27 (all four <code>APPROVED</code> comments present at head <code>cb24008</code>), the judge drove the shipped app through the browser like a user and scored it 2/4 on functional completeness. From the judge&#8217;s rationale on <a href="https://github.com/kunchenguid/org-bench/pull/503">PR #503</a>:</p><blockquote><p>&#8220;The main failure is persistence: reloading </p><p>http://127.0.0.1:54833</p><p> restored a blank sheet, losing all entered contents, so by the stated floor functional completeness cannot exceed 2. I also could not verify copy/paste relative-reference shifting or row/column insert-delete behavior in the UI; there were no visible insert/delete affordances, and a copy/paste attempt led to unstable behavior during capture.&#8221;</p></blockquote><p>Three review cycles, four specialized reviewers, and these gaps sailed through. Carol was focused on the storage <em>namespace</em>, not whether persistence actually round-tripped on reload. Dave was focused on the ARIA Tab model, not whether a user could type a value and see it commit. Alice and Ben approved on first pass and never looked back. The review gate was thorough in its lanes and completely silent on the thing a user would notice first.</p><div><hr></div><h2>What I take away from this</h2><p>Same model, same brief, same time budget. Six very different outcomes.</p><ul><li><p><strong>Apple</strong> care about polish but the hub-and-spoke structure made Steve the only person who could see the whole picture, which became a bottleneck and resulted in gaps.</p></li><li><p><strong>Amazon</strong> shipped fast and hit the finish line but with specific brief requirements unmet. The three-layer hierarchy caused information loss on the way up and the way down, and the PR/FAQ turned out to be the leader&#8217;s chain-of-thought rather than the team&#8217;s execution tool.</p></li><li><p><strong>Facebook</strong> shipped a beautifully polished spreadsheet where SUM(10,20,30) returned 40. The mesh let peers move fast without the leader, but also caused diffusion of responsibility.</p></li><li><p><strong>Google</strong> shipped the widest working feature set in the benchmark. Design-doc discipline turned review into mechanical comparison against approved criteria, so four middle integrators could catch in parallel what no single reviewer could catch serially.</p></li><li><p><strong>Microsoft</strong> shipped a broken product and wasted tokens doing it. Two rival divisions racing on contested surfaces duplicated their clipboard work four times and left the uncontested features (reload persistence, keyboard copy/paste, insert/delete UI) broken or absent.</p></li><li><p><strong>Oracle</strong> took the longest (28 rounds, longest in the benchmark) to ship a mid-pack product. Internal red tapes caused significant slow down yet didn&#8217;t help catch real product problems a customer would care about.</p></li></ul><p>I&#8217;m genuinely blown away by how different org structures and culture can have such a visible impact on their outcome, and how many interesting observations we can have by watching agents simulate human collaboration. </p><p>Full trajectories, PR comments, judge output, and shipped artifacts are all shared in <code>docs/&lt;topology&gt;/</code>. Every quote in this post is verbatim from <code>docs/&lt;topology&gt;/trajectory/messages.jsonl</code> or from the <a href="https://github.com/kunchenguid/org-bench/pulls?q=is%3Apr+is%3Aclosed+label%3Abenchmark-run">PRs raised by agents</a>.  </p><p>Feel free to go dig in yourself and see what else you&#8217;ll find. I&#8217;d be keen to hear your thoughts!</p>]]></content:encoded></item><item><title><![CDATA[Making a Polished TUI Demo Video Without a Video Editor]]></title><description><![CDATA[Recording, mocking, and polishing a terminal demo with off-the-shelf tools]]></description><link>https://blog.kunchenguid.com/p/making-a-polished-tui-demo-video</link><guid isPermaLink="false">https://blog.kunchenguid.com/p/making-a-polished-tui-demo-video</guid><dc:creator><![CDATA[Kun Chen]]></dc:creator><pubDate>Tue, 21 Apr 2026 16:42:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4Q-t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently put together a TUI demo gif for my <a href="https://github.com/kunchenguid/no-mistakes">no-mistakes</a> tool&#8217;s readme and came out of the process pretty happy with it: crisp text, a zoom on the key command, sensible pacing, about 700KB, and the whole thing regenerates with one <code>make</code> command.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4Q-t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4Q-t!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 424w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 848w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 1272w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4Q-t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif" width="1100" height="650" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:650,&quot;width&quot;:1100,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:692173,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://kunchenguid.substack.com/i/194938062?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4Q-t!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 424w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 848w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 1272w, https://substackcdn.com/image/fetch/$s_!4Q-t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F126f3f5f-1ec7-4376-bb65-216399d6d485_1100x650.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">no-mistakes TUI demo gif</figcaption></figure></div><p>I was a little surprised how far you can get with just a couple of off-the-shelf tools and some tuning. No video editor, no screen recording software, no manual export step. If you&#8217;re shipping a CLI or TUI and thinking about a readme gif, I figured the setup is worth writing up.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I&#8217;m a solo builder, previously L8 engineer at Meta, Microsoft, Atlassian. I share practical field notes about frontier agentic engineering.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Here&#8217;s how it works.</p><h2>The Stack</h2><p>Three tools, each doing one thing:</p><ul><li><p><code>vhs</code> drives the terminal and captures frames</p></li><li><p><code>ffmpeg</code> handles zoom, speedup, and color optimization</p></li><li><p><code>make</code> glues it together</p></li></ul><p>That&#8217;s the whole pipeline.</p><h2>VHS: Reproducible Script to Record Terminal Programs</h2><p><a href="https://github.com/charmbracelet/vhs">VHS</a> from Charm is the thing that makes this reproducible. You write a <code>.tape</code> file that describes terminal dimensions, env vars, and a sequence of <code>Type</code>/<code>Enter</code>/<code>Sleep</code> commands. VHS spins up a headless terminal, executes the script, and spits out a gif.</p><p>Here&#8217;s a snippet of my <code>demo.tape</code>:</p><pre><code><code>Set FontSize 50
Set Width 2750
Set Height 1625
Set Theme "Catppuccin Mocha"

Sleep 1s
Type "git push"
Sleep 1s
Type " no-mistakes"
Sleep 3s
Enter
Sleep 2s
Type "no-mistakes"
Sleep 3s
Enter

Sleep 9s            # wait for review step to surface findings
Sleep 2.5s          # linger on the approval screen
Type "f"            # press f to fix
Sleep 29s
Sleep 2s</code></code></pre><p>A few things worth calling out.</p><p><strong>Record at a huge resolution.</strong> The canvas is 2750x1625 at 50pt font. That&#8217;s way bigger than any terminal I actually use, and way bigger than the final gif. The main reason is to leave some headroom for zoom: later in the pipeline, ffmpeg crops a small region and upscales it for the intro zoom effect. If the source is low-res, that crop ends up pixelated. Recording big means I can zoom into any region and still get sharp output. Crisp text at the final output size is a nice bonus - downscaling 2750px to ~800px with a good filter keeps every character readable.</p><p><strong>Use </strong><code>Hide</code><strong> for offscreen setup.</strong> VHS has a <code>Hide</code> / <code>Show</code> pair that lets you run a setup block before the user-visible portion starts. In my tape, the <code>Hide</code> block creates a scratch git repo, initializes the tool&#8217;s config, sets up a bare upstream, and clears the screen. Not interesting to watch. Absolutely necessary for the demo to actually do something. <code>Show</code> kicks in and the recording begins.</p><p><code>Sleep</code><strong> values are hand-tuned.</strong> There&#8217;s no shortcut. I ran the tape, watched the output, bumped a number, ran again. This is the tedious part, but it&#8217;s also where the rhythm of the video comes from - the pauses are the difference between &#8220;watchable&#8221; and &#8220;what am I looking at.&#8221;</p><h2>Mock a Deterministic Demo</h2><p>One thing that comes up fast: if your tool does real work with real network calls, or real LLM agents, the recording is at the mercy of a stochastic system for something that needs to be identical every time. A review step that takes 30s today takes 45s tomorrow. Agents take different paths. Networks hiccup.</p><p>For <code>no-mistakes</code>, I added a demo mode behind an env var:</p><pre><code><code>Env NM_DEMO "1"</code></code></pre><p>Inside my program, that flag swaps out the real implementation for a canned mock. The TUI doesn&#8217;t know the difference - same step names, same log streaming, same approval flow, same step completion durations. The only thing that changes is what&#8217;s running underneath.</p><p>You don&#8217;t need this for every tool. If your CLI is deterministic and fast, skip it. But if your flagship flow takes minutes or talks to the outside world, you&#8217;ll want some version of it.</p><p>The key design decision, if there is one: <strong>the demo mode swap lives at the pipeline layer, not the UI layer</strong>. The TUI is identical between real and demo runs, which means the demo gif is also a low-key integration test. If the UI breaks, <code>make demo</code> shows it.</p><h2>Pacing: Real Time vs Displayed Time</h2><p>This is where it gets fun.</p><p>The real pipeline takes minutes. A review is maybe 30-45s. Tests can be a minute. CI is several minutes. Recording that is unusable.</p><p>But I also don&#8217;t want the TUI to show &#8220;Review (0.2s)&#8221; - that breaks the realism of the demo. The whole point is that it looks like a real run.</p><p>So every demo step carries two durations:</p><pre><code><code>&amp;demoStep{
    name:       types.StepReview,
    delay:      5 * time.Second,     // actually block this long
    displayDur: 45 * time.Second,    // report this to the TUI
    ...
}</code></code></pre><p>And the executor honors the override when reporting:</p><pre><code><code>durationMS := executionMS + time.Since(phaseStart).Milliseconds()
if durationOverrideMS &gt; 0 {
    durationMS = durationOverrideMS
}</code></code></pre><p>The UI cheerfully renders &#8220;Review - 45s&#8221; in the completed-step list, even though only 5 seconds of wall clock went by during recording.</p><p>The other half of pacing is <strong>log streaming</strong>. If you dump a wall of text in a single frame, the effect is jarring and unreadable. Spread the lines across the step&#8217;s duration instead:</p><pre><code><code>pause := total / time.Duration(len(lines))
if pause &lt; 50*time.Millisecond {
    pause = 50 * time.Millisecond
}
for i, line := range lines {
    if i &gt; 0 {
        demoWait(ctx, pause)
    }
    sctx.Log(line)
}</code></code></pre><p>So &#8220;Reviewing diff against main...&#8221; / &#8220;Analyzing changed files...&#8221; / &#8220;Checking for bugs...&#8221; appear at human-readable intervals. Same idea as a loading shimmer: it&#8217;s not about truth, it&#8217;s about communicating progress at the speed a viewer can follow.</p><h2>FFmpeg: The 20-Line Polish Pass</h2><p>This is the part that surprised me. I&#8217;d assumed &#8220;real&#8221; demo videos needed After Effects or at least something like iMovie for basic editing like zoom, transitions, and speed ramps. Turns out ffmpeg does all of it in a single filter chain.</p><p>VHS outputs a raw gif. Two ffmpeg passes turn it into the final gif and mp4.</p><p>Here&#8217;s the gif pass:</p><pre><code><code>ffmpeg -i demo_raw.gif -filter_complex "\
    [0:v]split[orig][zoom_src];\
    [zoom_src]crop=963:570:0:0,scale=1100:650:flags=lanczos[zoomed];\
    [orig]scale=1100:650:flags=lanczos[base];\
    [base][zoomed]overlay=0:0:enable='lt(t,4.04)',setpts=1.9*PTS,\
    split[s0][s1];\
    [s0]palettegen=max_colors=128[p];\
    [s1][p]paletteuse=dither=sierra2_4a\
" -r 10 -y demo.gif</code></code></pre><p>Three effects, stacked.</p><p><strong>Zoom-then-reveal.</strong> The first 4 seconds of the demo is the user typing <code>git push no-mistakes</code>, which is the whole pitch of the tool. Zooming in makes it unmissable. The filter splits the video into two streams, crops and upscales one (zoomed view of the top-left), and overlays it on the base stream only while <code>t &lt; 4.04s</code> via <code>enable='lt(t,4.04)'</code>. After that, the overlay is disabled and the full TUI reveals itself - which happens to be the moment the TUI actually launches. Visually it reads as &#8220;you typed this, now watch what happens.&#8221;</p><p><strong>1.9x speedup</strong> via <code>setpts=1.9*PTS</code>. Even with display durations clamped, the full demo runs about 53 seconds. Too long for a readme gif. 1.9x compresses it to about 28 seconds without anything feeling rushed, because the mock step pacing was tuned with this speedup in mind. You can (and should) tune your pacing and your speedup together as one loop.</p><p><strong>Palette optimization.</strong> <code>palettegen</code> samples the frames and picks 128 optimal colors, <code>paletteuse</code> applies them with Sierra2-4a dithering. Without this, the gif is either oversized or has ugly banding on text edges. With it, the final output sits around 700KB for a 28-second animation.</p><p>The mp4 pass is the same zoom and speedup filter chain, minus the palette dance, encoded to H.264. Twitter and most docs renderers prefer the mp4, readme uses the gif, both come out of the same source.</p><h2>The <code>make demo</code> Target</h2><p>All of it lives in one target:</p><pre><code><code>demo: build
    vhs demo.tape
    ffmpeg -i demo_raw.gif ... -y demo.gif
    ffmpeg -i demo_raw.gif ... -y demo.mp4
    rm -f demo_raw.gif</code></code></pre><p><code>make demo</code>. Gif updates, mp4 updates, intermediate file goes away. Runs in CI if I want. Produces the same output every time.</p><h2>Summary</h2><p>If you&#8217;re shipping a CLI or TUI, this is a really high leverage setup. My rough advice:</p><ol><li><p><strong>Use VHS, not screen recording.</strong> Scripted, deterministic, no cursor wobble.</p></li><li><p><strong>Record big.</strong> High resolution, large font. Downscale at the ffmpeg stage.</p></li><li><p><strong>Put </strong><code>Hide</code><strong>/</strong><code>Show</code><strong> around your setup.</strong> Your viewer doesn&#8217;t want to see <code>mktemp -d</code>.</p></li><li><p><strong>Tune pacing by ear.</strong> There&#8217;s no formula. Watch the output, adjust the sleeps, run again.</p></li><li><p><strong>Let ffmpeg do the flashy stuff.</strong> Zoom overlays, speed ramps, and palette optimization are all one filter chain away. No video editor required.</p></li><li><p><strong>If your tool is slow or non-deterministic, gate a mocked demo mode behind an env var.</strong></p></li></ol><p>An hour of work and a 20-line Makefile target gets you a demo that&#8217;s deterministic, easy to regenerate, and nice to look at. That&#8217;s a trade I&#8217;d happily make again, and hopefully this writeup saves you some of the figuring-out I had to do.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I&#8217;m a solo builder, previously L8 engineer at Meta, Microsoft, Atlassian. I share practical field notes about frontier agentic engineering.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How I Built a Reproducible Mac Setup with Nix]]></title><description><![CDATA[A pragmatic setup that lets me get any new Mac into working shape in seconds instead of spending a weekend reinstalling everything.]]></description><link>https://blog.kunchenguid.com/p/how-i-built-a-reproducible-mac-setup</link><guid isPermaLink="false">https://blog.kunchenguid.com/p/how-i-built-a-reproducible-mac-setup</guid><dc:creator><![CDATA[Kun Chen]]></dc:creator><pubDate>Sun, 05 Apr 2026 20:54:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YGAB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Setting up a new Mac always sounds easier than it actually is.</p><p>You tell yourself it will take an hour. Install a few apps. Copy some dotfiles. Tweak a few settings. Done.</p><p>Then a full weekend disappears.</p><p>Some of your setup lives in shell config. Some is buried in macOS settings. Some is in packages you installed years ago and forgot about. Some is in app configs that only make sense after months of iteration. None of it feels hard while you are building it gradually. It only becomes painful when you have to do it again.</p><p>That was the problem I wanted to solve. I wanted a reproducible core for my Mac setup. A setup I could reapply on a new machine. A setup I could open source. A setup structured enough to be dependable, but not so rigid that it becomes annoying to maintain.</p><p>That led me to this stack:</p><ul><li><p><a href="https://nixos.org/">Nix</a></p></li><li><p><a href="https://github.com/nix-darwin/nix-darwin">nix-darwin</a></p></li><li><p><a href="https://github.com/nix-community/home-manager">Home Manager</a></p></li><li><p>declarative <a href="https://brew.sh/">Homebrew</a></p></li></ul><p>All the source code I covered in this article can be found here:</p><ul><li><p><a href="https://github.com/kunchenguid/dotfiles-mac-nix">https://github.com/kunchenguid/dotfiles-mac-nix</a></p></li></ul><p>It&#8217;s a public, reusable core of my Mac setup. It is meant to be forked and adapted, not copied as a complete snapshot as is.</p><p>In this post, I will walk through the ideas behind it and how I built each piece.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I&#8217;m a solo builder, previously L8 engineer at Meta, Microsoft, Atlassian. I share practical field notes about frontier agentic engineering.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>What Nix, nix-darwin, and Home Manager actually do</h2><p>If you have never used this stack before, here is the short version.</p><h3>Nix</h3><p>Nix is a package manager and configuration system.</p><p>The reason people like it is that it lets you describe an environment declaratively. Instead of manually installing packages and hoping you remember what you did six months later, you define the environment in code.</p><p>For me, the value is simple: I want my machine setup written down in a form I can version, reapply, and evolve.</p><h3>nix-darwin</h3><p><code>nix-darwin</code> brings that model to macOS.</p><p>It lets you configure machine-level parts of your Mac, including things like:</p><ul><li><p>system defaults</p></li><li><p>login shell</p></li><li><p>system packages</p></li><li><p>Homebrew integration</p></li><li><p>primary user configuration</p></li></ul><p>So if Nix is the foundation, <code>nix-darwin</code> is the layer that makes it useful for a Mac.</p><h3>Home Manager</h3><p>Home Manager does something similar, but for your user environment.</p><p>Instead of configuring the machine itself, it configures the things that live in your home directory and shape your day-to-day workflow:</p><ul><li><p>user packages</p></li><li><p>Git config</p></li><li><p>shell behavior</p></li><li><p>fonts</p></li><li><p>application config files</p></li><li><p>environment variables</p></li></ul><p>I like this split because it keeps system concerns and user concerns from getting mixed together.</p><h3>Declarative Homebrew</h3><p>Even if you use Nix on macOS, <a href="https://brew.sh/">Homebrew</a> is still useful.</p><p>A lot of Mac apps are easiest to install that way, especially GUI apps. So instead of pretending Homebrew should disappear, I let <code>nix-darwin</code> manage it declaratively.</p><p>That gives me a setup where both Nix packages and Homebrew apps live in source control.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YGAB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YGAB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YGAB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png" width="1200" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d536609c-5329-4411-b65e-07c70acea9de_1200x630.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:137927,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://kunchenguid.substack.com/i/193280895?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YGAB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 424w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 848w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 1272w, https://substackcdn.com/image/fetch/$s_!YGAB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd536609c-5329-4411-b65e-07c70acea9de_1200x630.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Step 1: Bootstrap the machine once</h2><p>Before the declarative setup can take over, a fresh Mac still needs a small bootstrap step.</p><p>The reason is simple: on a brand new machine, the tools that apply the real configuration do not exist yet.</p><p>For this repo, the bootstrap layer lives in <code>setup/mac.sh</code>.</p><p>Its job is to install the minimum core tools needed to get the rest of the setup working:</p><ul><li><p><a href="https://determinate.systems/nix-installer/">Determinate Nix Installer</a> for installing <a href="https://nixos.org/">Nix</a></p></li><li><p><a href="https://brew.sh/">Homebrew</a> for the macOS package/app layer managed by <code>nix-darwin</code></p></li><li><p><code>darwin-rebuild</code> to apply the system configuration</p></li><li><p><a href="https://github.com/nvm-sh/nvm">nvm</a> and Node.js for a practical JavaScript/TypeScript runtime baseline</p></li></ul><p>Here is the bootstrap script:</p><pre><code><code>#!/bin/bash

set -euo pipefail

DOTFILES_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &amp;&gt; /dev/null &amp;&amp; cd .. &amp;&amp; pwd )

# Fail early if placeholder values have not been customized yet
if grep -R -n -E 'yourname|/Users/yourname|Your Name|you@example.com' \
  "$DOTFILES_DIR/flake.nix" \
  "$DOTFILES_DIR/nix" &gt;/dev/null 2&gt;&amp;1; then
  echo "Placeholder values are still present in the repo."
  echo "Please replace values like 'yourname', '/Users/yourname', 'Your Name', and 'you@example.com' before running setup/mac.sh."
  exit 1
fi

# Install Nix via Determinate if missing
if ! command -v nix &amp;&gt; /dev/null; then
  curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.sh/nix | sh -s -- install
fi

# Install Homebrew if missing
if ! command -v brew &amp;&gt; /dev/null; then
  /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi

# Apply the Nix configuration
if [ -x /run/current-system/sw/bin/darwin-rebuild ]; then
  sudo /run/current-system/sw/bin/darwin-rebuild switch --flake "$DOTFILES_DIR#mac"
else
  sudo nix run github:nix-darwin/nix-darwin -- switch --flake "$DOTFILES_DIR#mac"
fi

# Install nvm and a default Node.js if missing
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
  PROFILE=/dev/null bash -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash'
  [ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; . "$NVM_DIR/nvm.sh"
  nvm install --lts
fi
</code></code></pre><p>The system is now split in two phases:</p><ol><li><p><strong>Bootstrap phase</strong>: install the minimum needed to get going</p></li><li><p><strong>Declarative phase</strong>: let Nix, <code>nix-darwin</code>, and Home Manager manage the durable setup</p></li></ol><p>That bootstrap script is what you run on a <strong>brand new Mac</strong>, after cloning the repo and replacing the placeholder values with your own username, home directory, and Git identity. The script now checks for those placeholder values and fails early if you forgot.</p><p>In other words, the order is:</p><ol><li><p>Clone the repo</p></li><li><p>Replace placeholders like <code>yourname</code>, <code>/Users/yourname</code>, and your Git identity</p></li><li><p>Run <code>bash setup/mac.sh</code></p></li><li><p>Let the declarative setup take over from there</p></li></ol><p>After that first bootstrap, ongoing changes should mostly be made by editing the Nix config and running <code>darwin-rebuild switch --flake ~/github/dotfiles-mac-nix#mac</code>.</p><p>I also like having a small convenience alias for this. In the public repo, I added an opinionated version that assumes the repo lives at <code>~/github/dotfiles-mac-nix</code>:</p><pre><code><code>rebuild = "/run/current-system/sw/bin/darwin-rebuild switch --flake ~/github/dotfiles-mac-nix#mac";
</code></code></pre><p>That makes the common update loop a lot simpler: edit config, run <code>rebuild</code>, verify the result.</p><h2>Step 2: Create a flake as the entry point</h2><p>The first thing I did was create a <code>flake.nix</code> file.</p><p>A flake is just the top-level definition of the setup. It declares the dependencies and how they are wired together.</p><p>In my case, I wanted three inputs:</p><ul><li><p><code>nixpkgs</code> for packages</p></li><li><p><code>nix-darwin</code> for macOS system configuration</p></li><li><p><code>home-manager</code> for user configuration</p></li></ul><p>The file looks like this:</p><pre><code><code>{
  description = "Minimal macOS Nix setup with nix-darwin + Home Manager";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nix-darwin = {
      url = "github:LnL7/nix-darwin";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { nixpkgs, nix-darwin, home-manager, ... }: {
    darwinConfigurations.mac = nix-darwin.lib.darwinSystem {
      system = "aarch64-darwin";
      modules = [
        ./nix/host.nix
        home-manager.darwinModules.home-manager
        {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.backupFileExtension = "backup";
          home-manager.users.yourname = import ./nix/user.nix;
        }
      ];
    };
  };
}
</code></code></pre><p>This is the file that turns the repo from a pile of config into a coherent system.</p><h2>Step 3: Define the machine-level setup with nix-darwin</h2><p>Next I created <code>nix/host.nix</code>.</p><p>This file handles the machine-level parts of the setup: macOS defaults, Homebrew packages, the main user, the login shell, and system-level packages.</p><p>Here is the version from the public repo:</p><pre><code><code>{ pkgs, ... }:

{
  # If you use Determinate Nix Installer (recommended), let it manage Nix itself.
  nix.enable = false;

  nixpkgs.config.allowUnfree = true;

  homebrew = {
    enable = true;
    onActivation.cleanup = "zap";
    taps = [ ];
    brews = [
      "autoconf"
    ];
    casks = [
      "wezterm"
      "amethyst"
    ];
  };

  environment.systemPackages = with pkgs; [
    starship
  ];

  system.primaryUser = "yourname";
  users.users.yourname = {
    home = "/Users/yourname";
    shell = pkgs.zsh;
  };

  system.defaults = {
    NSGlobalDomain = {
      AppleInterfaceStyle = "Dark";
      KeyRepeat = 2;
      InitialKeyRepeat = 15;
      "com.apple.swipescrolldirection" = false;
      NSAutomaticCapitalizationEnabled = false;
      NSAutomaticPeriodSubstitutionEnabled = false;
      NSAutomaticSpellingCorrectionEnabled = false;
      NSAutomaticQuoteSubstitutionEnabled = false;
      NSNavPanelExpandedStateForSaveMode = true;
      NSNavPanelExpandedStateForSaveMode2 = true;
      AppleShowAllExtensions = true;
    };

    finder = {
      AppleShowAllExtensions = true;
      ShowPathbar = true;
    };

    trackpad = {
      Clicking = true;
    };
  };

  environment.systemPath = [
    "/run/current-system/sw/bin"
    "/etc/profiles/per-user/yourname/bin"
  ];

  system.stateVersion = 6;
}
</code></code></pre><p>This is where I put all the decisions that shape the machine itself.</p><p>For me, this is one of the highest-leverage parts of the setup. If I get a new Mac, I do not want to remember which settings I toggled manually in five different places. I want those decisions encoded once and re-applied.</p><h2>Step 4: Define the user environment with Home Manager</h2><p>After that, I created <code>nix/user.nix</code>.</p><p>This is the user-level configuration. It includes packages, fonts, Git settings, prompt configuration, shell behavior, and dotfile symlinks.</p><pre><code><code>{ config, pkgs, ... }:

let
  dotfilesDir = "${config.home.homeDirectory}/github/dotfiles-mac-nix";
in
{
  home.username = "yourname";
  home.homeDirectory = "/Users/yourname";
  home.stateVersion = "23.11";
  home.language.base = "en_US.UTF-8";

  home.packages = with pkgs; [
    git
    curl
    wget
    jq
    fd
    fastfetch
    ripgrep
    killall
    lazygit
    tree
    bun
    rustup
    zip
    unzip
    nerd-fonts.hack
    roboto
    noto-fonts
    noto-fonts-cjk-sans
    noto-fonts-color-emoji
    font-awesome
  ];

  fonts.fontconfig.enable = true;

  home.sessionVariables = {
    EDITOR = "vim";
  };

  programs.git = {
    enable = true;
    lfs.enable = true;
    signing.format = null;
    settings = {
      user = {
        name = "Your Name";
        email = "you@example.com";
      };
      core.editor = "vim";
      color.ui = true;
      push.autoSetupRemote = true;
      pull.rebase = true;
      rebase.updateRefs = true;
    };
  };

  programs.starship = {
    enable = true;
    settings = {
      command_timeout = 1000;
      add_newline = false;
      format = "$username$hostname$directory$git_branch$git_state$git_status$cmd_duration$line_break$character";
    };
  };

  programs.zsh = {
    enable = true;
    autosuggestion.enable = true;
    syntaxHighlighting.enable = true;
    shellAliases = {
      ".." = "cd ..";
      m = "git switch main";
      mst = "git switch master";
      pull = "git pull";
      push = "git push";
      pushf = "git push --force";
      add = "git add .";
      amend = "git commit --amend";
      reset = "git reset --soft HEAD^";
      rebasem = "git rebase -i main";
      rebasemst = "git rebase -i master";
      rebuild = "/run/current-system/sw/bin/darwin-rebuild switch --flake ~/github/dotfiles-mac-nix#mac";
    };
    initContent = ''
      bindkey '^f' autosuggest-accept
    '';
  };

  home.file = {
    ".config/wezterm".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/files/.config/wezterm";
  };
}
</code></code></pre><p>The exact package list is not the important part. The structure is.</p><p>This is the layer where I define the baseline environment I want in my user account, including identity, packages, shell config, and dotfile symlinks all in one place.</p><h2>Step 5: Add one real app config as an example</h2><p>I did not want this repo to be just Nix modules and placeholders, so I added one real application config: <a href="https://wezfurlong.org/wezterm/">WezTerm</a>.</p><p>The config lives in:</p><pre><code><code>files/.config/wezterm/wezterm.lua
</code></code></pre><p>And it gets linked into <code>~/.config/wezterm</code> through Home Manager.</p><p>The file itself is simple, but that is the point. It shows how to keep app config in the repo without turning the whole repo into a giant dump of personal preferences. I picked WezTerm because it is real enough to demonstrate the pattern while still being general enough for a public starter repo.</p><pre><code><code>local wezterm = require("wezterm")

local config = wezterm.config_builder()

local is_windows = os.getenv("OS") and os.getenv("OS"):lower():find("windows")
local is_macos = wezterm.target_triple:lower():find("darwin") ~= nil

config.color_scheme = "rose-pine-moon"
config.max_fps = 120
config.font = wezterm.font("Hack Nerd Font", { weight = "DemiBold" })
config.window_decorations = "INTEGRATED_BUTTONS|RESIZE"
config.window_frame = {
  font = wezterm.font("Hack Nerd Font", { weight = "Bold" }),
}
config.inactive_pane_hsb = {
  saturation = 0.0,
  brightness = 0.5,
}

if is_windows then
  config.win32_system_backdrop = "Acrylic"
  config.window_background_opacity = 0.7
  config.window_frame.font_size = 10.0
end

if is_macos then
  config.window_background_opacity = 0.8
  config.macos_window_background_blur = 50
  config.font_size = 15.0
  config.window_frame.font_size = 13.0
end

return config
</code></code></pre><h2>After Step 5: How I add more tools later</h2><p>Once the base setup is in place, the next question is obvious: how do I install more stuff over time?</p><p>My rule of thumb is simple.</p><h3>Use Nix / Home Manager for things that should be part of the reproducible environment</h3><p>That usually means:</p><ul><li><p>CLI tools I use regularly</p></li><li><p>fonts</p></li><li><p>shell utilities</p></li><li><p>language toolchains that I want declared in the repo</p></li><li><p>packages that belong in my default user environment</p></li></ul><p>For example, adding another CLI package usually means editing <code>nix/user.nix</code> and adding it to <code>home.packages</code>, then running:</p><pre><code><code>rebuild
</code></code></pre><h3>Use Homebrew for Mac apps that fit naturally there</h3><p>For GUI apps and some macOS-native tools, Homebrew is often still the right place.</p><p>That means editing <code>nix/host.nix</code> and adding a formula to <code>brews</code> or an app to <code>casks</code>, then applying the config again.</p><h3>Use ecosystem-specific package managers when that is the right abstraction</h3><p>Sometimes the right answer is not Nix or Homebrew.</p><p>For example:</p><ul><li><p><code>npm</code> for global JavaScript tooling when that fits your workflow</p></li><li><p>language-native package managers for project-specific dependencies</p></li></ul><p>I do not think a good setup means forcing every possible tool through one package manager. I think it means being clear about which layer owns what.</p><p>My rough mental model is:</p><ul><li><p><strong>Nix / Home Manager</strong> for reproducible baseline environment</p></li><li><p><strong>Homebrew</strong> for macOS apps and tools that fit naturally there</p></li><li><p><strong>language-specific package managers</strong> for ecosystem-specific or project-specific tooling</p></li></ul><h2>How to use this repo</h2><p>The repo is meant to be copied and adapted.</p><p>At a high level:</p><ol><li><p>Clone the repo under your home directory</p></li><li><p>Replace the placeholders for username, home directory, and Git identity</p></li><li><p>If you are on Intel, change the system target from <code>aarch64-darwin</code> to <code>x86_64-darwin</code></p></li><li><p>On a fresh Mac, run <code>bash setup/mac.sh</code></p></li><li><p>For later changes, edit the Nix config and run <code>darwin-rebuild switch --flake ~/github/dotfiles-mac-nix#mac</code></p></li></ol><p>Once your setup is reproducible, you stop relying on memory and habit to rebuild it. You can now also get a new Mac up and running with the exact same setup within seconds. </p>]]></content:encoded></item><item><title><![CDATA[Zero to One — Handbook for Entrepreneurial Engineers]]></title><description><![CDATA[Ever since I was a teenager, my coding adventure has always centered around one question - I have great ideas. How do I turn them into reality? This article aims to give some answers to this question.]]></description><link>https://blog.kunchenguid.com/p/zero-to-one-handbook-for-entrepreneurial</link><guid isPermaLink="false">https://blog.kunchenguid.com/p/zero-to-one-handbook-for-entrepreneurial</guid><dc:creator><![CDATA[Kun Chen]]></dc:creator><pubDate>Thu, 02 Apr 2026 20:59:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AeSg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>But wait&#8230; who am I now? And what real experience do I have to share?</em></p><p>I&#8217;m an L8 senior principal engineer previously at Meta, Microsoft, and Atlassian. I see myself as an engineer whose specialization isn&#8217;t in any particular tech stack, but in taking things from zero to one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I share practical field notes about my agentic engineering workflows and experience building agentic systems. I post deeper articles for important lessons worth documenting.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Over the years, I accumulated experience from -</p><ul><li><p>Having built over a hundred side projects. First time I made money was with a SaaS I built in high school over 20 years ago.</p></li><li><p>Being a founding engineer of <a href="https://www.facebook.com/gaming/play/">Facebook Instant Games</a>, and taking it to a large business with thousands of games and millions of players.</p></li><li><p>Then started <a href="https://www.msn.com/en-us/play">MSN Games</a> at Microsoft, again from zero and turned it into a business.</p></li><li><p>Recently at Atlassian I helped start <a href="https://www.atlassian.com/solutions/devops/ai-innovation">a suite of AI products for software developers</a>, from a pitch, to a prototype, to a state-of-the-art AI product portfolio.</p></li></ul><p>Perhaps the most counter-intuitive learning I&#8217;ve had from this journey is that you can actually have a startup founder experience while working in large companies. You may not be able to get a $100 million exit, but you&#8217;ll never have a month without paycheck, and over time the reward gets close as well.</p><p>If that sounds like something you are passionate about as well, this post is for you!</p><p>Let&#8217;s start with how to find great ideas to work on &#8212;</p><h2><strong>Want good ideas? Go find some problems.</strong></h2><p>There are countless ways to generate ideas -</p><ul><li><p>Sometimes we see a new technology coming out, and think &#8220;what can we do with this&#8221;?</p></li><li><p>Sometimes we find something painful, and think &#8220;can I solve this for myself&#8221;?</p></li><li><p>Other times, an apple fell off a tree and the rest was history.</p></li></ul><p>The world is fully of inspirations. More often than not, we don&#8217;t need just ideas, we need <em>good</em> ones that are worth our time.</p><p>A good idea is one that can succeed. An idea that can succeed is one that can provide value. An idea can provide value when it can solve real problems.</p><p>So the fundamental question is &#8212; <em>where can we find real problems</em>? If we can find a real problem and identify a viable solution, we have a great idea to work on.</p><p>I typically think of problems in the following taxonomy -</p><ul><li><p><strong>User problems </strong>&#8212; pain points that exist for end users. For example, having to load dishes into a dishwasher and other house chores everyday is a pain for me, and I would pay a reasonable price if a robot can help me get it done.</p></li><li><p><strong>Ecosystem problems </strong>&#8212; suppliers who produce parts for a robot may suffer from various challenges, such as high overhead and cost in managing logistics and customer support.</p></li><li><p><strong>Company problems </strong>&#8212; let&#8217;s say we have a company who&#8217;s building those robot. We may have our own problems, such as LLM bills being out of control.</p></li></ul><p>To know what real problems exist, you&#8217;d need to find ways to hang out and talk with people who may have those problems. Follow people who vent on twitter and listen to what they have to say; jump into a reddit community and find out what they are complaining about; go to events and talk to people, understand what they are going through and hear their problems from their own mouths.</p><p>The way you know you have found real problems is when you can <em>name</em> a few real people who will be eager to hear about a solution whenever you have one. When you have that, you will have much, much better intuition about whether an idea is good vs not. You may also get new ideas during that process.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AeSg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AeSg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 424w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 848w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AeSg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg" width="700" height="525" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:525,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AeSg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 424w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 848w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!AeSg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc831a6f6-6778-4091-b95b-0cda553a0240_700x525.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Credit &#8212; <a href="https://unsplash.com/photos/three-men-sitting-while-using-laptops-and-watching-man-beside-whiteboard-wD1LRb9OeEo">Austin Distel, Unsplash</a></figcaption></figure></div><p>If you aren&#8217;t doing this yet today, I highly recommend starting here.</p><h3><strong>What can you do today?</strong></h3><p>In a corporate environment, finding real problems is actually a lot easier than when you are working solo.</p><ol><li><p>Look up your company and teams&#8217; OKRs &#8212; every OKR is a problem that the group already collectively identified as a real problem waiting to be solved. If you think any of the problems may have a great solution that&#8217;s not being worked on yet, you have a new idea!</p></li><li><p>When you talk to people, always be curious what problems they are going through. If you&#8217;ve heard the same thing from multiple folks, there&#8217;s a good chance you just found a real problem.</p></li><li><p>Building network across teams and organizations. Read other teams&#8217; newsletters and updates to understand what they are doing. I can&#8217;t even count how many times a great idea came from seeing &#8220;Team A is doing something that can solve Team B&#8217;s problem with just a bit more work&#8221;.</p></li></ol><h2><strong>No big ideas yet? Build skills, credibility, and trust.</strong></h2><p>It&#8217;s absolutely okay if you aren&#8217;t always working on &#8220;your own idea&#8221;. I certainly wasn&#8217;t &#8212; very often I even deliberately carved out time to contribute to existing projects even when I have an exciting new idea to work on.</p><p>That&#8217;s because in order to succeed in taking a new idea from zero to one, you need skills, credibility and trust. You should be building these things when you aren&#8217;t building a new idea.</p><h3><strong>Skills</strong></h3><p>Both hard skills and soft skills are quite important.</p><p>Hard skills can be acquired by learning new things you haven&#8217;t done before &#8212; for a year or two when I was at Microsoft, I wrote more SQL than &#8220;real code&#8221; in order to run large map-reduce jobs. To be honest that wasn&#8217;t the most interesting thing to write, but it wasn&#8217;t until a few years later on when I discovered I was the only engineer on my team that could do data analysis fluently, and could constantly use that skill to identify growth opportunity for our new products, that I realized what I gained from those SQL jobs.</p><p>You can also build hard skills by strengthening knowledge in a specific domain that has enough depth, such as machine learning. By going deep, you can become an expert that can solve problems in ways others can&#8217;t.</p><p>Even if you aren&#8217;t increasing either breadth or depth, you can still increase your <em>efficiency</em> at doing the same things, which is a hard skill as well. For example, I can debug problems and do performance profiling very efficiently &#8212; this was from years of pushing myself &#8220;how can I do it faster next time&#8221;.</p><p>Soft skills on the other hand are often overlooked by us engineers, probably because we can go pretty far without being intentional about them.</p><p>But to be an entrepreneurial engineer, I believe soft skills are incredibly important. We need to communicate a lot &#8212; to understand different people&#8217;s problems, to sell a vision to stakeholders or a solution to customers, or to rally a team towards the same direction.</p><p>If you have no idea where to start on soft skills, I&#8217;ll give some book recommendations here (I&#8217;m not affiliated). These books genuinely changed me as a person.</p><p><a href="https://www.amazon.com/Never-Split-Difference-Negotiating-Depended/dp/0062407805">https://www.amazon.com/Never-Split-Difference-Negotiating-Depended/dp/0062407805</a> &#8212; this book talks about negotiations with terrorists but deeply under the hood it&#8217;s about understanding others and using that understanding to guide how you communicate</p><p><a href="https://www.amazon.com/Mom-Test-customers-business-everyone/dp/1492180742">https://www.amazon.com/Mom-Test-customers-business-everyone/dp/1492180742</a> &#8212; this book gives you the skills you need to get meaningful insights from others, and helps you avoid being misguided by your confirmation bias and noise that comes from others &#8220;being nice&#8221;</p><h3><strong>Credibility &amp; trust</strong></h3><p>Credibility is about establishing that you are capable.</p><p>If a random person on the street comes to you and say &#8212; &#8220;I have a great idea here! Can you give me $10 to help me build it?&#8221; How likely would you give them the money, or even listen to their idea, compared to when the exact same pitch comes from a serial-entrepreneur who you know has a track record of building useful things?</p><p>By consistently nailing the tasks you work on, you will be establishing invaluable credibility that makes it easy for others to not just believe in your idea, but also believe in your ability to make it happen.</p><p>Trust is slightly different &#8212; a person can be a well-known expert in their domain, but do you trust them enough to co-found a company together? You probably won&#8217;t until you&#8217;ve worked with this person for a while and got to know whether they are easy to talk to, if they do what they say, and respect your inputs.</p><p>You can build trust by having positive interactions with others &#8212; demonstrate that you understand what others care about, help people out, and (maybe surprisingly) even by getting others to help you. One of my previous articles &#8220;<a href="https://medium.com/@kunchenxyz/principal-engineers-toolkit-building-trust-4641fede7959">Building Trust</a>&#8221; here dives deep into this topic.</p><p>When you aren&#8217;t working on a new idea but instead contributing to a large existing project, that&#8217;s a great opportunity to build credibility and trust with others, so that when you do have a great idea to work on, it&#8217;s easy for you to find support. It also helps increase the chance that others would bring good ideas to you.</p><h2><strong>Finding time to build things</strong></h2><p>Now comes the hard part &#8212; we all have day jobs. How do we find time to build something new?</p><p>When we want to work on a new idea, we must first acknowledge that we&#8217;re taking on a risk. We simply won&#8217;t know whether an idea will work or not until we spent some time building and testing them in the real world. What we need is just enough resource (time and/or money) to pursue and validate the idea.</p><p>There are a grand total of two (legal) ways to get resource -</p><ol><li><p>You can use your own time or money. You would use evenings and weekends, or by quitting your day job and living on money you saved.</p></li><li><p>You can convince others to give you time or money. For example, attracting investors to fund your startup company.</p></li></ol><p>When in a corporate environment, I generally recommend going with #2, which is significantly easier than in the startup world &#8212; you mainly need to align with stakeholders on your prioritization. If everyone agrees that&#8217;s the most valuable thing you can work on, you are all good to go. Many tech companies also have dedicated time carved out, such as as hackathons, that give everyone the time needed to get a new idea off the ground.</p><p>Going with #1 is a last resort but also a valid option &#8212; you don&#8217;t need anyone&#8217;s permission to use your own time as the resource to do what you think should be done. Build it, show it, and go from there.</p><p>However, if you find yourself repeatedly resorting to this option, then there&#8217;s a more fundamental misalignment between what you vs your team believe to be important. You need to understand and tackle this disconnection instead of always consuming your own resource and time, which can lead to burnout.</p><h3><strong>Being scrappy</strong></h3><p>Regardless of the approach you take, you&#8217;ll find it significantly easier when you stay scrappy and use as little resource as possible. Asking to go dark for 3 months is very different from asking for a few days to do a spike.</p><p>But how to be scrappy? Mainly 5 things -</p><ol><li><p><strong>Minimize the initial scope</strong>. Avoid falling into the trap of assuming your idea will work well and try to build a perfect version of it that can launch straight to a million users worldwide. Instead, ask yourself &#8220;what&#8217;s the minimum version of a product that can either prove or disprove this idea?&#8221; Your prototype probably doesn&#8217;t need to support Mac, Windows and a hundred different Linux distributions all at the same time. Who&#8217;s the first user you&#8217;ll give the product to? Ask what they use and build just that.</p></li><li><p><strong>Write as little code as possible</strong>. What existing building blocks can you pull together to make a functional prototype of the idea? We&#8217;re living in a wonderful world full of amazing open source software and ML models. If you find yourself planning to build a complex system even for a prototype that doesn&#8217;t have to scale, you should be alarmed and question if you can assemble it more quickly.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TnlV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TnlV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 424w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 848w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TnlV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg" width="480" height="360" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:360,&quot;width&quot;:480,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!TnlV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 424w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 848w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!TnlV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef330c75-8fc0-425b-8535-a57502253ba5_480x360.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Wait, did I say 5 things? Nope, those two plus the meme I grabbed from the Internet should work for now! If you need more, let me know and I&#8217;ll iterate on it.</p><blockquote><p><em>See what I did there?</em></p></blockquote><h2><strong>I just built a thing. Now what?</strong></h2><p>If you did it right, by now you should be able to name a few people who are waiting to hear about your solution. And it&#8217;s time to reach out to those people whose problems you believe your solution will help with, and show them what you have!</p><p>What&#8217;s important though is to set the right expectations with yourself &#8212; this is <em>not</em> the time when you&#8217;ll hear people say</p><blockquote><p><em>What an amazing product! Here take my $299/month lifetime non-refundable subscription fee plus tax!</em></p></blockquote><p>Remember you probably just spent a few days hacking together a quick prototype. What you really want to get to are &#8212;</p><ol><li><p>Do they think this is the right solution to their problems? Why or why not?</p></li><li><p>How did they use your solution? Does that match what you intended</p></li><li><p>What key challenges do you need to tackle in order for this to work well?</p></li></ol><h3><strong>Iterate!</strong></h3><p>All of those above are extremely valuable information that&#8217;ll help you refine your approach, and get it closer to something useful. If possible, you should iterate on their feedback immediately -</p><ul><li><p>Not &#8220;<em>let me put this in the backlog and prioritize in our next sprint planning which happens in two weeks</em>&#8221;</p></li><li><p>But &#8220;<em>hold my beer &#8212; let me see if I can fix it in the next 15 minutes!</em>&#8221;</p></li></ul><p>The reason an extremely fast iteration loop is necessary for zero to one products is that you should generally expect that there are a lot of iterations needed to arrive at something that truly works. The next problem often isn&#8217;t visible until the first problem is fixed. The faster you iterate, the more likely you can get to a conclusive state before you run out of resources to continue funding this idea.</p><p>And if people did validate it&#8217;s the right direction, you will have a lot of concrete insights about how much more resource is needed to deliver a working solution, and what the return will look like once you deliver it &#8212; those are the key things you need in order to attract another round of investment to keep it going.</p><h3><strong>What if no one responds?</strong></h3><p>Another very possible outcome here is that no one even responded to you (except for a few emoji reactions). This can be demotivating for sure, and some people would simply stop here. But that&#8217;s not the entrepreneurial way! As the founder and CEO of your idea, it&#8217;s on you to take actions because nothing else will happen until you do.</p><p>One more slack message. Throw a calendar invite. Try reaching out to more people in case you were wrong about who had the problems. People are busy, and the world is increasingly distracting. You need to think creatively to stand out from the noise and get a conclusive read on your idea, so that you never walk away without meaningful learnings.</p><h2><strong>Play as a team</strong></h2><p>Think about all the products you&#8217;ve used today, and see how many of them were built by a solo engineer? The more things I built, the more I saw the power of having a team with diverse skillsets working well towards the same direction.</p><p>That said, a dysfunctional team can be counter-productive, and it&#8217;s not easy at all to set up a well functioning one. I would not claim to be an expert here &#8212; I still make lots of mistakes and not doing nearly enough, but I can share a few learnings that I found useful.</p><h3><strong>Inviting others early</strong></h3><p>Generally speaking, we can benefit from looping in potential collaborators as early as possible. Many companies&#8217; co founders met each other before their product was built &#8212; including my last company Atlassian&#8217;s founders Mike and Scott!</p><p>Having a close partner means you always have someone to brainstorm with, many different perspectives for what may work well vs not, and you can hold each other accountable to make progress. More people working together also means you can make progress and deliver more quickly, which helps build traction and momentum for the project.</p><p>There are many ways you can do this. Mike sent an email to the entire class and got Scott &#8212; as simple as that. In a corporate environment, you can simply share your insights, ideas, progress and results more actively &#8212; if they resonated, people will come to you and ask to work together. You should also actively subscribe to what others are building and reach out if you see good opportunities to collaborate.</p><h3><strong>Value cross-functional expertise</strong></h3><p>We engineers are a privileged group because we are the only discipline within a tech company that can solo build a new thing that works, all by ourselves.</p><p>This privilege would sometimes create a perception that we don&#8217;t need other disciplines to build something successful. Granted, there are solo entrepreneurs like <a href="https://x.com/levelsio">https://x.com/levelsio</a> who have indeed been operating mostly alone, the reality is that these are exceptions not the norm.</p><p>One of my fondest memory building new things was when I locked myself and a designer in the same room and we said &#8220;no one leaves until we have a good working demo&#8221;. We would both be at his laptop where he explained to me why the new button shouldn&#8217;t be a big red box, and we would both look at my screen when I told him the beautiful animation he put together in 3 minutes would take me hours to optimize.</p><p>In a corporate environment, we all have a huge advantage as we&#8217;re already surrounded by people who collectively have all the skills needed to take an idea all the way to market. Whether you can leverage this advantage will make a massive difference in your chance of success.</p><h3><strong>Share ownership and success</strong></h3><p>I&#8217;ve witnessed many times how <a href="https://en.wikipedia.org/wiki/Founder%27s_syndrome">Founder&#8217;s syndrome</a> caused founding members to struggle and ultimately leave the team when the team started to grow.</p><p>When you had a great idea and got it off the ground, you often have a particular vision and strong sense of ownership. That is good and necessary, as otherwise you probably wouldn&#8217;t even have gotten this far.</p><p>Now, one way or another, you have more team members joining you as a result of the success you established. You start getting questions on why your vision is the right one and everyone seems to be picturing something different. You start seeing the team come up with plans that don&#8217;t align with your thinking. You start finding yourself constantly correcting others &#8212; to varying degree of success &#8212; and you feel exhausted.</p><p>These are all signs that you need to <em>share</em> ownership with others, instead of controlling it.</p><ul><li><p>Instead of telling everyone what&#8217;s the right thing to do, take them through the same journey that allowed you to arrive at the conclusion.</p></li><li><p>Focus more on communicating the &#8220;why&#8221; and let people decide the &#8220;what&#8221; and &#8220;how&#8221;.</p></li><li><p>Be open that you might be wrong, and others may have better ideas that can lead to a better outcome, which is what you want.</p></li><li><p>Be okay with the fact that great things can happen without your involvement. Celebrate others people&#8217;s impact and don&#8217;t take their credit.</p></li></ul><p>The difference is between being a small part of something big, vs a big part of something small. You want to be the former &#8212; it&#8217;s a good thing both for yourself and for the group.</p><h3><strong>Assume full accountability</strong></h3><p>One downside of having a team is that it dilutes accountability. When a problem rises, you may think &#8220;oh someone else will get to it&#8221; not realizing that everyone was thinking the same, resulting in problems falling through the cracks.</p><p>The solution to this is to assume you have full accountability by default, for every single thing you work on. It&#8217;s a mindset that says -</p><blockquote><p><em>If this thing fails, it&#8217;s on me. <strong>No matter what</strong>.</em></p></blockquote><p>This is an extremely powerful mindset. When working on things from zero to one, there are often problems that don&#8217;t strictly fall under anyone&#8217;s predefined responsibility. By adopting this mentality, you are empowering yourself to take actions during ambiguity and do whatever is necessary to keep things moving in the right direction.</p><p>The beauty of this mindset is that you don&#8217;t need anyone to give you the permission. You don&#8217;t need to be given a CEO title, or be the most senior person in the room. You can just assume the accountability on yourself and start taking actions you think are necessary for the project to succeed but not being done, and that&#8217;s typically how great leaders emerge.</p><h2><strong>When should I keep going, and when should I stop?</strong></h2><p>We often hear the term &#8220;fail fast&#8221; &#8212; prove something doesn&#8217;t work and move to to a different idea immediately.</p><p>But people also say &#8220;dig deeper&#8221; &#8212; maybe the real solution is just one iteration away. Thomas Edison failed thousands of times before being able to build a light bulb that works.</p><p>Well.. that&#8217;s pretty confusing &#8212; how do we know which case we&#8217;re in?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!USPz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!USPz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 424w, https://substackcdn.com/image/fetch/$s_!USPz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 848w, https://substackcdn.com/image/fetch/$s_!USPz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 1272w, https://substackcdn.com/image/fetch/$s_!USPz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!USPz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png" width="700" height="541" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bee81c39-b8b6-4f38-9533-7a84238de267_700x541.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:541,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!USPz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 424w, https://substackcdn.com/image/fetch/$s_!USPz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 848w, https://substackcdn.com/image/fetch/$s_!USPz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 1272w, https://substackcdn.com/image/fetch/$s_!USPz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbee81c39-b8b6-4f38-9533-7a84238de267_700x541.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I&#8217;m not wise enough to give you a classifier at 100% accuracy. But here are a few things to chew on when you confront the question yourself -</p><ol><li><p>Make a clear distinction between &#8220;the problem is not real&#8221; vs &#8220;the solution doesn&#8217;t work&#8221; and figure out which case it is. Sometimes we think people have a big problem when they actually don&#8217;t care about it that much. If the feedback you are getting suggests that there&#8217;s no strong demand for solving the problem, it&#8217;s time to go back to the drawing board and try to identify a problem that&#8217;s real.</p></li><li><p>If people confirmed they have the problem, but they are just not happy with the solution you provided, figure out why. Ideally even before you began to solve the problem, you should have studied what solutions were tried in the past, why the problem is still there, so you can learn from them and avoid repeating the same attempt. Once you know why the solution doesn&#8217;t work, you can determine whether those factors are within your control or not, and decide next steps accordingly.</p></li><li><p>Assess whether you have the resource to build a working solution. Sometimes what you discover is that a working solution requires massive upfront investment or different domain expertise that you don&#8217;t have. You could either try and get those missing pieces, or move to a smaller, more manageable goal which sometimes can be a stepping stone towards the bigger dream.</p></li></ol><h2><strong>Managing the ups and downs</strong></h2><p>Working on things from zero to one is inherently risky. Most startup companies don&#8217;t make it, and the founders get zero (or even negative) financial gains.</p><p>The same holds in a corporate environment &#8212; we can&#8217;t ask for the freedom to work on risky ideas that may or may not generate value, and expect a GE rating regardless of the outcome just because the ideas are cool and you worked hard on them.</p><p>You will need to be comfortable with the rollercoaster ride that every entrepreneur will tell you they went through, and learn to manage the risks.</p><h3><strong>Know the bottom line</strong></h3><p>You can&#8217;t keep founding new startups if your family is starving &#8212; plain and simple. You need to regularly align with stakeholders and make sure you are meeting their core expectations and can keep the lights on.</p><h3><strong>Focus on long term growth</strong></h3><p>Understand that your performance rating is not the only thing you are gaining from your career. The rating rates your impact over the last 6 months, not you as a person. Even Steve Jobs had some bad performance reviews &#8212; so bad that he got removed from the company he founded &#8212; but no one today would look back on the story and say Jobs was incompetent.</p><p>What&#8217;s more important is your personal growth, which unfortunately is not as visible. You can understand it by asking yourself -</p><blockquote><p><em>What did I accomplish this month that I couldn&#8217;t before?</em></p></blockquote><p>As you acquire and strengthen your skills, trust that you will be making larger impact down the road. Good performance rating and financial gains will come as side effects when that happens, not a leading indicator.</p><h3><strong>Manage expectations</strong></h3><p>While entrepreneurs may enjoy the thrill from a rollercoaster ride, investors generally don&#8217;t like unpleasant surprises. This is why in every public company&#8217;s earnings call, good CEOs will set expectations clearly if they are anticipating anything to go slow next quarter.</p><p>The same idea applies when we work on zero to one projects &#8212; because how ambiguous things are, different stakeholders may have their own assumptions on how things will go. As the person who are working on the idea, you likely have the most insights about what to expect &#8212; make sure to over-communicate, especially any bad news, challenges and risks, so others also know what&#8217;s to come and can plan accordingly instead of getting a huge surprise when the results are revealed and it&#8217;s miles away their expectations.</p><h2><strong>Closing thoughts</strong></h2><p>I hope these experiences can be helpful to you, especially if going from zero to one is your passion as well. Please feel free to share with anyone else who may also find this interesting.</p><p>If you would like for me to dive into anything more deeply, or touch on something not covered, please feel free to leave a comment! If you also have thoughts on this topic, please also raise below so others coming to this page can see too.</p><p>Thanks for reading &#8212; until next time!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.kunchenguid.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>