Merge pull request 'forgejo management-token + organization/tasks created + workflow/board' (#20) from phase-pre/organization into main
Reviewed-on: #20
This commit is contained in:
commit
59c5a0c387
25 changed files with 2981 additions and 5 deletions
25
.forgejo/PULL_REQUEST_TEMPLATE.md
Normal file
25
.forgejo/PULL_REQUEST_TEMPLATE.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
## Summary
|
||||
<!-- 1–3 Bullet Points was geändert wurde -->
|
||||
-
|
||||
|
||||
## Closes
|
||||
<!-- Issue-Referenz: Closes #DIS-XXX -->
|
||||
Closes #
|
||||
|
||||
## Test Plan
|
||||
<!-- Welche Tests wurden hinzugefügt / angepasst? Wie manuell verifiziert? -->
|
||||
- [ ] Unit-Tests geschrieben und grün (`npm test`)
|
||||
- [ ] `npm run build` grün
|
||||
- [ ] `npm run lint` grün (wenn vorhanden)
|
||||
- [ ] Manuelle Verifikation:
|
||||
|
||||
## Breaking Changes
|
||||
<!-- Ja/Nein. Wenn ja: was muss der User beim Update tun? -->
|
||||
Nein
|
||||
|
||||
## Reviewer-Checkliste
|
||||
- [ ] Kein `shell: true` in `src/` (`grep -r "shell: true" src/` → leer)
|
||||
- [ ] Kein `process.env` direkt im Runner (nur via `sanitizedEnv`)
|
||||
- [ ] Kein absoluter Pfad aus dem Workspace-Root nach außen
|
||||
- [ ] Tests decken Error-Cases ab, nicht nur Happy Path
|
||||
- [ ] Kein `console.*` im Produktionscode (Pino stattdessen — ab Phase 1)
|
||||
29
.obsidian/workspace.json
vendored
29
.obsidian/workspace.json
vendored
|
|
@ -13,12 +13,12 @@
|
|||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "docs/ai-engineer-analyse.md",
|
||||
"file": "tasks/BOARD.md",
|
||||
"mode": "preview",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "ai-engineer-analyse"
|
||||
"title": "BOARD"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -183,10 +183,29 @@
|
|||
"bases:Neue Base erstellen": false
|
||||
}
|
||||
},
|
||||
"active": "8a9c3ac5d2b3b989",
|
||||
"active": "1decbe58679087d4",
|
||||
"lastOpenFiles": [
|
||||
"tasks/DIS-107.md",
|
||||
"tasks/DIS-106.md",
|
||||
"docs/workflow.md",
|
||||
"docs/backlog.md",
|
||||
"tasks/DIS-105.md",
|
||||
"tasks/DIS-104.md",
|
||||
"tasks/DIS-103.md",
|
||||
"tasks/DIS-102.md",
|
||||
"tasks/DIS-101.md",
|
||||
"tasks/DIS-008.md",
|
||||
"tasks/DIS-007.md",
|
||||
"tasks/DIS-006.md",
|
||||
"tasks/DIS-005.md",
|
||||
"tasks/DIS-004.md",
|
||||
"tasks/DIS-003.md",
|
||||
"tasks/DIS-002.md",
|
||||
"tasks/DIS-001.md",
|
||||
"tasks/BOARD.md",
|
||||
"tasks",
|
||||
"docs/ai-engineer-analyse.md",
|
||||
"docs/cli-feature-answers.md",
|
||||
"docs/development-plan.md",
|
||||
"docs/ai-engineer-analyse.md"
|
||||
"docs/development-plan.md"
|
||||
]
|
||||
}
|
||||
1040
docs/backlog.md
Normal file
1040
docs/backlog.md
Normal file
File diff suppressed because it is too large
Load diff
496
docs/workflow.md
Normal file
496
docs/workflow.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# DisClaw — Projekt- und Git-Workflow
|
||||
|
||||
Autor: Projekt- und Git-Manager-Agent
|
||||
Datum: 2026-04-08
|
||||
Quelle der Wahrheit für Inhalt: `docs/development-plan.md`.
|
||||
Quelle der Wahrheit für den Backlog: `docs/backlog.md`.
|
||||
|
||||
Dieses Dokument definiert, wie an DisClaw gearbeitet wird: wie Branches heißen,
|
||||
wie Forgejo strukturiert ist, wie Issues geschnitten werden, wie ein Developer-Agent
|
||||
(Claude Sonnet 4.6) pro Issue vorgeht, und wann eine Phase als abgeschlossen gilt.
|
||||
|
||||
Forgejo-Remote: `http://localhost:3000/dev/disclaw.git` (lokale Instanz).
|
||||
|
||||
---
|
||||
|
||||
## 1. Branch-Strategie
|
||||
|
||||
### 1.1 `main` ist protected
|
||||
|
||||
- `main` ist **protected**. Keine direkten Pushes, keine Force-Pushes, kein Rebase.
|
||||
- Änderungen kommen ausschließlich über Pull Requests.
|
||||
- Jeder PR benötigt mindestens ein Review-Approval und grüne Checks, bevor er gemerged wird.
|
||||
- Merge-Strategie: **Squash-Merge**. Eine Aufgabe = ein Commit auf `main`. Das hält die Historie
|
||||
linear und macht Rollback (`git revert <sha>`) trivial.
|
||||
|
||||
### 1.2 Naming-Convention für Feature-Branches
|
||||
|
||||
```
|
||||
phase-<n>/<kurz-slug>
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
- `phase-0/harden-spawn`
|
||||
- `phase-0/sanitize-env`
|
||||
- `phase-1/channel-queue`
|
||||
- `phase-1/split-for-discord`
|
||||
|
||||
**Begründung**: Der Phase-Prefix macht sofort sichtbar, an welchem Phasen-Ziel ein Branch
|
||||
arbeitet. Das hilft beim Filtern in Forgejo (`refs/heads/phase-0/*`), beim Review-Priorisieren
|
||||
und beim mentalen Mapping zu Milestones. Der Kurz-Slug ist imperativ und eindeutig
|
||||
(`harden-spawn`, nicht `fix-stuff`). Keine Trailing-Nummern, keine Issue-IDs im Branch-Namen —
|
||||
die Verknüpfung passiert im PR via `Closes #N`.
|
||||
|
||||
### 1.3 Lifecycle eines Feature-Branches
|
||||
|
||||
1. Issue wird in Forgejo `Ready` gesetzt und einem Developer-Agent zugewiesen.
|
||||
2. Developer-Agent checkt aktuellen `main` aus (`git fetch origin && git checkout main && git pull --ff-only`).
|
||||
3. Developer-Agent erstellt den Branch: `git checkout -b phase-<n>/<slug>`.
|
||||
4. Implementierung gegen die Definition of Done des Issues.
|
||||
5. Commits folgen Conventional-Commits (§5).
|
||||
6. Push: `git push -u origin phase-<n>/<slug>`.
|
||||
7. PR gegen `main` öffnen (Template in `.forgejo/PULL_REQUEST_TEMPLATE.md`).
|
||||
8. Review, Anpassungen, grüne Checks.
|
||||
9. Squash-Merge.
|
||||
10. Branch wird automatisch gelöscht (Forgejo-Setting: "Delete branch after merge").
|
||||
|
||||
### 1.4 Keine langlebigen Integration-Branches
|
||||
|
||||
- Kein `dev`, kein `next`, kein `phase-1-integration`.
|
||||
- Eine Phase gilt als abgeschlossen, wenn **alle Issues ihres Milestones** gemerged sind und
|
||||
der manuelle Smoke-Test (§8) durch ist.
|
||||
- Der einzige geteilte Branch ist `main`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Forgejo-Projektstruktur
|
||||
|
||||
### 2.1 Labels
|
||||
|
||||
Maximal schlank gehalten. Ziel: jeder Developer versteht das Label-Set in < 30 Sekunden.
|
||||
|
||||
| Name | Farbe (hex) | Zweck |
|
||||
|---|---|---|
|
||||
| `phase:0` | `#b71c1c` | Phase 0 — Härtung |
|
||||
| `phase:1` | `#e65100` | Phase 1 — Solides MVP |
|
||||
| `phase:2` | `#f9a825` | Phase 2 — Context-Effizienz (`--resume`) |
|
||||
| `phase:3` | `#2e7d32` | Phase 3 — UX-Feinschliff |
|
||||
| `phase:4` | `#1565c0` | Phase 4 — Skills & Permissions |
|
||||
| `phase:5` | `#4527a0` | Phase 5 — Streaming |
|
||||
| `phase:6` | `#424242` | Phase 6+ — Zukunft / Platzhalter |
|
||||
| `type:feat` | `#0e8a16` | Neues Feature |
|
||||
| `type:fix` | `#d93f0b` | Bugfix |
|
||||
| `type:refactor` | `#5319e7` | Umstrukturierung ohne Verhaltensänderung |
|
||||
| `type:test` | `#006b75` | Tests oder Test-Infrastruktur |
|
||||
| `type:docs` | `#1d76db` | Dokumentation |
|
||||
| `type:chore` | `#bfbfbf` | Build, CI, Dependencies, Housekeeping |
|
||||
| `priority:p0` | `#b60205` | Blocker, muss sofort |
|
||||
| `priority:p1` | `#d93f0b` | Wichtig in aktueller Phase |
|
||||
| `priority:p2` | `#fbca04` | Kann in späteren Phasen warten |
|
||||
| `blocked` | `#000000` | Hängt an anderem Issue — Body nennt Blocker |
|
||||
| `needs-review` | `#fbca04` | PR wartet auf Review |
|
||||
| `security` | `#b60205` | Sicherheitsrelevant (CVE, Secret-Leak, Injection) |
|
||||
|
||||
**Begründung**: Phase-Label + Type-Label + Priority-Label sind orthogonal und genügen, um
|
||||
einen Backlog zu filtern. Zusätzliche Labels wie `good-first-issue`, `help-wanted`,
|
||||
`discussion` lehnen wir ab — sie sind bei einem Zwei-Agent-Setup (PM + Developer) wertlos
|
||||
und erzeugen Noise. `security` ist ein Sonderfall: separat, weil es automatisierte
|
||||
Eskalations-Policies erlauben soll (z. B. P0 + security → sofortige Zuweisung).
|
||||
|
||||
### 2.2 Milestones
|
||||
|
||||
Ein Milestone pro Phase. Titel und Description exakt aus `docs/development-plan.md` §4
|
||||
abgeleitet:
|
||||
|
||||
| Milestone | Titel | Description (Kurzform der Definition of Done) |
|
||||
|---|---|---|
|
||||
| M0 | Phase 0 — Härtung | Kein `shell: true`, sanitizedEnv, Path-Containment, `--bare` + `--append-system-prompt-file`, Workspace-Root außerhalb Repo, CLAUDE.md mit Injection-Klausel, settings.json mit Deny-Liste. Alle P0-Security-Issues geschlossen. |
|
||||
| M1 | Phase 1 — Solides MVP | Pino-Logger, ChannelQueue, Semaphore, `splitForDiscord`, Zod-Schemas, codeblock-aware Splitting, `.env.example`, `npm test` grün, keine `console.*` im Prod-Code. |
|
||||
| M2 | Phase 2 — Context-Effizienz | `claude_session_id` in DB, Runner nutzt `--resume`, Fork-Case abgefangen, Usage-Metriken geloggt, Session überlebt Bot-Neustart. |
|
||||
| M3 | Phase 3 — UX-Feinschliff | Attachment-Download, Reaction-Markers, `sendResponse` mit Attachment-Fallback, Typing-Refresh 8s. |
|
||||
| M4 | Phase 4 — Skills & Permissions | Profile-Registry (5 Profiles), PreToolUse-Hook `guard-tool.cjs`, Audit-Log, `profile`-Option in `/new-agent`. |
|
||||
| M5 | Phase 5 — Streaming | `runAgentStream`, NDJSON-Parser defensiv, `stream-to-discord` mit Rate-Limit, Feature-Flag in `disclaw.yaml`. |
|
||||
| M6 | Phase 6+ — Zukunft | `/list-agents`, `/delete-agent`, `/agent-config`, Rate-Limits, Budget, Agent-zu-Agent, Docker, Dashboard. Nur Platzhalter, wird bei Phase-Start verfeinert. |
|
||||
|
||||
Ein Milestone ist **closed**, sobald alle seine Issues `closed` sind und der Smoke-Test
|
||||
erfolgreich durchgeführt wurde.
|
||||
|
||||
### 2.3 Kanban-Board (Forgejo Project)
|
||||
|
||||
Ein einziges Projekt namens **DisClaw Delivery** mit fünf Spalten. Kein separates Board
|
||||
pro Phase — Phase wird über Label-Filter dargestellt.
|
||||
|
||||
| Spalte | Kriterium für Einzug |
|
||||
|---|---|
|
||||
| `Backlog` | Issue existiert, wurde vom PM geschnitten, hat Titel, Phase-Label, Type-Label, Milestone. Noch nicht priorisiert für aktuelle Phase. |
|
||||
| `Ready` | Issue hat vollständige Definition of Done, alle Dependencies aufgelöst (keine `blocked`-Labels), Priority gesetzt, für aktuelle Phase priorisiert. Darf sofort von einem Developer-Agent gezogen werden. |
|
||||
| `In Progress` | Ein Developer-Agent ist zugewiesen, ein Feature-Branch existiert und wurde nach `origin` gepusht (mindestens der erste Commit). |
|
||||
| `In Review` | PR ist geöffnet, `needs-review`-Label gesetzt, CI-Checks laufen oder sind grün. |
|
||||
| `Done` | PR ist gemerged, Issue ist geschlossen. Kein manuelles Verschieben — Forgejo erledigt das automatisch über `Closes #N` im PR-Body. |
|
||||
|
||||
**Regel**: Eine Karte wird rückwärts verschoben, wenn ein Kriterium verloren geht
|
||||
(z. B. PR wird geschlossen ohne Merge → zurück nach `In Progress`; neue Abhängigkeit
|
||||
wird entdeckt → zurück nach `Backlog` mit `blocked`-Label).
|
||||
|
||||
### 2.4 Bootstrap via Forgejo-API
|
||||
|
||||
Die folgenden Endpunkte setzen Labels, Milestones und das Projekt-Board auf. Alle Pfade
|
||||
sind **echte Forgejo-API-Pfade** (Forgejo spricht die Gitea-API v1 und deckt zusätzlich
|
||||
eigene Project-Routen ab). Das Bootstrap passiert per Script (siehe §2.5); **dieses
|
||||
Dokument erstellt nichts direkt**.
|
||||
|
||||
Umgebungsvariablen:
|
||||
```
|
||||
FORGEJO_BASE=http://localhost:3000
|
||||
FORGEJO_OWNER=dev
|
||||
FORGEJO_REPO=disclaw
|
||||
FORGEJO_TOKEN=<personal-access-token mit repo-scope>
|
||||
```
|
||||
|
||||
**Labels anlegen**: `POST /api/v1/repos/{owner}/{repo}/labels`
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"phase:0","color":"#b71c1c","description":"Phase 0 — Härtung"}' \
|
||||
"$FORGEJO_BASE/api/v1/repos/$FORGEJO_OWNER/$FORGEJO_REPO/labels"
|
||||
```
|
||||
|
||||
**Milestones anlegen**: `POST /api/v1/repos/{owner}/{repo}/milestones`
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"Phase 0 — Härtung","description":"Kein shell:true, sanitizedEnv, ..."}' \
|
||||
"$FORGEJO_BASE/api/v1/repos/$FORGEJO_OWNER/$FORGEJO_REPO/milestones"
|
||||
```
|
||||
|
||||
**Issues anlegen**: `POST /api/v1/repos/{owner}/{repo}/issues`
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"...","body":"...","labels":[1,5,12],"milestone":1}' \
|
||||
"$FORGEJO_BASE/api/v1/repos/$FORGEJO_OWNER/$FORGEJO_REPO/issues"
|
||||
```
|
||||
(Label- und Milestone-IDs kommen aus der Response der Create-Calls oben.)
|
||||
|
||||
**Branch-Protection für `main`**: `POST /api/v1/repos/{owner}/{repo}/branch_protections`
|
||||
```bash
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"branch_name":"main",
|
||||
"enable_push":false,
|
||||
"required_approvals":1,
|
||||
"enable_status_check":true,
|
||||
"status_check_contexts":["build","test","lint"],
|
||||
"block_on_rejected_reviews":true,
|
||||
"block_on_outdated_branch":true
|
||||
}' \
|
||||
"$FORGEJO_BASE/api/v1/repos/$FORGEJO_OWNER/$FORGEJO_REPO/branch_protections"
|
||||
```
|
||||
|
||||
**Projekte (Kanban-Board)**: Forgejo unterstützt Projekte pro Repo unter
|
||||
`POST /api/v1/repos/{owner}/{repo}/projects` mit `{"title":"DisClaw Delivery","board_type":"basic_kanban"}`.
|
||||
Spalten werden nachgelagert angelegt über `POST /api/v1/projects/{id}/columns` mit
|
||||
`{"title":"Ready"}` etc.
|
||||
|
||||
**Alternative**: `tea` CLI (`https://gitea.com/gitea/tea`). Die relevanten Kommandos:
|
||||
```bash
|
||||
tea labels create --name "phase:0" --color "#b71c1c" --description "Phase 0 — Härtung"
|
||||
tea milestones create --title "Phase 0 — Härtung" --description "..."
|
||||
tea issues create --title "..." --body "..." --labels "phase:0,type:fix,priority:p0" --milestone "Phase 0 — Härtung"
|
||||
```
|
||||
`tea` authentifiziert sich über `tea login add` einmalig mit `FORGEJO_TOKEN`.
|
||||
|
||||
### 2.5 Bootstrap-Script (skizziert, NICHT angelegt)
|
||||
|
||||
Ein späteres Script unter `scripts/bootstrap-forgejo.ts` soll:
|
||||
|
||||
1. `docs/backlog.md` parsen (jedes Issue als YAML-Frontmatter-Block oder strukturiertes
|
||||
Markdown).
|
||||
2. Labels anlegen (idempotent: vorher `GET /labels`, skip wenn vorhanden).
|
||||
3. Milestones anlegen (idempotent).
|
||||
4. Projekt + Spalten anlegen (idempotent).
|
||||
5. Branch-Protection setzen.
|
||||
6. Für jedes Backlog-Issue: `POST /issues`, `DIS-XXX` im Backlog-File durch die echte
|
||||
Issue-Nummer ersetzen.
|
||||
7. Jede Karte ins `Backlog`-Kanban-Board einhängen.
|
||||
|
||||
Abhängigkeiten: `node-fetch` (oder Node 20 native), `js-yaml`, nichts weiteres.
|
||||
Läuft einmalig, ist re-runnable (idempotent). **Legt dieses Dokument nicht an.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Issue-Template
|
||||
|
||||
Jedes Issue folgt exakt diesem Template. Abweichungen werden vom PM vor dem Create
|
||||
korrigiert. Das Template landet zusätzlich unter
|
||||
`.forgejo/ISSUE_TEMPLATE/feature.md` als Forgejo-Template.
|
||||
|
||||
```markdown
|
||||
## Ziel
|
||||
<1–2 Sätze, was erreicht werden soll.>
|
||||
|
||||
## Kontext
|
||||
Referenz: `docs/development-plan.md` §<Abschnitt>, ggf. `docs/ai-engineer-analyse.md` §<Abschnitt>.
|
||||
<Kurze Begründung, warum das jetzt gemacht wird und welches Problem es löst.>
|
||||
|
||||
## Scope
|
||||
- **In**: <konkret was gemacht wird>
|
||||
- **Out**: <konkret was NICHT gemacht wird — damit kein Scope-Creep>
|
||||
|
||||
## Definition of Done
|
||||
- [ ] <konkreter, überprüfbarer Punkt 1>
|
||||
- [ ] <konkreter, überprüfbarer Punkt 2>
|
||||
- [ ] Unit-Tests geschrieben und grün (`npm test`)
|
||||
- [ ] Keine `console.*` im Produktionscode (falls betroffen)
|
||||
- [ ] PR-Template ausgefüllt, Review bestanden, CI grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/<pfad1>` — <zweck>
|
||||
- `tests/<pfad2>` — <zweck>
|
||||
|
||||
## Branch
|
||||
`phase-<n>/<slug>`
|
||||
|
||||
## Abhängigkeiten
|
||||
- Blockiert durch: #<issue-nr> (falls vorhanden)
|
||||
- Blockiert: #<issue-nr> (falls vorhanden)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. PR-Template
|
||||
|
||||
Datei: `.forgejo/PULL_REQUEST_TEMPLATE.md` (Forgejo liest dieses Verzeichnis; Fallback
|
||||
`.gitea/PULL_REQUEST_TEMPLATE.md` wird ebenfalls erkannt).
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
<1–3 Sätze: was ändert dieser PR und warum.>
|
||||
|
||||
## Closes
|
||||
Closes #<issue-nr>
|
||||
|
||||
## Changes
|
||||
- <Bullet für jede relevante Datei-Gruppe>
|
||||
|
||||
## Test plan
|
||||
- [ ] `npm run build` läuft grün
|
||||
- [ ] `npm test` läuft grün
|
||||
- [ ] Manueller Smoke-Test: <falls sinnvoll; sonst "n/a">
|
||||
- [ ] Neue Dateien haben Unit-Tests (falls anwendbar)
|
||||
|
||||
## Screenshots / Logs
|
||||
<optional — Log-Snippets bei Runtime-Änderungen>
|
||||
|
||||
## Breaking Changes
|
||||
- [ ] Keine
|
||||
- [ ] Ja: <Beschreibung + Migrationshinweis>
|
||||
|
||||
## Reviewer-Checklist
|
||||
- [ ] Code-Diff passt zum Issue-Scope (kein Scope-Creep)
|
||||
- [ ] Definition of Done aus dem Issue ist erfüllt
|
||||
- [ ] Keine neuen `console.*`-Calls (außer in Tests/Probe-Scripts)
|
||||
- [ ] Keine Secrets, Tokens oder absoluten Pfade in Logs
|
||||
- [ ] `shell: false` bei jedem Spawn (falls Spawn betroffen)
|
||||
- [ ] Sicherheitsrelevanter Code hat mindestens einen negativen Test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Commit-Convention
|
||||
|
||||
**Conventional Commits** mit Phase-Scope. Format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<optional body>
|
||||
|
||||
<optional footer: Closes #N>
|
||||
```
|
||||
|
||||
**Types** (deckungsgleich mit `type:*`-Labels): `feat`, `fix`, `refactor`, `test`, `docs`,
|
||||
`chore`, `perf`, `build`, `ci`.
|
||||
|
||||
**Scope**: Phase oder Modul. Bevorzugt Phase bei Phase-spezifischen Changes
|
||||
(`feat(phase-1): add channel queue`), Modul bei Cross-Phase-Infrastruktur
|
||||
(`fix(runner): handle ENOENT on claude.cmd`).
|
||||
|
||||
**Beispiele**:
|
||||
```
|
||||
feat(phase-0): sanitize child env before spawning claude
|
||||
|
||||
Removes DISCORD_BOT_TOKEN and all TOKEN_/SECRET_/API_KEY_ variables
|
||||
from the environment passed to the claude child process. Adds an
|
||||
allow-only whitelist helper in src/runtime/env.ts.
|
||||
|
||||
Closes #3
|
||||
```
|
||||
|
||||
```
|
||||
fix(runner): resolve claude.cmd via cross-spawn on Windows
|
||||
|
||||
CVE-2024-27980 blocks direct .cmd spawning without shell:true on
|
||||
Node 20.12+. cross-spawn handles the wrapper resolution and arg
|
||||
quoting transparently.
|
||||
|
||||
Closes #2
|
||||
```
|
||||
|
||||
```
|
||||
refactor(router): extract ChannelQueue into runtime/channel-queue.ts
|
||||
```
|
||||
|
||||
**Regel**: Subject <= 72 Zeichen, imperativ, kein Punkt am Ende. Body erklärt das
|
||||
*Warum*, nicht das *Was* — der Diff zeigt bereits das Was.
|
||||
|
||||
**Squash-Merge**: Beim Merge in `main` wird der PR-Titel zum finalen Commit-Subject.
|
||||
Der PM achtet deshalb darauf, dass der PR-Titel Conventional-Commits-konform ist.
|
||||
|
||||
---
|
||||
|
||||
## 6. Developer-Agent-Workflow (Claude Sonnet 4.6)
|
||||
|
||||
Ein Developer-Agent arbeitet **genau ein Issue pro Branch/PR**. Nie Issue-Pooling.
|
||||
|
||||
### 6.1 Ablauf pro Issue
|
||||
|
||||
1. **Ticket ziehen**: Issue aus `Ready` nehmen, sich selbst zuweisen, Status auf
|
||||
`In Progress` setzen.
|
||||
2. **Branch erstellen**:
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout main
|
||||
git pull --ff-only
|
||||
git checkout -b phase-<n>/<slug>
|
||||
```
|
||||
3. **Implementieren**: Strikt entlang der Definition of Done. Keine zusätzlichen
|
||||
Änderungen außerhalb des `Dateien (erwartet)`-Blocks — wenn eine zusätzliche Datei
|
||||
nötig wird, zuerst das Issue kommentieren, nicht einfach drauflosschreiben.
|
||||
4. **Tests**: Für jede neue reine Funktion ein Unit-Test. Integration-Tests nur, wenn
|
||||
das Issue es explizit verlangt oder ein Mehrkomponenten-Flow betroffen ist.
|
||||
5. **Commits**: Kleine, thematisch fokussierte Commits mit Conventional-Format.
|
||||
Der finale Squash verdichtet sie ohnehin, aber die Branch-Historie hilft beim Review.
|
||||
6. **Lokale Vor-Checks**:
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
Beide müssen grün sein, bevor gepusht wird.
|
||||
7. **Pushen**: `git push -u origin phase-<n>/<slug>`.
|
||||
8. **PR öffnen**: Via `tea pulls create` oder Forgejo-Web-UI. PR-Template ausfüllen,
|
||||
`Closes #<n>` einfügen, Label `needs-review` setzen. Spalte `In Review`.
|
||||
9. **Self-Review-Checklist**: Reviewer-Checklist im PR-Body selbst durchgehen, bevor
|
||||
um ein Review gebeten wird. Saves a round-trip.
|
||||
10. **Auf Review warten**. Bei Change-Requests: Anpassungen als neue Commits (kein
|
||||
Force-Push) auf denselben Branch.
|
||||
11. **Nach Merge**: Branch wird automatisch gelöscht, Issue schließt automatisch über
|
||||
`Closes #N`.
|
||||
|
||||
### 6.2 Rolle des Projekt-/Git-Manager-Agents (dieser Agent)
|
||||
|
||||
- Schneidet Issues gemäß `docs/development-plan.md` und schreibt sie in `docs/backlog.md`.
|
||||
- Priorisiert innerhalb eines Milestones (`priority:p0` > `p1` > `p2`).
|
||||
- Weist Developer-Agenten zu (im Ein-Developer-Setup trivial, bei mehreren nach Load).
|
||||
- Koordiniert Reviews, triggert bei festhängenden PRs nach.
|
||||
- Stellt Phasen-Abschluss fest, führt den Smoke-Test durch (siehe §8).
|
||||
- Entscheidet über `blocked`-Labels und löst Blocker auf, indem fehlende Vor-Issues
|
||||
nachgeschnitten werden.
|
||||
- **Schreibt keinen Produktionscode**. Ausnahme: Bootstrap-Scripts und Dokumentation.
|
||||
|
||||
### 6.3 Rollback bei kaputtem PR
|
||||
|
||||
- **PR ist nach Merge kaputt**: `git revert <merge-sha>` auf einem neuen Branch
|
||||
`phase-<n>/revert-<slug>`, normaler PR-Flow. Niemals `git reset` auf `main`.
|
||||
- **PR ist vor Merge kaputt**: Branch bleibt offen, `In Review` → `In Progress`,
|
||||
Developer-Agent fixt auf demselben Branch. Kein Force-Push. Keine neuen Branches.
|
||||
- **Feature muss komplett zurückgezogen werden**: Issue wieder öffnen, `blocked`-Label,
|
||||
Begründung als Kommentar, Revert-PR mergen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Test-Gate pro PR
|
||||
|
||||
Jeder PR muss vor Merge folgende Gates passieren:
|
||||
|
||||
1. **Build**: `npm run build` — TypeScript-Compile ohne Fehler.
|
||||
2. **Unit-Tests**: `npm test` — Vitest-Suite grün.
|
||||
3. **Lint** (sobald Phase 0 Issue "Add ESLint + `shell:false`-Rule" gemerged ist):
|
||||
`npm run lint` grün.
|
||||
4. **Manueller Smoke-Test**: Nur bei Änderungen am Bot-Startup, Runner, oder
|
||||
Discord-Integration. Im PR-Template als separater Haken dokumentiert.
|
||||
|
||||
**CI** wird in Phase 0 als eigenes Issue aufgesetzt (siehe `docs/backlog.md` Issue
|
||||
`DIS-008`: Forgejo Actions Workflow mit `build`, `test`, `lint` auf `ubuntu-latest`
|
||||
und `windows-latest`).
|
||||
|
||||
**Lokal ausführbar**:
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
Ein fehlgeschlagener Test = kein Merge. Keine `--skip`-Flags, kein `xit`-Workaround.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phasen-Übergang
|
||||
|
||||
### 8.1 Definition "Phase abgeschlossen"
|
||||
|
||||
Eine Phase ist genau dann abgeschlossen, wenn **alle folgenden** Bedingungen gelten:
|
||||
|
||||
1. Alle Issues des Milestones sind im Status `Done` (PR gemerged).
|
||||
2. Der manuelle Smoke-Test für die Phase ist durchgelaufen (siehe §8.2).
|
||||
3. Der PM hat das Milestone in Forgejo manuell auf `closed` gesetzt.
|
||||
4. Der User hat die nächste Phase explizit freigegeben (siehe §8.3).
|
||||
|
||||
### 8.2 Smoke-Test pro Phase
|
||||
|
||||
Der PM führt einen kurzen End-to-End-Test auf dem aktuellen `main` durch. Mindest-Umfang:
|
||||
|
||||
- **Phase 0**: Bot startet, `/new-agent` erstellt Workspace unter `~/.disclaw/workspaces/`,
|
||||
`.claude/settings.json` enthält Deny-Liste, ein Test-Prompt mit Eval-Versuch
|
||||
(`echo $DISCORD_BOT_TOKEN`) kommt leer zurück.
|
||||
- **Phase 1**: Drei Messages schnell hintereinander im gleichen Channel werden in
|
||||
Reihenfolge beantwortet, Logs sind strukturiertes JSON, `splitForDiscord` schneidet
|
||||
einen langen Codeblock sauber auf.
|
||||
- **Phase 2**: Zweite Message im selben Channel zeigt `cache_read_input_tokens > 0` im Log.
|
||||
- **Phase 3**: Bild-Upload im Discord wird vom Agent beschrieben.
|
||||
- **Phase 4**: Agent mit `profile:researcher` kann nicht schreiben; Audit-Log enthält
|
||||
blockierten Tool-Call.
|
||||
- **Phase 5**: Lange Antwort erscheint inkrementell, finaler Stand stimmt.
|
||||
|
||||
Das konkrete Skript wird in Phase 1 als `scripts/smoke/<phase>.md`-Checkliste gepflegt.
|
||||
|
||||
### 8.3 Keine automatische Phasen-Progression
|
||||
|
||||
Der PM **startet die nächste Phase nicht automatisch**. Der User muss explizit
|
||||
freigeben ("Phase X ist fertig, starte Phase Y"). Ohne diese Freigabe bleibt der Backlog
|
||||
für die nächste Phase im Zustand `Backlog` (noch nicht `Ready`).
|
||||
|
||||
Grund: Phasen-Übergänge sind der natürliche Moment, um den Plan zu überprüfen, die
|
||||
verbleibenden Epic-Level-Issues der nächsten Phase zu verfeinern, und eventuell
|
||||
gelernte Erkenntnisse zurückzuspielen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Anhang — Konventionen zusammengefasst
|
||||
|
||||
| Artefakt | Ort | Format |
|
||||
|---|---|---|
|
||||
| Branch-Name | Git | `phase-<n>/<slug>` |
|
||||
| Commit-Message | Git | Conventional Commits mit Phase-Scope |
|
||||
| Issue | Forgejo | Template §3, Labels aus §2.1, Milestone aus §2.2 |
|
||||
| PR | Forgejo | Template §4, squash-merge, `Closes #N` im Body |
|
||||
| PR-Template-Datei | Repo | `.forgejo/PULL_REQUEST_TEMPLATE.md` |
|
||||
| Issue-Template-Datei | Repo | `.forgejo/ISSUE_TEMPLATE/feature.md` |
|
||||
| Backlog | Repo | `docs/backlog.md` (Source of Truth vor Forgejo-Sync) |
|
||||
| Workflow | Repo | `docs/workflow.md` (dieses Dokument) |
|
||||
| Bootstrap-Script | Repo | `scripts/bootstrap-forgejo.ts` (noch nicht angelegt) |
|
||||
372
scripts/bootstrap-forgejo.py
Normal file
372
scripts/bootstrap-forgejo.py
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Bootstrap Forgejo issues for DisClaw. Run once after labels+milestones exist."""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
|
||||
TOKEN = "43a3a524105f5ef6049393344cc4d01d3bc96b8b"
|
||||
BASE = "http://localhost:3000/api/v1/repos/dev/disclaw"
|
||||
HEADERS = {"Authorization": f"token {TOKEN}", "Content-Type": "application/json"}
|
||||
|
||||
# Label IDs (set by bootstrap-labels step):
|
||||
# phase:0=1, phase:1=2, type:feat=8, type:fix=9, type:refactor=10
|
||||
# type:test=11, type:docs=12, type:chore=13, type:ci=14
|
||||
# priority:p0=15, priority:p1=16, priority:p2=17, security=18, blocked=19
|
||||
|
||||
PHASE0_ISSUES = [
|
||||
{
|
||||
"title": "DIS-001: Workspace-Root nach ~/.disclaw/workspaces/ verschieben",
|
||||
"milestone": 1,
|
||||
"labels": [1, 8, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Workspaces standardmaessig unter `~/.disclaw/workspaces/<agent>/` anlegen -- ausserhalb des DisClaw-Repos. "
|
||||
"Verhindert CLAUDE.md-Walk-Up-Leak.\n\n"
|
||||
"## Kontext\n"
|
||||
"Claude Code walkt Parent-Directories hoch und konkateniert alle CLAUDE.md. "
|
||||
"Workspace unter `disclaw/workspaces/` erbt die DisClaw-Projekt-Identitaet.\n"
|
||||
"Referenz: `docs/development-plan.md` Abschnitt 2.9, `docs/cli-feature-answers.md` Frage 8\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `disclaw.yaml` Default aendern, Tilde-Expansion in `src/config/loader.ts`, "
|
||||
"`new-agent.ts` legt Workspace unter resolvierten Root an, Warning-Log wenn Root innerhalb Repo, README-Migrationshinweis\n"
|
||||
"**Out:** Automatische Migration, `--bare`-Flags (DIS-004)\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `disclaw.yaml` Default ist `workspaces_root: \"~/.disclaw/workspaces\"`\n"
|
||||
"- [ ] `src/config/loader.ts` expandiert `~` auf Linux/Mac/Windows korrekt\n"
|
||||
"- [ ] `new-agent` legt Workspace unter neuem Root an, DB speichert absoluten Pfad\n"
|
||||
"- [ ] Warning-Log wenn Root innerhalb Repo\n"
|
||||
"- [ ] Unit-Test `tests/unit/workspace-root-resolve.test.ts` gruen\n"
|
||||
"- [ ] README-Abschnitt 'Migration bestehender Workspaces'\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/workspace-root-home`\n\n"
|
||||
"## Abhaengigkeiten\nKeine."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-002: shell:false + cross-spawn Windows-Fix im Runner",
|
||||
"milestone": 1,
|
||||
"labels": [1, 9, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Command-Injection ueber Discord-Input eliminieren. `shell: true` aus `runner.ts` entfernen. "
|
||||
"Windows-`.cmd`-Wrapper korrekt aufloesen, CVE-2024-27980 umgehen.\n\n"
|
||||
"## Kontext\n"
|
||||
"`src/agent/runner.ts` nutzt aktuell `shell: true` -- Shell-Injection-Vektor. "
|
||||
"`spawn('claude', args, { shell: false })` schlaegt auf Windows fehl (nur `claude.cmd`).\n"
|
||||
"Referenz: `docs/development-plan.md` Abschnitt 2.2\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `cross-spawn` als Dependency, `src/runtime/resolve-claude.ts` mit `resolveClaude()`, "
|
||||
"`runner.ts`: `shell: false`, `windowsHide: true`, UTF-8-Encoding\n"
|
||||
"**Out:** `sanitizedEnv()` (DIS-003), `--bare`-Flags (DIS-004)\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Kein `shell: true` im gesamten `src/`-Baum\n"
|
||||
"- [ ] `cross-spawn` in `package.json`\n"
|
||||
"- [ ] `resolveClaude()` wirft klare Fehlermeldung wenn nicht auffindbar\n"
|
||||
"- [ ] Unit-Test `tests/unit/resolve-claude.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/harden-spawn`\n\n"
|
||||
"## Abhaengigkeiten\nKeine. **Empfohlener Startpunkt.**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-003: sanitizedEnv() -- Secrets aus Child-Env entfernen",
|
||||
"milestone": 1,
|
||||
"labels": [1, 9, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Kein Secret (DISCORD_BOT_TOKEN etc.) im Environment des `claude`-Kindprozesses. "
|
||||
"Heute exfiltrierbar via `echo $DISCORD_BOT_TOKEN`.\n\n"
|
||||
"## Kontext\n"
|
||||
"`runner.ts` vererbt aktuell `process.env` vollstaendig.\n"
|
||||
"Referenz: `docs/development-plan.md` Abschnitt 2.6\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `src/runtime/env.ts`: `sanitizedEnv(extra?)` entfernt Token/Secret-Pattern, "
|
||||
"setzt `CI=true`, `DISCLAW_AGENT=1`, optionale `env_blocklist` via `disclaw.yaml`, Runner nutzt `sanitizedEnv`\n"
|
||||
"**Out:** Whitelist-Modus\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `sanitizedEnv()` ist pur, keine Seiteneffekte\n"
|
||||
"- [ ] Unit-Test `tests/unit/sanitize-env.test.ts` deckt alle Pattern-Cases\n"
|
||||
"- [ ] Runner nutzt `sanitizedEnv` an allen Spawn-Stellen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/sanitize-env`\n\n"
|
||||
"## Abhaengigkeiten\nKeine. Kann parallel zu DIS-002 laufen."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-004: Runner mit --bare + --append-system-prompt-file aufrufen",
|
||||
"milestone": 1,
|
||||
"labels": [1, 8, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Jeder CLI-Call verwendet `--bare` (skippt CLAUDE.md-Walk-Up, Hooks, Auto-Memory, MCP) "
|
||||
"und injiziert Agenten-Identitaet explizit via `--append-system-prompt-file <workspace>/CLAUDE.md`.\n\n"
|
||||
"## Kontext\n"
|
||||
"Ohne `--bare` walkt Claude Code alle Parent-Dirs hoch.\n"
|
||||
"Referenz: `docs/development-plan.md` Abschnitt 2.9, `docs/cli-feature-answers.md` Frage 8\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `runner.ts` Args immer mit `--bare` + `--append-system-prompt-file <ws>/CLAUDE.md`, "
|
||||
"Integration-Test mit `tests/fixtures/fake-claude.mjs`\n"
|
||||
"**Out:** `--resume`-Flag (Phase 2), Streaming-Flags (Phase 5)\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Args enthalten `[\"--bare\", \"--append-system-prompt-file\", \"<ws>/CLAUDE.md\"]`\n"
|
||||
"- [ ] Integration-Test `tests/integration/no-parent-claude-md-leak.test.ts` gruen\n"
|
||||
"- [ ] Dokumentations-Kommentar im Runner warum `--bare` zwingend ist\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/bare-and-append-identity`\n\n"
|
||||
"## Abhaengigkeiten\nBlockiert durch: **#2 (DIS-002)**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-005: Path-Traversal-Check in /new-agent",
|
||||
"milestone": 1,
|
||||
"labels": [1, 9, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"`/new-agent name:<x>` akzeptiert keinen Namen, der aus dem Workspace-Root ausbricht. "
|
||||
"Defense in depth: Regex + `path.resolve`-Containment-Check.\n\n"
|
||||
"## Kontext\n"
|
||||
"Edge-Cases: `..`, Unicode-Homoglyphen, `.`, leere Strings, Windows-reserved Names.\n"
|
||||
"Referenz: `docs/development-plan.md` Phase 0 Task 4\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** Nach Regex in `new-agent.ts`: Containment-Check via `path.resolve`. Saubere User-Fehlermeldung in Discord.\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Containment-Check aktiv in `src/commands/new-agent.ts`\n"
|
||||
"- [ ] Unit-Test `tests/unit/path-traversal.test.ts` deckt `../etc`, `..`, `./foo`, `a/../b`, leerer String, `CON`, Unicode\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/path-traversal-check`\n\n"
|
||||
"## Abhaengigkeiten\nKeine."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-006: .claude/settings.json-Template haerten + CLAUDE.md Injection-Klausel",
|
||||
"milestone": 1,
|
||||
"labels": [1, 8, 15, 18],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Jeder neue Agent bekommt ein gehaertetes `.claude/settings.json` (Deny-Liste gegen `../**`, `.env`, "
|
||||
"gefaehrliche Bash-Muster) und eine `CLAUDE.md` mit Prompt-Injection-Resistenz-Klausel.\n\n"
|
||||
"## Kontext\n"
|
||||
"Schema bestaetigt via `docs/cli-feature-answers.md` Frage 3.\n"
|
||||
"Referenz: `docs/development-plan.md` Abschnitt 2.5\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `src/agent/identity.ts` Settings-Template mit vollstaendiger `permissions.deny`-Liste, "
|
||||
"`CLAUDE.md`-Template erhaelt Sicherheits-Abschnitt\n"
|
||||
"**Out:** PreToolUse-Hook-Script (Phase 4), Profile-spezifische Settings (Phase 4)\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Frisch erstellter Workspace enthaelt `settings.json` mit vollstaendiger Deny-Liste\n"
|
||||
"- [ ] `CLAUDE.md`-Template enthaelt Injection-Resistenz-Absatz\n"
|
||||
"- [ ] Unit-Test `tests/unit/identity-templates.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/harden-settings-and-claudemd`\n\n"
|
||||
"## Abhaengigkeiten\nKeine."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-007: Lock-File gegen Doppelstart",
|
||||
"milestone": 1,
|
||||
"labels": [1, 8, 16],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Zwei gleichzeitig laufende DisClaw-Instanzen auf derselben DB/Workspace-Root werden verhindert.\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `src/runtime/lockfile.ts`: `acquireLock(path)` legt `.disclaw.lock` mit PID+Timestamp an. "
|
||||
"Stale-Lock-Recovery via process signal check. Graceful Shutdown loescht Lock.\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Zweiter Startversuch bricht mit 'DisClaw bereits aktiv (PID X)' ab\n"
|
||||
"- [ ] Stale-Lock wird automatisch uebernommen\n"
|
||||
"- [ ] Unit-Test `tests/unit/lockfile.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-0/startup-lockfile`\n\n"
|
||||
"## Abhaengigkeiten\nBlockiert durch: **#1 (DIS-001)**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-008: Forgejo Actions CI (build, test, lint)",
|
||||
"milestone": 1,
|
||||
"labels": [1, 14, 16],
|
||||
"body": (
|
||||
"## Ziel\n"
|
||||
"Jeder PR gegen `main` triggert CI: `npm run build`, `npm test`, `npm run lint`. Matrix: Linux + Windows.\n\n"
|
||||
"## Scope\n"
|
||||
"**In:** `.forgejo/workflows/ci.yml` mit `pull_request`-Trigger, Matrix `ubuntu-latest`/`windows-latest`, "
|
||||
"ESLint-Basis-Config, `npm run lint`-Script\n"
|
||||
"**Out:** Deployment-Jobs, Coverage, Release-Automation\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] CI laeuft gruen auf einem Test-PR\n"
|
||||
"- [ ] Matrix deckt Linux + Windows ab\n"
|
||||
"- [ ] `npm run lint` laeuft lokal\n"
|
||||
"- [ ] README-Abschnitt 'CI' vorhanden\n\n"
|
||||
"## Branch\n`phase-0/ci-pipeline`\n\n"
|
||||
"## Abhaengigkeiten\nSollte nach DIS-002, DIS-003, DIS-005 kommen."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
PHASE1_ISSUES = [
|
||||
{
|
||||
"title": "DIS-101: Pino-Logger einfuehren, console.* ersetzen",
|
||||
"milestone": 2,
|
||||
"labels": [2, 10, 16],
|
||||
"body": (
|
||||
"## Ziel\nStrukturiertes JSON-Logging ueber `pino`. Child-Logger pro Komponente. Kein `console.*` mehr im Produktionscode.\n\n"
|
||||
"## Scope\n**In:** `src/runtime/logger.ts` mit `rootLogger` + `childLogger(component)`, "
|
||||
"alle `console.*` in `src/` ersetzen, `pino-pretty` in `devDependencies`\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `grep -r 'console\\.' src/` liefert keine Treffer\n"
|
||||
"- [ ] Pino-Child-Logger mit `component:'runner'` im Output\n"
|
||||
"- [ ] Unit-Test `tests/unit/logger.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/pino-logger`\n\n## Abhaengigkeiten\nPhase 0 abgeschlossen."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-102: sanitizeForDiscord -- Pfade aus Agent-Output entfernen",
|
||||
"milestone": 2,
|
||||
"labels": [2, 8, 16, 18],
|
||||
"body": (
|
||||
"## Ziel\nAbsolute Pfade, Home-Dir und Token-Patterns vor Discord-Send redaktieren. Verhindert Info-Leak.\n\n"
|
||||
"## Scope\n**In:** `src/runtime/sanitize.ts` mit `sanitizeForDiscord(text, { repoRoot, home })`, "
|
||||
"Router nutzt Sanitizer vor jedem `channel.send`\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Test `tests/unit/sanitize-for-discord.test.ts` gruen (Repo-Pfad, Home-Pfad, Token-Regex, keine False-Positives)\n"
|
||||
"- [ ] Router nutzt Sanitizer an allen Send-Stellen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/sanitize-for-discord`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-103: ChannelQueue -- Per-Channel-FIFO",
|
||||
"milestone": 2,
|
||||
"labels": [2, 8, 15],
|
||||
"body": (
|
||||
"## Ziel\nMehrere Nachrichten im selben Channel seriell abarbeiten. Parallele Channels bleiben parallel. Keine Race Conditions.\n\n"
|
||||
"## Scope\n**In:** `src/runtime/channel-queue.ts` mit `ChannelQueue`-Klasse, Integration in `router.ts`\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Unit-Test `tests/unit/channel-queue.test.ts`: FIFO, Error-Recovery, Map-Cleanup gruen\n"
|
||||
"- [ ] Integration-Test `tests/integration/channel-queue-router.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/channel-queue`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-104: Globaler Semaphore (max_concurrent_agents)",
|
||||
"milestone": 2,
|
||||
"labels": [2, 8, 16],
|
||||
"body": (
|
||||
"## Ziel\nGlobaler Cap auf parallele `claude`-Prozesse. Default 4, konfigurierbar via `disclaw.yaml`.\n\n"
|
||||
"## Scope\n**In:** `src/runtime/concurrency.ts` mit `Semaphore(max)`, Runner umschliesst Spawn-Call\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Unit-Test `tests/unit/semaphore.test.ts`: Acquire, Queueing, Release, Error-Path gruen\n"
|
||||
"- [ ] Config-Default 4 in `disclaw.yaml`\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/concurrency-semaphore`\n\n## Abhaengigkeiten\nKeine harten. Kann parallel zu DIS-103."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-105: splitForDiscord -- Codeblock-aware Message-Splitting",
|
||||
"milestone": 2,
|
||||
"labels": [2, 8, 15],
|
||||
"body": (
|
||||
"## Ziel\nAntworten > 1900 Zeichen korrekt aufteilen, ohne Codeblöcke zu zerreissen. Fences am Split schliessen und im naechsten Chunk wieder oeffnen.\n\n"
|
||||
"## Scope\n**In:** `src/discord/split.ts` mit `splitForDiscord(text, limit = 1900): string[]`, Router sendet Chunks sequenziell\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Unit-Test `tests/unit/split-for-discord.test.ts`: kurz/am Limit/Codeblock/Fence/leer gruen\n"
|
||||
"- [ ] Kein Chunk ueberschreitet 2000 Zeichen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/split-for-discord`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-106: Runner-API-Refactoring -- RunResult-Typ",
|
||||
"milestone": 2,
|
||||
"labels": [2, 10, 16],
|
||||
"body": (
|
||||
"## Ziel\n`runner.ts` gibt typsicheren `RunResult` zurueck (discriminated union: success/timeout/cli-error/parse-error).\n\n"
|
||||
"## Scope\n**In:** `src/agent/types.ts` mit `RunResult`, `runner.ts` gibt `RunResult`, alle Aufrufer handhaben alle Cases\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `RunResult` ist discriminated union, kein `any`\n"
|
||||
"- [ ] Unit-Test `tests/unit/runner-result.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/runner-api-runresult`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-107: Zod-Schemas fuer Config, agent.yaml, CLI-JSON-Output",
|
||||
"milestone": 2,
|
||||
"labels": [2, 8, 16],
|
||||
"body": (
|
||||
"## Ziel\nAlle externen Daten (Config, `agent.yaml`, CLI-Output) via Zod validieren. Fruehzeitige Fehlermeldungen.\n\n"
|
||||
"## Scope\n**In:** `src/config/schema.ts`, `src/agent/schema.ts`, `zod` in `dependencies`\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Alle externen Eingaben durch Zod-Parse\n"
|
||||
"- [ ] Unit-Test `tests/unit/schemas.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/zod-schemas`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-108: DB-Schema SSOT -- src/db/schema.sql",
|
||||
"milestone": 2,
|
||||
"labels": [2, 10, 16],
|
||||
"body": (
|
||||
"## Ziel\nSQLite-Schema in einer `schema.sql`-Datei (Single Source of Truth). `database.ts` liest und wendet sie an.\n\n"
|
||||
"## Scope\n**In:** `src/db/schema.sql` mit allen CREATE TABLE-Statements, `database.ts` ohne Inline-DDL\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `schema.sql` enthaelt alle Tabellen und Indizes\n"
|
||||
"- [ ] `database.ts` hat kein Inline-DDL mehr\n"
|
||||
"- [ ] Integration-Test `tests/integration/db-init.test.ts` gruen\n"
|
||||
"- [ ] `npm run build && npm test` gruen\n\n"
|
||||
"## Branch\n`phase-1/db-schema-ssot`\n\n## Abhaengigkeiten\nBlockiert durch: **DIS-101**"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-109: .env.example + Vitest-Setup + npm test-Skript",
|
||||
"milestone": 2,
|
||||
"labels": [2, 13, 16],
|
||||
"body": (
|
||||
"## Ziel\nNeues Repo-Clone laeuft sofort: `.env.example`, Vitest konfiguriert, `npm test` funktioniert ohne weitere Schritte.\n\n"
|
||||
"## Scope\n**In:** `.env.example`, `vitest.config.ts`, `package.json` Scripts, `tests/`-Verzeichnis-Struktur\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] `npm test` laeuft gruen auf frischem Clone (nach `npm ci`)\n"
|
||||
"- [ ] `.env.example` deckt alle Pflichtfelder ab\n"
|
||||
"- [ ] `vitest.config.ts` konsistent mit `tsconfig.json`\n\n"
|
||||
"## Branch\n`phase-1/vitest-setup`\n\n## Abhaengigkeiten\nKeine. Frueher in Phase 1 besser."
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "DIS-110: docs/cli-feature-probe.md -- Manuelles CLI-Regression-Script",
|
||||
"milestone": 2,
|
||||
"labels": [2, 12, 17],
|
||||
"body": (
|
||||
"## Ziel\nStrukturiertes Probe-Script verifiziert Annahmen aus `docs/cli-feature-answers.md` gegen die installierte CLI. Muss vor Phase-2-Start ausgefuehrt werden.\n\n"
|
||||
"## Scope\n**In:** `docs/cli-feature-probe.md` mit step-by-step Kommandos fuer alle 4 Rest-Unsicherheiten (--resume-Flow, session_id, stream-json-Events, usage-Felder)\n\n"
|
||||
"## Definition of Done\n"
|
||||
"- [ ] Probe-Dokument enthaelt ausfuehrbare Kommandos fuer alle Unsicherheiten\n"
|
||||
"- [ ] Dokument von Developer ausgefuehrt und Ergebnisse eingetragen\n\n"
|
||||
"## Branch\n`phase-1/cli-probe`\n\n## Abhaengigkeiten\nKeine. Muss vor Phase-2-Freigabe abgeschlossen sein."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
def create_issues(issues, phase_name):
|
||||
print(f"\n=== {phase_name} ===")
|
||||
# Get existing to avoid dupes
|
||||
r = requests.get(f"{BASE}/issues?type=issues&state=open&limit=50&page=1", headers=HEADERS)
|
||||
existing = {i["title"] for i in r.json()} if r.ok else set()
|
||||
r2 = requests.get(f"{BASE}/issues?type=issues&state=closed&limit=50&page=1", headers=HEADERS)
|
||||
existing |= {i["title"] for i in r2.json()} if r2.ok else set()
|
||||
|
||||
for issue in issues:
|
||||
if issue["title"] in existing:
|
||||
print(f" SKIP (exists): {issue['title'][:60]}")
|
||||
continue
|
||||
r = requests.post(f"{BASE}/issues", headers=HEADERS, json=issue)
|
||||
if r.ok:
|
||||
num = r.json()["number"]
|
||||
print(f" #{num}: {issue['title'][:70]}")
|
||||
else:
|
||||
print(f" ERR {r.status_code}: {issue['title'][:60]} -- {r.text[:80]}")
|
||||
|
||||
create_issues(PHASE0_ISSUES, "Phase 0")
|
||||
create_issues(PHASE1_ISSUES, "Phase 1")
|
||||
print("\nDone.")
|
||||
94
tasks/BOARD.md
Normal file
94
tasks/BOARD.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# DisClaw — Kanban Board
|
||||
|
||||
> **Regeln**: Status nur in der Issue-Datei (`tasks/DIS-XXX.md`) ändern, dann hier die Karte verschieben.
|
||||
> Developer-Agent nimmt eine "Ready"-Karte, setzt Status auf `in-progress`, erstellt Branch, öffnet PR.
|
||||
> PM-Agent (oder User) reviewt PR und setzt Status auf `done`.
|
||||
|
||||
Letzte Aktualisierung: 2026-04-08
|
||||
|
||||
---
|
||||
|
||||
## Backlog
|
||||
*Noch nicht startbar — Abhängigkeiten offen oder Phase noch nicht freigegeben.*
|
||||
|
||||
| ID | Titel | Phase | Branch | Blockiert durch |
|
||||
|----|-------|-------|--------|-----------------|
|
||||
| [DIS-004](DIS-004.md) | Runner: `--bare` + `--append-system-prompt-file` | 0 | `phase-0/bare-and-append-identity` | DIS-002 |
|
||||
| [DIS-007](DIS-007.md) | Lock-File gegen Doppelstart | 0 | `phase-0/startup-lockfile` | DIS-001 |
|
||||
| [DIS-008](DIS-008.md) | Forgejo Actions CI | 0 | `phase-0/ci-pipeline` | DIS-002, DIS-003, DIS-005 |
|
||||
| [DIS-101](DIS-101.md) | Pino-Logger einführen | 1 | `phase-1/pino-logger` | Phase 0 done |
|
||||
| [DIS-102](DIS-102.md) | `sanitizeForDiscord` — Pfade redaktieren | 1 | `phase-1/sanitize-for-discord` | DIS-101 |
|
||||
| [DIS-103](DIS-103.md) | `ChannelQueue` — Per-Channel-FIFO | 1 | `phase-1/channel-queue` | DIS-101 |
|
||||
| [DIS-104](DIS-104.md) | Globaler Semaphore (`max_concurrent_agents`) | 1 | `phase-1/concurrency-semaphore` | — |
|
||||
| [DIS-105](DIS-105.md) | `splitForDiscord` — Codeblock-aware Splitting | 1 | `phase-1/split-for-discord` | DIS-101 |
|
||||
| [DIS-106](DIS-106.md) | Runner-API-Refactoring → `RunResult`-Typ | 1 | `phase-1/runner-api-runresult` | DIS-101 |
|
||||
| [DIS-107](DIS-107.md) | Zod-Schemas für Config, `agent.yaml`, CLI-Output | 1 | `phase-1/zod-schemas` | DIS-101 |
|
||||
| [DIS-108](DIS-108.md) | DB-Schema SSOT → `src/db/schema.sql` | 1 | `phase-1/db-schema-ssot` | DIS-101 |
|
||||
| [DIS-109](DIS-109.md) | `.env.example` + Vitest-Setup + `npm test` | 1 | `phase-1/vitest-setup` | — |
|
||||
| [DIS-110](DIS-110.md) | `docs/cli-feature-probe.md` — CLI-Regression-Script | 1 | `phase-1/cli-probe` | — |
|
||||
|
||||
---
|
||||
|
||||
## Ready
|
||||
*Startbereit — kein Blocker. Agent kann Branch anlegen und loslegen.*
|
||||
*Empfohlene Startreihenfolge: DIS-002 → DIS-003 → DIS-001*
|
||||
|
||||
| ID | Titel | Phase | Branch | Priorität |
|
||||
|----|-------|-------|--------|-----------|
|
||||
| [DIS-002](DIS-002.md) | `shell: false` + `cross-spawn` Windows-Fix | 0 | `phase-0/harden-spawn` | p0 |
|
||||
| [DIS-003](DIS-003.md) | `sanitizedEnv()` — Secrets aus Child-Env entfernen | 0 | `phase-0/sanitize-env` | p0 |
|
||||
| [DIS-001](DIS-001.md) | Workspace-Root nach `~/.disclaw/workspaces/` | 0 | `phase-0/workspace-root-home` | p0 |
|
||||
| [DIS-005](DIS-005.md) | Path-Traversal-Check in `/new-agent` | 0 | `phase-0/path-traversal-check` | p0 |
|
||||
| [DIS-006](DIS-006.md) | `.claude/settings.json`-Template härten + Injection-Klausel | 0 | `phase-0/harden-settings-and-claudemd` | p0 |
|
||||
|
||||
---
|
||||
|
||||
## In Progress
|
||||
*Branch existiert, Developer-Agent arbeitet aktiv daran.*
|
||||
|
||||
| ID | Titel | Branch | Agent | Gestartet |
|
||||
|----|-------|--------|-------|-----------|
|
||||
| — | | | | |
|
||||
|
||||
---
|
||||
|
||||
## In Review
|
||||
*PR ist offen, wartet auf Review.*
|
||||
|
||||
| ID | Titel | PR | Reviewer |
|
||||
|----|-------|----|----------|
|
||||
| — | | | |
|
||||
|
||||
---
|
||||
|
||||
## Done
|
||||
*Gemergt in `main`.*
|
||||
|
||||
| ID | Titel | Gemergt | PR |
|
||||
|----|-------|---------|----|
|
||||
| — | | | |
|
||||
|
||||
---
|
||||
|
||||
## Epics (Phase 2–6+)
|
||||
*Werden am Phasen-Anfang in granulare Issues verfeinert.*
|
||||
|
||||
| Epic | Titel | Phase | Status |
|
||||
|------|-------|-------|--------|
|
||||
| [DIS-EP-201](EPICS.md#dis-ep-201) | DB-Migration + `SessionStore`-Methoden | 2 | locked |
|
||||
| [DIS-EP-202](EPICS.md#dis-ep-202) | Runner-Integration `--resume` mit Fallback | 2 | locked |
|
||||
| [DIS-EP-203](EPICS.md#dis-ep-203) | Usage-Metriken + `agent_runs`-Tabelle | 2 | locked |
|
||||
| [DIS-EP-204](EPICS.md#dis-ep-204) | Integration-Tests Session-Resume + Fork-Case | 2 | locked |
|
||||
| [DIS-EP-301](EPICS.md#dis-ep-301) | Typing-Refresh + `sendResponse`-Helper | 3 | locked |
|
||||
| [DIS-EP-302](EPICS.md#dis-ep-302) | Attachment-Inbox für Discord-Bilder | 3 | locked |
|
||||
| [DIS-EP-303](EPICS.md#dis-ep-303) | Input-Limit + User-Feedback-Reactions | 3 | locked |
|
||||
| [DIS-EP-304](EPICS.md#dis-ep-304) | Tests send-response + Attachment-Inbox | 3 | locked |
|
||||
| [DIS-EP-401](EPICS.md#dis-ep-401) | Profile-Registry-Grundgerüst | 4 | locked |
|
||||
| [DIS-EP-402](EPICS.md#dis-ep-402) | Fünf Profile (developer/researcher/writer/ops/sandboxed) | 4 | locked |
|
||||
| [DIS-EP-403](EPICS.md#dis-ep-403) | `/new-agent profile:<name>` Option | 4 | locked |
|
||||
| [DIS-EP-404](EPICS.md#dis-ep-404) | PreToolUse-Hook `guard-tool.cjs` + Audit-Log | 4 | locked |
|
||||
| [DIS-EP-405](EPICS.md#dis-ep-405) | Integration-Tests Hook-Guard + Profile-Permissions | 4 | locked |
|
||||
| [DIS-EP-501](EPICS.md#dis-ep-501) | `runAgentStream()` mit `stream-json` | 5 | locked |
|
||||
| [DIS-EP-502](EPICS.md#dis-ep-502) | Defensiver NDJSON-Event-Parser | 5 | locked |
|
||||
| [DIS-EP-503](EPICS.md#dis-ep-503) | `stream-to-discord` — Edit-Debouncing | 5 | locked |
|
||||
| [DIS-EP-504](EPICS.md#dis-ep-504) | Feature-Flag + Fallback + Integration-Tests | 5 | locked |
|
||||
49
tasks/DIS-001.md
Normal file
49
tasks/DIS-001.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
id: DIS-001
|
||||
status: ready
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:feat, priority:p0, security]
|
||||
branch: phase-0/workspace-root-home
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-001: Workspace-Root nach `~/.disclaw/workspaces/` verschieben
|
||||
|
||||
## Ziel
|
||||
Workspaces werden standardmäßig unter `~/.disclaw/workspaces/<agent>/` (Linux/Mac) bzw.
|
||||
`%USERPROFILE%\.disclaw\workspaces\<agent>\` (Windows) angelegt. Verhindert CLAUDE.md-Walk-Up-Leak.
|
||||
|
||||
## Kontext
|
||||
Claude Code walkt beim Start Parent-Directories hoch und konkateniert alle gefundenen `CLAUDE.md`.
|
||||
Liegt ein Workspace unter `disclaw/workspaces/`, erbt jeder Agent die DisClaw-Projekt-CLAUDE.md.
|
||||
→ `docs/development-plan.md` §2.9, `docs/cli-feature-answers.md` Frage 8.
|
||||
|
||||
## Scope
|
||||
- **In**: `disclaw.yaml` Default ändern · Tilde-Expansion in `src/config/loader.ts` · `new-agent.ts` legt Workspace unter resolvierten Root an · Warning-Log wenn Root innerhalb Repo · README-Migrationshinweis
|
||||
- **Out**: Automatische Migration bestehender Workspaces · `--bare`-Flags (DIS-004)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `disclaw.yaml` Default ist `workspaces_root: "~/.disclaw/workspaces"`
|
||||
- [ ] `src/config/loader.ts` expandiert `~` auf Linux/Mac/Windows korrekt
|
||||
- [ ] `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` grün
|
||||
- [ ] README-Abschnitt „Migration bestehender Workspaces" vorhanden
|
||||
- [ ] `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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-001
|
||||
47
tasks/DIS-002.md
Normal file
47
tasks/DIS-002.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
id: DIS-002
|
||||
status: ready
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:fix, priority:p0, security]
|
||||
branch: phase-0/harden-spawn
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-002: `shell: false` + `cross-spawn` Windows-Fix im Runner
|
||||
|
||||
## Ziel
|
||||
Command-Injection über Discord-User-Input eliminieren. `shell: true` aus `src/agent/runner.ts`
|
||||
entfernen, durch `cross-spawn` ersetzen. Windows-`.cmd`-Wrapper wird korrekt aufgelöst,
|
||||
CVE-2024-27980 umgangen.
|
||||
|
||||
## Kontext
|
||||
`src/agent/runner.ts` nutzt aktuell `shell: true` → jeder Discord-Input landet in der Shell-Interpretation.
|
||||
`spawn("claude", args, { shell: false })` schlägt auf Windows fehl (nur `claude.cmd` existiert).
|
||||
→ `docs/development-plan.md` §2.2, `docs/cli-feature-answers.md` Frage 9.
|
||||
|
||||
## Scope
|
||||
- **In**: `cross-spawn` + `@types/cross-spawn` als Dependencies · `src/runtime/resolve-claude.ts`: `resolveClaude()` liest `CLAUDE_PATH`, fällt auf `where`/`which` zurück, cached in-memory · `runner.ts`: `shell: false`, `windowsHide: true`, `stdio: pipe`, `setEncoding("utf8")`
|
||||
- **Out**: `sanitizedEnv()` (DIS-003) · `--bare`-Flags (DIS-004)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Kein `shell: true` mehr im gesamten `src/`-Baum (`grep -r "shell: true" src/` → leer)
|
||||
- [ ] `cross-spawn` in `package.json#dependencies`
|
||||
- [ ] `resolveClaude()` wirft klare Fehlermeldung wenn `claude` nicht auffindbar
|
||||
- [ ] Unit-Test `tests/unit/resolve-claude.test.ts` deckt: `.env`-Override, PATH-Fallback, 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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine. **Empfohlener Startpunkt — zuerst lösen.**
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-002
|
||||
44
tasks/DIS-003.md
Normal file
44
tasks/DIS-003.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
id: DIS-003
|
||||
status: ready
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:fix, priority:p0, security]
|
||||
branch: phase-0/sanitize-env
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-003: `sanitizedEnv()` — Secrets aus Child-Env entfernen
|
||||
|
||||
## Ziel
|
||||
Kein Secret (`DISCORD_BOT_TOKEN` etc.) darf im Environment des `claude`-Kindprozesses landen.
|
||||
Ein Agent kann heute per `echo $DISCORD_BOT_TOKEN` den Bot-Token exfiltrieren.
|
||||
|
||||
## Kontext
|
||||
`src/agent/runner.ts` vererbt aktuell `process.env` vollständig.
|
||||
→ `docs/development-plan.md` §2.6, `docs/ai-engineer-analyse.md` §4.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/env.ts`: `sanitizedEnv(extra?)` entfernt `DISCORD_BOT_TOKEN`, `DISCORD_CLIENT_SECRET`, Pattern `/^(SECRET_|TOKEN_|API_KEY_?)/i`, setzt `CI=true`, `DISCLAW_AGENT=1`, `DISCLAW_AGENT_NAME=<name>` · optionale `env_blocklist` via `disclaw.yaml` · `runner.ts` nutzt `sanitizedEnv` statt `process.env`
|
||||
- **Out**: Whitelist-statt-Blacklist-Modus
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `sanitizedEnv()` ist pur, keine Seiteneffekte
|
||||
- [ ] Unit-Test `tests/unit/sanitize-env.test.ts` deckt: Token entfernt, Pattern-Match, PATH/HOME bleiben, Extras überschreiben, `env_blocklist` 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`
|
||||
- `tests/unit/sanitize-env.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine. Kann parallel zu DIS-002 laufen (beide editieren `runner.ts` — bei Merge-Konflikt rebased DIS-003 auf DIS-002).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-003
|
||||
44
tasks/DIS-004.md
Normal file
44
tasks/DIS-004.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
id: DIS-004
|
||||
status: backlog
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:feat, priority:p0, security]
|
||||
branch: phase-0/bare-and-append-identity
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-004: Runner mit `--bare` + `--append-system-prompt-file` aufrufen
|
||||
|
||||
## Ziel
|
||||
Jeder CLI-Call verwendet `--bare` (skippt CLAUDE.md-Walk-Up, Hooks, Auto-Memory, MCP) und
|
||||
injiziert die Agenten-Identität explizit über `--append-system-prompt-file <workspace>/CLAUDE.md`.
|
||||
Parent-CLAUDE.md-Leaks werden strukturell unmöglich.
|
||||
|
||||
## Kontext
|
||||
Claude Code walkt ohne `--bare` alle Parent-Dirs hoch und konkateniert CLAUDE.md-Dateien.
|
||||
→ `docs/development-plan.md` §2.9, `docs/cli-feature-answers.md` Frage 8.
|
||||
|
||||
## Scope
|
||||
- **In**: `runner.ts` Args-Builder immer `--bare` + `--append-system-prompt-file <ws>/CLAUDE.md` · Integration-Test mit `tests/fixtures/fake-claude.mjs` der System-Prompt als JSON echoed
|
||||
- **Out**: `--resume`-Flag (Phase 2) · Streaming-Flags (Phase 5)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Args enthalten `["--bare", "--append-system-prompt-file", "<ws>/CLAUDE.md"]`
|
||||
- [ ] Integration-Test `tests/integration/no-parent-claude-md-leak.test.ts` verifiziert: nur Workspace-CLAUDE.md im System-Prompt, keine Parent-Leaks
|
||||
- [ ] 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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-002** (sauberes Spawn-Setup muss vorher stehen).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-004
|
||||
48
tasks/DIS-005.md
Normal file
48
tasks/DIS-005.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
id: DIS-005
|
||||
status: ready
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:fix, priority:p0, security]
|
||||
branch: phase-0/path-traversal-check
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-005: Path-Traversal-Check in `/new-agent`
|
||||
|
||||
## Ziel
|
||||
`/new-agent name:<x>` akzeptiert keinen Namen, der aus dem Workspace-Root ausbricht.
|
||||
Defense in depth: Regex-Validierung + expliziter `path.resolve`-Containment-Check.
|
||||
|
||||
## Kontext
|
||||
Aktuell gibt es Regex-Validierung in `src/commands/new-agent.ts`, aber keinen
|
||||
`path.resolve`-Containment-Check. Edge-Cases: `..`, Unicode-Homoglyphen, `.`, leere Strings, Windows-reserved Names.
|
||||
→ `docs/development-plan.md` Phase 0 Task 4.
|
||||
|
||||
## Scope
|
||||
- **In**: Nach Regex in `new-agent.ts`:
|
||||
```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.
|
||||
- **Out**: Unicode-Normalisierung über Regex hinaus
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Containment-Check ist aktiv in `src/commands/new-agent.ts`
|
||||
- [ ] Unit-Test `tests/unit/path-traversal.test.ts` deckt: `../etc`, `..`, `./foo`, `a/../b`, leerer String, `CON` (Windows-reserved), Unicode-Homoglyphen
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/commands/new-agent.ts`
|
||||
- `tests/unit/path-traversal.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine. Kann parallel laufen.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-005
|
||||
56
tasks/DIS-006.md
Normal file
56
tasks/DIS-006.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
id: DIS-006
|
||||
status: ready
|
||||
phase: 0
|
||||
priority: p0
|
||||
labels: [phase:0, type:feat, priority:p0, security]
|
||||
branch: phase-0/harden-settings-and-claudemd
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-006: `.claude/settings.json`-Template härten + `CLAUDE.md`-Injection-Klausel
|
||||
|
||||
## Ziel
|
||||
Jeder neu erstellte Agent bekommt ein gehärtetes `.claude/settings.json` (Deny-Liste gegen
|
||||
`../**`, `.env`, gefährliche Bash-Muster) und eine `CLAUDE.md` mit Prompt-Injection-Resistenz-Klausel.
|
||||
|
||||
## Kontext
|
||||
Schema bestätigt via `docs/cli-feature-answers.md` Frage 3.
|
||||
→ `docs/development-plan.md` §2.5.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/agent/identity.ts` Settings-Template:
|
||||
```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(* /etc/*)", "Bash(* ~/.ssh/*)"],
|
||||
"defaultMode": "acceptEdits"
|
||||
},
|
||||
"cleanupPeriodDays": 90
|
||||
}
|
||||
```
|
||||
CLAUDE.md-Template erhält Abschnitt "Sicherheit & Prompt-Injection": Anweisungen aus Nachrichten dürfen Permissions nicht überschreiben.
|
||||
- **Out**: PreToolUse-Hook-Script (Phase 4) · Profile-spezifische Settings (Phase 4)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Frisch erstellter Workspace enthält settings.json mit vollständiger Deny-Liste
|
||||
- [ ] `CLAUDE.md`-Template enthält den Injection-Resistenz-Absatz
|
||||
- [ ] Unit-Test `tests/unit/identity-templates.test.ts` parsed gerendertes JSON und verifiziert Deny-Einträge + CLAUDE.md-Satz
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/agent/identity.ts`
|
||||
- `tests/unit/identity-templates.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-006
|
||||
42
tasks/DIS-007.md
Normal file
42
tasks/DIS-007.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: DIS-007
|
||||
status: backlog
|
||||
phase: 0
|
||||
priority: p1
|
||||
labels: [phase:0, type:feat, priority:p1]
|
||||
branch: phase-0/startup-lockfile
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-007: Lock-File gegen Doppelstart
|
||||
|
||||
## Ziel
|
||||
Zwei gleichzeitig laufende DisClaw-Instanzen auf derselben DB/Workspace-Root werden
|
||||
verhindert. Zweiter Start bricht mit klarer Fehlermeldung ab.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §7 (Risiko-Tabelle).
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/lockfile.ts`: `acquireLock(path)` legt `<workspaces_root>/.disclaw.lock` mit PID + ISO-Timestamp an. Stale-Lock-Recovery via `process.kill(pid, 0)`. · `src/index.ts` ruft `acquireLock` vor DB-Init auf · Graceful Shutdown löscht Lock
|
||||
- **Out**: Distributed Locking für Multi-Host
|
||||
|
||||
## 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: Acquire, Doppel-Acquire, Stale-Recovery, Release
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/runtime/lockfile.ts`
|
||||
- `src/index.ts`
|
||||
- `tests/unit/lockfile.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-001** (Lock liegt unter finalem `workspaces_root`).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-007
|
||||
45
tasks/DIS-008.md
Normal file
45
tasks/DIS-008.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
id: DIS-008
|
||||
status: backlog
|
||||
phase: 0
|
||||
priority: p1
|
||||
labels: [phase:0, type:ci, priority:p1]
|
||||
branch: phase-0/ci-pipeline
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-008: Forgejo Actions CI (build, test, lint)
|
||||
|
||||
## Ziel
|
||||
Jeder PR gegen `main` triggert eine CI-Pipeline: `npm run build`, `npm test`, `npm run lint`.
|
||||
Matrix: Linux + Windows. Ohne grüne Checks kein Merge.
|
||||
|
||||
## Kontext
|
||||
Forgejo Actions ist API-kompatibel mit GitHub Actions.
|
||||
→ `docs/development-plan.md` Phase 0 Task 8.
|
||||
|
||||
## Scope
|
||||
- **In**: `.forgejo/workflows/ci.yml` mit `pull_request`-Trigger, Matrix `ubuntu-latest`/`windows-latest`, Steps: checkout → setup-node 20 → `npm ci` → build → test · ESLint-Basis-Config + `no-shell-true`-Guard · `npm run lint`-Script in `package.json`
|
||||
- **Out**: Deployment-Jobs, 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` läuft lokal
|
||||
- [ ] README-Abschnitt „CI" vorhanden
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `.forgejo/workflows/ci.yml`
|
||||
- `.eslintrc.cjs`
|
||||
- `package.json`
|
||||
- `README.md`
|
||||
|
||||
## Abhängigkeiten
|
||||
Sollte nach DIS-002, DIS-003, DIS-005 kommen (Lint-Rules greifen auf sauberer Basis).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-008
|
||||
42
tasks/DIS-101.md
Normal file
42
tasks/DIS-101.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: DIS-101
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:refactor, priority:p1]
|
||||
branch: phase-1/pino-logger
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-101: Pino-Logger einführen, `console.*` ersetzen
|
||||
|
||||
## Ziel
|
||||
Strukturiertes JSON-Logging über `pino`. Child-Logger pro Komponente. Kein `console.*` mehr im Produktionscode.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2.7, Phase 1 Task 1.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/logger.ts` mit `rootLogger` + `childLogger(component)` · alle `console.*` in `src/index.ts`, `bot.ts`, `router.ts`, `runner.ts`, `new-agent.ts` ersetzen · `pino-pretty` in `devDependencies` als Pretty-Transport
|
||||
- **Out**: Log-Sampling, Log-Level-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 `component`-Key
|
||||
- [ ] `npm run build && 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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Phase 0 abgeschlossen (alle DIS-00x gemergt).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-101
|
||||
41
tasks/DIS-102.md
Normal file
41
tasks/DIS-102.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
id: DIS-102
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:feat, priority:p1, security]
|
||||
branch: phase-1/sanitize-for-discord
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-102: `sanitizeForDiscord` — Pfade aus Agent-Output entfernen
|
||||
|
||||
## Ziel
|
||||
Absolute Pfade, Home-Dir und Token-Patterns werden vor Discord-Send aus Agent-Antworten redaktiert.
|
||||
Verhindert Info-Leak in öffentliche Channels.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2.7 (Error-Handler-Strategie), Phase 1 Task 2.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/sanitize.ts` mit `sanitizeForDiscord(text, { repoRoot, home })` · Regex ersetzt repoRoot → `<disclaw>`, home → `~`, Token-Patterns → `<redacted>` · Router nutzt Sanitizer 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 run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/runtime/sanitize.ts`
|
||||
- `src/router.ts`
|
||||
- `tests/unit/sanitize-for-discord.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101** (gemeinsames Refactoring-Fenster im Router).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-102
|
||||
42
tasks/DIS-103.md
Normal file
42
tasks/DIS-103.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: DIS-103
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p0
|
||||
labels: [phase:1, type:feat, priority:p0]
|
||||
branch: phase-1/channel-queue
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-103: `ChannelQueue` — Per-Channel-FIFO
|
||||
|
||||
## Ziel
|
||||
Mehrere Nachrichten im selben Channel werden strikt seriell abgearbeitet. Parallele Channels
|
||||
bleiben parallel. Keine Race Conditions auf Workspace-Dateien.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2.3, Phase 1 Task 3.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/channel-queue.ts` mit `ChannelQueue`-Klasse · Integration in `router.ts`: `messageCreate` läuft durch `channelQueue.enqueue(channelId, () => runAgent(...))`
|
||||
- **Out**: Persistente Queue über Prozessneustart
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Unit-Test `tests/unit/channel-queue.test.ts`: FIFO-Ordnung, Error-Recovery (fehlgeschlagene Task blockiert Queue nicht), Map-Cleanup nach leerer Queue
|
||||
- [ ] Integration-Test `tests/integration/channel-queue-router.test.ts`: drei schnell enqueued Messages werden in Reihenfolge abgearbeitet
|
||||
- [ ] `npm run build && 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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101**.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-103
|
||||
42
tasks/DIS-104.md
Normal file
42
tasks/DIS-104.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: DIS-104
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:feat, priority:p1]
|
||||
branch: phase-1/concurrency-semaphore
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-104: Globaler Semaphore (`max_concurrent_agents`)
|
||||
|
||||
## Ziel
|
||||
Globaler Cap auf parallele `claude`-Prozesse verhindert Ressourcen-Thrashing. Default 4, konfigurierbar via `disclaw.yaml`.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2.3, Phase 1 Task 4.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/runtime/concurrency.ts` mit `Semaphore(max)` (`acquire()/release()`) · `runner.ts` umschließt Spawn-Call: `semaphore.acquire()` / `try { ... } finally { semaphore.release() }` · Config-Default 4 in `disclaw.yaml` kommentiert
|
||||
- **Out**: Dynamisches Auto-Scaling
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Unit-Test `tests/unit/semaphore.test.ts`: Acquire bei freien Slots, Queueing bei vollen Slots, Release gibt Wartende frei, Error-Path hält Counter konsistent
|
||||
- [ ] Config-Default 4 in `disclaw.yaml`
|
||||
- [ ] `npm run build && 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`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine harten. Kann parallel zu DIS-103 laufen.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-104
|
||||
41
tasks/DIS-105.md
Normal file
41
tasks/DIS-105.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
id: DIS-105
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p0
|
||||
labels: [phase:1, type:feat, priority:p0]
|
||||
branch: phase-1/split-for-discord
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-105: `splitForDiscord` — Codeblock-aware Message-Splitting
|
||||
|
||||
## 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
|
||||
→ `docs/development-plan.md` §2.4, Phase 1 Task 5.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/discord/split.ts` mit `splitForDiscord(text, limit = 1900): string[]` · Fence-Erkennung (` ``` `), Sprach-Tag wird im neuen Chunk beibehalten · Router nutzt Funktion und sendet Chunks sequenziell
|
||||
- **Out**: Streaming-Edit (Phase 5)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Unit-Test `tests/unit/split-for-discord.test.ts` deckt: kurze Nachricht (kein Split), genau am Limit, Codeblock mitten im Split, verschachtelter Fence, leerer Input
|
||||
- [ ] Kein Chunk überschreitet 2000 Zeichen
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/discord/split.ts`
|
||||
- `src/router.ts`
|
||||
- `tests/unit/split-for-discord.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101**.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-105
|
||||
43
tasks/DIS-106.md
Normal file
43
tasks/DIS-106.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
id: DIS-106
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:refactor, priority:p1]
|
||||
branch: phase-1/runner-api-runresult
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-106: Runner-API-Refactoring → `RunResult`-Typ
|
||||
|
||||
## Ziel
|
||||
`runner.ts` gibt einen typsicheren `RunResult`-Typ zurück statt rohem JSON/String.
|
||||
Ermöglicht saubere Fehlerunterscheidung (Timeout, CLI-Error, Parse-Error, Success).
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2 (Runner-Refactoring), Phase 1 Task 6.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/agent/types.ts` mit `RunResult` (discriminated union: `success | timeout | cli-error | parse-error`) · `runner.ts` gibt `RunResult` zurück · `router.ts` und alle Aufrufer nutzen den Typ
|
||||
- **Out**: Streaming-Variante (Phase 5)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `RunResult` ist ein discriminated union, kein `any`
|
||||
- [ ] Alle Aufrufer handhaben alle vier Cases explizit (TypeScript enforcement)
|
||||
- [ ] Unit-Test `tests/unit/runner-result.test.ts` mit Mock-Spawn für jeden Case
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/agent/types.ts`
|
||||
- `src/agent/runner.ts`
|
||||
- `src/router.ts`
|
||||
- `tests/unit/runner-result.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101** (Logger-Refactoring muss vorher stehen).
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-106
|
||||
45
tasks/DIS-107.md
Normal file
45
tasks/DIS-107.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
id: DIS-107
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:feat, priority:p1]
|
||||
branch: phase-1/zod-schemas
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-107: Zod-Schemas für Config, `agent.yaml`, CLI-JSON-Output
|
||||
|
||||
## Ziel
|
||||
Alle externen Daten (Config-Datei, `agent.yaml`, Claude-CLI-JSON-Output) werden via Zod validiert.
|
||||
Frühzeitige, verständliche Fehlermeldungen bei fehlerhafter Konfiguration.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §3 (Zod als Dependency), Phase 1 Task 7.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/config/schema.ts` (DisClaw-Config-Schema) · `src/agent/schema.ts` (`agent.yaml` + CLI-Output-Schema) · `src/config/loader.ts` + `identity.ts` nutzen Schemas · `zod` in `dependencies`
|
||||
- **Out**: Zod-Transforms für DB-Queries (separate Issue)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Alle externen Dateneingaben gehen durch Zod-Parse
|
||||
- [ ] Ungültige `disclaw.yaml` → verständliche Fehlermeldung mit Feldname und erwartetem Typ
|
||||
- [ ] Unit-Test `tests/unit/schemas.test.ts` für jeden Schema-Typ (valid + invalid cases)
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/config/schema.ts`
|
||||
- `src/agent/schema.ts`
|
||||
- `src/config/loader.ts`
|
||||
- `src/agent/identity.ts`
|
||||
- `package.json`
|
||||
- `tests/unit/schemas.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101**.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-107
|
||||
42
tasks/DIS-108.md
Normal file
42
tasks/DIS-108.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: DIS-108
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:refactor, priority:p1]
|
||||
branch: phase-1/db-schema-ssot
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-108: DB-Schema SSOT → `src/db/schema.sql`
|
||||
|
||||
## Ziel
|
||||
Das SQLite-Schema ist in einer einzigen `schema.sql`-Datei definiert (Single Source of Truth).
|
||||
`database.ts` liest und führt die Datei aus statt SQL inline zu haben.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2 (DB-Architektur), Phase 1 Task 8.
|
||||
|
||||
## Scope
|
||||
- **In**: `src/db/schema.sql` mit allen `CREATE TABLE`-Statements (inkl. `conversations`-Index) · `database.ts` liest Schema via `fs.readFileSync` und wendet es über `db.run()` auf die SQLite-Instanz an · Bestehende Inline-SQL entfernen
|
||||
- **Out**: DB-Migrations-System (kommt mit Phase 2 `session_id`-Spalte als erstes Delta)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `schema.sql` enthält alle Tabellen und Indizes
|
||||
- [ ] `database.ts` hat kein Inline-DDL mehr
|
||||
- [ ] Integration-Test `tests/integration/db-init.test.ts` mit tmp-SQLite verifiziert: Tabellen existieren, Indizes greifen, zweites Ausführen ist idempotent
|
||||
- [ ] `npm run build && npm test` grün
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `src/db/schema.sql`
|
||||
- `src/db/database.ts`
|
||||
- `tests/integration/db-init.test.ts`
|
||||
|
||||
## Abhängigkeiten
|
||||
Blockiert durch: **DIS-101**.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-108
|
||||
45
tasks/DIS-109.md
Normal file
45
tasks/DIS-109.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
id: DIS-109
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p1
|
||||
labels: [phase:1, type:chore, priority:p1]
|
||||
branch: phase-1/vitest-setup
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-109: `.env.example` + Vitest-Setup + `npm test`-Skript
|
||||
|
||||
## Ziel
|
||||
Neues Repo-Clone kommt sofort zum Laufen: `.env.example` zeigt alle Pflichtfelder,
|
||||
Vitest ist konfiguriert, `npm test` läuft ohne weitere Schritte.
|
||||
|
||||
## Kontext
|
||||
→ `docs/development-plan.md` §2.8 (Test-Strategie), Phase 1 Task 9.
|
||||
|
||||
## Scope
|
||||
- **In**: `.env.example` mit allen Feldern aus `src/config/loader.ts` (keine echten Werte) · `vitest.config.ts` mit `testEnvironment: "node"`, Coverage-Provider `v8`, Alias-Pfade · `package.json` `test`/`test:watch`/`test:coverage`-Scripts · `tests/` Verzeichnis-Struktur (`unit/`, `integration/`, `fixtures/`)
|
||||
- **Out**: Browser-Tests, E2E-Tests
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `npm test` läuft grün auf frischem Clone (nach `npm ci`)
|
||||
- [ ] `.env.example` deckt alle Pflichtfelder ab
|
||||
- [ ] `vitest.config.ts` vorhanden und konsistent mit `tsconfig.json`
|
||||
- [ ] Test-Verzeichnis-Struktur steht
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `.env.example`
|
||||
- `vitest.config.ts`
|
||||
- `package.json`
|
||||
- `tests/unit/.gitkeep`
|
||||
- `tests/integration/.gitkeep`
|
||||
- `tests/fixtures/.gitkeep`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine harten. Sollte früh in Phase 1 kommen, damit andere Tests darauf aufbauen können.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-109
|
||||
41
tasks/DIS-110.md
Normal file
41
tasks/DIS-110.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
id: DIS-110
|
||||
status: backlog
|
||||
phase: 1
|
||||
priority: p2
|
||||
labels: [phase:1, type:docs, priority:p2]
|
||||
branch: phase-1/cli-probe
|
||||
assignee: null
|
||||
started: null
|
||||
pr: null
|
||||
merged: null
|
||||
---
|
||||
|
||||
# DIS-110: `docs/cli-feature-probe.md` — Manuelles CLI-Regression-Script
|
||||
|
||||
## Ziel
|
||||
Ein strukturiertes, manuell ausführbares Probe-Script dokumentiert und verifiziert die
|
||||
in `docs/cli-feature-answers.md` formulierten Annahmen gegen die tatsächlich installierte CLI.
|
||||
Besonders kritisch: `--resume`-Verhalten und `session_id`-Pfad im JSON-Output.
|
||||
|
||||
## Kontext
|
||||
→ `docs/cli-feature-answers.md` (4 Rest-Unsicherheiten) · `docs/development-plan.md` §6.
|
||||
Muss vor Phase 2-Start ausgeführt und Ergebnisse dokumentiert werden.
|
||||
|
||||
## Scope
|
||||
- **In**: `docs/cli-feature-probe.md` mit step-by-step Kommandos, erwarteter vs. tatsächlicher Output-Template, Checkboxen für jede Annahme · Deckt: `--resume`-Flow, `session_id` Top-Level-Feld, `stream-json`-Event-Typen, `usage`-Felder, Linux-Session-Slug
|
||||
- **Out**: Automatisiertes Test-Script (overkill für diese Aufgabe)
|
||||
|
||||
## Definition of Done
|
||||
- [ ] `docs/cli-feature-probe.md` enthält ausführbare Kommandos für alle 4 Rest-Unsicherheiten
|
||||
- [ ] Jede Annahme hat ein "Expected" und "Actual"-Feld zum Ausfüllen
|
||||
- [ ] Dokument ist von einem Developer ausgeführt und Ergebnisse eingetragen (manuell verifiziert vor Phase 2 Start)
|
||||
|
||||
## Dateien (erwartet)
|
||||
- `docs/cli-feature-probe.md`
|
||||
|
||||
## Abhängigkeiten
|
||||
Keine. Kann jederzeit geschrieben werden, muss vor Phase-2-Freigabe ausgeführt sein.
|
||||
|
||||
## Spec
|
||||
→ `docs/backlog.md` #DIS-110
|
||||
131
tasks/EPICS.md
Normal file
131
tasks/EPICS.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# DisClaw — Epics (Phase 2–6+)
|
||||
|
||||
> Diese Epics werden am **Anfang jeder Phase** in granulare Issues verfeinert (analog zu Phase 0/1 im Backlog).
|
||||
> Bis dahin ist der Status `locked` — kein Branch, keine Entwicklung.
|
||||
> Freigabe durch explizite Entscheidung des Users nach Phasen-Abschluss.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Context-Effizienz (`--resume`)
|
||||
|
||||
Voraussetzung: Phase 0 + Phase 1 vollständig gemergt. `docs/cli-feature-probe.md` ausgeführt und `--resume`-Verhalten bestätigt.
|
||||
|
||||
### DIS-EP-201
|
||||
**DB-Migration + `SessionStore`-Methoden**
|
||||
Neue Spalte `session_id` in `workspaces`-Tabelle. `SessionStore`-Klasse mit `get/set/clear`-Methoden.
|
||||
Branch-Pattern wenn verfeinert: `phase-2/session-store`
|
||||
|
||||
### DIS-EP-202
|
||||
**Runner-Integration `--resume` mit DB-Fallback**
|
||||
`runner.ts` hängt `--resume <id>` an wenn `session_id` vorhanden. Fallback: strukturierte DB-Rehydrierung mit role-getaggten Turns. ID wird nach jedem Run aus JSON top-level gelesen und in DB aktualisiert.
|
||||
Branch-Pattern: `phase-2/runner-resume`
|
||||
|
||||
### DIS-EP-203
|
||||
**Usage-Metriken Logging + `agent_runs`-Tabelle**
|
||||
Cache-Hit-Statistiken aus CLI-Output extrahieren (falls verfügbar). `agent_runs`-Tabelle für Token-Tracking.
|
||||
Branch-Pattern: `phase-2/usage-metrics`
|
||||
|
||||
### DIS-EP-204
|
||||
**Integration-Tests Session-Resume + Fork-Case**
|
||||
Tests die `--resume`-Flow end-to-end verifizieren: erfolgreicher Resume, Session nicht gefunden (Fallback), Fork-Case (neue ID nach Resume).
|
||||
Branch-Pattern: `phase-2/session-tests`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — UX-Feinschliff
|
||||
|
||||
Voraussetzung: Phase 2 abgeschlossen.
|
||||
|
||||
### DIS-EP-301
|
||||
**Typing-Refresh 8s + `sendResponse`-Helper + Attachment-Fallback**
|
||||
Bot zeigt „tippt…" an solange Claude arbeitet, refresht alle 8s. `sendResponse(channel, text)`-Helper kapselt Split + Sanitize + Send.
|
||||
Branch-Pattern: `phase-3/typing-send-helper`
|
||||
|
||||
### DIS-EP-302
|
||||
**Attachment-Inbox für eingehende Discord-Bilder**
|
||||
Bilder aus Discord-Attachments werden in einen tmp-Ordner des Workspace-Agents gespeichert und via `@path/to/file.png`-Syntax in den Prompt injiziert.
|
||||
Branch-Pattern: `phase-3/attachment-inbox`
|
||||
|
||||
### DIS-EP-303
|
||||
**Input-Limit + User-Feedback-Reactions**
|
||||
Nachrichten > konfigurierbares Limit werden abgewiesen. Reaction-Feedback: 👀 (verarbeitung), ✅ (erfolg), ❌ (fehler), ⚠️ (timeout).
|
||||
Branch-Pattern: `phase-3/input-limit-reactions`
|
||||
|
||||
### DIS-EP-304
|
||||
**Unit-Tests `sendResponse` + Attachment-Inbox**
|
||||
Vollständige Unit-Test-Abdeckung für Phase-3-Komponenten.
|
||||
Branch-Pattern: `phase-3/ux-tests`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Skills und Permissions (Profile)
|
||||
|
||||
Voraussetzung: Phase 3 abgeschlossen.
|
||||
|
||||
### DIS-EP-401
|
||||
**Profile-Registry-Grundgerüst**
|
||||
`src/agent/profiles/registry.ts` mit `ProfileRegistry`-Interface und Lade-Mechanismus aus `src/agent/profiles/*.ts`.
|
||||
Branch-Pattern: `phase-4/profile-registry`
|
||||
|
||||
### DIS-EP-402
|
||||
**Fünf Profile: developer, researcher, writer, ops, sandboxed**
|
||||
Konkrete Profile mit je angepasstem `.claude/settings.json`-Template und CLAUDE.md-Abschnitt.
|
||||
Branch-Pattern: `phase-4/profiles-builtin`
|
||||
|
||||
### DIS-EP-403
|
||||
**`/new-agent profile:<name>` Option**
|
||||
`/new-agent name:x role:y profile:developer` nutzt das Profile-Template statt des Default.
|
||||
Branch-Pattern: `phase-4/new-agent-profile-option`
|
||||
|
||||
### DIS-EP-404
|
||||
**PreToolUse-Hook `guard-tool.cjs` + Audit-Log**
|
||||
Node.js-Script das als Hook vor Tool-Calls ausgeführt wird. Prüft gegen Deny-Liste, schreibt Audit-Log.
|
||||
Branch-Pattern: `phase-4/pretooluse-hook`
|
||||
|
||||
### DIS-EP-405
|
||||
**Integration-Tests Hook-Guard + Profile-Permissions**
|
||||
Tests für Hook-Script und Profile-Settings-Rendering.
|
||||
Branch-Pattern: `phase-4/profile-tests`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Streaming
|
||||
|
||||
Voraussetzung: Phase 4 abgeschlossen.
|
||||
|
||||
### DIS-EP-501
|
||||
**`runAgentStream()` mit `--output-format stream-json`**
|
||||
Parallele Runner-Implementierung die NDJSON-Stream vom CLI konsumiert.
|
||||
Flags: `--verbose --include-partial-messages`.
|
||||
Branch-Pattern: `phase-5/runner-stream`
|
||||
|
||||
### DIS-EP-502
|
||||
**Defensiver NDJSON-Event-Parser**
|
||||
`src/agent/stream-parser.ts` mit versioniertem Event-Dispatch, defensivem Parsing, Fallback bei unbekannten Event-Typen.
|
||||
Branch-Pattern: `phase-5/stream-parser`
|
||||
|
||||
### DIS-EP-503
|
||||
**`stream-to-discord` — Edit-Debouncing 1200ms, Rollover bei 1900 Zeichen**
|
||||
Discord-Nachricht wird inkrementell editiert während Claude schreibt. Min. 1200ms zwischen Edits. Neue Nachricht bei Überlauf.
|
||||
Branch-Pattern: `phase-5/stream-to-discord`
|
||||
|
||||
### DIS-EP-504
|
||||
**Feature-Flag + Fallback + Integration-Tests**
|
||||
`disclaw.yaml` → `streaming: false` (Default). Graceful Fallback auf Bulk-Send bei Streaming-Fehlern. Integration-Tests.
|
||||
Branch-Pattern: `phase-5/streaming-flag-tests`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6+ — Zukunft (Platzhalter)
|
||||
|
||||
Noch nicht geschätzt, nicht priorisiert. Ideen aus `docs/development-plan.md` §6+.
|
||||
|
||||
| Thema | Kurzbeschreibung |
|
||||
|-------|-----------------|
|
||||
| Agent-zu-Agent-Kommunikation | Agenten können einander Aufgaben delegieren via Discord-Mentions |
|
||||
| `/list-agents`, `/delete-agent` | Management-Channel-Befehle für Workspace-Verwaltung |
|
||||
| Web-Dashboard | Browser-UI für Agenten-Übersicht, Konversationen, Nutzung |
|
||||
| Docker-Isolation | Jeder Agent-Workspace in eigenem Container |
|
||||
| Automatische Workspace-Migration | Migration bestehender `./workspaces/`-Ordner zu neuem Root |
|
||||
| Multi-Guild-Support | Ein DisClaw-Bot für mehrere Discord-Server |
|
||||
| MCP-Server pro Workspace | Workspace-spezifische MCP-Konfiguration aktivierbar |
|
||||
Loading…
Reference in a new issue