Skip to content

Commit 4cc1bc1

Browse files
author
Ricardo Wagemaker
committed
minor changes to the doPlanning process
1 parent e6c4e39 commit 4cc1bc1

1 file changed

Lines changed: 182 additions & 31 deletions

File tree

internal/agentic/autonomous.go

Lines changed: 182 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,17 @@ The user wants to create a new application from scratch with the following descr
174174
"%s"
175175
176176
Please provide an implementation plan. Include:
177-
1. A project name. If the user specified a name, use that exact name. Otherwise suggest a short, lowercase, hyphenated name.
177+
1. A project name. If the user specified a name, use that EXACT name as-is. Otherwise suggest a short, lowercase, hyphenated name.
178178
2. A high-level architecture overview.
179-
3. The specific files and folder structure that will be created.
179+
3. The COMPLETE files and folder structure that will be created. List EVERY file with its full relative path from the project root (e.g. "backend/main.go", "frontend/index.html", "frontend/styles.css"). If the application has multiple components (frontend, backend, API, etc.), organize them into separate folders.
180180
4. The commands needed to initialize dependencies (e.g. go mod init, pip install).
181181
5. The command to run the application to test it.
182-
IMPORTANT: Use the programming language the user requested. If no language is specified, choose the most appropriate one.`, c.Description)
182+
183+
IMPORTANT RULES:
184+
- Use the programming language the user requested. If no language is specified, choose the most appropriate one.
185+
- If the user asks for a web application, you MUST include both frontend AND backend folders with complete implementations.
186+
- List every single file that will be created — do not summarize with "..." or "etc".
187+
- The project name MUST appear as "Project Name: <name>" on its own line.`, c.Description)
183188

184189
plan, err := aicall(c.AIClient, c.Model, prompt)
185190
if err != nil {
@@ -395,23 +400,34 @@ Assume we are already inside the project directory.`, c.Plan, pythonExample)
395400
}
396401

397402
func (c *AutonomousCreator) doFileCreation() (string, error) {
398-
prompt := fmt.Sprintf(`Given the implementation plan:
403+
prompt := fmt.Sprintf(`You are an expert autonomous software engineer.
404+
Given the implementation plan below, generate ALL the necessary code files for this project.
405+
406+
IMPLEMENTATION PLAN:
399407
%s
400408
401-
Generate all the necessary code files for this project.
409+
CRITICAL RULES:
410+
1. You MUST create EVERY file and folder described in the plan above. Do not skip any.
411+
2. If the plan specifies a frontend folder, you MUST generate frontend files inside that folder.
412+
3. If the plan specifies a backend folder, you MUST generate backend files inside that folder.
413+
4. All file paths must be RELATIVE to the project root (e.g. "backend/main.go", "frontend/index.html").
414+
5. Do NOT prefix paths with the project name — files are placed inside the project directory automatically.
415+
6. Generate complete, working code — not stubs or placeholders.
416+
7. Follow the EXACT folder structure from the plan.
417+
402418
Return the files inside standard Markdown code blocks with the relative filepath specified immediately before the code block.
403419
404-
Example:
405-
**main.go**
420+
Example format:
421+
**backend/main.go**
406422
`+"```go"+`
407423
package main
408-
// ...
424+
// full implementation ...
409425
`+"```"+`
410426
411-
**utils/helper.go**
412-
`+"```go"+`
413-
package utils
414-
// ...
427+
**frontend/index.html**
428+
`+"```html"+`
429+
<!DOCTYPE html>
430+
<!-- full implementation ... -->
415431
`+"```"+`
416432
417433
Only return the file paths and code blocks. No other text.`, c.Plan)
@@ -421,7 +437,45 @@ Only return the file paths and code blocks. No other text.`, c.Plan)
421437
return "", err
422438
}
423439

424-
// Simple parser for "**path/to/file.ext**\n```lang\ncontent\n```"
440+
c.FilesToMake = c.parseFileBlocks(response)
441+
442+
// Write files to disk
443+
createdFiles := c.writeFilesToDisk()
444+
445+
// Validate structure against the plan and ask AI to fill gaps
446+
missingResult, err := c.validateAndFillStructure(createdFiles)
447+
if err != nil {
448+
// Non-fatal: log but continue
449+
if c.logger != nil {
450+
c.logger.Log("Structure validation warning: %v", err)
451+
}
452+
}
453+
if missingResult != "" {
454+
createdFiles = c.getFileList()
455+
}
456+
457+
// Post-creation dependency resolution for Go projects
458+
projectType := c.detectProjectType()
459+
if projectType == "Go" {
460+
if err := c.runGoModTidy(); err != nil {
461+
return "", fmt.Errorf("failed to run go mod tidy: %v", err)
462+
}
463+
}
464+
465+
var resultMsg strings.Builder
466+
resultMsg.WriteString(fmt.Sprintf("ai-assist %s\nGenerated and saved %d files:\n- %s\n", getCurrentTime(), len(createdFiles), strings.Join(createdFiles, "\n- ")))
467+
if missingResult != "" {
468+
resultMsg.WriteString(missingResult)
469+
}
470+
resultMsg.WriteString("\nMoving to install dependencies...")
471+
472+
c.State = StateDependencies
473+
return resultMsg.String(), nil
474+
}
475+
476+
// parseFileBlocks parses the AI response for "**path/to/file.ext**\n```lang\ncontent\n```" blocks.
477+
func (c *AutonomousCreator) parseFileBlocks(response string) map[string]string {
478+
files := make(map[string]string)
425479
lines := strings.Split(response, "\n")
426480
var currentFile string
427481
var currentContent strings.Builder
@@ -432,15 +486,20 @@ Only return the file paths and code blocks. No other text.`, c.Plan)
432486

