Cases
CaseSoftware engineering

Automate the job, not the typing

One engineer pushed software automation on a live product with more than 500 users without delegating judgment or production authority.

Updated 6 Aug 202611 min readConfidence: High for a bounded side project; deliberately unproven for a primary-job environment.
Boundary

Tested on an independently operated side project with more than 500 users. This is not a Fleet case, and the autonomy level should not be copied into an employer's systems without that environment's controls.

  • 500+ real users
  • 1,700+ tests before independent review blocked the release
  • Five isolated implementation lanes with zero file collisions
  • Two release defects found by independent review
Final claim

The aim is not maximum autonomy. It is the highest autonomy you can defend.

The model in one minute

Automating software engineering did not remove the engineer. It separated the job into three systems with different owners.

The human owns direction, judgment, authority, and accountability. Agents inspect, implement, test, challenge, and prepare. Controls decide whether the resulting claims are strong enough to justify the next state transition.

The operating loop has six moves:

  1. 01Frame the outcome
  2. 02Bound the responsibility
  3. 03Delegate in isolation
  4. 04Verify independently
  5. 05Promote by evidence
  6. 06Encode the lesson
Every increase in autonomy must preserve the evidence and authority boundaries.
01

Human

Owns the irreversible calls

  • Direction
  • Judgment
  • Authority
  • Accountability
02

Agents

Supply bounded execution

  • Inspection
  • Implementation
  • Testing
  • Challenge
  • Preparation
03

Controls

Turn claims into evidence

  • Tests
  • Gates
  • Reviewers
  • Staging
  • Canaries
Human direction bounds agent execution. Independent controls decide whether its claims are strong enough to justify the next state transition.

This is the useful model:

The framework is not a new content category. It is the conclusion produced by the experiment below.

The real experiment

I wanted to know how far one software engineer could push the automation of software engineering.

Not in a demo repository. Not by asking an agent to generate a to-do app and declaring victory. I tested it on an independently operated product with more than 500 users, a real database, paid data, deployments, caches, browser journeys, and consequences when the system lies.

The boundary matters. I control this side project's product direction, repository, deployment path, and acceptable blast radius. This is not work from Fleet, and it is not evidence that the same autonomy is appropriate inside my primary job.

The experiment started with a familiar question:

That question became obsolete quickly. The agents could write plenty of code. In one stretch they delivered more than a dozen pull requests, a design overhaul, onboarding changes, dashboard work, and new product flows. I steered much of it from my phone. The machine did most of the typing.

The bottleneck moved upward.

I stopped writing most of the code. I did not stop being the engineer. I kept the work that became more valuable as implementation became cheaper:

  • framing the outcome;
  • decomposing the system into independent lanes;
  • deciding what evidence counts;
  • judging the product;
  • setting authority boundaries;
  • approving production.

Software engineering was never one activity. Typing merely consumed enough time to hide the rest.

Responsibility boundary

The split is not human versus machine. It is judgment versus execution, and authority versus capability.

ResponsibilityAgent roleHuman roleRequired proof
Decide what is worth buildingResearch and challenge assumptionsOwn direction and trade-offsA clear outcome and explicit exclusions
Understand the systemInspect code, data, workflows, and historyDecide which model is credibleCited findings and a dependency map
Implement the changeBuild in a bounded laneResolve product ambiguityFocused tests and a reviewable diff
Verify correctnessRun checks and reproduce failure pathsDecide whether the evidence is sufficientLogs, tests, deployed behavior, and independent oracles
Judge the productRender states and collect observationsOwn taste, language, and user valueDesktop, mobile, and interaction evidence
Promote to productionPrepare the exact releaseRetain final authorityNamed target, final diff, green controls, and rollback
Learn from failurePropose a durable controlDecide what deserves permanenceA test, gate, skill, playbook, or monitor

An agent may be capable of fetching a credential, merging a branch, changing a schema, or writing a plausible number into a card. Capability does not imply permission. A confident result does not imply proof. A green test does not imply a safe release sequence.

