feat(report): write weekly report as short bullets with haiku

This commit is contained in:
mika kuns
2026-09-30 10:05:24 +02:00
parent f2ff865dca
commit 8a31a67c74
3 changed files with 17 additions and 20 deletions
+12 -11
View File
@@ -697,20 +697,21 @@ public static class PromptFiles
"""; """;
private const string WeeklyReportDefault = """ private const string WeeklyReportDefault = """
You are generating a concise weekly standup report for a software developer, You are writing memory-jogger notes for a developer's weekly standup,
covering {start} to {end}. covering {start} to {end}. They read them aloud in their own words, so write
keywords, not sentences.
Rules: Rules:
- Write the ENTIRE report in German. - Write in German.
- Group by day. One "## <Wochentag>, <dd.MM.yyyy>" section per day that has - Group by day. One "## <Wochentag>, <dd.MM.>" heading per day that has
activity (German weekday names). Omit days with no activity. activity (German weekday names). Omit days with no activity.
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt", - Per day: 1–3 bullets, each at most ~6 words, noun style
"- Y behoben"). Merge related small work into one bullet. ("- LagerApp: Etikettendruck gefixt", "- ClaudeDo Listen-Ordner"). No "Habe …".
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise. - One bullet per topic — merge everything about the same feature/bug.
- Blend the developer's own notes and the derived activity into ONE deduplicated - Drop trivia: typo fixes, exploration, false starts, tooling/log noise.
bullet list per day. The notes are authoritative — never omit or contradict them. - Merge the developer's notes with the derived activity, deduplicated.
- Name the project/repo when it adds clarity. The notes are authoritative — never omit or contradict them.
- Output ONLY the dated sections. No preamble, no intro, no closing remarks. - Output ONLY the day sections. No preamble, no summary, no closing remarks.
Two sections follow below: an activity log derived from Claude session history, Two sections follow below: an activity log derived from Claude session history,
and the developer's own notes. Base the report on both; the notes are and the developer's own notes. Base the report on both; the notes are
@@ -40,14 +40,12 @@ public sealed class WeekReportService
public async Task<string> GenerateAsync(DateOnly start, DateOnly end, CancellationToken ct = default) public async Task<string> GenerateAsync(DateOnly start, DateOnly end, CancellationToken ct = default)
{ {
string[] excludes; string[] excludes;
string model;
IReadOnlyList<Data.Models.DailyNoteEntity> noteRows; IReadOnlyList<Data.Models.DailyNoteEntity> noteRows;
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct)) await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
{ {
var settings = await new AppSettingsRepository(ctx).GetAsync(ct); var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
excludes = ParseExcludes(settings.ReportExcludedPaths); excludes = ParseExcludes(settings.ReportExcludedPaths);
model = string.IsNullOrWhiteSpace(settings.DefaultModel) ? "sonnet" : settings.DefaultModel;
noteRows = await new DailyNoteRepository(ctx).ListBetweenAsync(start, end, ct); noteRows = await new DailyNoteRepository(ctx).ListBetweenAsync(start, end, ct);
} }
@@ -65,15 +63,13 @@ public sealed class WeekReportService
else else
{ {
var prompt = WeekReportPromptBuilder.Build(start, end, activity, notesByDay); var prompt = WeekReportPromptBuilder.Build(start, end, activity, notesByDay);
// Guard against argument injection via the model setting: model aliases/ids are // Summarising notes into bullets doesn't need the user's default model.
// alphanumerics, dashes and dots only. const string model = "haiku";
var safeModel = new string(model.Where(c => char.IsLetterOrDigit(c) || c is '-' or '.').ToArray());
if (safeModel.Length == 0) safeModel = "sonnet";
IReadOnlyList<string> args = IReadOnlyList<string> args =
[ [
"-p", "--output-format", "stream-json", "--verbose", "-p", "--output-format", "stream-json", "--verbose",
"--permission-mode", PermissionModeResolver.Resolve(safeModel, "auto"), "--permission-mode", PermissionModeResolver.Resolve(model, "auto"),
"--model", safeModel, "--model", model,
]; ];
var result = await _claude.RunAsync(args, prompt, Path.GetTempPath(), _ => Task.CompletedTask, ct); var result = await _claude.RunAsync(args, prompt, Path.GetTempPath(), _ => Task.CompletedTask, ct);
if (!result.IsSuccess) if (!result.IsSuccess)
@@ -25,7 +25,7 @@ public class WeekReportPromptBuilderTests
new DateOnly(2026, 5, 28), new DateOnly(2026, 6, 3), new DateOnly(2026, 5, 28), new DateOnly(2026, 6, 3),
new[] { repoA }, notes); new[] { repoA }, notes);
Assert.Contains("Write the ENTIRE report in German", prompt); Assert.Contains("Write in German", prompt);
var idxJun1 = prompt.IndexOf("2026-06-01", StringComparison.Ordinal); var idxJun1 = prompt.IndexOf("2026-06-01", StringComparison.Ordinal);
var idxJun2 = prompt.IndexOf("2026-06-02", StringComparison.Ordinal); var idxJun2 = prompt.IndexOf("2026-06-02", StringComparison.Ordinal);
Assert.True(idxJun1 >= 0 && idxJun2 > idxJun1); Assert.True(idxJun1 >= 0 && idxJun2 > idxJun1);