summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/spike/main.go3
-rw-r--r--internal/agentchannel/agentchannel.go10
-rw-r--r--internal/executor/agentmcp_test.go3
-rw-r--r--internal/executor/channel.go13
-rw-r--r--internal/executor/channel_test.go62
-rw-r--r--internal/executor/container_test.go1
6 files changed, 92 insertions, 0 deletions
diff --git a/cmd/spike/main.go b/cmd/spike/main.go
index 2841436..c2f3f8d 100644
--- a/cmd/spike/main.go
+++ b/cmd/spike/main.go
@@ -87,6 +87,9 @@ func (c *recordingChannel) ProposeRoleConfig(_ context.Context, cfg role.RoleCon
c.mu.Unlock()
return 1, nil
}
+func (c *recordingChannel) ReportVerdict(_ context.Context, approved bool, reasoning string) error {
+ return nil
+}
func main() {
agent := "claude"
diff --git a/internal/agentchannel/agentchannel.go b/internal/agentchannel/agentchannel.go
index 98104df..433b762 100644
--- a/internal/agentchannel/agentchannel.go
+++ b/internal/agentchannel/agentchannel.go
@@ -99,6 +99,16 @@ type AgentChannel interface {
// is currently "active" for that role; promoting a draft to active stays
// a human action via the existing POST /api/roles/{role}/activate.
ProposeRoleConfig(ctx context.Context, config role.RoleConfig) (version int, err error)
+ // ReportVerdict lets an evaluating agent (e.g. an arbitration-role task)
+ // report a structured approve/reject decision, instead of leaving it to
+ // be inferred by parsing the task's free-text summary later.
+ // Implementations record this as an event attached to the calling task's
+ // own ID; internal/scheduler.StoryOrchestrator's finalizeArbitration
+ // reads it back once the reporting task completes to decide REVIEW_READY
+ // vs NEEDS_FIX. A task that never calls this leaves no such event —
+ // callers reading it back must treat "no event found" as a distinct,
+ // backward-compatible case, not an error.
+ ReportVerdict(ctx context.Context, approved bool, reasoning string) error
}
// BlockedError is returned by Run when the agent asked the user a question and
diff --git a/internal/executor/agentmcp_test.go b/internal/executor/agentmcp_test.go
index 18299e8..c45964d 100644
--- a/internal/executor/agentmcp_test.go
+++ b/internal/executor/agentmcp_test.go
@@ -48,6 +48,9 @@ func (c *recordingChannel) ProposeRoleConfig(_ context.Context, cfg role.RoleCon
c.proposedRoleConfigs = append(c.proposedRoleConfigs, cfg)
return c.roleConfigVersion, nil
}
+func (c *recordingChannel) ReportVerdict(_ context.Context, approved bool, reasoning string) error {
+ return nil
+}
func resultText(t *testing.T, res *mcp.CallToolResult) string {
t.Helper()
diff --git a/internal/executor/channel.go b/internal/executor/channel.go
index 9392c77..55da7b1 100644
--- a/internal/executor/channel.go
+++ b/internal/executor/channel.go
@@ -273,4 +273,17 @@ func (c *storeChannel) ProposeRoleConfig(_ context.Context, config role.RoleConf
return row.Version, err
}
return row.Version, nil
+}
+
+func (c *storeChannel) ReportVerdict(_ context.Context, approved bool, reasoning string) error {
+ payload, _ := json.Marshal(struct {
+ Approved bool `json:"approved"`
+ Reasoning string `json:"reasoning"`
+ }{Approved: approved, Reasoning: reasoning})
+ return c.store.CreateEvent(&event.Event{
+ TaskID: c.taskID,
+ Kind: event.KindVerdictReported,
+ Actor: event.ActorAgent,
+ Payload: payload,
+ })
} \ No newline at end of file
diff --git a/internal/executor/channel_test.go b/internal/executor/channel_test.go
index 84cad89..23f0661 100644
--- a/internal/executor/channel_test.go
+++ b/internal/executor/channel_test.go
@@ -607,3 +607,65 @@ func TestStoreChannel_ProposeRoleConfig_EmitsRoleConfigProposedEvent(t *testing.
t.Errorf("payload mismatch: %+v (want role=evaluator_quality version=%d)", payload, version)
}
}
+
+// TestStoreChannel_ReportVerdict_EmitsVerdictReportedEvent proves calling
+// ReportVerdict records a KindVerdictReported event attached to the
+// *calling task's* own ID, payload {approved, reasoning} —
+// internal/scheduler.StoryOrchestrator's finalizeArbitration later reads an
+// arbitration task's own event stream for this to decide REVIEW_READY vs
+// NEEDS_FIX. Mirrors TestStoreChannel_ProposeRoleConfig_EmitsRoleConfigProposedEvent.
+func TestStoreChannel_ReportVerdict_EmitsVerdictReportedEvent(t *testing.T) {
+ store := &fakeChannelStore{}
+ ch := newStoreChannel(store, "arbitration-task-1")
+
+ if err := ch.ReportVerdict(context.Background(), false, "the CSS change breaks the running-log panel"); err != nil {
+ t.Fatalf("ReportVerdict: %v", err)
+ }
+
+ if len(store.createdEvents) != 1 {
+ t.Fatalf("expected 1 event, got %d", len(store.createdEvents))
+ }
+ ev := store.createdEvents[0]
+ if ev.Kind != event.KindVerdictReported {
+ t.Errorf("expected KindVerdictReported, got %q", ev.Kind)
+ }
+ if ev.Actor != event.ActorAgent {
+ t.Errorf("expected ActorAgent, got %q", ev.Actor)
+ }
+ if ev.TaskID != "arbitration-task-1" {
+ t.Errorf("expected event attached to calling task ID, got %q", ev.TaskID)
+ }
+
+ var payload struct {
+ Approved bool `json:"approved"`
+ Reasoning string `json:"reasoning"`
+ }
+ if err := json.Unmarshal(ev.Payload, &payload); err != nil {
+ t.Fatalf("unmarshal payload: %v", err)
+ }
+ if payload.Approved != false || payload.Reasoning != "the CSS change breaks the running-log panel" {
+ t.Errorf("unexpected payload: %+v", payload)
+ }
+}
+
+// TestStoreChannel_ReportVerdict_Approved proves the approved=true case
+// round-trips correctly too — not just the rejection path exercised above.
+func TestStoreChannel_ReportVerdict_Approved(t *testing.T) {
+ store := &fakeChannelStore{}
+ ch := newStoreChannel(store, "arbitration-task-2")
+
+ if err := ch.ReportVerdict(context.Background(), true, "meets all acceptance criteria"); err != nil {
+ t.Fatalf("ReportVerdict: %v", err)
+ }
+
+ var payload struct {
+ Approved bool `json:"approved"`
+ Reasoning string `json:"reasoning"`
+ }
+ if err := json.Unmarshal(store.createdEvents[0].Payload, &payload); err != nil {
+ t.Fatalf("unmarshal payload: %v", err)
+ }
+ if !payload.Approved {
+ t.Error("expected approved=true to round-trip as true")
+ }
+}
diff --git a/internal/executor/container_test.go b/internal/executor/container_test.go
index 6672dea..80b1996 100644
--- a/internal/executor/container_test.go
+++ b/internal/executor/container_test.go
@@ -855,3 +855,4 @@ func (noopChannel) ProposeEpic(_ context.Context, _ EpicProposal) (string, error
func (noopChannel) ProposeRoleConfig(_ context.Context, _ role.RoleConfig) (int, error) {
return 0, nil
}
+func (noopChannel) ReportVerdict(_ context.Context, _ bool, _ string) error { return nil }