Refine taskitem workflow guidance and sprint intake shaping

This commit is contained in:
Bill
2026-03-21 02:17:17 -06:00
parent 69243141dd
commit 017456a476
11 changed files with 1049 additions and 23 deletions

View File

@@ -0,0 +1,192 @@
# Agent Playbook: Whetstone-First Sprint Execution
This document is for agents working across Whetstone and target projects such as
`whimpwm`.
It captures the working method that produced better results on 2026-03-20 when
executing structured sprint work through the Whetstone MCP/taskitem pipeline.
## Why This Exists
Two failure modes show up repeatedly:
1. Agents bypass Whetstone too early and manually code around a weak planning
pipeline.
2. Agents trust generated taskitems too easily even when they are broad,
duplicated, off-project, or not executable by smaller workers.
The intended method is a middle path:
- use Whetstone first
- inspect the output critically
- correct taskitems manually when needed
- record the generator miss
- only then hand execution to constrained workers or continue with senior review
## Core Principle
Whetstone should remain the planning and task-shaping source of truth as early as
possible.
That does not mean raw markdown specs are ignored.
The phase model is:
1. Spec markdown is the source of truth before code exists.
2. Parsed planning artifacts are the source of truth after intake.
3. AST is the source of truth once code structure exists.
Agents should push work toward that progression, not collapse back to ad hoc
manual execution.
## Default Workflow
For a sprint execution request:
1. Find the active sprint spec or execution spec.
2. Run the Whetstone sprint taskitem pipeline.
3. Inspect generated taskitems before writing code.
4. Decide whether taskitems are:
- ready to execute
- salvageable with manual correction
- unusable and requiring a recorded gap
5. If taskitems are good, use them as the execution substrate.
6. If taskitems need correction, edit the taskitems manually and preserve the
corrected artifact in the run directory.
7. If taskitems are unusable, record the generator/validator failure in the gap
registry and do not pretend the pipeline succeeded.
8. Preserve run artifacts for LoRA and smaller-model training data.
9. After code generation or worker execution, perform a stronger model review and
tighten the result.
## The Important Distinction
Manual correction of taskitems is allowed when the generator is close but not yet
reliable enough.
Manual bypass of the taskitem layer is not the default.
That means:
- acceptable:
- rewrite titles
- narrow target files
- fix acceptance commands
- split one broad task into several bounded tasks
- add missing execution-contract fields
- not acceptable:
- ignore obviously bad taskitems and jump straight to handwritten code as if
the planning layer were fine
- treat a failed taskitem run as a success without recording what was wrong
## Taskitem Quality Bar
Generated taskitems are not execution-ready just because validation returned
`score=100`.
Before handing them to smaller agents, check:
- Does each task have a bounded objective?
- Do `targetFiles` point to the actual project and the likely ownership slice?
- Are acceptance commands valid for the project language?
- Are test files real?
- Is the title specific enough that a constrained worker could act on it?
- Is the task a concrete implementation slice rather than a broad planning
bucket?
Red flags include:
- blanket file sets repeated across every task
- wrong-project paths
- fabricated tests
- mixed-language commands such as `go test ./...` in a Rust repo
- titles like `Architect Review`, `Define verification surface`, or generic
`Plan ...` tasks with wide ownership
## When To Use Smaller Workers
Use smaller agents or AST-first codegen only after the taskitem is narrow enough
that a constrained worker can succeed without inventing scope.
Good worker task shape:
- one main objective
- a small, real file set
- valid verification commands
- explicit acceptance evidence
If the taskitem still requires a senior model to infer the real work, reshape it
first.
## Review Role Of The Stronger Model
The stronger model should own:
- sprint interpretation
- taskitem quality review
- manual taskitem correction
- gap diagnosis and registry updates
- final code review and hardening
The stronger model should not waste tokens doing narrow mechanical generation if
the taskitem is already well-shaped for a smaller worker.
## Gap Recording Rule
If the generator or validator fails in a meaningful way, record it.
Typical examples:
- generated taskitems are project-grounded but still not executable
- validator over-scores clearly weak tasks
- subsection/work-item structure is ignored
- self-targeting Whetstone sprints collapse into blanket internal tasks
Record the miss in the relevant Whetstone gap or registry path instead of
silently compensating for it.
## Data Capture Rule
Every meaningful run should leave artifacts useful for later LoRA or SLM work.
Preserve:
- raw pipeline run directory
- corrected taskitem artifact when manual fixes were required
- gap note or registry update when generation was wrong
The long-term goal is to make smaller models reliable on constrained workitems,
so the correction path itself is training data.
## Recommended Prompting Stance For Other Agents
When another agent is asked to execute a sprint:
- tell it Whetstone is the first stop, not optional scaffolding
- tell it to inspect generated taskitems critically
- tell it manual taskitem correction is permitted
- tell it manual code bypass is not the default when the planning layer failed
- tell it to record generator misses in the gap registry
- tell it to preserve corrected artifacts for training data
One short instruction that often helps:
`Use Whetstone first. If taskitems are weak, fix the taskitems and record the generator miss. Do not skip directly to handwritten code unless the user explicitly tells you to bypass the planning layer.`
## Suggested Use
Point other agents at this file before asking them to:
- execute a sprint
- generate taskitems for another repo
- prepare bounded work for smaller workers
- review whether a Whetstone run is actually handoff-ready
## Current Practical Naming
If "bootstrapping" feels ambiguous, use:
`Whetstone-first sprint execution playbook`
That name is explicit about what the document is for and how agents should use
it.

View File

@@ -8,6 +8,14 @@ and how run data must be captured for later LoRA curation.
- Applies to sprint plan execution (for example: `sprint46_plan.md` through `sprint100_plan.md`)
- Applies to all future agents working in this repository
## Related Guidance
For the practical agent workflow on when to trust generated taskitems, when to
manually correct them, and when to record generator misses instead of bypassing
the planning layer, also see:
`docs/AGENT_PLAYBOOK_WHESTONE_FIRST_SPRINT_EXECUTION_2026-03-20.md`
## Required MCP Pipeline
For each sprint plan, agents must run:

61
docs/sprint278_plan.md Normal file
View File

