<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ia64mail.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ia64mail.github.io/" rel="alternate" type="text/html" /><updated>2026-07-13T02:10:42+00:00</updated><id>https://ia64mail.github.io/feed.xml</id><title type="html">Research Notes — Ihor Cherednichenko</title><subtitle>Case-study research on AI-agent-driven software development: the ticket-driven development methodology, measured on Quorum — a working multi-agent research vehicle.</subtitle><author><name>Ihor Cherednichenko</name></author><entry><title type="html">The Ticket Library and the “Unknown Unknown” Problem</title><link href="https://ia64mail.github.io/quorum/the-ticket-library-and-the-unknown-unknown-problem/" rel="alternate" type="text/html" title="The Ticket Library and the “Unknown Unknown” Problem" /><published>2026-07-05T00:00:00+00:00</published><updated>2026-07-05T00:00:00+00:00</updated><id>https://ia64mail.github.io/quorum/the-ticket-library-and-the-unknown-unknown-problem</id><content type="html" xml:base="https://ia64mail.github.io/quorum/the-ticket-library-and-the-unknown-unknown-problem/"><![CDATA[<p>A mature codebase’s worst defects are the ones no single ticket, commit, or comment owns — emergent constraints assembled across years of individually reasonable decisions. They are hard on human developers and LLM agents alike, and for the same reason: the whole difficulty compresses into one question that can never be answered with absolute confidence.</p>

<blockquote>
  <p>Have I learned enough to make legitimate assumptions, so that my design or fix stands on a correct foundation?</p>
</blockquote>

<p>That property makes the “unknown unknown” class of problem the ideal experiment environment for measuring the real value of the ticket library — the most valuable artifact that <a href="https://ia64mail.github.io/quorum/ticket-driven-development-in-an-agentic-world/">ticket-driven development</a> produces. And as it turns out, the library roughly doubles the chances of success — and still guarantees nothing. Why an agent can hold the full library in its hands and ship the wrong fix anyway is the more interesting discovery, and the second half of this article.</p>

<p>I had a perfect laboratory for the question: <a href="https://github.com/ia64mail/quorum">Quorum</a>, the research vehicle behind this series, and a freshly discovered, genuinely un-ticketed latent defect inside it. That allowed a small statistical batch of problem-solving sessions on exactly the same codebase, the same agent fleet, the same prompt — the only variables being the presence or absence of the ticket library (the primary source for the agents’ knowledge mining) and of one historical inline comment sitting beside the defective line. Long story short: with the library, the fleet caught and fixed the defect in 4 of 6 runs; without it, in 2 of 6 — and every run that missed shipped the defect in a clean, tested, confident diff.</p>

<h2 id="the-experiment-setup">The experiment setup</h2>

<h3 id="the-bug-four-reasonable-decisions-one-defect">The bug: four reasonable decisions, one defect</h3>

<p>First, the minimum of Quorum internals the story needs. Quorum’s agents share a memory — the Context Store, a collection of small records (“we chose X because Y”) that agents write as they work. Whenever an agent starts a task, the system briefs it with the most recent of those records, within a small token budget: fetch everything in scope, prefer the newest. That briefing path is where the defect lives — and it took four development cycles to assemble it:</p>

<ol>
  <li><strong>An accident creates an order.</strong> The prototype store happens to return records oldest-first — a side effect of the data structure, never a promise.</li>
  <li><strong>The accident becomes a dependency.</strong> A consumer needs “newest first” and simply reverses the list — silently relying on an order nobody designed.</li>
  <li><strong>The ground shifts.</strong> The production backend arrives: it returns records in arbitrary order, and caps every query at 10,000 — harmless, the ticket reasons, because “context stores are small.”</li>
  <li><strong>The accident becomes a contract.</strong> A repair notices the broken ordering and restores it deliberately: sort oldest-first, document it, leave a comment — right beside the cap.</li>
</ol>

<p>Each step was reviewed and shipped as reasonable at the time it was taken; the full paper trail lives in the four tickets that took them: <a href="https://github.com/ia64mail/quorum/blob/main/tickets/QRM1-002-context-store-in-memory.md">QRM1-002</a>, <a href="https://github.com/ia64mail/quorum/blob/main/tickets/QRM4-002-bootstrap-context-assembly-service.md">QRM4-002</a>, <a href="https://github.com/ia64mail/quorum/blob/main/tickets/QRM5-005-opensearch-store.md">QRM5-005</a>, and <a href="https://github.com/ia64mail/quorum/blob/49-stabilization/tickets/55-bootstrap-getall-recency-ordering.md">#55</a>.</p>

<p><strong>Now add them up: at most 10,000 records, sorted oldest-first. The day any store outgrows the cap, the query returns the archive, not the news — the newest records, the only ones the briefing exists to deliver, are silently cut off, and the “prefer newer” reversal reorders stale survivors.</strong> Nobody decided this. #55’s own problem statement quotes the query verbatim — the cap in plain view — and never remarks on it. No ticket owns the seam between the cap and the sort, and that is what makes this an unknown unknown rather than a bug somebody wrote.</p>

<p>And it is a trap. The obvious one-line fix — flip the sort to descending, so the cap keeps the newest — cures the truncation and quietly regresses the system around it: every consumer was built against oldest-first, so the briefing that used to drop the newest records would now <em>prefer the oldest ones</em> — the very symptom the fix set out to cure. Whoever fixes this safely must first recover <em>why</em> ascending was chosen. That “why” survives in exactly two artifacts — ticket #55, and the four-line comment it left in the code — and those two artifacts are the experiment’s variables. This is also what makes the test fair: <strong>the answer is not in the library; the constraint is.</strong> No ticket contains the correct fix.</p>

<p>The library only carries the “why” that separates the safe fixes from the obvious one.</p>

<h3 id="the-design-one-fleet-one-prompt-two-switches">The design: one fleet, one prompt, two switches</h3>

<p>Every run replays the same session — the same agent fleet, the same codebase snapshot, and one frozen prompt that describes only the symptom (“appears to select the OLDEST entries rather than the most recent”) and demands a fix “WITHOUT regressing any existing behavior the code relies on,” naming no ticket, no file, and no fix.</p>

<p>Only two things vary, each a binary switch:</p>

<ul>
  <li><strong>The ticket library</strong> — present in the repository, or stripped from it, files and git history both;</li>
  <li><strong>The comment</strong> — the four lines #55 left beside the query, present or removed.</li>
</ul>

<p>Two switches give four arms — both present (<strong>A</strong>), library only (<strong>A′</strong>), comment only (<strong>B</strong>), neither (<strong>B′</strong>) — three runs each, twelve in total. A run succeeds only if it ships a <em>contract-safe</em> fix — truncation cured, ascending contract preserved, both backends, with tests; the failure that matters is the <em>confident miss</em> — fixing something visible nearby and shipping the latent defect untouched. Beyond pass/fail, every run also records <em>how</em> it came to know: every file read, every grep, every ticket opened. The full pre-registered protocol, the frozen prompt, the scoring rubric, and the per-run data — diffs, session logs, mining traces, score sheets — were captured for all twelve runs and sit in the experiment record this article is written against.</p>

<h2 id="tldr">TL;DR</h2>

<p>Twelve runs, four arms, three runs each. A cell counts the runs that caught the latent defect and shipped a contract-safe fix:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><code class="language-plaintext highlighter-rouge">#55</code> comment present</th>
      <th>comment removed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>ticket library present</strong></td>
      <td><strong>A</strong> — 3 of 3</td>
      <td><strong>A′</strong> — 1 of 3</td>
    </tr>
    <tr>
      <td><strong>ticket library removed</strong></td>
      <td><strong>B</strong> — 2 of 3</td>
      <td><strong>B′</strong> — 0 of 3</td>
    </tr>
  </tbody>
</table>

<h3 id="first-the-library-roughly-doubled-the-odds">First: the library roughly doubled the odds</h3>
<p>With the library present the fleet caught the defect in 4 of 6 runs; with it stripped, in 2 of 6 — and only the library-with-comment arm ever swept clean. The mining traces say why. Every winning run recovered the “why” behind the constraint <em>early in the discovery path</em>, while its picture of the problem was still forming; the one run that reached the same ticket late — dug out of git history after its fix was already written — shipped the defect anyway. <strong>What the library actually buys is concentrated, digested knowledge at the start of the investigation, where it shapes everything that follows.</strong></p>

<h3 id="second-nothing-gets-you-to-100--and-not-because-the-model-isnt-smart-enough">Second: nothing gets you to 100% — and not because the model isn’t smart enough</h3>
<p>The deepest, most elaborate run of the whole experiment — nine files, over a thousand lines, its own design document — missed the defect entirely. The misses fail in two human ways: the investigation closes early (<em>have I learned enough?</em> — answered yes, prematurely), or a correct fact is read as reassurance where it should have raised suspicion. The same <code class="language-plaintext highlighter-rouge">#55</code> record indicted the bug in all six recoveries and exonerated it in three of the six misses — the other three never saw it at all: removing four lines of comment moved the outcome whether the library was present or not. <strong>A fact does not carry its own interpretation — and that makes solving this class of problem a matter of far more than raw model capability.</strong></p>

<h3 id="third-static-code-analysis-is-not-enough--the-agents-themselves-keep-saying-so">Third: static code analysis is not enough — the agents themselves keep saying so</h3>
<p>In nearly every run, some agent went digging through git history — commit messages, blame, old pull requests — trying to reconstruct how the code evolved into its current shape. The instinct is right and the material is poor: commit messages carry pointers, not rationale, and run after run those digs changed nothing — the one that paid off did so by landing in a present ticket library. <strong>The library is that same instinct finally served with real content. And because in ticket-driven development the library is the primary product rather than a residue, it is an order of magnitude more precise, specific, and complete than any comment or commit message that happens to survive</strong> — a value gap I’d expect to pay off in every agentic session, not just the hunt for an unknown unknown (though that extrapolation is my read, not this experiment’s proof).</p>

<p>The next four sections walk the arms run by run — what each fleet read, what it concluded, and how the ones that missed did so with complete confidence.</p>

<h2 id="arm-a--library-and-comment-present-3-of-3">Arm A — library and comment present (3 of 3)</h2>

<p>Arm A is the control — the methodology as practiced, both artifacts in place — and it is clean in the way that matters: <strong>all three runs recovered the latent defect, and all three recovered it the same way</strong> — by pivoting off the code and into the ticket library early in the discovery path, before a line of the fix existed.</p>

<p>Reading the code brought each run to the suspicious line in <code class="language-plaintext highlighter-rouge">opensearch-store.ts</code> — <code class="language-plaintext highlighter-rouge">sort: createdAt asc</code> next to <code class="language-plaintext highlighter-rouge">size: 10000</code>, a <code class="language-plaintext highlighter-rouge">#55</code> comment right beside them. The code says <em>ascending, on purpose</em>. It does not say <em>why</em>. The pivot is the whole move: rather than trust or “fix” a line whose rationale it could not see, each run went and found the rationale in the library — <a href="https://github.com/ia64mail/quorum/blob/49-stabilization/tickets/55-bootstrap-getall-recency-ordering.md">#55</a> and its neighbor <a href="https://github.com/ia64mail/quorum/blob/49-stabilization/tickets/56-bootstrap-budget-sizing.md">#56</a>. The ticket carried the one thing the code could not: ascending order was chosen deliberately, <em>under the assumption that the stores stay small</em> — the very assumption the experiment’s prompt tells the fleet to discard.</p>

<p>With the “why” in hand, each run named the defect in its own words:</p>

<ul>
  <li><strong>A1</strong> — “CONFIRMED DEFECT … will manifest silently once any scope+id partition crosses 10,000 documents — exactly the ‘future scale’ framing of the task.”</li>
  <li><strong>A2</strong> — “The <code class="language-plaintext highlighter-rouge">asc</code> sort + <code class="language-plaintext highlighter-rouge">size: 10000</code> is the bug. … The newest items past position 10001 are silently dropped.”</li>
  <li><strong>A3</strong> — “latent-only deterministic defect. Real, mechanically inevitable, dormant at today’s data volume.”</li>
</ul>

