Hermann: A Twelve-Factor Developer That Lives in a Net

Yesterday a Petri net picked up a goal, proposed four ways to move it forward, waited for me to choose one, wrote the spec, hired a headless Claude Opus 5, measured the result against twelve factors, reviewed the diff against its own spec, and merged the pull request. The interesting part is not that an agent wrote code. It is that the process around the agent is itself a running net, and every step left a token behind.


The problem: a coding agent is not a development process

A headless coding agent is very good at the middle of the job. Point it at a repository, give it a task, and it will read the code, edit it, run the tests and hand you a diff. That part is solved, and it got solved fast.

The rest of the job is not. There is no record of why this task and not another one. The acceptance criteria lived in a prompt that is now gone. Nobody measured whether the result still respects the architecture you agreed on three weeks ago. The next session starts with an empty head, so the twentieth task is as uninformed as the first. And a person who wants to steer has to be sitting in the terminal at the exact moment the agent asks, or not at all.

Teams normally solve this with process: a backlog, a spec, a review, a pipeline, a release. That process lives in five systems which do not know about each other, and none of which can act on its own. So we bolt an agent onto the side of it and hope the humans keep the pieces aligned.

Hermann is the other way around. The process is a running program, the coding agent is one step inside it, and both live in the same runtime. This article is about how that is put together, what it produced, and what is still missing.


What an Agentic-Net is, in one minute

Agentic-Nets is a governed, event-sourced operating runtime for persistent, evolving processes. Concretely: a model is a set of places that hold JSON tokens, and transitions that consume tokens from their input places, do one unit of work, and emit tokens to their output places. That is the whole vocabulary. A transition can be a deterministic mapping, an HTTP call, a shell command dispatched to an executor, a single bounded model call, a full agent with a tool budget, or a link between nets.

The anatomy of a netPlaces hold the facts. Transitions do the work. Every arc runs place to transition, or transition to place.goalp-hermann-goalproposeone bounded model callquestionp-hermann-promptswrite specanswer contractspecp-hermann-specsone token{“specId”: “spec-002”, “status”: “draft”}placedurable, queryable statetransitionone unit of work, deployed on its owntokenone JSON fact, event-sourced

Two properties matter for everything that follows. First, every intermediate value is a token in a place, not a variable inside a process, so you can query the middle of a running workflow and see exactly what it knows. Second, every transition is deployed on its own: it has an inscription (a JSON document saying what it reads, what it does and where it writes), its own credentials, its own schedule and its own status. You can add one, stop one or replace one while the rest keeps running.

The consequence for an agent is the whole trick of this article. The agent is not the program. It is one transition. What it reads is a place. What it produces lands in a place. Whether it runs at all is a token somewhere else.


Hermann

Hermann is a persona spread over four nets in one session: setup, twelve-factor audit, iteration judgement and iteration build. Thirty-two lanes, ten scripts, one application on top. He owns one Spring Boot microservice at a time and builds it the twelve-factor way. He ships as a single package, so installing him is one call and he arrives with his nets, his scripts, his configuration and his application.

He is built on one rule about who is allowed to do what, and that rule is the reason the output can be trusted.

Who is allowed to do whatSCRIPTS MEASUREpython, on the executor hostcount tests, files, secondsscore the twelve factorsbuild, push, open the pull requestrender every brief that is readnever: decide what to buildnever: write prose about resultsMODELS JUDGEone shot, answer contract, no toolspropose the next iterationwrite the spec from your answerreview the diff against the specread a brief, never a repositorynever: produce a numbernever: touch the git hostTHE CODER WRITESheadless, inside the repositoryread, edit, run the buildonly what the approved spec saysleast-privilege tool listreports files, tests, notesnever: commit, push or mergenever: see a credentialEvery output of all three is a token in a place. Nothing important lives in a session.

Deterministic scripts measure. Every number in Hermann’s world (test counts, startup seconds, factor scores, diff stats, image tags) is produced by a Python script on the executor and written into a place. No model is ever asked to count anything, which removes the single most common way an agent quietly lies to you.

