ozali 0.2.0 → 0.4.0

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.
package/README.md CHANGED
@@ -61,11 +61,19 @@ ozali init # detecta agente, instala la skill, aísla el histórico, instal
61
61
  ozali doctor # health-check read-only (fuente de verdad, Engram, Cloud, Strict TDD, runner…)
62
62
  ozali update # actualiza la skill instalada a la versión del paquete
63
63
  ozali sync # lleva el histórico (docs + Engram) al repo de conocimiento de equipo
64
+ ozali audit # navega/audita la memoria de Engram del proyecto (o general)
64
65
  ```
65
66
 
67
+ `ozali audit` recorre lo que el equipo ha acumulado en Engram: dentro de un repo propone auditar
68
+ **ese proyecto** o **general** (todos los proyectos); fuera de un repo va directo a general. Usa
69
+ `--tui` para el navegador interactivo, `--search "<texto>"` para buscar y `--general` para forzar el
70
+ alcance. Sin Engram, audita el histórico local de `.ozali/docs/`.
71
+
66
72
  `init` también escribe un **perfil base de permisos** (`.claude/settings.json` para Claude Code,
67
73
  `opencode.json` para opencode) para reducir confirmaciones: deja libres comandos seguros y bloquea
68
- los destructivos. Es un template — tus reglas se conservan al re-correr `init`.
74
+ los destructivos. Es un template — tus reglas se conservan al re-correr `init`. Como Claude Code
75
+ **ignora** los permisos de un proyecto hasta confiar en él, `init` ofrece marcar el workspace como
76
+ confiable (`hasTrustDialogAccepted` en `~/.claude.json`); usa `--no-trust` para omitirlo.
69
77
 
70
78
  `init` también **instala Engram** y registra su MCP con `engram setup <agente>`: en modo
71
79
  interactivo te pregunta (default sí), y con `--yes` lo instala automáticamente. Usa brew en
@@ -73,9 +81,16 @@ macOS/Linux, `go install` en Windows, con binario precompilado como fallback. Si
73
81
  instalarlo, pasa `--no-engram` (arranca en modo `docs`). Opcionalmente habilita **Engram Cloud**
74
82
  (réplica de equipo opt-in) además del git-sync.
75
83
 
76
- Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--no-engram`, `--agent`,
77
- `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo mental completo en
78
- [docs/intended-usage.md](docs/intended-usage.md).
84
+ `init` también crea **ozali-jarvis**, un **orquestador always-on**: persona en `CLAUDE.md`/`AGENTS.md`
85
+ + subagente + hooks de recordatorio que hace que el agente, **en toda sesión y sin necesidad de
86
+ `/cdk`**, recupere contexto de Engram, registre el trabajo del equipo (memoria en contexto) y delegue
87
+ la ejecución disciplinada en `cdk`. Fija el proyecto en `.engram/config.json` para memoria
88
+ determinista. Con Engram en línea, jarvis y `cdk` operan **recall-first** (reusan memoria en vez de
89
+ releer) para gastar menos tokens/contexto. Omítelo con `--no-jarvis`.
90
+
91
+ Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--no-engram`, `--no-trust`,
92
+ `--no-jarvis`, `--agent`, `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo
93
+ mental completo en [docs/intended-usage.md](docs/intended-usage.md).
79
94
 
80
95
  ## Agentes soportados
81
96
 
package/cli/bin/ozali.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // Seguridad: sin dependencias ni scripts de instalación. Ejecuta seguro con
4
4
  // `pnpm dlx ozali` o `npx --ignore-scripts ozali`. Ver docs/security.md.
5
5
  import { c, err, pkgVersion } from "../lib/util.mjs";
6
- import { init, doctor, update, sync } from "../lib/commands.mjs";
6
+ import { init, doctor, update, sync, audit } from "../lib/commands.mjs";
7
7
 
8
8
  const HELP = `
9
9
  ${c.bold("ozali")} ${c.dim("v" + pkgVersion())} — bootstrap de IA por equipo (TDD/SDD + memoria Engram)
@@ -17,6 +17,7 @@ ${c.bold("Comandos:")}
17
17
  doctor Health-check read-only del proyecto (fuente de verdad, Engram, TDD…).
18
18
  update Actualiza la skill ozali instalada a la versión de este paquete.
19
19
  sync Sincroniza el histórico (docs + Engram) con el repo de conocimiento.
20
+ audit Navega/audita la memoria de Engram del proyecto (o general).
20
21
 
21
22
  ${c.bold("Opciones comunes:")}
22
23
  --yes, -y No interactivo: usa defaults.
@@ -24,10 +25,15 @@ ${c.bold("Opciones comunes:")}
24
25
  --agent <a> (init) claude-code | opencode | both.
25
26
  --scope <s> (init) project | global.
26
27
  --knowledge-repo <p> (init) Ruta del repo de conocimiento.
27
- --no-engram (init) No instalar Engram; arranca en modo docs.
28
+ --no-engram (init) No usar Engram; arranca en modo docs.
29
+ --no-trust (init) No marcar el workspace como confiable en Claude Code.
30
+ --no-jarvis (init) No crear el orquestador ozali-jarvis.
28
31
  --import (sync) Importa del repo de conocimiento a local.
29
32
  --push (sync) Hace push al remoto del repo de conocimiento.
30
33
  --cloud (sync) Replica también a Engram Cloud (si está habilitado).
34
+ --general (audit) Auditoría general (todos los proyectos en Engram).
35
+ --tui (audit) Abre el navegador interactivo de Engram.
36
+ --search <q> (audit) Busca <q> en la memoria.
31
37
  -h, --help Esta ayuda. -v, --version Versión.
32
38
 
33
39
  ${c.bold("Instalación segura:")}
