From 1e547dea18a7c384ca0277a706198fb2fa6c1b59 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 14:18:51 +0200 Subject: [PATCH] feat(roadblock): surface reported roadblocks in the review result Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Worker/Runner/TaskRunner.cs | 13 ++++++-- .../Runner/ReviewResultTests.cs | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index ce6caab..102a54b 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -335,15 +335,16 @@ public sealed class TaskRunner // so the sequential chain (which advances on terminal states) is unaffected. // Planning parents (PlanningPhase != None) are containers, not reviewable work. var finishedAt = DateTime.UtcNow; + var reviewResult = ComposeReviewResult(result.ResultMarkdown, result.Blocks); if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None) { - await _state.SubmitForReviewAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None); + await _state.SubmitForReviewAsync(task.Id, finishedAt, reviewResult, CancellationToken.None); await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt); } else { - await _state.CompleteAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None); + await _state.CompleteAsync(task.Id, finishedAt, reviewResult, CancellationToken.None); await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt); } @@ -413,4 +414,12 @@ public sealed class TaskRunner ? $"{basePrompt}\n\nCaptured error from the failed run:\n\n{capturedError!.Trim()}" : basePrompt; } + + public static string? ComposeReviewResult(string? result, IReadOnlyList blocks) + { + if (blocks.Count == 0) return result; + var section = "⚠ Roadblocks reported during the run:\n" + + string.Join('\n', blocks.Select(b => $"- {b}")); + return string.IsNullOrWhiteSpace(result) ? section : $"{result}\n\n{section}"; + } } diff --git a/tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs new file mode 100644 index 0000000..789bd7a --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Runner/ReviewResultTests.cs @@ -0,0 +1,30 @@ +using ClaudeDo.Worker.Runner; + +namespace ClaudeDo.Worker.Tests.Runner; + +public class ReviewResultTests +{ + [Fact] + public void No_blocks_returns_result_unchanged() + { + Assert.Equal("done", TaskRunner.ComposeReviewResult("done", Array.Empty())); + } + + [Fact] + public void Blocks_are_appended_as_a_section() + { + var outp = TaskRunner.ComposeReviewResult("done", new[] { "no creds", "db down" }); + Assert.Contains("⚠ Roadblocks", outp); + Assert.Contains("- no creds", outp); + Assert.Contains("- db down", outp); + Assert.Contains("done", outp); + } + + [Fact] + public void Null_result_with_blocks_still_lists_them() + { + var outp = TaskRunner.ComposeReviewResult(null, new[] { "x" }); + Assert.Contains("⚠ Roadblocks", outp); + Assert.Contains("- x", outp); + } +}