From d7e75e9e313b5d05e6bdf0180235d7cf01e27007 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 28 Aug 2026 15:55:29 +0400 Subject: [PATCH] Render a pending manual gate as stale when the tree has moved f4dbcf7 taught the verified branch about staleness and left the pending one. Run 10 showed a successor "automated checks passed at 94bd45c3b5d6" while HEAD was 7d04aef: the automated half of that phase was established against code that had since changed, and nothing said so. A human about to confirm the manual steps of a phase deserves to know the automated half no longer describes the tree. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1 --- internal/agentctx/agentctx.go | 6 ++++++ internal/agentctx/agentctx_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/internal/agentctx/agentctx.go b/internal/agentctx/agentctx.go index f1555e2..cef93af 100644 --- a/internal/agentctx/agentctx.go +++ b/internal/agentctx/agentctx.go @@ -459,6 +459,12 @@ func renderPlanProgress(in Input) string { fmt.Fprintf(&b, "- %s (%s): verified at %s, stale because the tree is now at %s\n", phase.ID, collapse(phase.Name), short(rec.AtSHA), short(in.Git.HeadSHA)) case rec.Status == domain.PlanPhaseVerified: fmt.Fprintf(&b, "- %s (%s): verified at %s\n", phase.ID, collapse(phase.Name), short(rec.AtSHA)) + case rec.Status == domain.PlanPhaseAwaitingManual && rec.Stale(in.Git.HeadSHA): + // A pending manual gate goes stale for the same reason a verified + // phase does. Run 10 rendered "automated checks passed at + // 94bd45c3b5d6" against a tree that had moved to 7d04aef, because + // only the verified branch consulted Stale. + fmt.Fprintf(&b, "- %s (%s): automated checks passed at %s, stale because the tree is now at %s, waiting for the human to confirm the manual steps\n", phase.ID, collapse(phase.Name), short(rec.AtSHA), short(in.Git.HeadSHA)) case rec.Status == domain.PlanPhaseAwaitingManual: fmt.Fprintf(&b, "- %s (%s): automated checks passed at %s, waiting for the human to confirm the manual steps\n", phase.ID, collapse(phase.Name), short(rec.AtSHA)) default: diff --git a/internal/agentctx/agentctx_test.go b/internal/agentctx/agentctx_test.go index 53579f4..ac887d9 100644 --- a/internal/agentctx/agentctx_test.go +++ b/internal/agentctx/agentctx_test.go @@ -707,3 +707,30 @@ func TestResearchBriefStatesTheFindingIDRule(t *testing.T) { } } } + +// A pending manual gate goes stale for the same reason a verified phase does. +// Run 10 rendered "automated checks passed at 94bd45c3b5d6" against a tree +// that had moved, because only the verified branch consulted Stale. +func TestPendingManualGateRendersStale(t *testing.T) { + plan := &workphase.PlanDoc{Phases: []workphase.PlanPhase{{ID: "phase-1", Name: "one"}}} + task := domain.Task{ + ID: "t1", PlanRef: "ref-a", + PlanProgress: &domain.PlanProgress{PlanRef: "ref-a", Phases: []domain.PlanPhaseRecord{ + {PlanRef: "ref-a", PhaseID: "phase-1", Status: domain.PlanPhaseAwaitingManual, AtSHA: strings.Repeat("a", 40)}, + }}, + } + got := renderPlanProgress(Input{ + Task: task, Phase: domain.WorkPhaseImplement, Plan: plan, + Git: GitState{HeadSHA: strings.Repeat("b", 40)}, + }) + if !strings.Contains(got, "stale because the tree is now at") { + t.Errorf("a pending manual gate at a moved sha did not render stale:\n%s", got) + } + fresh := renderPlanProgress(Input{ + Task: task, Phase: domain.WorkPhaseImplement, Plan: plan, + Git: GitState{HeadSHA: strings.Repeat("a", 40)}, + }) + if strings.Contains(fresh, "stale") { + t.Errorf("a pending manual gate at HEAD rendered stale:\n%s", fresh) + } +}