← All operating-loop essays

From the build · Customer operations

How I built the bug fix support loop from report to resolution

From a customer report to a verified release and a reply: how support and engineering carry one obligation all the way through.

A bug is not finished when the pull request merges.

The customer still has the problem until the fix reaches the version they use. They still need to know what changed. If they reply that it is still broken, the system needs to pick up that same obligation with its history intact.

I built the support loop around that complete path.

For our SaaS product, support starts in the product and runs through the orchestration layer into engineering, verification, release, and communication back to the customer. The interesting part is how it connects to the recursive development loop: the same machinery that builds a fix can also improve the process that found, routed, tested, and delivered it.

I want a customer report to result in two useful things. First, a resolved problem. Second, better operating knowledge, expressed somewhere the next run will actually use.

That requires a more precise architecture than “send the ticket to an agent.”

There are several different records involved. The customer case holds the conversation and the obligation to respond. A bug-loop record holds the engineering progression. A roadmap story defines the work. A pull request carries the implementation. Verification and deployment records establish where that implementation was tested and delivered.

Those records are connected, but they are not interchangeable.

One engineering defect may affect several customers. One customer case may need a clearer explanation rather than a code change. A story may be done from an implementation perspective while a native release is still outstanding. The model has to preserve those distinctions so that the automation can act on the right one.

Durable intake

The first boundary is intake.

In-app feedback lands in the SaaS product's database as a durable record. The product captures useful context such as the page, app version, and client information. The server stamps the user and organizational ownership, rather than accepting a client's claim about whose report it is.

That gives the customer a record even if the next system is unavailable.

Delivery to the orchestration layer is a separate operation. The relay sends a signed payload, and the receiving command deduplicates it using the source system, environment, and source record identity. If the sender loses the response and tries again, the orchestration layer can return the existing case.

That is the first place where a seemingly small implementation choice determines whether the loop can run unattended. A timeout after a successful write looks exactly like a failed write to a caller that received no answer. The identity has to survive the retry.

Email forwarding and the orchestration layer delivery have independent working sets and retry state. A failure in one lane should not stop the other or cause the successful lane to repeat its work unnecessarily. Customer identity has its own delivery lane too.

The identity relay projects the minimum account, branch, and approved-user context needed for support. It carries source versions, so an older event cannot overwrite a newer projection. Acknowledging an old event cannot mark a newer pending update delivered. When a user is erased, the retained projection follows that lifecycle instead of preserving contact details in an indefinite event history.

All of this is ordinary integration work. It is also what makes the later autonomy possible. An agent cannot reliably act on a case that exists twice, points at the wrong customer, or vanished between systems.

The support and engineering boundary

Once a bug-category case arrives in the orchestration layer, it enters the bug loop. Support has a customer-facing record. The engineering loop has a state and a link to that record, without needing a copy of the customer's words.

The support agent and the engineering agent have different responsibilities. The permission boundary makes that separation enforceable.

The support agent needs to understand the customer, ask for missing information, answer a help question, and keep the conversation moving. The engineering agent needs a technical description, reproduction, acceptance criteria, platform, build, and the work required to fix the problem.

Engineering does not need the customer's identity to repair a persistence bug.

So the handoff is a translation into a sanitized engineering story. It preserves what happened and what should happen, while keeping customer conversation in the support system. The orchestration layer's guards reject known forms of personal information in linked loop stories, including contact details, customer and account names, and sufficiently long verbatim excerpts from customer messages.

That guard sits below the UI and MCP paths. The model still has to reason carefully about what it writes, and a deterministic guard does not establish that every possible sensitive detail will be detected. But the common prohibited cases have an enforceable refusal rather than relying solely on a sentence in a prompt.

Figure 01

The handoff is a privacy boundary

  1. Customer caseConversation, identity, account context, and reply obligations stay with support.
  2. Sanitized handoffTechnical behavior, reproduction, expected result, platform, and build cross the boundary.
  3. Engineering storyThe engineering agent receives the technical contract and a link back to the case.
