fix: widen completing-milestone gate to accept "None required" and similar phrasings (#2931) (#3239)

The verification_operational gate used exact equality against "none",
causing a permanent dispatch-stop loop when the planning agent wrote
variants like "None required", "N/A", or "Not applicable". Extract
an isVerificationNotApplicable() helper with a regex that covers
common not-applicable phrasings and use it in the gate condition.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-30 16:30:57 -04:00 committed by GitHub
parent aa6cde32d9
commit 3d0eb32756
2 changed files with 98 additions and 1 deletions

View file

@ -129,6 +129,21 @@ export function setRewriteCount(basePath: string, count: number): void {
writeFileSync(filePath, JSON.stringify({ count, updatedAt: new Date().toISOString() }) + "\n");
}
// ─── Helpers ─────────────────────────────────────────────────────────────
/**
* Returns true when the verification_operational value indicates that no
* operational verification is needed. Covers common phrasings the planning
* agent may use: "None", "None required", "N/A", "Not applicable", etc.
*
* @see https://github.com/gsd-build/gsd-2/issues/2931
*/
export function isVerificationNotApplicable(value: string): boolean {
const v = (value ?? "").toLowerCase().trim();
if (!v || v === "none") return true;
return /^(?:none[\s._-]*(?:required|needed|planned)?|n\/?a|not[\s._-]+(?:applicable|required|needed)|no[\s._-]+operational[\s\S]*)$/i.test(v);
}
// ─── Rules ────────────────────────────────────────────────────────────────
export const DISPATCH_RULES: DispatchRule[] = [
@ -672,7 +687,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
if (isDbAvailable()) {
const milestone = getMilestone(mid);
if (milestone?.verification_operational &&
milestone.verification_operational.toLowerCase() !== "none") {
!isVerificationNotApplicable(milestone.verification_operational)) {
const validationPath = resolveMilestoneFile(basePath, mid, "VALIDATION");
if (validationPath) {
const validationContent = await loadFile(validationPath);

View file

@ -0,0 +1,82 @@
/**
* Regression test for #2931: completing-milestone gate should treat
* "None required", "N/A", "Not applicable", etc. as equivalent to "none"
* and skip the operational verification content check entirely.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { isVerificationNotApplicable } from "../auto-dispatch.ts";
test("isVerificationNotApplicable: bare 'none' is not applicable", () => {
assert.equal(isVerificationNotApplicable("none"), true);
});
test("isVerificationNotApplicable: 'None' (capitalized) is not applicable", () => {
assert.equal(isVerificationNotApplicable("None"), true);
});
test("isVerificationNotApplicable: 'NONE' (uppercase) is not applicable", () => {
assert.equal(isVerificationNotApplicable("NONE"), true);
});
test("isVerificationNotApplicable: 'None required' is not applicable (#2931)", () => {
assert.equal(isVerificationNotApplicable("None required"), true);
});
test("isVerificationNotApplicable: 'None needed' is not applicable", () => {
assert.equal(isVerificationNotApplicable("None needed"), true);
});
test("isVerificationNotApplicable: 'None planned' is not applicable", () => {
assert.equal(isVerificationNotApplicable("None planned"), true);
});
test("isVerificationNotApplicable: 'N/A' is not applicable", () => {
assert.equal(isVerificationNotApplicable("N/A"), true);
});
test("isVerificationNotApplicable: 'n/a' is not applicable", () => {
assert.equal(isVerificationNotApplicable("n/a"), true);
});
test("isVerificationNotApplicable: 'Not applicable' is not applicable", () => {
assert.equal(isVerificationNotApplicable("Not applicable"), true);
});
test("isVerificationNotApplicable: 'Not required' is not applicable", () => {
assert.equal(isVerificationNotApplicable("Not required"), true);
});
test("isVerificationNotApplicable: 'Not needed' is not applicable", () => {
assert.equal(isVerificationNotApplicable("Not needed"), true);
});
test("isVerificationNotApplicable: 'No operational verification needed' is not applicable", () => {
assert.equal(isVerificationNotApplicable("No operational verification needed"), true);
});
test("isVerificationNotApplicable: 'No operational' is not applicable", () => {
assert.equal(isVerificationNotApplicable("No operational"), true);
});
test("isVerificationNotApplicable: empty string is not applicable", () => {
assert.equal(isVerificationNotApplicable(""), true);
});
test("isVerificationNotApplicable: whitespace-only is not applicable", () => {
assert.equal(isVerificationNotApplicable(" "), true);
});
// Positive cases: these SHOULD require verification
test("isVerificationNotApplicable: 'Run load tests' requires verification", () => {
assert.equal(isVerificationNotApplicable("Run load tests"), false);
});
test("isVerificationNotApplicable: 'Verify API response times under load' requires verification", () => {
assert.equal(isVerificationNotApplicable("Verify API response times under load"), false);
});
test("isVerificationNotApplicable: 'Monitor error rates for 24h' requires verification", () => {
assert.equal(isVerificationNotApplicable("Monitor error rates for 24h"), false);
});