Add project taskitem pool and planner fixes

This commit is contained in:
Bill
2026-03-18 01:02:21 -06:00
parent af6fa169b7
commit 69243141dd
15 changed files with 1860 additions and 91 deletions

View File

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

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

View File

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

View File

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

View File

@@ -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(')');

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@
registerWorkflowTools();
registerWorkflowExecutionTools();
registerRoutingTools();
registerTaskitemPoolTools();
registerOrchestratorTools();
registerReviewTools();
registerArchitectIntakeTools();

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

View File

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