ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
// ============================================================================
// Dumb-Agent-Proof Contract Tests
// ============================================================================

/// Regression test: analysis status contract MUST include `completed`, `partial`, `failed`.
#[test]
fn test_analysis_status_contract_always_has_completed_partial_failed() {
    use crate::prompts::analysis::generate_analysis_prompt;
    use crate::prompts::analysis::generate_fix_analysis_prompt;
    use crate::workspace::MemoryWorkspace;

    let workspace = MemoryWorkspace::new_test();

    // Test analysis prompt
    let analysis_prompt = generate_analysis_prompt("plan", "diff", false, &workspace);
    assert!(
        analysis_prompt.contains("completed"),
        "Analysis prompt must include 'completed' status"
    );
    assert!(
        analysis_prompt.contains("partial"),
        "Analysis prompt must include 'partial' status"
    );
    assert!(
        analysis_prompt.contains("failed"),
        "Analysis prompt must include 'failed' status"
    );

    // Test fix analysis prompt
    let fix_analysis_prompt =
        generate_fix_analysis_prompt("issues", "diff", "fix_result", false, &workspace);
    assert!(
        fix_analysis_prompt.contains("completed"),
        "Fix analysis prompt must include 'completed' status"
    );
    assert!(
        fix_analysis_prompt.contains("partial"),
        "Fix analysis prompt must include 'partial' status"
    );
    assert!(
        fix_analysis_prompt.contains("failed"),
        "Fix analysis prompt must include 'failed' status"
    );
}

/// Regression test: planning XSD retry MUST enforce submission-fix-only behavior.
#[test]
fn test_planning_xsd_retry_enforces_submission_fix_only() {
    use crate::prompts::developer::prompt_planning_xsd_retry_with_context;
    use crate::workspace::MemoryWorkspace;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();

    let result = prompt_planning_xsd_retry_with_context(
        &context,
        "original prompt",
        "XSD error: missing element",
        "<invalid xml",
        &workspace,
    );

    // Must say FIX XML ONLY or similar scope lock
    assert!(
        result.contains("FIX XML ONLY")
            || result.contains("fix") && (result.contains("XML") || result.contains("xml")),
        "Planning XSD retry must enforce XML fix scope. Got:\n{result}"
    );

    // Must label prior artifacts as REFERENCE ONLY
    assert!(
        result.contains("REFERENCE ONLY") || result.contains("Reference"),
        "Planning XSD retry must label prior artifacts as REFERENCE ONLY. Got:\n{result}"
    );

    // Must forbid new planning/implementation work
    assert!(
        result.to_uppercase().contains("MUST NOT")
            || result.to_uppercase().contains("DO NOT")
            || result.contains("no new planning")
            || result.contains("no new implementation")
            || result.contains("no planning")
            || result.contains("no implementation"),
        "Planning XSD retry must forbid new planning/implementation work. Got:\n{result}"
    );

    // Must mention malformed XML as primary target
    assert!(
        result.contains("malformed") || result.contains("MALFORMED") || result.contains("XML"),
        "Planning XSD retry should mention malformed XML. Got:\n{result}"
    );
}

/// Regression test: developer iteration XSD retry MUST enforce submission-fix-only behavior.
#[test]
fn test_developer_iteration_xsd_retry_enforces_submission_fix_only() {
    use crate::prompts::developer::prompt_developer_iteration_xsd_retry_with_context;
    use crate::workspace::MemoryWorkspace;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();

    let result = prompt_developer_iteration_xsd_retry_with_context(
        &context,
        "original prompt",
        "plan content",
        "XSD error: invalid element",
        "<invalid xml",
        &workspace,
        false,
    );

    // Must say FIX XML ONLY or similar scope lock
    assert!(
        result.contains("FIX XML ONLY")
            || result.contains("fix") && (result.contains("XML") || result.contains("xml")),
        "Developer XSD retry must enforce XML fix scope. Got:\n{result}"
    );

    // Must label prior artifacts as REFERENCE ONLY
    assert!(
        result.contains("REFERENCE ONLY") || result.contains("Reference"),
        "Developer XSD retry must label prior artifacts as REFERENCE ONLY. Got:\n{result}"
    );

    // Must forbid new coding/implementation work
    assert!(
        result.to_uppercase().contains("MUST NOT")
            || result.to_uppercase().contains("DO NOT")
            || result.contains("no new code")
            || result.contains("no new implementation")
            || result.contains("no coding")
            || result.contains("no implementation"),
        "Developer XSD retry must forbid new coding/implementation work. Got:\n{result}"
    );
}

