#!/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//` 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 /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 /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\", \"/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:` 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.")