<p>All three runs trace one mining shape — the diagram renders it once; the runs differ only in how many tickets they opened (#55 and #56 everywhere; A3 also pulled #70 and the #49 epic note). The ticket node sits <strong>early, on the critical path</strong>, in all three — that placement <em>is</em> the result:</p>

<pre><code class="language-mermaid">%% Arm A (library present) — representative knowledge-mining path.
%% Drawn from A1's curated trace; A2 and A3 share this skeleton, varying only
%% in ticket breadth. 🎫 = ticket library, 📖 = code read,
%% 🔎 = search/grep, 💡 = insight, 🗄 = context-store write.
flowchart TD
    classDef ticket fill:#fde68a,stroke:#b45309,color:#000,stroke-width:2px;
    classDef code fill:#dbeafe,stroke:#1e40af,color:#000;
    classDef insight fill:#dcfce7,stroke:#15803d,color:#000;
    classDef store fill:#ede9fe,stroke:#7e22ce,color:#000;

    START(["architect dispatched"]):::insight

    subgraph P1[" 1 · Read the core code "]
        C1[/"📖 bootstrap-context.service.ts · the consumer · .reverse()"/]:::code
        C2[/"📖 context-store.abstract.ts · the getAll ordering contract"/]:::code
        C4[/"📖 opensearch-store.ts&lt;br/&gt;sort:asc + size:10000 + a #55 comment"/]:::code
    end

    subgraph P2[" 2 · PIVOT — open the ticket library "]
        F["💻 find tickets -name *55* -o *56*"]:::insight
        T55[["🎫 tickets/55-bootstrap-getall-recency-ordering.md&lt;br/&gt;LEARNS: asc was chosen on purpose,&lt;br/&gt;under the 'stores stay small' assumption"]]:::ticket
        T56[["🎫 tickets/56 (A3 also #70, #49 epic)"]]:::ticket
    end

    subgraph P3[" 3 · Confirm the mechanism "]
        G1["🔎 grep size:10000 / max_result_window → 10k is the hard ceiling"]:::code
    end

    CONCLUDE{{"💡 CONFIRMED DEFECT&lt;br/&gt;asc + 10k cap silently drops the NEWEST docs&lt;br/&gt;once any partition exceeds 10,000"}}:::insight
    W[("🗄 writes project-scope diagnosis&lt;br/&gt;→ bootstraps into teamlead + developer")]:::store

    START --&gt; C1
    C1 --&gt; C2 --&gt; C4
    C4 --&gt;|"code says 'asc for a reason' — but WHY? go find it"| F
    F --&gt; T55 --&gt; T56
    T55 -.-&gt;|"the 'why' = the small-store assumption&lt;br/&gt;the prompt tells us to discard"| CONCLUDE
    T56 --&gt; G1 --&gt; CONCLUDE
    CONCLUDE --&gt; W
</code></pre>

<p><strong>Two archetypes, one outcome.</strong> Where the runs diverged was the <em>shape</em> of the safe fix — and the divergence is instructive, because it shows the library handed over a constraint, not an answer:</p>

<ul>
  <li><strong>A1 redefined the contract.</strong> Newest-first end-to-end: it flipped the sort to descending in <em>both</em> backends, removed the consumer’s now-redundant reversal, and rewrote the ordering contract everywhere it was documented. (On the way it caught, unprompted, a second latent ordering bug nobody had asked about.)</li>
  <li><strong>A2 and A3 preserved the contract.</strong> They flipped only the OpenSearch query to descending — so the cap now truncates the <em>oldest</em> tail — then reversed the hits internally, returning them oldest-first as ever. The public contract, and every consumer built on it, untouched.</li>
</ul>

<p>Both archetypes are contract-safe: truncation cured, both backends covered, tests added. Two different fixes honoring the same recovered constraint — exactly what the fairness design predicts. The answer was never in the library; the constraint was.</p>

<p>One nuance, offered as an observation rather than a finding: the library carried each run <em>to</em> the constraint — it did not make anyone look further. Holding the ticket’s answer, A3 shipped a correct fix and stopped, while a run that later had to reconstruct the same story without the library dug deeper into the backend and stumbled into extra hardening no control run ever wrote. And a boundary this arm cannot see from inside: all three recoveries began at the same four-line comment. What happens to the same library when that comment is gone is the next section.</p>

<h2 id="arm-a--library-present-comment-removed-1-of-3">Arm A′ — library present, comment removed (1 of 3)</h2>

<p>Arm A leaves an obvious question hanging: every one of its recoveries began at the same four-line comment — so was the comment doing the heavy lifting? If the library is the load-bearing artifact, losing four lines of comment should cost nothing: the tickets are all still there. So the comment came out, the library stayed in — everything else held constant — for three more runs.</p>

<p><strong>The result is 1 of 3.</strong> Two runs missed the defect and shipped it — with the full ticket library present, read, and <em>cited by name</em>. All three runs walked Arm A’s path to the same place: the now-bare <code class="language-plaintext highlighter-rouge">sort: asc</code> + <code class="language-plaintext highlighter-rouge">size: 10000</code> line, and from there into the library and #55. They diverge only at the last step, in <em>how the ticket is read</em>:</p>

<ul>
  <li><strong>A4</strong> (recovered) — the one win, and it entered sideways: a surviving commit message still carried “(#55)”, the pointer was chased into the library, and the ticket’s small-store assumption finally met the cap. The fix was the contract-preserving archetype, chosen in so many words — flip the query “because it leaves the public contract unchanged.”</li>
  <li><strong>A5</strong> (missed) — read the same ticket, and the ticket <em>closed</em> the investigation: “OpenSearch honors this via <code class="language-plaintext highlighter-rouge">sort: [{ createdAt: 'asc' }]</code> … InMemoryStore does NOT.” The cap sat one line below the sort, on screen, and was skipped — nobody in the run ever connected the two lines.</li>
  <li><strong>A6</strong> (missed) — the sharpest form of the trap. The deepest investigation of the trio declared OpenSearch “already correct per #55” <strong>five times</strong> — and went further: it <em>inherited #55’s own blind spot</em>, skeptically re-testing the ticket’s claim about the in-memory store while never turning that same skepticism on the ticket’s OpenSearch assumption.</li>
</ul>

<p>The delta from Arm A’s diagram is small and decisive — the comment node is gone, and the ticket node, still present and still read, becomes a fork instead of a doorway:</p>

<pre><code class="language-mermaid">%% Arm A′ (library kept, comment removed) — the DELTA from the Arm-A diagram only.
%% Shared head (dispatch → core code) and tail (fix → ship) are trimmed; Arm A's diagram carries them.
%% The two A′-only facts: the #55 comment is GONE, and the ticket — still present,
%% still read, still cited — forks on HOW it is read.
flowchart TD
    classDef code fill:#dbeafe,stroke:#1e40af,color:#000;
    classDef gone fill:#f3f4f6,stroke:#9ca3af,color:#6b7280,stroke-dasharray:6 4;
    classDef ticket fill:#fde68a,stroke:#b45309,color:#000,stroke-width:2px;
    classDef insight fill:#dcfce7,stroke:#15803d,color:#000;
    classDef miss fill:#fee2e2,stroke:#b91c1c,color:#000;
    classDef bug  fill:#fee2e2,stroke:#b91c1c,color:#000,stroke-width:2px;

    C4[/"📖 (same skeleton as Arm A) → opensearch-store.ts&lt;br/&gt;sort:'asc' + size:10000"/]:::code
    GONE["❌ the #55 comment is GONE&lt;br/&gt;nothing on the line points at the cap×sort seam"]:::gone
    T55[["🎫 tickets/55 — still present, still read, still CITED&lt;br/&gt;a true record of a real fix — but no ticket owns the seam"]]:::ticket
    FORK{{"how is the ticket read?"}}:::miss

    A4["A4 · read as a QUESTION → recovered&lt;br/&gt;entry was sideways: a commit message still said (#55),&lt;br/&gt;the pointer was chased, size:10000 finally connected"]:::insight
    A5["A5 · read as REASSURANCE · cap on-screen, skipped&lt;br/&gt;'OpenSearch honors this via sort asc … &lt;br/&gt;InMemoryStore does NOT' · shallowest run"]:::miss
    A6["A6 · read as REASSURANCE — five times&lt;br/&gt;'already correct per #55' · inherited #55's blind spot&lt;br/&gt;deepest run of the trio · cap never surfaced"]:::miss

    BUG{{"🐛 production defect ships · 2 of 3&lt;br/&gt;a visible secondary bug fixed; cap×sort still live"}}:::bug

    C4 --&gt; GONE --&gt; T55 --&gt; FORK
    FORK --&gt; A4
    FORK --&gt; A5 --&gt; BUG
    FORK --&gt; A6 --&gt; BUG
</code></pre>

<p>That is the whole point of A′: <strong>the ticket library, present and consulted, did not guarantee recovery — because no single ticket owns the seam.</strong> #55 is a true record of a real fix, and read on its own it does what true records do for the place they don’t cover: it reassures. The comment never carried the answer; what it carried was <em>aim</em>. Sitting beside <code class="language-plaintext highlighter-rouge">size: 10000</code>, it made every Arm-A investigation read the cap and the sort <em>together</em> — the one framing under which #55’s small-store assumption stops being reassurance and becomes the indictment. Arm A made that connection three times out of three. Arm A′, holding the same library, made it once — and that once came in through a commit message, not through anything the code offered.</p>

<p>Note what this does <em>not</em> say: it does not say deeper investigation saves you — A6 was the most thorough run of the trio and missed anyway. The next arm flips both switches the other way: the library gone, the four-line comment left in place.</p>

<h2 id="arm-b--library-removed-comment-present-2-of-3">Arm B — library removed, comment present (2 of 3)</h2>

<p>Arm B is the condition most real codebases live in: no ticket library — stripped from the repository, files and git history both — and only the ordinary breadcrumbs left behind. Here, that means the four-line <code class="language-plaintext highlighter-rouge">#55</code> comment beside the query.</p>

<p><strong>The result is 2 of 3.</strong> Both recoveries traveled Arm A’s road up to one missing turn. Reading the code led to the same suspicious line — <code class="language-plaintext highlighter-rouge">sort: createdAt asc</code> next to <code class="language-plaintext highlighter-rouge">size: 10000</code>, the comment beside them — but this time the trail stops there. There is no <code class="language-plaintext highlighter-rouge">tickets/</code> to pivot into. The comment is the only surviving record of why ascending was chosen, and it carries the <em>what</em> — deliberate, per #55 — but not the <em>why</em>: the small-store assumption lived in the ticket, and the ticket is gone. The recovering runs bridged that gap from general knowledge — OpenSearch caps any query at 10,000 by default — and reasoned the rest out themselves. Each run named its conclusion in its own words:</p>

<ul>
  <li><strong>B2</strong> (recovered) — “OpenSearchStore.getAll at &gt;10000 docs silently returns the oldest 10000; bootstrap’s <code class="language-plaintext highlighter-rouge">.reverse()</code> never sees the actual newest items.”</li>
  <li><strong>B3</strong> (recovered) — “Defect: YES — confirmed, two distinct defects feeding the same surface symptom.”</li>
  <li><strong>B1</strong> (missed) — “OpenSearchStore.getAll was already correct (<code class="language-plaintext highlighter-rouge">sort: [{ createdAt: 'asc' }]</code>); only the InMemoryStore backend was affected.”</li>
</ul>

<p>The mining diagram is Arm A’s with the pivot replaced. Where Arm A’s recovery node is a ticket, early on the critical path in every run, Arm B’s is the same <code class="language-plaintext highlighter-rouge">#55</code> line — as a bare comment — and its value forks on the reading, exactly as the library forked in A′:</p>

<pre><code class="language-mermaid">%% Arm B (library stripped) — representative knowledge-mining path.
%% Drawn from B2's curated trace; B3 shares this skeleton and recovered the same way.
%% B1 reached the same #55 comment and MISSED — the dotted fork shows where.
%% 🧩 = the surviving #55 comment (breadcrumb), 📖 = code read,
%% 💡 = insight, 🗄 = context-store write, ❌ = no library to pivot to.
flowchart TD
    classDef breadcrumb fill:#fde68a,stroke:#b45309,color:#000,stroke-width:2px;
    classDef code fill:#dbeafe,stroke:#1e40af,color:#000;
    classDef insight fill:#dcfce7,stroke:#15803d,color:#000;
    classDef store fill:#ede9fe,stroke:#7e22ce,color:#000;
    classDef dead fill:#fee2e2,stroke:#b91c1c,color:#000;

    START(["architect dispatched"]):::insight

    subgraph P1[" 1 · Read the core code "]
        C1[/"📖 bootstrap-context.service.ts · the consumer · .reverse()"/]:::code
        C2[/"📖 context-store.abstract.ts · the getAll ordering contract"/]:::code
        C4[/"📖 opensearch-store.ts&lt;br/&gt;sort:asc + size:10000 + a #55 comment"/]:::code
    end

    subgraph P2[" 2 · The only surviving 'why' is a comment "]
        F["❌ no tickets/ to pivot to — the library is gone"]:::dead
        CM[["🧩 opensearch-store.ts:175 · the #55 comment&lt;br/&gt;LEARNS: asc is deliberate (the WHAT)&lt;br/&gt;but NOT why — the small-store rationale&lt;br/&gt;lived in the ticket, which was stripped"]]:::breadcrumb
    end

    subgraph P3[" 3 · Confirm the mechanism from general knowledge "]
        G1["💡 OpenSearch index.max_result_window caps any query at 10,000&lt;br/&gt;→ asc + 10k drops the NEWEST"]:::code
    end

    CONCLUDE{{"💡 RECOVERED&lt;br/&gt;asc + 10k cap silently drops the NEWEST docs&lt;br/&gt;once any partition exceeds 10,000"}}:::insight
    W[("🗄 writes project-scope diagnosis&lt;br/&gt;→ bootstraps into developer + review")]:::store

    START --&gt; C1
    C1 --&gt; C2 --&gt; C4
    C4 --&gt;|"code says 'asc for a reason' — but the reason isn't here"| F
    F --&gt; CM
    CM -.-&gt;|"read as a QUESTION → investigate the cap&lt;br/&gt;(B1: same line read as REASSURANCE → miss)"| G1
    G1 --&gt; CONCLUDE
    CONCLUDE --&gt; W
</code></pre>

<p><strong>One archetype, recovered twice.</strong> Where Arm A produced two shapes of safe fix, both Arm-B recoveries converged on one — the contract-preserving archetype A2 and A3 chose: flip the query to descending so the cap truncates the <em>oldest</em> tail, re-sort the hits internally, leave the public contract and every consumer untouched. Contract-safe both times, both backends, tests added. The comment carried even less than the ticket did — not the fix, not the rationale, only that <code class="language-plaintext highlighter-rouge">asc</code> was deliberate. The answer was never in the comment; only a pointer to a question was. When the investigation treated it as one, it recovered — twice, the same way.</p>

<p><strong>The third run is the arm’s load-bearing result.</strong> B1 reached the identical comment and drew the opposite conclusion: <em>asc, on purpose</em> — read as confirmation that the OpenSearch path was already correct. It fixed the visible secondary bug instead and shipped the production defect in a clean, tested, confident diff. The postscript stings: late in the run, the review phase even dug ticket #55 itself out of git history — the one artifact that could have reframed everything — but the fix was already written, and nothing changed.</p>

<p>Same defect, same comment, same fleet — a different roll. Without the library, recovery stops being a property of the process and becomes a coin-flip on whether the investigation interrogates the breadcrumb or trusts it: two runs of three interrogated it, one trusted it, and no team can run on a process that ships a latent defect one time in three. A′ already showed the library alone doesn’t cure the misreading — but it gives the investigation more threads to pull. Here everything hung on one four-line comment. The last arm removes it.</p>

<h2 id="arm-b--library-removed-comment-removed-0-of-3">Arm B′ — library removed, comment removed (0 of 3)</h2>

<p>Arm B left the breadcrumb in place, and twice out of three it was enough. So the last arm removes that too: no library, no comment — the only thing that changes from Arm B is four lines. What remains is the bare, executable defect — <code class="language-plaintext highlighter-rouge">sort: 'asc'</code> next to <code class="language-plaintext highlighter-rouge">size: 10000</code> — and whatever the fleet knows in general.</p>

<p><strong>The result is 0 of 3.</strong> Every run fixed the visible secondary bug, shipped the latent one, and reported success. The three runs walked the same skeleton — same code, same bare line — and differ only in <em>how</em> they walked past the cap:</p>

<ul>
  <li><strong>B4</strong> — never recognized the cap as a problem at all, declaring the OpenSearch path correct: “even at that limit a JS Array.sort runs in microseconds.”</li>
  <li><strong>B5</strong> — the near miss. It <em>did</em> surface the ceiling from general knowledge — “OpenSearch’s <code class="language-plaintext highlighter-rouge">size: 10000</code> ceiling on <code class="language-plaintext highlighter-rouge">getAll</code> … is a silent cutoff that will bite at scale” — then scoped it out of the task (“flag it separately”) and mischaracterized what it actually does. Recognized, deferred, gone. It even chased the <code class="language-plaintext highlighter-rouge">#55</code> pointers surviving in commit messages all the way to the original pull requests — and came back with nothing.</li>
  <li><strong>B6</strong> — the answer to “just investigate harder.” The most elaborate run of all twelve: 1,218 lines across nine files, ordering tie-breaks on both backends, a parity test suite, a 305-line design document — and not one mention of the cap anywhere. All of that rigor went into a nearby problem; none of it touched the defect.</li>
</ul>

<p>The delta from Arm B’s diagram mirrors A′’s: the comment node is gone, and with it the only fork that ever led to the defect —</p>

<pre><code class="language-mermaid">%% Arm B′ (neither artifact) — the DELTA from the Arm-B diagram only.
%% Shared head (dispatch → core code) and shared tail (visible fix → ship) are trimmed;
%% Arm B's diagram carries them. The two B′-only facts:
%% the breadcrumb is GONE, and the single miss forks three ways.
flowchart TD
    classDef code fill:#dbeafe,stroke:#1e40af,color:#000;
    classDef gone fill:#f3f4f6,stroke:#9ca3af,color:#6b7280,stroke-dasharray:6 4;
    classDef miss fill:#fee2e2,stroke:#b91c1c,color:#000;
    classDef bug  fill:#fee2e2,stroke:#b91c1c,color:#000,stroke-width:2px;

    C4[/"📖 (same skeleton as Arm B) → opensearch-store.ts&lt;br/&gt;sort:'asc' + size:10000"/]:::code
    GONE["❌ the #55 comment is GONE&lt;br/&gt;only the bare cap line remains — no 'why' to read"]:::gone
    FORK{{"the bare cap line is read — and walked past&lt;br/&gt;depth and effort vary, the outcome does not"}}:::miss

    B4["B4 · declared OS correct&lt;br/&gt;'a JS Array.sort runs in microseconds'&lt;br/&gt;0 cap recognition · shallowest"]:::miss
    B5["B5 · recognized → deferred&lt;br/&gt;'size:10000 ceiling … flag it separately'&lt;br/&gt;mechanism mischaracterized"]:::miss
    B6["B6 · cap never surfaced&lt;br/&gt;1,218 lines on determinism/parity&lt;br/&gt;the most elaborate run of all twelve"]:::miss

    BUG{{"🐛 production defect ships · 0 of 3&lt;br/&gt;a visible secondary bug fixed; cap×sort still live"}}:::bug

    C4 --&gt; GONE --&gt; FORK
    FORK --&gt; B4 --&gt; BUG
    FORK --&gt; B5 --&gt; BUG
    FORK --&gt; B6 --&gt; BUG
</code></pre>

<p>That is the whole point of B′: <strong>the miss is not rescued by depth, effort, or thoroughness.</strong> Removing one four-line comment took recovery from conditional (2 of 3) to never (0 of 3) — and, as A′ showed, the same four lines cost the library arm its clean sweep too (3 of 3 → 1 of 3). One artifact, load-bearing on both knowledge channels, with nothing in any process guarding its existence: a comment survives exactly until the refactor that deletes it.</p>

<p><strong>The library’s difference is not that it guarantees the catch — this experiment keeps saying it doesn’t — but that it makes the “why” reliably present and searchable, a ticket a keyword query will always surface, instead of a line that happens to still be there.</strong></p>

<p>What the twelve runs add up to is the last section.</p>

<h2 id="what-it-means">What it means</h2>

<p>One fact was held back from the arm stories, because it belongs here: the misses did not merely miss — they wrote themselves into the record. In <a href="https://ia64mail.github.io/quorum/ticket-driven-development-in-an-agentic-world/">ticket-driven development</a> every piece of work ends by writing back into the ticket library; that compounding is the engine the methodology runs on, and it has no preferred direction. When the investigation connected, the write-back made the library truer — A4’s ticket carries the recovered rationale forward. When it didn’t, the library gained a false record: A5’s authored ticket names the OpenSearch path its “Compliant peer for reference,” and A6’s declares itself “the InMemoryStore half of #55” — the wrong conclusion, stated with the same confidence as any true record, sitting exactly where the next keyword search will land. A knowledge base is not a safety net under the investigation; it is a mirror of it. And a half-right “why,” once recorded, reads exactly like a whole one. That is what makes reviewing agent-authored tickets one of the heaviest responsibilities the human carries in the tandem: not because the human is immune — a reviewer facing A6’s confident, internally consistent ticket would likely have nodded at the same seam — but because the review is the last moment a half-right “why” is still a draft, and not yet the record every future investigation will trust.</p>

<p>What should a team do with that? Three practices fall straight out of the twelve runs — none of them new, all of them newly measurable:</p>

<ul>
  <li><strong>Keep both artifacts.</strong> The experiment does not say tickets make comments obsolete. The comment carried <em>aim</em>, the ticket carried the <em>why</em>, and only the arm holding both ever swept clean; deleting four lines cost measurable odds on both knowledge channels. Treat a load-bearing comment as part of the contract it annotates — and keep it pointing into the library, because a bare “#55” was enough whenever it was followed.</li>
  <li><strong>Interrogate, don’t consult.</strong> Every miss that held an artifact read it as reassurance; every win read it as a question. The discipline that separates them fits in one sentence: a ticket tells you what was known when it was written — so ask what it does not cover, and what has changed since. A library treated as a checkbox produces the worst outcome the experiment has to offer: high confidence, a missed defect, and the wrong conclusion durably recorded.</li>
  <li><strong>Aim review at the seams.</strong> No ticket owned the cap×sort interaction — that is precisely what made it an unknown unknown. So the useful review question is not “does the change match the ticket?” but “which ticket owns this interaction — and if none does, an investigation belongs exactly here.”</li>
</ul>

<p>The honest bounds, stated plainly. Twelve runs, three per cell, one defect, one codebase — and a defect <em>chosen</em> because its “why” lived in the library, which is the very property under test. I designed, ran, and scored the experiment myself; the protocol was frozen before the first run, the diffs, traces, and scoring rubric are preserved in full in the experiment record, and every quote in this article is verbatim from those artifacts — so every run can be re-scored against its own record. Nothing here is a significance test. It is a directional case study — but the direction was consistent, and the mechanism behind it is visible in every single trace.</p>

<p>This article opened with the question every fix to an old system hangs on: <em>have I learned enough to make legitimate assumptions?</em> The experiment’s most uncomfortable finding is that the library never answers that question for you — A5 and A6 held the complete library, answered yes, and were wrong. What the library changes is whether the question is <em>answerable at all</em>: with it, the “why” is reliably on the table, early in the discovery path, at the price of writing it down as you go; without it, the answer depends on which comments happened to survive the last refactor.</p>

<p><strong>Human teams have always lost exactly this knowledge in exactly these ways — people leave, comments vanish in refactors, decisions live in chat threads nobody can search — and the agents did us one service by making the loss measurable: in this experiment, it cost half the odds of a safe fix. The unknown unknowns will keep assembling themselves out of individually reasonable decisions; that part is not optional. Keeping the “why” they will one day depend on — that part is.</strong></p>]]></content><author><name>Ihor Cherednichenko</name></author><category term="experiments" /><category term="knowledge-management" /><category term="agents" /><summary type="html"><![CDATA[A pre-registered 12-run experiment measuring what a ticket library is worth against a defect nobody wrote: roughly doubled odds of a safe fix — and still no guarantee.]]></summary></entry><entry><title type="html">Ticket-Driven Development in an Agentic World</title><link href="https://ia64mail.github.io/quorum/ticket-driven-development-in-an-agentic-world/" rel="alternate" type="text/html" title="Ticket-Driven Development in an Agentic World" /><published>2026-06-10T00:00:00+00:00</published><updated>2026-06-10T00:00:00+00:00</updated><id>https://ia64mail.github.io/quorum/ticket-driven-development-in-an-agentic-world</id><content type="html" xml:base="https://ia64mail.github.io/quorum/ticket-driven-development-in-an-agentic-world/"><![CDATA[<p>Over the last few years, AI has moved from a curiosity — “have you heard about this?” — to a force that is reshaping entire industries. Software development sits at the center of that shift. It was among the first fields to feel the change, and the change runs deeper here than almost anywhere else.</p>

