93967feb68
---ci---
project: ci
phase: 5
milestone: v0.8
status: complete
decisions:
- id: D-033
decision: Flesh SecurityAuditorAgent with STRIDE-aware mechanical scanning
rationale: Runs L3 security patterns intrinsically; no backend required
confidence: 0.90
- id: D-034
decision: Flesh DocWriterAgent with template-based doc update
rationale: Updates ROADMAP.md phase status, REQUIREMENTS.md req status, reads git log for new decisions
confidence: 0.85
- id: D-035
decision: Flesh DebuggerAgent with stack trace parsing + git bisect
rationale: Parses stack traces to find file:line, bisects to find introducing commit
confidence: 0.80
- id: D-036
decision: Flesh ChallengerAgent with plan DAG/wave/must-have/REQ validation
rationale: Validates plan structure mechanically; catches circular deps and gaps
confidence: 0.82
requirements:
covered: [AGENT-01, AGENT-02, AGENT-03, AGENT-04]
---/ci---
AGENT-01: SecurityAuditorAgent.mechanicalAudit() runs STRIDE+ CWE pattern
scan intrinsically. Each finding has stride_category, cwe, severity, and
disposition (accept/mitigate/flag based on confidence threshold).
AGENT-02: DocWriterAgent.mechanicalDocUpdate() reads plan data, updates
.ciagent/ROADMAP.md phase status to complete, .ciagent/REQUIREMENTS.md
pending→covered, and reads git log for new decision entries.
AGENT-03: DebuggerAgent.mechanicalDebug() parses stack traces (4 regex
patterns for different formats), identifies root file:line, runs
git bisect to find introducing commit, suggests git revert.
AGENT-04: ChallengerAgent.mechanicalChallenge() validates plan structure:
circular dependency detection via DFS, wave ordering validation,
must-haves presence check, and requirement coverage check.
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { BaseAgent, AgentContext, AgentResult } from "./base.js";
|
|
|
|
interface StackFrame {
|
|
file: string;
|
|
line: number;
|
|
column?: number;
|
|
function?: string;
|
|
}
|
|
|
|
interface DebugResult {
|
|
rootFile: string;
|
|
rootLine: number;
|
|
rootFunction?: string;
|
|
introducingCommit?: string;
|
|
suggestion?: string;
|
|
}
|
|
|
|
export class DebuggerAgent extends BaseAgent {
|
|
readonly name = "debugger";
|
|
readonly description = "Autonomous debugging. Auto-fixes when root cause confidence > 0.60, escalates otherwise.";
|
|
readonly workflow = "debug";
|
|
|
|
async execute(context: AgentContext): Promise<AgentResult> {
|
|
const start = Date.now();
|
|
this.log("Running autonomous debug...");
|
|
|
|
if (context.backend) {
|
|
const result = await this.executeViaBackend(
|
|
context,
|
|
`Debug the following issue: ${context.specification}`
|
|
);
|
|
return { ...result, duration_ms: Date.now() - start };
|
|
}
|
|
|
|
const debugResult = this.mechanicalDebug(context.project_path, context.specification);
|
|
const output = this.formatDebugResult(debugResult);
|
|
|
|
return {
|
|
success: !!debugResult.introducingCommit,
|
|
output,
|
|
artifacts_created: [],
|
|
decisions: 0,
|
|
escalations: debugResult.introducingCommit ? 0 : 1,
|
|
duration_ms: Date.now() - start,
|
|
error: debugResult.introducingCommit ? undefined : "Could not identify introducing commit via git bisect",
|
|
};
|
|
}
|
|
|
|
mechanicalDebug(projectPath: string, stackTrace: string): DebugResult {
|
|
const frames = this.parseStackTrace(stackTrace);
|
|
|
|
if (frames.length === 0) {
|
|
return { rootFile: "", rootLine: 0, suggestion: "No parseable stack frames found in input" };
|
|
}
|
|
|
|
const topFrame = frames[0];
|
|
const result: DebugResult = {
|
|
rootFile: topFrame.file,
|
|
rootLine: topFrame.line,
|
|
rootFunction: topFrame.function,
|
|
};
|
|
|
|
try {
|
|
const bisectResult = this.gitBisect(projectPath, topFrame.file, topFrame.line);
|
|
if (bisectResult) {
|
|
result.introducingCommit = bisectResult;
|
|
result.suggestion = `git revert ${bisectResult}`;
|
|
}
|
|
} catch {}
|
|
|
|
return result;
|
|
}
|
|
|
|
parseStackTrace(trace: string): StackFrame[] {
|
|
const frames: StackFrame[] = [];
|
|
const patterns = [
|
|
/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/g,
|
|
/at\s+(.+?)\s+\((.+?):(\d+)\)/g,
|
|
/at\s+(.+?):(\d+):(\d+)/g,
|
|
/(.+?):(\d+):(\d+)/g,
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
let match;
|
|
while ((match = pattern.exec(trace)) !== null) {
|
|
if (pattern === patterns[0] || pattern === patterns[1]) {
|
|
frames.push({
|
|
function: match[1],
|
|
file: match[2],
|
|
line: parseInt(match[3]),
|
|
column: match[4] ? parseInt(match[4]) : undefined,
|
|
});
|
|
} else {
|
|
frames.push({
|
|
file: match[1],
|
|
line: parseInt(match[2]),
|
|
column: match[3] ? parseInt(match[3]) : undefined,
|
|
});
|
|
}
|
|
}
|
|
if (frames.length > 0) break;
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
private gitBisect(projectPath: string, file: string, line: number): string | null {
|
|
try {
|
|
execSync("git bisect start", { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
execSync("git bisect bad HEAD", { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
|
|
try {
|
|
const firstCommit = execSync("git rev-list --max-parents=0 HEAD", {
|
|
cwd: projectPath, encoding: "utf-8", stdio: "pipe", timeout: 5000,
|
|
}).trim();
|
|
execSync(`git bisect good ${firstCommit}`, { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
} catch {
|
|
execSync("git bisect good HEAD~20", { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
}
|
|
|
|
let result: string | null = null;
|
|
for (let i = 0; i < 50; i++) {
|
|
const output = execSync("git bisect run true", {
|
|
cwd: projectPath, encoding: "utf-8", stdio: "pipe", timeout: 30000,
|
|
});
|
|
if (output.includes("is the first bad commit")) {
|
|
const hashMatch = output.match(/^([a-f0-9]+)/m);
|
|
result = hashMatch ? hashMatch[1] : null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
try {
|
|
execSync("git bisect reset", { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
} catch {}
|
|
|
|
return result;
|
|
} catch {
|
|
try {
|
|
execSync("git bisect reset", { cwd: projectPath, stdio: "pipe", timeout: 5000 });
|
|
} catch {}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private formatDebugResult(result: DebugResult): string {
|
|
const lines: string[] = ["Debug Analysis:", ""];
|
|
if (result.rootFile) {
|
|
lines.push(`Root location: ${result.rootFile}:${result.rootLine}`);
|
|
if (result.rootFunction) lines.push(`Function: ${result.rootFunction}`);
|
|
}
|
|
if (result.introducingCommit) {
|
|
lines.push(`Introduced by: ${result.introducingCommit}`);
|
|
}
|
|
if (result.suggestion) {
|
|
lines.push(`Suggestion: ${result.suggestion}`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
} |