ozali 0.3.0 → 0.4.1

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  # ozali
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/ozali.svg)](https://www.npmjs.com/package/ozali)
6
+
5
7
  **Bootstrap de IA por equipo — calibra tus proyectos, genera una skill de ejecución con TDD/SDD, y mantiene memoria de equipo (Engram) con histórico aislado.**
6
8
 
7
9
  </div>
@@ -59,10 +61,28 @@ git clone <repo> && node ozali/cli/bin/ozali.mjs init
59
61
  ```bash
60
62
  ozali init # detecta agente, instala la skill, aísla el histórico, instala+configura Engram
61
63
  ozali doctor # health-check read-only (fuente de verdad, Engram, Cloud, Strict TDD, runner…)
62
- ozali update # actualiza la skill instalada a la versión del paquete
64
+ ozali update # actualiza la instalación (skill ozali + ozali-jarvis + permisos) al paquete
63
65
  ozali sync # lleva el histórico (docs + Engram) al repo de conocimiento de equipo
66
+ ozali audit # navega/audita la memoria de Engram del proyecto (o general)
67
+ ```
68
+
69
+ `ozali audit` recorre lo que el equipo ha acumulado en Engram: dentro de un repo propone auditar
70
+ **ese proyecto** o **general** (todos los proyectos); fuera de un repo va directo a general. Usa
71
+ `--tui` para el navegador interactivo, `--search "<texto>"` para buscar y `--general` para forzar el
72
+ alcance. Sin Engram, audita el histórico local de `.ozali/docs/`.
73
+
74
+ ### Actualizar a una versión nueva
75
+
76
+ ```bash
77
+ pnpm add -g ozali@<versión> # actualiza el CLI global (pinea la versión por el cooldown de pnpm)
78
+ ozali update # en cada repo: refresca skill ozali + ozali-jarvis + permisos
64
79
  ```
65
80
 
81
+ `ozali update` también **crea ozali-jarvis** en repos inicializados con versiones anteriores. La skill
82
+ `cdk` la **regenera tu agente** (no el CLI): tras `ozali update`, abre el agente y vuelve a correr la
83
+ skill `ozali` para regenerar `cdk` con el contrato nuevo — tus docs por hito y el plan congelado se
84
+ conservan.
85
+
66
86
  `init` también escribe un **perfil base de permisos** (`.claude/settings.json` para Claude Code,
67
87
  `opencode.json` para opencode) para reducir confirmaciones: deja libres comandos seguros y bloquea
68
88
  los destructivos. Es un template — tus reglas se conservan al re-correr `init`. Como Claude Code
@@ -75,9 +95,16 @@ macOS/Linux, `go install` en Windows, con binario precompilado como fallback. Si
75
95
  instalarlo, pasa `--no-engram` (arranca en modo `docs`). Opcionalmente habilita **Engram Cloud**
76
96
  (réplica de equipo opt-in) además del git-sync.
77
97
 
98
+ `init` también crea **ozali-jarvis**, un **orquestador always-on**: persona en `CLAUDE.md`/`AGENTS.md`
99
+ + subagente + hooks de recordatorio que hace que el agente, **en toda sesión y sin necesidad de
100
+ `/cdk`**, recupere contexto de Engram, registre el trabajo del equipo (memoria en contexto) y delegue
101
+ la ejecución disciplinada en `cdk`. Fija el proyecto en `.engram/config.json` para memoria
102
+ determinista. Con Engram en línea, jarvis y `cdk` operan **recall-first** (reusan memoria en vez de
103
+ releer) para gastar menos tokens/contexto. Omítelo con `--no-jarvis`.
104
+
78
105
  Flags útiles: `--yes` (no interactivo), `--dry-run` (init sin escribir), `--no-engram`, `--no-trust`,
79
- `--agent`, `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo mental completo
80
- en [docs/intended-usage.md](docs/intended-usage.md).
106
+ `--no-jarvis`, `--agent`, `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo
107
+ mental completo en [docs/intended-usage.md](docs/intended-usage.md).
81
108
 
82
109
  ## Agentes soportados
83
110
 
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)
@@ -15,8 +15,9 @@ ${c.bold("Comandos:")}
15
15
  init Detecta el agente (Claude Code/opencode), instala la skill ozali,
