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:
parent
a3d08f7125
commit
6ee0f40083
2 changed files with 289 additions and 2 deletions
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue