Skip to content

Commit d70840f

Browse files
committed
fix: make plan extraction and journal persistence resilient to agent format variations
- Plan fallback: try multiple header patterns (## Session Plan, # Session Plan, ## Plan) - Plan fallback: accept output containing Task structure even without exact header - Journal: write fallback entry when agent doesn't output '## Day' prefix - Journal: handle both '## Day' and '# Day' markers - Journal: normalize any heading level to '## Day N' format
1 parent c9194a0 commit d70840f

3 files changed

Lines changed: 48 additions & 9 deletions

File tree

internal/evolution/git_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,13 @@ func TestPersistJournalEntry_NoDayMarker(t *testing.T) {
367367
e.persistJournalEntry(entry, "5")
368368

369369
data, _ := os.ReadFile(filepath.Join(dir, "docs/JOURNAL.md"))
370-
// should not modify the file
371-
if string(data) != "# iterate Evolution Journal\n" {
372-
t.Errorf("journal should be unchanged, got:\n%s", string(data))
370+
content := string(data)
371+
// should write a fallback entry wrapping the agent output
372+
if !strings.Contains(content, "## Day 5") {
373+
t.Errorf("should contain Day 5 header, got:\n%s", content)
374+
}
375+
if !strings.Contains(content, "This has no day marker at all") {
376+
t.Errorf("should contain agent output, got:\n%s", content)
373377
}
374378
}
375379

internal/evolution/phases.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,19 @@ func (e *Engine) RunPlanPhase(ctx context.Context, p iteragent.Provider, issues
4242
// If the agent didn't write SESSION_PLAN.md via tool call, extract it from text output.
4343
planPath := filepath.Join(e.repoPath, "SESSION_PLAN.md")
4444
if _, err := os.Stat(planPath); os.IsNotExist(err) && lastContent != "" {
45-
if idx := strings.Index(lastContent, "## Session Plan"); idx >= 0 {
46-
extracted := strings.TrimSpace(lastContent[idx:])
45+
// Try multiple extraction patterns — agents format things differently.
46+
extracted := ""
47+
for _, prefix := range []string{"## Session Plan", "## Session plan", "# Session Plan", "## Plan"} {
48+
if idx := strings.Index(lastContent, prefix); idx >= 0 {
49+
extracted = strings.TrimSpace(lastContent[idx:])
50+
break
51+
}
52+
}
53+
// Last resort: if output looks like it contains task structure, use the whole thing.
54+
if extracted == "" && (strings.Contains(lastContent, "Task 1") || strings.Contains(lastContent, "### Task")) {
55+
extracted = strings.TrimSpace(lastContent)
56+
}
57+
if extracted != "" {
4758
if writeErr := os.WriteFile(planPath, []byte(extracted), 0o644); writeErr == nil {
4859
e.logger.Info("extracted SESSION_PLAN.md from agent text output")
4960
}

internal/evolution/phases_communicate.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,26 @@ Rules:
212212

213213
// persistJournalEntry extracts and writes a valid journal entry to JOURNAL.md.
214214
func (e *Engine) persistJournalEntry(journalEntry string, day string) {
215-
if idx := strings.Index(journalEntry, "## Day"); idx >= 0 {
215+
if journalEntry == "" {
216+
e.logger.Warn("journal entry is empty — skipping journal write")
217+
return
218+
}
219+
220+
// Try to find the journal header. Agents format things differently.
221+
idx := strings.Index(journalEntry, "## Day")
222+
if idx < 0 {
223+
idx = strings.Index(journalEntry, "# Day")
224+
}
225+
226+
if idx >= 0 {
216227
extracted := journalEntry[idx:]
217228
if nextIdx := strings.Index(extracted[1:], "\n## "); nextIdx >= 0 {
218229
extracted = extracted[:nextIdx+1]
219230
}
220231
extracted = strings.TrimSpace(extracted)
221232
dayNum, _ := strconv.Atoi(day)
222233
if dayNum > 0 {
223-
dayPattern := regexp.MustCompile(`^## Day \d+`)
234+
dayPattern := regexp.MustCompile(`^(#+)\s*Day\s*\d+`)
224235
extracted = dayPattern.ReplaceAllString(extracted, fmt.Sprintf("## Day %d", dayNum))
225236
}
226237
journalPath := filepath.Join(e.repoPath, "docs/JOURNAL.md")
@@ -231,9 +242,22 @@ func (e *Engine) persistJournalEntry(journalEntry string, day string) {
231242
}
232243
header := "# iterate Evolution Journal\n"
233244
newContent := header + "\n" + extracted + "\n\n" + strings.TrimPrefix(strings.TrimPrefix(string(journal), header), "\n")
234-
_ = os.WriteFile(journalPath, []byte(newContent), 0o644) // best-effort; journal is append-mostly
245+
_ = os.WriteFile(journalPath, []byte(newContent), 0o644)
235246
} else {
236-
e.logger.Warn("agent output does not contain '## Day' — skipping journal write")
247+
// Fallback: wrap the agent's output in a proper journal entry.
248+
e.logger.Warn("agent output does not contain '## Day' — writing fallback journal entry")
249+
now := time.Now().UTC().Format("15:04")
250+
dayNum, _ := strconv.Atoi(day)
251+
fallback := fmt.Sprintf("## Day %d — %s — Evolution session\n\n%s\n", dayNum, now, strings.TrimSpace(journalEntry))
252+
journalPath := filepath.Join(e.repoPath, "docs/JOURNAL.md")
253+
_ = os.MkdirAll(filepath.Dir(journalPath), 0o755)
254+
journal, err := os.ReadFile(journalPath)
255+
if err != nil {
256+
e.logger.Warn("failed to read JOURNAL.md for journal update", "err", err)
257+
}
258+
header := "# iterate Evolution Journal\n"
259+
newContent := header + "\n" + fallback + "\n" + strings.TrimPrefix(strings.TrimPrefix(string(journal), header), "\n")
260+
_ = os.WriteFile(journalPath, []byte(newContent), 0o644)
237261
}
238262
}
239263

0 commit comments

Comments
 (0)