ozali 0.6.0 → 0.7.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
@@ -60,6 +60,7 @@ git clone <repo> && node ozali/cli/bin/ozali.mjs init
60
60
 
61
61
  ```bash
62
62
  ozali init # detecta agente, instala skills ozali + ozali-commit, aísla histórico, configura Engram
63
+ ozali workspace # multi-repo: escanea la carpeta raíz, remedia init/calibración y cablea la config conjunta
63
64
  ozali doctor # health-check read-only (fuente de verdad, Engram, Cloud, versión de cdk, Strict TDD…)
64
65
  ozali update # actualiza skills ozali + ozali-commit + ozali-jarvis + permisos; avisa si cdk quedó atrás
65
66
  ozali sync # lleva el histórico (docs + Engram) al repo de conocimiento de equipo
@@ -71,6 +72,31 @@ ozali audit # navega/audita la memoria de Engram del proyecto (o general)
71
72
  `--tui` para el navegador interactivo, `--search "<texto>"` para buscar y `--general` para forzar el
72
73
  alcance. Sin Engram, audita el histórico local de `.ozali/docs/`.
73
74
 
75
+ ### Workspaces multi-repo
76
+
77
+ Cuando abres en tu editor (VSCode, **Antigravity**) una **carpeta raíz con varios repos** que se
78
+ referencian entre sí, corre **una vez** en esa raíz:
79
+
80
+ ```bash
81
+ ozali workspace # escanea repos hijos, remedia los que faltan y escribe la config conjunta
82
+ ozali workspace --dry-run # solo muestra el inventario y el plan, sin escribir
83
+ ozali workspace --depth 2 # busca repos hasta 2 niveles (para raíces con subcarpetas de grupo)
84
+ ```
85
+
86
+ Qué hace, en orden: (1) **escanea** los repos hijos y reporta su estado —`✔ listo`, `⚠ sin calibrar`
87
+ (falta `cdk`) o `✖ sin init`—; (2) **remedia** con `ozali init` los que no lo tienen y te **guía** para
88
+ calibrar (correr la skill `ozali` en cada repo — eso lo hace el agente, no el CLI); (3) **infiere las
89
+ referencias** entre repos (dependencias npm cruzadas, submódulos git, `docker-compose`) y las confirma
90
+ contigo; y (4) escribe la config para **trabajar en conjunto**:
91
+
92
+ - `ozali-workspace.json` — manifiesto de miembros, estado y referencias.
93
+ - `<carpeta>.code-workspace` — workspace multi-root que abre todos los repos juntos.
94
+ - Orquestador **`ozali-workspace-jarvis`** en `CLAUDE.md`/`AGENTS.md` de la raíz: coordina los repos
95
+ según el manifiesto y delega la ejecución en el `cdk` de cada uno.
96
+
97
+ Es **idempotente**: re-córrelo cuando agregues repos o cambien las referencias. Detalle completo en
98
+ [`docs/workspaces.md`](docs/workspaces.md).
99
+
74
100
  ### Actualizar a una versión nueva
75
101
 
76
102
  ```bash
