Step 420: add MCP human review interface

This commit is contained in:
Bill
2026-02-16 16:19:52 -07:00
parent 52d1e71e39
commit 6d5a2be423
6 changed files with 463 additions and 0 deletions

View File

@@ -71,6 +71,8 @@ struct AgentPermissionPolicy {
method == "getWorkItem" ||
method == "getRoutingExplanation" ||
method == "getReviewPolicy" ||
method == "getReviewQueue" ||
method == "getReviewContext" ||
method == "getBlockers" ||
method == "getProgress" ||
method == "getEventStream" ||
@@ -108,6 +110,8 @@ struct AgentPermissionPolicy {
method == "routeAllReady" ||
method == "executeTask" ||
method == "setReviewPolicy" ||
method == "approveReviewItem" ||
method == "rejectReviewItem" ||
method == "orchestrateStep" ||
method == "orchestrateAdvance" ||
method == "orchestrateRunDeterministic" ||

View File

@@ -2602,6 +2602,145 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
});
}
// --- getReviewQueue ---
if (method == "getReviewQueue") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto reviewItems = state.workflow->queue.getByStatus(WI_REVIEW);
json arr = json::array();
for (const auto& wi : reviewItems) {
arr.push_back({
{"itemId", wi.id},
{"nodeName", wi.nodeName},
{"nodeType", wi.nodeType},
{"bufferId", wi.bufferId},
{"workerType", wi.workerType},
{"priority", wi.priority},
{"confidence", wi.result.confidence},
{"reviewRequired", wi.reviewRequired},
{"status", wi.status}
});
}
return headlessRpcResult(id, {
{"items", arr},
{"count", (int)arr.size()}
});
}
// --- getReviewContext ---
if (method == "getReviewContext") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto params = request.contains("params") ? request["params"] : json::object();
std::string itemId = params.value("itemId", "");
if (itemId.empty())
return headlessRpcError(id, -32602, "Missing itemId");
auto item = state.workflow->queue.getItem(itemId);
if (!item)
return headlessRpcError(id, -32602, "Work item not found");
std::string summary;
summary += "Review Item: " + item->id + "\n";
summary += "Node: " + item->nodeName + " (" + item->nodeType + ")\n";
summary += "Worker: " + item->workerType + ", Priority: " + item->priority + "\n";
summary += "Status: " + item->status + ", ReviewRequired: ";
summary += item->reviewRequired ? "true\n" : "false\n";
summary += "Confidence: " + std::to_string(item->result.confidence) + "\n";
summary += "Reasoning: " + item->result.reasoning + "\n";
summary += "Generated Code:\n" + item->result.generatedCode + "\n";
if (!item->rejectionFeedback.empty()) {
summary += "Previous Feedback: " + item->rejectionFeedback + "\n";
}
return headlessRpcResult(id, {
{"item", workItemToJson(*item)},
{"generatedCode", item->result.generatedCode},
{"confidence", item->result.confidence},
{"reasoning", item->result.reasoning},
{"dependencies", item->dependencies},
{"humanSummary", summary}
});
}
// --- approveReviewItem ---
if (method == "approveReviewItem") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto params = request.contains("params") ? request["params"] : json::object();
std::string itemId = params.value("itemId", "");
std::string feedback = params.value("feedback", "");
if (itemId.empty())
return headlessRpcError(id, -32602, "Missing itemId");
auto item = state.workflow->queue.getItem(itemId);
if (!item)
return headlessRpcError(id, -32602, "Work item not found");
if (item->status != WI_REVIEW)
return headlessRpcError(id, -32000,
"Cannot approve item in status: " + item->status);
WorkItem updated = *item;
bool ok = transitionWorkItem(updated, WI_COMPLETE);
if (!ok)
return headlessRpcError(id, -32000, "Approve transition failed");
if (!feedback.empty()) updated.result.reasoning += "\nReviewer Note: " + feedback;
state.workflow->queue.updateItem(itemId, updated);
state.workflow->recordChange(itemId, WI_REVIEW, WI_COMPLETE,
"human:" + sessionId, feedback);
return headlessRpcResult(id, {
{"success", true},
{"item", workItemToJson(updated)}
});
}
// --- rejectReviewItem ---
if (method == "rejectReviewItem") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto params = request.contains("params") ? request["params"] : json::object();
std::string itemId = params.value("itemId", "");
std::string feedback = params.value("feedback", "");
if (itemId.empty())
return headlessRpcError(id, -32602, "Missing itemId");
if (feedback.empty())
return headlessRpcError(id, -32602, "Missing feedback");
auto item = state.workflow->queue.getItem(itemId);
if (!item)
return headlessRpcError(id, -32602, "Work item not found");
if (item->status != WI_REVIEW)
return headlessRpcError(id, -32000,
"Cannot reject item in status: " + item->status);
bool ok = state.workflow->queue.reject(itemId, feedback);
if (!ok)
return headlessRpcError(id, -32000, "Reject failed");
state.workflow->recordChange(itemId, WI_REVIEW, WI_READY,
"human:" + sessionId, feedback);
auto updated = state.workflow->queue.getItem(itemId);
return headlessRpcResult(id, {
{"success", true},
{"item", updated ? workItemToJson(*updated) : json::object()}
});
}
// --- setReviewPolicy ---
if (method == "setReviewPolicy") {
if (!AgentPermissionPolicy::canInvoke(role, method))

View File

@@ -1630,6 +1630,62 @@ private:
}
void registerReviewTools() {
// whetstone_get_review_queue
tools_.push_back({"whetstone_get_review_queue",
"Get items currently awaiting human review with concise queue metadata.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_review_queue"] =
[this](const json& args) {
return callWhetstone("getReviewQueue", args);
};
// whetstone_get_review_context
tools_.push_back({"whetstone_get_review_context",
"Get full review context for one item: skeleton metadata, generated code, "
"confidence, reasoning, and a human-readable summary.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Review item ID"}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_get_review_context"] =
[this](const json& args) {
return callWhetstone("getReviewContext", args);
};
// whetstone_approve_item
tools_.push_back({"whetstone_approve_item",
"Approve a review item and mark it complete. Optional feedback is stored "
"as reviewer note.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Review item ID"}}},
{"feedback", {{"type", "string"},
{"description", "Optional reviewer note"}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_approve_item"] =
[this](const json& args) {
return callWhetstone("approveReviewItem", args);
};
// whetstone_reject_item
tools_.push_back({"whetstone_reject_item",
"Reject a review item back to ready state. Feedback is required to explain "
"what must change before re-review.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Review item ID"}}},
{"feedback", {{"type", "string"},
{"description", "Required rejection feedback"}}}
}}, {"required", json::array({"itemId", "feedback"})}}
});
toolHandlers_["whetstone_reject_item"] =
[this](const json& args) {
return callWhetstone("rejectReviewItem", args);
};
// whetstone_set_review_policy
tools_.push_back({"whetstone_set_review_policy",
"Configure auto-approve rules for the review gate. Rules specify "