/// Regression test: planning prompt MUST have explicit required sections and validation checklist.
#[test]
fn test_planning_prompt_has_validation_checklist() {
    use crate::workspace::MemoryWorkspace;

    // Use in-memory workspace
    let workspace = MemoryWorkspace::new_test();
    let partials = crate::prompts::partials::get_shared_partials();
    let template_content = include_str!("../templates/planning_xml.txt");
    let template = crate::prompts::Template::new(template_content);
    let variables = std::collections::HashMap::from([
        ("PROMPT", "test prompt".to_string()),
        (
            "PLAN_XML_PATH",
            workspace.absolute_str(".agent/tmp/plan.xml"),
        ),
        (
            "PLAN_XSD_PATH",
            workspace.absolute_str(".agent/tmp/plan.xsd"),
        ),
    ]);

    let result = template
        .render_with_partials(&variables, &partials)
        .unwrap_or_default();

    // Must have explicit required sections
    assert!(
        result.contains("ralph-summary")
            || result.contains("summary")
            || result.contains("Required"),
        "Planning prompt must mention required sections. Got:\n{result}"
    );

    // Must have some form of validation/checklist before submission
    assert!(
        result.contains("checklist")
            || result.contains("Checklist")
            || result.contains("verify")
            || result.contains("before output")
            || result.contains("Before writing")
            || result.contains("validation"),
        "Planning prompt must have validation checklist. Got:\n{result}"
    );

    // Must have minimum counts or explicit requirements
    assert!(
        result.contains("minimum")
            || result.contains("minimum")
            || result.contains("at least")
            || result.contains("required"),
        "Planning prompt must specify minimum counts or requirements. Got:\n{result}"
    );
}

/// Regression test: review prompt MUST prioritize and make issues actionable.
#[test]
fn test_review_prompt_makes_issues_actionable() {
    use crate::workspace::MemoryWorkspace;
    use std::path::PathBuf;

    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new(PathBuf::from("/tmp/test"));
    let result = prompt_review_xml_with_context(
        &context,
        "test prompt",
        "test plan",
        "test changes",
        &workspace,
    );

    // Must prioritize issues
    assert!(
        result.contains("priority")
            || result.contains("Priority")
            || result.contains("priority order")
            || result.contains("high-signal"),
        "Review prompt must prioritize issues. Got:\n{result}"
    );

    // Must make issues actionable (what is wrong, where, why, how to fix)
    assert!(
        result.contains("what") || result.contains("What"),
        "Review prompt must explain what is wrong. Got:\n{result}"
    );
    assert!(
        result.contains("where") || result.contains("Where"),
        "Review prompt must explain where the issue is. Got:\n{result}"
    );
    assert!(
        result.contains("fix") || result.contains("Fix"),
        "Review prompt must explain how to fix. Got:\n{result}"
    );
}

use super::*;
use crate::workspace::MemoryWorkspace;
use std::path::PathBuf;

