Step 382: add orchestrator RPC and MCP tools

This commit is contained in:
Bill
2026-02-16 12:47:58 -07:00
parent 5300be080a
commit 448d051077
8 changed files with 703 additions and 2 deletions

View File

@@ -2317,4 +2317,13 @@ target_link_libraries(step381_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step382_test tests/step382_test.cpp)
target_include_directories(step382_test PRIVATE src)
target_link_libraries(step382_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)

View File

@@ -70,7 +70,9 @@ struct AgentPermissionPolicy {
method == "getReadyTasks" ||
method == "getWorkItem" ||
method == "getRoutingExplanation" ||
method == "getReviewPolicy") {
method == "getReviewPolicy" ||
method == "getBlockers" ||
method == "getProgress") {
return true;
}
@@ -103,7 +105,11 @@ struct AgentPermissionPolicy {
method == "routeTask" ||
method == "routeAllReady" ||
method == "executeTask" ||
method == "setReviewPolicy") {
method == "setReviewPolicy" ||
method == "orchestrateStep" ||
method == "orchestrateAdvance" ||
method == "orchestrateRunDeterministic" ||
method == "submitExternalResult") {
return role == AgentRole::Refactor || role == AgentRole::Generator;
}

View File

@@ -14,6 +14,7 @@
// Deliberately omits GUI-only methods. Stays under 600 lines.
struct HeadlessEditorState;
#include "HeadlessOrchestratorRPC.h"
// Re-use the response helpers (same signature as AgentRPCHandler.h)
static inline json headlessRpcError(const json& id, int code,
@@ -2260,6 +2261,7 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
state.workflow = WorkflowState(projectName);
std::string bufferId = state.activeBuffer->path;
int count = state.workflow->populateFromSkeleton(state.activeAST(), bufferId);
state.workflowProgress = WorkflowProgress(count);
auto stats = state.workflow->getStats();
return headlessRpcResult(id, {
@@ -2612,5 +2614,10 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
return headlessRpcResult(id, state.reviewPolicy.toJson());
}
if (auto routed = tryHandleHeadlessOrchestratorRPC(
state, request, id, method, role); routed.has_value()) {
return routed.value();
}
return headlessRpcError(id, -32601, "Method not found");
}

View File

@@ -43,6 +43,7 @@
#include "WorkerRegistry.h"
#include "ContextAssembler.h"
#include "ReviewGate.h"
#include "WorkflowProgress.h"
#include <nlohmann/json.hpp>
#include <string>
@@ -141,6 +142,7 @@ struct HeadlessEditorState {
std::string defaultLanguage = "python";
bool verbose = false;
std::optional<WorkflowState> workflow;
std::optional<WorkflowProgress> workflowProgress;
RoutingEngine routingEngine;
WorkerRegistry workerRegistry = WorkerRegistry::getDefaultRegistry();
ContextAssembler contextAssembler;

View File

@@ -0,0 +1,287 @@
#pragma once
// Step 382: Orchestrator RPC surface for headless agent requests.
#include "HeadlessEditorState.h"
#include <optional>
static inline json orchestratorRpcError(const json& id, int code,
const std::string& msg) {
return {{"jsonrpc", "2.0"}, {"id", id},
{"error", {{"code", code}, {"message", msg}}}};
}
static inline json orchestratorRpcResult(const json& id, const json& result) {
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}};
}
static inline json orchestratorEventToJson(const OrchestratorEvent& event) {
return {
{"type", event.type},
{"itemId", event.itemId},
{"detail", event.detail},
{"timestamp", event.timestamp}
};
}
static inline json orchestratorEventsToJson(
const std::vector<OrchestratorEvent>& events) {
json arr = json::array();
for (const auto& event : events) arr.push_back(orchestratorEventToJson(event));
return arr;
}
static inline json orchestratorBlockersToJson(const std::vector<BlockerInfo>& blockers) {
json arr = json::array();
for (const auto& b : blockers) {
arr.push_back({
{"type", b.type},
{"itemIds", b.itemIds},
{"description", b.description}
});
}
return arr;
}
static inline std::map<std::string, BufferInfo> collectOrchestratorBufferInfos(
HeadlessEditorState& state) {
std::map<std::string, BufferInfo> infos;
for (const auto& [path, buf] : state.bufferStates) {
BufferInfo bi;
bi.path = path;
bi.content = buf->editBuf;
Module* ast = buf->sync.getAST();
bi.compactAst = ast ? toJsonCompactSummary(ast) : json::array();
bi.importGraph = json::object();
infos[path] = std::move(bi);
}
return infos;
}
inline std::optional<json> tryHandleHeadlessOrchestratorRPC(
HeadlessEditorState& state,
const json& request,
const json& id,
const std::string& method,
AgentRole role) {
if (method == "orchestrateStep") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
state.workerRegistry, state.contextAssembler,
state.reviewGate);
orchestrator.setReviewPolicy(state.reviewPolicy);
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
OrchestratorEvent event = orchestrator.step();
if (!state.workflowProgress) {
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
}
state.workflowProgress->recordEvent(event);
return orchestratorRpcResult(id, {{"event", orchestratorEventToJson(event)}});
}
if (method == "orchestrateAdvance") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
state.workerRegistry, state.contextAssembler,
state.reviewGate);
orchestrator.setReviewPolicy(state.reviewPolicy);
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
BatchResult batch = orchestrator.advanceBatch();
if (!state.workflowProgress) {
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
}
for (const auto& event : batch.events) {
state.workflowProgress->recordEvent(event);
}
return orchestratorRpcResult(id, {
{"events", orchestratorEventsToJson(batch.events)},
{"itemsAdvanced", batch.itemsAdvanced},
{"itemsBlocked", batch.itemsBlocked},
{"contextTokensSaved", batch.contextTokensSaved}
});
}
if (method == "orchestrateRunDeterministic") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
state.workerRegistry, state.contextAssembler,
state.reviewGate);
orchestrator.setReviewPolicy(state.reviewPolicy);
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
std::vector<OrchestratorEvent> allEvents;
int totalAdvanced = 0;
int totalBlocked = 0;
int totalSaved = 0;
while (true) {
BatchResult batch = orchestrator.advanceBatch();
if (batch.events.empty()) break;
bool anyProgress = false;
for (const auto& event : batch.events) {
if (event.type != "blocked") anyProgress = true;
allEvents.push_back(event);
}
totalAdvanced += batch.itemsAdvanced;
totalBlocked += batch.itemsBlocked;
totalSaved += batch.contextTokensSaved;
if (!anyProgress) break;
}
if (!state.workflowProgress) {
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
}
for (const auto& event : allEvents) {
state.workflowProgress->recordEvent(event);
}
return orchestratorRpcResult(id, {
{"events", orchestratorEventsToJson(allEvents)},
{"itemsAdvanced", totalAdvanced},
{"itemsBlocked", totalBlocked},
{"contextTokensSaved", totalSaved},
{"stats", state.workflow->getStats().toJson()},
{"blockers", orchestratorBlockersToJson(orchestrator.getBlockers())}
});
}
if (method == "getBlockers") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
state.workerRegistry, state.contextAssembler,
state.reviewGate);
orchestrator.setReviewPolicy(state.reviewPolicy);
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
auto blockers = orchestrator.getBlockers();
return orchestratorRpcResult(id, {
{"blockers", orchestratorBlockersToJson(blockers)},
{"count", (int)blockers.size()}
});
}
if (method == "getProgress") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
if (!state.workflowProgress) {
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
} else {
state.workflowProgress->setTotalItems(state.workflow->getStats().total);
}
ProgressSnapshot snapshot = state.workflowProgress->getSnapshot();
if (snapshot.blockers.empty()) {
WorkflowOrchestrator orchestrator(*state.workflow, state.routingEngine,
state.workerRegistry, state.contextAssembler,
state.reviewGate);
orchestrator.setReviewPolicy(state.reviewPolicy);
orchestrator.setBuffers(collectOrchestratorBufferInfos(state));
snapshot.blockers = orchestrator.getBlockers();
}
return orchestratorRpcResult(id, snapshot.toJson());
}
if (method == "submitExternalResult") {
if (!AgentPermissionPolicy::canInvoke(role, method))
return orchestratorRpcError(id, -32031, "Role not permitted");
if (!state.workflow)
return orchestratorRpcError(id, -32000, "No active workflow");
auto params = request.contains("params") ? request["params"] : json::object();
std::string itemId = params.value("itemId", "");
if (itemId.empty())
return orchestratorRpcError(id, -32602, "Missing itemId");
if (!params.contains("result") || !params["result"].is_object())
return orchestratorRpcError(id, -32602, "Missing result payload");
auto item = state.workflow->queue.getItem(itemId);
if (!item)
return orchestratorRpcError(id, -32602, "Work item not found");
if (!(item->workerType == "slm" || item->workerType == "llm"))
return orchestratorRpcError(id, -32000, "submitExternalResult only supports slm/llm items");
if (item->status != WI_IN_PROGRESS && item->status != WI_ASSIGNED)
return orchestratorRpcError(id, -32000,
"Item must be assigned or in-progress for external submission");
WorkItem updated = *item;
if (updated.status == WI_ASSIGNED) transitionWorkItem(updated, WI_IN_PROGRESS);
const auto& result = params["result"];
updated.result.generatedCode = result.value("generatedCode", "");
updated.result.confidence = result.value("confidence", 0.0f);
updated.result.reasoning = result.value("reasoning", "");
updated.result.tokensGenerated = result.value("tokensGenerated", 0);
updated.result.tokensBudget = result.value("tokensBudget",
estimateContextBudget(updated.contextWidth));
if (result.contains("astJson")) updated.result.astJson = result["astJson"];
if (result.contains("diagnostics") && result["diagnostics"].is_array()) {
updated.result.diagnostics = result["diagnostics"].get<std::vector<json>>();
}
state.workflow->queue.updateItem(itemId, updated);
std::vector<OrchestratorEvent> events;
events.push_back({
"executed",
itemId,
{{"workerType", updated.workerType},
{"confidence", updated.result.confidence},
{"tokensGenerated", updated.result.tokensGenerated}},
workItemTimestamp()
});
ReviewDecision reviewDecision =
state.reviewGate.shouldAutoApprove(updated, updated.result, state.reviewPolicy);
if (reviewDecision.approved) {
state.workflow->queue.complete(itemId);
events.push_back({"auto-approved", itemId,
{{"rule", reviewDecision.ruleMatched}},
workItemTimestamp()});
events.push_back({"completed", itemId, json::object(), workItemTimestamp()});
} else {
auto maybeUpdated = state.workflow->queue.getItem(itemId);
if (!maybeUpdated) {
return orchestratorRpcError(id, -32000, "Failed to reload item");
}
WorkItem reviewItem = *maybeUpdated;
transitionWorkItem(reviewItem, WI_REVIEW);
state.workflow->queue.updateItem(itemId, reviewItem);
events.push_back({"sent-to-review", itemId,
{{"reason", reviewDecision.reasoning}},
workItemTimestamp()});
}
if (!state.workflowProgress) {
state.workflowProgress = WorkflowProgress(state.workflow->getStats().total);
}
for (const auto& event : events) {
state.workflowProgress->recordEvent(event);
}
auto latest = state.workflow->queue.getItem(itemId);
return orchestratorRpcResult(id, {
{"success", true},
{"events", orchestratorEventsToJson(events)},
{"item", latest ? workItemToJson(*latest) : json::object()}
});
}
return std::nullopt;
}

