From 3ea356d29df4f4181f09bda17dbb919b7dee2312 Mon Sep 17 00:00:00 2001 From: Nick Tabeling Date: Fri, 10 Apr 2026 14:31:32 +0200 Subject: [PATCH] chore(DIS-151): add implementation plan to issue Co-Authored-By: Claude Sonnet 4.6 --- tasks/DIS-151.md | 175 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 19 deletions(-) diff --git a/tasks/DIS-151.md b/tasks/DIS-151.md index d1420ba..4a625ae 100644 --- a/tasks/DIS-151.md +++ b/tasks/DIS-151.md @@ -1,12 +1,12 @@ --- id: DIS-151 -status: ready +status: in-progress phase: 1.5 priority: p1 labels: [phase:1.5, type:feat, priority:p1] branch: refinement/channel-categories -assignee: null -started: null +assignee: developer-agent +started: 2026-04-10 pr: null merged: null --- @@ -32,38 +32,175 @@ oder Agents die aus der DB entfernt wurden aber noch einen Workspace haben. ### Startup-Reconciliation (`src/lifecycle/reconcile.ts`) Beim Start einmalig ausführen, nach DB-Init und Bot-Login: -1. **Alle Channels des Servers scannen** — welche sind in welcher Kategorie? +1. **Alle Channels des Servers scannen** — `guild.channels.fetch()` für vollständigen Cache 2. **DB-Einträge abgleichen** — für jeden Workspace-Eintrag in DB prüfen ob Channel noch existiert 3. **Workspace-Verzeichnisse scannen** — alle Verzeichnisse unter `workspaces_root` mit `agent.yaml` 4. **Einordnung**: - - DB-Eintrag + Workspace vorhanden + Channel existiert → **aktiv** (Kategorie + Channel-Name) - - Workspace vorhanden, aber kein DB-Eintrag / kein Channel → **inaktiv** (Kategorie verschieben oder Channel neu anlegen) - - Weder DB noch Workspace, aber Channel existiert → **archiv** (Channel in Archiv-Kategorie verschieben) - - Workspace existiert, kein Channel in aktiv/inaktiv/archiv → Workspace-Verzeichnis löschen (Cleanup) + - DB-Eintrag + Workspace vorhanden + Channel existiert → **aktiv** + - Workspace vorhanden, aber kein DB-Eintrag / kein Channel → **inaktiv** + - Weder DB noch Workspace, aber Channel existiert → **archiv** + - Workspace existiert, kein Channel in aktiv/inaktiv/archiv → Workspace-Verzeichnis löschen ### Live-Event: `channelDelete` -- Wenn ein Agent-Channel gelöscht wird: Workspace-Verzeichnis (`~/.disclaw/workspaces/`) ebenfalls löschen, DB-Eintrag entfernen +- Wenn ein Agent-Channel gelöscht wird: Workspace-Verzeichnis löschen, DB-Eintrag entfernen - Wenn Management-Channel gelöscht wird: automatisch neu anlegen (KI-002.1) - Kategorie-Channels (aktiv/inaktiv/archiv) werden bei Löschung neu angelegt -### `src/bot.ts` / `src/index.ts` -- `reconcileChannels(guild, db, config)` nach Bot-Ready aufrufen -- `channelDelete`-Event-Handler registrieren - ## Definition of Done - [ ] Kategorien werden beim Start automatisch angelegt (falls nicht vorhanden) - [ ] Channels werden korrekt eingeordnet (aktiv/inaktiv/archiv) - [ ] Workspace-Verzeichnisse ohne Channel werden beim Start gelöscht - [ ] `channelDelete`-Event: Workspace + DB-Eintrag werden entfernt - [ ] Management-Channel-Löschung: wird neu angelegt -- [ ] Unit-Test `tests/unit/reconcile.test.ts`: Mock-Guild, Mock-DB — alle Reconcile-Cases +- [ ] Unit-Test `tests/unit/reconcile.test.ts`: alle Reconcile-Cases grün - [ ] `npm run build && npm test` grün -## Dateien (erwartet) -- `src/lifecycle/reconcile.ts` -- `src/bot.ts` -- `src/index.ts` -- `tests/unit/reconcile.test.ts` +--- + +## Implementierungsplan (Senior Developer Review) + +### Analyse: Was fehlt + +| Was | Status | +|-----|--------| +| `db.deleteWorkspaceByChannelId()` | fehlt — muss in `database.ts` ergänzt werden | +| `src/lifecycle/` | komplett neu | +| `channelDelete`-Event-Handler | fehlt in `bot.ts` | +| `new-agent.ts` übergibt kein `parent` | Channel landet in keiner Kategorie | +| `managementChannelId` ist lokale Variable | muss mutierbar sein für Delete-Handler | + +### Neue Dateien + +| Datei | Zweck | +|---|---| +| `src/lifecycle/categories.ts` | `ensureCategories()` + `moveToCategory()` | +| `src/lifecycle/reconcile.ts` | Startup-Reconciliation-Algorithmus | +| `src/lifecycle/channel-delete-handler.ts` | `channelDelete`-Event-Logik | +| `tests/unit/reconcile.test.ts` | Unit-Tests | + +### Modifizierte Dateien + +| Datei | Änderung | +|---|---| +| `src/db/database.ts` | `deleteWorkspaceByChannelId(channelId): boolean` | +| `src/bot.ts` | State-Objekt, `ensureCategories` + `reconcile` aufrufen, `channelDelete`-Listener | +| `src/commands/new-agent.ts` | `parent: activeCategoryId` beim Channel-Erstellen | + +### Implementierungsreihenfolge + +``` +SCHRITT 1: src/db/database.ts + - deleteWorkspaceByChannelId(channelId: string): boolean + - Löscht zuerst conversations, dann workspace (manuelles CASCADE) + - Kein Schema-Change nötig + +SCHRITT 2: src/lifecycle/categories.ts (NEU) + - CATEGORY_NAMES-Konstante exportieren + - ResolvedCategories-Interface exportieren + - ensureCategories(guild: Guild): Promise + - moveToCategory(channelId: string, category: CategoryChannel): Promise + +SCHRITT 3: src/lifecycle/reconcile.ts (NEU) + - ReconcileResult-Interface exportieren + - reconcile(guild, db, categories, workspacesRoot, managementChannelId): Promise + - guild.channels.fetch() am Anfang für vollständigen Cache + - Management-Channel + Kategorie-Channels aus der Einordnung ausschließen + - workspace_path === "__management__" in DB-Abfragen filtern + - try/catch um jeden channel.setParent()-Aufruf + - try/catch um jedes fs.rmSync() + +SCHRITT 4: src/lifecycle/channel-delete-handler.ts (NEU) + - ChannelDeleteContext-Interface: + db, config, guild, categories, managementChannelId, + setManagementChannelId, setCategories + - handleChannelDelete(channel, ctx): Promise + - Fall 1: Agent-Channel → DB-Eintrag + Workspace-Dir löschen + - Fall 2: Management-Channel → neu erstellen, setManagementChannelId aufrufen + - Fall 3: Kategorie-Channel → ensureCategories() erneut aufrufen, setCategories aufrufen + +SCHRITT 5: src/bot.ts ANPASSEN + - State-Objekt: { managementChannelId: string|null, categories: ResolvedCategories|null } + - Nach Management-Channel-Setup im ready-Handler: + a) categories = await ensureCategories(guild) + b) await reconcile(guild, db, categories, config.workspaces_root, managementChannelId) + - Neuer Listener: client.on("channelDelete", ch => handleChannelDelete(ch, ctx)) + - categories an handleNewAgent() durchreichen + +SCHRITT 6: src/commands/new-agent.ts ANPASSEN + - Signatur: handleNewAgent(interaction, db, config, activeCategoryId?: string) + - parent: activeCategoryId bei guild.channels.create() setzen + +SCHRITT 7: tests/unit/reconcile.test.ts (NEU) + - Mock-Factories für Guild, DB, Categories + - vi.mock("node:fs") für Filesystem-Operationen + - Alle Test-Cases (siehe unten) + - npm run build && npm test grün +``` + +### Interfaces (Zusammenfassung) + +```typescript +// src/lifecycle/categories.ts +export const CATEGORY_NAMES = { + active: "🟢 Aktive Agents", + inactive: "🔴 Inaktive Agents", + archive: "🗄️ Archiv", +} as const; + +export interface ResolvedCategories { + active: CategoryChannel; + inactive: CategoryChannel; + archive: CategoryChannel; +} + +// src/lifecycle/reconcile.ts +export interface ReconcileResult { + active: string[]; // channel IDs moved to active + inactive: string[]; // channel IDs moved to inactive + archived: string[]; // channel IDs moved to archive + deletedWorkspaces: string[]; // workspace dirs deleted +} + +// src/lifecycle/channel-delete-handler.ts +export interface ChannelDeleteContext { + db: DisclawDatabase; + config: DisclawConfig; + guild: Guild; + categories: ResolvedCategories; + managementChannelId: string; + setManagementChannelId: (id: string) => void; + setCategories: (cats: ResolvedCategories) => void; +} + +// src/db/database.ts (Erweiterung) +deleteWorkspaceByChannelId(channelId: string): boolean; +``` + +### Testplan: `tests/unit/reconcile.test.ts` + +| # | Szenario | Erwartung | +|---|----------|-----------| +| 1 | DB + Workspace + Channel vorhanden | Channel → aktiv-Kategorie | +| 2 | Workspace + DB, kein Channel in Guild | Channel → inaktiv-Kategorie | +| 3 | Workspace, kein DB-Eintrag | → inaktiv | +| 4 | Channel in Guild, kein DB, kein Workspace | Channel → archiv-Kategorie | +| 5 | Workspace, kein Channel nirgends, kein DB | Workspace-Dir wird gelöscht | +| 6 | Management-Channel | wird nicht kategorisiert | +| 7 | Kategorie-Channels selbst | werden nicht als archiv eingestuft | +| 8 | Leerer workspaces_root, keine DB-Einträge | kein Fehler, leeres Result | +| 9 | channelDelete: Agent-Channel | DB-Eintrag + Workspace gelöscht | +| 10 | channelDelete: Management-Channel | Neuer Channel wird erstellt | +| 11 | channelDelete: Kategorie-Channel | Kategorie wird neu erstellt | +| 12 | ensureCategories: alle fehlen | 3 Kategorien werden erstellt | +| 13 | ensureCategories: teilweise vorhanden | Nur fehlende werden erstellt | + +### Randfälle + Risiken + +- `guild.channels.fetch()` vor Reconciliation — Cache kann nach `ready` unvollständig sein +- `workspace_path === "__management__"` in DB-Abfragen herausfiltern +- `fs.rmSync` auf Windows kann bei gesperrten Dateien fehlschlagen → immer `try/catch` +- Discord API Rate-Limits bei vielen Channels: `try/catch` pro `setParent()`, nicht alles parallel +- `channelDelete` kann während Reconciliation feuern → erst nach `await reconcile()` Listener registrieren ## Abhängigkeiten Phase 0 + Phase 1 abgeschlossen.