← Articles

// FIELD NOTE

You Can't Gate Your Way Out of a Sequencing Problem

Cory LaNou

Cory LaNou

You Can't Gate Your Way Out of a Sequencing Problem

You Can't Gate Your Way Out of a Sequencing Problem

Overview

A founder building a multi-agent coding system asked me what minimum validation gate he should require before letting it run unsupervised. His system takes one high-level prompt, splits it among frontend, backend, UI, and testing agents, then lets all of them modify the same codebase. Each agent works well alone. The system falls apart when they work together.

The gate is downstream of the failure. By the time it evaluates the branch, several agents may have spent hours building on assumptions that were never reconciled. A stricter checklist can reject the mess. It cannot turn those assumptions into one coherent design.

This follows my earlier post on the difference between a harness and an orchestrator. I am picking up from there instead of repeating it.

The setup that fails

The setup sounds reasonable at first. Give one agent the frontend, one the backend, one the UI, and another the tests. Each agent has a specialty, so each one should produce better work in its lane.

Software does not divide itself that cleanly. A user-facing change reaches through a handler, a data model, a template, client behavior, and tests. The frontend agent changes the component contract. The backend agent implements the old contract. The testing agent locks in a third interpretation. They all touch the same files or the same seams between files.

The decomposition created the collision before the agents started.

A gate only evaluates what already happened

A validation gate can answer useful questions. Does the project compile? Do the tests pass? Did the generated files change? Is coverage above the agreed floor? Does a reviewer approve the diff?

It cannot answer the earlier coordination question: did every agent build from the same architectural knowledge?

Two branches can compile and still introduce competing patterns for the same problem. Two agents can each satisfy their narrow task while leaving the combined system harder to understand. The gate sees the final artifacts. It does not travel backward and give the second agent knowledge that only existed after the first agent finished.

We solved this before AI

Nobody hires a hundred developers, hands each person an isolated task, and expects a coherent product to come back without coordination. We already know the practices that make parallel software work: small units of work, one owner per change, branch isolation, dependency order, review, and CI.

AI is a new tool doing familiar engineering work. Treating agents as a new category of worker has led teams to throw away the coordination practices they already trust.

Shape the work so every change has an owner, the right context, and a safe place to land. The number of specialists running at once comes after that.

Split by vertical change, not by role

Give one agent a complete unit of value. If the change needs a handler, a model update, a UI change, and tests, the same agent owns all four. The work may cross more layers, but it has one set of assumptions and one accountable owner.

That does not make every change independent. It removes collisions caused by the decomposition itself. When two vertical changes still overlap, the dependency becomes visible enough to order them.

A role split asks several agents to collaborate across every feature boundary. A vertical split lets one agent finish a coherent change before the next dependent change begins.

Isolate every unit of work

Each vertical change gets its own branch and git worktree. Agents never share a working tree. They cannot overwrite each other's uncommitted files, inherit a half-generated artifact, or mistake another agent's edits for their own.

The branch should start from freshly fetched, merged main. That detail matters. If a new agent branches from an arbitrary local checkout, isolation preserves the wrong starting point perfectly.

Detent's pinned worktree implementation fetches the remote default branch and uses that remote ref as the base for a new worktree branch:

func (l *LocalGit) addBranchedWorktree(ctx context.Context, path string, branch string) error {
	return l.addWorktreeWithPrune(ctx, func() error {
		exists, err := l.branchExists(ctx, branch)
		if err != nil {
			return err
		}
		if exists {
			_, err = l.runGit(ctx, "worktree", "add", path, branch)
			return err
		}

		baseRef, err := l.newBranchBaseRef(ctx)
		if err != nil {
			return err
		}
		_, err = l.runGit(ctx, "worktree", "add", "-b", branch, path, baseRef)
		return err
	})
}

func (l *LocalGit) newBranchBaseRef(ctx context.Context) (string, error) {
	remotes, err := l.runGit(ctx, "remote")
	if err != nil {
		return "", fmt.Errorf("list git remotes: %w", err)
	}
	if !stringListContains(remotes, defaultGitRemote) {
		return "HEAD", nil
	}

	branch, err := remoteDefaultBranch(ctx, l.sourceRoot, defaultGitRemote)
	if err != nil {
		return "", fmt.Errorf("resolve %s default branch: %w", defaultGitRemote, err)
	}
	remoteRef := defaultGitRemote + "/" + branch
	refspec := "+refs/heads/" + branch + ":refs/remotes/" + remoteRef
	if _, err := l.runGit(ctx, "fetch", defaultGitRemote, refspec); err != nil {
		return "", fmt.Errorf("fetch remote default branch %s: %w", remoteRef, err)
	}
	return remoteRef, nil
}

