↓ Skip to main content

slack-cached - Cache Slack threads, channels, and users to a local SQLite database

Note (2026-07-02): slack-cached has been renamed to slackx.

Slack is where your team’s decisions live, but the data doesn’t belong to you. Search is slow, threads scroll out of reach, and the moment you leave a workspace the history is gone. There’s no official CLI, and the web client is the only first-class way to read anything.

I built slack-cached to fix this. It’s a small Python CLI that caches Slack threads, channel messages, users, and channels to a local SQLite database. Once cached, the data is yours: query it with SQL, grep it, feed it to an LLM, or just read it offline.

The problem
#

Slack makes exporting and archival surprisingly hard. Export tools exist for admins, but most members aren’t admins. The search box returns messages, but not in a form you can slice, join, or version. Important decisions get buried in threads that nobody scrolls back to.

The problem gets worse when you want to do anything programmatic. Building a knowledge base, summarizing a channel, or tracking decisions all require raw access to the messages. Hitting the Slack API on demand works, but you pay the latency and rate-limit cost every time, and edits disappear if you only ever fetch live.

How slack-cached works
#

$ uv sync
$ uv run slack-cached --help
usage: slack-cached [-h] {fetch,show,fetch-users,fetch-channels,show-users,show-channels,poll} ...

Cache Slack threads to a local SQLite database.

Cache a thread from a URL (no stdout, just a one-line summary on stderr):

$ slack-cached fetch https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
cached 3 messages (3 new/updated, full) for C0123ABCDEF/1700000000.123456

Or by explicit channel and timestamp; run it again and it only asks Slack for what changed:

$ slack-cached fetch --channel C0123ABCDEF --ts 1700000000.123456
cached 3 messages (3 new/updated, full) for C0123ABCDEF/1700000000.123456

$ slack-cached fetch --channel C0123ABCDEF --ts 1700000000.123456   # run again
cached 4 messages (1 new/updated, incremental) for C0123ABCDEF/1700000000.123456

Read it back, human-readable by default:

$ slack-cached show https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
Thread C0123ABCDEF/1700000000.123456
3 message(s)

[2023-11-14T22:13:20+00:00] Alice Smith (alice)
    Has anyone tried the new deploy script?

[2023-11-14T22:15:12+00:00] Bob Lee (bob)
    Yes, but you need to bump the token first.

[2023-11-14T22:16:45+00:00] Alice Smith (alice)
    Thanks!

show auto-fetches if the thread isn’t cached yet, and renders real names like Alice Smith (alice) instead of raw user ids once you’ve cached users. Pass --json to get the raw records for piping into other tools:

$ slack-cached show --json https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
{
  "channel": "C0123ABCDEF",
  "channel_name": "deploys",
  "thread_ts": "1700000000.123456",
  "message_count": 3,
  "messages": [
    {
      "ts": "1700000000.123456",
      "user": "U1",
      "text": "Has anyone tried the new deploy script?",
      "payload": { "ts": "1700000000.123456", "user": "U1", "type": "message" },
      "user_name": "Alice Smith (alice)"
    },
    {
      "ts": "1700000112.000200",
      "user": "U2",
      "text": "Yes, but you need to bump the token first.",
      "payload": { "ts": "1700000112.000200", "user": "U2", "type": "message" },
      "user_name": "Bob Lee (bob)"
    },
    {
      "ts": "1700000205.500300",
      "user": "U1",
      "text": "Thanks!",
      "payload": { "ts": "1700000205.500300", "user": "U1", "type": "message" },
      "user_name": "Alice Smith (alice)"
    }
  ]
}

Channels, users, and polling
#

Cache every top-level message in a channel:

$ slack-cached fetch --channel C0123ABCDEF
cached 142 messages for C0123ABCDEF (38 fetched)

Add --full-threads to also pull every reply thread, so you get the full conversation tree:

$ slack-cached fetch --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)

For continuous capture, poll watches multiple channels and fetches new messages on a schedule. Each entry in --channels can be a channel id, a bare name (general), or a #-prefixed name (#general); names are resolved against the cached channels, which is why the per-cycle summary below reports ids. Progress goes to stderr, and (because of --json) one compact JSON object per cycle goes to stdout. Ctrl+C stops it gracefully:

$ slack-cached poll --channels #general,#engineering,#deploys --interval 5m --last 5m --full-threads --json
polling 3 channel(s) every 5m (lookback: 5m, full_threads: True, concurrency: 3)
{"cycle": 1, "elapsed_seconds": 1.234, "channels": [{"channel": "C001", "fetched": 3, "total": 312}, {"channel": "C002", "fetched": 4, "total": 188}, {"channel": "C003", "fetched": 0, "total": 27}]}
cycle 1: 7 new message(s) across 3 channel(s) in 1.2s
^C
poll stopped after 1 cycle(s)

That stdout stream is easy to wire into a downstream pipeline or a knowledge-base builder.

Finally, cache the workspace’s users and channels so threads can be rendered with real names:

$ slack-cached fetch-users
processed 184 users (184 added, 184 total in db)
$ slack-cached fetch-channels
processed 37 channels (37 added, 37 total in db)
$ slack-cached show-users
184 user(s)

U1  alice - Alice Smith
U2  bob - Bob Lee
...
$ slack-cached show-channels --json
{
  "channel_count": 37,
  "channels": [
    {
      "id": "C1",
      "name": "general",
      "is_private": false,
      "fetched_at": 1700000260.0,
      "payload": { "id": "C1", "name": "general", "is_channel": true }
    },
    ...
  ]
}

The refresh strategy
#

fetch always reaches out to Slack, but it’s incremental. On a re-fetch, it calls conversations.replies with oldest=<latest_cached_ts>, so the API returns only new replies and any edits at the boundary. Messages are upserted by ts, which means edits replace the old text in place instead of creating duplicates.

Rate limits are handled for you. HTTP 429 / ratelimited responses are retried with exponential backoff, up to five attempts, respecting the Retry-After header. That matters for --full-threads and poll, where you can easily fire hundreds of calls against a busy channel.

Where the cache lives
#

The database defaults to $XDG_CACHE_HOME/slack-cached/threads.db (typically ~/.cache/slack-cached/threads.db). Override it per command with --db /path/to/file.db. Because it’s plain SQLite, you can open it directly and ask it anything Slack’s search box can’t:

$ sqlite3 ~/.cache/slack-cached/threads.db \
    "SELECT (u.real_name || ' (' || u.name || ')') AS who, COUNT(*) AS msgs
     FROM messages m JOIN users u ON m.user = u.id
     WHERE m.channel = 'C0123ABCDEF'
     GROUP BY who ORDER BY msgs DESC LIMIT 5;"
Alice Smith (alice)|142
Bob Lee (bob)|87
Carol Ng (carol)|53

That query is the real point of the tool. The cache is not an opaque blob; it’s a table of messages you can SELECT from, join against users and channels, and export however you like.

Authentication
#

Credentials load from environment variables first, then a config file at $XDG_CONFIG_HOME/slack-cached/config:

SLACK_TOKEN=xoxb-...
SLACK_COOKIE=...
SLACK_API_BASE_URL=https://slack.com/api

SLACK_COOKIE is there for xoxc- web-client tokens, which need the matching cookie to authenticate. Every command also accepts --api-base-url, which is how the built-in fake Slack server plugs in.

A fake Slack server, for free
#

The repo ships slack-fake-server, a deterministic fake Slack API for testing:

$ uv run slack-fake-server --port 8199 --num-threads 50 --rate-limits
2026-06-15T12:00:00Z [info     ] fake_slack_server_starting   host=127.0.0.1 port=8199 seed=42 users=8 channels=5 threads=50 rate_limits=True

It serves conversations.list, conversations.replies, conversations.history, and users.list, and can simulate Slack-tier rate limiting. Point slack-cached at the fake server and you can develop and test against a realistic API without touching your real workspace:

$ slack-cached fetch --api-base-url http://localhost:8199/api --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)

When this is useful
#

  • Knowledge bases and channel summaries. Poll a set of channels, then build monthly digests or feed the SQLite cache to an LLM.
  • Decision tracking. Important calls often happen in threads. Cache them so they survive workspace churn and account turnover.
  • Offline access and archival. Keep a readable copy of the conversations you actually care about, independent of Slack’s retention window.
  • Bulk analysis. Once the data is in SQLite, you can answer questions with a query that Slack’s search box can’t express.

What to Do Next
#

$ git clone https://github.com/TomzxCode/slack-cached
$ cd slack-cached
$ uv sync
$ export SLACK_TOKEN=xoxb-...
$ slack-cached fetch-channels
processed 37 channels (37 added, 37 total in db)
$ slack-cached fetch --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)
$ slack-cached show https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
Thread C0123ABCDEF/1700000000.123456
3 message(s)

[2023-11-14T22:13:20+00:00] Alice Smith (alice)
    Has anyone tried the new deploy script?

[2023-11-14T22:15:12+00:00] Bob Lee (bob)
    Yes, but you need to bump the token first.

[2023-11-14T22:16:45+00:00] Alice Smith (alice)
    Thanks!

Repository and source code, documentation.

References
#


Developer Trust Profiles: Earned Scrutiny for Automated Code Review

When you automate code review, the obvious design is to treat every pull request identically. Same checks, same threshold, same outcome, regardless of author. This obvious design is wrong.

A reviewer that applies the same scrutiny to a developer who has shipped two hundred clean PRs and to a stranger on their first contribution is either too strict for the first or too lax for the second. It is a blunt instrument, and bluntness is the enemy of autonomy. The more you rely on automated approval to unblock work, the more you need a mechanism that varies scrutiny by evidence.

I built that mechanism as a pair of skills in my agent library, developer-trust-profile and initialize-developer-trust-profile. This is the idea behind them, and why I believe it is the missing primitive for autonomous code review.

The problem with a reviewer that has no memory
#