The useful unit of automation is a responsibility with five parts:

  1. an input;
  2. permitted actions;
  3. required evidence;
  4. a stopping condition;
  5. an accountable owner.

If any of those are missing, the task is not automated. It is merely moving quickly.

The worker should also stop at a named state. “Done” is not a state. “Built with focused tests green and no production authority exercised” is.

Evidence from real failures

The model did not emerge from a whiteboard. It came from controls failing, reviewers disagreeing, and numbers that were locally correct but globally wrong.

The gate caught its own builder

  1. 01
    Situation

    Hardcoded routes had left dead links behind after a page moved, so I introduced a route registry and a rule banning new hardcoded paths.

  2. 02
    Failure

    I was interrupted halfway through the migration. The repository still contained the exact pattern the new control claimed to prevent.

  3. 03
    What the system missed

    A convention could describe the desired future while allowing the current repository to contradict it. A first wrapper also printed an error but swallowed the failing exit status.

  4. 04
    Durable rule

    A factual convention is not real until the system can reject its violation, and a gate is not trusted until its negative path has been observed.

  5. 05
    Resulting control

    An AST-based route rule, a duplication ratchet, and a build gate whose failure status reaches CI.

export const ROUTES = {
  DASHBOARD: '/dashboard',
  MONEY_TOOLS: '/dashboard/money-tools',
  occupation: (code: string) => '/occupations/' + code,
} as const

The first useful thing the gate did was block the person building it. That is a much stronger proof than a green run on already-clean code.

The safety system refused a correct diagnosis

  1. 01
    Situation

    A data-seeding worker encountered half-rotated credentials and correctly diagnosed the missing replacement secret.

  2. 02
    Failure

    The worker attempted to fetch a stronger credential. An independent permission system refused twice.

  3. 03
    What the system missed

    Diagnosis and authority are separate responsibilities. The worker had enough context to know the repair, but not the authority to grant it to itself.

  4. 04
    Durable rule

    The system requesting more authority must not be the system that approves the request. Failure to obtain a judgment must fail closed.

  5. 05
    Resulting control

    A context-aware permission boundary that denies the external action and produces the exact five-minute human repair.

The denial was not friction. It was the architecture working. The useful properties were separation of duties, context awareness, fail-closed behavior, and denial with a useful handoff.

The independent reviewer found bugs in time

  1. 01
    Situation

    More than 1,700 tests, a production build, and a staging walk all passed for a release that depended on new data.

  2. 02
    Failure

    Production could deploy the application before the data arrived, cache an empty result, and remain wrong after every workflow became green. My first file-diff gate could also be bypassed by a later copy-only commit.

  3. 03
    What the system missed

    The checks proved a final state and one push. They did not model the release timeline or the obligations inherited through commit ancestry.

  4. 04
    Durable rule

    A successful prerequisite must cover the latest dependency-bearing commit and belong to the release that consumes it.

  5. 05
    Resulting control

    An independent reviewer plus a release gate that proves R is an ancestor of W and W is an ancestor of D.

Let R be the newest dependency-bearing commit, W a successful data-workflow commit, and D the release:

git merge-base --is-ancestor "$required_sha" "$workflow_sha"
git merge-base --is-ancestor "$workflow_sha" "$release_sha"

The first condition rejects a green run that predates the data change. The second rejects a run from an unrelated branch or a future commit that the release does not contain.

The arithmetic was correct and the product was wrong

  1. 01
    Situation

    One product surface displayed 534 while the source published 975 for the same visible label.

  2. 02
    Failure

    The code correctly summed every exact detailed row it could see, but many detailed cells were censored as less than 20. The exact source total existed at a coarser grain.

  3. 03
    What the system missed

    Each layer was locally correct, but the detailed grain could not support the product's exact regional claim.

  4. 04
    Durable rule

    Name the user-visible claim, its grain, time window, population, and uncertainty before debugging the calculation.

  5. 05
    Resulting control

    Separate exact, censored, and lower-bound semantics, then require the exact coarse total to cover the detailed floor.