16
16
  aísla el histórico, configura Engram y el repo de conocimiento.
17
17
  doctor Health-check read-only del proyecto (fuente de verdad, Engram, TDD…).
18
- update Actualiza la skill ozali instalada a la versión de este paquete.
18
+ update Actualiza la instalación (skill ozali + ozali-jarvis + permisos) al 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.
@@ -26,9 +27,13 @@ ${c.bold("Opciones comunes:")}
26
27
  --knowledge-repo <p> (init) Ruta del repo de conocimiento.
27
28
  --no-engram (init) No usar Engram; arranca en modo docs.
28
29
  --no-trust (init) No marcar el workspace como confiable en Claude Code.
30
+ --no-jarvis (init/update) No crear/refrescar el orquestador ozali-jarvis.
29
31
  --import (sync) Importa del repo de conocimiento a local.
30
32
  --push (sync) Hace push al remoto del repo de conocimiento.
31
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.
32
37
  -h, --help Esta ayuda. -v, --version Versión.
33
38
 
34
39
  ${c.bold("Instalación segura:")}
@@ -48,6 +53,10 @@ function parseArgs(argv) {
48
53
  else if (a === "--cloud") opts.cloud = true;
49
54
  else if (a === "--no-engram") opts.noEngram = true;
50
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];
51
60
  else if (a === "--agent") opts.agent = argv[++i];
52
61
  else if (a === "--scope") opts.scope = argv[++i];
53
62
  else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
@@ -66,11 +75,14 @@ async function main() {
66
75
  if (!cmd || opts.help || cmd === "help") { console.log(HELP); return 0; }
67
76
 
68
77
  const cwd = process.cwd();
78
+ // Banner de versión: confirma qué versión de ozali está corriendo en cada comando.
79
+ console.log(`${c.bold("ozali")} ${c.dim("v" + pkgVersion())}`);
69
80
  switch (cmd) {
70
81
  case "init": return await init(cwd, opts);
71
82
  case "doctor": return doctor(cwd);
72
- case "update": return update(cwd);
83
+ case "update": return update(cwd, opts);
73
84
  case "sync": return sync(cwd, opts);
85
+ case "audit": return await audit(cwd, opts);
74
86
  default:
75
87
  err(`comando desconocido "${cmd}". Usa ${c.bold("ozali --help")}.`);
76
88
  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";
@@ -111,6 +111,16 @@ export async function init(cwd, opts) {
111
111
  ensureOpencodeProfile(cwd);
112
112
  }
113
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
+
114
124
  // 3) gitignore del histórico aislado
115
125
  if (env.git.isRepo) {
116
126
  const { added } = ensureGitignore(cwd, [".ozali/", ".engram/"]);
@@ -286,6 +296,102 @@ function ensureClaudeCodeProfile(cwd, scope) {
286
296
  }
287
297
  }
288
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
+
289
395
  // Marca el proyecto como confiable en Claude Code (~/.claude.json). Sin esto, Claude Code
290
396
  // ignora los permisos de un .claude/settings.json de proyecto ("workspace not trusted").
291
397
  async function ensureClaudeWorkspaceTrust(cwd, opts) {
@@ -342,6 +448,13 @@ export function doctor(cwd) {
342
448
  add("Fuente de verdad", env.sot.found, env.sot.found ? `${env.sot.doc} + ${env.sot.dir}/` : "ausente (corre la skill 'ozali')");
343
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)");
344
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)");
345
458
  const cloudOn = !!(cfg && cfg.cloud && cfg.cloud.enabled);
346
459
  // Cloud es opt-in: "off" es un estado válido (no cuenta como fallo).
347
460
  add("Engram Cloud", true, cloudOn ? `enrolado → ${cfg.cloud.server || "server por defecto"}` : "off (opt-in, git-sync activo)");
@@ -381,9 +494,31 @@ export function doctor(cwd) {
381
494
  }
382
495
  }
383
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
+
384
507
  return bad === 0 ? 0 : 1;
385
508
  }
