Add step 616 queue readiness MCP tool

This commit is contained in:
Bill
2026-02-17 20:46:07 -07:00
parent ba620c3051
commit 39dbb220f0
5 changed files with 448 additions and 0 deletions

View File

@@ -4423,4 +4423,13 @@ target_link_libraries(step615_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step616_test tests/step616_test.cpp)
target_include_directories(step616_test PRIVATE src)
target_link_libraries(step616_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

@@ -18,6 +18,7 @@
#include <nlohmann/json.hpp>
#include "MarkdownSpecParser.h"
#include "AcceptanceCriteriaBinding.h"
#include "RequirementNormalizationConflictDetector.h"
#include "ScopeMilestoneDecomposer.h"
#include "TaskitemConfidenceAmbiguity.h"

View File

@@ -24,6 +24,20 @@
[this](const json& args) {
return runGenerateTaskitems(args);
};
tools_.push_back({"whetstone_queue_ready",
"Evaluate queue readiness for annotated taskitems.",
{{"type", "object"}, {"properties", {
{"tasks", {{"type", "array"},
{"description", "Annotated taskitems from whetstone_generate_taskitems."}}},
{"normalizedRequirements", {{"type", "array"},
{"description", "Normalized requirements (needed for acceptance-coverage binding)."}}}
}}, {"required", json::array({"tasks"})}}
});
toolHandlers_["whetstone_queue_ready"] =
[this](const json& args) {
return runQueueReady(args);
};
}
json runArchitectIntake(const json& args) {
@@ -309,3 +323,156 @@
}
return out;
}
json runQueueReady(const json& args) {
std::vector<AnnotatedTaskitem> tasks;
std::string error;
if (!parseAnnotatedTasksInput(args, &tasks, &error)) {
return {
{"success", false},
{"error", error}
};
}
std::vector<NormalizedRequirement> requirements;
if (!parseNormalizedRequirementsOptional(args, &requirements, &error)) {
return {
{"success", false},
{"error", error}
};
}
std::vector<BoundTaskitem> queue;
bool bindOk = AcceptanceCriteriaBinding::bind(tasks, requirements, &queue, &error);
int escalateCount = 0;
for (const auto& task : tasks) {
if (task.escalate) ++escalateCount;
}
json blockers = json::array();
int readyCount = 0;
if (!bindOk) {
blockers.push_back(error);
} else {
for (const auto& item : queue) {
if (item.task.base.queueReady && item.hasCoverage) ++readyCount;
}
}
if (readyCount == 0) blockers.push_back("no_ready_tasks");
if (escalateCount > 0) blockers.push_back("escalations_present");
return {
{"success", true},
{"ready", blockers.empty()},
{"blockers", blockers},
{"readyCount", readyCount},
{"escalateCount", escalateCount},
{"queueJson", boundQueueToJson(queue)}
};
}
static bool parseAnnotatedTasksInput(const json& args,
std::vector<AnnotatedTaskitem>* out,
std::string* error) {
if (!out || !error) return false;
error->clear();
out->clear();
if (!args.contains("tasks")) {
*error = "tasks_missing";
return false;
}
if (!args["tasks"].is_array()) {
*error = "tasks_not_array";
return false;
}
for (const auto& taskJson : args["tasks"]) {
if (!taskJson.is_object()) {
*error = "task_entry_invalid";
return false;
}
AnnotatedTaskitem task;
task.base.taskId = taskJson.value("taskId", "");
task.base.title = taskJson.value("title", "");
task.base.milestoneId = taskJson.value("milestoneId", "");
task.base.dependencyTaskIds = taskJson.value("dependencyTaskIds", std::vector<std::string>{});
task.base.prerequisiteOps = taskJson.value("prerequisiteOps", std::vector<std::string>{});
task.base.queueReady = taskJson.value("queueReady", false);
task.confidence = taskJson.value("confidence", 0);
task.ambiguityCount = taskJson.value("ambiguityCount", 0);
task.escalate = taskJson.value("escalate", false);
task.reasons = taskJson.value("reasons", std::vector<std::string>{});
if (task.base.taskId.empty() || task.base.title.empty() || task.base.milestoneId.empty()) {
*error = "task_entry_missing_fields";
return false;
}
out->push_back(task);
}
if (out->empty()) {
*error = "tasks_empty";
return false;
}
return true;
}
static bool parseNormalizedRequirementsOptional(const json& args,
std::vector<NormalizedRequirement>* out,
std::string* error) {
if (!out || !error) return false;
error->clear();
out->clear();
if (!args.contains("normalizedRequirements")) return true;
if (!args["normalizedRequirements"].is_array()) {
*error = "normalized_requirements_not_array";
return false;
}
for (const auto& requirementJson : args["normalizedRequirements"]) {
if (!requirementJson.is_object()) {
*error = "requirement_entry_invalid";
return false;
}
NormalizedRequirement requirement;
requirement.requirementId = requirementJson.value("requirementId", "");
requirement.normalizedText = requirementJson.value("normalizedText", "");
requirement.anchor = requirementJson.value("anchor", "");
requirement.sourceLine = requirementJson.value("sourceLine", 0);
requirement.ambiguous = requirementJson.value("ambiguous", false);
if (requirement.requirementId.empty() || requirement.normalizedText.empty()) {
*error = "requirement_entry_missing_fields";
return false;
}
if (!parseRequirementKind(requirementJson.value("kind", ""), &requirement.kind)) {
*error = "requirement_kind_invalid";
return false;
}
out->push_back(requirement);
}
return true;
}
static json boundQueueToJson(const std::vector<BoundTaskitem>& queue) {
json out = json::array();
for (const auto& item : queue) {
json checks = json::array();
for (const auto& check : item.checks) {
checks.push_back({
{"checkId", check.checkId},
{"text", check.text},
{"testSkeleton", check.testSkeleton}
});
}
out.push_back({
{"task", {
{"taskId", item.task.base.taskId},
{"title", item.task.base.title},
{"milestoneId", item.task.base.milestoneId},
{"queueReady", item.task.base.queueReady},
{"escalate", item.task.escalate}
}},
{"checks", checks},
{"hasCoverage", item.hasCoverage}
});
}
return out;
}

View File

@@ -0,0 +1,237 @@
// Step 616: whetstone_queue_ready MCP Tool (12 tests)
#include "MCPServer.h"
#include <iostream>
#include <string>
static int passed = 0, failed = 0;
#define TEST(name) { std::cout << " " << #name << "... "; }
#define PASS() { std::cout << "PASS\n"; ++passed; }
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
static json callTool(MCPServer& mcp, const std::string& name, const json& args) {
json req = {
{"jsonrpc", "2.0"},
{"id", 1},
{"method", "tools/call"},
{"params", {{"name", name}, {"arguments", args}}}
};
json resp = mcp.handleRequest(req);
std::string text = resp["result"]["content"][0].value("text", "{}");
return json::parse(text);
}
static std::string clearSpec() {
return
"## Goals\n"
"- Ship queue simulation\n"
"\n"
"## Constraints\n"
"- Keep APIs deterministic\n"
"\n"
"## Dependencies\n"
"- cmake\n"
"\n"
"## Acceptance Criteria\n"
"- queue has coverage checks\n";
}
static std::string riskySpec() {
return
"## Goals\n"
"- maybe improve workflow maybe maybe\n"
"\n"
"## Constraints\n"
"- Do not use dynamic allocation in hot path\n"
"- Use dynamic allocation in hot path for warmup\n"
"\n"
"## Dependencies\n"
"- maybe add another dependency\n"
"\n"
"## Acceptance Criteria\n"
"- maybe pass tests\n";
}
static json intakeThenGenerate(MCPServer& mcp, const std::string& markdown) {
json intake = callTool(mcp, "whetstone_architect_intake", {{"markdown", markdown}});
return {
{"intake", intake},
{"generated", callTool(mcp, "whetstone_generate_taskitems", {
{"normalizedRequirements", intake["normalizedRequirements"]},
{"conflicts", intake["conflicts"]}
})}
};
}
void test_tool_registered_accessor() {
TEST(tool_registered_accessor);
MCPServer mcp;
bool found = false;
for (const auto& tool : mcp.getTools()) {
if (tool.name == "whetstone_queue_ready") found = true;
}
CHECK(found, "tool should be registered");
PASS();
}
void test_tool_schema_requires_tasks() {
TEST(tool_schema_requires_tasks);
MCPServer mcp;
bool required = false;
for (const auto& tool : mcp.getTools()) {
if (tool.name != "whetstone_queue_ready") continue;
for (const auto& req : tool.inputSchema["required"]) {
if (req == "tasks") required = true;
}
}
CHECK(required, "tasks should be required");
PASS();
}
void test_success_with_generated_tasks_and_requirements() {
TEST(success_with_generated_tasks_and_requirements);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, clearSpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", flow["intake"]["normalizedRequirements"]}
});
CHECK(out.value("success", false), "success should be true");
PASS();
}
void test_ready_count_positive_for_clear_spec() {
TEST(ready_count_positive_for_clear_spec);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, clearSpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", flow["intake"]["normalizedRequirements"]}
});
CHECK(out.value("readyCount", 0) >= 1, "readyCount should be >= 1");
PASS();
}
void test_queue_json_contains_coverage_checks() {
TEST(queue_json_contains_coverage_checks);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, clearSpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", flow["intake"]["normalizedRequirements"]}
});
CHECK(out["queueJson"].is_array() && !out["queueJson"].empty(), "queueJson should be populated");
CHECK(out["queueJson"][0].value("hasCoverage", false), "first queue item should have coverage");
CHECK(out["queueJson"][0]["checks"].is_array() && !out["queueJson"][0]["checks"].empty(),
"first queue item should include checks");
PASS();
}
void test_risky_spec_surfaces_escalations() {
TEST(risky_spec_surfaces_escalations);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, riskySpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", flow["intake"]["normalizedRequirements"]}
});
CHECK(out.value("escalateCount", 0) >= 1, "escalateCount should be >= 1");
PASS();
}
void test_risky_spec_not_ready_due_to_blockers() {
TEST(risky_spec_not_ready_due_to_blockers);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, riskySpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", flow["intake"]["normalizedRequirements"]}
});
bool hasEscalationBlocker = false;
for (const auto& blocker : out["blockers"]) {
if (blocker == "escalations_present") hasEscalationBlocker = true;
}
CHECK(!out.value("ready", true), "ready should be false");
CHECK(hasEscalationBlocker, "escalation blocker expected");
PASS();
}
void test_missing_tasks_errors() {
TEST(missing_tasks_errors);
MCPServer mcp;
json out = callTool(mcp, "whetstone_queue_ready", json::object());
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "tasks_missing", "wrong error");
PASS();
}
void test_tasks_must_be_array() {
TEST(tasks_must_be_array);
MCPServer mcp;
json out = callTool(mcp, "whetstone_queue_ready", {{"tasks", "bad"}});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "tasks_not_array", "wrong error");
PASS();
}
void test_rejects_task_entries_missing_fields() {
TEST(rejects_task_entries_missing_fields);
MCPServer mcp;
json badTasks = json::array({{{"taskId", "task-1"}}});
json out = callTool(mcp, "whetstone_queue_ready", {{"tasks", badTasks}});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "task_entry_missing_fields", "wrong error");
PASS();
}
void test_without_requirements_reports_acceptance_blocker() {
TEST(without_requirements_reports_acceptance_blocker);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, clearSpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]}
});
bool hasAcceptanceBlocker = false;
for (const auto& blocker : out["blockers"]) {
if (blocker == "acceptance_requirements_empty") hasAcceptanceBlocker = true;
}
CHECK(out.value("success", false), "success should be true");
CHECK(hasAcceptanceBlocker, "acceptance blocker expected");
CHECK(!out.value("ready", true), "ready should be false");
PASS();
}
void test_invalid_normalized_requirements_type_errors() {
TEST(invalid_normalized_requirements_type_errors);
MCPServer mcp;
json flow = intakeThenGenerate(mcp, clearSpec());
json out = callTool(mcp, "whetstone_queue_ready", {
{"tasks", flow["generated"]["tasks"]},
{"normalizedRequirements", "bad"}
});
CHECK(!out.value("success", true), "success should be false");
CHECK(out.value("error", "") == "normalized_requirements_not_array", "wrong error");
PASS();
}
int main() {
std::cout << "Step 616: whetstone_queue_ready MCP Tool\n";
test_tool_registered_accessor(); // 1
test_tool_schema_requires_tasks(); // 2
test_success_with_generated_tasks_and_requirements(); // 3
test_ready_count_positive_for_clear_spec(); // 4
test_queue_json_contains_coverage_checks(); // 5
test_risky_spec_surfaces_escalations(); // 6
test_risky_spec_not_ready_due_to_blockers(); // 7
test_missing_tasks_errors(); // 8
test_tasks_must_be_array(); // 9
test_rejects_task_entries_missing_fields(); // 10
test_without_requirements_reports_acceptance_blocker(); // 11
test_invalid_normalized_requirements_type_errors(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -12123,3 +12123,37 @@ normalized intake JSON and returns annotated taskitems.
- `editor/src/MCPServer.h` within header-size limit (`519` <= `600`)
- `editor/tests/step615_test.cpp` within test-file size guidance (`220` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
### Step 616: `whetstone_queue_ready` MCP Tool
**Status:** PASS (12/12 tests)
Adds queue-readiness evaluation over annotated taskitems, including blockers,
ready-count/escalation summaries, and bound acceptance-check queue JSON.
**Files modified:**
- `editor/src/mcp/RegisterArchitectIntakeTools.h` - added:
- `whetstone_queue_ready` tool schema + handler
- annotated taskitem input parsing and validation
- optional normalized-requirement parsing
- acceptance binding + readiness/blocker derivation + queue JSON shaping
- `editor/src/MCPServer.h` - include wiring for acceptance binding types
- `editor/CMakeLists.txt` - `step616_test` target
**Files added:**
- `editor/tests/step616_test.cpp` - 12 tests covering:
- MCP tool registration/schema
- 614→615→616 success chain
- ready/blocker/escalation and queue-coverage behavior
- malformed inputs and missing acceptance requirement handling
**Verification run:**
- `cmake -S editor -B editor/build-native` - PASS
- `cmake --build editor/build-native --target step616_test step615_test` - PASS
- `./editor/build-native/step616_test` - PASS (12/12)
- `./editor/build-native/step615_test` - PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/mcp/RegisterArchitectIntakeTools.h` within header-size limit (`478` <= `600`)
- `editor/src/MCPServer.h` within header-size limit (`520` <= `600`)
- `editor/tests/step616_test.cpp` within test-file size guidance (`237` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`