ozali 0.7.0 → 0.8.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
@@ -80,7 +80,10 @@ referencian entre sí, corre **una vez** en esa raíz:
80
80
  ```bash
81
81
  ozali workspace # escanea repos hijos, remedia los que faltan y escribe la config conjunta
82
82
  ozali workspace --dry-run # solo muestra el inventario y el plan, sin escribir
83
+ ozali workspace --yes # no interactivo: acepta defaults y todas las referencias detectadas
83
84
  ozali workspace --depth 2 # busca repos hasta 2 niveles (para raíces con subcarpetas de grupo)
85
+ ozali workspace --doctor # health-check de TODOS los repos miembros + resumen (solo CLI)
86
+ ozali workspace --update # actualiza skills/permisos/jarvis de TODOS los repos miembros (solo CLI)
84
87
  ```
85
88
 
86
89
  Qué hace, en orden: (1) **escanea** los repos hijos y reporta su estado —`✔ listo`, `⚠ sin calibrar`
@@ -93,6 +96,13 @@ contigo; y (4) escribe la config para **trabajar en conjunto**:
93
96
  - `<carpeta>.code-workspace` — workspace multi-root que abre todos los repos juntos.
94
97
  - Orquestador **`ozali-workspace-jarvis`** en `CLAUDE.md`/`AGENTS.md` de la raíz: coordina los repos
95
98
  según el manifiesto y delega la ejecución en el `cdk` de cada uno.
99
+ - La skill **`ozali`** en la raíz (`.claude/skills/ozali/`) para **calibrar los miembros desde el
100
+ workspace** (modo target), sin abrir cada proyecto.
101
+
102
+ **Operar todo desde la raíz:** `ozali workspace --doctor` y `--update` corren el health-check y la
103
+ actualización sobre **todos** los repos miembros (solo CLI). Y para calibrar los `⚠ sin calibrar`,
104
+ abre el agente en la raíz y pídele a `ozali-workspace-jarvis` que *"calibre los repos pendientes"*: los
105
+ recorre uno por uno (con su GATE) sin que cambies de proyecto.
96
106
 
97
107
  Es **idempotente**: re-córrelo cuando agregues repos o cambien las referencias. Detalle completo en
98
108
  [`docs/workspaces.md`](docs/workspaces.md).
