Step 324: Workflow RPC + MCP Tools (12/12 tests)

8 new RPC methods and MCP tools for workflow lifecycle management:
create, inspect, assign, complete, reject, save. Role-based access
control enforced — Linter read-only, Refactor/Generator can mutate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-15 17:00:52 -07:00
parent ec0509f6ef
commit cc352af68e
7 changed files with 683 additions and 2 deletions

View File

@@ -65,7 +65,10 @@ struct AgentPermissionPolicy {
method == "validateEnvironment" ||
method == "getLoweringHints" ||
method == "getProjectModel" ||
method == "inferAnnotations") {
method == "inferAnnotations" ||
method == "getWorkflowState" ||
method == "getReadyTasks" ||
method == "getWorkItem") {
return true;
}
@@ -89,7 +92,12 @@ struct AgentPermissionPolicy {
method == "removeSemanticAnnotation" ||
method == "setEnvironment" ||
method == "createSkeleton" ||
method == "addSkeletonNode") {
method == "addSkeletonNode" ||
method == "createWorkflow" ||
method == "assignTask" ||
method == "completeTask" ||
method == "rejectTask" ||
method == "saveWorkflow") {
return role == AgentRole::Refactor || role == AgentRole::Generator;
}

View File

@@ -2246,5 +2246,201 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
});
}
// --- createWorkflow ---
if (method == "createWorkflow") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
auto params = request.contains("params") ? request["params"] : json::object();
std::string projectName = params.value("projectName", "");
if (projectName.empty())
return headlessRpcError(id, -32602, "Missing projectName");
if (!state.activeBuffer || !state.activeAST())
return headlessRpcError(id, -32000, "No active buffer with AST");
state.workflow = WorkflowState(projectName);
std::string bufferId = state.activeBuffer->path;
int count = state.workflow->populateFromSkeleton(state.activeAST(), bufferId);
auto stats = state.workflow->getStats();
return headlessRpcResult(id, {
{"itemCount", count},
{"phase", workflowPhaseToString(state.workflow->getPhase())},
{"stats", stats.toJson()}
});
}
// --- getWorkflowState ---
if (method == "getWorkflowState") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto stats = state.workflow->getStats();
return headlessRpcResult(id, {
{"phase", workflowPhaseToString(state.workflow->getPhase())},
{"stats", stats.toJson()},
{"readyCount", state.workflow->queue.readyCount()},
{"blockedCount", state.workflow->queue.blockedCount()}
});
}
// --- getReadyTasks ---
if (method == "getReadyTasks") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
auto ready = state.workflow->queue.getReady();
json items = json::array();
for (const auto& wi : ready) {
items.push_back(workItemToJson(wi));
}
return headlessRpcResult(id, {{"items", items}, {"count", (int)items.size()}});
}
// --- getWorkItem ---
if (method == "getWorkItem") {
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");
return headlessRpcResult(id, workItemToJson(*item));
}
// --- assignTask ---
if (method == "assignTask") {
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 assignee = params.value("assignee", "");
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");
WorkItem updated = *item;
bool ok = transitionWorkItem(updated, WI_ASSIGNED);
if (!ok)
return headlessRpcError(id, -32000, "Cannot assign item in status: " + updated.status);
updated.assignee = assignee;
state.workflow->queue.updateItem(itemId, updated);
state.workflow->recordChange(itemId, item->status, WI_ASSIGNED,
"agent:" + sessionId);
return headlessRpcResult(id, {{"success", true}, {"item", workItemToJson(updated)}});
}
// --- completeTask ---
if (method == "completeTask") {
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");
// Attach result
WorkItem updated = *item;
if (params.contains("result")) {
auto r = params["result"];
updated.result.generatedCode = r.value("generatedCode", "");
updated.result.confidence = r.value("confidence", 0.0f);
updated.result.reasoning = r.value("reasoning", "");
}
// Transition: must be in-progress (or review) to complete
if (updated.status == WI_IN_PROGRESS || updated.status == WI_REVIEW) {
transitionWorkItem(updated, WI_COMPLETE);
} else {
return headlessRpcError(id, -32000,
"Cannot complete item in status: " + updated.status);
}
state.workflow->queue.updateItem(itemId, updated);
state.workflow->recordChange(itemId, item->status, WI_COMPLETE,
"agent:" + sessionId);
// Re-evaluate dependencies via complete()
// (already handled internally since we set status to complete)
// Check for newly ready items
auto ready = state.workflow->queue.getReady();
json newlyReady = json::array();
for (const auto& wi : ready) {
newlyReady.push_back(workItemToJson(wi));
}
return headlessRpcResult(id, {
{"success", true},
{"newlyReady", newlyReady}
});
}
// --- rejectTask ---
if (method == "rejectTask") {
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 reason = params.value("reason", "");
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 reject item in status: " + item->status);
bool ok = state.workflow->queue.reject(itemId, reason);
if (!ok)
return headlessRpcError(id, -32000, "Reject failed");
state.workflow->recordChange(itemId, "review", WI_READY,
"human:" + sessionId, reason);
return headlessRpcResult(id, {{"success", true}});
}
// --- saveWorkflow ---
if (method == "saveWorkflow") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return headlessRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return headlessRpcError(id, -32000, "No active workflow");
std::string root = state.workspaceRoot.empty() ? "." : state.workspaceRoot;
auto sr = ::saveWorkflow(root, *state.workflow);
return headlessRpcResult(id, {
{"success", sr.success},
{"path", sr.path},
{"bytesWritten", sr.bytesWritten}
});
}
return headlessRpcError(id, -32601, "Method not found");
}

