disclaw/docs/backlog.md

35 KiB
Raw Blame History

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.