Refactor cockpit to use DockerTmuxController pattern

Based on claude-code-tools TmuxCLIController, this refactor:

- Added DockerTmuxController class for robust tmux session management
- Implements send_keys() with configurable delay_enter
- Implements capture_pane() for output retrieval
- Implements wait_for_prompt() for pattern-based completion detection
- Implements wait_for_idle() for content-hash-based idle detection
- Implements wait_for_shell_prompt() for shell prompt detection

Also includes workflow improvements:
- Pre-task git snapshot before agent execution
- Post-task commit protocol in agent guidelines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-14 10:42:16 -03:00
commit ec33ac1936
265 changed files with 92011 additions and 0 deletions

642
lib/knowledge_graph.py Normal file
View File

@@ -0,0 +1,642 @@
#!/usr/bin/env python3
"""
Luz Knowledge Graph - Centralized documentation storage
Four domains:
- sysadmin: Server admin docs, commands, procedures
- users: User management, permissions, workflows
- projects: Project-specific docs, features, APIs
- research: Research sessions, findings, sources
All use SQLite with FTS5 for full-text search.
"""
import json
import sqlite3
import uuid
import time
import os
import grp
import pwd
from pathlib import Path
from typing import Optional, Dict, List, Any
from datetime import datetime
# Knowledge graph paths
KG_BASE = Path("/etc/luz-knowledge")
KG_PATHS = {
"sysadmin": KG_BASE / "sysadmin.db",
"users": KG_BASE / "users.db",
"projects": KG_BASE / "projects.db",
"research": KG_BASE / "research.db",
}
# Entity types per domain
ENTITY_TYPES = {
"sysadmin": ["command", "service", "config", "procedure", "troubleshooting", "architecture"],
"users": ["user_type", "permission", "workflow", "guide", "policy"],
"projects": ["project", "feature", "api", "component", "changelog", "config"],
"research": ["session", "finding", "source", "synthesis", "query"],
}
# Relation types
RELATION_TYPES = [
"relates_to", # General relation
"depends_on", # Dependency
"documents", # Documentation link
"implements", # Implementation
"supersedes", # Replacement
"contains", # Parent-child
"references", # Cross-reference
"triggers", # Causal
]
# Access control per domain
# Format: {domain: {"read": [users/groups], "write": [users/groups]}}
# Special values: "admin" = admin user only, "operators" = operators group, "all" = everyone
KG_PERMISSIONS = {
"sysadmin": {"read": ["admin"], "write": ["admin"]},
"users": {"read": ["admin"], "write": ["admin"]},
"projects": {"read": ["admin", "operators"], "write": ["admin", "operators"]},
"research": {"read": ["all"], "write": ["all"]}, # All users can write research via Zen
}
def get_current_user() -> str:
"""Get current username."""
return pwd.getpwuid(os.getuid()).pw_name
def get_user_groups(username: str = None) -> List[str]:
"""Get groups for a user."""
if username is None:
username = get_current_user()
try:
groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem]
# Add primary group
primary_gid = pwd.getpwnam(username).pw_gid
primary_group = grp.getgrgid(primary_gid).gr_name
if primary_group not in groups:
groups.append(primary_group)
return groups
except KeyError:
return []
def check_permission(domain: str, action: str) -> bool:
"""Check if current user has permission for action on domain.
Args:
domain: KG domain (sysadmin, users, projects, research)
action: "read" or "write"
Returns:
True if permitted, False otherwise
"""
if domain not in KG_PERMISSIONS:
return False
allowed = KG_PERMISSIONS[domain].get(action, [])
# "all" means everyone
if "all" in allowed:
return True
username = get_current_user()
# Root always has access
if username == "root":
return True
# Check direct user match
if username in allowed:
return True
# Check group membership
user_groups = get_user_groups(username)
for group in allowed:
if group in user_groups:
return True
return False
class KnowledgeGraph:
"""Knowledge graph operations for a single domain."""
def __init__(self, domain: str, skip_permission_check: bool = False):
if domain not in KG_PATHS:
raise ValueError(f"Unknown domain: {domain}. Valid: {list(KG_PATHS.keys())}")
self.domain = domain
self.db_path = KG_PATHS[domain]
self._skip_permission_check = skip_permission_check
self._ensure_schema()
def _check_read(self):
"""Check read permission."""
if self._skip_permission_check:
return
if not check_permission(self.domain, "read"):
user = get_current_user()
raise PermissionError(f"User '{user}' does not have read access to '{self.domain}' KG")
def _check_write(self):
"""Check write permission."""
if self._skip_permission_check:
return
if not check_permission(self.domain, "write"):
user = get_current_user()
raise PermissionError(f"User '{user}' does not have write access to '{self.domain}' KG")
def _ensure_schema(self):
"""Create tables if they don't exist."""
KG_BASE.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# Main entities table
c.execute('''
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
type TEXT NOT NULL,
domain TEXT NOT NULL,
content TEXT,
metadata TEXT,
created_at REAL,
updated_at REAL,
source TEXT
)
''')
# Relations table
c.execute('''
CREATE TABLE IF NOT EXISTS relations (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
relation TEXT NOT NULL,
context TEXT,
weight INTEGER DEFAULT 1,
created_at REAL,
FOREIGN KEY (source_id) REFERENCES entities(id),
FOREIGN KEY (target_id) REFERENCES entities(id)
)
''')
# Observations table (notes, QA findings, etc.)
c.execute('''
CREATE TABLE IF NOT EXISTS observations (
id TEXT PRIMARY KEY,
entity_id TEXT NOT NULL,
content TEXT NOT NULL,
observer TEXT,
created_at REAL,
FOREIGN KEY (entity_id) REFERENCES entities(id)
)
''')
# FTS5 virtual table for full-text search
c.execute('''
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
name, type, content, metadata,
content='entities',
content_rowid='rowid'
)
''')
# Triggers to keep FTS in sync
c.execute('''
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
INSERT INTO entities_fts(rowid, name, type, content, metadata)
VALUES (NEW.rowid, NEW.name, NEW.type, NEW.content, NEW.metadata);
END
''')
c.execute('''
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
INSERT INTO entities_fts(entities_fts, rowid, name, type, content, metadata)
VALUES ('delete', OLD.rowid, OLD.name, OLD.type, OLD.content, OLD.metadata);
END
''')
c.execute('''
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
INSERT INTO entities_fts(entities_fts, rowid, name, type, content, metadata)
VALUES ('delete', OLD.rowid, OLD.name, OLD.type, OLD.content, OLD.metadata);
INSERT INTO entities_fts(rowid, name, type, content, metadata)
VALUES (NEW.rowid, NEW.name, NEW.type, NEW.content, NEW.metadata);
END
''')
# Indexes
c.execute('CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type)')
c.execute('CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name)')
c.execute('CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id)')
c.execute('CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id)')
c.execute('CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id)')
conn.commit()
conn.close()
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
# --- Entity Operations ---
def add_entity(self, name: str, entity_type: str, content: str = "",
metadata: dict = None, source: str = None) -> str:
"""Add or update an entity."""
self._check_write()
if entity_type not in ENTITY_TYPES.get(self.domain, []):
valid = ENTITY_TYPES.get(self.domain, [])
raise ValueError(f"Invalid type '{entity_type}' for {self.domain}. Valid: {valid}")
conn = self._connect()
c = conn.cursor()
now = time.time()
entity_id = str(uuid.uuid4())
metadata_json = json.dumps(metadata) if metadata else "{}"
# Upsert
c.execute('''
INSERT INTO entities (id, name, type, domain, content, metadata, created_at, updated_at, source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
type = excluded.type,
content = excluded.content,
metadata = excluded.metadata,
updated_at = excluded.updated_at,
source = excluded.source
''', (entity_id, name, entity_type, self.domain, content, metadata_json, now, now, source))
# Get the actual ID (might be existing)
c.execute('SELECT id FROM entities WHERE name = ?', (name,))
row = c.fetchone()
entity_id = row['id'] if row else entity_id
conn.commit()
conn.close()
return entity_id
def get_entity(self, name: str) -> Optional[Dict]:
"""Get entity by name."""
self._check_read()
conn = self._connect()
c = conn.cursor()
c.execute('SELECT * FROM entities WHERE name = ?', (name,))
row = c.fetchone()
conn.close()
if not row:
return None
return {
"id": row["id"],
"name": row["name"],
"type": row["type"],
"domain": row["domain"],
"content": row["content"],
"metadata": json.loads(row["metadata"]) if row["metadata"] else {},
"created_at": row["created_at"],
"updated_at": row["updated_at"],
"source": row["source"],
}
def get_entity_by_id(self, entity_id: str) -> Optional[Dict]:
"""Get entity by ID."""
self._check_read()
conn = self._connect()
c = conn.cursor()
c.execute('SELECT * FROM entities WHERE id = ?', (entity_id,))
row = c.fetchone()
conn.close()
if not row:
return None
return dict(row)
def list_entities(self, entity_type: str = None, limit: int = 100) -> List[Dict]:
"""List entities, optionally filtered by type."""
self._check_read()
conn = self._connect()
c = conn.cursor()
if entity_type:
c.execute('''
SELECT * FROM entities WHERE type = ?
ORDER BY updated_at DESC LIMIT ?
''', (entity_type, limit))
else:
c.execute('''
SELECT * FROM entities
ORDER BY updated_at DESC LIMIT ?
''', (limit,))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
def delete_entity(self, name: str) -> bool:
"""Delete entity and its relations/observations."""
self._check_write()
conn = self._connect()
c = conn.cursor()
# Get entity ID
c.execute('SELECT id FROM entities WHERE name = ?', (name,))
row = c.fetchone()
if not row:
conn.close()
return False
entity_id = row['id']
# Delete relations
c.execute('DELETE FROM relations WHERE source_id = ? OR target_id = ?',
(entity_id, entity_id))
# Delete observations
c.execute('DELETE FROM observations WHERE entity_id = ?', (entity_id,))
# Delete entity
c.execute('DELETE FROM entities WHERE id = ?', (entity_id,))
conn.commit()
conn.close()
return True
# --- Search ---
def search(self, query: str, limit: int = 20) -> List[Dict]:
"""Full-text search across entities."""
self._check_read()
conn = self._connect()
c = conn.cursor()
# FTS5 search
c.execute('''
SELECT e.*, rank
FROM entities_fts fts
JOIN entities e ON e.rowid = fts.rowid
WHERE entities_fts MATCH ?
ORDER BY rank
LIMIT ?
''', (query, limit))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
# --- Relations ---
def add_relation(self, source_name: str, target_name: str,
relation: str, context: str = None, weight: int = 1) -> Optional[str]:
"""Add relation between entities."""
self._check_write()
if relation not in RELATION_TYPES:
raise ValueError(f"Invalid relation: {relation}. Valid: {RELATION_TYPES}")
conn = self._connect()
c = conn.cursor()
# Get entity IDs
c.execute('SELECT id FROM entities WHERE name = ?', (source_name,))
source = c.fetchone()
c.execute('SELECT id FROM entities WHERE name = ?', (target_name,))
target = c.fetchone()
if not source or not target:
conn.close()
return None
rel_id = str(uuid.uuid4())
now = time.time()
c.execute('''
INSERT INTO relations (id, source_id, target_id, relation, context, weight, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (rel_id, source['id'], target['id'], relation, context, weight, now))
conn.commit()
conn.close()
return rel_id
def get_relations(self, entity_name: str, direction: str = "both") -> List[Dict]:
"""Get relations for an entity."""
self._check_read()
conn = self._connect()
c = conn.cursor()
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
row = c.fetchone()
if not row:
conn.close()
return []
entity_id = row['id']
if direction == "outgoing":
c.execute('''
SELECT r.*, e.name as target_name
FROM relations r
JOIN entities e ON e.id = r.target_id
WHERE r.source_id = ?
''', (entity_id,))
elif direction == "incoming":
c.execute('''
SELECT r.*, e.name as source_name
FROM relations r
JOIN entities e ON e.id = r.source_id
WHERE r.target_id = ?
''', (entity_id,))
else:
c.execute('''
SELECT r.*,
s.name as source_name,
t.name as target_name
FROM relations r
JOIN entities s ON s.id = r.source_id
JOIN entities t ON t.id = r.target_id
WHERE r.source_id = ? OR r.target_id = ?
''', (entity_id, entity_id))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
# --- Observations ---
def add_observation(self, entity_name: str, content: str, observer: str = "system") -> Optional[str]:
"""Add observation to an entity."""
self._check_write()
conn = self._connect()
c = conn.cursor()
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
row = c.fetchone()
if not row:
conn.close()
return None
obs_id = str(uuid.uuid4())
now = time.time()
c.execute('''
INSERT INTO observations (id, entity_id, content, observer, created_at)
VALUES (?, ?, ?, ?, ?)
''', (obs_id, row['id'], content, observer, now))
conn.commit()
conn.close()
return obs_id
def get_observations(self, entity_name: str) -> List[Dict]:
"""Get observations for an entity."""
self._check_read()
conn = self._connect()
c = conn.cursor()
c.execute('SELECT id FROM entities WHERE name = ?', (entity_name,))
row = c.fetchone()
if not row:
conn.close()
return []
c.execute('''
SELECT * FROM observations
WHERE entity_id = ?
ORDER BY created_at DESC
''', (row['id'],))
rows = c.fetchall()
conn.close()
return [dict(row) for row in rows]
# --- Stats ---
def stats(self) -> Dict:
"""Get KG statistics."""
conn = self._connect()
c = conn.cursor()
c.execute('SELECT COUNT(*) as count FROM entities')
entities = c.fetchone()['count']
c.execute('SELECT COUNT(*) as count FROM relations')
relations = c.fetchone()['count']
c.execute('SELECT COUNT(*) as count FROM observations')
observations = c.fetchone()['count']
c.execute('SELECT type, COUNT(*) as count FROM entities GROUP BY type')
by_type = {row['type']: row['count'] for row in c.fetchall()}
conn.close()
return {
"domain": self.domain,
"entities": entities,
"relations": relations,
"observations": observations,
"by_type": by_type,
}
# --- Cross-Domain Search ---
def search_all(query: str, limit: int = 20) -> Dict[str, List[Dict]]:
"""Search across all knowledge graphs."""
results = {}
for domain in KG_PATHS.keys():
try:
kg = KnowledgeGraph(domain)
results[domain] = kg.search(query, limit)
except Exception as e:
results[domain] = [{"error": str(e)}]
return results
def get_all_stats() -> Dict[str, Dict]:
"""Get stats from all knowledge graphs."""
stats = {}
for domain in KG_PATHS.keys():
try:
kg = KnowledgeGraph(domain)
stats[domain] = kg.stats()
except Exception as e:
stats[domain] = {"error": str(e)}
return stats
# --- CLI for testing ---
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: knowledge_graph.py <command> [args]")
print("Commands:")
print(" stats - Show all KG stats")
print(" search <query> - Search all KGs")
print(" add <domain> <name> <type> <content>")
print(" get <domain> <name>")
print(" list <domain> [type]")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "stats":
for domain, s in get_all_stats().items():
print(f"\n{domain}:")
for k, v in s.items():
print(f" {k}: {v}")
elif cmd == "search" and len(sys.argv) >= 3:
query = " ".join(sys.argv[2:])
results = search_all(query)
for domain, entities in results.items():
if entities and not entities[0].get("error"):
print(f"\n{domain}:")
for e in entities:
print(f" - {e.get('name', 'unknown')}: {e.get('type', '')}")
elif cmd == "add" and len(sys.argv) >= 5:
domain, name, etype = sys.argv[2:5]
content = " ".join(sys.argv[5:]) if len(sys.argv) > 5 else ""
kg = KnowledgeGraph(domain)
eid = kg.add_entity(name, etype, content)
print(f"Added: {eid}")
elif cmd == "get" and len(sys.argv) >= 4:
kg = KnowledgeGraph(sys.argv[2])
entity = kg.get_entity(sys.argv[3])
print(json.dumps(entity, indent=2))
elif cmd == "list" and len(sys.argv) >= 3:
kg = KnowledgeGraph(sys.argv[2])
etype = sys.argv[3] if len(sys.argv) > 3 else None
for e in kg.list_entities(etype):
print(f" - {e['name']}: {e['type']}")
else:
print(f"Unknown command: {cmd}")
sys.exit(1)