Der Stand lag ueber eine Bestandsaufnahme, drei Konzeptpapiere, vier Umsetzungsplaene und zwei Deploymentcenter-Dokumente verteilt - jedes mit eigener Reihenfolge, teils widersprueglich. Alle offenen Punkte daraus sind in docs/Roadmap.md zusammengefuehrt. Aufbau der neuen Roadmap - Statusvokabular: erledigt / beschlossen-offen / Entscheidung noetig / zurueckgestellt / Idee. Damit steht das Geparkte sichtbar drin, statt in einem Konzeptpapier zu verschwinden. - Herkunft-Spalte traegt das alte Kuerzel (S4, K2, B9, T4, F-A1, C1, DC1), damit die archivierten Papiere auffindbar bleiben, ohne sie zu lesen. - Abschnitt 1 Reihenfolge, 2 offene Entscheidungen, 3 die Vorhaben nach Bereich, 4 Ideenspeicher, 5 Chronik des Erledigten, 6 Modell-Einstufung, 7 Herkunftskarte. Was dabei sichtbar wurde - Neun Entscheidungen blockieren Arbeit, ohne dass sie Aufwand kosten - allen voran Matrix oder Rocket.Chat. Sie stehen jetzt gesammelt in Abschnitt 2 statt verstreut in den Diskussionsteilen der Konzepte. - B8 (keine Tiefenbegrenzung bei AgentComm) ist unveraendert offen. Das ist keine Theorie: A haelt sein Gate, waehrend es auf B wartet - ruft B nun A, warten beide bis zum Timeout. Der Testfall A13 dafuer fehlt bis heute. - Die vier Umsetzungsplaene vom 2026-08-05 sind alle unumgesetzt und waren in keiner Roadmap verzeichnet. Archiv Sechs Dokumente ziehen nach docs/archiv/: Bestandsaufnahme, Konzepte Backup/Finanz/Analyse, Linux-Portierung-Analyse, Lizenz-HardwareId-v2 (gegenstandslos - LicenseLabrador ist ersetzt), Deploymentcenter-Review und der 2.4-Integrationsplan. Sie bleiben als Begruendung lesbar, werden aber nicht mehr fortgeschrieben; das README ordnet jedes einzeln ein und warnt, dass ihre Quelltext-Verweise ins Leere gehen koennen. Bauplan bleibt Bauplan Taskboard, Audit, Staging, Memory, Agentenkommunikation, RocketChat und Deploymentcenter-Integration bleiben in docs/ - sie sind die Detailvorgabe fuer die Umsetzung, nicht Vorhabenlisten. Jedes bekommt oben eine Zeile, die seine Rolle und den Umsetzungsstand nennt und auf die Roadmap zeigt. Dasselbe fuer die vier Umsetzungsplaene: die Reihenfolge gilt in der Roadmap, nicht im Plan. Nebenbei repariert: Taskboard-Konzept und Teststrategie verwiesen auf AgentScheduler und ToolJobScheduler, die seit der Scanner-Konsolidierung geloescht sind. Alle Dokument-Verweise in docs/ sind geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
340 lines
19 KiB
Markdown
340 lines
19 KiB
Markdown
# Taskboard — Aufgaben statt Delay-Schleifen
|
|
|
|
> **Bauplan zu einem gebauten System.** Dateiformat, Wahrheitsaufteilung Datei/DB,
|
|
> Scanner-Verhalten und Invarianten. Umgesetzt; der Stand steht in der
|
|
> [Roadmap](Roadmap.md) 3.2. Die Abschnitte im Präsens beschreiben teils den Zustand
|
|
> *vor* der Umsetzung — sie begründen den Entwurf.
|
|
|
|
Setzt A1 aus der [Roadmap](Roadmap.md) um. Das Taskboard ist das Fundament, auf dem
|
|
Audit (A3), Staging (A2), Marktkalender (C1) und das Ergebnisregister (C7/C8)
|
|
aufsetzen. Es löst zugleich sechs Altpunkte auf einmal (F-A5 Queue, F-A4 Run-Historie,
|
|
B8 Rekursion, B6 `Task.Delay`-Überlauf, B7 Cron in Lokalzeit, T7 `RunAsync` vs.
|
|
`ChatAsync`).
|
|
|
|
Aufbau analog zum [Memory-Konzept](Memory-Konzept.md): erst warum die vorhandenen
|
|
Mechanismen nicht reichen, dann Dateiformat, Wahrheitsaufteilung, Scanner-Verhalten,
|
|
Invarianten, Migration.
|
|
|
|
## Das Problem
|
|
|
|
Heute gibt es zwei getrennte, je für sich unzureichende Wege, einen Agenten Arbeit
|
|
tun zu lassen:
|
|
|
|
- **Der Cron-Scheduler** (`AgentScheduler`, `ToolJobScheduler` — beide inzwischen
|
|
gelöscht, siehe unten) hing starr
|
|
am Agenten: eine Cron-Zeile je Agent, ausgeführt über ein `Task.Delay` bis zum
|
|
nächsten Termin. Ein jährlicher Termin bedeutet ein `Task.Delay` über Monate (B6).
|
|
Cron läuft in Lokalzeit ohne explizite Zone (B7). Ob mit oder ohne Kontext gelaufen
|
|
wird, entscheidet ein implizites Flag (`UseChatContext`, T7).
|
|
- **Die `coordination/*.md`-Dateien** im SharedWorkspace sind die improvisierte
|
|
Antwort der Agenten darauf, dass es kein Aufgabenmodell gibt: `task_*`-, `status_*`-
|
|
und `broadcast`-Dateien, per Konvention beschrieben, ohne Schema, ohne Claiming,
|
|
ohne Zustandsübergänge. Zwei Agenten, die dieselbe Datei „übernehmen", tun das ohne
|
|
jede Absicherung.
|
|
|
|
Delegation läuft heute über rekursives `send_message`/`spawn` — ein Agent ruft
|
|
synchron einen anderen, der wieder einen dritten (B8: Zyklengefahr, deshalb ein
|
|
`LoopGuard` als Notbremse). Es gibt keine Run-Historie am Auftrag und keine Queue.
|
|
|
|
## Warum ein neues Subsystem, nicht der vorhandene State-Store
|
|
|
|
Dieselbe Überlegung wie beim Gedächtnis: `IStateStore` ist eine Schlüssel-Wert-Tabelle
|
|
für kleine Marker. Ein Aufgabenmodell mit Status, Zuweisung, Abhängigkeiten und
|
|
Terminen darin abzulegen hieße, JSON in eine `Value`-Spalte zu schreiben — nicht
|
|
filterbar, nicht atomar claimbar, nicht auswertbar.
|
|
|
|
Der Kern ist eine **atomare Anspruchsnahme** (Claim). Genau das kann ein Dateisystem
|
|
nicht verlässlich und der Schlüssel-Wert-Store nicht ausdrücken, eine SQL-Zeile mit
|
|
einem bedingten `UPDATE` aber sehr wohl. Deshalb eine eigene Tabelle auf dem
|
|
vorhandenen [`SqliteStorage`](../src/ClawdDotNet.Core/Storage/SqliteStorage.cs) (WAL,
|
|
`busy_timeout`, prozessweite Schreib-Warteschlange) — dieselbe Grundlage, die schon
|
|
Gedächtnis, Zustand und Verbrauch teilen.
|
|
|
|
## Wahrheitsaufteilung — Datei ist Definition, DB ist Koordination
|
|
|
|
Die eine Entscheidung, an der alles hängt:
|
|
|
|
| Ebene | Wahrheit über | Wer schreibt |
|
|
|---|---|---|
|
|
| **Markdown-Datei** (Frontmatter + Rumpf) | die *Definition* der Aufgabe: Titel, Priorität, Assignee, Termin, Abnahme, Abhängigkeiten. Menschen- und agentenlesbar. | Mensch (Editor), Agent (`task_*`-Tool) |
|
|
| **SQLite-Tabelle `Tasks`** | den *Ausführungszustand*: Status, Claim, Lease, Last-Fired-Marker je Termin, Blocker-Auflösung. | ausschließlich das Taskboard selbst (Importer, Scanner, Tool) |
|
|
|
|
**Regel:** Für die Definition ist die Datei die Wahrheit. Für jede
|
|
Ausführungsentscheidung ist die DB die Wahrheit. Weichen beide ab (Absturz zwischen
|
|
DB-Claim und Datei-Schreiben), gewinnt die DB, und die Datei wird bei der
|
|
Reconciliation nachgezogen.
|
|
|
|
Warum nicht alles nur in die DB und die Datei als reine Projektion? Weil die Datei der
|
|
Bedienpunkt ist: Ein Mensch soll eine Aufgabe im Editor anlegen und ändern können, ein
|
|
Agent über sein Tool, und beides soll im SharedWorkspace sichtbar und versionierbar
|
|
bleiben. Warum nicht alles nur in Dateien? Weil das Claiming dort nicht atomar geht —
|
|
siehe oben. Die Aufteilung nimmt von beidem das Belastbare.
|
|
|
|
Der **Importer** ist die Brücke: Er liest die Frontmatter-Definition und spiegelt sie
|
|
idempotent in die DB-Zeile (`UPSERT` auf `task_id`). Er läuft beim Start (alle Dateien),
|
|
nach jeder `task_*`-Änderung (die betroffene Datei) und optional per
|
|
`FileSystemWatcher` mit Debounce (~500 ms, wie bei A4), damit von Hand editierte
|
|
Dateien zeitnah einfließen.
|
|
|
|
## Dateiformat
|
|
|
|
Aufgaben liegen als eine Datei je Aufgabe unter `SharedWorkspace/tasks/`. Der
|
|
Dateiname ist beschreibend (`recherche-nvda-earnings.md`); die stabile Identität ist
|
|
die `id` im Frontmatter, nicht der Name — so überlebt eine Aufgabe das Umbenennen.
|
|
|
|
```markdown
|
|
---
|
|
id: t-8f3a2c # stabil, beim Anlegen vergeben; Wahrheit der Identität
|
|
title: NVDA Earnings recherchieren
|
|
status: todo # backlog | todo | in_progress | in_review | done | canceled | blocked
|
|
type: work # work | approval | human_input
|
|
priority: 3 # 1 (niedrig) .. 5 (hoch)
|
|
assignee: "@crawler" # @new | @<agentId> | @human
|
|
when: # optional; fehlt = einmalige Aufgabe, sofort fällig
|
|
kind: cron # at | every | cron
|
|
value: "0 7 * * 1-5"
|
|
tz: Europe/Berlin # PFLICHT, wenn when gesetzt ist — kein Termin ohne Zone
|
|
require_approval: false # true = gilt erst nach Review (in_review) als done
|
|
acceptance: | # Abnahmekriterien, gegen die das Ergebnis geprüft wird
|
|
Aktuelle Zahlen mit Datum und Quelle, in SharedWorkspace/data/nvda.json abgelegt.
|
|
blocked_by: [t-4b1e] # diese Aufgabe startet erst, wenn alle Blocker done sind
|
|
onlyWhenMarketOpen: false # C1: Termin nur auslösen, wenn der Markt offen ist
|
|
---
|
|
|
|
Freitext-Rumpf: Auftragsbeschreibung, Kontext, Verweise. Geht als Aufgabenstellung
|
|
an den Agenten. Kommentare (Ergebnisse, Kritik, Reopen) werden unten angehängt.
|
|
```
|
|
|
|
Feldregeln:
|
|
|
|
- **`when.kind`**: `at` (einmaliger Zeitpunkt, ISO 8601), `every` (Intervall, z. B.
|
|
`30m`), `cron` (5-Felder-Ausdruck wie bisher). `tz` ist bei allen dreien Pflicht —
|
|
das ist die Antwort auf B7. Die vorhandene
|
|
[`CronExpression`](../src/ClawdDotNet.Core/Scheduling/CronExpression.cs) rechnet
|
|
heute zonen-blind in `DateTime.Now`; sie wird in einen zonen-bewussten Aufruf
|
|
gekapselt (nächsten Termin in `tz` bestimmen, dann in UTC vergleichen). Das
|
|
verzahnt sich mit R2 (`TimeProvider`) aus der [Teststrategie](Teststrategie.md).
|
|
- **`assignee`** ersetzt das `UseChatContext`-Flag durch eine explizite Angabe (T7):
|
|
- `@new` bzw. `@new:<agentId>` → frischer Lauf ohne Historie (`AgentEngine.RunAsync`).
|
|
Bloßes `@new` ist nur eindeutig, wenn die Instanz genau **einen** Agenten hat; sonst
|
|
benennt `@new:<agentId>` den Ziel-Agenten. Ist er nicht auflösbar, scheitert der
|
|
Dispatch mit klarer Meldung, statt einen falschen Agenten zu raten.
|
|
- `@<agentId>` → bestehender Agent mit seinem Kontext (`AgentEngine.ChatAsync`)
|
|
- `@human` → wartet auf einen Menschen; kein Modell-Lauf
|
|
- **`type`**: `work` (Standard), `approval` und `human_input`. Bei den letzten beiden
|
|
ist ein Mensch der Assignee; seine Antwort **ist** das Ergebnis und Input für
|
|
Folgeaufgaben. Kein Sonderpfad — nur ein Assignee, der kein Modell ist.
|
|
|
|
## Scanner-Verhalten
|
|
|
|
Ein einziger Takt (~60 s) statt vieler langer `Task.Delay`. Damit kennt das System
|
|
keine Monats-Delays mehr (B6), und ein verpasster Takt ist ein verpasster Termin, kein
|
|
Zeitbombe.
|
|
|
|
Je Takt:
|
|
|
|
1. **Fällige Termine bestimmen.** Aus jeder aktiven Task-Zeile den nächsten Termin in
|
|
ihrer `tz` berechnen und gegen „jetzt" prüfen. Ein Termin ist durch einen
|
|
**Occurrence-Key** eindeutig: `task_id` + geplante Feuerzeit (bei `at`/`cron`/`every`).
|
|
2. **Claim vor Lauf (at-most-once).** Bevor gelaufen wird, wird der Occurrence-Key
|
|
atomar beansprucht:
|
|
|
|
```sql
|
|
UPDATE Tasks
|
|
SET claim_token = @token, claimed_at = @now, last_occurrence = @occ, status = 'in_progress'
|
|
WHERE id = @id
|
|
AND status IN ('todo','backlog')
|
|
AND (last_occurrence IS NULL OR last_occurrence < @occ)
|
|
AND (claim_token IS NULL OR claimed_at < @leaseCutoff);
|
|
```
|
|
|
|
Genau eine Zeile betroffen = Anspruch gewonnen. Ein zweiter, gleichzeitiger Takt
|
|
findet die Bedingung nicht mehr erfüllt (Rowcount 0) und läuft **nicht** — so wird
|
|
ein doppelter Tick zu einem Lauf.
|
|
3. **Dispatch nach Assignee.** `@new` → `RunAsync`; `@<agent>` → `ChatAsync`; `@human`
|
|
→ kein Lauf, Status bleibt/wird `in_review` bzw. `todo`, die UI zeigt die Aufgabe
|
|
als wartend. Der Rumpf (+ Abnahmekriterien) ist die Nachricht an den Agenten.
|
|
4. **Abschluss verbuchen.** Last-Fired-Marker setzen (`last_occurrence = @occ`), Status
|
|
fortschreiben, Claim lösen. Bei `require_approval` → `in_review` statt `done`. Der
|
|
Lauf wird am Task verknüpft (Grundlage für A3-Receipts und die Run-Historie F-A4).
|
|
|
|
**Kein Retry-Sturm.** Der Marker wird auch bei einem **fehlgeschlagenen** Lauf gesetzt:
|
|
ein fehlgeschlagener Lauf bleibt der einzige Versuch für diesen Termin. Wiederholung
|
|
ist eine bewusste Entscheidung (neuer Termin oder Reopen), kein Automatismus. Das
|
|
schützt vor einem Agenten, der bei jedem Takt erneut in denselben Fehler läuft und
|
|
Budget verbrennt.
|
|
|
|
**Serialisierung mit den Chat-Läufen (B2).** Der Scanner ruft die Engine über
|
|
dieselben Einstiegspunkte wie WebView, ToolJob und AgentComm. `ChatAsync` ist bereits
|
|
je Agent über ein `SemaphoreSlim`-Gate serialisiert
|
|
([`AgentEngine`](../src/ClawdDotNet.Core/Engine/AgentEngine.cs)) — der Scanner fügt
|
|
sich dort ein, statt einen zweiten, konkurrierenden Pfad in den geteilten
|
|
Konversationskontext aufzumachen. Der Claim ist eine **DB**-Grenze (welcher Termin wird
|
|
behandelt), das Agent-Gate eine **Kontext**-Grenze (kein verschränkter Nachrichtenstrom).
|
|
Beide werden gebraucht; keine ersetzt die andere.
|
|
|
|
### Startup-Reconciliation
|
|
|
|
Beim Start wird Soll (Frontmatter) gegen Ist (DB-Marker/Claims) abgeglichen, statt
|
|
verpasste Läufe still zu überspringen:
|
|
|
|
- Alle Task-Dateien importieren (UPSERT), gelöschte Dateien in der DB als `archived`
|
|
markieren.
|
|
- **Stale Claims freigeben:** Ein Claim, dessen `claimed_at` älter ist als die
|
|
Lease-Dauer (ein während des Laufs abgestürzter Prozess), wird verworfen — die
|
|
Bedingung `claimed_at < @leaseCutoff` im Claim-`UPDATE` erledigt das ohnehin, die
|
|
Reconciliation stellt den Status zusätzlich von `in_progress` auf `todo` zurück.
|
|
- **Verpasste Termine erkennen:** Liegt der letzte planmäßige Termin nach dem
|
|
Last-Fired-Marker, ist ein Lauf ausgefallen. Er wird als solcher gemeldet (Log, später
|
|
A5), nicht heimlich verschluckt. Ob nachgeholt wird, ist Politik — Standard: einmal
|
|
nachholen, sonst würde ein über Nacht ausgeschalteter Rechner beim Start eine Welle
|
|
auslösen.
|
|
|
|
## Die drei Invarianten
|
|
|
|
Der Scanner-Kern ist in der Roadmap als der heikle Teil markiert (die Fehlerklasse, die
|
|
hier schon einmal schiefging). Er wird strikt gegen diese Invarianten gebaut und mit
|
|
Property-Tests (FsCheck, siehe Teststrategie) abgesichert:
|
|
|
|
1. **Nie zwei Claims auf einen Task-Termin.** Garantiert durch das bedingte `UPDATE`:
|
|
Rowcount ≤ 1 pro Occurrence-Key. Test: N parallele Claims auf denselben Key → genau
|
|
einer gewinnt.
|
|
2. **Kein Dispatch bei offenem Blocker.** Eine Aufgabe mit unerfüllten `blocked_by`
|
|
ist nicht `todo`, sondern `blocked`, und die Claim-Bedingung (`status IN
|
|
('todo','backlog')`) greift nicht. Test: Blocker offen → kein Lauf; letzter Blocker
|
|
`done` → genau ein Auto-Dispatch.
|
|
3. **Doppelter Tick = ein Lauf.** Zwei Takte im selben Fenster konkurrieren um denselben
|
|
Occurrence-Key; der Claim lässt nur einen durch. Test: zwei gleichzeitige
|
|
`ScanOnce` → ein Lauf, ein Marker.
|
|
|
|
Diese drei sind keine Kür, sondern die Bedingung dafür, dass der Kern mit einem
|
|
schwächeren Modell umgesetzt werden darf.
|
|
|
|
## Abhängigkeiten und Eskalation
|
|
|
|
- **`blocked_by` mit Auto-Dispatch:** Wird eine Aufgabe `done`, sucht das Board alle
|
|
Aufgaben, deren `blocked_by` sie enthält. Sind für eine davon **alle** Blocker `done`,
|
|
wechselt sie `blocked → todo` und der Scanner nimmt sie beim nächsten Takt auf.
|
|
- **Blocker-Eskalation:** Meldet ein Agent einen Blocker (`task_update status=blocked`
|
|
mit Begründung), fällt die Aufgabe und der Zuständige (Lead/Benutzer) wird
|
|
benachrichtigt. Bis A5 (Matrix) steht, geht das über den vorhandenen Log-/UI-Weg.
|
|
|
|
## Zusammenspiel mit Staging (A2) und Reopen
|
|
|
|
- **`require_approval` / `in_review`:** Eine Aufgabe mit `require_approval: true` gilt
|
|
nach dem Lauf nicht als `done`, sondern als `in_review`. Das ist der natürliche
|
|
Andockpunkt für A2: Die Freigabe erzeugt einen Folge-Task, der den Agenten mit dem
|
|
eingefrorenen Aufruf weckt (so bleibt der Lauf regulär beendet, kein pausierter
|
|
In-Memory-Zustand — genau die Architektur-Vorgabe aus der Roadmap-Einstufung für A2).
|
|
- **Reopen/Feedback:** Ergebnis + Kritik gehen per `task_comment` an **denselben**
|
|
Agenten zurück (`ChatAsync` in dessen Kontext), statt eine neue Aufgabe von vorn zu
|
|
beginnen. Die Aufgabe kehrt nach `todo`/`in_progress` zurück, der Verlauf am Task
|
|
bleibt erhalten.
|
|
|
|
## Agenten-Tool
|
|
|
|
Ein Tool `Taskboard` im Muster von `MemoryTool` (eine Aktion je Aufruf), das über einen
|
|
neuen `ITaskRepository` auf dem `AgentToolContext` arbeitet (analog `IMemoryRepository?
|
|
Memory`):
|
|
|
|
| Aktion | Zweck |
|
|
|---|---|
|
|
| `task_create` | Aufgabe anlegen — schreibt Datei **und** DB-Zeile (über den Importer). Vergibt die `id`. |
|
|
| `task_list` | Aufgaben filtern (Status, Assignee, Betreff). Gekappte, kontextschonende Ausgabe wie bei Memory. |
|
|
| `task_update` | Status/Felder ändern; `blocked` melden; Ergebnis eintragen. |
|
|
| `task_comment` | Kommentar/Kritik anhängen; mit Reopen den Zuständigen erneut wecken. |
|
|
|
|
Agent-zu-Agent-Delegation läuft künftig hierüber: Statt rekursivem `send_message` legt
|
|
ein Agent eine Aufgabe mit `assignee: @<other>` an. Das ist strukturell zyklenfrei (B8) —
|
|
eine Aufgabe ist ein Datensatz, kein synchroner Aufruf-Stack.
|
|
|
|
**Sicherheit:** Ein Task-Rumpf ist Prompt-Input für den Assignee. Fremdbestimmte Inhalte
|
|
(Ergebnisse anderer Tools, die in einen Task fließen) werden als Daten gerahmt, nicht als
|
|
Anweisung — dieselbe Linie wie K2. Irreversibles, das ein Task auslöst, läuft über A2.
|
|
|
|
## Migration
|
|
|
|
Die `coordination/*.md`-Dateien gehen im Taskboard auf. Die Altdateien haben kein Schema
|
|
(freies Markdown wie `# Task: …`, `## Status: ASSIGNED`), deshalb bewusst konservativ
|
|
(`CoordinationMigration`, beim Start ausgeführt):
|
|
|
|
- **`task_*`-Dateien** → einmalig als `backlog`-Aufgaben übernommen: Titel aus der
|
|
`# Task:`-Überschrift (sonst erste Überschrift, sonst Dateiname), das ganze Markdown
|
|
als Rumpf, `assignee: @human` als sicherer Default, bis ein Mensch sie zuordnet.
|
|
`backlog` (nicht `todo`), damit der Scanner nichts unbesehen ausführt.
|
|
- **`status_*`, `broadcast`, Incident-Berichte, `*.json`-Artefakte** → keine Aufgaben;
|
|
bleiben unangetastet (später nach A5/Matrix bzw. verfallen als Altbestand).
|
|
|
|
Idempotent durch **Verschieben statt Löschen**: eine übernommene `task_*`-Datei wandert
|
|
nach `coordination/migrated/` — die Historie bleibt, ein zweiter Start findet sie nicht
|
|
mehr. Vor der Migration greift die übliche Regel: frisches Backup.
|
|
|
|
## Ein Takt für alles — Ablösung der Alt-Scheduler
|
|
|
|
Der Scanner ist der **einzige** periodische Treiber. Die früheren `AgentScheduler` und
|
|
`ToolJobScheduler` (zwei `Task.Delay`-Schleifen mit B6/B7, ungetestet) sind **gelöscht** —
|
|
alles Periodische ist jetzt ein Task:
|
|
|
|
- **Geplanter Agent-Lauf** (früher `scheduler`-Config) → ein normaler Task mit
|
|
`when: cron` und Assignee `@new:<agent>`.
|
|
- **Tool-Job-Poll** (früher `toolJobs`-Config, z. B. `telegram_poll`) → ein Task vom Typ
|
|
**`tool_job`** mit `tool_name`/`job_type`. Beim fälligen Termin tickt der Dispatcher den
|
|
`IToolJobProvider` und weckt den Zielagenten (Assignee) nur, wenn der Tick etwas meldet —
|
|
mit oder ohne Kontext, je nach `ToolJobResult`.
|
|
|
|
Zwei Feinheiten, die dabei geradegezogen wurden:
|
|
|
|
- **Wiederkehrende Tasks** (`cron`/`every`/`tool_job`) kehren nach dem Feuern auf `todo`
|
|
zurück statt auf `done` — sonst liefe ein Cron-Task nur ein einziges Mal. Der
|
|
Last-Fired-Marker verhindert weiterhin, dass **derselbe** Termin doppelt feuert.
|
|
- **`backlog` ist ein Halte-Status**: Der Scanner claimt nur `todo`. Eine Aufgabe in
|
|
`backlog` (frisch importiert, migriert, oder ein deaktivierter Poll) ruht, bis ein
|
|
Mensch sie auf `todo` setzt.
|
|
|
|
Die Alt-Konfiguration (`scheduler`, `toolJobs`) wird beim Start einmalig und
|
|
nicht-destruktiv in Tasks migriert (`SchedulerTaskMigration`, stabile Ids
|
|
`sched-<agent>` / `tj-<agent>-<job>`). Ein manuelles „Jetzt ausführen" im Host läuft über
|
|
`TaskScanner.RunTaskNowAsync`.
|
|
|
|
## Verzahnung
|
|
|
|
- **C1 Marktkalender:** `onlyWhenMarketOpen` gehört ins Frontmatter, nicht in einen
|
|
eigenen Mechanismus — der Scanner überspringt einen Termin, wenn der Markt zu ist.
|
|
- **A3 Audit/Receipts:** Jeder Lauf wird am Task verknüpft; der Abschluss-Beleg (Schritte,
|
|
Tokens, Kosten) fällt daraus ab und macht C7 weitgehend zum Abfallprodukt.
|
|
- **F-A5/F-A4:** Das Board **ist** die Queue; die verknüpften Läufe **sind** die Historie.
|
|
|
|
## Umsetzungsreihenfolge
|
|
|
|
Bewusst so geschnitten, dass der heikle Kern zuletzt und gegen grüne Invarianten kommt:
|
|
|
|
1. **Modelle + `ITaskRepository` + Schema + Frontmatter-Parser.** Reine, testbare
|
|
Bausteine. Der Parser wird eng gebaut (die Frontmatter ist ein kleiner, flacher
|
|
Satz aus Skalaren und kurzen Listen) — keine YAML-Bibliothek, passend zum
|
|
dependency-armen Stil des Projekts. 4.6-tauglich.
|
|
2. **`Taskboard`-Tool + Importer.** Datei ↔ DB, `task_*`-Aktionen. 4.6-tauglich.
|
|
3. **Scanner-Kern** (Claim, Dispatch, Reconciliation, Auto-Dispatch) — strikt gegen die
|
|
drei Invarianten, mit Property-Tests. Der in der Roadmap für Opus 5/Fable markierte
|
|
Teil; mit Opus 4.8 nur streng nach diesem Dokument und mit den Invarianten-Tests als
|
|
Netz.
|
|
4. **Migration** der `coordination/*.md`.
|
|
|
|
Neue Subsysteme kommen mit Tests nach der [Teststrategie](Teststrategie.md); die
|
|
Invarianten-Tests sind bei Punkt 3 die Absicherung, kein Nice-to-have.
|
|
|
|
## Offen
|
|
|
|
- **Nachhol-Politik verpasster Termine** — umgesetzt als „höchstens einmal nachholen":
|
|
der Scanner nimmt den jüngsten verpassten Termin, nicht jeden einzelnen. Eine frische
|
|
Aufgabe holt zudem keinen Termin von **vor** ihrer Anlage nach. Ob das je Task
|
|
abschaltbar sein soll (`catchUp: true|false`), ist offen.
|
|
- **Marktkalender (C1)** — der Scanner fragt eine `IMarketCalendar` (derzeit Platzhalter
|
|
„immer offen"). C1 liefert später den echten Kalender; die Verzahnung steht.
|
|
- **DST-Randfall** — eine bei der Zeitumstellung nicht existierende Ortszeit
|
|
(Frühjahr, „02:30") wird derzeit übersprungen statt verschoben. Für die geplanten
|
|
Termine unkritisch; die Härtung gehört zur Scheduler-Nacharbeit (Teststrategie R4/R5).
|
|
- **Priorität als Reihenfolge** — bei mehreren fälligen Aufgaben desselben Agenten
|
|
bestimmt `priority` die Reihenfolge; ob strikt oder gewichtet, ist noch offen.
|
|
- **Aufräumen** — `done`/`canceled`-Aufgaben nach einer Frist archivieren, damit
|
|
`tasks/` nicht zuwächst (dieselbe Überlegung wie „Verfall" beim Gedächtnis).
|