Most automated review tools are stateless. They look at a diff, run their checks, and emit a verdict. Next PR, they start over, as if they had never seen the author before.

This is not how any experienced human reviewer works. When a trusted colleague opens a PR, you skim it, confirm the tests pass, and approve. When someone with a history of breaking the build opens one, you read every line. You already know who tends to forget tests, who mixes unrelated refactors into a single commit, who writes the clearest descriptions. That knowledge is not bias to be eliminated; it is signal, accumulated over hundreds of reviews, that tells you where to spend your attention.

A stateless automated reviewer throws all of that signal away. It re-derives, badly and from scratch, what a human reviewer simply remembers. The result is a system that either approves indiscriminately (unsafe) or applies maximum suspicion to everyone (slow, and corrosive to trust).

The trust profile is the answer to a direct question: how does an automated reviewer remember?

What a trust profile stores
#

Each developer gets a single file, ~/.developer-trust/{github_username}.md, and the whole directory is a git repository. The file accumulates observations across every review.

Four trust levels exist, and each one maps to a different behavior, not just a different label:

Level Meaning Effect on the automated reviewer
trusted Consistently clean, high-quality PRs Standard checks, lean toward approval on borderline cases
neutral Unknown or mixed track record Standard checks, default behavior
cautious History of issues, missed edge cases, unclear PRs Stricter interpretation, flag marginal cases as failures
always_reject Persistent quality or policy issues Skip entirely, never auto-approve, require manual review

Beyond the level, the profile keeps a running overview, lists of strengths and weaknesses observed across PRs, recurring PR patterns, and a full review history table with dates, repos, outcomes, and a one-line note per review.

Crucially, it is plain markdown in a git repo. That makes the system’s memory auditable, diffable, and portable. You can see the exact review that tipped an author from neutral to cautious, and the reasoning behind it. For a mechanism that gates code into production, that paper trail is not a nice-to-have; it is a requirement.

How earned trust changes review behavior
#

The levels are not decorative. They modify how the automated reviewer interprets its own checks.

The reviewer I run, quick-pr-review, evaluates a fixed set of gates: significant public interface changes, security-sensitive code, new dependencies, reversibility, passing CI, alignment with the linked issue’s acceptance criteria. For a neutral author, each gate is binary. For a trusted author, a borderline call (is this small new export a “significant” public interface change?) leans toward passing. For a cautious author, that same borderline call is treated as a failure.

This is differential scrutiny, and it is the whole point. The checks are identical; the threshold moves with evidence. A trusted author gets unblocked faster because the reviewer stops re-litigating cases it has effectively already won. A cautious author gets caught earlier, before a recurring weakness reaches production again.

And then there is always_reject, the hard stop. When an author sits at that level, the reviewer does not fetch the diff, does not post a comment, does not approve. It reports that the PR was skipped and that a human needs to look. This is the system saying: I have enough evidence to know I should not be making this decision.

The loop closes
#

The profile is not a static config file that a human maintains by hand. It is both consumed and produced by the review pipeline.

