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:
@@ -208,6 +208,8 @@ El contrato sigue `dod_quality.md` (golden + edge + error con evidencia ejecutab
|
|||||||
|
|
||||||
Devuelve `{total_new, events, by_classification, urgent, cursor}`. La clasificación de cada agente la produce `classify_fleet_termination` (pura) desde su estado (status + phase + dod_contract + dod_status + segundos ociosos).
|
Devuelve `{total_new, events, by_classification, urgent, cursor}`. La clasificación de cada agente la produce `classify_fleet_termination` (pura) desde su estado (status + phase + dod_contract + dod_status + segundos ociosos).
|
||||||
|
|
||||||
|
**No te vigiles a ti mismo.** Al procesar la cola, **ignora** los eventos de tu propia sesión y de cualquier agente con `role=orchestrator` (cruza el `session_id` del evento con `list_claude_fleet`). El orquestador no tiene `dod_contract` y aparecería como `MAL_LANZADO` — es ruido, no un ejecutor que vigilar. Solo actúas sobre los **ejecutores** (`role=executor` o sin role).
|
||||||
|
|
||||||
### Políticas por clasificación
|
### Políticas por clasificación
|
||||||
|
|
||||||
| Transición a… | Qué hace el orquestador |
|
| Transición a… | Qué hace el orquestador |
|
||||||
|
|||||||
@@ -196,15 +196,39 @@ USAGE
|
|||||||
left_pane=$($T new-session -d -s "$session" -n console -c "$cwd" -P -F '#{pane_id}')
|
left_pane=$($T new-session -d -s "$session" -n console -c "$cwd" -P -F '#{pane_id}')
|
||||||
$T send-keys -t "$left_pane" "$left_cmd" C-m
|
$T send-keys -t "$left_pane" "$left_cmd" C-m
|
||||||
|
|
||||||
# pane derecho = claude, dividiendo horizontalmente (split lado a lado).
|
# pane derecho = el ORQUESTADOR de la flota: un Claude que arranca ya en
|
||||||
|
# modo orquestador invocando el skill /orquestador como primer prompt. Es
|
||||||
|
# el Claude con el que el humano habla; vigila la flota por su DoD.
|
||||||
right_pane=$($T split-window -h -t "$left_pane" -c "$cwd" -P -F '#{pane_id}')
|
right_pane=$($T split-window -h -t "$left_pane" -c "$cwd" -P -F '#{pane_id}')
|
||||||
$T send-keys -t "$right_pane" "exec claude --dangerously-skip-permissions" C-m
|
$T send-keys -t "$right_pane" "exec claude --dangerously-skip-permissions '/orquestador'" C-m
|
||||||
|
|
||||||
# Fijar el ancho del pane izquierdo en columnas.
|
# Fijar el ancho del pane izquierdo en columnas.
|
||||||
$T resize-pane -t "$left_pane" -x "$cols"
|
$T resize-pane -t "$left_pane" -x "$cols"
|
||||||
|
|
||||||
# Foco inicial en el pane de claude (derecha).
|
# Foco inicial en el pane del orquestador (derecha).
|
||||||
$T select-pane -t "$right_pane"
|
$T select-pane -t "$right_pane"
|
||||||
|
|
||||||
|
# Marcar el orquestador con role=orchestrator en su goal.json para que la
|
||||||
|
# TUI lo pinee arriba (estrella). El sessionId no se conoce hasta que
|
||||||
|
# Claude escribe sessions/<PID>.json; mark_claude_role resuelve
|
||||||
|
# PID->sessionId esperando ese archivo. En background (no bloquea el
|
||||||
|
# arranque) y con sleep para que el `exec claude` reemplace al shell antes
|
||||||
|
# de leer el pane_pid. Fallo = no-fatal (el orquestador no se pinea).
|
||||||
|
if [[ -x "$repo_root/fn" ]]; then
|
||||||
|
( sleep 1
|
||||||
|
orch_pid=$($T display-message -p -t "$right_pane" '#{pane_pid}' 2>/dev/null || true)
|
||||||
|
[[ -n "$orch_pid" ]] && "$repo_root/fn" run mark_claude_role "$orch_pid" orchestrator >/dev/null 2>&1
|
||||||
|
) >/dev/null 2>&1 &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sembrar 1 ejecutor idle: una window detached con un claude normal,
|
||||||
|
# listo para recibir tarea del orquestador. Aparece en la TUI bajo el
|
||||||
|
# orquestador (role executor por defecto). Hereda FLEET_SOCKET/SESSION
|
||||||
|
# del server (set-environment), asi apunta a este perfil.
|
||||||
|
local idle_pane
|
||||||
|
idle_pane=$($T new-window -d -t "$session" -n claude -c "$cwd" -P -F '#{pane_id}')
|
||||||
|
$T send-keys -t "$idle_pane" "exec claude --dangerously-skip-permissions" C-m
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Si reutilizamos sesion (o por seguridad), derivar el pane ID de la TUI:
|
# Si reutilizamos sesion (o por seguridad), derivar el pane ID de la TUI:
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"""Marca el role de una sesion de Claude Code resolviendo PID -> sessionId.
|
||||||
|
|
||||||
|
El meta-orquestador de flota arranca un Claude Code y necesita marcarlo con su
|
||||||
|
role (orchestrator | executor) en su `goal.json` para que la TUI fleetview lo
|
||||||
|
clasifique (p. ej. pinear el orquestador arriba). El problema es de timing: el
|
||||||
|
`sessionId` no se conoce al arrancar el proceso; Claude Code escribe el archivo
|
||||||
|
`~/.claude/sessions/<PID>.json` (que contiene el `sessionId`) unos segundos
|
||||||
|
despues. Esta funcion espera ese archivo sondeando, resuelve el sessionId y
|
||||||
|
escribe SOLO la clave `role` en `~/.claude/goals/<sessionId>.json` preservando
|
||||||
|
todos los demas campos del goal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
_VALID_ROLES = ("orchestrator", "executor")
|
||||||
|
|
||||||
|
|
||||||
|
def mark_claude_role(
|
||||||
|
pid: int,
|
||||||
|
role: str,
|
||||||
|
wait_s: float = 10.0,
|
||||||
|
sessions_dir: str | None = None,
|
||||||
|
goals_dir: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Resuelve PID -> sessionId esperando el sessions/<pid>.json y marca el role.
|
||||||
|
|
||||||
|
Sondea `<sessions_dir>/<pid>.json` cada ~0.25s hasta `wait_s` segundos (medido
|
||||||
|
con time.monotonic) esperando que exista y contenga un `sessionId` no vacio.
|
||||||
|
Si lo encuentra, abre `<goals_dir>/<sessionId>.json` (dict vacio si no existe),
|
||||||
|
setea SOLO la clave `role` preservando el resto (goal, phase, dod, etc.), y lo
|
||||||
|
escribe atomicamente (tmp + os.replace). Si expira, devuelve un dict de error
|
||||||
|
sin lanzar excepcion (el launcher decide que hacer).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pid: PID del proceso Claude Code recien arrancado.
|
||||||
|
role: "orchestrator" o "executor". Cualquier otro valor lanza ValueError
|
||||||
|
sin tocar disco.
|
||||||
|
wait_s: segundos maximos a esperar el sessions/<pid>.json. Default 10.0.
|
||||||
|
sessions_dir: directorio de los sessions JSON. Default ~/.claude/sessions.
|
||||||
|
goals_dir: directorio de los goal JSON. Default ~/.claude/goals.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Si tuvo exito: dict con ok=True, pid, session_id, role y path (ruta del
|
||||||
|
goal escrito). Si expiro el poll: dict con ok=False, error (mensaje) y pid.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si `role` no esta en ("orchestrator", "executor").
|
||||||
|
"""
|
||||||
|
if role not in _VALID_ROLES:
|
||||||
|
raise ValueError(
|
||||||
|
f"role invalido: {role!r}. Debe ser uno de {_VALID_ROLES}"
|
||||||
|
)
|
||||||
|
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
if sessions_dir is None:
|
||||||
|
sessions_dir = os.path.join(home, ".claude", "sessions")
|
||||||
|
if goals_dir is None:
|
||||||
|
goals_dir = os.path.join(home, ".claude", "goals")
|
||||||
|
|
||||||
|
session_path = os.path.join(sessions_dir, f"{pid}.json")
|
||||||
|
|
||||||
|
# POLL: espera hasta wait_s segundos a que exista sessions/<pid>.json con un
|
||||||
|
# sessionId no vacio. Bucle corto con time.monotonic como deadline (no un
|
||||||
|
# unico sleep largo) para reaccionar en cuanto aparezca el archivo.
|
||||||
|
deadline = time.monotonic() + wait_s
|
||||||
|
sid = ""
|
||||||
|
while True:
|
||||||
|
sid = _read_session_id(session_path)
|
||||||
|
if sid:
|
||||||
|
break
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"error": f"timeout esperando sessions/{pid}.json",
|
||||||
|
"pid": pid,
|
||||||
|
}
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
goal_path = os.path.join(goals_dir, f"{sid}.json")
|
||||||
|
|
||||||
|
# Lee el goal existente (dict vacio si no existe o es ilegible/no-dict).
|
||||||
|
goal: dict = {}
|
||||||
|
if os.path.exists(goal_path):
|
||||||
|
try:
|
||||||
|
with open(goal_path, "r", encoding="utf-8") as fh:
|
||||||
|
loaded = json.load(fh)
|
||||||
|
if isinstance(loaded, dict):
|
||||||
|
goal = loaded
|
||||||
|
except (json.JSONDecodeError, ValueError, OSError):
|
||||||
|
goal = {}
|
||||||
|
|
||||||
|
# Setea SOLO la clave role, preservando todo lo demas.
|
||||||
|
goal["role"] = role
|
||||||
|
|
||||||
|
# Escritura atomica: tmp + os.replace en el mismo directorio.
|
||||||
|
os.makedirs(goals_dir, exist_ok=True)
|
||||||
|
tmp_path = goal_path + f".tmp.{os.getpid()}"
|
||||||
|
with open(tmp_path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(goal, fh, ensure_ascii=False, indent=2)
|
||||||
|
fh.flush()
|
||||||
|
os.fsync(fh.fileno())
|
||||||
|
os.replace(tmp_path, goal_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"pid": pid,
|
||||||
|
"session_id": sid,
|
||||||
|
"role": role,
|
||||||
|
"path": goal_path,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_session_id(session_path: str) -> str:
|
||||||
|
"""Lee el sessionId del sessions/<pid>.json. Devuelve "" si falta/ilegible/vacio."""
|
||||||
|
if not os.path.exists(session_path):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
with open(session_path, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
except (json.JSONDecodeError, ValueError, OSError):
|
||||||
|
return ""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return ""
|
||||||
|
sid = data.get("sessionId")
|
||||||
|
if isinstance(sid, str) and sid:
|
||||||
|
return sid
|
||||||
|
return ""
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Tests para mark_claude_role."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mark_claude_role import mark_claude_role
|
||||||
|
|
||||||
|
|
||||||
|
def _write_session(sessions_dir, pid, session_id):
|
||||||
|
"""Escribe un sessions/<pid>.json con el sessionId dado."""
|
||||||
|
os.makedirs(sessions_dir, exist_ok=True)
|
||||||
|
path = os.path.join(sessions_dir, f"{pid}.json")
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump({"sessionId": session_id, "cwd": "/tmp/whatever"}, fh)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _write_goal(goals_dir, session_id, goal):
|
||||||
|
"""Escribe un goal/<sessionId>.json con el dict dado."""
|
||||||
|
os.makedirs(goals_dir, exist_ok=True)
|
||||||
|
path = os.path.join(goals_dir, f"{session_id}.json")
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(goal, fh)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _read_goal(goals_dir, session_id):
|
||||||
|
path = os.path.join(goals_dir, f"{session_id}.json")
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sessions_presente_resuelve_y_escribe_role_preservando_otros_campos(tmp_path):
|
||||||
|
sessions_dir = str(tmp_path / "sessions")
|
||||||
|
goals_dir = str(tmp_path / "goals")
|
||||||
|
pid = 4242
|
||||||
|
sid = "abc-123-uuid"
|
||||||
|
|
||||||
|
_write_session(sessions_dir, pid, sid)
|
||||||
|
# Goal preexistente con campos que NO deben perderse.
|
||||||
|
_write_goal(
|
||||||
|
goals_dir,
|
||||||
|
sid,
|
||||||
|
{
|
||||||
|
"goal": "implementar fase 3",
|
||||||
|
"phase": "trabajando",
|
||||||
|
"dod": ["compila", "tests verdes"],
|
||||||
|
"dod_contract": {"capa1": "mecanica"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
res = mark_claude_role(pid, "orchestrator", wait_s=2.0,
|
||||||
|
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||||
|
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["pid"] == pid
|
||||||
|
assert res["session_id"] == sid
|
||||||
|
assert res["role"] == "orchestrator"
|
||||||
|
assert res["path"] == os.path.join(goals_dir, f"{sid}.json")
|
||||||
|
|
||||||
|
goal = _read_goal(goals_dir, sid)
|
||||||
|
assert goal["role"] == "orchestrator"
|
||||||
|
# Todos los demas campos del goal se preservan intactos.
|
||||||
|
assert goal["goal"] == "implementar fase 3"
|
||||||
|
assert goal["phase"] == "trabajando"
|
||||||
|
assert goal["dod"] == ["compila", "tests verdes"]
|
||||||
|
assert goal["dod_contract"] == {"capa1": "mecanica"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_role_invalido_lanza_value_error_sin_escribir(tmp_path):
|
||||||
|
sessions_dir = str(tmp_path / "sessions")
|
||||||
|
goals_dir = str(tmp_path / "goals")
|
||||||
|
pid = 4242
|
||||||
|
sid = "abc-123-uuid"
|
||||||
|
_write_session(sessions_dir, pid, sid)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
mark_claude_role(pid, "supervisor", wait_s=2.0,
|
||||||
|
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||||
|
|
||||||
|
# No escribio nada: el goals_dir ni siquiera deberia existir.
|
||||||
|
assert not os.path.exists(goals_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sessions_ausente_devuelve_timeout_sin_crash(tmp_path):
|
||||||
|
sessions_dir = str(tmp_path / "sessions")
|
||||||
|
goals_dir = str(tmp_path / "goals")
|
||||||
|
pid = 9999 # sin sessions/<pid>.json escrito
|
||||||
|
|
||||||
|
res = mark_claude_role(pid, "executor", wait_s=0.5,
|
||||||
|
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||||
|
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert res["pid"] == pid
|
||||||
|
assert "timeout" in res["error"]
|
||||||
|
assert f"{pid}.json" in res["error"]
|
||||||
|
# No se escribio ningun goal.
|
||||||
|
assert not os.path.exists(goals_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_goal_inexistente_se_crea_con_solo_role(tmp_path):
|
||||||
|
sessions_dir = str(tmp_path / "sessions")
|
||||||
|
goals_dir = str(tmp_path / "goals")
|
||||||
|
pid = 7
|
||||||
|
sid = "fresh-session-uuid"
|
||||||
|
_write_session(sessions_dir, pid, sid)
|
||||||
|
# No existe goal previo para esta sesion.
|
||||||
|
|
||||||
|
res = mark_claude_role(pid, "executor", wait_s=2.0,
|
||||||
|
sessions_dir=sessions_dir, goals_dir=goals_dir)
|
||||||
|
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["session_id"] == sid
|
||||||
|
assert res["role"] == "executor"
|
||||||
|
|
||||||
|
goal = _read_goal(goals_dir, sid)
|
||||||
|
assert goal == {"role": "executor"}
|
||||||
Reference in New Issue
Block a user