Step 380: rejection rerouting feedback loop
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::string> 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<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
idx = std::min(3, idx + rejectionCount);
|
||||
return order[idx];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<std::string>();
|
||||
if (j.contains("result")) a.result = WorkItemResult::fromJson(j["result"]);
|
||||
if (j.contains("feedback")) a.feedback = j["feedback"].get<std::string>();
|
||||
if (j.contains("rejectedAt")) a.rejectedAt = j["rejectedAt"].get<std::string>();
|
||||
if (j.contains("rejectedBy")) a.rejectedBy = j["rejectedBy"].get<std::string>();
|
||||
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<RejectionAttempt> 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<std::string>();
|
||||
if (j.contains("completedAt")) wi.completedAt = j["completedAt"].get<std::string>();
|
||||
if (j.contains("result")) wi.result = WorkItemResult::fromJson(j["result"]);
|
||||
if (j.contains("rejectionFeedback")) wi.rejectionFeedback = j["rejectionFeedback"].get<std::string>();
|
||||
if (j.contains("rejectionHistory")) {
|
||||
for (const auto& a : j["rejectionHistory"]) {
|
||||
wi.rejectionHistory.push_back(RejectionAttempt::fromJson(a));
|
||||
}
|
||||
}
|
||||
return wi;
|
||||
}
|
||||
|
||||
@@ -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<int>(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";
|
||||
}
|
||||
};
|
||||
|
||||
281
editor/tests/step380_test.cpp
Normal file
281
editor/tests/step380_test.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
// Step 380: Feedback loop rejection re-routing (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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<int>(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;
|
||||
}
|
||||
48
progress.md
48
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)
|
||||
|
||||
Reference in New Issue
Block a user