type PublishedCount =
  | { kind: 'exact'; value: number }
  | { kind: 'censored'; source: string }
  | { kind: 'floor'; value: number; omittedCells: number }

export function assertCoarseTotalCoversDetail(coarse: number, floor: number) {
  if (coarse < floor) throw new Error('Coarse total is below the detailed floor')
}

Two screenshots became the input to the investigation. The team normalized the visible claim, traced one value backward from label to source, located the first divergence, and preserved the finding as a failing contract.

Implementation guide

Start with one bounded workflow. Do not begin with a general autonomous engineering platform.

1. Choose a responsibility with a cheap rollback

Good first candidates repeat, produce visible evidence, and can be undone:

  • remove a deprecated UI pattern across known routes;
  • turn a recurring review catch into a CI assertion;
  • update a read model with a source-to-screen fixture;
  • create a staging canary for one named failure class.

Stop if the first experiment requires credential changes, destructive data operations, broad architecture migration, or production promotion. Those responsibilities need a trustworthy evidence system first.

2. Write the task contract before dispatch

# Outcome

Remove the fabricated engagement count from every article.

## May change

- Article UI and focused tests

## Must not change

- Published article copy
- Analytics configuration

## Evidence required

- Search returns zero matches for the old label
- Production build passes
- Staging HTML does not contain the fabricated count

## Stop and ask

- A real count source is discovered
- The change requires a database or tracking decision

The worker can choose implementation details inside the lane. It cannot silently choose a new product direction.

3. Make the boundary physical

Give independent work an isolated worktree and a written ownership contract.

git worktree add ../lane-remove-metric -b task/remove-metric
cd ../lane-remove-metric
npm ci
npm test

Five genuinely independent lanes completed with zero file collisions in one experiment. Collision freedom came from the decomposition, not from asking five agents to be careful in the same directory.

Stop dispatching when the slices share a schema, a migration, a central component, or an unresolved product decision. That is one coordinated change, not several independent lanes.

4. Separate builder and reviewer

The builder optimizes for completion. The reviewer optimizes for falsification.

Builder
  implement the scoped outcome
  prove focused behavior
  prepare staging evidence
  stop before production authority

Reviewer
  inspect the complete diff independently
  reconstruct dependencies
  find temporal and lineage failures
  exercise negative paths
  report blocked or ready

5. Assign each fact to its cheapest durable proof

  • Source and route facts belong to types or static checks.
  • Calculation meaning belongs to independent data fixtures.
  • Workflow order belongs to an ancestry or state-machine invariant.
  • Built behavior belongs to a browser test against the artifact.
  • Deployed behavior belongs to a canary against the named target.
  • Product taste belongs to a human looking at rendered states.
  • Production risk belongs to human approval on the exact diff.

Every new gate must demonstrate its own failure. A green-only history proves almost nothing.

6. Promote through observable states

scoped
  -> built
  -> staged
  -> independently reviewed
  -> ready for production
  -> approved
  -> released
  -> learned

Each transition names the facts that make it true. “Approved” attaches to a specific diff and CI state. If the diff changes, the approval expires.

The stopping condition is simple: do not advance when the evidence required by the next state is missing, stale, or attached to a different artifact.

Limits and boundaries

This operating model has been tested on an independently operated side project with more than 500 users. It has been used across UI changes, data workflows, release gates, browser verification, and parallel worktree delivery.

It has not been tested as a general policy for my primary job. An employer carries different security, privacy, legal, review, ownership, and incident obligations. The same task may deserve a lower autonomy level in that environment even when the agent capability is identical.

The human still owns:

  • product direction and ambiguous requirements;
  • taste, language, and user value;
  • credentials and changes to authority;
  • destructive or irreversible operations;
  • the acceptable blast radius;
  • final production approval.

Before increasing autonomy for any responsibility, ask:

  1. How quickly will I know the system is wrong?
  2. How cleanly can I undo the action?
  3. What evidence would prove the result is right?

If any answer is weak, the responsibility drops a level.

