11import { execSync } from "child_process" ;
22import fs from "fs" ;
33
4+ /**
5+ * AICODE Agent - Autonomous code modification agent for SlackONOS
6+ *
7+ * This agent uses AI (Claude, OpenAI, or Gemini) to generate code changes
8+ * based on natural language task descriptions from Slack admins.
9+ *
10+ * Key Features:
11+ * - Multi-provider AI support (Claude Sonnet 4.5 default)
12+ * - Safety checks (forbidden files, line limits, security patterns)
13+ * - Robust patch application with multiple fallback strategies
14+ * - Slack notifications for success/failure
15+ * - Automatic PR creation when tests pass
16+ *
17+ * Patch Application Strategies (tried in order):
18+ * 1. Standard git apply
19+ * 2. git apply --unidiff-zero (for exact line matching)
20+ * 3. git apply --ignore-whitespace (for whitespace variations)
21+ * 4. patch -p1 command (traditional Unix patch)
22+ */
23+
424// Support multiple AI providers
525const provider = process . env . AI_PROVIDER || "claude" ; // claude, openai, or gemini
626const task = process . env . TASK || "Improve code quality" ;
@@ -21,7 +41,7 @@ if (provider === "claude") {
2141 process . exit ( 1 ) ;
2242 }
2343 aiClient = new Anthropic ( { apiKey } ) ;
24- aiModel = process . env . CLAUDE_MODEL || "claude-3-5- sonnet-latest " ;
44+ aiModel = process . env . CLAUDE_MODEL || "claude-sonnet-4-5 " ;
2545 console . log ( `[AGENT] Using Claude (Anthropic) with model: ${ aiModel } ` ) ;
2646} else if ( provider === "openai" ) {
2747 // OpenAI (original)
@@ -154,7 +174,7 @@ try {
154174const prompt = `You are an autonomous coding agent for SlackONOS, a democratic music bot for Discord and Slack that controls Sonos speakers.
155175
156176CRITICAL SAFETY RULES:
157- - Output ONLY a valid unified git diff (starting with "diff --git")
177+ - Output ONLY a valid unified git diff format
158178- DO NOT modify authentication files (webauthn-handler.js, auth-handler.js)
159179- DO NOT modify config handling (config/*)
160180- DO NOT modify security-critical code
@@ -177,9 +197,25 @@ ${recentCommits}
177197TASK FROM ADMIN (${ requester } ):
178198${ task }
179199
180- Generate a safe, focused code change as a unified git diff. The diff will be applied with "git apply" so ensure it's properly formatted.
200+ CRITICAL: Generate ONLY the file changes in this EXACT format (no "diff --git" headers, no index lines, no hashes):
201+
202+ --- a/path/to/file.js
203+ +++ b/path/to/file.js
204+ @@ -10,6 +10,7 @@
205+ existing line
206+ another existing line
207+ +new line to add
208+ existing line
209+
210+ Rules for the diff format:
211+ 1. Start each file with "--- a/filepath" and "+++ b/filepath"
212+ 2. NO "diff --git" line, NO "index" line with hashes
213+ 3. Include enough context lines (unchanged lines) around changes
214+ 4. Use @@ -startLine,numLines +startLine,numLines @@ for hunks
215+ 5. Prefix added lines with "+", removed lines with "-", context lines with " " (space)
216+ 6. Include at least 3 lines of context before and after changes
181217
182- Remember: Output ONLY the git diff, no explanations, no markdown code blocks, just the raw diff .`;
218+ Output ONLY the diff content , no explanations, no markdown code blocks.` ;
183219
184220// Call AI provider with unified interface
185221async function callAI ( promptText ) {
@@ -217,18 +253,19 @@ try {
217253}
218254
219255// Extract diff from potential markdown code blocks
220- let diff = output ;
256+ let diff = output . trim ( ) ;
221257if ( output . includes ( "```" ) ) {
222258 // Extract content between code fences
223259 const match = output . match ( / ` ` ` (?: d i f f ) ? \n ( [ \s \S ] * ?) ` ` ` / ) ;
224260 if ( match ) {
225- diff = match [ 1 ] ;
261+ diff = match [ 1 ] . trim ( ) ;
226262 }
227263}
228264
229- // Validate diff format
230- if ( ! diff . includes ( "diff --git" ) ) {
231- const errorMsg = `Model did not return a valid diff. Output was:\n\`\`\`\n${ output . substring ( 0 , 500 ) } \n\`\`\`` ;
265+ // Validate diff format - accept either unified diff format
266+ const hasValidDiffFormat = diff . includes ( "--- a/" ) && diff . includes ( "+++ b/" ) ;
267+ if ( ! hasValidDiffFormat ) {
268+ const errorMsg = `Model did not return a valid diff format. Expected "--- a/" and "+++ b/" lines. Output was:\n\`\`\`\n${ output . substring ( 0 , 500 ) } \n\`\`\`` ;
232269 await handleError ( new Error ( errorMsg ) , "Invalid Diff Format" ) ;
233270 // handleError calls process.exit(1), so we never reach here
234271}
@@ -264,14 +301,64 @@ if (linesChanged > 300) {
264301
265302console . log ( `[AGENT] Generated diff with ${ linesChanged } lines changed` ) ;
266303
267- // Apply patch
304+ // Apply patch with multiple strategies
268305fs . writeFileSync ( "/tmp/aicode.patch" , diff ) ;
306+
307+ let patchApplied = false ;
308+ let lastError = null ;
309+
310+ // Strategy 1: Try with standard git apply
269311try {
270- sh ( "git apply --check /tmp/aicode.patch" ) ;
271- sh ( "git apply /tmp/aicode.patch" ) ;
272- console . log ( "[AGENT] Patch applied successfully" ) ;
312+ sh ( "git apply --check /tmp/aicode.patch 2>&1" ) ;
313+ sh ( "git apply /tmp/aicode.patch 2>&1" ) ;
314+ console . log ( "[AGENT] Patch applied successfully with 'git apply'" ) ;
315+ patchApplied = true ;
273316} catch ( err ) {
274- const errorMsg = `Failed to apply patch: ${ err . message } \n\nDiff preview:\n\`\`\`\n${ diff . substring ( 0 , 500 ) } \n\`\`\`` ;
317+ console . log ( `[AGENT] Standard git apply failed: ${ err . message } ` ) ;
318+ lastError = err ;
319+ }
320+
321+ // Strategy 2: Try with --unidiff-zero for exact line matching
322+ if ( ! patchApplied ) {
323+ try {
324+ sh ( "git apply --unidiff-zero --check /tmp/aicode.patch 2>&1" ) ;
325+ sh ( "git apply --unidiff-zero /tmp/aicode.patch 2>&1" ) ;
326+ console . log ( "[AGENT] Patch applied successfully with '--unidiff-zero'" ) ;
327+ patchApplied = true ;
328+ } catch ( err ) {
329+ console . log ( `[AGENT] git apply --unidiff-zero failed: ${ err . message } ` ) ;
330+ lastError = err ;
331+ }
332+ }
333+
334+ // Strategy 3: Try with more lenient whitespace handling
335+ if ( ! patchApplied ) {
336+ try {
337+ sh ( "git apply --ignore-whitespace --check /tmp/aicode.patch 2>&1" ) ;
338+ sh ( "git apply --ignore-whitespace /tmp/aicode.patch 2>&1" ) ;
339+ console . log ( "[AGENT] Patch applied successfully with '--ignore-whitespace'" ) ;
340+ patchApplied = true ;
341+ } catch ( err ) {
342+ console . log ( `[AGENT] git apply --ignore-whitespace failed: ${ err . message } ` ) ;
343+ lastError = err ;
344+ }
345+ }
346+
347+ // Strategy 4: Try patch command as fallback
348+ if ( ! patchApplied ) {
349+ try {
350+ sh ( "patch -p1 --dry-run < /tmp/aicode.patch 2>&1" ) ;
351+ sh ( "patch -p1 < /tmp/aicode.patch 2>&1" ) ;
352+ console . log ( "[AGENT] Patch applied successfully with 'patch' command" ) ;
353+ patchApplied = true ;
354+ } catch ( err ) {
355+ console . log ( `[AGENT] patch command failed: ${ err . message } ` ) ;
356+ lastError = err ;
357+ }
358+ }
359+
360+ if ( ! patchApplied ) {
361+ const errorMsg = `Failed to apply patch after trying multiple strategies.\n\nLast error: ${ lastError . message } \n\nDiff preview:\n\`\`\`\n${ diff . substring ( 0 , 1000 ) } \n\`\`\`` ;
275362 await handleError ( new Error ( errorMsg ) , "Patch Application Failed" ) ;
276363 // handleError calls process.exit(1), so we never reach here
277364}
0 commit comments