A wrong answer is worse than no answer. If a system whose whole job is to make failures quietly disappear writes the wrong fix, we assume the failure is handled and stop thinking about it. Meanwhile the real bug is still there in the code, and now nobody is looking for it.
Then we taught it how to be wrong
To stop the agent from shipping more silent errors, which would defeat the whole point, we had to teach it how to say no. Sometimes that means recognising there is nothing to do. Sometimes it means realising it cannot do the job safely and stopping right there. Both are answers the agent has to be allowed to give. 2 cases taught us why.
Case 1
Nothing to change
The agent was told to rename a field from required to isRequired. Nothing in the code used that field, so there was nothing to rename. Its instructions gave it no way to say so, and it hand-edited our own copy of the API specification instead. The result looked right. The reasoning was broken.
It had no way to report an empty task
Case 2
Already fixed
An app was supposed to change primary to isPrimary, but that app had already been fixed 2 days earlier. Again the task had no exit for "nothing to do", so the agent went looking for something to change and edited the stand-in test data instead, breaking a check that protects us.
It went looking for work that was done
Those were bugs in our instructions, not the agent's fault. So we changed how we write them. A brief can now say, in plain terms: nothing uses this field, no change is needed. And the agent can now stop and report back when a fix is beyond what it should safely attempt.
That turned out to be a real design improvement. A system that cannot say no, whether that is "nothing to do here" or "I cannot do this safely", is not a self-healing system. It is an autonomous system trying to look busy.
The system has to be quieter than the bugs
There is another lesson that only shows up once the system runs every single day. Automation that creates more noise than it removes is not really automation.
There is a cap on how many tasks a single run can produce. If a broken check suddenly reports 200 failures, the right response is not 200 pull requests. It is to tell a human that the check itself needs attention. Findings also do not pile up. The same problem showing up 3 nights in a row should stay 1 problem, not turn into 3 separate pieces of work.
We learned that one the hard way too. A CI check had that de-duplication switched off, and a single problem spun up 4 near-identical pull requests over a weekend.
A single problem spun up 4 near-identical pull requests over a weekend. The code was doing exactly what we had told it to. We just had not told it what good behaviour looked like.
Humans are still in the loop
When people talk about self-healing software, there is a pull toward jumping straight to the fully autonomous version. We have not, and that is not the right place to start. Before the system can put up a fix, it needs explicit sign-off. Silence does not count as a yes. Every fix still passes through a person. What has changed is what that person does: read the agent's explanation, approve the change, or send it back with notes.
What software should feel like
This started as a very practical problem in our own codebase. But the thing we actually care about is bigger.
Imagine software where your relationship with failure is different. Something breaks. The software notices before you have even reported it. It tells you what went wrong, and that it is already on it. 30 minutes later, the bug is gone.