433487
// Check for file name
434488
if !inBlock && strings.HasPrefix(trimmed, "**") && strings.HasSuffix(trimmed, "**") {
435-
currentFile = strings.Trim(trimmed, "*")
489+
currentFile = strings.Trim(trimmed, "* ")
490+
// Strip leading project name prefix if the AI accidentally included it
491+
// e.g. "ricardo/backend/main.go" -> "backend/main.go"
492+
if c.ProjectName != "" && strings.HasPrefix(currentFile, c.ProjectName+"/") {
493+
currentFile = strings.TrimPrefix(currentFile, c.ProjectName+"/")
494+
}
436495
continue
437496
}
438497

439498
if strings.HasPrefix(trimmed, "```") {
440499
if inBlock {
441500
// End of block
442501
if currentFile != "" {
443-
c.FilesToMake[currentFile] = currentContent.String()
502+
files[currentFile] = currentContent.String()
444503
}
445504
currentFile = ""
446505
currentContent.Reset()
@@ -456,37 +515,106 @@ Only return the file paths and code blocks. No other text.`, c.Plan)
456515
currentContent.WriteString(line + "\n")
457516
}
458517
}
518+
return files
519+
}
459520

460-
// Write files to disk
521+
// writeFilesToDisk writes all files in FilesToMake to the project directory.
522+
func (c *AutonomousCreator) writeFilesToDisk() []string {
461523
createdFiles := []string{}
462524
for relPath, content := range c.FilesToMake {
463525
// Port 5000 is blocked on Windows (firewall) and macOS Monterey+ (AirPlay).
464-
// Rewrite it to 8080 in all generated files.
465526
content = strings.ReplaceAll(content, "port=5000", "port=8080")
466527
content = strings.ReplaceAll(content, "port = 5000", "port = 8080")
467528
content = strings.ReplaceAll(content, ":5000", ":8080")
468529
c.FilesToMake[relPath] = content
469530

470531
absPath := filepath.Join(c.ProjectDir, relPath)
471-
// Ensure parent dirs exist
472532
os.MkdirAll(filepath.Dir(absPath), 0755)
473533

474534
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
475-
return "", fmt.Errorf("failed to write %s: %v", relPath, err)
535+
if c.logger != nil {
536+
c.logger.Log("Failed to write %s: %v", relPath, err)
537+
}
538+
continue
476539
}
477540
createdFiles = append(createdFiles, relPath)
478541
}
542+
return createdFiles
543+
}
479544

480-
// Post-creation dependency resolution for Go projects
481-
projectType := c.detectProjectType()
482-
if projectType == "Go" {
483-
if err := c.runGoModTidy(); err != nil {
484-
return "", fmt.Errorf("failed to run go mod tidy: %v", err)
545+
// getFileList returns a sorted list of all file paths in FilesToMake.
546+
func (c *AutonomousCreator) getFileList() []string {
547+
list := make([]string, 0, len(c.FilesToMake))
548+
for f := range c.FilesToMake {
549+
list = append(list, f)
550+
}
551+
return list
552+
}
553+
554+
// validateAndFillStructure asks the AI to compare the generated files against the plan
555+
// and generates any missing files. Returns a status message and error.
556+
func (c *AutonomousCreator) validateAndFillStructure(createdFiles []string) (string, error) {
557+
fileList := strings.Join(createdFiles, "\n")
558+
559+
prompt := fmt.Sprintf(`You are an expert software engineer validating a project structure.
560+
561+
IMPLEMENTATION PLAN:
562+
%s
563+
564+
FILES ACTUALLY CREATED:
565+
%s
566+
567+
Compare the plan against the files that were created. Identify ANY files or folders that the plan describes but are MISSING from the created list.
568+
569+
If ALL files from the plan are present, respond with exactly:
570+
STRUCTURE_OK
571+
572+
If files are missing, generate the missing files. Return them in this format:
573+
574+
MISSING_FILES:
575+
**<relative-path>**
576+
`+"```<lang>"+`
577+
<complete file content>
578+
`+"```"+`
579+
580+
Only output STRUCTURE_OK or the missing files. No other text.`, c.Plan, fileList)
581+
582+
response, err := aicall(c.AIClient, c.Model, prompt)
583+
if err != nil {
584+
return "", err
585+
}
586+
587+
trimmed := strings.TrimSpace(response)
588+
if strings.HasPrefix(trimmed, "STRUCTURE_OK") {
589+
return "", nil
590+
}
591+
592+
// Parse and write missing files
593+
missingFiles := c.parseFileBlocks(response)
594+
if len(missingFiles) == 0 {
595+
return "", nil
596+
}
597+
598+
var result strings.Builder
599+
result.WriteString(fmt.Sprintf("\nai-assist %s\nStructure validation found %d missing files — generating them now:\n", getCurrentTime(), len(missingFiles)))
600+
601+
for relPath, content := range missingFiles {
602+
content = strings.ReplaceAll(content, ":5000", ":8080")
603+
c.FilesToMake[relPath] = content
604+
605+
absPath := filepath.Join(c.ProjectDir, relPath)
606+
os.MkdirAll(filepath.Dir(absPath), 0755)
607+
608+
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
609+
if c.logger != nil {
610+
c.logger.Log("Failed to write missing file %s: %v", relPath, err)
611+
}
612+
continue
485613
}
614+
result.WriteString(fmt.Sprintf("- %s\n", relPath))
486615
}
487616

488-
c.State = StateDependencies
489-
return fmt.Sprintf("ai-assist %s\nGenerated and saved %d files:\n- %s\n\nMoving to install dependencies...", getCurrentTime(), len(createdFiles), strings.Join(createdFiles, "\n- ")), nil
617+
return result.String(), nil
490618
}
491619

492620

@@ -977,6 +1105,25 @@ func (c *AutonomousCreator) buildCodeContext() string {
9771105
return sb.String()
9781106
}
9791107

1108+
// buildCodeContextFromDisk re-reads all known project files from disk so the AI
1109+
// sees the latest state (including any fixes applied by the fallback fixer).
1110+
func (c *AutonomousCreator) buildCodeContextFromDisk() string {
1111+
var sb strings.Builder
1112+
for relPath := range c.FilesToMake {
1113+
absPath := filepath.Join(c.ProjectDir, relPath)
1114+
data, err := os.ReadFile(absPath)
1115+
if err != nil {
1116+
// Fall back to in-memory content
1117+
sb.WriteString(fmt.Sprintf("--- %s ---\n%s\n\n", relPath, c.FilesToMake[relPath]))
1118+
continue
1119+
}
1120+
content := string(data)
1121+
c.FilesToMake[relPath] = content // update in-memory map
1122+
sb.WriteString(fmt.Sprintf("--- %s ---\n%s\n\n", relPath, content))
1123+
}
1124+
return sb.String()
1125+
}
1126+
9801127
// runShellCmd executes a shell command in the project directory and returns output.
9811128
func (c *AutonomousCreator) runShellCmd(cmdStr string) ([]byte, error) {
9821129
var cmd *exec.Cmd
@@ -993,26 +1140,29 @@ func (c *AutonomousCreator) runShellCmd(cmdStr string) ([]byte, error) {
9931140
// It performs up to 3 fix attempts. Returns a log of what happened or an error if
9941141
// all attempts fail.
9951142
func (c *AutonomousCreator) aiDrivenFix(failedCmd, errorOutput, context string) (string, error) {
996-
codeCtx := c.buildCodeContext()
9971143
var result strings.Builder
9981144
maxAttempts := 3
9991145

10001146
for attempt := 1; attempt <= maxAttempts; attempt++ {
1147+
// Re-read files from disk each attempt so we have the latest state
1148+
codeCtx := c.buildCodeContextFromDisk()
1149+
10011150
if c.logger != nil {
10021151
c.logger.Log("Fix attempt %d/%d for %s error", attempt, maxAttempts, context)
10031152
}
10041153
result.WriteString(fmt.Sprintf("ai-assist %s\nFix attempt %d/%d for %s error...\n", getCurrentTime(), attempt, maxAttempts, context))
10051154

10061155
// Ask AI to diagnose and fix
1007-
prompt := fmt.Sprintf(`A %s error occurred while running: %s
1156+
prompt := fmt.Sprintf(`You are an expert software engineer debugging a project.
1157+
A %s error occurred while running: %s
10081158
10091159
Error output:
10101160
%s
10111161
1012-
Here are the project files:
1162+
Here are the current project files:
10131163
%s
10141164
1015-
Analyze the error and provide fixes. Return your response in EXACTLY this format:
1165+
Analyze the error carefully and provide fixes. Return your response in EXACTLY this format:
10161166
10171167
For each file that needs changing:
10181168
FIX_FILE: <relative path>
@@ -1028,7 +1178,8 @@ UNFIXABLE: <explanation>
10281178
Rules:
10291179
- Provide the COMPLETE file content, not just the changed lines.
10301180
- Do NOT wrap content in markdown code fences.
1031-
- Base fixes on the actual error message and code.`, context, failedCmd, errorOutput, codeCtx)
1181+
- Base fixes on the actual error message and code.
1182+
- If the error is about missing files or wrong paths, create the correct files.`, context, failedCmd, errorOutput, codeCtx)
10321183

10331184
fixResponse, err := aicall(c.AIClient, c.Model, prompt)
10341185
if err != nil {

0 commit comments

Comments
 (0)