<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Prince Odame — Writing</title>
    <link>https://princeodame.com/blog/</link>
    <atom:link href="https://princeodame.com/rss.xml" rel="self" type="application/rss+xml"/>
    <description>A software engineer with 9+ years of experience building high-traffic, business-critical systems.</description>
    <language>en</language>
    <managingEditor>opodame@gmail.com (Prince Odame)</managingEditor>
    <webMaster>opodame@gmail.com (Prince Odame)</webMaster>
    <copyright>© 2026 Prince Odame</copyright>
    <lastBuildDate>Thu, 10 Sep 2026 00:00:00 GMT</lastBuildDate>
    <image>
      <url>https://princeodame.com/og.png</url>
      <title>Prince Odame — Writing</title>
      <link>https://princeodame.com/blog/</link>
    </image>
    <item>
      <title>Unlearning How I Write Code</title>
      <link>https://princeodame.com/blog/unlearning-how-i-write-code/</link>
      <guid isPermaLink="true">https://princeodame.com/blog/unlearning-how-i-write-code/</guid>
      <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Prince Odame</dc:creator>
      <description>What I stopped doing when agents took the keyboard: seven habits I had to drop, and the one I kept.</description>
      <category>engineering</category>
      <category>ai-agents</category>
      <category>claude-code</category>
      <category>tooling</category>
      <content:encoded><![CDATA[<p>Some time this year I noticed the hardest part of a task had moved. It used to be writing the code. Now it's the stretch before any code exists, when an agent asks me question after question about what I want and I answer until there's nothing left to guess. I find that stretch tiring in a way typing never was. Then the code comes back, I read it against what I said I wanted, and most of the time it merges.</p>
<p>Andrej Karpathy coined "vibe coding" in early 2025 for the habit of accepting whatever the model writes without reading it. A year later he wrote that the professional version deserved a different word, and offered <a href="https://x.com/karpathy/status/2019137879310836075" target="_blank" rel="noopener noreferrer">agentic engineering</a>:</p>
<blockquote>
<p>"agentic" because the new default is that you are not writing the code directly 99% of the time, you are orchestrating agents who do and acting as oversight. "engineering" to emphasize that there is an art &#x26; science and expertise to it.</p>
</blockquote>
<p>I'm not sure the name will stick. The distinction will. Vibe coding is when you don't care about the code. This is when I care exactly as much as I always did and have stopped being the one typing it. Getting there meant dropping seven habits, most of which I was quite attached to.</p>
<figure>
<div style="background:#ffffff;border:1px solid #e4e7ec;border-radius:12px;padding:1.5rem 1rem 1.1rem;overflow-x:auto;">
<svg viewBox="0 0 560 150" role="img" aria-label="The loop this post describes: decide, spec, slice, build with several agents in parallel, verify, then tune the harness and go around again." style="display:block;margin:0 auto;max-width:100%;height:auto;">
  <defs>
    <marker id="loop-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto">
      <path d="M0 0 L8 4 L0 8 z" fill="#667085"/>
    </marker>
  </defs>
  <g font-family="SF Mono, Fira Code, Menlo, Consolas, monospace" font-size="11">
    <rect x="10" y="40" width="78" height="32" rx="6" fill="#eef1f4" stroke="#d0d5dd"/>
    <text x="49" y="60" text-anchor="middle" fill="#10141a">decide</text>
    <rect x="118" y="40" width="78" height="32" rx="6" fill="#eef1f4" stroke="#d0d5dd"/>
    <text x="157" y="60" text-anchor="middle" fill="#10141a">spec</text>
    <rect x="226" y="40" width="78" height="32" rx="6" fill="#eef1f4" stroke="#d0d5dd"/>
    <text x="265" y="60" text-anchor="middle" fill="#10141a">slice</text>
    <rect x="334" y="40" width="78" height="32" rx="6" fill="#eaf0f6" stroke="#4f6d8f"/>
    <text x="373" y="60" text-anchor="middle" fill="#4f6d8f">build xN</text>
    <rect x="442" y="40" width="78" height="32" rx="6" fill="#eef1f4" stroke="#d0d5dd"/>
    <text x="481" y="60" text-anchor="middle" fill="#10141a">verify</text>
    <text x="280" y="132" text-anchor="middle" fill="#667085" font-size="10">tune the harness, then go again</text>
  </g>
  <line x1="88" y1="56" x2="116" y2="56" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#loop-arrow)"/>
  <line x1="196" y1="56" x2="224" y2="56" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#loop-arrow)"/>
  <line x1="304" y1="56" x2="332" y2="56" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#loop-arrow)"/>
  <line x1="412" y1="56" x2="440" y2="56" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#loop-arrow)"/>
  <path d="M481 72 L481 110 L49 110 L49 74" fill="none" stroke="#98a2b3" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#loop-arrow)"/>
</svg>
</div>
<figcaption style="font-size:0.9rem;color:#667085;margin-top:0.6rem;">Only the build box writes code. Everything before it is deciding, everything after it is checking. That box is several agents building at once. The dashed line is tuning the harness: the instruction files, tools, and permissions the agent runs inside, changed so the same mistake can't recur. That line is the part I skipped for months.</figcaption>
</figure>
<h2>1. Starting with code</h2>
<p>The most expensive thing I ever handed an agent was a sentence like "add auth to this endpoint". I knew what I meant. The agent didn't, so it decided for me, one small decision after another, and I spent the afternoon in the review undoing the ones I disagreed with, one comment at a time. I did this more than once before it sank in that the afternoon was my fault.</p>
<p>So now the first thing I do is get interrogated. An agent asks me one question at a time, never a batch, until nothing about the idea is still an assumption I haven't said out loud. Which roles can hit this endpoint. What happens on token expiry. How a test proves an unauthorized call was refused. It does this because a skill tells it to. I use Claude Code, where a skill is a markdown file of instructions the agent loads when I type its name, so the same procedure runs the same way every time, including on the days I'd rather skip it. Only after the questions does anything get written down, and what gets written is a record of decisions, not a wish list. Matt Pocock, whose skills mine are forked from, is blunter about it than I'd be on <a href="https://www.aihero.dev/skills-to-spec" target="_blank" rel="noopener noreferrer">the page for <code>to-spec</code></a>, his skill that turns the interrogation into a written spec:</p>
<blockquote>
<p>Anything the spec asserts that you never actually said is a defect.</p>
</blockquote>
<p>My job was always deciding. I'd just been doing it in the wrong order, with the code as the medium, which is the slowest and most expensive medium there is for finding out you were wrong.</p>
<h2>2. Keeping the conventions in my head</h2>
<p>A new hire onboards once. An agent onboards every session, from zero, and it does not remember yesterday. Every "we don't do it that way here" that lived in my head got violated at the start of each run, and every time I was surprised, which in hindsight is the surprising part.</p>
<p>So the conventions go in a <code>CLAUDE.md</code>, a text file at the root of the repo that the agent reads at the start of every session. I'll call it the steering file. Boris Cherny, who built Claude Code, <a href="https://x.com/bcherny/status/2007179832300581177" target="_blank" rel="noopener noreferrer">describes his setup</a> as "surprisingly vanilla", and the part that does the work is <a href="https://x.com/bcherny/status/2007179840848597422" target="_blank" rel="noopener noreferrer">one file</a>:</p>
<blockquote>
<p>Our team shares a single CLAUDE.md for the Claude Code repo. We check it into git, and the whole team contributes multiple times a week. Anytime we see Claude do something incorrectly we add it to the CLAUDE.md, so Claude knows not to do it next time.</p>
</blockquote>
<p>Mine is longer than I'd like and I edit it most weeks. The same pressure lands on the code itself. An agent wants clear module boundaries, strong types, fast builds, and errors that say what actually went wrong. I've known this was good for humans for as long as I've done this job. It took a machine to make me do it.</p>
<h2>3. Being the test runner</h2>
<p>When the agent couldn't run the tests, it handed me code that didn't work, and I ran the tests. Prompt, wait, paste the error back, wait again. It felt like pairing. It wasn't. It was me being a slow, expensive shell script.</p>
<p>So the agent gets everything I use to check my own work: the linter, the unit tests, a headless browser it can drive from the command line. All of it has to be fast, because a slow loop for me is a slow loop for the agent, multiplied by every attempt it makes, and it makes a lot. An agent that can see its own failure loops on it until the tests pass. One that can't will tell you, with total confidence, that it's done.</p>
<h2>4. Watching it work</h2>
<p>One agent, one task, me staring at the terminal, is the same throughput as me typing, minus the typing. I did this for a while and called it a productivity gain. The gain showed up when the tasks got longer and the agents more numerous. "Implement this, write the tests, run them, don't come back until they pass" is a thirty-minute task. I can have up to seven of those running, a cap I set in my own tooling and haven't felt the need to raise. I stayed at one for longer than I should have, partly because it felt like losing control and partly because I was afraid of what seven of them would do with my credentials. The second fear was the right one and gets its own section. The first went away when I read Gergely Orosz's observation that <a href="https://x.com/GergelyOrosz/status/1976242900670480763" target="_blank" rel="noopener noreferrer">overseeing parallel agents comes naturally to senior engineers</a>, because they've spent years being the reviewer for a team's parallel work. That stung a little. I'd been that reviewer for years and hadn't made the connection.</p>
<p>An epic, a feature big enough to need a dozen tickets, gets cut into those tickets by another skill, and the tickets have dependency edges between them. The set of tickets whose blockers have all closed is the frontier, and every ticket on the frontier gets its own agent in its own git worktree, a separate checkout so they can't step on each other's files. When one merges, the frontier moves and the next batch goes out. Per-ticket merges land on an integration branch for the epic, and the one pull request from that branch into main is the one I review by hand. The skill is called <code>/implement-with-agent-team</code>; <a href="https://princeodame.com/blog/implement-with-agent-team/">the mechanics</a> are a post of their own. Reading the frontier and dispatching the next batch was never the hard part. I'd been doing it by hand for a month before that occurred to me.</p>
<p>Some numbers, because I'd distrust this post without them. Last ten weeks, the two repos I spend most of my time in. Five hundred and forty pull requests merged, about four hundred and ninety of them opened under my name by an agent I dispatched. Most were single tickets; eight epics account for about a hundred, each ending in one hand-reviewed pull request into main. Two of the five hundred and forty needed a follow-up fix within a day of merging, and nothing was rolled back. Two is low, and I'd have guessed higher. The integration branch catches most of the damage before main, and the eight epic pull requests are where I found the rest. Roughly four in five tickets built on the cheaper model; the rest went to the expensive one on purpose, for tickets where the spec left a judgment call the cheaper one kept getting wrong. The busiest week landed seventy-two merges in one repo. I read seventy-two pull requests that week, against seventy-two specs I'd already argued with an agent about. I was tired. It was a good week.</p>
<h2>5. Approving tool calls one at a time</h2>
<p>Clicking "approve" on every tool call is supervision theatre. I wasn't reading those calls. I was adding latency and telling myself it was oversight, and I kept it up for longer than I'll admit here.</p>
<p>Now I decide once which files an agent may write, which commands it may run, what it may reach on the network, and which credentials it holds, and Claude Code's permission settings enforce that instead of me. Production credentials are never on the list unless I've said so out loud that day. I started narrow and widened as the limits proved themselves, and the only thing that still needs a human is the action that can't be undone.</p>
<p>The most useful fence I have is a small hook, a script that runs right before a tool call and can block it. Mine runs whenever <code>/implement-with-agent-team</code> starts a new agent, and refuses the start if the model that agent will run on hasn't been named. It exists because the model defaults to whatever the spawning agent is running, so an expensive model would otherwise end up on a ticket sized for a cheap one, with nothing in the run's output ever saying so. Nothing told me. I noticed by accident.</p>
<h2>6. Fixing the mistake instead of the cause</h2>
<p>When an agent took a wrong turn, I used to correct it and move on. The turn came back the next session, because nothing about the session had changed, and I'd correct it again, slightly more irritated. Mitchell Hashimoto, who created Terraform, <a href="https://mitchellh.com/writing/my-ai-adoption-journey" target="_blank" rel="noopener noreferrer">calls the fix "harness engineering"</a>, the harness being everything the agent runs inside: "anytime you find an agent makes a mistake, you take the time to engineer a solution such that the agent never makes that mistake again."</p>
<p>The run that taught me this was an epic of eleven tickets in July. Two of them each added a column to the same table, and the ticket graph had no edge between them, because I didn't notice they touched the same migration when the epic was cut. Both built cleanly in their own worktrees. Both merged onto the integration branch within a minute of each other, and the second one's migration fell over on the first one's schema. Nothing reached main. But I spent an hour finding out why, and the fix was not "be more careful next time", because I'd have been exactly as careless the next time. The fix was a check in the ticket-cutting skill that lists every file each ticket expects to touch, flags any two tickets that name the same migration or model, and refuses to finish until I've added a blocking edge or said out loud that the overlap is fine. It has fired four times since. Three of those I would have missed again.</p>
<p>The harness rots, too. When a new model ships, half the workarounds in it are for a weakness the model no longer has, and I'm bad at pruning them. This is the habit I put off longest, and it's the one that makes the other five cheaper the longer I keep it up.</p>
<h2>7. Thinking this was about code</h2>
<p>The same loop runs the design doc, the sprint summary, this post. Decide, hand off, verify, fix the harness. I resisted that for a while too, on the grounds that writing was different. It isn't, particularly.</p>
<h2>The one I kept</h2>
<p>None of this is permission to stop caring. If anything the bar goes up, because the volume goes up. Simon Willison's rule, from <a href="https://simonwillison.net/2025/Mar/19/vibe-coding/" target="_blank" rel="noopener noreferrer">the piece where he drew the line around vibe coding</a>, is the one I hold to: don't commit code you couldn't explain to someone else.</p>
<p>I still review every pull request. What changed is what I read for. Early on I read every line, to learn where the model was reliable and where it wasn't, and I'd tell anyone starting now to do the same for a while. That stopped being possible with several running at once. Now the agent that wrote the code reviews it itself before opening the pull request, checking what a checklist can check. My eyes go to what a checklist can't: whether the design will hold up at production traffic, what the change does to the systems upstream and downstream, whether the security boundary still holds. If it regresses in production, it's mine, whoever typed it.</p>
<p>My first weeks were slower and I nearly gave up twice. I spent them writing steering files and cutting modules until they explained themselves. Writing the code got cheap. Deciding what to write still takes the whole afternoon, and I don't think that's going to change.</p>]]></content:encoded>
    </item>
    <item>
      <title>Building the /implement-with-agent-team skill</title>
      <link>https://princeodame.com/blog/implement-with-agent-team/</link>
      <guid isPermaLink="true">https://princeodame.com/blog/implement-with-agent-team/</guid>
      <pubDate>Thu, 03 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Prince Odame</dc:creator>
      <description>Why the tickets I hand Claude Code almost always come back mergeable, and how /implement-with-agent-team turns that trust into a skill that runs a whole epic to completion on its own.</description>
      <category>engineering</category>
      <category>ai-agents</category>
      <category>claude-code</category>
      <category>tooling</category>
      <content:encoded><![CDATA[<p>For most of this year, my Wednesday afternoons looked the same: open a ticket, run <code>/implement</code>, the skill that builds one ticket end to end, and watch an agent build it in a fresh context. What I noticed, ticket after ticket, wasn't really the code. It was my own behavior around it. I'd skim the diff, check that the tests it wrote actually failed before the fix and passed after, and merge. I'd stopped reading every line months before I admitted that to myself.</p>
<p>That wasn't recklessness. By the time a ticket ever reached <code>/implement</code>, it had already been through three rounds of scrutiny. I'd been grilled about it one question at a time until nothing was still a guess. That got folded into a spec. The spec got cut into a slice sized to fit one context and worth merging on its own. What the agent received wasn't a request so much as a decision I'd already made and written down. <code>/implement</code> just had to go execute it, test-first, against a checklist it wrote for itself and re-checked before ever calling the work done. Once I saw that clearly, the question stopped being "can I trust this output" and became "why am I still doing this one ticket at a time."</p>
<p>An epic is rarely one ticket. It's a dozen, with real edges between them: three that can start today, four that can't start until the first three close, one at the bottom of the graph that everything else quietly depends on. Running that manually meant becoming, personally, the scheduler: watching what had merged, figuring out what it unblocked, typing <code>/implement</code> again. <code>/implement-with-agent-team</code> is what happens when you take that job away from the person holding it, because the person was never the part doing anything hard. The skill calls me the <strong>lead</strong>: I dispatch, verify, and merge. It calls each agent it spawns a <strong>teammate</strong>: the teammate only writes code.</p>
<h2>The problem with a shared to-do list</h2>
<p>Before this skill, "run a few tickets in parallel" meant writing a prompt myself every time: here are the tickets, here's roughly what depends on what, go. I did that for over a month before I ever formalized it. It worked, but it was bespoke labor for every batch, and it depended on me keeping the dependency graph straight, from memory alone.</p>
<p>The tempting fix is a shared to-do list: some file sitting alongside the repo that every teammate reads from and writes back to, tracking which tickets are done, in flight, or blocked. It's also the wrong fix. That file would sit next to two records that already exist and already have to agree with each other: the git branches, and the issue tracker itself. A third record of the same facts doesn't remove that disagreement, it adds a third way for it to happen, and every drift is its own small debugging session. That's exactly the kind of state-sync bug that's tedious to find and depressingly easy to reintroduce the next time you change the code.</p>
<h2>GitHub is already the state machine</h2>
<p>The real fix is structural, not a smarter sync algorithm. GitHub already <em>is</em> the state machine:</p>
<ul>
<li>The dependency graph already exists, as the tickets' native "blocked by" links.</li>
<li>Progress already exists, as open versus closed.</li>
<li>Completion already exists, because a merged pull request that references a ticket closes it.</li>
</ul>
<p>So there's no second copy. A small script the skill carries with it, <code>tickets.mjs</code>, reads all of this fresh from the tracker on every call and writes exactly one thing back: a plain, readable comment on the ticket itself, the only claim mechanism it has. Something a person skimming the ticket later understands right away, not a code only the script can decode. A missed notification or a stale read stops being a bug, because the next call just re-derives the truth from the tracker instead of trusting something cached from a moment ago.</p>
<h2>What you actually type</h2>
<p>I invoke the skill myself; it never invokes itself mid-task. I tell it, in plain language, what to work on, an epic or a specific set of tickets, and it figures out the rest:</p>
<pre><code>/implement-with-agent-team #1125
/implement-with-agent-team backend tickets of #1125
/implement-with-agent-team tickets 948 949 950
</code></pre>
<p>That second form matters more than it looks. When an epic spans two repositories, saying "backend tickets" is enough to scope one run to the backend half and leave the frontend half for a separate session, instead of one run trying to do both at once.</p>
<h2>Reading the frontier</h2>
<p>The skill's central idea is the <strong>frontier</strong>: every open ticket whose blockers have all closed. That's the set of work that can start right now, and <code>tickets.mjs frontier</code> recomputes it from the tracker on every call. Nothing about it is stored.</p>
<figure>
<div style="background:#ffffff;border:1px solid #e4e7ec;border-radius:12px;padding:1.5rem 1rem 1.1rem;overflow-x:auto;">
<svg viewBox="0 0 460 210" role="img" aria-label="Four tickets in a dependency graph. Ticket 948 is done, 949 is ready, 950 and 951 are blocked behind 949." style="display:block;margin:0 auto;max-width:100%;height:auto;">
  <defs>
    <marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto">
      <path d="M0 0 L8 4 L0 8 z" fill="#98a2b3"/>
    </marker>
  </defs>
  <line x1="96" y1="105" x2="152" y2="105" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#arrow)"/>
  <line x1="248" y1="95" x2="302" y2="58" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#arrow)"/>
  <line x1="248" y1="115" x2="302" y2="152" stroke="#98a2b3" stroke-width="1.5" marker-end="url(#arrow)"/>
  <g font-family="SF Mono, Fira Code, Menlo, Consolas, monospace" font-size="12">
    <rect x="10" y="88" width="86" height="34" rx="6" fill="#eef1f4" stroke="#d0d5dd"/>
    <text x="53" y="103" text-anchor="middle" fill="#10141a">#948</text>
    <text x="53" y="115" text-anchor="middle" fill="#667085" font-size="9.5">DONE</text>
    <rect x="162" y="88" width="86" height="34" rx="6" fill="#eaf0f6" stroke="#4f6d8f"/>
    <text x="205" y="103" text-anchor="middle" fill="#4f6d8f">#949</text>
    <text x="205" y="115" text-anchor="middle" fill="#4f6d8f" font-size="9.5">READY</text>
    <rect x="312" y="24" width="86" height="34" rx="6" fill="#f9fafb" stroke="#e4e7ec"/>
    <text x="355" y="39" text-anchor="middle" fill="#667085">#950</text>
    <text x="355" y="51" text-anchor="middle" fill="#98a2b3" font-size="9.5">BLOCKED</text>
    <rect x="312" y="152" width="86" height="34" rx="6" fill="#f9fafb" stroke="#e4e7ec"/>
    <text x="355" y="167" text-anchor="middle" fill="#667085">#951</text>
    <text x="355" y="179" text-anchor="middle" fill="#98a2b3" font-size="9.5">BLOCKED</text>
  </g>
</svg>
</div>
<figcaption style="font-size:0.9rem;color:#667085;margin-top:0.6rem;">The frontier is every open ticket whose blockers have all closed. When #949 merges, #950 and #951 both become takeable at once, and both get dispatched together, in a single message.</figcaption>
</figure>
<p>Each ticket carries exactly one status, and only three of them expect anything from me:</p>
<table>
<thead>
<tr>
<th>Status</th>
<th>Meaning</th>
<th>My move</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>READY</code></td>
<td>Blockers all closed, unclaimed</td>
<td>Claim it and dispatch a teammate</td>
</tr>
<tr>
<td><code>BUILDING</code></td>
<td>A teammate already has it</td>
<td>Wait</td>
</tr>
<tr>
<td><code>REVIEW</code></td>
<td>A teammate reported back, unverified</td>
<td>Verify it, then merge</td>
</tr>
<tr>
<td><code>BLOCKED</code></td>
<td>A blocker is still open</td>
<td>Nothing; it frees itself</td>
</tr>
<tr>
<td><code>STUCK</code></td>
<td>Nothing available can move it forward</td>
<td>Report it, carry on without it</td>
</tr>
<tr>
<td><code>DONE</code></td>
<td>Closed</td>
<td>Nothing</td>
</tr>
</tbody>
</table>
<h2>The loop</h2>
<p>I run this until the frontier reports the epic closed. It's genuinely cyclical: every merge can unlock new tickets, which is why a run keeps going instead of stopping after one pass.</p>
<ol>
<li><strong>Read the frontier.</strong> The first action each time, including when resuming yesterday's run.</li>
<li><strong>Claim every ready ticket</strong>, up to seven at once. Claiming a ticket records the model it'll build with, sonnet by default, and the branch it should build against, and is what stops the next read from giving the same ticket to two teammates.</li>
<li><strong>Dispatch them all in one message</strong>, one fresh teammate per ticket, each in its own worktree, an isolated copy of the repo it can build in without affecting anyone else's. This is where the actual parallelism happens; spawning one at a time and waiting between each would make the whole run serial for no reason.</li>
<li><strong>Take each return</strong>: verify the pull request actually does what it claims, record it, merge it, confirm the merge actually closed the ticket rather than trusting the report that it would.</li>
<li><strong>Back to the top.</strong></li>
</ol>
<p>A teammate's brief is deliberately short. It names the ticket as <code>owner/repo#number</code>, so the teammate reads the real thing instead of my paraphrase of it. It says to invoke <code>/implement</code> and follow it, and it gives the branch and base I just claimed. It requires the pull request to say <code>Closes #&#x3C;number></code> in the body, and to come back with the PR number, what ran and its counts, and the teammate's own <code>code-review</code> output verbatim. If a rule in the ticket turns out to be wrong or impossible, the brief asks it to say so with evidence, not quietly work around it. I spawn every ticket in the batch this way, including the one that looks too small to bother with and the one in the repository I'm already sitting in. A ticket that looks too hard to delegate gets a missing fact added to its brief, not a decision I make myself instead.</p>
<p>By default a run doesn't target the org's actual default branch at all. The first ticket that needs one creates an integration branch named after the epic, and every ticket in the run builds against it. Merging a ticket's pull request onto that branch needs no approval from me, because it isn't shared history, nobody depends on it yet, and a bad merge there costs nothing but a follow-up ticket. The one pull request that does get reviewed by a human is the single one opened from that integration branch into the org's real default branch once the whole epic is done, and I open it, I never merge it myself. That's the one review that still happens manually, because it's the only merge in the whole run that actually changes the branch other people's work depends on.</p>
<h2>When a ticket stops</h2>
<p>When a ticket comes back without a working pull request, there are only two honest outcomes, and just one of them needs me. Either the teammate found a real dependency the graph was missing, in which case it gets recorded as a native "blocked by" link and frees itself automatically once that blocker closes, or nothing available can move the ticket forward, in which case the run reports it and continues without it rather than stalling the whole epic over one ticket.</p>
<h2>Why a missed notification doesn't stop the run</h2>
<p>Agent notifications occasionally just don't arrive. Because nothing about this skill is remembered between turns, a missed notification can't corrupt anything; the next frontier read just sees the pull request and continues. The real risk is quieter: the run can keep waiting, unaware the news already happened. So I keep a change feed running alongside the work, watching the epic and printing one line per status change, and it exits on its own once the epic is done. That still leaves one failure mode neither a notification nor a status change would ever reveal: a teammate that stopped before ever opening a pull request. There's no event for that, so the feed doesn't wait for one. A ticket sitting <code>BUILDING</code> for twenty-five minutes with no result yet gets flagged stale on its own, told apart from a build that's merely slow, and given to a fresh teammate instead of being abandoned.</p>
<h2>Keeping the cost visible</h2>
<p>Every teammate spawn has to name the model it's running on. That's a deliberate, enforced choice: the parameter is optional and inherits from whatever's running the skill, so an expensive model could easily work through a ticket sized for a cheap one, quietly, with nothing in the run's output ever saying so. A <code>PreToolUse</code> hook, code that runs right before a tool call and can block it, is set up automatically when the skill is installed. It refuses any spawn under this skill that hasn't stated its model explicitly, and it separately refuses one spawned as a fork, a lightweight spawn mode that always inherits the lead's own model regardless of what's asked for, so it can never actually be made cheap. The hook only looks at spawns this skill makes, so it never affects anything else running on the same machine. Most tickets are specced tightly enough that sonnet, the cheaper model, builds them correctly; the ones that need real judgment the spec can't pre-make get opus instead, deliberately, and the split gets reported at the end of the run.</p>
<h2>What it deliberately doesn't do</h2>
<p>This skill has a short list of jobs it deliberately refuses, and the refusals are as much the design as anything it does do:</p>
<ul>
<li><strong>It doesn't plan.</strong> Tickets arrive already specced, with blocking links already in place. A ticket set with no dependency edges at all gets returned rather than ordered by guesswork.</li>
<li><strong>It doesn't write the brief.</strong> A teammate reads the ticket itself; the ticket <em>is</em> the spec.</li>
<li><strong>It doesn't triage for me.</strong> Judging whether a review finding actually matters is my job, not something this skill decides for me.</li>
<li><strong>It doesn't keep state.</strong> That's the one that made the rest of this possible: not tracking anything of its own is what lets the whole skill reduce to something small enough to actually reason about.</li>
</ul>
<p>Everything that happens before it is where the actual trust gets built, one step at a time, long before any teammate writes any code.</p>
<p>When I have an idea worth turning into tickets, I start by getting grilled about it, with the <code>grilling</code> skill. That means one question at a time, never a batch, each one checked against a running tree of decisions, until nothing about the idea is still an assumption I never stated explicitly. If a question needs a fact rather than a choice, say, whether a piece of code already does something, or a table already has a column, a sub-agent checks it. I'm never asked to search for anything I don't have to. Only the real decisions come to me, one at a time, with a recommendation attached.</p>
<p>Once that conversation is done, <code>to-spec</code> synthesizes it, with no second interview, straight into a spec: a problem statement, user stories, the implementation and testing decisions we actually made, and what's explicitly out of scope. <code>to-tickets</code> then cuts that spec into tickets. Each one is a vertical slice through every layer the feature affects, sized to fit one context, and each one states exactly which other tickets have to finish before it can start. I get quizzed back on that breakdown before any of it ships, because getting the blocking edges wrong here is the one mistake that would break everything <code>/implement-with-agent-team</code> does afterward.</p>
<p>By the time a ticket reaches a teammate, the teammate invokes <code>/implement</code> to actually build it, and that adds one more layer of self-checking. Before writing any code, <code>/implement</code> turns every acceptance criterion into a gate in its own ledger, stated as something it can observe directly. It isn't allowed to call the work done while any gate is still unmet. It builds test-first at the seams the spec already agreed on, and it ends by running its own code review and committing.</p>
<p>Five things happen before an epic turns into code, and only the last one actually writes any of it. <code>grilling</code> interrogates the idea. <code>to-spec</code> writes the spec. <code>to-tickets</code> cuts the tickets. <code>implement-with-agent-team</code> runs the resulting graph to completion, one merge at a time. <code>implement</code>, running inside every teammate it spawns, is the one that writes the code. That order is what let me stop rereading every diff by the time it reached me. The ticket a teammate gets was never really a request. It was a decision I'd already made, several skills earlier, deliberately.</p>
<p>For a single ticket with nothing depending on it, I still just run <code>/implement</code> directly and skip all of the steps above. This skill is worth using specifically when there's a batch with real dependency structure between the pieces.</p>
<h2>How to get it</h2>
<p>It ships as a Claude Code plugin, in <a href="https://github.com/Odame/skills" target="_blank" rel="noopener noreferrer">my fork</a> of Matt Pocock's <a href="https://github.com/mattpocock/skills" target="_blank" rel="noopener noreferrer">skill repo</a>, bundled alongside <code>to-spec</code>, <code>to-tickets</code>, <code>implement</code>, <code>grilling</code>, and the rest of the set it depends on:</p>
<pre><code>claude plugin marketplace add Odame/skills
claude plugin install odame-skills@odame
</code></pre>
<p>That fork-specific install matters: <code>implement-with-agent-team</code> is my own addition and only exists in <a href="https://github.com/Odame/skills" target="_blank" rel="noopener noreferrer"><code>Odame/skills</code></a>, so Matt's own marketplace listing (<code>claude plugin install mattpocock-skills</code>, no <code>@odame</code>) won't have it. The plugin id is <code>odame-skills</code>, but every skill and slash command inside it still keeps the name Matt documents in his own repo; only the id differs. Everything else in the bundle is his, tracked against <a href="https://github.com/mattpocock/skills" target="_blank" rel="noopener noreferrer"><code>mattpocock/skills</code></a> with any local changes recorded in <a href="https://github.com/Odame/skills/blob/main/PATCHES.md" target="_blank" rel="noopener noreferrer"><code>PATCHES.md</code></a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Hello World</title>
      <link>https://princeodame.com/blog/hello-world/</link>
      <guid isPermaLink="true">https://princeodame.com/blog/hello-world/</guid>
      <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Prince Odame</dc:creator>
      <description>How this site&apos;s blog is built: a plain-markdown Gatsby setup with no new dependencies.</description>
      <category>meta</category>
      <category>gatsby</category>
      <content:encoded><![CDATA[<p>This <a href="https://princeodame.com/">site</a> has been a static portfolio for years: one page, hardcoded sections, no blog. I wanted a place to write about the things I actually build, so I added one.</p>
<h2>Reusing the existing content pipeline</h2>
<p>The constraint I set myself: no new dependencies. This site already runs <code>gatsby-source-filesystem</code> and <code>gatsby-transformer-remark</code> to pull markdown into the homepage: job history, project descriptions, the about section all work this way. A blog is just more of the same content pipeline, pointed at a new folder.</p>
<p>So a post here is a directory:</p>
<pre><code>src/content/blog/2026-09-02-hello-world/index.md
</code></pre>
<h2>From folder to URL</h2>
<p>The date prefix is cosmetic; it makes the folder list in order on disk. <code>gatsby-node.js</code> strips it before turning the folder into a URL, so this post lives at <code>/blog/hello-world/</code>, not <code>/blog/2026-09-02-hello-world/</code>. Slugs come from <a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#onCreateNode" target="_blank" rel="noopener noreferrer"><code>createFilePath</code></a>, one of Gatsby's node APIs; page creation happens in <a href="https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#createPages" target="_blank" rel="noopener noreferrer"><code>createPages</code></a>, which runs a GraphQL query over <code>allMarkdownRemark</code> and calls <code>createPage</code> once per result.</p>
<h2>Minimal frontmatter, on purpose</h2>
<p>Frontmatter is small on purpose: <code>title</code>, <code>date</code>, <code>description</code>, <code>tags</code>. Nothing about the blog needed more than that yet. If it does later, I'll add a field when there's an actual page that reads it, not before.</p>
<p>That's the whole setup. More posts to come, on whatever I'm building at the time.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
