Add step 615 taskitem generation MCP tool
This commit is contained in:
@@ -4414,4 +4414,13 @@ target_link_libraries(step614_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step615_test tests/step615_test.cpp)
|
||||
target_include_directories(step615_test PRIVATE src)
|
||||
target_link_libraries(step615_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)
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "MarkdownSpecParser.h"
|
||||
#include "RequirementNormalizationConflictDetector.h"
|
||||
#include "ScopeMilestoneDecomposer.h"
|
||||
#include "TaskitemConfidenceAmbiguity.h"
|
||||
#include "TaskitemGeneratorV2.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
|
||||
@@ -10,6 +10,20 @@
|
||||
[this](const json& args) {
|
||||
return runArchitectIntake(args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_generate_taskitems",
|
||||
"Generate and annotate taskitems from normalized intake requirements.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"normalizedRequirements", {{"type", "array"},
|
||||
{"description", "Normalized requirements from whetstone_architect_intake."}}},
|
||||
{"conflicts", {{"type", "array"},
|
||||
{"description", "Optional requirement conflicts from whetstone_architect_intake."}}}
|
||||
}}, {"required", json::array({"normalizedRequirements"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_generate_taskitems"] =
|
||||
[this](const json& args) {
|
||||
return runGenerateTaskitems(args);
|
||||
};
|
||||
}
|
||||
|
||||
json runArchitectIntake(const json& args) {
|
||||
@@ -133,3 +147,165 @@
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
json runGenerateTaskitems(const json& args) {
|
||||
RequirementNormalizationResult normalized;
|
||||
std::string error;
|
||||
if (!parseNormalizedInput(args, &normalized, &error)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", error}
|
||||
};
|
||||
}
|
||||
|
||||
DecomposedScopePlan plan;
|
||||
if (!ScopeMilestoneDecomposer::decompose(normalized, &plan, &error)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", error}
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<GeneratedTaskitem> generated;
|
||||
if (!TaskitemGeneratorV2::generate(plan, &generated, &error)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", error}
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<AnnotatedTaskitem> annotated;
|
||||
if (!TaskitemConfidenceAmbiguity::annotate(generated, normalized, &annotated, &error)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", error}
|
||||
};
|
||||
}
|
||||
|
||||
int escalateCount = 0;
|
||||
for (const auto& task : annotated) {
|
||||
if (task.escalate) ++escalateCount;
|
||||
}
|
||||
|
||||
return {
|
||||
{"success", true},
|
||||
{"tasks", annotatedTaskitemsToJson(annotated)},
|
||||
{"planSummary", {
|
||||
{"milestoneCount", (int)plan.milestones.size()},
|
||||
{"overallUncertainty", plan.overallUncertainty}
|
||||
}},
|
||||
{"conflictCount", (int)normalized.conflicts.size()},
|
||||
{"ambiguousRequirementCount", countAmbiguousRequirements(normalized.requirements)},
|
||||
{"escalateCount", escalateCount}
|
||||
};
|
||||
}
|
||||
|
||||
static bool parseNormalizedInput(const json& args,
|
||||
RequirementNormalizationResult* out,
|
||||
std::string* error) {
|
||||
if (!out || !error) return false;
|
||||
error->clear();
|
||||
out->requirements.clear();
|
||||
out->conflicts.clear();
|
||||
|
||||
if (!args.contains("normalizedRequirements")) {
|
||||
*error = "normalized_requirements_missing";
|
||||
return false;
|
||||
}
|
||||
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->requirements.push_back(requirement);
|
||||
}
|
||||
|
||||
if (out->requirements.empty()) {
|
||||
*error = "normalized_requirements_empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!args.contains("conflicts")) return true;
|
||||
if (!args["conflicts"].is_array()) {
|
||||
*error = "conflicts_not_array";
|
||||
return false;
|
||||
}
|
||||
for (const auto& conflictJson : args["conflicts"]) {
|
||||
if (!conflictJson.is_object()) {
|
||||
*error = "conflict_entry_invalid";
|
||||
return false;
|
||||
}
|
||||
RequirementConflict conflict;
|
||||
conflict.leftRequirementId = conflictJson.value("leftRequirementId", "");
|
||||
conflict.rightRequirementId = conflictJson.value("rightRequirementId", "");
|
||||
conflict.conflictType = conflictJson.value("conflictType", "");
|
||||
conflict.detail = conflictJson.value("detail", "");
|
||||
if (conflict.leftRequirementId.empty() || conflict.rightRequirementId.empty()) {
|
||||
*error = "conflict_entry_missing_fields";
|
||||
return false;
|
||||
}
|
||||
out->conflicts.push_back(conflict);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseRequirementKind(const std::string& kind,
|
||||
NormalizedRequirementKind* outKind) {
|
||||
if (!outKind) return false;
|
||||
if (kind == "goal") {
|
||||
*outKind = NormalizedRequirementKind::Goal;
|
||||
return true;
|
||||
}
|
||||
if (kind == "constraint") {
|
||||
*outKind = NormalizedRequirementKind::Constraint;
|
||||
return true;
|
||||
}
|
||||
if (kind == "dependency") {
|
||||
*outKind = NormalizedRequirementKind::Dependency;
|
||||
return true;
|
||||
}
|
||||
if (kind == "acceptance") {
|
||||
*outKind = NormalizedRequirementKind::Acceptance;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static json annotatedTaskitemsToJson(const std::vector<AnnotatedTaskitem>& tasks) {
|
||||
json out = json::array();
|
||||
for (const auto& task : tasks) {
|
||||
out.push_back({
|
||||
{"taskId", task.base.taskId},
|
||||
{"title", task.base.title},
|
||||
{"milestoneId", task.base.milestoneId},
|
||||
{"dependencyTaskIds", task.base.dependencyTaskIds},
|
||||
{"prerequisiteOps", task.base.prerequisiteOps},
|
||||
{"queueReady", task.base.queueReady},
|
||||
{"confidence", task.confidence},
|
||||
{"ambiguityCount", task.ambiguityCount},
|
||||
{"escalate", task.escalate},
|
||||
{"reasons", task.reasons}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
220
editor/tests/step615_test.cpp
Normal file
220
editor/tests/step615_test.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
// Step 615: whetstone_generate_taskitems 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 intake parser\n"
|
||||
"\n"
|
||||
"## Constraints\n"
|
||||
"- Keep modules header only\n"
|
||||
"\n"
|
||||
"## Dependencies\n"
|
||||
"- nlohmann json\n"
|
||||
"\n"
|
||||
"## Acceptance Criteria\n"
|
||||
"- tests pass\n";
|
||||
}
|
||||
|
||||
static std::string riskySpec() {
|
||||
return
|
||||
"## Goals\n"
|
||||
"- maybe improve intake and maybe improve review\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 cache lib\n"
|
||||
"\n"
|
||||
"## Acceptance Criteria\n"
|
||||
"- maybe latency under 50ms\n";
|
||||
}
|
||||
|
||||
static json intakeOutput(MCPServer& mcp, const std::string& markdown) {
|
||||
return callTool(mcp, "whetstone_architect_intake", {{"markdown", markdown}});
|
||||
}
|
||||
|
||||
static json taskitemArgsFromIntake(const json& intake) {
|
||||
return {
|
||||
{"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_generate_taskitems") found = true;
|
||||
}
|
||||
CHECK(found, "tool should be registered");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_tool_schema_requires_normalized_requirements() {
|
||||
TEST(tool_schema_requires_normalized_requirements);
|
||||
MCPServer mcp;
|
||||
bool required = false;
|
||||
for (const auto& tool : mcp.getTools()) {
|
||||
if (tool.name != "whetstone_generate_taskitems") continue;
|
||||
for (const auto& req : tool.inputSchema["required"]) {
|
||||
if (req == "normalizedRequirements") required = true;
|
||||
}
|
||||
}
|
||||
CHECK(required, "normalizedRequirements should be required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_success_for_intake_output() {
|
||||
TEST(success_for_intake_output);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, clearSpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
CHECK(out.value("success", false), "success should be true");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_returns_non_empty_tasks() {
|
||||
TEST(returns_non_empty_tasks);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, clearSpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty array");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_task_contains_annotation_fields() {
|
||||
TEST(task_contains_annotation_fields);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, clearSpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
const auto& task = out["tasks"][0];
|
||||
CHECK(task.contains("confidence"), "confidence missing");
|
||||
CHECK(task.contains("ambiguityCount"), "ambiguityCount missing");
|
||||
CHECK(task.contains("escalate"), "escalate missing");
|
||||
CHECK(task.contains("reasons"), "reasons missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_conflict_count_roundtrips_from_input() {
|
||||
TEST(conflict_count_roundtrips_from_input);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, riskySpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
CHECK(out.value("conflictCount", 0) >= 1, "expected conflict count >= 1");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_risky_input_triggers_escalation() {
|
||||
TEST(risky_input_triggers_escalation);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, riskySpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
CHECK(out.value("escalateCount", 0) >= 1, "expected escalations");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_clear_input_has_non_escalated_task() {
|
||||
TEST(clear_input_has_non_escalated_task);
|
||||
MCPServer mcp;
|
||||
json intake = intakeOutput(mcp, clearSpec());
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", taskitemArgsFromIntake(intake));
|
||||
bool foundNonEscalated = false;
|
||||
for (const auto& task : out["tasks"]) {
|
||||
if (!task.value("escalate", true)) foundNonEscalated = true;
|
||||
}
|
||||
CHECK(foundNonEscalated, "expected at least one non-escalated task");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_missing_normalized_requirements_errors() {
|
||||
TEST(missing_normalized_requirements_errors);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems", json::object());
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "normalized_requirements_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_normalized_requirements_must_be_array() {
|
||||
TEST(normalized_requirements_must_be_array);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems",
|
||||
{{"normalizedRequirements", "bad"}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "normalized_requirements_not_array", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_rejects_empty_normalized_requirements() {
|
||||
TEST(rejects_empty_normalized_requirements);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems",
|
||||
{{"normalizedRequirements", json::array()}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "normalized_requirements_empty", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_rejects_invalid_requirement_kind() {
|
||||
TEST(rejects_invalid_requirement_kind);
|
||||
MCPServer mcp;
|
||||
json bad = json::array({{
|
||||
{"requirementId", "goal-1"},
|
||||
{"kind", "invalid-kind"},
|
||||
{"normalizedText", "ship it"},
|
||||
{"anchor", "goals"},
|
||||
{"sourceLine", 2},
|
||||
{"ambiguous", false}
|
||||
}});
|
||||
json out = callTool(mcp, "whetstone_generate_taskitems",
|
||||
{{"normalizedRequirements", bad}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "requirement_kind_invalid", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 615: whetstone_generate_taskitems MCP Tool\n";
|
||||
|
||||
test_tool_registered_accessor(); // 1
|
||||
test_tool_schema_requires_normalized_requirements(); // 2
|
||||
test_success_for_intake_output(); // 3
|
||||
test_returns_non_empty_tasks(); // 4
|
||||
test_task_contains_annotation_fields(); // 5
|
||||
test_conflict_count_roundtrips_from_input(); // 6
|
||||
test_risky_input_triggers_escalation(); // 7
|
||||
test_clear_input_has_non_escalated_task(); // 8
|
||||
test_missing_normalized_requirements_errors(); // 9
|
||||
test_normalized_requirements_must_be_array(); // 10
|
||||
test_rejects_empty_normalized_requirements(); // 11
|
||||
test_rejects_invalid_requirement_kind(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
34
progress.md
34
progress.md
@@ -12089,3 +12089,37 @@ single MCP tool call for architect intake.
|
||||
- `editor/src/MCPServer.h` within header-size limit (`516` <= `600`)
|
||||
- `editor/tests/step614_test.cpp` within test-file size guidance (`193` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 615: `whetstone_generate_taskitems` MCP Tool
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Wires `ScopeMilestoneDecomposer` + `TaskitemGeneratorV2` +
|
||||
`TaskitemConfidenceAmbiguity` into a single MCP tool call that consumes
|
||||
normalized intake JSON and returns annotated taskitems.
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/mcp/RegisterArchitectIntakeTools.h` - added:
|
||||
- `whetstone_generate_taskitems` tool schema + handler
|
||||
- normalized requirement/conflict JSON parsing and validation
|
||||
- decompose/generate/annotate pipeline + annotated taskitem JSON output
|
||||
- `editor/src/MCPServer.h` - include wiring for decomposition/taskitem modules
|
||||
- `editor/CMakeLists.txt` - `step615_test` target
|
||||
|
||||
**Files added:**
|
||||
- `editor/tests/step615_test.cpp` - 12 tests covering:
|
||||
- MCP tool registration/schema
|
||||
- intake-output-to-taskitem generation path
|
||||
- confidence/escalation/conflict behavior
|
||||
- malformed input and validation error paths
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step615_test step614_test` - PASS
|
||||
- `./editor/build-native/step615_test` - PASS (12/12)
|
||||
- `./editor/build-native/step614_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/mcp/RegisterArchitectIntakeTools.h` within header-size limit (`311` <= `600`)
|
||||
- `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`
|
||||
|
||||
Reference in New Issue
Block a user