Current confidence is high for this bounded side-project setting. Confidence is deliberately lower for team and employer environments until their authority, review, security, and incident boundaries are encoded explicitly.

Final takeaway

The aim is not maximum autonomy. It is the highest autonomy you can defend with evidence, reversibility and retained accountability.

The human frames the outcome and owns the irreversible calls. Agents supply speed, breadth, and persistence. Tests, gates, reviewers, staging, and canaries make their claims expensive to fake.

Do not copy the autonomy level blindly. Copy the method: bound the responsibility, demand independent evidence, keep authority separate, and let every failure improve the system.

Reusable source · inspect before copying

The complete files, embedded here

Read the assumptions and failure paths in context. Copy only the parts that fit your own system and authority model.

Production review skillSKILL.md · markdown
---
name: review-production-change
description: Independently audit a production release for hidden correctness, data, dependency, environment, and rollout risks. Use after implementation and staging verification, especially when the change includes ETL output, caches, background workflows, or deployment ordering.
---

# Review Production Change

Act as an independent release reviewer. Treat the implementation summary, tests, and green staging result as evidence, not proof.

## Establish the release boundary

1. Identify the production base, proposed head, acceptance criteria, rollout plan, and rollback path.
2. Inspect the complete diff.
3. List application, schema, data, workflow, cache, configuration, deployment, test, and monitoring changes.
4. Separate experimental changes from the production change.
5. Stop and report if unrelated work cannot safely ride with the release.

## Build the risk model

Ask:

- What must exist before this code runs?
- What happens between two workflows?
- Can a later commit bypass a pending prerequisite?
- Does green evidence name the target environment?
- Can a cache preserve an empty result?
- Are totals computed at the grain claimed by the product?
- Can missing or censored data look like zero?

Prioritize temporal and lineage risks.

## Verify independently

1. Trace one visible value from source through parser, storage, query, cache, API, and UI.
2. Exercise the negative path for each new gate.
3. Confirm workflow ancestry when one deployment depends on another.
4. Run relevant tests and the production build.
5. Open staging and test the changed path.
6. Derive at least one oracle from source evidence or an external observation.

## Report findings

Use this format:

```text
[severity] Short finding title

Evidence:
- file, workflow, query, or visible behavior

Failure mode:
- concrete sequence that produces harm

Required correction:
- smallest change that closes the gap
```

Choose one decision: `blocked`, `ready for staging`, or `ready for production PR`.

Never merge or deploy because the review is green. Preserve the human approval boundary.
Release ancestry gaterelease-ancestry-gate.sh · shell
#!/usr/bin/env bash
set -euo pipefail

workflow_name="${1:-}"
required_sha="${2:-}"
release_sha="${3:-}"
branch_filter="${BRANCH_FILTER:-main}"
max_attempts="${MAX_ATTEMPTS:-120}"
poll_seconds="${POLL_SECONDS:-30}"

if [[ -z "$workflow_name" || -z "$required_sha" || -z "$release_sha" ]]; then
  echo "usage: release-ancestry-gate.sh <workflow> <required-sha> <release-sha>" >&2
  exit 2
fi

git cat-file -e "${required_sha}^{commit}"
git cat-file -e "${release_sha}^{commit}"

for ((attempt = 1; attempt <= max_attempts; attempt += 1)); do
  if ! successful_shas="$(gh run list \
    --workflow "$workflow_name" \
    --branch "$branch_filter" \
    --status success \
    --limit 100 \
    --json headSha \
    --jq '.[].headSha')"; then
    echo "Could not read workflow runs." >&2
    exit 1
  fi

  while IFS= read -r run_sha; do
    [[ -n "$run_sha" ]] || continue
    git cat-file -e "${run_sha}^{commit}" 2>/dev/null || continue

    if git merge-base --is-ancestor "$required_sha" "$run_sha" \
      && git merge-base --is-ancestor "$run_sha" "$release_sha"; then
      echo "$workflow_name succeeded at $run_sha and covers $required_sha."
      exit 0
    fi
  done <<< "$successful_shas"

  if [[ "$attempt" -lt "$max_attempts" ]]; then
    sleep "$poll_seconds"
  fi