@@ -152,6 +162,7 @@ Claude Code y opencode (perfiles de permisos para ambos en
152
162
  |---|---|
153
163
  | Guía de uso (usuarios generales) | [docs/guia-de-uso.md](docs/guia-de-uso.md) |
154
164
  | Uso previsto (modelo mental) | [docs/intended-usage.md](docs/intended-usage.md) |
165
+ | Compilado y uso local (repo clonado) | [docs/compilado-local.md](docs/compilado-local.md) |
155
166
  | Seguridad del instalador (npx/pnpm) | [docs/security.md](docs/security.md) |
156
167
  | Histórico aislado y memoria de equipo | [docs/team-history.md](docs/team-history.md) |
157
168
  | Desplegar Engram Cloud (VPS) | [docs/deploy-cloud-vps.md](docs/deploy-cloud-vps.md) |
package/cli/bin/ozali.mjs CHANGED
@@ -16,6 +16,7 @@ ${c.bold("Comandos:")}
16
16
  ozali-commit, aísla el histórico, configura Engram y el repo de conocimiento.
17
17
  workspace Multi-repo: escanea los repos de la carpeta raíz, remedia los que no tienen
18
18
  ozali init, guía la calibración y escribe la config para trabajar en conjunto.
19
+ Con --doctor / --update opera sobre TODOS los repos miembros desde la raíz.
19
20
  doctor Health-check read-only del proyecto (fuente de verdad, Engram, versión de cdk, TDD…).
20
21
  update Actualiza la instalación (skills ozali + ozali-commit + ozali-jarvis + permisos)
21
22
  al paquete y avisa si la skill cdk quedó desactualizada.
@@ -29,6 +30,8 @@ ${c.bold("Opciones comunes:")}
29
30
  --agent <a> (init) claude-code | opencode | both.
30
31
  --scope <s> (init) project | global.
31
32
  --depth <n> (workspace) Niveles a escanear bajo la raíz (default 1).
33
+ --doctor (workspace) Health-check de TODOS los repos miembros + resumen.
34
+ --update (workspace) Actualiza (skills/permisos/jarvis) TODOS los repos miembros.
32
35
  --knowledge-repo <p> (init) Ruta del repo de conocimiento.
33
36
  --no-engram (init) No usar Engram; arranca en modo docs.
34
37
  --no-trust (init) No marcar el workspace como confiable en Claude Code.
@@ -73,6 +76,8 @@ function parseArgs(argv) {
73
76
  else if (a === "--agent") opts.agent = argv[++i];
74
77
  else if (a === "--scope") opts.scope = argv[++i];
75
78
  else if (a === "--depth") opts.depth = argv[++i];
79
+ else if (a === "--doctor") opts.wsDoctor = true;
80
+ else if (a === "--update") opts.wsUpdate = true;
76
81
  else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
77
82
  else if (a === "-h" || a === "--help") opts.help = true;
78
83
  else if (a === "-v" || a === "--version") opts.version = true;
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import {
6
6
  c, ok, warn, err, info, step,
7
7
  SKILL_SRC, COMMIT_SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
8
- ensureGitignore, tryExec, spawnCmd, which, engramAssetName,
8
+ ensureGitignore, tryExec, spawnCmd, which, engramAssetName, pickEngramAsset,
9
9
  projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL, gitInfo,
10
10
  } from "./util.mjs";
11
11
  import { detectAll, detectWorkspace, detectReferences } from "./detect.mjs";
@@ -305,19 +305,28 @@ function installEngram() {
305
305
  return false;
306
306
  }
307
307
 
308
- const ENGRAM_RELEASES_API = "https://api.github.com/repos/Gentleman-Programming/engram/releases/latest";
309
- const ENGRAM_RELEASES_DL = "https://github.com/Gentleman-Programming/engram/releases/download";
308
+ // Lista de releases (NO /releases/latest: ese endpoint puede devolver un tag especial
309
+ // sin binarios, p. ej. `pi-v*`). Recorremos la lista y elegimos el release estable.
310
+ const ENGRAM_RELEASES_LIST = "https://api.github.com/repos/Gentleman-Programming/engram/releases?per_page=30";
310
311
 
311
- /** Última versión publicada de Engram (sin la "v" inicial), o null si no hay red. */
312
- function latestEngramVersion() {
313
- let raw = null;
314
- if (which("curl")) raw = tryExec("curl", ["-fsSL", ENGRAM_RELEASES_API]);
315
- else if (which("wget")) raw = tryExec("wget", ["-qO-", ENGRAM_RELEASES_API]);
312
+ /** GET de texto con curl (o wget). Devuelve el body o null si no hay red/herramienta. */
313
+ function fetchText(url) {
314
+ if (which("curl")) return tryExec("curl", ["-fsSL", url]);
315
+ if (which("wget")) return tryExec("wget", ["-qO-", url]);
316
+ return null;
317
+ }
318
+
319
+ /**
320
+ * Resuelve { version, url } del binario precompilado de Engram para este SO/arch,
321
+ * consultando la lista de releases y quedándose con el release estable más reciente
322
+ * que contenga el asset. Devuelve null si no hay red/herramienta o no hay binario.
323
+ */
324
+ function resolveEngramAsset(platform, arch) {
325
+ const raw = fetchText(ENGRAM_RELEASES_LIST);
316
326
  if (!raw) return null;
317
- try {
318
- const tag = JSON.parse(raw).tag_name;
319
- return tag ? String(tag).replace(/^v/, "") : null;
320
- } catch { return null; }
327
+ let releases;
328
+ try { releases = JSON.parse(raw); } catch { return null; }
329
+ return pickEngramAsset(releases, platform, arch);
321
330
  }
322
331
 
323
332
  /** Descarga url → dest con curl (o wget como fallback). Devuelve true si tuvo éxito. */
@@ -359,14 +368,13 @@ function installEngramFromTarball() {
359
368
  return false;
360
369
  }
361
370
 
362
- const version = latestEngramVersion();
363
- if (!version) {
364
- warn("No pude resolver la última versión de Engram (¿sin red o sin curl/wget?).");
371
+ const resolved = resolveEngramAsset(plat, process.arch);
372
+ if (!resolved) {
373
+ warn("No pude resolver un binario precompilado de Engram para tu SO/arch (¿sin red, sin curl/wget, o release sin assets?).");
365
374
  return false;
366
375
  }
367
-
376
+ const { version, url } = resolved;
368
377
  const asset = engramAssetName(plat, process.arch, version);
369
- const url = `${ENGRAM_RELEASES_DL}/v${version}/${asset}`;
370
378
  info(`Descargando binario precompilado de Engram ${c.bold("v" + version)} (${process.arch})…`);
371
379
 
372
380
  let tmpDir;
@@ -785,6 +793,10 @@ export async function workspace(cwd, opts = {}) {
785
793
  step("Repos detectados");
786
794
  printMembers(ws.members);
787
795
 
796
+ // Modos batch (Track 1): operan sobre los miembros del workspace ya existente y salen.
797
+ if (opts.wsDoctor) return workspaceDoctor(ws.members);
798
+ if (opts.wsUpdate) return workspaceUpdate(ws.members, opts);
799
+
788
800
  // Fase B — remediación de los que no tienen ozali init
789
801
  const missing = ws.members.filter((m) => m.status === "missing-init");
790
802
  if (missing.length && opts.dryRun) {
@@ -828,6 +840,7 @@ export async function workspace(cwd, opts = {}) {
828
840
  const agent = manifest.agent;
829
841
  if (agent === "claude-code" || agent === "both") {
830
842
  ensureWorkspaceJarvisClaudeCode(cwd);
843
+ ensureWorkspaceOzaliSkill(cwd); // Track 2: skill ozali en la raíz → calibrar miembros desde el workspace
831
844
  if (!opts.noTrust) await ensureClaudeWorkspaceTrust(cwd, opts);
832
845
  }
833
846
  if (agent === "opencode" || agent === "both") ensureWorkspaceJarvisOpencode(cwd);
@@ -841,11 +854,68 @@ export async function workspace(cwd, opts = {}) {
841
854
  step("Siguientes pasos");
842
855
  const wsFile = `${path.basename(cwd)}.code-workspace`;
843
856
  console.log(` 1. Abre el workspace en tu editor: ${c.bold(wsFile)} ${c.dim("(VSCode/Antigravity → Open Workspace).")}`);
844
- if (needCal.length) console.log(` 2. Calibra los repos pendientes (${c.bold(needCal.map((m) => m.dir).join(", "))}) corriendo la skill ${c.bold("ozali")} en cada uno.`);
857
+ if (needCal.length) {
858
+ console.log(` 2. Calibra los pendientes (${c.bold(needCal.map((m) => m.dir).join(", "))}) ${c.dim("sin salir del workspace:")}`);
859
+ console.log(` ${c.dim("abre el agente en la raíz y pide a")} ${c.bold("ozali-workspace-jarvis")} ${c.dim('que "calibre los repos pendientes"')}`);
860
+ console.log(` ${c.dim("(usa la skill")} ${c.bold("ozali")} ${c.dim("en modo target, repo por repo con su GATE).")}`);
861
+ }
862
+ console.log(` ${c.dim("• Salud de todos los repos:")} ${c.bold("ozali workspace --doctor")}`);
863
+ console.log(` ${c.dim("• Actualizar todos los repos:")} ${c.bold("ozali workspace --update")}`);
845
864
  console.log(` ${c.dim("Re-corre")} ${c.bold("ozali workspace")} ${c.dim("cuando agregues repos o cambien las referencias (es idempotente).")}`);
846
865
  return 0;
847
866
  }
848
867
 
868
+ /** Track 1 — health-check de todos los miembros (doctor por repo) + resumen consolidado. */
869
+ function workspaceDoctor(members) {
870
+ const results = [];
871
+ for (const m of members) {
872
+ console.log("");
873
+ console.log(c.bold(c.magenta(`── ${m.dir} ──`)));
874
+ if (m.status === "missing-init") {
875
+ warn(`Sin ozali init → córrelo (o re-corre ${c.bold("ozali workspace")}).`);
876
+ results.push({ dir: m.dir, ok: false, note: "sin init" });
877
+ continue;
878
+ }
879
+ const code = doctor(m.path);
880
+ results.push({ dir: m.dir, ok: code === 0, note: code === 0 ? "todo en orden" : "puntos a atender" });
881
+ }
882
+ step("Resumen del workspace");
883
+ const pad = Math.max(4, ...members.map((m) => m.dir.length));
884
+ for (const r of results) {
885
+ console.log(` ${r.ok ? c.green("✔") : c.yellow("✖")} ${r.dir.padEnd(pad)} ${c.dim(r.note)}`);
886
+ }
887
+ return results.every((r) => r.ok) ? 0 : 1;
888
+ }
889
+
890
+ /** Track 1 — update de todos los miembros ozali (skills/permisos/jarvis) + resumen. */
891
+ function workspaceUpdate(members, opts) {
892
+ const results = [];
893
+ let failed = 0;
894
+ for (const m of members) {
895
+ console.log("");
896
+ console.log(c.bold(c.magenta(`── ${m.dir} ──`)));
897
+ if (m.status === "missing-init") {
898
+ warn("Sin ozali init → nada que actualizar (córrelo primero).");
899
+ results.push({ dir: m.dir, mark: c.yellow("—"), note: "sin init (saltado)" });
900
+ continue;
901
+ }
902
+ const code = update(m.path, opts);
903
+ if (code !== 0) failed++;
904
+ results.push({ dir: m.dir, mark: code === 0 ? c.green("✔") : c.yellow("✖"), note: code === 0 ? "actualizado" : "revisar" });
905
+ }
906
+ step("Resumen del workspace");
907
+ const pad = Math.max(4, ...members.map((m) => m.dir.length));
908
+ for (const r of results) console.log(` ${r.mark} ${r.dir.padEnd(pad)} ${c.dim(r.note)}`);
909
+ info(`La skill ${c.bold("cdk")} la regenera el agente. Re-corre ${c.bold("ozali workspace")} para refrescar estados.`);
910
+ return failed > 0 ? 1 : 0;
911
+ }
912
+
913
+ /** Track 2 — instala la skill `ozali` en la raíz para calibrar miembros desde el workspace. */
914
+ function ensureWorkspaceOzaliSkill(root) {
915
+ copyDir(SKILL_SRC, path.join(root, ".claude", "skills", "ozali"));
916
+ ok(`Skill ${c.bold("ozali")} instalada en la raíz (${c.bold(".claude/skills/ozali")}) para calibrar miembros desde el workspace.`);
917
+ }
918
+
849
919
  /** Primer .ozali/config.json entre los miembros ya inicializados (para heredar defaults). */
850
920
  function inheritedConfig(members) {
851
921
  for (const m of members) {
@@ -1,7 +1,7 @@
1
1
  // detect.mjs — detección read-only del entorno del proyecto destino.
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { exists, which, gitInfo, nodeMajor, HOME, readJSON, projectName, DEFAULT_KNOWLEDGE } from "./util.mjs";
4
+ import { exists, which, gitInfo, tryExec, nodeMajor, HOME, readJSON, projectName, DEFAULT_KNOWLEDGE } from "./util.mjs";
5
5
 
6
6
  /** Variante de fuente de verdad: {found, doc, dir, variant} */
7
7
  export function detectSourceOfTruth(cwd) {
@@ -121,9 +121,21 @@ function isKnowledgeRepo(dir) {
121
121
  return d === k || d.startsWith(k + path.sep);
122
122
  }
123
123
 
124
+ /**
125
+ * ¿`dir` es la RAÍZ de su propio repo git? No basta con estar dentro de un work tree
126
+ * (una subcarpeta de un repo también lo está): exigimos que el toplevel de git sea `dir`.
127
+ * Así, correr `ozali workspace` dentro de un repo NO trata sus subcarpetas como miembros.
128
+ */
129
+ function isRepoRoot(dir) {
130
+ const top = tryExec("git", ["-C", dir, "rev-parse", "--show-toplevel"]);
131
+ if (!top) return false;
132
+ try { return fs.realpathSync(top) === fs.realpathSync(dir); } catch { return false; }
133
+ }
134
+
124
135
  /**
125
136
  * Junta las rutas de repos git bajo `root` hasta `depth` niveles. Un directorio que
126
- * ES repo git se trata como hoja (no se desciende dentro). Salta ocultos/ignorados.
137
+ * ES la raíz de su propio repo git se trata como hoja (no se desciende dentro). Salta
138
+ * ocultos/ignorados.
127
139
  */
128
140
  function collectRepoDirs(root, depth, level = 1, acc = []) {
129
141
  let entries;
@@ -133,7 +145,7 @@ function collectRepoDirs(root, depth, level = 1, acc = []) {
133
145
  if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
134
146
  const full = path.join(root, e.name);
135
147
  if (isKnowledgeRepo(full)) continue;
136
- if (gitInfo(full).isRepo) { acc.push(full); continue; } // repo = hoja
148
+ if (isRepoRoot(full)) { acc.push(full); continue; } // repo propio = hoja
137
149
  if (level < depth) collectRepoDirs(full, depth, level + 1, acc);
138
150
  }
139
151
  return acc;
package/cli/lib/util.mjs CHANGED
@@ -37,6 +37,29 @@ export function engramAssetName(platform, arch, version) {
37
37
  return `engram_${version}_${os}_${a}.${ext}`;
38
38
  }
39
39
 
40
+ /**
41
+ * Elige el binario precompilado de Engram para un SO/arch dado a partir de la lista de
42
+ * releases (formato de la API de GitHub `/releases`). Se queda con el release ESTABLE
43
+ * más reciente cuyo tag sea semver `vX.Y.Z` y que **realmente contenga** el asset
44
+ * esperado, y devuelve su `browser_download_url` real. Ignora tags no-semver (p. ej.
45
+ * `pi-v*`, builds de Raspberry Pi que NO traen binarios) y draft/prerelease.
46
+ * Función pura (sin red). Devuelve { version, url } o null.
47
+ */
48
+ export function pickEngramAsset(releases, platform, arch) {
49
+ if (!Array.isArray(releases)) return null;
50
+ for (const r of releases) {
51
+ if (!r || r.draft || r.prerelease) continue;
52
+ const m = /^v(\d+\.\d+\.\d+)$/.exec(r.tag_name || "");
53
+ if (!m) continue; // salta tags no-semver (pi-v*, etc.)
54
+ const version = m[1];
55
+ const name = engramAssetName(platform, arch, version);
56
+ if (!name) return null; // SO/arch sin binario publicado
57
+ const asset = Array.isArray(r.assets) ? r.assets.find((a) => a && a.name === name) : null;
58
+ if (asset && asset.browser_download_url) return { version, url: asset.browser_download_url };
59
+ }
60
+ return null;
61
+ }
62
+
40
63
  // ---- colores (sin deps; respeta NO_COLOR y no-TTY) --------------------------
41
64
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
42
65
  const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
@@ -6,7 +6,7 @@ import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { engramAssetName } from "../lib/util.mjs";
9
+ import { engramAssetName, pickEngramAsset } from "../lib/util.mjs";
10
10
 
11
11
  const HERE = path.dirname(fileURLToPath(import.meta.url));
12
12
  const BIN = path.resolve(HERE, "..", "bin", "ozali.mjs");
@@ -257,6 +257,8 @@ test("workspace escribe manifiesto + .code-workspace + jarvis y es idempotente",
257
257
  assert.ok(fs.existsSync(wsFile), ".code-workspace escrito");
258
258
  assert.equal(JSON.parse(fs.readFileSync(wsFile, "utf8")).folders.length, 2, "multi-root con 2 folders");
259
259
  assert.match(fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8"), /ozali-workspace-jarvis:start/, "bloque jarvis en CLAUDE.md");
260
+ // Track 2: la skill ozali queda instalada en la raíz para calibrar miembros desde el workspace
261
+ assert.ok(fs.existsSync(path.join(root, ".claude", "skills", "ozali", "SKILL.md")), "skill ozali en la raíz");
260
262
 
261
263
  // idempotencia: re-correr no duplica
262
264
  run(["workspace", "--yes", "--no-trust"], root);
@@ -268,6 +270,51 @@ test("workspace escribe manifiesto + .code-workspace + jarvis y es idempotente",
268
270
  }
269
271
  });
270
272
 
273
+ test("workspace --doctor revisa cada miembro y saca resumen consolidado", () => {
274
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-ws-"));
275
+ try {
276
+ wsRepo(root, "api", { config: true, cdk: true, pkg: { name: "api", version: "1.0.0" } });
277
+ wsRepo(root, "web", { config: true, cdk: true, pkg: { name: "web" } });
278
+ const { stdout } = run(["workspace", "--doctor"], root, true); // exit 1 esperado (repos sin sot/engram)
279
+ assert.match(stdout, /health-check/i, "corre doctor por repo");
280
+ assert.match(stdout, /Resumen del workspace/, "imprime el resumen consolidado");
281
+ assert.match(stdout, /api/, "menciona el repo api en el resumen");
282
+ assert.match(stdout, /web/, "menciona el repo web en el resumen");
283
+ assert.ok(!fs.existsSync(path.join(root, "ozali-workspace.json")), "--doctor no escribe config");
284
+ } finally {
285
+ fs.rmSync(root, { recursive: true, force: true });
286
+ }
287
+ });
288
+
289
+ test("workspace --update actualiza cada miembro y salta los sin init", () => {
290
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-ws-"));
291
+ try {
292
+ wsRepo(root, "api", { config: true, cdk: true, pkg: { name: "api", version: "1.0.0" } });
293
+ wsRepo(root, "bare", { pkg: { name: "bare" } }); // missing-init → se salta
294
+ const { stdout } = run(["workspace", "--update", "--yes", "--no-jarvis"], root);
295
+ assert.match(stdout, /Resumen del workspace/, "imprime el resumen consolidado");
296
+ assert.match(stdout, /Sin ozali init/, "salta el repo sin init");
297
+ assert.ok(!fs.existsSync(path.join(root, "ozali-workspace.json")), "--update no escribe config");
298
+ } finally {
299
+ fs.rmSync(root, { recursive: true, force: true });
300
+ }
301
+ });
302
+
303
+ test("workspace no trata subcarpetas de un repo como miembros (solo repos propios)", () => {
304
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-ws-"));
305
+ try {
306
+ execFileSync("git", ["init", "-q"], { cwd: root }); // la raíz ES un repo git
307
+ fs.mkdirSync(path.join(root, "src")); // subcarpeta simple, NO repo propio
308
+ fs.writeFileSync(path.join(root, "src", "index.js"), "//\n");
309
+ wsRepo(root, "pkg", { config: true, cdk: true, pkg: { name: "pkg", version: "1.0.0" } }); // repo propio anidado
310
+ const { stdout } = run(["workspace", "--dry-run"], root);
311
+ assert.match(stdout, /\bpkg\b/, "incluye el repo propio anidado");
312
+ assert.doesNotMatch(stdout, /\bsrc\b/, "no incluye la subcarpeta que no es repo propio");
313
+ } finally {
314
+ fs.rmSync(root, { recursive: true, force: true });
315
+ }
316
+ });
317
+
271
318
  test("init --dry-run no escribe nada", () => {
272
319
  const dir = tmpProject();
273
320
  try {
@@ -335,3 +382,31 @@ test("engramAssetName respeta la convención de release por SO/arch", () => {
335
382
  assert.equal(engramAssetName("linux", "ia32", "1.17.0"), null, "arch no soportada → null");
336
383
  assert.equal(engramAssetName("freebsd", "x64", "1.17.0"), null, "SO no soportado → null");
337
384
  });
385
+
386
+ test("pickEngramAsset ignora tags no-semver sin binarios (pi-v*) y draft/prerelease", () => {
387
+ // Shape real de la API de GitHub: el 'latest' es pi-v0.1.9 (0 assets); los binarios
388
+ // viven en tags vX.Y.Z. Además metemos un prerelease más nuevo que debe saltarse.
389
+ const releases = [
390
+ { tag_name: "pi-v0.1.9", prerelease: false, draft: false, assets: [] },
391
+ { tag_name: "v2.0.0", prerelease: true, draft: false, assets: [
392
+ { name: "engram_2.0.0_linux_amd64.tar.gz", browser_download_url: "https://x/pre" },
393
+ ] },
394
+ { tag_name: "v1.17.0", prerelease: false, draft: false, assets: [
395
+ { name: "checksums.txt", browser_download_url: "https://x/sum" },
396
+ { name: "engram_1.17.0_linux_amd64.tar.gz", browser_download_url: "https://x/engram_1.17.0_linux_amd64.tar.gz" },
397
+ { name: "engram_1.17.0_darwin_arm64.tar.gz", browser_download_url: "https://x/darwin" },
398
+ ] },
399
+ ];
400
+ // Linux x64 → salta pi-v* (sin assets) y el prerelease → v1.17.0, con la URL REAL del asset.
401
+ assert.deepEqual(pickEngramAsset(releases, "linux", "x64"), {
402
+ version: "1.17.0",
403
+ url: "https://x/engram_1.17.0_linux_amd64.tar.gz",
404
+ });
405
+ // macOS arm64 → mismo release, su asset darwin_arm64.
406
+ assert.deepEqual(pickEngramAsset(releases, "darwin", "arm64"), { version: "1.17.0", url: "https://x/darwin" });
407
+ // Arch sin binario → null.
408
+ assert.equal(pickEngramAsset(releases, "linux", "ia32"), null);
409
+ // Sin releases utilizables → null.
410
+ assert.equal(pickEngramAsset([{ tag_name: "pi-v0.1.9", assets: [] }], "linux", "x64"), null);
411
+ assert.equal(pickEngramAsset(null, "linux", "x64"), null);
412
+ });
@@ -0,0 +1,182 @@
1
+ # Compilado y uso local (para probar el repo clonado)
2
+
3
+ ← [README](../README.md)
4
+
5
+ Guía para cuando **clonas este repositorio** y quieres compilarlo/empaquetarlo y **probarlo en local**
6
+ antes de publicarlo o para desarrollar sobre él.
7
+
8
+ > **Aclaración importante — no hay "compilación".** `ozali` es un CLI **Node de cero dependencias y
9
+ > ESM puro** (solo usa módulos `node:*`). **No hay build step**, ni TypeScript que transpilar, ni
10
+ > `dist/`. "Compilar" aquí significa una de dos cosas: (a) **correrlo tal cual** desde el fuente, o
11
+ > (b) **empaquetar el artefacto distribuible** (`npm pack` → un `.tgz`, el "archivo base") para
12
+ > instalarlo y probarlo como lo haría un usuario final.
13
+
14
+ ---
15
+
16
+ ## Requisitos
17
+
18
+ - **Node ≥ 16** (`node --version`). Es lo único imprescindible.
19
+ - **git** (el CLI lee identidad/estado del repo).
20
+ - **pnpm** o **npm** (opcional; solo para `test`, `pack`, `link` o instalación global). No hay `npm
21
+ install` que correr: **el repo no tiene dependencias**.
22
+
23
+ ```bash
24
+ git clone https://github.com/jimmybathrooms/ozali.git
25
+ cd ozali
26
+ ```
27
+
28
+ ---
29
+
30
+ ## 1. Correr desde el fuente (sin instalar nada)
31
+
32
+ Como no hay dependencias ni build, el CLI corre directo:
33
+
34
+ ```bash
35
+ node cli/bin/ozali.mjs --version # imprime la versión de package.json (p. ej. 0.8.0)
36
+ node cli/bin/ozali.mjs --help # lista de comandos
37
+ node cli/bin/ozali.mjs doctor # health-check en el cwd
38
+ ```
39
+
40
+ Esta es la forma **más auditable**: ejecutas exactamente el código que ves, sin capa de instalación.
41
+
42
+ ---
43
+
44
+ ## 2. Correr la batería de pruebas
45
+
46
+ ```bash
47
+ npm test # (o: pnpm test) → node --test cli/test/*.test.mjs
48
+ ```
49
+
50
+ Debe salir todo en verde. Los tests son `node:test` puro (sin dependencias) y cubren init, doctor,
51
+ update, workspace, seguridad del paquete y los helpers de release de Engram.
52
+
53
+ ---
54
+
55
+ ## 3. Empaquetar el "archivo base" (tarball distribuible)
56
+
57
+ El artefacto que se publicaría en npm es un tarball `.tgz`. Se genera con:
58
+
59
+ ```bash
60
+ npm pack # (o: pnpm pack)
61
+ # → crea ozali-<version>.tgz en la raíz del repo (el cwd)
62
+ ```
63
+
64
+ Para tu versión actual produce, por ejemplo, **`ozali-0.8.0.tgz`**. Es **idéntico** a lo que recibiría
65
+ un usuario desde npm: incluye solo lo declarado en `package.json > files` (`cli/`, `skill/`,
66
+ `skill-commit/`, `templates/`, `docs/`, `README.md`, `LICENSE`).
67
+
68
+ Para **ver qué contiene sin generar el archivo**:
69
+
70
+ ```bash
71
+ npm pack --dry-run # lista archivos, tamaño y nombre del tarball, sin escribir nada
72
+ ```
73
+
74
+ > El `.tgz` es un artefacto temporal: **no lo commitees**. Bórralo tras probar (o añádelo a
75
+ > `.gitignore` como `ozali-*.tgz`).
76
+
77
+ ### Extraer / inspeccionar el tarball
78
+
79
+ El tarball es un `.tar.gz` estándar. Para extraerlo y revisar el "archivo base" tal como quedaría
80
+ instalado:
81
+
82
+ ```bash
83
+ tar -xzf ozali-0.8.0.tgz # crea una carpeta package/ con el contenido publicable
84
+ ls -R package # inspecciona la estructura
85
+ tar -tzf ozali-0.8.0.tgz # o solo LISTA el contenido, sin extraer
86
+ ```
87
+
88
+ La carpeta `package/` es exactamente lo que npm coloca en `node_modules/ozali` al instalar.
89
+
90
+ ---
91
+
92
+ ## 4. Usarlo en local para pruebas
93
+
94
+ Tres formas, de la más cómoda para desarrollar a la más fiel al usuario final:
95
+
96
+ ### a) `npm link` — bucle de desarrollo (refleja tus cambios al instante)
97
+
98
+ ```bash
99
+ npm link # (o: pnpm link --global)
100
+ # ahora 'ozali' está en tu PATH, apuntando al clon local
101
+ ozali --version
102
+ ozali doctor
103
+ ```
104
+
105
+ Como es un symlink al repo, cualquier edición que hagas en `cli/` se ve de inmediato (no hace falta
106
+ re-linkear). Para deshacer: `npm unlink -g ozali` (o `npm rm -g ozali`).
107
+
108
+ ### b) Instalar el tarball global — prueba el artefacto real
109
+
110
+ Instala el `.tgz` como lo haría un usuario, para validar el paquete exacto que publicarías:
111
+
112
+ ```bash
113
+ npm i -g ./ozali-0.8.0.tgz # (o: pnpm add -g ./ozali-0.8.0.tgz)
114
+ ozali --version
115
+ ```
116
+
117
+ Para desinstalar: `npm rm -g ozali`.
118
+
119
+ ### c) Efímero, sin instalar en el PATH — como `npx`/`pnpm dlx`
120
+
121
+ ```bash
122
+ npx ./ozali-0.8.0.tgz init # corre init desde el tarball, sin dejar 'ozali' instalado
123
+ pnpm dlx ./ozali-0.8.0.tgz init # equivalente con pnpm
124
+ ```
125
+
126
+ > **Nota de seguridad:** el flag `--ignore-scripts` que recomendamos para instalar desde npm es
127
+ > irrelevante aquí, porque `ozali` **no tiene dependencias ni lifecycle scripts** (nada que ejecutar
128
+ > en install). Detalle en [security.md](security.md).
129
+
130
+ ---
131
+
132
+ ## 5. Probar en un proyecto sandbox
133
+
134
+ Crea un repo de juguete y corre el flujo real sin ensuciar nada:
135
+
136
+ ```bash
137
+ mkdir /tmp/ozali-sandbox && cd /tmp/ozali-sandbox && git init
138
+
139
+ # opción sin instalar: apunta al bin del clon
140
+ node /ruta/al/clon/ozali/cli/bin/ozali.mjs init --dry-run # muestra el plan, no escribe
141
+ node /ruta/al/clon/ozali/cli/bin/ozali.mjs init --yes --no-engram # init real, sin instalar Engram
142
+ node /ruta/al/clon/ozali/cli/bin/ozali.mjs doctor # verifica el resultado
143
+ ```
144
+
145
+ Para probar **workspaces multi-repo**, crea una carpeta con varios repos git dentro y corre:
146
+
147
+ ```bash
148
+ node /ruta/al/clon/ozali/cli/bin/ozali.mjs workspace --dry-run # inventario + plan, sin escribir
149
+ node /ruta/al/clon/ozali/cli/bin/ozali.mjs workspace --doctor # health-check de todos los miembros
150
+ ```
151
+
152
+ (Si hiciste `npm link`, sustituye todo lo anterior por simplemente `ozali <comando>`.)
153
+
154
+ ---
155
+
156
+ ## 6. Verificar y limpiar
157
+
158
+ ```bash
159
+ ozali --version # debe coincidir con la "version" de package.json
160
+ npm rm -g ozali # desinstala el global (link o tarball)
161
+ rm -f ozali-*.tgz # borra el tarball de prueba
162
+ rm -rf package/ # borra la extracción del tarball, si la hiciste
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Resumen
168
+
169
+ | Quiero… | Comando |
170
+ |---|---|
171
+ | Correr sin instalar | `node cli/bin/ozali.mjs <cmd>` |
172
+ | Correr los tests | `npm test` |
173
+ | Empaquetar el distribuible | `npm pack` → `ozali-<version>.tgz` |
174
+ | Ver el contenido del paquete | `npm pack --dry-run` |
175
+ | Extraer el tarball | `tar -xzf ozali-<version>.tgz` → `package/` |
176
+ | Desarrollar con `ozali` en PATH | `npm link` |
177
+ | Probar el artefacto real | `npm i -g ./ozali-<version>.tgz` |
178
+ | Probar efímero | `npx ./ozali-<version>.tgz init` |
179
+
180
+ > ¿Buscas **publicar** una versión nueva (no solo probar)? Eso es otro flujo: bump de `version` en
181
+ > `package.json` → commit → tag `vX.Y.Z` → push; el tag dispara la publicación a npm con provenance
182
+ > vía GitHub Actions.
@@ -81,6 +81,7 @@ Lleva el histórico y la memoria al repo de conocimiento del equipo. Tus compañ
81
81
  | Comando | Qué hace |
82
82
  |---|---|
83
83
  | `ozali init` | Prepara el proyecto (lo corres una vez por repo). |
84
+ | `ozali workspace` | Configura varios repos de una carpeta para trabajar en conjunto (lo corres en la carpeta raíz). |
84
85
  | `ozali doctor` | Revisa que todo esté bien. Es solo lectura, no cambia nada. |
85
86
  | `ozali update` | Actualiza skill ozali + ozali-jarvis + permisos (y guía cómo regenerar cdk). |
86
87
  | `ozali sync` | Sube el histórico/memoria al repo de equipo. |
@@ -171,6 +172,51 @@ Atajos: `ozali audit --tui` abre un navegador interactivo de la memoria; `ozali
171
172
  busca un término; `ozali audit --general` fuerza el alcance general. Si no tienes Engram, audita el
172
173
  histórico local de `.ozali/docs/`.
173
174
 
175
+ ---
176
+
177
+ ## Trabajar con varios repos a la vez (`ozali workspace`)
178
+
179
+ ¿Tienes una carpeta que agrupa **varios repositorios que se hablan entre sí** (una API, su front, una
180
+ librería compartida) y quieres que tu agente trabaje con todos juntos? Corre **una vez** en esa
181
+ carpeta raíz:
182
+
183
+ ```bash
184
+ ozali workspace # escanea los repos hijos, prepara los que falten y arma la config conjunta
185
+ ozali workspace --dry-run # solo te muestra el inventario y el plan, sin escribir nada
186
+ ozali workspace --yes # sin preguntas: acepta los defaults y las referencias detectadas
187
+ ozali workspace --depth 2 # busca repos hasta 2 niveles (si están en subcarpetas de grupo)
188
+ ```
189
+
190
+ Qué hace, en orden:
191
+
192
+ 1. **Revisa** cada repo y te dice cómo está: `✔ listo`, `⚠ sin calibrar` (le falta correr la skill
193
+ `ozali`) o `✖ sin init` (nunca se preparó).
194
+ 2. **Prepara** los que están `✖ sin init` corriendo `ozali init` por ti, y te **guía** para calibrar
195
+ los que están `⚠ sin calibrar` (abrir ese repo y escribir *"diagnostica el proyecto"* — eso lo
196
+ hace tu agente, no el comando).
197
+ 3. **Detecta las referencias** entre repos (dependencias npm cruzadas, submódulos, `docker-compose`) y
198
+ te pide confirmarlas.
199
+ 4. **Escribe la configuración** para trabajar en conjunto:
200
+ - `ozali-workspace.json` — el mapa de repos, su estado y sus referencias (esto **sí** se commitea).
201
+ - `<carpeta>.code-workspace` — ábrelo en VSCode/Antigravity y verás todos los repos juntos.
202
+ - **ozali-workspace-jarvis** en el `CLAUDE.md`/`AGENTS.md` de la raíz: un orquestador que coordina
203
+ los repos según ese mapa y delega el trabajo de código en el `cdk` de cada uno.
204
+
205
+ **Sin ir a cada proyecto:** desde la carpeta raíz puedes operar todos los repos de una vez:
206
+
207
+ ```bash
208
+ ozali workspace --doctor # revisa la salud de todos los repos y saca un resumen
209
+ ozali workspace --update # actualiza skills, permisos y jarvis de todos los repos
210
+ ```
211
+
212
+ Y para **calibrar** (generar `cdk`) los que estén `⚠ sin calibrar`, ya **no** abres cada proyecto:
213
+ abre tu agente en la carpeta raíz y pídele *"calibra los repos pendientes"* — el orquestador los
214
+ recorre **uno por uno** (cada uno con su aprobación) sin que cambies de proyecto.
215
+
216
+ Es **idempotente**: vuelve a correrlo cuando agregues un repo o cambien las referencias — no duplica
217
+ nada. Cada repo conserva su autonomía (su propio jarvis, su `cdk` y su memoria); el workspace solo
218
+ los **coordina**. Detalle completo en [workspaces.md](workspaces.md).
219
+
174
220
  ## Preguntas frecuentes
175
221
 
176
222
  **¿Tengo que usar Engram?**
@@ -185,6 +231,10 @@ Lo corriste con `dlx`/`npx`, que es temporal. Instálalo global: `pnpm add -g oz
185
231
  **¿Esto ensucia mi repositorio?**
186
232
  No. El histórico y la memoria se aíslan (`.ozali/`, `.engram/` van al `.gitignore`).
187
233
 
234
+ **Tengo varios repos que dependen entre sí. ¿Los conecto?**
235
+ Sí: abre la carpeta que los agrupa y corre `ozali workspace`. Prepara los que falten y arma una
236
+ config para que tu agente trabaje con todos juntos. Ver [arriba](#trabajar-con-varios-repos-a-la-vez-ozali-workspace).
237
+
188
238
  **Veo "this workspace has not been trusted" y mis permisos no aplican.**
189
239
  Es el aviso de seguridad de Claude Code: ignora los permisos del proyecto hasta confiar en él.
190
240
  Deja que `init` lo marque como confiable, o abre Claude Code en la carpeta y acepta el diálogo una
@@ -26,6 +26,18 @@ ozali workspace --no-trust # no marca la raíz como confiable en Claude Code
26
26
  Un CLI **no puede auto-dispararse** con solo hacer `cd`; por eso es un comando explícito e
27
27
  **idempotente**: re-córrelo cuando agregues repos o cambien las referencias.
28
28
 
29
+ ### Operar todos los repos desde la raíz
30
+
31
+ Para no entrar repo por repo, dos modos batch corren un comando sobre **todos los miembros**:
32
+
33
+ ```bash
34
+ ozali workspace --doctor # health-check de cada repo miembro + resumen consolidado
35
+ ozali workspace --update # actualiza skills/permisos/jarvis de cada repo + avisa de cdk desfasado
36
+ ```
37
+
38
+ Ambos son **solo CLI** (no necesitan el agente) y saltan los repos `✖ sin init`. La **calibración**
39
+ (generar `cdk`) sí la hace el agente, pero también **sin cambiar de proyecto** — ver abajo.
40
+
29
41
  ## Qué hace, paso a paso
30
42
 
31
43
  1. **Escaneo (read-only).** Enumera los repos git hijos y reporta el estado ozali de cada uno:
@@ -37,10 +49,11 @@ Un CLI **no puede auto-dispararse** con solo hacer `cd`; por eso es un comando e
37
49
  instala skills, aísla histórico, configura Engram). Hereda `agent`/`scope`/`knowledge-repo` de un
38
50
  repo ya inicializado para mantener el equipo consistente.
39
51
 
40
- 3. **Guía de calibración.** El CLI **no calibra** (eso lo hace el agente). Para cada repo
41
- `⚠ sin calibrar`, imprime la instrucción: abre ese repo en tu agente y corre la skill **`ozali`**
42
- ("diagnostica el proyecto") para generar su `cdk`. El orquestador de workspace también queda
43
- cableado para conducir esa calibración repo por repo.
52
+ 3. **Guía de calibración.** El CLI **no calibra** (eso lo hace el agente), pero ya **no** tienes que
53
+ abrir cada repo por separado: `ozali workspace` instala la skill **`ozali`** en la **raíz**, y el
54
+ orquestador `ozali-workspace-jarvis` la usa **en modo target** para calibrar cada repo
55
+ `⚠ sin calibrar` **desde el workspace**, repo por repo y con su propio 🛑 GATE. Abre el agente en la
56
+ raíz y pídele *"calibra los repos pendientes"*.
44
57
 
45
58
  4. **Referencias (auto-detección + confirmación).** Infiere aristas dirigidas `from → to`:
46
59
  - `npm-dep` — el `name` de un repo aparece en `dependencies`/`devDependencies`/`peerDependencies` de otro.
@@ -58,6 +71,7 @@ En la carpeta raíz:
58
71
  | `ozali-workspace.json` | Manifiesto: miembros (ruta, proyecto, estado, fuente de verdad), referencias, `knowledgeRepo`, cloud. Fuente de verdad del workspace. |
59
72
  | `<carpeta>.code-workspace` | Workspace **multi-root** de VSCode/Antigravity: abre todos los repos juntos. Merge idempotente si ya existe. |
60
73
  | `CLAUDE.md` / `AGENTS.md` (raíz) | Bloque marcado del orquestador **`ozali-workspace-jarvis`** + subagente / agente de opencode. |
74
+ | `.claude/skills/ozali/` (raíz) | La skill **`ozali`** instalada en la raíz para **calibrar miembros en modo target** sin cambiar de proyecto (solo Claude Code). |
61
75
 
62
76
  Si la raíz es a su vez un repo git, se añade `.claude/`, `.engram/` y `.ozali/` a su `.gitignore`
63
77
  (los artefactos locales del agente no se commitean). `ozali-workspace.json` y `.code-workspace` **sí**
@@ -78,5 +92,5 @@ miembros). Su trabajo:
78
92
  ## Fuera de alcance (por ahora)
79
93
 
80
94
  - Auto-sugerencia al abrir la carpeta (tarea de editor/hook).
81
- - `ozali doctor`/`sync` agregados a nivel workspace.
95
+ - `ozali sync` agregado a nivel workspace (`--doctor`/`--update` ya existen; falta `--sync`).
82
96
  - Detección de referencias más allá de npm/submódulos/compose (imports TS/py, `go.work`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {
package/skill/SKILL.md CHANGED
@@ -26,6 +26,28 @@ produce documentación, la calibración, el plan y los artefactos de la skill `c
26
26
 
27
27
  ---
28
28
 
29
+ ## Modo workspace — calibrar un repo miembro (target)
30
+
31
+ Normalmente `ozali` opera sobre el **repo actual** (cwd). Pero cuando te invocan desde la **raíz de un
32
+ workspace** (existe `ozali-workspace.json`) para calibrar un **repo miembro**, operas sobre un
33
+ **target de subcarpeta** en vez del cwd. En ese caso:
34
+
35
+ - **Todas las rutas son relativas al `<target>`**, no a la raíz: la fuente de verdad
36
+ (`<target>/AI.md` + `<target>/.ai/`, o su variante `IA.md` + `.ia/`), la skill que generas
37
+ (`<target>/.claude/skills/cdk/`), el histórico (`<target>/.ozali/docs/`) y
38
+ `<target>/.engram/config.json`.
39
+ - **Identidad git del miembro:** usa `git -C <target> …` (no la raíz).
40
+ - **Engram:** guarda con el `project_name` de `<target>/.engram/config.json` (equivale al
41
+ `members[].project` del manifiesto). **No mezcles** memoria entre repos.
42
+ - **Un target por corrida**, secuencial, con su **propio 🛑 GATE**. No calibres varios a la vez sin
43
+ aprobación.
44
+ - Al terminar, sugiere **re-correr `ozali workspace`** en la raíz para refrescar el estado del miembro
45
+ en el manifiesto (`needs-calibration` → `ready`).
46
+
47
+ Si te invocan sin target (uso normal en un solo repo), ignora esta sección y sigue con el cwd.
48
+
49
+ ---
50
+
29
51
  ## Flujo general (7 fases)
30
52
 
31
53
  ```
@@ -13,29 +13,73 @@ ejecución disciplinada dentro de cada uno.
13
13
  > Cada repo miembro conserva su propia autonomía: su `ozali-jarvis`, su skill `cdk`, su fuente de
14
14
  > verdad (`.ai/`/`.ia/`) y su memoria Engram. Tú **no** los reemplazas: los **coordinas**.
15
15
 
16
+ ## El manifiesto (`ozali-workspace.json`) — tu única fuente de verdad
17
+
18
+ Léelo en la raíz **antes de cualquier cosa**. Es lo que `ozali workspace` escribió; su esquema:
19
+
20
+ - `members[]` — cada repo miembro:
21
+ - `path` — carpeta relativa a la raíz (el repo donde ejecutas).
22
+ - `project` — **nombre del proyecto en Engram** de ese repo. Úsalo para acotar la memoria (ver §1);
23
+ **no** dependas de `mem_current_project`, que detecta la raíz del workspace, no el miembro.
24
+ - `status` — `ready` / `needs-calibration` / `missing-init`. **Es una foto** del último
25
+ `ozali workspace`: puede haber quedado atrás (ver §2).
26
+ - `sot` — variante de fuente de verdad del repo (`ai`/`ia`/…) o `null` si no la tiene.
27
+ - `references[]` — aristas dirigidas `{ from, to, kind }` entre miembros (`from` consume a `to`):
28
+ - `kind: "npm-dep"` — `from` declara a `to` en sus dependencias npm.
29
+ - `kind: "git-submodule"` — `to` está montado como submódulo dentro de `from`.
30
+ - `kind: "compose"` — un servicio de `from` construye desde `to` (`docker-compose`).
31
+ - `knowledgeRepo` — repo de conocimiento **compartido** por el equipo (histórico + memoria sincronizada).
32
+ - `cloud` — config de Engram Cloud (réplica de equipo) si está habilitada.
33
+ - `agent` — agente objetivo (`claude-code` / `opencode` / `both`).
34
+
35
+ **No inventes** miembros ni referencias: si algo no está en el manifiesto, no lo asumas.
36
+
16
37
  ## 1. Al iniciar (recall-first, a nivel workspace)
17
- - **Lee `ozali-workspace.json`** en la raíz: es la fuente de verdad de los **miembros**, su **estado**
18
- (`ready` / `needs-calibration` / `missing-init`) y las **referencias** entre ellos (`from → to`).
19
- - Ubica la tarea: ¿qué repo(s) toca y a través de qué referencias impacta a otros?
20
- - Recupera contexto de memoria **de cada proyecto involucrado** (no de todos): confirma el proyecto de
21
- cada repo con `mem_current_project` y usa `mem_search`/`mem_context` acotado a esos proyectos.
38
+ - **Lee `ozali-workspace.json`** y ubica la tarea: ¿qué repo(s) toca y, vía qué `references`, impacta a
39
+ otros?
40
+ - Recupera contexto de memoria **solo de los proyectos involucrados** (no de todos): para cada miembro
41
+ en juego usa su `members[].project` con `mem_search`/`mem_context` acotado a ese proyecto.
22
42
  - **Recuperación selectiva:** nunca vuelques toda la memoria; trae solo lo necesario por repo.
23
43
 
24
44
  ## 2. Antes de tocar código — verifica que el repo esté listo
25
- - Si un miembro está `missing-init`, dilo y sugiere `ozali init` en ese repo (o re-correr
26
- `ozali workspace` en la raíz).
27
- - Si un miembro está `needs-calibration` (sin `cdk`), **condúcelo**: sugiere abrir ese repo y correr la
28
- skill **`ozali`** ("diagnostica el proyecto") para generar su `cdk` antes de ejecutar cambios.
45
+ - El `status` del manifiesto es una foto; **confírmalo contra la realidad** antes de confiar en él: un
46
+ repo está calibrado si existe `<path>/.claude/skills/cdk/SKILL.md`.
47
+ - Si un miembro está `missing-init` (sin `.ozali/config.json`), dilo y sugiere `ozali init` en ese repo.
48
+ - Si está `needs-calibration` (tiene init pero **falta `cdk`**), **condúcelo** desde aquí (ver sección
49
+ siguiente): ya **no** hace falta abrir cada repo por separado.
50
+ - Si calibraste o inicializaste un repo desde aquí, el manifiesto quedó **desactualizado**: sugiere
51
+ re-correr `ozali workspace` en la raíz para refrescar estados y referencias.
52
+
53
+ ## Calibrar o preparar miembros desde la raíz (sin cambiar de proyecto)
54
+
55
+ No abras cada repo por separado. Desde este workspace prepara y calibra en secuencia:
56
+
57
+ - **`missing-init`** (sin `.ozali/config.json`): pide correr `ozali init <path>` en ese miembro, o
58
+ `ozali workspace` en la raíz (que lo inicializa por ti).
59
+ - **`needs-calibration`** (falta `cdk`): invoca la skill **`ozali`** instalada en la raíz **en modo
60
+ target**, apuntando a `<path>` del miembro. Genera `AI.md`/`.ai/` y su `cdk` **dentro de esa
61
+ subcarpeta**, guardando en Engram con el `members[].project` de ese repo.
62
+ - Hazlo **repo por repo, secuencialmente**, respetando el 🛑 **GATE** de cada uno: nunca calibres
63
+ varios sin aprobación.
64
+ - Para **revisar** o **actualizar** todos de golpe (sin agente, solo CLI): `ozali workspace --doctor`
65
+ y `ozali workspace --update`.
66
+ - Tras inicializar/calibrar, sugiere **re-correr `ozali workspace`** en la raíz para refrescar los
67
+ estados del manifiesto.
29
68
 
30
69
  ## 3. Para ejecutar cambios → delega en el repo correcto
31
70
  - Un cambio se ejecuta **dentro** del repo que le corresponde, usando **su** skill `cdk` (plan, GATE,
32
71
  TDD, docs por hito). Tú preparas el contexto cruzado; el `cdk` del repo ejecuta.
33
- - **Cambios que cruzan repos** (p. ej. un contrato de API que consume un front): secuencia el trabajo
34
- siguiendo las `references` (primero el `to`/proveedor, luego el `from`/consumidor), y deja registrado
35
- en la memoria de cada repo qué cambió y por qué, enlazando ambos lados.
72
+ - **Cambios que cruzan repos:** secuencia según las `references`, **primero el `to` (proveedor), luego
73
+ el `from` (consumidor)**. Según el `kind`:
74
+ - `npm-dep` publica/enlaza el paquete de `to` antes de que `from` consuma la nueva versión.
75
+ - `git-submodule` — actualiza `to`, luego mueve el puntero del submódulo en `from` y verifícalo.
76
+ - `compose` — reconstruye el servicio de `to` antes de levantar `from`.
77
+ - Deja registrado en la memoria de **cada** repo qué cambió y por qué, enlazando ambos lados de la
78
+ referencia.
36
79
 
37
80
  ## 4. Al cerrar
38
- - Resume por repo tocado con `mem_session_summary` (solo el agente top-level).
81
+ - Resume por repo tocado con `mem_session_summary` (solo el agente top-level), usando el `project` de
82
+ cada miembro.
39
83
  - Si el cambio afectó una referencia entre repos, anótalo en **ambos** proyectos para que el recall
40
84
  futuro lo encuentre desde cualquiera de los dos.
41
85