35 KiB
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,5–2 Dev-Tage pro Issue, einzeln testbar).
- Phase 2–5 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-configSlash-Commands- User-Rate-Limits (Token-Bucket)
- Daily-Budget pro Agent (
agent_usage-Tabelle) - Agent-zu-Agent-Kommunikation (Router-Delegation)
- Docker-Isolation (
DockerRunnerals zweiteAgentEngine-Implementierung) - Web-Dashboard (Fastify + shared DB)
- Alternative AI-Engine (
ClaudeSdkEngineviaclaude-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.