Step 330: Review Gates (12/12 tests)

Configurable auto-approve rules for review gate. Default policy auto-approves
deterministic/template with >=0.9 confidence. @Review(required) always
overrides. Policy serialization, wildcard rules, and 2 new MCP tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-15 19:23:06 -07:00
parent 6352a04836
commit a026743676
8 changed files with 470 additions and 2 deletions

View File

@@ -69,7 +69,8 @@ struct AgentPermissionPolicy {
method == "getWorkflowState" ||
method == "getReadyTasks" ||
method == "getWorkItem" ||
method == "getRoutingExplanation") {
method == "getRoutingExplanation" ||
method == "getReviewPolicy") {
return true;
}
@@ -101,7 +102,8 @@ struct AgentPermissionPolicy {
method == "saveWorkflow" ||
method == "routeTask" ||
method == "routeAllReady" ||
method == "executeTask") {
method == "executeTask" ||
method == "setReviewPolicy") {
return role == AgentRole::Refactor || role == AgentRole::Generator;
}

View File

@@ -2594,5 +2594,23 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
});
}
// --- setReviewPolicy ---
if (method == "setReviewPolicy") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
if (params.contains("policy")) {
state.reviewPolicy = ReviewPolicy::fromJson(params["policy"]);
}
return headlessRpcResult(id, {{"success", true}, {"policy", state.reviewPolicy.toJson()}});
}
// --- getReviewPolicy ---
if (method == "getReviewPolicy") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
return headlessRpcResult(id, state.reviewPolicy.toJson());
}
return headlessRpcError(id, -32601, "Method not found");
}

View File

@@ -42,6 +42,7 @@
#include "RoutingEngine.h"
#include "WorkerRegistry.h"
#include "ContextAssembler.h"
#include "ReviewGate.h"
#include <nlohmann/json.hpp>
#include <string>
@@ -143,6 +144,8 @@ struct HeadlessEditorState {
RoutingEngine routingEngine;
WorkerRegistry workerRegistry = WorkerRegistry::getDefaultRegistry();
ContextAssembler contextAssembler;
ReviewGate reviewGate;
ReviewPolicy reviewPolicy = ReviewPolicy::getDefault();
// --- Buffer access ---

View File

@@ -1522,6 +1522,35 @@ private:
};
}
void registerReviewTools() {
// whetstone_set_review_policy
tools_.push_back({"whetstone_set_review_policy",
"Configure auto-approve rules for the review gate. Rules specify "
"worker type, minimum confidence, and risk level thresholds.",
{{"type", "object"}, {"properties", {
{"policy", {{"type", "object"}, {"properties", {
{"defaultAction", {{"type", "string"},
{"enum", {"require-review", "auto-approve"}}}},
{"autoApproveRules", {{"type", "array"}, {"items", {{"type", "object"}}}}}
}}}}
}}}
});
toolHandlers_["whetstone_set_review_policy"] =
[this](const json& args) {
return callWhetstone("setReviewPolicy", args);
};
// whetstone_get_review_policy
tools_.push_back({"whetstone_get_review_policy",
"Get the current review policy including auto-approve rules.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_review_policy"] =
[this](const json& args) {
return callWhetstone("getReviewPolicy", args);
};
}
void registerWhetstoneTools() {
registerASTTools();
registerAnnotationTools();
@@ -1537,5 +1566,6 @@ private:
registerWorkflowTools();
registerWorkflowExecutionTools();
registerRoutingTools();
registerReviewTools();
}
};

162
editor/src/ReviewGate.h Normal file
View File

