feat(gsd): wire blocking behavior and strict mode for enhanced verification

Integrates pre/post-execution checks into auto-mode:
- auto-verification.ts: runEnhancedPreChecks/runEnhancedPostChecks integration
- auto-post-unit.ts: pause control flow when blocking checks fail
- Respects enhanced_verification_strict preference for blocking vs warning

Control flow: blocking failures trigger auto-mode pause for user review.
This commit is contained in:
Alan Alwakeel 2026-04-03 16:18:34 -04:00
parent a3d08f7125
commit 6ee0f40083
2 changed files with 289 additions and 2 deletions

View file

@ -18,6 +18,7 @@ import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
import { loadPrompt } from "./prompt-loader.js";
import {
resolveSliceFile,
resolveSlicePath,
resolveTaskFile,
resolveMilestoneFile,
resolveTasksDir,
@ -59,6 +60,10 @@ import { validateFileChanges } from "./safety/file-change-validator.js";
import { validateContent } from "./safety/content-validator.js";
import { resolveSafetyHarnessConfig } from "./safety/safety-harness.js";
import { resolveExpectedArtifactPath as resolveArtifactForContent } from "./auto-artifact-paths.js";
import { loadEffectiveGSDPreferences } from "./preferences.js";
import { getSliceTasks } from "./gsd-db.js";
import { runPreExecutionChecks, type PreExecutionResult } from "./pre-execution-checks.js";
import { writePreExecutionEvidence } from "./verification-evidence.js";
/** Maximum verification retry attempts before escalating to blocker placeholder (#2653). */
const MAX_VERIFICATION_RETRIES = 3;
@ -772,6 +777,107 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
}
}
// ── Pre-execution checks (after plan-slice completes) ──
if (
s.currentUnit &&
s.currentUnit.type === "plan-slice"
) {
let preExecPauseNeeded = false;
await runSafely("postUnitPostVerification", "pre-execution-checks", async () => {
// Check preferences — respect enhanced_verification and enhanced_verification_pre
const prefs = loadEffectiveGSDPreferences()?.preferences;
const enhancedEnabled = prefs?.enhanced_verification !== false; // default true
const preEnabled = prefs?.enhanced_verification_pre !== false; // default true
if (!enhancedEnabled || !preEnabled) {
debugLog("postUnitPostVerification", {
phase: "pre-execution-checks",
skipped: true,
reason: "disabled by preferences",
});
return;
}
// Parse the unit ID to get milestone/slice IDs
const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit!.id);
if (!mid || !sid) {
debugLog("postUnitPostVerification", {
phase: "pre-execution-checks",
skipped: true,
reason: "could not parse milestone/slice from unit ID",
});
return;
}
// Get tasks for this slice from DB
const tasks = getSliceTasks(mid, sid);
if (tasks.length === 0) {
debugLog("postUnitPostVerification", {
phase: "pre-execution-checks",
skipped: true,
reason: "no tasks found for slice",
});
return;
}
// Run pre-execution checks
const result: PreExecutionResult = await runPreExecutionChecks(tasks, s.basePath);
// Log summary to stderr in existing verification output format
const emoji = result.status === "pass" ? "✅" : result.status === "warn" ? "⚠️" : "❌";
process.stderr.write(
`gsd-pre-exec: ${emoji} Pre-execution checks ${result.status} for ${mid}/${sid} (${result.durationMs}ms)\n`,
);
// Log individual check results
for (const check of result.checks) {
const checkEmoji = check.passed ? "✓" : check.blocking ? "✗" : "⚠";
process.stderr.write(
`gsd-pre-exec: ${checkEmoji} [${check.category}] ${check.target}: ${check.message}\n`,
);
}
// Write evidence JSON to slice artifacts directory
const slicePath = resolveSlicePath(s.basePath, mid, sid);
if (slicePath) {
writePreExecutionEvidence(result, slicePath, mid, sid);
}
// Notify UI
if (result.status === "fail") {
const blockingCount = result.checks.filter(c => !c.passed && c.blocking).length;
ctx.ui.notify(
`Pre-execution checks failed: ${blockingCount} blocking issue${blockingCount === 1 ? "" : "s"} found`,
"error",
);
preExecPauseNeeded = true;
} else if (result.status === "warn") {
ctx.ui.notify(
`Pre-execution checks passed with warnings`,
"warning",
);
// Strict mode: treat warnings as blocking
if (prefs?.enhanced_verification_strict === true) {
preExecPauseNeeded = true;
}
}
debugLog("postUnitPostVerification", {
phase: "pre-execution-checks",
status: result.status,
checkCount: result.checks.length,
durationMs: result.durationMs,
});
});
// Check for blocking failures after runSafely completes
if (preExecPauseNeeded) {
debugLog("postUnitPostVerification", { phase: "pre-execution-checks", pausing: true, reason: "blocking failures detected" });
await pauseAuto(ctx, pi);
return "stopped";
}
}
// ── Triage check ──
if (
!s.stepMode &&

View file

@ -11,9 +11,10 @@
*/
import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent";
import { mkdirSync, writeFileSync } from "node:fs";
import { resolveSliceFile, resolveSlicePath } from "./paths.js";
import { parseUnitId } from "./unit-id.js";
import { isDbAvailable, getTask } from "./gsd-db.js";
import { isDbAvailable, getTask, getSliceTasks, type TaskRow } from "./gsd-db.js";
import { loadEffectiveGSDPreferences } from "./preferences.js";
import {
runVerificationGate,
@ -21,9 +22,11 @@ import {
captureRuntimeErrors,
runDependencyAudit,
} from "./verification-gate.js";
import { writeVerificationJSON } from "./verification-evidence.js";
import { writeVerificationJSON, type PostExecutionCheckJSON, type EvidenceJSON } from "./verification-evidence.js";
import { logWarning } from "./workflow-logger.js";
import { runPostExecutionChecks, type PostExecutionResult } from "./post-execution-checks.js";
import type { AutoSession } from "./auto/session.js";
import type { VerificationResult as VerificationGateResult } from "./types.js";
import { join } from "node:path";
export interface VerificationContext {
@ -183,6 +186,128 @@ export async function runPostUnitVerification(
return "continue";
}
// ── Post-execution checks (run after main verification passes for execute-task units) ──
let postExecChecks: PostExecutionCheckJSON[] | undefined;
let postExecBlockingFailure = false;
if (result.passed && mid && sid && tid) {
// Check preferences — respect enhanced_verification and enhanced_verification_post
const enhancedEnabled = prefs?.enhanced_verification !== false; // default true
const postEnabled = prefs?.enhanced_verification_post !== false; // default true
if (enhancedEnabled && postEnabled && isDbAvailable()) {
try {
// Get the completed task from DB
const taskRow = getTask(mid, sid, tid);
if (taskRow && taskRow.key_files && taskRow.key_files.length > 0) {
// Get all tasks in the slice
const allTasks = getSliceTasks(mid, sid);
// Filter to prior completed tasks (status = 'complete' or 'done', before current task)
const priorTasks = allTasks.filter(
(t: TaskRow) =>
(t.status === "complete" || t.status === "done") &&
t.id !== tid &&
t.sequence < taskRow.sequence
);
// Run post-execution checks
const postExecResult: PostExecutionResult = runPostExecutionChecks(
taskRow,
priorTasks,
s.basePath
);
// Store checks for evidence JSON
postExecChecks = postExecResult.checks;
// Log summary to stderr with gsd-post-exec: prefix
const emoji =
postExecResult.status === "pass"
? "✅"
: postExecResult.status === "warn"
? "⚠️"
: "❌";
process.stderr.write(
`gsd-post-exec: ${emoji} Post-execution checks ${postExecResult.status} for ${mid}/${sid}/${tid} (${postExecResult.durationMs}ms)\n`
);
// Log individual check results
for (const check of postExecResult.checks) {
const checkEmoji = check.passed
? "✓"
: check.blocking
? "✗"
: "⚠";
process.stderr.write(
`gsd-post-exec: ${checkEmoji} [${check.category}] ${check.target}: ${check.message}\n`
);
}
// Check for blocking failures
if (postExecResult.status === "fail") {
postExecBlockingFailure = true;
const blockingCount = postExecResult.checks.filter(
(c) => !c.passed && c.blocking
).length;
ctx.ui.notify(
`Post-execution checks failed: ${blockingCount} blocking issue${blockingCount === 1 ? "" : "s"} found`,
"error"
);
} else if (postExecResult.status === "warn") {
ctx.ui.notify(
`Post-execution checks passed with warnings`,
"warning"
);
// Strict mode: treat warnings as blocking
if (prefs?.enhanced_verification_strict === true) {
postExecBlockingFailure = true;
}
}
}
} catch (postExecErr) {
// Post-execution check errors are non-fatal — log and continue
process.stderr.write(
`gsd-post-exec: error — ${(postExecErr as Error).message}\n`
);
}
}
}
// Re-write verification evidence JSON with post-execution checks
if (postExecChecks && postExecChecks.length > 0 && mid && sid && tid) {
try {
const sDir = resolveSlicePath(s.basePath, mid, sid);
if (sDir) {
const tasksDir = join(sDir, "tasks");
// Add postExecutionChecks to the result for the JSON write
const resultWithPostExec = {
...result,
// Mark as failed if there was a blocking post-exec failure
passed: result.passed && !postExecBlockingFailure,
};
// Manually write with postExecutionChecks field
writeVerificationJSONWithPostExec(
resultWithPostExec,
tasksDir,
tid,
s.currentUnit.id,
postExecChecks,
postExecBlockingFailure ? attempt + 1 : undefined,
postExecBlockingFailure ? maxRetries : undefined
);
}
} catch (evidenceErr) {
process.stderr.write(
`verification-evidence: post-exec write error — ${(evidenceErr as Error).message}\n`
);
}
}
// Update result.passed based on post-execution checks
if (postExecBlockingFailure) {
result.passed = false;
}
// ── Auto-fix retry logic ──
if (result.passed) {
s.verificationRetryCount.delete(s.currentUnit.id);
@ -231,3 +356,59 @@ export async function runPostUnitVerification(
return "continue";
}
}
/**
* Write verification evidence JSON with post-execution checks included.
* This is a variant of writeVerificationJSON that adds the postExecutionChecks field.
*/
function writeVerificationJSONWithPostExec(
result: VerificationGateResult,
tasksDir: string,
taskId: string,
unitId: string,
postExecutionChecks: PostExecutionCheckJSON[],
retryAttempt?: number,
maxRetries?: number,
): void {
mkdirSync(tasksDir, { recursive: true });
const evidence: EvidenceJSON = {
schemaVersion: 1,
taskId,
unitId: unitId ?? taskId,
timestamp: result.timestamp,
passed: result.passed,
discoverySource: result.discoverySource,
checks: result.checks.map((check) => ({
command: check.command,
exitCode: check.exitCode,
durationMs: check.durationMs,
verdict: check.exitCode === 0 ? "pass" : "fail",
})),
...(retryAttempt !== undefined ? { retryAttempt } : {}),
...(maxRetries !== undefined ? { maxRetries } : {}),
postExecutionChecks,
};
if (result.runtimeErrors && result.runtimeErrors.length > 0) {
evidence.runtimeErrors = result.runtimeErrors.map(e => ({
source: e.source,
severity: e.severity,
message: e.message,
blocking: e.blocking,
}));
}
if (result.auditWarnings && result.auditWarnings.length > 0) {
evidence.auditWarnings = result.auditWarnings.map(w => ({
name: w.name,
severity: w.severity,
title: w.title,
url: w.url,
fixAvailable: w.fixAvailable,
}));
}
const filePath = join(tasksDir, `${taskId}-VERIFY.json`);
writeFileSync(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
}