diff --git a/apps/edge/internal/openai/hot_path_dispatch.go b/apps/edge/internal/openai/hot_path_dispatch.go index bccbe96f..1f5fbac3 100644 --- a/apps/edge/internal/openai/hot_path_dispatch.go +++ b/apps/edge/internal/openai/hot_path_dispatch.go @@ -1722,7 +1722,7 @@ func hotPathStageRunInput(snapshot hotPathDispatchSnapshot, prompt string) map[s } if tools := hotPathChatTools(hotPathStageProviderTools(snapshot)); len(tools) > 0 { input["tools"] = tools - input["tool_choice"] = "auto" + input["tool_choice"] = hotPathChatStageToolChoice(snapshot) } options := cloneAnyMap(snapshot.Stage.Options) if options == nil { @@ -1743,7 +1743,7 @@ func hotPathChatStageBody(snapshot hotPathDispatchSnapshot, prompt, target strin } if tools := hotPathChatTools(hotPathStageProviderTools(snapshot)); len(tools) > 0 { body["tools"] = tools - body["tool_choice"] = "auto" + body["tool_choice"] = hotPathChatStageToolChoice(snapshot) } reserved := map[string]struct{}{"model": {}, "messages": {}, "tools": {}, "stream": {}} if snapshot.OutputBudget.Limited { @@ -1763,7 +1763,7 @@ func hotPathAnthropicStageBody(snapshot hotPathDispatchSnapshot, prompt, target } if tools := hotPathAnthropicTools(hotPathStageProviderTools(snapshot)); len(tools) > 0 { body["tools"] = tools - body["tool_choice"] = map[string]any{"type": "auto"} + body["tool_choice"] = map[string]any{"type": hotPathChatStageToolChoice(snapshot)} } reserved := map[string]struct{}{"model": {}, "messages": {}, "tools": {}, "stream": {}} if snapshot.OutputBudget.Limited { @@ -1774,6 +1774,13 @@ func hotPathAnthropicStageBody(snapshot hotPathDispatchSnapshot, prompt, target return json.Marshal(body) } +func hotPathChatStageToolChoice(snapshot hotPathDispatchSnapshot) string { + if snapshot.Phase == hotPathPhaseReviewActive && len(snapshot.Transcript) == 0 { + return "required" + } + return "auto" +} + func hotPathStageProviderTools(snapshot hotPathDispatchSnapshot) []any { return cloneAnySlice(snapshot.Tools) } diff --git a/apps/edge/internal/openai/hot_path_light_test.go b/apps/edge/internal/openai/hot_path_light_test.go index f03c9d4f..99a6e075 100644 --- a/apps/edge/internal/openai/hot_path_light_test.go +++ b/apps/edge/internal/openai/hot_path_light_test.go @@ -103,6 +103,21 @@ func TestWorkerProviderReceivesOnlyCallerTools(t *testing.T) { } } +func TestInitialReviewRequiresToolUseBeforeTerminal(t *testing.T) { + initial := hotPathDispatchSnapshot{Phase: hotPathPhaseReviewActive} + if got := hotPathChatStageToolChoice(initial); got != "required" { + t.Fatalf("initial review tool choice = %q, want required", got) + } + retry := initial + retry.Transcript = []hotPathStageExchange{{}} + if got := hotPathChatStageToolChoice(retry); got != "auto" { + t.Fatalf("continued review tool choice = %q, want auto", got) + } + if got := hotPathChatStageToolChoice(hotPathDispatchSnapshot{Phase: hotPathPhaseLocalActive}); got != "auto" { + t.Fatalf("worker tool choice = %q, want auto", got) + } +} + func TestHotPathLightRequiresPlanReadBeforeWorkerCompletion(t *testing.T) { for _, endpoint := range []string{"openai", "anthropic"} { endpoint := endpoint diff --git a/apps/edge/internal/openai/hot_path_stage_input.go b/apps/edge/internal/openai/hot_path_stage_input.go index 11b75473..c20acf49 100644 --- a/apps/edge/internal/openai/hot_path_stage_input.go +++ b/apps/edge/internal/openai/hot_path_stage_input.go @@ -11,6 +11,7 @@ import ( const hotPathReviewSystemPrompt = `You are the Reviewer in a compact Plan -> Work -> Review -> Repair pipeline. Read the exact issued Plan and worker-filled Review before judging the task. Treat Review as worker evidence, not proof. Inspect the actual caller-workspace result and rerun the Plan's applicable verification with ordinary caller tools. +Your first response must issue tool calls that read both exact artifact paths and inspect the actual caller-workspace result; do not return terminal prose first. Check correctness, completeness, requirement coverage, verification trust, and unjustified deviations. If a defect exists, establish its evidence and root cause, choose one concrete fix, repair it with ordinary caller tools, and reverify in this same Review stage. Do not rewrite the reserved Plan or Review artifacts. Do not create a separate Result or final-review document. diff --git a/apps/edge/internal/openai/provider_model_rewrite.go b/apps/edge/internal/openai/provider_model_rewrite.go index 30c87196..05803ea5 100644 --- a/apps/edge/internal/openai/provider_model_rewrite.go +++ b/apps/edge/internal/openai/provider_model_rewrite.go @@ -40,6 +40,7 @@ REVIEW path: %s You are the Planner. Apply this compact Plan workflow before authoring the pair: 1. Analyze the immutable user task first. Do not create a separate analysis artifact. 2. Preserve every explicit requirement, constraint, deliverable, and acceptance condition. Do not invent extra scope. + Copy every exact literal, filename, command, and required output string from the task verbatim into an executable Plan step or Verification bullet. 3. Convert that analysis into 2-6 closed, executable steps. The Worker must be able to implement without rediscovering requirements or choosing among alternatives. 4. Write 1-3 deterministic verification bullets with observable pass conditions. 5. Make the final Plan step require the Worker to read the pending REVIEW path above, then as the Work stage's final action replace that same file while preserving its exact headings. Every Plan item status must be exactly "completed"; actual changes, actual verification evidence, and deviations must be non-empty. diff --git a/apps/edge/internal/openai/provider_model_rewrite_test.go b/apps/edge/internal/openai/provider_model_rewrite_test.go index edcd929c..f93d899e 100644 --- a/apps/edge/internal/openai/provider_model_rewrite_test.go +++ b/apps/edge/internal/openai/provider_model_rewrite_test.go @@ -279,6 +279,7 @@ func TestHotPathSelectorPairInstructionCarriesCompactPlanContract(t *testing.T) "You are the Planner.", "Analyze the immutable user task first.", "Preserve every explicit requirement, constraint, deliverable, and acceptance condition.", + "Copy every exact literal, filename, command, and required output string", "The Worker must be able to implement without rediscovering requirements", "Make the final Plan step require the Worker to read the pending REVIEW path", "Every Plan item status must be exactly \"completed\"",