Crew Resource Management for AI Pairing
Cockpit crews survive on explicit roles, readbacks, and standard callouts. AI pairing gets better when developers ste...
17 min read
28.07.2026, By Stephan Schwab
Three clients came to me within a few months, each with a working application built using an AI coding tool. None was a developer. Different tools, different industries, same moment of truth: they were close enough to launch that the business assumed the hard part was over. It was not. All three had a tangle of dependencies nobody could explain, defects that kept multiplying without a reliable way to find them, deployment by SSH and hope, and either no tests or tests quietly disabled because they were inconvenient. The AI had done what it was asked to do. Nobody had asked the questions that make software safe to change. Before an AI-built application goes live, somebody must trace dependencies, make deployment repeatable, decide what behavior must be checked, and own the failures that will appear outside the happy path. That is not a last-minute developer's eye. It is the difference between a useful prototype and a system the business can trust. If someone brought your team a nearly finished AI-built product tomorrow, could you tell them what must be true before it reaches users?
I want to be honest about what impressed me first: the applications actually ran. Real data, real workflows — not toy demos. All three were functional and nearly ready to open to users. The features worked. The integrations connected. The basic flows held together.
That is genuinely remarkable. Five years ago none of these people could have gotten this far without hiring a developer from day one. The AI tools had lowered the floor dramatically.
The ceiling, however, had not moved.
The applications differed in domain and scope. The underlying problems were nearly identical.
Dependencies without ownership. Each project had accumulated dozens of libraries. Some were pinned to specific versions because something had broken when they were updated. Others were unpinned entirely and quietly drifting. Nobody knew which dependencies were actually in use and which had been pulled in by something the AI suggested three refactors ago and never cleaned up. The requirements.txt files looked like archaeology digs — layers of decisions, none of them documented, several of them contradictory.
Defects with no feedback loop. The debugging process was: open the application, click around as a user, notice something looked wrong, then describe it to the AI and ask for a fix. No error monitoring. No log aggregation. No awareness of what the application was doing internally. The AI would suggest a change, the change would be applied, and the cycle would repeat. In more than one case a fix had introduced a regression that went unnoticed for days — because the only signal was what you could see from the outside, and nobody was looking at the inside.
Tests that were more decorative than functional. One client had no tests at all. The topic had never come up — why would it? Tests are not a user-visible feature. The application either did what you expected when you clicked through it, or it didn’t. That was the only feedback loop they knew. The second had a small pytest suite that passed locally and had been failing in a different environment for so long that the CI check had been disabled to unblock a release. The third had tests for the parts of the application that were easiest to test, which happened to be the parts that had never broken.
Deployment as an afterthought. In the Python and PHP world there is a long-standing culture where deploying means copying a file to a server and the change is live. No build step, no artifact, no pipeline. The AI tools — Claude Code, GitHub Copilot — had absorbed that culture and reproduced it without comment. SSH in, pull from the repository, restart the service. In ecosystems where a compiler produces an artifact, teams tend to develop a more deliberate deployment workflow by necessity. Here, that pressure never existed, and the AI never introduced it.
None of this is a criticism of the clients. They had done what made sense given what they knew. The AI tools had never told them these things mattered. The tools had answered the questions they were asked. They had not volunteered the questions that weren’t asked.
But the same is true of most human developers. The pattern of waiting to be told — answering the question asked, not the question that should have been asked — is not a flaw in the AI. It is a deeply embedded social behaviour that most organisations actively select for. Developers who raise concerns, suggest structural improvements, or push back on bad decisions are routinely experienced as difficult. Developers who deliver what was requested, on time, without friction, are called good team players. The incentive structure is explicit.
This goes back further than software. Frederick Winslow Taylor, whose Principles of Scientific Management (1911) shaped industrial work for the better part of a century, formalised the separation between those who think and those who execute. Managers plan. Workers carry out. The worker who questions the plan is a problem to be managed. Most schooling in industrial societies was designed around the same logic: produce people who follow instructions reliably, not people who interrogate whether the instructions make sense.
The AI is a very fast, very capable executor. It does not interrogate. It does not push back. In that sense it is a perfect product of the system Taylor imagined — and a mirror held up to what most organisations have always actually rewarded in the humans they hire.
All three had landed on similar technology choices independently. Python because it appeared constantly in AI-related content and the AI tools seemed fluent in it. React because it was the most visible frontend framework in tutorials, YouTube videos, and whatever the AI suggested when asked “what should I use for the frontend.”
These are not bad choices. Python is genuinely good for backends at this scale. React is a reasonable frontend framework. The problem wasn’t the technology. The problem was that the technology had been chosen by vibes — what seemed popular, what the AI defaulted to, what tutorials showed — rather than by any analysis of what the application actually needed.
Nobody had made these decisions deliberately. The AI had made them implicitly, one prompt at a time, and the decisions had accumulated into an architecture that nobody had designed and nobody fully understood.
One case stood out. The client had built the application through long conversations with an AI assistant, and the AI had been — there is no better word for it — enthusiastic. It praised the work. It suggested improvements. It proposed solutions to problems that did not yet exist.
The result was a codebase that looked like it was preparing for a future the application had not earned yet. OpenTofu for infrastructure that a single server did not need. Redis partially wired in, connected to nothing. Half-finished feature toggles scattered across the code, none of them controlling anything real. A full Playwright test suite, committed and never run. Alongside it, a pytest suite with most tests marked skip — and not one but several GitHub Actions workflows, each deploying a different container image, none of them waiting for tests to pass.
That last detail deserves a moment. Eleven containers in total, with a hard split between frontend and backend services. For an application that one person had built and was about to launch alone. The AI had absorbed the patterns of enterprise software architecture from its training data — microservices, separation of concerns at the infrastructure level, independent deployment pipelines — and had applied them faithfully. It had also absorbed the organisational dysfunctions that come with those patterns in large companies: siloed ownership, pipelines that optimise for keeping things moving regardless of quality signals, infrastructure complexity that exists because it always has. None of that was appropriate here. The AI did not know that. It had no way to know that. It was pattern-matching against examples from a very different context.
This is a specific failure mode that some AI tools fall into: they optimise for the appearance of progress. Adding infrastructure signals seriousness. Adding tests signals quality. Suggesting Redis signals that the system is ready to scale. None of it needs to work. None of it needs to be appropriate. The AI gets to keep the conversation going and the user gets to feel like things are moving forward. Nobody is there to say: you don’t need this yet, and adding it now is going to cost you.
A senior developer in that conversation would have pushed back. Not because the ideas were wrong in the abstract — OpenTofu is a legitimate tool, Redis is a legitimate tool, feature toggles are a legitimate pattern — but because they were the wrong answer to the wrong question at the wrong time. Knowing when not to add something is most of what separates experience from enthusiasm.
When I started looking at the dependency situation in each project, the surface problem was obvious: outdated libraries, security advisories ignored, version conflicts that had been papered over with pins. That’s fixable. The deeper problem was that none of the clients could answer a basic question: what does this application actually depend on, and why?
The AI had added libraries when asked to implement features. It had sometimes added libraries as intermediate steps and then not removed them when the approach changed. It had occasionally suggested libraries that were popular at some point in its training data but had since been superseded or abandoned. And because the clients were not developers, they had no frame of reference for evaluating any of it. When the AI said “add requests to your requirements,” they added it. When the AI said “we’ll need celery for this background task,” they added it. Whether those choices still made sense six months later was not a question they had the tools to ask.
The practical consequence was a Frankenstein system. Layer after layer of AI-selected libraries, each added to solve a problem in the moment, none of them revisited. When I went through the dependency lists properly, around sixty percent could be removed without losing a single piece of working functionality. They were just there — leftovers from abandoned approaches, duplicates doing the same job as something else already present, libraries pulled in for a feature that had been rewritten three sessions later in a completely different way.
This is not unique to AI-built systems. I have seen the exact same pattern in human-built codebases at large clients, consistently, for years. Management changes direction. Developers dutifully start the new approach. The old approach is never cleaned up — not out of laziness, but because nobody authorised the cleanup, and the developers were not going to raise their hand and propose extra work that had no ticket. The dead code stays. The unused libraries stay. The half-finished abstractions stay. The codebase becomes an archaeological record of every strategic pivot that came down from above, with none of the evidence removed. The AI just produces the same result faster and without needing a management layer to trigger the pivot — a new prompt is enough.
The absence of tests is usually described as a quality problem. It is also a trust problem.
Without tests, you cannot verify that a change did what you intended. You cannot verify that it didn’t break something else. You cannot hand the codebase to another developer and say “the tests will tell you if you broke anything.” You cannot deploy with confidence. You cannot refactor safely. You are permanently in a state of hoping that what you remember about the code is still true.
Non-developers do not know that executable tests exist as a practice. They know what testing means in the everyday sense — click through the application, see if it works — but automated tests, test suites, test-driven development: these are not part of their mental model at all. If they have heard of tests, it is as a vague technical concept, something developers do, possibly something you ask the AI to add when you remember. What they do not know is that the presence or absence of tests shapes the entire trajectory of the codebase — that tests are the most reliable way to give an AI coding agent stable ground to work from, and that a codebase built without them accumulates structural problems that make adding them later progressively harder.
This is the cruelest part of the trajectory: the moment tests become most valuable is the moment they become hardest to add. A codebase with no test discipline bakes untestability into its structure. Functions that do too many things. Dependencies that are hardcoded rather than injected. Side effects embedded in business logic. By the time you want tests, you need a refactor first, and you can’t refactor safely without tests.
In one case there was a core calculation function that was over 300 lines long, read from a database, called an external API, applied business rules, formatted output, and logged to a file, all in sequence, with no separation of concerns. Writing a test for it would have required either a real database, a real API key, and a real filesystem, or a significant rewrite. It had never been tested. During the review, edge cases surfaced that produced wrong results — the kind of thing that would have reached users on day one.
One client had a programming background — not recent, but real. Decades ago, before frameworks and cloud and AI, he had written code. He brought that instinct to structure the work, which was good. The direction he took it, less so.
He had adopted what he called spec-driven development: detailed Markdown files describing how the application should behave. Dozens of them, accumulated over months. They documented features, edge cases, decisions made and revisited, requirements that had evolved as the application grew. He used them as context for every conversation with Claude, expecting the model to apply them faithfully and consistently.
The problem was that the specs had become a record of the application’s history rather than a description of its current state. They contained contradictions — earlier decisions that had been superseded by later ones, without the earlier ones being removed. They described behaviour that had been partially implemented, or implemented differently, or abandoned. Claude, given this context, did what a model does with conflicting instructions: it produced results that were locally plausible but globally inconsistent. Features regressed. Behaviour changed between sessions for no apparent reason. The client had no way to detect this because there were no executable checks — only more Markdown.
The fix was uncomfortable but straightforward: delete the spec files. Not archive them — delete them. Then write tests that described what the application actually did, right now, verifiably. Once the test suite captured the real behaviour, Claude had something stable to work against. Regressions became visible immediately. Development stabilised.
The lesson is not that documentation is useless. It is that documentation cannot do the job of tests. A Markdown file can describe intended behaviour. Only a test can verify it. The model does not know which of your specs are current, which are aspirational, and which are simply wrong — unless you give it something that runs.
One owner came to me with what they understood as a handful of permission bugs: certain actions that should have been restricted to specific roles were available to everyone. Sensitive operations — the kind that only an administrator or a designated approver should be able to perform — were open to any user who happened to be logged in.
Read as bugs, these look small — a few checks in the wrong place. Read correctly, they are symptoms of a missing authorisation model. The application had roles in name only. There was no consistent concept of who is allowed to do what, enforced in one place, applied everywhere. Permissions had been added ad hoc, feature by feature, wherever the AI happened to wire one in when asked — and skipped everywhere it wasn’t explicitly requested. For an application dealing with sensitive organisational actions, that is not a cosmetic defect. It is the kind of gap that, in production, becomes an incident with legal consequences.
A non-developer reports this as “some users can do things they shouldn’t.” That is an accurate description of the symptom. What they cannot see is that the symptom points to the absence of an entire architectural layer — authentication and authorisation as a deliberate, centralised design decision rather than a scatter of conditionals. The AI never built that layer because it was never asked to think about the system as a system. It was asked to add features. Each feature worked in isolation. The cross-cutting concern that should have governed all of them simply never existed.
The technology choice made this almost inevitable. Build the same application on Spring Boot and you get Spring Security essentially for free — a security framework with roots going back two decades, battle-tested across thousands of enterprise systems, that hands you authentication, role- and expression-based authorisation, method-level access control, CSRF protection, session handling, and password encoding as a single coherent layer you are expected to wire in. The entire ecosystem assumes security is a cross-cutting concern, because the framework was built by people who had seen what happens when it isn’t. In the typical AI-chosen Python stack — FastAPI, Flask — none of that exists. You get OAuth2 primitives and dependency-injection hooks at best, and you assemble the rest yourself, library by library, decision by decision. There is no equivalent that arrives whole and forces the question on you. (Django is the exception that proves the rule — it ships a real auth and permissions system — but Django is not what the AI reaches for when a non-developer asks for a backend.) So the model picked the stack with the least built-in protection, and then faithfully built an application that had none. Nobody chose that outcome. It fell out of two defaults stacked on top of each other.
This is where the limits of outsourcing judgement to a model become concrete. The AI can implement a permission check flawlessly. What it does not do, unprompted, is step back and ask: what is the access model for this entire application, who are the actors, what are the trust boundaries, and where must they be enforced? Those are questions of design and domain understanding. They require someone who has seen what happens when they are not asked — usually because they have cleaned up the aftermath. That experience does not yet come in a model. It comes from having been responsible for the consequences.
None of this is an argument against AI coding tools. They are genuinely powerful, and the clients in these stories got further with them than they could have alone. The point is narrower and more durable: building software is not the same as producing code, and the gap between the two is exactly where experienced human judgement still lives.
The AI answers the question it is asked. It does not know which questions matter. It does not push back, weigh trade-offs against a context it cannot see, or recognise the absence of something that should be there. It will happily build the wrong thing well. Knowing what to build, what to leave out, when to stop adding, and where the real risks hide — that is the work that does not outsource. Not yet, and not because the models are weak. Because that judgement is formed by consequences, and a model has never been on the hook for one.
So if you have built something impressive with an AI tool and you are about to open it to real users, the most valuable thing you can do is have someone with real experience look at it first — not to take it away from you, but to ask the questions nobody thought to ask.
Full disclosure: this article was written with the help of an LLM — but not by the LLM. The stories, the judgement, the argument, and every claim in it are mine. The model helped me shape the words. It did not supply the experience. That distinction is, in a sense, the whole point.
Tell me what is happening. I listen, ask a few practical questions, and reflect back what I see: where the risk may sit, what may be blocking delivery, and what looks worth checking next. No pitch, no obligation. Confidential and direct.
Talk it through. Practical reflection, no pitch.
Start a ConversationA senior developer for your team
Our Developer Advocate writes production code with your team, improves the pipeline, and accelerates delivery. 60-70% coding, 30-40% coaching. A temporary teammate who ships from day one.