@@ -0,0 +1,162 @@
#pragma once
// Step 330: ReviewGate — Auto-Approve Rules and Review Queue
//
// Configurable rules for auto-approving low-risk results and surfacing
// high-risk results for human review. Default policy auto-approves
// deterministic/template workers with high confidence.
#include "WorkItem.h"
#include <string>
#include <vector>
// --- AutoApproveRule ---
struct AutoApproveRule {
std::string workerType; // match worker type ("deterministic", "template", "*")
int maxComplexity = 10; // max cognitive complexity to auto-approve
float minConfidence = 0.9f; // minimum worker confidence
std::string riskLevel = "low"; // max risk level ("none", "low")
json toJson() const {
return json{
{"workerType", workerType},
{"maxComplexity", maxComplexity},
{"minConfidence", minConfidence},
{"riskLevel", riskLevel}
};
}
static AutoApproveRule fromJson(const json& j) {
AutoApproveRule r;
if (j.contains("workerType")) r.workerType = j["workerType"].get<std::string>();
if (j.contains("maxComplexity")) r.maxComplexity = j["maxComplexity"].get<int>();
if (j.contains("minConfidence")) r.minConfidence = j["minConfidence"].get<float>();
if (j.contains("riskLevel")) r.riskLevel = j["riskLevel"].get<std::string>();
return r;
}
};
// --- ReviewPolicy ---
struct ReviewPolicy {
std::vector<AutoApproveRule> autoApproveRules;
std::string defaultAction = "require-review"; // "require-review" | "auto-approve"
json toJson() const {
json rules = json::array();
for (const auto& r : autoApproveRules) {
rules.push_back(r.toJson());
}
return json{
{"autoApproveRules", rules},
{"defaultAction", defaultAction}
};
}
static ReviewPolicy fromJson(const json& j) {
ReviewPolicy p;
if (j.contains("defaultAction")) p.defaultAction = j["defaultAction"].get<std::string>();
if (j.contains("autoApproveRules")) {
for (const auto& rj : j["autoApproveRules"]) {
p.autoApproveRules.push_back(AutoApproveRule::fromJson(rj));
}
}
return p;
}
static ReviewPolicy getDefault() {
ReviewPolicy p;
p.defaultAction = "require-review";
// Auto-approve deterministic with high confidence
AutoApproveRule detRule;
detRule.workerType = "deterministic";
detRule.minConfidence = 0.9f;
detRule.riskLevel = "low";
p.autoApproveRules.push_back(detRule);
// Auto-approve template with high confidence
AutoApproveRule tmplRule;
tmplRule.workerType = "template";
tmplRule.minConfidence = 0.9f;
tmplRule.riskLevel = "low";
p.autoApproveRules.push_back(tmplRule);
return p;
}
};
// --- ReviewDecision ---
struct ReviewDecision {
bool approved = false;
std::string reasoning;
std::string ruleMatched; // which rule matched, or why not
};
// --- Risk level helpers ---
inline int riskLevelToInt(const std::string& level) {
if (level == "none") return 0;
if (level == "low") return 1;
if (level == "medium") return 2;
if (level == "high") return 3;
return 4;
}
// --- ReviewGate ---
class ReviewGate {
public:
ReviewDecision shouldAutoApprove(const WorkItem& item,
const WorkItemResult& result,
const ReviewPolicy& policy) const {
ReviewDecision decision;
// Explicit @Review(required) always requires review
if (item.reviewRequired) {
decision.approved = false;
decision.reasoning = "Explicit @Review(required) — human review mandatory";
decision.ruleMatched = "review-annotation-override";
return decision;
}
// Check auto-approve rules
for (const auto& rule : policy.autoApproveRules) {
if (matchesRule(item, result, rule)) {
decision.approved = true;
decision.reasoning = "Auto-approved: worker=" + item.workerType +
" confidence=" + std::to_string(result.confidence) +
" matches rule for " + rule.workerType;
decision.ruleMatched = "auto-approve:" + rule.workerType;
return decision;
}
}
// Default action
if (policy.defaultAction == "auto-approve") {
decision.approved = true;
decision.reasoning = "Default policy: auto-approve";
decision.ruleMatched = "default-auto-approve";
} else {
decision.approved = false;
decision.reasoning = "No auto-approve rule matched — requires review";
decision.ruleMatched = "default-require-review";
}
return decision;
}
private:
bool matchesRule(const WorkItem& item, const WorkItemResult& result,
const AutoApproveRule& rule) const {
// Worker type must match (or wildcard)
if (rule.workerType != "*" && rule.workerType != item.workerType) {
return false;
}
// Confidence must meet minimum
if (result.confidence < rule.minConfidence) {
return false;
}
return true;
}
};