- 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
16 KiB
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 60–109 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, gibtint | Nonezurück.register_project(name, git_url, description, ...)– nur Anlegen/Aktualisieren, mit Plausiprüfung.
resolve_project()wird zum dünnen Komfort-Wrapper, der bei fehlendem ProjektLookupErrorwirft statt heimlich anzulegen. Derlast_active_at-UPSERT bleibt erhalten.plan_mode_guard.pyarbeitet projekt-unabhängig: liest/schreibt einen globalen Keyorchestrator_mode(nicht pro project_id). Keinresolve_project()-Aufruf mehr.- Pattern + Blacklist verhindern ungültige Namen direkt bei
register_project(). basename-Fallbacks inmigrate_a0_to_db.pyundapi/project_state_get.pywerden entfernt.
Pattern-Akzeptanzkriterien (explizit):
- Erlaubt:
^[a-z][a-z0-9-]{1,40}$(lowercase, Buchstaben/Ziffern/Bindestrich, 2–41 Zeichen, mit Buchstabe beginnend). - Nicht erlaubt: Punkte, Underscores, Umlaute, Leerzeichen, Großbuchstaben.
Blacklist-Klärung:
a0-developmentist bewusst in der Blacklist. Wer es künftig als DB-Eintrag will: Blacklist-Eintrag entfernen + explizitregister_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_projectblockiert, weilplan_mode_guardkeinresolve_projectmehr ruft (siehe §3.7).
Out-of-scope (explizit):
helpers/library/db.py:PatternDB.register_project(DB-Layer, nicht Project-Layer) – separater Fix.- Side-Note:
leocrmhat bereits orch_kv-Einträge (phase=implementation,orchestrator_mode=implementation_allowed,project_root=/a0/usr/workdir/dev-projects/leocrm) aus früherer Session. Inkonsistenz mitproject_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):
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 60–109):
@_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):
@_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 1–19, 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 123–136, Bug 6):
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:
pid = get_project_id(name)
if pid is None:
raise LookupError(f"project '{name}' is not registered")
Betroffen (alle in tools/):
block_compactor.py:297block_resume.py:54next_step.py:49orchestrator_state.py:48plan_mode_guard.py:76→ wird komplett umgebaut, siehe §3.7quality_gate.py:388repo_manifest.py:64resume_checker.py:40scorecard_update.py:43tool_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 395–419 (basename-Fallback entfernen, Bug 3):
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):
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. inproject_statemitproject_id=0(Platzhalter) ODER in einer neuen Tabelleplugin_settings(key, value_json). - Pragmatisch: erstere Variante (Platzhalter project_id=0) – keine Schema-Änderung.
Konkret (Pseudocode für plan_mode_guard.py):
@_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):
test_validate_project_name_ok–"crm-system","web-cad","rentman-clone","a1b"→ oktest_validate_project_name_pattern_fail–"A0","Crm System","a","","my.app","cool_app","müller"→ ValueErrortest_validate_project_name_blacklist–"a0","a0-development","workdir","dev-projects"→ ValueErrortest_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_guardwird komplett umgebaut (kein resolve_project). Mitigation: LookupError-Handling + globaler Mode-Key. - Legacy-Daten: §3.6.
- Aktive User-Flows:
resolve_projectmit 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:
leocrmmatcht Pattern und ist nicht in Blacklist → keine Daten-Migration nötig. - plan_mode_guard-Migration: globale Sicht unter
project_id=0lö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_engineerSubagent - Bug 1–7 adressiert (inkl. Bug 7: plan_mode_guard-Umbau)
- 17 Call-Sites (9 Tools + 7 Helper + 1 Skript) angepasst
plan_mode_guard.pyumgebaut (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_reviewerRe-Review nach Implementierung
Geschätzter Aufwand: 5–6 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 ✓