#[test]
fn test_prompt_review_xml_with_context() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new(PathBuf::from("/tmp/test"));
    let result = prompt_review_xml_with_context(
        &context,
        "test prompt",
        "test plan",
        "test changes",
        &workspace,
    );
    // prompt_content is no longer embedded - reviewer reads PROMPT.md.backup directly
    assert!(!result.contains("test prompt"));
    assert!(result.contains("PROMPT.md.backup"));
    assert!(result.contains("test plan"));
    assert!(result.contains("test changes"));
    assert!(result.contains("REVIEW MODE"));
    assert!(result.contains("<ralph-issues>"));
    assert!(
        result.contains("Focus on high-signal, user-impacting issues"),
        "review_xml should prioritize high-signal, user-impacting findings"
    );
    assert!(
        result.contains("If no important issues are found, explicitly state why"),
        "review_xml should require an explicit no-issues rationale"
    );
    assert!(
        result.contains("Use parallel review agents only for independent review tracks"),
        "review_xml should provide conditional guidance for parallel review agents"
    );

    // Read-only modes: reviewer must still write exactly one XML file.
    assert!(
        result.contains("explicitly authorized") && result.contains("EXACTLY ONE file"),
        "review_xml should explicitly authorize writing exactly one XML file"
    );
    assert!(
        result.contains("MANDATORY"),
        "review_xml should mark XML file write mandatory"
    );
    assert!(
        result.contains("Not writing") && result.contains("FAILURE"),
        "review_xml should say not writing XML is a failure"
    );
    assert!(
        result.contains("does not conform") && result.contains("XSD") && result.contains("FAILURE"),
        "review_xml should say non-XSD XML is a failure"
    );
    assert!(
        result.contains("READ-ONLY")
            && (result.contains("EXCEPT FOR writing")
                || result.contains("except for writing")
                || result.contains("Except for writing"))
            && result.contains("issues.xml"),
        "review_xml should be read-only except for writing issues.xml"
    );

    assert!(
        !result.contains("DO NOT print")
            && !result.contains("Do NOT print")
            && !result.contains("ONLY acceptable output")
            && !result.contains("The ONLY acceptable output"),
        "review_xml should not include stdout suppression wording"
    );

    // Shared partials should be expanded (no raw partial directives left in output)
    assert!(
        result.contains("*** UNATTENDED MODE - NO USER INTERACTION ***"),
        "review_xml should render shared/_unattended_mode partial"
    );
    assert!(
        !result.contains("{{>"),
        "review_xml should not contain raw partial directives"
    );
}

#[test]
fn test_prompt_review_xml_with_context_allows_empty_plan_and_changes() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new(PathBuf::from("/tmp/test"));
    let result = prompt_review_xml_with_context(&context, "prompt", "", "", &workspace);

    assert!(
        !result.contains("{{PLAN}}"),
        "review prompt must not contain unresolved {{PLAN}} placeholder"
    );
    assert!(
        !result.contains("{{CHANGES}}"),
        "review prompt must not contain unresolved {{CHANGES}} placeholder"
    );
    assert!(
        result.contains("(no plan available)"),
        "review prompt should include a default when plan content is empty"
    );
    assert!(
        result.contains("(no diff available)"),
        "review prompt should include a default when changes/diff content is empty"
    );
}

#[test]
fn test_prompt_review_xml_with_context_uses_inline_plan_and_changes_when_present() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new(PathBuf::from("/tmp/test"));
    let result =
        prompt_review_xml_with_context(&context, "prompt", "plan here", "diff here", &workspace);

    assert!(result.contains("plan here"));
    assert!(result.contains("diff here"));

    assert!(
        !result.contains("(no plan available)"),
        "default plan text should not appear when plan is present"
    );
    assert!(
        !result.contains("(no diff available)"),
        "default diff text should not appear when diff is present"
    );
}

