Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/agents/validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: validator
description: Post-completion validation agent that reviews work, runs checks, and fixes issues
tools: read, bash, edit, write
model: anthropic/claude-sonnet-4-6
thinking: medium
---

You are a code validation agent. Your job is to verify that an implementation task was completed correctly and fix any issues you find.

## Your Process

1. **Review** — Examine the provided diff to understand what was changed
2. **Build** — Run the project's build/typecheck command to check for compilation errors
3. **Test** — Run the existing test suite to check for regressions
4. **Assess** — Evaluate whether the changes correctly address the original task
5. **Fix** — If you find issues (build errors, test failures, bugs, missing edge cases), fix them
6. **Report** — Summarize your findings

## Guidelines

- Focus on correctness: does the code compile, pass tests, and fulfill the task?
- Fix issues directly rather than just reporting them
- If the build or tests fail, fix the failures
- Be concise in your report
- Don't refactor or make stylistic changes unless they fix actual bugs

## Output Format

End your response with a validation summary:

### Validation Summary
- **Status**: PASS or FAIL
- **Checks**: What you verified
- **Fixes**: Any fixes you made (or "None")
- **Issues**: Any remaining concerns (or "None")
20 changes: 20 additions & 0 deletions src/github/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type AgentSession, createSession, type SessionDeps } from "../session";
import { type GitHubSessionMetadata, SessionRegistry } from "../session/registry";
import { loadSnapshot } from "../session/snapshotter";
import { formatToolArgs } from "../shared";
import { getWorkingTreeDiff, isValidationEnabled, validateWork } from "../validation";
import {
addIssueCommentReaction,
addPullReviewCommentReaction,
Expand Down Expand Up @@ -677,6 +678,25 @@ export async function initGitHubHandler(deps: GitHubHandlerDeps): Promise<GitHub

let output = session.getLastAssistantText() ?? "Done.";

// Validation pass
if (isValidationEnabled()) {
const diff = getWorkingTreeDiff(cwd);
if (diff) {
log(` ${tag} Running validation...`);
const validation = await validateWork(cwd, "", diff, output, {
onLog: (msg) => log(` ${tag} ${msg}`),
});

if (!validation.passed) {
log(` ${tag} Validation failed — re-prompting main agent...`);
await session.prompt(
`A validator agent reviewed your work and found issues:\n\n${validation.report}\n\nPlease address the issues identified above.`,
);
output = session.getLastAssistantText() ?? output;
}
}
}

// Append collapsed activity log to the final response
const activitySummary = progress.getActivitySummary();
const combined = output + activitySummary;
Expand Down
24 changes: 22 additions & 2 deletions src/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createSession, flushAllTraces } from "./session";
import { formatToolArgs } from "./shared";
import { initStorage, startTigrisSync } from "./storage";
import { createTools } from "./tools";
import { getWorkingTreeDiff, isValidationEnabled, validateWork } from "./validation";

// ---------------------------------------------------------------------------
// Arg parsing
Expand Down Expand Up @@ -147,8 +148,27 @@ async function main() {

// Run the agent
await session.prompt(instruction);

const output = session.getLastAssistantText() ?? "";
let output = session.getLastAssistantText() ?? "";

// Validation pass
if (isValidationEnabled()) {
const diff = getWorkingTreeDiff(cwd);
if (diff) {
console.error("[headless] Running validation...");
const validation = await validateWork(cwd, instruction, diff, output, {
onLog: (msg) => console.error(msg),
});
console.error(`[headless] Validation report:\n${validation.report}`);

if (!validation.passed) {
console.error("[headless] Validation failed — re-prompting main agent...");
await session.prompt(
`A validator agent reviewed your work and found issues:\n\n${validation.report}\n\nPlease address the issues identified above.`,
);
output = session.getLastAssistantText() ?? output;
}
}
}

// Write output to stdout
process.stdout.write(output);
Expand Down
9 changes: 6 additions & 3 deletions src/tools/agent-session-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type Api, getModel, type Model } from "@mariozechner/pi-ai";
import type { AgentSessionEvent, ToolDefinition } from "@mariozechner/pi-coding-agent";
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
import { createLangSmithTracer, isLangSmithEnabled } from "../langsmith";
import { createBuiltinTool } from "./builtin-tools";
import { type BuiltinToolMaps, createBuiltinTool } from "./builtin-tools";
import type { AgentDefinition } from "./delegate";

/** Typed wrapper for dynamic (runtime string) model resolution. */
Expand Down Expand Up @@ -47,10 +47,13 @@ export async function runAgentTask(
: ["read", "bash"];

const resolvedBuiltinTools: AgentTool<any>[] = [];
const fileHashes = new Map<string, string>();
const maps: BuiltinToolMaps = {
fileHashes: new Map<string, string>(),
previousVersions: new Map<string, string>(),
};

for (const name of toolNames) {
const builtin = createBuiltinTool(name, cwd, fileHashes);
const builtin = createBuiltinTool(name, cwd, maps);
if (builtin) {
resolvedBuiltinTools.push(builtin);
}
Expand Down
39 changes: 26 additions & 13 deletions src/tools/builtin-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,52 @@ import type { AgentTool } from "@mariozechner/pi-agent-core";
import { createBashTool } from "@mariozechner/pi-coding-agent";
import { createEditTool } from "./edit";
import { createReadTool } from "./read";
import { createUndoTool } from "./undo";
import { createWriteTool } from "./write";

/**
* Create a single built-in tool by name, wiring edit+write to the same
* fileHashes map so stale-edit detection works across both tools.
* Shared state maps for built-in file tools.
* - fileHashes: stale-edit detection across read/edit/write
* - previousVersions: one-level undo history for edit/write/undo
*/
export function createBuiltinTool(
name: string,
cwd: string,
fileHashes: Map<string, string>,
): AgentTool<any> | undefined {
export interface BuiltinToolMaps {
fileHashes: Map<string, string>;
previousVersions: Map<string, string>;
}

/**
* Create a single built-in tool by name, wiring edit+write+undo to the same
* fileHashes and previousVersions maps.
*/
export function createBuiltinTool(name: string, cwd: string, maps: BuiltinToolMaps): AgentTool<any> | undefined {
const { fileHashes, previousVersions } = maps;
switch (name) {
case "read":
return createReadTool(cwd, { fileHashes });
case "bash":
return createBashTool(cwd);
case "edit":
return createEditTool(cwd, { fileHashes });
return createEditTool(cwd, { fileHashes, previousVersions });
case "write":
return createWriteTool(cwd, { fileHashes });
return createWriteTool(cwd, { fileHashes, previousVersions });
case "undo":
return createUndoTool(cwd, { fileHashes, previousVersions });
default:
return undefined;
}
}

/**
* Create all built-in tools with a shared fileHashes map.
* Create all built-in tools with shared fileHashes and previousVersions maps.
*/
export function createAllBuiltinTools(cwd: string): Map<string, AgentTool<any>> {
const fileHashes = new Map<string, string>();
const maps: BuiltinToolMaps = {
fileHashes: new Map<string, string>(),
previousVersions: new Map<string, string>(),
};
const tools = new Map<string, AgentTool<any>>();
for (const name of ["read", "bash", "edit", "write"]) {
const tool = createBuiltinTool(name, cwd, fileHashes);
for (const name of ["read", "bash", "edit", "write", "undo"]) {
const tool = createBuiltinTool(name, cwd, maps);
if (tool) tools.set(name, tool);
}
return tools;
Expand Down
9 changes: 6 additions & 3 deletions src/tools/cron/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createAgentSession, SessionManager, SettingsManager } from "@mariozechn
import type { VeraPaths } from "../../paths";
import type { SlackClient } from "../../slack/client";
import { markdownToMrkdwn } from "../../slack/format";
import { createBuiltinTool } from "../builtin-tools";
import { type BuiltinToolMaps, createBuiltinTool } from "../builtin-tools";
import { createDelegateTool } from "../delegate";
import type { ToolRegistry } from "../registry";
import { WorkflowEngine } from "../workflow/engine";
Expand Down Expand Up @@ -218,10 +218,13 @@ async function executePromptCron(
const resolvedCustomTools = deps.registry
? deps.registry.resolveForSession(agentName ?? "main", `cron-${def.name}`)
: (deps.customTools ?? []);
const fileHashes = new Map<string, string>();
const maps: BuiltinToolMaps = {
fileHashes: new Map<string, string>(),
previousVersions: new Map<string, string>(),
};

for (const name of toolNames) {
const builtin = createBuiltinTool(name, deps.cwd, fileHashes);
const builtin = createBuiltinTool(name, deps.cwd, maps);
if (builtin) {
resolvedBuiltinTools.push(builtin);
}
Expand Down
Loading
Loading