@@ -0,0 +1,61 @@
# Sprint 278: Structured Planning Artifact As Post-Intake Source Of Truth
## Goal
Make Whetstone treat markdown specs as pre-code truth, but treat a structured
planning artifact as the source of truth for task decomposition after intake.
This sprint exists to close the remaining architectural gap behind GR-035:
`architect_intake` and `RequirementsParser` do produce useful structure, but
`generate_taskitems` still depends too heavily on raw markdown-shaped heuristics.
The result is that explicit work items like `W1`/`W2`/`W3` are partially
recognized but still get merged back into broad plan tasks.
The target architecture is:
1. Markdown spec is the source of truth before code exists.
2. Intake produces a structured planning artifact that becomes the planning
source of truth.
3. Task decomposition and queue shaping consume that artifact rather than
re-inferring everything from raw prose.
4. Once code exists, AST remains the source of truth for code structure/editing.
## Steps
| Step | File | What |
|------|------|------|
| 1990 | `RequirementsParser.h` | Preserve subsection/work-item context in parsed requirements so `### W1...` blocks survive intake as structured planning inputs. |
| 1991 | `ArchitectIntakeProcessor.h` | Emit explicit planning-artifact structure for bounded work items and subsection-scoped requirement groups instead of only a flat normalized requirement list. |
| 1992 | `ScopeMilestoneDecomposer.h` | Prefer structured work-item groups as decomposition roots; broad project/context goals must not dominate explicit `W1`/`W2`/`W3` slices. |
| 1993 | `TaskitemGeneratorV2.h` / `RegisterArchitectIntakeTools.h` | Generate taskitems from the structured planning artifact with file/language/verification hints attached to each work item, not reconstructed from global prose. |
| 1994 | Sprint 278 integration | Run the `whimpwm` Sprint 002 recomposition spec through the pipeline and verify that generated taskitems align to bounded work items, scoped file ownership, and Rust-only verification. |
## Dependencies
- Builds on: `RequirementsParser.h`, `ArchitectIntakeProcessor.h`,
`ScopeMilestoneDecomposer.h`, `TaskitemGeneratorV2.h`,
`RegisterArchitectIntakeTools.h`
- Regression target: GR-035 in
`docs/generator_readiness_gap_registry_2026-02-26.md`
- Validation artifact: `whimpwm` sprint spec at
`/home/bill/Documents/whimpwm/docs/sprints/sprint_002_policy_driven_recomposition_execution_spec.md`
## Constraints
- Do not treat raw markdown text as the long-lived planning source of truth
after intake has already structured it.
- Do not weaken AST-first execution principles; this sprint is about the
pre-code planning layer, not replacing AST as code truth.
- Do not regress the existing Rust-only contract improvements for GR-035:
no `go test ./...`, no malformed file paths, no Whetstone-workspace fallback
leakage into external project taskitems.
- Keep any new planning artifact deterministic and serializable so it can be
used for LoRA/SLM training data capture later.
## Acceptance Criteria
- Intake preserves bounded work-item structure from markdown specs in a
structured planning artifact.
- Decomposition consumes that structure directly and splits `W1`/`W2`/`W3`
style work items into distinct task roots.
- `generate_taskitems` for the `whimpwm` Sprint 002 spec produces bounded,
work-item-shaped tasks rather than broad plan bundles.
- Generated taskitems retain Rust-only verification and real `whimpwm`
file/test grounding.
- Regression tests pass for parser, decomposition, and taskitem generation.

View File