Bounded model calls judge. Four lanes make judgements: which iterations to propose, what the spec should say, whether the diff matches the spec, and what the daily note says. Each is a one-shot call with an answer contract, a required set of fields with types. An answer that misses the contract is routed to an error place instead of the output place, so a malformed judgement can never pass itself off as a result.

The headless coding agent writes code, inside an approved spec, in one repository, with a least-privilege tool list. It never commits, never pushes and never talks to the git host. The pipeline does that, after it has run the build itself.


One iteration, end to end

Every iteration is the same loop, and it has exactly three points where a person decides something. The rest runs on its own, and stops safely wherever a person is needed, because waiting is just a token sitting in a place.

One iteration: three human decisions, everything else on railsthe merge starts the next iterationgoalproposechoice, interview or goalquestionyou choosewrite speccriteria, API, migrationdraft specyou approvethe coderheadless, in the repopull requestverifyimage, container, probesevidencereviewdiff against the specverdictyou mergemergesquash, tag, re-auditmain, taggedWAITING IS FREEA question with no answer is justa token sitting in a place. Nothingholds a lease, nothing times out,nothing burns budget overnight.a person decidesthree per iteration: choose, approve, mergethe net runsscripts measure, contracts gate, evidence accumulates

Walk it once. A trigger lands, either because you asked or because the previous merge started the next round. A script renders one brief: the goal, the accepted architecture decisions, the current state of the service, the last scorecard, the open risks, the specs already merged and the twelve factor cards. The model never reads the repository. It reads a document that a script wrote from measured facts.

The propose lane then answers in one of three modes, and it picks the mode itself:

  • Choice. Three or four concrete next iterations, each small enough for one pull request, each with effort, risk and the factors it touches, exactly one marked as recommended. It prefers a failing factor, then a blocking risk, then an upgrade, then the most valuable missing capability of the goal.
  • Interview. At most three precise questions, used when the next step depends on something only you know: a domain rule, an integration, a non-functional requirement.
  • Goal definition. Used when the goal is still a placeholder. It asks for the goal in one line plus a paragraph, and offers three example goal statements to adapt.
Hermann's Next iteration tab showing four proposed increments with effort, risk and factor chips
The first human decision. Hermann has read the goal, the scorecard and everything merged so far, and proposes four next steps in dependency order. One is marked recommended. The free-text box beside it is not decoration: you can answer with your own option, and the spec will be written from your words instead.

You answer. A second script renders a second brief from your answer, and the spec lane writes the spec: user story, acceptance criteria as Given/When/Then statements, the API shape, the data change with its migration file name, the configuration as environment variables with defaults, the tests that prove each criterion, the factors touched, and an explicit out-of-scope list. You approve it, optionally with notes that go straight into the coder’s brief.

Then the coder lane spawns the headless agent on a fresh branch. When it stops, the pipeline runs the authoritative build, commits, pushes and opens the pull request. A separate lane proves that pull request in a clean worktree: build and tests, a container image tagged by the commit, the image started against a real Postgres on a private network, startup measured until the health endpoint answers, liveness and readiness probed, a SIGTERM sent and the shutdown timed, and a check that stdout is structured JSON. Only then does the review lane see a bounded diff and check it against the acceptance criteria. You merge, or send it back with notes and let the coder continue on the same branch.

The application on top of these nets has six sections: Setup, Goal and Architecture, Next iteration, Work, Quality and Journal. The important thing about it is what it is not. It is not a separate system with its own database. Every button writes a token into a place the nets already read, every panel is a query over a place, and an MCP client sees exactly the same stores and the same actions. The web page is a view, not the truth.


The twelve factors are twelve lanes

The twelve-factor methodology is the closest thing our industry has to a shared definition of a well-behaved service. It is also mostly enforced by memory and code review, which means it is enforced unevenly. Hermann turns each factor into three artefacts: a card he reads when he writes a spec and briefs the coder, a check lane that scores the repository from 0 to 3 with the evidence it used, and a fix recipe that the proposal lane can pick up.

