Add step 615 taskitem generation MCP tool

This commit is contained in:
Bill
2026-02-17 20:43:37 -07:00
parent 8b7537ad86
commit ba620c3051
5 changed files with 442 additions and 0 deletions

View File

@@ -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)

View File

@@ -19,6 +19,9 @@
#include "MarkdownSpecParser.h"
#include "RequirementNormalizationConflictDetector.h"
#include "ScopeMilestoneDecomposer.h"
#include "TaskitemConfidenceAmbiguity.h"
#include "TaskitemGeneratorV2.h"
using json = nlohmann::json;

View File

@@ -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;
}

View 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;
}

View File

@@ -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`