@@ -29,6 +29,7 @@ public:
std::string line;
std::string sectionTitle;
std::string sectionAnchor;
std::string subsectionTitle;
std::string proseBuffer;
int proseStartLine = 0;
int lineNo = 0;
@@ -50,9 +51,23 @@ public:
++lineNo;
std::string trimmed = trim(line);
if (isHeading(trimmed)) {
const int level = headingLevel(trimmed);
if (level >= 3 && isRequirementHeading(sectionAnchor)) {
flushProse();
subsectionTitle = trimHeading(trimmed);
ParsedRequirement req;
req.text = normalizeText(subsectionTitle);
req.sourceText = subsectionTitle;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
} else {
flushProse();
sectionTitle = trimHeading(trimmed);
sectionAnchor = anchorize(sectionTitle);
subsectionTitle.clear();
}
continue;
}
@@ -71,10 +86,11 @@ public:
std::string bulletText = trim(trimmed.substr(2));
if (bulletText.empty()) continue;
if (isQuotedExampleItem(bulletText)) continue;
if (tryParseFieldList(bulletText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
std::string contextualText = applySubsectionContext(subsectionTitle, bulletText);
if (tryParseFieldList(contextualText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
ParsedRequirement req;
req.text = normalizeText(bulletText);
req.sourceText = bulletText;
req.text = normalizeText(contextualText);
req.sourceText = contextualText;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
@@ -89,20 +105,22 @@ public:
std::string itemText = trim(trimmed.substr(markerEnd + 1));
if (itemText.empty()) continue;
if (isQuotedExampleItem(itemText)) continue;
std::string contextualText = applySubsectionContext(subsectionTitle, itemText);
ParsedRequirement req;
req.text = normalizeText(itemText);
req.sourceText = itemText;
req.text = normalizeText(contextualText);
req.sourceText = contextualText;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
} else if (isProseLine(trimmed)) {
std::string contextualText = applySubsectionContext(subsectionTitle, trimmed);
if (proseBuffer.empty()) {
proseBuffer = trimmed;
proseBuffer = contextualText;
proseStartLine = lineNo;
} else {
proseBuffer += " ";
proseBuffer += trimmed;
proseBuffer += contextualText;
}
} else {
flushProse();
@@ -172,6 +190,12 @@ private:
return intakeCompactLowerAlnumSpaces(text);
}
static std::string applySubsectionContext(const std::string& subsectionTitle,
const std::string& text) {
if (subsectionTitle.empty()) return text;
return subsectionTitle + ": " + text;
}
static bool isHeading(const std::string& line) {
if (line.size() < 2) return false;
if (line[0] != '#') return false;
@@ -180,6 +204,12 @@ private:
return hashes > 0 && hashes < line.size() && line[hashes] == ' ';
}
static int headingLevel(const std::string& line) {
int hashes = 0;
while (hashes < static_cast<int>(line.size()) && line[hashes] == '#') ++hashes;
return hashes;
}
static std::string trimHeading(const std::string& headingLine) {
std::size_t i = 0;
while (i < headingLine.size() && headingLine[i] == '#') ++i;

View File

@@ -87,6 +87,16 @@ private:
return false;
}
static std::string workItemGroupKey(const std::string& text) {
if (text.size() < 2) return "";
if (text[0] != 'w') return "";
std::size_t i = 1;
while (i < text.size() && std::isdigit(static_cast<unsigned char>(text[i]))) ++i;
if (i == 1) return "";
if (i < text.size() && text[i] != '.' && text[i] != ':' && text[i] != ' ' && text[i] != '-') return "";
return text.substr(0, i);
}
static bool isPlanningMetaRequirement(const NormalizedRequirement& requirement) {
const std::string lower = lowerCopy(requirement.normalizedText);
if (requirement.anchor == "environment-projection-targets" ||
@@ -162,6 +172,25 @@ private:
return summary.empty() ? text : summary;
}
static std::string stripLeadingDisplayVerb(std::string text) {
const std::vector<std::string> prefixes = {
"implement ",
"plan ",
"verify ",
"apply constraint ",
"integrate dependency ",
"implement feature ",
"define field "
};
const std::string lower = lowerCopy(text);
for (const auto& prefix : prefixes) {
if (lower.rfind(prefix, 0) == 0 && text.size() > prefix.size()) {
return text.substr(prefix.size());
}
}
return text;
}
static std::string workstreamPrefix(NormalizedRequirementKind kind) {
if (kind == NormalizedRequirementKind::Goal) return "Plan";
if (kind == NormalizedRequirementKind::Requirement) return "Implement";
@@ -177,6 +206,8 @@ private:
? requirement.normalizedText
: requirement.sourceText;
const std::string lower = lowerCopy(sourceText);
const std::string itemKey = workItemGroupKey(lower);
if (!itemKey.empty()) return itemKey;
if (requirement.kind == NormalizedRequirementKind::Acceptance) return "verification";
if (containsAny(lower, {"smithay", "dependency", "cargo", "substrate"})) return "smithay-bootstrap";
@@ -207,7 +238,6 @@ private:
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()
@@ -219,6 +249,28 @@ private:
}
workstream.intent = intent;
workstream.uncertaintyScore = computeUncertainty(requirements);
if (!requirements.empty() &&
key != "docs-alignment" &&
key != "verification" &&
key != "state-definitions") {
const auto& lead = requirements.front();
const std::string& textForDisplay = lead.sourceText.empty()
? lead.normalizedText
: lead.sourceText;
const std::string prefix = workstreamPrefix(lead.kind);
std::string titleSource = stripLeadingDisplayVerb(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);
} else {
workstream.title = workstreamGroupTitle(key);
}
return workstream;
}

View File

@@ -325,6 +325,14 @@
static std::string normalizeTargetFileCandidate(const std::string& raw) {
std::string value = trimPathToken(raw);
if (value.empty()) return "";
while (!value.empty() &&
(value.back() == '.' || value.back() == ':' || value.back() == '`')) {
value.pop_back();
}
while (!value.empty() &&
(value.front() == '`' || value.front() == '"' || value.front() == '\'')) {
value.erase(value.begin());
}
if (value.rfind("./", 0) == 0) value = value.substr(2);
const std::string homeDocsPrefix = "/home/bill/Documents/";
if (value.rfind(homeDocsPrefix, 0) == 0) {
@@ -333,6 +341,9 @@
if (value.rfind("CLionProjects/whetstone_DSL/", 0) == 0) {
value = value.substr(std::string("CLionProjects/whetstone_DSL/").size());
}
if (value.find('`') != std::string::npos) return "";
if (value.find('\\') != std::string::npos) return "";
if (value.find("..") != std::string::npos) return "";
if (!looksLikePathToken(value)) return "";
return value;
}
@@ -357,12 +368,30 @@
return out;
}
static std::string extractActiveProjectRootFromText(const std::string& text) {
const std::string lower = intakeToLowerCopy(text);
if (lower.find("active project") == std::string::npos) return "";
const std::string prefix = "/home/bill/Documents/";
const std::size_t pos = text.find(prefix);
if (pos == std::string::npos) return "";
std::size_t end = pos;
while (end < text.size()) {
const char c = text[end];
if (c == ' ' || c == '\n' || c == '\t' || c == '`' || c == '"' || c == '\'' || c == ',' || c == ';' || c == ')') {
break;
}
++end;
}
return text.substr(pos, end - pos);
}
static json collectExecutionHints(const std::vector<NormalizedRequirement>& requirements) {
std::vector<std::string> stepIds;
std::vector<std::string> files;
std::vector<std::string> tools;
std::vector<std::string> commands;
std::vector<std::string> repoSignalCandidates;
std::string projectRoot;
bool explicitCrossProjectIntent = false;
const std::vector<std::string> knownCrossProjectRepos = {
@@ -381,6 +410,10 @@
const std::string text = requirement.normalizedText;
const std::string lower = intakeToLowerCopy(sourceText);
if (projectRoot.empty()) {
projectRoot = extractActiveProjectRootFromText(sourceText);
}
if (lower.find("cross-repo") != std::string::npos ||
lower.find("multi-project") != std::string::npos ||
lower.find("multi-repo") != std::string::npos ||
@@ -453,7 +486,8 @@
{"targetFiles", sortedUnique(files)},
{"requiredTools", sortedUnique(tools)},
{"acceptanceCommands", sortedUnique(commands)},
{"crossProjectRepos", sortedUnique(repoSignals)}
{"crossProjectRepos", sortedUnique(repoSignals)},
{"projectRoot", projectRoot}
};
}
@@ -623,12 +657,74 @@
appendUnique(&scoped, path);
}
}
if (scoped.empty()) return defaultTargetFiles;
(void)defaultTargetFiles;
if (scoped.empty()) return json::array();
json out = json::array();
for (const auto& file : sortedUnique(scoped)) out.push_back(file);
return out;
}
static bool commandStartsWith(const std::string& value, const std::string& prefix) {
return value.rfind(prefix, 0) == 0;
}
static bool commandCompatibleWithTargets(const std::string& command,
bool sawRust,
bool sawPython,
bool sawGo,
bool sawJsonOnly) {
const std::string lower = intakeToLowerCopy(trimPathToken(command));
if (lower.empty()) return false;
if (lower == "whetstone_validate_taskitem") return true;
if (commandStartsWith(lower, "cargo ")) return sawRust;
if (commandStartsWith(lower, "pytest") || commandStartsWith(lower, "python3 ")) {
return sawPython || sawJsonOnly;
}
if (commandStartsWith(lower, "go test")) return sawGo;
return !(sawRust || sawPython || sawGo || sawJsonOnly);
}
static json sanitizeAcceptanceCommands(const json& candidateCommands,
const json& targetFiles) {
bool sawRust = false;
bool sawPython = false;
bool sawGo = false;
bool sawJsonOnly = true;
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;
sawJsonOnly = false;
} else if (endsWith(path, ".py")) {
sawPython = true;
sawJsonOnly = false;
} else if (endsWith(path, ".go") || endsWith(path, "go.mod")) {
sawGo = true;
sawJsonOnly = false;
} else if (!endsWith(path, ".json") && !endsWith(path, ".schema.json")) {
sawJsonOnly = false;
}
}
}
std::vector<std::string> filtered;
if (candidateCommands.is_array()) {
for (const auto& commandJson : candidateCommands) {
if (!commandJson.is_string()) continue;
const std::string command = trimPathToken(commandJson.get<std::string>());
if (command.empty()) continue;
if (!commandCompatibleWithTargets(command, sawRust, sawPython, sawGo, sawJsonOnly)) continue;
appendUnique(&filtered, command);
}
}
json out = json::array();
for (const auto& command : sortedUnique(filtered)) out.push_back(command);
return out;
}
static json inferWorkspaceTaskTargets(const AnnotatedTaskitem& task,
const std::string& workspaceRoot) {
namespace fs = std::filesystem;
@@ -887,20 +983,44 @@
static json inferTestFiles(const AnnotatedTaskitem& task,
const json& targetFiles,
const std::string& verificationType) {
const std::string& verificationType,
const std::string& workspaceRoot) {
namespace fs = std::filesystem;
std::vector<std::string> out;
if (verificationType != "unit" && verificationType != "integration") {
return json::array();
}
bool sawRust = false;
if (targetFiles.is_array()) {
for (const auto& fileJson : targetFiles) {
if (!fileJson.is_string()) continue;
appendDerivedTestFile(&out, fileJson.get<std::string>());
const std::string file = fileJson.get<std::string>();
if (endsWith(file, ".rs") || endsWith(file, "Cargo.toml")) sawRust = true;
if (file.find("/tests/") != std::string::npos || file.rfind("tests/", 0) == 0) {
appendUnique(&out, file);
continue;
}
appendDerivedTestFile(&out, file);
}
}
if (sawRust && !workspaceRoot.empty()) {
std::error_code ec;
const fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec);
if (!ec) {
const fs::path preferred = root / "crates" / "whimpwm" / "tests" / "main_test.rs";
if (fs::exists(preferred, ec) && fs::is_regular_file(preferred, ec)) {
out.clear();
appendUnique(&out, "crates/whimpwm/tests/main_test.rs");
}
}
}
if (out.empty()) {
if (sawRust) {
appendUnique(&out, "crates/whimpwm/tests/main_test.rs");
} else {
appendUnique(&out, "tests/test_" + task.base.taskId + ".py");
}
}
json jsonOut = json::array();
for (const auto& file : sortedUnique(out)) jsonOut.push_back(file);
return jsonOut;
@@ -929,12 +1049,13 @@
const json& hints,
bool strictExecutionContract,
const std::string& workspaceRoot) {
const std::string effectiveWorkspaceRoot = hints.value("projectRoot", 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);
json testFiles = inferTestFiles(task, targetFiles, verificationType, effectiveWorkspaceRoot);
if (strictExecutionContract) {
if (!requiredTools.is_array() || requiredTools.empty()) {
@@ -958,11 +1079,11 @@
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
targetFiles = crossProjectTargets;
} else {
json taskTargets = inferWorkspaceTaskTargets(task, workspaceRoot);
json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot);
if (taskTargets.is_array() && !taskTargets.empty()) {
targetFiles = taskTargets;
} else {
json workspaceTargets = inferWorkspaceFallbackTargets(workspaceRoot);
json workspaceTargets = inferWorkspaceFallbackTargets(effectiveWorkspaceRoot);
if (workspaceTargets.is_array() && !workspaceTargets.empty()) {
targetFiles = workspaceTargets;
} else {
@@ -979,12 +1100,26 @@
if (!stepIds.is_array() || stepIds.empty()) {
stepIds = inferStepIds(task, targetFiles, verificationType);
}
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) {
acceptanceCommands = inferAcceptanceCommands(task, targetFiles, verificationType);
acceptanceCommands = sanitizeAcceptanceCommands(acceptanceCommands, targetFiles);
json inferredAcceptanceCommands = inferAcceptanceCommands(task, targetFiles, verificationType);
std::vector<std::string> mergedAcceptanceCommands;
if (acceptanceCommands.is_array()) {
for (const auto& commandJson : acceptanceCommands) {
if (commandJson.is_string()) appendUnique(&mergedAcceptanceCommands, commandJson.get<std::string>());
}
}
if (inferredAcceptanceCommands.is_array()) {
for (const auto& commandJson : inferredAcceptanceCommands) {
if (commandJson.is_string()) appendUnique(&mergedAcceptanceCommands, commandJson.get<std::string>());
}
}
acceptanceCommands = json::array();
for (const auto& command : sortedUnique(mergedAcceptanceCommands)) {
acceptanceCommands.push_back(command);
}
if ((!testFiles.is_array() || testFiles.empty()) &&
(verificationType == "unit" || verificationType == "integration")) {
testFiles = inferTestFiles(task, targetFiles, verificationType);
testFiles = inferTestFiles(task, targetFiles, verificationType, effectiveWorkspaceRoot);
}
}

View File

@@ -52,6 +52,8 @@
issue.find("execution contract") != std::string::npos ||
issue.find("verificationType") != std::string::npos ||
issue.find("testFiles") != std::string::npos ||
issue.find("malformed_target_path") != std::string::npos ||
issue.find("incompatible_acceptance_commands") != std::string::npos ||
issue.find("reasons are minimal") != std::string::npos) {
return "under_constrained_taskitem";
}
@@ -127,6 +129,46 @@
return sawTarget;
}
static bool hasMalformedTargetPath(const json& targetFiles) {
if (!targetFiles.is_array()) return false;
for (const auto& target : targetFiles) {
if (!target.is_string()) continue;
const std::string path = target.get<std::string>();
if (path.empty()) return true;
if (path.find('`') != std::string::npos || path.find('\\') != std::string::npos) return true;
if (path.find("..") != std::string::npos) return true;
if (path.back() == '.' || path.back() == ':' || path.back() == '/') return true;
}
return false;
}
static bool hasIncompatibleAcceptanceCommands(const json& targetFiles,
const json& acceptanceCommands) {
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) return false;
bool sawRust = false;
bool sawPython = false;
bool sawGo = false;
if (targetFiles.is_array()) {
for (const auto& target : targetFiles) {
if (!target.is_string()) continue;
const std::string path = lowerCopyValidation(target.get<std::string>());
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".rs") == 0) sawRust = true;
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".py") == 0) sawPython = true;
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".go") == 0) sawGo = true;
if (path == "cargo.toml" || (path.size() >= 10 && path.compare(path.size() - 10, 10, "cargo.toml") == 0)) sawRust = true;
if (path == "go.mod" || (path.size() >= 6 && path.compare(path.size() - 6, 6, "go.mod") == 0)) sawGo = true;
}
}
for (const auto& commandJson : acceptanceCommands) {
if (!commandJson.is_string()) continue;
const std::string command = lowerCopyValidation(commandJson.get<std::string>());
if (command.rfind("go test", 0) == 0 && !sawGo) return true;
if (command.rfind("cargo ", 0) == 0 && !sawRust) return true;
if ((command.rfind("pytest", 0) == 0 || command.rfind("python3 ", 0) == 0) && !sawPython) return true;
}
return false;
}
static std::string stableValidationDigest(const std::string& input) {
unsigned long long state = 1469598103934665603ull;
for (unsigned char c : input) {
@@ -335,6 +377,15 @@
lockIssues.push_back("cross_project_target_mismatch: generated targetFiles do not match expected project roots");
dominantGapClass = "cross_project_targeting_gap";
}
if (hasMalformedTargetPath(targetFiles)) {
lockIssues.push_back("malformed_target_path: generated targetFiles contain invalid path tokens");
dominantGapClass = "under_constrained_taskitem";
}
const json acceptanceCommands = contract.value("acceptanceCommands", json::array());
if (hasIncompatibleAcceptanceCommands(targetFiles, acceptanceCommands)) {
lockIssues.push_back("incompatible_acceptance_commands: generated commands do not match detected repo language");
dominantGapClass = "under_constrained_taskitem";
}
}
}
json recommendedTools = recommendedToolsForValidationGap(dominantGapClass);

