tramalia-cli 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tramalia/__init__.py +11 -0
- tramalia/__main__.py +96 -0
- tramalia/cli/__init__.py +0 -0
- tramalia/cli/commands.py +363 -0
- tramalia/cli/menu.py +56 -0
- tramalia/cli/render.py +133 -0
- tramalia/core/__init__.py +0 -0
- tramalia/core/context.py +71 -0
- tramalia/core/detect.py +59 -0
- tramalia/core/doctor.py +63 -0
- tramalia/core/evidence.py +53 -0
- tramalia/core/governance.py +193 -0
- tramalia/core/handoff.py +38 -0
- tramalia/core/proc.py +26 -0
- tramalia/core/project.py +61 -0
- tramalia/core/scaffold.py +174 -0
- tramalia/core/skills.py +68 -0
- tramalia/core/tools.py +119 -0
- tramalia/mcp_server.py +94 -0
- tramalia/templates/project/.claude/agents/documentador.md +12 -0
- tramalia/templates/project/.claude/agents/ejecutor.md +15 -0
- tramalia/templates/project/.claude/agents/planificador.md +14 -0
- tramalia/templates/project/.claude/agents/resolutor-profundo.md +15 -0
- tramalia/templates/project/.claude/agents/revisor.md +14 -0
- tramalia/templates/project/.tramalia/config.json.jinja +15 -0
- tramalia/templates/project/.tramalia/current-task.md +8 -0
- tramalia/templates/project/.tramalia/skills/01-spec-governance/SKILL.md +26 -0
- tramalia/templates/project/.tramalia/skills/02-federated-agent-memory/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/03-context-token-saver/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/04-minimalist-engineering/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/05-code-quality-review/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/06-security-gate/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/07-database-engineering/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/08-tool-execution-gate/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/09-observability-first/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/10-evidence-and-handoff/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/11-legacy-modernization/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/12-multi-agent-review/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills/13-documentation-handoff/SKILL.md +25 -0
- tramalia/templates/project/.tramalia/skills.toml +10 -0
- tramalia/templates/project/AGENTS.md.jinja +40 -0
- tramalia/templates/project/CLAUDE.md.jinja +8 -0
- tramalia/templates/project/docs/ai/00-resumen-proyecto.md.jinja +21 -0
- tramalia/templates/project/docs/ai/01-arquitectura.md +23 -0
- tramalia/templates/project/docs/ai/02-reglas-codigo.md +17 -0
- tramalia/templates/project/docs/ai/03-reglas-base-datos.md +18 -0
- tramalia/templates/project/docs/ai/04-reglas-seguridad.md +13 -0
- tramalia/templates/project/docs/ai/05-decisiones-adr.md +9 -0
- tramalia/templates/project/docs/ai/06-intentos-fallidos.md +14 -0
- tramalia/templates/project/docs/ai/07-handoff-agentes.md +4 -0
- tramalia/templates/project/docs/ai/08-comandos-proyecto.md +16 -0
- tramalia/templates/project/docs/ai/09-quality-gates.md +10 -0
- tramalia/templates/project/docs/ai/10-contexto-operativo.md +16 -0
- tramalia/templates/project/docs/ai/11-reglas-ux-ui.md +12 -0
- tramalia/templates/project/specs/checklist.md +12 -0
- tramalia/templates/project/specs/constitution.md +9 -0
- tramalia/templates/project/specs/plan.md +13 -0
- tramalia/templates/project/specs/specification.md +16 -0
- tramalia/templates/project/specs/tasks.md +12 -0
- tramalia/tui.py +140 -0
- tramalia_cli-0.9.1.dist-info/METADATA +180 -0
- tramalia_cli-0.9.1.dist-info/RECORD +66 -0
- tramalia_cli-0.9.1.dist-info/WHEEL +4 -0
- tramalia_cli-0.9.1.dist-info/entry_points.txt +2 -0
- tramalia_cli-0.9.1.dist-info/licenses/LICENSE +201 -0
- tramalia_cli-0.9.1.dist-info/licenses/NOTICE +5 -0
tramalia/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Tramalia: capa fina de orquestación para desarrollo con múltiples agentes IA.
|
|
2
|
+
|
|
3
|
+
Principio de diseño: Tramalia no implementa capacidades, las orquesta.
|
|
4
|
+
Diagnostica (doctor), delega la ejecución a herramientas externas (mise, copier,
|
|
5
|
+
serena, semgrep, ...) y solo construye lo que nadie más hace bien
|
|
6
|
+
(detector, evidence pack y handoff).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.9.1"
|
|
10
|
+
__author__ = "Michael Jim Scott Bravo"
|
|
11
|
+
__license__ = "Apache-2.0"
|
tramalia/__main__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Punto de entrada de la CLI. Usa argparse (stdlib) para no exigir dependencias;
|
|
2
|
+
Rich/Questionary se activan solos si están instalados (extra `pretty`).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from tramalia import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
p = argparse.ArgumentParser(
|
|
15
|
+
prog="tramalia",
|
|
16
|
+
description="Capa fina que orquesta herramientas externas para desarrollo con agentes IA.",
|
|
17
|
+
)
|
|
18
|
+
p.add_argument("--version", action="version", version=f"tramalia {__version__}")
|
|
19
|
+
p.add_argument("--plain", action="store_true", help="salida sin colores (terminal básica)")
|
|
20
|
+
sub = p.add_subparsers(dest="command")
|
|
21
|
+
|
|
22
|
+
sub.add_parser("menu", help="menú interactivo")
|
|
23
|
+
d = sub.add_parser("doctor", help="diagnostica las herramientas requeridas")
|
|
24
|
+
d.add_argument("--fix", action="store_true", help="intenta instalar lo que falte vía mise")
|
|
25
|
+
sub.add_parser("detect", help="detecta el stack del proyecto")
|
|
26
|
+
ini = sub.add_parser("init", help="inicializa la estructura (copier)")
|
|
27
|
+
ini.add_argument("--with-headroom", action="store_true",
|
|
28
|
+
help="agrega Headroom (compresión) al .mcp.json (opt-in explícito)")
|
|
29
|
+
ini.add_argument("--with-ponytail", action="store_true",
|
|
30
|
+
help="agrega el MCP de Ponytail al .mcp.json (requiere `tramalia skills` + Node)")
|
|
31
|
+
sub.add_parser("gates", help="ejecuta quality gates (mise run gates)")
|
|
32
|
+
sub.add_parser("context", help="genera contexto / token-saver (repomix + serena)")
|
|
33
|
+
ev = sub.add_parser("evidence", help="genera el evidence pack (ej: tramalia evidence TASK-001)")
|
|
34
|
+
ev.add_argument("task_pos", nargs="?", metavar="TAREA", default=None,
|
|
35
|
+
help="ID de la tarea; si se omite, se usa .tramalia/current-task.md")
|
|
36
|
+
ev.add_argument("--task", default=None, help="ID de la tarea (alternativa al posicional)")
|
|
37
|
+
ev.add_argument("--engram", action="store_true",
|
|
38
|
+
help="exporta a Engram (memoria persistente N2, opt-in)")
|
|
39
|
+
ho = sub.add_parser("handoff", help="crea un handoff multiagente (ej: tramalia handoff TASK-001)")
|
|
40
|
+
ho.add_argument("task_pos", nargs="?", metavar="TAREA", default=None,
|
|
41
|
+
help="ID de la tarea; si se omite, se usa .tramalia/current-task.md")
|
|
42
|
+
ho.add_argument("--task", default=None, help="ID de la tarea (alternativa al posicional)")
|
|
43
|
+
ho.add_argument("--agent", default=None, help="agente ejecutor (def: config agents.primary)")
|
|
44
|
+
ho.add_argument("--reviewer", default=None, help="agente revisor (def: config agents.reviewer)")
|
|
45
|
+
ho.add_argument("--engram", action="store_true",
|
|
46
|
+
help="exporta a Engram (memoria persistente N2, opt-in)")
|
|
47
|
+
cl = sub.add_parser("close",
|
|
48
|
+
help="ritual de cierre: gates → evidence → handoff (ej: tramalia close TASK-001)")
|
|
49
|
+
cl.add_argument("task_pos", nargs="?", metavar="TAREA", default=None,
|
|
50
|
+
help="ID de la tarea; si se omite, se usa .tramalia/current-task.md")
|
|
51
|
+
cl.add_argument("--task", default=None, help="ID de la tarea (alternativa al posicional)")
|
|
52
|
+
cl.add_argument("--agent", default=None, help="agente ejecutor (def: config agents.primary)")
|
|
53
|
+
cl.add_argument("--reviewer", default=None, help="agente revisor (def: config agents.reviewer)")
|
|
54
|
+
cl.add_argument("--allow-fail", action="store_true",
|
|
55
|
+
help="cierra aunque fallen gates (requiere excepción documentada)")
|
|
56
|
+
cl.add_argument("--model", default=None,
|
|
57
|
+
help="modelo usado por el agente ejecutor (queda en metadata.json)")
|
|
58
|
+
cl.add_argument("--engram", action="store_true", help="exporta el cierre a Engram (N2)")
|
|
59
|
+
sub.add_parser("log", help="pista de auditoría: cierres registrados (evidence packs)")
|
|
60
|
+
sy = sub.add_parser("sync", help="propaga reglas y subagentes a otros agentes (rulesync)")
|
|
61
|
+
sy.add_argument("--to", default=None,
|
|
62
|
+
help="targets separados por coma (def: copilot,cursor,cline)")
|
|
63
|
+
sy.add_argument("--features", default=None,
|
|
64
|
+
help="features de rulesync a propagar (def: rules,subagents)")
|
|
65
|
+
sk = sub.add_parser("skills", help="administra skills declaradas (.tramalia/skills.toml)")
|
|
66
|
+
sk.add_argument("action", nargs="?", choices=["sync", "list"], default="sync")
|
|
67
|
+
sub.add_parser("update", help="actualiza todo (mise + copier + skills)")
|
|
68
|
+
sub.add_parser("mcp", help="levanta el Tramalia MCP (fachada nivel 1, stdio)")
|
|
69
|
+
sub.add_parser("ui", help="abre el dashboard TUI (requiere extra [tui])")
|
|
70
|
+
return p
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main(argv: list[str] | None = None) -> int:
|
|
74
|
+
# En Windows la consola puede no ser UTF-8; lo forzamos para acentos y símbolos.
|
|
75
|
+
for stream in (sys.stdout, sys.stderr):
|
|
76
|
+
try:
|
|
77
|
+
stream.reconfigure(encoding="utf-8")
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
raw = list(argv if argv is not None else sys.argv[1:])
|
|
82
|
+
# --plain se acepta en cualquier posición; se extrae antes de parsear
|
|
83
|
+
plain = "--plain" in raw
|
|
84
|
+
raw = [a for a in raw if a != "--plain"]
|
|
85
|
+
args = build_parser().parse_args(raw)
|
|
86
|
+
|
|
87
|
+
from tramalia.cli import render
|
|
88
|
+
if plain:
|
|
89
|
+
render.set_plain(True)
|
|
90
|
+
|
|
91
|
+
from tramalia.cli import commands
|
|
92
|
+
return commands.dispatch(args.command or "menu", args)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
raise SystemExit(main())
|
tramalia/cli/__init__.py
ADDED
|
File without changes
|
tramalia/cli/commands.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Implementación de cada comando. La mayoría hace *shell-out* transparente a la
|
|
2
|
+
herramienta real (regla de diseño: el façade muestra el comando, pasa la salida
|
|
3
|
+
tal cual y nunca esconde errores). doctor/detect son lógica propia.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from tramalia.cli import menu, render
|
|
12
|
+
from tramalia.core import doctor as doctor_core
|
|
13
|
+
from tramalia.core import proc
|
|
14
|
+
from tramalia.core.detect import detect_stack, enabled_features
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_initialized(root: Path) -> bool:
|
|
18
|
+
return (root / "AGENTS.md").exists() or (root / ".tramalia").exists()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run(cmd: list[str]) -> int:
|
|
22
|
+
"""Ejecuta un comando externo mostrando exactamente su salida."""
|
|
23
|
+
render.info(f"→ {' '.join(cmd)}")
|
|
24
|
+
try:
|
|
25
|
+
return proc.run(cmd).returncode
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
render.err(f"no se encontró '{cmd[0]}'. Corre `tramalia doctor` para instalarlo.")
|
|
28
|
+
return 127
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
|
|
33
|
+
def cmd_doctor(args) -> int:
|
|
34
|
+
report = doctor_core.diagnose(Path.cwd())
|
|
35
|
+
code = render.doctor(report)
|
|
36
|
+
if getattr(args, "fix", False) and report.missing_blocking:
|
|
37
|
+
if doctor_core.fix(report):
|
|
38
|
+
render.info("`mise install` ejecutado; re-evaluando…")
|
|
39
|
+
return render.doctor(doctor_core.diagnose(Path.cwd()))
|
|
40
|
+
render.warn("no se pudo auto-instalar (instala primero mise — ver enlace arriba).")
|
|
41
|
+
return code
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_detect(args) -> int:
|
|
45
|
+
root = Path.cwd()
|
|
46
|
+
stack = detect_stack(root)
|
|
47
|
+
feats = enabled_features(stack)
|
|
48
|
+
render.header(root.name, stack, _is_initialized(root))
|
|
49
|
+
render.info(f"gates aplicables: {', '.join(feats)}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_init(args) -> int:
|
|
54
|
+
from tramalia.core import scaffold
|
|
55
|
+
root = Path.cwd()
|
|
56
|
+
stack = detect_stack(root)
|
|
57
|
+
answers = {
|
|
58
|
+
"project_name": root.name,
|
|
59
|
+
"stacks": stack,
|
|
60
|
+
"features": enabled_features(stack),
|
|
61
|
+
"primary_agent": "codex",
|
|
62
|
+
"reviewer_agent": "claude",
|
|
63
|
+
"with_headroom": getattr(args, "with_headroom", False),
|
|
64
|
+
"with_ponytail": getattr(args, "with_ponytail", False),
|
|
65
|
+
}
|
|
66
|
+
render.header(root.name, stack, _is_initialized(root))
|
|
67
|
+
results = scaffold.scaffold(root, answers)
|
|
68
|
+
for rel, state in results:
|
|
69
|
+
(render.ok if state == "creado" else render.info)(f"{state:>6} {rel}")
|
|
70
|
+
creados = sum(1 for _, s in results if s == "creado")
|
|
71
|
+
render.ok(f"init listo: {creados} creados, {len(results) - creados} ya existían.")
|
|
72
|
+
render.info("revisa AGENTS.md y mise.toml; instala lo que falte con `tramalia doctor`.")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_gates(args) -> int:
|
|
77
|
+
if shutil.which("mise") is None:
|
|
78
|
+
render.err("falta 'mise'. Corre `tramalia doctor` para los pasos de instalación.")
|
|
79
|
+
return 127
|
|
80
|
+
return _run(["mise", "run", "gates"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_context(args) -> int:
|
|
84
|
+
from tramalia.core import context
|
|
85
|
+
results = context.build_context(Path.cwd())
|
|
86
|
+
for rel in results:
|
|
87
|
+
render.ok(f"generado .tramalia/context/{rel}")
|
|
88
|
+
if shutil.which("repomix") is None:
|
|
89
|
+
render.info("repomix ausente: project-map se generó con el árbol stdlib.")
|
|
90
|
+
render.info("para snapshot completo: `mise use npm:repomix`.")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _engram_save(title: str, body: str) -> None:
|
|
95
|
+
"""Export opt-in a Engram (memoria persistente N2). Nunca automático."""
|
|
96
|
+
if shutil.which("engram") is None:
|
|
97
|
+
render.warn("engram no está instalado; se omite el export a memoria persistente.")
|
|
98
|
+
return
|
|
99
|
+
if _run(["engram", "save", title, body]) == 0:
|
|
100
|
+
render.ok("exportado a Engram (memoria persistente N2).")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _interactive_ask_task():
|
|
104
|
+
"""Prompt de tarea solo si hay terminal interactiva (los scripts no se cuelgan)."""
|
|
105
|
+
import sys
|
|
106
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
107
|
+
return None
|
|
108
|
+
return lambda: menu.ask_text("ID de la tarea (ver specs/tasks.md)", "TASK-001")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolver(args):
|
|
112
|
+
"""Aplica la cadena de defaults: posicional > --task > current-task > prompt."""
|
|
113
|
+
from tramalia.core import project
|
|
114
|
+
return project.resolve_close_args(
|
|
115
|
+
Path.cwd(),
|
|
116
|
+
getattr(args, "task_pos", None),
|
|
117
|
+
getattr(args, "task", None),
|
|
118
|
+
getattr(args, "agent", None),
|
|
119
|
+
getattr(args, "reviewer", None),
|
|
120
|
+
ask=_interactive_ask_task(),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cmd_evidence(args) -> int:
|
|
125
|
+
from tramalia.core import evidence
|
|
126
|
+
task, _, _ = _resolver(args)
|
|
127
|
+
target = evidence.build_evidence(Path.cwd(), task)
|
|
128
|
+
render.ok(f"evidence pack creado: {target.relative_to(Path.cwd())}")
|
|
129
|
+
render.info("completa summary.md, risks.md y next-steps.md antes de cerrar.")
|
|
130
|
+
if getattr(args, "engram", False):
|
|
131
|
+
_engram_save(f"evidence {task}", f"Evidence pack de {task} en {target}.")
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def cmd_handoff(args) -> int:
|
|
136
|
+
from tramalia.core import handoff
|
|
137
|
+
task, agent, reviewer = _resolver(args)
|
|
138
|
+
path = handoff.new_handoff(Path.cwd(), task, agent, reviewer)
|
|
139
|
+
render.ok(f"handoff agregado a {path.relative_to(Path.cwd())}")
|
|
140
|
+
if getattr(args, "engram", False):
|
|
141
|
+
_engram_save(f"handoff {task}",
|
|
142
|
+
f"Handoff de {task}; ejecutor {agent or '?'}, revisor {reviewer or '?'}.")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def cmd_close(args) -> int:
|
|
147
|
+
from tramalia.core import governance
|
|
148
|
+
task, agent, reviewer = _resolver(args)
|
|
149
|
+
res = governance.close(
|
|
150
|
+
Path.cwd(), task, agent, reviewer,
|
|
151
|
+
allow_fail=getattr(args, "allow_fail", False),
|
|
152
|
+
model=getattr(args, "model", None) or "",
|
|
153
|
+
)
|
|
154
|
+
if not res.gates_ran:
|
|
155
|
+
render.warn("gates no ejecutados (mise ausente); registrado como excepción en el pack.")
|
|
156
|
+
else:
|
|
157
|
+
for name, code, _ in res.gates:
|
|
158
|
+
(render.ok if code == 0 else render.err)(
|
|
159
|
+
f"gate {name}: {'ok' if code == 0 else 'FALLA'}")
|
|
160
|
+
render.ok(f"evidence: {res.evidence_dir.relative_to(Path.cwd())} (estado: {res.status})")
|
|
161
|
+
render.ok(f"handoff: {res.handoff_path.relative_to(Path.cwd())}")
|
|
162
|
+
render.info(f"metadata: {(res.evidence_dir / 'metadata.json').relative_to(Path.cwd())}")
|
|
163
|
+
if getattr(args, "engram", False):
|
|
164
|
+
_engram_save(f"close {task}",
|
|
165
|
+
f"Cierre de {task}; estado {res.status}; fallidos: {', '.join(res.failed) or 'ninguno'}.")
|
|
166
|
+
if res.blocked:
|
|
167
|
+
render.err(f"cierre BLOQUEADO por gates fallidos: {', '.join(res.failed)}.")
|
|
168
|
+
render.info("usa --allow-fail solo con una excepción documentada en risks.md.")
|
|
169
|
+
return 1
|
|
170
|
+
render.ok(f"tarea {task} cerrada con evidencia verificable.")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_LOG_MARKS = {
|
|
175
|
+
"passed": "✓ passed",
|
|
176
|
+
"passed_with_exceptions": "⚠ con excepciones (forzado)",
|
|
177
|
+
"blocked": "✗ bloqueado",
|
|
178
|
+
"no_gates": "○ sin gates",
|
|
179
|
+
None: "○ —",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_log(args) -> int:
|
|
184
|
+
from tramalia.core import governance
|
|
185
|
+
entries = governance.read_log(Path.cwd())
|
|
186
|
+
if not entries:
|
|
187
|
+
render.info("sin cierres registrados todavía. Usa `tramalia close`.")
|
|
188
|
+
return 0
|
|
189
|
+
render.info(f"pista de auditoría — {len(entries)} cierres (más reciente primero):")
|
|
190
|
+
for e in entries:
|
|
191
|
+
mark = _LOG_MARKS.get(e.get("status"), "○ —")
|
|
192
|
+
extra = f" · {e['agent']}" if e.get("agent") else ""
|
|
193
|
+
if e.get("model"):
|
|
194
|
+
extra += f" ({e['model']})"
|
|
195
|
+
render.ok(f"{e['id']} · {mark}{extra}")
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_sync(args) -> int:
|
|
200
|
+
if shutil.which("rulesync") is None:
|
|
201
|
+
render.err("falta 'rulesync'. Instálalo con: mise use npm:rulesync")
|
|
202
|
+
return 127
|
|
203
|
+
if not (Path.cwd() / "AGENTS.md").exists():
|
|
204
|
+
render.err("no hay AGENTS.md. Ejecuta `tramalia init` primero.")
|
|
205
|
+
return 1
|
|
206
|
+
# CLAUDE.md/Codex no se incluyen: ya leen AGENTS.md nativamente.
|
|
207
|
+
# Targets válidos en rulesync v9: copilot, cursor, cline, antigravity-cli, zed, junie, warp, …
|
|
208
|
+
targets = getattr(args, "to", None) or "copilot,cursor,cline"
|
|
209
|
+
wanted = {f.strip() for f in
|
|
210
|
+
(getattr(args, "features", None) or "rules,subagents").split(",") if f.strip()}
|
|
211
|
+
code = 0
|
|
212
|
+
if "rules" in wanted:
|
|
213
|
+
render.info(f"reglas: AGENTS.md → {targets} (rulesync)")
|
|
214
|
+
code |= _run(["rulesync", "convert", "--from", "agentsmd",
|
|
215
|
+
"--to", targets, "--features", "rules"])
|
|
216
|
+
if "subagents" in wanted:
|
|
217
|
+
if (Path.cwd() / ".claude" / "agents").exists():
|
|
218
|
+
render.info(f"subagentes: .claude/agents → {targets} (rulesync)")
|
|
219
|
+
# best-effort: no todos los targets soportan subagentes; rulesync lo reporta.
|
|
220
|
+
code |= _run(["rulesync", "convert", "--from", "claudecode",
|
|
221
|
+
"--to", targets, "--features", "subagents"])
|
|
222
|
+
else:
|
|
223
|
+
render.info("sin .claude/agents; omitiendo fan-out de subagentes.")
|
|
224
|
+
return code
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def cmd_skills(args) -> int:
|
|
228
|
+
from tramalia.core import skills
|
|
229
|
+
root = Path.cwd()
|
|
230
|
+
action = getattr(args, "action", None) or "sync"
|
|
231
|
+
|
|
232
|
+
if action == "list":
|
|
233
|
+
items = skills.read_skills(root)
|
|
234
|
+
if not items:
|
|
235
|
+
render.info("no hay skills declaradas en .tramalia/skills.toml")
|
|
236
|
+
for s in items:
|
|
237
|
+
render.ok(f"{s.get('name', '?')} ← {s.get('source', '')}")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
results = skills.sync_skills(root)
|
|
241
|
+
if not results:
|
|
242
|
+
render.info("no hay skills declaradas en .tramalia/skills.toml (todas comentadas).")
|
|
243
|
+
return 0
|
|
244
|
+
for name, act in results:
|
|
245
|
+
ok = act in ("clonada", "actualizada")
|
|
246
|
+
(render.ok if ok else render.warn)(f"{act:>12} {name}")
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cmd_update(args) -> int:
|
|
251
|
+
from tramalia.core import skills
|
|
252
|
+
render.info("update = mise upgrade + skills sync (+ copier update, futuro)")
|
|
253
|
+
code = 0
|
|
254
|
+
if shutil.which("mise"):
|
|
255
|
+
code |= _run(["mise", "upgrade"])
|
|
256
|
+
else:
|
|
257
|
+
render.warn("mise ausente; omitiendo `mise upgrade`.")
|
|
258
|
+
results = skills.sync_skills(Path.cwd())
|
|
259
|
+
if results:
|
|
260
|
+
for name, act in results:
|
|
261
|
+
ok = act in ("clonada", "actualizada")
|
|
262
|
+
(render.ok if ok else render.warn)(f"skill {act}: {name}")
|
|
263
|
+
else:
|
|
264
|
+
render.info("sin skills externas declaradas que sincronizar.")
|
|
265
|
+
return code
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def cmd_mcp(args) -> int:
|
|
269
|
+
try:
|
|
270
|
+
import mcp # noqa: F401
|
|
271
|
+
except ImportError:
|
|
272
|
+
render.err('falta el SDK MCP. Instálalo con: pip install "tramalia-cli[mcp]"')
|
|
273
|
+
return 127
|
|
274
|
+
from tramalia import mcp_server
|
|
275
|
+
render.info("levantando Tramalia MCP (stdio)… Ctrl+C para detener.")
|
|
276
|
+
mcp_server.run()
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def cmd_ui(args) -> int:
|
|
281
|
+
try:
|
|
282
|
+
import textual # noqa: F401
|
|
283
|
+
except ImportError:
|
|
284
|
+
render.err('falta Textual. Instálalo con: pip install "tramalia-cli[tui]"')
|
|
285
|
+
return 127
|
|
286
|
+
from tramalia import tui
|
|
287
|
+
tui.run()
|
|
288
|
+
return 0
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _guided_args(command: str):
|
|
292
|
+
"""Prompts guiados para close/handoff/evidence desde el menú (modo novato).
|
|
293
|
+
|
|
294
|
+
Prellena con los defaults reales del proyecto: current-task.md y config.json.
|
|
295
|
+
"""
|
|
296
|
+
import argparse
|
|
297
|
+
from tramalia.core import project
|
|
298
|
+
root = Path.cwd()
|
|
299
|
+
primary, rev = project.default_agents(root)
|
|
300
|
+
task = menu.ask_text("ID de la tarea (ver specs/tasks.md)",
|
|
301
|
+
project.current_task_id(root) or "TASK-001")
|
|
302
|
+
agent = reviewer = ""
|
|
303
|
+
if command in ("close", "handoff"):
|
|
304
|
+
agent = menu.ask_text("agente ejecutor", primary or "codex")
|
|
305
|
+
reviewer = menu.ask_text("agente revisor sugerido", rev or "claude")
|
|
306
|
+
return argparse.Namespace(task=task, task_pos=None, agent=agent, reviewer=reviewer,
|
|
307
|
+
engram=False, allow_fail=False)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _show_last_close(root: Path) -> None:
|
|
311
|
+
from tramalia.core import governance
|
|
312
|
+
entries = governance.read_log(root)
|
|
313
|
+
if entries:
|
|
314
|
+
last = entries[0]
|
|
315
|
+
mark = _LOG_MARKS.get(last.get("status"), "○ —")
|
|
316
|
+
render.info(f"último cierre: {last['id']} · {mark}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_menu(args) -> int:
|
|
320
|
+
root = Path.cwd()
|
|
321
|
+
while True:
|
|
322
|
+
stack = detect_stack(root)
|
|
323
|
+
render.header(root.name, stack, _is_initialized(root))
|
|
324
|
+
_show_last_close(root)
|
|
325
|
+
try:
|
|
326
|
+
choice = menu.choose()
|
|
327
|
+
except (KeyboardInterrupt, EOFError):
|
|
328
|
+
return 0
|
|
329
|
+
if choice == "quit":
|
|
330
|
+
return 0
|
|
331
|
+
run_args = _guided_args(choice) if choice in ("close", "handoff", "evidence") else args
|
|
332
|
+
try:
|
|
333
|
+
dispatch(choice, run_args)
|
|
334
|
+
except (KeyboardInterrupt, EOFError):
|
|
335
|
+
render.warn("acción cancelada.")
|
|
336
|
+
print()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
_HANDLERS = {
|
|
340
|
+
"doctor": cmd_doctor,
|
|
341
|
+
"detect": cmd_detect,
|
|
342
|
+
"init": cmd_init,
|
|
343
|
+
"gates": cmd_gates,
|
|
344
|
+
"context": cmd_context,
|
|
345
|
+
"evidence": cmd_evidence,
|
|
346
|
+
"handoff": cmd_handoff,
|
|
347
|
+
"close": cmd_close,
|
|
348
|
+
"log": cmd_log,
|
|
349
|
+
"sync": cmd_sync,
|
|
350
|
+
"skills": cmd_skills,
|
|
351
|
+
"update": cmd_update,
|
|
352
|
+
"mcp": cmd_mcp,
|
|
353
|
+
"ui": cmd_ui,
|
|
354
|
+
"menu": cmd_menu,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def dispatch(command: str, args) -> int:
|
|
359
|
+
handler = _HANDLERS.get(command)
|
|
360
|
+
if handler is None:
|
|
361
|
+
render.err(f"comando desconocido: {command}")
|
|
362
|
+
return 2
|
|
363
|
+
return handler(args)
|
tramalia/cli/menu.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Menú interactivo: usa questionary si está; si no, menú numerado stdlib."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import questionary
|
|
7
|
+
_HAS_Q = True
|
|
8
|
+
except Exception:
|
|
9
|
+
_HAS_Q = False
|
|
10
|
+
|
|
11
|
+
# (clave, etiqueta) — la clave es el comando que se ejecuta
|
|
12
|
+
OPTIONS: list[tuple[str, str]] = [
|
|
13
|
+
("close", "★ cerrar tarea: gates → evidence → handoff"),
|
|
14
|
+
("log", "ver pista de auditoría (cierres)"),
|
|
15
|
+
("doctor", "diagnosticar herramientas requeridas"),
|
|
16
|
+
("detect", "detectar el stack del proyecto"),
|
|
17
|
+
("init", "inicializar o reparar la estructura"),
|
|
18
|
+
("gates", "ejecutar quality gates (mise run gates)"),
|
|
19
|
+
("context", "generar contexto / token-saver (repomix + serena)"),
|
|
20
|
+
("evidence", "generar evidence pack"),
|
|
21
|
+
("handoff", "crear handoff multiagente"),
|
|
22
|
+
("sync", "sincronizar reglas a otros agentes (rulesync)"),
|
|
23
|
+
("skills", "administrar skills (sync desde sus repos)"),
|
|
24
|
+
("update", "actualizar todo (mise + skills)"),
|
|
25
|
+
("ui", "abrir dashboard (TUI)"),
|
|
26
|
+
("quit", "salir"),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ask_text(prompt: str, default: str = "") -> str:
|
|
31
|
+
"""Pregunta un texto (questionary si está; input stdlib si no)."""
|
|
32
|
+
if _HAS_Q:
|
|
33
|
+
answer = questionary.text(prompt, default=default, qmark="?").ask()
|
|
34
|
+
return (answer if answer is not None else default).strip() or default
|
|
35
|
+
raw = input(f"{prompt} [{default}]> ").strip()
|
|
36
|
+
return raw or default
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def choose() -> str:
|
|
40
|
+
if _HAS_Q:
|
|
41
|
+
answer = questionary.select(
|
|
42
|
+
"¿qué quieres hacer?",
|
|
43
|
+
choices=[questionary.Choice(title=label, value=key) for key, label in OPTIONS],
|
|
44
|
+
qmark="?",
|
|
45
|
+
instruction="(↑↓ navegar · ⏎ seleccionar)",
|
|
46
|
+
).ask()
|
|
47
|
+
return answer or "quit"
|
|
48
|
+
|
|
49
|
+
# fallback stdlib
|
|
50
|
+
print("\n¿qué quieres hacer?")
|
|
51
|
+
for i, (_, label) in enumerate(OPTIONS, 1):
|
|
52
|
+
print(f" {i:>2}. {label}")
|
|
53
|
+
raw = input("opción> ").strip()
|
|
54
|
+
if not raw.isdigit() or not (1 <= int(raw) <= len(OPTIONS)):
|
|
55
|
+
return "quit"
|
|
56
|
+
return OPTIONS[int(raw) - 1][0]
|
tramalia/cli/render.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Render con Rich si está disponible; si no, texto plano (terminal básica).
|
|
2
|
+
|
|
3
|
+
Así la CLI corre sin instalar nada y se ve bonita automáticamente cuando
|
|
4
|
+
instalas el extra `pretty` (rich + questionary).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from tramalia.core.doctor import Report
|
|
10
|
+
|
|
11
|
+
_PLAIN = False
|
|
12
|
+
|
|
13
|
+
try: # modo bonito opcional
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich import box
|
|
18
|
+
_console: "Console | None" = Console()
|
|
19
|
+
_HAS_RICH = True
|
|
20
|
+
except Exception: # pragma: no cover - fallback stdlib
|
|
21
|
+
_console = None
|
|
22
|
+
_HAS_RICH = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_plain(value: bool) -> None:
|
|
26
|
+
global _PLAIN
|
|
27
|
+
_PLAIN = value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _rich() -> bool:
|
|
31
|
+
return _HAS_RICH and not _PLAIN
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_NEED = {"bootstrap": "base", "stack": "stack"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _need_label(tool) -> str:
|
|
38
|
+
if tool.category == "feature":
|
|
39
|
+
return f"gate:{tool.feature}"
|
|
40
|
+
return _NEED.get(tool.category, tool.category)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def header(project: str, stack: list[str], initialized: bool) -> None:
|
|
44
|
+
estado = "inicializado" if initialized else "no inicializado"
|
|
45
|
+
stack_txt = " · ".join(stack) if stack else "—"
|
|
46
|
+
if _rich():
|
|
47
|
+
_console.print(Panel(
|
|
48
|
+
f"proyecto [bold]{project}[/bold] stack [bold]{stack_txt}[/bold] "
|
|
49
|
+
f"estado [{'green' if initialized else 'yellow'}]{estado}[/]",
|
|
50
|
+
title="Tramalia", border_style="cyan", box=box.ROUNDED,
|
|
51
|
+
))
|
|
52
|
+
else:
|
|
53
|
+
print("=" * 60)
|
|
54
|
+
print(f"Tramalia · proyecto {project} · stack {stack_txt} · {estado}")
|
|
55
|
+
print("=" * 60)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def doctor(report: Report) -> int:
|
|
59
|
+
"""Imprime el diagnóstico. Devuelve el exit code (0 si nada bloqueante falta)."""
|
|
60
|
+
stack_txt = " · ".join(report.stack) if report.stack else "—"
|
|
61
|
+
if _rich():
|
|
62
|
+
table = Table(box=box.SIMPLE_HEAVY, expand=False)
|
|
63
|
+
table.add_column("herramienta", style="bold")
|
|
64
|
+
table.add_column("necesidad", style="dim")
|
|
65
|
+
table.add_column("estado")
|
|
66
|
+
table.add_column("detalle / cómo obtenerla", overflow="fold")
|
|
67
|
+
for s in report.statuses:
|
|
68
|
+
if s.present:
|
|
69
|
+
estado = "[green]✓ ok[/green]"
|
|
70
|
+
detalle = s.version or "(instalada)"
|
|
71
|
+
elif s.tool.category == "feature":
|
|
72
|
+
estado = "[yellow]○ opcional[/yellow]"
|
|
73
|
+
detalle = s.tool.install_hint
|
|
74
|
+
else:
|
|
75
|
+
estado = "[red]✗ falta[/red]"
|
|
76
|
+
detalle = s.tool.install_hint
|
|
77
|
+
if s.tool.runtime == "node":
|
|
78
|
+
detalle += " [magenta]· requiere Node[/magenta]"
|
|
79
|
+
table.add_row(s.tool.cmd, _need_label(s.tool), estado, detalle)
|
|
80
|
+
_console.print(f"\n[dim]stack detectado:[/dim] {stack_txt}")
|
|
81
|
+
_console.print(table)
|
|
82
|
+
else:
|
|
83
|
+
print(f"\nstack detectado: {stack_txt}")
|
|
84
|
+
print(f"{'herramienta':<14}{'necesidad':<16}{'estado':<11}detalle")
|
|
85
|
+
print("-" * 78)
|
|
86
|
+
for s in report.statuses:
|
|
87
|
+
if s.present:
|
|
88
|
+
estado, detalle = "ok", (s.version or "(instalada)")
|
|
89
|
+
elif s.tool.category == "feature":
|
|
90
|
+
estado, detalle = "opcional", s.tool.install_hint
|
|
91
|
+
else:
|
|
92
|
+
estado, detalle = "FALTA", s.tool.install_hint
|
|
93
|
+
if s.tool.runtime == "node":
|
|
94
|
+
detalle += " · requiere Node"
|
|
95
|
+
print(f"{s.tool.cmd:<14}{_need_label(s.tool):<16}{estado:<11}{detalle}")
|
|
96
|
+
|
|
97
|
+
if report.needs_node:
|
|
98
|
+
_warn(f"Node no está instalado y lo requieren: {', '.join(report.node_tools)}.")
|
|
99
|
+
_info("instálalo con `mise use node@22` (o nvm) para usar sync / ux / context completo.")
|
|
100
|
+
|
|
101
|
+
blocking = report.missing_blocking
|
|
102
|
+
optional = report.missing_optional
|
|
103
|
+
if blocking:
|
|
104
|
+
names = ", ".join(s.tool.cmd for s in blocking)
|
|
105
|
+
_warn(f"faltan herramientas requeridas: {names}")
|
|
106
|
+
_info("instálalas con los comandos de arriba y vuelve a correr `tramalia doctor`.")
|
|
107
|
+
_info("una vez que tengas mise, el resto se instala con `mise install`.")
|
|
108
|
+
return 1
|
|
109
|
+
if optional:
|
|
110
|
+
names = ", ".join(s.tool.cmd for s in optional)
|
|
111
|
+
_info(f"opcionales ausentes (se activan al usar su gate): {names}")
|
|
112
|
+
_ok("todo lo requerido está presente.")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _ok(msg: str) -> None:
|
|
117
|
+
_console.print(f"[green]✓[/green] {msg}") if _rich() else print(f"[ok] {msg}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _warn(msg: str) -> None:
|
|
121
|
+
_console.print(f"[yellow]▲[/yellow] {msg}") if _rich() else print(f"[!] {msg}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _info(msg: str) -> None:
|
|
125
|
+
_console.print(f"[cyan]i[/cyan] {msg}") if _rich() else print(f"[i] {msg}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _err(msg: str) -> None:
|
|
129
|
+
_console.print(f"[red]✗[/red] {msg}") if _rich() else print(f"[x] {msg}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# alias públicos
|
|
133
|
+
ok, warn, info, err = _ok, _warn, _info, _err
|
|
File without changes
|