Agentflow is the scheduler I use to run Codex, Claude Code and GitHub Copilot workers against one durable work graph. A controller session plans the work, launches workers, reviews what they return and records each result in Beads. Earlier posts covered the original scheduler, its durable task state in Beads and the persistent controller. Agentflow 0.0.12 adds controls for the failures that a real delivery exposed.
The delivery hardened the release chain for my Pac-Man demo application for Kubernetes. That chain builds the application, publishes a Helm chart, updates a demo deployment and publishes a site. Every agreed outcome was accepted in the end. The work still took nearly six days of calendar time, and the workflow’s own records were wrong when it finished.
The issue
The problems were in what the workflow reported about itself.
The workflow reported open work as finished
After the root task closed, a later audit found a CI gate that was still open. The gate belonged to an older commit whose checks had failed, and a newer commit had since been accepted in its place. The status command still reported ok: true. It only compared claimed tasks with live worker sessions. A missing parent or an empty task listing gave the same answer as a finished workflow.
Agentflow had no supported way to record that the newer accepted result replaced the failed one. The usual workaround was to edit task metadata by hand, which could overwrite the evidence of the original failure.
A passed CI gate was reported as a failure
The CI watcher closed a gate after its checks passed. The close was written, but the response timed out. The watcher reported a failure without reading the gate back, and a second run was rejected because the gate was already closed.
Merge commits had a separate gap. The watcher could follow checks on a pull request head. It could not confirm that the expected workflows had run on one exact merge commit on main.
The launch budget kept growing
Agentflow calculated the launch budget from the number of tasks in the graph. A small review finding often produced another fix task, two reviews and a CI gate, so each correction raised the cap. Every increase was small and looked reasonable when it was approved. The total ended at more than four times the budget approved at the start. No single step asked for a forecast of the whole delivery.
Recovery of a failed tooling launch stayed on the critical path through repeated repair cycles. More than 20 calendar hours passed before the work moved to the fallback that finally delivered it.
Large review packages and silent reviewers
Nothing measured what an external reviewer received. One review package held nearly 600 files and 4.8 MB, and a file could appear in it more than once. That reviewer used up its credit allowance and finished without a verdict.
A reviewer that exited cleanly also looked the same as one that approved the change. The result contract had no verdict field, so a missing or stale review could pass as approval.
Usage records had a related gap. They mixed configured allowances, provider-reported usage and local estimates, so an overshoot could be calculated from an estimate.
Passing evidence did not prove the system ran
The repository settings API reported that GitHub Actions was enabled on a fork, but no workflow ran there. Component tests passed while the release chain broke where its stages joined. A dispatch payload carried more fields than the documented boundary allowed. A later fix left a checkout step reading an old field.
An acceptance row could be marked passed with any evidence string. A goal that needed proof from the hosted environment could therefore close on a settings response.
Native subagent work had no checked way back
When a managed worker’s provider login expired, its launch could not be recovered. Native subagents running in the provider’s own session finished the remaining work, and the tasks were closed by hand. Nothing checked that the returned evidence belonged to the task, the current claim or the approved base commit. Status listed those tasks as claims without a session. It could not tell a worker that had been seen a minute ago from one that had never been observed.
The cause
Apart from budget growth and package size, these failures had one cause. A fact was inferred from a signal that did not contain it:
- An empty or partial listing was read as “no open work”.
- A clean exit was read as approval.
- A timeout was read as a failure.
- A settings response was read as “the workflow runs”.
- A missing session record could pass for “the worker has stopped”.
- An estimate could stand in for actual usage.
Agentflow treated each signal as an answer when it needed to report “unknown” and stop.
Four design gaps made the effect worse.
Limits came from current state. The launch cap was recalculated from the graph, so anything that grew the graph also raised the limit.
Evidence had no provenance. An acceptance row stored a status and a free-text string. It did not record whether the evidence came from a static check, a local run or the hosted environment. It also did not record whether the evidence covered one component or the whole chain. Validation could only confirm that some evidence existed.
Steps that changed state were not safe to repeat. Closing a CI gate writes state. If the response is lost, the caller must read the state back and accept an exact replay of the same operation. Without both, a retry fails on the state that the first attempt already wrote.
Authority followed the actor’s name instead of the specific claim. When the same actor reclaimed a task, a launch from the earlier claim could still return results.
Some of the delay was not caused by the tool. The controller session kept the tooling recovery on the critical path for too long and split related fixes into many small correction cycles. Agentflow did not make those decisions, but it gave no signal when their cumulative cost grew.
The fix
Agentflow 0.0.12 adds a control for each gap. The budget and package controls set limits that the work cannot move on its own. The others follow one rule. When a signal is missing, partial or from the wrong source, report it as unknown and block the step that depends on it.
Audit completion from one complete listing
agentflow controller status --root . --workflow-root <root-id> --json now includes a completion_audit. The audit reads every task under the root, including CI gates, from one complete listing. Open work under a closed parent, or a CI gate without an accepted passing result, keeps the root incomplete. A failed, truncated or inconsistent listing marks the audit as unavailable, so a listing error cannot look like a finished root.
When a newer commit passes the checks that an older commit failed, record the replacement instead of editing the failed gate:
agentflow ci supersede --bead <failed-gate> --successor <passed-gate> \
--workflow-root <root-id> --reason "<why>" --decision-ref <decision>
The original failure stays in the record, and the root can complete because a later result was accepted in its place. When a closed root passes the audit, agentflow controller rotate writes a terminal handoff packet with nothing left to continue. A new session then has no stale next steps to resume.
To build this elsewhere, traverse parent relationships recursively. Do not filter by an ID prefix or a ready queue. For each task, report the original result, any administrative disposition such as “superseded”, and whether the task still blocks delivery. Treat a listing without the root, or with a task whose parent is missing, as unavailable. A small version looks like this:
def audit_root(root_id, list_tree):
try:
tasks = list_tree(root_id) # one complete read: the root and all descendants
except ListingError:
return {"status": "unavailable"}
ids = {t.id for t in tasks}
children = [t for t in tasks if t.id != root_id]
if root_id not in ids or any(t.parent not in ids for t in children):
return {"status": "unavailable"} # an empty or partial listing proves nothing
blocking = [
t for t in children
if t.status != "closed"
or (t.kind == "ci-gate" and t.result != "passed" and not t.superseded_by)
]
return {"status": "complete" if not blocking else "incomplete",
"blocking": [t.id for t in blocking]}
Make CI observation repeatable and read-only
After a lost acknowledgement, the CI watcher now makes one bounded read-back of the exact gate. It matches the repository, the pull request and the immutable head commit against a stored check receipt. An exact replay of a passed and closed gate returns exit 0. Pending, malformed, mismatched or unavailable evidence returns exit 2. Repeated observations reuse an existing failure-triage task and never rerun a workflow.
For merge commits, agentflow ci watch-commit observes an explicit set of workflows on one full commit SHA:
agentflow ci watch-commit --repo <owner>/<repo> --sha <full-commit-sha> \
--workflow-id <workflow-id> --workflow-id <workflow-id> --once
The workflow set is required, so a workflow that never started is reported as missing instead of being ignored. The command reports missing, pending, failed, duplicate, drifted, unavailable and deadline states. It is read-only. Closing a CI gate or rerunning a workflow is a separate step, even after a passing observation.
To build this elsewhere, keep observation and action in separate steps. The observer takes an exact commit and an expected set, and returns a state for each item. A separate step decides whether to close, retry or escalate, and that step must accept an exact replay of its own last write.
Freeze the launch budget at approval
Run these when the owner approves execution:
agentflow budget freeze --workflow-root <root-id>
agentflow budget status --workflow-root <root-id>
The freeze records the number of launchable tasks and the launch cap. With the default multiplier of 2, a plan with 12 launchable tasks gets a cap of 24 launches. Graph growth, retries, correction tasks, fallback and context rotation cannot raise or reset the cap. The cap counts this root’s managed launch records only. When launches reach the cap, dispatch stops until an increase is appended:
agentflow budget revise --workflow-root <root-id> --new-cap 32 \
--decision-ref <decision> --reason "<why the plan changed>"
Each revision keeps the earlier caps and the consumption at the time of the revision. The budget status command reports cumulative launches, active concurrency, open tasks, the remaining serial path and external waits as separate values.
A task can also name its approach in its launch metadata. Each failure of that approach is recorded with agentflow budget approach --outcome failure. After two failures, every further launch that uses the approach is blocked until an owner records --outcome decision. The block also applies to replacement tasks.
Usage records now keep three values apart:
agentflow usage record copilot --source dashboard \
--allowance-credits 30 --actual-credits 42.5 --estimated-credits 25
--actual-credits requires a provider-authoritative source, such as the provider dashboard or its usage command. Overshoot is calculated from actual usage only.
To build this elsewhere, store the approved baseline once and append each revision to a history. Do not derive a limit from the current plan. Count failures by approach as well as by task, because a replacement task can repeat the same idea.
Measure what a reviewer receives and require a verdict
agentflow handoff preflight now measures the exact package that an external worker receives. That package includes the handoff, its manifest, the declared context files and the pinned skill files. Preflight reports the file count, total bytes, duplicate bytes and a package digest. Owners declare byte limits for each task class and provider:
{
"package_limits": {
"focused-review/claude": 120000
}
}
Agentflow ships no default limit. Without a declared limit, preflight reports the package as uncalibrated. A package over its limit fails preflight. On a manual preflight, an owner can approve one exact oversized package by its digest:
agentflow handoff preflight handoff.md \
--package-exception <package-digest> --decision-ref <decision>
The exception matches one digest. A changed package has a different digest and needs a new decision. Controller dispatch rebuilds the handoff, so it has no exception path. An oversized dispatch must shrink its context, or the owner must raise the declared limit.
External review handoffs must now return a verdict of approve or changes-requested. The verdict is bound to the reviewed base commit and the exact handoff. Agentflow rejects a missing, malformed, stale or mismatched verdict and reports the review as unavailable. A clean exit is never treated as approval.
To build this elsewhere, hash the complete outbound package, including duplicate files. Compare its size with a limit taken from your own measurements. Make the verdict a required field in the result schema. Check that it names the commit and the request it reviewed.
Record where acceptance evidence came from
Each acceptance row names the lane that can prove it. The lanes are static, rendered, local-runtime, architecture-specific, hosted-runtime, manual-ui and approved-untested. Rows for the hosted-runtime and architecture-specific lanes now pass only when the evidence names the same lane:
agentflow acceptance set acceptance.json --id <row-id> --status passed \
--evidence "<workflow-run-url>" \
--evidence-lane hosted-runtime --evidence-scope whole-chain
Mark a row that needs end-to-end proof with acceptance create --whole-chain <row-id>. That row passes only with --evidence-scope whole-chain. New acceptance matrices also record their evidence boundary, so validation, root completion and native acceptance all reject a hand-edited pass from the wrong lane.
The lane changes what a pass means. A settings API response shows configuration, and a component test receipt covers one component. Neither shows that the hosted system runs from end to end.
To build this elsewhere, add two fields to each acceptance result: the class of evidence and its scope. Reject a pass when either is weaker than the row requires. Before a live cutover, rehearse the complete chain through its production entry points and inject failures between the stages.
Accept native subagent work through a separate lane
A native subagent runs inside the provider’s own session, so Agentflow cannot authenticate its result the way it does for a managed worker. Version 0.0.12 adds a separate lane that the controller observes:
agentflow native approve --workflow-root <root-id> --task <task-id> \
--provider codex --base <base-sha> \
--approval-ref <decision> --approved-by <owner>
agentflow native observe --workflow-root <root-id> --task <task-id> \
--provider codex --session-id <session-id> --state present --observer <controller>
agentflow native accept --workflow-root <root-id> --task <task-id> --result result.json
Approval requires an active claim and an acceptance matrix, and the approver cannot be the actor that holds the claim. Each observation is bound to the current claim generation.
Acceptance checks the result against the approved base commit, the claim generation and the acceptance rows, and records the reported head commit. It also requires the rows to have passed controller validation. It then records a receipt and closes the task. Replaying the same acceptance has no further effect. The lane refuses tasks that require publication or credential use.
controller status now reports native visibility as confirmed_native, stale after 300 seconds, absent or unobservable. A missing record alone produces unobservable. Only an exact-session query can report absent.
Task authority is now bound to the claim generation as well as the actor. When a task is reclaimed, even by the same actor, launches and results from the earlier claim are fenced off. Controller resumes follow the same idea. An authenticated controller resume rotates the controller’s fencing epoch, so authority held under the previous epoch stops working. Agentflow carries active launches and budgets into the new epoch and keeps their original launch history. It rejects a resume from a different controller, a future or malformed epoch, and a changed claim.
To build this elsewhere, give every claim a generation number and require each result to carry it. Reject a result whose generation does not match the current claim, and keep approval and execution with different actors.
Install Agentflow 0.0.12
Agentflow 0.0.12 is a public preview. Install the tagged release in an isolated tool environment:
uv tool install "git+https://github.com/saintdle/agentflow.git@v0.0.12"
Then check the command and its local dependencies:
agentflow --version
agentflow doctor
External review handoffs created with an earlier version do not request a verdict, so regenerate them after you upgrade. Acceptance matrices from earlier versions continue to validate without the new evidence boundary.
The release notes list every change, and most entries link to the public issue behind them. The workflow guide and the configuration guide describe each command and setting in more detail.