View File

@@ -37,6 +37,8 @@
#include "SidecarPersistence.h"
#include "AnnotationInference.h"
#include "SkeletonAST.h"
#include "WorkflowState.h"
#include "WorkflowPersistence.h"
#include <nlohmann/json.hpp>
#include <string>
@@ -134,6 +136,7 @@ struct HeadlessEditorState {
std::string workspaceRoot;
std::string defaultLanguage = "python";
bool verbose = false;
std::optional<WorkflowState> workflow;
// --- Buffer access ---

View File

@@ -1355,6 +1355,117 @@ private:
};
}
void registerWorkflowExecutionTools() {
// whetstone_create_workflow
tools_.push_back({"whetstone_create_workflow",
"Create a workflow from the active buffer's skeleton AST. "
"Populates work items from skeleton tasks with routing annotations.",
{{"type", "object"}, {"properties", {
{"projectName", {{"type", "string"},
{"description", "Name for the workflow project"}}}
}}, {"required", json::array({"projectName"})}}
});
toolHandlers_["whetstone_create_workflow"] =
[this](const json& args) {
return callWhetstone("createWorkflow", args);
};
// whetstone_get_workflow_state
tools_.push_back({"whetstone_get_workflow_state",
"Get current workflow state including phase, stats, ready and blocked counts.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_workflow_state"] =
[this](const json& args) {
return callWhetstone("getWorkflowState", args);
};
// whetstone_get_ready_tasks
tools_.push_back({"whetstone_get_ready_tasks",
"Get work items ready for assignment, ordered by priority.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_ready_tasks"] =
[this](const json& args) {
return callWhetstone("getReadyTasks", args);
};
// whetstone_get_work_item
tools_.push_back({"whetstone_get_work_item",
"Get full details of a single work item including result if present.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Work item ID"}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_get_work_item"] =
[this](const json& args) {
return callWhetstone("getWorkItem", args);
};
// whetstone_assign_task
tools_.push_back({"whetstone_assign_task",
"Assign a ready work item to a worker.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Work item ID to assign"}}},
{"assignee", {{"type", "string"},
{"description", "Worker identifier"}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_assign_task"] =
[this](const json& args) {
return callWhetstone("assignTask", args);
};
// whetstone_complete_task
tools_.push_back({"whetstone_complete_task",
"Mark a work item as complete with generated result. "
"Triggers dependency cascade — blocked items may become ready.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Work item ID to complete"}}},
{"result", {{"type", "object"}, {"properties", {
{"generatedCode", {{"type", "string"},
{"description", "Generated code output"}}},
{"confidence", {{"type", "number"},
{"description", "Confidence score 0.0-1.0"}}},
{"reasoning", {{"type", "string"},
{"description", "Explanation of decisions"}}}
}}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_complete_task"] =
[this](const json& args) {
return callWhetstone("completeTask", args);
};
// whetstone_reject_task
tools_.push_back({"whetstone_reject_task",
"Reject a work item back to the queue with feedback.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Work item ID to reject"}}},
{"reason", {{"type", "string"},
{"description", "Rejection reason/feedback"}}}
}}, {"required", json::array({"itemId"})}}
});
toolHandlers_["whetstone_reject_task"] =
[this](const json& args) {
return callWhetstone("rejectTask", args);
};
// whetstone_save_workflow
tools_.push_back({"whetstone_save_workflow",
"Persist the current workflow state to a sidecar JSON file.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_save_workflow"] =
[this](const json& args) {
return callWhetstone("saveWorkflow", args);
};
}
void registerWhetstoneTools() {
registerASTTools();
registerAnnotationTools();
@@ -1368,5 +1479,6 @@ private:
registerEnvironmentTools();
registerTrainingDataTools();
registerWorkflowTools();
registerWorkflowExecutionTools();
}
};