From b99957e2fc7d2a6bcf8e2503924e361f071202dd Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 12:36:36 -0700 Subject: [PATCH] Step 380: rejection rerouting feedback loop --- editor/CMakeLists.txt | 9 + editor/src/ContextAssembler.h | 9 +- editor/src/RoutingEngine.h | 50 ++++++ editor/src/WorkItem.h | 40 +++++ editor/src/WorkflowOrchestrator.h | 42 ++++- editor/tests/step380_test.cpp | 281 ++++++++++++++++++++++++++++++ progress.md | 48 +++++ 7 files changed, 475 insertions(+), 4 deletions(-) create mode 100644 editor/tests/step380_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f8d3962..c7fef6c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2299,4 +2299,13 @@ target_link_libraries(step379_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step380_test tests/step380_test.cpp) +target_include_directories(step380_test PRIVATE src) +target_link_libraries(step380_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/ContextAssembler.h b/editor/src/ContextAssembler.h index 132749a..22d4c8b 100644 --- a/editor/src/ContextAssembler.h +++ b/editor/src/ContextAssembler.h @@ -59,9 +59,12 @@ public: // Always include annotations (high priority) // Annotations are embedded in nodeAst for now - // Skeleton intent from result reasoning (if re-routed) - if (!item.result.reasoning.empty() && - item.result.reasoning.find("feedback") != std::string::npos) { + // Rejection feedback is first-class context for re-routed attempts. + if (!item.rejectionFeedback.empty()) { + ctx.feedbackFromRejection = item.rejectionFeedback; + } else if (!item.result.reasoning.empty() && + item.result.reasoning.find("feedback") != std::string::npos) { + // Backward-compatible fallback for older serialized items. ctx.feedbackFromRejection = item.result.reasoning; } diff --git a/editor/src/RoutingEngine.h b/editor/src/RoutingEngine.h index 9d96b71..543b96e 100644 --- a/editor/src/RoutingEngine.h +++ b/editor/src/RoutingEngine.h @@ -182,6 +182,36 @@ public: return decisions; } + RoutingDecision routeWithHistory(const WorkItem& item, int rejectionCount) const { + RoutingDecision base = route(item); + if (rejectionCount <= 0) return base; + + // Escalate one level per rejection attempt, based on the last rejected + // worker. This prevents skipping stages (e.g. slm -> human). + std::string priorWorker = base.workerType; + if (!item.rejectionHistory.empty() && + !item.rejectionHistory.back().workerType.empty()) { + priorWorker = item.rejectionHistory.back().workerType; + } + + if (priorWorker == "human") { + base.workerType = "human"; + base.reviewRequired = true; + base.reasoning += " | remains human after rejection"; + base.agentRole = agentRoleForWorker(base.workerType); + return base; + } + + base.workerType = escalateWorker(priorWorker); + base.contextWidth = widenedContext(base.contextWidth, 1); + base.contextBudgetTokens = estimateContextBudget(base.contextWidth); + base.reviewRequired = true; + base.confidence = std::max(0.4f, base.confidence - 0.10f); + base.reasoning += " | escalated after rejection history"; + base.agentRole = agentRoleForWorker(base.workerType); + return base; + } + // Route and apply: updates the WorkItem's workerType + reviewRequired void routeAndApply(WorkItem& item) const { auto decision = route(item); @@ -189,4 +219,24 @@ public: item.reviewRequired = decision.reviewRequired; item.contextWidth = decision.contextWidth; } + +private: + static std::string escalateWorker(const std::string& workerType) { + if (workerType == "deterministic" || workerType == "template") return "slm"; + if (workerType == "slm") return "llm"; + return "human"; + } + + static std::string widenedContext(const std::string& ctx, int rejectionCount) { + std::vector order = {"local", "file", "project", "cross-project"}; + int idx = 0; + for (size_t i = 0; i < order.size(); ++i) { + if (order[i] == ctx) { + idx = static_cast(i); + break; + } + } + idx = std::min(3, idx + rejectionCount); + return order[idx]; + } }; diff --git a/editor/src/WorkItem.h b/editor/src/WorkItem.h index 37ee191..716d629 100644 --- a/editor/src/WorkItem.h +++ b/editor/src/WorkItem.h @@ -52,6 +52,34 @@ struct WorkItemResult { } }; +struct RejectionAttempt { + std::string workerType; + WorkItemResult result; + std::string feedback; + std::string rejectedAt; + std::string rejectedBy; + + json toJson() const { + return json{ + {"workerType", workerType}, + {"result", result.toJson()}, + {"feedback", feedback}, + {"rejectedAt", rejectedAt}, + {"rejectedBy", rejectedBy} + }; + } + + static RejectionAttempt fromJson(const json& j) { + RejectionAttempt a; + if (j.contains("workerType")) a.workerType = j["workerType"].get(); + if (j.contains("result")) a.result = WorkItemResult::fromJson(j["result"]); + if (j.contains("feedback")) a.feedback = j["feedback"].get(); + if (j.contains("rejectedAt")) a.rejectedAt = j["rejectedAt"].get(); + if (j.contains("rejectedBy")) a.rejectedBy = j["rejectedBy"].get(); + return a; + } +}; + // --- WorkItem status values --- inline const std::string WI_PENDING = "pending"; @@ -89,6 +117,8 @@ struct WorkItem { // Result WorkItemResult result; + std::string rejectionFeedback; + std::vector rejectionHistory; }; // --- Helper: ISO timestamp --- @@ -195,6 +225,10 @@ inline json workItemToJson(const WorkItem& wi) { j["assignedAt"] = wi.assignedAt; j["completedAt"] = wi.completedAt; j["result"] = wi.result.toJson(); + j["rejectionFeedback"] = wi.rejectionFeedback; + json attempts = json::array(); + for (const auto& a : wi.rejectionHistory) attempts.push_back(a.toJson()); + j["rejectionHistory"] = attempts; return j; } @@ -216,5 +250,11 @@ inline WorkItem workItemFromJson(const json& j) { if (j.contains("assignedAt")) wi.assignedAt = j["assignedAt"].get(); if (j.contains("completedAt")) wi.completedAt = j["completedAt"].get(); if (j.contains("result")) wi.result = WorkItemResult::fromJson(j["result"]); + if (j.contains("rejectionFeedback")) wi.rejectionFeedback = j["rejectionFeedback"].get(); + if (j.contains("rejectionHistory")) { + for (const auto& a : j["rejectionHistory"]) { + wi.rejectionHistory.push_back(RejectionAttempt::fromJson(a)); + } + } return wi; } diff --git a/editor/src/WorkflowOrchestrator.h b/editor/src/WorkflowOrchestrator.h index 577a35a..48ff38c 100644 --- a/editor/src/WorkflowOrchestrator.h +++ b/editor/src/WorkflowOrchestrator.h @@ -170,6 +170,32 @@ public: return out; } + bool rejectAndRequeue(const std::string& itemId, + const std::string& feedback, + const std::string& rejectedBy = "reviewer") { + auto maybeItem = state_->queue.getItem(itemId); + if (!maybeItem.has_value()) return false; + WorkItem item = maybeItem.value(); + if (item.status != WI_REVIEW && item.status != WI_IN_PROGRESS) return false; + + RejectionAttempt attempt; + attempt.workerType = item.workerType; + attempt.result = item.result; + attempt.feedback = feedback; + attempt.rejectedAt = workItemTimestamp(); + attempt.rejectedBy = rejectedBy; + item.rejectionHistory.push_back(attempt); + item.rejectionFeedback = feedback; + item.result.reasoning = "feedback: " + feedback; + + applyFeedbackHints(item, feedback); + + item.status = WI_REJECTED; + transitionWorkItem(item, WI_READY); + item.reviewRequired = true; + return state_->queue.updateItem(itemId, item); + } + private: WorkflowState* state_ = nullptr; RoutingEngine* routing_ = nullptr; @@ -217,7 +243,8 @@ private: if (!maybeItem.has_value()) return events; WorkItem item = maybeItem.value(); - RoutingDecision decision = routing_->route(item); + RoutingDecision decision = routing_->routeWithHistory( + item, static_cast(item.rejectionHistory.size())); item.workerType = decision.workerType; item.contextWidth = decision.contextWidth; item.reviewRequired = decision.reviewRequired; @@ -281,4 +308,17 @@ private: return events; } + + static void applyFeedbackHints(WorkItem& item, const std::string& feedback) { + auto contains = [&](const std::string& token) { + return feedback.find(token) != std::string::npos; + }; + if (contains("context=local")) item.contextWidth = "local"; + if (contains("context=file")) item.contextWidth = "file"; + if (contains("context=project")) item.contextWidth = "project"; + if (contains("context=cross-project")) item.contextWidth = "cross-project"; + if (contains("worker=human")) item.workerType = "human"; + if (contains("worker=llm")) item.workerType = "llm"; + if (contains("worker=slm")) item.workerType = "slm"; + } }; diff --git a/editor/tests/step380_test.cpp b/editor/tests/step380_test.cpp new file mode 100644 index 0000000..7339c4e --- /dev/null +++ b/editor/tests/step380_test.cpp @@ -0,0 +1,281 @@ +// Step 380: Feedback loop rejection re-routing (12 tests) + +#include +#include +#include +#include "WorkflowOrchestrator.h" + +static WorkItem makeItem(const std::string& id, + const std::string& name, + const std::string& workerType, + bool reviewRequired = false, + const std::string& contextWidth = "local") { + WorkItem w; + w.id = id; + w.nodeId = id + "_node"; + w.nodeName = name; + w.nodeType = "Function"; + w.bufferId = "buf"; + w.contextWidth = contextWidth; + w.workerType = workerType; + w.reviewRequired = reviewRequired; + w.priority = "medium"; + w.status = WI_PENDING; + w.createdAt = workItemTimestamp(); + return w; +} + +int main() { + int passed = 0; + + // Test 1: review rejection re-enters ready queue + { + WorkflowState ws("r1"); + ws.queue.enqueue(makeItem("w1", "getName", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); // sent-to-review + assert(ws.queue.getByStatus(WI_REVIEW).size() == 1); + assert(orch.rejectAndRequeue("w1", "fix style", "alice")); + auto item = ws.queue.getItem("w1"); + assert(item.has_value()); + assert(item->status == WI_READY); + std::cout << "Test 1 PASSED: rejected item re-enters ready queue\n"; + passed++; + } + + // Test 2: feedback is included in next context bundle + { + WorkflowState ws("r2"); + ws.queue.enqueue(makeItem("w2", "getData", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w2", "needs edge-case handling")); + orch.step(); // escalated to slm + auto item = ws.queue.getItem("w2"); + assert(item.has_value()); + assert(item->workerType == "slm"); + assert(item->result.astJson.value("feedbackFromRejection", "") == + "needs edge-case handling"); + std::cout << "Test 2 PASSED: feedback carried into next context\n"; + passed++; + } + + // Test 3: deterministic/template rejection escalates to SLM + { + WorkflowState ws("r3"); + ws.queue.enqueue(makeItem("w3", "getX", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w3", "not robust enough")); + orch.step(); + auto item = ws.queue.getItem("w3"); + assert(item.has_value()); + assert(item->workerType == "slm"); + std::cout << "Test 3 PASSED: first escalation is template->slm\n"; + passed++; + } + + // Test 4: second rejection escalates SLM to LLM + { + WorkflowState ws("r4"); + ws.queue.enqueue(makeItem("w4", "getY", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w4", "attempt 1")); + orch.step(); // slm blocked in-progress + assert(orch.rejectAndRequeue("w4", "attempt 2")); + orch.step(); // llm blocked in-progress + auto item = ws.queue.getItem("w4"); + assert(item.has_value()); + assert(item->workerType == "llm"); + std::cout << "Test 4 PASSED: second escalation is slm->llm\n"; + passed++; + } + + // Test 5: third rejection escalates LLM to human + { + WorkflowState ws("r5"); + ws.queue.enqueue(makeItem("w5", "getZ", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w5", "attempt 1")); + orch.step(); + assert(orch.rejectAndRequeue("w5", "attempt 2")); + orch.step(); + assert(orch.rejectAndRequeue("w5", "attempt 3")); + orch.step(); + auto item = ws.queue.getItem("w5"); + assert(item.has_value()); + assert(item->workerType == "human"); + std::cout << "Test 5 PASSED: third escalation is llm->human\n"; + passed++; + } + + // Test 6: rejection history preserves worker/result/feedback metadata + { + WorkflowState ws("r6"); + WorkItem i = makeItem("w6", "getMeta", "template", true); + i.result.generatedCode = "return self.meta"; + ws.queue.enqueue(i); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w6", "metadata missing", "reviewer-1")); + auto item = ws.queue.getItem("w6"); + assert(item.has_value()); + assert(item->rejectionHistory.size() == 1); + assert(item->rejectionHistory[0].workerType == "template"); + assert(item->rejectionHistory[0].feedback == "metadata missing"); + assert(item->rejectionHistory[0].rejectedBy == "reviewer-1"); + std::cout << "Test 6 PASSED: rejection history metadata preserved\n"; + passed++; + } + + // Test 7: multiple rejections accumulate in history + { + WorkflowState ws("r7"); + ws.queue.enqueue(makeItem("w7", "getAcc", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w7", "first")); + orch.step(); + assert(orch.rejectAndRequeue("w7", "second")); + auto item = ws.queue.getItem("w7"); + assert(item.has_value()); + assert(item->rejectionHistory.size() == 2); + std::cout << "Test 7 PASSED: multiple rejection attempts accumulate\n"; + passed++; + } + + // Test 8: reviewer annotation hints apply to queued item + { + WorkflowState ws("r8"); + ws.queue.enqueue(makeItem("w8", "getHints", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w8", "context=project worker=llm")); + auto item = ws.queue.getItem("w8"); + assert(item.has_value()); + assert(item->contextWidth == "project"); + assert(item->workerType == "llm"); + std::cout << "Test 8 PASSED: feedback hints applied to item annotations\n"; + passed++; + } + + // Test 9: rerouted item widens context over original + { + WorkflowState ws("r9"); + ws.queue.enqueue(makeItem("w9", "getWide", "template", true, "local")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w9", "needs broader context")); + orch.step(); + auto item = ws.queue.getItem("w9"); + assert(item.has_value()); + assert(item->contextWidth == "file"); + std::cout << "Test 9 PASSED: context width escalates after rejection\n"; + passed++; + } + + // Test 10: escalation does not skip levels + { + WorkflowState ws("r10"); + ws.queue.enqueue(makeItem("w10", "getNoSkip", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); + assert(orch.rejectAndRequeue("w10", "attempt 1")); + orch.step(); + auto afterFirst = ws.queue.getItem("w10"); + assert(afterFirst.has_value()); + assert(afterFirst->workerType == "slm"); + assert(orch.rejectAndRequeue("w10", "attempt 2")); + orch.step(); + auto afterSecond = ws.queue.getItem("w10"); + assert(afterSecond.has_value()); + assert(afterSecond->workerType == "llm"); + std::cout << "Test 10 PASSED: escalation remains stepwise\n"; + passed++; + } + + // Test 11: rejected human task remains human + { + WorkflowState ws("r11"); + ws.queue.enqueue(makeItem("w11", "manualTask", "human")); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + orch.step(); // human blocked + assert(orch.rejectAndRequeue("w11", "still needs manual intervention")); + orch.step(); + auto item = ws.queue.getItem("w11"); + assert(item.has_value()); + assert(item->workerType == "human"); + std::cout << "Test 11 PASSED: human rejection remains human\n"; + passed++; + } + + // Test 12: rejection count tracked and invalid reject state denied + { + WorkflowState ws("r12"); + ws.queue.enqueue(makeItem("w12", "getCount", "template", true)); + RoutingEngine routing; + auto workers = WorkerRegistry::getDefaultRegistry(); + ContextAssembler assembler; + ReviewGate review; + WorkflowOrchestrator orch(ws, routing, workers, assembler, review); + // Not in review/in-progress yet + assert(!orch.rejectAndRequeue("w12", "too early")); + orch.step(); + assert(orch.rejectAndRequeue("w12", "now reject")); + auto item = ws.queue.getItem("w12"); + assert(item.has_value()); + assert(static_cast(item->rejectionHistory.size()) == 1); + std::cout << "Test 12 PASSED: rejection count tracked and status guarded\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index 6648d5a..000930b 100644 --- a/progress.md +++ b/progress.md @@ -2831,6 +2831,54 @@ project-context reuse, and batch-level progress accounting. - `editor/src/WorkflowOrchestrator.h` remains within header-size limit (`284` lines <= `600`) +### Step 380: Feedback Loop — Rejection Re-Routing +**Status:** PASS (12/12 tests) + +Implemented rejection-aware rerouting in the orchestration loop with +attempt-history preservation, stepwise escalation, and feedback-aware context +assembly for re-attempts. + +**Files created:** +- `editor/tests/step380_test.cpp` — 12 tests covering: + 1. review rejection re-enters ready queue + 2. rejection feedback appears in next worker context bundle + 3. first escalation template/deterministic -> slm + 4. second escalation slm -> llm + 5. third escalation llm -> human + 6. rejection history metadata preservation + 7. multi-rejection accumulation + 8. reviewer feedback-hints (`context=...`, `worker=...`) applied + 9. context widening after rejection + 10. no escalation-level skipping across repeated rejections + 11. human rejection remains human-routed + 12. rejection count tracking + reject-state guard + +**Files modified:** +- `editor/src/WorkItem.h` — add `RejectionAttempt` history on `WorkItem`, + add first-class `rejectionFeedback` field, and JSON serialization/deserialization + support for both +- `editor/src/RoutingEngine.h` — add `routeWithHistory(...)` and + stepwise `escalateWorker(...)` policy that advances one level per rejection + (template/deterministic -> slm -> llm -> human) without skipping +- `editor/src/WorkflowOrchestrator.h` — add `rejectAndRequeue(...)`, + preserve attempt metadata, apply reviewer feedback hints, and route + with rejection history +- `editor/src/ContextAssembler.h` — include first-class rejection feedback + in `WorkerContext.feedbackFromRejection` (with backward-compatible fallback) +- `editor/CMakeLists.txt` — `step380_test` target + +**Verification run:** +- `step380_test` — PASS (12/12) new step coverage +- `step379_test` — PASS (12/12) regression coverage +- `step378_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/WorkflowOrchestrator.h` within header-size limit (`324` <= `600`) +- `editor/src/RoutingEngine.h` within header-size limit (`242` <= `600`) +- `editor/src/WorkItem.h` within header-size limit (`260` <= `600`) +- `editor/src/ContextAssembler.h` within header-size limit (`192` <= `600`) +- `editor/tests/step380_test.cpp` within test-file size guidance (`281` lines) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)