Working unattended

How Konstruct runs a blueprint while you are somewhere else

An agent that reports its own progress is an agent grading its own homework.

Running a plan unattended is a different problem from helping someone write code. If I am sitting there, I am the check: I read what it did, I notice it skipped a task, I stop it. Walk away for an hour and every one of those checks goes away at once, and what is left is an agent whose only account of its work is its own.

That account is optimistic. Not maliciously, and not as a defect of any particular model — the bias is trained in. Models are tuned on human feedback, and human feedback favors the agreeable answer, which makes “yes, it’s done” exactly the shape of answer the tuning rewards. A model that has spent twenty turns on a phase and believes it is finished will say it is finished — and one that merely wants to be finished will often say it too. A system that treats saying so as being so marches on to the next phase carrying the mistake with it.

So a blueprint run in Konstruct is built around one rule: the executor never gets to decide it is done. Everything below is what falls out of taking that seriously — how the work is split up, what has to be true before a phase closes, and how the run tells stuck from slow.

One session per phase

The obvious way to execute a plan is one long conversation: seed an agent with the whole thing and let it work down the list. It falls apart on anything real. By phase four the context window is mostly a transcript of phases one through three, the early decisions are buried under debugging noise, and the model is spending its attention on history instead of the task in front of it.

A run is instead a chain of sessions, one per phase. When a phase turn settles, the orchestrator gates it, summarizes it by replaying that session, appends the summary to a ledger on the blueprint, retires the session, and opens a fresh one seeded with the summaries of everything that came before. The next phase starts with a clean context and a short account of the work so far, not the full recording of it.

A phase turn settles, is gated and summarized, its session is retired, and a fresh session is opened seeded with prior summaries. Phase 1 session executor turns Gate complete? Summarize replay session Phase 2 session fresh context summaries carried forward — transcripts are not retired
Every phase boundary retires a session and opens a new one. What crosses the boundary is a summary, not a transcript.

This is the same argument as batching reads, one level up: context spent on history is context not spent on the work. It also has a second effect that matters more for unattended running — because sessions are disposable, the run’s real state lives on the blueprint rather than inside a conversation, so it survives a session dying, a restart, or a phase being re-run.

Finishing is not the executor’s call

The executor has a phase_done tool, and it can mark its own tasks complete. Neither of those closes a phase. They are a request to close a phase, and the difference is the whole design.

Every settled phase turn runs the completion gate, whether or not phase_done was called, and whether or not every task is already self-marked complete. Only a passing verdict from the gate advances the run. Before the gate existed, nothing in the system was capable of contradicting the executor’s own accounting — if it said the phase was finished, the phase was finished, and the run moved on.

The completion gate: mechanical evidence and executable verification may only reject; the adjudicated read at the bottom is the only tier that can return complete. Executor: “phase done” a request, not a verdict Mechanical evidence files changed? touchpoints touched? may only reject Verification commands build, lint, targeted tests — failures escalate with attribution may only reject Adjudicated read fresh session, read-only, opens the changed files the only tier that may approve complete — the phase closes; accepted defects are named and carried forward
The cheap tiers run first and can only ever say no. The only route to yes is the adjudicated read at the bottom.

The only route to complete is an adjudicated read: an independent, read-only, tool-equipped turn in a fresh session with none of the executor’s history in its context. It opens the files that actually changed and accounts for each task individually. Closing requires both a verdict of complete and concrete evidence for every single task — evidence with a minimum length, so a bare “done” does not qualify. Anything short of that comes back as incomplete, naming the specific tasks still outstanding.

There is a middle verdict: complete with defects. The adjudicator can close a phase while naming specific problems it found — real, but not worth blocking the run for. Accepted defects are not forgiven, either: each one is written to the verdict ledger, seeded into every later phase so the work that follows knows what it is building on, and carried through to the final report.

It costs one bounded read-only turn against a phase that just spent many expensive ones, which is a rate I will take. And it is deliberately fail-closed: if the adjudicator cannot be reached, phases do not close. A gate that approves when its only real check is unavailable is not a gate.

The rung I deleted

The gate did not start out this strict. It had a cheap deterministic tier in the middle: if every task in a phase was self-marked completed, close the phase. No model turn, nothing read, nothing checked. It was fast and it was free and it was wrong.

It was wrong because tasks_complete meant, precisely, “the executor said so” — the exact thing the gate exists to not accept. And it was worse than a bad rule in the abstract, because it was the path most phases actually took. The adversarial tier I had built, the one doing the real work, was sitting there almost never running. The gate looked rigorous on paper and was rubber-stamping in practice.