386
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
+
387
522
  function readStrictTdd(cwd, sot) {
388
523
  const f = path.join(cwd, sot.dir, "context", "tech-stack.md");
389
524
  if (!exists(f)) return { found: false };
@@ -393,20 +528,72 @@ function readStrictTdd(cwd, sot) {
393
528
  }
394
529
 
395
530
  // =========================================================== update ==========
396
- export function update(cwd) {
397
- step("ozali update actualizar skill instalada");
531
+ // Lleva una instalación existente al paquete actual: refresca la skill ozali (con sus
532
+ // references), los perfiles de permisos y **crea/refresca ozali-jarvis** (clave para repos
533
+ // inicializados antes de 0.4.0). La skill `cdk` la regenera el AGENTE (no el CLI): se detecta
534
+ // y se guía la regeneración.
535
+ export function update(cwd, opts = {}) {
536
+ step("ozali update — actualizar la instalación al paquete actual");
398
537
  const env = detectAll(cwd);
399
- if (!env.skill.installed) { warn("No hay skill ozali instalada. Corre " + c.bold("ozali init") + " primero."); return 1; }
400
- for (const p of env.skill.paths) {
401
- copyDir(SKILL_SRC, p);
402
- ok(`Actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
403
- }
404
538
  const cfgPath = CONFIG_PATH(cwd);
405
539
  const cfg = readJSON(cfgPath);
540
+ if (!env.skill.installed && !cfg) {
541
+ warn("No hay instalación de ozali en esta ruta. Corre " + c.bold("ozali init") + " primero.");
542
+ return 1;
543
+ }
544
+
545
+ // 1) Skill ozali (incluye las references: la base desde la que el agente regenera cdk)
546
+ if (env.skill.installed) {
547
+ for (const p of env.skill.paths) {
548
+ copyDir(SKILL_SRC, p);
549
+ ok(`Skill ozali actualizada: ${path.relative(cwd, p) || p} → v${pkgVersion()}`);
550
+ }
551
+ } else {
552
+ warn("Skill ozali no instalada en esta ruta (corre " + c.bold("ozali init") + " para instalarla).");
553
+ }
554
+
555
+ // Agente/scope: del config; si falta, infiere del entorno.
556
+ const agent = (cfg && cfg.agent) || (env.agents.opencode.present && !env.agents.claudeCode.present ? "opencode"
557
+ : env.agents.claudeCode.present && env.agents.opencode.present ? "both" : "claude-code");
558
+ const scope = (cfg && cfg.scope) || "project";
559
+
560
+ // 2) Perfiles base de permisos (idempotente: recoge defaults nuevos del paquete)
561
+ if (agent === "claude-code" || agent === "both") ensureClaudeCodeProfile(cwd, scope);
562
+ if (agent === "opencode" || agent === "both") ensureOpencodeProfile(cwd);
563
+
564
+ // 3) ozali-jarvis: crea el orquestador en repos previos a 0.4.0 y refresca el resto.
565
+ if (!opts.noJarvis) {
566
+ const proj = projectName(cwd);
567
+ const engPath = path.join(cwd, ".engram", "config.json");
568
+ if (!exists(engPath)) { writeJSON(engPath, { project_name: proj }); ok(`Proyecto de memoria fijado en ${c.bold(".engram/config.json")} (${proj}).`); }
569
+ if (agent === "claude-code" || agent === "both") ensureJarvisClaudeCode(cwd);
570
+ if (agent === "opencode" || agent === "both") ensureJarvisOpencode(cwd);
571
+ }
572
+
573
+ // 4) Skill cdk: la genera el AGENTE (Fase 6), el CLI no la regenera.
574
+ const cdk = detectCdk(cwd);
575
+ if (cdk.installed) {
576
+ step("Skill cdk (generada por el agente)");
577
+ warn("cdk no se actualiza desde el CLI: la regenera tu agente con el contrato nuevo.");
578
+ info("Abre tu agente y vuelve a correr la skill " + c.bold("ozali") + " para regenerar cdk (ozali-jarvis, recall-first §7, telemetría).");
579
+ info("Tus docs por hito (" + c.bold(".ozali/docs/cdk/") + ") y el plan congelado se conservan.");
580
+ }
581
+
582
+ // 5) versión del config
406
583
  if (cfg) { cfg.version = pkgVersion(); cfg.updatedAt = new Date().toISOString(); writeJSON(cfgPath, cfg); }
584
+ ok(`Instalación al día con ozali v${pkgVersion()}.`);
407
585
  return 0;
408
586
  }
409
587
 
588
+ /** ¿Existe la skill cdk (generada por el agente)? */
589
+ function detectCdk(cwd) {
590
+ const paths = [
591
+ path.join(cwd, ".claude", "skills", "cdk", "SKILL.md"),
592
+ path.join(HOME, ".claude", "skills", "cdk", "SKILL.md"),
593
+ ].filter(exists);
594
+ return { installed: paths.length > 0, paths };
595
+ }
596
+
410
597
  // ============================================================= sync ===========
411
598
  export function sync(cwd, opts) {
412
599
  step(`ozali sync${opts.import ? " --import" : ""} — histórico ↔ repo de conocimiento`);
@@ -496,3 +683,77 @@ export function sync(cwd, opts) {
496
683
  }
497
684
  return 0;
498
685
  }
686
+
687
+ // ============================================================ audit ===========
688
+ // Navega/audita la memoria de Engram: del proyecto actual o general (todos los
689
+ // proyectos). Sin contexto de proyecto en la ruta → general. Sin Engram → audita
690
+ // el histórico local de documentos.
691
+ export async function audit(cwd, opts) {
692
+ step("ozali audit — auditoría de memoria (Engram)");
693
+ const env = detectAll(cwd);
694
+ const cfg = readJSON(CONFIG_PATH(cwd));
695
+ const engramCfg = readJSON(path.join(cwd, ".engram", "config.json"));
696
+ const project = (engramCfg && engramCfg.project_name) || (cfg && cfg.project) || projectName(cwd);
697
+ const hasProjectContext = env.git.isRepo || !!cfg || !!engramCfg;
698
+
699
+ // Resolver alcance: --general fuerza general; sin contexto → general; en repo se propone elegir.
700
+ let scope = (opts.general || !hasProjectContext) ? "general" : "project";
701
+ if (!hasProjectContext) info("Sin contexto de proyecto en esta ruta → auditoría " + c.bold("general") + ".");
702
+ else if (scope === "project" && !opts.general && !opts.yes) {
703
+ scope = await select("¿Qué auditoría quieres?", [
704
+ { value: "project", label: `Proyecto (${c.bold(project)})` },
705
+ { value: "general", label: "General (todos los proyectos en Engram)" },
706
+ ], 0);
707
+ }
708
+
709
+ // Sin Engram → auditar el histórico local de documentos.
710
+ if (!env.engram.available) {
711
+ warn("Engram no está instalado → auditoría desde documentos locales.");
712
+ return auditFromDocs(cwd, project);
713
+ }
714
+
715
+ // Navegador interactivo.
716
+ if (opts.tui) {
717
+ info("Abriendo el navegador interactivo de Engram (engram tui)…");
718
+ return spawnCmd("engram", ["tui"], { cwd });
719
+ }
720
+
721
+ if (scope === "general") {
722
+ step("Proyectos en Engram");
723
+ spawnCmd("engram", ["projects", "list"], { cwd });
724
+ step("Estadísticas (global)");
725
+ spawnCmd("engram", ["stats"], { cwd });
726
+ step("Contexto reciente");
727
+ spawnCmd("engram", ["context"], { cwd });
728
+ } else {
729
+ step(`Contexto reciente — ${project}`);
730
+ spawnCmd("engram", ["context", project], { cwd });
731
+ step("Estadísticas (global)");
732
+ spawnCmd("engram", ["stats"], { cwd });
733
+ }
734
+ if (opts.search) {
735
+ step(`Búsqueda: "${opts.search}"`);
736
+ spawnCmd("engram", ["search", opts.search], { cwd });
737
+ }
738
+ info("Navegación interactiva: " + c.bold("ozali audit --tui") + " · búsqueda: " + c.bold('ozali audit --search "<texto>"'));
739
+ return 0;
740
+ }
741
+
742
+ function auditFromDocs(cwd, project) {
743
+ const docsDir = path.join(cwd, ".ozali", "docs", "cdk");
744
+ if (exists(docsDir)) {
745
+ step(`Hitos documentados localmente — ${project}`);
746
+ const hitos = fs.readdirSync(docsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
747
+ if (hitos.length) for (const e of hitos) console.log(" • " + e.name);
748
+ else info("Aún no hay hitos en .ozali/docs/cdk/.");
749
+ } else {
750
+ info("No hay histórico local en .ozali/docs/cdk/ todavía.");
751
+ }
752
+ const metrics = readJSON(path.join(cwd, ".ozali", "metrics", "token-metrics.json"));
753
+ if (metrics && Array.isArray(metrics.hits) && metrics.hits.length) {
754
+ step("Uso de tokens (últimos hitos)");
755
+ for (const h of metrics.hits.slice(-5)) console.log(` ${c.dim(h.hito || "?")}: total ${h.total ?? "N/A"}`);
756
+ }
757
+ info("Instala Engram para auditoría buscable/acumulativa (" + c.bold("ozali doctor") + ").");
758
+ return 0;
759
+ }
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", () => {
@@ -71,6 +82,51 @@ 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");
116
+ } finally {
117
+ fs.rmSync(dir, { recursive: true, force: true });
118
+ }
119
+ });
120
+
121
+ test("update agrega ozali-jarvis en un repo que no lo tenía", () => {
122
+ const dir = tmpProject();
123
+ try {
124
+ run(["init", "--yes", "--no-engram", "--no-trust", "--no-jarvis", "--agent", "claude-code", "--scope", "project", "--knowledge-repo", path.join(dir, ".k")], dir);
125
+ assert.ok(!fs.existsSync(path.join(dir, ".claude", "agents", "ozali-jarvis.md")), "precondición: sin jarvis");
126
+ run(["update"], dir);
127
+ assert.ok(fs.existsSync(path.join(dir, ".claude", "agents", "ozali-jarvis.md")), "update crea subagente jarvis");
128
+ assert.match(fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8"), /ozali-jarvis:start/, "update crea bloque jarvis");
129
+ assert.ok(fs.existsSync(path.join(dir, ".engram", "config.json")), "update fija .engram/config.json");
74
130
  } finally {
75
131
  fs.rmSync(dir, { recursive: true, force: true });
76
132
  }
@@ -82,9 +82,10 @@ Lleva el histórico y la memoria al repo de conocimiento del equipo. Tus compañ
82
82
  |---|---|
83
83
  | `ozali init` | Prepara el proyecto (lo corres una vez por repo). |
84
84
  | `ozali doctor` | Revisa que todo esté bien. Es solo lectura, no cambia nada. |
85
- | `ozali update` | Actualiza la skill a la última versión. |
85
+ | `ozali update` | Actualiza skill ozali + ozali-jarvis + permisos (y guía cómo regenerar cdk). |
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
 
@@ -119,6 +120,23 @@ Ejemplo — permitir Docker además de lo de fábrica:
119
120
 
120
121
  ---
121
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
+
122
140
  ## Engram: la memoria del equipo (opcional)
123
141
 
124
142
  Durante `init`, ozali **instala Engram por ti**: en modo interactivo te pregunta (basta con aceptar),
@@ -142,6 +160,17 @@ Más detalle técnico en [engram-convention.md](../skill/references/engram-conve
142
160
 
143
161
  ---
144
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
+
145
174
  ## Preguntas frecuentes
146
175
 
147
176
  **¿Tengo que usar Engram?**
@@ -161,6 +190,11 @@ Es el aviso de seguridad de Claude Code: ignora los permisos del proyecto hasta
161
190
  Deja que `init` lo marque como confiable, o abre Claude Code en la carpeta y acepta el diálogo una
162
191
  vez. Tras eso, el aviso desaparece y los permisos surten efecto.
163
192
 
193
+ **Actualicé ozali pero no veo ozali-jarvis (o cdk quedó viejo).**
194
+ Corre `ozali update` en el repo: refresca la skill ozali, los permisos y **crea/actualiza
195
+ ozali-jarvis**. La skill `cdk` la regenera tu agente: tras `ozali update`, abre el agente y vuelve a
196
+ correr la skill `ozali` para regenerar `cdk` con lo nuevo (tus docs por hito y el plan se conservan).
197
+
164
198
  **¿Funciona en Windows?**
165
199
  Sí. Los permisos base incluyen variantes PowerShell, y Engram se instala con `go install` o un
166
200
  binario descargable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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.