<p>This article opens a series on one narrow but consequential question: how AI changes the <em>methodology</em> of building software — not the tooling or the autocomplete, but the process itself. To what extent does agent-driven development actually move the practice? Where does the real benefit come from? And how do you build a process around it that is sustainable, reviewable, and cost-efficient — rather than a novelty that collapses under its own enthusiasm?</p>

<p>What follows are my opinionated answers, and they did not start in a lab. The approach I call ticket-driven development was first put under real probation on a large commercial codebase: a mature enterprise product spanning more than fifty repositories, in languages from JavaScript to Kotlin/Java and C# — some thirty years old, others only weeks old — maintained by a geographically distributed team of roughly ten developers. It was adopted there gradually, tested under genuine production pressure, and the results were good enough to be worth understanding properly. That is what led me, about six months ago, to build <a href="https://github.com/ia64mail/quorum">Quorum</a>: a research vehicle for multi-agent, semi-autonomous software development, where the same methodology could be studied and measured in isolation.</p>

<h2 id="what-is-a-ticket">What is a ticket?</h2>

<p>A ticket is a ticket — or it always has been. It is one of the oldest and most familiar artifacts in software development: a technical specification of the work to come, a definition of a unit of work, the requirement a developer sets out to satisfy. Tickets have been with us since the beginning of the discipline and barely qualify as a new idea. What agentic development changes is not the artifact but its role.</p>

<blockquote>
  <p>We no longer write the code. We write the ticket — and the agent compiles it.</p>
</blockquote>

<p>That single shift is the whole of this article. In conventional practice the ticket is “paperwork around the code”: a description of work that someone then performs by typing. In ticket-driven development the ticket IS the work — “source code” written in natural language — and the code becomes its compiled output.</p>

<p align="center">
    <img src="https://github.com/user-attachments/assets/430b88b5-bcfc-42b0-98d2-b806cfbc67b0" alt="Conventional development compiles hand-written source code to an executable; ticket-driven development inserts a human-authored ticket and an AI-agent compile stage ahead of the same source code." width="700" />
  </p>

<p><em>The yellow highlight marks each pipeline’s <strong>product value</strong>. Conventionally it rests on the source code the developer types by hand. In ticket-driven development it moves left — onto the ticket and the AI agent that compiles it — while the developer steps out of the line to drive that agent through engineering.</em></p>

<p>The shift is not just where the human types. Conventionally the product’s value is locked in the <strong>source code</strong> — the consumed output of human effort. In ticket-driven development that value moves to the <strong>ticket</strong> — the retained, reusable output of the same effort — while the source code becomes regenerable output the agent rebuilds on demand. The human’s contribution stops being a spent input and becomes a durable asset.</p>