One audit: one digest, twelve lanes, one scorecarddigestone commitI CodebaseII DependenciesIII ConfigIV Backing servicesV Build, release, runVI ProcessesVII Port bindingVIII ConcurrencyIX DisposabilityX Dev/prod parityXI LogsXII Admin processesreportsscore plus evidencescorecardgrade36 / 36 grade AEach lane reads the same digest, scores its own factor from 0 to 3, and files the paths and lines it used. A failing factor becomes a proposal.

Here is what the twelve lanes actually look at. None of them asks a model anything.

FactorWhat the lane checks in a Spring Boot repository
I CodebaseA git checkout with exactly one remote on the git host, a main branch, no nested repositories
II DependenciesMaven wrapper committed, no SNAPSHOT versions, dependency:analyze reports nothing used but undeclared
III ConfigNo literal credentials or hostnames in main resources, datasource URL bound to an environment variable
IV Backing servicesEvery backing-service setting environment-bound, a compose file present, no embedded database at runtime
V Build, release, runMulti-stage Dockerfile, no build tool in the run stage, an image tagged by the commit it was built from
VI ProcessesNo HTTP session state, no local file writes, no static mutable collections
VII Port bindingserver.port from the environment, jar packaging, the port declared in the Dockerfile
VIII ConcurrencyNo unlocked @Scheduled, no in-memory cache manager, JVM memory sized relative to the container
IX DisposabilityGraceful shutdown with a timeout, exec-form entrypoint so SIGTERM reaches the JVM, measured startup
X Dev/prod parityCompose runs the production engine, tests use Testcontainers, no embedded database in tests
XI LogsNo file appenders, structured console format, no System.out logging
XII Admin processesA migration tool, versioned migration files, a one-off admin runner keyed by the environment
The Quality tab showing a twelve-factor scorecard of 36 out of 36 with per-factor evidence
The scorecard after the second merge. Each row carries the evidence the lane used, so the score is checkable rather than assertable. On the right, the twelve cards the model reads when it writes a spec, which is why new code tends to arrive compliant instead of being corrected afterwards.

Two honest caveats. The score is a heuristic, not a certification: it reads the POM, the resources, the Dockerfile, the compose file, the logging setup and the Java sources with targeted checks, and a determined developer can satisfy it without being twelve-factor. And a perfect score on a young repository is easy. The value is not the number. The value is that the number moves, carries evidence, and turns a regression into a proposal on the next iteration instead of a comment in a review three weeks later.


The coding agent is a token, not a call

The first version of the coder lane called Claude Code directly, with the binary name and the flags baked into a Python script. That is wrong for the same reason a hardcoded database URL is wrong: it makes a deployment decision into a code change, and it makes everyone who installs Hermann inherit my choice of vendor.

So the agent definition moved into the net, into a place that sits right next to the lane that uses it. One token per headless agent, and the token is the command template:

{
  "agentId": "claude-code",
  "title": "Claude Code (headless)",
  "binary": "claude",
  "command": ["claude", "-p", "--model", "${model}",
              "--allowedTools", "${allowedTools}",
              "--max-turns", "${maxTurns}",
              "--no-session-persistence", "--output-format", "json"],
  "promptVia": "stdin",
  "resultFormat": "claude-json",
  "defaultModel": "claude-opus-5",
  "models": ["claude-opus-5", "claude-sonnet-5"],
  "allowedTools": "Read,Grep,Glob,Edit,Write,MultiEdit,Bash(./mvnw:*),Bash(git status:*)",
  "maxTurns": "80",
  "timeoutMin": "45"
}

Whoever installs Hermann edits those tokens. Whoever operates him picks the agent and the model in the application, and the choice is a token write like any other. A Codex CLI definition ships next to the Claude one. Adding a third is a token, not a release. The prompt always travels on stdin, because a quoted argument can lose its quotes on the way through an executor and leave you with an agent running with no instructions at all.