The receiving command enforces the boundary. Sanitization guards help reject known personal-information patterns; they do not prove perfect detection.

Leases and fix routes

The next boundary is ownership.

When a case reaches a step that needs work, the database creates the task for that step. There is one task per case and step. If the case returns to the step, the existing task is requeued for another round.

Workers claim those tasks with a lease. Concurrent claims use row locking so they do not all receive the same queued job. The claim returns a fresh token, and the worker has to present it to extend, finish, or release the task.

The fresh token matters because multiple sessions can represent the same bot. Knowing which agent owns the work does not tell us which of its two sessions currently has authority.

If a worker disappears, the lease expires and the task can return to the queue. If that original worker later wakes up, its old token cannot complete the new worker's claim. Attempts are counted, and repeated failures eventually produce a visible need for a person instead of an endless retry loop.

The case transition and the task completion are coupled through the same command. If the requested state transition is refused, the worker has not somehow completed the task anyway.

This is the same basic idea used by the newer Yard development loop: work is durable, authority is temporary, and the current attempt is explicit. The support handoff queue and the Yard work leases remain separate contracts because they govern different units. They connect through the shared story rather than pretending a support triage task and a code review are the same thing.

Triage determines the route before the system starts changing code.

The engineering agent establishes whether the issue can be reproduced, what result would count as fixed, and what test would demonstrate the difference. It also determines whether the work fits the bounded autonomous-fix class.

A small change with a clear reproduction is different from a migration, a permissions change, a billing change, or a question about what the product should do. The route needs to name someone capable of acting. Assigning a case to a bot that cannot perform the required operation is a hidden queue.

The fix worker also needs limits on persistence. If the reproduction does not behave as predicted, the scope grows beyond the declared area, or repeated CI failures remain unexplained, handing the work back with a specific reason is a valid outcome.

That does not mean every technical block should become a question for me. A missing dependency or a problem another authorized worker can solve should stay in the work system. A product decision needs a decision record with options. The architecture needs to distinguish the two.

For the eligible small-fix route, there is a deliberately restricted execution path. The fallback fixer, for example, records a trusted base, reads scope rules from that base, and judges the resulting commit before pushing it. It refuses changes to the machinery used to decide whether its own work is acceptable.

It is also explicitly opt-in. Its existence does not imply that it is scheduled or enabled everywhere. The broader Yard loop supplies the route for planned and more general engineering work under its own story contracts and controls.

This is one of the places the loops join. Support defines the problem and retains the customer obligation. The development machinery owns the bounded implementation. Neither needs to replace the other.

Shared work and individual obligations

The relationship between cases and work is equally important.

If five customers report the same defect, I want five customer relationships preserved and one coherent engineering fix. The support queue can group cases beneath their shared bug, showing the case count, oldest wait, stage, owner, and next action.

That collapse removes duplicate engineering coordination. It does not erase the separate conversations. Each customer may have a different platform, a different installed build, or a reply still owed.

When the shared story advances, the linked cases can consume that evidence. When a customer writes back, that case's communication obligation becomes current again even if the engineering work has progressed.

This is why one status field cannot explain the whole operation.

The bug state machine tracks the technical progression: received, support triaged, engineering triaged, claimed, pull request open, merged, staging verified, awaiting release, and released, with explicit alternative and reopen paths.

Alongside it, the support pipeline derives eight gates from facts already in the orchestration layer: filed, reachable, acknowledged, triaged, fixed, verified on staging, released, and told and closed.

A gate has an owner, a clock, evidence, and a statement of what clears it.

If the customer has just sent another message, the acknowledgment gate can become current again while the fixed gate remains passed. The system should show that a reply is owed without pretending the code became unfixed.

