feat: launcher arranca orquestador + idle, pin por role (flow 0012, fase 3b)
- mark_claude_role (python/functions/infra): resuelve PID->sessionId esperando sessions/<PID>.json y escribe role en el goal.json sin pisar el resto. 4 tests. - launch_fleetclaude: el pane derecho arranca el ORQUESTADOR con el skill /orquestador embebido como primer prompt; tras arrancar, mark_claude_role le pone role=orchestrator (en background, no-fatal) para que la TUI lo pinee arriba; ademas siembra 1 ejecutor idle inicial en su propia window. - skill /orquestador: regla 'no te vigiles a ti mismo' (ignora en la cola su propia sesion y cualquier role=orchestrator). Validado en vivo (perfil aislado): claude /orquestador entra en modo, role marcado, idle sembrado, pin correcto, fleet2 intacto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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]
|
||||
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).
|
||||
Reference in New Issue
Block a user