View File

@@ -0,0 +1,25 @@
#include "RequirementsParser.h"
#include <iostream>
static int p=0,f=0;
#define T(n) { std::cout<<" "<<#n<<"... "; }
#define P() { std::cout<<"PASS\n"; ++p; }
#define F(m) { std::cout<<"FAIL: "<<m<<"\n"; ++f; }
#define C(c,m) if(!(c)){F(m);return;}
void t1(){T(empty_markdown);auto r=RequirementsParser::parse("");C(r.empty(),"e");P();}
void t2(){T(single_bullet_requirements);auto r=RequirementsParser::parse("## Requirements\n- Do the thing\n");C(r.size()==1,"s");P();}
void t3(){T(multiple_bullets);auto r=RequirementsParser::parse("## Requirements\n- A\n- B\n");C(r.size()==2,"s");P();}
void t4(){T(ignore_constraints_section);auto r=RequirementsParser::parse("## Constraints\n- A\n");C(r.empty(),"e");P();}
void t5(){T(anchorize_lower_hyphen);auto a=RequirementsParser::anchorize("API Surface");C(a=="api-surface","a");P();}
void t6(){T(heading_requirements_true);C(RequirementsParser::isRequirementHeading("requirements"),"h");P();}
void t7(){T(heading_features_true);C(RequirementsParser::isRequirementHeading("features"),"h");P();}
void t8(){T(heading_constraints_false);C(!RequirementsParser::isRequirementHeading("constraints"),"h");P();}
void t9(){T(heading_goal_false);C(!RequirementsParser::isRequirementHeading("goal"),"h");P();}
void t10(){T(field_list_splits);auto r=RequirementsParser::parse("## Requirements\n- WorkItem has: job_id (string), priority (int)\n");C(r.size()==2,"s");C(r[0].isField&&r[1].isField,"f");P();}
void t11(){T(field_name_type_extract);auto r=RequirementsParser::parse("## Requirements\n- WorkItem has: job_id (string), priority (int)\n");C(r[0].fieldName=="job_id","n");C(r[0].fieldType=="string","t");P();}
void t12(){T(source_line_and_section_title);auto r=RequirementsParser::parse("# T\n## Requirements\n- Item\n");C(r.size()==1,"s");C(r[0].sourceLine==3,"l");C(r[0].sectionTitle=="Requirements","t");P();}
void t13(){T(work_item_subheadings_preserved_inside_requirement_section);auto r=RequirementsParser::parse("## Work Items\n### W1. Introduce Composition Types\n- Goal:\n- add a dedicated composition layer\n");C(r.size()>=3,"s");C(r[0].sectionTitle=="Work Items","t");C(r[0].sourceText=="W1. Introduce Composition Types","h");C(r[1].sourceText.find("W1. Introduce Composition Types:")!=std::string::npos,"ctx");P();}
int main(){std::cout<<"Step 1849: RequirementsParser\n";t1();t2();t3();t4();t5();t6();t7();t8();t9();t10();t11();t12();t13();std::cout<<"\nResults: "<<p<<"/"<<(p+f)<<" passed\n";return f?1:0;}

