# Goal-driven engineering: the software factory nobody has to ask

> Goal-driven engineering: write a goal once, and a monitoring agent watches the codebase, notices when the goal stops holding, and opens the fix.

- **Source:** https://ainativesoftware.engineering/goal-driven-engineering
- **Site:** AI-Native Software Engineering — https://ainativesoftware.engineering/book

- **Published:** 2026-09-22
- **Updated:** 2026-09-22
- **Reading time:** 11 minutes
- **Tags:** goal-driven engineering, software factory, dark factory, AI coding agents, autonomous agents, AI-native engineering

Most teams that adopt AI coding agents land in the same place. The agent is good, the harness around it is decent, and work moves faster once somebody has decided what the work is. That last part did not change, and it is where the queue sits now.

A software factory does not change it either. You hand it a spec, it plans, writes, tests, and hands back a pull request. Real throughput, and still a fulfilment system: it starts when a person asks. Everything nobody has asked about keeps slipping one merge at a time. A dependency goes four majors behind. The checkout page's largest contentful paint creeps from 2.1 seconds to 3.4 and stays there, because the person who would have caught it was shipping something else.

Goal-driven engineering closes that gap. You describe the finish line once, and a system keeps the codebase on the right side of it.

## Executive summary

**What it is**

A goal written in plain words, with a command that measures it and a target that decides it. A goal manager runs that command on a trigger, compares the reading against the target, and closes the gap by filing an issue or by sending the work to a software factory. The goal stays registered, so it keeps being enforced after everybody has moved on.

**What it buys you**

Regressions get caught by the merge that caused them instead of by a customer six weeks later. Work that never gets prioritised, like dependency lag and performance budgets, gets done. And the standard your team argues about once ends up written in a file that something actually enforces.

**What it costs**

More pull requests arriving at the same reviewers, which is the constraint in most teams already. Goals that are wrong alert until people stop reading them. And it only works on top of gates you trust, because from the third rung up those gates are what stands between an agent and your main branch.

Every component, and the loop that makes it a loop.

**What people write.**