#[test]
fn test_prompt_review_xsd_retry_with_context() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new_test();
    let result = prompt_review_xsd_retry_with_context(
        &context,
        "test prompt",
        "test plan",
        "test changes",
        "XSD error",
        "last output",
        &workspace,
    );
    assert!(result.contains("XSD error"));
    assert!(result.contains(".agent/tmp/issues.xml"));
    assert!(result.contains(".agent/tmp/issues.xsd"));

    // FIX XML ONLY retry: must enforce submission-fix-only behavior
    assert!(
        result.contains("FIX XML ONLY"),
        "review_xsd_retry should say FIX XML ONLY"
    );

    // Must label prior artifacts as REFERENCE ONLY
    assert!(
        result.contains("REFERENCE ONLY"),
        "review_xsd_retry should have REFERENCE ONLY section"
    );

    // Must forbid new review/implementation work
    assert!(
        result.contains("DO NOT DO"),
        "review_xsd_retry should have DO NOT DO section"
    );

    // Must emphasize malformed XML as primary target
    assert!(
        result.contains("PRIMARY OBJECTIVE"),
        "review_xsd_retry should emphasize primary objective"
    );

    // Must have anti-actions
    assert!(
        result.contains("Do NOT review"),
        "review_xsd_retry should forbid new review work"
    );

    assert!(
        !result.contains("DO NOT print")
            && !result.contains("Do NOT print")
            && !result.contains("ONLY acceptable output")
            && !result.contains("The ONLY acceptable output"),
        "review_xsd_retry should not include stdout suppression wording"
    );

    // Shared partials should be expanded
    assert!(
        result.contains("*** UNATTENDED MODE - NO USER INTERACTION ***"),
        "review_xsd_retry should render shared/_unattended_mode partial"
    );
    assert!(
        !result.contains("{{>"),
        "review_xsd_retry should not contain raw partial directives"
    );

    // Verify files were written to workspace
    assert!(workspace.was_written(".agent/tmp/issues.xsd"));
    assert!(workspace.was_written(".agent/tmp/last_output.xml"));
}

#[test]
fn test_prompt_fix_xml_with_context() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new(PathBuf::from("/tmp/test"));
    let result = prompt_fix_xml_with_context(
        &context,
        "test prompt",
        "test plan",
        "test issues",
        &[],
        &workspace,
    );
    assert!(result.contains("test issues"));
    assert!(result.contains("FIX MODE"));
    assert!(result.contains("<ralph-fix-result>"));
    assert!(
        result.contains("Run relevant unit/integration tests"),
        "fix_mode_xml should require running relevant tests beyond listed issues"
    );
    assert!(
        result.contains("If tests or investigation reveal additional real bugs"),
        "fix_mode_xml should require fixing additional real bugs discovered incidentally"
    );
    assert!(
        result.contains("DO NOT ONLY FIX the listed issues"),
        "fix_mode_xml should explicitly forbid narrow fixing when other bugs are discovered"
    );
    assert!(
        result.contains("Ensure your final changes are validated with relevant checks"),
        "fix_mode_xml should require final validation/checklist discipline"
    );
    assert!(
        result.contains("AGENTS.md") && result.contains("CLAUDE.md"),
        "fix_mode_xml should reference project-specific agent instruction files for required checks"
    );
    assert!(
        !result.contains("ISSUES TO FIX"),
        "fix_mode_xml should avoid narrow-scope section labels"
    );
    assert!(
        !result.contains("Fix the issues listed above. For each issue:"),
        "fix_mode_xml should not frame work as only the listed issues"
    );
    assert!(
        !result.contains("you may explore LIMITEDLY"),
        "fix_mode_xml should not restrict investigation when additional concrete bugs are found"
    );
    assert!(
        result.contains("Address the listed review findings and any additional concrete defects"),
        "fix_mode_xml should explicitly broaden scope to concrete discovered defects"
    );

    // Shared partials should be expanded
    assert!(
        result.contains("*** UNATTENDED MODE - NO USER INTERACTION ***"),
        "fix_mode_xml should render shared/_unattended_mode partial"
    );
    assert!(
        !result.contains("{{>"),
        "fix_mode_xml should not contain raw partial directives"
    );
}

