ozali 0.6.1 → 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 +26 -0
- package/cli/bin/ozali.mjs +6 -1
- package/cli/lib/commands.mjs +195 -8
- package/cli/lib/detect.mjs +123 -1
- package/cli/test/smoke.test.mjs +62 -1
- package/docs/workspaces.md +82 -0
- package/package.json +1 -1
- package/templates/ozali-workspace-jarvis.md +45 -0
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
|
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);
|
package/cli/lib/commands.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 = `${
|
|
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(`${
|
|
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)");
|
package/cli/lib/detect.mjs
CHANGED
|
@@ -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
|
+
}
|
package/cli/test/smoke.test.mjs
CHANGED
|
@@ -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.
|
|
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": {
|
|
@@ -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.
|