<p>In an agent-driven workflow the ticket becomes the cornerstone of the process, carrying several distinct loads at once:</p>

<ul>
  <li><strong>It is the requirement.</strong> A well-defined statement of the work — the specific slice of business logic or system behavior being addressed — written before any code exists. This is <em>what</em> the developer and the product intend to build.</li>
  <li><strong>It drives the implementation.</strong> The ticket records the decisions already made — approach, structure, constraints, the non-obvious choices and their rationale — so the agent implements rather than improvises. This is <em>how</em> the agent compiles it.</li>
  <li><strong>It is the cheapest point of disagreement.</strong> The ticket is reviewed before any code exists, so design flaws surface as comments on a spec rather than as bugs in an implementation — and changing course costs nothing but an edit. Disagreeing with a plan is cheap; disagreeing with code is expensive.</li>
  <li><strong>It is the verification blueprint.</strong> Acceptance criteria are concrete, checkable conditions that define “done.” Code review runs against the same ticket that drove the implementation — a check of compilation correctness, not an abstract opinion about code quality.</li>
  <li><strong>It feeds the documentation.</strong> Completed tickets accumulate the knowledge of an entire feature or subsystem, and at milestone boundaries they are synthesized into system documentation — an explicit, agent-assisted, human-reviewed pipeline. This is how project-level documentation stays current semi-automatically.</li>
  <li><strong>It compounds.</strong> Tickets form a sequential history of every development step, and each new change begins as a new ticket written against that history. As the library grows, drafting an accurate, fully-contextualized ticket takes less and less developer input — the context is already on file.</li>
</ul>

<p>Beyond the development loop itself, the same library pays off at other levels too:</p>

<ul>
  <li>It consolidates what is otherwise scattered across meeting notes, chat threads, and commit messages into a single, narrated history of the team’s daily work.</li>
  <li>It educates: a new team member — or a new agent — reads the sequence and absorbs both the business requirements and the way the team builds.</li>
  <li>It makes collaboration genuinely asynchronous: a precise ticket replaces the meeting, and a geographically distributed team reviews the artifact on its own schedule instead of hunting for a shared hour.</li>
  <li>It reduces the bus factor: when a developer moves on, the “why” behind their work stays in the repository rather than leaving with them.</li>
  <li>It is reachable by the same search that finds the code: a ticket is a markdown file beside the code it produced, so any keyword or symbol search an agent runs over the repository sweeps the tickets in the same pass — and because the agent compiled the code from the ticket, the two share a vocabulary, so a search for a method or variable name tends to land on the ticket that defined it.</li>
  <li>It is tool-agnostic: the product knowledge — what was built and why — lives in the repository, not in a tracker. Jira, GitHub Projects, or whatever comes next handles only the disposable process layer — iteration, status, assignment — that the knowledge never depends on.</li>