#[test]
fn test_prompt_fix_xsd_retry_with_context() {
    let context = TemplateContext::default();
    let workspace = MemoryWorkspace::new_test();
    let result = prompt_fix_xsd_retry_with_context(
        &context,
        "test issues",
        "XSD error",
        "last output",
        &workspace,
    );
    assert!(result.contains("XSD error"));
    assert!(result.contains(".agent/tmp/fix_result.xml"));
    assert!(result.contains(".agent/tmp/fix_result.xsd"));
    // Verify files were written to workspace
    assert!(workspace.was_written(".agent/tmp/fix_result.xsd"));
    assert!(workspace.was_written(".agent/tmp/last_output.xml"));
}

// =========================================================================
// Tests for _with_references variants
// =========================================================================

#[test]
fn test_prompt_review_xml_with_references_small_content() {
    use crate::prompts::content_builder::PromptContentBuilder;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();

    let refs = PromptContentBuilder::new(&workspace)
        .with_plan("Small plan content".to_string())
        .with_diff("Small diff content".to_string(), "abc123")
        .build();

    let result = prompt_review_xml_with_references(&context, &refs, &workspace);

    // Should embed content inline
    assert!(result.contains("Small plan content"));
    assert!(result.contains("Small diff content"));
    assert!(result.contains("REVIEW MODE"));
}

#[test]
fn test_prompt_review_xml_with_references_large_plan() {
    use crate::prompts::content_builder::PromptContentBuilder;
    use crate::prompts::content_reference::MAX_INLINE_CONTENT_SIZE;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();
    let large_plan = "p".repeat(MAX_INLINE_CONTENT_SIZE + 1);

    let refs = PromptContentBuilder::new(&workspace)
        .with_plan(large_plan)
        .with_diff("Small diff".to_string(), "abc123")
        .build();

    let result = prompt_review_xml_with_references(&context, &refs, &workspace);

    // Should reference PLAN.md file, not embed content
    assert!(result.contains(".agent/PLAN.md"));
    assert!(result.contains("plan.xml"));
    assert!(result.contains("Small diff"));
}

#[test]
fn test_prompt_review_xml_with_references_large_diff() {
    use crate::prompts::content_builder::PromptContentBuilder;
    use crate::prompts::content_reference::MAX_INLINE_CONTENT_SIZE;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();
    let large_diff = "d".repeat(MAX_INLINE_CONTENT_SIZE + 1);

    let refs = PromptContentBuilder::new(&workspace)
        .with_plan("Small plan".to_string())
        .with_diff(large_diff, "abc123def")
        .build();

    let result = prompt_review_xml_with_references(&context, &refs, &workspace);

    // Should instruct to use git diff fallback commands, not embed content
    assert!(result.contains("git diff abc123def"));
    assert!(result.contains("git diff --cached abc123def"));
    assert!(result.contains("Small plan"));
}

#[test]
fn test_prompt_review_xml_with_references_both_large() {
    use crate::prompts::content_builder::PromptContentBuilder;
    use crate::prompts::content_reference::MAX_INLINE_CONTENT_SIZE;

    let workspace = MemoryWorkspace::new_test();
    let context = TemplateContext::default();
    let large_plan = "p".repeat(MAX_INLINE_CONTENT_SIZE + 1);
    let large_diff = "d".repeat(MAX_INLINE_CONTENT_SIZE + 1);

    let refs = PromptContentBuilder::new(&workspace)
        .with_plan(large_plan)
        .with_diff(large_diff, "start123")
        .build();

    let result = prompt_review_xml_with_references(&context, &refs, &workspace);

    // Both should be referenced by file/git command
    assert!(result.contains(".agent/PLAN.md"));
    assert!(result.contains("git diff start123"));
    assert!(result.contains("git diff --cached start123"));
    // Should not contain the large content
    let pppp = "p".repeat(100);
    assert!(!result.contains(&pppp));
}