--- name: mark_claude_role kind: function lang: py domain: infra version: "1.0.0" purity: impure signature: "def mark_claude_role(pid: int, role: str, wait_s: float = 10.0, sessions_dir: str | None = None, goals_dir: str | None = None) -> dict" description: "Marca el role (orchestrator | executor) de una sesion de Claude Code resolviendo PID -> sessionId. Sondea ~/.claude/sessions/.json (escrito por Claude Code unos segundos despues de arrancar) hasta wait_s segundos con deadline time.monotonic, extrae el sessionId y escribe SOLO la clave `role` en ~/.claude/goals/.json preservando el resto del goal (goal, phase, dod, dod_contract...). Escritura atomica tmp + os.replace. Si el sessions JSON no aparece a tiempo devuelve ok=False timeout sin lanzar. Pensado para el launcher del meta-orquestador de flota (fleetview) que necesita clasificar el orquestador frente a los executors." tags: [fleet, claude-fleet, role, session, goal, orchestrator, launcher, pid] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: ["os", "json", "time"] params: - name: pid desc: "PID del proceso Claude Code recien arrancado. Se usa para localizar ~/.claude/sessions/.json" - name: role desc: "role a marcar: 'orchestrator' o 'executor'. Cualquier otro valor lanza ValueError sin tocar disco" - name: wait_s desc: "segundos maximos a esperar (sondeo cada ~0.25s) a que aparezca sessions/.json con un sessionId no vacio. Default 10.0" - name: sessions_dir desc: "directorio de los sessions JSON. Default ~/.claude/sessions" - name: goals_dir desc: "directorio de los goal JSON. Default ~/.claude/goals (se crea si falta)" output: "dict. En exito: ok=True, pid (int), session_id (str), role (str), path (ruta del goal escrito). En timeout del poll: ok=False, error (str con 'timeout esperando sessions/.json'), pid (int). NO lanza en timeout; el launcher decide" tested: true tests: - "sessions presente resuelve y escribe role preservando otros campos" - "role invalido lanza ValueError sin escribir" - "sessions ausente devuelve timeout sin crash" - "goal inexistente se crea con solo role" test_file_path: "python/functions/infra/mark_claude_role_test.py" file_path: "python/functions/infra/mark_claude_role.py" --- ## Ejemplo ```python import os from infra.mark_claude_role import mark_claude_role # El launcher de flota acaba de arrancar un Claude Code orquestador (subprocess). proc = launch_claude_orchestrator(...) # devuelve un objeto con .pid # Marca su role: espera a que Claude Code escriba ~/.claude/sessions/.json # (puede tardar unos segundos) y luego pone role=orchestrator en su goal.json. res = mark_claude_role(proc.pid, "orchestrator", wait_s=15.0) if res["ok"]: print(f"marcado: {res['session_id']} -> {res['role']} en {res['path']}") else: # Timeout: el sessions/.json no aparecio a tiempo. El launcher decide # (reintentar mas tarde, loguear, dejar sin role hasta el proximo barrido). print("no se pudo marcar:", res["error"]) ``` ## Cuando usarla Usala en el launcher del meta-orquestador de flota justo despues de arrancar un proceso Claude Code, cuando ya conoces su PID pero todavia NO su sessionId. Resuelve el mapeo PID -> sessionId esperando el archivo que Claude Code escribe unos segundos despues, y deja marcado el role en el goal.json para que la TUI fleetview clasifique al orquestador frente a los executors. Tambien sirve para re-marcar el role de una sesion ya existente sin pisar el resto de su goal. ## Gotchas - **Impura**: lee `~/.claude/sessions/.json` y escribe `~/.claude/goals/.json` (crea `goals_dir` si falta). - **Timing del sessions JSON**: Claude Code NO escribe `sessions/.json` al instante de arrancar — tarda unos segundos. Por eso la funcion sondea hasta `wait_s` (deadline con `time.monotonic`, no un unico `sleep` largo) y reacciona en cuanto el archivo aparece con un `sessionId` no vacio. Ajusta `wait_s` segun cuanto tarde tu maquina en arrancar la sesion. - **Timeout NO lanza**: si el archivo no aparece a tiempo devuelve `{"ok": False, "error": "timeout esperando sessions/.json", "pid": pid}`. Es decision deliberada: el launcher decide si reintentar o continuar. Solo `role` invalido lanza (ValueError), y lo hace antes de tocar disco. - **NO pisa otros campos del goal**: abre el goal existente (dict vacio si no existe o es JSON invalido / no-dict), setea UNICAMENTE la clave `role` y conserva todo lo demas (`goal`, `phase`, `dod`, `dod_contract`, etc.). Si el goal.json no existia, lo crea con solo `{"role": ...}`. - **Escritura atomica**: escribe a un archivo temporal y hace `os.replace`, asi un lector concurrente (la TUI fleetview) nunca ve un goal.json a medio escribir. - **sessionId vacio o sessions JSON ilegible**: se trata como "aun no listo" y se sigue sondeando hasta el deadline (no es un error inmediato).