disclaw/docs/backlog.md

1040 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DisClaw — Issue-Backlog
Autor: Projekt- und Git-Manager-Agent
Datum: 2026-04-08
Quelle: `docs/development-plan.md` §4, präzisiert durch `docs/ai-engineer-analyse.md` und
`docs/cli-feature-answers.md`.
Dieses Dokument ist die **Single Source of Truth für den Backlog**, bevor die Issues
nach Forgejo synchronisiert werden. Jede Issue-Karte hat eine lokale ID (`DIS-XXX`),
die das Bootstrap-Script auf die echte Forgejo-Issue-Nummer mappt.
**Konvention**:
- Phase 0 und Phase 1 sind **granular** geschnitten (0,52 Dev-Tage pro Issue, einzeln testbar).
- Phase 25 sind auf **Epic-Level** — werden am Phasen-Anfang verfeinert.
- Phase 6+ ist reiner Platzhalter.
---
## Phase 0 — Härtung (8 Issues)
### DIS-001: Workspace-Root nach `~/.disclaw/workspaces/` verschieben
**Phase**: 0
**Labels**: `phase:0`, `type:feat`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/workspace-root-home`
**Issue-Body**:
```
## Ziel
Workspaces werden standardmäßig unter `~/.disclaw/workspaces/<agent>/` (Linux/Mac) bzw.
`%USERPROFILE%\.disclaw\workspaces\<agent>\` (Windows) angelegt. Der Pfad ist über
`disclaw.yaml` → `workspaces_root` konfigurierbar; der Default zeigt aber immer außerhalb
des DisClaw-Repo-Roots.
## Kontext
Referenz: `docs/development-plan.md` §2.9 und Phase 0, Task 3.
Grund: Claude Code walkt beim Start Parent-Directories hoch und konkateniert alle
gefundenen `CLAUDE.md`-Dateien in den System-Prompt. Liegt ein Workspace unterhalb von
`C:\Code\side\disclaw\`, erbt jeder Agent die Projekt-`CLAUDE.md` von DisClaw selbst.
Das kontaminiert die Agenten-Identität und ist nicht reproduzierbar.
## Scope
- **In**:
- `disclaw.yaml` Default-Wert auf `~/.disclaw/workspaces` ändern.
- `src/config/loader.ts` um Tilde-Expansion erweitern (`~` → `os.homedir()`), absolute
Pfade per `path.resolve` normalisieren.
- `src/commands/new-agent.ts` legt Workspaces unter dem resolvierten Root an.
- Warning-Log beim Bot-Start, wenn `workspaces_root` innerhalb des DisClaw-Repo-Roots
liegt (erkannt via `path.relative(repoRoot, wsRoot)` startet nicht mit `..`).
- Manueller Migrationshinweis für bestehende `./workspaces/<agent>/`-Ordner im README.
- **Out**:
- Automatische Migration bestehender Workspaces (kommt in Phase 6+).
- `claudeMdExcludes`-Rendering (gehört zu DIS-005, wenn Dev-Mode aktiv).
- `--bare` + `--append-system-prompt-file` (gehört zu DIS-004).
## Definition of Done
- [ ] `disclaw.yaml` hat Default `workspaces_root: "~/.disclaw/workspaces"`.
- [ ] `src/config/loader.ts` expandiert `~` zuverlässig auf Linux, Mac, Windows.
- [ ] `new-agent` legt Workspace unter dem neuen Root an, DB speichert absoluten Pfad.
- [ ] Warning-Log feuert, wenn Root innerhalb des Repos liegt.
- [ ] Unit-Test `tests/unit/workspace-root-resolve.test.ts` deckt Tilde-Expansion,
absolute-path-Normalisierung und Repo-internen Warn-Case ab.
- [ ] README-Abschnitt „Migration bestehender Workspaces" hinzugefügt.
- [ ] `npm run build`, `npm test` grün.
## Dateien (erwartet)
- `disclaw.yaml`
- `src/config/loader.ts`
- `src/commands/new-agent.ts`
- `tests/unit/workspace-root-resolve.test.ts`
- `README.md`
## Branch
`phase-0/workspace-root-home`
## Abhängigkeiten
- Keine.
```
---
### DIS-002: `shell: false` + `cross-spawn` Windows-Fix im Runner
**Phase**: 0
**Labels**: `phase:0`, `type:fix`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/harden-spawn`
**Issue-Body**:
```
## Ziel
Command-Injection über Discord-User-Input eliminieren. `shell: true` aus
`src/agent/runner.ts` entfernen und durch `cross-spawn` ersetzen. Windows-`.cmd`-Wrapper
(`claude.cmd`) wird korrekt aufgelöst, CVE-2024-27980 wird umgangen.
## Kontext
Referenz: `docs/development-plan.md` §2.2, Phase 0 Task 1.
`src/agent/runner.ts:187` nutzt aktuell `shell: true`. Jeder Discord-User-Input landet
damit direkt in der Shell-Interpretation → Command-Injection-Vektor.
## Scope
- **In**:
- `cross-spawn` und `@types/cross-spawn` als Dependencies hinzufügen.
- `src/runtime/resolve-claude.ts` (neu): `resolveClaude()`-Funktion, liest `CLAUDE_PATH`
aus `.env`, fällt bei leer auf `where claude` / `which claude` zurück, cached Ergebnis
in-memory, wirft verständliche Fehlermeldung wenn nicht auffindbar.
- `src/agent/runner.ts`: `spawn` → `cross-spawn`-Import, `shell: false`,
`windowsHide: true`, `stdio: ["pipe", "pipe", "pipe"]`, `child.stdout.setEncoding("utf8")`.
- **Out**:
- `sanitizedEnv()` (eigenes Issue DIS-003).
- `--bare` / `--append-system-prompt-file` (DIS-004).
## Definition of Done
- [ ] Kein `shell: true` mehr im gesamten `src/`-Baum (grep-verifiziert).
- [ ] `cross-spawn` in `package.json#dependencies`, Types in `devDependencies`.
- [ ] `resolveClaude()` wirft klare Fehlermeldung wenn `claude` nicht auf PATH und
`CLAUDE_PATH` leer ist.
- [ ] Unit-Test `tests/unit/resolve-claude.test.ts` mockt `which`/`where`-Aufrufe und
prüft: (a) `.env`-Pfad-Override, (b) Fallback auf PATH-Lookup, (c) Error bei
Nichtfund.
- [ ] `npm run build`, `npm test` grün.
## Dateien (erwartet)
- `package.json`
- `src/runtime/resolve-claude.ts`
- `src/agent/runner.ts`
- `tests/unit/resolve-claude.test.ts`
## Branch
`phase-0/harden-spawn`
## Abhängigkeiten
- Keine.
```
---
### DIS-003: `sanitizedEnv()` — Secrets aus Child-Env entfernen
**Phase**: 0
**Labels**: `phase:0`, `type:fix`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/sanitize-env`
**Issue-Body**:
```
## Ziel
Kein Secret (insbesondere `DISCORD_BOT_TOKEN`) darf im Environment des `claude`-Kindprozesses
landen. Ein Agent kann heute per `echo $DISCORD_BOT_TOKEN` den Bot-Token exfiltrieren.
## Kontext
Referenz: `docs/development-plan.md` §2.6, Phase 0 Task 2.
`src/agent/runner.ts:181` vererbt aktuell `process.env` vollständig. Analyse
`docs/ai-engineer-analyse.md` §4.5.
## Scope
- **In**:
- `src/runtime/env.ts` (neu): `sanitizedEnv(extra?: Record<string, string>): NodeJS.ProcessEnv`.
Entfernt: `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_SECRET`, `DISCORD_PUBLIC_KEY`, alles
matching `/^(DISCLAW_SECRET_|SECRET_|TOKEN_|API_KEY_?)/i`. Fügt hinzu: `CI=true`,
`DISCLAW_AGENT=1`, `DISCLAW_AGENT_NAME=<name>`.
- `src/agent/runner.ts` verwendet `sanitizedEnv({ DISCLAW_AGENT_NAME: ws.agent_name })`
statt `...process.env`.
- Konfigurierbare Block-Liste über `disclaw.yaml` → `env_blocklist: []` (optional,
additiv zur Default-Liste).
- **Out**:
- Whitelist-statt-Blacklist-Modus (kann später nachgezogen werden).
## Definition of Done
- [ ] `sanitizedEnv()` ist pur, hat keine Seiteneffekte, akzeptiert optionale Extras.
- [ ] Unit-Test `tests/unit/sanitize-env.test.ts` verifiziert:
(a) `DISCORD_BOT_TOKEN` wird entfernt,
(b) `SECRET_FOO`, `TOKEN_BAR`, `API_KEY` werden entfernt,
(c) `PATH`, `HOME`, `USERPROFILE` bleiben,
(d) Extras überschreiben Default-Set,
(e) `env_blocklist` aus Config wird angewendet.
- [ ] Runner nutzt `sanitizedEnv` an allen Spawn-Stellen.
- [ ] `npm run build`, `npm test` grün.
## Dateien (erwartet)
- `src/runtime/env.ts`
- `src/agent/runner.ts`
- `src/config/loader.ts` (neues optionales Feld `env_blocklist`)
- `tests/unit/sanitize-env.test.ts`
## Branch
`phase-0/sanitize-env`
## Abhängigkeiten
- Keine. Kann parallel zu DIS-002 laufen (betrifft unterschiedliche Zeilen im Runner,
aber beide editieren `runner.ts` — bei Merge-Konflikt gewinnt DIS-002 zuerst, DIS-003
rebased).
```
---
### DIS-004: Runner mit `--bare` + `--append-system-prompt-file` aufrufen
**Phase**: 0
**Labels**: `phase:0`, `type:feat`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/bare-and-append-identity`
**Issue-Body**:
```
## Ziel
Jeder CLI-Call verwendet `--bare` (skippt CLAUDE.md-Walk-Up, Hooks, Skills, Plugins, MCP,
Auto-Memory) und injiziert die Agenten-Identität explizit über
`--append-system-prompt-file <workspace>/CLAUDE.md`. Parent-CLAUDE.md-Leaks werden
strukturell unmöglich.
## Kontext
Referenz: `docs/development-plan.md` §2.9, Phase 0 Task 7.
Quelle: `docs/cli-feature-answers.md` Frage 8. `--bare` wird laut Doku zukünftiger
Default für `-p`.
## Scope
- **In**:
- `src/agent/runner.ts`: CLI-Args-Builder immer `--bare` und `--append-system-prompt-file`
anhängen, mit `path.join(ws.workspace_path, "CLAUDE.md")`.
- Integration-Test mit Fake-Claude (`tests/fixtures/fake-claude.mjs`), der den
empfangenen System-Prompt als JSON echoed, Assertion: ausschließlich die Workspace-
`CLAUDE.md` ist drin, keine Parent-Leaks.
- **Out**:
- `--resume`-Flag (gehört zu Phase 2).
- Streaming-Flags (Phase 5).
## Definition of Done
- [ ] `runner.ts` baut Args mit `["-p", msg, "--output-format", "json", "--bare",
"--append-system-prompt-file", "<ws>/CLAUDE.md"]` (plus Existing).
- [ ] Integration-Test `tests/integration/no-parent-claude-md-leak.test.ts` nutzt
Fake-Claude und verifiziert den empfangenen System-Prompt-Gehalt.
- [ ] Dokumentations-Kommentar im Runner, warum `--bare` zwingend ist.
- [ ] `npm run build`, `npm test` grün.
## Dateien (erwartet)
- `src/agent/runner.ts`
- `tests/fixtures/fake-claude.mjs`
- `tests/integration/no-parent-claude-md-leak.test.ts`
## Branch
`phase-0/bare-and-append-identity`
## Abhängigkeiten
- Blockiert durch: DIS-002 (gemeinsames Spawn-Setup; einfacher auf sauberer Basis).
```
---
### DIS-005: Path-Traversal-Check in `/new-agent`
**Phase**: 0
**Labels**: `phase:0`, `type:fix`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/path-traversal-check`
**Issue-Body**:
```
## Ziel
`/new-agent name:<x>` akzeptiert keinen Namen, der aus dem Workspace-Root ausbricht,
auch nicht in Edge-Cases (`..`, Unicode-Homoglyphen, `.`, leere Strings).
## Kontext
Referenz: `docs/development-plan.md` Phase 0 Task 4. Analyse §4.3.
Aktuell gibt es eine Regex-Validierung in `src/commands/new-agent.ts`, aber keinen
expliziten `path.resolve`-Containment-Check. Defense in depth: Regex plus Containment.
## Scope
- **In**:
- `src/commands/new-agent.ts`: Nach Regex-Validierung:
```ts
const root = path.resolve(config.workspaces_root);
const wsPath = path.resolve(root, name);
if (!wsPath.startsWith(root + path.sep)) throw new Error("Path traversal");
```
- Saubere User-Fehlermeldung im Discord-Channel („Ungültiger Agent-Name").
- **Out**:
- Unicode-Normalisierung über Regex hinaus (genügen aktuell `[a-z0-9-]+`).
## Definition of Done
- [ ] Containment-Check ist aktiv.
- [ ] Unit-Test `tests/unit/path-traversal.test.ts` prüft:
`../etc`, `..`, `./foo`, `a/../b`, leerer String, `CON` (Windows-reserved), Unicode.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/commands/new-agent.ts`
- `tests/unit/path-traversal.test.ts`
## Branch
`phase-0/path-traversal-check`
## Abhängigkeiten
- Keine. Kann parallel laufen.
```
---
### DIS-006: `.claude/settings.json`-Template härten + `CLAUDE.md`-Injection-Klausel
**Phase**: 0
**Labels**: `phase:0`, `type:feat`, `priority:p0`, `security`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/harden-settings-and-claudemd`
**Issue-Body**:
```
## Ziel
Jeder neu erstellte Agent bekommt ein gehärtetes `.claude/settings.json` (Deny-Liste
gegen `../**`, `Read(./.env)`, gefährliche Bash-Muster) und eine `CLAUDE.md` mit einer
expliziten Prompt-Injection-Resistenz-Klausel.
## Kontext
Referenz: `docs/development-plan.md` §2.5, Phase 0 Tasks 5+6. Settings-Schema bestätigt
via `docs/cli-feature-answers.md` Frage 3.
## Scope
- **In**:
- `src/agent/identity.ts`: Settings-Template gemäß §2.5 aus dem Plan:
```json
{
"permissions": {
"allow": ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"],
"ask": ["Bash(git push *)"],
"deny": [
"Read(../**)", "Write(../**)", "Edit(../**)",
"Read(./.env)", "WebFetch",
"Bash(rm -rf *)", "Bash(curl * | sh)",
"Bash(ssh *)", "Bash(scp *)",
"Bash(* /etc/*)", "Bash(* ~/.ssh/*)",
"Bash(powershell -Command *)"
],
"defaultMode": "acceptEdits"
},
"cleanupPeriodDays": 90
}
```
- `CLAUDE.md`-Template erhält einen Abschnitt „Sicherheit & Prompt-Injection", der
klar stellt: Anweisungen aus Nachrichten, die außerhalb dieser CLAUDE.md stehen,
dürfen Permissions nicht überschreiben.
- `PreToolUse`-Hook-Stub wird in Phase 4 (DIS-Epic-P4) ergänzt — **nicht in diesem
Issue**.
- **Out**:
- Der tatsächliche Hook-Guard-Node-Script (Phase 4).
- Profile-spezifische Settings (Phase 4).
## Definition of Done
- [ ] Frisch erstellter Agent-Workspace enthält settings.json mit vollständiger Deny-Liste.
- [ ] `CLAUDE.md` enthält den Injection-Resistenz-Absatz (im Template).
- [ ] Unit-Test `tests/unit/identity-templates.test.ts` parsed das gerenderte JSON und
verifiziert die Presence der Deny-Einträge und des CLAUDE.md-Satzes.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/agent/identity.ts`
- `tests/unit/identity-templates.test.ts`
## Branch
`phase-0/harden-settings-and-claudemd`
## Abhängigkeiten
- Keine.
```
---
### DIS-007: Lock-File gegen Doppelstart
**Phase**: 0
**Labels**: `phase:0`, `type:feat`, `priority:p1`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/startup-lockfile`
**Issue-Body**:
```
## Ziel
Zwei gleichzeitig laufende DisClaw-Instanzen auf derselben DB/Workspace-Root werden
verhindert. Der zweite Start bricht mit klarer Fehlermeldung ab.
## Kontext
Referenz: `docs/development-plan.md` §7 (Risiko-Tabelle, „Zwei DisClaw-Instanzen gegen
dieselbe DB/Workspace").
## Scope
- **In**:
- `src/runtime/lockfile.ts` (neu): `acquireLock(path)` legt `<workspaces_root>/.disclaw.lock`
an mit aktueller PID und ISO-Timestamp. Prüft bei existierendem File, ob der PID noch
lebt (`process.kill(pid, 0)`). Wenn nein, stale-lock übernehmen.
- `src/index.ts` ruft `acquireLock` vor DB-Init auf; bei Fehlschlag: klare Fehlermeldung,
exit 1.
- Graceful Shutdown löscht das Lock-File.
- **Out**:
- Distributed Locking für Multi-Host-Deployments (out of scope für MVP).
## Definition of Done
- [ ] Zweiter Startversuch bricht mit „DisClaw bereits aktiv (PID X)" ab.
- [ ] Stale-Lock (PID tot) wird automatisch übernommen.
- [ ] Unit-Test `tests/unit/lockfile.test.ts` mit tmp-Dir deckt: Acquire, Doppel-Acquire,
Stale-Lock-Recovery, Release.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/lockfile.ts`
- `src/index.ts`
- `tests/unit/lockfile.test.ts`
## Branch
`phase-0/startup-lockfile`
## Abhängigkeiten
- Blockiert durch: DIS-001 (Lock liegt unter dem finalen `workspaces_root`).
```
---
### DIS-008: Forgejo Actions CI (build, test, lint)
**Phase**: 0
**Labels**: `phase:0`, `type:ci`, `priority:p1`
**Milestone**: Phase 0 — Härtung
**Branch**: `phase-0/ci-pipeline`
**Issue-Body**:
```
## Ziel
Jeder PR gegen `main` triggert eine CI-Pipeline, die `npm run build`, `npm test` und
(sobald ESLint eingeführt) `npm run lint` ausführt. Ohne grüne Checks kein Merge.
## Kontext
Referenz: `docs/development-plan.md` Phase 0 Task 8 (implizit durch §7 Risiko-Mitigation
„CI-Test auf Windows-Runner"). Forgejo Actions ist API-kompatibel mit GitHub Actions.
## Scope
- **In**:
- `.forgejo/workflows/ci.yml`: Trigger `pull_request` gegen `main`. Matrix:
`ubuntu-latest`, `windows-latest`. Steps: checkout, setup-node 20, `npm ci`,
`npm run build`, `npm test`.
- ESLint-Config-Basis (`.eslintrc.cjs` mit `no-child-process-shell-true`-Custom-Rule
oder einfach Grep-Guard in einem Script), `package.json` `lint`-Script.
- Branch-Protection für `main` verlangt die Checks `build`, `test`, `lint` (setup
via API-Call, siehe `docs/workflow.md` §2.4).
- **Out**:
- Deployment-Jobs, Code-Coverage-Upload, Release-Automation.
## Definition of Done
- [ ] CI-Workflow liegt unter `.forgejo/workflows/ci.yml`.
- [ ] Pipeline läuft grün auf einem Test-PR (manuell verifiziert).
- [ ] Matrix deckt Linux + Windows ab.
- [ ] `npm run lint`-Script existiert und läuft lokal.
- [ ] README-Abschnitt „CI" verweist auf den Workflow.
## Dateien (erwartet)
- `.forgejo/workflows/ci.yml`
- `.eslintrc.cjs`
- `package.json`
- `README.md`
## Branch
`phase-0/ci-pipeline`
## Abhängigkeiten
- Sollte spät in Phase 0 kommen (nachdem mindestens DIS-002, DIS-003, DIS-005 im Code
sind), damit die Lint-Rules auf sauberer Basis greifen.
```
---
## Phase 1 — Solides MVP (10 Issues)
### DIS-101: Pino-Logger einführen, `console.*` ersetzen
**Phase**: 1
**Labels**: `phase:1`, `type:refactor`, `priority:p1`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/pino-logger`
**Issue-Body**:
```
## Ziel
Strukturiertes JSON-Logging über `pino`. Child-Logger pro Komponente. Keine
`console.log/.error` mehr im Produktionscode.
## Kontext
Referenz: `docs/development-plan.md` §2.7, Phase 1 Task 1.
## Scope
- **In**: `src/runtime/logger.ts` mit `rootLogger` und `childLogger(component)`. Alle
`console.*`-Calls in `src/index.ts`, `src/bot.ts`, `src/router.ts`, `src/agent/runner.ts`,
`src/commands/new-agent.ts` ersetzen. `pino-pretty` in `devDependencies`, in dev als
Pretty-Transport.
- **Out**: Log-Sampling, Log-Levels-Hot-Reload, externe Log-Ziele.
## Definition of Done
- [ ] `grep -r "console\." src/` liefert keine Treffer.
- [ ] Pino-Child-Logger mit `component:"runner"` etc. im Output sichtbar.
- [ ] Unit-Test `tests/unit/logger.test.ts` verifiziert, dass `childLogger` den
`component`-Key setzt.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/logger.ts`
- `src/index.ts`, `src/bot.ts`, `src/router.ts`, `src/agent/runner.ts`, `src/commands/new-agent.ts`
- `tests/unit/logger.test.ts`
- `package.json`
## Branch
`phase-1/pino-logger`
## Abhängigkeiten
- Phase 0 abgeschlossen.
```
---
### DIS-102: `sanitizeForDiscord(text, rootDir)` — Pfade aus User-Output entfernen
**Phase**: 1
**Labels**: `phase:1`, `type:feat`, `priority:p1`, `security`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/sanitize-for-discord`
**Issue-Body**:
```
## Ziel
Absolute Pfade, Home-Dir und Tokens werden vor dem Discord-Send aus Agent-Antworten
entfernt. Verhindert Info-Leak in öffentliche Channels.
## Kontext
Referenz: `docs/development-plan.md` §2.7 (Error-Handler-Strategie), Phase 1 Task 2.
## Scope
- **In**: `src/runtime/sanitize.ts` mit `sanitizeForDiscord(text: string, ctx: { repoRoot: string; home: string })`.
Ersetzt Regex-basiert `repoRoot` → `<disclaw>`, `home` → `~`, bekannte Token-Regexes → `<redacted>`.
Router ruft es vor jedem `channel.send`.
- **Out**: Content-Moderation, PII-Detection.
## Definition of Done
- [ ] Test `tests/unit/sanitize-for-discord.test.ts` deckt: Repo-Pfad, Home-Pfad,
Discord-Token-Regex, keine False-Positives auf harmlosem Text.
- [ ] Router nutzt Sanitizer an allen Send-Stellen.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/sanitize.ts`
- `src/router.ts`
- `tests/unit/sanitize-for-discord.test.ts`
## Branch
`phase-1/sanitize-for-discord`
## Abhängigkeiten
- Blockiert durch: DIS-101 (gemeinsames Refactoring-Fenster im Router).
```
---
### DIS-103: `ChannelQueue` — Per-Channel-FIFO
**Phase**: 1
**Labels**: `phase:1`, `type:feat`, `priority:p0`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/channel-queue`
**Issue-Body**:
```
## Ziel
Mehrere Nachrichten im selben Channel werden strikt seriell abgearbeitet. Parallele
Channels bleiben parallel. Keine Race Conditions mehr auf Workspace-Dateien.
## Kontext
Referenz: `docs/development-plan.md` §2.3, Phase 1 Task 3. Status-Quo §1 (3).
## Scope
- **In**:
- `src/runtime/channel-queue.ts` mit `ChannelQueue`-Klasse wie in §2.3 skizziert.
- Integration in `src/router.ts`: jeder `messageCreate`-Handler läuft durch
`channelQueue.enqueue(channelId, () => runAgent(...))`.
- **Out**: Persistente Queue über Prozessneustart.
## Definition of Done
- [ ] Unit-Test `tests/unit/channel-queue.test.ts` verifiziert: FIFO-Ordnung,
Error-Recovery (eine fehlgeschlagene Task blockiert die Queue nicht), Map-Cleanup
nach leerer Queue.
- [ ] Integration-Test `tests/integration/channel-queue-router.test.ts` mit Mock-Router:
drei schnell hintereinander enqueued Messages werden in Reihenfolge abgearbeitet.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/channel-queue.ts`
- `src/router.ts`
- `tests/unit/channel-queue.test.ts`
- `tests/integration/channel-queue-router.test.ts`
## Branch
`phase-1/channel-queue`
## Abhängigkeiten
- Blockiert durch: DIS-101.
```
---
### DIS-104: Globaler Semaphore (`max_concurrent_agents`)
**Phase**: 1
**Labels**: `phase:1`, `type:feat`, `priority:p1`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/concurrency-semaphore`
**Issue-Body**:
```
## Ziel
Ein globaler Cap auf parallele `claude`-Prozesse verhindert Ressourcen-Thrashing.
Default 4, konfigurierbar via `disclaw.yaml` → `max_concurrent_agents`.
## Kontext
Referenz: `docs/development-plan.md` §2.3 (zweiter Absatz), Phase 1 Task 4.
## Scope
- **In**: `src/runtime/concurrency.ts` mit simpler `Semaphore(max: number)`-Klasse
(`acquire()/release()`). In `src/agent/runner.ts` umschließt der Spawn-Call
ein `semaphore.acquire()` / `try { ... } finally { semaphore.release() }`.
- **Out**: Dynamisches Auto-Scaling.
## Definition of Done
- [ ] Unit-Test `tests/unit/semaphore.test.ts` deckt: Acquire bei freien Slots,
Queueing bei vollen Slots, Release gibt wartende Tasks frei, Error-Path hält
den Counter konsistent.
- [ ] Config-Default 4, in `disclaw.yaml` kommentiert.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/concurrency.ts`
- `src/agent/runner.ts`
- `src/config/loader.ts`
- `disclaw.yaml`
- `tests/unit/semaphore.test.ts`
## Branch
`phase-1/concurrency-semaphore`
## Abhängigkeiten
- Keine harten. Kann parallel zu DIS-103 laufen.
```
---
### DIS-105: `splitForDiscord` — Codeblock-aware Message-Splitting
**Phase**: 1
**Labels**: `phase:1`, `type:feat`, `priority:p0`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/split-for-discord`
**Issue-Body**:
```
## Ziel
Antworten > 1900 Zeichen werden korrekt aufgeteilt, ohne Codeblöcke zu zerreißen.
Fences werden am Split geschlossen und im nächsten Chunk wieder geöffnet.
## Kontext
Referenz: `docs/development-plan.md` §2.4, Phase 1 Task 5. Status-Quo §1 (5).
## Scope
- **In**:
- `src/discord/split.ts` mit `splitForDiscord(text: string, limit = 1900): string[]`.
- Fence-Erkennung (```` ``` ````), Sprache-Tag wird im neuen Chunk beibehalten.
- Sehr lange Ausgaben (> 8000 Zeichen) bleiben als ein String — Fallback auf
Attachment passiert in Phase 3 (DIS-Epic-P3).
- Integration in `src/router.ts`.
- **Out**: Attachment-Fallback (Phase 3), Streaming-Split (Phase 5).
## Definition of Done
- [ ] Unit-Test `tests/unit/split-for-discord.test.ts` deckt: Plain-Text-Split an
Newlines, Codeblock in der Mitte (wird geschlossen/eröffnet mit gleichem Tag),
mehrere Fences, sehr lange Einzelzeile (hard-split), leerer Input.
- [ ] `router.ts` nutzt die neue Funktion.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/discord/split.ts`
- `src/router.ts`
- `tests/unit/split-for-discord.test.ts`
## Branch
`phase-1/split-for-discord`
## Abhängigkeiten
- Blockiert durch: DIS-101.
```
---
### DIS-106: Runner-API-Refactoring → `RunResult`-Typ
**Phase**: 1
**Labels**: `phase:1`, `type:refactor`, `priority:p1`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/runner-runresult-api`
**Issue-Body**:
```
## Ziel
`runAgent()` liefert einen typisierten `RunResult = { text, sessionId?, usage?, toolCalls? }`.
Bereitet Phase 2 (`sessionId` speichern) und Phase 5 (`runAgentStream()`) ohne API-Bruch
vor.
## Kontext
Referenz: `docs/development-plan.md` Phase 1 Task 6, §5.6 (`AgentEngine`-Interface).
## Scope
- **In**:
- `src/agent/engine.ts` (neu): `AgentEngine`-Interface, `RunOptions`, `RunResult`,
`StreamEvent`-Stub (wird in Phase 5 gefüllt).
- `src/agent/runner.ts`: Rückgabetyp auf `RunResult` vereinheitlichen. Parser
extrahiert `text` und optional `sessionId`, `usage`.
- Router nutzt nur noch `result.text`.
- **Out**:
- `SessionStore`-Integration (Phase 2).
- Streaming-Implementierung (Phase 5).
## Definition of Done
- [ ] `AgentEngine`-Interface existiert, `ClaudeCliEngine` implementiert es.
- [ ] Router compiled gegen `RunResult`, kein `any`.
- [ ] Unit-Test `tests/unit/parse-claude-output.test.ts` deckt verschiedene JSON-Formate
ab (text direkt, nested `result.content`, fehlerhafte Outputs).
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/agent/engine.ts`
- `src/agent/runner.ts`
- `src/router.ts`
- `tests/unit/parse-claude-output.test.ts`
## Branch
`phase-1/runner-runresult-api`
## Abhängigkeiten
- Blockiert durch: DIS-004 (bare/append fließt in Args ein).
```
---
### DIS-107: Zod-Schemas für Config, `agent.yaml`, CLI-JSON-Output
**Phase**: 1
**Labels**: `phase:1`, `type:feat`, `priority:p1`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/zod-schemas`
**Issue-Body**:
```
## Ziel
Alle Grenzen zwischen untrusted Input und Code sind Zod-validiert: `disclaw.yaml`,
`agent.yaml`, Claude-CLI-JSON-Output. Keine `as`-Casts mehr.
## Kontext
Referenz: `docs/development-plan.md` §3 (`zod`), Phase 1 Tasks 7+8.
## Scope
- **In**:
- `src/runtime/zod-schemas.ts` (neu): `disclawConfigSchema`, `agentYamlSchema`,
`claudeJsonOutputSchema`.
- `src/config/loader.ts`: Parse → Zod → `DisclawConfig`.
- `src/agent/runner.ts`: `parseClaudeJsonOutput` validiert via Zod (safe-parse, logged
bei Fehler, fällt auf Best-Effort-Text zurück).
- **Out**: Schema-Generation aus TypeScript-Types.
## Definition of Done
- [ ] Alle `as`-Casts aus `loader.ts` und `runner.ts` entfernt.
- [ ] Unit-Test `tests/unit/zod-schemas.test.ts` deckt: valide/invalide YAMLs,
valide/invalide CLI-JSON-Samples.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/runtime/zod-schemas.ts`
- `src/config/loader.ts`
- `src/agent/runner.ts`
- `tests/unit/zod-schemas.test.ts`
## Branch
`phase-1/zod-schemas`
## Abhängigkeiten
- Blockiert durch: DIS-106 (gemeinsamer Runner-Touch).
```
---
### DIS-108: Schema-SSOT: `src/db/schema.sql` ist die Single Source
**Phase**: 1
**Labels**: `phase:1`, `type:refactor`, `priority:p2`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/schema-ssot`
**Issue-Body**:
```
## Ziel
Kein inline `SCHEMA_SQL`-String mehr in `database.ts`. `schema.sql` wird zur Laufzeit
geladen und ausgeführt. Verhindert Schema-Drift.
## Kontext
Referenz: `docs/development-plan.md` Status-Quo §1 (13), Phase 1 Task 9.
## Scope
- **In**: `src/db/database.ts` liest `schema.sql` via `fs.readFileSync` und führt es
beim Init aus. Inline-String wird entfernt.
- **Out**: Echte Migrationen mit Version-Tracking (kommt mit Phase 2).
## Definition of Done
- [ ] Inline-SCHEMA_SQL existiert nicht mehr.
- [ ] Unit-Test `tests/unit/database-init.test.ts` verifiziert, dass eine frische DB
alle erwarteten Tabellen hat.
- [ ] `npm test` grün.
## Dateien (erwartet)
- `src/db/database.ts`
- `tests/unit/database-init.test.ts`
## Branch
`phase-1/schema-ssot`
## Abhängigkeiten
- Keine.
```
---
### DIS-109: `.env.example` + Vitest-Setup + `npm test`-Skript
**Phase**: 1
**Labels**: `phase:1`, `type:chore`, `priority:p1`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/tooling-vitest-envexample`
**Issue-Body**:
```
## Ziel
`.env.example` ist im Repo. Vitest ist als Test-Runner konfiguriert. `npm test`
funktioniert Out-of-the-Box.
## Kontext
Referenz: `docs/development-plan.md` §2.8, Phase 1 Tasks 12+13. Status-Quo §1 (10, 12).
## Scope
- **In**:
- `.env.example` mit allen relevanten Variablen (`DISCORD_BOT_TOKEN=`,
`DISCORD_GUILD_ID=`, `CLAUDE_PATH=`).
- `vitest.config.ts` minimal (default genügt).
- `package.json`: `"test": "vitest run"`, `"test:watch": "vitest"`,
`vitest`, `@types/node` in devDeps.
- **Out**: Coverage-Reports, CI-Reporter-Config.
## Definition of Done
- [ ] `.env.example` existiert und wird im README erwähnt.
- [ ] `npm test` läuft lokal (mindestens ein Dummy-Test grün) auf sauberem `npm ci`.
- [ ] `package.json#devDependencies` enthält Vitest.
## Dateien (erwartet)
- `.env.example`
- `vitest.config.ts`
- `package.json`
- `README.md`
## Branch
`phase-1/tooling-vitest-envexample`
## Abhängigkeiten
- Sollte früh in Phase 1 kommen, damit nachfolgende Issues direkt Tests schreiben können.
```
---
### DIS-110: `docs/cli-feature-probe.md` — Manuelles Regression-Script
**Phase**: 1
**Labels**: `phase:1`, `type:docs`, `priority:p2`
**Milestone**: Phase 1 — Solides MVP
**Branch**: `phase-1/cli-feature-probe-doc`
**Issue-Body**:
```
## Ziel
Ein dokumentiertes manuelles Script, das die kritischen CLI-Annahmen aus
`cli-feature-answers.md` gegen die lokal installierte `claude`-CLI verifiziert:
Session-ID top-level, `usage`-Block-Pfade, `stream-json`-Event-Typen, Session-Slug-Regel.
## Kontext
Referenz: `docs/development-plan.md` Phase 1 Task 14, §6 (Offene Rest-Unsicherheiten).
## Scope
- **In**: `docs/cli-feature-probe.md` mit kopierbaren Probe-Commands (siehe
`cli-feature-answers.md` für die Basis). Tabelle „Erwartet / Beobachtet" als
Vorlage zum Ausfüllen.
- **Out**: Automatisierung der Probes (bleibt manuell bis Phase 2).
## Definition of Done
- [ ] Dokument existiert, enthält mindestens die 4 Probes aus den offenen Fragen.
- [ ] Jeder Probe ist eine Expected-Output-Zeile zugeordnet.
## Dateien (erwartet)
- `docs/cli-feature-probe.md`
## Branch
`phase-1/cli-feature-probe-doc`
## Abhängigkeiten
- Keine.
```
---
## Phase 2 — Context-Effizienz (`--resume`) — Epic-Level (4 Issues)
Am Phasen-Anfang verfeinern.
### DIS-EP-201: DB-Migration + `SessionStore`-Methoden
`claude_session_id`, `session_updated_at` additiv hinzufügen, DB-Repo-Methoden
`getSession/setSession/clearSession` inklusive Timestamp-Update. Siehe Plan §2.1.
### DIS-EP-202: Runner-Integration `--resume` mit Fallback
Runner nutzt `SessionStore`, führt Retry ohne `--resume` bei Resume-Fehler durch, updated
nach jedem Run die ID defensiv. Siehe Plan §2.1.
### DIS-EP-203: Usage-Metriken logging + `agent_runs`-Tabelle
`usage`-Block aus JSON-Output parsen (Pfade empirisch via Probe-Script bestimmen), pro
Run als strukturierter Log-Event + optional in neue `agent_runs`-Tabelle.
### DIS-EP-204: Integration-Tests Session-Resume + Fork-Case
Fake-Claude-Fixture simuliert Erst-Run, Resume-Run, Fork-Szenario (neue Session-ID im
zweiten Run), Resume-Fehler → Fallback. Siehe Plan Phase 2 DoD.
---
## Phase 3 — UX-Feinschliff — Epic-Level (4 Issues)
### DIS-EP-301: Typing-Refresh 8s + `sendResponse`-Helper + Attachment-Fallback
Siehe Plan Phase 3 Tasks 1+2.
### DIS-EP-302: Attachment-Inbox für eingehende Discord-Bilder
Download in `<ws>/.disclaw-inbox/`, Prompt-Referenz, TTL-Cleanup. Plan Phase 3 Task 3.
### DIS-EP-303: Input-Limit + User-Feedback-Reactions (`👀/✅/❌/⚠️`)
Plan Phase 3 Tasks 4+5.
### DIS-EP-304: Unit-Tests für `send-response` und Attachment-Inbox
Plan Phase 3 Task 6.
---
## Phase 4 — Skills und Permissions (Profile) — Epic-Level (5 Issues)
### DIS-EP-401: Profile-Registry-Grundgerüst
`src/agent/profiles/index.ts` + `ProfileTemplate`-Interface, Platzhalter-Rendering.
Plan §5.3, Phase 4 Tasks 1+2.
### DIS-EP-402: Fünf Profiles — developer, researcher, writer, ops, sandboxed
Templates für `CLAUDE.md` und `settings.json` pro Rolle. Plan Phase 4 Task 1.
### DIS-EP-403: `/new-agent profile:<name>` Option
Slash-Command-Erweiterung mit Choice-Liste. Plan Phase 4 Task 3.
### DIS-EP-404: PreToolUse-Hook `guard-tool.cjs` + Audit-Log
Hook-Script wird beim Workspace-Create nach `.claude/hooks/guard-tool.cjs` gerendert.
Jeder Tool-Call gegen `workspace_abs_path` geprüft, Audit-Log nach
`<ws>/.disclaw-audit.jsonl`. Plan §2.5, Phase 4 Tasks 5+6.
### DIS-EP-405: Integration-Tests Hook-Guard + Profile-Permissions
Fake-Claude löst bösartigen Tool-Call aus → Hook muss ihn blocken. Plan Phase 4 Task 7.
---
## Phase 5 — Streaming — Epic-Level (4 Issues)
### DIS-EP-501: `runAgentStream()` mit `stream-json` + `--verbose` + `--include-partial-messages`
Zweiter Engine-Export, NDJSON-Output vom CLI. Plan Phase 5 Task 1.
### DIS-EP-502: Defensiver NDJSON-Event-Parser
Dispatch nach `type`, unbekannte Events skippen, session_id + usage aus `result`-Event
extrahieren. Plan Phase 5 Task 2.
### DIS-EP-503: `stream-to-discord` — Edit-Debouncing 1200ms, Rollover bei 1900 Zeichen
Respektiert Discord-Rate-Limits. Plan Phase 5 Task 3.
### DIS-EP-504: Feature-Flag + Fallback + Integration-Tests
`streaming: true/false` in `disclaw.yaml`, automatischer Bulk-Fallback bei Stream-Fehler.
Plan Phase 5 Tasks 4+5.
---
## Phase 6+ — Zukunft (Platzhalter)
Nicht jetzt planen. Wird beim Phasen-Übergang Phase 5 → 6 verfeinert.
**Themen** (aus `docs/development-plan.md` §4 Phase 6+):
- `/list-agents`, `/delete-agent`, `/agent-config` Slash-Commands
- User-Rate-Limits (Token-Bucket)
- Daily-Budget pro Agent (`agent_usage`-Tabelle)
- Agent-zu-Agent-Kommunikation (Router-Delegation)
- Docker-Isolation (`DockerRunner` als zweite `AgentEngine`-Implementierung)
- Web-Dashboard (Fastify + shared DB)
- Alternative AI-Engine (`ClaudeSdkEngine` via `claude-agent-sdk`)
- `/migrate-workspaces`-Command für Legacy-In-Repo-Workspaces
- Linux/Mac Session-Slug-Verifikation für `/delete-agent`
Label `phase:6`, keine Milestones jetzt.
---
## Start-Reihenfolge der ersten 3 Issues
Beim Start von Phase 0 zieht der erste Developer-Agent die folgenden Issues **in dieser
Reihenfolge**:
### 1. DIS-002 — `shell: false` + `cross-spawn` Windows-Fix
**Warum zuerst**: Größter akuter Security-Fix (Command-Injection) und strukturelle
Voraussetzung für alle weiteren Runner-Änderungen. Danach ist der Runner auf jedem
Betriebssystem überhaupt erst wieder zuverlässig spawn-fähig. Blockt DIS-004 und DIS-106.
Keine Dependencies, sofort startbar.
### 2. DIS-003 — `sanitizedEnv()`
**Warum direkt danach**: Zweiter P0-Security-Fix (Token-Exfiltration via
`$DISCORD_BOT_TOKEN`). Minimale Code-Berührung am Runner, hohes Risiko-Reward-Verhältnis.
Kein struktureller Abhängigkeitsgraph zu DIS-002 außer Merge-Reihenfolge auf
`runner.ts` — deshalb als zweites.
### 3. DIS-001 — Workspace-Root nach `~/.disclaw/workspaces/`
**Warum drittes**: Strukturelle Voraussetzung für DIS-004 (`--append-system-prompt-file`
setzt einen sauberen Workspace-Pfad voraus) und DIS-007 (Lock-File liegt unter dem neuen
Root). Keine direkte Kollision mit DIS-002/003 (anderer Code-Pfad: `loader.ts`,
`new-agent.ts`, nicht `runner.ts`), kann also in einem parallelen Branch gestartet
werden, sobald DIS-002 gemerged ist — aber die serielle Reihenfolge ist sicherer, wenn
nur ein Developer-Agent aktiv ist.
Ab dem Abschluss dieser drei Issues sind DIS-004, DIS-005, DIS-006, DIS-007 alle
unblocked und können parallel bearbeitet werden. DIS-008 (CI) kommt bewusst am Ende
von Phase 0, damit Lint-Rules auf sauberer Basis greifen.