</ul>

<p>Just as important is what a ticket is NOT:</p>

<ul>
  <li><strong>Not a copy of the code.</strong> Tickets carry small, focused snippets as hints — a type definition, an interface, a config fragment — never the implementation; the codebase remains the source of truth for “how” (<a href="https://github.com/ia64mail/quorum/blob/main/tickets/README.md">tickets/README.md</a>). This keeps a focused ticket in the low hundreds of lines — the Quorum library’s median is just under 200 — and limits the confusion and code drift that accumulate as a ticket ages into a historical snapshot.</li>
  <li><strong>Not a deliberation log.</strong> Drafting may explore alternatives, and the ticket may name the ones rejected and why — but it records the final decision, unambiguous and closed. A ticket the agent is expected to compile cannot be open-ended.</li>
  <li><strong>Not documentation.</strong> Documentation is a living reference, updated as the system evolves; a ticket is a time snapshot, written once and almost never modified. It describes the system’s intended transition from state A to state B — and never pretends to describe the current state.</li>
</ul>

<h2 id="does-it-hold-up">Does it hold up?</h2>

<p>A methodology pitch owes the reader evidence, not conviction. So before closing, four measurements — drawn from Quorum’s instrumented history and from the enterprise adoption.</p>

<h3 id="what-it-costs">What it costs</h3>

<p>No single ledger answers this question. Three measurement windows are available — the enterprise adoption before and after its agentic transition, and Quorum’s instrumented history — and each has a different blind spot. The pre-agentic enterprise era is fully measured: compensation known, output known. The agentic enterprise era has known output and unchanged developer cost, but the AI spend riding on top of those salaries was never isolated. Quorum is the mirror image: every API call is metered to the cent, but the author’s time is an after-hours budget no payroll records. A direct comparison between any two of the three would be inaccurate — held side by side, they still bracket the scale of the shift reliably.</p>

<p>Velocity first, because it needs no cost model at all. The same measurement applies to all three windows: lines of merged change per developer-month, counted identically (additions plus deletions on default branches), with machine-generated bulk commits excluded on all sides.</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Method</th>
      <th>Merged lines / dev-month</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Enterprise, pre-agentic</td>
      <td>conventional</td>
      <td>568</td>
    </tr>
    <tr>
      <td>Enterprise, agentic</td>
      <td>ticket-driven, one agent per developer</td>
      <td>5,955 (~10×)</td>
    </tr>
    <tr>
      <td>Quorum, research project (May 2026)</td>
      <td>ticket-driven, orchestrated agent fleet</td>
      <td>~30,300* (~53×)</td>
    </tr>
  </tbody>
</table>

<p>* <em>Ticket prose is counted on both sides — the enterprise team co-locates ticket files in pull requests just as Quorum does. Measured from a single after-hours author; the instrumented seven-ticket window projects to ~29,000, so the month’s figure is a steady cadence, not a sprint.</em></p>

<p>The middle row is the important one. Those are the same developers as in the row above — same product, same salaries; the only thing that changed is the method. They moved to ticket-driven work: at first on tickets I wrote for them, later on tickets they wrote themselves. Output grew tenfold. The third row changes the setup rather than the people — one author driving a whole fleet of role-based agents instead of pairing with a single assistant — and that buys roughly another factor of five.</p>

<p>Now cost — and only two of the three rows can be priced honestly. The enterprise salaries are known for both eras, but what the organization now spends on AI on top of them was never tracked, so the middle row stays open-ended. Quorum is the opposite: the AI bill is metered to the cent, and the human side is an assumption — a few hours of my attention, priced at double a senior salary to keep the estimate honest.</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Developer cost</th>
      <th>AI cost</th>
      <th>Cost per merged line</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Enterprise, pre-agentic (conventional)</td>
      <td>$5–10K/month, assumed</td>
      <td>—</td>
      <td>$9–18 (approximated)</td>
    </tr>
    <tr>
      <td>Enterprise, agentic (ticket-driven)</td>
      <td>unchanged</td>
      <td>not isolated</td>
      <td>≥ $0.8–1.7 (salary-only floor)</td>
    </tr>
    <tr>
      <td>Quorum, research project (ticket-driven fleet)</td>
      <td>$10-15K/month, assumed</td>
      <td>$45.73, metered</td>
      <td>~$0.21 (approximated)</td>
    </tr>
  </tbody>
</table>

<p>The baseline itself is no anomaly: decades of industry measurement, from Capers Jones to the Cutter Consortium and COCOMO, put a normal senior team at a few hundred lines a month and roughly $10–20 per line — exactly where the pre-agentic row sits.</p>

<p>Read the outer rows together: a line of code that cost $9–18 the conventional way comes out at roughly twenty cents — and even then, the AI bill is the small part of the figure; most of it is still the human. <strong>That is the whole cost story of ticket-driven development: you keep paying the developer, the developer produces an order of magnitude more, and the AI riding on top costs pennies</strong>. In these measurements, the magnitude is tenfold for a team that changed nothing but its method — and roughly fifty-fold for an agent fleet in the hands of one practiced author.</p>

<h3 id="whether-quality-erodes">Whether quality erodes</h3>

<p>The most common objection to agent-written code is entropy — surely code produced at ten times the speed rots just as fast. Two metrics answer it together, because each is blind without the other. Halstead Volume treats code as vocabulary: it counts the distinct operators and operands and how often they recur, condensing a codebase into one number — how much information it carries. Divided by lines, it becomes a density: how much each line “says”. Cyclomatic complexity measures what Volume cannot see: the number of independent decision paths through each function — how branchy the logic is, and roughly how many tests it takes to cover. Density alone is ambiguous: a line can be packed because it is expressive, or packed because it is overloaded. Branching resolves the ambiguity — dense and branchy is spaghetti; dense with simple control flow is concise. Both measurements were run over the same three windows as before, with identical tooling end to end.</p>

<p>Density first, measured across each codebase’s full history:</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Volume per line</th>
      <th>Volume per file</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Enterprise, pre-agentic (conventional)</td>
      <td>47 → 63, then flat</td>
      <td>~4,400</td>
    </tr>
    <tr>
      <td>Enterprise, agentic (ticket-driven)</td>
      <td>~46 → 71–74, still rising</td>
      <td>~7,200–8,700</td>
    </tr>
    <tr>
      <td>Quorum, research project (ticket-driven fleet)</td>
      <td>43 → 79, still rising</td>
      <td>~9,400</td>
    </tr>
  </tbody>
</table>

<p>Hand-written code ramps up early and then holds its density for years — it grows larger, not denser. Ticket-driven, agent-compiled code is markedly denser per line and per file, and keeps getting denser. On its own that fact could be read either way, which is exactly the ambiguity the second metric exists to resolve.</p>

