---
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/<pid>.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/<sessionId>.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, orchestration]
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/<pid>.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/<pid>.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/<pid>.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/<pid>.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/<pid>.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/<pid>.json` y escribe
  `~/.claude/goals/<sessionId>.json` (crea `goals_dir` si falta).
- **Timing del sessions JSON**: Claude Code NO escribe `sessions/<pid>.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/<pid>.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).