So the fix was deleting a feature. That rung is gone, and its removal is still written into the source above the ladder, because a comment explaining why something is absent is the only thing that stops it being helpfully re-added six months later.

Cheap checks may only say no

Two tiers run before the adjudicator, and both are bound by the same rule: they can reject, they can never approve.

The first is mechanical and costs nothing — a deterministic cross-check of the executor’s claim against the tool calls of every session the phase used. Did any files actually change? Do the changes touch the files the plan flagged as relevant? A phase that claims completion while having changed nothing at all is contradicting itself, and that is worth knowing before spending anything on a model turn.

The second is executable. Where the plan’s author wired concrete pass/fail commands to a phase — a build, a lint, a targeted test run — those get run, through the same workspace daemon that contains everything else. Running the tests is strictly cheaper and strictly more trustworthy than asking a model to eyeball a transcript and form an opinion about whether the tests would pass.

A failing command is not taken at face value, either. Verification commands are usually repo-wide — a typecheck, a build — and a repo-wide command fails for repo-wide reasons, including a sibling package that was broken before the run ever started. Blaming the executor for that is a category error, and retrying cannot fix it: the retry runs the same command into the same unrelated breakage. So the gate pulls the failing file paths out of the command output, intersects them with the files the phase touched, and tells the adjudicator whether the failure implicates the phase’s own files or points at files it never went near. That check is an overlap, not a before-and-after baseline — it can tell “these failures are in files this phase never touched”, and nothing subtler than that.

No cheap check ends a phase on its own, though. A mechanical veto and a failing verification command both escalate to the adjudicator rather than terminating, because the cheap tiers are reasoning about metadata and exit codes, and the adjudicator is the only tier that can open the files and see what is there. Cheap checks are good at catching an obviously false claim. They are not good enough to be the last word.

Telling stuck apart from slow

A phase that fails the gate gets re-armed: the executor is sent back in with a nudge naming what is still outstanding. That nudge accumulates across attempts. Without that, attempt three has no idea attempt two was told the identical thing and failed to act on it, and you get an agent placidly reproducing the same no-op turn until the budget runs out. From the second retry the executor also gets a fresh session: a session that has failed the same phase twice is carrying the context of its own failure, so the next attempt starts clean, seeded with a digest of the earlier attempts instead of the transcript that produced them.

Retries are capped at three, but a bare counter is a blunt instrument: it treats a phase that is steadily shrinking its outstanding work exactly like one that is wedged, so consecutive verdicts are compared by their outstanding set — the specific tasks and defects still named. A shrinking or shifting set is progress, and it keeps going. Two verdicts in a row naming the identical set means the next attempt will reproduce the same failure, and the run escalates to stalled immediately instead of burning through the rest of its retry budget. Stalled is parked, not crashed: the run stops spending attempts, says which phase it gave up on, and waits for you to look at it and kick it again.

Consecutive gate verdicts are compared by outstanding set: a shrinking set re-arms, an identical set escalates to stalled. Verdict N−1 outstanding: a, b, c Verdict N outstanding: ? compare smaller or different progress — re-arm identical stuck — escalate now try again, up to 3 hand it back to me
Progress buys another attempt. Repeating yourself does not.

Watching all of this is a plan-keyed watchdog on a thirty-second tick, asking one question: is this run still moving? It catches two kinds of stuck. The quiet kind, where no session is running at all, is easy. The silent kind is the harder one — a session that claims to be running and has emitted nothing for five minutes. Checking whether a turn is running can never catch that, because a hung turn sits in exactly that state forever, so progress is measured from actual output instead.

Two details in there were paid for the hard way. The watchdog is keyed by plan rather than by session, because a session-keyed timer disarms itself at precisely the moment the run is most likely to break — mid-advance, when the old session is retired and the new one does not exist yet. And every orchestration turn carries a real deadline, because a wedged provider stream never settles, the finally never runs, and the guard it was holding stays held. That is how you end up with a watchdog that is alive, ticking, visibly active in the UI, and completely inert.

What’s waiting when you come back

The last phase closing does not end the run — it starts the report. That report is assembled in pieces, not in one enormous turn: a short pass per phase in order, then the changed files, then verification, then the summary and caveats. Each phase pass sees only that phase’s summary and its changed files, so no single turn ever has to hold the whole run in its head.

Every step is persisted as it completes, which is why the document fills in section by section while you watch instead of appearing all at once at the end. It also means a report interrupted halfway is a half-written report rather than nothing at all.

And every phase close is written to an audit trail on the run: which tier closed it, on what evidence, what the verdict said, and any defects it accepted along the way. When a run finishes and something is wrong anyway — which happens, because none of this makes the work correct, it only makes the accounting honest — the trail is what lets you find the phase where the story stopped matching the code.