@@ -46,6 +52,11 @@ function parseArgs(argv) {
46
52
  else if (a === "--push") opts.push = true;
47
53
  else if (a === "--cloud") opts.cloud = true;
48
54
  else if (a === "--no-engram") opts.noEngram = true;
55
+ else if (a === "--no-trust") opts.noTrust = true;
56
+ else if (a === "--no-jarvis") opts.noJarvis = true;
57
+ else if (a === "--general") opts.general = true;
58
+ else if (a === "--tui") opts.tui = true;
59
+ else if (a === "--search") opts.search = argv[++i];
49
60
  else if (a === "--agent") opts.agent = argv[++i];
50
61
  else if (a === "--scope") opts.scope = argv[++i];
51
62
  else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
@@ -69,6 +80,7 @@ async function main() {
69
80
  case "doctor": return doctor(cwd);
70
81
  case "update": return update(cwd);
71
82
  case "sync": return sync(cwd, opts);
83
+ case "audit": return await audit(cwd, opts);
72
84
  default:
73
85
  err(`comando desconocido "${cmd}". Usa ${c.bold("ozali --help")}.`);
74
86
  return 1;
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
5
5
  c, ok, warn, err, info, step,
6
- SKILL_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
6
+ SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
7
7
  ensureGitignore, tryExec, spawnCmd, which,
8
8
  projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME,
9
9
  } from "./util.mjs";
@@ -49,15 +49,16 @@ export async function init(cwd, opts) {
49
49
 
50
50
  // Engram
51
51
  let memoryMode = "docs";
52
- if (env.engram.available) {
52
+ if (opts.noEngram) {
53
+ info("--no-engram: arranco en modo " + c.bold("docs") + " (sin usar Engram).");
54
+ } else if (env.engram.available) {
53
55
  memoryMode = "hybrid";
54
56
  ok(`Engram disponible (${env.engram.bin}). Modo de memoria: ${c.bold("hybrid")} (docs + Engram).`);
55
57
  } else {
56
58
  warn("Engram no está instalado.");
57
- // Decisión de instalar: --no-engram/--dry-run nunca instalan; --yes usa el default (sí);
58
- // interactivo pregunta.
59
- if (opts.dryRun && !opts.noEngram) info("(dry-run) Aquí instalaría y configuraría Engram.");
60
- const installNow = (opts.noEngram || opts.dryRun) ? false
59
+ // --dry-run no instala; --yes usa el default (sí); interactivo pregunta.
60
+ if (opts.dryRun) info("(dry-run) Aquí instalaría y configuraría Engram.");
61
+ const installNow = opts.dryRun ? false
61
62
  : (opts.yes ? true : await confirm("¿Instalo y configuro Engram ahora?", true));
62
63
  if (installNow) {
63
64
  if (opts.yes) info("Modo no interactivo: instalando Engram automáticamente…");
@@ -103,11 +104,23 @@ export async function init(cwd, opts) {
103
104
  // 2) perfiles base de permisos (idempotentes, merge mínimo) por agente
104
105
  if (agent === "claude-code" || agent === "both") {
105
106
  ensureClaudeCodeProfile(cwd, scope);
107
+ // Claude Code ignora los permisos de un .claude/settings.json de proyecto hasta confiar en él.
108
+ if (scope === "project" && !opts.noTrust) await ensureClaudeWorkspaceTrust(cwd, opts);
106
109
  }
107
110
  if (agent === "opencode" || agent === "both") {
108
111
  ensureOpencodeProfile(cwd);
109
112
  }
110
113
 
114
+ // 2.5) ozali-jarvis: orquestador always-on (memoria Engram + puente a cdk).
115
+ if (!opts.noJarvis) {
116
+ const proj = projectName(cwd);
117
+ // Fija el proyecto para escrituras deterministas de memoria (se deriva igual por cada miembro).
118
+ writeJSON(path.join(cwd, ".engram", "config.json"), { project_name: proj });
119
+ ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`);
120
+ if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
121
+ if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
122
+ }
123
+
111
124
  // 3) gitignore del histórico aislado
112
125
  if (env.git.isRepo) {
113
126
  const { added } = ensureGitignore(cwd, [".ozali/", ".engram/"]);
@@ -283,6 +296,130 @@ function ensureClaudeCodeProfile(cwd, scope) {
283
296
  }
284
297
  }
285
298
 
299
+ // ----------------------------------------------------------------- ozali-jarvis
300
+ const JARVIS_BEGIN = "<!-- ozali-jarvis:start -->";
301
+ const JARVIS_END = "<!-- ozali-jarvis:end -->";
302
+
303
+ /** Cuerpo del bloque jarvis para CLAUDE.md / AGENTS.md (sin el frontmatter del template). */
304
+ function jarvisPersonaBody() {
305
+ const tpl = fs.readFileSync(path.join(TEMPLATES_SRC, "ozali-jarvis.md"), "utf8");
306
+ // Quita el frontmatter YAML (--- ... ---) y deja el cuerpo markdown.
307
+ return tpl.replace(/^---[\s\S]*?---\s*/, "").trim();
308
+ }
309
+
310
+ /** Inserta/actualiza un bloque marcado en un archivo markdown (idempotente). */
311
+ function upsertMarkedBlock(file, body) {
312
+ const block = `${JARVIS_BEGIN}\n${body}\n${JARVIS_END}`;
313
+ let txt = exists(file) ? fs.readFileSync(file, "utf8") : "";
314
+ const re = new RegExp(`${JARVIS_BEGIN}[\\s\\S]*?${JARVIS_END}`);
315
+ let changed;
316
+ if (re.test(txt)) {
317
+ const next = txt.replace(re, block);
318
+ changed = next !== txt; txt = next;
319
+ } else {
320
+ txt = (txt.trim() ? txt.replace(/\s*$/, "") + "\n\n" : "") + block + "\n";
321
+ changed = true;
322
+ }
323
+ fs.writeFileSync(file, txt);
324
+ return changed;
325
+ }
326
+
327
+ function ensureJarvisClaudeCode(cwd) {
328
+ // 1) persona en CLAUDE.md (always-on)
329
+ const claudeMd = path.join(cwd, "CLAUDE.md");
330
+ const ch = upsertMarkedBlock(claudeMd, jarvisPersonaBody());
331
+ ok(`ozali-jarvis ${ch ? "escrito" : "ya presente"} en ${c.bold("CLAUDE.md")} (orquestador por defecto).`);
332
+ // 2) subagente
333
+ const agentFile = path.join(cwd, ".claude", "agents", "ozali-jarvis.md");
334
+ ensureDir(path.dirname(agentFile));
335
+ fs.copyFileSync(path.join(TEMPLATES_SRC, "ozali-jarvis.md"), agentFile);
336
+ ok(`Subagente ${c.bold(".claude/agents/ozali-jarvis.md")} instalado.`);
337
+ // 3) hooks de recordatorio (idempotentes)
338
+ ensureJarvisHooks(cwd);
339
+ }
340
+
341
+ function ensureJarvisHooks(cwd) {
342
+ const p = path.join(cwd, ".claude", "settings.json");
343
+ const cfg = readJSON(p, {});
344
+ cfg.hooks = cfg.hooks || {};
345
+ const reminder = (msg) => ({ hooks: [{ type: "command", command: `echo '[ozali-jarvis] ${msg}'` }] });
346
+ const want = {
347
+ SessionStart: reminder("recall-first: confirma proyecto (mem_current_project) y recupera contexto de Engram (mem_context) antes de actuar."),
348
+ PreCompact: reminder("antes de compactar: persiste el state recuperable en Engram (engram-convention §4)."),
349
+ SessionEnd: reminder("cierre: registra lo trabajado en Engram (scope project, español) y haz mem_session_summary."),
350
+ };
351
+ let added = 0;
352
+ for (const [evt, val] of Object.entries(want)) {
353
+ const arr = cfg.hooks[evt] || (cfg.hooks[evt] = []);
354
+ // idempotencia: no duplicar el recordatorio ozali-jarvis para ese evento
355
+ const has = JSON.stringify(arr).includes("[ozali-jarvis]");
356
+ if (!has) { arr.push(val); added++; }
357
+ }
358
+ writeJSON(p, cfg);
359
+ if (added) ok(`Hooks de recordatorio de ozali-jarvis añadidos a ${c.bold(".claude/settings.json")} (${added}).`);
360
+ else info("Hooks de ozali-jarvis ya presentes en Claude Code.");
361
+ }
362
+
363
+ function ensureJarvisOpencode(cwd) {
364
+ // 1) persona en AGENTS.md
365
+ const agentsMd = path.join(cwd, "AGENTS.md");
366
+ const ch = upsertMarkedBlock(agentsMd, jarvisPersonaBody());
367
+ ok(`ozali-jarvis ${ch ? "escrito" : "ya presente"} en ${c.bold("AGENTS.md")} (orquestador por defecto).`);
368
+ // 2) agente en opencode.json
369
+ const p = path.join(cwd, "opencode.json");
370
+ const cfg = readJSON(p, {});
371
+ cfg.$schema = cfg.$schema || "https://opencode.ai/config.json";
372
+ cfg.agent = cfg.agent || {};
373
+ if (!cfg.agent["ozali-jarvis"]) {
374
+ cfg.agent["ozali-jarvis"] = {
375
+ mode: "primary",
376
+ description: "Orquestador del proyecto: memoria Engram + puente a la skill cdk.",
377
+ prompt: "{file:./AGENTS.md}",
378
+ };
379
+ writeJSON(p, cfg);
380
+ ok(`Agente ${c.bold("ozali-jarvis")} (primary) añadido a ${c.bold("opencode.json")}.`);
381
+ } else {
382
+ info("Agente ozali-jarvis ya presente en opencode.json.");
383
+ }
384
+ // 3) plugin de recordatorio
385
+ const plugin = path.join(cwd, ".opencode", "plugins", "ozali-jarvis.js");
386
+ if (!exists(plugin)) {
387
+ ensureDir(path.dirname(plugin));
388
+ fs.copyFileSync(path.join(TEMPLATES_SRC, "ozali-jarvis-plugin.js"), plugin);
389
+ ok(`Plugin ${c.bold(".opencode/plugins/ozali-jarvis.js")} instalado.`);
390
+ } else {
391
+ info("Plugin de ozali-jarvis ya presente en opencode.");
392
+ }
393
+ }
394
+
395
+ // Marca el proyecto como confiable en Claude Code (~/.claude.json). Sin esto, Claude Code
396
+ // ignora los permisos de un .claude/settings.json de proyecto ("workspace not trusted").
397
+ async function ensureClaudeWorkspaceTrust(cwd, opts) {
398
+ const p = path.join(HOME, ".claude.json");
399
+ if (!exists(p)) {
400
+ info("Claude Code aún no tiene ~/.claude.json; al abrirlo aquí, acepta el diálogo de confianza.");
401
+ return;
402
+ }
403
+ let cfg;
404
+ try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); }
405
+ catch { warn("No pude leer ~/.claude.json; acepta el diálogo de confianza de Claude Code manualmente."); return; }
406
+ cfg.projects = cfg.projects || {};
407
+ cfg.projects[cwd] = cfg.projects[cwd] || {};
408
+ if (cfg.projects[cwd].hasTrustDialogAccepted === true) {
409
+ info("Claude Code ya confía en este workspace.");
410
+ return;
411
+ }
412
+ const doTrust = opts.yes ? true
413
+ : await confirm("¿Marcar este proyecto como confiable en Claude Code? (necesario para que apliquen los permisos)", true);
414
+ if (!doTrust) {
415
+ info("Workspace no marcado como confiable: Claude Code ignorará los permisos hasta que aceptes su diálogo.");
416
+ return;
417
+ }
418
+ cfg.projects[cwd].hasTrustDialogAccepted = true;
419
+ fs.writeFileSync(p, JSON.stringify(cfg, null, 2)); // formato que usa Claude Code (indent 2, sin newline final)
420
+ ok("Workspace marcado como confiable en Claude Code (los permisos de .claude/settings.json ya aplican).");
421
+ }
422
+
286
423
  function ensureOpencodeProfile(cwd) {
287
424
  const p = path.join(cwd, "opencode.json");
288
425
  const base = readJSON(p, {});
@@ -311,6 +448,13 @@ export function doctor(cwd) {
311
448
  add("Fuente de verdad", env.sot.found, env.sot.found ? `${env.sot.doc} + ${env.sot.dir}/` : "ausente (corre la skill 'ozali')");
312
449
  add("Skill ozali instalada", env.skill.installed, env.skill.installed ? env.skill.paths.map((p) => path.relative(cwd, p) || p).join(", ") : "no instalada (ozali init)");
313
450
  add("Engram", env.engram.available, env.engram.available ? env.engram.bin : "no instalado → modo docs");
451
+ if (env.engram.available) {
452
+ const online = tryExec("engram", ["doctor"], { cwd }) !== null;
453
+ add("Engram en línea", online, online ? "engram doctor OK" : "engram doctor no responde");
454
+ }
455
+ const jarvis = detectJarvis(cwd);
456
+ // jarvis es opt-in (--no-jarvis): informativo, no cuenta como fallo.
457
+ add("ozali-jarvis", true, jarvis.present ? `configurado (${jarvis.where.join(", ")})` : "no configurado (--no-jarvis)");
314
458
  const cloudOn = !!(cfg && cfg.cloud && cfg.cloud.enabled);
315
459
  // Cloud es opt-in: "off" es un estado válido (no cuenta como fallo).
316
460
  add("Engram Cloud", true, cloudOn ? `enrolado → ${cfg.cloud.server || "server por defecto"}` : "off (opt-in, git-sync activo)");
@@ -350,9 +494,31 @@ export function doctor(cwd) {
350
494
  }
351
495
  }
352
496
 
497
+ // Tendencia de uso de tokens (la escribe cdk en cada hito; informativo).
498
+ const metrics = readJSON(path.join(cwd, ".ozali", "metrics", "token-metrics.json"));
499
+ if (metrics && Array.isArray(metrics.hits) && metrics.hits.length) {
500
+ step("Tendencia de tokens (últimos hitos)");
501
+ for (const h of metrics.hits.slice(-3)) {
502
+ const saved = h.savedByRecall ? ` ${c.green("(ahorro recall: " + h.savedByRecall + ")")}` : "";
503
+ console.log(` ${c.dim(h.hito || "?")}: total ${h.total ?? "N/A"}${saved}`);
504
+ }
505
+ }
506
+
353
507
  return bad === 0 ? 0 : 1;
354
508
  }
355
509
 
510
+ /** Detecta si ozali-jarvis está configurado en el proyecto y dónde. */
511
+ function detectJarvis(cwd) {
512
+ const where = [];
513
+ const hasBlock = (f) => exists(f) && fs.readFileSync(f, "utf8").includes(JARVIS_BEGIN);
514
+ if (hasBlock(path.join(cwd, "CLAUDE.md"))) where.push("CLAUDE.md");
515
+ if (hasBlock(path.join(cwd, "AGENTS.md"))) where.push("AGENTS.md");
516
+ if (exists(path.join(cwd, ".claude", "agents", "ozali-jarvis.md"))) where.push("subagente");
517
+ const oc = readJSON(path.join(cwd, "opencode.json"));
518
+ if (oc && oc.agent && oc.agent["ozali-jarvis"]) where.push("opencode");
519
+ return { present: where.length > 0, where };
520
+ }
521
+
356
522
  function readStrictTdd(cwd, sot) {
357
523
  const f = path.join(cwd, sot.dir, "context", "tech-stack.md");
358
524
  if (!exists(f)) return { found: false };
@@ -465,3 +631,77 @@ export function sync(cwd, opts) {
465
631
  }
466
632
  return 0;
467
633
  }
634
+
635
+ // ============================================================ audit ===========
636
+ // Navega/audita la memoria de Engram: del proyecto actual o general (todos los
637
+ // proyectos). Sin contexto de proyecto en la ruta → general. Sin Engram → audita
638
+ // el histórico local de documentos.
639
+ export async function audit(cwd, opts) {
640
+ step("ozali audit — auditoría de memoria (Engram)");
641
+ const env = detectAll(cwd);
642
+ const cfg = readJSON(CONFIG_PATH(cwd));
643
+ const engramCfg = readJSON(path.join(cwd, ".engram", "config.json"));
644
+ const project = (engramCfg && engramCfg.project_name) || (cfg && cfg.project) || projectName(cwd);
645
+ const hasProjectContext = env.git.isRepo || !!cfg || !!engramCfg;
646
+
647
+ // Resolver alcance: --general fuerza general; sin contexto → general; en repo se propone elegir.
648
+ let scope = (opts.general || !hasProjectContext) ? "general" : "project";
649
+ if (!hasProjectContext) info("Sin contexto de proyecto en esta ruta → auditoría " + c.bold("general") + ".");
650
+ else if (scope === "project" && !opts.general && !opts.yes) {
651
+ scope = await select("¿Qué auditoría quieres?", [
652
+ { value: "project", label: `Proyecto (${c.bold(project)})` },
653
+ { value: "general", label: "General (todos los proyectos en Engram)" },
654
+ ], 0);
655
+ }
656
+
657
+ // Sin Engram → auditar el histórico local de documentos.
658
+ if (!env.engram.available) {
659
+ warn("Engram no está instalado → auditoría desde documentos locales.");
660
+ return auditFromDocs(cwd, project);
661
+ }
662
+
663
+ // Navegador interactivo.
664
+ if (opts.tui) {
665
+ info("Abriendo el navegador interactivo de Engram (engram tui)…");
666
+ return spawnCmd("engram", ["tui"], { cwd });
667
+ }
668
+
669
+ if (scope === "general") {
670
+ step("Proyectos en Engram");
671
+ spawnCmd("engram", ["projects", "list"], { cwd });
672
+ step("Estadísticas (global)");
673
+ spawnCmd("engram", ["stats"], { cwd });
674
+ step("Contexto reciente");
675
+ spawnCmd("engram", ["context"], { cwd });
676
+ } else {
677
+ step(`Contexto reciente — ${project}`);
678
+ spawnCmd("engram", ["context", project], { cwd });
679
+ step("Estadísticas (global)");
680
+ spawnCmd("engram", ["stats"], { cwd });
681
+ }
682
+ if (opts.search) {
683
+ step(`Búsqueda: "${opts.search}"`);
684
+ spawnCmd("engram", ["search", opts.search], { cwd });
685
+ }
686
+ info("Navegación interactiva: " + c.bold("ozali audit --tui") + " · búsqueda: " + c.bold('ozali audit --search "<texto>"'));
687
+ return 0;
688
+ }
689
+
690
+ function auditFromDocs(cwd, project) {
691
+ const docsDir = path.join(cwd, ".ozali", "docs", "cdk");
692
+ if (exists(docsDir)) {
693
+ step(`Hitos documentados localmente — ${project}`);
694
+ const hitos = fs.readdirSync(docsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
695
+ if (hitos.length) for (const e of hitos) console.log(" • " + e.name);
696
+ else info("Aún no hay hitos en .ozali/docs/cdk/.");
697
+ } else {
698
+ info("No hay histórico local en .ozali/docs/cdk/ todavía.");
699
+ }
700
+ const metrics = readJSON(path.join(cwd, ".ozali", "metrics", "token-metrics.json"));
701
+ if (metrics && Array.isArray(metrics.hits) && metrics.hits.length) {
702
+ step("Uso de tokens (últimos hitos)");
703
+ for (const h of metrics.hits.slice(-5)) console.log(` ${c.dim(h.hito || "?")}: total ${h.total ?? "N/A"}`);
704
+ }
705
+ info("Instala Engram para auditoría buscable/acumulativa (" + c.bold("ozali doctor") + ").");
706
+ return 0;
707
+ }
package/cli/lib/util.mjs CHANGED
@@ -10,6 +10,7 @@ import { execFileSync } from "node:child_process";
10
10
  const HERE = path.dirname(fileURLToPath(import.meta.url));
11
11
  export const PKG_ROOT = path.resolve(HERE, "..", "..");
12
12
  export const SKILL_SRC = path.join(PKG_ROOT, "skill");
13
+ export const TEMPLATES_SRC = path.join(PKG_ROOT, "templates");
13
14
 
14
15
  export function pkgVersion() {
15
16
  try {
@@ -33,9 +33,20 @@ test("--version imprime la versión del package.json", () => {
33
33
  assert.equal(stdout.trim(), pkg.version);
34
34
  });
35
35
 
36
- test("--help menciona los 4 comandos", () => {
36
+ test("--help menciona los comandos", () => {
37
37
  const { stdout } = run(["--help"]);
38
- for (const cmd of ["init", "doctor", "update", "sync"]) assert.match(stdout, new RegExp(cmd));
38
+ for (const cmd of ["init", "doctor", "update", "sync", "audit"]) assert.match(stdout, new RegExp(cmd));
39
+ });
40
+
41
+ test("audit imprime cabecera y no rompe (general)", () => {
42
+ const dir = tmpProject();
43
+ try {
44
+ // Sin Engram → fallback a docs; con Engram → comandos read-only. Ambos exit 0.
45
+ const { stdout } = run(["audit", "--general", "--yes"], dir);
46
+ assert.match(stdout, /auditoría de memoria/);
47
+ } finally {
48
+ fs.rmSync(dir, { recursive: true, force: true });
49
+ }
39
50
  });
40
51
 
41
52
  test("comando desconocido sale con código 1", () => {
@@ -57,7 +68,7 @@ test("doctor en proyecto vacío reporta pendientes (exit 1)", () => {
57
68
  test("init --yes instala la skill, aísla el histórico y escribe config", () => {
58
69
  const dir = tmpProject();
59
70
  try {
60
- run(["init", "--yes", "--no-engram", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
71
+ run(["init", "--yes", "--no-engram", "--no-trust", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
61
72
  assert.ok(fs.existsSync(path.join(dir, ".claude", "skills", "ozali", "SKILL.md")), "SKILL.md instalada");
62
73
  assert.ok(fs.existsSync(path.join(dir, ".ozali", "config.json")), "config escrita");
63
74
  const cfg = JSON.parse(fs.readFileSync(path.join(dir, ".ozali", "config.json"), "utf8"));
@@ -71,6 +82,37 @@ test("init --yes instala la skill, aísla el histórico y escribe config", () =>
71
82
  const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
72
83
  assert.ok(Array.isArray(settings.permissions.allow) && settings.permissions.allow.length > 0, "allow no vacío");
73
84
  assert.ok(settings.permissions.deny.includes("Bash(rm -rf *)"), "deny bloquea rm -rf");
85
+ // ozali-jarvis (Claude Code)
86
+ assert.match(fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8"), /ozali-jarvis:start/, "bloque jarvis en CLAUDE.md");
87
+ assert.ok(fs.existsSync(path.join(dir, ".claude", "agents", "ozali-jarvis.md")), "subagente jarvis");
88
+ assert.ok(settings.hooks && settings.hooks.SessionStart, "hooks de recordatorio jarvis");
89
+ const eng = JSON.parse(fs.readFileSync(path.join(dir, ".engram", "config.json"), "utf8"));
90
+ assert.ok(eng.project_name, ".engram/config.json con project_name");
91
+ } finally {
92
+ fs.rmSync(dir, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("init --no-jarvis no crea el orquestador", () => {
97
+ const dir = tmpProject();
98
+ try {
99
+ run(["init", "--yes", "--no-engram", "--no-trust", "--no-jarvis", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
100
+ assert.ok(!fs.existsSync(path.join(dir, "CLAUDE.md")), "sin CLAUDE.md");
101
+ assert.ok(!fs.existsSync(path.join(dir, ".claude", "agents", "ozali-jarvis.md")), "sin subagente jarvis");
102
+ assert.ok(!fs.existsSync(path.join(dir, ".engram", "config.json")), "sin .engram/config.json");
103
+ } finally {
104
+ fs.rmSync(dir, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ test("init opencode crea jarvis (AGENTS.md + agente + plugin)", () => {
109
+ const dir = tmpProject();
110
+ try {
111
+ run(["init", "--yes", "--no-engram", "--no-trust", "--agent", "opencode", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
112
+ assert.match(fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8"), /ozali-jarvis:start/, "bloque jarvis en AGENTS.md");
113
+ const oc = JSON.parse(fs.readFileSync(path.join(dir, "opencode.json"), "utf8"));
114
+ assert.equal(oc.agent["ozali-jarvis"].mode, "primary", "agente jarvis primary en opencode.json");
115
+ assert.ok(fs.existsSync(path.join(dir, ".opencode", "plugins", "ozali-jarvis.js")), "plugin jarvis opencode");
74
116
  } finally {
75
117
  fs.rmSync(dir, { recursive: true, force: true });
76
118
  }
@@ -85,6 +85,7 @@ Lleva el histórico y la memoria al repo de conocimiento del equipo. Tus compañ
85
85
  | `ozali update` | Actualiza la skill a la última versión. |
86
86
  | `ozali sync` | Sube el histórico/memoria al repo de equipo. |
87
87
  | `ozali sync --import` | Baja lo que el equipo ya guardó. |
88
+ | `ozali audit` | Navega/audita la memoria de Engram (qué se ha hecho). |
88
89
 
89
90
  ¿Algo no funciona? Corre **`ozali doctor`** primero: te dice qué falta y cómo arreglarlo.
90
91
 
@@ -101,6 +102,11 @@ Es **un punto de partida, no una jaula**: puedes editar ese archivo y añadir lo
101
102
  necesite. Si vuelves a correr `ozali init`, tus reglas **se conservan** (no se borran ni se
102
103
  duplican).
103
104
 
105
+ > **Confianza del workspace (Claude Code):** por seguridad, Claude Code **ignora** los permisos de
106
+ > un proyecto hasta que confías en él (verás un aviso *"this workspace has not been trusted"*).
107
+ > `init` te ofrece marcarlo como confiable automáticamente; si prefieres hacerlo tú, abre Claude
108
+ > Code en la carpeta y acepta el diálogo de confianza una vez. Con `--no-trust` ozali no lo toca.
109
+
104
110
  Ejemplo — permitir Docker además de lo de fábrica:
105
111
 
106
112
  ```json
@@ -114,6 +120,23 @@ Ejemplo — permitir Docker además de lo de fábrica:
114
120
 
115
121
  ---
116
122
 
123
+ ## ozali-jarvis: el orquestador (no necesitas /cdk)
124
+
125
+ `init` crea **ozali-jarvis**, el "cerebro" por defecto de tu proyecto. A partir de ahí, **en cada
126
+ sesión y sin escribir `/cdk`**, tu agente:
127
+
128
+ - **recuerda** lo que el equipo trabajó antes (recupera contexto de Engram al iniciar),
129
+ - **registra** lo que van haciendo (decisiones, acciones, aprendizajes) para que quede acumulado,
130
+ - y cuando toca **escribir código con disciplina**, delega en la skill `cdk` (plan, aprobación, TDD).
131
+
132
+ Sigues pudiendo invocar `/cdk` cuando quieras; jarvis simplemente lo usa como referencia y mantiene
133
+ todo en contexto. ¿No lo quieres? `init --no-jarvis`.
134
+
135
+ ### Menos tokens, más contexto
136
+ Con Engram en línea, jarvis y `cdk` trabajan **recall-first**: antes de releer archivos o re-analizar,
137
+ reutilizan el análisis/resumen que ya está en memoria (si el código no cambió). Eso gasta **menos
138
+ tokens** y mantiene el contexto enfocado. Puedes ver la tendencia de consumo con `ozali doctor`.
139
+
117
140
  ## Engram: la memoria del equipo (opcional)
118
141
 
119
142
  Durante `init`, ozali **instala Engram por ti**: en modo interactivo te pregunta (basta con aceptar),
@@ -137,6 +160,17 @@ Más detalle técnico en [engram-convention.md](../skill/references/engram-conve
137
160
 
138
161
  ---
139
162
 
163
+ ## Auditar lo que se ha hecho (`ozali audit`)
164
+
165
+ ¿Quieres ver qué ha registrado el equipo? `ozali audit`:
166
+
167
+ - **Dentro de un repo:** te propone auditar **ese proyecto** o **todos** (general).
168
+ - **Fuera de un repo** (una ruta cualquiera): va directo a la auditoría **general**.
169
+
170
+ Atajos: `ozali audit --tui` abre un navegador interactivo de la memoria; `ozali audit --search "rfc"`
171
+ busca un término; `ozali audit --general` fuerza el alcance general. Si no tienes Engram, audita el
172
+ histórico local de `.ozali/docs/`.
173
+
140
174
  ## Preguntas frecuentes
141
175
 
142
176
  **¿Tengo que usar Engram?**
@@ -151,6 +185,11 @@ Lo corriste con `dlx`/`npx`, que es temporal. Instálalo global: `pnpm add -g oz
151
185
  **¿Esto ensucia mi repositorio?**
152
186
  No. El histórico y la memoria se aíslan (`.ozali/`, `.engram/` van al `.gitignore`).
153
187
 
188
+ **Veo "this workspace has not been trusted" y mis permisos no aplican.**
189
+ Es el aviso de seguridad de Claude Code: ignora los permisos del proyecto hasta confiar en él.
190
+ Deja que `init` lo marque como confiable, o abre Claude Code en la carpeta y acepta el diálogo una
191
+ vez. Tras eso, el aviso desaparece y los permisos surten efecto.
192
+
154
193
  **¿Funciona en Windows?**
155
194
  Sí. Los permisos base incluyen variantes PowerShell, y Engram se instala con `go install` o un
156
195
  binario descargable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Bootstrap interactivo de IA por equipo: calibra el proyecto, genera la skill de ejecución (cdk) con TDD/SDD, y mantiene memoria de equipo (Engram) con histórico aislado.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "cli/",
14
14
  "skill/",
15
+ "templates/",
15
16
  "docs/",
16
17
  "README.md",
17
18
  "LICENSE"
package/skill/SKILL.md CHANGED
@@ -308,12 +308,25 @@ recuperable, acumulativo). Así la herramienta **aprende de lo que el equipo va
308
308
  completo (naming, llamadas inline, degradación) en
309
309
  [`references/engram-convention.md`](references/engram-convention.md).
310
310
 
311
+ > **Orquestador por defecto (`ozali-jarvis`):** `ozali init` crea el agente **ozali-jarvis**
312
+ > (persona en CLAUDE.md/AGENTS.md + subagente + hooks) que opera **memoria-aware en toda sesión, sin
313
+ > necesidad de `/cdk`**: recupera contexto de Engram al iniciar, registra el trabajo del equipo
314
+ > conforme avanza, y **delega la ejecución disciplinada en `cdk`**. `cdk` y jarvis comparten el mismo
315
+ > contrato de memoria (esta convención).
316
+
311
317
  **Mínimos que `cdk` debe cablear** (resumen; el detalle y los `mem_*` exactos están en la convención):
312
318
 
313
- - **Al iniciar un hito** → `mem_save_prompt` (captura el prompt real una vez) + `mem_search` de
314
- `cdk/{hito}/state`, `cdk/_project/testing-capabilities` y `cdk/{hito}/*` → `mem_get_observation`
315
- de lo relevante. Si hay `state` con pendientes, **reanuda**; con `testing-capabilities` resuelve
316
- el modo TDD.
319
+ - **Al iniciar un hito** → handshake "Engram en línea" (`mem_current_project` + `mem_context`) +
320
+ `mem_save_prompt` (captura el prompt real una vez) + `mem_search` de `cdk/{hito}/state`,
321
+ `cdk/_project/testing-capabilities`, `cdk/_project/token-metrics` y `cdk/{hito}/*`
322
+ `mem_get_observation` **selectivo** de lo relevante. Si hay `state` con pendientes, **reanuda**; con
323
+ `testing-capabilities` resuelve el modo TDD.
324
+ - **Recall-first (ahorro de tokens/contexto)** → antes de releer/re-analizar, reusar el
325
+ `analisis`/`resumen-tecnico` previo si su `ultimo_commit` sigue vigente (guard de staleness); si el
326
+ código cambió, re-analizar solo el delta. Recuperación selectiva y restauración tras compactación
327
+ desde `state`+`bitacora`. Ver convención §7.
328
+ - **Telemetría** → espejar `uso-tokens` a `cdk/{hito}/uso-tokens`, agregar en
329
+ `cdk/_project/token-metrics` y escribir `.ozali/metrics/token-metrics.json` (lo lee `ozali doctor`).
317
330
  - **Al aprobar el GATE** → `mem_save` de `cdk/{hito}/plan-aprobado` **una sola vez** (inmutable).
318
331
  - **Durante el ciclo** → `mem_update` (upsert) de `cdk/{hito}/bitacora` y `cdk/{hito}/state` tras
319
332
  cada transición de fase (el archivo `05` sigue siendo append-only como fuente de verdad).
@@ -55,13 +55,15 @@ Para cada subagente, `cdk` debe generar un `.claude/agents/<rol>.md` con frontma
55
55
  ### 3. project-analyzer
56
56
  - **Misión:** entender el código existente y el impacto del cambio. *No se planea lo que
57
57
  no se ha entendido: primero el terreno, después el plano.*
58
- - **Responsabilidades:** **primero, SIEMPRE**, ejecuta el harness
59
- `node .claude/skills/cdk/verify-structure.mjs --grep <palabra-clave>` su salida es la
60
- **base factual** del análisis (estructura real, clases clave, discrepancias doc↔código,
61
- candidatos a **reutilización antes de crear**). Luego mapea dependencias, identifica
62
- **hitos de importancia** y riesgos, superficies afectadas y contratos en riesgo. Marca
63
- los riesgos **🔴 BLOQUEANTES** que detienen el flujo hasta resolverse. Es el autor del
64
- **Documento 1 (análisis)**.
58
+ - **Responsabilidades:** **recall antes de recomputar** — primero `mem_search` del `analisis`/
59
+ `resumen-tecnico` previo del área; si existe con `ultimo_commit` vigente, **reúsalo** (vía
60
+ `mem_get_observation` selectivo) en vez de releer archivos grandes; si el código cambió, analiza
61
+ **solo el delta** (convención §7). Ejecuta **siempre** el harness
62
+ `node .claude/skills/cdk/verify-structure.mjs --grep <palabra-clave>` (estructural, barato) como
63
+ **base factual** (estructura real, clases clave, discrepancias doc↔código, candidatos a
64
+ **reutilización antes de crear**). Luego mapea dependencias, identifica **hitos de importancia** y
65
+ riesgos, superficies afectadas y contratos en riesgo. Marca los riesgos **🔴 BLOQUEANTES** que
66
+ detienen el flujo hasta resolverse. Es el autor del **Documento 1 (análisis)**.
65
67
  - **Entrada:** tarea. **Salida:** informe de impacto + hitos (+ preguntas abiertas al usuario).
66
68
  - **Herramientas:** solo lectura (Read, Grep, Glob) + ejecución del harness (Bash/PowerShell
67
69
  acotado a `node verify-structure.mjs`).
@@ -70,7 +72,10 @@ Para cada subagente, `cdk` debe generar un `.claude/agents/<rol>.md` con frontma
70
72
  - **Misión:** enrutar el trabajo entre subagentes e integrar resultados.
71
73
  - **Responsabilidades:** decide qué subagente actúa y en qué orden; gestiona iteraciones
72
74
  executioners ⇄ tester; consolida la salida final. **Alimenta al `project-documenter`** con
73
- los eventos del ciclo (dudas, decisiones, desvíos) para la bitácora `05`.
75
+ los eventos del ciclo (dudas, decisiones, desvíos) para la bitácora `05`. **Hace cumplir el
76
+ recall-first** (convención §7): recuperación selectiva (no volcar toda la memoria), restauración
77
+ desde `state`+`bitacora` tras compactación, y consulta `cdk/_project/token-metrics` al iniciar para
78
+ ajustar la agresividad del recall.
74
79
  - **Modo `--auto`:** si el hito se invocó con `--auto`, recorre el hito de punta a punta
75
80
  **sin prompts** —incluida la **lectura de rutas no contempladas** durante el análisis y la
76
81
  ejecución post-GATE— con el **único alto en el 🛑 GATE del plan**. Si algo cae **fuera de
@@ -117,6 +122,8 @@ Para cada subagente, `cdk` debe generar un `.claude/agents/<rol>.md` con frontma
117
122
  - `03-resumen-tecnico.md`, `04-resumen-usuario.md` y `06-uso-tokens.md` se escriben **al
118
123
  cierre**. El `06` solo rellena métricas si el proveedor es **Claude/Anthropic** (fuente
119
124
  `/cost`); con otro proveedor deja la estructura + nombre del proveedor y métricas `N/A`.
125
+ Además **espeja** `06` a Engram (`cdk/{hito}/uso-tokens`), actualiza el agregado
126
+ `cdk/_project/token-metrics` y escribe `.ozali/metrics/token-metrics.json` (lo lee `ozali doctor`).
120
127
  - **Herramientas:** escritura de documentación (Read, Write).
121
128
 
122
129
  ### 8. tester
@@ -291,6 +291,13 @@ usuario, en:
291
291
  | Total de tiempo de sesión | <n / N/A> |
292
292
  | Costo estimado | <$ / N/A> |
293
293
 
294
+ ## Ahorro por recall (Engram)
295
+ | Métrica | Valor |
296
+ | :-- | :-- |
297
+ | ¿Se reusó memoria? (recall-first) | sí / no |
298
+ | Artefactos reusados | <p. ej. cdk/<hito>/analisis @ <commit>> |
299
+ | Relectura evitada (estimada) | <n tokens / N/A> |
300
+
294
301
  ## Notas
295
302
  - <observaciones sobre el consumo del hito; deja vacío o "N/A" si no puedes encontrar información del proveedor>
296
303
  ```
@@ -299,3 +306,23 @@ usuario, en:
299
306
  > derivar ni inventar cifras de otro sistema: solo registra el nombre del proveedor y deja
300
307
  > el resto en `N/A`. El objetivo es **mantener la estructura presente** para no perder la
301
308
  > trazabilidad del hito.
309
+
310
+ ### Espejo en Engram + agregado (telemetría)
311
+
312
+ Al cerrar, además del doc, `cdk` **espeja** las métricas a Engram y mantiene un agregado por
313
+ proyecto (ver [`engram-convention.md`](engram-convention.md) §7):
314
+
315
+ - `cdk/{hito}/uso-tokens` — métricas del hito (mismo contenido que el doc).
316
+ - `cdk/_project/token-metrics` — agregado de los últimos hitos (para ajustar el recall-first).
317
+ - `.ozali/metrics/token-metrics.json` — copia **local** que lee `ozali doctor` para la tendencia:
318
+
319
+ ```json
320
+ {
321
+ "hits": [
322
+ { "hito": "<slug>", "input": 0, "output": 0, "total": 0, "savedByRecall": 0, "at": "<ISO-8601>" }
323
+ ]
324
+ }
325
+ ```
326
+
327
+ > `savedByRecall` = estimación de tokens de relectura evitados por reusar memoria. Con proveedor sin
328
+ > métricas, deja `0`/`N/A` pero mantén la estructura.
@@ -222,3 +222,50 @@ Internamente (Fase C lo implementa):
222
222
 
223
223
  > Trazabilidad: cada doc lleva `ultimo_commit`/`Commit:` del repo principal en su encabezado, así
224
224
  > el histórico aislado siempre apunta al código exacto que documenta.
225
+
226
+ ---
227
+
228
+ ## 7. Rendimiento: recall-first + telemetría de tokens
229
+
230
+ El objetivo de esta sección es **gastar menos tokens y contexto** reutilizando lo que el equipo ya
231
+ guardó en Engram, en vez de recomputarlo. Aplica tanto al uso **ambiental** (orquestador
232
+ `ozali-jarvis`, sin `/cdk`) como por **hito** (`cdk`).
233
+
234
+ ### 7.1 Handshake "Engram en línea"
235
+ Al iniciar sesión/hito: `mem_current_project` (confirma que coincide con `.engram/config.json`) +
236
+ `mem_context`. Si responde → **modo recall-first activo**. Si no responde → degrada a `docs` (§1);
237
+ la ausencia de Engram nunca bloquea.
238
+
239
+ ### 7.2 Recall-first con guard de staleness
240
+ **Antes** de releer archivos o re-analizar, `mem_search` del `analisis` / `plan-aprobado` /
241
+ `resumen-tecnico` del área. Cada artefacto lleva `ultimo_commit` (§6):
242
+
243
+ - Si el **SHA actual coincide** (o los archivos del área no cambiaron) → **reusar** el resumen
244
+ conciso vía `mem_get_observation`, **sin releer el código**. Ahorra tokens de entrada.
245
+ - Si **cambió** → re-analizar **solo el delta** (los archivos tocados desde `ultimo_commit`), no todo.
246
+
247
+ > El harness `verify-structure.mjs` (estructural, barato) puede seguir corriendo; lo que se evita es
248
+ > **releer archivos grandes** que ya tienen un resumen vigente en memoria.
249
+
250
+ ### 7.3 Recuperación selectiva
251
+ `mem_search` devuelve **previews truncados**. Haz `mem_get_observation` **solo** de los pocos
252
+ artefactos que realmente necesitas. **Nunca** vuelques toda la memoria al contexto.
253
+
254
+ ### 7.4 Restauración tras compactación
255
+ Tras una compactación (o reabrir sesión), reconstruye desde `state` (§4) + `bitacora` —ambos
256
+ concisos— en vez de re-derivar todo el contexto releyendo conversación y archivos.
257
+
258
+ ### 7.5 Guardar conciso para recall barato
259
+ Guarda resúmenes **estructurados y deduplicados** (no volcados crudos). Mientras más conciso el
260
+ artefacto, menos cuesta recuperarlo después.
261
+
262
+ ### 7.6 Telemetría de tokens (medir → mejorar)
263
+ - Espeja el uso de tokens del hito a `cdk/{hito}/uso-tokens` y mantén un agregado por proyecto en
264
+ `cdk/_project/token-metrics`.
265
+ - También escribe un resumen **local** en `.ozali/metrics/token-metrics.json` (últimos N hitos:
266
+ `{ hito, input, output, total, savedByRecall, at }`) para que `ozali doctor` muestre la tendencia
267
+ sin consultar el MCP.
268
+ - **Al iniciar**, recupera `cdk/_project/token-metrics`: si hitos similares fueron pesados, sé más
269
+ agresivo con recall-first (resume antes, evita relecturas grandes).
270
+
271
+ > `savedByRecall` = estimación de relectura evitada por reusar memoria (ver plantilla `06-uso-tokens`).
@@ -0,0 +1,27 @@
1
+ // ozali-jarvis-plugin — recordatorios de memoria (Engram) para opencode.
2
+ // Generado por `ozali init`. NO es la fuente de verdad del comportamiento:
3
+ // la persona ozali-jarvis vive en AGENTS.md + el agente de opencode.json.
4
+ // Este plugin solo refuerza, en eventos de sesión, el ciclo recall → capturar → resumir.
5
+ // Marca de idempotencia: ozali-jarvis (no reescribir si ya existe esta marca).
6
+
7
+ /** @type {import("@opencode-ai/plugin").Plugin} */
8
+ export const OzaliJarvis = async () => {
9
+ const note = (m) => { try { console.log(`[ozali-jarvis] ${m}`); } catch { /* noop */ } };
10
+ return {
11
+ event: async ({ event }) => {
12
+ switch (event && event.type) {
13
+ case "session.created":
14
+ note("recall-first: confirma proyecto (mem_current_project) y recupera contexto de Engram (mem_context) antes de actuar.");
15
+ break;
16
+ case "experimental.session.compacting":
17
+ note("antes de compactar: persiste el `state` recuperable en Engram (ver engram-convention §4).");
18
+ break;
19
+ case "session.idle":
20
+ note("cierre: registra lo trabajado en Engram (scope: project, español) y haz mem_session_summary.");
21
+ break;
22
+ default:
23
+ break;
24
+ }
25
+ },
26
+ };
27
+ };
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: ozali-jarvis
3
+ description: Orquestador principal del proyecto. Memoria-aware por defecto (Engram) y puente hacia la skill cdk. Úsalo/actúa como jarvis para CUALQUIER trabajo en el repo — recupera contexto del equipo antes de actuar, registra decisiones y aprendizajes conforme avanzas (aunque no se invoque /cdk), y delega la ejecución disciplinada de código en cdk.
4
+ ---
5
+
6
+ # ozali-jarvis — orquestador del proyecto
7
+
8
+ Eres **ozali-jarvis**, el orquestador por defecto de este repositorio. Tu trabajo es mantener al
9
+ equipo **en contexto** usando la memoria de Engram y delegar la ejecución de código a la skill
10
+ `cdk`. Operas así en **toda** sesión, sin necesidad de que el usuario escriba `/cdk`.
11
+
12
+ > Engram ya inyecta su **Memory Protocol** en la superficie de instrucciones (vía `engram setup`).
13
+ > Esta persona lo **complementa**: no dupliques sus reglas; añade la orquestación y el wiring a `cdk`.
14
+
15
+ ## 1. Al iniciar (recall-first)
16
+ - Confirma el proyecto con `mem_current_project` (debe coincidir con `.engram/config.json`).
17
+ - Recupera contexto reciente del equipo: `mem_context` + `mem_search` de lo relacionado a la tarea.
18
+ - **Recuperación selectiva:** `mem_search` devuelve previews truncados; haz `mem_get_observation`
19
+ **solo** de lo que necesitas. Nunca vuelques toda la memoria al contexto.
20
+ - **No recomputes lo ya sabido:** si existe un `analisis`/`resumen-tecnico`/`plan-aprobado` del área
21
+ con `ultimo_commit` vigente, reúsalo en vez de releer el código (ver
22
+ `references/engram-convention.md` §7, *recall-first con guard de staleness*).
23
+
24
+ ## 2. Durante el trabajo (captura ambiental, en español, scope: project)
25
+ - Registra en Engram las **decisiones, acciones y aprendizajes** del equipo conforme ocurren —
26
+ **aunque no se haya invocado `/cdk`**. Así el trabajo queda acumulado y buscable.
27
+ - Guarda **conciso y estructurado** (no volcados crudos) para que el recall futuro cueste menos.
28
+ - Idioma y scope según `references/engram-convention.md` §1.5: memoria compartida en **español**,
29
+ `scope: project`; lo puramente personal va en `scope: personal`.
30
+
31
+ ## 3. Para ejecutar cambios de código → delega en `cdk`
32
+ - Cuando el trabajo implique escribir/modificar código con disciplina (plan, GATE, TDD, docs por
33
+ hito), **usa la skill `cdk`**. jarvis prepara el contexto; `cdk` ejecuta.
34
+ - Si `cdk` aún no existe en el proyecto, sugiere generarla corriendo la skill **`ozali`**
35
+ ("diagnostica el proyecto").
36
+
37
+ ## 4. Al cerrar
38
+ - Cierra la sesión con `mem_session_summary` (solo el agente top-level, nunca un subagente) — qué se
39
+ logró, decisiones clave y pendientes.
40
+ - Antes de una compactación de contexto, persiste el `state` recuperable (ver convención §4).
41
+
42
+ ## Reglas duras
43
+ - La memoria **nunca bloquea**: si Engram no responde, sigue en modo `docs` y anótalo.
44
+ - **Persistir antes de responder** cuando guardes en Engram dentro de un subagente.
45
+ - No inventes contenido de memoria; si no hay contexto, no fabriques.