View File

@@ -1522,6 +1522,86 @@ private:
};
}
void registerOrchestratorTools() {
// whetstone_orchestrate_step
tools_.push_back({"whetstone_orchestrate_step",
"Advance one ready workflow item through the orchestration loop. "
"Returns one orchestrator event (routed/executed/completed/blocked).",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_orchestrate_step"] =
[this](const json& args) {
return callWhetstone("orchestrateStep", args);
};
// whetstone_orchestrate_advance
tools_.push_back({"whetstone_orchestrate_advance",
"Advance all currently ready workflow items in one batch. "
"Returns event stream and batch counters.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_orchestrate_advance"] =
[this](const json& args) {
return callWhetstone("orchestrateAdvance", args);
};
// whetstone_orchestrate_run_deterministic
tools_.push_back({"whetstone_orchestrate_run_deterministic",
"Run deterministic/template workflow items to completion. "
"Stops automatically at human or external-model blockers.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_orchestrate_run_deterministic"] =
[this](const json& args) {
return callWhetstone("orchestrateRunDeterministic", args);
};
// whetstone_get_blockers
tools_.push_back({"whetstone_get_blockers",
"Get current workflow blockers grouped by type: needs-human, "
"needs-review, and needs-external-model.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_blockers"] =
[this](const json& args) {
return callWhetstone("getBlockers", args);
};
// whetstone_get_progress
tools_.push_back({"whetstone_get_progress",
"Get workflow progress snapshot: completion %, throughput, ETA, "
"worker stats, and active blockers.",
{{"type", "object"}, {"properties", json::object()}}
});
toolHandlers_["whetstone_get_progress"] =
[this](const json& args) {
return callWhetstone("getProgress", args);
};
// whetstone_submit_result
tools_.push_back({"whetstone_submit_result",
"Submit external SLM/LLM output for a prepared workflow item. "
"Orchestrator applies review gate and advances lifecycle.",
{{"type", "object"}, {"properties", {
{"itemId", {{"type", "string"},
{"description", "Workflow item ID receiving external output"}}},
{"result", {{"type", "object"}, {"properties", {
{"generatedCode", {{"type", "string"}}},
{"confidence", {{"type", "number"}}},
{"reasoning", {{"type", "string"}}},
{"tokensGenerated", {{"type", "integer"}}},
{"tokensBudget", {{"type", "integer"}}},
{"astJson", {{"type", "object"}}},
{"diagnostics", {{"type", "array"}, {"items", {{"type", "object"}}}}}
}}}}
}}, {"required", json::array({"itemId", "result"})}}
});
toolHandlers_["whetstone_submit_result"] =
[this](const json& args) {
return callWhetstone("submitExternalResult", args);
};
}
void registerReviewTools() {
// whetstone_set_review_policy
tools_.push_back({"whetstone_set_review_policy",
@@ -1566,6 +1646,7 @@ private:
registerWorkflowTools();
registerWorkflowExecutionTools();
registerRoutingTools();
registerOrchestratorTools();
registerReviewTools();
}
};

View File

@@ -0,0 +1,250 @@
// Step 382: Orchestrator RPC + MCP tools (12 tests)
#include <cassert>
#include <iostream>
#include <string>
#include <vector>
#include "HeadlessEditorState.h"
#include "HeadlessAgentRPCHandler.h"
#include "MCPServer.h"
static WorkItem makeItem(const std::string& id,
const std::string& name,
const std::string& workerType = "",
const std::string& contextWidth = "local") {
WorkItem item;
item.id = id;
item.nodeId = id + "_node";
item.nodeName = name;
item.nodeType = "Function";
item.bufferId = "main.py";
item.workerType = workerType;
item.contextWidth = contextWidth;
item.priority = "medium";
item.status = WI_PENDING;
item.createdAt = workItemTimestamp();
return item;
}
static HeadlessEditorState makeState(AgentRole role = AgentRole::Generator) {
HeadlessEditorState state;
state.defaultLanguage = "python";
state.openBuffer("main.py", "def a():\n return 1\n", "python");
state.setAgentRole("test-session", role);
state.workflow = WorkflowState("wf");
return state;
}
static json rpc(HeadlessEditorState& state, const std::string& method,
const json& params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method},
{"params", params}};
return handleHeadlessAgentRequest(state, request, "test-session");
}
static json mcpCall(MCPServer& server, const std::string& method,
const json& params = json::object()) {
json request = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method},
{"params", params}};
return server.handleRequest(request);
}
int main() {
int passed = 0;
// Test 1: orchestrateStep advances one template task
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("t1", "getValue", "template"));
auto resp = rpc(state, "orchestrateStep");
assert(resp.contains("result"));
assert(resp["result"]["event"]["type"] == "completed");
std::cout << "Test 1 PASSED: orchestrateStep advances one task\n";
passed++;
}
// Test 2: orchestrateAdvance processes a batch of ready items
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("a", "getA", "template"));
state.workflow->queue.enqueue(makeItem("b", "getB", "template"));
auto resp = rpc(state, "orchestrateAdvance");
assert(resp.contains("result"));
assert(resp["result"]["itemsAdvanced"].get<int>() == 2);
std::cout << "Test 2 PASSED: orchestrateAdvance handles batches\n";
passed++;
}
// Test 3: orchestrateRunDeterministic stops at blockers
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("d1", "getFast", "template"));
state.workflow->queue.enqueue(makeItem("h1", "manual", "human"));
auto resp = rpc(state, "orchestrateRunDeterministic");
assert(resp.contains("result"));
assert(resp["result"]["stats"]["complete"].get<int>() == 1);
assert(!resp["result"]["blockers"].empty());
std::cout << "Test 3 PASSED: runDeterministic stops at blockers\n";
passed++;
}
// Test 4: getBlockers reports human items
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("h1", "manual", "human"));
rpc(state, "orchestrateStep"); // move to blocked human state
auto blockers = rpc(state, "getBlockers");
assert(blockers.contains("result"));
assert(blockers["result"]["count"].get<int>() >= 1);
std::cout << "Test 4 PASSED: getBlockers reports workflow blockers\n";
passed++;
}
// Test 5: getProgress returns valid snapshot fields
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("t1", "getV", "template"));
rpc(state, "orchestrateStep");
auto resp = rpc(state, "getProgress");
assert(resp.contains("result"));
assert(resp["result"].contains("completionPercent"));
assert(resp["result"].contains("itemsPerMinute"));
assert(resp["result"].contains("byWorkerType"));
std::cout << "Test 5 PASSED: getProgress returns snapshot\n";
passed++;
}
// Test 6: submitExternalResult accepts LLM output and advances task
{
auto state = makeState();
state.reviewPolicy.defaultAction = "auto-approve";
WorkItem llm = makeItem("x1", "buildGraph", "", "project");
state.workflow->queue.enqueue(llm);
rpc(state, "orchestrateStep"); // routes to llm, blocks in-progress
auto submit = rpc(state, "submitExternalResult", {
{"itemId", "x1"},
{"result", {
{"generatedCode", "def buildGraph(data):\n return {}"},
{"confidence", 0.92},
{"reasoning", "external model result"},
{"tokensGenerated", 40}
}}
});
assert(submit.contains("result"));
assert(submit["result"]["success"].get<bool>());
auto item = state.workflow->queue.getItem("x1");
assert(item.has_value());
assert(item->status == WI_COMPLETE);
std::cout << "Test 6 PASSED: submitExternalResult advances agent task\n";
passed++;
}
// Test 7: linter can read progress but cannot orchestrate
{
auto state = makeState(AgentRole::Linter);
state.workflow->queue.enqueue(makeItem("l1", "getName", "template"));
auto progress = rpc(state, "getProgress");
auto step = rpc(state, "orchestrateStep");
assert(progress.contains("result"));
assert(step.contains("error"));
std::cout << "Test 7 PASSED: linter read-only orchestration access\n";
passed++;
}
// Test 8: MCP tool registration includes orchestrator tools
{
MCPServer server;
const auto& tools = server.getTools();
std::vector<std::string> needed = {
"whetstone_orchestrate_step",
"whetstone_orchestrate_advance",
"whetstone_orchestrate_run_deterministic",
"whetstone_get_blockers",
"whetstone_get_progress",
"whetstone_submit_result"
};
int found = 0;
for (const auto& t : tools) {
for (const auto& n : needed) {
if (t.name == n) found++;
}
}
assert(found == 6);
std::cout << "Test 8 PASSED: MCP orchestrator tools registered\n";
passed++;
}
// Test 9: MCP orchestrate_run_deterministic executes through callback
{
auto state = makeState();
state.workflow->queue.enqueue(makeItem("m1", "getOne", "template"));
MCPServer server;
server.setRpcCallback([&state](const json& request) {
return handleHeadlessAgentRequest(state, request, "test-session");
});
auto resp = mcpCall(server, "tools/call", {
{"name", "whetstone_orchestrate_run_deterministic"},
{"arguments", json::object()}
});
assert(resp.contains("result"));
assert(resp["result"]["isError"].get<bool>() == false);
std::cout << "Test 9 PASSED: MCP run_deterministic tool call works\n";
passed++;
}
// Test 10: combined flow updates progress after external submit
{
auto state = makeState();
state.reviewPolicy.defaultAction = "auto-approve";
state.workflow->queue.enqueue(makeItem("d1", "getFast", "template"));
state.workflow->queue.enqueue(makeItem("x1", "buildGraph", "", "project"));
rpc(state, "orchestrateRunDeterministic");
auto blockers = rpc(state, "getBlockers");
assert(blockers["result"]["count"].get<int>() >= 1);
rpc(state, "submitExternalResult", {
{"itemId", "x1"},
{"result", {
{"generatedCode", "def buildGraph(data):\n return data"},
{"confidence", 0.88},
{"reasoning", "llm output"},
{"tokensGenerated", 24}
}}
});
auto progress = rpc(state, "getProgress");
assert(progress["result"]["completedItems"].get<int>() == 2);
std::cout << "Test 10 PASSED: combined orchestrator flow tracked\n";
passed++;
}
// Test 11: submitExternalResult rejects non-agent worker types
{
auto state = makeState();
WorkItem t = makeItem("t1", "getName", "template");
transitionWorkItem(t, WI_READY);
transitionWorkItem(t, WI_ASSIGNED);
transitionWorkItem(t, WI_IN_PROGRESS);
state.workflow->queue.enqueue(t);
auto resp = rpc(state, "submitExternalResult", {
{"itemId", "t1"},
{"result", {{"generatedCode", "return name"}}}
});
assert(resp.contains("error"));
std::cout << "Test 11 PASSED: submitExternalResult enforces worker type\n";
passed++;
}
// Test 12: orchestrator RPC methods require active workflow
{
HeadlessEditorState state;
state.setAgentRole("test-session", AgentRole::Generator);
auto resp = rpc(state, "orchestrateAdvance");
assert(resp.contains("error"));
std::cout << "Test 12 PASSED: workflow guards enforced\n";
passed++;
}
std::cout << "\nResults: " << passed << "/12\n";
assert(passed == 12);
return 0;
}

View File

@@ -2919,6 +2919,65 @@ worker metrics, blockers, and timeline entries.
- `editor/src/WorkflowProgress.h` within header-size limit (`303` <= `600`)
- `editor/tests/step381_test.cpp` within test-file size guidance (`196` lines)
### Step 382: Orchestrator RPC + MCP
**Status:** PASS (12/12 tests)
Added orchestrator control RPC methods and MCP tool bindings so a client can
step/advance workflows, run deterministic items, inspect blockers/progress, and
submit external model output back into the workflow loop.
**Files created:**
- `editor/src/HeadlessOrchestratorRPC.h` — extracted orchestrator RPC handling:
- `orchestrateStep`
- `orchestrateAdvance`
- `orchestrateRunDeterministic`
- `getBlockers`
- `getProgress`
- `submitExternalResult`
- event/blocker JSON serialization helpers and buffer-context collection
- `editor/tests/step382_test.cpp` — 12 tests covering:
1. `orchestrateStep` advances one task
2. `orchestrateAdvance` processes batch
3. `orchestrateRunDeterministic` stops at blockers
4. `getBlockers` reports human blockers
5. `getProgress` snapshot shape
6. `submitExternalResult` accepts LLM output and advances lifecycle
7. Linter role can read progress but cannot orchestrate
8. MCP tool registration for all 6 orchestrator tools
9. MCP `whetstone_orchestrate_run_deterministic` callback wiring
10. combined deterministic + external-submit + progress flow
11. submit-result worker-type guard
12. no-workflow guard behavior
**Files modified:**
- `editor/src/HeadlessAgentRPCHandler.h` — delegated orchestrator-method
handling to `tryHandleHeadlessOrchestratorRPC(...)`
- `editor/src/HeadlessEditorState.h` — add `workflowProgress` state
- `editor/src/AgentPermissionPolicy.h` — permissions for new orchestrator methods
(`getProgress/getBlockers` read-only, orchestration/submit methods write)
- `editor/src/MCPServer.h` — register orchestrator MCP tools:
- `whetstone_orchestrate_step`
- `whetstone_orchestrate_advance`
- `whetstone_orchestrate_run_deterministic`
- `whetstone_get_blockers`
- `whetstone_get_progress`
- `whetstone_submit_result`
- `editor/CMakeLists.txt``step382_test` target
**Verification run:**
- `step382_test` — PASS (12/12) new step coverage
- `step381_test` — PASS (12/12) regression coverage
- `step380_test` — PASS (12/12) regression coverage
- `step379_test` — PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/HeadlessOrchestratorRPC.h` within header-size limit (`287` <= `600`)
- `editor/src/WorkflowProgress.h` within header-size limit (`303` <= `600`)
- `editor/tests/step382_test.cpp` within test-file size guidance (`250` lines)
- Legacy oversized headers remain and should be split in follow-up:
- `editor/src/HeadlessAgentRPCHandler.h` (`2623` > `600`)
- `editor/src/MCPServer.h` (`1652` > `600`)
# Roadmap Planning — Sprints 12-25+
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)