<p>So, branching — a snapshot of each codebase as it stands today:</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Enterprise, pre-agentic</th>
      <th>Enterprise, agentic</th>
      <th>Quorum, research project</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Average complexity per function</td>
      <td>1.28</td>
      <td>1.26–1.30</td>
      <td>1.19</td>
    </tr>
    <tr>
      <td>Median / 99th-percentile complexity</td>
      <td>1 / 7</td>
      <td>1 / 6–7</td>
      <td>1 / 5</td>
    </tr>
    <tr>
      <td>Share of functions above complexity 10</td>
      <td>0.5%</td>
      <td>0.2–0.6%</td>
      <td>0.3%</td>
    </tr>
    <tr>
      <td>Functions per file</td>
      <td>4.0</td>
      <td>6.6–10.0</td>
      <td>10.3</td>
    </tr>
    <tr>
      <td>Average function length (lines)</td>
      <td>11.4</td>
      <td>10.9–11.8</td>
      <td>8.8</td>
    </tr>
    <tr>
      <td>Average parameters per function</td>
      <td>0.51</td>
      <td>0.18–0.26</td>
      <td>0.09</td>
    </tr>
  </tbody>
</table>

<p>The extra density carries no branching penalty: complexity per function is statistically identical across all three windows — low everywhere, with the overwhelming majority of functions a straight line of logic. And reading down the columns, the pattern is monotonic across the methodology spectrum — conventional, ticket-driven, ticket-driven fleet: <strong>the deeper into ticket-driven development, the simpler the control flow, the smaller the functions, the leaner the signatures, the more decomposed the files.</strong> The density is expressiveness, not tangle. One scope note, which is really the point of this article: every agentic window in these tables is ticket-driven. The measurements say nothing about agents improvising from free-form prompts — they describe what agents produce when they compile reviewed tickets.</p>

<p><em>Caveat:</em> the two instruments answer different questions on different clocks — the density figures span each project’s whole history, while the branching snapshot describes only the code as it stands today. A manual baseline set against a handful of independent ticket-driven codebases is a consistent trend, not a proof. And neither metric sees readability, test coverage, or architectural soundness — this is strong structural evidence against erosion, not a full quality verdict.</p>

<h3 id="how-far-a-ticket-library-takes-an-agent">How far a ticket library takes an agent</h3>

<p>The two measurements above describe the development loop: what it costs to compile tickets into code, and what that code looks like afterward. This one is about everything else. Tickets compound — each one adds context for the next — and past some depth the library stops being a development aid and becomes the operating knowledge of the whole project: deep enough to drive work at levels the tickets were never written for.</p>

<p>One episode from the enterprise adoption measures the effect directly. The task was not implementation but verification: confirm that a freshly built cross-service feature works end-to-end, in a live multi-tenant test environment, across five repositories/apps. The human steering for the entire session amounted to a few sentences of intent — about eighty words in total, none of them procedural: not which logs to read, not the expected event sequences, not how the feature was wired, not even how to trigger it. Everything operational — environment access, the exact event topics and payload schema, the log signatures to grep on each service, a ready-made acceptance matrix — the agent reconstructed from the ticket library: ~500 tickets accumulated across those five repositories over 12 months of the practice. Before running the test, it predicted the system’s event fan-out <strong>byte-for-byte</strong> from a single ticket, and the live trace confirmed it.</p>

<p>That asymmetry — a paragraph of intent in, a five-repository verification plan out — is what the cost table cannot show. The library converted the agent’s work from discovery into verification: nearly every action was a check against a ticketed expectation rather than open exploration. Against an empty library, the same eighty words would have bought a multi-hour reverse-engineering exercise, with a human feeding in by hand every operational detail the tickets supplied here. <strong>The deeper the library, the higher the level at which a few words of intent can steer the project — not just implementing the next ticket, but verifying the system those tickets built — because everything beneath the intent is already on file.</strong></p>

<h3 id="whether-developers-accept-it">Whether developers accept it</h3>

<p>Nine of ten developers — front-end and back-end specialists across Java/C#, JavaScript/TypeScript, and DevOps/Terraform stacks, spread over three geographically distributed teams — settled into the workflow over a roughly three-month adoption period and built stable, repeatable habits: ticket first, review before implementation, delegate to the agent, review alongside the agent. The most telling observation: <strong>none of them let go of the work once the ticket was written.</strong> They stay close to the code and verify everything — they simply no longer type most of it.</p>

<p><em>Caveat:</em> this is the author’s first-person observation of his own teams, not instrumented data.</p>

<h2 id="references">References</h2>

<p>The measurements above rest on long-established methodology. The primary sources, and the tools that apply them:</p>

<ul>
  <li>Maurice H. Halstead, <em>Elements of Software Science</em>. Elsevier North-Holland, 1977 — the origin of the Volume and Difficulty metrics used throughout “Whether quality erodes.”</li>
  <li>Thomas J. McCabe, <a href="https://doi.org/10.1109/TSE.1976.233837">“A Complexity Measure”</a>. <em>IEEE Transactions on Software Engineering</em>, SE-2(4), 1976 — cyclomatic complexity, including the original recommendation of 10 as the per-function warning threshold.</li>
  <li>Arthur H. Watson and Thomas J. McCabe, <a href="https://www.mccabe.com/pdf/mccabe-nist235r.pdf"><em>Structured Testing: A Testing Methodology Using the Cyclomatic Complexity Metric</em></a>. NIST Special Publication 500-235, 1996 — that threshold’s standardization in testing practice.</li>
  <li>Capers Jones, <em>Applied Software Measurement: Global Analysis of Productivity and Quality</em>, 3rd ed. McGraw-Hill, 2008 — the few-hundred-LOC-per-month productivity band cited in “What it costs.” Jones himself later warned that line-based comparison rewards verbosity and punishes deletion; this article uses the band as a sanity bracket, not a target.</li>
  <li>Barry W. Boehm, <em>Software Engineering Economics</em>. Prentice-Hall, 1981 — the COCOMO cost model behind the per-line estimates cited in “What it costs.”</li>
  <li>Cutter Consortium benchmark studies — the per-delivered-line cost band for senior teams at standard rates.</li>
  <li><a href="https://github.com/terryyin/lizard">lizard</a> — the open-source analyzer behind every cyclomatic-complexity figure in this article.</li>
  <li><a href="https://github.com/ia64mail/quorum/blob/main/tools/entropy-report/README.md"><code class="language-plaintext highlighter-rouge">tools/entropy-report</code></a> — Quorum’s Halstead tokenizer and per-commit aggregation pipeline; its cyclomatic companion drives lizard over the identical source set, so both metrics describe exactly the same code. The same toolchain, unchanged, produced every measurement in every window.</li>
</ul>

<p><em>The datasets behind the enterprise rows are anonymized internal measurements; they are not published.</em></p>]]></content><author><name>Ihor Cherednichenko</name></author><category term="methodology" /><category term="agents" /><summary type="html"><![CDATA[What a ticket becomes when the agent compiles it — and whether the methodology holds up on cost, complexity, and adoption, measured on an enterprise codebase and the Quorum research vehicle.]]></summary></entry></feed>