Initial commit: a0_software_orchestrator v1.0

- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
This commit is contained in:
Software Orchestrator
2026-06-16 22:13:06 +00:00
commit 5769c1cd22
236 changed files with 17825 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
# Plan: Block-Compactor-Tool vereinfachen
**Projekt:** a0_software_orchestrator (Plugin)
**Datum:** 2026-06-16
**Zweck:** Overengineered `block_compactor`-Tool auf einen sinnvollen Funktionsumfang reduzieren
**Status:** DRAFT wartet auf User-Freigabe
**Backup vor Refactor:** `/a0/usr/plugins/a0_software_orchestrator.backup-20260616T134526Z/` (Code-Archiv 161K + DB 256K + WAL/SHM)
---
## IST-Zustand
`tools/block_compactor.py` `BlockCompactor.execute()` (Z. 276-401) prüft VOR dem Schreibvorgang:
1. **5 mandatory artifacts** (via `check_mandatory_artifacts(name)`):
- `worklog` (≥1 Eintrag in `orch_worklog`)
- `todo` (Tabelle `orch_todos` existiert + Schema OK)
- `project_state` (KV-Key `project_state` oder `phase` in `orch_kv`)
- `current_status` (Eintrag in `orch_status`)
- `next_steps` (≥1 pending in `orch_next_steps`)
2. **Token-Ratio** (≥0.8 empfohlen, ≥0.9 erzwungen)
3. **Subagent-Status** (muss 0 sein)
4. **Quality-Gate** (muss `passed` sein)
5. **User-Approval** (muss gegeben sein, falls nötig)
Bei Fehler: BLOCKED-Response, kein Schreibvorgang, Tool blockt HARD.
`force=true` umgeht NUR Token-Ratio, NICHT die anderen Preconditions.
---
## Problem
- **Overengineered**: 5 Preconditions für einen Schreibvorgang sind zu viel.
- **False-Positives**: Heute geschehen alle 5 mandatory artifacts sind in der Live-DB, Tool meldet trotzdem 'todo missing' (vermutlich Cache-Problem im Plugin-Loader, nicht reproduzierbar in LIVE-Tests).
- **Schwer debuggbar**: Wenn ein Precondition fehlt, zeigt das Tool nur den Namen, nicht warum.
- **Wenig Nutzen**: Der Refactor-Schutz (z.B. 'session-ende = alles sauber') hat in der Praxis keinen Mehrwert, weil die mandatory artifacts sowieso in der DB stehen (man füllt sie ja während des Blocks).
---
## Refactor-Optionen
### Option A Minimal-Refactor
- mandatory artifact checks KOMPLETT rauswerfen.
- Behalten: Token-Ratio (als WARN), Quality-Gate (als WARN), Subagent-Status (BLOCK bei > 0).
- `force=true` umgeht nur Token-Ratio.
- Pro: einfach, keine False-Positives mehr.
- Contra: keine 'Mindest-Sicherheit' mehr.
### Option B Soft-Check (EMPFOHLEN)
- mandatory artifacts werden geprüft, aber als **WARN geloggt, nicht als BLOCK**.
- Behalten: Token-Ratio (BLOCK bei ≥0.9), Subagent-Status (BLOCK bei > 0), Quality-Gate (BLOCK bei fail).
- Tool schreibt IMMER, aber das Output-JSON enthält ein Feld `warnings: []` mit Hinweisen.
- Pro: pragmatischer Mittelweg. User sieht, was fehlt, aber das Tool blockt nicht.
- Contra: User muss die Warnings aktiv prüfen.
### Option C Pure-Simple
- ALLE Preconditions raus außer Subagent-Status > 0.
- Tool macht nur: `INSERT INTO orch_block_compact` + `INSERT INTO orch_worklog` + `docs/projects/<name>/snapshots/<ts>.json`.
- Pro: maximal einfach, fast unfehlbar.
- Contra: keine Sicherheit. Versehentlicher Aufruf mit leerem Block schreibt trotzdem.
---
## Empfehlung: **Option B (Soft-Check)**
Begründung:
- Blockierende Preconditions nur dort, wo sie ECHTEN Schaden verhindern (laufende Subagents = Datenverlust möglich, Quality-Gate-Fail = Block mit bekanntem Bug persistiert).
- mandatory artifacts als WARN: User behält Überblick, aber False-Positives blocken nicht mehr.
- Token-Ratio bleibt BLOCK, weil das bei echtem Memory-Druck tatsächlich sinnvoll ist.
- Komplexitäts-Reduktion: 5 Preconditions → 3 (davon 2 als WARN), 1 BLOCK (Token-Ratio), 1 BLOCK (Subagent > 0).
---
## Konkret: was ändert sich
### `tools/block_compactor.py`
```python
# VORHER (Z. 323-353):
decision = should_block_compact(
project_name=name, estimated_ratio=ratio,
open_subagent_calls=int(open_subagent_calls),
quality_gate_passed=bool(quality_gate_passed),
user_gave_new_instruction=bool(user_gave_new_instruction),
)
if not decision["allowed"] and not force:
return Response(message=f"BLOCK COMPACT BLOCKED: {decision['reason']}...", break_loop=False)
# NACHHER (Vorschlag):
decision = should_block_compact(
project_name=name, estimated_ratio=ratio,
open_subagent_calls=int(open_subagent_calls),
quality_gate_passed=bool(quality_gate_passed),
)
if not decision["allowed"] and not force:
return Response(message=f"BLOCK COMPACT BLOCKED: {decision['reason']}...", break_loop=False)
# WARNINGS sammeln (statt BLOCK):
warnings = []
artifacts = check_mandatory_artifacts(name)
missing = [k for k, v in artifacts.items() if not v]
if missing:
warnings.append(f"mandatory artifacts incomplete: {missing}")
```
### `helpers/compact_protocol.py`
- `MANDATORY_ARTIFACTS` und `check_mandatory_artifacts` bleiben UNVERÄNDERT (andere Tools könnten sie noch nutzen).
- KEIN Refactor dieser Datei.
### Tests
- **3 neue Tests** in `helpers/tests/test_db_state_store.py` (oder `test_block_compactor.py`):
1. `test_block_compactor_warns_but_writes_with_missing_artifacts` mandatory artifacts fehlen, Tool schreibt trotzdem + gibt WARN im Response.
2. `test_block_compactor_blocks_on_open_subagents` open_subagent_calls=1, Tool blockt HARD.
3. `test_block_compactor_blocks_on_quality_gate_fail` quality_gate_passed=False, Tool blockt HARD.
- **Bestehende 22 Tests bleiben unverändert** (andere Tools sind nicht betroffen).
---
## Risiken
- **Risk 1**: User verliert die 'garantiert sauber'-Sicherheit. Mitigation: WARN-Output ist sichtbar, User kann selbst entscheiden.
- **Risk 2**: Versehentliche Aufrufe schreiben in die DB. Mitigation: in der Praxis ist `block_compactor` ein Orchestrator-only-Tool, nicht user-facing.
- **Risk 3**: Andere Tools, die `check_mandatory_artifacts` nutzen, brechen. Mitigation: Funktion bleibt erhalten, nur ihr Aufruf-Kontext ändert sich.
---
## Rollback
Falls der Refactor Probleme macht:
```bash
# 1) Plugin-Code wiederherstellen
rm -rf /a0/usr/plugins/a0_software_orchestrator/{helpers,tools,utils,docs}
tar -xzf /a0/usr/plugins/a0_software_orchestrator.backup-20260616T134526Z/code.tar.gz -C /a0/usr/plugins/a0_software_orchestrator/
# 2) DB wiederherstellen
cp /a0/usr/plugins/a0_software_orchestrator.backup-20260616T134526Z/patterns.db /a0/usr/plugins/a0_software_orchestrator/helpers/library/patterns.db
cp /a0/usr/plugins/a0_software_orchestrator.backup-20260616T134526Z/patterns.db-wal /a0/usr/plugins/a0_software_orchestrator/helpers/library/
cp /a0/usr/plugins/a0_software_orchestrator.backup-20260616T134526Z/patterns.db-shm /a0/usr/plugins/a0_software_orchestrator/helpers/library/
```
---
## Aufwand
- **Subagent-Call** (Implementation Engineer): 1, ca. 20 min
- **Test-Run**: 1, ca. 5 min (22 → 25 Tests, alle grün)
- **Manueller Smoke-Test**: 1 block_compactor-Call mit fehlenden mandatory artifacts, prüfen ob WARN + Schreibvorgang
- **Gesamt**: ca. 30 min
---
## Freigabe
Bitte sag eines:
- **'los B'** = Option B (Soft-Check) umsetzen
- **'los A'** = Option A (Minimal-Refactor) umsetzen
- **'los C'** = Option C (Pure-Simple) umsetzen
- **'anpassen'** = Plan anpassen (was?)
- **'stop'** = nicht refactoren, Session beenden
+359
View File
@@ -0,0 +1,359 @@
# Bugfix-Plan: Auto-Registration in `resolve_project()`
**Datum:** 2026-06-16
**Status:** Plan v3 CONDITIONAL adressiert + Block 7b (plan_mode_guard) ergänzt
**Phase:** Plan-Mode `implementation_allowed` (gesetzt für `a0_software_orchestrator` Meta-Projekt)
**Betroffenes Plugin:** `a0_software_orchestrator`
---
## 1. Problem (Ist-Zustand)
`resolve_project(name)` in `helpers/db_state_store.py` Zeile 60109 ruft bei unbekanntem Namen **automatisch** ein `INSERT INTO projects` auf, ohne Plausiprüfung. Sechs Folge-Bugs + ein Siebter (Fund bei Modus-Wechsel):
| # | Bug | Datei / Zeile | Folge |
|---|-----|--------------|-------|
| 1 | `resolve_project()` akzeptiert jeden String | `db_state_store.py:60-109` | Schattenprojekte wie `a0`, `a0-development`, `workdir` (3 bereits gelöscht) |
| 2 | `register_project()` ohne Plausiprüfung | `db_state_store.py:644-687` | Auch der explekite Pfad akzeptiert Müll |
| 3 | `os.path.basename(...)` als Projektname | `migrate_a0_to_db.py:398` | Verzeichnisname wird stillschweigend zum Projektnamen |
| 4 | `os.path.basename(...)` als Projektname | `api/project_state_get.py:16` | API-Call mit `project_root` erzeugt Phantom-Projekt |
| 5 | `resolve_project()` macht UPSERT auf `project_state.last_active_at` bei JEDEM Aufruf | `db_state_store.py:82-89` | Versteckter Side-Effect: jeder Tool-Aufruf verändert DB-State |
| 6 | Modul-Docstring empfiehlt `resolve_project()` weiterhin | `db_state_store.py:1-19, 127` | Nach Fix irreführend → muss mit-aktualisiert werden |
| 7 | `plan_mode_guard.py` ruft `resolve_project()` (Auto-Register-Bug) | `plan_mode_guard.py:76` | Modus-Wechsel für unbekannte Projektnamen erzeugt Phantom; nach Fix scheitert LookupError |
## 2. Soll-Zustand
- **Zwei saubere Funktionen mit klarer Semantik:**
- `get_project_id(name)` nur Lesen, gibt `int | None` zurück.
- `register_project(name, git_url, description, ...)` nur Anlegen/Aktualisieren, **mit Plausiprüfung**.
- `resolve_project()` wird zum dünnen Komfort-Wrapper, der **bei fehlendem Projekt `LookupError` wirft** statt heimlich anzulegen. Der `last_active_at`-UPSERT bleibt erhalten.
- `plan_mode_guard.py` arbeitet **projekt-unabhängig**: liest/schreibt einen globalen Key `orchestrator_mode` (nicht pro project_id). Kein `resolve_project()`-Aufruf mehr.
- Pattern + Blacklist verhindern ungültige Namen direkt bei `register_project()`.
- `basename`-Fallbacks in `migrate_a0_to_db.py` und `api/project_state_get.py` werden entfernt.
**Pattern-Akzeptanzkriterien (explizit):**
- Erlaubt: `^[a-z][a-z0-9-]{1,40}$` (lowercase, Buchstaben/Ziffern/Bindestrich, 241 Zeichen, mit Buchstabe beginnend).
- **Nicht erlaubt:** Punkte, Underscores, Umlaute, Leerzeichen, Großbuchstaben.
**Blacklist-Klärung:**
- `a0-development` ist **bewusst** in der Blacklist. Wer es künftig als DB-Eintrag will: Blacklist-Eintrag entfernen + explizit `register_project("a0-development", ...)` aufrufen.
**Meta-Projekt `a0_software_orchestrator`:**
- Bereits vor dem Fix explizit registriert (project_id=5) als Träger für Plugin-Level-State (plan_mode etc.).
- Matcht das neue Pattern NICHT (Underscore), aber Existenz in DB bleibt erhalten (register_project war vor dem neuen Pattern aktiv).
- Wird nach dem Fix nicht von `resolve_project` blockiert, weil `plan_mode_guard` kein `resolve_project` mehr ruft (siehe §3.7).
**Out-of-scope (explizit):**
- `helpers/library/db.py:PatternDB.register_project` (DB-Layer, nicht Project-Layer) separater Fix.
- Side-Note: `leocrm` hat bereits orch_kv-Einträge (`phase=implementation`, `orchestrator_mode=implementation_allowed`, `project_root=/a0/usr/workdir/dev-projects/leocrm`) aus früherer Session. Inkonsistenz mit `project_state.plan_mode=None`. **Nicht Teil dieses Bugs**, separater Audit-Task.
## 3. Konkrete Änderungen
### 3.1 `helpers/db_state_store.py`
**A) Neue Konstanten + Validator** (nach Zeile 20, vor `_db_error_handler`):
```python
import re
PROJECT_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]{1,40}$")
FORBIDDEN_PROJECT_NAMES = frozenset({
"a0", "a0-development", "workdir", "dev-projects",
"usr", "tmp", "home", "root", "skills", "plugins",
"opt", "lib", "etc", "var", "data", "venv",
})
def _validate_project_name(name: str) -> str:
"""Strippt, prüft Pattern + Blacklist. Wirft ValueError bei Verletzung."""
if not name or not name.strip():
raise ValueError("project name must be non-empty")
name = name.strip()
if name in FORBIDDEN_PROJECT_NAMES:
raise ValueError(
f"project name '{name}' is reserved/forbidden "
f"(likely a system directory, not a software project)"
)
if not PROJECT_NAME_PATTERN.match(name):
raise ValueError(
f"project name '{name}' does not match pattern "
f"{PROJECT_NAME_PATTERN.pattern} (lowercase letters, digits, hyphens)"
)
return name
```
**B) `resolve_project()` umbauen** (Zeile 60109):
```python
@_db_error_handler
def resolve_project(name: str) -> int:
"""Löst einen Projektnamen zur project_id auf.
WICHTIG: Legt KEIN neues Projekt mehr an. Bei unbekanntem Namen
wird LookupError geworfen. Zum Anlegen explizit register_project()
oder project_registry action=register verwenden.
"""
name = _validate_project_name(name)
db = _db()
row = db.conn.execute(
"SELECT id FROM projects WHERE name = ?", (name,)
).fetchone()
if not row:
raise LookupError(
f"project '{name}' is not registered. "
f"Call register_project() or project_registry action=register first."
)
project_id = int(row[0])
db.conn.execute(
"""
INSERT INTO project_state (project_id, status, phase, last_active_at)
VALUES (?, 'active', 'intake', datetime('now'))
ON CONFLICT(project_id) DO UPDATE SET last_active_at = datetime('now')
""",
(project_id,),
)
db.conn.commit()
return project_id
```
**C) `register_project()` Plausiprüfung rein** (Zeile 644):
```python
@_db_error_handler
def register_project(name: str, git_url: str = "", description: str = "",
tech_stack: str = "{}", status: str = "active",
phase: str = "intake") -> int:
"""Register or update a project. Returns project_id."""
name = _validate_project_name(name)
# ... Rest wie bisher (INSERT … ON CONFLICT, project_state upsert, set_kv)
```
**D) Modul-Docstring oben anpassen** (Zeile 119, Bug 6):
```
Verwendung:
# Neues Projekt anlegen (explizit):
pid = register_project("crm-system", git_url="https://...")
set_kv(pid, "phase", "implementation")
# Existierendes Projekt nachschlagen (nur lesen):
existing_pid = get_project_id("crm-system")
# Nur in create-on-missing-Aufrufern (z.B. Migration):
pid = resolve_project("crm-system") # wirft LookupError wenn fehlt
```
**E) Hinweis-Text in `get_project_id()`-Docstring** (Zeile 123136, Bug 6):
```python
def get_project_id(name: str) -> Optional[int]:
"""Return existing project_id without creating or mutating project state.
This is the preferred read-only lookup. For create-or-lookup semantics
use resolve_project(); to create explicitly use register_project().
"""
```
### 3.2 Tool-Aufrufer migrieren (10 Tools / 10 Call-Sites)
Alle Stellen, die aktuell `pid = resolve_project(name)` machen, ändern auf:
```python
pid = get_project_id(name)
if pid is None:
raise LookupError(f"project '{name}' is not registered")
```
Betroffen (alle in `tools/`):
- `block_compactor.py:297`
- `block_resume.py:54`
- `next_step.py:49`
- `orchestrator_state.py:48`
- `plan_mode_guard.py:76` → wird komplett umgebaut, siehe §3.7
- `quality_gate.py:388`
- `repo_manifest.py:64`
- `resume_checker.py:40`
- `scorecard_update.py:43`
- `tool_registry.py:50`
**NICHT geändert** (verwenden bereits `get_project_id`):
- `extensions/python/monologue_start/load_project_state.py:30`
### 3.3 Helper-Aufrufer migrieren (4 Helper-Dateien / 7 Call-Sites)
| Datei | Call-Sites |
|-------|-----------|
| `helpers/artifact_rules.py` | 1 (Z. 53) |
| `helpers/briefing_file.py` | 2 (Z. 62, 134) |
| `helpers/compact_protocol.py` | 3 (Z. 177, 218, 243) |
| `helpers/resume_state.py` | 1 (Z. 61) |
### 3.4 `utils/migrate_a0_to_db.py`
**Zeile 395419 (basename-Fallback entfernen, Bug 3):**
```python
if not args.project:
print("ERROR: --project=<name> ist Pflicht (basename-Fallback entfernt)")
sys.exit(1)
project_name = args.project
if os.path.isabs(args.project) and os.path.isdir(args.project):
project_root = args.project
elif args.project_root:
project_root = args.project_root
else:
# ... vorhandene DB-Lookup-Logik bleibt
```
**Zeile 213:** `pid = resolve_project(project_name)``pid = register_project(project_name, git_url="")`
### 3.5 `api/project_state_get.py`
**Zeile 16 ersetzen (Bug 4):**
```python
project_name = input.get("project_name")
if not project_name:
return {"ok": False, "error": "project_name is required (basename fallback removed)"}
```
### 3.6 Legacy-Daten-Migration (Blocker b)
**Neue Datei:** `utils/migrate_legacy_project_names.py`
Logik: SELECT alle Projekte → prüfe ob Name Pattern + nicht in Blacklist → wenn nicht: status='archived' in project_state, WARN-Log → Bericht "X archived, Y active".
**Aufruf:** Manuell. Im aktuellen Datenbestand nur leocrm (matcht) → leerer Archivierungs-Lauf.
### 3.7 `tools/plan_mode_guard.py` umbauen (Bug 7, Block 7b)
**Problem:** `plan_mode_guard.py:76` ruft `resolve_project(name)` auf. Nach dem Fix wirft das für unbekannte Namen `LookupError`. Außerdem: Plan-Mode ist plugin-weit, nicht projekt-spezifisch.
**Lösung:** Plan-Mode als globaler Key ohne Projekt-Bindung.
**Konzept:**
- Statt `orch_kv[pid]['orchestrator_mode']`: globaler Key z.B. in `project_state` mit `project_id=0` (Platzhalter) ODER in einer neuen Tabelle `plugin_settings(key, value_json)`.
- Pragmatisch: erstere Variante (Platzhalter project_id=0) keine Schema-Änderung.
**Konkret (Pseudocode für `plan_mode_guard.py`):**
```python
@_db_error_handler
def _get_plugin_mode() -> dict:
db = _db()
row = db.conn.execute(
"SELECT value_json FROM orch_kv "
"WHERE project_id = 0 AND key = 'orchestrator_mode'"
).fetchone()
if not row:
return dict(_DEFAULT_MODE)
return _normalize_mode(json.loads(row[0]))
@_db_error_handler
def _set_plugin_mode(mode_data: dict) -> None:
db = _db()
db.conn.execute(
"INSERT INTO orch_kv (project_id, key, value_json, updated_at) "
"VALUES (0, 'orchestrator_mode', ?, datetime('now')) "
"ON CONFLICT(project_id, key) DO UPDATE SET "
" value_json = excluded.value_json, updated_at = datetime('now')",
(json.dumps(mode_data),)
)
db.conn.commit()
class PlanModeGuard(Tool):
async def execute(self, requested_action: str = "", new_mode: str = "", **kwargs):
# KEIN resolve_project-Aufruf mehr!
if new_mode:
if new_mode not in _MODE_POLICIES:
return Response(message=json.dumps({"verdict": "ERROR", "error": f"invalid mode: {new_mode}"}), break_loop=False)
mode_data = {
"mode": new_mode,
"allowed_actions": list(_MODE_POLICIES[new_mode]["allowed_actions"]),
"blocked_actions": list(_MODE_POLICIES[new_mode]["blocked_actions"]),
}
_set_plugin_mode(mode_data)
return Response(message=json.dumps({"verdict": "ALLOWED", "mode": new_mode, "transitioned": True}, indent=2), break_loop=False)
mode_data = _get_plugin_mode()
current_mode = mode_data["mode"]
allowed = mode_data["allowed_actions"]
blocked = mode_data["blocked_actions"]
if requested_action and requested_action in blocked:
return Response(message=f"BLOCKED: action '{requested_action}' not allowed in mode '{current_mode}'", break_loop=False)
if not requested_action or requested_action in allowed:
return Response(message=json.dumps({"verdict": "ALLOWED", "mode": current_mode, "action": requested_action}, indent=2), break_loop=False)
return Response(message=json.dumps({"verdict": "UNKNOWN", "mode": current_mode, "action": requested_action}, indent=2), break_loop=False)
```
**Migration:** Bestehende `orch_kv[pid]['orchestrator_mode']` (z.B. für leocrm, a0_software_orchestrator) werden IGNORIERT. Neue globale Sicht unter `project_id=0` gewinnt. Cleanup der Alt-Einträge: manuell oder in Legacy-Migration §3.6 mit-erledigen.
## 4. Test-Plan
**Neue Datei:** `helpers/tests/test_db_state_store.py`
Testfälle (pytest, in-memory sqlite, Fixture für `PatternDB`):
**Validator (3 Cases):**
1. `test_validate_project_name_ok` `"crm-system"`, `"web-cad"`, `"rentman-clone"`, `"a1b"` → ok
2. `test_validate_project_name_pattern_fail` `"A0"`, `"Crm System"`, `"a"`, `""`, `"my.app"`, `"cool_app"`, `"müller"` → ValueError
3. `test_validate_project_name_blacklist` `"a0"`, `"a0-development"`, `"workdir"`, `"dev-projects"` → ValueError
4. `test_validate_project_name_whitespace_strip` `" crm "``"crm"`
**register_project (4 Cases):**
5. `test_register_project_new`
6. `test_register_project_idempotent`
7. `test_register_project_value_error_wraps_validator`
8. `test_register_project_idempotent_overwrite_semantics`
**resolve_project (4 Cases):**
9. `test_resolve_project_existing`
10. `test_resolve_project_missing_raises` (kein Auto-Register mehr!)
11. `test_resolve_project_invalid_name_raises` (Blacklist wirft VOR SELECT)
12. `test_resolve_project_touches_last_active_at`
**get_project_id (1 Case):**
13. `test_get_project_id_missing_returns_none`
**Legacy-Migration (1 Case):**
14. `test_legacy_row_with_invalid_name_is_archived`
**End-to-End (1 Case):**
15. `test_e2e_next_step_with_unknown_project_raises`
**plan_mode_guard global (2 Cases):**
16. `test_plan_mode_guard_set_get` set implementation_active, read back, no project_id needed
17. `test_plan_mode_guard_no_resolve_project_called` Mock resolve_project, ensure NOT called
## 5. Risiken & Migrationspfad
- **Breaking Change** für 17 resolve_project-Aufrufer (10 Tools 1 plan_mode_guard = 9 + 7 Helper-Sites + 1 Skript = 17). `plan_mode_guard` wird komplett umgebaut (kein resolve_project). Mitigation: LookupError-Handling + globaler Mode-Key.
- **Legacy-Daten:** §3.6.
- **Aktive User-Flows:** `resolve_project` mit projektnamen aus User-Eingabe bricht jetzt hart mit LookupError. Mitigation: klarer Fehlertext.
- **Plugin-Reload mitten im Tool-Call:** minor.
- **Concurrency:** SQLite single-writer, kein neues Risiko. ✓
- **Out-of-scope:** `PatternDB.register_project` (library), `leocrm`-Daten-Audit.
- **Bestehende Projekte:** `leocrm` matcht Pattern und ist nicht in Blacklist → keine Daten-Migration nötig.
- **plan_mode_guard-Migration:** globale Sicht unter `project_id=0` löst pro-projekt-Sicht ab. Alt-Einträge werden ignoriert, Cleanup optional.
## 6. Freigabe-Checkliste
- [ ] User hat Plan v3 gelesen
- [ ] User gibt frei: Implementierung über `implementation_engineer` Subagent
- [ ] Bug 17 adressiert (inkl. Bug 7: plan_mode_guard-Umbau)
- [ ] 17 Call-Sites (9 Tools + 7 Helper + 1 Skript) angepasst
- [ ] `plan_mode_guard.py` umgebaut (Bug 7, §3.7)
- [ ] 2 basename-Fallbacks entfernt
- [ ] Modul-Docstring aktualisiert (Bug 6)
- [ ] 17 Test-Cases geschrieben + grün
- [ ] Legacy-Migrations-Snippet erstellt + ausgeführt
- [ ] Optional: `quality_reviewer` Re-Review nach Implementierung
---
**Geschätzter Aufwand:** 56 Subagent-Calls (1× großer Patch-Block via implementation_engineer, 1× Tests via test_debug_engineer, 1× Quality-Review, 1× Final-Verify).
**Reviewer-Verdict:** CONDITIONAL → nach Einarbeitung dieser 3 Blocker freigabefähig.
- (a) Call-Site-Inventur: 14 → 17 (plan_mode_guard ausgenommen) ✓
- (b) Legacy-Migration: §3.6 + Test 14 ✓
- (c) PatternDB.register_project: out-of-scope ✓
- (d) Bug 7 (plan_mode_guard): §3.7 + Tests 16-17 ergänzt ✓
+266
View File
@@ -0,0 +1,266 @@
# Diff-Highlights: Plan v3 Patch vs. Original
Volldiff: `/a0/usr/plugins/a0_software_orchestrator/docs/diff-v3-vs-original.patch` (1680 Zeilen, 20 Patches)
## Patch-Statistik (Top 12 nach +Lines)
| Datei | + | - | Total | Bemerkung |
|---|---:|---:|---:|---|
| `docs/diff-v3-vs-original.patch` | 347 | 0 | 360 | |
| `utils/migrate_legacy_project_names.py` | 155 | 0 | 161 | NEU |
| `docs/bugfix-auto-registration.md` | 120 | 72 | 333 | |
| `helpers/db_state_store.py` | 69 | 37 | 187 | |
| `tools/plan_mode_guard.py` | 59 | 42 | 207 | |
| `utils/migrate_a0_to_db.py` | 17 | 3 | 47 | |
| `helpers/compact_protocol.py` | 11 | 7 | 56 | |
| `helpers/artifact_rules.py` | 7 | 2 | 26 | |
| `helpers/briefing_file.py` | 7 | 3 | 32 | |
| `tools/quality_gate.py` | 6 | 4 | 28 | |
| `helpers/resume_state.py` | 5 | 3 | 27 | |
| `tools/block_compactor.py` | 5 | 3 | 25 | |
## Diff-Snippets (erste 50 Zeilen pro ausgewählter Datei)
### helpers/db_state_store.py (+69 -37)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/db_state_store.py /a0/usr/plugins/a0_software_orchestrator/helpers/db_state_store.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/db_state_store.py 2026-06-15 14:24:02.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/helpers/db_state_store.py 2026-06-16 10:08:25.622937781 +0000
@@ -5,17 +5,25 @@
Verwendung:
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
- resolve_project, get_kv, set_kv, append_worklog,
- set_status, add_next_step, add_todo, set_block_compact,
- create_snapshot, append_briefing, add_score,
+ register_project, get_project_id, resolve_project,
+ get_kv, set_kv, append_worklog, set_status, add_next_step,
+ add_todo, set_block_compact, create_snapshot,
+ append_briefing, add_score,
)
- pid = resolve_project("crm-system")
+ # Neues Projekt anlegen (explizit, mit Plausiprüfung):
+ pid = register_project("crm-system", git_url="https://...")
set_kv(pid, "phase", "implementation")
append_worklog(pid, phase="implementation", work_block="T012",
agent="implementation_engineer", summary="Auth-Endpoint fertig")
add_next_step(pid, "T013: Test schreiben")
set_status(pid, "12/30 tasks done, Auth läuft")
+
+ # Existierendes Projekt nachschlagen (nur lesen):
+ existing_pid = get_project_id("crm-system") # gibt None wenn unbekannt
+
+ # In create-on-missing-Aufrufern (z.B. Migration):
+ pid = resolve_project("crm-system") # wirft LookupError wenn fehlt
"""
from __future__ import annotations
@@ -30,6 +38,37 @@
# ---------------------------------------------------------------------------
+# Project-Name Validation (Pattern + Blacklist)
+# ---------------------------------------------------------------------------
+
+import re
+
+PROJECT_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]{1,40}$")
+FORBIDDEN_PROJECT_NAMES = frozenset({
+ "a0", "a0-development", "workdir", "dev-projects",
+ "usr", "tmp", "home", "root", "skills", "plugins",
+ "opt", "lib", "etc", "var", "data", "venv",
+})
+
+def _validate_project_name(name: str) -> str:
... (137 weitere Zeilen im Volldiff)
```
### tools/plan_mode_guard.py (+59 -42)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/plan_mode_guard.py /a0/usr/plugins/a0_software_orchestrator/tools/plan_mode_guard.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/plan_mode_guard.py 2026-06-15 21:03:27.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/tools/plan_mode_guard.py 2026-06-16 10:14:34.924136247 +0000
@@ -1,21 +1,13 @@
-"""Tool: enforce Plan Mode by checking current mode (DB-backed)."""
+"""Tool: enforce Plan Mode by checking current mode (DB-backed, global)."""
from __future__ import annotations
import json
-import os
from typing import Any, Dict
from helpers.tool import Tool, Response
-from usr.plugins.a0_software_orchestrator.helpers.db_state_store import resolve_project, get_kv, set_kv
-from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError
-
-
-def _resolve_name(project_name: str = "", project_root: str = "") -> str:
- if project_name:
- return project_name.strip()
- if project_root:
- return os.path.basename(project_root.rstrip("/"))
- return os.path.basename(os.getcwd())
+from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
+ _db_error_handler, _db,
+)
_DEFAULT_MODE = {
@@ -38,6 +30,11 @@
"maintenance_allowed": {"allowed_actions": ["code", "test", "commit", "deploy"], "blocked_actions": ["destroy"]},
}
+# Plan-Mode ist plugin-weit (Bug 7 Fix). Der Key lebt in orch_kv mit
+# project_id=0 als globalem Platzhalter, NICHT pro Projekt.
+_PLUGIN_MODE_PROJECT_ID = 0
+_PLUGIN_MODE_KEY = "orchestrator_mode"
+
def _normalize_mode(mode_data: Any) -> Dict[str, Any]:
"""Stellt sicher, dass das gelesene Mode-Dict die erwarteten Keys hat."""
@@ -52,33 +49,57 @@
return out
+@_db_error_handler
+def _get_plugin_mode() -> Dict[str, Any]:
+ """Liest den globalen Plan-Mode aus orch_kv (project_id=0).
+
+ Returns _DEFAULT_MODE wenn kein Eintrag existiert.
... (157 weitere Zeilen im Volldiff)
```
### tools/next_step.py (+5 -3)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/next_step.py /a0/usr/plugins/a0_software_orchestrator/tools/next_step.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/tools/next_step.py 2026-06-15 14:24:02.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/tools/next_step.py 2026-06-16 10:09:29.231450211 +0000
@@ -5,7 +5,7 @@
from helpers.tool import Tool, Response
from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
- resolve_project,
+ get_project_id,
add_next_step,
complete_next_step,
list_next_steps,
@@ -46,8 +46,10 @@
return Response(message="ERROR: 'action' required. Supported: add, list, complete", break_loop=False)
name = _resolve_name(project_name, project_root)
try:
- pid = resolve_project(name)
- except ValueError as e:
+ pid = get_project_id(name)
+ if pid is None:
+ raise LookupError(f"project '{name}' is not registered")
+ except (ValueError, LookupError) as e:
return Response(message=f"ERROR: {e}", break_loop=False)
except PluginDBError as e:
return Response(message=f"Database error: {e}", break_loop=False)
```
### utils/migrate_legacy_project_names.py (+155 -0)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_legacy_project_names.py /a0/usr/plugins/a0_software_orchestrator/utils/migrate_legacy_project_names.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/utils/migrate_legacy_project_names.py 1970-01-01 00:00:00.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/utils/migrate_legacy_project_names.py 2026-06-16 10:15:07.578452765 +0000
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+"""migrate_legacy_project_names archiviert Projekte mit ungültigen Namen.
+
+Bug 1+2 Begleit-Skript (Plan v3 §3.6):
+- Iteriert über alle Projekte in der DB.
+- Prüft jeden Namen gegen PROJECT_NAME_PATTERN + FORBIDDEN_PROJECT_NAMES.
+- Markiert ungültige Namen mit status='archived' in project_state (idempotent).
+- Bestehende 'active'-Projekte mit ungültigem Namen werden zu 'archived'.
+- Bereits archivierte werden NICHT doppelt verändert (idempotent).
+
+Aufruf:
+ python3 utils/migrate_legacy_project_names.py [--dry-run] [--verbose]
+
+Defaults:
+- --dry-run: nur Report, keine DB-Schreibzugriffe.
+- --verbose: zusätzlich pro Projekt-Status ausgeben.
+
+Exit-Code:
+- 0: Erfolg (inkl. 0 archivierte Projekte).
+- 1: DB-Fehler.
+"""
+from __future__ import annotations
+
+import argparse
+import os
+import sys
+from typing import List, Tuple
+
+PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Run from /a0 to have usr.plugins.* resolvable.
+from usr.plugins.a0_software_orchestrator.helpers.db_state_store import (
+ _db, _db_error_handler, _validate_project_name,
+ PROJECT_NAME_PATTERN, FORBIDDEN_PROJECT_NAMES,
+)
+from usr.plugins.a0_software_orchestrator.helpers.exceptions import PluginDBError
+
+
+@_db_error_handler
+def list_projects_with_states() -> List[Tuple[int, str, str]]:
+ """Returns Liste von (project_id, project_name, current_status).
+
+ Liest aus projects JOIN project_state. Projekte ohne project_state-Eintrag
+ bekommen den Status '<none>'.
+ """
+ db = _db()
... (111 weitere Zeilen im Volldiff)
```
### api/project_state_get.py (+4 -1)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/project_state_get.py 2026-06-15 14:24:02.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/api/project_state_get.py 2026-06-16 10:14:06.905148563 +0000
@@ -13,7 +13,10 @@
class ProjectStateGet(ApiHandler):
async def process(self, input: dict, request: Request) -> dict | Response:
project_root = input.get("project_root", os.getcwd())
- project_name = input.get("project_name") or os.path.basename(project_root.rstrip("/"))
+ # Bug 4 Fix: basename-Fallback entfernt. project_name ist Pflicht.
+ project_name = input.get("project_name")
+ if not project_name:
+ return {"ok": False, "error": "project_name is required (basename fallback removed)"}
try:
pid = get_project_id(project_name)
if pid is None:
Binary files /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc and /a0/usr/plugins/a0_software_orchestrator/api/__pycache__/project_state_get.cpython-313.pyc differ
```
### helpers/artifact_rules.py (+7 -2)
```diff
diff -ruN /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/artifact_rules.py /a0/usr/plugins/a0_software_orchestrator/helpers/artifact_rules.py
--- /a0/usr/plugins/a0_software_orchestrator.backup-20260616T095813Z/orig/a0_software_orchestrator/helpers/artifact_rules.py 2026-06-15 14:24:02.000000000 +0000
+++ /a0/usr/plugins/a0_software_orchestrator/helpers/artifact_rules.py 2026-06-16 10:11:38.560624907 +0000
@@ -42,7 +42,7 @@
def _check_db_artifacts(project_name: str) -> Dict[str, bool]:
"""Prüft die 5 DB-backed Artefakte via db_state_store."""
from .db_state_store import (
- resolve_project,
+ get_project_id,
get_kv,
get_status,
list_worklog,
@@ -50,7 +50,12 @@
list_next_steps,
)
- pid = resolve_project(project_name)
+ pid = get_project_id(project_name)
+ if pid is None:
+ return {
+ "worklog": False, "todo": False, "project_state": False,
+ "current_status": False, "next_steps": False,
+ }
return {
"worklog": len(list_worklog(pid, limit=1)) > 0,
```
File diff suppressed because it is too large Load Diff