Add project taskitem pool and planner fixes
This commit is contained in:
@@ -79,7 +79,8 @@ struct AgentPermissionPolicy {
|
||||
method == "getBlockers" ||
|
||||
method == "getProgress" ||
|
||||
method == "getEventStream" ||
|
||||
method == "getRecentEvents") {
|
||||
method == "getRecentEvents" ||
|
||||
method == "listTaskitemPool") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -114,6 +115,10 @@ struct AgentPermissionPolicy {
|
||||
method == "assignTask" ||
|
||||
method == "completeTask" ||
|
||||
method == "rejectTask" ||
|
||||
method == "importTaskitemPool" ||
|
||||
method == "claimTaskitem" ||
|
||||
method == "releaseTaskitem" ||
|
||||
method == "completePooledTaskitem" ||
|
||||
method == "saveWorkflow" ||
|
||||
method == "routeTask" ||
|
||||
method == "routeAllReady" ||
|
||||
|
||||
218
editor/src/ArchitectIntakeProcessor.h
Normal file
218
editor/src/ArchitectIntakeProcessor.h
Normal file
@@ -0,0 +1,218 @@
|
||||
#pragma once
|
||||
|
||||
#include "MarkdownSpecParser.h"
|
||||
#include "RequirementNormalizationConflictDetector.h"
|
||||
#include "RequirementsParser.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct IntakeResult {
|
||||
std::vector<nlohmann::json> normalizedRequirements;
|
||||
std::vector<nlohmann::json> conflicts;
|
||||
nlohmann::json parsedSpec = nlohmann::json::object();
|
||||
int requirementCount = 0;
|
||||
int fieldCount = 0;
|
||||
int constraintCount = 0;
|
||||
int conflictCount = 0;
|
||||
bool hasConflicts = false;
|
||||
bool success = false;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class ArchitectIntakeProcessor {
|
||||
public:
|
||||
static IntakeResult process(const std::string& markdown) {
|
||||
IntakeResult out;
|
||||
|
||||
ParsedMarkdownSpec parsed;
|
||||
std::string error;
|
||||
if (!MarkdownSpecParser::parse(markdown, &parsed, &error)) {
|
||||
out.error = error;
|
||||
return out;
|
||||
}
|
||||
|
||||
out.parsedSpec = parsedSpecToJson(parsed);
|
||||
|
||||
RequirementNormalizationResult normalized;
|
||||
bool normalizedOk = RequirementNormalizationConflictDetector::normalize(parsed, &normalized, &error);
|
||||
if (!normalizedOk && error != "no_requirements_found") {
|
||||
out.error = error;
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::vector<ParsedRequirement> parsedRequirements = RequirementsParser::parse(markdown);
|
||||
if (!normalizedOk && parsedRequirements.empty()) {
|
||||
out.error = error;
|
||||
return out;
|
||||
}
|
||||
|
||||
int reqIndex = 1;
|
||||
int goalIndex = 1;
|
||||
int constraintIndex = 1;
|
||||
int dependencyIndex = 1;
|
||||
int acceptanceIndex = 1;
|
||||
int featureIndex = 1;
|
||||
int fieldIndex = 1;
|
||||
std::set<std::string> emittedKeys;
|
||||
|
||||
for (const auto& req : normalized.requirements) {
|
||||
out.normalizedRequirements.push_back(normalizedRequirementToJson(req));
|
||||
emittedKeys.insert(requirementDedupKey(req.anchor, req.sourceLine, req.normalizedText));
|
||||
if (req.kind == NormalizedRequirementKind::Constraint) ++out.constraintCount;
|
||||
if (req.kind == NormalizedRequirementKind::Goal || req.kind == NormalizedRequirementKind::Requirement ||
|
||||
req.kind == NormalizedRequirementKind::Feature) {
|
||||
++out.requirementCount;
|
||||
}
|
||||
if (req.kind == NormalizedRequirementKind::Field) ++out.fieldCount;
|
||||
}
|
||||
|
||||
for (const auto& parsedReq : parsedRequirements) {
|
||||
NormalizedRequirement req;
|
||||
if (parsedReq.isField) {
|
||||
req.requirementId = "field-" + std::to_string(fieldIndex++);
|
||||
req.kind = NormalizedRequirementKind::Field;
|
||||
} else {
|
||||
req.kind = inferKindFromSectionAnchor(parsedReq.sectionAnchor);
|
||||
if (req.kind == NormalizedRequirementKind::Goal) {
|
||||
req.requirementId = "goal-" + std::to_string(goalIndex++);
|
||||
} else if (req.kind == NormalizedRequirementKind::Constraint) {
|
||||
req.requirementId = "constraint-" + std::to_string(constraintIndex++);
|
||||
} else if (req.kind == NormalizedRequirementKind::Dependency) {
|
||||
req.requirementId = "dependency-" + std::to_string(dependencyIndex++);
|
||||
} else if (req.kind == NormalizedRequirementKind::Acceptance) {
|
||||
req.requirementId = "acceptance-" + std::to_string(acceptanceIndex++);
|
||||
} else if (req.kind == NormalizedRequirementKind::Feature) {
|
||||
req.requirementId = "feature-" + std::to_string(featureIndex++);
|
||||
} else {
|
||||
req.requirementId = "requirement-" + std::to_string(reqIndex++);
|
||||
}
|
||||
}
|
||||
req.normalizedText = parsedReq.text;
|
||||
req.sourceText = parsedReq.sourceText.empty() ? parsedReq.text : parsedReq.sourceText;
|
||||
req.anchor = parsedReq.sectionAnchor;
|
||||
req.sourceLine = parsedReq.sourceLine;
|
||||
req.ambiguous = isAmbiguous(parsedReq.text);
|
||||
|
||||
const std::string dedupKey = requirementDedupKey(req.anchor, req.sourceLine, req.normalizedText);
|
||||
if (emittedKeys.count(dedupKey) != 0) continue;
|
||||
emittedKeys.insert(dedupKey);
|
||||
|
||||
nlohmann::json j = normalizedRequirementToJson(req);
|
||||
j["sourceText"] = parsedReq.sourceText;
|
||||
j["sectionTitle"] = parsedReq.sectionTitle;
|
||||
if (parsedReq.isField) {
|
||||
j["fieldName"] = parsedReq.fieldName;
|
||||
j["fieldType"] = parsedReq.fieldType;
|
||||
++out.fieldCount;
|
||||
} else {
|
||||
if (req.kind == NormalizedRequirementKind::Constraint) ++out.constraintCount;
|
||||
++out.requirementCount;
|
||||
}
|
||||
out.normalizedRequirements.push_back(j);
|
||||
}
|
||||
|
||||
for (const auto& conflict : normalized.conflicts) {
|
||||
out.conflicts.push_back({
|
||||
{"leftRequirementId", conflict.leftRequirementId},
|
||||
{"rightRequirementId", conflict.rightRequirementId},
|
||||
{"conflictType", conflict.conflictType},
|
||||
{"detail", conflict.detail}
|
||||
});
|
||||
}
|
||||
|
||||
out.conflictCount = static_cast<int>(out.conflicts.size());
|
||||
out.hasConflicts = !out.conflicts.empty();
|
||||
out.success = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool isAmbiguous(const std::string& text) {
|
||||
const std::string lower = intakeToLower(text);
|
||||
return lower.find("maybe") != std::string::npos ||
|
||||
lower.find("possibly") != std::string::npos ||
|
||||
lower.find("somehow") != std::string::npos ||
|
||||
lower.find("etc") != std::string::npos ||
|
||||
lower.find("nice") != std::string::npos;
|
||||
}
|
||||
|
||||
static std::string requirementDedupKey(const std::string& anchor,
|
||||
int sourceLine,
|
||||
const std::string& normalizedText) {
|
||||
return anchor + "|" + std::to_string(sourceLine) + "|" + normalizedText;
|
||||
}
|
||||
|
||||
static NormalizedRequirementKind inferKindFromSectionAnchor(const std::string& anchor) {
|
||||
if (anchor == "goals") return NormalizedRequirementKind::Goal;
|
||||
if (anchor == "constraints" ||
|
||||
anchor == "observability" ||
|
||||
anchor == "security-safety" ||
|
||||
anchor == "performance-budgets" ||
|
||||
anchor == "rollout-migration-rollback") {
|
||||
return NormalizedRequirementKind::Constraint;
|
||||
}
|
||||
if (anchor == "dependencies") return NormalizedRequirementKind::Dependency;
|
||||
if (anchor == "acceptance-criteria") return NormalizedRequirementKind::Acceptance;
|
||||
if (anchor == "features") return NormalizedRequirementKind::Feature;
|
||||
return NormalizedRequirementKind::Requirement;
|
||||
}
|
||||
|
||||
static std::string requirementKindToString(NormalizedRequirementKind kind) {
|
||||
if (kind == NormalizedRequirementKind::Goal) return "goal";
|
||||
if (kind == NormalizedRequirementKind::Constraint) return "constraint";
|
||||
if (kind == NormalizedRequirementKind::Dependency) return "dependency";
|
||||
if (kind == NormalizedRequirementKind::Acceptance) return "acceptance";
|
||||
if (kind == NormalizedRequirementKind::Requirement) return "requirement";
|
||||
if (kind == NormalizedRequirementKind::Feature) return "feature";
|
||||
if (kind == NormalizedRequirementKind::Field) return "field";
|
||||
return "goal";
|
||||
}
|
||||
|
||||
static nlohmann::json normalizedRequirementToJson(const NormalizedRequirement& requirement) {
|
||||
return {
|
||||
{"requirementId", requirement.requirementId},
|
||||
{"kind", requirementKindToString(requirement.kind)},
|
||||
{"normalizedText", requirement.normalizedText},
|
||||
{"sourceText", requirement.sourceText.empty() ? requirement.normalizedText : requirement.sourceText},
|
||||
{"anchor", requirement.anchor},
|
||||
{"sourceLine", requirement.sourceLine},
|
||||
{"ambiguous", requirement.ambiguous}
|
||||
};
|
||||
}
|
||||
|
||||
static nlohmann::json parsedSpecToJson(const ParsedMarkdownSpec& parsed) {
|
||||
nlohmann::json sections = nlohmann::json::array();
|
||||
for (const auto& section : parsed.sections) {
|
||||
sections.push_back({
|
||||
{"title", section.title},
|
||||
{"anchor", section.anchor},
|
||||
{"headingLine", section.headingLine}
|
||||
});
|
||||
}
|
||||
|
||||
auto itemsToJson = [](const std::vector<ParsedRequirementItem>& items) {
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
for (const auto& item : items) {
|
||||
out.push_back({
|
||||
{"text", item.text},
|
||||
{"sectionTitle", item.sectionTitle},
|
||||
{"anchor", item.anchor},
|
||||
{"line", item.line}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
return {
|
||||
{"sections", sections},
|
||||
{"goals", itemsToJson(parsed.goals)},
|
||||
{"constraints", itemsToJson(parsed.constraints)},
|
||||
{"dependencies", itemsToJson(parsed.dependencies)},
|
||||
{"acceptanceCriteria", itemsToJson(parsed.acceptanceCriteria)}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
struct HeadlessEditorState;
|
||||
#include "HeadlessOrchestratorRPC.h"
|
||||
#include "GenerationQualityGates.h"
|
||||
#include "TaskitemPoolPersistence.h"
|
||||
|
||||
static inline json headlessRpcError(const json& id, int code,
|
||||
const std::string& msg) {
|
||||
|
||||
@@ -592,12 +592,26 @@ private:
|
||||
ms);
|
||||
sessionRecorder_.record(rec);
|
||||
}
|
||||
std::string text = result.dump(2);
|
||||
response["result"] = {
|
||||
{"content", json::array({{{"type", "text"}, {"text", text}}})},
|
||||
{"isError", false},
|
||||
{"whetstoneVersionHeader", buildVersionHeader(toolName)}
|
||||
};
|
||||
if (hasCallError(result)) {
|
||||
const json& err = result["error"];
|
||||
std::string text;
|
||||
if (err.is_object()) text = err.value("message", err.dump(2));
|
||||
else if (err.is_string()) text = err.get<std::string>();
|
||||
else text = err.dump(2);
|
||||
response["result"] = {
|
||||
{"content", json::array({{{"type", "text"}, {"text", text}}})},
|
||||
{"isError", true},
|
||||
{"error", err},
|
||||
{"whetstoneVersionHeader", buildVersionHeader(toolName)}
|
||||
};
|
||||
} else {
|
||||
std::string text = result.dump(2);
|
||||
response["result"] = {
|
||||
{"content", json::array({{{"type", "text"}, {"text", text}}})},
|
||||
{"isError", false},
|
||||
{"whetstoneVersionHeader", buildVersionHeader(toolName)}
|
||||
};
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
response["result"] = {
|
||||
{"content", json::array({{{"type", "text"}, {"text", std::string("Error: ") + e.what()}}})},
|
||||
@@ -708,7 +722,7 @@ private:
|
||||
};
|
||||
json resp = rpcCallback_(request);
|
||||
if (resp.contains("result")) return resp["result"];
|
||||
if (resp.contains("error")) return {{"error", resp["error"]["message"]}};
|
||||
if (resp.contains("error")) return {{"error", resp["error"]}};
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -1165,6 +1179,7 @@ private:
|
||||
#include "mcp/RegisterWorkflowTools.h"
|
||||
#include "mcp/RegisterWorkflowExecutionTools.h"
|
||||
#include "mcp/RegisterRoutingTools.h"
|
||||
#include "mcp/RegisterTaskitemPoolTools.h"
|
||||
#include "mcp/RegisterOrchestratorTools.h"
|
||||
#include "mcp/RegisterReviewTools.h"
|
||||
#include "mcp/RegisterArchitectIntakeTools.h"
|
||||
|
||||
@@ -29,22 +29,48 @@ public:
|
||||
std::string line;
|
||||
std::string sectionTitle;
|
||||
std::string sectionAnchor;
|
||||
std::string proseBuffer;
|
||||
int proseStartLine = 0;
|
||||
int lineNo = 0;
|
||||
|
||||
auto flushProse = [&]() {
|
||||
if (proseBuffer.empty() || !isRequirementHeading(sectionAnchor)) return;
|
||||
ParsedRequirement req;
|
||||
req.text = normalizeText(proseBuffer);
|
||||
req.sourceText = proseBuffer;
|
||||
req.sourceLine = proseStartLine;
|
||||
req.sectionAnchor = sectionAnchor;
|
||||
req.sectionTitle = sectionTitle;
|
||||
out.push_back(req);
|
||||
proseBuffer.clear();
|
||||
proseStartLine = 0;
|
||||
};
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++lineNo;
|
||||
std::string trimmed = trim(line);
|
||||
if (isHeading(trimmed)) {
|
||||
flushProse();
|
||||
sectionTitle = trimHeading(trimmed);
|
||||
sectionAnchor = anchorize(sectionTitle);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isRequirementHeading(sectionAnchor)) continue;
|
||||
if (!isRequirementHeading(sectionAnchor)) {
|
||||
flushProse();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.empty()) {
|
||||
flushProse();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBullet(trimmed)) {
|
||||
flushProse();
|
||||
std::string bulletText = trim(trimmed.substr(2));
|
||||
if (bulletText.empty()) continue;
|
||||
if (isQuotedExampleItem(bulletText)) continue;
|
||||
if (tryParseFieldList(bulletText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
|
||||
ParsedRequirement req;
|
||||
req.text = normalizeText(bulletText);
|
||||
@@ -53,17 +79,38 @@ public:
|
||||
req.sectionAnchor = sectionAnchor;
|
||||
req.sectionTitle = sectionTitle;
|
||||
out.push_back(req);
|
||||
} else if (isProseLine(trimmed)) {
|
||||
} else if (isOrderedList(trimmed)) {
|
||||
flushProse();
|
||||
std::size_t markerEnd = 0;
|
||||
while (markerEnd < trimmed.size() &&
|
||||
std::isdigit(static_cast<unsigned char>(trimmed[markerEnd]))) {
|
||||
++markerEnd;
|
||||
}
|
||||
std::string itemText = trim(trimmed.substr(markerEnd + 1));
|
||||
if (itemText.empty()) continue;
|
||||
if (isQuotedExampleItem(itemText)) continue;
|
||||
ParsedRequirement req;
|
||||
req.text = normalizeText(trimmed);
|
||||
req.sourceText = trimmed;
|
||||
req.text = normalizeText(itemText);
|
||||
req.sourceText = itemText;
|
||||
req.sourceLine = lineNo;
|
||||
req.sectionAnchor = sectionAnchor;
|
||||
req.sectionTitle = sectionTitle;
|
||||
out.push_back(req);
|
||||
} else if (isProseLine(trimmed)) {
|
||||
if (proseBuffer.empty()) {
|
||||
proseBuffer = trimmed;
|
||||
proseStartLine = lineNo;
|
||||
} else {
|
||||
proseBuffer += " ";
|
||||
proseBuffer += trimmed;
|
||||
}
|
||||
} else {
|
||||
flushProse();
|
||||
}
|
||||
}
|
||||
|
||||
flushProse();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -84,12 +131,32 @@ public:
|
||||
}
|
||||
|
||||
static bool isRequirementHeading(const std::string& anchor) {
|
||||
return anchor == "requirements" ||
|
||||
return anchor == "goals" ||
|
||||
anchor == "constraints" ||
|
||||
anchor == "acceptance-criteria" ||
|
||||
anchor == "required-artifacts" ||
|
||||
anchor == "observability" ||
|
||||
anchor == "security-safety" ||
|
||||
anchor == "performance-budgets" ||
|
||||
anchor == "rollout-migration-rollback" ||
|
||||
anchor == "environment-projection-targets" ||
|
||||
anchor == "requirements" ||
|
||||
anchor == "features" ||
|
||||
anchor == "behavior" ||
|
||||
anchor == "behaviour" ||
|
||||
anchor == "interface" ||
|
||||
anchor == "api";
|
||||
anchor == "api" ||
|
||||
containsToken(anchor, "purpose") ||
|
||||
containsToken(anchor, "objective") ||
|
||||
containsToken(anchor, "target") ||
|
||||
containsToken(anchor, "output") ||
|
||||
containsToken(anchor, "deliverable") ||
|
||||
containsToken(anchor, "scope") ||
|
||||
containsToken(anchor, "work") ||
|
||||
containsToken(anchor, "taskitem") ||
|
||||
containsToken(anchor, "execution") ||
|
||||
containsToken(anchor, "canonical") ||
|
||||
containsToken(anchor, "roadmap");
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -125,13 +192,37 @@ private:
|
||||
return (line[0] == '-' || line[0] == '*') && line[1] == ' ';
|
||||
}
|
||||
|
||||
static bool isOrderedList(const std::string& line) {
|
||||
std::size_t i = 0;
|
||||
while (i < line.size() && std::isdigit(static_cast<unsigned char>(line[i]))) ++i;
|
||||
if (i == 0 || i + 1 >= line.size()) return false;
|
||||
if (line[i] != '.' && line[i] != ')') return false;
|
||||
return line[i + 1] == ' ';
|
||||
}
|
||||
|
||||
static bool isProseLine(const std::string& trimmed) {
|
||||
if (trimmed.size() < 10) return false;
|
||||
if (isHeading(trimmed)) return false;
|
||||
if (isOrderedList(trimmed)) return false;
|
||||
if (trimmed[0] == '`' || trimmed[0] == '|' || trimmed[0] == '>') return false;
|
||||
if (!trimmed.empty() && trimmed.back() == ':') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool containsToken(const std::string& text, const std::string& token) {
|
||||
if (text == token) return true;
|
||||
if (text.find(token + "-") != std::string::npos) return true;
|
||||
if (text.find("-" + token) != std::string::npos) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool isQuotedExampleItem(const std::string& text) {
|
||||
if (text.size() < 2) return false;
|
||||
const char first = text.front();
|
||||
const char last = text.back();
|
||||
return ((first == '"' && last == '"') || (first == '\'' && last == '\''));
|
||||
}
|
||||
|
||||
static std::string maybeFieldType(const std::string& token) {
|
||||
std::size_t l = token.find('(');
|
||||
std::size_t r = token.rfind(')');
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
#include "RequirementNormalizationConflictDetector.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct DecomposedWorkstream {
|
||||
std::string workstreamId;
|
||||
std::string title;
|
||||
std::string intent;
|
||||
std::vector<std::string> requirementIds;
|
||||
int uncertaintyScore = 0; // 0..100
|
||||
};
|
||||
@@ -41,10 +43,12 @@ public:
|
||||
DecomposedScopePlan plan;
|
||||
plan.milestones.push_back(buildMilestone("milestone-1", "Intake Foundation", normalized, {
|
||||
NormalizedRequirementKind::Goal,
|
||||
NormalizedRequirementKind::Constraint
|
||||
NormalizedRequirementKind::Requirement,
|
||||
NormalizedRequirementKind::Feature,
|
||||
NormalizedRequirementKind::Field,
|
||||
NormalizedRequirementKind::Dependency
|
||||
}));
|
||||
plan.milestones.push_back(buildMilestone("milestone-2", "Execution Readiness", normalized, {
|
||||
NormalizedRequirementKind::Dependency,
|
||||
NormalizedRequirementKind::Acceptance
|
||||
}));
|
||||
|
||||
@@ -60,6 +64,196 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static bool startsWith(const std::string& text, const std::string& prefix) {
|
||||
return text.rfind(prefix, 0) == 0;
|
||||
}
|
||||
|
||||
static std::string lowerCopy(const std::string& text) {
|
||||
std::string out = text;
|
||||
for (char& c : out) {
|
||||
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isMetadataRequirement(const NormalizedRequirement& requirement) {
|
||||
return startsWith(lowerCopy(requirement.normalizedText), "explicit target file:");
|
||||
}
|
||||
|
||||
static bool containsAny(const std::string& text, const std::vector<std::string>& needles) {
|
||||
for (const auto& needle : needles) {
|
||||
if (text.find(needle) != std::string::npos) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool isPlanningMetaRequirement(const NormalizedRequirement& requirement) {
|
||||
const std::string lower = lowerCopy(requirement.normalizedText);
|
||||
if (requirement.anchor == "environment-projection-targets" ||
|
||||
requirement.anchor == "observability" ||
|
||||
requirement.anchor == "performance-budgets" ||
|
||||
requirement.anchor == "rollout-migration-rollback") {
|
||||
return true;
|
||||
}
|
||||
if (requirement.kind == NormalizedRequirementKind::Constraint &&
|
||||
containsAny(lower, {
|
||||
"do not generate tasks",
|
||||
"generated queue",
|
||||
"task queue",
|
||||
"planning only sprint",
|
||||
"planner should",
|
||||
"queue quality",
|
||||
"planning run",
|
||||
"small worker"
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
if (containsAny(lower, {
|
||||
"active project is",
|
||||
"target queue size is",
|
||||
"p95 planning overhead",
|
||||
"this sprint changes planning artifacts only"
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::string normalizeRequirementSummary(const std::string& text) {
|
||||
std::string cleaned;
|
||||
cleaned.reserve(text.size());
|
||||
bool lastWasSpace = false;
|
||||
for (char c : text) {
|
||||
const bool keep = (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_' || c == '/' || c == '.' || c == '-';
|
||||
if (keep) {
|
||||
cleaned.push_back(c);
|
||||
lastWasSpace = false;
|
||||
} else if (!lastWasSpace) {
|
||||
cleaned.push_back(' ');
|
||||
lastWasSpace = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> words;
|
||||
std::string current;
|
||||
auto flush = [&]() {
|
||||
if (current.empty()) return;
|
||||
words.push_back(current);
|
||||
current.clear();
|
||||
};
|
||||
for (char c : cleaned) {
|
||||
if (c == ' ') flush();
|
||||
else current.push_back(c);
|
||||
}
|
||||
flush();
|
||||
|
||||
std::string summary;
|
||||
int kept = 0;
|
||||
for (const auto& word : words) {
|
||||
const std::string lower = lowerCopy(word);
|
||||
if (lower == "shall" || lower == "should" || lower == "must" || lower == "the") continue;
|
||||
if (!summary.empty()) summary.push_back(' ');
|
||||
summary += word;
|
||||
if (++kept >= 8) break;
|
||||
}
|
||||
return summary.empty() ? text : summary;
|
||||
}
|
||||
|
||||
static std::string workstreamPrefix(NormalizedRequirementKind kind) {
|
||||
if (kind == NormalizedRequirementKind::Goal) return "Plan";
|
||||
if (kind == NormalizedRequirementKind::Requirement) return "Implement";
|
||||
if (kind == NormalizedRequirementKind::Feature) return "Implement feature";
|
||||
if (kind == NormalizedRequirementKind::Field) return "Define field";
|
||||
if (kind == NormalizedRequirementKind::Constraint) return "Apply constraint";
|
||||
if (kind == NormalizedRequirementKind::Dependency) return "Integrate dependency";
|
||||
return "Verify";
|
||||
}
|
||||
|
||||
static std::string workstreamGroupKey(const NormalizedRequirement& requirement) {
|
||||
const std::string sourceText = requirement.sourceText.empty()
|
||||
? requirement.normalizedText
|
||||
: requirement.sourceText;
|
||||
const std::string lower = lowerCopy(sourceText);
|
||||
|
||||
if (requirement.kind == NormalizedRequirementKind::Acceptance) return "verification";
|
||||
if (containsAny(lower, {"smithay", "dependency", "cargo", "substrate"})) return "smithay-bootstrap";
|
||||
if (containsAny(lower, {"focus", "stream", "binding", "coupled", "split", "routing", "state"})) {
|
||||
return "focus-stream-state";
|
||||
}
|
||||
if (containsAny(lower, {"compositor", "wayland", "bootstrap", "scaffold", "runtime"})) {
|
||||
return "compositor-scaffold";
|
||||
}
|
||||
if (containsAny(lower, {"doc", "readme", "roadmap", "vision"})) return "docs-alignment";
|
||||
if (containsAny(lower, {"build", "verify", "validation", "test"})) return "verification";
|
||||
if (requirement.kind == NormalizedRequirementKind::Field) return "state-definitions";
|
||||
return "implementation-foundation";
|
||||
}
|
||||
|
||||
static std::string workstreamGroupTitle(const std::string& key) {
|
||||
if (key == "smithay-bootstrap") return "Bootstrap Smithay workspace";
|
||||
if (key == "compositor-scaffold") return "Scaffold minimal compositor runtime";
|
||||
if (key == "focus-stream-state") return "Define focus-stream state model";
|
||||
if (key == "docs-alignment") return "Align docs with bootstrap architecture";
|
||||
if (key == "verification") return "Define verification and readiness surface";
|
||||
if (key == "state-definitions") return "Define foundational state types";
|
||||
return "Implement initial foundation slice";
|
||||
}
|
||||
|
||||
static DecomposedWorkstream buildGroupedWorkstream(const std::string& workstreamId,
|
||||
const std::string& key,
|
||||
const std::vector<NormalizedRequirement>& requirements) {
|
||||
DecomposedWorkstream workstream;
|
||||
workstream.workstreamId = workstreamId;
|
||||
workstream.title = workstreamGroupTitle(key);
|
||||
std::string intent;
|
||||
for (const auto& requirement : requirements) {
|
||||
const std::string sourceText = requirement.sourceText.empty()
|
||||
? requirement.normalizedText
|
||||
: requirement.sourceText;
|
||||
if (!intent.empty()) intent += " ";
|
||||
intent += sourceText;
|
||||
workstream.requirementIds.push_back(requirement.requirementId);
|
||||
}
|
||||
workstream.intent = intent;
|
||||
workstream.uncertaintyScore = computeUncertainty(requirements);
|
||||
return workstream;
|
||||
}
|
||||
|
||||
static DecomposedWorkstream buildRequirementWorkstream(const std::string& workstreamId,
|
||||
const NormalizedRequirement& requirement) {
|
||||
DecomposedWorkstream workstream;
|
||||
workstream.workstreamId = workstreamId;
|
||||
|
||||
// GR-029 fix (step 1989): use sourceText for intent + title so output
|
||||
// preserves original capitalisation and class names. Fall back to
|
||||
// normalizedText only when sourceText is absent (synthetic requirements).
|
||||
const std::string& textForDisplay = requirement.sourceText.empty()
|
||||
? requirement.normalizedText
|
||||
: requirement.sourceText;
|
||||
workstream.intent = textForDisplay;
|
||||
|
||||
// Build title: strip the leading verb from sourceText when it duplicates
|
||||
// workstreamPrefix (e.g. "Implement ITargetResolver…" → prefix="Implement",
|
||||
// strip leading "Implement " from textForDisplay before summarising).
|
||||
const std::string prefix = workstreamPrefix(requirement.kind);
|
||||
std::string titleSource = textForDisplay;
|
||||
const std::string prefixLower = lowerCopy(prefix);
|
||||
const std::string titleSourceLower = lowerCopy(titleSource);
|
||||
if (titleSourceLower.rfind(prefixLower, 0) == 0 &&
|
||||
titleSource.size() > prefixLower.size() &&
|
||||
titleSource[prefixLower.size()] == ' ') {
|
||||
titleSource = titleSource.substr(prefixLower.size() + 1);
|
||||
}
|
||||
workstream.title = prefix + " " + normalizeRequirementSummary(titleSource);
|
||||
|
||||
workstream.requirementIds.push_back(requirement.requirementId);
|
||||
workstream.uncertaintyScore = computeUncertainty({requirement});
|
||||
return workstream;
|
||||
}
|
||||
|
||||
static DecomposedMilestone buildMilestone(const std::string& milestoneId,
|
||||
const std::string& title,
|
||||
const RequirementNormalizationResult& normalized,
|
||||
@@ -70,27 +264,38 @@ private:
|
||||
|
||||
std::vector<NormalizedRequirement> selected;
|
||||
for (const auto& requirement : normalized.requirements) {
|
||||
if (containsKind(kinds, requirement.kind)) selected.push_back(requirement);
|
||||
if (containsKind(kinds, requirement.kind) &&
|
||||
!isMetadataRequirement(requirement) &&
|
||||
!isPlanningMetaRequirement(requirement)) {
|
||||
selected.push_back(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selected.empty()) {
|
||||
DecomposedWorkstream primary;
|
||||
primary.workstreamId = milestoneId + "-ws-1";
|
||||
primary.title = title + " Primary";
|
||||
for (const auto& requirement : selected) primary.requirementIds.push_back(requirement.requirementId);
|
||||
primary.uncertaintyScore = computeUncertainty(selected);
|
||||
milestone.workstreams.push_back(primary);
|
||||
std::map<std::string, std::vector<NormalizedRequirement>> grouped;
|
||||
for (const auto& requirement : selected) {
|
||||
grouped[workstreamGroupKey(requirement)].push_back(requirement);
|
||||
}
|
||||
|
||||
int extraIndex = 1;
|
||||
for (const auto& kv : grouped) {
|
||||
milestone.workstreams.push_back(
|
||||
buildGroupedWorkstream(milestoneId + "-ws-" + std::to_string(extraIndex++),
|
||||
kv.first,
|
||||
kv.second));
|
||||
}
|
||||
|
||||
DecomposedWorkstream review;
|
||||
review.workstreamId = milestoneId + "-ws-2";
|
||||
review.workstreamId = milestoneId + "-ws-" + std::to_string(extraIndex++);
|
||||
review.title = "Architect Review";
|
||||
review.intent = "Resolve conflicting normalized requirements before execution.";
|
||||
for (const auto& conflict : normalized.conflicts) {
|
||||
review.requirementIds.push_back(conflict.leftRequirementId);
|
||||
review.requirementIds.push_back(conflict.rightRequirementId);
|
||||
}
|
||||
dedupeIds(&review.requirementIds);
|
||||
if (!review.requirementIds.empty()) {
|
||||
review.uncertaintyScore = std::min(100, primary.uncertaintyScore + 20);
|
||||
review.uncertaintyScore = std::min(100, computeUncertainty(selected) + 20);
|
||||
milestone.workstreams.push_back(review);
|
||||
}
|
||||
}
|
||||
@@ -113,6 +318,7 @@ private:
|
||||
if (requirement.ambiguous) score += 15;
|
||||
if (requirement.kind == NormalizedRequirementKind::Constraint) score += 5;
|
||||
if (requirement.kind == NormalizedRequirementKind::Acceptance) score -= 2;
|
||||
if (requirement.kind == NormalizedRequirementKind::Field) score += 1;
|
||||
}
|
||||
if (requirements.size() > 6) score += 10;
|
||||
return std::max(0, std::min(100, score));
|
||||
|
||||
@@ -24,6 +24,9 @@ struct TaskitemInput {
|
||||
std::vector<std::string> dependencyTaskIds;
|
||||
int executionSpecificityScore = 0;
|
||||
bool strictExecutionContract = false;
|
||||
bool hasTestFiles = false;
|
||||
bool hasVerificationType = false;
|
||||
std::string verificationType;
|
||||
bool capabilityCompileFailed = false;
|
||||
bool capabilityTestsFailed = false;
|
||||
bool capabilityToolchainMissing = false;
|
||||
@@ -60,12 +63,6 @@ public:
|
||||
if (item.reasons.empty()) {
|
||||
scoreValue -= 20;
|
||||
out.issues.push_back("reasons missing or empty");
|
||||
} else if (item.reasons.size() == 1) {
|
||||
scoreValue -= 25;
|
||||
out.issues.push_back("reasons has fewer than 2 entries");
|
||||
} else if (item.reasons.size() < 3) {
|
||||
scoreValue -= 10;
|
||||
out.issues.push_back("reasons has fewer than 3 entries");
|
||||
}
|
||||
|
||||
if (item.confidence == 0) {
|
||||
@@ -78,9 +75,9 @@ public:
|
||||
out.issues.push_back("title empty");
|
||||
}
|
||||
|
||||
if (!item.dependencyTaskIds.empty()) {
|
||||
if (item.dependencyTaskIds.size() > 1) {
|
||||
scoreValue -= 5;
|
||||
out.issues.push_back("dependencyTaskIds not empty");
|
||||
out.issues.push_back("dependencyTaskIds include multiple upstream tasks");
|
||||
}
|
||||
|
||||
if (item.strictExecutionContract) {
|
||||
@@ -92,6 +89,22 @@ public:
|
||||
scoreValue -= 10;
|
||||
out.issues.push_back("execution contract metadata missing");
|
||||
}
|
||||
if (!item.hasVerificationType || item.verificationType.empty()) {
|
||||
scoreValue -= 10;
|
||||
out.issues.push_back("verificationType missing from execution contract");
|
||||
} else if (item.verificationType != "unit" &&
|
||||
item.verificationType != "integration" &&
|
||||
item.verificationType != "schema" &&
|
||||
item.verificationType != "smoke" &&
|
||||
item.verificationType != "docs") {
|
||||
scoreValue -= 10;
|
||||
out.issues.push_back("verificationType invalid for execution contract");
|
||||
}
|
||||
if ((item.verificationType == "unit" || item.verificationType == "integration") &&
|
||||
!item.hasTestFiles) {
|
||||
scoreValue -= 15;
|
||||
out.issues.push_back("testFiles missing for code verification contract");
|
||||
}
|
||||
}
|
||||
|
||||
if (item.capabilityCompileFailed || item.capabilityTestsFailed || item.capabilityToolchainMissing) {
|
||||
|
||||
352
editor/src/TaskitemPoolPersistence.h
Normal file
352
editor/src/TaskitemPoolPersistence.h
Normal file
@@ -0,0 +1,352 @@
|
||||
#pragma once
|
||||
// Project-scoped taskitem pool persistence for external agent execution.
|
||||
|
||||
#include "WorkflowPersistence.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <fcntl.h>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
using taskitem_pool_json = nlohmann::json;
|
||||
|
||||
inline std::string taskitemPoolSanitizeProjectName(const std::string& value) {
|
||||
std::string out;
|
||||
out.reserve(value.size());
|
||||
for (char c : value) {
|
||||
const bool keep = (c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '_';
|
||||
out.push_back(keep ? c : '-');
|
||||
}
|
||||
while (!out.empty() && out.front() == '-') out.erase(out.begin());
|
||||
while (!out.empty() && out.back() == '-') out.pop_back();
|
||||
return out.empty() ? "project" : out;
|
||||
}
|
||||
|
||||
inline std::string taskitemPoolInferProjectName(const std::string& workspaceRoot,
|
||||
const std::string& requested) {
|
||||
if (!requested.empty()) return taskitemPoolSanitizeProjectName(requested);
|
||||
namespace fs = std::filesystem;
|
||||
if (!workspaceRoot.empty()) {
|
||||
std::error_code ec;
|
||||
fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec);
|
||||
if (!ec && !root.empty()) {
|
||||
const std::string leaf = root.filename().string();
|
||||
if (!leaf.empty()) return taskitemPoolSanitizeProjectName(leaf);
|
||||
}
|
||||
}
|
||||
return "project";
|
||||
}
|
||||
|
||||
inline std::string taskitemPoolSidecarPath(const std::string& workspaceRoot,
|
||||
const std::string& projectName) {
|
||||
return workspaceRoot + "/.whetstone/" + taskitemPoolSanitizeProjectName(projectName) +
|
||||
".taskitem_pool.json";
|
||||
}
|
||||
|
||||
inline std::string taskitemPoolLockPath(const std::string& workspaceRoot,
|
||||
const std::string& projectName) {
|
||||
return workspaceRoot + "/.whetstone/" + taskitemPoolSanitizeProjectName(projectName) +
|
||||
".taskitem_pool.lock";
|
||||
}
|
||||
|
||||
struct TaskitemPoolLockGuard {
|
||||
int fd = -1;
|
||||
std::string path;
|
||||
|
||||
TaskitemPoolLockGuard() = default;
|
||||
TaskitemPoolLockGuard(const TaskitemPoolLockGuard&) = delete;
|
||||
TaskitemPoolLockGuard& operator=(const TaskitemPoolLockGuard&) = delete;
|
||||
|
||||
TaskitemPoolLockGuard(TaskitemPoolLockGuard&& other) noexcept
|
||||
: fd(other.fd), path(std::move(other.path)) {
|
||||
other.fd = -1;
|
||||
}
|
||||
|
||||
TaskitemPoolLockGuard& operator=(TaskitemPoolLockGuard&& other) noexcept {
|
||||
if (this == &other) return *this;
|
||||
release();
|
||||
fd = other.fd;
|
||||
path = std::move(other.path);
|
||||
other.fd = -1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~TaskitemPoolLockGuard() { release(); }
|
||||
|
||||
bool valid() const { return fd >= 0; }
|
||||
|
||||
void release() {
|
||||
if (fd < 0) return;
|
||||
flock(fd, LOCK_UN);
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::optional<TaskitemPoolLockGuard> acquireTaskitemPoolLock(
|
||||
const std::string& workspaceRoot,
|
||||
const std::string& projectName,
|
||||
bool exclusive,
|
||||
std::string* error = nullptr) {
|
||||
TaskitemPoolLockGuard guard;
|
||||
guard.path = taskitemPoolLockPath(workspaceRoot, projectName);
|
||||
if (!ensureDirectoryExists(guard.path)) {
|
||||
if (error) *error = "taskitem_pool_lock_dir_failed";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
guard.fd = open(guard.path.c_str(), O_RDWR | O_CREAT, 0666);
|
||||
if (guard.fd < 0) {
|
||||
if (error) *error = "taskitem_pool_lock_open_failed";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const int lockMode = exclusive ? LOCK_EX : LOCK_SH;
|
||||
if (flock(guard.fd, lockMode) != 0) {
|
||||
if (error) *error = "taskitem_pool_lock_failed";
|
||||
close(guard.fd);
|
||||
guard.fd = -1;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return guard;
|
||||
}
|
||||
|
||||
struct TaskitemPoolEntry {
|
||||
std::string itemId;
|
||||
std::string taskId;
|
||||
std::string title;
|
||||
std::string intent;
|
||||
std::string milestoneId;
|
||||
std::vector<std::string> requirementIds;
|
||||
std::vector<std::string> dependencyTaskIds;
|
||||
std::vector<std::string> resourceLocks;
|
||||
std::string status = "ready";
|
||||
std::string assignee;
|
||||
std::string workerType;
|
||||
std::string importedAt;
|
||||
std::string claimedAt;
|
||||
std::string completedAt;
|
||||
std::string sourceLabel;
|
||||
taskitem_pool_json executionContract = taskitem_pool_json::object();
|
||||
taskitem_pool_json task = taskitem_pool_json::object();
|
||||
taskitem_pool_json result = taskitem_pool_json::object();
|
||||
|
||||
taskitem_pool_json toJson() const {
|
||||
return {
|
||||
{"itemId", itemId},
|
||||
{"taskId", taskId},
|
||||
{"title", title},
|
||||
{"intent", intent},
|
||||
{"milestoneId", milestoneId},
|
||||
{"requirementIds", requirementIds},
|
||||
{"dependencyTaskIds", dependencyTaskIds},
|
||||
{"resourceLocks", resourceLocks},
|
||||
{"status", status},
|
||||
{"assignee", assignee},
|
||||
{"workerType", workerType},
|
||||
{"importedAt", importedAt},
|
||||
{"claimedAt", claimedAt},
|
||||
{"completedAt", completedAt},
|
||||
{"sourceLabel", sourceLabel},
|
||||
{"executionContract", executionContract},
|
||||
{"task", task},
|
||||
{"result", result}
|
||||
};
|
||||
}
|
||||
|
||||
static TaskitemPoolEntry fromJson(const taskitem_pool_json& j) {
|
||||
TaskitemPoolEntry out;
|
||||
out.itemId = j.value("itemId", "");
|
||||
out.taskId = j.value("taskId", out.itemId);
|
||||
out.title = j.value("title", "");
|
||||
out.intent = j.value("intent", "");
|
||||
out.milestoneId = j.value("milestoneId", "");
|
||||
out.requirementIds = j.value("requirementIds", std::vector<std::string>{});
|
||||
out.dependencyTaskIds = j.value("dependencyTaskIds", std::vector<std::string>{});
|
||||
out.resourceLocks = j.value("resourceLocks", std::vector<std::string>{});
|
||||
out.status = j.value("status", "ready");
|
||||
out.assignee = j.value("assignee", "");
|
||||
out.workerType = j.value("workerType", "");
|
||||
out.importedAt = j.value("importedAt", "");
|
||||
out.claimedAt = j.value("claimedAt", "");
|
||||
out.completedAt = j.value("completedAt", "");
|
||||
out.sourceLabel = j.value("sourceLabel", "");
|
||||
out.executionContract = j.value("executionContract", taskitem_pool_json::object());
|
||||
out.task = j.value("task", taskitem_pool_json::object());
|
||||
out.result = j.value("result", taskitem_pool_json::object());
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
struct TaskitemPoolState {
|
||||
std::string projectName;
|
||||
std::string createdAt;
|
||||
std::string updatedAt;
|
||||
std::vector<TaskitemPoolEntry> items;
|
||||
|
||||
taskitem_pool_json toJson() const {
|
||||
taskitem_pool_json arr = taskitem_pool_json::array();
|
||||
for (const auto& item : items) arr.push_back(item.toJson());
|
||||
return {
|
||||
{"projectName", projectName},
|
||||
{"createdAt", createdAt},
|
||||
{"updatedAt", updatedAt},
|
||||
{"items", arr}
|
||||
};
|
||||
}
|
||||
|
||||
static TaskitemPoolState fromJson(const taskitem_pool_json& j) {
|
||||
TaskitemPoolState state;
|
||||
state.projectName = j.value("projectName", "");
|
||||
state.createdAt = j.value("createdAt", "");
|
||||
state.updatedAt = j.value("updatedAt", "");
|
||||
for (const auto& item : j.value("items", taskitem_pool_json::array())) {
|
||||
if (!item.is_object()) continue;
|
||||
state.items.push_back(TaskitemPoolEntry::fromJson(item));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
struct TaskitemPoolSaveResult {
|
||||
bool success = false;
|
||||
std::string path;
|
||||
int bytesWritten = 0;
|
||||
int itemCount = 0;
|
||||
};
|
||||
|
||||
inline std::optional<TaskitemPoolState> loadTaskitemPool(const std::string& workspaceRoot,
|
||||
const std::string& projectName) {
|
||||
const std::string path = taskitemPoolSidecarPath(workspaceRoot, projectName);
|
||||
std::ifstream ifs(path);
|
||||
if (!ifs.is_open()) return std::nullopt;
|
||||
std::ostringstream oss;
|
||||
oss << ifs.rdbuf();
|
||||
const std::string content = oss.str();
|
||||
if (content.empty()) return std::nullopt;
|
||||
try {
|
||||
return TaskitemPoolState::fromJson(taskitem_pool_json::parse(content));
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
inline TaskitemPoolSaveResult saveTaskitemPool(const std::string& workspaceRoot,
|
||||
const TaskitemPoolState& state) {
|
||||
namespace fs = std::filesystem;
|
||||
TaskitemPoolSaveResult out;
|
||||
out.path = taskitemPoolSidecarPath(workspaceRoot, state.projectName);
|
||||
if (!ensureDirectoryExists(out.path)) return out;
|
||||
const std::string content = state.toJson().dump(2);
|
||||
const std::string tempPath = out.path + ".tmp";
|
||||
std::ofstream ofs(tempPath, std::ios::trunc);
|
||||
if (!ofs.is_open()) return out;
|
||||
ofs << content;
|
||||
ofs.close();
|
||||
if (ofs.fail()) return out;
|
||||
std::error_code ec;
|
||||
fs::rename(fs::path(tempPath), fs::path(out.path), ec);
|
||||
if (ec) {
|
||||
fs::remove(fs::path(out.path), ec);
|
||||
ec.clear();
|
||||
fs::rename(fs::path(tempPath), fs::path(out.path), ec);
|
||||
}
|
||||
if (ec) return out;
|
||||
out.success = true;
|
||||
out.bytesWritten = static_cast<int>(content.size());
|
||||
out.itemCount = static_cast<int>(state.items.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
inline taskitem_pool_json taskitemPoolCounts(const TaskitemPoolState& state) {
|
||||
std::map<std::string, int> counts;
|
||||
for (const auto& item : state.items) counts[item.status] += 1;
|
||||
taskitem_pool_json out = taskitem_pool_json::object();
|
||||
out["total"] = static_cast<int>(state.items.size());
|
||||
for (const auto& kv : counts) out[kv.first] = kv.second;
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::optional<taskitem_pool_json> loadTaskitemImportPayload(const std::string& workspaceRoot,
|
||||
const std::string& path,
|
||||
std::string* error) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path input(path);
|
||||
if (input.is_relative()) input = fs::path(workspaceRoot) / input;
|
||||
std::ifstream ifs(input);
|
||||
if (!ifs.is_open()) {
|
||||
if (error) *error = "taskitems_file_not_found";
|
||||
return std::nullopt;
|
||||
}
|
||||
std::ostringstream oss;
|
||||
oss << ifs.rdbuf();
|
||||
try {
|
||||
return taskitem_pool_json::parse(oss.str());
|
||||
} catch (...) {
|
||||
if (error) *error = "taskitems_file_parse_failed";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
inline taskitem_pool_json extractTaskitemArray(const taskitem_pool_json& payload) {
|
||||
if (payload.is_array()) return payload;
|
||||
if (payload.is_object() && payload.contains("tasks") && payload["tasks"].is_array()) {
|
||||
return payload["tasks"];
|
||||
}
|
||||
return taskitem_pool_json::array();
|
||||
}
|
||||
|
||||
inline TaskitemPoolEntry taskitemPoolEntryFromTask(const taskitem_pool_json& task,
|
||||
const std::string& sourceLabel) {
|
||||
TaskitemPoolEntry out;
|
||||
out.itemId = task.value("taskId", "");
|
||||
out.taskId = out.itemId;
|
||||
out.title = task.value("title", "");
|
||||
out.intent = task.value("intent", "");
|
||||
out.milestoneId = task.value("milestoneId", "");
|
||||
out.requirementIds = task.value("requirementIds", std::vector<std::string>{});
|
||||
out.dependencyTaskIds = task.value("dependencyTaskIds", std::vector<std::string>{});
|
||||
out.resourceLocks = task.value("resourceLocks", std::vector<std::string>{});
|
||||
out.status = task.value("queueReady", true) ? "ready" : "blocked";
|
||||
out.importedAt = workItemTimestamp();
|
||||
out.sourceLabel = sourceLabel;
|
||||
out.executionContract = task.value("executionContract", taskitem_pool_json::object());
|
||||
out.task = task;
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool taskitemDependenciesSatisfied(const TaskitemPoolState& state,
|
||||
const TaskitemPoolEntry& item) {
|
||||
if (item.dependencyTaskIds.empty()) return true;
|
||||
std::map<std::string, std::string> statusByTaskId;
|
||||
for (const auto& candidate : state.items) statusByTaskId[candidate.taskId] = candidate.status;
|
||||
for (const auto& dep : item.dependencyTaskIds) {
|
||||
auto it = statusByTaskId.find(dep);
|
||||
if (it == statusByTaskId.end()) continue;
|
||||
if (it->second != "complete") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void recomputeTaskitemPoolStatuses(TaskitemPoolState* state) {
|
||||
if (!state) return;
|
||||
for (auto& item : state->items) {
|
||||
if (item.status == "complete" || item.status == "claimed") continue;
|
||||
item.status = taskitemDependenciesSatisfied(*state, item) ? "ready" : "blocked";
|
||||
}
|
||||
state->updatedAt = workItemTimestamp();
|
||||
}
|
||||
@@ -265,6 +265,306 @@
|
||||
});
|
||||
}
|
||||
|
||||
auto resolveTaskitemPoolProject = [&](const json& params) {
|
||||
const std::string requested = params.value("projectName", "");
|
||||
return taskitemPoolInferProjectName(state.workspaceRoot, requested);
|
||||
};
|
||||
|
||||
// --- importTaskitemPool ---
|
||||
if (method == "importTaskitemPool") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (state.workspaceRoot.empty())
|
||||
return headlessRpcError(id, -32000, "No workspace root");
|
||||
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
const std::string projectName = resolveTaskitemPoolProject(params);
|
||||
const bool replace = params.value("replace", false);
|
||||
const std::string sourceLabel = params.value("sourceLabel", params.value("taskitemsPath", ""));
|
||||
std::string lockError;
|
||||
auto poolLock = acquireTaskitemPoolLock(state.workspaceRoot, projectName, true, &lockError);
|
||||
if (!poolLock.has_value())
|
||||
return headlessRpcError(id, -32000, lockError);
|
||||
|
||||
taskitem_pool_json payload = params.value("taskitems", taskitem_pool_json::array());
|
||||
if ((!payload.is_array() || payload.empty()) && params.value("taskitemsPath", std::string()).size() > 0) {
|
||||
std::string importError;
|
||||
auto loaded = loadTaskitemImportPayload(state.workspaceRoot,
|
||||
params.value("taskitemsPath", ""),
|
||||
&importError);
|
||||
if (!loaded.has_value()) return headlessRpcError(id, -32000, importError);
|
||||
payload = loaded.value();
|
||||
}
|
||||
|
||||
taskitem_pool_json tasks = extractTaskitemArray(payload);
|
||||
if (!tasks.is_array() || tasks.empty())
|
||||
return headlessRpcError(id, -32602, "No taskitems to import");
|
||||
|
||||
TaskitemPoolState pool;
|
||||
if (auto existing = loadTaskitemPool(state.workspaceRoot, projectName); existing.has_value()) {
|
||||
pool = existing.value();
|
||||
} else {
|
||||
pool.projectName = projectName;
|
||||
pool.createdAt = workItemTimestamp();
|
||||
}
|
||||
if (replace) pool.items.clear();
|
||||
|
||||
int importedCount = 0;
|
||||
int updatedCount = 0;
|
||||
for (const auto& task : tasks) {
|
||||
if (!task.is_object()) continue;
|
||||
TaskitemPoolEntry entry = taskitemPoolEntryFromTask(task, sourceLabel);
|
||||
if (entry.itemId.empty()) continue;
|
||||
|
||||
bool replaced = false;
|
||||
for (auto& existing : pool.items) {
|
||||
if (existing.itemId != entry.itemId) continue;
|
||||
const std::string priorStatus = existing.status;
|
||||
const std::string priorAssignee = existing.assignee;
|
||||
const std::string priorClaimedAt = existing.claimedAt;
|
||||
const std::string priorCompletedAt = existing.completedAt;
|
||||
const taskitem_pool_json priorResult = existing.result;
|
||||
existing = entry;
|
||||
if (!replace && (priorStatus == "claimed" || priorStatus == "complete")) {
|
||||
existing.status = priorStatus;
|
||||
existing.assignee = priorAssignee;
|
||||
existing.claimedAt = priorClaimedAt;
|
||||
existing.completedAt = priorCompletedAt;
|
||||
existing.result = priorResult;
|
||||
}
|
||||
replaced = true;
|
||||
++updatedCount;
|
||||
break;
|
||||
}
|
||||
if (!replaced) {
|
||||
pool.items.push_back(entry);
|
||||
++importedCount;
|
||||
}
|
||||
}
|
||||
|
||||
recomputeTaskitemPoolStatuses(&pool);
|
||||
auto saveResult = saveTaskitemPool(state.workspaceRoot, pool);
|
||||
if (!saveResult.success) {
|
||||
return headlessRpcError(id, -32000, "Unable to save taskitem pool");
|
||||
}
|
||||
state.eventStream.emit({
|
||||
"taskitem_pool.imported",
|
||||
"",
|
||||
{
|
||||
{"projectName", projectName},
|
||||
{"importedCount", importedCount},
|
||||
{"updatedCount", updatedCount},
|
||||
{"counts", taskitemPoolCounts(pool)}
|
||||
},
|
||||
workItemTimestamp()
|
||||
});
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true},
|
||||
{"projectName", projectName},
|
||||
{"path", saveResult.path},
|
||||
{"importedCount", importedCount},
|
||||
{"updatedCount", updatedCount},
|
||||
{"itemCount", saveResult.itemCount},
|
||||
{"counts", taskitemPoolCounts(pool)}
|
||||
});
|
||||
}
|
||||
|
||||
// --- listTaskitemPool ---
|
||||
if (method == "listTaskitemPool") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (state.workspaceRoot.empty())
|
||||
return headlessRpcError(id, -32000, "No workspace root");
|
||||
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
const std::string projectName = resolveTaskitemPoolProject(params);
|
||||
std::string lockError;
|
||||
auto poolLock = acquireTaskitemPoolLock(state.workspaceRoot, projectName, false, &lockError);
|
||||
if (!poolLock.has_value())
|
||||
return headlessRpcError(id, -32000, lockError);
|
||||
auto pool = loadTaskitemPool(state.workspaceRoot, projectName);
|
||||
if (!pool.has_value())
|
||||
return headlessRpcError(id, -32000, "Taskitem pool not found");
|
||||
|
||||
recomputeTaskitemPoolStatuses(&pool.value());
|
||||
const std::string statusFilter = params.value("status", "");
|
||||
json items = json::array();
|
||||
for (const auto& item : pool->items) {
|
||||
if (!statusFilter.empty() && item.status != statusFilter) continue;
|
||||
items.push_back(item.toJson());
|
||||
}
|
||||
return headlessRpcResult(id, {
|
||||
{"projectName", projectName},
|
||||
{"items", items},
|
||||
{"count", (int)items.size()},
|
||||
{"counts", taskitemPoolCounts(*pool)}
|
||||
});
|
||||
}
|
||||
|
||||
// --- claimTaskitem ---
|
||||
if (method == "claimTaskitem") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (state.workspaceRoot.empty())
|
||||
return headlessRpcError(id, -32000, "No workspace root");
|
||||
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
const std::string projectName = resolveTaskitemPoolProject(params);
|
||||
const std::string itemId = params.value("itemId", "");
|
||||
const std::string assignee = params.value("assignee", "");
|
||||
if (itemId.empty() || assignee.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId or assignee");
|
||||
std::string lockError;
|
||||
auto poolLock = acquireTaskitemPoolLock(state.workspaceRoot, projectName, true, &lockError);
|
||||
if (!poolLock.has_value())
|
||||
return headlessRpcError(id, -32000, lockError);
|
||||
|
||||
auto pool = loadTaskitemPool(state.workspaceRoot, projectName);
|
||||
if (!pool.has_value())
|
||||
return headlessRpcError(id, -32000, "Taskitem pool not found");
|
||||
|
||||
recomputeTaskitemPoolStatuses(&pool.value());
|
||||
TaskitemPoolEntry* claimed = nullptr;
|
||||
for (auto& item : pool->items) {
|
||||
if (item.itemId != itemId) continue;
|
||||
if (item.status != "ready")
|
||||
return headlessRpcError(id, -32000, "Taskitem not ready");
|
||||
item.status = "claimed";
|
||||
item.assignee = assignee;
|
||||
item.workerType = params.value("workerType", item.workerType.empty() ? "llm" : item.workerType);
|
||||
item.claimedAt = workItemTimestamp();
|
||||
claimed = &item;
|
||||
break;
|
||||
}
|
||||
if (!claimed)
|
||||
return headlessRpcError(id, -32602, "Taskitem not found");
|
||||
|
||||
auto saveResult = saveTaskitemPool(state.workspaceRoot, *pool);
|
||||
if (!saveResult.success)
|
||||
return headlessRpcError(id, -32000, "Unable to save taskitem pool");
|
||||
state.eventStream.emit({
|
||||
"taskitem_pool.claimed",
|
||||
itemId,
|
||||
{{"projectName", projectName}, {"assignee", assignee}, {"workerType", claimed->workerType}},
|
||||
workItemTimestamp()
|
||||
});
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true},
|
||||
{"projectName", projectName},
|
||||
{"item", claimed->toJson()},
|
||||
{"counts", taskitemPoolCounts(*pool)}
|
||||
});
|
||||
}
|
||||
|
||||
// --- releaseTaskitem ---
|
||||
if (method == "releaseTaskitem") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (state.workspaceRoot.empty())
|
||||
return headlessRpcError(id, -32000, "No workspace root");
|
||||
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
const std::string projectName = resolveTaskitemPoolProject(params);
|
||||
const std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
std::string lockError;
|
||||
auto poolLock = acquireTaskitemPoolLock(state.workspaceRoot, projectName, true, &lockError);
|
||||
if (!poolLock.has_value())
|
||||
return headlessRpcError(id, -32000, lockError);
|
||||
|
||||
auto pool = loadTaskitemPool(state.workspaceRoot, projectName);
|
||||
if (!pool.has_value())
|
||||
return headlessRpcError(id, -32000, "Taskitem pool not found");
|
||||
|
||||
TaskitemPoolEntry* released = nullptr;
|
||||
for (auto& item : pool->items) {
|
||||
if (item.itemId != itemId) continue;
|
||||
if (item.status != "claimed")
|
||||
return headlessRpcError(id, -32000, "Taskitem not claimed");
|
||||
const std::string expectedAssignee = params.value("assignee", "");
|
||||
if (!expectedAssignee.empty() && item.assignee != expectedAssignee)
|
||||
return headlessRpcError(id, -32000, "Taskitem claimed by different assignee");
|
||||
item.status = "ready";
|
||||
item.assignee.clear();
|
||||
item.claimedAt.clear();
|
||||
released = &item;
|
||||
break;
|
||||
}
|
||||
if (!released)
|
||||
return headlessRpcError(id, -32602, "Taskitem not found");
|
||||
|
||||
recomputeTaskitemPoolStatuses(&pool.value());
|
||||
auto saveResult = saveTaskitemPool(state.workspaceRoot, *pool);
|
||||
if (!saveResult.success)
|
||||
return headlessRpcError(id, -32000, "Unable to save taskitem pool");
|
||||
state.eventStream.emit({
|
||||
"taskitem_pool.released",
|
||||
itemId,
|
||||
{{"projectName", projectName}, {"reason", params.value("reason", "")}},
|
||||
workItemTimestamp()
|
||||
});
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true},
|
||||
{"projectName", projectName},
|
||||
{"item", released->toJson()},
|
||||
{"counts", taskitemPoolCounts(*pool)}
|
||||
});
|
||||
}
|
||||
|
||||
// --- completePooledTaskitem ---
|
||||
if (method == "completePooledTaskitem") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (state.workspaceRoot.empty())
|
||||
return headlessRpcError(id, -32000, "No workspace root");
|
||||
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
const std::string projectName = resolveTaskitemPoolProject(params);
|
||||
const std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
std::string lockError;
|
||||
auto poolLock = acquireTaskitemPoolLock(state.workspaceRoot, projectName, true, &lockError);
|
||||
if (!poolLock.has_value())
|
||||
return headlessRpcError(id, -32000, lockError);
|
||||
|
||||
auto pool = loadTaskitemPool(state.workspaceRoot, projectName);
|
||||
if (!pool.has_value())
|
||||
return headlessRpcError(id, -32000, "Taskitem pool not found");
|
||||
|
||||
TaskitemPoolEntry* completed = nullptr;
|
||||
for (auto& item : pool->items) {
|
||||
if (item.itemId != itemId) continue;
|
||||
if (item.status != "claimed" && item.status != "ready")
|
||||
return headlessRpcError(id, -32000, "Taskitem not claimable for completion");
|
||||
item.status = "complete";
|
||||
item.completedAt = workItemTimestamp();
|
||||
item.result = params.value("result", json::object());
|
||||
completed = &item;
|
||||
break;
|
||||
}
|
||||
if (!completed)
|
||||
return headlessRpcError(id, -32602, "Taskitem not found");
|
||||
|
||||
recomputeTaskitemPoolStatuses(&pool.value());
|
||||
auto saveResult = saveTaskitemPool(state.workspaceRoot, *pool);
|
||||
if (!saveResult.success)
|
||||
return headlessRpcError(id, -32000, "Unable to save taskitem pool");
|
||||
state.eventStream.emit({
|
||||
"taskitem_pool.completed",
|
||||
itemId,
|
||||
{{"projectName", projectName}},
|
||||
workItemTimestamp()
|
||||
});
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true},
|
||||
{"projectName", projectName},
|
||||
{"item", completed->toJson()},
|
||||
{"counts", taskitemPoolCounts(*pool)}
|
||||
});
|
||||
}
|
||||
|
||||
// --- createWorkflow ---
|
||||
if (method == "createWorkflow") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
{"libraryPolicy", {{"type", "object"},
|
||||
{"description", "Library selection policy: mode=strict|hybrid, expansionMode=auto_include|ask_user|suggest_options."}}},
|
||||
{"strictExecutionContract", {{"type", "boolean"},
|
||||
{"description", "Require concrete execution contract metadata (stepIds/files/tools/tests)."}}}
|
||||
{"description", "Require concrete execution contract metadata (stepIds/files/tools/tests)."}}},
|
||||
{"workspace", {{"type", "string"},
|
||||
{"description", "Optional active workspace root for workspace-aware target inference."}}}
|
||||
}}, {"required", json::array({"normalizedRequirements"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_generate_taskitems"] =
|
||||
@@ -199,6 +201,19 @@
|
||||
c == '_' || c == '/' || c == '.' || c == '-';
|
||||
}
|
||||
|
||||
static bool containsStandaloneToken(const std::string& text, const std::string& token) {
|
||||
if (text.empty() || token.empty()) return false;
|
||||
std::size_t pos = 0;
|
||||
while ((pos = text.find(token, pos)) != std::string::npos) {
|
||||
const bool leftOk = pos == 0 || !isTokenChar(text[pos - 1]);
|
||||
const std::size_t end = pos + token.size();
|
||||
const bool rightOk = end >= text.size() || !isTokenChar(text[end]);
|
||||
if (leftOk && rightOk) return true;
|
||||
pos = end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void appendUnique(std::vector<std::string>* out, const std::string& value) {
|
||||
if (!out || value.empty()) return;
|
||||
for (const auto& existing : *out) {
|
||||
@@ -347,7 +362,8 @@
|
||||
std::vector<std::string> files;
|
||||
std::vector<std::string> tools;
|
||||
std::vector<std::string> commands;
|
||||
std::vector<std::string> repoSignals;
|
||||
std::vector<std::string> repoSignalCandidates;
|
||||
bool explicitCrossProjectIntent = false;
|
||||
|
||||
const std::vector<std::string> knownCrossProjectRepos = {
|
||||
"cad_orchestrator",
|
||||
@@ -359,8 +375,18 @@
|
||||
};
|
||||
|
||||
for (const auto& requirement : requirements) {
|
||||
const std::string sourceText = requirement.sourceText.empty()
|
||||
? requirement.normalizedText
|
||||
: requirement.sourceText;
|
||||
const std::string text = requirement.normalizedText;
|
||||
const std::string lower = intakeToLowerCopy(text);
|
||||
const std::string lower = intakeToLowerCopy(sourceText);
|
||||
|
||||
if (lower.find("cross-repo") != std::string::npos ||
|
||||
lower.find("multi-project") != std::string::npos ||
|
||||
lower.find("multi-repo") != std::string::npos ||
|
||||
lower.find("ecosystem") != std::string::npos) {
|
||||
explicitCrossProjectIntent = true;
|
||||
}
|
||||
|
||||
std::size_t pos = 0;
|
||||
while ((pos = lower.find("step ", pos)) != std::string::npos) {
|
||||
@@ -383,16 +409,16 @@
|
||||
}
|
||||
|
||||
pos = 0;
|
||||
while ((pos = text.find('`', pos)) != std::string::npos) {
|
||||
std::size_t end = text.find('`', pos + 1);
|
||||
while ((pos = sourceText.find('`', pos)) != std::string::npos) {
|
||||
std::size_t end = sourceText.find('`', pos + 1);
|
||||
if (end == std::string::npos) break;
|
||||
std::string candidate = text.substr(pos + 1, end - pos - 1);
|
||||
std::string candidate = sourceText.substr(pos + 1, end - pos - 1);
|
||||
std::string normalizedPath = normalizeTargetFileCandidate(candidate);
|
||||
if (!normalizedPath.empty()) appendUnique(&files, normalizedPath);
|
||||
pos = end + 1;
|
||||
}
|
||||
|
||||
const auto inlinePathTokens = extractPathTokensFromText(text);
|
||||
const auto inlinePathTokens = extractPathTokensFromText(sourceText);
|
||||
for (const auto& p : inlinePathTokens) appendUnique(&files, p);
|
||||
|
||||
pos = 0;
|
||||
@@ -413,10 +439,15 @@
|
||||
if (lower.find("cmake --build") != std::string::npos) appendUnique(&commands, "cmake --build .");
|
||||
|
||||
for (const auto& repo : knownCrossProjectRepos) {
|
||||
if (lower.find(repo) != std::string::npos) appendUnique(&repoSignals, repo);
|
||||
if (containsStandaloneToken(lower, repo)) appendUnique(&repoSignalCandidates, repo);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> repoSignals;
|
||||
if (explicitCrossProjectIntent || sortedUnique(repoSignalCandidates).size() >= 2) {
|
||||
repoSignals = sortedUnique(repoSignalCandidates);
|
||||
}
|
||||
|
||||
return {
|
||||
{"stepIds", sortedUnique(stepIds)},
|
||||
{"targetFiles", sortedUnique(files)},
|
||||
@@ -456,6 +487,73 @@
|
||||
return sortedUnique(targets);
|
||||
}
|
||||
|
||||
static json inferWorkspaceFallbackTargets(const std::string& workspaceRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
if (workspaceRoot.empty()) return json::array();
|
||||
std::error_code ec;
|
||||
fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec);
|
||||
if (ec || root.empty() || !fs::exists(root, ec) || !fs::is_directory(root, ec)) {
|
||||
return json::array();
|
||||
}
|
||||
|
||||
std::vector<std::string> targets;
|
||||
auto appendIfExists = [&](const fs::path& rel) {
|
||||
if (targets.size() >= 4) return;
|
||||
std::error_code localEc;
|
||||
const fs::path abs = root / rel;
|
||||
if (fs::exists(abs, localEc) && fs::is_regular_file(abs, localEc)) {
|
||||
appendUnique(&targets, rel.generic_string());
|
||||
}
|
||||
};
|
||||
|
||||
appendIfExists("Cargo.toml");
|
||||
appendIfExists("pyproject.toml");
|
||||
appendIfExists("package.json");
|
||||
appendIfExists("go.mod");
|
||||
appendIfExists("CMakeLists.txt");
|
||||
appendIfExists("README.md");
|
||||
appendIfExists("src/main.rs");
|
||||
appendIfExists("src/lib.rs");
|
||||
|
||||
if (targets.size() < 4) {
|
||||
const std::vector<fs::path> preferredNested = {
|
||||
fs::path("crates") / "whimpwm" / "Cargo.toml",
|
||||
fs::path("crates") / "whimpwm" / "src" / "main.rs",
|
||||
fs::path("crates") / "whimpwm" / "src" / "lib.rs"
|
||||
};
|
||||
for (const auto& rel : preferredNested) appendIfExists(rel);
|
||||
}
|
||||
|
||||
if (targets.size() < 4) {
|
||||
auto endsWithLocal = [](const std::string& value, const std::string& suffix) {
|
||||
return value.size() >= suffix.size() &&
|
||||
value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
};
|
||||
for (fs::recursive_directory_iterator it(root, ec), end; !ec && it != end && targets.size() < 4; it.increment(ec)) {
|
||||
if (ec) break;
|
||||
if (!it->is_regular_file(ec)) continue;
|
||||
const fs::path rel = it->path().lexically_relative(root);
|
||||
const std::string relStr = rel.generic_string();
|
||||
if (relStr.rfind(".git/", 0) == 0 || relStr.rfind("build/", 0) == 0 || relStr.rfind("target/", 0) == 0) {
|
||||
continue;
|
||||
}
|
||||
const std::string lower = intakeToLowerCopy(relStr);
|
||||
const bool interesting = endsWithLocal(lower, ".rs") ||
|
||||
lower == "readme.md" ||
|
||||
lower == "cargo.toml" ||
|
||||
lower == "package.json" ||
|
||||
lower == "pyproject.toml" ||
|
||||
lower == "go.mod" ||
|
||||
lower == "cmakelists.txt";
|
||||
if (interesting) appendUnique(&targets, relStr);
|
||||
}
|
||||
}
|
||||
|
||||
json out = json::array();
|
||||
for (const auto& file : sortedUnique(targets)) out.push_back(file);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool hasCrossProjectIntent(const std::vector<NormalizedRequirement>& requirements) {
|
||||
int hitCount = 0;
|
||||
for (const auto& requirement : requirements) {
|
||||
@@ -466,10 +564,10 @@
|
||||
lower.find("ecosystem") != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
if (lower.find("cad_orchestrator") != std::string::npos) ++hitCount;
|
||||
if (lower.find("hivemind") != std::string::npos) ++hitCount;
|
||||
if (lower.find("gleaner") != std::string::npos) ++hitCount;
|
||||
if (lower.find("constcad") != std::string::npos) ++hitCount;
|
||||
if (containsStandaloneToken(lower, "cad_orchestrator")) ++hitCount;
|
||||
if (containsStandaloneToken(lower, "hivemind")) ++hitCount;
|
||||
if (containsStandaloneToken(lower, "gleaner")) ++hitCount;
|
||||
if (containsStandaloneToken(lower, "constcad")) ++hitCount;
|
||||
if (hitCount >= 2) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -500,26 +598,29 @@
|
||||
return normalizeTargetFileCandidate(raw);
|
||||
}
|
||||
|
||||
static std::string requirementIdFromSliceTitle(const std::string& title) {
|
||||
const std::string prefix = "Requirement Slice ";
|
||||
if (title.rfind(prefix, 0) != 0) return "";
|
||||
std::string id = title.substr(prefix.size());
|
||||
return trimPathToken(id);
|
||||
}
|
||||
|
||||
static json selectTaskScopedTargetFiles(const AnnotatedTaskitem& task,
|
||||
const std::vector<NormalizedRequirement>& requirements,
|
||||
const json& defaultTargetFiles) {
|
||||
std::vector<std::string> scoped;
|
||||
const std::string sliceRequirementId = requirementIdFromSliceTitle(task.base.title);
|
||||
for (const auto& requirement : requirements) {
|
||||
const std::string file = parseExplicitTargetFileFromRequirementText(requirement.normalizedText);
|
||||
if (file.empty()) continue;
|
||||
const bool exactMatch = !sliceRequirementId.empty() && requirement.requirementId == sliceRequirementId;
|
||||
const bool containsMatch = !sliceRequirementId.empty() &&
|
||||
task.base.title.find(requirement.requirementId) != std::string::npos;
|
||||
if (exactMatch || containsMatch) {
|
||||
appendUnique(&scoped, file);
|
||||
bool matchesTask = false;
|
||||
for (const auto& requirementId : task.base.requirementIds) {
|
||||
if (requirementId == requirement.requirementId) {
|
||||
matchesTask = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchesTask) continue;
|
||||
|
||||
const std::string sourceText = requirement.sourceText.empty()
|
||||
? requirement.normalizedText
|
||||
: requirement.sourceText;
|
||||
const std::string file = parseExplicitTargetFileFromRequirementText(sourceText);
|
||||
if (!file.empty()) appendUnique(&scoped, file);
|
||||
|
||||
const auto inlinePathTokens = extractPathTokensFromText(sourceText);
|
||||
for (const auto& path : inlinePathTokens) {
|
||||
appendUnique(&scoped, path);
|
||||
}
|
||||
}
|
||||
if (scoped.empty()) return defaultTargetFiles;
|
||||
@@ -528,6 +629,68 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
static json inferWorkspaceTaskTargets(const AnnotatedTaskitem& task,
|
||||
const std::string& workspaceRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
std::vector<std::string> targets;
|
||||
const std::string lowerTitle = intakeToLowerCopy(task.base.title + " " + task.base.intent);
|
||||
const fs::path root(workspaceRoot);
|
||||
const bool hasCargoWorkspace = !workspaceRoot.empty() && fs::exists(root / "Cargo.toml");
|
||||
const fs::path crateCargo = root / "crates" / "whimpwm" / "Cargo.toml";
|
||||
const fs::path crateMain = root / "crates" / "whimpwm" / "src" / "main.rs";
|
||||
const fs::path crateFocus = root / "crates" / "whimpwm" / "src" / "focus_streams.rs";
|
||||
const fs::path crateCompositor = root / "crates" / "whimpwm" / "src" / "compositor.rs";
|
||||
const fs::path readme = root / "README.md";
|
||||
const fs::path vision = root / "docs" / "vision.md";
|
||||
|
||||
auto pushRelative = [&](const fs::path& path) {
|
||||
if (workspaceRoot.empty()) return;
|
||||
std::error_code ec;
|
||||
const fs::path rel = fs::relative(path, root, ec);
|
||||
if (ec) return;
|
||||
appendUnique(&targets, rel.generic_string());
|
||||
};
|
||||
|
||||
if (containsStandaloneToken(lowerTitle, "smithay") ||
|
||||
lowerTitle.find("dependency") != std::string::npos ||
|
||||
lowerTitle.find("cargo") != std::string::npos) {
|
||||
if (hasCargoWorkspace) pushRelative(root / "Cargo.toml");
|
||||
if (fs::exists(crateCargo)) pushRelative(crateCargo);
|
||||
}
|
||||
if (lowerTitle.find("compositor") != std::string::npos ||
|
||||
lowerTitle.find("wayland") != std::string::npos ||
|
||||
lowerTitle.find("scaffold") != std::string::npos ||
|
||||
lowerTitle.find("runtime") != std::string::npos) {
|
||||
if (fs::exists(crateMain)) pushRelative(crateMain);
|
||||
pushRelative(crateCompositor);
|
||||
}
|
||||
if (lowerTitle.find("focus") != std::string::npos ||
|
||||
lowerTitle.find("stream") != std::string::npos ||
|
||||
lowerTitle.find("binding") != std::string::npos ||
|
||||
lowerTitle.find("coupled") != std::string::npos ||
|
||||
lowerTitle.find("split") != std::string::npos ||
|
||||
lowerTitle.find("state") != std::string::npos) {
|
||||
if (fs::exists(crateMain)) pushRelative(crateMain);
|
||||
pushRelative(crateFocus);
|
||||
}
|
||||
if (lowerTitle.find("doc") != std::string::npos ||
|
||||
lowerTitle.find("readme") != std::string::npos ||
|
||||
lowerTitle.find("vision") != std::string::npos) {
|
||||
if (fs::exists(readme)) pushRelative(readme);
|
||||
if (fs::exists(vision)) pushRelative(vision);
|
||||
}
|
||||
if (lowerTitle.find("verify") != std::string::npos ||
|
||||
lowerTitle.find("build") != std::string::npos ||
|
||||
lowerTitle.find("test") != std::string::npos) {
|
||||
if (hasCargoWorkspace) pushRelative(root / "Cargo.toml");
|
||||
if (fs::exists(crateMain)) pushRelative(crateMain);
|
||||
}
|
||||
|
||||
json out = json::array();
|
||||
for (const auto& file : sortedUnique(targets)) out.push_back(file);
|
||||
return out;
|
||||
}
|
||||
|
||||
static json inferResourceLocks(const AnnotatedTaskitem& task) {
|
||||
std::vector<std::string> locks;
|
||||
const std::string title = intakeToLowerCopy(task.base.title);
|
||||
@@ -549,26 +712,229 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool endsWith(const std::string& value, const std::string& suffix) {
|
||||
return value.size() >= suffix.size() &&
|
||||
value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
|
||||
static bool isCodeTargetFile(const std::string& path) {
|
||||
return endsWith(path, ".cpp") || endsWith(path, ".cc") || endsWith(path, ".cxx") ||
|
||||
endsWith(path, ".c") || endsWith(path, ".h") || endsWith(path, ".hpp") ||
|
||||
endsWith(path, ".py") || endsWith(path, ".rs") || endsWith(path, ".ts") ||
|
||||
endsWith(path, ".tsx") || endsWith(path, ".js") || endsWith(path, ".jsx") ||
|
||||
endsWith(path, ".go");
|
||||
}
|
||||
|
||||
static bool isSchemaTargetFile(const std::string& path) {
|
||||
return endsWith(path, ".schema.json") ||
|
||||
(endsWith(path, ".json") && intakeToLowerCopy(path).find("schema") != std::string::npos);
|
||||
}
|
||||
|
||||
static bool isDocTargetFile(const std::string& path) {
|
||||
return endsWith(path, ".md") || endsWith(path, ".txt") || endsWith(path, ".rst");
|
||||
}
|
||||
|
||||
static std::string pathStem(const std::string& path) {
|
||||
std::size_t slash = path.find_last_of('/');
|
||||
const std::string leaf = slash == std::string::npos ? path : path.substr(slash + 1);
|
||||
std::size_t dot = leaf.find('.');
|
||||
return dot == std::string::npos ? leaf : leaf.substr(0, dot);
|
||||
}
|
||||
|
||||
static std::string inferVerificationType(const AnnotatedTaskitem& task, const json& targetFiles) {
|
||||
bool sawCode = false;
|
||||
bool sawSchema = false;
|
||||
bool sawDocs = false;
|
||||
if (targetFiles.is_array()) {
|
||||
for (const auto& fileJson : targetFiles) {
|
||||
if (!fileJson.is_string()) continue;
|
||||
const std::string path = fileJson.get<std::string>();
|
||||
sawCode = sawCode || isCodeTargetFile(path);
|
||||
sawSchema = sawSchema || isSchemaTargetFile(path);
|
||||
sawDocs = sawDocs || isDocTargetFile(path);
|
||||
}
|
||||
}
|
||||
if (sawCode) return "unit";
|
||||
if (sawSchema) return "schema";
|
||||
if (sawDocs) return "docs";
|
||||
const std::string lowerTitle = intakeToLowerCopy(task.base.title);
|
||||
if (lowerTitle.find("test") != std::string::npos) return "unit";
|
||||
if (lowerTitle.find("schema") != std::string::npos) return "schema";
|
||||
if (lowerTitle.find("doc") != std::string::npos || lowerTitle.find("readme") != std::string::npos) return "docs";
|
||||
return "smoke";
|
||||
}
|
||||
|
||||
static json inferStepIds(const AnnotatedTaskitem& task,
|
||||
const json& targetFiles,
|
||||
const std::string& verificationType) {
|
||||
std::vector<std::string> steps = {
|
||||
"inspect_scope"
|
||||
};
|
||||
const std::string lowerTitle = intakeToLowerCopy(task.base.title + " " + task.base.intent);
|
||||
if (lowerTitle.find("smithay") != std::string::npos ||
|
||||
lowerTitle.find("dependency") != std::string::npos ||
|
||||
lowerTitle.find("cargo") != std::string::npos) {
|
||||
appendUnique(&steps, "update_dependencies");
|
||||
} else if (verificationType == "docs") {
|
||||
appendUnique(&steps, "align_docs");
|
||||
} else if (verificationType == "unit" || verificationType == "integration") {
|
||||
appendUnique(&steps, "implement_changes");
|
||||
} else {
|
||||
appendUnique(&steps, "apply_changes");
|
||||
}
|
||||
|
||||
if (verificationType == "unit") {
|
||||
appendUnique(&steps, "run_unit_verification");
|
||||
} else if (verificationType == "integration") {
|
||||
appendUnique(&steps, "run_integration_verification");
|
||||
} else if (verificationType == "docs") {
|
||||
appendUnique(&steps, "review_doc_alignment");
|
||||
} else {
|
||||
appendUnique(&steps, "run_smoke_verification");
|
||||
}
|
||||
|
||||
if (targetFiles.is_array() && targetFiles.empty()) {
|
||||
appendUnique(&steps, "resolve_target_files");
|
||||
}
|
||||
|
||||
json out = json::array();
|
||||
for (const auto& step : sortedUnique(steps)) out.push_back(step);
|
||||
return out;
|
||||
}
|
||||
|
||||
static json inferAcceptanceCommands(const AnnotatedTaskitem& task,
|
||||
const json& targetFiles,
|
||||
const std::string& verificationType) {
|
||||
std::vector<std::string> commands;
|
||||
bool sawRust = false;
|
||||
if (targetFiles.is_array()) {
|
||||
for (const auto& fileJson : targetFiles) {
|
||||
if (!fileJson.is_string()) continue;
|
||||
const std::string path = fileJson.get<std::string>();
|
||||
if (endsWith(path, ".rs") || endsWith(path, "Cargo.toml")) sawRust = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sawRust) {
|
||||
appendUnique(&commands, "cargo check");
|
||||
if (verificationType == "unit" || verificationType == "integration") {
|
||||
appendUnique(&commands, "cargo test -p whimpwm");
|
||||
}
|
||||
} else if (verificationType == "schema") {
|
||||
appendUnique(&commands, "python3 -m json.tool");
|
||||
} else if (verificationType == "docs") {
|
||||
appendUnique(&commands, "whetstone_validate_taskitem");
|
||||
}
|
||||
|
||||
appendUnique(&commands, "whetstone_validate_taskitem");
|
||||
json out = json::array();
|
||||
for (const auto& command : sortedUnique(commands)) out.push_back(command);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void appendDerivedTestFile(std::vector<std::string>* out, const std::string& file) {
|
||||
if (!out || file.empty()) return;
|
||||
const std::string stem = pathStem(file);
|
||||
if (stem.empty()) return;
|
||||
|
||||
if (endsWith(file, ".py")) {
|
||||
appendUnique(out, "tests/test_" + stem + ".py");
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".schema.json")) {
|
||||
appendUnique(out, "tests/test_" + stem + "_schema.py");
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".json")) {
|
||||
appendUnique(out, "tests/test_" + stem + ".py");
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".cpp") || endsWith(file, ".cc") || endsWith(file, ".cxx") ||
|
||||
endsWith(file, ".c") || endsWith(file, ".h") || endsWith(file, ".hpp")) {
|
||||
std::string testPath = file;
|
||||
std::size_t srcPos = testPath.find("/src/");
|
||||
if (srcPos != std::string::npos) {
|
||||
testPath.replace(srcPos, 5, "/tests/");
|
||||
} else {
|
||||
std::size_t slash = testPath.find_last_of('/');
|
||||
if (slash != std::string::npos) {
|
||||
testPath = testPath.substr(0, slash + 1) + "tests/" + testPath.substr(slash + 1);
|
||||
} else {
|
||||
testPath = "tests/" + testPath;
|
||||
}
|
||||
}
|
||||
std::size_t dot = testPath.find_last_of('.');
|
||||
if (dot != std::string::npos) {
|
||||
testPath = testPath.substr(0, dot) + "_test.cpp";
|
||||
} else {
|
||||
testPath += "_test.cpp";
|
||||
}
|
||||
appendUnique(out, testPath);
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".rs")) {
|
||||
appendUnique(out, "tests/" + stem + "_test.rs");
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".go")) {
|
||||
appendUnique(out, "tests/" + stem + "_test.go");
|
||||
return;
|
||||
}
|
||||
if (endsWith(file, ".ts") || endsWith(file, ".tsx") || endsWith(file, ".js") || endsWith(file, ".jsx")) {
|
||||
appendUnique(out, "tests/" + stem + ".test.ts");
|
||||
}
|
||||
}
|
||||
|
||||
static json inferTestFiles(const AnnotatedTaskitem& task,
|
||||
const json& targetFiles,
|
||||
const std::string& verificationType) {
|
||||
std::vector<std::string> out;
|
||||
if (verificationType != "unit" && verificationType != "integration") {
|
||||
return json::array();
|
||||
}
|
||||
if (targetFiles.is_array()) {
|
||||
for (const auto& fileJson : targetFiles) {
|
||||
if (!fileJson.is_string()) continue;
|
||||
appendDerivedTestFile(&out, fileJson.get<std::string>());
|
||||
}
|
||||
}
|
||||
if (out.empty()) {
|
||||
appendUnique(&out, "tests/test_" + task.base.taskId + ".py");
|
||||
}
|
||||
json jsonOut = json::array();
|
||||
for (const auto& file : sortedUnique(out)) jsonOut.push_back(file);
|
||||
return jsonOut;
|
||||
}
|
||||
|
||||
static int executionSpecificityScore(const json& executionContract) {
|
||||
const int stepCount = executionContract.value("stepIds", json::array()).size();
|
||||
const int fileCount = executionContract.value("targetFiles", json::array()).size();
|
||||
const int toolCount = executionContract.value("requiredTools", json::array()).size();
|
||||
const int commandCount = executionContract.value("acceptanceCommands", json::array()).size();
|
||||
const int testFileCount = executionContract.value("testFiles", json::array()).size();
|
||||
const bool hasVerificationType = executionContract.contains("verificationType") &&
|
||||
executionContract["verificationType"].is_string() &&
|
||||
!executionContract["verificationType"].get<std::string>().empty();
|
||||
int score = 0;
|
||||
score += std::min(30, stepCount * 10);
|
||||
score += std::min(25, fileCount * 5);
|
||||
score += std::min(20, toolCount * 5);
|
||||
score += std::min(25, commandCount * 5);
|
||||
score += std::min(25, stepCount * 10);
|
||||
score += std::min(20, fileCount * 5);
|
||||
score += std::min(15, toolCount * 5);
|
||||
score += std::min(15, commandCount * 5);
|
||||
score += std::min(15, testFileCount * 5);
|
||||
if (hasVerificationType) score += 10;
|
||||
return std::clamp(score, 0, 100);
|
||||
}
|
||||
|
||||
static json buildExecutionContract(const AnnotatedTaskitem& task,
|
||||
const json& hints,
|
||||
bool strictExecutionContract) {
|
||||
bool strictExecutionContract,
|
||||
const std::string& workspaceRoot) {
|
||||
json stepIds = hints.value("stepIds", json::array());
|
||||
json targetFiles = hints.value("targetFiles", json::array());
|
||||
json requiredTools = hints.value("requiredTools", json::array());
|
||||
json acceptanceCommands = hints.value("acceptanceCommands", json::array());
|
||||
std::string verificationType = inferVerificationType(task, targetFiles);
|
||||
json testFiles = inferTestFiles(task, targetFiles, verificationType);
|
||||
|
||||
if (strictExecutionContract) {
|
||||
if (!requiredTools.is_array() || requiredTools.empty()) {
|
||||
@@ -579,26 +945,46 @@
|
||||
});
|
||||
}
|
||||
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) {
|
||||
acceptanceCommands = json::array({
|
||||
"step615_test",
|
||||
"step616_test",
|
||||
"step682_test"
|
||||
});
|
||||
if (verificationType == "unit" || verificationType == "integration") {
|
||||
acceptanceCommands = json::array({"pytest", "whetstone_validate_taskitem"});
|
||||
} else if (verificationType == "schema") {
|
||||
acceptanceCommands = json::array({"python3 -m json.tool", "whetstone_validate_taskitem"});
|
||||
} else {
|
||||
acceptanceCommands = json::array({"whetstone_validate_taskitem"});
|
||||
}
|
||||
}
|
||||
if (!targetFiles.is_array() || targetFiles.empty()) {
|
||||
json crossProjectTargets = inferCrossProjectTargetsFromHints(hints);
|
||||
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
|
||||
targetFiles = crossProjectTargets;
|
||||
} else {
|
||||
targetFiles = json::array({
|
||||
"editor/src/mcp/RegisterArchitectIntakeTools.h",
|
||||
"editor/src/mcp/RegisterValidationTools.h",
|
||||
"tools/mcp/run_sprint_taskitem_pipeline.sh"
|
||||
});
|
||||
json taskTargets = inferWorkspaceTaskTargets(task, workspaceRoot);
|
||||
if (taskTargets.is_array() && !taskTargets.empty()) {
|
||||
targetFiles = taskTargets;
|
||||
} else {
|
||||
json workspaceTargets = inferWorkspaceFallbackTargets(workspaceRoot);
|
||||
if (workspaceTargets.is_array() && !workspaceTargets.empty()) {
|
||||
targetFiles = workspaceTargets;
|
||||
} else {
|
||||
targetFiles = json::array({
|
||||
"editor/src/mcp/RegisterArchitectIntakeTools.h",
|
||||
"editor/src/mcp/RegisterValidationTools.h",
|
||||
"tools/mcp/run_sprint_taskitem_pipeline.sh"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
verificationType = inferVerificationType(task, targetFiles);
|
||||
if (!stepIds.is_array() || stepIds.empty()) {
|
||||
stepIds = json::array({"1939", "1940", "1941"});
|
||||
stepIds = inferStepIds(task, targetFiles, verificationType);
|
||||
}
|
||||
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) {
|
||||
acceptanceCommands = inferAcceptanceCommands(task, targetFiles, verificationType);
|
||||
}
|
||||
if ((!testFiles.is_array() || testFiles.empty()) &&
|
||||
(verificationType == "unit" || verificationType == "integration")) {
|
||||
testFiles = inferTestFiles(task, targetFiles, verificationType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +993,8 @@
|
||||
{"targetFiles", targetFiles},
|
||||
{"requiredTools", requiredTools},
|
||||
{"acceptanceCommands", acceptanceCommands},
|
||||
{"testFiles", testFiles},
|
||||
{"verificationType", verificationType},
|
||||
{"evidenceArtifacts", json::array({
|
||||
"logs/taskitem_runs/" + task.base.taskId + "/trace.jsonl",
|
||||
"logs/taskitem_runs/" + task.base.taskId + "/gate_summary.json"
|
||||
@@ -743,6 +1131,7 @@
|
||||
}
|
||||
|
||||
const bool strictExecutionContract = args.value("strictExecutionContract", false);
|
||||
const std::string workspaceRoot = args.value("workspace", "");
|
||||
const json executionHints = collectExecutionHints(normalized.requirements);
|
||||
int escalateCount = 0;
|
||||
int missingContractCount = 0;
|
||||
@@ -753,7 +1142,9 @@
|
||||
json taskJson = {
|
||||
{"taskId", task.base.taskId},
|
||||
{"title", task.base.title},
|
||||
{"intent", task.base.intent},
|
||||
{"milestoneId", task.base.milestoneId},
|
||||
{"requirementIds", task.base.requirementIds},
|
||||
{"dependencyTaskIds", task.base.dependencyTaskIds},
|
||||
{"prerequisiteOps", task.base.prerequisiteOps},
|
||||
{"queueReady", task.base.queueReady},
|
||||
@@ -765,7 +1156,7 @@
|
||||
json taskExecutionHints = executionHints;
|
||||
taskExecutionHints["targetFiles"] = selectTaskScopedTargetFiles(task, normalized.requirements,
|
||||
executionHints.value("targetFiles", json::array()));
|
||||
json contract = buildExecutionContract(task, taskExecutionHints, strictExecutionContract);
|
||||
json contract = buildExecutionContract(task, taskExecutionHints, strictExecutionContract, workspaceRoot);
|
||||
if (libraryDispatchEnabled) {
|
||||
std::string dispatchText = task.base.title;
|
||||
if (!libraryDispatchContext.empty()) {
|
||||
@@ -908,6 +1299,7 @@
|
||||
NormalizedRequirement requirement;
|
||||
requirement.requirementId = requirementJson.value("requirementId", "");
|
||||
requirement.normalizedText = requirementJson.value("normalizedText", "");
|
||||
requirement.sourceText = requirementJson.value("sourceText", requirement.normalizedText);
|
||||
requirement.anchor = requirementJson.value("anchor", "");
|
||||
requirement.sourceLine = requirementJson.value("sourceLine", 0);
|
||||
requirement.ambiguous = requirementJson.value("ambiguous", false);
|
||||
@@ -992,7 +1384,9 @@
|
||||
out.push_back({
|
||||
{"taskId", task.base.taskId},
|
||||
{"title", task.base.title},
|
||||
{"intent", task.base.intent},
|
||||
{"milestoneId", task.base.milestoneId},
|
||||
{"requirementIds", task.base.requirementIds},
|
||||
{"dependencyTaskIds", task.base.dependencyTaskIds},
|
||||
{"prerequisiteOps", task.base.prerequisiteOps},
|
||||
{"queueReady", task.base.queueReady},
|
||||
@@ -1045,7 +1439,17 @@
|
||||
const bool hasFiles = contract.contains("targetFiles") && contract["targetFiles"].is_array() && !contract["targetFiles"].empty();
|
||||
const bool hasTools = contract.contains("requiredTools") && contract["requiredTools"].is_array() && !contract["requiredTools"].empty();
|
||||
const bool hasCommands = contract.contains("acceptanceCommands") && contract["acceptanceCommands"].is_array() && !contract["acceptanceCommands"].empty();
|
||||
if (!(hasSteps && hasFiles && hasTools && hasCommands)) ++executionContractMissingCount;
|
||||
const bool hasVerificationType = contract.contains("verificationType") &&
|
||||
contract["verificationType"].is_string() &&
|
||||
!contract["verificationType"].get<std::string>().empty();
|
||||
const std::string verificationType = contract.value("verificationType", "");
|
||||
const bool requiresTestFiles = verificationType == "unit" || verificationType == "integration";
|
||||
const bool hasTestFiles = contract.contains("testFiles") && contract["testFiles"].is_array() &&
|
||||
!contract["testFiles"].empty();
|
||||
if (!(hasSteps && hasFiles && hasTools && hasCommands && hasVerificationType &&
|
||||
(!requiresTestFiles || hasTestFiles))) {
|
||||
++executionContractMissingCount;
|
||||
}
|
||||
if (contract.value("executionSpecificityScore", 0) < 60) ++executionSpecificityLowCount;
|
||||
}
|
||||
}
|
||||
@@ -1191,7 +1595,9 @@
|
||||
AnnotatedTaskitem task;
|
||||
task.base.taskId = taskJson.value("taskId", "");
|
||||
task.base.title = taskJson.value("title", "");
|
||||
task.base.intent = taskJson.value("intent", "");
|
||||
task.base.milestoneId = taskJson.value("milestoneId", "milestone-1");
|
||||
task.base.requirementIds = taskJson.value("requirementIds", std::vector<std::string>{});
|
||||
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", true);
|
||||
@@ -1240,6 +1646,7 @@
|
||||
NormalizedRequirement requirement;
|
||||
requirement.requirementId = requirementJson.value("requirementId", "");
|
||||
requirement.normalizedText = requirementJson.value("normalizedText", "");
|
||||
requirement.sourceText = requirementJson.value("sourceText", requirement.normalizedText);
|
||||
requirement.anchor = requirementJson.value("anchor", "");
|
||||
requirement.sourceLine = requirementJson.value("sourceLine", 0);
|
||||
requirement.ambiguous = requirementJson.value("ambiguous", false);
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
registerWorkflowTools();
|
||||
registerWorkflowExecutionTools();
|
||||
registerRoutingTools();
|
||||
registerTaskitemPoolTools();
|
||||
registerOrchestratorTools();
|
||||
registerReviewTools();
|
||||
registerArchitectIntakeTools();
|
||||
|
||||
72
editor/src/mcp/RegisterTaskitemPoolTools.h
Normal file
72
editor/src/mcp/RegisterTaskitemPoolTools.h
Normal file
@@ -0,0 +1,72 @@
|
||||
void registerTaskitemPoolTools() {
|
||||
tools_.push_back({"whetstone_import_taskitem_pool",
|
||||
"Import generated taskitems into a project-scoped pool stored under .whetstone.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"}}},
|
||||
{"taskitemsPath", {{"type", "string"},
|
||||
{"description", "Path to a generated taskitems JSON file."}}},
|
||||
{"taskitems", {{"type", "array"},
|
||||
{"description", "Taskitem objects to import directly."}}},
|
||||
{"replace", {{"type", "boolean"}}},
|
||||
{"sourceLabel", {{"type", "string"}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_import_taskitem_pool"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("importTaskitemPool", args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_list_taskitem_pool",
|
||||
"List taskitems from the project-scoped taskitem pool.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"}}},
|
||||
{"status", {{"type", "string"},
|
||||
{"description", "Optional status filter: ready, blocked, claimed, complete."}}}
|
||||
}}}
|
||||
});
|
||||
toolHandlers_["whetstone_list_taskitem_pool"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("listTaskitemPool", args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_claim_taskitem",
|
||||
"Claim a pooled taskitem for an external agent or worker.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"}}},
|
||||
{"itemId", {{"type", "string"}}},
|
||||
{"assignee", {{"type", "string"}}},
|
||||
{"workerType", {{"type", "string"}}}
|
||||
}}, {"required", json::array({"itemId", "assignee"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_claim_taskitem"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("claimTaskitem", args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_release_taskitem",
|
||||
"Release a claimed pooled taskitem back to the project pool.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"}}},
|
||||
{"itemId", {{"type", "string"}}},
|
||||
{"assignee", {{"type", "string"}}},
|
||||
{"reason", {{"type", "string"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_release_taskitem"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("releaseTaskitem", args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_complete_pooled_taskitem",
|
||||
"Mark a pooled taskitem complete with optional result payload.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"}}},
|
||||
{"itemId", {{"type", "string"}}},
|
||||
{"result", {{"type", "object"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_complete_pooled_taskitem"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("completePooledTaskitem", args);
|
||||
};
|
||||
}
|
||||
@@ -50,7 +50,9 @@
|
||||
static std::string classifyValidationIssueGapClass(const std::string& issue) {
|
||||
if (issue.find("executionSpecificityScore") != std::string::npos ||
|
||||
issue.find("execution contract") != std::string::npos ||
|
||||
issue.find("reasons has fewer than 3") != std::string::npos) {
|
||||
issue.find("verificationType") != std::string::npos ||
|
||||
issue.find("testFiles") != std::string::npos ||
|
||||
issue.find("reasons are minimal") != std::string::npos) {
|
||||
return "under_constrained_taskitem";
|
||||
}
|
||||
if (issue.find("cross_project_target_mismatch") != std::string::npos) {
|
||||
@@ -63,10 +65,10 @@
|
||||
issue.find("prerequisiteOps missing") != std::string::npos) {
|
||||
return "environment_or_dependency_gap";
|
||||
}
|
||||
if (issue.find("dependencyTaskIds not empty") != std::string::npos) {
|
||||
if (issue.find("dependencyTaskIds include multiple upstream tasks") != std::string::npos) {
|
||||
return "task_dependency_or_planning_gap";
|
||||
}
|
||||
return "generator_capability_gap";
|
||||
return "under_constrained_taskitem";
|
||||
}
|
||||
|
||||
static json recommendedToolsForValidationGap(const std::string& gapClass) {
|
||||
@@ -241,6 +243,23 @@
|
||||
if (itemJson.contains("execution_contract") && itemJson["execution_contract"].is_object()) {
|
||||
const json& contract = itemJson["execution_contract"];
|
||||
item.executionSpecificityScore = contract.value("executionSpecificityScore", 0);
|
||||
item.hasTestFiles = contract.contains("testFiles") &&
|
||||
contract["testFiles"].is_array() &&
|
||||
!contract["testFiles"].empty();
|
||||
item.hasVerificationType = contract.contains("verificationType") &&
|
||||
contract["verificationType"].is_string() &&
|
||||
!contract["verificationType"].get<std::string>().empty();
|
||||
item.verificationType = contract.value("verificationType", "");
|
||||
} else if (itemJson.contains("executionContract") && itemJson["executionContract"].is_object()) {
|
||||
const json& contract = itemJson["executionContract"];
|
||||
item.executionSpecificityScore = contract.value("executionSpecificityScore", 0);
|
||||
item.hasTestFiles = contract.contains("testFiles") &&
|
||||
contract["testFiles"].is_array() &&
|
||||
!contract["testFiles"].empty();
|
||||
item.hasVerificationType = contract.contains("verificationType") &&
|
||||
contract["verificationType"].is_string() &&
|
||||
!contract["verificationType"].get<std::string>().empty();
|
||||
item.verificationType = contract.value("verificationType", "");
|
||||
} else {
|
||||
item.executionSpecificityScore = itemJson.value("execution_specificity_score", 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user