Help and idea cases skip the engineering and release gates that do not apply. Waiting on a customer should not be treated as an overdue action by a bot. A phantom case with no inbound message should be recognizable as a different problem from a legitimate case waiting on engineering.

Those distinctions make the queue operationally useful. I can ask what is stuck, who can act, and what evidence would move it forward, without reading every timeline.

The UI and MCP tools read the same underlying work and gate calculations. A bot and a person should not see incompatible definitions of “needs attention.”

Figure 02

One fix, several customer obligations

Several linked casesEach keeps its customer, platform, release needs, and reply state
  1. Shared engineering storyOne reproduction, implementation, and exact-commit review
  2. Shared release evidenceRecorded deployments establish which fixes reached which lanes
Each case evaluates its own gatesA shared fix does not erase an individual customer’s communication or release requirements.
Cases converge on shared engineering work, then use the resulting facts to satisfy their own obligations.

Verification and release

The handoff into engineering then has to produce proof.

Every loop fix is expected to carry an acceptance check tied to its story. For a browser flow, that can be a Playwright test tagged with the story key. A database or backend defect can use an appropriate pgTAP or Deno test.

The check needs to reproduce the original failure and pass with the correction. It should exercise the actual boundary that failed. A component test of a disabled button does not prove that a native device preserves a draft after the operating system interrupts the app.

The distinction matters because the next automated transition depends on the strength of that evidence.

After a staging deployment, the verification workflow runs the applicable tagged browser acceptance checks against the deployed environment. It reports a signed result tied to the story and deployed commit. The orchestration layer records that result with the run and evidence.

A pass can move the associated case to staging verified. A failure sends the case back into engineering and reopens the work. If the required test is absent or the test session could not sign in, the result is untested. It does not advance the case.

The same result for the same story and commit is idempotent. Replaying the report does not create another independent success or keep moving the state machine.

Physical-device acceptance has its own evidence requirements. The actual installed build, operating system, device, role, and tested variants matter. A browser pass cannot accept an iPhone behavior. A pass on yesterday's installed build cannot establish the condition of today's candidate.

That evidence belongs in the orchestration layer too. Otherwise a screenshot or a statement in a chat becomes the only record of a test, and the next worker has to reconstruct what it meant.

Now comes the boundary that support systems often compress too aggressively: release.

Merged means the source change was accepted. Staging verified means a particular deployed staging check passed. Released means the fix reached the required production lane.

The orchestration layer derives release evidence from recorded deployments and their workflow provenance. It checks the selected commit and whether the story's merged pull requests are actually included. Open work on the story matters as well. A release-looking message from a worker is not sufficient.

An in-progress workflow is not a completed deployment. A lost webhook is not proof that no deployment happened. The reconciliation path reads the provider again and records the fact when it can establish it.

Native delivery adds another dimension. A story can name the iOS, Android, or over-the-air lanes it requires. A web deployment containing the same source does not automatically release that native story. The required lanes need their own release evidence.

This is how the customer loop stays honest about availability.

It also explains why release authority remains explicit. A build for device testing does not authorize a store upload. Recording a native release can trigger downstream customer communication, so that record is itself an operational action with consequences.

The combined path looks like this:

Figure 03

Evidence advances the customer outcome

  1. MergedThe source change is accepted.
  2. Staging verifiedApplicable acceptance checks pass on the deployed commit.
  3. ReleasedEvidence covers every production lane required by the story.
  4. Customer toldNotice and delivery state are recorded separately.
  5. ResolvedClosure follows the case policy, with replies still able to reopen work.

Failed acceptance returns to engineering. A continuing customer problem returns to triage. Recurring failures become operating improvements.

Untested checks do not advance verification. A web release does not establish native availability. Closure is not proof of customer confirmation.

Communication and closure

Communication is part of completion.

The system has standard notices for receipt, work starting, and a released fix. The fixed notice follows the released state. Merging or passing staging does not trigger that promise.