Before a review, quick-pr-review reads the author’s profile to set its thresholds. After the review, it writes back: it appends a row to the history, merges new observations into the strengths and weaknesses, and reconsiders the trust level if the accumulated evidence warrants it. Each profile update is committed to the local git repo with a message like Update alice trust profile (approved: acme/api#42).

This makes the system a slow learner rather than a judge. A single review does not move the trust level, unless it is egregious. Trust degrades through recurring patterns observed across many PRs, and the level only shifts when the weight of evidence demands it. That hysteresis is deliberate. It prevents one bad day from permanently labeling a developer, and it prevents one lucky PR from granting unwarranted autonomy.

Bootstrapping without survivorship bias
#

The hardest part of any reputation system is the cold start. A new contributor, or a contributor new to the system, starts at neutral with an empty file. That is safe but unhelpful; you want a profile grounded in reality, not a blank slate that takes months to fill.

The initialize skill bootstraps a profile from history by scanning the author’s last N pull requests across every repository the token can see. The detail that separates this from a naive implementation is that it samples both merged and rejected PRs.

Sampling only merged PRs is survivorship bias in its purest form. It would make every developer look good, because the failures were quietly closed and forgotten. By pulling closed-without-merge PRs as well, and by deriving the outcome from actual review data (was there a changes-requested that was never superseded?) rather than assuming merge equals approval, the bootstrap produces a profile that reflects how a developer actually works, not just their wins.

Processed oldest-first, each historical PR feeds into the same update pipeline as a live review, so a freshly initialized profile looks exactly like one that was built up review by review over time.

Real limitations
#

The design has real trade-offs, and they are worth naming.

Profiles live on the reviewer’s machine and are not shared. This is a feature (no global reputation database, no public scoring of humans) and a cost (each reviewer builds a different picture, and the memory does not transfer). For a single operator running their own agents, that is the right trade. For an organization, you would want a shared, access-controlled store, and the design does not pretend otherwise.

There is a risk that a reputation hardens into something a developer cannot escape. The skills mitigate this by removing observations that recent evidence contradicts and by reconsidering the level on every update. But any system that summarizes a human into a label can lock them in. The fix is transparency (everything is in a diffable file) and a human who can edit the file when the summary is wrong.

There is also a gaming risk. Once authors know a trust system exists, they can optimize for it. On balance this is fine: optimizing for clean, well-tested, well-scoped PRs that reference their issues is exactly the behavior you wanted anyway. The system fails safe, because the worst case is that people produce better PRs.

Why this matters more in the age of LLM-authored code
#

I have written elsewhere about why human review of LLM-generated code is a poor use of attention (see The Future of Code Review). The short version is that the human’s leverage has moved upstream, to specifying the problem, while machines verify compliance.

But that argument has a gap. Even after you accept automated review, you are left with a follow-up question: should the automated reviewer treat every author identically? The answer is no, and for a reason that is sharper now than it was five years ago.

In an LLM-heavy workflow, the “author” of a PR is increasingly a human paired with a model, or an agent operating on a human’s behalf. The trust profile stops measuring just a developer’s coding skill and starts measuring something more valuable: the quality of a human’s oversight of their tools. Two developers can submit PRs that an LLM wrote. One vets the output carefully, keeps PRs atomic, links the spec, and ships reversible changes. The other rubber-stamps whatever the model produced, mixes concerns, and breaks public interfaces. A stateless reviewer cannot tell them apart. A reviewer with a trust profile can, and it calibrates its scrutiny accordingly.

Seen this way, the trust profile is the layer beneath the automated reviewer: the checks define what “good” looks like, and the profile decides how much to trust that a given author is delivering that standard.

But that framing also points at the profile’s eventual obsolescence, which is the goal.

A bridge, not a destination
#

There is a sense in which the trust profile is a mechanism I want to make obsolete.

The ideal end state is not a finely calibrated reputation system that perfectly sorts developers into tiers. The ideal end state is that there is nothing to sort. Every contributor, senior engineer or new hire, funnels their work through agents that enforce the same standards: atomic PRs, tests that pass, interfaces that do not break, changes that are reversible and tied to a spec. The output converges into something homogeneous, and by the time it reaches the code stage it is, for all practical purposes, perfect. Authorship stops carrying signal, because every author is producing the same uniform quality through the same disciplined pipeline.

In that world the reviewer’s checks still run, but they never flag anything, because the problems were engineered out upstream rather than caught downstream. And the trust profile, with nothing left to differentiate, collapses: everyone sits at trusted, the level never moves, and the file stops being worth reading.

That is the accurate framing for the trust profile. It is not the destination; it is the bridge. Today’s reality is heterogeneous, a mix of careful and careless, hand-written and agent-generated, vetted and rubber-stamped, and the profile does useful work precisely because that variance exists. As the variance shrinks, so does the profile’s job, until the most successful outcome is that no one, me included, needs it anymore.


The Importance of Context When Interacting with LLMs

Most interactions with LLMs fail not because the model lacks capability, but because the user fails to provide enough context for the model to succeed. Context is not a prompt engineering trick. It is the entire mechanism by which a frozen set of weights produces behavior relevant to your specific situation.

What Context Means for an LLM
#

A large language model is a fixed function at inference time. Its weights were determined during training and do not change. Everything you want the model to know about your current task, your constraints, your codebase, your domain, and your preferences must be communicated through the context window.

This window has three conceptual regions.

  1. The system prompt establishes persistent instructions and persona.
  2. The conversation history provides the back-and-forth of the interaction.
  3. The retrieved or injected context supplies external knowledge that the model was not trained on.

When people say “prompt engineering,” they are usually talking about the system prompt and a handful of few-shot examples. When people say “RAG,” they are talking about dynamically injecting retrieved documents into the third region. When people say “context engineering,” they are talking about the deliberate design of all three regions together.

In-Context Learning: Why This Works at All
#

The GPT-3 paper demonstrated something surprising: a sufficiently large language model can learn new tasks from examples provided in the prompt, without any gradient updates. This is in-context learning, and it is the reason context matters so much.

A model that has never seen your internal API conventions can follow them perfectly if you show it three examples. A model that does not know your company’s style guide can adopt it verbatim if you paste it in. The model is not truly “learning” in the statistical sense. The model is recognizing patterns in the provided context and extending them.

This means the quality of the context is the quality of the output. Vague context produces vague output. Contradictory context produces contradictory output. Missing context produces plausible-sounding output that is wrong in ways specific to your situation.

The Spectrum of Context Strategies
#

Context can be provided along a spectrum, from minimal to extensive.

Zero-shot prompting relies entirely on the model’s pre-trained knowledge. Zero-shot prompting works for generic tasks (summarize this text, translate this sentence) because the training data likely contained millions of similar examples. Zero-shot prompting fails for domain-specific tasks (generate a query against our proprietary schema, review code against our internal standards) because the model has never seen your specific conventions.

Few-shot prompting injects examples into the context window. The original GPT-3 evaluation showed that performance scales with the number of examples, but with diminishing returns. Three to five high-quality examples often capture 80% of the benefit. Example quality matters more than example quantity. One example that perfectly demonstrates the desired behavior outperforms ten mediocre ones.

Retrieval-augmented generation (RAG) dynamically fetches relevant documents and injects the documents into the context. The original RAG paper showed that combining a parametric model with non-parametric retrieval produces better grounded answers than either alone. RAG addresses the fundamental limitation that a model’s training data is a snapshot in time and cannot contain proprietary or recent information.

Long-context ingestion bypasses retrieval by stuffing everything into a large context window. Models such as Gemini with million-token context windows make the approach technically feasible. The question is whether dumping everything into the window actually works. Liu et al. demonstrated that models suffer from a “lost in the middle” effect: information at the beginning and end of the context is retrieved more reliably than information in the middle. Simply increasing the window does not linearly increase the model’s ability to use the information.

Context engineering is the emerging discipline of orchestrating all of these strategies deliberately. Context engineering recognizes that context is not just “what you put in the prompt,” but a system that includes retrieval logic, example selection, conversation management, and information ordering.

Where Context Breaks Down
#

Understanding the failure modes of context is as important as understanding how to provide context.

Attention dilution. Every token in the context window competes for the model’s finite attention. Adding irrelevant context does not just waste tokens. Adding irrelevant context actively degrades performance on the relevant portions. Liu et al.’s “Lost in the Middle” showed that even when the relevant information is present in the context, retrieval accuracy drops when the context is cluttered with noise. The lesson: more context is not always better. The right context, curated and ordered, outperforms a dump of everything that might be relevant.

Instruction following degrades at scale. System prompts and instructions are more reliably followed when the instructions are prominent in the context. As the context window fills with retrieved documents, conversation history, and examples, the model’s adherence to its original instructions weakens. Weakening adherence is why many production systems re-inject key instructions at the end of long contexts, not just the beginning.

Stale context. In a long agent session, early conversation turns become increasingly irrelevant. The model continues to weigh the early turns in its attention computation. The result is drift, where the model brings up constraints or preferences from turn 3 that are no longer applicable at turn 30. Effective agent systems summarize or prune earlier context to maintain relevance.

Implicit context gaps. The most dangerous context failures are the ones you do not notice. You assume the model knows that your API returns snake_case JSON, that your timestamps are in UTC, that your user IDs are integers not strings. The model does not know any of these things unless you tell it. Each implicit assumption is a potential bug in generated code or a hallucinated fact in generated text.

Practical Context Design
#

For engineers building with LLMs, context design is the highest-leverage activity. Here are concrete patterns that work.

Layer your context deliberately. Start with a system prompt that defines the task, constraints, and output format. Follow with retrieved documents that are directly relevant to the current query. Then provide conversation history, pruned to the most recent and relevant turns. End by restating the specific question or instruction. This ordering respects the model’s attention patterns, which weight the beginning and end of context most heavily.

Curate your examples. A few-shot example should demonstrate the hardest case, not the easiest. If your task involves edge cases (empty inputs, special characters, ambiguous queries), show examples of those edge cases. Showing five variations of the happy path teaches the model less than one example of each difficult variant.

Use structured context. When injecting retrieved information, format the information consistently. Markdown headers, delimited sections, and numbered lists give the model explicit boundaries between pieces of information. A blob of unstructured text forces the model to spend attention on parsing structure rather than reasoning about content.

Separate context from instruction. Mixing “here is some reference information” with “now do this task” in the same unstructured block reduces reliability. Explicitly mark where context ends and instructions begin. Many production systems use XML tags or special tokens for this purpose.

Test your context independently. Before deploying an LLM pipeline, test whether the model can answer simple factual questions based solely on the provided context. If the model cannot reliably retrieve a fact that is clearly present in the context, the context is too long, too noisy, or poorly structured. The test is a fast diagnostic that catches many context problems before the context problems become production incidents.

Context Engineering as a Discipline
#

The shift from “prompt engineering” to “context engineering” reflects a maturation in how we think about LLM interactions. Prompt engineering suggests that the right magic words unlock better performance. Context engineering recognizes that the entire information environment determines the output.

The distinction matters because the distinction changes where you invest effort. If you believe in magic prompts, you spend your time iterating on wording. If you believe in context engineering, you invest in retrieval systems, example libraries, context ordering, and information architecture.

The results from Anthropic’s work on contextual retrieval illustrate the payoff well. By adding a small amount of context to each retrieved chunk (explaining how the chunk relates to the broader document), Anthropic reduced retrieval failure rates by 67%. Not by changing the model. Not by changing the prompt. By changing how context was prepared and presented.

Chain-of-thought prompting is another example. The model’s reasoning improves not because you asked the model to “think step by step” as a magic incantation, but because you expanded the context window with intermediate reasoning steps. The model uses its own generated context as additional input for subsequent tokens. Context is not just what you provide. Context is also what the model generates and then consumes.

The Uncomfortable Implication
#

If context determines output quality, the ceiling on LLM performance in production is not the model’s capability but the quality of the context pipeline feeding the model.

The conclusion is uncomfortable because the hardest engineering problem in LLM applications turns out not to be model selection or fine-tuning. The hard problem is information retrieval, information architecture, and information presentation. These are old problems from search engineering and information science, now applied to a new interface.

The engineers who build the best LLM products will not necessarily be the ones who understand transformer architectures most deeply. The best LLM products will be built by the engineers who can design systems that surface the right information, at the right time, in the right format, and place the information where the model will actually attend to it.

Context is not a feature you add to an LLM application; it is the application.

References
#


The Merge Gate: Do You Need a Human to Approve Your Pull Requests?

My previous piece was about code review: a human reading code before it ships. This one is narrower, and blunter. It is about the merge gate, the specific act of requiring a human to click “approve” before code can land in a codebase.

People treat review and approval as the same thing. They are not. You can review code without being able to block it. You can approve code without reading it. The valuable act (reading) and the gating act (merging) have been fused by our tooling, and that fusion is worth pulling apart.

Most arguments for keeping a human in the merge loop collapse once you ask a single question. What does the approval click certify that the automated gates did not?

What the Approval Click Actually Certifies
#

Watch what happens when a pull request gets approved.

The approver sees a green checkmark from CI. They see a passing test suite. They scan the diff for a few seconds. They click approve.

Ask them, afterward, what they certified. They will struggle to answer.

They did not re-run the tests; CI already did that. They did not verify the implementation against the spec; they assumed someone else did. They did not reason about every edge case; the diff was too long to reason about in three minutes. They did not assess whether the change could be undone; that is not what diffs show.

The approval click, in most teams, certifies exactly one thing: that a specific human was awake and present at the moment the PR was merged. That is a low-information event, and it is doing almost no safety work.

The actual safety is being produced by the systems around the gate. The test suite. The linter. The static analyzer. The deployment pipeline. The rollback path. The human approval is layered on top, taking credit for safety it did not generate.

The Cost of the Gate
#

A gate that does little safety work still extracts a cost. Several costs, in fact, and they compound.

Latency. Every pull request waits for a human. That human is in a meeting, asleep, on another team, or simply not looking at their inbox. Cycle time stretches from minutes to hours to days. Work that could ship this morning ships next Tuesday.

Batch amplification. This is the subtlest and most damaging cost. When approval is expensive to obtain, people batch. They hold three small changes until they have a fourth, because each approval is a fixed-cost interruption. The gate incentivizes the exact thing it should discourage: larger, riskier changes. A mechanism designed to keep changes safe ends up making them less safe, because the unit of review grows to fill the cost of getting reviewed.

Context switching. Every approval is an interruption for the approver. Their work is paused, their context is swapped, their focus is fractured. You are taxing your most experienced engineers to perform a low-information ritual.

Single point of failure. If only two people can approve a given area and both are out, the pull request stalls. Approval authority concentrates, and concentration creates bottlenecks and bus factors.

Soft target. The approver is the cheapest attack surface in the entire pipeline. You can harden CI, pin dependencies, and scan for secrets, but a tired approver who clicks approve on a social-engineered change defeats all of it. A human gate is a human vulnerability.

What Actually Makes a Merge Safe
#

Strip the ritual away and ask what keeps a merge safe. It is a short list, and the human approver is not on it.

A comprehensive test suite that runs on every change. Static analysis that catches the classes of bugs tests miss. A deployment pipeline that canaries before it fully rolls out. Feature flags so new behavior can be disabled without a redeploy. A rollback path that has actually been tested, not assumed. Blast-radius limits that cap how much any single change can touch.

Each of these operates on reality, not on a human’s prediction of reality. A canary either surfaces the problem or it does not. An approver might notice the problem, or might be thinking about lunch.

The gate is taking credit for safety the system produces. Once you see this, you cannot unsee it.

The Accountability Objection
#

The first objection is accountability. Without a human approval, who is responsible for the code that ships?

I made this argument in the previous piece, and I will not repeat all of it here. The short version is that a human glancing at a diff was never truly responsible for what shipped. Responsibility lives upstream, in who decided the problem was worth solving and who wrote the specification.

The approval signature adds nothing to that picture. It is a name on a line, demanded because it feels like accountability. When a change breaks production, the approver is not the one who gets blamed, disciplined, or even consulted. The signature exists to allocate blame after the fact, not to prevent harm before it.

What remains, once you accept this, is the audit and compliance framing. That is the stronger case for keeping a human signature, and it deserves its own answer.

The Compliance Objection
#

“But regulation requires human approval.”

Sometimes it does. More often, regulation requires traceability, a named owner, a documented decision, an auditable path. Those are not the same thing as a tired human clicking approve at four in the afternoon on a Friday.

When a rule says a change must be “reviewed and approved”, it is trying to ensure that someone with authority consciously decided the change was acceptable. That intent can be satisfied several ways. A named owner who signed off on a specification. An automated gate whose rules were themselves approved by a human. A risk classification that a human defined and a machine enforces consistently.

The function is accountability and traceability. The form is “a human clicks a button”. Teams satisfy the form and skip the function all the time. The sound path is the reverse: satisfy the function rigorously, and let the form follow.

If your auditor insists that safety lives in a specific human keystroke, you have an education problem, not an engineering one.

The Binary Mistake
#

The deeper error is treating approval as binary. Either every pull request needs a human, or no pull request does.

This is wrong, and it is not how anyone actually behaves. A README typo and a production schema migration are both pull requests. They do not need the same gate. Treating them the same is not caution; it is a failure to think about risk.

The right unit of gating is not “is this a pull request”. It is “what is the blast radius of this change, and is it reversible”.

A one-line documentation fix is low blast radius and trivially reversible. Let it merge on green. No human needs to see it.

A change that drops a database column is high blast radius and may be irreversible. That deserves a human looking at it, carefully, with time.

A change that alters an authentication boundary is medium blast radius but high trust impact. That deserves a human, and probably more than one.

The properties that should trigger a human gate are properties of the change: irreversibility, blast radius, trust-boundary crossing, external commitment. They are not properties of the artifact. When you gate on the change instead of the artifact, the fraction of changes that need a human collapses to a small minority.

The Small Set Where a Human Gate Genuinely Adds Value
#

Frankly, there is a set of changes where a human in the merge loop is not theater. It is small, but it is real.

Irreversible changes. Data destruction, schema drops, deletions of public content, sending real money, publishing to external systems. Once these execute, you cannot call them back. A human who understands the irreversibility should look at them, because the automated gates can only verify forward correctness, not undo impossibility.

Trust-boundary changes. Authentication, authorization, permission models, security-sensitive code paths. These are exactly where a subtle mistake is both likely and catastrophic. A human reviewer adds value here, not because they will catch every bug, but because the cost of a miss is high enough to justify the latency.

Changes to the gating system itself. You do not want the merge gate to auto-approve changes to the merge gate. That is the one place circularity will bite you. A human reviews the rules that the machine enforces.

External commitments. Public API changes, contractually obligated behaviors, compliance-relevant logs. These have consequences outside the codebase, and a human should confirm the external surface is intentional.

Note what is not on this list. Styling. Refactors within a single module. New tests. Documentation. Internal-only features. Dependency bumps that pass audit. These are the overwhelming majority of pull requests. They do not need a human gate. They need the automated gates, and then they need to merge.

The Transition
#

You do not get to “no human gate for most changes” by decree. You get there by making the default path safe.

Start by making the low-blast-radius path auto-merge on green. Documentation, tests, internal-only changes within a single module. CI passes, the merge happens, nobody clicks anything.

Then compute blast radius automatically. Which files changed. Did the change touch the public API. Did it touch the database schema. Did it add a dependency. Did it change infrastructure or deployment configuration. Did it cross a security boundary.

Each of these is a machine-checkable property. Route the change to the human gate only when it crosses a threshold.

When a change does reach the human gate, change what the human is actually doing. They are no longer reviewing code line by line. They are reviewing risk. Is this change as irreversible as the system thinks it is? Is the rollback plan real? Is the blast radius acceptable? Does the external commitment match what was approved upstream?

Reviewing risk is high-leverage work. Reviewing diffs for style is not. The transition moves the human from the low-leverage activity to the high-leverage one, and it frees the rest of the pipeline to move at the speed the machines can sustain.

This is not a proposal. It is a description of the workflow from the previous piece, where a pull request is checked against concrete gates: does it introduce security-sensitive changes, add dependencies, change public interfaces, is it reversible, do the tests pass, does it satisfy the acceptance criteria. What this article adds is the principle behind that workflow. The human gate is the exception, selected by the properties of the change, not the default triggered by the existence of a pull request. The skills that implement it are publicly available.

The Merge Button Is a Ritual
#

The merge button exists because our tools gave us a button. We built a workflow around it, assigned it meaning, and then treated the meaning as structural.

It is not. The safety of a codebase is produced by the systems around the merge, not by the merge approval itself. The click certifies almost nothing those systems did not already certify. And it extracts a real cost: latency, batched risk, fractured focus, concentrated bus factors, and a soft target for anyone who wants to slip something through.

The defensible position is not “no humans in the loop”. It is “humans in the loop where they add value, out of the loop where they do not”.

Most pull requests do not need a human to approve them. A small, identifiable minority do. The mistake of the current default is that it treats every change as if it belonged to that minority.

Free the majority to merge on green. Reserve the human gate for the changes where it actually certifies something the machine cannot. And stop pretending the button is what keeps you safe.

The gate was never the safety. The system was.


Rethinking Code Review in the Age of LLMs

Code review is a bottleneck. I am no longer convinced it is a useful one.

This is not a conclusion I arrived at lightly. Code review has been one of the most reliable quality gates in software engineering for decades. But the assumption underlying code review, that a human reading code before it ships catches meaningful problems, is worth re-examining when most of that code was written by an LLM.

What Code Review Was Supposed to Do
#

Code review served several purposes simultaneously.

It caught bugs before they reached production. It enforced consistency across a codebase. It spread knowledge between team members. It forced the author to organize their thoughts before presenting them to a peer.

Each of these purposes assumed something important: that the person who wrote the code and the person reviewing it were both human, that the author had thought carefully about each line, and that the reviewer could rely on the author’s intent as context.

When an LLM writes the code, these assumptions break.

The LLM did not think carefully about each line. The LLM does not have intent in the way a human does. The reviewer cannot ask the author “what were you trying to do here?” and get a meaningful answer, because the author is a statistical model that generated the most probable next token.

The loss of a human author changes the nature of the review fundamentally, and not in the direction most people assume.

Why Reviewing LLM Code Is Different
#

When a human writes code, code review is a conversation between two people who share a mental model. The reviewer can trust that the author made deliberate choices, even imperfect ones. Differences between what the reviewer expects and what the code does are interesting signals, because they represent a gap between two human understandings of the same problem.

When an LLM writes code, there is no shared mental model. The code is the output of a pattern-matching process. Sometimes it is correct. Sometimes it is subtly wrong in ways that look correct. Sometimes it is obviously wrong.

The reviewer’s job shifts from “does this match the author’s intent?” to “does this do what I want?” This sounds like the same question, but it is not. The first question allows the reviewer to leverage the author’s reasoning. The second question requires the reviewer to independently verify every assumption the code makes.

This is harder, more tedious, and less effective than reviewing human-written code. The reviewer is not building on the author’s thinking. They are reconstructing it from scratch, line by line.

The Bottleneck Argument
#

Here is the practical problem.

LLMs can generate code orders of magnitude faster than humans can review it. A developer who used to spend six hours implementing a feature might now spend thirty minutes prompting an LLM and one hour reviewing the output.

The ratio of review time to implementation time has inverted. Where review used to be a small fraction of the development cycle, it is now the dominant fraction.

And the quality of that review is worse, not better, because reviewing code you did not write is cognitively different from reviewing code you understand deeply.

This creates a specific kind of bottleneck: one where the throughput-limiting step is also the lowest-quality step. You are spending most of your time on the part of the process where you add the least value.

What I Would Rather Be Doing
#

If I am going to spend my limited cognitive budget, I want to spend it on the decisions that matter most.

Deciding which problems to solve. Understanding whether a feature should exist at all. Designing the boundary between components. Choosing the right abstraction for the domain. Thinking about how users will actually interact with what we build.

These are high-leverage activities. They determine whether the code that gets written is useful, not just correct.

Reviewing LLM-generated code for style, naming conventions, and obvious bugs is low-leverage work. An automated tool can do it faster and more consistently than I can. A linter does not get tired. An automated test suite does not lose focus after the third function.

The hours I spend reviewing code that an LLM wrote are hours I am not spending on the problems that only a human can solve. That trade-off did not used to exist, because writing code and reviewing code were both human activities and the time allocation was roughly balanced. Now the balance is broken, and the opportunity cost of review is much higher.

Why Automatic Review and Approval Make Sense
#

I have no objection to automated code review. I have no objection to automated PR approval.

The instinct to keep a human in the loop for every change is driven by fear, not by a rational assessment of what the human actually contributes at that point in the process.

Consider what a good code reviewer does today. They check that tests pass. They look for obvious bugs. They verify naming conventions. They ensure the change aligns with the stated goal.

Every one of these checks can be automated. Tests pass or they do not. Static analysis tools catch bugs more reliably than tired humans scanning diffs. Linters enforce style more consistently than any reviewer. Alignment with the stated goal can be checked by having an LLM compare the PR description with the actual changes.

The Wrong Stage to Catch Subtle Issues
#

The remaining argument for human review is that humans catch subtle issues that automated tools miss: architectural problems, subtle security vulnerabilities, misunderstandings of the domain.

These are real concerns, but they are not best addressed at the PR level. They are best addressed at the specification level. If you write a precise specification, automated verification can confirm the implementation matches it. If the specification is vague, no amount of human review will save you from building the wrong thing.

Some would argue that catching these issues during review is better than not catching them at all. This is hard to disagree with in isolation. Of course a subtle bug caught at review is better than the same bug reaching production.

But the better-late-than-never framing hides the real trade-off. The question is not whether review catches some problems. It is whether review is the best place to catch them, and whether the time spent reviewing could catch more problems if spent elsewhere.

When you catch an architectural flaw at review time, the code is already written. Fixing it means rework, rebase, re-test, re-review. Catching the same flaw during specification costs a conversation. The later you catch it, the more expensive it is, and code review is one of the latest stages in the pipeline.

More importantly, relying on review to catch subtle issues is unreliable by design. A human reviewer catches what they happen to notice, when they happen to be alert, on the changes they happen to read carefully. Some issues get caught. Many do not. You are depending on luck and attention, not on a system.

The alternative is to build the catching into the system itself. A precise specification catches architectural misunderstandings before code exists. A comprehensive test suite catches behavioral bugs on every run, not just when a reviewer is paying attention. Static analysis catches security patterns deterministically. Each of these catches issues systematically, on every change, forever.

Review catches issues once, for the reviewer who happens to be in front of the diff. A good test catches the same class of issue every time it runs, for as long as the codebase exists. If you find the same kind of subtle issue during review more than once, the answer is not to keep reviewing harder. The answer is to encode that check into your automated gates so it never depends on a human noticing again.

“Catching it late is better than not catching it” is true. It is also an argument for accepting a process that catches too little, too late, at the highest possible cost.

The review should happen before the code is written, not after. Spend the human effort on the spec. Let the machines verify compliance.

The Quality Maximization Myth
#

Some would argue that this misses the point. The goal of code review is not just to check gates, it is to make the code the best it can be. A good reviewer suggests a cleaner abstraction, spots a performance issue the author missed, proposes a name that communicates intent better. Review is quality maximization, not just verification.

This sounds right until you press on what “the best it can be” actually means.

It is subjective. It is unbounded. There is always a cleaner abstraction, a faster algorithm, a better name. The pursuit has no natural stopping point, which is why code reviews so often devolve into bikeshedding over style preferences that do not measurably improve the outcome.

When the code was written by a human, a second perspective genuinely improved the implementation. Two brains could find a better approach than one. But when the code is generated by an LLM, the reviewer is the only brain in the loop, and their suggestions compete with the option of simply regenerating the code against a better specification.

If the code is not good enough, the answer is not to have a human improve it line by line during review. The answer is to improve the specification, the test suite, or the generation prompt, and let the machine produce a better version. That scales. Human suggestions on a diff do not.

The quality of the code is bounded by the quality of the specification that produced it. If you want better code, write a better spec. Reviewing the output is the most expensive, least scalable way to improve it.

The Reallocation
#

Here is what I am proposing.

Instead of spending 30% of development time on code review, spend 5% on automated verification and reallocate the saved hours to specification, problem selection, and domain understanding.

The quality of the software will not decrease. The tests still run. The linters still lint. The static analysis still analyzes. What changes is where the human attention goes.

Right now, human attention is concentrated at the end of the pipeline, reviewing output. It should be concentrated at the beginning of the pipeline, defining what the output should be.

This is not a radical idea. It is the same principle behind test-driven development: define what you want first, then build it. The difference is that now the builder is a machine, and the definition is the only place where human judgment is irreplaceable.

This Is Not Hypothetical
#

None of this is a prediction about the future. I run this approach today.

My development workflow front-loads human effort into specification, requirements, and problem selection. Before any code is written, there is an issue with acceptance criteria, a requirements document, and a technical specification, each reviewed and approved. That is where my judgment gets spent.

Code generation happens against that specification. When a pull request is opened, an automated review pipeline checks it against concrete gates. Does it introduce security-sensitive changes? Does it add new dependencies? Does it change public interfaces? Is the change reversible? Do the tests pass? Does the diff actually satisfy the acceptance criteria from the linked issue?

If all gates pass, the PR is approved automatically. No human reads the diff. No human clicks approve.

When the gates flag something, the PR is held for manual review. This is not the same as reviewing every change. It is reviewing the changes that carry real risk, which is a much smaller set.

The distinction matters. I am not advocating for shipping unreviewed code. I am advocating for shipping code that has been verified by systems more reliable than a tired human scanning a diff, and reserving human attention for the small fraction of changes where it actually adds value.

The infrastructure for this exists. The gates are concrete. The approach works because the specification does the work that code review used to do poorly. The skills that implement this workflow are publicly available.

Who Is Responsible
#

The first objection is accountability. If no human reads the code before it ships, who owns the problems it creates?

The real answer is that a human reviewing a pull request for ten minutes was never truly responsible for that code. They provided a rubber stamp. They glanced at the diff, checked that the tests passed, and clicked approve. When that code caused an outage six months later, nobody blamed the reviewer. They blamed the author, the test suite, the deployment process, or the requirements.

What about the reviewer who spends thirty minutes, or an hour? They are doing substantive work, not rubber-stamping. The characterization above does not apply to them. They genuinely understand the change, question the design, and catch real issues.

But the argument against mandatory review does not depend on reviews being shallow. It depends on where that hour of expert attention is best spent.

An hour of review catches issues once, for one change, depending on that reviewer being sharp that day. An hour spent improving the specification prevents the entire class of issue from reaching implementation. An hour spent writing a regression test catches the bug on every future run, not just the one time a human happened to read the code.

The thorough review is real work. It is just not the highest-leverage work that person could be doing with that hour. And at scale, it is unsustainable: if every PR requires an hour of human review and the LLM produces ten PRs a day, you need ten hours of review to keep up. That is not a process that scales.

Code review creates an illusion of accountability without delivering it. The signature on the PR is accountability theater.

Real responsibility lives upstream. The person who decided this problem was worth solving owns the outcome. The person who wrote the specification owns whether the implementation matches intent. The person who designed the deployment pipeline owns how quickly a bad change can be contained.

Removing human review does not remove responsibility. It forces you to locate responsibility where it actually belongs: in the decisions that guided the work, not in the person who scanned it at the end.

What About Outages
#

The second objection is safety. What if the LLM makes a decision that takes down production?

This is a real risk, but code review is the wrong tool to mitigate it.

Most production outages are not caused by bugs that a reviewer would catch. They are caused by configuration changes, unexpected data formats, load patterns, dependency failures, and integration issues that only surface under real traffic. A human reading a diff is making a guess about what might happen. Production behavior is the ground truth.

If you want to prevent outages, invest in the systems that observe and contain actual behavior.

Feature flags so a change can be turned off without a redeploy. Canary deployments so a bad change reaches 1% of traffic before it reaches 100%. Monitoring and alerting so a regression is detected in minutes, not hours. Fast, tested rollback paths so recovery does not depend on someone remembering how the old version worked.

These tools are more reliable than code review because they operate on reality rather than prediction. A reviewer might miss a subtle interaction. A canary deployment will surface it.

What About Reversibility
#

A related concern is that an LLM might produce changes that are hard to undo. A sprawling refactor, a database migration that is not backward compatible, a change that entangles two previously independent systems.

This is a serious problem, but again, human review is not the safeguard people think it is.

Reviewers focus on forward correctness. They ask “does this do what it should?” They rarely ask “can we undo this cleanly?” Even when they do, reversibility is hard to assess by reading a diff. It depends on what happens in production after the change lands, on data migrations that have already run, on other changes that build on top of it.

The solution is to make reversibility a structural property of how changes are made, not a property enforced by review.

Keep changes small and independent. Require database migrations to be backward compatible, deployed before the code that depends on them. Use feature flags so new behavior can be disabled without reverting code. Treat large, entangling refactors as high-risk changes that warrant extra process, not as the default mode of operation.

These are constraints you encode in your pipeline and your specification process. They do not require a human to read every line of every PR. They require discipline at the level where decisions are made.

What About Code Quality and Maintainability
#

A third concern is that without human review, LLM-generated code will degrade into slop. Inconsistent naming, duplicated logic, unnecessary abstractions, dead code, patterns that do not match the rest of the codebase. Each individual change passes its tests, but the codebase slowly rots.

This is a real risk, and it deserves a real answer. The answer is not that review prevents it. The answer is that review catches it inconsistently, after the fact, one PR at a time.

Most of what we call code quality is measurable. Cyclomatic complexity is a number. Duplication is detectable. Dead code is identifiable by static analysis. Naming conventions are enforceable by linters. File length, function length, import depth, test coverage gaps, all of these are machine-checkable properties.

If you care about maintainability, encode the constraints that produce it. Set complexity limits that fail the build. Run duplication detectors on every PR. Require test coverage above a threshold. Lint aggressively. Block PRs that introduce unused exports or dead code.

These checks run on every change, consistently, without getting tired or distracted. A human reviewer might flag a function that is too complex. A complexity gate will flag every function that exceeds the threshold, every time, for as long as the rule exists.

Slop also has an upstream cause. When the specification is vague about architecture, naming, and patterns, the LLM fills the gap with whatever it has seen most often in its training data. That output is generic by default. It will not match your codebase’s conventions unless the specification tells it what those conventions are.

This means the fight against slop is won at the specification level, not at the review level. A specification that includes the patterns to follow, the existing abstractions to reuse, and the naming conventions to respect produces cleaner code than a vague spec plus a human reviewer cleaning up the output.

For what slips through the automated gates, frequent maintenance sweeps catch accumulated decay. Run a dead code analysis weekly. Run a duplication detector weekly. Review complexity trends after every merge. This is a more systematic approach than hoping each PR’s reviewer notices the slow accumulation of mess.

Code quality is a property of your constraints, not of your reviewers.

What I Am Not Saying
#

I am not saying all code review should be eliminated tomorrow. Legacy codebases, critical security infrastructure, and domains where correctness is life-or-death may still benefit from human review.

I am not saying code review was never useful. It was, for decades, one of the best tools we had. I am saying the tool’s value has changed because the context has changed.

I am not saying I trust LLMs to always produce correct code. I am saying that human code review is not the best way to ensure correctness when the code was machine-generated. Automated verification, comprehensive test suites, and precise specifications are better tools for that job.

The Question Worth Asking
#

The next time you open a pull request full of LLM-generated code and start reviewing it line by line, ask yourself: what am I actually checking?

If you are checking style, a linter does it better. If you are checking correctness, tests do it better. If you are checking whether the code solves the right problem, you should have answered that question before the code was written.

Code review made sense when humans wrote all the code. It makes less sense when humans define the problem and machines implement the solution. The bottleneck has moved. Our processes should move with it.


Scaling the LLM Agent Company

In Exponential growth software company I explored the constraints that make scaling a human company hard: onboarding bottlenecks, culture dilution, coordination overhead, institutional knowledge decay. Each of these constraints grows with the number of people you employ. A company where the workforce is entirely composed of LLM agents inverts most of these problems. The old constraints evaporate, but new ones emerge from a different direction.

What Disappears
#

Onboarding becomes instantiation. An LLM agent starts productive the moment it is created. There is no training period, no ramp-up, no senior employee pausing their work to bring a new hire up to speed. Spawning 100 agents costs roughly the same operational effort as spawning 1. The ratio of onboarded to onboarders that mattered for human companies becomes irrelevant.

Culture is exactly what you specify. Agents follow their instructions. If you want 1,000 agents to behave identically, you give them the same system prompt. There is no culture clash, no informal norms developing in opposition to the official ones, no gradual drift as new hires bring outside habits. The culture is the prompt.

Coordination scales differently. Agents do not have cognitive limits on the number of peers they interact with. A single orchestration agent can coordinate with hundreds of worker agents without getting overwhelmed. Communication between agents is structured, lossless, and near-instantaneous. The team structures, single points of contact, and redundancy tradeoffs that human organizations require are replaced by protocol design.

Institutional knowledge does not decay. Everything an agent knows is explicit in its instructions, tools, and retrieved context. There is no tacit knowledge locked in someone’s head, no risk of losing the person who understands the legacy system. When you replace an agent, the replacement has access to exactly the same information.

What Replaces It
#

The fundamental principle from the original article still holds: anything that scales linearly or superlinearly with itself needs to be optimized to grow sublinearly. The bottleneck has moved, but it has not disappeared.

Unit economics
#

Each agent invocation costs compute and API calls. At scale, the marginal cost of an additional agent is small but not zero. If your revenue per task is lower than the cost of the compute required to complete it, you have the same fundamental problem as an unprofitable human company. A human company that loses money on every employee-hour goes bankrupt. An agent company that loses money on every inference call goes bankrupt just as fast, it just happens in milliseconds instead of months.

Correlated failures
#

With humans, errors are diverse and partially self-correcting through independent judgment. Two engineers given the same task will produce different bugs. With agents sharing the same prompt, errors are correlated and systemic. A single flawed instruction propagated across 1,000 agents produces 1,000 instances of the same mistake at scale. Consistently wrong is worse than inconsistently right. Correlated errors make quality control the central bottleneck. You need evaluation pipelines, guardrails, and feedback loops that themselves must scale. The testing infrastructure becomes the company’s most critical asset, more important than the agents it tests.

Orchestration complexity
#

While individual agents do not get overwhelmed, the system as a whole can still produce emergent failures. Agents acting on stale information, conflicting instructions, or subtle misinterpretations of their goals can cascade into problems that are hard to diagnose because the system operates at a speed and scale humans cannot directly oversee. The orchestration layer is the new management layer. Its complexity grows with the number of agents and the richness of their interactions. Unlike human management, where adding managers adds judgment and adaptability, adding orchestration logic adds rigidity. Every new rule is a new potential point of failure.

Infrastructure brittleness
#

A human can work around a broken tool with creativity. An agent generally cannot. If an API goes down, every agent depending on it stalls. If a database schema changes unexpectedly, every agent writing to it produces corrupted data. The brittleness of automated systems means that reliability engineering and observability become more important, not less, as you scale. The company that cannot detect a degrading agent within seconds will compound the damage across its entire workforce simultaneously.

Model dependency
#

Your company’s capacity is bounded by what the underlying models can do. If the model provider changes behavior, degrades performance, or raises prices, your entire workforce is affected at the same time. This is a vendor dependency unlike any single human employee leaving. It is more like all your employees sharing the same brain, and that brain being operated by a third party. Diversifying across models is a partial hedge, but it introduces the same coordination complexity as a multilingual workforce.

Where the Moat Lives
#

When anyone can spawn an equally capable agent workforce, the advantage is no longer in having employees. The moat shifts to the quality of your instructions (prompts as institutional knowledge), the design of your orchestration (workflows as management), your data flywheels (evaluation data as competitive advantage), and domain-specific tools and integrations (proprietary capabilities the agents use). The company is no longer its people. It is its prompts, its pipelines, and its proprietary context.

The Pattern
#

The original article concluded that human scaling requires optimizing every linear cost down to sublinear. The same conclusion applies here, but the costs are different. Human companies optimize hiring, onboarding, and culture. Agent companies optimize inference cost, error correlation, orchestration complexity, and infrastructure reliability. The companies that scale exponentially in this era will be the ones that treat their agent workforce as a system to be engineered, not a team to be managed.

References
#


ghx - A CLI for agentic code reviews on GitHub

I’ve been building AI agents that review pull requests, and the official gh CLI doesn’t have what they need. Agents can’t leave inline comments on specific lines, can’t manage pending reviews, and can’t edit or delete comments. These are basic operations for any code review workflow, and they’re only accessible through the GitHub web UI or the raw GraphQL API.

So I built ghx, a CLI designed to make agentic code reviews practical.

What ghx does that gh doesn’t
#

Inline comments on files and lines. The most fundamental operation for a code review agent: comment on a specific line of a diff.

$ ghx pr comment 42 --file src/main.go --line 10 --body "Nit: use fmt.Errorf"
Created inline comment on src/main.go:10 (thread PRRT_kwDOC0I7As5vKgVn)

$ ghx pr comment 42 --file src/main.go --line 10-15 --body "Consider extracting this"
Created inline comment on src/main.go:10-15 (thread PRRT_kwDOC0I7As5vKgVq)

File-level comments (without --line), top-level PR comments, and replies to existing threads are all supported.

Pending reviews. Accumulate review comments without submitting them immediately, then approve or comment when ready:

$ ghx pr comment 42 --file src/main.go --line 10 --body "Nit" --pending
Added pending inline comment on src/main.go:10 (thread PRRT_kwDOC0I7As5vKgVz, review PRR_kwDOC0I7As4B9Y2z)

$ ghx pr review submit 42 --event APPROVE --body "LGTM"
Submitted review PRR_kwDOC0I7As4B9Y2z as APPROVE

Edit and delete comments. Fix a typo or remove a comment. Use ghx pr threads 42 --ids to list IDs:

$ ghx pr threads 42 --ids
PRRT_kwDOC0I7As5vKgVn  src/main.go:10  [open]
  PRC_kwDOC0I7As5TKxYc  reviewer  Nit: use fmt.Errorf

  PRC_kwDOC0I7As5TKxYd  author  Good catch, will fix.

Then edit or delete the comment:

$ ghx pr comment edit PRC_kwDOC0I7As5TKxYc --body "Use fmt.Errorf instead of errors.New"
Updated comment PRC_kwDOC0I7As5TKxYc

$ ghx pr comment delete PRC_kwDOC0I7As5TKxYc
Deleted comment PRC_kwDOC0I7As5TKxYc

Review thread management. List, filter, and inspect review threads:

$ ghx pr threads 42
src/main.go:10  [open]
  reviewer  Nit: use fmt.Errorf
  author    Good catch, will fix.

src/main.go:45-52  [resolved]
  reviewer  Consider extracting this into a helper
  author    Done in 3a1b2c4

Filter by state with --state open, --state resolved, or --state all.

Issue comments and viewing. Add, edit, and delete issue comments, and view issues with their full comment history:

$ ghx issue view 42
Fix race condition in worker pool  [open]  author

The worker pool has a race condition when multiple goroutines access
the shared counter without proper synchronization.

2 comment(s):

  contributor  I can reproduce this with `go test -race ./...`
  author       Fixed in #50

$ ghx issue comment 42 --body "This is fixed in #50"
Created comment IC_kwDOC0I7As5TKxZa on issue #42

The stash system
#

ghx has a local stash system for review comments, modeled after git stash.

The main use case is enabling agents to batch many comments at once before submitting them all as a single review. Instead of making individual API calls per comment, an agent can stash comments locally and pop them into a pending review in one operation:

$ ghx pr comment 42 --file src/main.go --line 10 --body "Nit" --stash
Stashed comment on src/main.go:10 (stash@{0} now has 1 threads)

$ ghx pr comment 42 --file src/main.go --line 20-25 --body "Extract this" --stash
Stashed comment on src/main.go:20-25 (stash@{0} now has 2 threads)

$ ghx pr review stash list 42
stash@{0}:  2 threads, 2 comments
  src/main.go   10      1 comment(s)
  src/main.go   20-25   1 comment(s)

$ ghx pr review stash pop 42
Popped stash@{0} (2 threads, 2 comments) into review PRR_kwDOC0I7As4B9Y2z

The stash also solves a GitHub API constraint: you can’t mix immediate comments with pending review comments on the same PR. When you submit an immediate comment on a PR that has a pending review, ghx automatically stashes the pending review, submits the comment, and restores the pending review. The stash lets agents use a push-pop workflow (stash, comment, comment, pop) instead of repeating push-comment-pop for every immediate comment.

The stash supports multiple entries, just like git stash: push, pop, drop, and list.

Getting started
#

ghx is a single Go binary. Download the latest release from GitHub, or install with Go:

go install github.com/tomzxcode/ghx@main

ghx picks up your existing GH_TOKEN, GITHUB_TOKEN, or gh auth login credentials. No extra configuration needed. All commands accept --repo OWNER/REPO or auto-detect from the current git remote.

ghx is MIT-licensed and written in Go with no runtime dependencies beyond the GitHub API.


The Backlog Is Not a Dumping Ground: Managing the Backlog of a Large Software Product

A well-managed backlog is the single most leveraged artifact in product development. When it works, teams ship the right things at a sustainable pace. When it doesn’t, the organization accumulates stale tickets, conflicting priorities, and endless planning meetings. For large software products, where dozens of teams feed from the same corpus of work, the cost of poor backlog hygiene is enormous and mostly invisible.

The Backlog at Scale Behaves Differently
#

On a small team, the backlog is a conversation. The product owner and a handful of engineers look at a list, discuss what matters most, and pick up work. At scale, the backlog becomes an information system. It mediates between strategy and execution, between stakeholders who want things and teams who build things, and between the present state of the product and its future state.

The Scrum Guide defines the product backlog as “an emergent, ordered list of what is needed to improve the product” and notes that it is the single source of work for the team. That definition is clean and useful, but in practice, large organizations layer in program backlogs, initiative backlogs, discovery backlogs, and technical debt backlogs. The first challenge is deciding what structure you need and resisting the urge to create a new list every time someone feels their priorities aren’t getting enough visibility.

Marty Cagan, “Inspired” argues that the strongest product teams maintain a clear distinction between product discovery (figuring out what to build) and product delivery (building it), and that conflating the two is a root cause of bloated backlogs. Items land in the backlog before anyone has validated that they are worth building. The backlog becomes a wish list rather than a commitment queue.

Four Principles That Change Everything
#

Most backlog dysfunction traces back to violations of one of four principles.

1. The Backlog Is Ordered, Not Merely Prioritized
#

“Prioritized” suggests labels like “high,” “medium,” and “low.” Those labels are nearly useless because everything ends up “high.” Ordered means the backlog is a ranked list: item 1 is more important than item 2, which is more important than item 3. A strict ordering is harder to produce, and that difficulty is the point. Forcing a strict ordering surfaces the trade-offs that stakeholders would otherwise avoid.

Woody Zuill’s approach of “pulling” work rather than “pushing” it is relevant here. When the backlog is strictly ordered, teams pull from the top. No cherry-picking, no lobbying for the pet feature. The rank order is the contract.

At scale, this principle applies at every level. Initiatives within a program are ordered. Epics within an initiative are ordered. Stories within a sprint are ordered. If you cannot say which of two items matters more, you are not ready to put both in the backlog.

2. Only Refined Items Belong in the Delivery Backlog
#

Jeff Patton’s user story mapping technique provides a framework for separating discovery from delivery. The “backbone” of user activities and the stories beneath them form a map of the product’s possible future. Only stories that have been discussed, estimated, and accepted by the team belong in the sprint-level delivery backlog. Everything else lives in a discovery or discovery-adjacent artifact.

Separating discovery from delivery is a critical structural choice. Many organizations dump every idea, bug report, customer request, and technical improvement into the same GitHub project. Within months, that project contains thousands of items in no particular order. The team treats it as a write-only data structure.

A practical pattern: maintain two lists. The product backlog contains everything the team might ever work on, loosely grouped by theme. The sprint backlog (or “ready” queue) contains only items that meet a definition of ready: they have acceptance criteria, they are small enough to complete in a single sprint, and the team has discussed them. Nothing moves from the product backlog to the sprint backlog without passing through a refinement session.

The Scrum Guide calls this ongoing activity “backlog refinement,” and regular sessions where the team reviews the top portion of the backlog, splits large items, and ensures alignment with the product goal are essential. The key discipline is not the ceremony itself but the agreement that unrefined items will not be scheduled.

3. Prioritization Frameworks Are a Means, Not an End
#

Several frameworks exist to help teams rank backlog items:

Framework Mechanism Best suited for
RICE (Reach, Impact, Confidence, Effort) Numerical score combining reach, impact, confidence, and effort Product teams comparing features across different domains
WSJF (Weighted Shortest Job First) Cost of delay divided by job size Organizations using SAFe or managing program-level backlogs
Opportunity Scoring Measures importance and satisfaction for each outcome Discovery-phase prioritization when outcomes are unclear
ICE (Impact, Confidence, Ease) Lightweight scoring for rapid triage Fast-moving teams that need quick decisions

These frameworks are useful because they make the prioritization criteria explicit. They are dangerous when teams treat the resulting score as the final answer. A RICE score is only as good as the estimates that feed it, and those estimates are often wrong.

The right approach is to pick one framework that matches your organization’s needs, use it consistently, and revisit the scoring regularly. The conversation that happens when two stakeholders disagree about a score is where the real value lives. The number itself is a forcing function, not a decision.

4. Say No Early and Often
#

The most important word in backlog management is “no.” Every item in the backlog carries a cognitive tax. Engineers scroll past it in planning. Product managers feel obligated to explain why it hasn’t been done. Stakeholders check on it periodically. The larger the backlog, the more time the team spends managing the backlog instead of working from it.

Derek Sivers’ “hell yeah or no” heuristic applies here. If an item does not clearly advance the product’s current goals, it should be declined, archived, or moved to a separate “someday” list that nobody is expected to maintain. A product backlog with 30 well-ordered items is more useful than one with 300 loosely grouped items.

At scale, this principle requires organizational courage. Every stakeholder believes their request is important. The product manager’s job is to say no to the things that are less important than the current top priority, even when that is uncomfortable. Cagan frames this as the difference between product teams (who are empowered to solve problems) and feature teams (who are handed a list to implement). Empowered teams can say no because they understand the problem they are solving and can judge whether a given request advances the product’s goals.

Structural Patterns for Large Products
#

When a product involves multiple teams, shared infrastructure, and cross-cutting concerns, the backlog structure needs to accommodate complexity without collapsing under it.

The Tiered Backlog
#

Large products benefit from a tiered backlog structure:

  1. Strategic tier: Outcomes, OKRs, or product goals for the quarter. Owned by leadership and product management. Updated quarterly.
  2. Initiative tier: Named efforts that advance a strategic outcome. Owned by product managers. Updated monthly.
  3. Delivery tier: Epics and stories that decompose an initiative. Owned by individual teams. Updated continuously.

Each tier feeds the one below it. The strategic tier determines which initiatives enter the initiative tier. The initiative tier determines which epics and stories appear in the delivery tier. Information flows up through demos, retrospectives, and metrics.

The critical rule: no item in a lower tier can exist without a parent in the tier above it. Orphaned work is the primary source of backlog bloat. If someone wants to add a story that doesn’t map to an active initiative, the answer is either to create an initiative (which triggers the prioritization process) or to decline the story.

The Obeya Room Pattern
#

Borrowed from lean manufacturing and popularized by Toyota, the Obeya (“big room”) is a physical or virtual space where cross-functional representatives meet regularly to review the state of the product. The backlog is visualized on the wall (or in a shared tool), and the group discusses priorities, blockers, and dependencies in real time.

For large products, the Obeya pattern addresses a problem that no tool solves: alignment. When eight teams are pulling from the same backlog, local optimization is the default. Each team optimizes for its own velocity and its own priorities. The Obeya creates a forum where global optimization can happen.

The Regular Grooming Rhythm
#

A weekly or biweekly refinement session where the product manager and the team review the top 10-15 items in the backlog is a widely recommended practice. The goal is not to estimate everything but to ensure that the next two to three sprints’ worth of work is well understood.

For large products, this rhythm needs to exist at every tier. Leadership reviews the strategic tier quarterly. Product managers review the initiative tier monthly. Teams refine the delivery tier weekly. The cadence prevents the backlog from becoming stale and ensures that the most important items are always the most visible.

Common Failure Modes
#

The infinite backlog. A backlog that never shrinks is not a backlog; it is a suggestion box. If the backlog has more than a few hundred items, most of them are irrelevant. Archive aggressively.

The stakeholder lobby. In large organizations, stakeholders learn to mark everything as “critical” or to escalate directly to engineering managers. The ordered backlog is the defense against the stakeholder lobby. If the item is not in the top of the ranked list, it does not get worked on, regardless of who asked for it.

The technical debt blind spot. Product backlogs tend to favor features because features have visible stakeholders. Technical debt has no natural advocate. The solution is to allocate a fixed percentage of capacity (often 20-30%) to technical improvement and to make that allocation explicit in the backlog.

Pointless early estimation. Spending hours estimating items that are months away from being worked on is waste. Estimate just enough to support prioritization, and re-estimate when the item moves into the delivery backlog. Ron Jeffries, “Story Points Revisited” argues, as one of the originators of story points, that estimation should serve planning, not become an end in itself.

Conflating bugs and features. A bug is a commitment to fix something that was promised. A feature is a new investment. Mixing bugs and features in the same backlog without distinguishing between them leads to either under-investment in new value (because bugs always feel urgent) or neglect of quality (because features always feel more strategic). Separate them, fund them differently, and track them separately.

What to Do Next
#

If your backlog is currently a mess, here is a sequence that works:

  1. Archive everything older than six months that has not been touched. If it mattered, someone would have advocated for it. You can always pull items out of the archive.
  2. Identify the current product goal. If you cannot state it in one sentence, you are not ready to prioritize the backlog.
  3. Map every remaining item to the goal. Items that don’t map go to a separate “someday” list.
  4. Force-rank the top 20 items. No ties. This exercise will surface every disagreement about priorities, which is exactly what you need.
  5. Refine the top 5-10 items until they meet your definition of ready. Only these items are eligible for the next sprint.
  6. Set a weekly refinement cadence and protect it. The backlog decays without maintenance.

A healthy backlog is small, ordered, and refined. It reflects a clear product goal and a shared understanding of what matters most. Maintaining the backlog is not glamorous work, but it is the work that makes everything else possible.


What I've built and what I need: May 2026

The past month has been about turning repetitive workflows into reusable skills, and the gaps that remain are mostly about making those skills smarter, not more numerous.

What I Have Been Working On
#

Built a full SDLC skill pipeline. I shipped a comprehensive software development lifecycle orchestrator in agents that chains over 20 sub-skills, from issue creation through learnings capture. It handles phase contracts, backtracking when upstream artifacts are incomplete, and fast paths for small work like bug fixes and config changes. The pipeline tracks artifact status via YAML frontmatter and stores everything under .sdlc/ with a consistent directory structure.

Made issue tracking actually useful. I have been using create-issue heavily over the past month to track gaps in the software I am building. It is not perfect, but it beats not tracking the work, and it captures more context than I would take the time to write by hand.

Automated PR descriptions. I use create-pr-description to generate PR descriptions based on code changes and intent. Writing those manually was slow and inconsistent; now the descriptions reflect what actually changed without the manual effort.

What I Currently Need
#

Test the SDLC pipeline on real work. The SDLC skill is built but has not been stress-tested end to end on real feature work. I need to run it through enough real scenarios to surface the gaps between the design and practical use.

Scheduled issue-to-PR automation in openchamber. Openchamber can already create a worktree per directory and execute a prompt, but it does not run on a regular schedule. I need it to pick up new issues, execute the full pipeline, and open PRs without manual triggering.

Automated issue triaging in open source projects. I built a triage-issues skill but have not used it on my own repositories. The goal is to reduce the burden of going through issues to identify duplicates and decide whether they should be acted on.

Memory that agents manage automatically. Right now memory requires explicit reads and writes. I need agents to store, retrieve, and decay knowledge across sessions without me prompting them to do it.

Contextual Slack support. I need a way for users asking for help on Slack to receive contextually relevant information, and for the system to learn from human-to-human interactions and the answers people give each other. I started building slack-cached to cache and query Slack conversations.

Automatic context clearing between execution and review. Running implementation and review in the same context introduces bias. I need a mechanism, likely using forked subagents, to /clear between execution and review automatically so the reviewer starts fresh.

Accuracy pass on daily summaries. I have accumulated daily summaries that contain inaccuracies. I need something to go through them and correct what is wrong, rather than letting bad data compound over time.

Incremental PR description updates. When I update a PR after the description is written, I need create-pr-description to adjust minimally, appending or amending what changed, rather than regenerating the whole thing from scratch.

Skill usage tracking. I need to know how often each skill is invoked and when. Without that data, I cannot tell which skills are worth keeping and which I never use.

Replying to inline PR comments. gh does not support replying to inline review comments programmatically. Tools like gh-pr-review exist but feel awkward for what should be a straightforward operation. I need a clean way to post inline replies as part of the review and feedback skills. I started building ghx to address this gap.


Software Engineering Teams in the Age of AI: Smaller, Sharper, Intentionally Imperfect

LLM-powered coding assistants have changed what an individual engineer can produce in a day. The harder question is what this means for how teams should be organized, how they should work together, and which of their existing processes are still worth keeping. My answer is counterintuitive: the teams that thrive will not be the ones that adopt AI fastest or eliminate the most process. They will be the ones that correctly distinguish between friction that wastes time and friction that prevents mistakes.

What Actually Changed
#

AI-assisted development compresses the time from idea to working code. An engineer with a capable LLM can prototype a feature in hours that used to take days. Boilerplate, tests, documentation scaffolding, and CRUD endpoints all move closer to free.

But the things that remain expensive have not changed at all. Deciding whether to build a feature, choosing the right abstraction, understanding the domain deeply, and aligning technical work with business goals are as hard as they ever were. In many ways the expensive parts are harder now, because the temptation to just generate and ship is stronger.

This creates a specific tension for teams. When individuals can produce more code, the bottleneck shifts from production to coordination and judgment. Team design has to account for this shift, not ignore it.

Team Size: Follow the Problem Boundary
#

There is a persistent urge to declare an ideal team size. Amazon popularized the “two-pizza team” heuristic. Agile methodology settled on 3 to 9. Various management frameworks have their own magic numbers.

AI does not provide a new magic number, but it does shift the trade-off toward smaller teams.

Here is why. Communication overhead scales quadratically with team size. A team of 4 has 6 communication channels. A team of 8 has 28. When each individual ships faster because of AI assistance, the team hits the coordination ceiling sooner. The marginal output of the fifth or sixth engineer starts getting eaten by the cost of keeping everyone aligned.

Smaller teams also benefit from clearer ownership. When three people own a service, there is no ambiguity about who is responsible for it. When ten people own a service, everyone assumes someone else is handling the monitoring, the tests, the deployment pipeline.

But smaller is not always better, and this is where the nuance matters.

A team that is too small for its domain will fragment its attention across too many concerns. Three engineers trying to own a payments system, a notification platform, and a data pipeline will do none of them well. They will produce code quickly with AI assistance, but they will produce the wrong code, in the wrong abstractions, because no one has the mental space to think deeply about any one domain.

The heuristic I would use is not a fixed number. It is the smallest team that can own a coherent domain end-to-end. In practice this often lands between 3 and 5 people, but the number should follow the problem boundary, not the other way around.

A team of 3 that owns a single well-bounded service is better than a team of 8 that owns six loosely related ones. But a team of 6 that owns a genuinely integrated platform is better than splitting that platform across two teams of 3 that now have to coordinate across a boundary that should not exist.

Friction: Some of It Is Structural
#

The instinct when a new efficiency tool arrives is to use it to remove every source of friction. AI makes code review faster, so why not automate it? AI can summarize meetings, so why not eliminate them? AI can write documentation, so why not stop requiring it?

This instinct is partially right. A lot of process friction is genuine waste. Waiting three days for a manager to approve a deployment that could be automated. Holding a 30-minute standup where eleven people say “yesterday I worked on tickets, today I will work on tickets.” Filing a Jira ticket for a one-line config change.

But some friction serves a purpose, and removing that friction silently degrades the team’s output quality over time.

Code review is the clearest example. Before AI, code review served multiple functions: catching bugs, enforcing style, sharing knowledge, and forcing the author to think about their code one more time before it shipped. AI can handle the style and formatting portion completely. It can catch obvious bugs. What AI cannot do is evaluate whether the code solves the right problem, whether the abstraction will survive the next feature request, or whether the approach is consistent with how the rest of the system works.

In fact, code review becomes more important with AI-generated code, not less. When a human writes every line, you can assume the author thought about each line at some level, even if imperfectly. When an LLM generates code, the author may not have read every line carefully. The reviewer can no longer rely on the author’s intent as a safety net. The reviewer has to verify both correctness and intent independently.

This is harder work than traditional code review. It means reviews should be slower, not faster. The process should have more friction, not less. What should change is what the friction is applied to: less time on style, more time on substance.

The same principle applies to other forms of deliberation.

Sprint planning, done well, is the moment when the team asks “are we working on the right things?” That is productive friction. Sprint planning, done poorly, is an hour of reading ticket descriptions aloud. That is waste.

Architecture discussions are productive friction when they prevent the team from building on a flawed foundation. Architecture discussions are waste when they become philosophical debates that never converge on a decision.

The discipline is in telling the difference. The test I use: does a given process force someone to think about something they would otherwise skip? If yes, keep the process, even if it feels slow. If no, automate the process or remove it.

Processes Worth Keeping
#

Beyond code review, a few processes become more valuable in an AI-accelerated environment.

Architecture decision records (ADRs). When code is cheap to produce, the cost of building on the wrong abstraction is disproportionately high. A one-page document that captures what was decided, why, and what alternatives were considered is worth more than the five minutes it takes to write. AI can draft these from a conversation, but the team still needs to have the conversation.

Incident retrospectives. Retrospectives are one of the few processes that compound knowledge over time. When a production incident occurs, the team that writes down what happened, why, and what the team will change gets progressively harder to break. The team that fixes the bug and moves on repeats the same class of mistake forever. AI can assist with drafting the timeline from logs and alerts, but the insight about what to change has to come from the people who were in the room.

Onboarding. Onboarding is paradoxically harder in AI-heavy teams. Historically, junior engineers built deep familiarity with a codebase by writing code in it, struggling with its conventions, and learning its quirks through repetition. When AI handles much of the writing, that struggle disappears, and with it, the learning. Teams need to be more intentional about how they transfer knowledge. Structured pairing, documented design decisions, and explicit mentorship become more important, not less.

Specification before implementation. The ability to write a clear specification is now the highest-leverage skill in software engineering. A precise spec turns an LLM from a mediocre pair programmer into a highly effective one. A vague spec turns the LLM into a hallucination engine. Teams that invest in specification quality will outproduce teams that skip straight to prompting.

Processes Worth Eliminating
#

Some processes survive on inertia alone. AI gives permission to rethink them.

Status meetings that are not decisions. If a standup is just a round-robin of activity reports, replace the standup with an AI-generated summary of yesterday’s commits, PRs, and tickets. Reserve synchronous time for discussions that require back-and-forth. Most status updates do not require back-and-forth.

Granular task estimation. When AI can generate implementation drafts, estimating individual tasks in story points becomes less accurate and less useful. The time spent decomposing work into one-point, two-point, and three-point tickets is time not spent on the actual work. Replace granular estimation with outcome-level planning: what do we want to ship this cycle, and are we on track?

Manual test writing for boilerplate. AI handles test scaffolding well. Engineers should focus on test design (what cases matter, what edge cases exist, what invariants must hold) and let the tooling handle the mechanical work of writing assertions and setup code. The test plan is the valuable artifact. The test file is increasingly a commodity.

Elaborate approval workflows. If a change passes CI, passes automated security scanning, and passes peer review, the change should not also need approval from a manager who has not read the code. Every gate that does not add information is pure delay.

The Structure of a Team That Gets This Right
#

Here is the team I would design for this era.

Four or five people who own a clear domain. They spend less time writing boilerplate and more time debating trade-offs. Their code reviews are rigorous about intent and lightweight about style. They write ADRs for non-obvious decisions and skip ADRs for obvious ones. They do not hold meetings that could be a paragraph of text. They write specifications before they prompt.

They treat AI as an amplifier of judgment, not a replacement for it. The judgment is still the team’s job. The tooling just makes the execution of that judgment faster.

What Does Not Change
#

For all the shifts, some things do not change.

Trust between team members cannot be generated by a language model. Psychological safety, the ability to say “I think this approach is wrong” without fear, remains the single strongest predictor of team performance. A team of mediocre engineers who trust each other will outperform a team of brilliant engineers who do not trust each other, with or without AI assistance.

Shared understanding of the problem domain cannot be delegated to tooling. If no one on the team deeply understands the business context, the code will be technically correct and strategically wrong, faster than ever.

And the discipline to build less, not more, remains the hardest skill. When implementation is nearly free, the temptation to overbuild is constant. The teams that thrive will be the ones where someone at the table says “we don’t need this,” and the rest of the team listens.