@@ -87,7 +113,7 @@ automáticamente** al contrato vigente (eliminando `copsis-commit` y cableando `
87
113
  estampa la versión — tus docs por hito y el plan congelado se conservan.
88
114
 
89
115
  > **Commit del hito (`ozali-commit`):** `init`/`update` instalan la skill `ozali-commit`
90
- > (`.claude/skills/ozali-commit/`), sucesora de `copsis-commit`. `cdk` la invoca al cerrar cada hito
116
+ > (`.claude/skills/ozali-commit/`). `cdk` la invoca al cerrar cada hito
91
117
  > para generar el commit convencional (feat/fix/hotfix/refactor + scope) tras tu aprobación.
92
118
 
93
119
  `init` también escribe un **perfil base de permisos** (`.claude/settings.json` para Claude Code,
@@ -145,7 +171,7 @@ Claude Code y opencode (perfiles de permisos para ambos en
145
171
 
146
172
  > Siguiente paso natural: crear el repo en GitHub y publicar la primera versión (`pnpm publish` vía tag), más probar el bootstrap en un repo real del equipo.
147
173
 
148
- > Linaje: `ozali` evoluciona la skill `copsis-doctor`, nutrida con conceptos de
174
+ > Linaje: `ozali` se nutre de conceptos de
149
175
  > [gentle-ai](https://github.com/Gentleman-Programming/gentle-ai) (Engram, calibración SDD/TDD,
150
176
  > distribución plug-n-play).
151
177
 
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, audit, cloud } from "../lib/commands.mjs";
6
+ import { init, doctor, update, sync, audit, cloud, workspace } 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)
@@ -14,6 +14,8 @@ ${c.bold("Uso:")}
14
14
  ${c.bold("Comandos:")}
15
15
  init Detecta el agente (Claude Code/opencode), instala las skills ozali y
16
16
  ozali-commit, aísla el histórico, configura Engram y el repo de conocimiento.
17
+ workspace Multi-repo: escanea los repos de la carpeta raíz, remedia los que no tienen
18
+ ozali init, guía la calibración y escribe la config para trabajar en conjunto.
17
19
  doctor Health-check read-only del proyecto (fuente de verdad, Engram, versión de cdk, TDD…).
18
20
  update Actualiza la instalación (skills ozali + ozali-commit + ozali-jarvis + permisos)
19
21
  al paquete y avisa si la skill cdk quedó desactualizada.
@@ -26,6 +28,7 @@ ${c.bold("Opciones comunes:")}
26
28
  --dry-run (init) Muestra el plan sin escribir nada.
27
29
  --agent <a> (init) claude-code | opencode | both.
28
30
  --scope <s> (init) project | global.
31
+ --depth <n> (workspace) Niveles a escanear bajo la raíz (default 1).
29
32
  --knowledge-repo <p> (init) Ruta del repo de conocimiento.
30
33
  --no-engram (init) No usar Engram; arranca en modo docs.
31
34
  --no-trust (init) No marcar el workspace como confiable en Claude Code.
@@ -69,6 +72,7 @@ function parseArgs(argv) {
69
72
  else if (a === "--stats") opts.stats = true;
70
73
  else if (a === "--agent") opts.agent = argv[++i];
71
74
  else if (a === "--scope") opts.scope = argv[++i];
75
+ else if (a === "--depth") opts.depth = argv[++i];
72
76
  else if (a === "--knowledge-repo") opts.knowledgeRepo = argv[++i];
73
77
  else if (a === "-h" || a === "--help") opts.help = true;
74
78
  else if (a === "-v" || a === "--version") opts.version = true;
@@ -89,6 +93,7 @@ async function main() {
89
93
  console.log(`${c.bold("ozali")} ${c.dim("v" + pkgVersion())}`);
90
94
  switch (cmd) {
91
95
  case "init": return await init(cwd, opts);
96
+ case "workspace": return await workspace(cwd, opts);
92
97
  case "doctor": return doctor(cwd);
93
98
  case "update": return update(cwd, opts);
94
99
  case "sync": return sync(cwd, opts);
@@ -6,9 +6,9 @@ import {
6
6
  c, ok, warn, err, info, step,
7
7
  SKILL_SRC, COMMIT_SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
8
8
  ensureGitignore, tryExec, spawnCmd, which, engramAssetName,
9
- projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL,
9
+ projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL, gitInfo,
10
10
  } from "./util.mjs";
11
- import { detectAll } from "./detect.mjs";
11
+ import { detectAll, detectWorkspace, detectReferences } from "./detect.mjs";
12
12
  import { ask, confirm, select } from "./prompt.mjs";
13
13
 
14
14
  const CONFIG_PATH = (cwd) => path.join(cwd, ".ozali", "config.json");
@@ -623,17 +623,17 @@ const JARVIS_BEGIN = "<!-- ozali-jarvis:start -->";
623
623
  const JARVIS_END = "<!-- ozali-jarvis:end -->";
624
624
 
625
625
  /** Cuerpo del bloque jarvis para CLAUDE.md / AGENTS.md (sin el frontmatter del template). */
626
- function jarvisPersonaBody() {
627
- const tpl = fs.readFileSync(path.join(TEMPLATES_SRC, "ozali-jarvis.md"), "utf8");
626
+ function jarvisPersonaBody(tpl = "ozali-jarvis.md") {
627
+ const raw = fs.readFileSync(path.join(TEMPLATES_SRC, tpl), "utf8");
628
628
  // Quita el frontmatter YAML (--- ... ---) y deja el cuerpo markdown.
629
- return tpl.replace(/^---[\s\S]*?---\s*/, "").trim();
629
+ return raw.replace(/^---[\s\S]*?---\s*/, "").trim();
630
630
  }
631
631
 
632
632
  /** Inserta/actualiza un bloque marcado en un archivo markdown (idempotente). */
633
- function upsertMarkedBlock(file, body) {
634
- const block = `${JARVIS_BEGIN}\n${body}\n${JARVIS_END}`;
633
+ function upsertMarkedBlock(file, body, begin = JARVIS_BEGIN, end = JARVIS_END) {
634
+ const block = `${begin}\n${body}\n${end}`;
635
635
  let txt = exists(file) ? fs.readFileSync(file, "utf8") : "";
636
- const re = new RegExp(`${JARVIS_BEGIN}[\\s\\S]*?${JARVIS_END}`);
636
+ const re = new RegExp(`${begin}[\\s\\S]*?${end}`);
637
637
  let changed;
638
638
  if (re.test(txt)) {
639
639
  const next = txt.replace(re, block);
@@ -757,6 +757,193 @@ function ensureOpencodeProfile(cwd) {
757
757
  ok(`Perfil base de permisos de opencode en ${c.bold("opencode.json")} (lectura+comandos libres, ediciones confirman).`);
758
758
  }
759
759
 
760
+ // ========================================================= workspace =========
761
+ // Configuración multi-repo: escanea repos hijos de una carpeta raíz, remedia los
762
+ // que no tienen ozali init, guía la calibración (que hace el agente), y escribe
763
+ // 3 capas de config: manifiesto + .code-workspace + orquestador de workspace.
764
+ const WS_JARVIS_BEGIN = "<!-- ozali-workspace-jarvis:start -->";
765
+ const WS_JARVIS_END = "<!-- ozali-workspace-jarvis:end -->";
766
+ const WS_MANIFEST = (root) => path.join(root, "ozali-workspace.json");
767
+
768
+ const STATUS_LABEL = {
769
+ "ready": () => c.green("✔ listo"),
770
+ "needs-calibration": () => c.yellow("⚠ sin calibrar (falta cdk)"),
771
+ "missing-init": () => c.red("✖ sin init"),
772
+ };
773
+
774
+ export async function workspace(cwd, opts = {}) {
775
+ step("ozali workspace — configuración multi-repo");
776
+ const depth = opts.depth ? (parseInt(opts.depth, 10) || 1) : 1;
777
+
778
+ // Fase A — escaneo (read-only)
779
+ let ws = detectWorkspace(cwd, { depth });
780
+ if (ws.members.length === 0) {
781
+ warn("No encontré repositorios git como hijos de esta carpeta.");
782
+ info("Corre " + c.bold("ozali workspace") + " desde la carpeta que agrupa tus repos (o usa " + c.bold("--depth 2") + ").");
783
+ return 1;
784
+ }
785
+ step("Repos detectados");
786
+ printMembers(ws.members);
787
+
788
+ // Fase B — remediación de los que no tienen ozali init
789
+ const missing = ws.members.filter((m) => m.status === "missing-init");
790
+ if (missing.length && opts.dryRun) {
791
+ info(`(dry-run) Aquí correría ${c.bold("ozali init")} en: ${c.bold(missing.map((m) => m.dir).join(", "))}.`);
792
+ } else if (missing.length) {
793
+ step("Repos sin ozali init");
794
+ const base = inheritedConfig(ws.members);
795
+ const shared = {
796
+ agent: opts.agent || (base && base.agent),
797
+ scope: opts.scope || (base && base.scope),
798
+ knowledgeRepo: opts.knowledgeRepo || (base && base.knowledgeRepo),
799
+ };
800
+ for (const m of missing) {
801
+ const go = opts.yes ? true : await confirm(`¿Correr ${c.bold("ozali init")} en ${c.bold(m.dir)}?`, true);
802
+ if (!go) { info(`Saltado: ${m.dir}.`); continue; }
803
+ await init(m.path, { ...opts, ...shared });
804
+ }
805
+ ws = detectWorkspace(cwd, { depth }); // re-escanea tras remediar
806
+ }
807
+
808
+ // Guía de calibración (el CLI NO puede calibrar; lo hace el agente)
809
+ const needCal = ws.members.filter((m) => m.status === "needs-calibration");
810
+ if (needCal.length) {
811
+ step("Calibración pendiente (la hace tu agente, no el CLI)");
812
+ warn(`${needCal.length} repo(s) tienen ozali init pero aún no generan su skill ${c.bold("cdk")}.`);
813
+ for (const m of needCal) {
814
+ console.log(` • ${c.bold(m.dir)} → abre el repo en tu agente y corre la skill ${c.bold("ozali")} (${c.dim('"diagnostica el proyecto"')}).`);
815
+ }
816
+ }
817
+
818
+ // Fase C — referencias entre repos (auto-detección + confirmación)
819
+ const references = await confirmReferences(detectReferences(ws.members), opts);
820
+
821
+ if (opts.dryRun) { warn("--dry-run: no escribo nada. Plan mostrado arriba."); return 0; }
822
+
823
+ // Fase D — escritura de la configuración del workspace
824
+ step("Escribiendo configuración del workspace");
825
+ const manifest = writeWorkspaceManifest(cwd, ws.members, references, opts);
826
+ writeCodeWorkspace(cwd, ws.members);
827
+
828
+ const agent = manifest.agent;
829
+ if (agent === "claude-code" || agent === "both") {
830
+ ensureWorkspaceJarvisClaudeCode(cwd);
831
+ if (!opts.noTrust) await ensureClaudeWorkspaceTrust(cwd, opts);
832
+ }
833
+ if (agent === "opencode" || agent === "both") ensureWorkspaceJarvisOpencode(cwd);
834
+
835
+ if (gitInfo(cwd).isRepo) {
836
+ const { added } = ensureGitignore(cwd, [".claude/", ".engram/", ".ozali/"]);
837
+ if (added.length) ok(`.gitignore de la raíz actualizado: ${added.join(", ")}.`);
838
+ }
839
+
840
+ // Siguientes pasos
841
+ step("Siguientes pasos");
842
+ const wsFile = `${path.basename(cwd)}.code-workspace`;
843
+ 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.`);
845
+ console.log(` ${c.dim("Re-corre")} ${c.bold("ozali workspace")} ${c.dim("cuando agregues repos o cambien las referencias (es idempotente).")}`);
846
+ return 0;
847
+ }
848
+
849
+ /** Primer .ozali/config.json entre los miembros ya inicializados (para heredar defaults). */
850
+ function inheritedConfig(members) {
851
+ for (const m of members) {
852
+ const cfg = readJSON(path.join(m.path, ".ozali", "config.json"));
853
+ if (cfg) return cfg;
854
+ }
855
+ return null;
856
+ }
857
+
858
+ function printMembers(members) {
859
+ const pad = Math.max(4, ...members.map((m) => m.dir.length));
860
+ for (const m of members) {
861
+ const label = (STATUS_LABEL[m.status] || (() => m.status))();
862
+ const sot = m.sot.found ? c.dim(`sot:${m.sot.variant}`) : c.dim("sot:—");
863
+ const eng = m.engramProject ? c.dim(` engram:${m.engramProject}`) : "";
864
+ console.log(` ${c.bold(m.dir.padEnd(pad))} ${label} ${sot}${eng}`);
865
+ }
866
+ }
867
+
868
+ async function confirmReferences(detected, opts) {
869
+ if (detected.length === 0) { info("No detecté referencias automáticas entre los repos."); return []; }
870
+ step("Referencias detectadas entre repos");
871
+ for (const e of detected) console.log(` • ${c.bold(e.fromDir)} → ${c.bold(e.toDir)} ${c.dim("(" + e.kind + ")")}`);
872
+ if (opts.yes) return detected;
873
+ if (await confirm(`¿Registrar las ${detected.length} referencias detectadas?`, true)) return detected;
874
+ const kept = [];
875
+ for (const e of detected) {
876
+ if (await confirm(` ¿Registrar ${e.fromDir} → ${e.toDir} (${e.kind})?`, true)) kept.push(e);
877
+ }
878
+ return kept;
879
+ }
880
+
881
+ function writeWorkspaceManifest(root, members, references, opts) {
882
+ const existing = readJSON(WS_MANIFEST(root)) || {};
883
+ const base = inheritedConfig(members) || {};
884
+ const agent = opts.agent || existing.agent || base.agent || "claude-code";
885
+ const manifest = {
886
+ version: pkgVersion(),
887
+ root,
888
+ agent,
889
+ knowledgeRepo: opts.knowledgeRepo || existing.knowledgeRepo || base.knowledgeRepo || DEFAULT_KNOWLEDGE,
890
+ cloud: base.cloud || existing.cloud || { enabled: false },
891
+ members: members.map((m) => ({ path: m.dir, project: m.project, status: m.status, sot: m.sot.found ? m.sot.variant : null })),
892
+ references: references.map((e) => ({ from: e.fromDir, to: e.toDir, kind: e.kind })),
893
+ createdAt: existing.createdAt || new Date().toISOString(),
894
+ updatedAt: new Date().toISOString(),
895
+ };
896
+ writeJSON(WS_MANIFEST(root), manifest);
897
+ ok(`Manifiesto escrito en ${c.bold("ozali-workspace.json")} (${members.length} repos, ${references.length} referencias).`);
898
+ return manifest;
899
+ }
900
+
901
+ function writeCodeWorkspace(root, members) {
902
+ const file = path.join(root, `${path.basename(root)}.code-workspace`);
903
+ const existing = readJSON(file) || {};
904
+ const folders = Array.isArray(existing.folders) ? existing.folders.slice() : [];
905
+ const have = new Set(folders.map((f) => f && f.path));
906
+ for (const m of members) if (!have.has(m.dir)) folders.push({ path: m.dir });
907
+ const cfg = {
908
+ folders,
909
+ settings: existing.settings || {},
910
+ extensions: existing.extensions || { recommendations: ["anthropic.claude-code"] },
911
+ };
912
+ writeJSON(file, cfg);
913
+ ok(`Workspace de editor escrito en ${c.bold(path.basename(file))} (${folders.length} carpetas, multi-root).`);
914
+ }
915
+
916
+ function ensureWorkspaceJarvisClaudeCode(root) {
917
+ const claudeMd = path.join(root, "CLAUDE.md");
918
+ const ch = upsertMarkedBlock(claudeMd, jarvisPersonaBody("ozali-workspace-jarvis.md"), WS_JARVIS_BEGIN, WS_JARVIS_END);
919
+ ok(`ozali-workspace-jarvis ${ch ? "escrito" : "ya presente"} en ${c.bold("CLAUDE.md")} de la raíz.`);
920
+ const agentFile = path.join(root, ".claude", "agents", "ozali-workspace-jarvis.md");
921
+ ensureDir(path.dirname(agentFile));
922
+ fs.copyFileSync(path.join(TEMPLATES_SRC, "ozali-workspace-jarvis.md"), agentFile);
923
+ ok(`Subagente ${c.bold(".claude/agents/ozali-workspace-jarvis.md")} instalado.`);
924
+ }
925
+
926
+ function ensureWorkspaceJarvisOpencode(root) {
927
+ const agentsMd = path.join(root, "AGENTS.md");
928
+ const ch = upsertMarkedBlock(agentsMd, jarvisPersonaBody("ozali-workspace-jarvis.md"), WS_JARVIS_BEGIN, WS_JARVIS_END);
929
+ ok(`ozali-workspace-jarvis ${ch ? "escrito" : "ya presente"} en ${c.bold("AGENTS.md")} de la raíz.`);
930
+ const p = path.join(root, "opencode.json");
931
+ const cfg = readJSON(p, {});
932
+ cfg.$schema = cfg.$schema || "https://opencode.ai/config.json";
933
+ cfg.agent = cfg.agent || {};
934
+ if (!cfg.agent["ozali-workspace-jarvis"]) {
935
+ cfg.agent["ozali-workspace-jarvis"] = {
936
+ mode: "primary",
937
+ description: "Orquestador multi-repo: coordina repos hermanos según ozali-workspace.json.",
938
+ prompt: "{file:./AGENTS.md}",
939
+ };
940
+ writeJSON(p, cfg);
941
+ ok(`Agente ${c.bold("ozali-workspace-jarvis")} (primary) añadido a ${c.bold("opencode.json")}.`);
942
+ } else {
943
+ info("Agente ozali-workspace-jarvis ya presente en opencode.json.");
944
+ }
945
+ }
946
+
760
947
  // =========================================================== doctor ==========