The Setup tab showing the coding agent picker with Claude Code selected, its command template and claude-opus-5 as the model
The coding agent is configuration, not code. The card shows the resolved command line, the models the definition offers, and the overrides for model, turn limit, timeout and tool list. Everything here is a token in the build net.
The coder lane resolves its agent from the netapproved specconfigcoder definitionsthe coder lanecommand transitionspawnsEXECUTOR HOSTcwd ~/hermann/agentic-net-notifyclaude -p –model claude-opus-5 \ –allowedTools Read,Edit,Bash(./mvnw:*) \ –max-turns 80 –output-format json < the brief on stdinthe brief: goal, principles, decisions,the approved spec, the twelve cardsthe lane files the result as a tokenrun + pull requestagent and model recordedSwap the definition token and the next iteration is built by a different agent. Nothing else in the net changes.

Every run records which agent and which model produced it, so the history says who wrote what. When a future model is better or cheaper, the migration is a dropdown, and the comparison is honest because the spec, the verification and the review stay identical on both sides.


What it built, measured

Hermann built a throwaway service first, to prove the loop end to end. Then I pointed him at something real: a component of Agentic-Nets itself. The goal I gave him, in his own Goal tab:

A notification hub for Agentic-Nets. Operators register subscriptions: a source (a model, a place, an optional query filter, a poll interval) and a channel (a webhook URL with a shared secret). A poller reads new tokens through the gateway, renders a message, delivers it with retries and idempotent delivery records, and exposes a REST API to manage subscriptions and inspect deliveries. It exists so that a proposal waiting in an inbox, an approval request or an interview question reaches a person without anyone watching a screen.

That is a deliberately self-serving choice. The thing Hermann most obviously lacks is a way to tell me that he is waiting for me, and it is small enough to slice, and it exercises the factors that actually bite: a backing service, stateless workers, idempotent retries, one-off admin processes.

Two services, three merged pull requests, all numbers measured by the pipeline rather than reported by a model:

sample-service, spec-001agentic-net-notify, spec-002
ScopeCreate and read orders via RESTSubscription resource with a secure webhook channel
CoderClaude Code, default modelClaude Code, claude-opus-5
Diff13 files, 381 insertions28 files, 1342 insertions
Tests6 green18 green
Agent time556 s, 62 turns, $1.70815 s, 48 turns, $4.73
Verificationpass: startup 3.1 s, shutdown 0.3 s, JSON logspass: startup 3.1 s, shutdown 0.2 s, JSON logs
Review verdictapprove, no pointsapprove, no points
Scorecard after merge36 of 36, grade A36 of 36, grade A
The Work tab showing two merged runs with branch, pull request, build, coder cost, verification and review
The Work tab. Each card is one run: the branch, the pull request, the build the pipeline ran, which agent and model did the work and what it cost, the measured verification, and the review verdict. This is the audit trail, and it is also the user interface.

The shape of the time is worth a picture, because it is not what people assume. The model calls are cheap and fast. The coding agent is the whole cost, and everything else is rounding.

Where the time goes in one iterationspec-002, measured on one laptop, from the iteration trigger to the merged tagpropose the options20 swrite the spec40 sthe coding agent815 s, 48 turns, $4.73build, image, container, probes90 sreview the diff15 smerge, tag, re-audit25 sThe three human decisions are not on this chart. They take exactly as long as you take, and nothing is burning while you think.

One detail I did not expect. The coder wrote its own field notes into the run record, and they were useful: that Spring Boot 4.1 moved to Jackson 3 and repackaged several test-slice annotations, that it chose three environment variable names the spec had left unspecified, and that it added one validation rule the spec did not mention. That last line is exactly what a reviewer needs to see, and it survived because the run is a token, not a scrollback buffer.


Why this shape holds

The state of the work is not in a chat log. The goal, the accepted decisions, every spec, every run, every verification, every verdict and every scorecard is a token in a place. Close the laptop, come back next week, and the next brief is built from the same facts. There is no context to re-establish because the context is the model, in the database sense of the word.