The database queues the notice, and delivery happens through a worker. Queueing the communication does not need to make the case transition fail if the mail provider is unavailable. Delivery has its own retries and durable result.

the support agent's tailored replies are a separate capability. They have role checks, request identity, limits, and an AI disclosure. A promise that a staff member will follow up leaves that obligation visible; the bot cannot make it disappear by writing the promise itself.

Written, queued, sent, and received by a human are different claims. The loop records the delivery evidence it has. It should never convert “we queued an email” into “the customer has been told.”

If delivery fails or there is no usable recipient, that remains actionable. A support workflow that stops caring after generating the words has left the important part unfinished.

Once the fixed notice is sent, the configured quiet period allows automatic resolution if the customer does not reply. That is an operational closure rule, not proof that the customer personally retested and confirmed the fix.

A reply after the notice prevents quiet closure. A later reply can reopen the same case. The original conversation, linked defect, and release evidence stay together, so support can understand whether the customer is seeing the same problem or something new.

The loop therefore has a way to disagree with its own apparent success.

That is essential. If every released fix permanently erased the possibility of failure, the system would measure its own confidence rather than customer outcomes.

Stalls have a similar treatment.

A sweeper checks overdue stages, exhausted attempts, and other known failure conditions. It raises a deduplicated alert with the case, owner, cause, and action that would clear it. The human-needed feed combines these obligations with such things as replies owed and failed deliveries.

The alert resolves when the underlying condition changes. A bot does not improve the operation by dismissing the warning while leaving the customer blocked.

For me, that is the useful escalation contract. Tell me which decision or action is missing, give me the evidence, and stop asking me to reconstruct the workflow. Routine movement should remain in the system.

Learning across both loops

There is another return path after resolution: QA and learning.

Resolved cases can be examined for difficulty, repeated replies, reopening, or a manual concern. A QA review snapshots the cases, linked bugs, and stage timelines. A human reviewer can record a process change, documentation fix, or that no change is needed.

That is different from a code review. A technically correct patch can still have taken an unreasonable amount of coordination to reach the customer. A case can expose a missing support explanation or an unclear release handoff without revealing a defect in the implementation.

Those findings can become work in the recursive loop.

The operating-rules repository records recurring regressions and gaps as structured root-cause analyses. It connects the failure to the cause, missed review dimension, relevant operator, and proposed mechanism. The reporting and auditor procedures can then identify patterns and propose a change to how future work is planned, reviewed, or verified.

Here is an illustrative example of both loops working together.

Several customers report that a draft disappears after reconnecting. Their cases link to one engineering story. The builder fixes the persistence behavior, the reviewer checks the exact change, and the relevant acceptance evidence follows the fix through staging and release.

Each customer still has a separate communication and follow-up obligation. If one reports that the problem remains on a particular native build, that fact stays attached to their case. It does not invalidate unrelated evidence, and it does not disappear inside the aggregate.

Suppose the investigation shows that the original tests covered a successful save but never interrupted a save. The product correction is one outcome. The recursive correction is a durable test and, if the pattern warrants it, a review requirement for persistence changes.

Suppose instead the fix was correct but the customer notice never left the queue. Then the improvement belongs in delivery recovery or alerting. The system should learn the actual failure, not automatically prescribe another code test.

This is what I mean when I say the loops collapse and chain together.

They collapse around shared facts: one defect, one story, one reviewed commit, one set of release evidence. They chain across responsibilities: a customer need becomes engineering work; engineering produces a release; the release creates a communication obligation; the outcome becomes evidence for improving the process.

The customer cases remain distinct where the obligations are distinct. The learning records remain distinct from the events they explain. The connection is strong enough to automate the handoffs without losing that detail.

That is the architecture I want behind autonomous operations: a system that can carry work forward, tell the truth about what happened, and make its next attempt better.

When a customer reports a bug, the response should improve the product. Over time, it should also improve the way we operate the company that builds it.

All operating-loop essays ↗