done

echo "Timed out waiting for $workflow_name to cover $required_sha." >&2
exit 1
Gate testsrelease-ancestry-gate.test.sh · shell
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
subject="$script_dir/release-ancestry-gate.sh"
fixture_dir="$(mktemp -d)"
trap 'rm -rf -- "$fixture_dir"' EXIT

git -C "$fixture_dir" init -q
git -C "$fixture_dir" config user.email "test@example.com"
git -C "$fixture_dir" config user.name "Release Gate Test"

printf 'zero\n' > "$fixture_dir/value.txt"
git -C "$fixture_dir" add value.txt
git -C "$fixture_dir" commit -qm "before requirement"
before_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'one\n' > "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "required"
required_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'two\n' >> "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "pipeline"
pipeline_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

printf 'three\n' >> "$fixture_dir/value.txt"
git -C "$fixture_dir" commit -qam "release"
release_sha="$(git -C "$fixture_dir" rev-parse HEAD)"

fake_bin="$fixture_dir/bin"
mkdir -p "$fake_bin"
# The generated fake reads FAKE_SUCCESS_SHA when it runs, not while this fixture is created.
# shellcheck disable=SC2016
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$FAKE_SUCCESS_SHA"\n' > "$fake_bin/gh"
chmod +x "$fake_bin/gh"

run_gate() {
  (
    cd "$fixture_dir"
    PATH="$fake_bin:$PATH" \
      FAKE_SUCCESS_SHA="$1" \
      MAX_ATTEMPTS=1 \
      POLL_SECONDS=0 \
      bash "$subject" "Example Data Pipeline" "$required_sha" "$release_sha"
  )
}

run_gate "$required_sha"
run_gate "$pipeline_sha"

if run_gate "$before_sha" 2>/dev/null; then
  echo "expected a run before the requirement to fail" >&2
  exit 1
fi

echo "workflow ancestry tests passed"
Data contractdashboard-data-contract.mjs · javascript
export function parsePublishedCount(raw) {
  const token = String(raw ?? '').trim()

  if (/^<\d+$/.test(token)) {
    const upperExclusive = Number(token.slice(1))
    return {
      kind: 'censored',
      exact: null,
      lower: 0,
      upper: upperExclusive - 1,
      source: token,
    }
  }

  const exact = Number(token.replaceAll(',', ''))
  if (!Number.isSafeInteger(exact) || exact < 0) {
    throw new Error(`Invalid published count: ${JSON.stringify(token)}`)
  }

  return { kind: 'exact', exact, lower: exact, upper: exact, source: token }
}

export function assertCoarseTotalCoversDetail(coarse, detailedFloor) {
  if (coarse.kind === 'exact' && coarse.exact < detailedFloor) {
    throw new Error(
      `Coarse exact total ${coarse.exact} is below detailed exact-only floor ${detailedFloor}`
    )
  }
}

export function displayCount(count) {
  return count.kind === 'exact' ? count.exact.toLocaleString('en-AU') : count.source
}
Contract testsdashboard-data-contract.test.mjs · javascript
import assert from 'node:assert/strict'
import test from 'node:test'

import {
  assertCoarseTotalCoversDetail,
  displayCount,
  parsePublishedCount,
} from './dashboard-data-contract.mjs'

test('keeps a censored value as an interval', () => {
  assert.deepEqual(parsePublishedCount('<20'), {
    kind: 'censored',
    exact: null,
    lower: 0,
    upper: 19,
    source: '<20',
  })
})

test('rejects a coarse total below the detailed floor', () => {
  assert.throws(
    () => assertCoarseTotalCoversDetail(parsePublishedCount('99'), 100),
    /below detailed exact-only floor/
  )
})

