diff --git a/editor/src/mcp/RegisterArchitectIntakeTools.h b/editor/src/mcp/RegisterArchitectIntakeTools.h index 1b90df4..5f71a9a 100644 --- a/editor/src/mcp/RegisterArchitectIntakeTools.h +++ b/editor/src/mcp/RegisterArchitectIntakeTools.h @@ -214,6 +214,29 @@ return false; } + // Returns true if `token` appears in `text` immediately preceded by a negation + // word ("no", "not", "without", "avoid", "don't", "do not"). Used to suppress + // cross-project repo signals when the spec explicitly says to avoid a project. + static bool isPrecededByNegation(const std::string& text, const std::string& token) { + static const std::vector kNegations = { + "no ", "not ", "without ", "avoid ", "don't ", "do not " + }; + std::size_t pos = 0; + while ((pos = text.find(token, pos)) != std::string::npos) { + const std::size_t end = pos + token.size(); + const bool rightOk = end >= text.size() || !isTokenChar(text[end]); + if (rightOk) { + for (const auto& neg : kNegations) { + if (pos >= neg.size() && text.substr(pos - neg.size(), neg.size()) == neg) { + return true; + } + } + } + pos = end; + } + return false; + } + static void appendUnique(std::vector* out, const std::string& value) { if (!out || value.empty()) return; for (const auto& existing : *out) { @@ -471,8 +494,14 @@ if (lower.find("npm test") != std::string::npos) appendUnique(&commands, "npm test"); if (lower.find("cmake --build") != std::string::npos) appendUnique(&commands, "cmake --build ."); + // Skip acceptance-criteria requirements: they describe what to check, + // not what to target, and often name projects explicitly to exclude them. + if (requirement.anchor == "acceptance-criteria") continue; for (const auto& repo : knownCrossProjectRepos) { - if (containsStandaloneToken(lower, repo)) appendUnique(&repoSignalCandidates, repo); + if (containsStandaloneToken(lower, repo) && + !isPrecededByNegation(lower, repo)) { + appendUnique(&repoSignalCandidates, repo); + } } } @@ -588,6 +617,18 @@ return out; } + static bool isWhetstoneWorkspaceRoot(const std::string& workspaceRoot) { + namespace fs = std::filesystem; + if (workspaceRoot.empty()) return false; + std::error_code ec; + const fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec); + if (ec || root.empty() || !fs::exists(root, ec) || !fs::is_directory(root, ec)) { + return false; + } + return fs::exists(root / "editor/src/mcp/RegisterArchitectIntakeTools.h", ec) && + fs::exists(root / "tools/mcp/run_sprint_taskitem_pipeline.sh", ec); + } + static bool hasCrossProjectIntent(const std::vector& requirements) { int hitCount = 0; for (const auto& requirement : requirements) { @@ -732,21 +773,47 @@ 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 bool hasCMakeWorkspace = !workspaceRoot.empty() && fs::exists(root / "CMakeLists.txt"); 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"; + const fs::path cmakeLists = root / "CMakeLists.txt"; + const fs::path docsArchitecture = root / "docs" / "architecture.md"; + const fs::path docsRoadmap = root / "docs" / "roadmap.md"; + const fs::path widgetHeader = root / "include" / "whimptk" / "WidgetPrimitives.h"; + const fs::path toolkitHeader = root / "include" / "whimptk" / "Toolkit.h"; + const fs::path widgetSource = root / "src" / "WidgetPrimitives.cpp"; + const fs::path toolkitSource = root / "src" / "Toolkit.cpp"; + const fs::path demoSource = root / "examples" / "whimptk_demo.cpp"; auto pushRelative = [&](const fs::path& path) { if (workspaceRoot.empty()) return; std::error_code ec; + if (!fs::exists(path, ec) || !fs::is_regular_file(path, ec)) return; const fs::path rel = fs::relative(path, root, ec); if (ec) return; appendUnique(&targets, rel.generic_string()); }; + const bool mentionsDocs = lowerTitle.find("doc") != std::string::npos || + lowerTitle.find("readme") != std::string::npos || + lowerTitle.find("roadmap") != std::string::npos || + lowerTitle.find("architecture") != std::string::npos; + const bool mentionsBuild = lowerTitle.find("verify") != std::string::npos || + lowerTitle.find("build") != std::string::npos || + lowerTitle.find("test") != std::string::npos || + lowerTitle.find("smoke") != std::string::npos; + const bool mentionsDemo = lowerTitle.find("demo") != std::string::npos || + lowerTitle.find("example") != std::string::npos; + const bool mentionsWidget = lowerTitle.find("widget") != std::string::npos || + lowerTitle.find("primitive") != std::string::npos || + lowerTitle.find("content") != std::string::npos || + lowerTitle.find("binding") != std::string::npos || + lowerTitle.find("presentation") != std::string::npos; + if (containsStandaloneToken(lowerTitle, "smithay") || lowerTitle.find("dependency") != std::string::npos || lowerTitle.find("cargo") != std::string::npos) { @@ -782,6 +849,29 @@ if (fs::exists(crateMain)) pushRelative(crateMain); } + if (hasCMakeWorkspace) { + if (mentionsDocs) { + if (fs::exists(readme)) pushRelative(readme); + if (fs::exists(docsRoadmap)) pushRelative(docsRoadmap); + if (fs::exists(docsArchitecture)) pushRelative(docsArchitecture); + } + if (mentionsWidget) { + if (fs::exists(widgetHeader)) pushRelative(widgetHeader); + if (fs::exists(widgetSource)) pushRelative(widgetSource); + if (fs::exists(toolkitHeader)) pushRelative(toolkitHeader); + if (fs::exists(toolkitSource)) pushRelative(toolkitSource); + } + if (mentionsDemo) { + if (fs::exists(demoSource)) pushRelative(demoSource); + if (fs::exists(toolkitSource)) pushRelative(toolkitSource); + if (fs::exists(widgetSource)) pushRelative(widgetSource); + } + if (mentionsBuild) { + if (fs::exists(cmakeLists)) pushRelative(cmakeLists); + if (fs::exists(demoSource)) pushRelative(demoSource); + } + } + json out = json::array(); for (const auto& file : sortedUnique(targets)) out.push_back(file); return out; @@ -1049,7 +1139,13 @@ const json& hints, bool strictExecutionContract, const std::string& workspaceRoot) { - const std::string effectiveWorkspaceRoot = hints.value("projectRoot", workspaceRoot); + std::string effectiveWorkspaceRoot = workspaceRoot; + if (hints.contains("projectRoot") && hints["projectRoot"].is_string()) { + const std::string hintedProjectRoot = hints["projectRoot"].get(); + if (!hintedProjectRoot.empty()) { + effectiveWorkspaceRoot = hintedProjectRoot; + } + } json stepIds = hints.value("stepIds", json::array()); json targetFiles = hints.value("targetFiles", json::array()); json requiredTools = hints.value("requiredTools", json::array()); @@ -1075,24 +1171,26 @@ } } if (!targetFiles.is_array() || targetFiles.empty()) { - json crossProjectTargets = inferCrossProjectTargetsFromHints(hints); - if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) { - targetFiles = crossProjectTargets; + // Try workspace-local targets first before falling back to cross-project. + json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot); + if (taskTargets.is_array() && !taskTargets.empty()) { + targetFiles = taskTargets; } else { - json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot); - if (taskTargets.is_array() && !taskTargets.empty()) { - targetFiles = taskTargets; - } else { json workspaceTargets = inferWorkspaceFallbackTargets(effectiveWorkspaceRoot); 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" - }); - } + // Only use cross-project targets if the workspace itself has nothing. + json crossProjectTargets = inferCrossProjectTargetsFromHints(hints); + if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) { + targetFiles = crossProjectTargets; + } else if (isWhetstoneWorkspaceRoot(effectiveWorkspaceRoot)) { + targetFiles = json::array({ + "editor/src/mcp/RegisterArchitectIntakeTools.h", + "editor/src/mcp/RegisterValidationTools.h", + "tools/mcp/run_sprint_taskitem_pipeline.sh" + }); + } } } }