Add step 616 queue readiness MCP tool
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "MarkdownSpecParser.h"
|
||||
#include "AcceptanceCriteriaBinding.h"
|
||||
#include "RequirementNormalizationConflictDetector.h"
|
||||
#include "ScopeMilestoneDecomposer.h"
|
||||
#include "TaskitemConfidenceAmbiguity.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<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;
|
||||
}
|
||||
|
||||
237
editor/tests/step616_test.cpp
Normal file
237
editor/tests/step616_test.cpp
Normal 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;
|
||||
}
|
||||
34
progress.md
34
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`
|
||||
|
||||
Reference in New Issue
Block a user