Tell the planner what it was convened to fix

F66, found live on run 20. A replan reopens the plan phase and the session
it convenes was given the ordinary plan brief and an empty template: no
mention that a plan already exists, what contradicted it, which phase, what
evidence, or that its output supersedes an accepted plan. All of it was
already durable on PlanMismatchRecorded and none of it reached the agent,
so nothing stopped the replacement from sealing with the same
contradiction in it.

The contradiction is now projected onto the task and rendered above the
sealed artifacts, because it changes how they should be read. Its lifetime
is bounded at both ends: a sealed replacement answers it, and so does a
human reply to the stop it caused. Only the phases a mismatch can reopen
render it, since an implementer looks at the code rather than at an account
of the last contradiction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
2026-08-29 20:47:44 +04:00
parent 1330ad9943
commit edf00761fd
5 changed files with 131 additions and 0 deletions
+19
View File
@@ -312,6 +312,9 @@ func (s *Store) apply(e domain.Event) error {
}
t.PlanRef = p.ArtifactRef
t.PlanProgress = nil
// The replacement answers the contradiction. Carrying it
// further would put a solved problem in every later context.
t.PlanMismatch = nil
}
}
t.WorkPhase = p.Phase
@@ -487,6 +490,19 @@ func (s *Store) apply(e domain.Event) error {
t.Version = e.Version
s.replaceTask(e.TaskID, t)
return nil
case domain.EventPlanMismatchRecorded:
// Projected so the phase this reopens can be told what reopened it.
// The event is the record; this is the live instruction derived from
// it, and it stops being live as soon as a replacement seals or the
// stop is answered.
var m domain.PlanMismatch
if err := json.Unmarshal(e.Payload, &m); err != nil {
return err
}
t.PlanMismatch = &m
t.Version = e.Version
s.replaceTask(e.TaskID, t)
return nil
case domain.EventPlanPhaseVerified:
var pp domain.PlanPhaseRecord
if err := json.Unmarshal(e.Payload, &pp); err != nil {
@@ -596,6 +612,9 @@ func (s *Store) apply(e domain.Event) error {
if t.State != domain.StateBlocked {
t.DecisionRequest = nil
t.Blocker, t.BlockReason = "", ""
// The human's answer outranks the plan and stands as an ordinary
// decision, so the contradiction it settled is no longer live.
t.PlanMismatch = nil
}
if phase, ok := p["lifecycle_phase"].(string); ok && phase != "" {
t.LifecyclePhase = phase