From 39dbb220f0e81dc773969b04b327ed630b5ccda1 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 20:46:07 -0700 Subject: [PATCH] Add step 616 queue readiness MCP tool --- editor/CMakeLists.txt | 9 + editor/src/MCPServer.h | 1 + editor/src/mcp/RegisterArchitectIntakeTools.h | 167 ++++++++++++ editor/tests/step616_test.cpp | 237 ++++++++++++++++++ progress.md | 34 +++ 5 files changed, 448 insertions(+) create mode 100644 editor/tests/step616_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index a14bc9f..a771ac9 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/MCPServer.h b/editor/src/MCPServer.h index d31de3e..65aa807 100644 --- a/editor/src/MCPServer.h +++ b/editor/src/MCPServer.h @@ -18,6 +18,7 @@ #include #include "MarkdownSpecParser.h" +#include "AcceptanceCriteriaBinding.h" #include "RequirementNormalizationConflictDetector.h" #include "ScopeMilestoneDecomposer.h" #include "TaskitemConfidenceAmbiguity.h" diff --git a/editor/src/mcp/RegisterArchitectIntakeTools.h b/editor/src/mcp/RegisterArchitectIntakeTools.h index 30e0a57..4b87e5a 100644 --- a/editor/src/mcp/RegisterArchitectIntakeTools.h +++ b/editor/src/mcp/RegisterArchitectIntakeTools.h @@ -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 tasks; + std::string error; + if (!parseAnnotatedTasksInput(args, &tasks, &error)) { + return { + {"success", false}, + {"error", error} + }; + } + + std::vector requirements; + if (!parseNormalizedRequirementsOptional(args, &requirements, &error)) { + return { + {"success", false}, + {"error", error} + }; + } + + std::vector 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* 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{}); + task.base.prerequisiteOps = taskJson.value("prerequisiteOps", std::vector{}); + 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{}); + 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* 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& 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; + } diff --git a/editor/tests/step616_test.cpp b/editor/tests/step616_test.cpp new file mode 100644 index 0000000..de31306 --- /dev/null +++ b/editor/tests/step616_test.cpp @@ -0,0 +1,237 @@ +// Step 616: whetstone_queue_ready MCP Tool (12 tests) + +#include "MCPServer.h" + +#include +#include + +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; +} diff --git a/progress.md b/progress.md index 626d2bb..044303f 100644 --- a/progress.md +++ b/progress.md @@ -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`