#!/usr/bin/env python3 import argparse import json import re from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] STATUS_PATH = ROOT / 'work' / 'runtime-status.json' MATRIX_PATH = ROOT / 'harness' / 'agents.matrix.yml' ARTIFACTS_DIR = ROOT / 'work' / 'artifacts' DEFAULT_EMOJIS = { 'leader': '🧭', 'triager': '🧩', 'architect': 'πŸ—οΈ', 'implementer': 'πŸ› οΈ', 'reviewer': 'πŸ”', 'security': 'πŸ”’', 'qa': 'πŸ§ͺ', 'documenter': 'πŸ“š', } GATE_FILES = { 'reviewer': 'reviewer.json', 'security': 'security.json', 'qa': 'qa.json', 'documenter': 'documenter.md', 'leader': 'leader-close.json', } def now_iso(): return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z') def load_json(path: Path, default=None): if not path.exists(): return default return json.loads(path.read_text(encoding='utf-8')) def save_json(path: Path, payload): path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') def load_role_emojis(): emojis = dict(DEFAULT_EMOJIS) if not MATRIX_PATH.exists(): return emojis current_role = None for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines(): match_role = re.match(r'^ ([a-z_]+):\s*$', line) if match_role: current_role = match_role.group(1) continue match_emoji = re.match(r'^\s{4}emoji:\s*["\']?(.*?)["\']?\s*$', line) if match_emoji and current_role: emojis[current_role] = match_emoji.group(1) return emojis def default_status(): return { 'feature_id': None, 'stage': 'idle', 'agent': 'leader', 'action': 'Sin ejecuciΓ³n activa', 'state': 'waiting', 'next_agent': 'leader', 'waiting_for': 'Seleccionar una feature pending y actualizar este estado', 'updated_at': now_iso(), 'timeline': [], } def load_status(): status = load_json(STATUS_PATH, default_status()) base = default_status() for key, value in base.items(): status.setdefault(key, value) if not isinstance(status.get('timeline'), list): status['timeline'] = [] return status def gate_status(feature_id): gates = {} if not feature_id: return gates feature_dir = ARTIFACTS_DIR / feature_id for gate, filename in GATE_FILES.items(): path = feature_dir / filename if not path.exists(): gates[gate] = 'pending' continue if gate == 'documenter': gates[gate] = 'approved' continue try: payload = json.loads(path.read_text(encoding='utf-8')) gates[gate] = 'approved' if payload.get('verdict') == 'APPROVED' else 'present' except Exception: gates[gate] = 'invalid' return gates def render_gate(gate, state, emojis): icon = { 'approved': 'βœ…', 'pending': '⏳', 'present': '⚠️', 'invalid': '❌', }.get(state, 'β€’') label = { 'leader': 'close', 'documenter': 'docs', }.get(gate, gate) return f"{icon} {emojis.get(gate, 'β€’')} {label}: {state.upper()}" def show_status(): status = load_status() emojis = load_role_emojis() feature_id = status.get('feature_id') or 'β€”' current_agent = status.get('agent', 'leader') next_agent = status.get('next_agent') or 'β€”' gates = gate_status(status.get('feature_id')) print('╔══════════════════════════════════════════════════════════════╗') print('β•‘ ARNES Β· Runtime Status β•‘') print('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•') print(f"Feature activa : {feature_id}") print(f"Stage actual : {status.get('stage', 'β€”')}") print(f"Agente actual : {emojis.get(current_agent, 'β€’')} {current_agent}") print(f"AcciΓ³n : {status.get('action', 'β€”')}") print(f"Estado : {status.get('state', 'β€”')}") print(f"Siguiente : {emojis.get(next_agent, 'β€’')} {next_agent}") print(f"Esperando : {status.get('waiting_for', 'β€”')}") print(f"Actualizado : {status.get('updated_at', 'β€”')}") print() print('Gates') if gates: for gate in ['reviewer', 'security', 'qa', 'documenter', 'leader']: print(f" {render_gate(gate, gates.get(gate, 'pending'), emojis)}") else: print(' β€” Sin feature activa β€”') print() print('Timeline') timeline = status.get('timeline', [])[-8:] if not timeline: print(' β€” Sin eventos β€”') return for item in timeline: agent = item.get('agent', 'leader') emoji = emojis.get(agent, 'β€’') ts = item.get('ts', 'β€”') stage = item.get('stage', 'β€”') state = item.get('state', 'β€”') message = item.get('message', '') print(f" - {ts} Β· {emoji} {agent} Β· {stage} Β· {state} Β· {message}") def set_status(args): status = load_status() if args.feature_id is not None: status['feature_id'] = args.feature_id or None if args.stage is not None: status['stage'] = args.stage if args.agent is not None: status['agent'] = args.agent if args.action is not None: status['action'] = args.action if args.state is not None: status['state'] = args.state if args.next_agent is not None: status['next_agent'] = args.next_agent if args.waiting_for is not None: status['waiting_for'] = args.waiting_for status['updated_at'] = now_iso() event_message = args.note or status.get('action') or 'Estado actualizado' status['timeline'].append({ 'ts': status['updated_at'], 'agent': status.get('agent', 'leader'), 'stage': status.get('stage', 'β€”'), 'state': status.get('state', 'β€”'), 'message': event_message, }) status['timeline'] = status['timeline'][-20:] save_json(STATUS_PATH, status) show_status() def reset_status(_args): status = default_status() status['updated_at'] = now_iso() save_json(STATUS_PATH, status) show_status() def build_parser(): parser = argparse.ArgumentParser(description='Renderiza y actualiza el estado visible de ARNES.') sub = parser.add_subparsers(dest='command', required=True) sub.add_parser('show', help='Muestra el panel visible de estado') set_parser = sub.add_parser('set', help='Actualiza el estado runtime y aΓ±ade evento a timeline') set_parser.add_argument('--feature-id') set_parser.add_argument('--stage') set_parser.add_argument('--agent') set_parser.add_argument('--action') set_parser.add_argument('--state') set_parser.add_argument('--next-agent') set_parser.add_argument('--waiting-for') set_parser.add_argument('--note') sub.add_parser('reset', help='Resetea el estado runtime a idle') return parser def main(): parser = build_parser() args = parser.parse_args() if args.command == 'show': show_status() elif args.command == 'set': set_status(args) elif args.command == 'reset': reset_status(args) else: parser.print_help() return 1 return 0 if __name__ == '__main__': raise SystemExit(main())