Besprechung als eigenes Mittel: Termin, Tagesordnung, eine Runde je Teilnehmer, Protokoll (#686)

Aufgabentyp meeting mit Teilnehmerliste (Frontmatter, SQLite, Taskboard-Werkzeug,
WebUI). MeetingRunner: jeder Teilnehmer einmal in Reihenfolge, Beitraege
gerahmt als Daten des Verfassers, danach schliesst die Leitung mit Ergebnis und
Aufgaben. Protokoll schreibt die Instanz in den Bereich der Einheit der Leitung.
Teilnehmer nur aus dem Adressbuch der Leitung, hoechstens 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-28 13:12:13 +02:00
co-authored by Claude Opus 5.5
parent 2df2fc476f
commit f2bf8b5b59
19 changed files with 684 additions and 25 deletions
+2 -1
View File
@@ -135,7 +135,8 @@ public sealed class AppHost : IAsyncDisposable
/// <summary>Das Taskboard fuer die Oberflaechen (#594). Zustandslos, deshalb je Zugriff neu.</summary>
public TaskAdministration TaskAdmin => new(
Board, AuditRepository, Scanner, Usage, Pricing, () => Instance.Agents,
LoggerFactory.CreateLogger("ClawdDotNet.App.Tasks"));
LoggerFactory.CreateLogger("ClawdDotNet.App.Tasks"),
organisation: () => Core.Organisation.OrgChart.From(Instance));
/// <summary>#711: The team overview. Stateless, like <see cref="TaskAdmin"/>.</summary>
public TeamOverview Team => new(Engine, Staging, AuditRepository, Usage, () => Instance.Agents,
@@ -24,7 +24,9 @@ public sealed record TaskInput(
string ParentId = "",
// #717: fill work — at the latest by (ISO or yyyy-MM-dd), and the author's estimate per run.
string? FillBy = null,
decimal? EstimateUsd = null);
decimal? EstimateUsd = null,
// #686: a meeting's participants, in the order they speak. Given: the task is a meeting.
IReadOnlyList<string>? Participants = null);
/// <summary>
/// Das Taskboard fuer die Oberflaechen (#594): anlegen, aendern, pausieren, sofort ausfuehren,
@@ -46,7 +48,8 @@ public sealed class TaskAdministration(
ModelPricingCatalog? pricing,
Func<IReadOnlyList<AgentConfig>> agents,
ILogger logger,
TimeProvider? clock = null)
TimeProvider? clock = null,
Func<Core.Organisation.OrgChart>? organisation = null)
{
private readonly TimeProvider _clock = clock ?? TimeProvider.System;
@@ -93,6 +96,8 @@ public sealed class TaskAdministration(
return AdminResult.Failure(parentProblem);
if (CheckFill(input, when, out var fillBy) is { } fillProblem)
return AdminResult.Failure(fillProblem);
if (CheckMeeting(input, assignee) is { } meetingProblem)
return AdminResult.Failure(meetingProblem);
var created = await board.CreateAsync(new TaskItem
{
@@ -103,7 +108,8 @@ public sealed class TaskAdministration(
Assignee = assignee,
When = when,
Status = TaskItemStatus.Todo,
Type = TaskItemType.Work,
Type = IsMeeting(input) ? TaskItemType.Meeting : TaskItemType.Work,
Participants = Participants(input),
ParentId = input.ParentId.Trim(),
FillBy = fillBy,
EstimateUsd = input.EstimateUsd
@@ -123,9 +129,15 @@ public sealed class TaskAdministration(
return AdminResult.Failure(parentProblem);
if (CheckFill(input, when, out var fillBy) is { } fillProblem)
return AdminResult.Failure(fillProblem);
if (CheckMeeting(input, assignee) is { } meetingProblem)
return AdminResult.Failure(meetingProblem);
var updated = await board.UpdateAsync(id, current => current with
{
// A work task stays work, a tool job a tool job; only participants make a meeting.
Type = IsMeeting(input) ? TaskItemType.Meeting
: current.Type == TaskItemType.Meeting ? TaskItemType.Work : current.Type,
Participants = Participants(input),
Title = input.Title.Trim(),
Body = input.Body.Trim(),
Acceptance = input.Acceptance.Trim(),
@@ -163,6 +175,25 @@ public sealed class TaskAdministration(
return null;
}
private static bool IsMeeting(TaskInput input) => Participants(input).Count > 0;
private static IReadOnlyList<string> Participants(TaskInput input)
=> (input.Participants ?? []).Select(p => p.Trim()).Where(p => p.Length > 0).ToList();
/// <summary>
/// #686: A meeting has an agent in the chair, and participants the chair can address —
/// the same rule as for an agent that schedules one.
/// </summary>
private string? CheckMeeting(TaskInput input, string assignee)
{
if (!IsMeeting(input))
return null;
var chair = TaskAssignee.AgentId(assignee);
if (string.IsNullOrWhiteSpace(chair))
return "A meeting needs an agent in the chair.";
return MeetingRunner.Refusal(Participants(input), chair, organisation?.Invoke());
}
/// <summary>#714: The parent must exist and must not lie below the task itself.</summary>
private async Task<string?> CheckParentAsync(string? taskId, string parentId, CancellationToken ct)
{