summaryrefslogtreecommitdiff
path: root/internal/scheduler/story_orchestrator.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/scheduler/story_orchestrator.go')
-rw-r--r--internal/scheduler/story_orchestrator.go20
1 files changed, 16 insertions, 4 deletions
diff --git a/internal/scheduler/story_orchestrator.go b/internal/scheduler/story_orchestrator.go
index a526552..31efa39 100644
--- a/internal/scheduler/story_orchestrator.go
+++ b/internal/scheduler/story_orchestrator.go
@@ -486,6 +486,16 @@ func (o *StoryOrchestrator) ensureArbitration(ctx context.Context, st *story.Sto
// fix-attempt, capped) through different wiring suited to where a
// story-level anchor exists (root) or doesn't (nested) -- not a special case
// in the review mechanism itself, which stays 100% identical at every depth.
+// Approval requires an EXPLICIT verdict with approved=true; an explicit
+// rejection OR no structured verdict reported at all both reject the node.
+// This is fail-closed by design: an earlier version defaulted to approval
+// when no verdict was reported ("preserving prior behavior for agents that
+// don't report a structured verdict"). A live production run (2026-07-11)
+// demonstrated why that default is dangerous for a review gate: an
+// arbitration agent that never calls report_verdict (because its role had
+// no system prompt telling it to -- see SeedRoleConfigs) shipped work an
+// evaluator had already found factually wrong, entirely undetected, because
+// "no verdict" was silently treated the same as "approved."
func (o *StoryOrchestrator) finalizeArbitration(ctx context.Context, st *story.Story, node, arbitration *task.Task) {
events, err := o.Store.ListEvents(st.ID, 0)
if err != nil {
@@ -521,7 +531,10 @@ func (o *StoryOrchestrator) finalizeArbitration(ctx context.Context, st *story.S
o.logf("story orchestrator: emit arbitration_decided", "storyID", st.ID, "error", err)
}
- if hasVerdict && !approved {
+ if !hasVerdict || !approved {
+ if !hasVerdict {
+ o.logf("story orchestrator: finalize arbitration: no verdict reported, treating as rejection (fail-closed)", "storyID", st.ID, "taskID", node.ID, "arbitrationID", arbitration.ID)
+ }
if isRoot {
st.Status = "NEEDS_FIX"
if err := o.Store.UpdateStory(st); err != nil {
@@ -533,9 +546,8 @@ func (o *StoryOrchestrator) finalizeArbitration(ctx context.Context, st *story.S
return
}
- // Approved (or no structured verdict reported -- preserves the prior
- // unconditional-approve default). node only becomes COMPLETED here,
- // after arbitration.
+ // Approved -- an explicit, structured verdict with approved=true. node
+ // only becomes COMPLETED here, after arbitration.
if err := o.Store.UpdateTaskState(node.ID, task.StateCompleted); err != nil {
o.logf("story orchestrator: finalize arbitration: promote node to completed", "storyID", st.ID, "taskID", node.ID, "error", err)
return