- You and your team — Write the goals, own them, answer the escalations
- Goal registry — goals/*.yml in Git, reviewed in a pull request
- Triggers — Schedule, merge, release, advisory, telemetry alert

**The goal manager.**

- Observer — Runs the signal, records the reading
- Memory — Tried, landed, rejected, and why
- Planner — The cheapest action, or none
- Guard — Authority and budget, before anything starts

**Execution.**

- Software factory — An orchestrator and worker agents, one per repo in scope
- Harness gates — Checks, LLM review, routed human review
- main — Merged once every gate is green

**Production.**

- Deploy — Behind a flag, released as a canary
- Production — Real traffic, real data, real money
- Telemetry — Field metrics, errors, latency, spend

**The record.**

- Issues, ledger and dashboard — Every observation and action, where your team already reads

How the work moves through it:

1. You write a goal and commit it to the registry. Nothing else in the picture is written by a person.
2. A trigger wakes the goal manager. The observer runs that goal's signal and records what it read.
3. The planner compares the reading against the target, reads the memory, and picks an action or picks none.
4. The guard checks the goal's authority and budget before any agent starts.
5. Below its authority, the goal manager stops at an issue. Above it, it sends a spec, a scope and a budget to the software factory.
6. The factory works across every repository in scope and opens pull requests, which pass the same harness gates as human work before they reach main.
7. Merged changes deploy behind a flag and go out as a canary.
8. Production telemetry feeds straight back in as a signal source, which is the only way to measure a goal about latency, error rate or spend.
9. Every observation and action lands in the record, and the record is what a person reads.

Four things to build, in this order. Each one is a section below.

**1. Write one goal properly**
Ten fields, including the command that measures it and the person who owns it. If you cannot write the command, what you have is a wish.

**2. Run the loop with no agent in it**
A trigger, the signal, and a file it appends to. You are checking whether the signal is stable and the threshold is right before anything can act on either.

**3. Add the goal manager**
An observer, a memory of what was already tried, a planner that is allowed to choose nothing, and a guard that enforces authority and budget. This is the only part you have to build.

**4. Grant autonomy one rung at a time**
Report, then issues, then drafts, then pull requests, then merges. Per goal, against evidence, never all at once.

## The definition

|  | Interactive mode | The software factory | Goal-driven engineering |
| --- | --- | --- | --- |
| Who starts the work | You, on every turn | You, once per request | Nobody. A trigger does |
| What you hand over | A message | A spec | A goal |
| It ends | When you stop typing | When the pull request lands | A standing goal never ends |
| It fits | Novel work, unfamiliar code, anything you will only recognise when you see it | Work you can specify up front and check without reading it | Properties that have to stay true after everybody has moved on |

Goal-driven engineering makes the unit of work a **standing goal**: a plain-language statement of something that has to be true about a system, with a signal that measures it and a target that decides it. A monitoring agent runs the signal on a trigger, compares the reading against the target, and acts on the difference.

That is a control loop, and the control-theory words fit without stretching. The goal is the setpoint, the signal is the sensor, the monitoring agent is the controller, the software factory is the actuator. The codebase is what gets controlled, and every merge is a disturbance.

1. **Observe** (Sensor). Run the goal's signal against the codebase as it stands, and write the number down.
2. **Compare** (Error). Hold the number against the target. The gap between them is the only reason the loop ever does anything.
3. **Decide** (Controller). Read what was already tried, then pick the cheapest action that closes the gap. Doing nothing counts.
4. **Act** (Actuator). File the issue, or send a spec to the factory and open the pull request. Then wait for the codebase to change and measure again.

> **The one-sentence version**
>
> A software factory answers requests. Goal-driven engineering removes the need for somebody to make them.

> **Where this comes from**
>
> I have not run this at full scale, and I would rather say so up front. Every piece of it is already in production somewhere: scheduled checks, Dependabot, coverage gates, and the goal pattern that Claude Code exposes as `/goal` and Codex as goal mode. What I have not seen is a team wiring those pieces into one loop, with a memory, against goals they wrote themselves. That assembly is what this article argues for.

## Two kinds of goal

"Goal" is doing two jobs here, and a system that runs both through the same loop gets one of them wrong.

An **invariant goal** has to keep holding. Coverage above a floor. No critical advisory open longer than 72 hours. It never completes: it is met today, and tomorrow a change nobody connected to it can breach it. That is the reason something has to keep watching.

A **terminal goal** has a finish line. Move all eleven services off the deprecated auth client. Implement this spec. Progress is the measure, and when it lands you retire the goal.

Terminal goals need a stopping rule and an attempt limit, or an agent that cannot reach the line keeps trying. Invariant goals need the opposite. They have to survive being met, and they have to stay cheap, because they will run thousands of times and act almost never.

## What a goal is made of

Everything downstream reads this record. Every agent, every gate, every issue it files can only be as good as what is written here.

- `intent` — Plain words: what has to be true, and why anybody cares. Every agent in the chain reads this before it decides anything, and nothing downstream repairs a vague one.
- `scope` — The repositories and the paths the goal covers. A goal can span repositories, which is most of the value. Without a scope it wanders into ones you did not mean.
- `signal` — A command that returns the current value, or a verdict. The sensor. If you cannot write it, what you have is a wish.
- `target` — The value or the condition that counts as met. It turns a number into a pass or a fail, so a person and an agent can argue about the code and never about whether the goal holds.
- `triggers` — When to measure: a schedule, a merge, a release, an external event. Cheap signals run on every merge, expensive ones nightly. This field is where most of the running cost is decided.
- `authority` — The most it may do without being asked: report, open an issue, open a draft, open a pull request, merge. Autonomy is granted per goal. Dependency bumps can merge themselves long before a schema migration should open a draft.
- `budget` — Ceilings: pull requests a week, turns an attempt, spend a month. A loop with no ceiling is an outage with a changelog. It is also what stops one badly written goal from flooding the review queue.
- `escalation` — What happens when it cannot reach the target: after so many failed attempts, stop and ask a named person. Some gaps need a decision rather than another attempt, and without this the loop grinds away at one only a person can give.
- `owner` — One person, by name. A goal nobody owns becomes alerts nobody reads.
- `review` — A date the goal gets re-read, after which it expires. Goals rot, and an expired one is easier to spot than a wrong one.

One goal, written out. Plain files in the repository, reviewed in a pull request like anything else that can change your code.

```yaml
id: perf-entry-routes
intent: >
  The three routes that carry our signups have to stay fast on a mid-range
  phone. Historically they regress through images and third-party tags, not
  through our own JavaScript, so look there first.
scope:
  repos: [web]
  paths: ["app/(marketing)/**", "next.config.ts"]
signal: npm run lighthouse:ci -- --routes / /pricing /checkout --metric performance
target: ">= 90 on every route"
guard: npm run bundle:report          # a route made lazy to win the score gets caught here
triggers:
  - on: merge
    branch: main
  - on: schedule
    cron: "0 3 * * *"
authority: draft-pr                   # L2 on the ladder
budget:
  pull_requests_per_week: 2
  turns_per_attempt: 25
escalation:
  after_failed_attempts: 2
  notify: "@alfonso"
owner: "@alfonso"
review: 2027-03-01
```

## Goals and wishes

Most first attempts are wishes. They are sentences everybody agrees with, which is a much weaker property than it sounds.

One question settles it: **can you write the command that measures this?** If you cannot, you have a wish, and handing a wish to an autonomous system buys you confident pull requests against a standard nobody stated.

1. Wish: "Keep the code maintainable". Goal: "No file in `src/` goes over 400 lines, and no function over a cyclomatic complexity of 12". What the rewrite added: A command that returns a number.
2. Wish: "Improve performance". Goal: "p75 LCP on `/`, `/pricing` and `/checkout` stays at or under 2.5 seconds, measured on the last 7 days of field data". What the rewrite added: What is measured, where, and against which window.
3. Wish: "Stay up to date". Goal: "No production dependency is more than one minor behind, and no major is more than 90 days behind its release". What the rewrite added: A deadline, so the gap cannot grow quietly.
4. Wish: "Keep test coverage high". Goal: "Branch coverage on changed files never lands below the figure the last release shipped with, and mutation score on `services/billing/` stays above 60%". What the rewrite added: A baseline that moves with you, and a second signal the first one cannot be gamed against.

Look at the fourth one. Coverage on its own is an instruction to raise coverage, and the cheapest way to raise coverage is to delete the failing test. Any goal expressed as one number is a number the system will move, by the cheapest route it can find.

## How you would build one

Nothing here needs a model that does not exist yet. Four bands, one of them new work.

**Triggers.** What wakes the system up. None of them is a person filing a ticket.

- Schedule — Nightly or weekly, for signals that cost money to run
- Merge event — The cheapest and the most useful: measure what just changed
- Release — Check the goals that only mean something against shipped code
- External feed — A CVE advisory, an upstream release, a provider deprecation
- Production telemetry — An error budget burning, a field metric slipping, spend climbing

**The control plane.** The part you have to build. It decides whether anything should happen, and it is the only part allowed to say no.

- Goal registry — The goals as files in the repository, reviewed like code
- Observer — Runs the signal, records the value, names the drift
- Memory — What was proposed, merged, rejected and why, per goal
- Planner — Picks the cheapest action that closes the gap, including none
- Guard — Enforces authority and budget before a single agent starts

**The execution plane.** The software factory. Goal-driven engineering keeps it busy without you.

- Orchestrator — Turns the planner's action into a spec and a run
- Worker agents — Do the work, in parallel, across every repository in scope
- The harness — Your existing gates: checks, LLM review, routed human review

**The record.** What a person looks at. If this layer is noisy, the system gets switched off within a month.

- Ledger — Every observation and every action, with the evidence attached
- Issues and pull requests — The output, in the tools your team already reads
- Dashboard — One row per goal: where it stands and when it was last checked

Triggers are events you already emit. The execution plane is a software factory you either run already or can buy. The record layer is your issue tracker plus a page that reads the ledger. The control plane is the missing piece, and it is small: a registry of goal files, something that runs a command on a trigger, a store of what happened before, and a planner that picks the next action or picks none.

Watch the planner, because it is where people over-build. Its job is to pick the cheapest action that could close the gap, with doing nothing on the list, or it will generate work for its own sake.

## Memory is what separates it from a linter

Take the memory out and you have a scheduled check that files a ticket, which is about a decade old.

Memory holds, per goal: what the signal read over time, what was proposed, what landed, what was rejected and why, and what is now out of bounds. That is what stops the second attempt at a problem from being a rerun of the first. The agent can open an issue saying coverage on `services/billing` has fallen for the fourth sprint running, that it proposed generated tests in March and you rejected them as meaningless, and that this drop sits in the two files that handle refunds. A linter repeats one sentence forever, at the same volume, until people learn to scroll past it.

Record the decision, not the discussion. Memory that stores threads rots within a quarter.

## The autonomy ladder

The first thing people ask is whether an agent should be allowed to open pull requests on its own. Autonomy belongs to each goal rather than to the system, and it is granted one rung at a time against evidence.

- **L0 Report.** Measures the signal and writes it to the ledger. Opens nothing. How a goal earns it: Nothing. Every goal starts here, including the obvious ones.
- **L1 Raise.** Files an issue with the number, the change that moved it, and its reading of why. How a goal earns it: Two weeks at level 0 where every alert it would have raised is one you agree with.
- **L2 Draft.** Opens a draft pull request with a proposed fix and leaves it for a person to finish. How a goal earns it: Issues a human acted on more often than closed.
- **L3 Propose.** Opens a pull request ready for review, every gate already passed. How a goal earns it: Drafts that cleared the gates without somebody rewriting them.
- **L4 Merge.** Merges on its own, inside a blast radius agreed in advance, behind a flag it can roll back. How a goal earns it: A quarter at level 3 with no rollback traced to this goal.

A dependency bump can reach level 4 in a month. A schema migration may never leave level 1. Leaving a goal one rung too low costs somebody a button click. Putting it one rung too high costs you a merge at two in the morning, into a gate that was weaker than it looked.

The rungs are mine, and the bar on each one is a starting point rather than a measurement. I would expect a team that ran this for a year to move them, and to end up with different bars for different kinds of goal.

## One goal, end to end

The performance goal above, over two weeks: p75 LCP on `/checkout` at or under 2.5 seconds.

A chart of 14 daily readings against the goal "p75 LCP on /checkout stays at or under 2.5s". Illustrative numbers, not a measurement. It runs near 2.05s, breaks the 2.5s ceiling on day 6, stays over for three days, and comes back under after the fix merges.

- Day 6: PR #412 merges a hero image with no width
- Day 6: Observer runs on merge, goal breached, issue #415 opened (the system acting)
- Day 8: Factory opens #418, gates pass, a person merges it (the system acting)

Day 6 is the part to look at. With nothing watching, day 6 is invisible. The pull request that caused it was reviewed, approved and merged by people checking whether the feature worked, and the regression surfaces weeks later through a complaint or somebody opening Lighthouse for another reason, by which point it is tangled up with thirty other merges.

With the goal registered, the observer runs on the merge event, reads 3.42 against a target of 2.5, checks the memory, and opens this.

**Issue #415, opened 11 minutes after the merge**

**Monitor**

**Goal breached: p75 LCP on `/checkout` is 3.42s, target 2.5s.**

The regression starts at `a41f2c9` (PR #412, "Add seasonal hero"). LCP on that route was 2.18s on the previous commit. The new hero image is 1.4 MB, served as a PNG with no `width` or `height`, so it is both the LCP element and a layout shift.

History: this goal was breached in March by an uncompressed image in the same folder, and the fix then was to route it through the image pipeline. That pipeline exists. This file bypasses it.

Proposed action: convert the asset, set explicit dimensions, add the folder to the pipeline's glob. I am inside budget and my authority is draft pull requests, so I will open one unless somebody closes this within the hour.

**What happened**

PR #418 opened 40 minutes later. The gates passed. A person read the diff, agreed with the glob change, and merged it on day 8. The signal came back under target on the next run and the goal closed itself.

A threshold alert cannot write that issue. It names the commit, because it measured either side of one. It remembers March. And its proposal is the glob change, which fixes the next hero image as well as this one.

Multiply that across a handful of goals and repositories, and you get the layer a person looks at.

A mock of the record layer: 6 goals across 4 repositories.

| Goal | Repos | Kind | Status | Now | Target | Level | Last action |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Lighthouse performance stays above 90 on the three entry routes | web | invariant | Drifting | 87 | ≥ 90 | L2 | Opened #418, 2h ago |
| Branch coverage never lands below the figure the last release shipped with | api | invariant | Breached | 74% | ≥ 81% | L1 | Opened #417, 1d ago |
| No critical advisory stays unpatched for more than 72 hours | api, web, jobs | invariant | Met | 0 open | 0 open | L4 | Merged #414, 3d ago |
| No production dependency is more than one minor behind | api, web, jobs, sdk | invariant | Met | 1 minor | ≤ 1 minor | L4 | Merged #410, 5d ago |
| Every service is off the deprecated auth client | api, jobs, sdk | terminal | Running | 7 of 11 | 11 of 11 | L3 | Opened #419, 40m ago |
| The public API contract never breaks without a version bump | api, sdk | invariant | Met | 0 breaks | 0 breaks | L3 | Reported, 6h ago |

## What quietly breaks it

None of these are bugs.

- **The review queue floods.** Six goals, each allowed three pull requests a week, is eighteen diffs nobody asked for landing on the same two reviewers. The fix: Budget per goal, and count the queue as a resource the way you count spend. While the queue is the constraint, autonomy anywhere else buys nothing.
- **The signal gets gamed.** Coverage climbs because a failing test was deleted. The bundle shrinks because a route got lazy-loaded into a slower first paint. The number is honest and the codebase is worse. The fix: Pair every goal with a guard the same change cannot satisfy: coverage with mutation score, bundle size with LCP, latency with error rate.
- **Two goals want opposite things.** Stay on the latest major of the framework, and keep the bundle under 200 KB. One release makes both impossible at once, and the system will quietly pick one. The fix: Give goals an explicit precedence, and make reporting a conflict a legal outcome. An agent that stops and names the trade-off is behaving correctly.
- **The memory rots.** The same fix arrives for the fourth time, because the three rejections were comments on closed pull requests and nothing read them back. The fix: Write the decision into the goal's memory rather than the discussion: what was tried, what happened, what is now out of bounds and why.
- **Nobody reads it any more.** A threshold set too tight alerts every week, everyone learns to close it, and the week it is right nothing happens. The fix: An alert people ignore is a broken goal. Re-tune it or retire it, and hold every goal to its review date.
- **Autonomy granted too early.** A goal goes to level 4 because the first three fixes were clean, and the fourth merges at two in the morning into a gate that was never as strong as it looked. The fix: Move one goal up one rung at a time, with the evidence written down somewhere a sceptic can read it.

The first one kills most pilots. Human review is already the constraint in most teams, and a system built to generate more work to review finds that constraint in week one. Budget the queue before you budget anything else.

## You cannot start here

This sits on top of a harness. The signal has to be a command that runs the same way on your laptop and in CI. The gates have to be trustworthy enough that a passing pull request means something, because from level 2 up they are what stands between an autonomous agent and your main branch. And you need a fast way to undo a change, because the system will eventually be wrong in a way nobody predicted.

Without those, goal-driven engineering will faithfully automate a process you do not trust. The [harness guide](https://ainativesoftware.engineering/harness) covers what to put in the repository and what to wire around it, the [baby steps](https://ainativesoftware.engineering/baby-steps) put that work in order, and the [Readiness Analyzer](https://ainativesoftware.engineering/readiness) scores where you stand in about ninety seconds.

## The smallest version worth running

Start with one goal, not with the control plane.

**1. Pick the boring regression**
The thing that has slipped twice already and that you can measure with a command you already have: coverage, bundle size, a Lighthouse score, dependency lag. Pick the most measurable goal you have rather than the most important one.

**2. Write the record, all ten fields**
Owner and review date included. If you cannot fill the signal field with a real command, stop and go write that command. It is worth having whether or not any of this follows.

**3. Run it at level 0 for two weeks**
A cron job, the command, and a file it appends to. No agent yet. You are finding out whether the signal is stable and whether the threshold is right, and you will almost certainly move the threshold.

**4. Move to level 1 and read every issue**
Now let it file issues, with the evidence and the commit it blames. Track one number: of the issues it opened, how many you are glad to have. My own cutoff would be about two thirds, though that figure is a guess and I would revise it after a month of real issues.

**5. Add the memory before you add the second goal**
The first time it proposes something you already rejected, you have found the thing that decides whether any of this scales. Fix it while you still have one goal.

**6. Then a second goal, and only then a factory**
Two goals running honestly at level 1 beats six at level 3 that everybody has stopped reading.

## Where this goes

The dark factory is the usual end state for all of this: a spec in one end, a tested change out the other, almost nobody in the middle. It is a real destination, and parts of it already work on tasks whose intent is easy to write down and whose correctness a machine can judge.

Somebody still has to decide what should be true. Goal-driven engineering is the claim that the decision is the durable artifact, more durable than any ticket, and that once it is written down properly the same machinery that builds everything else can enforce it.

That moves your job one more step. You stopped writing all the code and started running the line that writes it. Now you write the goals the line is held to, decide how much rope each one gets, and read the few things that need you.

The book goes deeper into the machinery: the goal pattern and the dark factory in the orchestration chapter, and the gates that make any of it safe to run in the verification chapter. It is [out now in Early Release](https://ainativesoftware.engineering/book).

## Frequently asked questions

### What is goal-driven engineering?

Goal-driven engineering makes the unit of work a standing goal instead of a ticket. You write the goal in plain words, pair it with a command that measures whether it holds, and give it a scope, a budget and a level of authority. A monitoring agent runs that command on a schedule or on every merge, compares the result against the target, and acts when the two come apart, by opening an issue or by sending the work to a software factory that opens a pull request. Nobody files the request. The goal is the request, and it stays filed.

### How is it different from a software factory or a dark factory?

A software factory is reactive. It turns a spec into a reviewed change, and it does nothing until somebody hands it one. Goal-driven engineering is the layer that decides when to hand it one. You need both in practice: goal-driven engineering with no factory can only file issues, and a factory with no goals only ever works on what a person already noticed.

### What makes a good goal?

A good goal has a signal you can run as a command and a target to compare it against. "Keep the code maintainable" cannot be checked, so it is a wish. "No file in src/ goes over 400 lines" can be checked, so it is a goal. The test is simple: if you cannot write the command that measures it, you do not have a goal yet. A good goal also names one owner, carries a budget, and has a date on which somebody re-reads it.

### Is it safe to let an agent open pull requests unprompted?

It is as safe as the gates the pull request has to pass, which is why goal-driven engineering sits on top of a harness rather than replacing one. Autonomy is granted per goal on a five-rung ladder, starting at report-only and moving up one rung at a time against evidence. A goal only opens issues after a stretch where every alert it would have raised was one you agreed with, and it only merges on its own inside a blast radius agreed in advance, behind a flag that can roll it back.

### What stops the agent from gaming its own metric?

Nothing, if you give it one number. Coverage climbs when a failing test is deleted, and a bundle shrinks when a route is lazy-loaded into a slower first paint. Pair every goal with a guard signal the same change cannot satisfy: coverage with mutation score, bundle size with LCP, latency with error rate. A goal with one number is an instruction to move that number.

### Do I need this if I already have CI, Dependabot and a linter?

Those tools are the same idea with a fixed set of goals somebody else wrote, and with nothing to do about them beyond failing a build or opening one templated bump. Goal-driven engineering lets you write the goals yourself, in plain words, across several repositories at once, and gives the thing watching them a memory of what it already tried and the ability to open a real change. If a linter is the right tool for your goal, use the linter. Most of the goals teams care about cannot be written as a lint rule.

---

_AI-Native Software Engineering by Alfonso Graziano (O'Reilly Media, Early Release; print edition February 2027). Every page of ainativesoftware.engineering is also served as Markdown: append `.md` to any URL. Index: https://ainativesoftware.engineering/llms.txt — whole site in one file: https://ainativesoftware.engineering/llms-full.txt._