View File

@@ -0,0 +1,252 @@
// Step 1989: GR-029 fix — project-grounded taskitem titles, intents, and step IDs
//
// Three bugs fixed in buildRequirementWorkstream + buildExecutionContract:
//
// 1. Double prefix: "Implement implement itargetresolver..." → workstreamPrefix
// already adds "Implement" and normalizedText had it lowercased too.
// Fix: use sourceText for display; strip leading verb if it duplicates prefix.
//
// 2. Lowercase intent: intent was normalizedText (all-lowercase). Fix: use
// sourceText so class names, paths, and caps are preserved.
//
// 3. Hardcoded whetstone step IDs: strictExecutionContract path always injected
// ["1939","1940","1941"] when stepIds were empty, even for external project
// specs. Fix: leave stepIds empty when none found in the spec.
//
// t1: Goal requirement title uses sourceText, no double "Implement implement"
// t2: Grouped workstream still preserves requirement wording for non-empty output
// t3: Intent field preserves original capitalisation from sourceText
// t4: Intent falls back to normalizedText when sourceText is absent
// t5: buildExecutionContract does not inject whetstone step IDs when none extracted
// t6: grouped workstream titles use requirement summary instead of generic bucket labels
// t7: work-item subsection context creates distinct grouped workstreams
#include "ScopeMilestoneDecomposer.h"
#include "RequirementNormalizationConflictDetector.h"
#include <iostream>
#include <string>
#include <algorithm>
static int p = 0, f = 0;
#define T(n) { std::cout << " " << #n << "... "; }
#define P() { std::cout << "PASS\n"; ++p; }
#define F(m) { std::cout << "FAIL: " << (m) << "\n"; ++f; }
#define C(c, m) if (!(c)) { F(m); return; }
static NormalizedRequirement makeGoal(const std::string& id,
const std::string& sourceText,
const std::string& normalizedText) {
NormalizedRequirement r;
r.requirementId = id;
r.kind = NormalizedRequirementKind::Goal;
r.sourceText = sourceText;
r.normalizedText = normalizedText;
r.ambiguous = false;
r.sourceLine = 1;
return r;
}
static NormalizedRequirement makeRequirement(const std::string& id,
const std::string& sourceText,
const std::string& normalizedText) {
NormalizedRequirement r;
r.requirementId = id;
r.kind = NormalizedRequirementKind::Requirement;
r.sourceText = sourceText;
r.normalizedText = normalizedText;
r.ambiguous = false;
r.sourceLine = 2;
return r;
}
static bool contains(const std::string& haystack, const std::string& needle) {
return haystack.find(needle) != std::string::npos;
}
void t1() {
T(goal_title_no_double_implement);
RequirementNormalizationResult norm;
norm.requirements.push_back(makeGoal(
"goal-1",
"Implement ITargetResolver pure virtual interface in include/whimp/app/TargetResolver.h",
"implement itargetresolver pure virtual interface in include whimp app targetresolver h"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty(), "no milestones");
C(!plan.milestones[0].workstreams.empty(), "no workstreams");
const std::string title = plan.milestones[0].workstreams[0].title;
// Must NOT have "Implement implement" (double prefix)
std::string lower = title;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
C(!contains(lower, "implement implement"), "double prefix found: " + title);
C(title.rfind("Implement ", 0) == 0 || title.rfind("Plan ", 0) == 0, "expected meaningful prefix: " + title);
// Must contain the class name from sourceText
C(contains(title, "ITargetResolver"), "class name missing: " + title);
P();
}
void t2() {
T(grouped_requirement_title_preserves_requirement_wording);
RequirementNormalizationResult norm;
norm.requirements.push_back(makeRequirement(
"r-1",
"Implement control-plane composition types in crates/whimpwm/src/presentation.rs",
"implement control plane composition types in crates whimpwm src presentation rs"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty");
const std::string title = plan.milestones[0].workstreams[0].title;
std::string lower = title;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
C(!contains(lower, "implement implement"), "double prefix: " + title);
C(contains(title, "composition types"), "requirement wording missing: " + title);
P();
}
void t3() {
T(intent_preserves_sourcetext_capitalisation);
RequirementNormalizationResult norm;
norm.requirements.push_back(makeGoal(
"goal-2",
"Implement LastFocusResolver using xdotool key forwarding in include/whimp/app/TargetResolver.h",
"implement lastfocusresolver using xdotool key forwarding in include whimp app targetresolver h"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty");
const std::string intent = plan.milestones[0].workstreams[0].intent;
// Intent must contain the original-case class name
C(contains(intent, "LastFocusResolver"), "class name missing from intent: " + intent);
// Must NOT be all-lowercase
C(intent != "implement lastfocusresolver using xdotool key forwarding in include whimp app targetresolver h",
"intent is still all-lowercase: " + intent);
P();
}
void t4() {
T(intent_fallback_to_normalizedtext_when_sourcetext_absent);
RequirementNormalizationResult norm;
NormalizedRequirement r;
r.requirementId = "goal-3";
r.kind = NormalizedRequirementKind::Goal;
r.sourceText = ""; // absent
r.normalizedText = "implement fiforesolver write key string to fifo";
r.ambiguous = false;
r.sourceLine = 3;
norm.requirements.push_back(r);
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty");
const std::string intent = plan.milestones[0].workstreams[0].intent;
// Falls back to normalizedText
C(!intent.empty(), "intent empty");
C(contains(intent, "fiforesolver") || contains(intent, "FifoResolver"),
"fallback intent wrong: " + intent);
P();
}
void t5() {
T(no_whetstone_step_ids_injected_for_external_spec);
// This test validates via the decomposer + taskitem generator output.
// We check that stepIds in the executionContract come from the spec,
// not from hardcoded whetstone defaults (1939/1940/1941).
// The decomposer doesn't emit stepIds — that's contract territory.
// We just validate title/intent are correct and no internal IDs leak
// from ScopeMilestoneDecomposer itself.
RequirementNormalizationResult norm;
norm.requirements.push_back(makeGoal(
"goal-4",
"Implement GazeWindowResolver reading /tmp/whimp_gaze_target",
"implement gazewindowresolver reading tmp whimp gaze target"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty");
const std::string title = plan.milestones[0].workstreams[0].title;
const std::string intent = plan.milestones[0].workstreams[0].intent;
// Verify no whetstone-internal text leaks from decomposer
C(!contains(title, "1939") && !contains(title, "1940") && !contains(title, "1941"),
"whetstone step ID leaked into title: " + title);
C(!contains(intent, "whetstone_") && !contains(intent, "RegisterArchitectIntakeTools"),
"whetstone-internal text in intent: " + intent);
C(title.rfind("Implement ", 0) == 0 || title.rfind("Plan ", 0) == 0, "expected meaningful prefix: " + title);
C(contains(title, "GazeWindowResolver"), "class name missing: " + title);
P();
}
void t6() {
T(grouped_title_uses_requirement_summary_not_generic_bucket);
RequirementNormalizationResult norm;
norm.requirements.push_back(makeGoal(
"goal-5",
"`whimpwm` is a Rust-based compositor project.",
"whimpwm is a rust based compositor project"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty() && !plan.milestones[0].workstreams.empty(), "empty");
const std::string title = plan.milestones[0].workstreams[0].title;
C(title.find("Scaffold minimal compositor runtime") == std::string::npos,
"generic bucket title leaked: " + title);
C(title.find("whimpwm") != std::string::npos || title.find("Rust-based compositor project") != std::string::npos,
"requirement summary missing: " + title);
P();
}
void t7() {
T(work_item_subsections_group_separately);
RequirementNormalizationResult norm;
norm.requirements.push_back(makeRequirement(
"r-w1a",
"W1. Introduce Composition Types",
"w1 introduce composition types"));
norm.requirements.push_back(makeRequirement(
"r-w1b",
"W1. Introduce Composition Types: add typed layout structs in crates/whimpwm/src/presentation.rs",
"w1 introduce composition types add typed layout structs in crates whimpwm src presentation rs"));
norm.requirements.push_back(makeRequirement(
"r-w2a",
"W2. Translate Policy Artifact Into Composition Inputs",
"w2 translate policy artifact into composition inputs"));
norm.requirements.push_back(makeRequirement(
"r-w2b",
"W2. Translate Policy Artifact Into Composition Inputs: extend crates/whimpwm/src/presentation_artifact.rs",
"w2 translate policy artifact into composition inputs extend crates whimpwm src presentation artifact rs"));
DecomposedScopePlan plan;
std::string err;
C(ScopeMilestoneDecomposer::decompose(norm, &plan, &err), "decompose: " + err);
C(!plan.milestones.empty(), "no milestones");
C(plan.milestones[0].workstreams.size() >= 2, "expected multiple workstreams");
bool sawW1 = false, sawW2 = false;
for (const auto& ws : plan.milestones[0].workstreams) {
if (ws.title.find("W1") != std::string::npos) sawW1 = true;
if (ws.title.find("W2") != std::string::npos) sawW2 = true;
}
C(sawW1, "missing W1 workstream");
C(sawW2, "missing W2 workstream");
P();
}
int main() {
std::cout << "Step 1989: GR-029 fix — project-grounded taskitem titles, intents, stepIds\n";
t1(); t2(); t3(); t4(); t5(); t6(); t7();
std::cout << "\n" << p << "/" << (p + f) << " passed\n";
return f > 0 ? 1 : 0;
}

View File

@@ -230,6 +230,147 @@ void test_cross_project_repo_signal_grounds_target_files() {
PASS();
}
void test_project_requirement_generates_specific_task_title_and_files() {
TEST(project_requirement_generates_specific_task_title_and_files);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-guidance-bridge"},
{"kind", "goal"},
{"normalizedText", "Implement GuidanceBridge formulas in WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp and WHIMP/software/cpp/src/guidance/GuidanceBridge.h"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& task = out["tasks"][0];
std::string title = task.value("title", "");
std::string intent = task.value("intent", "");
CHECK(title.find("GuidanceBridge") != std::string::npos, "title should mention GuidanceBridge");
CHECK(title.find("Intake Foundation") == std::string::npos, "title should not fallback to generic milestone wording");
CHECK(intent.find("GuidanceBridge") != std::string::npos, "intent should preserve requirement text");
const auto& files = task["executionContract"]["targetFiles"];
bool hasCpp = false;
bool hasHeader = false;
for (const auto& f : files) {
if (!f.is_string()) continue;
std::string v = f.get<std::string>();
if (v == "WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp") hasCpp = true;
if (v == "WHIMP/software/cpp/src/guidance/GuidanceBridge.h") hasHeader = true;
}
CHECK(hasCpp, "expected cpp target file");
CHECK(hasHeader, "expected header target file");
PASS();
}
void test_single_project_product_name_does_not_trigger_cross_repo_fallback() {
TEST(single_project_product_name_does_not_trigger_cross_repo_fallback);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-hivemind-shell-onboarding"},
{"kind", "goal"},
{"normalizedText", "Add onboarding plan for installing hivemind_shell in docs/onboarding/hivemind_shell_ubuntu.md"},
{"anchor", "requirements"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& files = out["tasks"][0]["executionContract"]["targetFiles"];
bool hasLocalDoc = false;
for (const auto& f : files) {
if (!f.is_string()) continue;
std::string v = f.get<std::string>();
if (v == "docs/onboarding/hivemind_shell_ubuntu.md") hasLocalDoc = true;
CHECK(v != "hivemind/README.md", "single-project product name should not trigger cross-repo fallback");
}
CHECK(hasLocalDoc, "expected local onboarding doc target");
PASS();
}
void test_strict_contract_infers_test_files_and_verification_type_for_code_work() {
TEST(strict_contract_infers_test_files_and_verification_type_for_code_work);
MCPServer mcp;
json reqs = json::array({{
{"requirementId", "goal-guidance-bridge-tests"},
{"kind", "goal"},
{"normalizedText", "Implement GuidanceBridge formulas in WHIMP/software/cpp/src/guidance/GuidanceBridge.cpp and WHIMP/software/cpp/src/guidance/GuidanceBridge.h"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
}});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs}, {"strictExecutionContract", true}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& contract = out["tasks"][0]["executionContract"];
CHECK(contract.value("verificationType", "") == "unit", "expected unit verification type");
const auto& tests = contract["testFiles"];
CHECK(tests.is_array() && !tests.empty(), "expected inferred test files");
bool hasGuidanceBridgeTest = false;
for (const auto& testFile : tests) {
if (!testFile.is_string()) continue;
if (testFile.get<std::string>() == "WHIMP/software/cpp/tests/guidance/GuidanceBridge_test.cpp") {
hasGuidanceBridgeTest = true;
}
}
CHECK(hasGuidanceBridgeTest, "expected inferred GuidanceBridge C++ test path");
PASS();
}
void test_rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file() {
TEST(rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file);
MCPServer mcp;
json reqs = json::array({
{
{"requirementId", "goal-rust-1"},
{"kind", "goal"},
{"normalizedText", "Implement control-plane recomposition in crates/whimpwm/src/render.rs and crates/whimpwm/src/presentation.rs"},
{"sourceText", "Implement control-plane recomposition in `crates/whimpwm/src/render.rs` and `crates/whimpwm/src/presentation.rs`"},
{"anchor", "goals"},
{"sourceLine", 1},
{"ambiguous", false}
},
{
{"requirementId", "constraint-rust-1"},
{"kind", "constraint"},
{"normalizedText", "Do not generate or verify go tasks for this sprint"},
{"sourceText", "Do not generate or verify Go tasks for this sprint."},
{"anchor", "constraints"},
{"sourceLine", 2},
{"ambiguous", false}
}
});
json out = callTool(mcp, "whetstone_generate_taskitems",
{{"normalizedRequirements", reqs},
{"strictExecutionContract", true},
{"workspace", "/home/bill/Documents/whimpwm"}});
CHECK(out.value("success", false), "success should be true");
CHECK(out["tasks"].is_array() && !out["tasks"].empty(), "tasks should be non-empty");
const auto& contract = out["tasks"][0]["executionContract"];
bool sawGo = false;
bool sawCargo = false;
for (const auto& commandJson : contract["acceptanceCommands"]) {
if (!commandJson.is_string()) continue;
const std::string command = commandJson.get<std::string>();
if (command.find("go test") != std::string::npos) sawGo = true;
if (command.find("cargo ") != std::string::npos) sawCargo = true;
}
CHECK(!sawGo, "go test should be removed for Rust repo");
CHECK(sawCargo, "cargo command should be present for Rust repo");
bool sawMainTest = false;
for (const auto& testFileJson : contract["testFiles"]) {
if (!testFileJson.is_string()) continue;
if (testFileJson.get<std::string>() == "crates/whimpwm/tests/main_test.rs") sawMainTest = true;
}
CHECK(sawMainTest, "expected real whimpwm Rust test file");
PASS();
}
int main() {
std::cout << "Step 615: whetstone_generate_taskitems MCP Tool\n";
@@ -246,6 +387,10 @@ int main() {
test_rejects_empty_normalized_requirements(); // 11
test_rejects_invalid_requirement_kind(); // 12
test_cross_project_repo_signal_grounds_target_files(); // 13
test_project_requirement_generates_specific_task_title_and_files(); // 14
test_single_project_product_name_does_not_trigger_cross_repo_fallback(); // 15
test_strict_contract_infers_test_files_and_verification_type_for_code_work(); // 16
test_rust_repo_contract_drops_wrong_language_commands_and_uses_real_test_file(); // 17
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;

View File

@@ -166,9 +166,84 @@ void t9() {
PASS();
}
void t10() {
TEST(strict_contract_flags_missing_verification_metadata);
MCPServer mcp;
auto workspace = makeWorkspace();
json item = validTaskitem();
item["execution_contract"] = {
{"stepIds", json::array({"1939"})},
{"targetFiles", json::array({"python/hivemind_installer/models.py"})},
{"requiredTools", json::array({"whetstone_generate_taskitems"})},
{"acceptanceCommands", json::array({"pytest"})},
{"executionSpecificityScore", 75}
};
auto out = callTool(mcp, "whetstone_validate_taskitem", {
{"taskitems", json::array({item})},
{"workspace", workspace},
{"strict_execution_contract", true}
});
CHECK(out.value("success", false), "expected success");
CHECK(out["report"]["results"].size() == 1, "expected one result");
const auto& result = out["report"]["results"][0];
CHECK(result.value("gap_class", "") == "under_constrained_taskitem",
"expected under_constrained_taskitem");
bool sawVerificationIssue = false;
for (const auto& issue : result["issues"]) {
if (!issue.is_string()) continue;
const std::string text = issue.get<std::string>();
if (text.find("verificationType") != std::string::npos ||
text.find("testFiles") != std::string::npos) {
sawVerificationIssue = true;
}
}
CHECK(sawVerificationIssue, "expected verification metadata issue");
PASS();
}
void t11() {
TEST(malformed_paths_and_wrong_language_commands_are_under_constrained);
MCPServer mcp;
json item = validTaskitem();
item["execution_contract"] = {
{"targetFiles", json::array({
"crates/whimpwm/src/render.rs",
"crates/whimpwm/tests/`.",
"crates/whimpwm`."
})},
{"acceptanceCommands", json::array({
"cargo test",
"go test ./..."
})},
{"verificationType", "unit"},
{"testFiles", json::array({"crates/whimpwm/tests/main_test.rs"})},
{"executionSpecificityScore", 90}
};
auto out = callTool(mcp, "whetstone_validate_taskitem", {
{"taskitems", json::array({item})},
{"strict_execution_contract", true}
});
CHECK(out.value("success", false), "expected success");
CHECK(out["report"]["results"].size() == 1, "expected one result");
const auto& result = out["report"]["results"][0];
CHECK(result.value("gap_class", "") == "under_constrained_taskitem",
"expected under_constrained_taskitem");
bool sawMalformed = false;
bool sawIncompatible = false;
for (const auto& issue : result["issues"]) {
if (!issue.is_string()) continue;
const std::string text = issue.get<std::string>();
if (text.find("malformed_target_path") != std::string::npos) sawMalformed = true;
if (text.find("incompatible_acceptance_commands") != std::string::npos) sawIncompatible = true;
}
CHECK(sawMalformed, "expected malformed target path issue");
CHECK(sawIncompatible, "expected incompatible acceptance commands issue");
PASS();
}
int main() {
std::cout << "Step 682: whetstone_validate_taskitem MCP tool\n";
t1(); t2(); t3(); t4(); t5(); t6(); t7(); t8(); t9();
t1(); t2(); t3(); t4(); t5(); t6(); t7(); t8(); t9(); t10(); t11();
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}