The human decision points are few, and they are the right ones. Choose the work, approve the contract, accept the result. Those are the three moments where judgement is expensive and irreversible. Everything between them is either measurement or bounded generation, and both are cheap to redo.

Measurement and judgement are separated by construction. A model that cannot count also cannot inflate. When the review says the tests pass, it is reading a number a script produced from a surefire report, not remembering what it saw scroll past.

Every part is replaceable while it runs. The coding agent is a token. The factor checks are ten Python files. The proposal prompt is an inscription you can edit in the Studio and see take effect on the next fire. Nothing here is a framework you have to fork.

An agent in a terminalA CI pipelineHermann
Decides what to build nextYou, in the promptNothing, it only reactsProposes, you choose
Remembers past decisionsUntil the session endsNoTokens in places, indefinitely
Acceptance criteriaIn the prompt, then goneWhatever the tests assertAn approved spec, kept in the repo and the net
Measures quality attributesIf you ask nicelyWhat you scriptedTwelve lanes with evidence, every commit
Runs while you sleepUntil it needs youYes, on a triggerYes, and it parks on a token when it needs you
Swap the model or vendorNew command lineNot applicableA token edit, history keeps both

What is not true yet

An article that only lists wins is marketing, so here is the other column.

  • Hermann reviews his own work. The reviewer is a different lane with a different prompt and a measured diff, but it is the same family of model. A second opinion from a different vendor is one token away and I have not proven it is better. Treat the review as a checklist that catches sloppiness, not as an independent auditor.
  • Three merged pull requests is not a track record. Two of them were on a throwaway service. The interesting failure modes (a spec that is wrong, a coder that gets stuck, a regression that the factor lanes do not catch) have not happened yet in front of me.
  • The security net and the version-currency net are designed, not built. Dependency, code and secret scanning plus a threat pass per spec, and a weekly check of the Spring and Java lines, are the next two nets. The scanners are installed on the host and the design says a blocking finding refuses a merge. That is a plan, not a proof.
  • The Codex definition is untested. It ships as a second option, but that binary is not on my executor host, so it has never run. The application shows the binary as missing, which is at least honest.
  • It costs real money. Two features, $6.43 of agent time. That is cheap against a developer hour and expensive against nothing, and it scales with how much you let it retry.
  • A person is still the only reviewer who can say “this is not what I meant”. The whole design assumes you show up three times per iteration. If you rubber-stamp, you get a fast machine for producing plausible code, which is exactly the failure mode this was built to avoid.

Try it

Agentic-Nets runs as a desktop install (one download, no Docker required for the runtime itself) or as a Docker Compose stack. Hermann needs a Docker host for his git server and the verification containers, plus a JDK, Maven and one headless coding agent on the machine that runs the executor.

# the runtime
git clone https://github.com/alexejsailer/agentic-nets
cd agentic-nets/deployment && cp .env.template .env
docker compose -f docker-compose.hub-only.yml up -d

# a package is one artifact: nets, scripts, seeds, application
node capabilities/tools/pack.mjs package --dir hermann
node capabilities/tools/pack.mjs publish --dir hermann

# install it into a fresh model, then open Studio and pick Applications
curl -X POST "$GATEWAY/api/hub/install" -H "Authorization: Bearer $TOKEN" \
  -d '{"source":"local","name":"hermann","version":"0.1.0","targetModelId":"hermann"}'

Then: check the infrastructure, provision the git host, name your service, define the goal, and press the button that asks Hermann what to do first. He will tell you the goal is missing before he tells you anything else, which is the correct order.


Hermann was designed and built in a single working day on one laptop, using Claude Code inside Agentic-Nets, and the service he is now building is a component of Agentic-Nets itself. Every number in this article was measured by his own pipeline and read back out of the runtime, not estimated.

Related reading: Agentic-Nets: The First Multi-Layer Agent Runtime With Live Applications on Top for the runtime and the application layer, and Ten Reasons I Run Agentic-Nets Instead of n8n for how this differs from workflow automation.

Leave a Reply

Your email address will not be published. Required fields are marked *