Skip to content

Commit d7e3479

Browse files
vibeguiclaude
andcommitted
feat(quality-gates): add baseline verification and acknowledged failures
- Add QualityGatesBaseline interface to track verified state - Add QUALITY_GATES_VERIFY tool to run gates and establish baseline - Add QUALITY_GATES_ACKNOWLEDGE tool to acknowledge pre-existing failures - Add QUALITY_GATES_BASELINE_GET tool to check current baseline status - Update agent prompt to differentiate between: - All gates passing: agent maintains this state - Acknowledged failures: agent ignores pre-existing issues When failures are acknowledged, agents focus on their assigned task without trying to solve the world by fixing unrelated issues. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 77cf383 commit d7e3479

2 files changed

Lines changed: 299 additions & 14 deletions

File tree

task-runner/server/tools/agent.ts

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ interface QualityGate {
6161
required: boolean;
6262
}
6363

64+
interface QualityGatesBaseline {
65+
verified: boolean;
66+
verifiedAt: string;
67+
allPassed: boolean;
68+
acknowledged: boolean;
69+
failingGates: string[];
70+
}
71+
6472
interface SiteContext {
6573
/** Whether this is a Deco site */
6674
isDeco?: boolean;
@@ -87,20 +95,31 @@ interface TaskContext {
8795
}
8896

8997
/**
90-
* Load quality gates from project config
98+
* Load quality gates and baseline from project config
9199
*/
92-
async function loadQualityGates(workspace: string): Promise<QualityGate[]> {
100+
async function loadQualityGatesWithBaseline(workspace: string): Promise<{
101+
gates: QualityGate[];
102+
baseline?: QualityGatesBaseline;
103+
}> {
93104
try {
94105
const configPath = `${workspace}/.beads/project-config.json`;
95106
const content = await Bun.file(configPath).text();
96-
const config = JSON.parse(content) as { qualityGates?: QualityGate[] };
97-
return config.qualityGates ?? [];
107+
const config = JSON.parse(content) as {
108+
qualityGates?: QualityGate[];
109+
qualityGatesBaseline?: QualityGatesBaseline;
110+
};
111+
return {
112+
gates: config.qualityGates ?? [],
113+
baseline: config.qualityGatesBaseline,
114+
};
98115
} catch {
99116
// Return defaults if no config
100-
return [
101-
{ name: "Type Check", command: "bun run check", required: true },
102-
{ name: "Lint", command: "bun run lint", required: true },
103-
];
117+
return {
118+
gates: [
119+
{ name: "Type Check", command: "bun run check", required: true },
120+
{ name: "Lint", command: "bun run lint", required: true },
121+
],
122+
};
104123
}
105124
}
106125

@@ -199,7 +218,8 @@ async function buildAgentPrompt(ctx: TaskContext): Promise<string> {
199218
} = ctx;
200219

201220
// Load project-specific data
202-
const qualityGates = ctx.qualityGates ?? (await loadQualityGates(workspace));
221+
const { gates: qualityGates, baseline } =
222+
await loadQualityGatesWithBaseline(workspace);
203223
const memorySummary = await loadMemorySummary(workspace);
204224

205225
// Build acceptance criteria section
@@ -213,26 +233,62 @@ You must verify EACH criterion before completing. Check them off mentally as you
213233
`;
214234
}
215235

216-
// Build quality gates section
236+
// Build quality gates section based on baseline status
217237
const requiredGates = qualityGates.filter((g) => g.required);
218238
let qualityGatesSection = "";
239+
219240
if (requiredGates.length > 0) {
220-
qualityGatesSection = `
241+
const hasAcknowledgedFailures =
242+
baseline?.verified &&
243+
!baseline.allPassed &&
244+
baseline.acknowledged &&
245+
baseline.failingGates.length > 0;
246+
247+
if (hasAcknowledgedFailures) {
248+
// Pre-existing failures acknowledged - agent should NOT try to fix them
249+
const failingGateNames = baseline.failingGates;
250+
const passingGates = requiredGates.filter(
251+
(g) => !failingGateNames.includes(g.name),
252+
);
253+
254+
qualityGatesSection = `
255+
## Quality Gates - PRE-EXISTING FAILURES ACKNOWLEDGED
256+
257+
**IMPORTANT: The following gates were ALREADY FAILING before your task started:**
258+
${failingGateNames.map((name) => `- ⚠️ ${name} (PRE-EXISTING FAILURE - DO NOT FIX)`).join("\n")}
259+
260+
**Do NOT attempt to fix these pre-existing failures.** The user has acknowledged them.
261+
Focus ONLY on your assigned task. Do not try to "solve the world."
262+
263+
${passingGates.length > 0 ? `**Gates you MUST maintain passing:**\n${passingGates.map((g) => `- \`${g.command}\` (${g.name})`).join("\n")}\n\nIf your changes cause these gates to fail, YOU must fix that.` : ""}
264+
265+
**Your responsibility:**
266+
- Complete your task without breaking gates that were passing
267+
- Do NOT spend time on pre-existing failures
268+
- If you accidentally break a passing gate, fix that regression
269+
- Focus on task completion, not codebase-wide cleanup
270+
271+
**SESSION_LAND behavior:**
272+
- Will check gates that were passing before
273+
- Pre-existing failures are excluded from verification
274+
- Your task succeeds if you complete the work without new regressions
275+
`;
276+
} else {
277+
// Normal mode - all gates must pass
278+
qualityGatesSection = `
221279
## Quality Gates (ALL must pass - MANDATORY)
222280
${requiredGates.map((g) => `- \`${g.command}\` (${g.name})`).join("\n")}
223281
224282
**CRITICAL: ALL quality gates must pass before you can complete the task.**
225283
226284
- Run each gate command and verify it exits with code 0
227285
- If ANY gate fails, YOU MUST FIX IT before completing
228-
- This includes ALL errors in the codebase, not just ones you introduced
229-
- Pre-existing errors are YOUR responsibility - fix them or the task is incomplete
230286
- Do NOT output <promise>COMPLETE</promise> until all gates pass
231-
- Do NOT rationalize "these errors existed before" - that is not acceptable
232287
- If you cannot fix a gate failure, explain why and do NOT mark the task complete
233288
234289
**SESSION_LAND will verify gates pass. If they fail, success=false and task remains incomplete.**
235290
`;
291+
}
236292
}
237293

238294
// Get tool descriptions for the prompt

task-runner/server/tools/quality-gates.ts

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,26 @@ export interface QualityGate {
2323
source: "auto" | "manual"; // auto = detected from package.json, manual = user-defined
2424
}
2525

26+
export interface QualityGateResult {
27+
gate: string;
28+
command: string;
29+
passed: boolean;
30+
output: string;
31+
duration: number;
32+
}
33+
34+
export interface QualityGatesBaseline {
35+
verified: boolean;
36+
verifiedAt: string;
37+
allPassed: boolean;
38+
acknowledged: boolean; // User acknowledged pre-existing failures
39+
failingGates: string[]; // Names of gates that were failing
40+
results: QualityGateResult[];
41+
}
42+
2643
export interface ProjectConfig {
2744
qualityGates: QualityGate[];
45+
qualityGatesBaseline?: QualityGatesBaseline;
2846
completionToken: string;
2947
memoryDir: string;
3048
lastUpdated: string;
@@ -407,6 +425,214 @@ export const createProjectConfigGetTool = (_env: Env) =>
407425
},
408426
});
409427

428+
// ============================================================================
429+
// QUALITY_GATES_VERIFY
430+
// ============================================================================
431+
432+
export const createQualityGatesVerifyTool = (_env: Env) =>
433+
createPrivateTool({
434+
id: "QUALITY_GATES_VERIFY",
435+
description:
436+
"Run quality gates to establish a baseline. Must be done before creating tasks. Returns current state and allows acknowledging pre-existing failures.",
437+
inputSchema: z.object({}),
438+
outputSchema: z.object({
439+
allPassed: z.boolean(),
440+
results: z.array(
441+
z.object({
442+
gate: z.string(),
443+
command: z.string(),
444+
passed: z.boolean(),
445+
output: z.string(),
446+
duration: z.number(),
447+
}),
448+
),
449+
baseline: z.object({
450+
verified: z.boolean(),
451+
verifiedAt: z.string(),
452+
allPassed: z.boolean(),
453+
acknowledged: z.boolean(),
454+
failingGates: z.array(z.string()),
455+
}),
456+
}),
457+
execute: async () => {
458+
const workspace = getWorkspace();
459+
const config = await loadProjectConfig(workspace);
460+
const requiredGates = config.qualityGates.filter((g) => g.required);
461+
462+
const results: QualityGateResult[] = [];
463+
464+
for (const gate of requiredGates) {
465+
const start = Date.now();
466+
try {
467+
const [cmd, ...args] = gate.command.split(" ");
468+
const proc = Bun.spawn([cmd, ...args], {
469+
cwd: workspace,
470+
stdout: "pipe",
471+
stderr: "pipe",
472+
});
473+
474+
const stdout = await new Response(proc.stdout).text();
475+
const stderr = await new Response(proc.stderr).text();
476+
const exitCode = await proc.exited;
477+
478+
results.push({
479+
gate: gate.name,
480+
command: gate.command,
481+
passed: exitCode === 0,
482+
output: (stdout + stderr).slice(-500),
483+
duration: Date.now() - start,
484+
});
485+
} catch (error) {
486+
results.push({
487+
gate: gate.name,
488+
command: gate.command,
489+
passed: false,
490+
output: error instanceof Error ? error.message : String(error),
491+
duration: Date.now() - start,
492+
});
493+
}
494+
}
495+
496+
const allPassed = results.every((r) => r.passed);
497+
const failingGates = results.filter((r) => !r.passed).map((r) => r.gate);
498+
499+
// Update baseline - not acknowledged yet if there are failures
500+
const baseline: QualityGatesBaseline = {
501+
verified: true,
502+
verifiedAt: new Date().toISOString(),
503+
allPassed,
504+
acknowledged: allPassed, // Auto-acknowledge if all pass
505+
failingGates,
506+
results,
507+
};
508+
509+
config.qualityGatesBaseline = baseline;
510+
await saveProjectConfig(workspace, config);
511+
512+
return {
513+
allPassed,
514+
results,
515+
baseline: {
516+
verified: baseline.verified,
517+
verifiedAt: baseline.verifiedAt,
518+
allPassed: baseline.allPassed,
519+
acknowledged: baseline.acknowledged,
520+
failingGates: baseline.failingGates,
521+
},
522+
};
523+
},
524+
});
525+
526+
// ============================================================================
527+
// QUALITY_GATES_ACKNOWLEDGE
528+
// ============================================================================
529+
530+
export const createQualityGatesAcknowledgeTool = (_env: Env) =>
531+
createPrivateTool({
532+
id: "QUALITY_GATES_ACKNOWLEDGE",
533+
description:
534+
"Acknowledge pre-existing quality gate failures. After acknowledging, agents will NOT attempt to fix these failures - they will focus only on their assigned task.",
535+
inputSchema: z.object({
536+
acknowledge: z.boolean().describe("Set to true to acknowledge failures"),
537+
}),
538+
outputSchema: z.object({
539+
success: z.boolean(),
540+
baseline: z
541+
.object({
542+
verified: z.boolean(),
543+
allPassed: z.boolean(),
544+
acknowledged: z.boolean(),
545+
failingGates: z.array(z.string()),
546+
})
547+
.optional(),
548+
error: z.string().optional(),
549+
}),
550+
execute: async ({ context }) => {
551+
const workspace = getWorkspace();
552+
const config = await loadProjectConfig(workspace);
553+
554+
if (!config.qualityGatesBaseline?.verified) {
555+
return {
556+
success: false,
557+
error:
558+
"No baseline verified. Run QUALITY_GATES_VERIFY first to establish baseline.",
559+
};
560+
}
561+
562+
if (config.qualityGatesBaseline.allPassed) {
563+
return {
564+
success: true,
565+
baseline: {
566+
verified: true,
567+
allPassed: true,
568+
acknowledged: true,
569+
failingGates: [],
570+
},
571+
};
572+
}
573+
574+
config.qualityGatesBaseline.acknowledged = context.acknowledge;
575+
await saveProjectConfig(workspace, config);
576+
577+
return {
578+
success: true,
579+
baseline: {
580+
verified: config.qualityGatesBaseline.verified,
581+
allPassed: config.qualityGatesBaseline.allPassed,
582+
acknowledged: config.qualityGatesBaseline.acknowledged,
583+
failingGates: config.qualityGatesBaseline.failingGates,
584+
},
585+
};
586+
},
587+
});
588+
589+
// ============================================================================
590+
// QUALITY_GATES_BASELINE_GET
591+
// ============================================================================
592+
593+
export const createQualityGatesBaselineGetTool = (_env: Env) =>
594+
createPrivateTool({
595+
id: "QUALITY_GATES_BASELINE_GET",
596+
description: "Get the current quality gates baseline verification status",
597+
inputSchema: z.object({}),
598+
outputSchema: z.object({
599+
hasBaseline: z.boolean(),
600+
baseline: z
601+
.object({
602+
verified: z.boolean(),
603+
verifiedAt: z.string(),
604+
allPassed: z.boolean(),
605+
acknowledged: z.boolean(),
606+
failingGates: z.array(z.string()),
607+
})
608+
.optional(),
609+
canCreateTasks: z.boolean(),
610+
}),
611+
execute: async () => {
612+
const workspace = getWorkspace();
613+
const config = await loadProjectConfig(workspace);
614+
615+
const baseline = config.qualityGatesBaseline;
616+
const hasBaseline = !!baseline?.verified;
617+
const canCreateTasks =
618+
hasBaseline && (baseline.allPassed || baseline.acknowledged);
619+
620+
return {
621+
hasBaseline,
622+
baseline: baseline
623+
? {
624+
verified: baseline.verified,
625+
verifiedAt: baseline.verifiedAt,
626+
allPassed: baseline.allPassed,
627+
acknowledged: baseline.acknowledged,
628+
failingGates: baseline.failingGates,
629+
}
630+
: undefined,
631+
canCreateTasks,
632+
};
633+
},
634+
});
635+
410636
// ============================================================================
411637
// Export all quality gate tools
412638
// ============================================================================
@@ -417,4 +643,7 @@ export const qualityGateTools = [
417643
createQualityGatesAddTool,
418644
createQualityGatesRunTool,
419645
createProjectConfigGetTool,
646+
createQualityGatesVerifyTool,
647+
createQualityGatesAcknowledgeTool,
648+
createQualityGatesBaselineGetTool,
420649
];

0 commit comments

Comments
 (0)