ozali 0.3.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,8 +61,14 @@ 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
74
  los destructivos. Es un template — tus reglas se conservan al re-correr `init`. Como Claude Code
@@ -75,9 +81,16 @@ macOS/Linux, `go install` en Windows, con binario precompilado como fallback. Si
75
81
  instalarlo, pasa `--no-engram` (arranca en modo `docs`). Opcionalmente habilita **Engram Cloud**
76
82
  (réplica de equipo opt-in) además del git-sync.
77
83
 
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
+
78
91
  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).
92
+ `--no-jarvis`, `--agent`, `--scope`, `--knowledge-repo`, `--import`/`--push`/`--cloud` (sync). Modelo
93
+ mental completo en [docs/intended-usage.md](docs/intended-usage.md).
81
94
 
82
95
  ## Agentes soportados
83
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.
@@ -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) No crear 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];
@@ -71,6 +80,7 @@ async function main() {
71
80
  case "doctor": return doctor(cwd);
72
81
  case "update": return update(cwd);
73
82
  case "sync": return sync(cwd, opts);
83
+ case "audit": return await audit(cwd, opts);
74
84
  default:
75
85
  err(`comando desconocido "${cmd}". Usa ${c.bold("ozali --help")}.`);
76
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";
@@ -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 };
@@ -496,3 +631,77 @@ export function sync(cwd, opts) {
496
631
  }
497
632
  return 0;
498
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", () => {
@@ -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
 
@@ -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?**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.3.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.