<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Vivek Chopra</title>
    <link>https://www.vivekchopra.com/</link>
    <atom:link href="https://www.vivekchopra.com/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Notes on AI-native platforms, regulated markets, scaling engineering orgs, and operational reliability.</description>
    <language>en-us</language>
    <lastBuildDate>Sat, 31 Oct 2026 07:00:00 GMT</lastBuildDate>
    <item>
      <title>Altdown: RSS for agents*</title>
      <link>https://www.vivekchopra.com/posts/altdown</link>
      <description>Reader mode for agents, made by the publisher.</description>
      <content:encoded><![CDATA[<p>Altdown is reader mode for agents, made by the publisher.</p>
<h2 id="not-a-feed" tabindex="-1">Not a feed <a class="heading-anchor" href="#not-a-feed" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Think of it as RSS for agents,* except that instead of telling you what's new, it tells you what a page says. RSS is a subscription. It tells a reader what changed. The copy does not replace the feed. A browser's reader mode guesses which part of a page is the content. The publisher states it, instead of the browser guessing.</p>
<p>The web was built for people sitting in front of a browser, and nearly everything about a modern page assumes one: the nav bar, the sticky header, the related-posts rail, the analytics scripts. A growing share of the readers on those pages are now software acting for someone. An assistant summarizing a page, a coding agent checking a reference before it writes a call, a research tool pulling twenty articles into one answer. Those agents receive the same page a person does, pay for all of it in tokens, and then guess which part was the article. Sometimes the guess is right. Often it keeps the footer and drops the code sample.</p>
<p>Altdown, short for Alternate Markdown, is small on purpose. A site publishes a Markdown copy of each page's main content as a plain file next to the page, and the page announces that copy with one line in its <code>&lt;head&gt;</code>. There is no server logic, no proxy, and no custom HTTP header.</p>
<p>This site does that. <a href="https://www.vivekchopra.com/posts/new-sdlc">The New SDLC</a> announces its copy like this:</p>
<figure class="code-block"><figcaption><span>HTML</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="HTML example"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">&#x3C;</span><span style="--shiki-light:#116329;--shiki-dark:#7EE787">link</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> rel</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"alternate"</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> type</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"text/markdown"</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> href</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"/posts/new-sdlc.md"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">></span></span></code></pre></figure>
<p>The copy is at <a href="https://www.vivekchopra.com/posts/new-sdlc.md">https://www.vivekchopra.com/posts/new-sdlc.md</a>. Its metadata, without the description and generator lines, looks like this:</p>
<figure class="code-block"><figcaption><span>Markdown</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Markdown example"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">---</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">altdown</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">1</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">canonical</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">https://www.vivekchopra.com/posts/new-sdlc</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">title</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">The New SDLC</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">published</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">2026-04-26T00:00:00Z</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">updated</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">2026-04-26T00:00:00Z</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">lang</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">en</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">author</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">Vivek Chopra</span></span>
<span class="line"><span style="--shiki-light:#116329;--shiki-dark:#7EE787">signals</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: </span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">search=yes, ai-input=yes, ai-train=no</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">---</span></span></code></pre></figure>
<p>The link element is the same pattern RSS autodiscovery has used for twenty years. That is the part worth borrowing. The job is not. A site should have both. This one does: <a href="https://www.vivekchopra.com/feed.xml">the feed</a> and the copies.</p>
<h2 id="what-an-agent-gets" tabindex="-1">What an agent gets <a class="heading-anchor" href="#what-an-agent-gets" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>An agent that follows the link reads the content and nothing else. The publisher decides what the main content is, because the publisher knows. The copies on this site leave out the header, the footer, and the related posts. The home page's copy, <a href="https://www.vivekchopra.com/index.md">https://www.vivekchopra.com/index.md</a>, is the introduction and the list of posts.</p>
<p>A site can also publish an index of every copy, with a last-modified time on each, announced from <code>robots.txt</code>. An agent that wants the whole site reads the index, fetches the copies, and on its next visit stops when it reaches a date it has already seen. The same way a feed reader syncs a feed. On this site the line sits next to the sitemaps:</p>
<figure class="code-block"><figcaption><span>Code</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Code example"><code><span class="line"><span>Sitemap: https://www.vivekchopra.com/sitemap.xml</span></span>
<span class="line"><span>Sitemap: https://www.vivekchopra.com/feed.xml</span></span>
<span class="line"><span>Altdown: https://www.vivekchopra.com/altdown.json</span></span></code></pre></figure>
<p>The index is <a href="https://www.vivekchopra.com/altdown.json">https://www.vivekchopra.com/altdown.json</a>. It lists the copies. It does not contain them.</p>
<p>The copy may contain only text a person can see on the page. A publisher cannot add keywords, hidden text, or instructions addressed to software, and cannot leave out part of the main content. The one exception is an optional <code>description</code>, capped at 300 characters, which an agent must treat as a hint. The page and the copy also point at each other: the link names the copy, the copy's <code>canonical</code> names the page, and both live on the same origin. If that check fails, the agent reads the HTML. A site that adopts Altdown badly loses nothing it had before.</p>
<p><code>search</code>, <code>ai-input</code>, and <code>ai-train</code> reuse Cloudflare's content signal names. A missing signal means unspecified, so publishing a copy never implies consent to training. Altdown does not decide who may fetch a site. That is still <code>robots.txt</code>. It does not let an agent act on a site. It also differs from <a href="https://www.vivekchopra.com/llms.txt"><code>llms.txt</code></a>, a description of the site at a fixed path that nothing checks against the pages. Altdown is a copy of each page, announced by the page and checkable against it. The two can coexist.</p>
<h2 id="status" tabindex="-1">Status <a class="heading-anchor" href="#status" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>This is version 1.0 in draft. The full specification is at <a href="https://www.altdown.org/spec/" class="external">altdown.org/spec</a>, or <a href="https://www.altdown.org/spec/index.md" class="external">as Markdown</a>. <a href="https://www.altdown.org/" class="external">altdown.org</a> publishes every page in the format it describes. I would like to hear from publishers who try it on a real site, and from people building agents that read the web. <a href="https://github.com/agentwebcommons/altdown/issues" class="external">Open an issue</a> with what breaks.</p>
]]></content:encoded>
      <pubDate>Sat, 31 Oct 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://www.vivekchopra.com/posts/altdown</guid>
      <category>AI-native platforms</category>
    </item>
    <item>
      <title>Plan before you prompt</title>
      <link>https://www.vivekchopra.com/posts/plan-before-you-prompt</link>
      <description>How to steer coding agents with four small documents (architecture, ADR, plan, tasks) so your team makes the design decisions instead of the agent.</description>
      <content:encoded><![CDATA[<p>You can one-shot a trivial application (the so-called &quot;build me a web application and don't make mistakes&quot; prompt). Anything more complex, work that has to be maintained and built upon, needs a more structured workflow.</p>
<p>The same discipline as in the days before AI coding tools applies: define clear product requirements and acceptance criteria, think through the technical design, write down the architecture, break the work into smaller tasks, and implement incrementally with checks and reviews. This post covers that approach: four documents for the architecture, one ADR, a plan, and the tasks, used to steer the coding agent.</p>
<h2 id="the-four-documents" tabindex="-1">The four documents <a class="heading-anchor" href="#the-four-documents" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Each document has a singular purpose:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Document</th>
<th>The question it answers</th>
<th>Lifespan</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ARCHITECTURE.md</code></td>
<td>What does the system look like right now?</td>
<td>Permanent, updated in every branch that changes a boundary</td>
</tr>
<tr>
<td>ADR</td>
<td>Why did we make this one hard-to-reverse decision, and why did the alternatives lose?</td>
<td>Permanent, immutable once accepted</td>
</tr>
<tr>
<td><code>PLAN.md</code></td>
<td>How will this specific piece of work happen, and what is out of scope?</td>
<td>Lives while the branch is open</td>
</tr>
<tr>
<td><code>TASKS.md</code></td>
<td>What are the checkable steps, in order, with a verify command for each?</td>
<td>Lives while the branch is open</td>
</tr>
</tbody>
</table></div>
<p><strong>The architecture document describes what exists today.</strong> Not where the system is headed. Not what was considered and rejected. The moment it mixes current state with future plans, it stops being a reference. Keep the diagram as text (PlantUML or Mermaid) so it lives in git and an agent can update it in the same commit as the code.</p>
<p><strong>An ADR records one decision.</strong> The context states the constraints. The decision is one sentence. For example: &quot;We will inject <code>sleep</code> and <code>now</code> into the client.&quot; Every serious alternative appears, including the obvious shortcut, with the reason it loses. Consequences include the cost. A Confirmation section lists checks a later reviewer or agent can run. Not every change needs an ADR. Write one when a change alters a contract, a service boundary, a lifecycle, or an owner, or when it locks in something hard to reverse. Bug fixes do not qualify.</p>
<p><strong>The plan describes the work and its boundaries.</strong> The boundaries matter as much as the stack. A plan that only lists what to build accumulates scope as the agent reads it, because &quot;why not add auth while I'm here&quot; is a natural inference for a model. Writing down what is out of scope closes that door.</p>
<p><strong>A task is verifiable or it is not a task.</strong> &quot;Improve API handling&quot; is a mood. This is a task:</p>
<figure class="code-block"><figcaption><span>text</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="text example"><code><span class="line"><span>- [ ] T021 Parse rel="next" from the GitHub Link header — src/rate_limited_cli/pagination.py — verify: uv run pytest tests/test_pagination.py</span></span></code></pre></figure>
<p>You can read that checklist after the agent runs and know whether the work happened without opening a single source file. Add <code>Do not …</code> tasks where the work could drift past an ADR, and make the verify step the ADR's own Confirmation check. The rule is then enforced by a test rather than by memory.</p>
<p>Acceptance criteria round out the set. Define &quot;done&quot; before the agent starts: a 404 prints &quot;user not found,&quot; not a traceback; three mocked pages produce exactly three requests; tests pass with no network. The agent then checks against a list you wrote while you were thinking clearly, not one it invents while trying to make tests green.</p>
<h2 id="where-everything-lives" tabindex="-1">Where everything lives <a class="heading-anchor" href="#where-everything-lives" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The layout separates the permanent record from the per-feature working set. Permanent documents sit at the top of <code>docs/</code>. Everything tied to one feature sits in one folder named after that feature.</p>
<figure class="code-block"><figcaption><span>text</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="text example"><code><span class="line"><span>.</span></span>
<span class="line"><span>├── AGENTS.md                      # the one file every agent reads first</span></span>
<span class="line"><span>├── CLAUDE.md                      # one line: "Read AGENTS.md"</span></span>
<span class="line"><span>├── .cursor/rules/                 # Cursor rules, scoped by glob</span></span>
<span class="line"><span>├── .claude/commands/plan.md       # /plan: turn an accepted ADR into a plan and tasks</span></span>
<span class="line"><span>├── .github/PULL_REQUEST_TEMPLATE.md</span></span>
<span class="line"><span>├── docs/</span></span>
<span class="line"><span>│   ├── ARCHITECTURE.md            # the system as it exists today</span></span>
<span class="line"><span>│   ├── adr/</span></span>
<span class="line"><span>│   │   ├── README.md              # index, status legend, when an ADR is required</span></span>
<span class="line"><span>│   │   └── 2026-10-30-split-github-client-into-collaborators.md</span></span>
<span class="line"><span>│   ├── features/</span></span>
<span class="line"><span>│   │   ├── v1-repos-command/      # PLAN.md, TASKS.md, ACCEPTANCE.md, PROMPTS.md</span></span>
<span class="line"><span>│   │   └── resilient-github-client/</span></span>
<span class="line"><span>│   └── templates/                 # ADR, PLAN, TASKS, ACCEPTANCE, PROMPTS sources</span></span>
<span class="line"><span>├── src/</span></span>
<span class="line"><span>└── tests/</span></span></code></pre></figure>
<p>Three choices in that tree are deliberate.</p>
<p><strong>ADRs use a date-slug filename, not a sequence number.</strong> <code>2026-10-30-split-github-client-into-collaborators.md</code> is the ADR's ID. Two engineers on two branches can each add an ADR without a merge conflict over who gets <code>0007</code>. The filename sorts chronologically and reads as a sentence in a PR title.</p>
<p><strong>Each feature owns one folder.</strong> <code>docs/features/&lt;slug&gt;/</code> holds the plan, the task list, the acceptance criteria, and the bounded prompts for that feature. When the branch merges, the folder stays as a record of how the change happened. Nobody reads it by default, and that is fine. The plan links to its ADRs; the ADRs never link back to a plan, because an ADR outlives any one plan.</p>
<p><strong>The architecture doc is singular and lives at the top.</strong> There is one current system, so there is one description of it. Every feature branch that moves a boundary updates it in the same PR, and the PR template asks for that update by name.</p>
<p>The product spec sits outside the repo, in whatever tracker the team uses. The ADR and plan link to it. Scope, deferred items, and the success measure are copied from the spec into the plan, never inferred.</p>
<h2 id="from-decision-to-merge" tabindex="-1">From decision to merge <a class="heading-anchor" href="#from-decision-to-merge" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<figure class="code-block"><figcaption><span>mermaid</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="mermaid example"><code><span class="line"><span>flowchart LR</span></span>
<span class="line"><span>    A[Spec in the tracker] --> B[ADR opened: Proposed]</span></span>
<span class="line"><span>    B --> C[Reviewed in a PR]</span></span>
<span class="line"><span>    C --> D{{Status: Accepted}}</span></span>
<span class="line"><span>    D --> E["/plan: plan and tasks"]</span></span>
<span class="line"><span>    E --> F[One phase per prompt]</span></span>
<span class="line"><span>    F --> G[Review the diff]</span></span>
<span class="line"><span>    G -->|next phase| F</span></span>
<span class="line"><span>    G --> H[Merge, with ARCHITECTURE.md updated]</span></span></code></pre></figure>
<p>The decision is reviewed before any code exists. The code is reviewed one phase at a time.</p>
<ol>
<li>The spec names the problem, the v1 scope, the deferred list, and one success measure. It lives in the tracker, not the repo.</li>
<li>The engineer opens a PR that adds one ADR with <code>Status: Proposed</code>. The PR contains no code.</li>
<li>A reviewer accepts or rejects from the file alone. On rejection the reason goes under Decision and the status becomes <code>Rejected</code>. The file stays as a record.</li>
<li>On agreement the status becomes <code>Accepted</code>. Open questions must be empty first.</li>
<li><code>/plan docs/adr/&lt;id&gt;.md</code> reads the accepted ADR and the spec, inspects the anchors in the code, and drafts the plan and the tasks. It reports gaps instead of filling them. A gap reported is useful. A gap filled with a guess is an unreviewed architectural decision.</li>
<li>Each prompt names one phase: which tasks to implement, what not to implement, which tests to run, and where to stop. Phase 0 is foundation and blocks the rest. Each later phase is one user-visible slice with its own independent check.</li>
<li>You review the diff at the phase boundary. A diff that spans five decisions is a diff whose reasoning you have lost.</li>
<li>The PR that merges the feature also updates <code>ARCHITECTURE.md</code>. The PR template asks for it.</li>
</ol>
<p>The gate is step 4. Nothing downstream begins until a human has accepted the decision in writing.</p>
<h2 id="a-worked-example-rate-limited-cli" tabindex="-1">A worked example: <a href="https://github.com/vivekchopra/rate-limited-cli" class="external">rate-limited-cli</a> <a class="heading-anchor" href="#a-worked-example-rate-limited-cli" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>To make this concrete, the template repo builds a small Python CLI twice. The first pass is v1. The second is a feature branch that adds the parts v1 deliberately left out.</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">rate-limited-cli</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> repos</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> octocat</span></span></code></pre></figure>
<p>The command fetches every public repository for a GitHub user, follows pagination, and prints a summary: repo count, stars, forks, and remaining API quota. GitHub is a good sample API because its edge cases are real. Unauthenticated requests are capped at 60 an hour. Rate limit state arrives in <code>x-ratelimit-*</code> headers. List endpoints paginate through a <code>Link</code> header, and GitHub tells you to follow <code>rel=&quot;next&quot;</code> rather than increment a page number yourself.</p>
<p><strong>v1 needs no ADR.</strong> It is the first version of a small tool, and nothing in it is hard to reverse. It needs a plan, a task list, and acceptance criteria. The plan fixes the stack (Python 3.12, uv, Typer, httpx, pytest with respx, Ruff) and the surface (<code>repos USER --limit --json</code>). It also says what v1 excludes: no authentication, no retry engine, no persistent cache. The client does the minimum correctly. It follows <code>Link: rel=&quot;next&quot;</code> until the link disappears and stops early when <code>--limit</code> is reached.</p>
<p><strong>The second pass is where ADRs earn their keep.</strong> The feature branch adds token authentication, rate-limit handling, retry with backoff, ETag caching, and clearer diagnostics. The design question has changed. It is no longer &quot;how do I call GitHub.&quot; It is how five concerns interact without turning <code>GitHubClient</code> into one large method that does all of them badly. That is a hard-to-reverse decision, so it gets an ADR.</p>
<p>The ADR records the priority order for rate limits. If <code>Retry-After</code> is present, honor it. If the primary limit is exhausted, wait until reset, but only up to a bound. Otherwise treat it as a secondary limit and back off exponentially. It records the split of <code>GitHubClient</code> into four collaborators: <code>AuthConfig</code>, <code>RateLimitPolicy</code>, <code>RetryPolicy</code>, and <code>CacheStore</code>. And it records what lost. Inlining everything in the client is untestable. Always sleeping until reset means a CLI hangs for 40 minutes because that is when the quota resets. A <code>--token</code> flag puts a secret in shell history, so <code>GITHUB_TOKEN</code> from the environment wins. A plugin framework is not needed, so it is not built.</p>
<p>One detail from the ADR is worth keeping concrete, because it decides whether retry code is testable or miserable. Do not call <code>time.sleep</code> and <code>time.time</code> directly. Inject them.</p>
<figure class="code-block"><figcaption><span>Python</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Python example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">class</span><span style="--shiki-light:#953800;--shiki-dark:#FFA657"> GitHubClient</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">:</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    def</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> __init__</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(self, </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">*</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, sleep</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">time.sleep, now</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">time.time):</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">        ...</span></span></code></pre></figure>
<p>A test passes <code>sleep=delays.append</code> and a fixed <code>now</code>, then asserts <code>delays == [5]</code> after a mocked 429 with <code>Retry-After: 5</code>. The suite never waits five seconds. The ADR's Confirmation section turns this into a check an agent can run: grep the client for <code>time.sleep(</code> and expect zero matches. A <code>Do not …</code> task in <code>TASKS.md</code> carries that same check as its verify step.</p>
<p><code>ARCHITECTURE.md</code> changes in the same branch. The diagram that showed <code>CLI -&gt; GitHubClient -&gt; GitHub</code> now shows the four collaborators. The ADR says <em>why</em> the split happened. The architecture doc says <em>what</em> the system looks like now. If the same sentence appears in both, one of them is redundant, and it is usually the architecture doc trying to be a decision log.</p>
<h2 id="wiring-the-agent-to-the-documents" tabindex="-1">Wiring the agent to the documents <a class="heading-anchor" href="#wiring-the-agent-to-the-documents" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The documents only help if the agent reads them. Every tool has its own instruction file, and the fastest way to drift is to maintain four copies of the same rules. Keep one source and point everything else at it.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>File</th>
<th>Read by</th>
<th>What belongs in it</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AGENTS.md</code></td>
<td>Codex, OpenCode, Cursor, and anything that follows the open convention</td>
<td>The project map, the commands to run, the design rules, and the order in which to read <code>docs/</code></td>
</tr>
<tr>
<td><code>CLAUDE.md</code></td>
<td>Claude Code</td>
<td>One line: read <code>AGENTS.md</code>. Nothing else, so the two never disagree</td>
</tr>
<tr>
<td><code>.cursor/rules/*.mdc</code></td>
<td>Cursor</td>
<td>Short rules scoped by glob: <code>src/**</code> says where each concern lives; <code>docs/adr/**</code> says ADRs are immutable once accepted</td>
</tr>
<tr>
<td><code>.claude/commands/plan.md</code></td>
<td>Claude Code as <code>/plan</code></td>
<td>The procedure that turns an accepted ADR into a plan and tasks, with its stop conditions</td>
</tr>
<tr>
<td><code>docs/features/&lt;slug&gt;/PROMPTS.md</code></td>
<td>You, pasting one at a time</td>
<td>The bounded prompts for each phase</td>
</tr>
<tr>
<td><code>.github/PULL_REQUEST_TEMPLATE.md</code></td>
<td>The reviewer</td>
<td>Which ADR this PR implements, which tasks it closes, whether <code>ARCHITECTURE.md</code> changed</td>
</tr>
</tbody>
</table></div>
<p>Three rules keep the instruction files useful.</p>
<p><strong>Tell the agent what to read, in order.</strong> <code>AGENTS.md</code> lists <code>docs/ARCHITECTURE.md</code>, then the ADRs the current branch cites, then the feature folder. An agent that reads the plan before the architecture doc will propose a plan for a system that does not exist.</p>
<p><strong>Tell the agent when to stop.</strong> Every bounded prompt ends with &quot;Update only completed task checkboxes. Stop.&quot; The <code>/plan</code> command stops if the ADR is not <code>Accepted</code>, if the spec cannot be opened, or if a plan file already exists. A procedure without a stop condition becomes a procedure that runs until something breaks.</p>
<p><strong>Treat <code>TASKS.md</code> as the agent's memory.</strong> Agents lose context between sessions. A task list with checkboxes and verify commands lets the next session pick up one unfinished task, run its verify step, and mark it done. That is the same pattern Anthropic describes for long-running agents: an initializer turns the request into a feature list and a progress file, and each later session works one item and leaves the file updated for the next.</p>
<h2 id="what-survives-the-merge" tabindex="-1">What survives the merge <a class="heading-anchor" href="#what-survives-the-merge" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The plan and the task list are useful while the branch is open and mostly useless six months later. Nobody reviewing the code in a year needs <code>docs/features/resilient-github-client/TASKS.md</code> to understand why caching works the way it does. They need the README, the current <code>ARCHITECTURE.md</code>, and the ADR. The feature folder stays in the repo as a record of how the change happened. It is not the primary documentation.</p>
<p>This is not about producing more documentation. It is about making the design decisions before the agent makes them for you by default. Writing the ADR first is slower than typing &quot;add retry logic with backoff&quot; into a prompt. It is also the difference between reviewing a plan you agree with and reverse-engineering a decision from a diff.</p>
<p>The template repo, with the ADR and plan templates, the <code>/plan</code> command, the agent instruction files, and both passes of the sample CLI, is <a href="https://github.com/vivekchopra/rate-limited-cli" class="external">vivekchopra/rate-limited-cli</a>. Clone it, delete the sample feature, and keep the structure.</p>
<h2 id="references" tabindex="-1">References <a class="heading-anchor" href="#references" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p><strong>Decision records</strong></p>
<ul>
<li>Michael Nygard, <a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" class="external">Documenting Architecture Decisions</a> (2011). The original ADR post: context, decision, status, consequences.</li>
<li><a href="https://adr.github.io/madr/" class="external">MADR: Markdown Architectural Decision Records</a>. The template this ADR shape most resembles, including decision drivers, considered options, and a Confirmation section.</li>
</ul>
<p><strong>Spec, plan, and tasks</strong></p>
<ul>
<li><a href="https://github.com/github/spec-kit" class="external">GitHub Spec Kit</a>. Constitution, spec, plan, tasks, implement. Source of the T001 numbering, <code>[P]</code> parallel markers, phased tasks, and a cross-artifact consistency check before implementation.</li>
<li><a href="https://github.com/github/spec-kit/issues/1934" class="external">Spec Kit issue #1934</a>. Proposes explicit <code>(depends on T001)</code> suffixes, because <code>[P]</code> alone does not tell an agent which tasks are safe to run together.</li>
<li><a href="https://kiro.dev/docs/specs/" class="external">AWS Kiro: Specs</a>. Requirements, design, and tasks files. Each task lists the requirement IDs it covers, and independent tasks run in dependency-ordered waves.</li>
</ul>
<p><strong>Agents working across sessions</strong></p>
<ul>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents" class="external">Effective harnesses for long-running agents</a>. An initializer turns the request into a feature list, a progress file, and git history; each later session picks one unfinished feature, verifies it, then marks it done.</li>
<li>Anthropic, <a href="https://www.anthropic.com/engineering/claude-code-best-practices" class="external">Claude Code: Best practices for agentic coding</a>. Plan before coding, and keep a checklist in a file for multi-step work.</li>
<li><a href="https://agents.md/" class="external">AGENTS.md</a>. The open convention for a single agent instruction file that Codex, Cursor, and others read.</li>
</ul>
<p><strong>Sample project</strong></p>
<ul>
<li><a href="https://github.com/vivekchopra/rate-limited-cli" class="external">vivekchopra/rate-limited-cli</a>. The sample CLI and the docs that shaped it.</li>
<li><a href="https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api" class="external">GitHub REST API: Using pagination</a> and <a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api" class="external">Rate limits</a>. The upstream behavior the sample client is built against.</li>
</ul>
]]></content:encoded>
      <pubDate>Sat, 03 Oct 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://www.vivekchopra.com/posts/plan-before-you-prompt</guid>
      <category>AI-native platforms</category>
    </item>
    <item>
      <title>Guardrails for AI-generated code</title>
      <link>https://www.vivekchopra.com/posts/ai-generated-code-guardrails</link>
      <description>A four-layer guardrails model for AI-generated code (scope, structural, behavioral, human verification), plus notes on a shrinking architectural half-life.</description>
      <content:encoded><![CDATA[<p>The share of AI-generated code inside many engineering organizations has been climbing sharply over the past year. The distribution is uneven, but I now often hear engineers say that nearly all of the code they write is AI-assisted in some form, whether through prompt-and-paste workflows in chat windows, copilots embedded in editors, or autonomous agents operating inside CI pipelines.</p>
<p>The upside is obvious: higher velocity. As an engineering manager, how do you mitigate the downside? Will an AI-generated change break production? Worse: will it pass every unit test, integration test, and CI check you have and still introduce a bug that surfaces weeks later? And what happens to the architecture itself as the rate of code change climbs and the half-life of design assumptions shrinks?</p>
<p>I increasingly see AI-generated code fail in ways that differ from human-authored code: confident calls to APIs that do not exist, change sets much larger than a human would typically author for the same task, and tests that pass because they encode the same incorrect assumptions as the implementation itself. I have been working through a layered model of guardrails to mitigate some of these issues:</p>
<ol>
<li><strong>Scope and authority:</strong> what the AI is allowed to touch.</li>
<li><strong>Structural:</strong> what the code must look like before it reaches review.</li>
<li><strong>Behavioral:</strong> what the code must prove it actually does.</li>
<li><strong>Human verification:</strong> what a person must check before merge.</li>
</ol>
<p>(My deeper concern though is architectural drift: AI systems are good at producing locally reasonable changes, but they do not naturally preserve long-term consistency across an entire system. Over time, this creates what I think of as <strong>“architecture slop”</strong>: a codebase where each individual change makes sense in isolation while the overall structure gradually loses coherence. More on that in a follow-up post!)</p>
<h2 id="layer-1-scope-and-authority" tabindex="-1">Layer 1: Scope and authority <a class="heading-anchor" href="#layer-1-scope-and-authority" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Define as engineering policy what AI can do without escalation, what requires review, and what should be largely human-authored.</p>
<ul>
<li><strong>Green zones:</strong> This is where AI can generate code freely. POCs/prototypes, Internal tooling, test scaffolding, documentation, greenfield features behind a feature flag, build scripts, log parsers, one-off data migrations.</li>
<li><strong>Yellow zones:</strong> AI can suggest, but a human authors the final commit. User-facing flows, database schemas, public APIs, anything that crosses a service boundary.</li>
<li><strong>Red zones:</strong> AI can draft or assist with review, but never authors the final commit, and a senior reviewer signs off in addition. Terraform scripts, Authentication and session handling, payment processing, anything that touches PII or regulated data, security-sensitive code paths, cryptographic primitives.</li>
</ul>
<p>A concrete example: SQL migrations. An AI agent can be good at drafting them, but they belong in a yellow zone, which means the agent can draft the migration but a human has to read it, run it locally, and sign off before it runs against staging. The agent does most of the rote work, and the human catches the one in twenty cases where the migration would have locked a high-traffic table during business hours or dropped an index that some downstream report was depending on. Putting migrations fully into the green zone would make those rare failures much more dangerous.</p>
<p>Across all three zones, the engineer who merges the change owns it, whether or not an AI wrote it.</p>
<p>The other dimension worth defining is the scope of any single change. Adding a new endpoint that lives in its own module is a bounded change with a contained blast radius. Refactoring the request pipeline across twenty files is not. The first is fine for AI to handle; the second is exactly where AI tends to make confident, sweeping mistakes that take hours to unpick.</p>
<p>As an example, engineering policies could limit AI-generated code to one new module per PR, and require authors to write a summary in their own words for any PR that crosses module boundaries or changes more than 300 lines.</p>
<h2 id="layer-2-structural-guardrails" tabindex="-1">Layer 2: Structural guardrails <a class="heading-anchor" href="#layer-2-structural-guardrails" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>These are automated checks that run before a human ever reviews the code, and they catch the bad habits AI-generated code tends to exhibit.</p>
<h3 id="dependency-policy-checks" tabindex="-1">Dependency policy checks <a class="heading-anchor" href="#dependency-policy-checks" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>AI-based coding tools are enthusiastic about pulling in libraries. They often import something that solves a problem you already have a solution for, or pick an abandoned package that has not seen a commit in years. A written justification for any new dependency, even one or two lines in the PR description, makes someone stop and ask whether the dependency is actually needed. Every new dependency should be a deliberate choice and not just an autocomplete.</p>
<h3 id="hallucinated-api-checks" tabindex="-1">Hallucinated API checks <a class="heading-anchor" href="#hallucinated-api-checks" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>These checks matter most in dynamically typed languages, where calls to non-existent methods only surface when the code actually runs. An undefined-symbol linter (pyflakes, ESLint's no-undef) catches the easy cases: importing a module that does not exist, or calling a function that is not defined in the codebase or its dependencies. A type checker in strict mode (mypy, pyright, the TypeScript compiler) catches the harder ones, including invented methods on real objects (<code>user.getDisplayName()</code> when the real method is <code>user.displayName</code>) and return values used as if they could not be <code>None</code>.</p>
<p>Take the last case as a worked example. The AI generates:</p>
<figure class="code-block"><figcaption><span>Python</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Python example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">def</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> get_user_email</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(user_id: </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">str</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">) -> </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">str</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">:</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    user </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> db.users.find_one({</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"id"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">: user_id})</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> user.email</span></span></code></pre></figure>
<p>mypy or pyright in strict mode rejects this immediately: <code>find_one</code> returns <code>Optional[User]</code>, not <code>User</code>, so <code>user.email</code> will throw on the unhappy path. A human reviewer would probably miss it on first read because the code looks reasonable. A type checker catches it on every run, and most editors flag it in real time as the engineer types.</p>
<h2 id="layer-3-behavioral-guardrails" tabindex="-1">Layer 3: Behavioral guardrails <a class="heading-anchor" href="#layer-3-behavioral-guardrails" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<figure class="quote-card"><blockquote cite="https://x.com/copyconstruct/status/2054691951681970608"><p>end-to-end testing &gt; unit tests, in the vibecoding era. A massive, almost entirely agent-coded refactor passed all unit and pre-merge tests but broke a critical feature. It was only caught due to my own excessive paranoia making me run end-to-end tests before the prod deploy.</p></blockquote><figcaption>Cindy Sridharan (@copyconstruct), <a href="https://x.com/copyconstruct/status/2054691951681970608">May 2026 on X</a></figcaption></figure>
<figure class="quote-card"><blockquote cite="https://x.com/fatih/status/2054708456448508174"><p>I was a huge unit test supporter, but honestly, it’s no longer worth it. Agents are superb at writing extremely bad unit tests, and they still look good on paper. We’re also shifting slightly to more and more e2e tests at @PlanetScale. Luckily with agents, that shift is also manageable.</p></blockquote><figcaption>Fatih Arslan (@fatih), <a href="https://x.com/fatih/status/2054708456448508174">May 2026 on X</a></figcaption></figure>
<p>The traditional <a href="https://martinfowler.com/articles/practical-test-pyramid.html" class="external">testing pyramid</a> puts unit tests at the wide base, integration tests in the middle, and end-to-end tests at the narrow top. It emphasized unit tests because they are fast, relatively stable, and cheap to run repeatedly. End-to-end tests, by contrast, have historically been slower, more brittle, and more expensive to maintain.</p>
<p>AI systems are extremely good at generating unit tests that appear thorough while proving very little, because the tests often encode the same assumptions and blind spots as the implementation they are validating.</p>
<p>Large agent-coded refactors can pass full unit-test suites and pre-merge checks and still break critical end-to-end behavior. Unit tests will still have a role, but the key guardrail needs to shift to end-to-end and behavioral tests.</p>
<p>I see two major shifts happening in how we test.</p>
<p>The first is around test scope. There is a class of bugs in AI-generated systems that will routinely escape unit-test guardrails. For example, an endpoint may call the correct helpers, but in the wrong order. Every unit test against those helpers will still pass because each helper behaves correctly in isolation. An end-to-end test is often the only thing that catches that the system as a whole no longer behaves the way users expect. For every user-facing workflow that matters, there should be at least one end-to-end test that exercises the full stack.</p>
<p>Historically, end-to-end tests were avoided because they were slower, flakier, and more expensive to maintain. That tradeoff is beginning to shift now that AI systems themselves can generate and maintain large portions of the test suite.</p>
<p>The second shift applies across every level of testing. Tests should validate what the system does, not how the implementation happens to be organized internally. A behavioral test passes when the system produces the correct output for a given input. A structure-coupled test passes only when the internals follow a particular shape: a specific field was mutated, a helper was called, or a sequence of operations occurred in a specific order.</p>
<p>Consider a function that calculates shipping cost. Both tests below are unit tests; the difference is in what they assert.</p>
<figure class="code-block"><figcaption><span>Python</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Python example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">def</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> calculate_shipping</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(order: Order) -> Decimal:</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    weight </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> sum</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(item.weight </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">for</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> item </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">in</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> order.items)</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    zone </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> lookup_zone(order.destination)</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    base </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> WEIGHT_TABLE</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">[zone][weight_bucket(weight)]</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    return</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> base </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">+</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> tax_for(order.destination, base)</span></span></code></pre></figure>
<p>A structure-coupled test might look like this:</p>
<figure class="code-block"><figcaption><span>Python</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Python example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">def</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> test_calculate_shipping_calls_dependencies</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">():</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    mock_zone </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> patch(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"shipping.lookup_zone"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="--shiki-light:#953800;--shiki-dark:#FFA657">return_value</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"ZONE_A"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">)</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    mock_tax </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> patch(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"shipping.tax_for"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, </span><span style="--shiki-light:#953800;--shiki-dark:#FFA657">return_value</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">Decimal(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"1.50"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">))</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    with</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> mock_zone, mock_tax:</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">        result </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> calculate_shipping(sample_order)</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    assert</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> mock_zone.called</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    assert</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> mock_tax.called_with(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"US-CA"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">, Decimal(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"10.00"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">))</span></span></code></pre></figure>
<p>This test passes only when the internals are arranged a specific way. The moment you regenerate the function and the AI inlines <code>lookup_zone</code> or combines the tax calculation into the base lookup, the test breaks even though the code is still correct. The test is punishing you for refactoring.</p>
<p>A behavioral test for the same function:</p>
<figure class="code-block"><figcaption><span>Python</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Python example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">def</span><span style="--shiki-light:#8250DF;--shiki-dark:#D2A8FF"> test_calculate_shipping_returns_correct_total</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">():</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    order </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> Order(</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">        items</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">[Item(</span><span style="--shiki-light:#953800;--shiki-dark:#FFA657">weight</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">Decimal(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"2.5"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">))],</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">        destination</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"US-CA"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">,</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">    )</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">    assert</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> calculate_shipping(order) </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">==</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> Decimal(</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"12.47"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">)</span></span></code></pre></figure>
<p>This passes whenever the function produces the right answer, regardless of how it gets there. The implementation can be regenerated repeatedly, and as long as the behavior remains correct, the test continues to pass.</p>
<p>Structure-coupling matters more under AI-generated code than it did under human-authored code because of a higher rate of implementation churn. A human refactoring a module often has a mental model of how that code should be organized and code tends to preserve that shape over time. An AI regenerating the module will happily restructure things in ways that are functionally equivalent but structurally very different. Tests coupled to structure become noisy with every code regeneration, and will end up being ignored.</p>
<h3 id="what-about-unit-tests" tabindex="-1">What about unit tests? <a class="heading-anchor" href="#what-about-unit-tests" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>This does not mean unit testing should be abandoned. End-to-end tests do not tell you where the breaking change is, and the combinatorial logic paths in your code cannot all be exercised by end-to-end tests alone.</p>
<p>You need to understand which unit tests should be kept. A useful heuristic: if you regenerated this code tomorrow, would the test still make sense? If yes, keep it. If it would break just because the internal implementation shifted, then rewrite it against the public interface or delete it.</p>
<h3 id="practical-behavioral-test-guardrails" tabindex="-1">Practical behavioral test guardrails <a class="heading-anchor" href="#practical-behavioral-test-guardrails" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<ul>
<li><strong>Require integration tests for new public interfaces.</strong> Every new endpoint, queue handler, or exported function gets at least one test that calls it the way a real caller would. One happy path and one realistic failure case are usually enough to catch the things mocks let through.</li>
<li><strong>Use property-based tests for combinatorial logic.</strong> Tools like Hypothesis in Python or fast-check in JavaScript generate inputs the AI did not think of, which is precisely the failure mode AI-generated code is prone to: empty inputs, unicode strings, negative numbers, off-by-one boundaries at the limits of an integer range.</li>
<li><strong>Require additional human review whenever AI-generated changes modify both implementation and behavioral expectations test simultaneously.</strong> One common failure mode is an AI system “fixing” a failing test by weakening the assertions rather than addressing the underlying issue.</li>
<li><strong>Measure coverage on the diff, not the whole codebase.</strong> Whole-codebase thresholds are easy to game: engineers add tests to easy code to keep the number up, and the hard code stays untested. Coverage on changed lines is honest. Setting the threshold at 80% on changed code routinely takes coverage of new code from the mid-40s to the high-70s within a quarter, with the only visible behavior change being that engineers stop shipping untested branches.</li>
</ul>
<h2 id="layer-4-human-verification" tabindex="-1">Layer 4: Human verification <a class="heading-anchor" href="#layer-4-human-verification" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The last layer is what a human must do before code merges. Generic code review is not enough, because reviewers skim AI-generated code the same way they skim human code, and AI code is plausible-looking in ways that defeat skimming. The classic failure is the reviewer who approves with “LGTM” on a 400-line AI-generated diff.</p>
<p>How deep human review needs to go depends on the zone (Layer 1). In green zones, an AI code reviewer in CI is enough on its own; the volume of internal tooling and test scaffolding being generated does not justify pulling a human onto every PR, and the blast radius if something is wrong is small. In yellow zones, the AI reviewer clears the obvious issues but a human reviewer is required on top, with the three practices below. In red zones, the AI reviewer is advisory at best, never gating: a senior reviewer with explicit authority over the code path signs off in addition to the three practices, and for the most sensitive code (payments, auth, crypto, regulated data) a second human reviewer is worth the friction.</p>
<p>Three practices may help reviewers avoid skim mode on yellow- and red-zone AI-generated PRs (especially red zone!):</p>
<ul>
<li>
<p><strong>Require the reviewer to write or modify at least one test as part of the review.</strong> This forces engagement with the behavior, not just the diff. If you cannot write a test for it, you do not understand it well enough to approve it. This sounds like overhead, and it is, but the time it costs is small compared to the time spent debugging things that were rubber-stamped.</p>
</li>
<li>
<p><strong>Require a written justification for any new dependency in the PR description before review starts.</strong> This is the same practice from Layer 2 but enforced by a human reviewer, and it catches cases the linter does not, such as a dependency added through a transitive upgrade rather than a direct import. The forcing function matters more than the prose itself; even one sentence is usually enough to prevent the fifth date library from landing in <code>package.json</code>.</p>
</li>
<li>
<p><strong>Require the PR author to write the diff explanation in the description without AI assistance, for any change above a certain size.</strong> An author who cannot do this has not read the code carefully enough to ship it. This is uncomfortable to enforce, but it forces the engineer to spend time understanding the change.</p>
</li>
</ul>
<h2 id="planning-for-a-shrinking-architectural-half-life" tabindex="-1">Planning for a shrinking architectural half-life <a class="heading-anchor" href="#planning-for-a-shrinking-architectural-half-life" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Guardrails keep individual changes safe. They do not solve the longer-term problem, which is that all software architecture ages. Most codebases start showing their age around the two-year mark, when accumulated decisions and shifting business requirements begin to outgrow the architecture they were originally built around.</p>
<p>Several forces are compressing the effective lifespan of software architecture at the same time.</p>
<p>The first is sheer <strong>code volume</strong>. AI dramatically lowers the cost of producing code, which means systems accumulate complexity much faster than before.</p>
<p>The second is <strong>implementation churn</strong>. Decisions that teams previously deferred for months can now be regenerated in an afternoon. Each individual change may be locally reasonable, but over time the cumulative effect pushes the architecture in inconsistent directions.</p>
<p>The third is <strong>faster product cycles</strong>. AI lowers the cost of shipping software, which means competitors iterate faster, customer expectations shift faster, and requirements evolve more rapidly.</p>
<p>The fourth is what I think of as <strong>“local coherence with global drift”</strong>. Each individual change looks reasonable because the AI optimized it against the immediate context it was given. But no single mind consistently holds the broader architectural model in view, so consistency gradually erodes: patterns emerge that were never deliberately chosen, conventions drift, and abstractions appear that nobody consciously designed. Over time, the codebase begins to resemble a city where every block was designed independently, without a shared plan. That is what I would describe as <strong>“architecture slop”</strong>.</p>
<p>Taken together, these forces likely compress the traditional two- to three-year architectural refactor cycle significantly, especially in fast-moving products. Refactoring at a two-year mark may itself become too slow and too expensive to keep pace.</p>
<p>More on this in a follow-up post!</p>
<h2 id="closing-thoughts-and-caveats" tabindex="-1">Closing thoughts and caveats <a class="heading-anchor" href="#closing-thoughts-and-caveats" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Most of us are still figuring out what effective guardrails for AI-generated code actually look like. The ideas above are not a finished framework, but rather an exercise in thinking in public.</p>
<p>What is clear though is that we are dealing with far larger volumes of code, much higher rates of change, and failure modes that differ from traditional human-authored software. The next couple of years of engineering management are going to be about figuring out which practices need to change, and how.</p>
]]></content:encoded>
      <pubDate>Sun, 17 May 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/ai-generated-code-guardrails</guid>
      <category>AI-native platforms</category>
      <category>Operational reliability</category>
    </item>
    <item>
      <title>Death to Scrum?</title>
      <link>https://www.vivekchopra.com/posts/death-to-scrum</link>
      <description>If the process of software development is changing this dramatically, what happens to Scrum?</description>
      <content:encoded><![CDATA[<p>I wrote recently about how AI-assisted development is <a href="https://www.vivekchopra.com/posts/new-sdlc">reshaping the software development lifecycle</a>. Specs are becoming the primary artifact, validation is becoming continuous, and implementation is collapsing from weeks to hours.</p>
<p>That led to an interesting question in the <a href="https://www.linkedin.com/feed/update/urn:li:activity:7454899311416459264/?dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287457772382712209408%2Curn%3Ali%3Aactivity%3A7454899311416459264%29" class="external">comments</a>: if the SDLC is changing this dramatically, what happens to Scrum?</p>
<p>My short answer on LinkedIn: probably not in the form most teams practice today. This post expands on that with a bit more reflection and some predictions.</p>
<figure class="figure figure-narrow"><a href="https://www.vivekchopra.com/assets/death-to-scrum.png"><img src="https://www.vivekchopra.com/assets/death-to-scrum.png" alt="Death to Scrum: parody of the Death to Smoochy movie poster" loading="lazy"></a><figcaption>(With apologies to <a href="https://en.wikipedia.org/wiki/Death_to_Smoochy">Death to Smoochy</a>; all rights belong to their respective owners)</figcaption></figure>
<h2 id="what-the-original-agile-manifesto-got-right" tabindex="-1">What the original Agile Manifesto got right <a class="heading-anchor" href="#what-the-original-agile-manifesto-got-right" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>In 2001, seventeen software developers met at Snowbird, Utah and wrote a <a href="https://agilemanifesto.org" class="external">68-word document</a> that reshaped how we build software. The Agile Manifesto valued:</p>
<ul>
<li>Individuals and interactions over processes and tools</li>
<li>Working software over comprehensive documentation</li>
<li>Customer collaboration over contract negotiation</li>
<li>Responding to change over following a plan</li>
</ul>
<p>The important thing is that these were principles, not procedures.</p>
<p>I believe that the manifesto and the twelve principles behind these values still hold up: continuous delivery of valuable software, welcoming change, sustainable pace, technical excellence, self-organizing teams, and regular reflection. The core insight still holds: teams doing uncertain work need fast feedback loops.</p>
<h2 id="scrum-is-what-got-built-on-top" tabindex="-1">Scrum is what got built on top <a class="heading-anchor" href="#scrum-is-what-got-built-on-top" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Over twenty-five years, &quot;agile&quot; became Scrum, Scrum became ceremonies, ceremonies became calendar invites, and calendar invites became overhead. Most engineering teams today practice some version of Scrum: sprint planning, daily standups, sprint reviews, retrospectives, story point estimation, velocity tracking, burndown charts, backlog grooming.</p>
<p>The original manifesto warned about this in its first value, individuals and interactions over processes and tools. The warning did not survive contact with enterprise project management.</p>
<p>This is not a screed against Scrum, which I have been <a href="https://www.vivekchopra.com/posts/nine-years-of-scrum">guilty of in the past</a>. Scrum solved real problems. It was an implementation strategy optimized for the economics of software development in the early 2000s: expensive engineering capacity, slow feedback loops, difficult deployments, and limited visibility into work. It gave teams a regular cadence, made work visible, and created accountability without micromanagement. For a generation of engineering teams, it was a meaningful improvement over what came before. The question is whether the conditions that made Scrum useful still hold.</p>
<h2 id="what-scrum-was-actually-for" tabindex="-1">What Scrum was actually for <a class="heading-anchor" href="#what-scrum-was-actually-for" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Most of Scrum's machinery exists to manage handoffs and coordinate scarce engineering capacity.</p>
<ul>
<li><strong>Sprint planning</strong> exists because humans cannot reliably predict how long work will take. Story points and planning poker were never really about precision. They were social tools for surfacing disagreement and forcing conversations about uncertainty.</li>
<li><strong>Standups</strong> exist because work is distributed across people who cannot see each other's progress. The ritual creates a forced synchronization point to surface blockers early.</li>
<li><strong>Sprint reviews</strong> exist because product, engineering, and stakeholders do not have a shared view of what is being built. The demo is the moment of truth.</li>
<li><strong>Retrospectives</strong> exist because teams need a structured space to examine their own process, away from the pressure of delivery.</li>
<li><strong>Backlog grooming and triage</strong> exist because issues enter the system in inconsistent shape and someone has to do the work of reading, refining, and routing them.</li>
<li><strong>Sprint metrics</strong> like velocity, burndown, and cycle time exist because leadership needs predictability and visibility into work that is otherwise opaque.</li>
</ul>
<p>Each of these solved a real problem: coordinating between humans is expensive, and software organizations evolved processes to manage that cost.</p>
<h2 id="what-changes-when-agents-enter-the-system" tabindex="-1">What changes when agents enter the system <a class="heading-anchor" href="#what-changes-when-agents-enter-the-system" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>AI tools change the underlying conditions:</p>
<ul>
<li><strong>Mechanical implementation capacity is becoming dramatically less scarce.</strong> Translating requirements into working software increasingly takes hours instead of weeks. The bottleneck shifts from writing code to deciding what should exist, validating it, and safely operating it. The constraint is no longer &quot;how do we maximize the throughput of our limited engineering team&quot; but &quot;how do we ensure what gets generated is correct, safe, and worth shipping.&quot;</li>
<li><strong>Estimation is becoming vestigial.</strong> When implementation collapses to hours, the question is no longer &quot;how long will this take&quot; but &quot;is this worth doing&quot; and &quot;is the spec good enough to execute against.&quot; Story points are an answer to a question we are no longer asking.</li>
<li><strong>Handoffs are being absorbed into agents.</strong> Triage, grooming, routing, and even initial drafting can be done by agents that have access to customer feedback, internal context, and the codebase. Linear is already building for this with <a href="https://linear.app/intake" class="external">Intake</a> and <a href="https://linear.app/next" class="external">the broader vision they describe for issue tracking</a>: support tickets, Slack threads, and customer call transcripts can be auto-converted into well-scoped issues, deduplicated, routed, and labeled. It does not eliminate product thinking or engineering judgment, just the procedural translation work. The triage meeting becomes optional.</li>
<li><strong>Coordination overhead changes shape.</strong> Teams still need architectural alignment, operational ownership, security review, and prioritization. What shrinks is the amount of procedural synchronization required just to keep implementation moving.</li>
<li><strong>Sprint cadence is increasingly arbitrary.</strong> Two-week sprints made sense when shipping was hard. When deployment is continuous and validation is automated, the sprint boundary increasingly becomes a calendar artifact rather than a delivery constraint.</li>
</ul>
<p>The original manifesto is not wrong, but the rituals that grew on top of it were optimized for a constraint that is dissolving.</p>
<h2 id="a-new-manifesto" tabindex="-1">A new manifesto? <a class="heading-anchor" href="#a-new-manifesto" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>If we wrote the Agile Manifesto today, knowing what we know about AI-assisted development, the values would not change much. Working software over comprehensive documentation. Customer collaboration over contract negotiation. Responding to change over following a plan.</p>
<p>The manifesto ages surprisingly well, but the Scrum workflows built around it do not:</p>
<ul>
<li><strong>Specs and intent over tickets and stories.</strong> When code can be generated in minutes, the scarce thing is the precise description of what to build. Engineering judgment moves from implementation to specification.</li>
<li><strong>Continuous flow over sprint cadence.</strong> When work moves at the speed of agent throughput and validation is continuous, fixed sprints become less meaningful as the primary unit of planning and delivery. The discipline shifts from &quot;what fits in this sprint&quot; to &quot;what is worth doing now and what does done look like.&quot;</li>
<li><strong>Validation and safety over raw throughput.</strong> As implementation accelerates, the bottleneck increasingly shifts to testing, review, security, observability, and release confidence. The challenge is no longer generating code quickly, but ensuring what gets generated is correct, safe, maintainable, and production-ready.</li>
<li><strong>Outcomes over velocity.</strong> Velocity measures how much a team produces, which made sense when production was the bottleneck. The new bottleneck is whether what gets produced is correct, useful, and shipped. Track outcomes, not output.</li>
<li><strong>Human judgment over human throughput.</strong> When agents do more of the procedural work, the human contribution shifts toward intent, taste, judgment, and accountability. The team structure that made sense for human throughput does not necessarily fit a world where one engineer with strong domain knowledge and good agent orchestration can often execute work that previously required multiple coordinated implementation roles.</li>
</ul>
<p>How do we evolve the engineering team toward this?</p>
<h2 id="gradual-evolution" tabindex="-1">Gradual evolution <a class="heading-anchor" href="#gradual-evolution" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The shift will likely not happen all at once, and most teams will resist it for the same reason most teams resisted agile in 2003: the existing process is what they are comfortable with, and the new one is unfamiliar and unproven. Given this, some practical changes that could work and are not too disruptive:</p>
<ol>
<li>
<p><strong>Start with intake, not sprints.</strong> The most leveraged early change is automating triage and intake. Agents can convert support tickets, Slack threads, and customer calls into well-scoped issues with the right labels and routing. This reduces the meeting load immediately and proves out the pattern that agents can do procedural work well.</p>
</li>
<li>
<p><strong>Replace estimation with prioritization.</strong> Stop running estimation rituals. Instead, ask two questions for each piece of work: is this worth doing, and is the spec good enough that someone (human or agent) can execute against it. If the answer to either is no, the work is not ready. Story points add no information that a yes/no on these two questions does not already provide.</p>
</li>
<li>
<p><strong>Make (most) standups async.</strong> The daily standup is the easiest ceremony to retire. Replace it with an async update from issue tracker activity. Keep a weekly sync for things that genuinely need synchronous discussion (architecture, blockers, customer issues), and let the rest happen through writing.</p>
</li>
<li>
<p><strong>Treat the spec as the artifact.</strong> Code review will not scale if the team is generating ten times more code. The leverage point moves earlier in the process and spec review becomes the new code review. The questions the spec review should answer:</p>
<ul>
<li>What behavior are we expecting</li>
<li>What are the important edge cases</li>
<li>What non-functional constraints matter (e.g. security, reliability, performance)</li>
<li>How will we know the result is correct</li>
</ul>
<p>An engineer and a PM should align on this before any code is generated.</p>
</li>
<li>
<p><strong>Keep sprint reviews, but reframe retrospectives.</strong> Demos still matter: showing working software to stakeholders and customers is one of the most valuable things a team does. Keep those. Retrospectives also matter, but the focus shifts from &quot;how did our process work&quot; to &quot;what should change in our process.&quot; Use retros to question the rituals themselves, not just refine them.</p>
</li>
<li>
<p><strong>Be deliberate about which ceremonies to keep.</strong> Retiring all of Scrum at once is a bad idea. Some ceremonies serve a real purpose beyond coordination, especially as a team is learning to work this way. Keep the ones that do, and gradually drop the ones that have become routine. The test should be simple: if the meeting were canceled, would anything actually break?</p>
</li>
</ol>
<h2 id="metrics-for-the-new-world" tabindex="-1">Metrics for the new world <a class="heading-anchor" href="#metrics-for-the-new-world" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The old Scrum metrics measured human throughput inside a fixed cadence. They are mostly the wrong metrics now.</p>
<p>The metrics that will survive the transition are the ones tied to outcomes and flow:</p>
<ul>
<li><strong>Lead time</strong> i.e. time from idea to production. This is the most important metric: as you compress the SDLC with AI tooling, lead time should shrink visibly. If it is not shrinking, what you are doing is not delivering value at the pace it should.</li>
<li><strong>Cycle time</strong> from in-progress to done, per piece of work. This is useful for finding bottlenecks. If cycle time is going up while lead time is going down, work is sitting in queues somewhere (most commonly: in review).</li>
<li><strong>Defect escape rate.</strong> Bugs found in production divided by bugs found before production. As code generation accelerates, this is the canary metric for whether your validation is keeping up.</li>
<li><strong>Spec quality.</strong> This is harder to measure quantitatively, but observable: how often does work get rejected at spec review, how often does generated output need significant rework, how often does a finished feature not match the original intent. These are leading indicators of whether the team is treating the spec as the primary artifact.</li>
<li><strong>Time to validate a hypothesis.</strong> From &quot;we think users want X&quot; to &quot;we have evidence one way or the other.&quot; As prototyping gets faster and feedback loops tighten, this number should shrink dramatically. If it is not shrinking, you are using faster tools without faster thinking.</li>
<li><strong>Validation latency.</strong> Cycle time measures total time in progress. Validation latency measures specifically how long work waits in testing, review, security checks, and deployment validation queues before it is trusted enough to ship to production. As code generation accelerates, this is where most of the cycle time will live.</li>
<li><strong>Customer-reported issues per release.</strong> This is an old metric, but it is still valuable, and is the ultimate test of whether faster shipping is also better shipping.</li>
</ul>
<p>The metrics to retire or deprioritize:</p>
<ul>
<li><strong>Velocity</strong> assumes humans are the bottleneck and story points are the unit of throughput: both of which are decreasingly true.</li>
<li><strong>Sprint burndown</strong> assumes a fixed sprint boundary, which is increasingly artificial.</li>
<li><strong>Planned-to-done ratio</strong> measures how good a team is at predicting work in a sprint, which matters less when sprints are less rigid.</li>
</ul>
<p>These metrics measure the wrong layer: they tell you how predictable your sprint is, not whether you are building the right thing or shipping it well.</p>
<h2 id="principles-not-procedures" tabindex="-1">Principles, not procedures <a class="heading-anchor" href="#principles-not-procedures" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The Agile Manifesto's four values and twelve principles will survive intact. The principles translate, but the Scrum procedures built on top of them won't. These were tools optimized for a world where implementation throughput and human coordination were the dominant constraints. As those constraints dissolve, the tools become overhead.</p>
<p>The original Agile Manifesto came out of frustration with waterfall. The next evolution will come from teams who realized parts of Scrum had become ceremony for ceremony's sake and started adopting workflows that better match how software gets built with AI-assisted tools.</p>
<p>Right now, most teams are layering new tools onto old processes. What you get is a faster version of the same process, not a different one.</p>
]]></content:encoded>
      <pubDate>Thu, 07 May 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/death-to-scrum</guid>
      <category>Scaling engineering orgs</category>
      <category>AI-native platforms</category>
    </item>
    <item>
      <title>The New SDLC</title>
      <link>https://www.vivekchopra.com/posts/new-sdlc</link>
      <description>AI is compressing the development cycle and changing how product, engineering, design, QA, and security teams operate.</description>
      <content:encoded><![CDATA[<h2 id="traditional-software-development-lifecycle" tabindex="-1">Traditional Software Development Lifecycle <a class="heading-anchor" href="#traditional-software-development-lifecycle" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Over the past decade, most mature engineering teams have converged on a shared understanding of what a well-run software development lifecycle should look like to build software that meets customer requirements, and is reliable, supportable, scalable, and secure.</p>
<p>The process usually starts with getting to a clear understanding of what should be built and why. That is where the Product Manager or Product Team comes in. This initial phase, often called product discovery, involves customer interviews to refine the problem, and may also include building Figma prototypes or a simple mocked UX to gather customer feedback. The output of this phase is a set of requirements for the engineering team.</p>
<p>Once there is confidence in the product direction, the team moves into a more structured flow. Requirements are written down in a way that engineering can act on, typically as a Product Requirements Document (PRD). Work is then broken into smaller pieces with clear acceptance criteria, often tracked as tickets in tools like Jira or Linear. At this point, engineering teams also start thinking about how the feature will be tested. What are the critical paths? Where are the edge cases? What would failure look like?</p>
<p>In parallel, the design team works on wireframes, some of which may have been used during product discovery, and evolves them into a more refined user experience and pixel-perfect UI. This ideally builds on an existing design system, which provides a centralized set of reusable components and standards.</p>
<p>Mature engineering organizations typically create an engineering design document at this stage. This document is reviewed by other engineers, and <a href="https://adr.github.io/" class="external">key technical decisions are recorded</a>. The design also includes security and reliability considerations. This may involve building a lightweight threat model to think through how the system could be misused. Where does data enter the system? What are the trust boundaries? What happens if users try to access something they should not?</p>
<p>The team also defines a test and release strategy. Testing includes unit tests for core logic, integration tests for service interactions, and end-to-end tests for critical workflows. Smoke tests are defined to ensure that core functionality does not break with new changes.</p>
<p>During implementation, a well-run SDLC requires that every change goes through code review. A good reviewer looks at correctness, coding standards, security concerns, and scalability. These checks are often supported by automations run in the Continuous Integration (CI) pipeline. These include functional checks (typically unit tests) as well as Application Security (AppSec) checks: static analysis tools scan for common vulnerabilities, and dependency scanners flag outdated or insecure libraries on every change.</p>
<p>After code is merged into a designated branch for a release candidate, the release validation process includes running a broader set of automated tests along with manual exploratory testing. Performance testing may also be conducted at this stage. The goal is to validate that the release is ready for production.</p>
<p>Deployment is typically handled in a controlled manner. Techniques such as canary releases and feature flags allow teams to roll out changes gradually and limit the impact if something goes wrong.</p>
<p>Once the code is running in production, the operations/site reliability function, whether a dedicated team or a rotating responsibility within engineering, focuses on monitoring and alerting. This includes tracking error rates, latency, and overall system health.</p>
<p>The security counterpart to site reliability/operations, the Security Operations (SecOps) team, monitors production for unusual behavior such as repeated failed logins or unexpected access patterns. Periodic penetration testing may be conducted to simulate real-world attacks and uncover issues that automated tools might miss.</p>
<figure class="figure"><a href="https://www.vivekchopra.com/assets/SDLC.png"><img src="https://www.vivekchopra.com/assets/SDLC.png" alt="Traditional SDLC diagram showing the flow from product discovery through requirements, design, testing, implementation, QA, deployment, and into site reliability and security monitoring" loading="lazy"></a><figcaption>Traditional Software Development Lifecycle</figcaption></figure>
<p>While this diagram gives the impression of a waterfall process, modern engineering teams run the process far more iteratively. Design and engineering are involved during discovery, security and QA planning starts at design time, QA runs alongside implementation, and deployment is a continuous capability rather than a discrete phase. Feedback loops exist at every stage.</p>
<figure class="figure"><a href="https://www.vivekchopra.com/assets/SDLC_realistic.png"><img src="https://www.vivekchopra.com/assets/SDLC_realistic.png" alt="SDLC with iterative feedback loops" loading="lazy"></a><figcaption>SDLC with iterative feedback loops</figcaption></figure>
<p>Even with all of these feedback loops in place, shipping complex software changes still took weeks. AI-assisted development tools are compressing that cadence enough that the shape of the SDLC process itself starts to change.</p>
<h2 id="how-ai-will-change-the-way-we-build-software" tabindex="-1">How AI Will Change the Way We Build Software <a class="heading-anchor" href="#how-ai-will-change-the-way-we-build-software" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<blockquote>
<p>The future is already here — it's just not evenly distributed. ~ William Gibson</p>
</blockquote>
<p>The core structure of the SDLC described above does not go away: teams will still need to understand the problem, design a solution, build it, test it, and run it in production. What will change is the speed of iteration, the shape of the work, and the boundaries between roles.</p>
<h2 id="product-faster-validation-tighter-loops" tabindex="-1">Product: Faster Validation, Tighter Loops <a class="heading-anchor" href="#product-faster-validation-tighter-loops" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p><em><strong>The PM's job will move from writing detailed requirements to driving rapid validation.</strong></em></p>
<p>Tools like <a href="https://v0.dev" class="external">v0</a>, <a href="https://lovable.dev" class="external">Lovable</a>, and <a href="https://bolt.new" class="external">Bolt</a> let product teams generate mockups and working prototypes in an afternoon without pulling in engineering. <a href="https://perplexity.ai" class="external">Perplexity</a> accelerates competitive research and customer insight synthesis. Feedback cycles that used to take weeks will take days.</p>
<p>Requirements do not go away, but they get lighter. You still need a clear problem definition, success metrics, and the constraints that matter.</p>
<p>The risk is that faster prototyping gets mistaken for faster thinking. Compressing the discovery cycle is only valuable if the team is still asking the hard questions: whether the problem is real, whether the solution fits, whether the assumptions hold. Moving quickly through discovery without that discipline does not shorten the path to a good product. <em><strong>The teams getting this right use the faster tools to run more experiments, not fewer conversations.</strong></em></p>
<h2 id="engineering-from-writing-code-to-validating-it" tabindex="-1">Engineering: From Writing Code to Validating It <a class="heading-anchor" href="#engineering-from-writing-code-to-validating-it" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The development team is where this gets most disruptive. AI tools can generate large amounts of code quickly. This increases velocity, but it also increases the need for discipline. The role will shift from writing code to validating it. Less time spent generating implementations, more time spent thinking through correctness, edge cases, and system behavior.</p>
<p>In practice, this means using tools like <a href="https://cursor.com" class="external">Cursor</a>, <a href="https://claude.ai/code" class="external">Claude Code</a>, or <a href="https://github.com/features/copilot" class="external">GitHub Copilot</a> to generate initial implementations, iterating quickly on small changes instead of writing everything from scratch, and spending more time reviewing logic, edge cases, and failure modes.</p>
<p>Best practices for AI-assisted development are still evolving. On one end, engineers are learning to be more deliberate in how they prompt, making assumptions and constraints explicit rather than leaving gaps for the model to fill. On the other end, more formal approaches are emerging. <a href="https://www.vivekchopra.com/posts/spec-driven-development">Spec-driven development</a> treats a written spec defining inputs, outputs, and constraints as the primary artifact, with code following from it rather than the other way around. AI development platforms like <a href="https://kiro.dev" class="external">Kiro</a> are making spec-driven development more accessible: instead of starting from a blank page, you describe what you want in natural language, and Kiro generates a structured first pass at requirements, architecture, and an implementation plan. From there, engineers refine it into a spec they can actually build against. Another approach is to use contract testing, such as <a href="https://pact.io" class="external">Pact</a>. Contracts are defined upfront, and implementations are validated against them. When agents are writing code, this gives you a clear guardrail by shifting the focus away from reviewing every line toward verifying that the code satisfies the contract.</p>
<p>Regardless of the approach, the underlying principle is the same: define guardrails for AI-assisted development. <em><strong>When code can be generated in minutes, the scarce resource is not the implementation but the precise description of what it must do.</strong></em> Whether that means a formal spec, a contract, a test written before the code, or simply a more explicit prompt, the goal is the same: give the model something to execute against rather than gaps to fill.</p>
<p>Code review will matter more, not less, but the nature of it will change. Reviewers are running into a scaling problem. If an engineer can generate ten times more code in the same amount of time, the review load grows just as fast, but reviewer attention does not scale with it. Reviews risk becoming superficial, and a rubber-stamped review is worse than no review because it creates a false sense of safety. This is where a new class of AI-assisted code review tools is emerging. Tools like <a href="https://optimalai.dev" class="external">Optimal AI's Optibot</a>, <a href="https://coderabbit.ai" class="external">CodeRabbit</a>, and <a href="https://qodo.ai" class="external">Qodo</a> sit inside the pull request flow, flagging breaking changes, risky patterns, and logic issues with awareness of the broader codebase rather than just the lines being changed. They bring context a human reviewer would need to go find manually: cross-repo dependencies, past decisions, related components. The reviewer still makes the call, but starts from a better position.</p>
<p>But tooling alone will not solve the underlying challenges. More of the safety net needs to shift toward things that scale automatically: tests that actually exercise behavior, complexity metrics that flag hard-to-reason-about code, and static analysis that catches entire classes of issues without human intervention. Code review will still matter, especially for architecture decisions and sensitive areas. But relying on it as the primary quality gate for the volume of code AI can produce is a shaky assumption.</p>
<p>Engineers will need to move up a level of abstraction: less time spent writing code, more time spent thinking through correctness and system behavior. Most engineering teams are still hiring, training, and structuring work for the old version of the job.</p>
<h2 id="qa-from-gatekeeper-to-continuous-validation" tabindex="-1">QA: From Gatekeeper to Continuous Validation <a class="heading-anchor" href="#qa-from-gatekeeper-to-continuous-validation" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Because shipping velocity is higher, quality checks need to be stronger and more automated. QA teams already define test strategies early and build continuous regression coverage. What changes is that test generation itself gets automated.</p>
<p>Newer tools like <a href="https://meticulous.ai" class="external">Meticulous</a> and <a href="https://reflect.run" class="external">Reflect</a> are going further than traditional testing frameworks like <a href="https://playwright.dev/" class="external">Playwright</a> and <a href="https://www.cypress.io/" class="external">Cypress</a>, automatically generating regression tests by observing how users actually interact with a product rather than requiring engineers to write every scenario by hand. The goal is coverage that keeps pace with the rate at which code is being produced.</p>
<p><em><strong>QA will become less of a technical function and more of a product one.</strong></em> When test generation is largely automated, the value is no longer in writing and running tests. It is in knowing which edge cases matter, what a failure actually means for a user, and where automated coverage is likely to be shallow. That requires someone who deeply understands the product, not just the testing framework.</p>
<p>The role does not disappear, but teams that keep defining it around test execution will find it increasingly hard to justify.</p>
<h2 id="design-from-artifact-production-to-user-experience-decisions" tabindex="-1">Design: From Artifact Production to User Experience Decisions <a class="heading-anchor" href="#design-from-artifact-production-to-user-experience-decisions" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>AI tools are compressing the design function faster than most design teams have anticipated. Tools like v0 and Lovable mean that a PM or engineer can generate a working UI in an afternoon without a designer in the room. The prototyping phase, which used to be a core design deliverable, is now something almost anyone can produce.</p>
<p><em><strong>So will we still need designers?</strong></em></p>
<p>What these tools cannot reliably do is make good decisions about information hierarchy, user mental models, or accessibility. Those are judgment calls that require understanding users deeply.</p>
<p><em><strong>If design is defined around producing artifacts, AI tooling will replace it. If it is defined around user experience decisions, skipping it shows up later as a product that feels incoherent.</strong></em></p>
<p>The teams that get it right will use faster tooling to get to higher-fidelity feedback sooner, with design involved at the decision-making level rather than the artifact-production level.</p>
<h2 id="security-more-surface-area-legacy-controls" tabindex="-1">Security: More Surface Area, Legacy Controls <a class="heading-anchor" href="#security-more-surface-area-legacy-controls" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Generated code can include insecure patterns, outdated libraries, and incorrect assumptions about authentication or data handling. These tools are built to produce working code, not necessarily safe code, and what used to surface over weeks of development can now appear in a single afternoon session.</p>
<p>The threat landscape is also moving faster. AI models are accelerating the pace at which vulnerabilities are discovered and exploited. They are already being used to analyze codebases and identify attack surfaces at a speed and scale that human security researchers cannot match, which means the <a href="https://www.vivekchopra.com/posts/llm-appsec">window between a vulnerability being introduced and being exploited is shrinking</a>. AI-driven attacks are becoming more sophisticated. Models are being used to craft more convincing phishing attempts and automate reconnaissance. They can also adapt to defenses at a pace manual attackers could not match.</p>
<p>Application security tooling has to evolve in response. Static analysis (SAST) has historically been pattern-based and signature-driven. Traditional tools like <a href="https://semgrep.dev" class="external">Semgrep</a> and <a href="https://snyk.io" class="external">Snyk</a> work well for known vulnerability classes but struggle with the context-dependent issues that AI-generated code tends to introduce. A newer class of AI-assisted security tools is emerging to fill that gap. Tools like <a href="https://optimalsecurity.ai" class="external">Optimal AppSec Agent</a>, <a href="https://aikido.dev" class="external">Aikido</a>, and <a href="https://socket.dev" class="external">Socket</a> review pull requests and dependencies with broader codebase context, catching issues that pattern-based tools miss because they have no awareness of how the system fits together.</p>
<p>The same shift is happening in dynamic testing (DAST). Traditional tools like <a href="https://portswigger.net/burp" class="external">Burp Suite</a> and <a href="https://www.zaproxy.org/" class="external">ZAP</a> run defined payloads against defined endpoints, relying on the human running them to interpret findings and chain them together. Newer AI-driven tools like <a href="https://xbow.com/" class="external">XBOW</a> and <a href="https://www.pynt.io/" class="external">Pynt</a> can reason about application context, generate targeted test cases based on what an endpoint actually does, and chain findings the way a human pen tester would. The category is earlier than AI-assisted SAST, but evolving along the same path.</p>
<p>The tooling shift alone does not address the broader problem. Shipping velocity and manual security processes are on a collision course. AppSec and SecOps will need to be largely automated. The focus will shift to designing the right controls upfront rather than auditing after the fact.</p>
<p>AI agents also introduce a threat vector that the traditional SDLC did not have to account for. When an agent can read files, execute commands, and operate with some autonomy, the questions around access control, least privilege, and trust boundaries become significantly more complicated. What can the agent modify? What happens if it is manipulated through a prompt injection in a file it is asked to process? Most teams do not yet have good answers, and the industry is still working out what the right patterns even look like. How you <a href="https://www.vivekchopra.com/posts/ai-agent-auth">scope agent permissions</a>, how you defend against prompt injection, how you audit what an agent actually did: these are open problems, and the conventions that will eventually govern them are still being written.</p>
<p>Threat modeling at the design stage and penetration testing in production still matter. <em><strong>But as shipping velocity increases, any manual process sitting in between becomes a bottleneck.</strong></em></p>
<figure class="figure"><a href="https://www.vivekchopra.com/assets/New_SDLC_v3.png"><img src="https://www.vivekchopra.com/assets/New_SDLC_v3.png" alt="SDLC with AI-assisted development tools" loading="lazy"></a><figcaption>Changes in SDLC with AI-assisted development tools</figcaption></figure>
<h2 id="the-faster-horses-are-here" tabindex="-1">The Faster Horses Are Here <a class="heading-anchor" href="#the-faster-horses-are-here" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<blockquote>
<p>If I had asked people what they wanted, they would have said faster horses. ~ Henry Ford (allegedly)</p>
</blockquote>
<p>Cursor, v0, Lovable, Claude Code: each of these tools compress steps that used to take days into hours. But they fit into the existing models: the engineer still writes and reviews code, the product manager still owns the requirements, and the QA engineer still runs tests. The roles are intact (for now); the work just moves faster.</p>
<p>The open question is whether what comes next is an even faster horse, or something else entirely.</p>
]]></content:encoded>
      <pubDate>Sun, 26 Apr 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/new-sdlc</guid>
      <category>AI-native platforms</category>
      <category>Scaling engineering orgs</category>
    </item>
    <item>
      <title>From Reading Changelogs to Finding Zero-Days: LLMs and the Future of AppSec</title>
      <link>https://www.vivekchopra.com/posts/llm-appsec</link>
      <description>The old model of security patches cannot survive contact with systems that operate at machine speed.</description>
      <content:encoded><![CDATA[<p>For a long time, applying security upgrades promptly was a critical component of Application Security. One of the first signals I got that something fundamental was changing came at BSides SF last year, where Mackenzie Jackson from Aikido Security demonstrated something genuinely unsettling. He had pointed a large language model at open-source changelogs and asked it to find vulnerabilities that had been &quot;shadow patched&quot;: fixed without ever being formally published as a CVE.</p>
<p>This happens more often than you would think. A maintainer fixes a serious security bug, writes something vague in the changelog like &quot;redacted tokens&quot; or &quot;increase encryption work factor,&quot; and moves on. The LLM's job was to read that deliberately obfuscating language, figure out what it actually meant, cross-reference it against public CVE databases, and flag the gaps. Because these were never published as CVEs, downstream developers had no way of knowing they were running vulnerable versions.</p>
<figure class="embed embed-video"><iframe src="https://www.youtube.com/embed/KXp8-OqMdN8" title="Mackenzie Jackson: Using AI to spot shadow patches in open source software (BSides SF)" loading="lazy" allowfullscreen=""></iframe><figcaption>Mackenzie Jackson, BSides SF: Using AI to spot shadow patches in open source software</figcaption></figure>
<p>Over the course of 2024, <a href="https://www.aikido.dev/blog/meet-intel-aikidos-open-source-threat-feed-powered-by-llms" class="external">Aikido's pipeline</a> uncovered more than 500 undisclosed vulnerabilities in open-source packages, and 67% were still undisclosed months after the fix had shipped. Over half of the high and critical ones had never been disclosed at all. These were not obscure libraries: they found undisclosed vulnerabilities in Axios (56 million weekly downloads), Apache's echarts, Chainlit. Remote code execution was the most common category among the critical findings.</p>
<blockquote>
<p>&quot;We have to assume that the bad guys have done this too, and that they can find and exploit these vulnerabilities before organizations patch them.&quot;<br>
Mackenzie Jackson</p>
</blockquote>
<h2 id="from-reading-about-bugs-to-actually-finding-them" tabindex="-1">From reading about bugs to actually finding them <a class="heading-anchor" href="#from-reading-about-bugs-to-actually-finding-them" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Aikido's work was clever, but it operated within a defined boundary. The LLMs were not discovering new vulnerabilities so much as surfacing ones that humans had already found and fixed but neglected to disclose. The model was a very sophisticated reader of commit diffs and release notes.</p>
<p>That boundary did not last long. In October 2025, <a href="https://openai.com/index/introducing-aardvark/" class="external">OpenAI unveiled Aardvark</a>, an autonomous security agent built on GPT-5. Aardvark did not just read changelogs. It read actual source code, analyzed entire repositories, built threat models from scratch, scanned incoming commits for security implications, and then attempted to exploit what it found in a sandboxed environment to confirm the vulnerability was real. In benchmark testing it identified 92% of known and synthetically introduced vulnerabilities, and it had already earned ten CVE identifiers from bugs it found in open-source projects on its own.</p>
<p>This was a qualitative shift: the LLM was no longer a passive reader spotting what humans had missed disclosing. It was an active hunter finding what humans had missed entirely, doing the full loop from discovery through exploitation through patch generation without a human in the chair. Aardvark, by every subsequent measure, turned out to be a modest preview of what was coming.</p>
<h2 id="the-vulnerability-avalanche" tabindex="-1">The vulnerability avalanche <a class="heading-anchor" href="#the-vulnerability-avalanche" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>In 2024 alone, over 40,000 CVEs were reported, already overwhelming security teams worldwide. The time between disclosure and exploitation had been compressing for years, and the window defenders relied on was narrowing toward zero. But all of that assumed the bottleneck was on the discovery side: vulnerabilities were hard to find, expensive to analyze, and required deep human expertise to weaponize. Remove that bottleneck by handing the job to a model that can read millions of lines of code in minutes and reason about memory corruption, type confusion, and race conditions, and the math changes in a way that does not have a gentle term for it.</p>
<p>Open source was particularly exposed. We all remember Log4j in 2021: a single critical library maintained by a handful of volunteers, buried four layers deep in the dependency tree, turned out to be the load-bearing wall for half the internet. But Log4j was found by a human researcher at Alibaba who happened to notice something odd. The question now on the table was: what happens when models can systematically audit every package on PyPI, npm, and crates.io in parallel, around the clock?</p>
<h2 id="mythos-and-project-glasswing" tabindex="-1">Mythos and Project Glasswing <a class="heading-anchor" href="#mythos-and-project-glasswing" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>On April 7, 2026, <a href="https://www.anthropic.com/glasswing" class="external">Anthropic published something</a> that read less like a product announcement (which it was!) and more like a threat briefing.</p>
<p>Project Glasswing brought together AWS, Apple, Broadcom, Cisco, CrowdStrike, Google, JPMorganChase, the Linux Foundation, Microsoft, NVIDIA, and Palo Alto Networks around a single premise: AI models have reached a level of coding capability where they can surpass all but the most skilled humans at finding and exploiting software vulnerabilities, and the clock is ticking before those capabilities proliferate beyond responsible actors.</p>
<p>At the center of the announcement was Claude Mythos Preview, an unreleased frontier model that had discovered thousands of zero-day vulnerabilities, including some in every major operating system and every major web browser. Not shadow patches in changelogs, not synthetic bugs in benchmark repos, but real, previously unknown, exploitable flaws in the software that runs the actual world.</p>
<p>A few of the disclosed examples: Mythos found a 27-year-old vulnerability in OpenBSD, an OS whose entire reputation is built around being security-hardened, that allowed an attacker to remotely crash any machine just by connecting to it. It found a 16-year-old bug in FFmpeg in a line of code that automated testing tools had hit five million times without catching. It independently discovered and chained together multiple Linux kernel vulnerabilities to escalate from ordinary user access to full system control. These are the kinds of bugs that win Pwn2Own competitions and command six-figure bounties. The model found them autonomously.</p>
<p>On the CyberGym benchmark for vulnerability reproduction, Mythos scored 83.1% compared to Opus 4.6's 66.6%. On SWE-bench Verified it hit 93.9%. Anthropic is not making Mythos Preview generally available. Instead it is being provided to launch partners and over 40 organizations that build or maintain critical software infrastructure, backed by up to $100 million in usage credits and $4 million in direct donations to open-source security organizations. The operational focus is defensive: vulnerability detection, black-box testing, endpoint security, penetration testing.</p>
<h2 id="the-dual-use-problem" tabindex="-1">The dual-use problem <a class="heading-anchor" href="#the-dual-use-problem" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Every capability that makes Mythos valuable to defenders (the ability to reason about code semantics, chain together subtle bugs across subsystems, generate working exploits) is exactly what makes it valuable to attackers. The only real question is who gets there first.</p>
<p>Anthropic was explicit about this, noting that it will not be long before such capabilities proliferate beyond responsible actors and that the fallout for economies, public safety, and national security could be severe. CrowdStrike's CTO Elia Zaitsev put it more directly: the window between a vulnerability being discovered and being exploited has collapsed from months to minutes with AI, and that is not a reason to slow down but a reason to move together, faster.</p>
<p>The trajectory from BSides SF 2025 to Project Glasswing is a story in three parts of a future that arrived faster than anyone expected. First, LLMs learned to read what humans had written about bugs. Then they learned to find bugs that humans had missed entirely. Then they got better at it than all but the very best humans alive.</p>
<p>The old model of human-paced vulnerability research, disclosure, and patching cannot survive contact with systems that operate at machine speed and machine scale. The software ecosystem has accumulated decades of technical debt, patching at scale remains painful, and attackers only need to find one bug while defenders need to find all of them.</p>
<p>Or, if you are a glass half-full kind of person:</p>
<figure class="quote-card"><blockquote cite="https://x.com/pmarca/status/2041598446042542243"><p>Every security flaw discovered by AI was there before AI, waiting to be discovered either by people or by AI. The world has never been good at securing computer systems; finally with AI we are going to get good.</p></blockquote><figcaption>Marc Andreessen (@pmarca), <a href="https://x.com/pmarca/status/2041598446042542243">April 7, 2026 on X</a></figcaption></figure>
]]></content:encoded>
      <pubDate>Tue, 07 Apr 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/llm-appsec</guid>
      <category>AI-native platforms</category>
      <category>Regulated markets</category>
    </item>
    <item>
      <title>Spec First, Then Code</title>
      <link>https://www.vivekchopra.com/posts/spec-driven-development</link>
      <description>LLMs have no context. You have to give it to them.</description>
      <content:encoded><![CDATA[<p>Every AI coding session starts blank. The model has no memory of your project, no knowledge of your conventions, no idea what your build command is or which libraries you prefer or what you decided six months ago when you chose this architecture. It knows how to write code in general. It does not know how to write code for your system.</p>
<p>This is the core problem with how most teams use AI coding tools. You open a chat, describe what you want, get code back, iterate. For small isolated tasks that's fine. It breaks down when the problem has real constraints or multiple steps, because the model is filling gaps with reasonable guesses that may have nothing to do with how your system actually works. You then spend the session correcting those guesses: &quot;no, we use pnpm not npm,&quot; &quot;no, don't modify the migrations directly,&quot; &quot;no, errors go through ApiError not raw throws.&quot; Those corrections vanish when the session ends. Next session, same thing.</p>
<p>The fix is not better prompts. It is giving the model the context it needs. There are three ways to do that.</p>
<h2 id="agentsmd-tell-the-model-about-your-codebase-once" tabindex="-1">AGENTS.md: tell the model about your codebase once <a class="heading-anchor" href="#agentsmd-tell-the-model-about-your-codebase-once" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p><a href="https://thepromptshelf.dev/blog/agents-md-vs-claude-md/" class="external">AGENTS.md</a> is an open standard (stewarded by the Linux Foundation, supported across Claude Code, Cursor, GitHub Copilot, Codex CLI, and Gemini CLI) for committing project context directly to your repository. Every AI tool that reads it gets the same starting point. CLAUDE.md is Claude Code's native equivalent with some additional capabilities, but Claude Code reads AGENTS.md as a fallback, so one file covers most tools.</p>
<p>The right mental model is a README written for an AI rather than a human. Not an exhaustive specification of everything about the project, but the specific things the model cannot infer from the code itself: build and test commands, hard constraints, architectural decisions that aren't obvious from the directory structure.</p>
<p>Three things that are worth knowing before you write one. First, keep it human-written and short. ETH Zurich researchers tested context files across four AI agents and found that <a href="https://arxiv.org/pdf/2602.11988" class="external">LLM-generated files hurt performance, reducing task success rates while increasing inference cost.</a> Human-written files showed roughly a four percentage point improvement. Auto-generate your AGENTS.md and you're paying more to get worse results. Second, don't put code style rules in it. That's your linter's job, and it does it deterministically and cheaply. Third, be aware that frontier models can follow around 150-200 instructions reliably. Claude Code's own system prompt already uses roughly 50 of those, so every line you add to AGENTS.md is competing for a finite budget. Include only what matters universally.</p>
<figure class="code-block"><figcaption><span>Markdown</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Markdown example"><code><span class="line"><span style="--shiki-light:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold"># AGENTS.md</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Project</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">E-commerce API. Node.js 22 / TypeScript strict / Fastify 5 / PostgreSQL 16 via Drizzle ORM.</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Commands</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> `pnpm test`</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> — unit tests</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> `make test-integration`</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> — integration tests (requires local DB)</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> `pnpm lint`</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> — ESLint + Prettier</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Hard constraints</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> Never modify files in /migrations directly</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> All handlers must validate input with Zod schemas</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> API errors use ApiError class, never raw throws</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> Do not modify anything in vendor/</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-light-font-weight:bold;--shiki-dark:#79C0FF;--shiki-dark-font-weight:bold">## Architecture</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> /src/api — route handlers only</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> /src/services — business logic, no DB access</span></span>
<span class="line"><span style="--shiki-light:#953800;--shiki-dark:#FFA657">-</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> /src/db — Drizzle schema and queries only</span></span></code></pre></figure>
<p>If your team uses multiple AI tools, maintain AGENTS.md as the single source of truth and have CLAUDE.md reference it with only Claude-specific additions. One file to keep current rather than several slowly drifting apart.</p>
<h2 id="spec-driven-development-tell-the-model-what-youre-building" tabindex="-1">Spec-driven development: tell the model what you're building <a class="heading-anchor" href="#spec-driven-development-tell-the-model-what-youre-building" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>AGENTS.md handles persistent project context. Spec-driven development handles the context for a specific piece of work. The idea is simple: write a spec before you ask for code. Not a lengthy design document, just an explicit statement of what the thing should do, what goes in, what comes out, and what the constraints are. Then generate code from that.</p>
<p>The reason this matters is that a vague prompt forces the model to make assumptions. Ask for &quot;a function to process customer transactions&quot; and the model will produce something plausible, but it will guess at error handling, edge cases, and output format. You iterate to correct those guesses, which is slow and produces code that reflects the model's assumptions more than your requirements. Define the inputs, outputs, deduplication rules, and error behavior upfront, and the model is executing against a contract instead of guessing. You get something usable on the first pass instead of something you spend an hour correcting</p>
<p>GitHub's <a href="https://github.blog/ai-and-ml/generative-ai/spec-driven-development-with-ai-get-started-with-a-new-open-source-toolkit/" class="external">Spec Kit</a> formalizes this into four phases: specify (what you're building and why), plan (stack, architecture, constraints), tasks (break it into reviewable chunks), implement (the agent works through tasks one at a time). Each phase produces artifacts that feed the next, and each has a checkpoint where you review before moving on. The insight in their framing is that the spec is the primary artifact and the code is a derivative, which inverts where most teams spend their attention. You should be reviewing the spec carefully and letting the code follow from it, not the other way around.</p>
<p>For more complex multi-step work, the <a href="https://github.com/bmad-code-org/BMAD-METHOD" class="external">BMAD method</a> extends this thinking across the full development lifecycle. Rather than a single spec for a feature, you work through problem definition, spec, design, and task breakdown before any code is written. Each step's output becomes the next step's input. The practical benefit is that errors get caught early: a flawed spec is much cheaper to fix than flawed code, and the model has continuity across the whole effort rather than starting fresh at each turn.</p>
<h2 id="screenshots-tell-the-model-what-you-can-see" tabindex="-1">Screenshots: tell the model what you can see <a class="heading-anchor" href="#screenshots-tell-the-model-what-you-can-see" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>For UI and visual work, screenshots are one of the most underrated tools for giving a model precise context. Describing a visual problem in words is surprisingly hard. &quot;The button is too close to the header&quot; is ambiguous in ways you don't realize until the model fixes the wrong button on the wrong screen. A screenshot with the element visible removes most of that ambiguity in a way that text cannot.</p>
<p>Claude handles image input well. The prompt pattern that works is straightforward: attach the screenshot, point at the specific element that needs to change, and describe what you want differently. Reference what you can see in the image rather than class names or component names the model may not recognize from the codebase. The same applies to design references. If you have a Figma frame or a screenshot of how something should look, attaching it is almost always more precise than trying to describe the visual in words.</p>
<h2 id="the-pattern" tabindex="-1">The pattern <a class="heading-anchor" href="#the-pattern" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>All three of these are the same idea at different levels. AGENTS.md closes the gap between the model and your project. A spec closes the gap between the model and your intent for a specific feature. A screenshot closes the gap between the model and what you can see but struggle to describe. In each case you are replacing the model's guesswork with your actual requirements.</p>
<p>Get this right and the model does what you actually want. Get it wrong and you spend most of your time correcting assumptions the model should never have had to make. A clever prompt on top of thin context is still thin context.</p>
]]></content:encoded>
      <pubDate>Sun, 05 Apr 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/spec-driven-development</guid>
      <category>AI-native platforms</category>
    </item>
    <item>
      <title>Something Has Changed With AI Coding</title>
      <link>https://www.vivekchopra.com/posts/ai-coding-changed</link>
      <description>For most of the past year my view on AI coding tools was old-fashioned. Something has changed.</description>
      <content:encoded><![CDATA[<p>For most of the past year, my view on AI coding tools was somewhat old-fashioned: great for prototyping, but not something I would trust with production code without a fair amount of rewriting.</p>
<p>Something has changed over the past two months.</p>
<p>The first thing is a formalization of how to actually do development with AI tools that I've been playing with: <a href="https://github.blog/ai-and-ml/generative-ai/spec-driven-development-with-ai-get-started-with-a-new-open-source-toolkit/" class="external">spec-driven development</a>. Rather than just prompting and iterating, you invest upfront in a living specification to capture intent, and use that to drive the implementation. It changes the dynamic considerably. Instead of the AI guessing what you want, it has something to reason against.</p>
<p>More fundamentally though, it is the models themselves.</p>
<p>Claude Opus 4.6 in particular feels like a different category, not just an incremental improvement. Seems I'm not the only one who thinks so. Professor Knuth just published a paper titled <a href="https://cs.stanford.edu/~knuth/papers/claude-cycles.pdf" class="external">&quot;Claude's Cycles&quot;</a> describing how Opus 4.6 solved an open graph theory problem he had been working on for several weeks, in about an hour. He ends the paper with &quot;Hats off to Claude!&quot;</p>
<p>Clearly, I'm not the only one noticing this!</p>
]]></content:encoded>
      <pubDate>Tue, 03 Mar 2026 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/ai-coding-changed</guid>
      <category>AI-native platforms</category>
    </item>
    <item>
      <title>OAuth Is Not Enough: Authorization Challenges for Autonomous AI Agents</title>
      <link>https://www.vivekchopra.com/posts/ai-agent-auth</link>
      <description>OAuth gives us delegated access, but by itself it can&apos;t ensure that autonomous agents stay inside the guardrails.</description>
      <content:encoded><![CDATA[<p>As you roll out AI agents into your enterprise stack, it's tempting to assume that the security and governance controls built for humans and traditional services will simply carry over. Not so. OAuth gives us delegated access, but by itself it can't ensure that autonomous agents stay inside the guardrails.</p>
<h2 id="the-gaps" tabindex="-1">The Gaps <a class="heading-anchor" href="#the-gaps" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<h3 id="too-much-power-not-enough-context" tabindex="-1">Too much power, not enough context <a class="heading-anchor" href="#too-much-power-not-enough-context" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>Once an agent gets an OAuth token it can keep firing off API calls — often far beyond what the user had in mind. Scopes like <code>calendar.readwrite</code> can't express intent like &quot;only book meetings on weekdays between 9–5.&quot;</p>
<h3 id="who-actually-pressed-the-button" tabindex="-1">Who actually pressed the button? <a class="heading-anchor" href="#who-actually-pressed-the-button" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>OAuth tokens tell you which user is behind a request, but not which agent (or sub-agent) made it. We need &quot;actor&quot; tokens that travel with each call for true accountability.</p>
<h3 id="one-time-consent-vs-continuous-decision-making" tabindex="-1">One-time consent vs. continuous decision-making <a class="heading-anchor" href="#one-time-consent-vs-continuous-decision-making" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>Humans can't approve every micro-action an always-on agent takes. We need real-time checks and step-up permissions — not a static &quot;yes&quot; at login.</p>
<h3 id="standing-access-vs-just-in-time-access" tabindex="-1">Standing access vs. just-in-time access <a class="heading-anchor" href="#standing-access-vs-just-in-time-access" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>Long-lived tokens give an agent perpetual power it may need only once a week. That &quot;standing privilege&quot; is a gift to attackers using prompt-injection and other techniques. Agents should instead request short-lived, task-scoped tokens on the fly and drop them when the job is done — zero standing privilege (ZSP) for agents.</p>
<blockquote>
<p>OAuth 2.0/2.1 is a great starting point, but on its own it can't guarantee that AI agents stay inside the guardrails. What is needed is layered controls: task-scoped tokens, policy engines, and audit hooks to keep AI agents safe and aligned.</p>
</blockquote>
<h2 id="working-paper" tabindex="-1">Working Paper <a class="heading-anchor" href="#working-paper" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>I've outlined the gaps and a proposed layered mitigation strategy in full below, and on <a href="https://www.techrxiv.org/users/928372/articles/1299852-oauth-is-not-enough-authorization-challenges-for-autonomous-ai-agents" class="external">TechRxiv</a>. Feedback welcome!</p>
<figure class="embed embed-pdf"><iframe src="https://www.vivekchopra.com/assets/OAuth_is_not_enough.pdf" title="Working paper (PDF)" loading="lazy"></iframe><figcaption><a href="https://www.vivekchopra.com/assets/OAuth_is_not_enough.pdf">Open the PDF</a></figcaption></figure>
]]></content:encoded>
      <pubDate>Tue, 10 Jun 2025 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/ai-agent-auth</guid>
      <category>AI-native platforms</category>
      <category>Regulated markets</category>
    </item>
    <item>
      <title>Managing by Metrics</title>
      <link>https://www.vivekchopra.com/posts/managing-by-metrics</link>
      <description>How to select, use, and think about engineering metrics. The difference between good metrics and the ones that just make you feel good.</description>
      <content:encoded><![CDATA[<p>Metrics are one of those topics where everyone agrees they matter and almost no one agrees on which ones to use. Engineering teams end up with dashboards full of numbers reviewed in sprint ceremonies and then ignored, or with a single velocity number that becomes a proxy for everything and measures nothing useful. The purpose of a metric is to answer a question you care about, not to have a number. Think of them like the instruments on a car dashboard: a compass tells you direction, a speedometer tells you how fast, a fuel gauge tells you how much runway you have left. You need all three to drive well, and none of them by itself is the whole picture.</p>
<p>For an engineering team, the two questions I want to be able to answer at any point are: are we a high-performing team that is shipping customer value with a low failure rate, and how are our customers actually experiencing the service? Everything else is secondary.</p>
<h2 id="what-makes-a-metric-good" tabindex="-1">What makes a metric good <a class="heading-anchor" href="#what-makes-a-metric-good" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>A good metric is:</p>
<ul>
<li><strong>Relevant</strong> to the questions you are trying to answer, not just easy to collect.</li>
<li><strong>Actionable</strong> in the sense that if it moved in the wrong direction, you would know what to do about it. If the answer is no, it is probably measuring the wrong thing.</li>
<li><strong>Quantifiable and comparable</strong> over time, so you can tell whether things are getting better or worse.</li>
<li><strong>Measuring outcomes, not output.</strong> Output is what the team produced. Outcomes are the effect that production had on customers and the business. This is the criterion teams most often get wrong.</li>
</ul>
<h2 id="the-traps-to-avoid" tabindex="-1">The traps to avoid <a class="heading-anchor" href="#the-traps-to-avoid" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<ul>
<li><strong>Vanity metrics</strong> look good but tell you nothing actionable. Story point velocity is a classic example. If the team completed 1,385 points in a quarter, up from 991 the quarter before, that looks like progress. But it bundles customer-facing features, bug fixes, and internal refactors into a single number. You cannot tell from it whether customers got more value or whether the team just worked harder.</li>
<li><strong>Watermelon metrics</strong> are green on the outside and red on the inside. A team reports 97% support SLA compliance, which sounds healthy until you look at which bugs are actually being fixed on time. If the P1s are handled but the P3s are quietly accumulating into a backlog that degrades the product experience, the headline number is misleading you.</li>
<li><strong>Too many metrics</strong> is its own failure mode. When everything is measured, nothing is prioritized. Teams spend time maintaining dashboards instead of acting on them. Pick a small number that between them answer your most important questions, and be ruthless about adding new ones.</li>
<li><strong>Metrics not aligned to business outcomes</strong> create perverse incentives. If engineers are measured on tickets closed, they will close tickets. If they are measured on features adopted by customers, they will think harder about whether what they are building actually solves the problem.</li>
</ul>
<h2 id="a-worked-example-change-failure-rate" tabindex="-1">A worked example: change failure rate <a class="heading-anchor" href="#a-worked-example-change-failure-rate" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Change failure rate is the percentage of deployments that cause a failure in production, measured by rollbacks, patches, and hotfixes. It satisfies almost every criterion for a good metric: it is quantifiable, comparable week over week, directly tied to customer experience, and highly actionable.</p>
<p>When we first started tracking it on one of my teams, the number came back at 27%. The benchmark for a healthy engineering team is under 1%. That gap was uncomfortable, but discomfort is exactly what a good metric should produce when something is wrong. We could look week by week at which deployments failed, trace them back to specific changes, and course-correct on code review practices, test coverage, and deployment process. Within two quarters we were under 1%, and we would not have known where to start without that number in front of us.</p>
<h2 id="an-example-metric-set" tabindex="-1">An example metric set <a class="heading-anchor" href="#an-example-metric-set" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Here is how I structured engineering metrics for a 25-30 person engineering org, but the broad principles apply to larger and smaller orgs. Essentially, there were two questions that I was trying to answer: (a) Are we a high-performing engineering org? (b) How do customers experience our product?</p>
<h3 id="are-we-a-high-performing-engineering-org" tabindex="-1">Are we a high-performing engineering org? <a class="heading-anchor" href="#are-we-a-high-performing-engineering-org" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>A high-performing engineering org consistently ships value to customers over time, with a low change failure rate. The metrics that answer this question are:</p>
<ul>
<li><strong>Value shipped</strong> (story points of completed customer-facing feature work). The benchmark we used was 1 point per developer per day, giving a quarterly target of roughly 1,040 points for a 16-person team working across a 65-day quarter. If this number drops, the next question is why, which leads to the diagnostic metrics below.</li>
<li><strong>Value vs. KTLO ratio.</strong> KTLO (&quot;keep the lights on&quot;) covers bug fixes, reliability work, onboarding, and tech debt. A team spending most of its time on KTLO is not shipping much customer value, and that ratio is worth tracking explicitly. We started with total story point velocity and then broke it into these two buckets from Q2 onwards, which made the number considerably more useful.</li>
<li><strong>Planned vs. unplanned work.</strong> A high proportion of unplanned work is a signal that the team is being pulled off roadmap commitments by fires, and that the system is less stable than it should be.</li>
<li><strong>Change failure rate.</strong> The share of deployments that result in an incident, rollback, or failure, calculated as (number of failed changes / total number of changes) * 100. Benchmark is under 1%. When we started tracking this we were at 27%, which was the clearest possible signal that something needed to change in how we were testing and reviewing code.</li>
</ul>
<h3 id="how-do-customers-experience-the-product" tabindex="-1">How do customers experience the product? <a class="heading-anchor" href="#how-do-customers-experience-the-product" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<ul>
<li><strong>Bug impact score.</strong> Rather than counting raw bug numbers, this is a weighted sum by severity, so a P0 bug counts for much more than a P3. It is a proxy for the impact product quality has on customers, and more honest than a simple bug count that treats all issues the same. Our target was a weekly bug impact score below 10.</li>
<li><strong>Support SLIs.</strong> Timeliness in fixing customer-reported bugs, by criticality. The targets we used: P0 bugs mitigated within 0 to 1 business days, P1 bugs within 3 business days. Without severity-based targets, teams naturally optimize for closing the easy tickets, which inflates overall SLA compliance while the high-severity issues sit.</li>
<li><strong>Uptime.</strong> Measured as (total available time / total time) * 100, with availability defined as the ability to log in and have the application load within baseline latency. For a SaaS product with contractual uptime commitments, this is non-negotiable to track continuously rather than in periodic reports.</li>
</ul>
<h2 id="leading-vs-lagging-indicators" tabindex="-1">Leading vs. lagging indicators <a class="heading-anchor" href="#leading-vs-lagging-indicators" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Leading indicators are input-oriented and measure things that predict future outcomes, like deployment frequency or test coverage. Lagging indicators are output-oriented and measure results, like uptime or customer retention. You need both. Lagging indicators tell you how you did; leading indicators tell you where you are headed. A team that only tracks lagging metrics is always looking in the rearview mirror, finding out about problems only after they have already landed on customers.</p>
<h2 id="cadence" tabindex="-1">Cadence <a class="heading-anchor" href="#cadence" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>A metric reviewed quarterly is not a management tool, it is a retrospective artifact. For metrics to actually change behavior, they need to be reviewed at a cadence short enough to act on. Change failure rate is worth looking at weekly. Uptime warrants continuous monitoring. Velocity makes sense at the sprint level. The right cadence depends on how quickly the underlying thing can change, but if you cannot do anything with a metric between reviews, you are reviewing it too infrequently.</p>
<p>The practical approach: start by asking what questions you most need to answer, then work backwards to the metrics that would answer them. Three to five metrics that between them address shipping velocity, quality, and reliability is usually enough for an engineering team. Add a metric only when you can articulate what decision it will help you make, and be willing to retire the ones that no longer drive action. The goal is not a comprehensive picture of everything the team is doing. It is enough signal to run the team well.</p>
]]></content:encoded>
      <pubDate>Mon, 07 Oct 2024 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/managing-by-metrics</guid>
      <category>Scaling engineering orgs</category>
      <category>Operational reliability</category>
    </item>
    <item>
      <title>Engineering Culture and Principles</title>
      <link>https://www.vivekchopra.com/posts/engineering-culture</link>
      <description>Culture is the default way a team operates when no one is watching.</description>
      <content:encoded><![CDATA[<p>Culture is the default way a team operates: how people make decisions, how they treat one another, and how they behave when no one is watching. A few years ago I started writing this down for my team. Not as a manifesto, but as a working document: here is what we value, here is how we work, here is what good looks like.</p>
<p>I'm sharing it here because the questions it tries to answer come up constantly in hiring, in onboarding, and in the day-to-day of running an engineering org. If any of it is useful to other teams, all the better.</p>
<p><a href="https://www.vivekchopra.com/assets/engineering-culture.pdf">Download as PDF</a></p>
<h2 id="engineering-culture" tabindex="-1">Engineering Culture <a class="heading-anchor" href="#engineering-culture" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Culture is what the team defaults to without being told. It shows up in how people respond to an outage, how they handle disagreement, and whether they fix the broken thing they stumbled across or walk past it. Three values anchor how I want the team to operate.</p>
<h3 id="ownership" tabindex="-1">Ownership <a class="heading-anchor" href="#ownership" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>You build it, you operate it. If you see something broken, you fix it. Ownership is not limited to your service or your ticket but is a mindset: the difference between observing that something is broken and doing something about it.</p>
<h3 id="agency" tabindex="-1">Agency <a class="heading-anchor" href="#agency" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>Agency is the degree of control individuals and teams have over their work and their ability to influence outcomes. High agency looks like reaching out to unblock yourself and pressing on rather than sitting on a ticket waiting for another team to respond.</p>
<h3 id="customer-empathy" tabindex="-1">Customer Empathy <a class="heading-anchor" href="#customer-empathy" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h3>
<p>Engineers need to feel customer pain directly, not through two layers of abstraction. Rotation through support and customer-facing roles creates a virtuous loop that feeds back into engineering design, and the reminder worth repeating is that we are solving a problem, not writing code.</p>
<h2 id="engineering-principles" tabindex="-1">Engineering Principles <a class="heading-anchor" href="#engineering-principles" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>These are the principles that guide how we design, build, and operate. They are not rules to be followed blindly but the condensed output of hard lessons.</p>
<ul>
<li><strong>Start small, iterate, refine.</strong> Design for the future, but be pragmatic about what needs to be built today, bias for action over lengthy analysis, and learn by iterating on both the business side (adoption, customer demand) and the technical side.</li>
<li><strong>Speed is good; velocity is better.</strong> Speed is a scalar but velocity is a vector: it has magnitude and direction, and the team needs to be aligned on where it is going, not just how fast it is moving.</li>
<li><strong>Our job is to solve customer problems, not write code.</strong> If open source or commercial software already does what we need, use it and do not reinvent the wheel. Ask why before asking how.</li>
<li><strong>Focus is everything.</strong> Spreading thin sets us up for failure, so stack rank the work and resist the trap where everything is top priority, which means nothing is. Avoid yak shaving.</li>
<li><strong>Write things down.</strong> Documenting decisions, designs, and meeting notes with precision brings a level of clarity that verbal discussions do not, supports remote work, and reduces single points of failure on people.</li>
<li><strong>Automate all the things.</strong> Automation brings repeatability, reliability, and scalability, and if you are doing something manually more than twice, it should be automated.</li>
<li><strong>Measure outcomes, not effort.</strong> Have quantifiable goals measured at a regular cadence and make the metrics actionable. When quality trends the wrong way, pause feature development to address it, because customer-facing features shipped matters more than tickets closed.</li>
<li><strong>Debate vigorously, then commit.</strong> Be open to ideas that are not your own, and once a decision is made, commit to it as a team rather than continuing to relitigate it.</li>
<li><strong>Shift left on quality, security, and observability.</strong> These are not afterthoughts to be bolted on at the end but are built in during the initial planning and implementation phase.</li>
</ul>
<h2 id="getting-there" tabindex="-1">Getting There <a class="heading-anchor" href="#getting-there" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Culture does not change by writing a document. It changes by changing what gets rewarded, and some questions worth asking honestly are: Are engineers rewarded for shipping fast or for writing maintainable, scalable code? Are managers rewarded for velocity or for building strong, sustainable teams? Are teams rewarded for fixing outages quickly or for preventing them in the first place?</p>
<p>What you reward gets repeated. The culture described here is the one we are working toward, and holding ourselves to these principles in practice, especially when it is inconvenient, is how we get there.</p>
]]></content:encoded>
      <pubDate>Fri, 12 Apr 2024 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/engineering-culture</guid>
      <category>Scaling engineering orgs</category>
    </item>
    <item>
      <title>Giving Negative Feedback</title>
      <link>https://www.vivekchopra.com/posts/giving-negative-feedback</link>
      <description>Most managers either put off negative feedback or reach for the sandwich method.</description>
      <content:encoded><![CDATA[<p>Giving negative feedback is one of the less pleasant parts of being a manager, and something I've personally struggled with. Most managers either put it off until it's too late, save it for the annual review, or reach for approaches that don't work.</p>
<p>The most common of these is the &quot;sandwich method&quot; — negative feedback wrapped between two pieces of positive feedback. There is plenty of empirical evidence that <a href="https://hbr.org/2013/04/the-sandwich-approach-undermin/" class="external">it doesn't work</a>. The negative part gets buried or ignored, and the person leaves the conversation unsure what you actually wanted them to change.</p>
<p>The purpose of feedback is to effect change. For that it needs to be timely (given when the behavior happens, not weeks later), specific, and focused on the action rather than the person. One piece of feedback at a time — a long list is hard to act on.</p>
<h2 id="a-framework-that-works" tabindex="-1">A Framework That Works <a class="heading-anchor" href="#a-framework-that-works" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>My Toastmasters club uses a template for speech feedback that translates surprisingly well to almost any situation. It breaks feedback into three sections: what you saw, what you heard, what you felt.</p>
<blockquote>
<p><strong>What you saw:</strong> Gestures, use of space, eye contact, mannerisms. Pick the one observation that has the most value to learn from.</p>
<p><strong>What you heard:</strong> Structure, opening, conclusion, tone, inflection, emphasis. Again, pick the one that matters most.</p>
<p><strong>What you felt:</strong> What was the impact on you? Did you feel moved, engaged, lost, bored?</p>
</blockquote>
<p>The specifics are for speeches, but the structure is widely applicable. It's detailed, it's actionable, it addresses behavior rather than character, and it forces you to prioritize the one or two things that will actually move the needle rather than cataloguing everything.</p>
<h2 id="how-to-phrase-it" tabindex="-1">How to Phrase It <a class="heading-anchor" href="#how-to-phrase-it" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Direct feedback doesn't have to be delivered harshly. People respond better to being encouraged to improve than to being criticized for what they did. Two approaches from the same Toastmasters playbook:</p>
<ul>
<li>Focus on improvement: &quot;What would have made this even better...&quot;</li>
<li>Walk with the person: &quot;I've had the same challenge, and here is what helped me...&quot;</li>
</ul>
<p>The second one is underused. It's honest, it's human, and it tends to land well.</p>
]]></content:encoded>
      <pubDate>Wed, 08 Aug 2018 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/giving-negative-feedback</guid>
      <category>Scaling engineering orgs</category>
    </item>
    <item>
      <title>Parsing Text Files in Shell</title>
      <link>https://www.vivekchopra.com/posts/parsing-text-files-shell</link>
      <description>I spent years reaching for awk before landing on something simpler, built right into how the shell reads data.</description>
      <content:encoded><![CDATA[<p>Parsing structured data files is one of those things you end up writing constantly in shell. I spent years reaching for <code>awk</code> or fiddling with <code>cut</code> options before landing on something far simpler, built right into how the shell reads data.</p>
<p>If you want to parse a colon-delimited file like <code>/etc/passwd</code>, all you need is:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">IFS</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">':'</span></span></code></pre></figure>
<p><code>IFS</code> is the Internal Field Separator. Set it to your delimiter and word splitting does the rest.</p>
<h2 id="in-practice" tabindex="-1">In Practice <a class="heading-anchor" href="#in-practice" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">line</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">'nobody:foo:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">IFS</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">":"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">($line)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "username: ${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[0]}"</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "uid:      ${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[2]}"</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "home:     ${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[5]}"</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "shell:    ${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[6]}"</span></span></code></pre></figure>
<p>Note the single quotes around <code>$line</code>. Double quotes would let the shell expand the <code>*</code> wildcard to filenames in the current directory.</p>
<p>Here's a complete script you can invoke as <code>script.sh &lt; /etc/passwd</code>:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">#!/bin/bash</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">while</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> read</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> line</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">do</span></span>
<span class="line"><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">  # skip comment lines</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  [[ ${line} </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">==</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72"> \#*</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> ]] &#x26;&#x26; </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">continue</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">  set</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> -f</span><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">  # disable globbing so '*' isn't expanded to filenames</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  IFS</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">':'</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  array</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">(${line})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  username</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[0]}"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  uid</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[2]}"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  gid</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[3]}"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  info</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[4]}"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  home</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[5]}"</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">  shell</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[6]}"</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">  echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$username</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">  uid=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$uid</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">  home=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$home</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">  shell=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$shell</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">  set</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> +f</span><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">  # re-enable globbing</span></span>
<span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">done</span></span></code></pre></figure>
<h2 id="other-useful-array-tricks" tabindex="-1">Other Useful Array Tricks <a class="heading-anchor" href="#other-useful-array-tricks" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Iterate over elements:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">for</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> element </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">in</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">@</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">]}"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">do</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$element</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">done</span></span></code></pre></figure>
<p>Iterate with index:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">for</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> index </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">in</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">!</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">@</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">]}"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">do</span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF"> echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$index</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> ${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[index]}"</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">; </span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">done</span></span></code></pre></figure>
<p>Count elements:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">#</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">@</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">]}"</span></span></code></pre></figure>
<h2 id="here-be-dragons" tabindex="-1">Here Be Dragons <a class="heading-anchor" href="#here-be-dragons" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Shell arrays have no safety wires. You can unset individual elements or insert at arbitrary indices, which means the array you're working with may be <a href="https://en.wikipedia.org/wiki/Sparse_array" class="external">sparse</a>:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">unset</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "array[5]"</span><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">    # remove element 5</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array[9]</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">"ha ha"</span><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E">    # insert at index 9, skipping 7 and 8</span></span></code></pre></figure>
<p>So this will silently give you the wrong answer:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E"># breaks on sparse arrays</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">count</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">${</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">#</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array[</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">@</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">]}</span></span>
<span class="line"><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">last_index</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">=</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">`</span><span style="--shiki-light:#953800;--shiki-dark:#FFA657">expr</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> $count</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> - </span><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">1</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">`</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">$last_index</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">]}"</span></span></code></pre></figure>
<p>Use this instead:</p>
<figure class="code-block"><figcaption><span>Bash</span><button type="button" class="copy-code" hidden>Copy</button></figcaption><pre class="shiki" tabindex="0" aria-label="Bash example"><code><span class="line"><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E"># Bash 2.05b+</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">@</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">]</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">:</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3"> -1</span><span style="--shiki-light:#CF222E;--shiki-dark:#FF7B72">:</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">1</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">}"</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6E7781;--shiki-dark:#8B949E"># Bash 4.2+</span></span>
<span class="line"><span style="--shiki-light:#0550AE;--shiki-dark:#79C0FF">echo</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF"> "${</span><span style="--shiki-light:#1F2328;--shiki-dark:#E6EDF3">array</span><span style="--shiki-light:#0A3069;--shiki-dark:#A5D6FF">[-1]}"</span></span></code></pre></figure>
<p>Shell arrays: powerful, footgun-adjacent.</p>
]]></content:encoded>
      <pubDate>Sat, 25 Jul 2015 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/parsing-text-files-shell</guid>
      <category>Operational reliability</category>
    </item>
    <item>
      <title>9 Years of Scrum, and All I Got Is a Lousy T-shirt (Sizing)</title>
      <link>https://www.vivekchopra.com/posts/nine-years-of-scrum</link>
      <description>Nine years of scrum, with varying degrees of rigor.</description>
      <content:encoded><![CDATA[<p>I've been on scrum teams for about nine years now, with varying degrees of rigor. In that time I've watched it go from a newfangled methodology to the de facto way projects get managed (or mismanaged). The list below is what I've learned along the way. As most lists go, it's somewhat idiosyncratic. YMMV.</p>
<h2 id="scrum-is-not-suited-for-all-types-of-projects" tabindex="-1">Scrum is not suited for all types of projects <a class="heading-anchor" href="#scrum-is-not-suited-for-all-types-of-projects" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Lest this sound like heresy, let me clarify. Scrum expects that the team is left relatively undisturbed to work on tasks picked up during a sprint. If a team's work is largely reactive (and this is the case for teams handling a fair number of customer issues), there are methodologies that fit better, like Kanban.</p>
<p>Customer issues are also notoriously hard to estimate. Triage, reproduction, fix, verification: traditional scrum estimation techniques (t-shirt sizing, planning poker) often fall apart here.</p>
<h2 id="inspect-and-adapt-instead-of-blindly-following-the-rules" tabindex="-1">Inspect and adapt instead of blindly following the rules <a class="heading-anchor" href="#inspect-and-adapt-instead-of-blindly-following-the-rules" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>I've lost count of how many times I've heard the complaint about &quot;not doing scrum right&quot; when the team is faced with changing business priorities mid-sprint. If your scrum master starts quoting rules by chapter and verse, you may be doing it wrong.</p>
<p>Think about the spirit of the law, not the letter. The agile manifesto is worth rereading:</p>
<ul>
<li>Individuals and interactions over processes and tools</li>
<li>Working software over comprehensive documentation</li>
<li>Customer collaboration over contract negotiation</li>
<li>Responding to change over following a plan</li>
</ul>
<p>You refactor code all the time. You should not be afraid to refactor your process based on what does or doesn't work for your situation.</p>
<p>Some questions worth asking in retrospectives: Does your process make you wait until the end of a sprint to push to production? Since tasks are worked on in priority order, is it OK for high-priority items completed early to sit waiting for lower-priority items? For a SaaS product delivering value continuously, how does a rigid two-week sprint cycle actually help?</p>
<p>On unexpected high-priority customer issues: do you invoke &quot;abnormal sprint termination&quot; as a weapon, or tell the customer to wait until the next planning meeting? Teams can handle this pragmatically. Adding a ticket mid-sprint is fine occasionally, as long as you drop something of equal weight. Let the product owner make that call; it also forces an honest assessment of the interruption's actual priority.</p>
<h2 id="make-standups-matter" tabindex="-1">Make standups matter <a class="heading-anchor" href="#make-standups-matter" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>A peer manager once disparaged the standup as ten minutes of people going around saying &quot;I ate yesterday, I will eat today, nothing is blocking me from eating.&quot; (He didn't use the word &quot;eat,&quot; but you get the idea.)</p>
<p>What a standup is not good for:</p>
<ul>
<li>Status updates. Status belongs in tickets.</li>
<li>Raising blockers. Don't wait until tomorrow to surface something blocking you today. That's a wasted day.</li>
</ul>
<p>What it is good for: refocusing the team on what matters, and passing the baton between team members. Five minutes or less. First thing in the morning, before anyone context-switches into deep work. Ideally the only recurring meeting the team attends in a day.</p>
<h2 id="use-the-right-metrics" tabindex="-1">Use the right metrics <a class="heading-anchor" href="#use-the-right-metrics" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Scrum tooling tends to push teams toward velocity as the headline metric: tickets closed per sprint. That's a vanity metric. The right measure is customer value delivered.</p>
<h2 id="done-means-shipped" tabindex="-1">Done means shipped <a class="heading-anchor" href="#done-means-shipped" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Scrum teams often congratulate themselves in review meetings on the number of tickets &quot;accepted&quot; by the product owner. Code that passed review and tests feels done. But for a customer, it means nothing until it's in their hands.</p>
<p>Done means the code is shipped. For a SaaS product that means migration scripts, release notes, runbook updates, and supporting the path through staging to production. Keeping a ticket open through those stages forces you to track the metric that actually matters: customer value delivered.</p>
<h2 id="watch-the-big-picture" tabindex="-1">Watch the big picture <a class="heading-anchor" href="#watch-the-big-picture" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>The rapid develop-demo-feedback loop works well for execution and allows quick course correction. But it is also one of the sources of trouble on agile projects.</p>
<p>Scrum focuses on small steps and bite-sized chunks of functionality. That's a good idea, but only if the overarching architecture and roadmap have been defined up front. Without that, small steps can walk the team off a cliff: delivering product that is hard to support, extend, or scale.</p>
<p>A good technical leader needs to hold the big picture in their head, articulate it clearly to the team, and make sure it guides day-to-day decisions.</p>
<h2 id="broken-windows" tabindex="-1">Broken windows <a class="heading-anchor" href="#broken-windows" aria-label="Link to this section"><span aria-hidden="true">#</span></a></h2>
<p>Backlog prioritization means the team only works on high-value issues. Small, low-priority problems (an inconsistent form field label, misaligned UI elements, a confusing error message) never get picked up in a backlog of sufficient depth.</p>
<p>That's not a problem by itself. But a collection of these things, over time, is exactly the broken windows effect: individually minor, collectively the thing that makes a product feel shoddy and unmaintained.</p>
<p>A good product manager keeps these from falling through the cracks permanently: rolling them into a higher-priority usability story, using them as onboarding exercises for new engineers, or carving out a cleanup sprint now and then.</p>
]]></content:encoded>
      <pubDate>Mon, 01 Jun 2015 07:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://vivekchopra.com/posts/nine-years-of-scrum</guid>
      <category>Scaling engineering orgs</category>
    </item>
  </channel>
</rss>