761
948
  export function doctor(cwd) {
762
949
  step("ozali doctor — health-check (read-only)");
@@ -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 } from "./util.mjs";
4
+ import { exists, which, gitInfo, 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) {
@@ -109,3 +109,125 @@ export function detectAll(cwd) {
109
109
  testing: detectTesting(cwd),
110
110
  };
111
111
  }
112
+
113
+ // ===================================================== workspace (multi-repo) ==
114
+
115
+ const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", "vendor", "target"]);
116
+
117
+ /** ¿La raíz misma es el knowledge repo (o vive dentro de él)? Para no auto-escanearlo. */
118
+ function isKnowledgeRepo(dir) {
119
+ const k = path.resolve(DEFAULT_KNOWLEDGE);
120
+ const d = path.resolve(dir);
121
+ return d === k || d.startsWith(k + path.sep);
122
+ }
123
+
124
+ /**
125
+ * 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.
127
+ */
128
+ function collectRepoDirs(root, depth, level = 1, acc = []) {
129
+ let entries;
130
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return acc; }
131
+ for (const e of entries) {
132
+ if (!e.isDirectory()) continue;
133
+ if (e.name.startsWith(".") || IGNORE_DIRS.has(e.name)) continue;
134
+ const full = path.join(root, e.name);
135
+ if (isKnowledgeRepo(full)) continue;
136
+ if (gitInfo(full).isRepo) { acc.push(full); continue; } // repo = hoja
137
+ if (level < depth) collectRepoDirs(full, depth, level + 1, acc);
138
+ }
139
+ return acc;
140
+ }
141
+
142
+ /** Estado ozali de un repo: missing-init | needs-calibration | ready. */
143
+ function memberStatus(hasConfig, hasCdk) {
144
+ if (!hasConfig) return "missing-init";
145
+ if (!hasCdk) return "needs-calibration";
146
+ return "ready";
147
+ }
148
+
149
+ /**
150
+ * Inventario multi-repo de una carpeta raíz. Read-only. Por cada repo git hijo arma
151
+ * su estado ozali (init/calibración), fuente de verdad, proyecto Engram y nombre de
152
+ * package.json (para inferir referencias). `existing` = manifiesto previo, si lo hay.
153
+ */
154
+ export function detectWorkspace(root, opts = {}) {
155
+ const depth = Math.max(1, opts.depth || 1);
156
+ const dirs = collectRepoDirs(root, depth);
157
+ const members = dirs.map((full) => {
158
+ const hasConfig = exists(path.join(full, ".ozali", "config.json"));
159
+ const hasCdk = exists(path.join(full, ".claude", "skills", "cdk", "SKILL.md"));
160
+ const engramCfg = readJSON(path.join(full, ".engram", "config.json"));
161
+ const pkg = readJSON(path.join(full, "package.json"));
162
+ const g = gitInfo(full);
163
+ return {
164
+ dir: path.relative(root, full) || path.basename(full),
165
+ path: full,
166
+ project: projectName(full),
167
+ pkgName: pkg && typeof pkg.name === "string" ? pkg.name : null,
168
+ sot: detectSourceOfTruth(full),
169
+ hasConfig,
170
+ hasCdk,
171
+ engramProject: engramCfg && engramCfg.project_name ? engramCfg.project_name : null,
172
+ status: memberStatus(hasConfig, hasCdk),
173
+ git: { branch: g.branch || null, remote: g.remote || null },
174
+ };
175
+ }).sort((a, b) => a.dir.localeCompare(b.dir));
176
+ const existing = readJSON(path.join(root, "ozali-workspace.json"));
177
+ return { root, members, existing };
178
+ }
179
+
180
+ /**
181
+ * Infiere referencias entre repos (aristas dirigidas {from, to, kind}). Zero-dep,
182
+ * best-effort: dependencias npm cruzadas, submódulos git y contextos de docker-compose.
183
+ * `from` depende de / apunta a `to`. Devuelve aristas únicas.
184
+ */
185
+ export function detectReferences(members) {
186
+ const byPkg = new Map();
187
+ const byDir = new Map();
188
+ for (const m of members) {
189
+ if (m.pkgName) byPkg.set(m.pkgName, m);
190
+ byDir.set(path.basename(m.dir), m);
191
+ }
192
+ const edges = [];
193
+ const seen = new Set();
194
+ const push = (from, to, kind) => {
195
+ if (!from || !to || from.dir === to.dir) return;
196
+ const key = `${from.dir}→${to.dir}:${kind}`;
197
+ if (seen.has(key)) return;
198
+ seen.add(key);
199
+ edges.push({ from: from.project, to: to.project, fromDir: from.dir, toDir: to.dir, kind });
200
+ };
201
+
202
+ for (const m of members) {
203
+ // 1) dependencias npm cruzadas
204
+ const pkg = readJSON(path.join(m.path, "package.json"));
205
+ if (pkg) {
206
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
207
+ for (const name of Object.keys(deps)) {
208
+ const target = byPkg.get(name);
209
+ if (target) push(m, target, "npm-dep");
210
+ }
211
+ }
212
+ // 2) submódulos git (.gitmodules → path de cada submódulo)
213
+ const gm = path.join(m.path, ".gitmodules");
214
+ if (exists(gm)) {
215
+ const txt = fs.readFileSync(gm, "utf8");
216
+ for (const match of txt.matchAll(/^\s*path\s*=\s*(.+)$/gim)) {
217
+ const target = byDir.get(path.basename(match[1].trim()));
218
+ if (target) push(m, target, "git-submodule");
219
+ }
220
+ }
221
+ // 3) docker-compose (build context que apunte a un repo hermano)
222
+ for (const f of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
223
+ const cf = path.join(m.path, f);
224
+ if (!exists(cf)) continue;
225
+ const txt = fs.readFileSync(cf, "utf8");
226
+ for (const match of txt.matchAll(/context:\s*(\.{1,2}\/\S+)/gi)) {
227
+ const target = byDir.get(path.basename(match[1].trim().replace(/\/+$/, "")));
228
+ if (target) push(m, target, "compose");
229
+ }
230
+ }
231
+ }
232
+ return edges;
233
+ }
@@ -36,7 +36,7 @@ test("--version imprime la versión del package.json", () => {
36
36
 
37
37
  test("--help menciona los comandos", () => {
38
38
  const { stdout } = run(["--help"]);
39
- for (const cmd of ["init", "doctor", "update", "sync", "audit"]) assert.match(stdout, new RegExp(cmd));
39
+ for (const cmd of ["init", "workspace", "doctor", "update", "sync", "audit"]) assert.match(stdout, new RegExp(cmd));
40
40
  });
41
41
 
42
42
  test("audit imprime cabecera y no rompe (general)", () => {
@@ -207,6 +207,67 @@ test("update también instala ozali-commit en repos previos", () => {
207
207
  }
208
208
  });
209
209
 
210
+ // ------------------------------------------------------------------ workspace
211
+ function wsRepo(root, name, { config = false, cdk = false, pkg = null } = {}) {
212
+ const dir = path.join(root, name);
213
+ fs.mkdirSync(dir, { recursive: true });
214
+ execFileSync("git", ["init", "-q"], { cwd: dir });
215
+ if (config) {
216
+ fs.mkdirSync(path.join(dir, ".ozali"), { recursive: true });
217
+ fs.writeFileSync(path.join(dir, ".ozali", "config.json"), JSON.stringify({ agent: "claude-code" }));
218
+ }
219
+ if (cdk) {
220
+ fs.mkdirSync(path.join(dir, ".claude", "skills", "cdk"), { recursive: true });
221
+ fs.writeFileSync(path.join(dir, ".claude", "skills", "cdk", "SKILL.md"), "---\nname: cdk\n---\n");
222
+ }
223
+ if (pkg) fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkg));
224
+ }
225
+
226
+ test("workspace escanea y clasifica repos hijos sin escribir (dry-run)", () => {
227
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-ws-"));
228
+ try {
229
+ wsRepo(root, "api", { config: true, cdk: true, pkg: { name: "api", version: "1.0.0" } });
230
+ wsRepo(root, "web", { config: true, pkg: { name: "web", dependencies: { api: "^1" } } });
231
+ wsRepo(root, "lib-bare", { pkg: { name: "lib" } });
232
+ const { stdout } = run(["workspace", "--dry-run"], root);
233
+ assert.match(stdout, /listo/, "clasifica api como listo");
234
+ assert.match(stdout, /sin calibrar/, "clasifica web como sin calibrar");
235
+ assert.match(stdout, /sin init/, "clasifica lib-bare como sin init");
236
+ assert.match(stdout, /web → api \(npm-dep\)/, "detecta la referencia npm web→api");
237
+ assert.match(stdout, /no escribo nada/, "dry-run no escribe");
238
+ assert.ok(!fs.existsSync(path.join(root, "ozali-workspace.json")), "dry-run: no hay manifiesto");
239
+ } finally {
240
+ fs.rmSync(root, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test("workspace escribe manifiesto + .code-workspace + jarvis y es idempotente", () => {
245
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ozali-ws-"));
246
+ try {
247
+ // Ambos ready (con cdk) → sin missing-init, no invoca init pesado.
248
+ wsRepo(root, "api", { config: true, cdk: true, pkg: { name: "api", version: "1.0.0" } });
249
+ wsRepo(root, "web", { config: true, cdk: true, pkg: { name: "web", dependencies: { api: "^1" } } });
250
+ run(["workspace", "--yes", "--no-trust"], root);
251
+
252
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, "ozali-workspace.json"), "utf8"));
253
+ assert.equal(manifest.members.length, 2, "manifiesto con 2 miembros");
254
+ assert.ok(manifest.references.some((r) => r.from === "web" && r.to === "api"), "referencia web→api en el manifiesto");
255
+
256
+ const wsFile = path.join(root, `${path.basename(root)}.code-workspace`);
257
+ assert.ok(fs.existsSync(wsFile), ".code-workspace escrito");
258
+ assert.equal(JSON.parse(fs.readFileSync(wsFile, "utf8")).folders.length, 2, "multi-root con 2 folders");
259
+ assert.match(fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8"), /ozali-workspace-jarvis:start/, "bloque jarvis en CLAUDE.md");
260
+
261
+ // idempotencia: re-correr no duplica
262
+ run(["workspace", "--yes", "--no-trust"], root);
263
+ const claude = fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8");
264
+ assert.equal((claude.match(/ozali-workspace-jarvis:start/g) || []).length, 1, "no duplica el bloque jarvis");
265
+ assert.equal(JSON.parse(fs.readFileSync(wsFile, "utf8")).folders.length, 2, "no duplica folders");
266
+ } finally {
267
+ fs.rmSync(root, { recursive: true, force: true });
268
+ }
269
+ });
270
+
210
271
  test("init --dry-run no escribe nada", () => {
211
272
  const dir = tmpProject();
212
273
  try {
@@ -0,0 +1,82 @@
1
+ # Workspaces multi-repo (`ozali workspace`)
2
+
3
+ ← [README](../README.md)
4
+
5
+ Objetivo: cuando abres en tu editor (VSCode, **Antigravity** — ambos de la familia VSCode) una
6
+ **carpeta raíz que agrupa varios repositorios relacionados** (una API, su front, librerías
7
+ compartidas), que el agente pueda **trabajar en conjunto** con todos, porque tienen **referencias
8
+ entre ellos**.
9
+
10
+ `ozali` opera por defecto **repo por repo**. El comando `ozali workspace` añade una capa de
11
+ coordinación **a nivel de la carpeta raíz**, sin quitarle autonomía a ningún repo miembro (cada uno
12
+ conserva su `ozali-jarvis`, su skill `cdk`, su fuente de verdad y su memoria Engram).
13
+
14
+ ## Uso
15
+
16
+ Corre **una vez** en la carpeta raíz (la que contiene los repos):
17
+
18
+ ```bash
19
+ ozali workspace # escanea, remedia y escribe la config conjunta
20
+ ozali workspace --dry-run # solo inventario + plan, sin escribir nada
21
+ ozali workspace --yes # no interactivo (acepta defaults y todas las referencias detectadas)
22
+ ozali workspace --depth 2 # busca repos hasta 2 niveles bajo la raíz
23
+ ozali workspace --no-trust # no marca la raíz como confiable en Claude Code
24
+ ```
25
+
26
+ Un CLI **no puede auto-dispararse** con solo hacer `cd`; por eso es un comando explícito e
27
+ **idempotente**: re-córrelo cuando agregues repos o cambien las referencias.
28
+
29
+ ## Qué hace, paso a paso
30
+
31
+ 1. **Escaneo (read-only).** Enumera los repos git hijos y reporta el estado ozali de cada uno:
32
+ - `✔ listo` — tiene `.ozali/config.json` **y** su skill `cdk` (`.claude/skills/cdk/SKILL.md`).
33
+ - `⚠ sin calibrar` — tiene `ozali init` pero **falta `cdk`** (aún no se calibró).
34
+ - `✖ sin init` — no tiene `.ozali/config.json`.
35
+
36
+ 2. **Remediación.** Por cada repo `✖ sin init`, ofrece correr **`ozali init`** (el "prepare":
37
+ instala skills, aísla histórico, configura Engram). Hereda `agent`/`scope`/`knowledge-repo` de un
38
+ repo ya inicializado para mantener el equipo consistente.
39
+
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.
44
+
45
+ 4. **Referencias (auto-detección + confirmación).** Infiere aristas dirigidas `from → to`:
46
+ - `npm-dep` — el `name` de un repo aparece en `dependencies`/`devDependencies`/`peerDependencies` de otro.
47
+ - `git-submodule` — un `path` de `.gitmodules` apunta a un repo hermano.
48
+ - `compose` — un `build.context` de `docker-compose.yml` apunta a un repo hermano.
49
+
50
+ Te muestra lo detectado y lo confirmas (con `--yes` se aceptan todas).
51
+
52
+ ## Qué escribe
53
+
54
+ En la carpeta raíz:
55
+
56
+ | Artefacto | Qué es |
57
+ |---|---|
58
+ | `ozali-workspace.json` | Manifiesto: miembros (ruta, proyecto, estado, fuente de verdad), referencias, `knowledgeRepo`, cloud. Fuente de verdad del workspace. |
59
+ | `<carpeta>.code-workspace` | Workspace **multi-root** de VSCode/Antigravity: abre todos los repos juntos. Merge idempotente si ya existe. |
60
+ | `CLAUDE.md` / `AGENTS.md` (raíz) | Bloque marcado del orquestador **`ozali-workspace-jarvis`** + subagente / agente de opencode. |
61
+
62
+ Si la raíz es a su vez un repo git, se añade `.claude/`, `.engram/` y `.ozali/` a su `.gitignore`
63
+ (los artefactos locales del agente no se commitean). `ozali-workspace.json` y `.code-workspace` **sí**
64
+ son commiteables/compartibles.
65
+
66
+ ## El orquestador `ozali-workspace-jarvis`
67
+
68
+ Es una persona de agente que **lee `ozali-workspace.json` en runtime** (no se re-templatiza al cambiar
69
+ miembros). Su trabajo:
70
+
71
+ - Recall-first **a nivel workspace**: ubica qué repo(s) toca la tarea y recupera memoria **solo** de
72
+ los proyectos involucrados.
73
+ - Verifica que el repo esté `listo` antes de tocar código; si está `sin init`/`sin calibrar`, lo dice.
74
+ - **Delega la ejecución** al `cdk` del repo correspondiente (plan, GATE, TDD, docs por hito).
75
+ - En cambios que **cruzan repos**, secuencia según las `references` (primero el proveedor/`to`, luego
76
+ el consumidor/`from`) y anota el cambio en la memoria de **ambos** repos.
77
+
78
+ ## Fuera de alcance (por ahora)
79
+
80
+ - Auto-sugerencia al abrir la carpeta (tarea de editor/hook).
81
+ - `ozali doctor`/`sync` agregados a nivel workspace.
82
+ - 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.6.0",
3
+ "version": "0.7.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
@@ -92,14 +92,19 @@ Guarda estos valores; se reutilizan en Fase 6 y en la documentación.
92
92
  4. **Migración automática (`MIGRAR`)** — aplica los deltas del changelog de
93
93
  [`references/cdk-contract.md`](references/cdk-contract.md) sobre el `cdk` instalado, **sin** pasar
94
94
  por el GATE, porque preserva el `cdk` del usuario y solo actualiza el contrato:
95
- - **Elimina toda referencia a `copsis-commit`** y reemplázala por **`ozali-commit`** (la skill de
96
- commit vigente, instalada en `.claude/skills/ozali-commit/`).
95
+ - **Elimina toda referencia a la nomenclatura heredada** (`copsis-commit`, `copsis-doctor`) y
96
+ reemplázala por la de `ozali`: `copsis-commit` → **`ozali-commit`** (instalada en
97
+ `.claude/skills/ozali-commit/`); refs a plantillas o a la skill `copsis-doctor` → sus
98
+ equivalentes de `ozali`. No debe quedar ninguna mención a `copsis-*`.
99
+ - **Migra el histórico legado**: si existe `.copsis/docs/cdk/` (la ruta de `copsis-doctor`),
100
+ **mueve** su contenido a `.ozali/docs/cdk/` (fusiona **por hito**, **sin sobrescribir** lo ya
101
+ existente) y **borra** la carpeta `.copsis/` una vez vacía.
97
102
  - Aplica el resto de deltas del changelog (recall-first, telemetría, etc. según la versión).
98
103
  - **Estampa** `cdk_contract_version: N` en el frontmatter del `cdk`.
99
- - **Preserva** intactos `.ozali/docs/cdk/` (histórico por hito) y los planes congelados
100
- (`02-plan-aprobado.md`).
101
- - **Reporta** al usuario, en texto, qué cambió (referencias migradas, secciones actualizadas,
102
- versión nueva).
104
+ - **Preserva** intactos los docs ya en `.ozali/docs/cdk/` (histórico por hito) y los planes
105
+ congelados (`02-plan-aprobado.md`).
106
+ - **Reporta** al usuario, en texto, qué cambió (referencias migradas, docs reubicados de `.copsis/`
107
+ a `.ozali/`, secciones actualizadas, versión nueva).
103
108
  - **Excepción → GATE:** si un delta exige **regeneración estructural** (reescribir subagentes,
104
109
  cambiar el harness, rehacer el wiring de TDD), eso **no** es migración automática: preséntalo en
105
110
  el plan del GATE (Fase 5) y regénéralo en la Fase 6.
@@ -45,14 +45,23 @@ El número vive en **un solo lugar** (el marcador HTML de la primera línea, for
45
45
 
46
46
  Al migrar un `cdk` legado o desactualizado, **siempre**:
47
47
 
48
- 1. **Elimina toda referencia a `copsis-commit`** (nombre heredado de versiones anteriores) y
49
- **reemplázala por `ozali-commit`** la skill de commit vigente que `ozali init`/`ozali update`
50
- instalan en `.claude/skills/ozali-commit/`.
51
- 2. **Preserva** el histórico por hito (`.ozali/docs/cdk/`) y los planes congelados
48
+ 1. **Elimina toda referencia a la nomenclatura heredada** (`copsis-commit`, `copsis-doctor` nombres
49
+ de versiones anteriores del ecosistema, predecesoras de `ozali`) y **reemplázala por la de
50
+ `ozali`**:
51
+ - `copsis-commit` **`ozali-commit`** (la skill de commit vigente que `ozali init`/`ozali update`
52
+ instalan en `.claude/skills/ozali-commit/`).
53
+ - Refs a plantillas o a la skill **`copsis-doctor`** → sus equivalentes de `ozali` (esta skill y
54
+ sus `references/`). No debe quedar ninguna mención a `copsis-*` en el `cdk` migrado.
55
+ 2. **Migra el histórico legado de docs**: si existe `.copsis/docs/cdk/` (la ruta que usaba
56
+ `copsis-doctor`), **mueve** su contenido a `.ozali/docs/cdk/` — fusionando **por hito** y **sin
57
+ sobrescribir** lo que ya exista en el destino — y **elimina** la carpeta `.copsis/` una vez vacía.
58
+ Si `.copsis/` contiene otras subrutas legadas (p. ej. `.copsis/metrics/`), reubícalas bajo
59
+ `.ozali/` de forma equivalente antes de borrar `.copsis/`.
60
+ 3. **Preserva** el histórico por hito ya en `.ozali/docs/cdk/` y los planes congelados
52
61
  (`02-plan-aprobado.md`): la migración del contrato **nunca** los borra ni reescribe.
53
- 3. **Estampa** `cdk_contract_version: N` en el frontmatter del `cdk` migrado.
54
- 4. **Reporta** al usuario, en texto, qué cambió (referencias migradas, secciones actualizadas,
55
- versión nueva).
62
+ 4. **Estampa** `cdk_contract_version: N` en el frontmatter del `cdk` migrado.
63
+ 5. **Reporta** al usuario, en texto, qué cambió (referencias migradas, docs reubicados de `.copsis/`
64
+ a `.ozali/`, secciones actualizadas, versión nueva).
56
65
 
57
66
  ---
58
67
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: ozali-commit
3
- description: Genera un "commit summary" convencional (feat/fix/hotfix/refactor) con prefijo, scope y descripción a partir de los cambios EN STAGE y commitea tras tu aprobación. Úsala cuando el usuario quiera "commitea", "haz el commit", "genera el mensaje de commit", "cierra el hito con commit", o cuando otra skill (p. ej. cdk) cierre un hito. Sucesora de copsis-commit.
3
+ description: Genera un "commit summary" convencional (feat/fix/hotfix/refactor) con prefijo, scope y descripción a partir de los cambios EN STAGE y commitea tras tu aprobación. Úsala cuando el usuario quiera "commitea", "haz el commit", "genera el mensaje de commit", "cierra el hito con commit", o cuando otra skill (p. ej. cdk) cierre un hito.
4
4
  ---
5
5
 
6
6
  # Skill: ozali-commit
7
7
 
8
8
  `ozali-commit` produce el **mensaje de commit** de un cambio siguiendo
9
9
  [Conventional Commits](https://www.conventionalcommits.org/) y lo **commitea tras tu aprobación**.
10
- Es la skill de commit vigente del ecosistema `ozali` (sucede a la antigua `copsis-commit`).
10
+ Es la skill de commit vigente del ecosistema `ozali`.
11
11
 
12
12
  `cdk` la invoca en el **cierre de cada hito**, pero también puedes usarla suelta en cualquier repo.
13
13
 
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: ozali-workspace-jarvis
3
+ description: Orquestador de un WORKSPACE multi-repo de ozali. Coordina varios repositorios hermanos que se referencian entre sí. Úsalo/actúa como jarvis de workspace para CUALQUIER trabajo que cruce repos — lee el manifiesto ozali-workspace.json, recupera contexto de todos los proyectos involucrados antes de actuar, y delega la ejecución de código en el ozali-jarvis y la skill cdk de CADA repo miembro.
4
+ ---
5
+
6
+ # ozali-workspace-jarvis — orquestador multi-repo
7
+
8
+ Eres **ozali-workspace-jarvis**, el orquestador de esta **carpeta raíz que agrupa varios
9
+ repositorios** relacionados. Existes porque los repos miembros **se referencian entre sí** y a veces
10
+ un cambio abarca a más de uno. Tu trabajo es mantener la coherencia **entre repos** y delegar la
11
+ ejecución disciplinada dentro de cada uno.
12
+
13
+ > Cada repo miembro conserva su propia autonomía: su `ozali-jarvis`, su skill `cdk`, su fuente de
14
+ > verdad (`.ai/`/`.ia/`) y su memoria Engram. Tú **no** los reemplazas: los **coordinas**.
15
+
16
+ ## 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.
22
+ - **Recuperación selectiva:** nunca vuelques toda la memoria; trae solo lo necesario por repo.
23
+
24
+ ## 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.
29
+
30
+ ## 3. Para ejecutar cambios → delega en el repo correcto
31
+ - Un cambio se ejecuta **dentro** del repo que le corresponde, usando **su** skill `cdk` (plan, GATE,
32
+ 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.
36
+
37
+ ## 4. Al cerrar
38
+ - Resume por repo tocado con `mem_session_summary` (solo el agente top-level).
39
+ - Si el cambio afectó una referencia entre repos, anótalo en **ambos** proyectos para que el recall
40
+ futuro lo encuentre desde cualquiera de los dos.
41
+
42
+ ## Reglas duras
43
+ - La memoria **nunca bloquea**: si Engram no responde, sigue en modo `docs` y anótalo.
44
+ - **No inventes** referencias ni miembros: si algo no está en `ozali-workspace.json`, no lo asumas.
45
+ - No edites el histórico ni los planes congelados de ningún repo miembro.