feat(roadblock): surface reported roadblocks in the review result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 14:18:51 +02:00
parent 56ebc2803f
commit 1e547dea18
2 changed files with 41 additions and 2 deletions

View File

@@ -335,15 +335,16 @@ public sealed class TaskRunner
// so the sequential chain (which advances on terminal states) is unaffected. // so the sequential chain (which advances on terminal states) is unaffected.
// Planning parents (PlanningPhase != None) are containers, not reviewable work. // Planning parents (PlanningPhase != None) are containers, not reviewable work.
var finishedAt = DateTime.UtcNow; var finishedAt = DateTime.UtcNow;
var reviewResult = ComposeReviewResult(result.ResultMarkdown, result.Blocks);
if (task.ParentTaskId is null && task.PlanningPhase == PlanningPhase.None) 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.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt); await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt);
} }
else 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.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt); 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}\n\nCaptured error from the failed run:\n\n{capturedError!.Trim()}"
: basePrompt; : basePrompt;
} }
public static string? ComposeReviewResult(string? result, IReadOnlyList<string> 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}";
}
} }

View File

@@ -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<string>()));
}
[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);
}
}