The merge is the one place those isolated changes reconcile. Git exposes the textual conflicts. CI exposes behavioral conflicts. Review handles design conflicts that automation cannot judge.

Sequence knowledge, not calendar slots

Suppose issue one establishes a new pattern and issue two needs to use it. If both run from the same old version of main, each agent invents an implementation. A human eventually gets two competing patterns and has to pick a winner.

Run issue one first, merge it, then start issue two from that merged result. The second agent now receives the first agent's decision as code, tests, and history. Dependency order gets knowledge to the next agent.

The mechanism can stay boring. Detent's dependency-line parser at the pinned commit is one regular expression and one small function:

var pattern = regexp.MustCompile(`(?i)^\s*(?:[-*+]\s+)?(?:[*_~]+)?\s*(?:blocked\s+by|depends[\s-]+on)(?:[*_~]+)?\s*(?::\s*|\s+)(?:[*_~]+)?\s*(.+)\s*$`)

func Match(line string) (string, bool) {
	matches := pattern.FindStringSubmatch(line)
	if len(matches) != 2 {
		return "", false
	}
	return matches[1], true
}

The policy behind it does the real work. The readiness check requires every blocker to be ready. A resolved blocker counts when its issue is closed, its state is terminal, or its pull request is merged under the default readiness policy.

func dependencyBlockersReady(blockers []dependencyBlocker, cfg DependencyAutoUnblockConfig, terminalStates []string) bool {
	if len(blockers) == 0 {
		return false
	}
	for _, blocker := range blockers {
		if !dependencyBlockerReady(blocker, cfg, terminalStates) {
			return false
		}
	}
	return true
}

func dependencyBlockerReady(blocker dependencyBlocker, cfg DependencyAutoUnblockConfig, terminalStates []string) bool {
	if blocker.Resolved {
		if blocker.Issue.Closed || stateIn(blocker.Issue.State, terminalStates) {
			return true
		}
		if cfg.Readiness == DependencyReadinessTerminalOrMerged && pullRequestMerged(blocker.Issue.PullRequest) {
			return true
		}
		return false
	}
	if strings.TrimSpace(blocker.Ref.State) == "" {
		return false
	}
	return stateIn(blocker.Ref.State, terminalStates)
}

That is sequencing expressed as policy: dependent work waits until the knowledge it needs has landed.

Then the gate gets easy

Once ownership, isolation, and order are correct, the gate has one job: decide whether one branch can merge.

The gate for this site runs generation, lint, and the race-enabled test suite. Detent's own gate is larger because its failure surface is larger. At the pinned commit, it checks generated configuration, builds the binary, runs lint and vet, audits with NilAway, runs race tests, and enforces both a 70 percent total coverage threshold and a 50 percent per-package floor with documented exceptions.

The pinned Makefile spells that contract out:

COVERAGE_THRESHOLD := 70.0
PACKAGE_COVERAGE_FLOOR := 50
PACKAGE_COVERAGE_EXCEPTIONS := scripts/coverage-exceptions.txt

check: check-unlocked

check-unlocked: check-generated build lint vet nilaway-audit test-race test-cover test-cover-packages
	@echo "All checks passed."

test-cover-packages: test-cover
	go run ./tools/covercheck -profile $(COVERPROFILE) -floor $(PACKAGE_COVERAGE_FLOOR) -exceptions $(PACKAGE_COVERAGE_EXCEPTIONS)

Another project may add human review. A low-risk project may merge automatically and deploy. Those are policy choices around one isolated change, which makes them measurable and adjustable.

Autonomy is earned with evidence

You do not need to decide on day one that the system is safe to run unattended. Start supervised. Record what the merge gate catches. Tighten it when a real failure gets through. Remove a human checkpoint only after the automated checks have repeatedly caught the failures you care about.

That turns autonomy into an operating measurement. You trust the system because its boundaries have produced evidence, not because the orchestrator completed a demo once.

Takeaway

When several agents work well alone and fail together, inspect the shape and order of the work before adding another approval step. Give one agent a vertical change, isolate it in a worktree from merged main, and make dependent changes wait for the knowledge they need. Then the gate can do the narrow job it is good at: deciding whether one branch is ready to merge.

Want more AI development insights?

Subscribe to the newsletter for weekly tips on using AI in professional development.

Subscribe to Newsletter

// KEEP READING

More articles