test('renders the published semantics', () => {
  assert.equal(displayCount(parsePublishedCount('1,095')), '1,095')
  assert.equal(displayCount(parsePublishedCount('<20')), '<20')
})
Builder and reviewer promptsbuilder-and-reviewer-prompts.md · markdown
# Builder and reviewer role prompts

## Builder

Implement the scoped change and prove it on staging.

1. Restate the visible acceptance criteria.
2. Identify code, schema, data, workflow, cache, and environment dependencies.
3. Keep experiments outside the production diff.
4. Add focused tests before broad regression tests.
5. Run the production build.
6. Deploy to staging and test the path in a browser.
7. Record commands, URLs, workflow runs, and uncertainty.
8. Stop before production authority.

Leave evidence another reviewer can reproduce.

## Reviewer

Review this release independently from the builder.

1. Inspect the complete diff and acceptance criteria.
2. Reconstruct dependencies without relying on the builder's explanation.
3. Find a temporal sequence that tests may not cover.
4. Check whether an unrelated later commit can bypass a prerequisite.
5. Trace one visible value from source to UI.
6. Test the failure path of each new gate.
7. Verify the named staging environment in a browser.
8. Report findings by severity before the release decision.

Do not implement findings or approve a merge unless explicitly authorized.
Promotion state machineproduction-promotion.md · markdown
# Production promotion state machine

Move work through observable states. Do not mark a state complete from an agent summary alone.

## Scoped

- The issue names the visible problem and acceptance criteria.
- Experimental work is explicitly excluded.
- The production diff is known.

## Built

- Implementation and focused tests are complete.
- Data operations are idempotent.
- Rollback behavior is documented.

## Staged

- Required data and schema state exists in staging.
- Relevant tests and the production build pass.
- A browser walk verifies the deployed path.

## Independently reviewed

- A reviewer with fresh context inspects the complete diff.
- Negative paths and workflow ordering are tested.
- Findings are fixed and rechecked.

## Ready for production PR

- The release contains only approved changes.
- Dependency gates bind workflow success to release ancestry.
- CI is green. No merge has occurred.

## Approved

- A human owner explicitly authorizes rollout.
- The final diff and CI state have not changed since approval.

## Released

- The approved merge path is used.
- Read-only checks confirm the production target.

## Learned

- Each failure mode becomes a test, gate, skill, playbook, or monitor.
Optional appendix

Field notes behind the model

The page above contains the complete argument. These notes preserve the individual incidents and the order in which the model emerged.

27 June 2026 · Reframe the jobI stopped writing the code. I did not stop being the engineer.I ship more by typing almost none of it. The workflow, honestly: delegate the building, keep the deciding and the verifying, and treat every “done” as a claim until it is proven.7 min read29 June 2026 · Encode the controlsI built a check, and the first thing it caught was meI turned one rule I only enforced by discipline into a gate that fails the build. The first thing it caught was my own half-finished work.7 min read6 July 2026 · Orchestrate the workI ran the robot factory by hand firstA full manual dress-rehearsal of the L5 feedback-to-fix factory turned eight pages of handwritten notes into 24 findings, four tickets, and four parallel build lanes. It taught me what to automate before automating it.6 min read8 July 2026 · Bound the authorityThe machine that said no to my machineA data-seeding agent hit half-rotated keys, tried to fetch replacements, and a second AI refused twice. Why a denied request was the system working, and the guardrail I am glad I do not own.6 min read22 July 2026 · Prove the releaseThe reviewer rejected my fix twice. Both times it was right.Why passing tests and a good staging walk did not prove a data-dependent release was safe, and how an independent reviewer found two temporal bugs.4 min read23 July 2026 · Prove the releaseData before code is a graph problemModel a data-dependent deployment as a commit-ancestry invariant instead of a current-push checklist.3 min read25 July 2026 · Prove the releaseTwo screenshots became a failing data contractA debugging workflow that normalizes two conflicting claims, traces one value to source, and preserves the finding as an executable contract.4 min read26 July 2026 · Prove the releaseI stopped managing agents with memoryA founder-oriented state machine for moving agentic work from scope to production evidence without relying on remembered instructions.4 min read