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.
Files changed (66) hide show
  1. tramalia/__init__.py +11 -0
  2. tramalia/__main__.py +96 -0
  3. tramalia/cli/__init__.py +0 -0
  4. tramalia/cli/commands.py +363 -0
  5. tramalia/cli/menu.py +56 -0
  6. tramalia/cli/render.py +133 -0
  7. tramalia/core/__init__.py +0 -0
  8. tramalia/core/context.py +71 -0
  9. tramalia/core/detect.py +59 -0
  10. tramalia/core/doctor.py +63 -0
  11. tramalia/core/evidence.py +53 -0
  12. tramalia/core/governance.py +193 -0
  13. tramalia/core/handoff.py +38 -0
  14. tramalia/core/proc.py +26 -0
  15. tramalia/core/project.py +61 -0
  16. tramalia/core/scaffold.py +174 -0
  17. tramalia/core/skills.py +68 -0
  18. tramalia/core/tools.py +119 -0
  19. tramalia/mcp_server.py +94 -0
  20. tramalia/templates/project/.claude/agents/documentador.md +12 -0
  21. tramalia/templates/project/.claude/agents/ejecutor.md +15 -0
  22. tramalia/templates/project/.claude/agents/planificador.md +14 -0
  23. tramalia/templates/project/.claude/agents/resolutor-profundo.md +15 -0
  24. tramalia/templates/project/.claude/agents/revisor.md +14 -0
  25. tramalia/templates/project/.tramalia/config.json.jinja +15 -0
  26. tramalia/templates/project/.tramalia/current-task.md +8 -0
  27. tramalia/templates/project/.tramalia/skills/01-spec-governance/SKILL.md +26 -0
  28. tramalia/templates/project/.tramalia/skills/02-federated-agent-memory/SKILL.md +25 -0
  29. tramalia/templates/project/.tramalia/skills/03-context-token-saver/SKILL.md +25 -0
  30. tramalia/templates/project/.tramalia/skills/04-minimalist-engineering/SKILL.md +25 -0
  31. tramalia/templates/project/.tramalia/skills/05-code-quality-review/SKILL.md +25 -0
  32. tramalia/templates/project/.tramalia/skills/06-security-gate/SKILL.md +25 -0
  33. tramalia/templates/project/.tramalia/skills/07-database-engineering/SKILL.md +25 -0
  34. tramalia/templates/project/.tramalia/skills/08-tool-execution-gate/SKILL.md +25 -0
  35. tramalia/templates/project/.tramalia/skills/09-observability-first/SKILL.md +25 -0
  36. tramalia/templates/project/.tramalia/skills/10-evidence-and-handoff/SKILL.md +25 -0
  37. tramalia/templates/project/.tramalia/skills/11-legacy-modernization/SKILL.md +25 -0
  38. tramalia/templates/project/.tramalia/skills/12-multi-agent-review/SKILL.md +25 -0
  39. tramalia/templates/project/.tramalia/skills/13-documentation-handoff/SKILL.md +25 -0
  40. tramalia/templates/project/.tramalia/skills.toml +10 -0
  41. tramalia/templates/project/AGENTS.md.jinja +40 -0
  42. tramalia/templates/project/CLAUDE.md.jinja +8 -0
  43. tramalia/templates/project/docs/ai/00-resumen-proyecto.md.jinja +21 -0
  44. tramalia/templates/project/docs/ai/01-arquitectura.md +23 -0
  45. tramalia/templates/project/docs/ai/02-reglas-codigo.md +17 -0
  46. tramalia/templates/project/docs/ai/03-reglas-base-datos.md +18 -0
  47. tramalia/templates/project/docs/ai/04-reglas-seguridad.md +13 -0
  48. tramalia/templates/project/docs/ai/05-decisiones-adr.md +9 -0
  49. tramalia/templates/project/docs/ai/06-intentos-fallidos.md +14 -0
  50. tramalia/templates/project/docs/ai/07-handoff-agentes.md +4 -0
  51. tramalia/templates/project/docs/ai/08-comandos-proyecto.md +16 -0
  52. tramalia/templates/project/docs/ai/09-quality-gates.md +10 -0
  53. tramalia/templates/project/docs/ai/10-contexto-operativo.md +16 -0
  54. tramalia/templates/project/docs/ai/11-reglas-ux-ui.md +12 -0
  55. tramalia/templates/project/specs/checklist.md +12 -0
  56. tramalia/templates/project/specs/constitution.md +9 -0
  57. tramalia/templates/project/specs/plan.md +13 -0
  58. tramalia/templates/project/specs/specification.md +16 -0
  59. tramalia/templates/project/specs/tasks.md +12 -0
  60. tramalia/tui.py +140 -0
  61. tramalia_cli-0.9.1.dist-info/METADATA +180 -0
  62. tramalia_cli-0.9.1.dist-info/RECORD +66 -0
  63. tramalia_cli-0.9.1.dist-info/WHEEL +4 -0
  64. tramalia_cli-0.9.1.dist-info/entry_points.txt +2 -0
  65. tramalia_cli-0.9.1.dist-info/licenses/LICENSE +201 -0
  66. 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())
File without changes
@@ -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