Skip to content

feat(goal): a steer is a gated obligation, not advisory text - #535

Merged
ion-alpha-dev merged 2 commits into
mainfrom
feat/steer-gated-obligation
Aug 14, 2026
Merged

feat(goal): a steer is a gated obligation, not advisory text#535
ion-alpha-dev merged 2 commits into
mainfrom
feat/steer-gated-obligation

Conversation

@ion-alpha-dev

Copy link
Copy Markdown
Collaborator

What

An operator can now redirect a goal that is already running, and the redirect is an obligation rather than a suggestion: it lives on the goal's desired state, is delivered to the run on every turn it survives, and refuses the run's completion claim until the run has said what it did about it.

flynn steer <run-id> "<instruction>" is the operator surface. goal.Spec.Steers carries the redirects, goal.Status.Steers records what the run was given and the account that discharged it, goal.SteerJudge rules on the account, and evidence.ModelSteerJudge is the shipped judge.

Why

Steering that is only injected into the prompt is steering that gets ignored. The clearest record of it is a timeline rather than an argument: a human sent five messages in thirty minutes, each folded into the prompt as advisory prose, and the loop finished its build, passed its own review ten seconds after being asked to read the guidance, and committed thirty-eight seconds later. Every message arrived. None of them changed a decision, because nothing downstream of the prompt ever asked whether they had been addressed.

So the primitive is not "deliver the text", it is that the run cannot report success while an obligation is outstanding. Three rules are what make it one.

It is delivered on every turn it survives, not once. A message folded into the transcript when it arrived is one the pruner, the compactor and a reseed are all free to drop before the turn that finally claims completion, so it is rendered from the durable record each turn instead.

It is discharged by an account, and the account is judged. When the run claims it is finished, its own statement of what it did is held against every redirect outstanding, and a judge decides whether that statement addresses each one. A redirect the judge does not name stays outstanding: silence is a refusal, so a judge that answers half the question leaves the other half unpaid. A goal steered on a host with no judge wired stalls as SteerJudgeMissing and is released when one is wired, which is the same rule WithInvariantAudit follows.

It cannot be withdrawn by the run. Once a reconcile has taken a redirect on, dropping it or rewording it is refused as a terminal spec fault, for the reason an invariant's is: the one party who must not be able to edit an obligation is the party under it, and the spec is not a surface only the operator can reach. Issuing another is always allowed, which is how a wrong one is corrected.

A steer is deliberately not an amendment. Rewriting the objective changes what done means, and the grader then rules on the amended text, so the correction disappears into the definition of success. A redirect leaves the objective and the stop condition untouched and is checked separately.

How to verify

go test ./goal/... ./mission/... ./evidence/... ./cmd/flynn/ -run 'Steer|Redirect|Account|Acknowledg'

End to end: start a goal, flynn steer <run-id> "you are writing to sessions; write to events instead" from another terminal, and the next turn carries the redirect and the rule it will be held to. A run that finishes without saying what it did about it settles stalled as SteerUnaddressed, with the instruction and the run's own account of finishing both quoted in the message.

The tests carry the argument: the completion claim is refused while a redirect is unanswered (goal/steerrun_test.go), the redirect survives a turn whose transcript never carried it (mission/steer_test.go), an acceptance quoting nothing from the account discharges nothing (evidence/steerjudge_test.go), and a withdrawn or reworded redirect is refused at admission (goal/steer_test.go).

Notes for reviewers

The judgement is asked at the completion claim rather than after each step, which is the opposite placement to the invariant audit and comes from the same argument. A term of the run is checked before the stop evaluator so a breach cannot be traded against having finished; a redirect is checked at the claim because the claim is where the run states what it did, and that statement is the thing being judged.

A goal that reported completion and is steered afterwards is judged by the same rule against the account it already gave, so it settles saying the redirect was not addressed. flynn steer refuses a finished run up front rather than letting that happen, since holding a finished account against an instruction that did not exist when it was written stops a run for something nobody could have acted on.

Taking on a new redirect clears the non-convergence count. That count stops a run that is being told nothing new, and an operator redirect is the run being told something new by the party the count was standing in for.

Left out on purpose: the kill switch from the same piece of work (a stop that takes effect at tool dispatch rather than at a step boundary, with a self-test proving it engages). It shares nothing with this beyond the operator holding both, and it belongs in its own change.

An operator's redirect to a running goal now lives on the goal's desired
state, is delivered to the run on every turn it survives, and refuses the
run's completion claim until the run has said what it did about it.

Steering that only appends text to the prompt is steering that gets
ignored: the recorded case is five messages in thirty minutes, all
delivered, all ignored, and a commit thirty-eight seconds after the last
one. So the primitive is not delivery, it is that the run cannot report
success while an obligation is outstanding.

Spec.Steers carries the redirects and Status.Steers records what the run
was given and the account that discharged it. A redirect the run has been
handed cannot be dropped or reworded, the way an invariant cannot: the
party under an obligation is the one party who must not be able to edit
it. Issuing another is always allowed, which is how a wrong one is
corrected.

Discharge goes through goal.SteerJudge, asked at the completion claim
with the run's own account of finishing. A redirect the judge does not
name stays outstanding, and the goal settles un-done with the redirect
and the account quoted. evidence.ModelSteerJudge is the shipped judge: it
rules on one redirect at a time, refuses an acceptance that quotes
nothing from the account, and records each judgement on the run's spine.
A goal steered with no judge wired stalls as SteerJudgeMissing and is
released when one is wired.

`flynn steer <run-id> "<instruction>"` is the operator surface. It writes
to the durable record, so it redirects a run this process is not driving,
and it refuses a run that has already finished.

Signed-off-by: Ion Alpha <contact@ionalpha.io>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.52399% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cmd/flynn/steer.go 92.98% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

The paths a redirect takes when something around it fails were untested: a record that cannot be read or written, a judge that cannot be asked or recorded, a redirect set the reconciler would refuse at admission, and a finding about a redirect the status has not taken on yet.

Each of these decides whether an operator's instruction stays outstanding or is silently discharged, so each is now pinned by a test that says which way it must fall.

Signed-off-by: Ion Alpha <contact@ionalpha.io>
@ion-alpha-dev
ion-alpha-dev merged commit abff77e into main Aug 14, 2026
28 checks passed
@ion-alpha-dev
ion-alpha-dev deleted the feat/steer-gated-obligation branch August 14, 2026 23:28
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant